Skip to content

silas-dsc/symphony

Folders and files

NameName
Last commit message
Last commit date

Latest commit

 

History

19 Commits
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 

Repository files navigation

Symphony

An autonomous coding orchestrator. Symphony polls a Linear board, picks up tickets in your configured active states, and spawns a headless Claude Code agent per ticket. Each agent clones a fresh workspace, implements the ticket end-to-end (branch → code → tests → PR), and updates Linear as it goes. A live terminal dashboard lets you watch everything in real time.

Linear board  ──poll──▶  Symphony orchestrator  ──spawn──▶  Claude Code agent × N
                                │                                    │
                         /status (HTTP)                     workspace + git + PR
                                │
                         symphony-status (TUI)

Prerequisites

Dependency Min version Purpose
Node.js 20 Runtime
Claude Code CLI latest Agent runner
GitHub CLI (gh) 2.x Agents create PRs
Git 2.x Workspace cloning
Linear account Issue source

Installing prerequisites

macOS
# Node.js — via nvm (recommended) or direct installer
curl -o- https://raw.githubusercontent.com/nvm-sh/nvm/v0.39.7/install.sh | bash
source ~/.zshrc          # or ~/.bashrc if you use bash
nvm install 20
nvm use 20

# Claude Code CLI
npm install -g @anthropic-ai/claude-code

# GitHub CLI
brew install gh

# Authenticate gh (do this once)
gh auth login
Windows

Symphony's workspace hooks run as Bash scripts (bash -l). Windows requires WSL 2 (Windows Subsystem for Linux). Run everything inside a WSL terminal.

# Inside WSL (Ubuntu/Debian):

# Node.js via nvm
curl -o- https://raw.githubusercontent.com/nvm-sh/nvm/v0.39.7/install.sh | bash
source ~/.bashrc
nvm install 20
nvm use 20

# Claude Code CLI
npm install -g @anthropic-ai/claude-code

# GitHub CLI
(type -p wget >/dev/null || (sudo apt update && sudo apt-get install wget -y)) \
  && sudo mkdir -p -m 755 /etc/apt/keyrings \
  && wget -qO- https://cli.github.com/packages/githubcli-archive-keyring.gpg \
     | sudo tee /etc/apt/keyrings/githubcli-archive-keyring.gpg > /dev/null \
  && sudo chmod go+r /etc/apt/keyrings/githubcli-archive-keyring.gpg \
  && echo "deb [arch=$(dpkg --print-architecture) signed-by=/etc/apt/keyrings/githubcli-archive-keyring.gpg] https://cli.github.com/packages stable main" \
     | sudo tee /etc/apt/sources.list.d/github-cli.list > /dev/null \
  && sudo apt update && sudo apt install gh -y

# Authenticate gh (do this once)
gh auth login

Tip: SSH agent forwarding works differently in WSL. If your repo uses SSH remotes, follow GitHub's WSL SSH guide.


Installation

git clone git@github.com:silas-dsc/symphony.git
cd symphony
npm install
npm run build

Configuration

1. Environment variables

Copy the example and fill in your values:

cp .env.example .env
Variable Required Description
LINEAR_API_KEY Yes Linear personal API key. Generate at linear.app/settings/apiPersonal API keys
ANTHROPIC_API_KEY No Anthropic API key. If omitted, Claude Code uses browser OAuth instead (see Claude Code auth below)

2. WORKFLOW.md

WORKFLOW.md is the single configuration file that controls both the orchestrator and the prompt sent to each agent. It uses YAML front matter for settings, with the rest of the file as a Liquid-templated prompt.

A minimal example:

---
tracker:
  kind: linear
  project_slug: "ALL"       # or a specific project slug; "ALL" = whole team
  team_key: "ENG"           # required when project_slug is "ALL"
  active_states:
    - In Progress
  terminal_states:
    - Done
    - Cancelled
    - Canceled
    - Closed
    - Duplicate

polling:
  interval_ms: 30000        # how often to poll Linear (ms)

workspace:
  root: ~/code/workspaces   # where per-ticket clones are created

hooks:
  after_create: |           # runs once after workspace is cloned
    git clone git@github.com:my-org/my-repo.git .
    npm install
  before_remove: |          # runs before a workspace is deleted
    echo "Cleaning up"

agent:
  max_concurrent_agents: 3
  max_turns: 30
  max_retry_backoff_ms: 300000
---

You are an autonomous coding agent working on {{ issue.identifier }}: {{ issue.title }}
...

All configuration fields

Field Default Description
tracker.kind linear Only linear is supported
tracker.project_slug Linear project slug, or "ALL" to watch a whole team
tracker.team_key Linear team key (e.g. "ENG"); required when project_slug is "ALL"
tracker.active_states ["Todo","In Progress"] States that trigger agent dispatch
tracker.terminal_states ["Done","Cancelled",…] States that stop a running agent and clean up its workspace
tracker.endpoint https://api.linear.app/graphql Linear GraphQL endpoint
tracker.api_key $LINEAR_API_KEY Override env-var lookup with a literal key (not recommended)
polling.interval_ms 30000 Poll interval in milliseconds
workspace.root system temp dir Absolute path (supports ~) where per-ticket workspaces are created
hooks.after_create Shell script run once after the workspace directory is created
hooks.before_run Shell script run before each agent attempt
hooks.after_run Shell script run after each agent attempt
hooks.before_remove Shell script run before the workspace is deleted
hooks.timeout_ms 600000 Timeout for any single hook (ms); after_create can be slow on cold caches
agent.max_concurrent_agents 10 Total agents running in parallel
agent.max_turns 20 Maximum Claude turns per attempt before the agent is considered stalled
agent.max_retry_backoff_ms 300000 Maximum retry back-off (ms) for failed agents
agent.max_concurrent_agents_by_state {} Per-state concurrency cap, e.g. { "in progress": 2 }
server.port 7777 Port for the status HTTP server (loopback only)

