One model directs and holds approval authority. The other does all the building. They talk through a shared channel, round after round, until the work is actually done — and you only step in when it decides you're needed.
Writes the acceptance criteria, reviews every round against the real diff, and decides when it's done.
Implements the instruction, runs the checks, reports back what changed — and pushes back when the manager is wrong.
A model reviewing its own work is a rubber stamp — it makes the same blind-spot mistake twice. The whole point of the pair is that a different model, in the same reasoning ballpark, catches what the first one missed. So the roles are split hard and never cross.
git diff every round — verifies claims against evidence, not proseControl passes on a sentinel — a single tag on the final line that the relay parses mechanically. No prose interpretation, no ambiguity about whose turn it is or whether to stop.
The manager always receives git status and git diff HEAD next to the doer's report. "Tests pass" with no output shown is treated as unverified — and sent back.
The manager carries a NOT-priorities list (no style nits, no theoretical edge cases, no re-raising settled points) so the loop converges instead of bikeshedding forever.
BLOCKED_NEED_HUMAN is reserved for scope changes, credentials, and destructive actions. The human leaves the loop — they don't leave the building.
A round cap and a per-session continuation cap mean two polite models can't loop forever thanking each other. It stops, and hands you the transcript.
This is the actual exchange that shipped PR #49 — a CSV export button — in three rounds. The moment that matters is round two.
Six acceptance criteria, including: values must be correctly escaped, button disabled when the list is empty, no schema or auth files touched. Then a precise first instruction: study the sites-page analog, export the visible columns.
Built it, mirroring the analog. Volunteered its own deviations — flagged an unrequested refactor and pushed back on three points instead of quietly complying.
Checked the diff and noticed the reported test results weren't in the evidence. Demanded raw command output with exit codes before it would approve anything.
Supplied line-numbered proofs and raw playwright / lint output. Everything looked green.
Caught the real defect: the CSV had 8 headers but the table has 6 named columns — Claude had silently split "Contact Info" into separate fields. Ruled it a strict miss and sent back a precise fix.
A same-model self-review almost certainly rubber-stamps this. A different model, reading the criterion literally, didn't.
Collapsed to 6 columns, then went further than asked — round-tripped the output through the repo's own CSV parser to prove embedded newlines don't split records.
Point it at a repo and a task. GPT drives Claude start to finish and exits when the criteria are met, it's blocked, or the round cap hits. Full transcripts land in runs/.
Fires when Claude tries to end a turn. GPT reads the final message and diff, then either lets it end or blocks with feedback. Inert by default — enable per repo with a flag file.
llm-pair/bin/pair.sh --cwd ~/work/some-repo --yolo --max-rounds 6 \ "Add X, following existing patterns, with a test"
--yolo gives the doer --dangerously-skip-permissions — use it only in a throwaway worktree. Without it, the doer runs in acceptEdits: file edits allowed, unapproved shell commands denied. Best practice is to point it at a fresh git worktree so a bad run can't touch your live tree.
# merge hooks/settings-snippet.json into your Claude settings once, then per repo: touch .llm-pair-manager # enable · delete the file to switch it off
No OpenAI subscription. The manager runs on your existing OpenRouter key, which is a few cents of tokens per round.
Already set on this machine. Swap the manager model any time with PAIR_MANAGER_MODEL.
claude CLI, plus jq and curlThe doer is Claude Code itself; jq/curl drive the manager calls and parse the relay.
The manager is plain OpenRouter chat calls. If the loop earns its keep, the upgrade path is an agentic manager that reads code itself — the protocol and both modes stay identical.
| Variable | Default | What it does |
|---|---|---|
| PAIR_MANAGER_MODEL | gpt-5.6-sol | Manager model — any OpenRouter slug |
| PAIR_MANAGER_EFFORT | high | Manager reasoning effort |
| PAIR_MAX_ROUNDS | 8 | Driver-loop round cap |
| PAIR_HOOK_MAX | 3 | Stop-hook continuations per session |
| PAIR_DIFF_LIMIT | 20000 | Chars of diff shown to the manager |
| PAIR_TIMEOUT | 300 | Seconds per manager API call |
The loop is sharp for one shape of work and genuinely the wrong tool for another. Knowing the difference is most of the value.