For: developers with a Linear test project who want to run Symphony locally and start contributing.
Symphony-TS is a TypeScript implementation of the Symphony specification.
In one sentence: A long-running daemon that polls a Linear board, creates isolated workspaces for each active issue, launches a Codex (OpenAI coding agent) subprocess per issue, and manages concurrency, retries, state reconciliation, and observability.
Core data flow:
WORKFLOW.md (config + prompt template)
|
v
[Orchestrator] -- polls Linear --> active Issues
|
v
[WorkspaceManager] -- creates /tmp/symphony_workspaces/<issue-key>/
|
v
[AgentRunner] -- spawns codex app-server subprocess in workspace
|
v
Codex agent works on the issue, writes back to Linear via linear_graphql tool
src/
cli/ # Entry point: main.ts - parses CLI args, loads config, starts runtime
config/ # WORKFLOW.md parsing, typed config resolution, hot-reload file watcher
domain/ # Core type definitions (Issue, RunAttempt, LiveSession, etc.)
orchestrator/ # Dispatch core (core.ts) + runtime host (runtime-host.ts)
agent/ # AgentRunner (spawns Codex subprocess) + prompt builder
tracker/ # Linear GraphQL client, queries, response normalization
codex/ # Codex app-server protocol client + linear_graphql dynamic tool
workspace/ # Workspace directory management, path safety, lifecycle hooks
logging/ # Structured logging, session metrics, runtime snapshots
observability/ # Optional HTTP dashboard server
errors/ # Error code constants
index.ts # Public API exports
tests/ # Vitest tests, mirroring src/ structure
Key files at a glance:
| Concern | File |
|---|---|
| Dispatch logic | src/orchestrator/core.ts |
| Runtime startup | src/orchestrator/runtime-host.ts |
| Config resolution | src/config/config-resolver.ts |
| Defaults | src/config/defaults.ts |
| Linear client | src/tracker/linear-client.ts |
| Agent launch | src/agent/runner.ts |
| Prompt construction | src/agent/prompt-builder.ts |
| Codex protocol | src/codex/app-server-client.ts |
| CLI entry | src/cli/main.ts |
| Domain model | src/domain/model.ts |
Make sure you have:
- Node.js >= 22
- pnpm >= 10
- Codex CLI installed (
codex app-servercommand must be available)
node --version # must be v22+
pnpm --version # must be 10+
codex --version # must support codex app-serverpnpm install
pnpm buildBuild output goes to dist/. The CLI entry point is dist/src/cli/main.js.
Verify the build:
node dist/src/cli/main.js --help- Go to Linear -> Settings -> API -> Personal API Keys
- Create a new key and copy it
- Find your test project's slug (visible in the URL or project settings)
Export the key as an environment variable (never commit it):
export LINEAR_API_KEY="lin_api_xxxxxxxxxxxx"Or put it in an untracked .env.local and source it yourself — Symphony does not auto-load .env files.
Create a WORKFLOW.md in the target repository (the codebase Codex will work in), or in any directory you will run Symphony from.
Minimal working example:
---
tracker:
kind: linear
api_key: $LINEAR_API_KEY
project_slug: "your-project-slug"
active_states:
- "Todo"
- "In Progress"
terminal_states:
- "Closed"
- "Cancelled"
- "Canceled"
- "Duplicate"
- "Done"
polling:
interval_ms: 30000
workspace:
root: ~/symphony_workspaces
agent:
max_concurrent_agents: 2
max_turns: 20
codex:
command: "codex app-server"
approval_policy: "never"
turn_timeout_ms: 3600000
stall_timeout_ms: 300000
---
You are a software engineer working on a Linear issue.
Issue: {{ issue.identifier }} - {{ issue.title }}
State: {{ issue.state }}
Description: {{ issue.description | default: "No description provided." }}
{% if attempt %}Retry attempt: {{ attempt }}{% endif %}
Work on this issue. When done, use the linear_graphql tool to transition the
issue to "In Review" and leave a comment summarizing what you did.WORKFLOW.md field reference:
For an annotated file covering every field with defaults and comments, see WORKFLOW.template.md.
| Field | Description | Default |
|---|---|---|
tracker.kind |
Tracker backend. Only linear is supported |
linear |
tracker.endpoint |
GraphQL endpoint for the Linear API | https://api.linear.app/graphql |
tracker.api_key |
Linear API key; use $ENV_VAR to reference env |
Reads LINEAR_API_KEY env var |
tracker.project_slug |
Linear project slug — required | None |
tracker.active_states |
Issue states that trigger dispatch | [Todo, In Progress] |
tracker.terminal_states |
States that trigger workspace cleanup | [Closed, Cancelled, Canceled, Duplicate, Done] |
polling.interval_ms |
Poll interval in milliseconds | 30000 |
workspace.root |
Root directory for all workspaces | <os.tmpdir()>/symphony_workspaces |
hooks.after_create |
Shell command run after workspace is created | null |
hooks.before_run |
Shell command run before each agent turn (fatal on non-zero exit) | null |
hooks.after_run |
Shell command run after each agent turn (errors suppressed) | null |
hooks.before_remove |
Shell command run before workspace removal (errors suppressed) | null |
hooks.timeout_ms |
Max time in ms for any single hook | 60000 |
agent.max_concurrent_agents |
Global agent concurrency cap | 10 |
agent.max_turns |
Max Codex turns per run | 20 |
agent.max_retry_backoff_ms |
Max retry back-off delay in ms (exponential cap) | 300000 |
agent.max_concurrent_agents_by_state |
Per-state concurrency overrides (map of state → limit) | {} |
codex.command |
Shell command to launch Codex | codex app-server |
codex.approval_policy |
Codex approval policy: never / on-failure / always |
Inherits Codex default |
codex.thread_sandbox |
Thread-level sandbox mode (e.g. workspace-write) |
null |
codex.turn_sandbox_policy |
Per-turn sandbox policy object | null |
codex.turn_timeout_ms |
Max wall-clock time in ms for a full agent turn | 3600000 |
codex.read_timeout_ms |
Max time in ms to wait for the next Codex event before declaring stream stalled | 5000 |
codex.stall_timeout_ms |
Max silent time in ms before a running agent is declared stalled and stopped | 300000 |
server.port |
HTTP dashboard port; omit or null to disable |
null |
observability.dashboard_enabled |
Enable live dashboard updates when the HTTP server is running | true |
observability.refresh_ms |
Dashboard heartbeat interval in ms for time-based refreshes | 1000 |
observability.render_interval_ms |
Minimum spacing in ms between pushed dashboard renders | 16 |
The prompt body uses Liquid template syntax. Available variables:
{{ issue.identifier }},{{ issue.title }},{{ issue.description }}{{ issue.state }},{{ issue.url }},{{ issue.labels }}{{ attempt }}—nullon first run, integer on retries
# Run from the directory containing WORKFLOW.md
node dist/src/cli/main.js --acknowledge-high-trust-preview
# Or specify the WORKFLOW.md path explicitly
node dist/src/cli/main.js /path/to/your/WORKFLOW.md \
--acknowledge-high-trust-preview
# Enable the optional HTTP dashboard
node dist/src/cli/main.js --acknowledge-high-trust-preview --port 3000
--acknowledge-high-trust-previewis a required safety flag. Symphony runs agent code without sandboxing by default; this flag confirms you understand that.
- Open your Linear test project
- Create an issue and set its state to
TodoorIn Progress - Wait for the next poll cycle (default: 30 seconds)
- Watch Symphony's terminal output — the issue should be dispatched
- Codex will run inside
~/symphony_workspaces/<issue-key>/
pnpm test # run all tests once
pnpm test:watch # watch mode
pnpm typecheck # TypeScript type check only
pnpm lint # Biome lint check
pnpm format # Biome auto-formatEach issue moves through these internal states:
unclaimed -> claimed -> running -> (retry_queued -> running)* -> released
- unclaimed: fetched from Linear but not yet reserved
- claimed: slot reserved; prevents duplicate dispatch
- running: Codex agent is active
- retry_queued: agent exited, waiting to re-dispatch (normal exit: 1s delay; abnormal exit: exponential backoff capped at 5 minutes)
- released: issue reached a terminal state; claim freed
Configure shell scripts in WORKFLOW.md that run at workspace lifecycle points:
hooks:
after_create: |
git clone https://github.com/your-org/your-repo.git .
npm install
before_run: |
git pull --rebase
after_run: |
echo "Agent finished"
before_remove: |
echo "Workspace being cleaned up"
timeout_ms: 60000after_create is the most important hook — use it to clone your repo into the fresh workspace before the agent starts.
Every Codex agent run automatically gets a linear_graphql tool injected, allowing the agent to read and write Linear directly:
# Example mutation an agent might run to update issue state
mutation UpdateState($id: String!, $stateId: String!) {
issueUpdate(id: $id, input: { stateId: $stateId }) {
success
}
}agent:
max_concurrent_agents: 5 # global cap
max_concurrent_agents_by_state: # per-state fine-grained control
"in progress": 3
"todo": 2State keys are matched case-insensitively.
These fields take effect on the next poll tick without restarting Symphony:
polling.interval_msagent.max_concurrent_agentsagent.max_retry_backoff_mshooks.timeout_ms
Issues are not being dispatched after startup
- Verify
tracker.project_slugmatches exactly (check Linear project URL) - Verify
LINEAR_API_KEYis set and valid - Check that the issue's current state matches an entry in
active_states(comparison is case-insensitive after trim)
codex app-server command not found
- Confirm Codex CLI is installed and on
PATH - Use an absolute path in WORKFLOW.md:
codex.command: "/usr/local/bin/codex app-server"
Agent stalls and never finishes
codex.stall_timeout_ms(default 5 minutes) will kill and retry a stalled agent- Set
codex.stall_timeout_ms: 0to disable stall detection
How to watch runtime state
- Structured JSON logs are the primary observability surface
- Launch with
--port 3000to access the HTTP dashboard athttp://localhost:3000 - The dashboard serves an initial HTML snapshot, then stays current over
/api/v1/events