Symphony turns project work into isolated, autonomous implementation runs, so teams manage work instead of supervising coding agents.
This is a Rust implementation of the Symphony Service Specification. Symphony polls an
issue tracker, creates a per-issue workspace, and runs a coding-agent session inside it — then keeps
watching the tracker so a ticket moving to Done stops the run and cleans up after it.
Two agent backends ship: the Codex app-server (default, what the spec targets) and Claude Code in headless mode. Everything above the execution layer is identical either way.
Warning
Symphony runs a coding agent unattended against your repository. Read docs/SECURITY.md before pointing it at anything you care about.
No credentials, no Codex install, no network:
make demoThat builds the project and runs it against examples/demo/, driving
symphony-mock-codex — a simulator that speaks the Codex app-server wire protocol. Open
http://127.0.0.1:8420 to watch sessions run.
While it runs, edit examples/demo/issues.json: move DEMO-2 to "Done" and watch reconciliation
stop the session and delete its workspace. Edit examples/demo/WORKFLOW.md and watch the new
settings apply without a restart.
WORKFLOW.md ──▶ config + prompt ──┐
▼
issue tracker ──▶ poll tick ──▶ orchestrator ──▶ worker ──▶ workspace/ ──▶ codex app-server
▲ │ (per issue) │
└── reconcile ─┘◀──────── runtime events ──────────────────┘
│
└──▶ structured logs + dashboard + JSON API
Everything is driven by one repo-owned file. WORKFLOW.md holds runtime settings in YAML front
matter and the per-issue prompt in its Markdown body. Symphony watches it and re-applies changes
live — no restart, and an invalid edit keeps the last known good configuration instead of taking the
service down.
| Guarantee | Where |
|---|---|
| One authority mutates scheduling state, so an issue can never be dispatched twice | src/orchestrator/ |
Agents run only inside <workspace.root>/<sanitized-issue-key> | src/workspace.rs |
| Terminal tickets stop live runs and get their workspaces removed | src/orchestrator/mod.rs |
| Failures retry with exponential backoff; clean exits re-check the tracker after 1s | src/orchestrator/mod.rs |
| Tracker credentials never enter the agent's process environment | src/shell.rs, src/tracker/ |
Requires Rust 1.82+ and a POSIX shell (bash or sh) on PATH. On Windows, Git Bash satisfies
this; without it, hooks fall back to cmd /C.
cargo build --releaseFor real runs you also need an agent CLI on PATH: the
Codex CLI with codex app-server available (default), or
the Claude Code CLI for agent.kind: claude_code.
symphony [OPTIONS] [path-to-WORKFLOW.md]| Option | Meaning |
|---|---|
<path-to-WORKFLOW.md> | Workflow file to run. Defaults to ./WORKFLOW.md |
--port <PORT> | Serve the dashboard and JSON API on loopback. Overrides server.port. 0 picks an ephemeral port |
--log-file <PATH> | Mirror structured logs to a file as well as stderr |
-h, --help / -V, --version | Usage and version |
SYMPHONY_LOG sets verbosity using tracing filter syntax (default info).
Exit codes: 0 clean start and shutdown, 1 startup validation failed, 2 usage error.
WORKFLOW.md in this repo is a commented, production-shaped example targeting
Linear. The full field reference is SPEC.md §5.3; the summary:
| Key | Default | Notes |
|---|---|---|
tracker.kind | — | Required.file or linear |
tracker.provider | {} | Adapter-owned. $VAR values resolve from the environment |
tracker.required_labels | [] | Every label must be present. Case-insensitive |
tracker.active_states / terminal_states | adapter default | Provider-native names, compared case-insensitively |
polling.interval_ms | 30000 | |
workspace.root | <temp>/symphony_workspaces | ~ and $VAR expand; relative paths resolve against WORKFLOW.md |
hooks.after_create / before_run / after_run / before_remove | none | Shell scripts run with the workspace as cwd |
hooks.timeout_ms | 60000 | |
agent.kind | codex | Extension.codex or claude_code |
agent.max_concurrent_agents | 10 | |
agent.max_turns | 20 | Turns inside one worker session |
agent.max_retry_backoff_ms | 300000 | Caps 10s · 2^(attempt-1) |
agent.max_concurrent_agents_by_state | {} | Per-state overrides |
agent.max_continuations_per_issue | 25 | Extension. Suspends an issue that keeps finishing without leaving its state. 0 disables |
codex.command | codex app-server | Run via the POSIX shell in the workspace |
codex.approval_policy | on-request | Passed through to Codex |
codex.thread_sandbox | workspace-write | Passed through to Codex |
codex.turn_sandbox_policy | omitted | Passed through to Codex |
codex.turn_timeout_ms | 3600000 | Silence window while a turn streams |
codex.read_timeout_ms | 5000 | Request/response timeout |
codex.stall_timeout_ms | 300000 | Orchestrator-side; 0 disables |
codex.auto_approve | false | Extension. See docs/SECURITY.md |
claude.* | — | Extension. Claude Code backend settings — see docs/backends/claude-code.md |
server.port | none | Extension. Enables the console and JSON API |
server.board_states | [] | Extension. Extra console columns for states Symphony does not poll, such as Human Review |
Hook failures are fatal for after_create (aborts workspace creation) and before_run (aborts the
attempt); after_run and before_remove failures are logged and ignored.
The Markdown body is rendered per issue with Liquid semantics in strict mode — an unknown
variable or filter fails the run rather than silently rendering nothing. Available variables are
issue (every normalized field, including labels and blocked_by) and attempt (nil on the
first run, an integer on retries and continuations).
Only the first turn of a session gets this prompt. Continuation turns run on the same live thread and receive short continuation guidance instead, so the task is never restated into a context that already contains it.
| Kind | Reference | Notes |
|---|---|---|
codex (default) | SPEC.md §10 | Codex app-server over JSON-RPC/stdio. What core conformance targets |
claude_code | docs/backends/claude-code.md | Claude Code CLI in headless mode. Ships an MCP bridge so tracker tools work without giving the agent your credential |
Starting Symphony from inside an interactive Claude Code session used to destabilise that session:
the agent inherited CLAUDECODE and CLAUDE_CODE_SESSION_ID and presented as the same session.
Symphony now strips that namespace before launch — see
co-existing with an interactive Claude Code.
A workflow prompt must move the ticket. Symphony re-dispatches after every clean worker exit
(SPEC.md §7.1); an issue that never leaves an active state would otherwise loop forever, spawning an
agent process each pass. agent.max_continuations_per_issue bounds that and reports the issue as
suspended.
| Kind | Profile | Use for |
|---|---|---|
file | docs/adapters/file.md | Local runs, demos, CI, a checked-in work queue. Ships three scoped agent tools |
linear | docs/adapters/linear.md | Production. Ships three scoped agent tools |
Symphony reads from trackers; it does not write to them. Ticket mutations happen through
provider-native tools that the agent calls and Symphony executes host-side with its own credential,
so the agent never sees a token. A successful run usually ends at a handoff state such as
Human Review, not Done.
Structured key=value logs go to stderr (and optionally a file). Issue records carry issue_id and
issue_identifier; session records add session_id.
With --port or server.port set, Symphony also serves:
| Route | Returns |
|---|---|
GET / | Operator console (self-contained HTML, light and dark) |
GET /api/v1/state | The board, running sessions, retry queue, suspended issues, token/runtime/cost totals, rate limits |
GET /api/v1/<issue_identifier> | Per-issue debug detail; 404 with an error envelope if unknown |
POST /api/v1/refresh | Queues an immediate poll + reconciliation cycle (202) |
GET/PUT /api/v1/workflow | Read or replace WORKFLOW.md. A save that does not parse is refused |
POST /api/v1/<id>/state | Move the issue to {"state_name": "..."} |
POST /api/v1/<id>/stop | Stop the live run, keep the workspace |
POST /api/v1/<id>/dispatch | Dispatch now, without waiting for the next poll |
POST /api/v1/<id>/retry | Fire a queued retry immediately |
POST /api/v1/<id>/unsuspend | Clear a continuation-guard suspension |
POST /api/v1/<id>/workspace/remove | Delete the issue's workspace directory |
POST /api/v1/<id>/remove | Remove the issue from the tracker, where the adapter supports it |
Binds loopback by default and has no authentication: anything that can reach the port can
dispatch agents and edit the workflow. Errors use {"error":{"code":"...","message":"..."}}.
Symphony's control surface is a kanban board over tracker state, because state is the control:
the orchestrator dispatches anything in active_states and reconciliation stops a run the moment an
issue leaves them. Dragging a card from Todo to In Progress starts an agent; dragging it to
Done stops one.
Per-card controls cover the run lifecycle — stop, dispatch now, retry now, clear a suspension,
remove the workspace, remove the issue. Idle cards say why they are idle (missing required label(s): ready, blocked by MT-9, all 4 agent slots are busy) rather than leaving you to infer
it. WORKFLOW.md is editable in place, and a save that does not parse is refused instead of
replacing a working configuration.
It is keyboard-first: Ctrl+K opens a command palette over every issue and action, j/k move the selection, 1…9 move an issue to column N, and ? lists the rest.
Columns come from tracker.active_states and terminal_states. Handoff states are not polled, so
add them to server.board_states to get a column you can drag work back out of.
make check # fmt --check, clippy -D warnings, and the full test suite
make test
make demoThe suite is organized around SPEC.md §17's validation matrix — see
docs/CONFORMANCE.md for the requirement-by-requirement mapping. Tests drive
the real orchestrator, transport, MCP bridge and HTTP surface; only the agent process is simulated,
by symphony-mock-codex and symphony-mock-claude.
The spec leaves several decisions to implementations. This one chose:
- Trust posture. For Codex, approvals are refused by default;
codex.auto_approveopts into granting them, and user-input requests always fail the run rather than stalling it. For Claude Code,claude.permission_modedefaults tobypassPermissions— the agent is granted what it needs to finish unattended, which makes real isolation around the process mandatory. See docs/SECURITY.md. - Launcher shell.
bash -lc, falling back tosh -lc, thencmd /Con Windows hosts with no POSIX shell. Windows' WSLbash.exeshims are skipped — they are POSIX shells, but they run inside a Linux VM rather than on the host holding the workspace — so a Windows host resolves Git Bash, fromPATHor its usual install location. Killing a session kills the whole process tree, since Windows has noexec. - Workspace keys. Characters outside
[A-Za-z0-9._-]become_; if that changes the identifier (or it exceeds 96 characters), an 80-bit base32 digest of the original is appended, so distinct identifiers cannot collide. - Blank vs. absent config. An explicitly blank
codex.commandis a validation error, not a request for the default. - Workspace population. Not built in. Use
after_createandbefore_runhooks. - Restart recovery. Scheduler state is in-memory by design. A restart recovers by sweeping terminal workspaces, re-polling and re-dispatching; retry timers and live sessions do not survive.
Not implemented: the Appendix A SSH worker extension.
Apache License 2.0. See NOTICE.
spec/SPEC.md is redistributed unmodified from openai/symphony under the same license.
This is an independent implementation, not affiliated with or endorsed by OpenAI.