llm-pair · running in ~/work/agend-harness

Two models.
No human in the middle.

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.

GPT manages Claude builds OpenRouter — no Codex/ChatGPT sub two integration modes
The idea

Writer and reviewer are never the same model

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.

Manager Holds the gate

GPT via OpenRouter · never edits code
  • Writes 3–7 concrete acceptance criteria up front
  • Reads the real git diff every round — verifies claims against evidence, not prose
  • Decides the verdict; the loop ends only on its word
  • Never touches a file itself

Doer Does the work

Claude Code · resumable session
  • Implements exactly the current instruction
  • Runs lint / types / tests and reports raw output
  • Pushes back with evidence when the manager is wrong
  • Doesn't decide when it's finished
The channel between them is deliberately dumb — persistent threads on each side, and a script that just relays the latest message across. All the intelligence is in the two models and the protocol they speak, not the pipe.
The protocol

Every reply ends in one word

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

CONTINUE — keep going, here's the next step SOLUTION_COMPLETE — criteria verifiably met BLOCKED_NEED_HUMAN — a person must decide
01

Verify, don't trust

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.

02

Anti-thrash

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.

03

Escape hatch

BLOCKED_NEED_HUMAN is reserved for scope changes, credentials, and destructive actions. The human leaves the loop — they don't leave the building.

04

Hard caps

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.

A real run · clients CSV export

Watch it catch a bug a solo review would miss

This is the actual exchange that shipped PR #49 — a CSV export button — in three rounds. The moment that matters is round two.

GPT · manager kickoff

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.

CONTINUE ↓
Claude · doer round 1

Built it, mirroring the analog. Volunteered its own deviations — flagged an unrequested refactor and pushed back on three points instead of quietly complying.

CONTINUE ↑
GPT · manager round 1 review

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.

CONTINUE ↓
Claude · doer round 2

Supplied line-numbered proofs and raw playwright / lint output. Everything looked green.

CONTINUE ↑
GPT · manager round 2 review — the catch

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.

CONTINUE ↓
Claude · doer round 3

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.

DONE ↑
SOLUTION_COMPLETE — 3 rounds → committed → PR #49
Two ways to run it

Pick the shape that fits the job

① Driver loop

bin/pair.sh · fully autonomous

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

② Stop hook

GPT gates your normal sessions

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.

① run a task end-to-end
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.

② enable the gate in a repo
# 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
Setup

What it needs

No OpenAI subscription. The manager runs on your existing OpenRouter key, which is a few cents of tokens per round.

1
OPENROUTER_API_KEY in your environment

Already set on this machine. Swap the manager model any time with PAIR_MANAGER_MODEL.

2
The claude CLI, plus jq and curl

The doer is Claude Code itself; jq/curl drive the manager calls and parse the relay.

3
That's it — no Codex CLI, no ChatGPT plan

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.

Tuning

Knobs, all optional

VariableDefaultWhat it does
PAIR_MANAGER_MODELgpt-5.6-solManager model — any OpenRouter slug
PAIR_MANAGER_EFFORThighManager reasoning effort
PAIR_MAX_ROUNDS8Driver-loop round cap
PAIR_HOOK_MAX3Stop-hook continuations per session
PAIR_DIFF_LIMIT20000Chars of diff shown to the manager
PAIR_TIMEOUT300Seconds per manager API call
Judgement

When to reach for it — and when not

The loop is sharp for one shape of work and genuinely the wrong tool for another. Knowing the difference is most of the value.

Good fit

  • A bounded feature inside a git repo, with a pattern to follow
  • Anything where "done" is checkable against a diff or a test
  • Work you want a second, independent model to gate before it lands
  • Tasks safe to isolate in a worktree and let run unattended

Wrong tool

  • No repo to diff against — the manager has nothing to verify
  • Fixing the very tooling the doer's commands run through
  • Open-ended research or judgement calls with no checkable outcome
  • A trivial one-line change — the overhead outweighs the loop