Skip to content

keinstn/baton

Repository files navigation

Baton

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.

Documents

  • docs/SPEC.md — the Baton service specification (language-agnostic, normative, same chapter structure as Symphony's SPEC.md)
  • CLAUDE.md — architecture overview and implementation conventions for contributors

Installation

Prerequisites

  • Node.js ≥ 22
  • gh CLI — used by the agent inside each workspace
  • The coding agent binary matching your agent.kind:
  • 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 login PATH; if it is not, set agent.<kind>.command to a standalone absolute path. Native Windows paths like C:\Tools\claude.exe are normalized automatically; quote them if the path contains spaces. Hooks receive BATON_WORKSPACE in Git Bash path form and, on Windows, also BATON_WORKSPACE_NATIVE for native-tool interop.

Build from source

git clone https://github.com/keinstn/baton.git
cd baton/copilot
npm install
npm run build

Run Baton with npm exec after building:

npm exec baton -- WORKFLOW.md

Board Setup

Before running Baton, create and configure a GitHub Projects v2 board.

1. Create a project board

Navigate to your organization or user profile → ProjectsNew 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 → IssuesLabelsNew 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 SettingsDeveloper settingsPersonal access tokensFine-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 gh commands
  • Repository access for the repos your issues live in if workspace hooks or agent gh commands 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/5project_number: 5).

Usage

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:

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: copilot

If 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.md

WORKFLOW.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.

Triage (automated labeling)

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: copilot

See 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.md

Running every 30 minutes keeps the ai-ready queue populated without human intervention — pair it with a Baton instance polling the same board.

Review Sync (one-shot board state sync)

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 to tracker.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 Review

Run 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-run

How it works (one paragraph)

Every 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.

Architecture

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
Loading

Security

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) and allow_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 gh writes 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.

Release flow

Baton uses Changesets to manage version bumps from develop to main.

On develop

  1. Add your code or docs change as usual.
  2. If the change should affect the next release, run npm run changeset and commit the generated .changeset/*.md file with a major, minor, or patch bump for baton.
  3. Merge feature/fix PRs into develop.

When releasing to main

  1. Merge develop into main.
  2. .github/workflows/release.yml runs on the main push and uses changesets/action to open or update a version PR.
    • Configure a repo secret named CHANGESETS_GITHUB_TOKEN with a PAT that can create PRs and trigger normal pull_request CI; the default Actions GITHUB_TOKEN is not sufficient for this cross-workflow trigger path. The release workflow uses that same token for both checkout and changesets/action so the generated branch and PR are created by the same CI-capable identity.
  3. That version PR runs npm run version-packages, which applies the accumulated changesets and updates package.json, package-lock.json, and CHANGELOG.md.
  4. Merge the version PR into main.
  5. The same release workflow sees the version bump commit, creates vX.Y.Z, and publishes a GitHub Release with generated notes.
  6. Merge main back into develop so the consumed .changeset/*.md files 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 the v1.0.0 release.
  • 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.

License

Apache License 2.0 (same as Symphony).

About

Conducts Claude Code and Copilot agents against a GitHub Projects board. Engineers manage the board; Baton manages the agents. Port of OpenAI's Symphony.

Topics

Resources

Stars

Watchers

Forks

Releases

Packages

Contributors

Languages