Baton conducts coding agents against a GitHub Projects board — the thing you wave to make a symphony happen.
Baton is a long-running automation service that continuously reads work items from a
GitHub Projects (v2) board, creates an isolated workspace for each issue, and runs a coding
agent session — Claude Code CLI (via claude -p subprocess mode) or GitHub Copilot CLI —
for that issue inside the workspace. Engineers manage the work on the board; Baton manages the
agents.
Baton is a port of the Symphony service specification with two adapter layers swapped:
| Layer | Symphony | Baton |
|---|---|---|
| Issue tracker | Linear (GraphQL) | GitHub Projects v2 (GraphQL) |
| Coding agent | Codex app-server | Claude Code CLI (claude -p) / Copilot CLI |
Everything else — the polling orchestrator, claim/retry/reconciliation state machine, per-issue
workspaces, the repository-owned WORKFLOW.md contract, and the observability requirements —
follows the Symphony spec unchanged.
Warning
Baton runs coding agents with auto-approved permissions in trusted environments. Read the Security section before pointing it at a real board.
docs/SPEC.md— the Baton service specification (language-agnostic, normative, same chapter structure as Symphony'sSPEC.md)CLAUDE.md— architecture overview and implementation conventions for contributors
Prerequisites
- Node.js ≥ 22
ghCLI — used by the agent inside each workspace- The coding agent binary matching your
agent.kind:claude_code→ Claude Code CLIcopilot→ GitHub Copilot CLI
- On Windows: Git Bash — Baton runs every agent turn and
workspace hook via
bash -lc. The agent CLI must be reachable from Git Bash's loginPATH; if it is not, setagent.<kind>.commandto a standalone absolute path. Native Windows paths likeC:\Tools\claude.exeare normalized automatically; quote them if the path contains spaces. Hooks receiveBATON_WORKSPACEin Git Bash path form and, on Windows, alsoBATON_WORKSPACE_NATIVEfor native-tool interop.
Build from source
git clone https://github.com/keinstn/baton.git
cd baton/copilot
npm install
npm run buildRun Baton with npm exec after building:
npm exec baton -- WORKFLOW.mdBefore running Baton, create and configure a GitHub Projects v2 board.
1. Create a project board
Navigate to your organization or user profile → Projects → New project. Choose the Board template so issues are arranged in columns.
2. Configure Status columns
A new board starts with Todo, In Progress, and Done. If your prompt instructs the agent
to move issues to a custom state when done (e.g. In Review), add that column by clicking
+ at the right edge of the board and selecting New option.
The column names must exactly match the values you reference in WORKFLOW.md. Configure which
states Baton should pick up work from:
tracker:
active_states: [Todo, In Progress] # Baton picks up issues in these states
# terminal_states: [Done] # (optional) default: [Done]The state the agent moves issues to when done is not a config key — specify it in your
WORKFLOW.md prompt body:
When done, move the issue's Status to "In Review" on the project board.
2b. (Copilot reviewer workflow only) Add custom fields
If you use the two-workflow handoff pattern (examples/WORKFLOW.md implementer +
examples/WORKFLOW.copilot.md reviewer), add these Project custom fields:
Handoff Count(number)Last Reviewed SHA(text)
examples/WORKFLOW.copilot.md uses these fields to track reviewer-only handoffs and avoid duplicate
increments when the PR head SHA has not changed. The reviewer workflow resolves field node IDs by
name at runtime, so do not hardcode field IDs in WORKFLOW files. If either field is missing or
unreadable, the reviewer workflow escalates to human attention by moving the item to In Review.
3. Create a label
Go to the target repository → Issues → Labels → New label. Create a label named
ai-ready (or whatever you list under required_labels in WORKFLOW.md). Baton only
dispatches issues that carry this label.
Tip: The Triage companion script can automate this step — it evaluates issues with an LLM and applies the label automatically.
4. Generate a Personal Access Token
Go to Settings → Developer settings → Personal access tokens → Fine-grained tokens and create a token with the scopes your setup needs:
- Projects — Read for the orchestrator itself; add write only if the same auth context will
also perform project updates from workspace hooks or agent
ghcommands - Repository access for the repos your issues live in if workspace hooks or agent
ghcommands will use that same token
Export it before running Baton:
export GITHUB_TOKEN=ghp_...The project_number for WORKFLOW.md is the trailing integer in the project URL
(e.g. https://github.com/orgs/my-org/projects/5 → project_number: 5).
1. Create a WORKFLOW.md
The WORKFLOW.md file is the single configuration + prompt contract for your project. Copy one of the examples as a starting point:
examples/WORKFLOW.md— Claude Code implementation workflowexamples/WORKFLOW.copilot.md— GitHub Copilot PR review workflow
Edit the YAML front matter to point at your GitHub Project:
tracker:
kind: github_projects
owner: my-org # GitHub org or user
project_number: 5 # Project board number
token: $GITHUB_TOKEN # Fine-grained PAT or GitHub App token
active_states: [Todo, In Progress]
required_labels: [ai-ready]
agent:
kind: claude_code # or: copilotIf you run the two example workflows together, start two Baton processes against the same
project board and add an Agent Review status. The Claude implementation workflow hands an item
from In Progress to Agent Review; the Copilot review workflow either returns it to
In Progress for fixes or advances it to In Review for human review. Give each process its own
workspace.root and its own server.port so they never share a working tree and are individually
reachable via /api/v1/state; then point baton-dashboard at both URLs to view them together on
a single page. The review workflow re-syncs to the pushed PR head, so it does not need the
implementer's local state.
2. Set environment variables
export GITHUB_TOKEN=ghp_... # PAT/App token used by the tracker and, unless you separate auth, inherited by hooks/agent subprocesses
export LOG_LEVEL=info # Log verbosity: debug | info | warn | error (default: info)3. Run Baton
npm exec baton -- WORKFLOW.mdWORKFLOW.md defaults to ./WORKFLOW.md when omitted.
| Flag | Description |
|---|---|
--port N / -p N |
Enable the HTTP dashboard on port N (overrides server.port in front matter) |
Baton polls the board on every polling.interval_ms tick, dispatches eligible issues to agent workers, and logs structured JSON to stderr. Send SIGINT or SIGTERM to shut down gracefully.
Set LOG_LEVEL=debug to enable verbose diagnostic output (subprocess PIDs, agent events, GitHub API timing, tick cycle details).
4. Monitor with baton-dashboard (optional)
baton-dashboard is a companion CLI that serves a single-page view aggregating live state from
one or more Baton instances. The browser fetches each instance's /api/v1/state directly via
Promise.allSettled (no server-side polling) and the page auto-refreshes every 30 seconds.
Create a config file (default path: ./baton-dashboard.yaml):
server:
host: "127.0.0.1"
port: 8800
targets:
- name: "implementer"
url: "http://127.0.0.1:8787"
- name: "reviewer"
url: "http://127.0.0.1:8788"See examples/baton-dashboard.yaml for the full reference
example. server.host defaults to 127.0.0.1 and server.port defaults to 8888 when omitted.
npm exec baton-dashboard # uses ./baton-dashboard.yaml
npm exec baton-dashboard -- config.yaml # explicit config path
npm exec baton-dashboard -- --port 9000 # override server.port| Flag | Description |
|---|---|
--port N / -p N |
Override server.port from the config file |
Each Baton instance must have server.port configured (in WORKFLOW.md front matter or via
--port) for the dashboard to reach its /api/v1/state endpoint.
The scripts/triage/ companion CLI automates the ai-ready label gate described in
Board Setup §3. Instead of manually reviewing issues and applying the
label, the triage script fetches issues from the configured GitHub Projects v2 board, evaluates
them with an LLM (Claude or Copilot), and either applies the ai-ready label to ready issues
or posts a clarification comment on ambiguous ones.
Minimal TRIAGE.md config
tracker:
token: $GITHUB_TOKEN
owner: my-org
owner_type: organization
project_number: 5
todo_state: Todo
ai_ready_label: ai-ready
evaluator:
kind: claude_code # or: copilotSee examples/TRIAGE.md for the full reference config with inline
comments (model override, timeout, repo filter, and the LiquidJS prompt template).
Run once
npm run triage [TRIAGE.md]
# or: npx tsx scripts/triage/index.ts [TRIAGE.md]Run on a schedule (cron)
*/30 * * * * cd /path/to/repo && npx tsx scripts/triage/index.ts TRIAGE.mdRunning every 30 minutes keeps the ai-ready queue populated without human intervention — pair
it with a Baton instance polling the same board.
The scripts/review-sync/ companion CLI reconciles Project Status for issues in review-related
columns by checking linked open PR reviewThreads resolution state.
For each issue in tracker.source_states:
- If any linked open PR has unresolved
reviewThreads→ move issue totracker.in_progress_state - If all linked open PRs have all threads resolved → move issue to
tracker.in_review_state - If there are no linked open PRs, or linked open PRs have no review threads → skip (no move)
review-sync discovers linked PRs from issue cross-references and uses only
reviewThreads.isResolved as the move signal.
Minimal REVIEW_SYNC.md config
tracker:
token: $GITHUB_TOKEN
owner: my-org
owner_type: organization # organization | user (default: organization)
project_number: 5
source_states:
- In Review
- Agent Review
in_progress_state: In Progress
in_review_state: In ReviewRun once
npm run review-sync [REVIEW_SYNC.md]
# or: npx tsx scripts/review-sync/index.ts [REVIEW_SYNC.md]Dry run (no mutation)
npm run review-sync -- --dry-run
npm run review-sync -- [REVIEW_SYNC.md] --dry-runEvery polling.interval_ms, Baton queries the configured Project board for issues whose Status is
in active_states (e.g. Todo, In Progress) and carries the required_labels. Eligible issues
are claimed and dispatched to a worker, which prepares a per-issue workspace (clone via hooks),
renders the issue into the WORKFLOW.md prompt template, and drives a coding-agent session
(claude -p or copilot -p) in that workspace. The agent does the work and performs all tracker
writes itself with the gh CLI — commenting progress, opening a PR, and moving the Status out of
active_states (e.g. to In Review as instructed in the prompt).
Baton stops sessions whose issues leave the active states and cleans up workspaces for terminal
issues.
flowchart TD
Board["📋 GitHub Projects Board\n(Status: Todo / In Progress)"]
subgraph Baton["Baton (long-running service)"]
Orchestrator["Orchestrator\npoll · claim · retry · reconcile"]
TrackerAdapter["Tracker Adapter\nGitHub Projects v2 GraphQL"]
Worker["Worker\n(per issue)"]
WorkspaceManager["Workspace Manager\nclone · after_create · before_run"]
AgentRunner["Agent Runner"]
ClaudeCode["Claude Code CLI\n(-p subprocess)"]
CopilotCLI["Copilot CLI"]
end
Board -->|"poll every interval_ms"| TrackerAdapter
TrackerAdapter -->|"eligible issues"| Orchestrator
Orchestrator -->|"dispatch"| Worker
Worker --> WorkspaceManager
WorkspaceManager -->|"workspace ready"| Worker
Worker --> AgentRunner
AgentRunner --> ClaudeCode
AgentRunner --> CopilotCLI
ClaudeCode -->|"gh CLI: comment · PR · status update"| Board
CopilotCLI -->|"gh CLI: comment · PR · status update"| Board
Baton runs a coding agent with auto-approved permissions, so treat its configuration as a
security boundary. The full model is in docs/SPEC.md §15; the operational
essentials:
- Issue content is untrusted input. On public repositories, issue titles, bodies, and
comments are externally controlled — treat them as potential prompt injection. Gate dispatch
with
required_labels(a label only maintainers can apply) so only triaged issues reach the agent. - Keep the tool allowlist minimal. Grant only what the workflow needs (
Bash(gh:*),Bash(git:*), the project's build/test commands,Edit,Write).bypassPermissions(Claude Code) andallow_all_tools(Copilot) MUST be an explicit, deliberate operator choice. - Scope the token narrowly. Use a fine-grained PAT (or GitHub App) limited to the target
repositories and the minimum scopes. The orchestrator only needs Projects read; the agent's
ghwrites can use a separately scoped token. - Require human review of agent output. Enable branch protection and required PR review on target repositories so agent-authored changes cannot merge unreviewed. Consider running the workspace root under a dedicated OS user or container sandbox.
Baton uses Changesets to manage version bumps from
develop to main.
On develop
- Add your code or docs change as usual.
- If the change should affect the next release, run
npm run changesetand commit the generated.changeset/*.mdfile with amajor,minor, orpatchbump forbaton. - Merge feature/fix PRs into
develop.
When releasing to main
- Merge
developintomain. .github/workflows/release.ymlruns on themainpush and useschangesets/actionto open or update a version PR.- Configure a repo secret named
CHANGESETS_GITHUB_TOKENwith a PAT that can create PRs and trigger normalpull_requestCI; the default ActionsGITHUB_TOKENis not sufficient for this cross-workflow trigger path. The release workflow uses that same token for both checkout andchangesets/actionso the generated branch and PR are created by the same CI-capable identity.
- Configure a repo secret named
- That version PR runs
npm run version-packages, which applies the accumulated changesets and updatespackage.json,package-lock.json, andCHANGELOG.md. - Merge the version PR into
main. - The same release workflow sees the version bump commit, creates
vX.Y.Z, and publishes a GitHub Release with generated notes. - Merge
mainback intodevelopso the consumed.changeset/*.mdfiles and the released package version stay in sync on the development branch before the next cycle starts.
Bump guidelines
major— breaking CLI/config/workflow contract changes or an intentional compatibility reset like thev1.0.0release.minor— new user-facing capability, such as a new CLI surface or materially expanded behavior.patch— bug fixes, documentation clarifications, and internal maintenance that should ship in the next release.
Apache License 2.0 (same as Symphony).