llm-handoff is a file-based dispatch loop for multi-CLI AI coding workflows.
It runs one agent CLI at a time against a single repository, passes work
between them through one Markdown file, and only advances when that file
parses, routes, and validates.
Prompts are advisory. Validators are authoritative.
Built for solo developers coordinating Codex, Claude Code, or Antigravity
(agy) on one repo without git worktrees, pull-request choreography, or a
service orchestrator.

Contents
- Status
- Quick Start
- Files Or Linear: Where Work Is Tracked
- Gotchas
- Gates And Scanners
- How It Works
- Agent Safety Defaults
- What This Is And Is Not
- Why Not LangGraph, AutoGen, Or CrewAI?
- Evidence So Far
- Known Limitations
- Further Reading
- License
Status
Version 0.1.0, the first tagged release. It has been validated in three repositories so far. Names and configuration can still change between 0.x releases, and every change is listed in CHANGELOG.md. It is not a production-certified tool for arbitrary repositories.
Quick Start
You need Python 3.11 or newer, Git, and at least one provider CLI signed in:
Claude Code (claude), Codex (codex), or Antigravity (agy). The
dispatcher launches these CLIs; it does not install them or manage their
logins.
Gemini CLI (gemini) is no longer supported. Gemini CLI stopped serving
personal Google accounts on 2026-06-18, so its provider was removed; use
Antigravity (agy) for Google models. A config that still names
provider: gemini fails at load with a message saying so.
Two directories are involved: the llm-handoff install, and the target
repository where agents do the work. Commands below run from anywhere once the
package is installed.
1. Install
pipx install llm-handoff
llm-handoff --help
python -m pip install llm-handoff inside a virtual environment works too.
To install the latest main instead of the release, use
python -m pip install git+https://github.com/choughton/llm-handoff.git. To
run from a source checkout, see INSTALL.md;
python -m llm_handoff ... then replaces llm-handoff ... in every command.
2. Initialize the target repository
Preview what will be copied, then copy it:
llm-handoff init path/to/your-project --dry-run
llm-handoff init path/to/your-project
This writes the reference workflow into the target repo: AGENTS.md,
PROJECT_STATE.md, dispatch_config.yaml, docs/handoff/HANDOFF.md, the
role prompts under docs/handoff/, and provider agent files under .claude/,
.codex/, and .agents/. Existing identical files are skipped;
changed files stop the run unless you pass --force.
init picks a role-to-provider mapping for you. With all three of Claude
Code, Codex, and Antigravity (agy) on PATH, it writes the reference
mapping (Antigravity agy planner and frontend, Codex backend, Claude
auditor, validator, and finalizer). With two, it moves only the roles the
missing CLI held, and keeps the quality auditor on a different vendor from
both implementers:
CLIs on PATH |
Moved roles |
|---|---|
| Claude Code and Codex | planner to Claude, frontend to Codex |
Claude Code and agy |
backend to agy |
Codex and agy |
frontend to Codex; auditor, validator, finalizer to agy |
With only one, every role goes on that CLI. Linear mode does not support
antigravity roles, so with --tracker linear the reference mapping puts
planner and frontend on Claude, and the two mappings with agy become
all-Claude or all-Codex. Pass --provider claude, --provider codex, or
--provider antigravity to force a single-vendor setup. The command prints
the mapping it chose. See
Two-CLI Mappings.
Whenever backend or frontend runs on Codex, which includes the reference
mapping, the written config uses commit_mode: dispatcher. Codex's sandbox
keeps .git read-only, so a Codex implementer asked to commit its own work
hands back blocked_implementation_failure instead. In dispatcher mode the
dispatcher commits the in-scope files for it. The all-Claude,
all-Antigravity, and Claude-and-agy mappings have no Codex implementer and
use commit_mode: agent.
To track work in Linear instead of in files, add the tracker flags here. See
Files Or Linear first. You can
switch later with init --tracker, but only between stories.
3. Run the preflight check
llm-handoff doctor --repo-root path/to/your-project
doctor checks the config, Git and the commit count, each configured role's
CLI binary, the agy permission settings when a role runs on Antigravity,
interpreter grants, the next-agent normalizer, the gate and reviewer settings,
whether a Codex implementer has dispatcher commits, configured scanners, the Linear connectors in Linear mode, and whether the
handoff file routes. It exits 1 on any error. Fix every FAIL before
continuing; read the WARN lines too.
4. Tell the planner what to build
In the target repo, open docs/handoff/HANDOFF.md. The starter file already
routes to planner. Replace the body with what you want built, in enough
detail for a planner to scope a first bounded story:
---
next_agent: planner
reason: Scope the first bounded implementation task.
producer: user
---
# Initial Handoff
Read AGENTS.md and PROJECT_STATE.md. The goal of this epic is <what you
want>. Scope the first story and write a Work Packet for it.
Also fill in PROJECT_STATE.md. It is the durable summary the planner reads
at the start of every cycle and the finalizer updates when an epic closes.
5. Dry run, then run
llm-handoff --repo-root path/to/your-project --dry-run
The dry run reports the route and the provider CLI it would launch without launching anything. When it is clean:
llm-handoff --repo-root path/to/your-project
The loop now reads the handoff, runs one role, validates the rewritten
handoff, and continues, pauses, or escalates to you. Watch
docs/handoff/HANDOFF.md, git log, and logs/dispatch/ in the target repo.
To stop cleanly, run llm-handoff stop --repo-root path/to/your-project from
another terminal. The running agent finishes, its work is committed and
checked as usual, and the dispatcher exits before the next dispatch. While a
role runs, a first Ctrl+C in the dispatcher's console asks for the same stop;
a second Ctrl+C kills the running agent immediately. Outside a role's cycle,
and under supervised dispatch, Ctrl+C stops at once; see
Stopping The Dispatcher.
When the loop pauses and you are at a terminal, an operator prompt lets you
press Enter to send the planner back in, type @backend, @frontend,
@auditor, or @tpm (when a tpm is configured) with a message to dispatch
that role directly, or type retry or quit. Editing HANDOFF.md by hand also resumes it.
When the pause shows a planner's ## Proposed Backlog, Enter approves it
instead and typed text revises it; on approval the planner records the
approved epics in the project state file, and with
commit_mode: dispatcher the dispatcher commits that file alone as a local
commit. In Linear mode the same approval also lets the planner create the
approved issues through its own Linear MCP connector; proposals and
revisions make no Linear change. See
Operator Prompt and
docs/LINEAR.md.
At any pause you may also replace HANDOFF.md with plain words, for example
I made the edits. A person needs no frontmatter; every handoff an agent
writes still does. Make the edit while the dispatcher runs: the first file a
run sees is its starting point, not an edit. An edit with valid frontmatter
routes as written. Otherwise the planner runs once to interpret it: it gets
your text quoted and the last valid handoff the dispatcher saw, checks the
branch, uncommitted files, and recent commits, and rewrites only
HANDOFF.md into a valid handoff. It does not carry out your request, and
when your intent is unclear it routes back to you with one plain question.
If that pass does not produce a valid handoff, the loop pauses rather than
retrying. After an operator story, the note is not proof: the planner
compares your commits with the requested edits before routing to the
auditor, and names each problem with its fix otherwise. See
Operator Edits and the handbook's
Operator Stories section.
While a role runs in an interactive run, a chat line at the bottom of the
console takes messages. A plain message goes to a side chat session for the
running role. @<role> <message> (for example @auditor why the bounce?)
resumes that role's own session from its latest finished dispatch in this
run instead; a later dispatch of the role replaces the session it resumes.
Chat runs on Claude, Codex, and agy (antigravity) roles, and Claude, Codex,
and agy sessions can be resumed. When the target role is the one running,
has not finished a dispatch in this run, reported no session, or ran on
another provider, the message goes to the side session with a notice that
says why. Chat answers keep the role's own permissions (on agy, the role's
agent and --mode accept-edits or unsafe_bypass), and a chat change to
HANDOFF.md follows the usual audit and takeover handling; see
Operator Chat. For chat on agy, edit
event parsing is tested against a recorded agy 1.4.2 stream-json capture
and mocked streams; the automated tests do not run an end-to-end
interactive console chat against a live agy.
For the same path with per-platform commands, see QUICKSTART.md. For the slower explanation, see WALKTHROUGH.md.
Files Or Linear: Where Work Is Tracked
llm-handoff has two modes for where the backlog lives. HANDOFF.md is the
routing state in both. What differs is where the planner finds the next piece
of work and where evidence is recorded.
| File mode (default) | Linear mode | |
|---|---|---|
| Backlog | Epics and stories the planner writes into HANDOFF.md frontmatter (epic_id, story_id, remaining_stories), seeded by what you describe in the handoff body and PROJECT_STATE.md. |
A pool of Linear issues: one team, filtered by one project or one label, in a configured eligible state. |
| Who picks the next story | The tpm when agents.tpm is configured, after it sizes, splits, and orders the open epic's stories; otherwise the planner, from the epic's plan. |
The tpm when agents.tpm is configured, otherwise the planner, in both cases by Linear priority, then manual order, then creation date. |
| Durable record | Git commits plus the ledger in COMPLETED_WORK.md. |
Git commits plus YAML evidence comments on the issue and issue-state changes. |
| Who talks to Linear | Nobody. | The agents, through their own CLI's Linear MCP connector. The dispatcher never calls Linear and holds no Linear credential. |
| Extra requirements | None. | enforce_work_packet: true (the default), a Linear MCP server configured in each provider CLI you use, and Linear-mode prompt text in AGENTS.md and HANDBOOK.md. |
Starting in Linear mode
Add the tracker flags to init. On a repository that already has
dispatch_config.yaml, the same command switches it instead; see
Switching modes later:
llm-handoff init path/to/your-project --tracker linear --tracker-team ENG --tracker-project Platform
Give exactly one of --tracker-project and --tracker-label. The Linear MCP
server name defaults to linear; change it with --tracker-mcp-server. This
writes a tracker block into dispatch_config.yaml and renders the Linear
prompt sections into AGENTS.md and docs/handoff/HANDBOOK.md between
<!-- llm-handoff:linear:begin --> and <!-- llm-handoff:linear:end -->
markers. After an applied init or switch to Linear, init prints the
remaining setup steps: configure and authenticate the named MCP server in
each provider CLI, then run doctor and the optional probe from the
repository root. docs/LINEAR.md walks through the whole
path, from init to the first dispatcher run.
Then check the connectors. doctor prints one tracker connector line per
provider CLI, saying whether that CLI's MCP config defines the server. It
does not prove the connector authenticates; for that, run the opt-in probe
against a real issue:
llm-handoff doctor --repo-root path/to/your-project --probe-tracker ENG-123
Set tracker.claude_mcp_config to a file that defines only the Linear
server. Without it, Claude roles load every MCP server in your Claude
configuration, and the startup banner warns about it.
In Linear mode an empty or missing HANDOFF.md is not an error. The
dispatcher gives the planner one cycle to pick an issue from the pool and
write the lease.
Switching modes later
Run init --tracker on the initialized repository, between stories, with no
dispatcher running:
llm-handoff init path/to/your-project --tracker linear --tracker-team ENG --tracker-project Platform --dry-run
llm-handoff init path/to/your-project --tracker none --dry-run
init first refuses a repository that is not safe to change: a dispatcher
lock or run-state file, a handoff that holds a tracker_issue lease or an
active assignment, or a config that does not load. A switch then changes
only the managed tracker block of dispatch_config.yaml, the Linear blocks
of AGENTS.md and docs/handoff/HANDBOOK.md, and the tools: line of each
Claude subagent. It keeps your agents block, so --provider and
--verification-command are refused with --tracker here. It does not
rewrite HANDOFF.md, move issues, or call Linear. Going to Linear fails
when the existing config would not load in Linear mode, for example with an
antigravity role; change that by hand first.
Files you edited inside those managed parts are conflicts, and nothing is
written. So is a switch to files when your own text follows a managed block,
because a switch back adds the block at the end of the file; move the block
to the end first. --force leaves those files as they are, or replaces or
removes a managed block, and prints each one. A switch to files with
--force removes a handbook Linear block, even an edited one, so no Linear
instructions stay in file mode. It does not override a malformed
# llm-handoff:tracker marker, malformed or repeated Linear markers in the
handbook on a switch to files, a tracker key outside the managed block, or
a linked path. A switch to Linear and back restores the files byte for byte,
and running the same mode again changes nothing.
After switching to files, write a planner handoff describing the next epic.
After switching to Linear, empty HANDOFF.md and start the dispatcher; the
planner rebuilds the lease from the pool. Run doctor after either change.
The full key reference and validation rules are in
Tracker (Optional Linear Mode).
Gotchas
Things that stop a first run or quietly change what the loop does.
- Codex implementers need dispatcher commits. Codex's
workspace-writesandbox cannot write to.git, so wheneverbackendorfrontendruns on Codex (the reference mapping, both two-CLI mappings with Codex, andinit --provider codex),initwritescommit_mode: dispatcherfor you. Keep it, and keep the work-claim gate on (the default), which dispatcher commits require. Withcommit_mode: agentevery implementer handback comes back asblocked_implementation_failure, anddoctorreports it as aFAIL. See Dispatcher Commits. - Two reviewers run on every audit by default. A read-only security
reviewer runs beside the quality auditor. It uses the first installed CLI
that differs from the implementer's vendor. With a single vendor it still
runs, with a warning that the review is not independent. Opting out takes
audit.reviewers: [quality]plus a writtenaudit.security_opt_out_reason. - The planner, or the tpm when one is configured, must write a Work
Packet. With the work-claim gate on (the default), a handoff that routes
work to
backendorfrontendwithout a parseable## Work Packetfails validation. The shipped planner and tpm prompts ask for one. See Gates And Scanners. - The Verification command is agent-written and runs with your
permissions. Set
verification_allowed_prefixes(for example["python -m pytest"]) so the gate only runs commands you expect. - Scanners are off until you list them, and
required: trueis the default, so a listed scanner that is not installed fails every claim it applies to. See Gates And Scanners. - Codex with an API key needs a flag. Codex roles authenticated with
OPENAI_API_KEYinstead ofcodex loginneeduse_api_key_env: true. By default agents get an allowlisted environment, not your shell. - Editing control files stops the loop. A role that changes
AGENTS.md, the handoff prompts,dispatch_config.yaml, or anything under.claude/,.codex/,.gemini/, or.agents/without listing the path in its Work Packet routes the handoff touser. - Nothing pushes by default.
publish.mode: localcommits locally.merge-commitandprmodes are explicit opt-ins;prneedscommit_mode: dispatcherand theghCLI. See Publishing. - Windows. Run the dispatcher inside WSL2 or a container with no push credential. Claude Code's Bash sandbox needs WSL2, and Codex's native Windows sandbox is experimental. See the threat model.
- Pauses are not failures. With the default
pause_recovery: planner, the planner gets one recovery attempt per pause before the dispatcher waits for you. Three bounces of the same claim trip a circuit breaker. See Pause Recovery.
Gates And Scanners
Three checks sit between an implementer's "done" and the auditor. Two are on by default; scanners are opt-in and run inside the work-claim gate, so they need it on.
| Gate | Default | What it checks | Details |
|---|---|---|---|
| Work-claim gate | on (enforce_work_packet: true) |
For a ready_for_review handoff from backend or `fro |