Skip to content

Repository files navigation

Symphony

CILicense

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.

Try it in 30 seconds

No credentials, no Codex install, no network:

make demo

That 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.

How it works

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.

GuaranteeWhere
One authority mutates scheduling state, so an issue can never be dispatched twicesrc/orchestrator/
Agents run only inside <workspace.root>/<sanitized-issue-key>src/workspace.rs
Terminal tickets stop live runs and get their workspaces removedsrc/orchestrator/mod.rs
Failures retry with exponential backoff; clean exits re-check the tracker after 1ssrc/orchestrator/mod.rs
Tracker credentials never enter the agent's process environmentsrc/shell.rs, src/tracker/

Install

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 --release

For 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.

Usage

symphony [OPTIONS] [path-to-WORKFLOW.md]
OptionMeaning
<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, --versionUsage 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.

Configuring WORKFLOW.md

WORKFLOW.md in this repo is a commented, production-shaped example targeting Linear. The full field reference is SPEC.md §5.3; the summary:

KeyDefaultNotes
tracker.kindRequired.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_statesadapter defaultProvider-native names, compared case-insensitively
polling.interval_ms30000
workspace.root<temp>/symphony_workspaces~ and $VAR expand; relative paths resolve against WORKFLOW.md
hooks.after_create / before_run / after_run / before_removenoneShell scripts run with the workspace as cwd
hooks.timeout_ms60000
agent.kindcodexExtension.codex or claude_code
agent.max_concurrent_agents10
agent.max_turns20Turns inside one worker session
agent.max_retry_backoff_ms300000Caps 10s · 2^(attempt-1)
agent.max_concurrent_agents_by_state{}Per-state overrides
agent.max_continuations_per_issue25Extension. Suspends an issue that keeps finishing without leaving its state. 0 disables
codex.commandcodex app-serverRun via the POSIX shell in the workspace
codex.approval_policyon-requestPassed through to Codex
codex.thread_sandboxworkspace-writePassed through to Codex
codex.turn_sandbox_policyomittedPassed through to Codex
codex.turn_timeout_ms3600000Silence window while a turn streams
codex.read_timeout_ms5000Request/response timeout
codex.stall_timeout_ms300000Orchestrator-side; 0 disables
codex.auto_approvefalseExtension. See docs/SECURITY.md
claude.*Extension. Claude Code backend settings — see docs/backends/claude-code.md
server.portnoneExtension. 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.

Prompt template

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.

Agent backends

KindReferenceNotes
codex (default)SPEC.md §10Codex app-server over JSON-RPC/stdio. What core conformance targets
claude_codedocs/backends/claude-code.mdClaude 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.

Tracker adapters

KindProfileUse for
filedocs/adapters/file.mdLocal runs, demos, CI, a checked-in work queue. Ships three scoped agent tools
lineardocs/adapters/linear.mdProduction. 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.

Observability

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:

RouteReturns
GET /Operator console (self-contained HTML, light and dark)
GET /api/v1/stateThe 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/refreshQueues an immediate poll + reconciliation cycle (202)
GET/PUT /api/v1/workflowRead or replace WORKFLOW.md. A save that does not parse is refused
POST /api/v1/<id>/stateMove the issue to {"state_name": "..."}
POST /api/v1/<id>/stopStop the live run, keep the workspace
POST /api/v1/<id>/dispatchDispatch now, without waiting for the next poll
POST /api/v1/<id>/retryFire a queued retry immediately
POST /api/v1/<id>/unsuspendClear a continuation-guard suspension
POST /api/v1/<id>/workspace/removeDelete the issue's workspace directory
POST /api/v1/<id>/removeRemove 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":"..."}}.

The console

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, 19 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.

Development

make check # fmt --check, clippy -D warnings, and the full test suite
make test
make demo

The 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.

Implementation-defined behavior

The spec leaves several decisions to implementations. This one chose:

  • Trust posture. For Codex, approvals are refused by default; codex.auto_approve opts into granting them, and user-input requests always fail the run rather than stalling it. For Claude Code, claude.permission_mode defaults to bypassPermissions — 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 to sh -lc, then cmd /C on Windows hosts with no POSIX shell. Windows' WSL bash.exe shims 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, from PATH or its usual install location. Killing a session kills the whole process tree, since Windows has no exec.
  • 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.command is a validation error, not a request for the default.
  • Workspace population. Not built in. Use after_create and before_run hooks.
  • 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.

License

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.

About

Symphony: a Rust implementation of the OpenAI Symphony orchestrator spec — turns tracker issues into isolated, autonomous coding-agent runs.

Resources

Security policy

Stars

0 stars

Watchers

0 watching

Forks

Releases

Packages

Contributors

Languages