Prompt template variables

The text below the YAML front matter is a Liquid template. Available variables:

Variable Type Description
issue.id string Linear internal UUID
issue.identifier string Human identifier, e.g. ENG-123
issue.title string Issue title
issue.description string | null Issue description (Markdown)
issue.state string Current workflow state name
issue.priority number | null Priority (0 = none, 1 = urgent, 4 = low)
issue.url string | null Linear issue URL
issue.labels string[] Label names (lowercased)
issue.branchName string | null Suggested git branch name from Linear
attempt number | null Retry attempt number (null on first attempt)

Claude Code authentication

Claude Code must be authenticated before Symphony can use it. Two options:

Option A — Browser OAuth (no API key needed)

Run the interactive CLI once and log in:

claude
# Type /login and follow the browser prompt

macOS: Credentials are stored in the system Keychain under Claude Code-credentials. They persist across reboots automatically.

Windows (WSL): Credentials are stored in ~/.claude/.credentials.json inside WSL. Re-authenticate if you get Not logged in errors after a reboot.

Option B — API key

Add ANTHROPIC_API_KEY=sk-ant-... to .env. This takes precedence over OAuth credentials and is better suited for server/CI environments.

Get a key at console.anthropic.com/settings/keys.


Running Symphony

# Start the orchestrator (reads WORKFLOW.md from the current directory)
node dist/index.js

# Or specify a different workflow file
node dist/index.js /path/to/WORKFLOW.md

# Override the status server port
node dist/index.js --port 8080

Symphony will:

  1. Validate configuration
  2. Fetch the Linear team URL for display in the TUI
  3. Poll Linear every polling.interval_ms milliseconds
  4. Spawn a Claude Code agent for each eligible ticket (up to max_concurrent_agents)
  5. Retry failed agents with exponential back-off
  6. Clean up workspaces when tickets reach a terminal state

Stop with Ctrl-C. In-flight agents are given 2 seconds to exit cleanly.


Status dashboard

While Symphony is running, open a second terminal:

node dist/status.js
┌ SYMPHONY STATUS
Agents: 2/3
Throughput: 142 tps
Runtime: 4m 12s
Tokens: in 84,231 | out 12,450 | total 96,681
Rate Limits: claude (five_hour) | status allowed | resets in 4h 31m | overage n/a
Project: https://linear.app/my-org/team/ENG/all
Next refresh: 1s

├ Running

  ISSUE                                  STAGE          PID      AGE / TURN  TOKENS     SESSION       EVENT
  ───────────────────────────────────────────────────────────────────────────────────────────────────────────
● ENG-42: Add Stripe webhook handling    In Progress    98123    3m 2s / 8   24,300     ab12...ef56   tool_use: Read src/payments/webhook.ts
● ENG-51: Fix login redirect loop        In Progress    98456    1m 18s / 3  8,100      cd34...gh78   tool_use: Bash git status

Press q or Ctrl-C to exit. Options:

node dist/status.js --port 8080       # connect to a non-default port
node dist/status.js --refresh-ms 500  # faster refresh

Architecture overview

src/
  index.ts        — entry point; CLI args, logger, starts orchestrator + status server
  orchestrator.ts — poll loop, dispatch, retry queue, state reconciliation
  agent.ts        — spawns `claude` subprocess, streams JSON events, returns AgentResult
  linear.ts       — GraphQL client for Linear (issues, states, team URL)
  workspace.ts    — creates/removes per-ticket directories; runs hooks via bash -l
  config.ts       — parses WORKFLOW.md (YAML front matter + Liquid prompt template)
  server.ts       — tiny HTTP server on 127.0.0.1:<port> serving GET /status as JSON
  status.ts       — full-screen ANSI TUI; polls /status and re-renders in place
  types.ts        — shared TypeScript interfaces

Development

npm run dev        # run with tsx (no build step, hot-ish reload via restart)
npm run build      # compile TypeScript → dist/

The project uses NodeNext module resolution. All imports inside src/ must include the .js extension (TypeScript compiles these to .js in dist/).


Troubleshooting

Symptom Fix
tracker.api_key is required Make sure .env exists with LINEAR_API_KEY=… and you ran node dist/index.js (not tsx src/index.ts without dotenv)
Not logged in · Please run /login Re-authenticate Claude Code: run claude, type /login
Hook times out Increase hooks.timeout_ms in WORKFLOW.md; default is 10 min
Status TUI shows Connection error The orchestrator isn't running, or is on a different port (use --port)
issue_title shows as identifier only The orchestrator was started before a recent update — restart it
Agents stall with no events for 5 min Symphony auto-terminates stalled agents and retries; check logs for the error

Platform notes

macOS Windows
Shell for hooks /bin/bash login shell Requires WSL 2 — hooks will fail on native Windows
nvm nvm.sh nvm-windows (outside WSL) or nvm.sh inside WSL
Claude Code credentials macOS Keychain (persist across reboots) ~/.claude/.credentials.json in WSL (may need re-auth after reboot)
SSH keys ~/.ssh/ + ssh-agent via Keychain Needs explicit ssh-agent setup in WSL — see GitHub docs
gh auth brew install gh && gh auth login Install inside WSL as shown in Prerequisites
File paths Standard POSIX Use WSL paths (/home/user/…), not Windows paths (C:\…)

About

No description, website, or topics provided.

Resources

Stars

Watchers

Forks

Releases

No releases published

Packages

 
 
 

Contributors