New
Apps 715Makers 715This week 715
Promote your app−30%
2views in
the last 30 days

llm-handoff

Run Claude Code, Codex, and Antigravity on one repo. Agents hand off work through one Markdown...

Built with Claude/Claude Code

by choughton · from USUnited States · added Oct 29, 2026
no ratings0 comments

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.

llm-handoff dispatch loop

Contents

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-write sandbox cannot write to .git, so whenever backend or frontend runs on Codex (the reference mapping, both two-CLI mappings with Codex, and init --provider codex), init writes commit_mode: dispatcher for you. Keep it, and keep the work-claim gate on (the default), which dispatcher commits require. With commit_mode: agent every implementer handback comes back as blocked_implementation_failure, and doctor reports it as a FAIL. 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 written audit.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 backend or frontend without a parseable ## Work Packet fails 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: true is 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_KEY instead of codex login need use_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 to user.
  • Nothing pushes by default. publish.mode: local commits locally. merge-commit and pr modes are explicit opt-ins; pr needs commit_mode: dispatcher and the gh CLI. 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

Description taken from the repository README.

Ratings

–
0 ratings
5 0
4 0
3 0
2 0
1 0

Rate this app

Sign in to rate

Comments 0

Sign in to comment

No comments yet.

Related apps

#handoff · #agents · #python