An autonomous coding agent orchestrator that polls GitHub Issues, creates isolated git worktrees, and runs Claude Code CLI against each issue — hands-free.
Inspired by OpenAI's Symphony spec, rebuilt from scratch for Claude Code.
How It Works
WORKFLOW.md → Orchestrator → Worker (per issue)
│ │
│ ├─ git worktree create
│ ├─ hooks (before_run)
│ ├─ claude -p "<prompt>"
│ ├─ check issue state
│ └─ hooks (after_run)
│
├─ Poller (gh issue list)
├─ Dispatcher (concurrency control)
└─ Reconciler (stale run detection)
Baton runs project-local — one instance per repo, started from the project directory. It polls GitHub Issues matching your configured filters, spins up isolated git worktrees, and lets Claude Code work on each issue autonomously with multi-turn retries.
Quick Start
# Install pip install -e . # Copy the example workflow cp WORKFLOW.md.example WORKFLOW.md # Edit WORKFLOW.md to configure labels, concurrency, prompt template, etc. # Start the orchestrator baton start
WORKFLOW.md
A single file controls everything — YAML front matter for config, Markdown body for the prompt template:
--- tracker: kind: github labels: ["agent"] exclude_labels: ["blocked"] polling: interval_ms: 30000 agent: max_concurrent: 3 max_turns: 5 command: claude permission_mode: acceptEdits mcp_servers: - name: playwright command: npx @playwright/mcp@latest hooks: before_run: | git fetch origin main && git rebase origin/main timeout_ms: 60000 --- You are an autonomous software engineer working on issue #{{ issue.number }}: {{ issue.title }}. {{ issue.body }} When done, commit, push, and create a PR linking to #{{ issue.number }}.
Config Reference
| Key | Default | Description |
|---|---|---|
tracker.kind |
github |
Issue tracker (currently only GitHub) |
tracker.labels |
[] |
Filter issues by these labels |
tracker.exclude_labels |
[] |
Skip issues with these labels |
tracker.assignee |
null |
Filter by assignee (@me for yourself) |
polling.interval_ms |
30000 |
Poll interval in milliseconds |
agent.max_concurrent |
3 |
Max parallel agents |
agent.max_turns |
5 |
Max Claude turns per issue |
agent.command |
claude |
Agent CLI command |
agent.permission_mode |
acceptEdits |
Claude Code permission mode |
agent.mcp_servers |
[] |
MCP servers to pass to Claude |
hooks.after_create |
null |
Run after worktree creation (e.g., npm install) |
hooks.before_run |
null |
Run before each agent turn |
hooks.after_run |
null |
Run after each agent turn |
hooks.timeout_ms |
60000 |
Hook timeout |
Prompt Template Variables
| Variable | Description |
|---|---|
{{ issue.number }} |
Issue number |
{{ issue.title }} |
Issue title |
{{ issue.body }} |
Issue body |
{{ issue.labels }} |
List of label names |
{{ issue.url }} |
Issue URL |
{{ attempt }} |
Retry attempt number (null on first run) |
Per-Issue Skills
Add a ## Skills section to any issue body to activate additional skills for that issue:
## Skills - playwright - accessibility-checker
CLI
# Start the orchestrator baton start # uses ./WORKFLOW.md baton start -w my-workflow.md # custom workflow file baton start -v # verbose logging # Check status baton status # Stop # Ctrl+C in the terminal, or kill the process baton stop
Architecture
| Module | Purpose |
|---|---|
symphony/config.py |
WORKFLOW.md YAML parser with typed defaults |
symphony/tracker.py |
GitHub Issues client via gh CLI |
symphony/workspace.py |
Git worktree lifecycle manager |
symphony/hooks.py |
Shell hook executor with timeout |
symphony/prompt.py |
Jinja2 template renderer |
symphony/worker.py |
Claude Code CLI subprocess runner |
symphony/state.py |
In-memory state with JSON persistence |
symphony/orchestrator.py |
Poll-dispatch-reconcile event loop |
symphony/cli.py |
Click CLI entry point |
symphony/log.py |
Color-coded terminal logging |
State Machine
Unclaimed → Claimed → Running → RetryQueued → Released
↑ │
└────────────┘
- PR created: Release claim immediately, move to next issue
- Normal exit (no PR): 1s retry delay (continuation check)
- Error exit: Exponential backoff (10s, 20s, 40s... up to configured max)
- Issue closed: Release claim + clean up worktree
Real-World Example: Building a Todo App
We tested Baton by having it autonomously build a complete todo app from scratch. We created 3 GitHub Issues, ran baton start, and walked away. Here's what happened:
Setup:
mkdir todo-app && cd todo-app && git init gh repo create todo-app --public --source=. --push gh label create baton --color "6366f1"
Created 3 issues labeled baton:
- Create basic todo app HTML structure
- Add JavaScript to create and delete todos
- Add localStorage persistence
WORKFLOW.md with agent-browser verification:
--- tracker: kind: github labels: [baton] agent: max_concurrent: 1 max_turns: 3 command: claude permission_mode: bypassPermissions --- You are an autonomous software engineer working on issue #{{ issue.number }}: {{ issue.title }}. {{ issue.body }} ## Verification (REQUIRED before creating PR) Use agent-browser to verify your work: agent-browser open http://localhost:3456 agent-browser snapshot -i agent-browser click, fill, type to test interactions If verification fails, fix the issues before proceeding.
Result: Baton picked up each issue sequentially, created isolated worktrees, wrote the code, launched a local server, used agent-browser to visually verify the acceptance criteria, then committed, pushed, and opened PRs — all without human intervention.
Each PR included verification results in the description:
Verification with agent-browser
- Opened
http://localhost:3456and confirmed the page renders correctly- Took screenshot verifying centered card layout with styled input and button
- Ran
agent-browser snapshot -iconfirming interactive elements: textbox and button
3 issues created. 3 PRs opened and merged. Zero manual coding.
Requirements
- Python 3.11+
- Claude Code CLI (
claude) - GitHub CLI (
gh) — authenticated - Git
Development
pip install -e ".[dev]"
pytest tests/ -vBlog Post
Read the full write-up on how and why Baton was built: I Built an Orchestrator That Watches GitHub Issues and Sends Agents to Fix Them
License
MIT