From cd3032458c08e971b589d5cc3dea625a3501feee Mon Sep 17 00:00:00 2001 From: movq Date: Wed, 29 Apr 2026 21:03:21 -0500 Subject: [PATCH] feat(symphony): tracker-agnostic orchestrator plugin MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Sketch of a Claude Code port of openai/symphony — a daemon-style orchestrator that polls an issue tracker, dispatches isolated worker agents per issue, and reconciles state across ticks. Tracker-agnostic by design: the project's CLAUDE.md documents how to talk to whatever tracker is in use (GitHub Issues, Gitea, tracker CLI, Linear, etc.); the plugin reads those instructions rather than shipping per-tracker adapters. Includes: - /symphony-init for first-time setup (detects gh/gitea/tracker, writes the tracker section into project CLAUDE.md, drops WORKFLOW.md, offers to schedule the tick) - /symphony-tick fired by /schedule cron entries (idempotent, silent, never prompts) - per-issue worker agent in isolated worktrees - on-disk state in .symphony/state.json (survives cron cold starts) Known divergences from the spec are documented in the plugin CLAUDE.md (no streaming agent telemetry; no codex.* knobs; hooks run inside the worker rather than the orchestrator). Co-Authored-By: Claude Opus 4.7 (1M context) --- symphony/CLAUDE.md | 105 +++++++++++++++++++++ symphony/agents/symphony-worker.md | 69 ++++++++++++++ symphony/commands/symphony-init.md | 17 ++++ symphony/commands/symphony-tick.md | 20 ++++ symphony/skills/symphony-init/SKILL.md | 125 +++++++++++++++++++++++++ symphony/skills/symphony/SKILL.md | 124 ++++++++++++++++++++++++ symphony/templates/WORKFLOW.md | 66 +++++++++++++ symphony/templates/state.schema.json | 44 +++++++++ 8 files changed, 570 insertions(+) create mode 100644 symphony/CLAUDE.md create mode 100644 symphony/agents/symphony-worker.md create mode 100644 symphony/commands/symphony-init.md create mode 100644 symphony/commands/symphony-tick.md create mode 100644 symphony/skills/symphony-init/SKILL.md create mode 100644 symphony/skills/symphony/SKILL.md create mode 100644 symphony/templates/WORKFLOW.md create mode 100644 symphony/templates/state.schema.json diff --git a/symphony/CLAUDE.md b/symphony/CLAUDE.md new file mode 100644 index 0000000..da5dca1 --- /dev/null +++ b/symphony/CLAUDE.md @@ -0,0 +1,105 @@ +# symphony + +Claude Code port of the [openai/symphony](https://github.com/openai/symphony) orchestrator +spec. A long-running, repo-owned automation that polls **the project's issue tracker**, +spawns isolated worker agents for eligible issues, and reconciles state across ticks. + +The execution layer is Claude Code's `Agent` tool instead of `codex app-server`. Everything +above that — workflow contract, polling, workspaces, retries, hooks — maps to existing +primitives. + +**Tracker-agnostic by design.** Symphony's spec hardcoded Linear; this port does not. The +plugin only knows the *shape* of an issue (id, title, state, etc.) and asks the consuming +repo to provide the integration. See "Tracker contract" below. + +## Layout + +- `commands/symphony-init.md` — one-time interactive setup. Detects tracker, writes + CLAUDE.md tracker section, drops WORKFLOW.md, optionally schedules the tick. +- `commands/symphony-tick.md` — slash command fired by `/schedule`. One tick = one pass. + Silent and non-interactive; safe to fire on a cron. +- `skills/symphony-init/SKILL.md` — the init/interview logic. +- `skills/symphony/SKILL.md` — the dispatcher. Loads `WORKFLOW.md`, queries the + tracker via the project's integration, decides what to start/stop/retry, writes state. +- `agents/symphony-worker.md` — per-issue executor. Owns one issue, runs in an isolated + worktree, hands off when done. +- `templates/WORKFLOW.md` — workflow contract `symphony-init` copies into the repo. +- `templates/state.schema.json` — shape of the on-disk orchestrator state file. + +## User flow + +1. **First time**: `/symphony-init` in the project root. Interview + write. +2. **Steady state**: `/symphony-tick` is fired by a `/schedule` cron entry (also set up + by init unless the user declines). +3. **Re-config**: re-run `/symphony-init`; it detects drift and proposes updates. + +## Tracker contract + +The plugin treats trackers as a capability the *project* provides, not something Symphony +adapts to. To use the plugin, the consuming repo's `CLAUDE.md` MUST document, for the +tracker named in `WORKFLOW.md`'s `tracker.kind`, how to: + +1. **List active issues** — return id, identifier, title, description, state, labels, + priority, branch_name, url, updated_at. Filtered by a list of "active" state names. +2. **Get one issue's current state** — for reconciliation. +3. **Transition an issue's state** — used by the worker, not the orchestrator. +4. **Comment on an issue** — used by the worker. + +Concretely, a project CLAUDE.md section looks like: + +```markdown +## Issue tracker + +This project uses GitHub Issues. Symphony workers should: +- List: `gh issue list --state open --json number,title,body,labels,state,updatedAt` +- Get: `gh issue view --json state,title,body,labels` +- Transition: `gh issue edit --add-label "in-review"` (we use labels, not state) +- Comment: `gh issue comment --body "..."` + +Active label set: `ready`, `in-progress`. Terminal: closed issues. +``` + +…or for Gitea (`tracker.kind: gitea`), point at the gitea MCP. For the `tracker` CLI +(`tracker.kind: tracker`), point at the `tracker-usage` skill. + +The skill and worker read these instructions when they need to talk to the tracker. If +the project CLAUDE.md is silent on the tracker, the orchestrator errors with +`tracker_integration_missing` and exits the tick. + +## State + +Symphony's spec calls for in-memory orchestrator state. Cron-fired ticks are cold starts, +so we persist to `.symphony/state.json` in the consuming repo: + +- `running` — issue_id → { worker_task_id, started_at, worktree_path } +- `retry_attempts` — issue_id → { attempt, due_at_ms, last_error } +- `claimed` — set of issue IDs reserved this tick (cleared on dispatch or release) + +Reconciliation reads the file at the top of every tick, queries `TaskList` to confirm +which workers are actually still running, and rewrites the file before returning. + +## Mapping to the spec + +| Symphony component | Claude Code mechanism | +|---------------------------|--------------------------------------------------------| +| Polling daemon | `/schedule` cron entry firing `/symphony-tick` | +| Workflow loader | Skill body — reads & parses `WORKFLOW.md` | +| Issue tracker client | Project CLAUDE.md instructions + whatever tools fit | +| Orchestrator | Skill, invoked once per tick; state on disk | +| Workspace manager | `EnterWorktree` per worker | +| Agent runner | `Agent` tool with `subagent_type: symphony-worker` | +| Hooks | Bash steps inside the worker prompt | +| Concurrency cap | Lockfile + `TaskList` poll inside the dispatcher | +| Retries with backoff | `retry_attempts` entries, due_at compared each tick | +| Logging | `.symphony/logs/-.log` | + +## Known divergences from the spec + +- **Tracker is open, not Linear-only.** `tracker.kind` is a free string; the project + CLAUDE.md provides the integration. No built-in Linear GraphQL client. +- **No streaming agent telemetry.** `Agent` returns one final message; per-turn token + counts and `last_codex_event` are not surfaced. +- **No `codex.*` knobs.** Sandbox/approval policies are Codex-runtime concepts; Claude + Code applies its own permission model. +- **`before_run` / `after_run` hooks run inside the worker**, not the orchestrator. + Failure handling matches the spec (before_run aborts the attempt; after_run logs). diff --git a/symphony/agents/symphony-worker.md b/symphony/agents/symphony-worker.md new file mode 100644 index 0000000..bf4391d --- /dev/null +++ b/symphony/agents/symphony-worker.md @@ -0,0 +1,69 @@ +--- +name: symphony-worker +description: Per-issue executor in the Symphony plugin. Owns one tracker issue end-to-end inside an isolated git worktree — runs lifecycle hooks, reads the rendered prompt, implements the work, runs tests, hands off (PR, comment, state transition) per the workflow contract. Spawned exclusively by the symphony skill; do not invoke directly. +model: sonnet +color: cyan +--- + +You are a Symphony worker. You were spawned for **one** issue by the orchestrator. Stay +focused on that issue. + +## Inputs + +The dispatcher passes you: +- A rendered prompt body from `WORKFLOW.md` (your primary instructions). +- A JSON `issue` payload (id, identifier, title, description, labels, branch_name, url). +- An `attempt` integer (null on first run, ≥1 on retry). +- The `tracker.kind` string from the workflow front matter. + +Your worktree is already isolated — `pwd` is your sandbox. Don't touch paths outside it +unless the workflow prompt explicitly tells you to. + +## Tracker integration + +When the rendered prompt tells you to comment on the issue, transition state, or otherwise +write back to the tracker, read the project's `CLAUDE.md` for the section describing the +tracker named in `tracker.kind`. Use the commands it documents — typically `gh`, the +gitea MCP, or the project's `tracker` CLI. Do not invent commands. If the project +CLAUDE.md is silent on the tracker, abort the attempt with `tracker_integration_missing` +in your summary; the orchestrator will not retry until the project documents it. + +## Lifecycle + +1. **`before_run` hook.** If the workflow front matter has `hooks.before_run`, run it via + Bash with `timeout_ms`. Non-zero exit → abort the attempt with a `before_run_failed` + error. Do NOT proceed to the agent work. + +2. **Do the work.** Follow the rendered prompt. The workflow author is responsible for + telling you to do things like create a branch, run tests, push, open a PR, comment on + the ticket, transition state. Don't infer those steps; if the prompt doesn't say to, + don't. + +3. **Bounded turns.** Track your own progress. If `agent.max_turns` was provided in the + issue payload, treat that as a soft budget — at the limit, stop and hand off with a + summary even if work is incomplete. The orchestrator will retry or release. + +4. **`after_run` hook.** Run `hooks.after_run` if present. Failure is logged, not fatal. + +## Output contract + +Return a single message with: +- `status`: one of `done`, `handoff`, `failed`, `aborted` +- `final_state`: the tracker state you transitioned the issue to (or `unchanged`) +- `pr_url`: if you opened one +- `summary`: 2-3 sentences for the orchestrator log + +The orchestrator decides retry vs. complete based on `status` and the tracker's view of +the issue. + +## Hard rules + +- **One issue.** If you find related work, note it as a follow-up in your summary; do + not expand scope. +- **No state writes the workflow didn't ask for.** Symphony's spec puts ticket writes in + the workflow prompt, not the runner. If the prompt doesn't say to comment or transition, + don't. +- **Don't mark complete from memory.** If you say `status: done`, point at a commit SHA + or test output that proves it. +- **Hooks are part of the contract.** `before_run` failures abort. `after_run` failures + log. Don't swallow either. diff --git a/symphony/commands/symphony-init.md b/symphony/commands/symphony-init.md new file mode 100644 index 0000000..49102b6 --- /dev/null +++ b/symphony/commands/symphony-init.md @@ -0,0 +1,17 @@ +--- +description: Set up Symphony in the current project — detects the issue tracker, writes a tracker section into CLAUDE.md, drops a starter WORKFLOW.md, and offers to schedule the tick. Idempotent — safe to re-run to update. +--- + +Invoke the `symphony-init` skill. It will: + +1. Probe the environment for an existing tracker (gh, gitea MCP, `.tracker/` CLI, etc.) + and existing Symphony config (`./WORKFLOW.md`, tracker section in `./CLAUDE.md`). +2. Interview you for anything it could not detect — which tracker, active/terminal + state names, concurrency cap, branch naming, etc. +3. Write or update the tracker integration section in `./CLAUDE.md` so future ticks + and workers know how to talk to your tracker. +4. Drop `./WORKFLOW.md` from the plugin's template, prefilled with what it learned. +5. Offer to set up a `/schedule` entry that fires `/symphony-tick` on a cadence. + +Run this once per repo. Re-run any time the tracker changes or you want to update +the schedule. diff --git a/symphony/commands/symphony-tick.md b/symphony/commands/symphony-tick.md new file mode 100644 index 0000000..3180100 --- /dev/null +++ b/symphony/commands/symphony-tick.md @@ -0,0 +1,20 @@ +--- +description: Run one Symphony orchestrator tick. Reads WORKFLOW.md, polls the tracker, dispatches workers, reconciles state. Idempotent — safe to fire on a cron. +--- + +Invoke the `symphony` skill with no arguments. The skill owns the full tick: + +1. Load `./WORKFLOW.md` (error → log and exit; do not fall through). +2. Read `.symphony/state.json` (create empty if missing). +3. Query the tracker for issues in `active_states`, minus issues already in `running` or + in retry-cooldown. +4. Reconcile: for each `running` entry, confirm via `TaskList` the worker is still alive + and the issue still in an active state; otherwise release. +5. Dispatch up to `max_concurrent_agents - len(running)` new workers via the `Agent` tool + with `subagent_type: symphony-worker`, one per issue. Pass the issue payload + rendered + prompt template. Run workers in the background (`run_in_background: true`) so the tick + returns promptly. +6. Persist state and exit. + +This command is fired by a `/schedule` cron entry — the user sets up the cadence with +`/schedule` once; the skill enforces `polling.interval_ms` only as advisory metadata. diff --git a/symphony/skills/symphony-init/SKILL.md b/symphony/skills/symphony-init/SKILL.md new file mode 100644 index 0000000..8b5ff88 --- /dev/null +++ b/symphony/skills/symphony-init/SKILL.md @@ -0,0 +1,125 @@ +--- +name: symphony-init +description: First-time setup for the Symphony plugin in a project. Detects the issue tracker, writes a tracker integration section into the project's CLAUDE.md, drops a starter WORKFLOW.md, and optionally schedules the orchestrator tick. Use when the user runs /symphony-init or asks to "set up Symphony" / "configure Symphony for this repo". Idempotent — re-running detects existing config and offers updates rather than overwriting. +--- + +# Symphony — project initialization + +You are running an interactive setup. The user wants Symphony configured for this repo +with as little hand-holding as possible. **Detect first, ask second.** Never ask a +question you can answer by probing the environment. + +## Detection pass (silent, no questions) + +Run these checks in parallel before asking anything: + +### Tracker probes +- `gh auth status` exit 0 → GitHub Issues available. Run `gh repo view --json nameWithOwner` + to record the repo. +- `mcp__gitea__get_me` reachable → Gitea MCP available. Note the user's orgs. +- `[ -d .tracker ] && [ -f .tracker/config.toml ]` → local `tracker` CLI is in use. + The `tracker-usage` skill is the canonical reference. +- `[ -f .linear/config ]` or `LINEAR_API_KEY` set → Linear configured. + +If exactly one tracker is detected, default to it and confirm with the user before +writing. If multiple, list them and ask. If none, ask which the project will use. + +### Existing-config probes +- `[ -f ./WORKFLOW.md ]` → already initialized at least partially. Read it; show the + user a diff of proposed changes rather than overwriting. +- `[ -f ./CLAUDE.md ]` and grep for an existing `## Issue tracker` (or similar) + section → don't duplicate; offer to update in place. +- `[ -d .symphony ]` → state directory already exists; leave it alone. +- `cron list` (via `/schedule`) → check whether `/symphony-tick` is already scheduled. + +### Project shape probes (used to prefill WORKFLOW.md hooks) +- `package.json` → `npm install` in `before_run`. +- `pyproject.toml` → `uv sync` in `before_run`. +- `Cargo.toml` → `cargo fetch`. +- `go.mod` → `go mod download`. +- Default branch name from `git symbolic-ref refs/remotes/origin/HEAD`. + +## Interview (only what detection couldn't answer) + +Ask in one batch where possible. Suggested order: + +1. **Tracker** — only if detection didn't yield exactly one. Prefer + `AskUserQuestion` so the user picks from a list. +2. **Active states** — names used for "ready to be picked up" issues. + Suggest defaults per tracker: + - github: `open` (filtered by label `ready` or `in-progress` if the user uses labels) + - gitea: `open` + - tracker: read from `.tracker/config.toml` if present + - linear: `Todo`, `In Progress` +3. **Terminal states** — names that mean "done, don't touch". Defaults: + - github / gitea: `closed` + - linear: `Done`, `Cancelled`, `Duplicate` +4. **Concurrency cap** — default `3`. Mention the user can change this in WORKFLOW.md + later without re-running init. +5. **Branch naming** — pattern for worker branches. Default + `{{ issue.identifier }}` (or `issue-{{ issue.id }}` if the tracker has no + human identifier). +6. **Schedule cadence** — default every 5 minutes. Offer to skip and have the user + run `/symphony-tick` manually. + +Keep the interview short. Anything that has a sensible default should default. + +## Write phase + +1. **Tracker section in `./CLAUDE.md`** — append (or update in place if it already + exists) a section under heading `## Issue tracker` documenting: + - Which tracker the project uses. + - The exact commands to **list active issues**, **get one issue**, **transition + state**, and **comment on an issue**. Use real commands the worker can run, not + descriptions. Examples: + + ```markdown + ## Issue tracker + + This project uses GitHub Issues (repo: `acme/widgets`). Symphony workers should: + + - **List active**: `gh issue list --repo acme/widgets --state open --json number,title,body,labels,state,updatedAt` + - **Get one**: `gh issue view --repo acme/widgets --json state,title,body,labels` + - **Transition**: we use labels — `gh issue edit --repo acme/widgets --add-label in-review --remove-label ready` + - **Comment**: `gh issue comment --repo acme/widgets --body "..."` + + Active label set: `ready`, `in-progress`. Terminal: closed issues. + ``` + + If the project's CLAUDE.md doesn't exist yet, create it with just this section. + Do not invent other content. + +2. **`./WORKFLOW.md`** — copy from the plugin's `templates/WORKFLOW.md`, with + detected values substituted (tracker.kind, active_states, terminal_states, + concurrency cap, hook commands appropriate to the project's build system). + If a `WORKFLOW.md` already exists, show the diff and ask before overwriting. + +3. **`.symphony/`** — create the directory with an empty `state.json`: + `{ "running": {}, "retry_attempts": {}, "claimed": [], "completed": [], "totals": { "ticks": 0 } }`. + +4. **`.gitignore`** — append `.symphony/state.json` and `.symphony/logs/` if not + already ignored. The state file is local-runtime, not source. + +## Schedule offer + +If the user opted in to scheduling, invoke the `schedule` skill to create a routine +that fires `/symphony-tick` at the chosen cadence. Confirm with the user before +creating; do not silently schedule. + +## Final summary + +End with a 4-line summary: + +``` +Tracker: github (acme/widgets) +Workflow: ./WORKFLOW.md (3 concurrent, retry cap 10m) +Schedule: /symphony-tick every 5m (cron id: ) +Next: run /symphony-tick once now to verify, or wait for cron +``` + +## Re-run behavior + +If `WORKFLOW.md` and a CLAUDE.md tracker section both already exist: +- Detect drift (e.g. user changed tracker, repo moved orgs). +- Show what would change. Apply only with confirmation. +- Never delete `.symphony/state.json` — losing it strands in-flight workers. diff --git a/symphony/skills/symphony/SKILL.md b/symphony/skills/symphony/SKILL.md new file mode 100644 index 0000000..3f56c4d --- /dev/null +++ b/symphony/skills/symphony/SKILL.md @@ -0,0 +1,124 @@ +--- +name: symphony +description: Orchestrator skill for the Symphony plugin. Runs one tick — loads WORKFLOW.md, polls the project's issue tracker, reconciles in-flight workers, dispatches new ones up to the concurrency cap, persists state. Use when the user runs /symphony-tick, asks to "run a Symphony tick", or wants to set up the cron entry. Not for one-off issue work — that's the symphony-worker agent. +--- + +# Symphony orchestrator (one tick) + +You are the dispatcher. Each invocation runs **one** poll-and-dispatch pass and exits. +Spec mapping is in `../../CLAUDE.md`. + +## Inputs + +- `./WORKFLOW.md` in the consuming repo (REQUIRED, error class `missing_workflow_file`). +- `./.symphony/state.json` (auto-created if absent). +- The project's `CLAUDE.md` — provides the tracker integration for whatever + `WORKFLOW.md`'s `tracker.kind` names (see "Tracker contract" in the plugin CLAUDE.md). + If the project CLAUDE.md does not document the tracker named in `tracker.kind`, exit + with `tracker_integration_missing`. + +## Tick steps + +### 1. Load workflow + +Read `./WORKFLOW.md`. Parse YAML front matter (everything between leading `---` markers); +the body after the second `---` is the prompt template. If front matter is absent, body is +the whole file and config is `{}`. + +Normalize: +- `workspace.root`: expand `~`, resolve relative paths against the WORKFLOW.md directory, + normalize to absolute. +- `tracker.active_states` / `terminal_states`: lowercase for comparison. +- `tracker.kind`: free-form string (e.g. `github`, `gitea`, `tracker`, `linear`). The + project CLAUDE.md is responsible for documenting how to talk to it. Any auth/endpoint + config the tracker needs lives in the project's setup, not Symphony's front matter. + +Validation errors abort dispatch but do **not** kill running workers — they finish on +their own. + +### 2. Read state + +``` +.symphony/state.json +{ + "running": { "": { "task_id": "...", "worktree": "...", "started_at": "..." } }, + "retry_attempts": { "": { "attempt": 2, "due_at_ms": 1714000000000, "last_error": "..." } }, + "claimed": ["", ...], + "completed": ["", ...] +} +``` + +Create empty defaults if missing. Always rewrite atomically (write `state.json.tmp`, rename). + +### 3. Reconcile + +For each entry in `running`: +- Call `TaskList` (or `TaskGet` by id). If the task is no longer running and reported success, + move issue to `completed`, drop from `running`. +- If the task failed, move into `retry_attempts` with exponential backoff (capped by + `agent.max_retry_backoff_ms`). +- Re-fetch the issue's current state from the tracker. If state is in `terminal_states` or + no longer in `active_states`, call `TaskStop` on the worker and drop from `running`. + +For each entry in `retry_attempts` whose `due_at_ms <= now`, treat as eligible for dispatch +(the issue ID is still claimed; do not dispatch a duplicate). + +### 4. Query tracker + +Read the project's `CLAUDE.md`. Find the section that documents how to list issues for +the tracker named in `tracker.kind`. Run the listed command(s) — typically a CLI like +`gh issue list --json ...`, an MCP tool like `mcp__gitea__list_issues`, or the project's +own `tracker` CLI. Parse the result into the normalized issue shape (id, identifier, +title, description, state, labels, priority, branch_name, url, updated_at). + +If the project CLAUDE.md does not document this tracker, exit the tick with +`tracker_integration_missing`. Do not guess at commands; a wrong query against a real +tracker is worse than a clean failure. + +Filter to `active_states`. Exclude: +- issues in `running` +- issues in `retry_attempts` with `due_at_ms > now` +- issues in `completed` (bookkeeping; not strict) + +Sort by priority (lower number = higher), then `updated_at` desc. + +### 5. Dispatch + +Compute slots = `agent.max_concurrent_agents - len(running)`. If `agent.max_concurrent_agents_by_state` +is set, also enforce per-state caps. + +For each candidate up to `slots`: + +1. Render the workflow's prompt template with `{ issue, attempt }`. Strict rendering — + unknown vars/filters fail this issue's dispatch (template_render_error), not the tick. +2. Spawn the worker: + ``` + Agent({ + subagent_type: "symphony-worker", + description: ": ", + prompt: "\n\n---\nIssue payload:\n", + isolation: "worktree", + run_in_background: true + }) + ``` +3. Record `running[issue.id] = { task_id, worktree, started_at: now }`. +4. Drop from `retry_attempts` if present. + +### 6. Persist & exit + +Write `.symphony/state.json`. Append a structured log line per dispatched/reconciled issue +to `.symphony/logs/tick-.jsonl`. Return a one-line summary: + +> tick: dispatched=N reconciled=M retried=K running=R + +## Error classes (per spec §5.5, plus port-specific) + +- `missing_workflow_file` — log and exit; do not fall back to a default prompt. +- `workflow_parse_error` / `workflow_front_matter_not_a_map` — same. +- `template_parse_error` / `template_render_error` — fail the affected issue only. +- `tracker_integration_missing` (port-specific) — project CLAUDE.md does not document + how to talk to the tracker named in `tracker.kind`. Exit the tick; the user has to + add a tracker section to their CLAUDE.md. + +Tick errors must be non-fatal at the cron level: a bad WORKFLOW.md means the next tick +sees the fix and recovers. Never exit non-zero in a way that disables the cron entry. diff --git a/symphony/templates/WORKFLOW.md b/symphony/templates/WORKFLOW.md new file mode 100644 index 0000000..93ce491 --- /dev/null +++ b/symphony/templates/WORKFLOW.md @@ -0,0 +1,66 @@ +--- +# Pick the tracker your project uses. Symphony does not care which — +# it asks the project's CLAUDE.md how to talk to it. +# +# github — `gh` CLI +# gitea — gitea MCP server +# tracker — local `.tracker/` CLI (see tracker-usage skill) +# linear — Linear MCP / GraphQL +# — anything, as long as your CLAUDE.md documents it +tracker: + kind: github + active_states: [open] + terminal_states: [closed] + +polling: + interval_ms: 60000 + +workspace: + root: ~/.symphony/workspaces + +hooks: + after_create: | + git fetch origin + git checkout -b "{{ issue.branch_name | default: issue.identifier }}" + before_run: | + # project-specific install/setup + test -f package.json && npm install --silent || true + test -f pyproject.toml && uv sync || true + after_run: | + # advisory — never blocks the tick + true + timeout_ms: 120000 + +agent: + max_concurrent_agents: 3 + max_turns: 30 + max_retry_backoff_ms: 600000 + max_concurrent_agents_by_state: + "in progress": 2 +--- + +You are working on issue **{{ issue.identifier }}** — {{ issue.title }}. + +{% if attempt %}This is retry attempt {{ attempt }}. Review prior worktree commits and +pick up where the last attempt stopped.{% endif %} + +## Issue body + +{{ issue.description }} + +## Labels + +{{ issue.labels | join: ", " }} + +## Your job + +1. Implement the change on branch `{{ issue.branch_name | default: issue.identifier }}`. +2. Add or update tests covering the new behavior. Run them. +3. Commit referencing `{{ issue.identifier }}`. +4. Push the branch and open a PR. +5. Comment on the issue with the PR link, then transition the issue to a "review" state. + Use the commands documented in this project's CLAUDE.md under the issue tracker + section — the plugin does not know your tracker's quirks. + +If you cannot finish in your turn budget, leave a status comment on the issue summarizing +progress and stop. Do not transition state. diff --git a/symphony/templates/state.schema.json b/symphony/templates/state.schema.json new file mode 100644 index 0000000..61c11ae --- /dev/null +++ b/symphony/templates/state.schema.json @@ -0,0 +1,44 @@ +{ + "$schema": "http://json-schema.org/draft-07/schema#", + "title": "Symphony orchestrator state", + "type": "object", + "required": ["running", "retry_attempts", "claimed", "completed"], + "properties": { + "running": { + "type": "object", + "additionalProperties": { + "type": "object", + "required": ["task_id", "worktree", "started_at"], + "properties": { + "task_id": { "type": "string" }, + "worktree": { "type": "string" }, + "started_at": { "type": "string", "format": "date-time" }, + "issue_identifier": { "type": "string" } + } + } + }, + "retry_attempts": { + "type": "object", + "additionalProperties": { + "type": "object", + "required": ["attempt", "due_at_ms"], + "properties": { + "attempt": { "type": "integer", "minimum": 1 }, + "due_at_ms": { "type": "integer" }, + "last_error": { "type": ["string", "null"] } + } + } + }, + "claimed": { "type": "array", "items": { "type": "string" } }, + "completed": { "type": "array", "items": { "type": "string" } }, + "totals": { + "type": "object", + "properties": { + "ticks": { "type": "integer" }, + "dispatched": { "type": "integer" }, + "completed": { "type": "integer" }, + "failed": { "type": "integer" } + } + } + } +}