An autonomous backlog dispatcher for agent-driven development: a long-running Claude Code session works your Linear (or GitHub Projects) board top to bottom, handing each task to a fresh developer agent, each finished pull request to an adversarial reviewer agent, and each conflicting PR to a cleaner agent, while you keep the two jobs that matter - ordering the backlog and merging.
Everything ships as one self-contained dispatcher binary per platform plus a
Claude Code plugin; a committed dispatcher.config.json at your repository
root is the only project-specific piece. Full documentation with flow charts
lives at https://backside4charter.github.io/dispatcher/.
flowchart LR
owner((You))
board[(Linear board)]
github[(GitHub<br/>branches and PRs)]
subgraph session[Claude Code session]
disp["/dispatcher:start loop"]
dev[developer agent]
rev[reviewer agent]
cln[cleaner agent]
end
bin[dispatcher binary]
owner -->|orders the backlog| board
owner -->|reviews and merges| github
disp -->|dispatcher board ...| bin
bin <-->|GraphQL| board
disp -->|spawns| dev & rev & cln
dev -->|commit and open PR<br/>as the developer app| github
rev -->|line comments<br/>as the reviewer app| github
cln -->|merge main, resolve conflicts| github
github -.->|merge completes the issue| board
The quickest route is to hand the setup to your AI: open Claude Code in the repository your pull requests land in and tell it
Set up the dispatcher in this repository by following
https://backside4charter.github.io/dispatcher/ai/setup
It installs the binary, discovers your Linear team, project and states, writes the config, enables the plugin, and asks you for the few things only you can provide. By hand, the steps are:
-
Install the binary (one line per platform; pin a release with
DISPATCHER_VERSION=x.y.z):# Windows powershell -ExecutionPolicy Bypass -c "irm https://raw.githubusercontent.com/backside4charter/dispatcher/main/install.ps1 | iex"
# macOS / Linux curl -fsSL https://raw.githubusercontent.com/backside4charter/dispatcher/main/install.sh | sh
-
Put your Linear API key on the machine. Either export
LINEAR_API_KEY, or save{"Linear": "lin_api_..."}as.secrets/api-keys.jsonin your repository (the directory is gitignored). The key comes from Linear > Settings > Account > Security & access. -
Run the setup wizard inside your repository:
cd your-repo dispatcher initWith the API key present, teams, projects and workflow states are pickers. The wizard writes
dispatcher.config.json, enables the Claude Code plugin in.claude/settings.json, and prints a checklist of anything still missing (theghCLI, the GitHub App keys, the webhook extension). -
Fill in the two
TODOagent ids in thelinear.agentssection of the config (see What to set up in Linear), and, if you want bot-authored commits and reviews, thegithubAppssection (see the GitHub Apps guide). Confirm with:dispatcher board config dispatcher board poll <milestone>
-
Optionally start the event channel in its own terminal so board and PR changes wake the loop immediately instead of at its polling interval:
dispatcher listen
-
Start the loop in a Claude Code session opened at your repository:
/dispatcher:start v1.1.0Name one or more milestones. The dispatcher polls, claims, spawns workers, verifies their pull requests, and reports in its status text. Stop it with
/dispatcher:stop(drain) or/dispatcher:stop now(abort). Everything durable lives on the board and on GitHub, so stopping loses nothing.
Prefer manual installation? Download a binary from
Releases
(windows-x64, linux-x64, linux-arm64, darwin-x64, darwin-arm64) and
run dispatcher help.
Tasks are issues in one Linear project. The dispatcher works a set of milestones in the board's manual order, which is your priority signal. Every issue moves through seven workflow states; your team can call them anything, the config maps each name onto its role.
stateDiagram-v2
direction LR
state "In Progress / developer" as dev
state "In Progress / reviewer" as rev
state "Human Review" as HumanReview
state "Changes Requested" as ChangesRequested
[*] --> Backlog
Backlog --> Ready: you decide it is ready
Ready --> dev: dispatch
dev --> rev: PR open, every box ticked
rev --> HumanReview: reviewer passes
rev --> ChangesRequested: reviewer requests changes
HumanReview --> ChangesRequested: you request changes (review-sync)
ChangesRequested --> dev: dispatch, resume the same PR
HumanReview --> Done: you merge
dev --> Question: worker needs your decision
rev --> Question: reviewer needs your decision
Question --> Ready: you answer (no PR yet)
Question --> ChangesRequested: you answer (PR exists)
Done --> [*]
classDef backlog stroke:#bec2c8,stroke-width:2px
classDef ready stroke:#0079d4,stroke-width:2px
classDef progress stroke:#f2c94c,stroke-width:2px
classDef changes stroke:#eb5757,stroke-width:2px
classDef question stroke:#9b51e0,stroke-width:2px
classDef review stroke:#ff7cb9,stroke-width:2px
classDef done stroke:#00a81c,stroke-width:2px
class Backlog backlog
class Ready ready
class dev progress
class rev progress
class ChangesRequested changes
class Question question
class HumanReview review
class Done done
Readymeans never worked on;Changes Requestedmeans worked on and sent back, so it always has an open PR to resume. Changes Requested outranks Ready.BacklogandQuestionare yours: the dispatcher never dispatches them. A worker that hits a decision only you can make parks its task in Question with the question as an issue comment.Human Reviewmeans a PR is waiting on you.Doneis written by your merge, through Linear's GitHub automation, never by an agent.- Which agent phase a row is in while
In Progressis carried by Linear's delegate, not by the state. A claim (one comment on the issue naming the role, the Claude session and a timestamp) says which session is working it right now and doubles as a heartbeat; claims older than 90 minutes are treated as a dead session's and taken over.
The dispatcher session is deliberately lightweight: it polls the board, claims a row, spawns a worker in a fresh context, verifies the result against GitHub, updates the board, and paces itself. Workers never touch board state.
| Worker | Takes | Produces | In flight |
|---|---|---|---|
developer |
a Ready or Changes Requested issue |
one branch, one PR into main, in its own git worktree |
up to 2 |
reviewer |
a row delegated to the reviewer with an open PR | line-anchored findings on the PR and a PASS / CHANGES_REQUESTED / QUESTION verdict; never edits code |
up to 2 |
cleaner |
an open PR that conflicts with main |
a merge of main into the branch with conflicts resolved and nothing lost |
1, in its own slot |
Each loop iteration processes finished workers, re-stamps live claims, tops
up both queues, brings bot-authored PRs up to date with main, dispatches the
cleaner at conflicts, scans the whole project for rows stranded by dead
sessions, and prunes finished worktrees. Wakes come from worker completions,
from the event channel, and from a fallback timer.
GitHub will not let an account approve its own pull request, so agent work is
committed and opened as a developer GitHub App and reviewed as a separate
reviewer GitHub App. That keeps every PR reviewable by you and makes the
AI review an independent signal. dispatcher commit, dispatcher pr and
dispatcher token --app reviewer mint short-lived installation tokens from
the apps' private keys; the reviewer only ever posts COMMENT reviews, so an
agent can never satisfy a required-approval rule.
- Event channel.
dispatcher listenruns a loopback listener fed by the officialgh webhook forwardextension (no public ingress) and by a Linear poller every 30 seconds. The loop armsdispatcher waitin the background and treats its exit as a wake. If the channel is down the loop still works, at polling latency. - Review sync. A small GitHub Actions workflow runs
dispatcher review-syncon everypull_request_review; when you request changes on a PR, its issue goes back toChanges Requestedwithin seconds, whether or not a dispatcher session is awake.
| Command | Purpose |
|---|---|
board <subcommand> |
read and write the board: config, states, milestones, poll, issue, claims, pr-issues, state, claim, assign, release, comment, label, link-pr |
listen / status / wait / consume |
the local event channel |
token / identity / pr / commit |
agent GitHub App identity tooling |
review-sync |
CI hook: owner change request to Changes Requested |
prune-worktrees |
remove finished agent worktrees safely |
init |
interactive per-repository setup |
-
A team and a project. The project is the board; its manual issue order is the priority order. Create project milestones (
v1.1.0, ...); the dispatcher is scoped to milestones you name when you start it. -
Seven workflow states on the team, one per role. The defaults the wizard offers:
Role Default name Meaning backlogBacklog not to be worked on yet readyReady agent-workable, never worked changesRequestedChanges Requested sent back, has an open PR inProgressIn Progress a developer or reviewer holds it questionQuestion parked on a decision only you can make humanReviewHuman Review a PR is waiting on you doneDone merged (a completedtype state) -
Two labels: one meaning "check with me before starting" (default
Confirm with user) and one marking design-sensitive work (defaultUI). -
Two agent app users to delegate work to, the developer and the reviewer. Each is a Linear OAuth application with agent capabilities, installed into the workspace with
actor=app, after which it appears as a user that issues can be delegated to. Put their user ids inlinear.agents; find them with your API key:query { users(filter: { app: { eq: true } }) { nodes { id name displayName } } }
-
The GitHub integration, enabled for the workspace and, per team, the automation PR merged -> Done. Task branches are named
task/<identifier>-<slug>and PR bodies carryFixes <identifier>, which is what links a PR to its issue and lets the merge complete it. -
A personal API key for the account the dispatcher acts as (step 2 of the install). Every board write and comment goes out under this account; agent comments are tagged
**[developer]**,**[reviewer]**,**[cleaner]**or**[dispatcher]**on their first line.
The Linear setup guide walks through each of these step by step; the alternative GitHub Projects backend is described in the System breakdown.
dispatcher.config.json at the repository root is committed and is also the
marker the binary locates the repository by. It names the platform, the
project, what the team calls each state and label, the repository PRs land in,
the two agent identities on each side, and optionally the listener port. See
the configuration section of the System breakdown
for every field, or src/testing/board-fixtures.ts
for a complete fictional example.
Credentials are looked up, never configured: the Linear API key from
LINEAR_API_KEY or .secrets/api-keys.json, GitHub board access through
your gh auth, and each GitHub App's private key from
.secrets/<slug>.private-key.pem (or the environment variable named in its
config entry).
Requires Bun at the version pinned in .bun-version
(compiled binaries embed the compiling runtime, so the pin is exact) and
optionally just.
bun install
just check # typecheck + tests
just run board config
just compile # host-platform binary into dist/
just compile all # every target (cross-compiles from any host)
just docs # serve the documentation site locallyThe documentation site is a Docusaurus project under website/, deployed to
GitHub Pages by the docs workflow on every push to main.
Releases are cut by tagging: bump package.json, tag v<version>, push the
tag, and the release workflow compiles every target and publishes the
binaries.