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>
5.3 KiB
name, description
| name | description |
|---|---|
| symphony | 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.mdin the consuming repo (REQUIRED, error classmissing_workflow_file)../.symphony/state.json(auto-created if absent).- The project's
CLAUDE.md— provides the tracker integration for whateverWORKFLOW.md'stracker.kindnames (see "Tracker contract" in the plugin CLAUDE.md). If the project CLAUDE.md does not document the tracker named intracker.kind, exit withtracker_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": { "<issue_id>": { "task_id": "...", "worktree": "...", "started_at": "..." } },
"retry_attempts": { "<issue_id>": { "attempt": 2, "due_at_ms": 1714000000000, "last_error": "..." } },
"claimed": ["<issue_id>", ...],
"completed": ["<issue_id>", ...]
}
Create empty defaults if missing. Always rewrite atomically (write state.json.tmp, rename).
3. Reconcile
For each entry in running:
- Call
TaskList(orTaskGetby id). If the task is no longer running and reported success, move issue tocompleted, drop fromrunning. - If the task failed, move into
retry_attemptswith exponential backoff (capped byagent.max_retry_backoff_ms). - Re-fetch the issue's current state from the tracker. If state is in
terminal_statesor no longer inactive_states, callTaskStopon the worker and drop fromrunning.
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_attemptswithdue_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:
- 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. - Spawn the worker:
Agent({ subagent_type: "symphony-worker", description: "<issue.identifier>: <issue.title>", prompt: "<rendered template>\n\n---\nIssue payload:\n<json>", isolation: "worktree", run_in_background: true }) - Record
running[issue.id] = { task_id, worktree, started_at: now }. - Drop from
retry_attemptsif present.
6. Persist & exit
Write .symphony/state.json. Append a structured log line per dispatched/reconciled issue
to .symphony/logs/tick-<YYYYMMDD>.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 intracker.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.