Files
claude-plugins/symphony/CLAUDE.md
movq cd3032458c feat(symphony): tracker-agnostic orchestrator plugin
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) <noreply@anthropic.com>
2026-04-29 21:03:21 -05:00

5.5 KiB

symphony

Claude Code port of the 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:

## 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 <number> --json state,title,body,labels`
- Transition: `gh issue edit <number> --add-label "in-review"` (we use labels, not state)
- Comment: `gh issue comment <number> --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/<issue>-<ts>.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).