Symphony is a long-running orchestration service that continuously reads work from a GitHub Project, dispatches coding agents to work on issues in isolated repository workspaces, and manages the full lifecycle from dispatch through PR creation and handoff.
GitHub Project (Todo/In Progress)
│
▼
┌─────────┐ poll every N seconds
│Symphony │◄────────────────────────────
│Orchestr.│ │
└────┬────┘ │
│ dispatch eligible items │
▼ │
┌─────────┐ │
│ Worker │ clone repo, create branch │
│ │ run agent in workspace │
└────┬────┘ │
│ │
▼ │
┌─────────┐ │
│ claude │ read files, edit code, │
│ -p │ run tests, commit changes │
└────┬────┘ │
│ │
▼ │
┌─────────┐ │
│Write- │ push branch, create PR, │
│back │ update project status │
└────┬────┘ │
│ │
▼ │
GitHub PR ──── Human Review ────────────┘
- Go 1.26+ — for building Symphony
- git — for workspace operations
- Claude CLI — authenticated locally via
claude login - GitHub PAT — fine-grained token with repo, project, and issues permissions
- gh CLI — for the agent to post workpad comments on issues
# 1. Clone and build
git clone https://github.com/shivamstaq/github-symphony.git
cd github-symphony
go build -o symphony ./cmd/symphony
# 2. Create .env in the directory where you'll run Symphony
echo "GITHUB_TOKEN=ghp_your_token_here" > .env
# 3. Authenticate Claude CLI (one-time)
claude login
# 4. Copy and edit the example workflow
cp WORKFLOW.md.example WORKFLOW.md
# Edit WORKFLOW.md: set your tracker.owner, tracker.project_number, tracker.project_scope
# 5. Validate your setup
./symphony --doctor WORKFLOW.md
# 6. Run Symphony
./symphony --port 9097 WORKFLOW.mdImportant: Symphony loads
.envfrom the current working directory. Alwayscdinto the directory containing your.envbefore running the binary. Flags must come before the positionalWORKFLOW_PATHargument.
Go to your GitHub profile → Projects → New project, or use gh:
gh project create --owner YOUR_USER --title "My Project" --format jsonNote the number from the output (e.g., "number": 6).
Your project needs a Status single-select field with at least these values:
| Status | Purpose |
|---|---|
| Todo | Items ready for agent execution |
| In Progress | Items currently being worked on |
| Human Review | Items waiting for human review (handoff state) |
| Done | Completed items |
Symphony dispatches items in active_values (default: Todo, Ready, In Progress) and ignores items in terminal_values (default: Done, Closed, Cancelled). After an agent creates a PR, Symphony moves the item to the handoff_project_status value (e.g., Human Review).
# Create issues in your repository
gh issue create --repo YOUR_USER/YOUR_REPO \
--title "Fix the flaky test" \
--body "The auth module test fails intermittently."
gh issue create --repo YOUR_USER/YOUR_REPO \
--title "Add input validation" \
--body "Config parser accepts invalid values silently."
# Add them to your project
gh project item-add PROJECT_NUMBER --owner YOUR_USER \
--url https://github.com/YOUR_USER/YOUR_REPO/issues/1
gh project item-add PROJECT_NUMBER --owner YOUR_USER \
--url https://github.com/YOUR_USER/YOUR_REPO/issues/2Use GitHub's native issue dependencies to control execution order. If issue #2 depends on issue #1:
- Open issue #2 on GitHub
- In the sidebar, click "Add blocked by" and select issue #1
Symphony will only dispatch issue #2 after issue #1 is closed. Sub-issues are also respected — a parent issue with open sub-issues won't be dispatched until all children are closed.
Copy the example and customize:
cp WORKFLOW.md.example WORKFLOW.mdEdit the YAML front matter:
---
tracker:
kind: github
owner: YOUR_USER # GitHub username or org
project_number: 6 # from step 1
project_scope: user # "user" or "organization"
active_values:
- Todo
- In Progress
terminal_values:
- Done
- Closed
- Cancelled
github:
token: $GITHUB_TOKEN
agent:
kind: claude_code
max_concurrent_agents: 3 # parallel agents
max_turns: 5 # re-invocations per issue
stall_timeout_ms: 600000 # kill stalled agents after 10 min
claude:
model: sonnet # or opus, haiku
permission_profile: bypassPermissions
git:
branch_prefix: symphony/
polling:
interval_ms: 30000 # poll every 30 seconds
pull_request:
open_pr_on_success: true
draft_by_default: true
handoff_project_status: Human Review
comment_on_issue_with_pr: true
---The Markdown body below the front matter is the prompt template sent to the agent. It uses Go template syntax ({{.work_item.title}}, etc.) and is rendered per issue with full context. See WORKFLOW.md.example for a complete playbook including workpad comments and retry handling.
echo "GITHUB_TOKEN=ghp_your_fine_grained_pat" > .envRequired PAT permissions:
- Repository: Read and Write (for cloning, pushing branches, creating PRs)
- Projects: Read and Write (for fetching items, updating status fields)
- Issues: Read and Write (for posting comments, reading state)
# Validate everything is configured correctly
./symphony --doctor WORKFLOW.md
# Run Symphony (TUI dashboard appears automatically in terminal)
./symphony --port 9097 WORKFLOW.mdThe TUI shows running agents, retry queue, recent events, and summary stats. Press q to quit gracefully.
- Poll: Fetches all items from your GitHub Project in active status values
- Filter: Checks eligibility — blocked items, terminal items, already-running items are skipped
- Dispatch: Sends eligible items to worker goroutines (up to
max_concurrent_agents) - Workspace: Clones the repository, creates a git worktree with a deterministic branch (
symphony/<owner>_<repo>_<number>) - CLAUDE.md: Generates a context file in the workspace with issue details for the agent
- Agent: Invokes
claude -p --output-format jsonwith the rendered prompt. Claude reads files, edits code, runs tests, and commits changes. - Session: On continuation turns, uses
--resume <session_id>so Claude has memory of prior work - Detect: Checks if the agent created any git commits
- Write-back: If commits exist — pushes the branch, creates/updates a draft PR, comments on the issue, moves the project status to "Human Review"
- Handoff: Marks the item as handed off. Symphony stops dispatching it.
- Repeat: Polls again, picks up newly eligible items
All configuration lives in WORKFLOW.md YAML front matter:
| Key | Default | Description |
|---|---|---|
tracker.kind |
required | github |
tracker.owner |
required | GitHub user or org |
tracker.project_number |
required | Project V2 number |
tracker.project_scope |
organization |
user or organization |
tracker.active_values |
[Todo, Ready, In Progress] |
Project status values eligible for dispatch |
tracker.terminal_values |
[Done, Closed, Cancelled] |
Terminal status values (stop execution) |
github.token |
$GITHUB_TOKEN |
PAT or $VAR env reference |
agent.kind |
required | claude_code, opencode, or codex |
agent.max_concurrent_agents |
10 |
Maximum parallel workers |
agent.max_turns |
20 |
Re-invocations per work item before giving up |
agent.stall_timeout_ms |
300000 |
Kill stalled workers after this duration |
claude.model |
— | Model override (sonnet, opus, haiku) |
claude.permission_profile |
bypassPermissions |
Claude CLI permission mode |
claude.allowed_tools |
all | Restrict agent tools (e.g., [Read, Edit, Bash]) |
git.branch_prefix |
symphony/ |
Branch name prefix |
git.use_worktrees |
true |
Use git worktrees (recommended) |
polling.interval_ms |
30000 |
Poll interval in milliseconds |
pull_request.open_pr_on_success |
true |
Create PR after agent commits |
pull_request.draft_by_default |
true |
Create draft PRs |
pull_request.handoff_project_status |
— | Status value for handoff (e.g., Human Review) |
pull_request.comment_on_issue_with_pr |
true |
Post PR link as issue comment |
server.port |
— | HTTP server port (disabled if unset) |
symphony [flags] [WORKFLOW_PATH]
Arguments:
WORKFLOW_PATH Path to WORKFLOW.md (default: ./WORKFLOW.md)
Flags:
--port PORT Start HTTP server on PORT
--log-level LVL Log level: debug, info, warn, error (default: info)
--log-format FMT Log format: text, json (default: text)
--state-dir PATH Persistent state directory
--doctor Validate config and environment, then exit
--no-tui Disable TUI dashboard, use plain log output
Flags must come before the positional WORKFLOW_PATH argument (Go flag package limitation).
# Validate setup (checks config, GitHub connectivity, claude binary)
./symphony --doctor WORKFLOW.md
# Run with TUI dashboard + HTTP API
./symphony --port 9097 WORKFLOW.md
# Run without TUI (for CI/Docker/piped output)
./symphony --no-tui --log-format json --port 9097 WORKFLOW.md
# Debug mode (verbose logging)
./symphony --port 9097 --log-level debug WORKFLOW.mdWhen running in a terminal, Symphony displays a live Bubble Tea dashboard:
🎵 Symphony Uptime: 00:14:32
Agents: 2/5 running │ Dispatched: 7 │ Handed Off: 3
──────────────────────────────────────────────────────────
RUNNING AGENTS
Issue Phase Time Tokens
──────────────────────────────────────────────────────
org/repo#4 streaming_turn 3m12s 12.4k
org/repo#7 launching 0m48s 3.1k
RETRY QUEUE
org/repo#1 → due in 8s (attempt 2)
RECENT EVENTS
09:14:32 org/repo#4 PR created → pull/12
09:14:01 org/repo#7 Workspace created (worktree)
09:13:12 org/repo#1 Blocked by #4 (state: open)
──────────────────────────────────────────────────────────
[q] Quit [r] Refresh
Disable with --no-tui for plain log output.
When --port is set, Symphony exposes:
| Endpoint | Method | Description |
|---|---|---|
/healthz |
GET | Health check with uptime, auth mode, running count, last poll time |
/metrics |
GET | Prometheus-format metrics (12 symphony_* metrics) |
/api/v1/state |
GET | Full orchestrator runtime snapshot (JSON) |
/api/v1/work-items/{id} |
GET | Single work item details |
/api/v1/refresh |
POST | Trigger immediate reconciliation |
/api/v1/webhooks/github |
POST | GitHub webhook ingress (requires github.webhook_secret) |
Symphony uses a fine-grained Personal Access Token for all GitHub API operations. Set GITHUB_TOKEN in your .env file. No GitHub App registration required.
Symphony does not manage agent API keys. Each agent subprocess inherits the full parent environment and uses its own local credentials:
| Adapter | Auth Method | Setup |
|---|---|---|
| Claude Code | Local OAuth via ~/.claude |
Run claude login once |
| OpenCode | Local config | Configure opencode |
| Codex | Local config | Configure codex |
No ANTHROPIC_API_KEY is required — the claude CLI handles auth via its own OAuth flow.
Symphony preserves Claude session IDs across continuation turns. When an issue is re-invoked:
- The previous
session_idis loaded from the persistent store (bbolt) - Claude is invoked with
--resume <session_id> - Claude has full memory of prior conversation, tool results, and file changes
Additionally, a CLAUDE.md file is generated in each workspace with issue context. Claude reads this automatically on every invocation, providing persistent instructions even without session resumption.
Symphony respects two types of GitHub issue relationships:
If issue A is blocked by issue B (via GitHub's "Blocked by" feature), Symphony will not dispatch A until B is closed. All blockers must be closed for an item to be eligible.
If a parent issue has open sub-issues, Symphony skips the parent and dispatches eligible sub-issues instead. When all sub-issues are closed, the parent becomes eligible with enriched context about the completed child work.
12 metrics exposed in Prometheus text format:
| Metric | Type | Description |
|---|---|---|
symphony_active_runs |
gauge | Current running workers |
symphony_max_concurrent_agents |
gauge | Configured concurrency limit |
symphony_retry_queue_depth |
gauge | Pending retries |
symphony_tokens_total{direction} |
counter | Token usage (input/output/total) |
symphony_sessions_started_total |
counter | Total agent sessions |
symphony_github_writebacks_total |
counter | PR/comment operations |
symphony_dispatches_total |
counter | Total dispatches |
symphony_work_item_state{state} |
gauge | Items by state |
symphony_errors_total |
counter | Error count |
symphony_pr_handoffs_total |
counter | Successful handoffs |
docker compose up -dStarts 5 services:
- Symphony (port 9097) — orchestrator with HTTP API
- VictoriaMetrics (port 8428) — metrics storage (Prometheus-compatible)
- VictoriaLogs (port 9428) — searchable log storage
- Vector — log collector (reads Symphony JSON logs, pushes to VictoriaLogs)
- Grafana (port 3097, admin/admin) — dashboards for metrics + logs
Query logs in Grafana using LogsQL:
work_item_id:"github:PVTI_xxx" # All logs for a work item
log.level:error # All errors
msg:"claude CLI" # Agent activity
cmd/symphony/main.go CLI entrypoint, wiring, signal handling, TUI launch
internal/
config/ WORKFLOW.md loader, typed config, validation, file watcher
orchestrator/ Poll loop, dispatch, eligibility, retry, reconciliation
worker.go Multi-turn agent execution loop + session preservation
events.go Event bus for TUI and logging
source_bridge.go GitHub → orchestrator type bridge
github/ GraphQL queries, PR/comment write-back, auth, tools
adapter/ Claude CLI adapter (exec + JSON parse)
workspace/ Git clone/worktree, branches, hooks, push
prompt/ Go template rendering with missingkey=error
state/ bbolt persistent state (retries, sessions, totals)
server/ HTTP API, Prometheus metrics, health check
webhook/ GitHub webhook signature verification
tui/ Bubble Tea terminal dashboard
tracker/ Abstract interfaces for multi-backend support
logging/ Structured log setup
ssh/ SSH worker extension (stub)
# Unit tests (no external dependencies)
go test ./... -count=1
# Integration tests (requires GITHUB_TOKEN — loads from .env two levels up)
cd test/integration && go test -tags=integration -v -count=1
# Lint
golangci-lint run# Build image (Go binary + git, no Node.js needed)
docker build -t symphony .
# Run full stack with observability
docker compose up -d
# View logs
docker compose logs -f symphony
# Stop
docker compose downSymphony includes multiple safety mechanisms:
- Max continuation retries (default 10): Prevents infinite re-dispatch loops
- Continuation backoff (5s → 10s → 20s → 30s): Avoids rapid-fire re-invocations
- Stall detection: Kills workers silent for longer than
stall_timeout_ms - Incomplete data rejection: Items with failed dependency fetches are skipped
- Token sanitization: Git auth tokens are masked in all log output
- Clone mutex: Prevents concurrent bare repo clones to the same cache path
- Context cancellation: Workers stop promptly on SIGTERM; Claude processes are killed
- Handoff on PR creation: PR creation unconditionally triggers handoff (prevents re-dispatch loops)
- Eligibility checks: 10+ rules checked before dispatch (active status, open state, not blocked, not claimed, slots available, per-repo limits, per-status limits, sub-issue check, Pass2 data completeness)
See LICENSE.