# 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` entry inside the long-lived orchestrator session (also set up by init unless the user declines). 3. **Other triggers**: ticks can also be fired by remote channels (Discord DM, webhook, another session). The reconciliation logic handles cross-session liveness via worker heartbeats. 4. **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. ## Runtime model The orchestrator runs inside a **long-lived Claude Code session**. `/schedule` doesn't cold-start each tick — it sets the prompt for the running session. Tasks spawned in tick N stay visible to `TaskList` / `TaskGet` in tick N+1 within that session, so the session itself is the primary store of "what's running right now." Symphony stays open to **other trigger sources** too: a Discord channel could DM the session and ask for a tick, a webhook hook could trigger one, and a separate user session could conceivably spawn tasks against the same project. To keep all three modes honest, two layers carry state: 1. **`TaskList` / `TaskGet`** — fast path, authoritative for liveness *within this session*. 2. **`.symphony/state.json` + worker heartbeats** — durable record across sessions and restarts. Authoritative when `TaskList` doesn't know about a task ID. ### Reconciliation logic For each entry in `state.json`'s `running` map: - **In our `TaskList`** → it's our task; check status via `TaskGet`; reconcile normally. - **Not in our `TaskList`** → another session may own it, or the session that spawned it exited. Read the worker's heartbeat file (`.symphony/heartbeats/.json`). - **Heartbeat fresh** (within `agent.heartbeat_stale_ms`, default 5min) → leave alone; another session is owning it. - **Heartbeat stale** → consider abandoned; mark for retry, drop from `running`. - **No heartbeat** → ditto. This keeps the single-session case simple (TaskList wins) while supporting multi-session and crash recovery cleanly. ### State file shape - `running` — issue_id → { task_id, session_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) - `completed` — set of issue IDs (bookkeeping) ### Worker heartbeats Workers write `.symphony/heartbeats/.json` with `{ timestamp, status, task_id, session_id }` every minute (configurable via `agent.heartbeat_interval_ms`). On graceful exit they remove the file. Stale heartbeats — and stale `running` entries with no heartbeat at all — are how Symphony detects abandonment. ## Mapping to the spec | Symphony component | Claude Code mechanism | |---------------------------|--------------------------------------------------------| | Polling daemon | `/schedule` firing `/symphony-tick` in a long-lived session | | Workflow loader | Skill body — reads & parses `WORKFLOW.md` | | Issue tracker client | Project CLAUDE.md instructions + whatever tools fit | | Orchestrator | Skill, runs each tick within the same session | | Workspace manager | `EnterWorktree` per worker | | Agent runner | `Agent` tool with `subagent_type: symphony-worker` | | Hooks | Bash steps inside the worker prompt | | Concurrency cap | `TaskList` count + `state.json` cross-check | | Retries with backoff | `retry_attempts` entries, due_at compared each tick | | Liveness across sessions | Worker heartbeats in `.symphony/heartbeats/` | | 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).