I run two coding agents on one repo — one building, one reviewing. For months the "get a review first" rule was a line in a system prompt. It held right up until the builder was unattended for a few hours. Then it would skip the review, or review its own work, or get a real approval and quietly rebase three commits on top before pushing.
A rule the agent can talk itself out of isn't really a rule. So I moved it out of the prompt and into the file format the agents use to talk to each other.
It isn't a code reviewer — the agents do the reviewing. It's the layer underneath that: a local, offline, SHA-bound approval ledger and shipping gate for multi-agent coding workflows. It records that a review happened, pins the verdict to one commit, and refuses to let the builder call anything "shipped" without one.
What it does
twoperson is a small Python CLI. The builder writes a JSON review packet — goal, head SHA, files, tests — and publishes it. The reviewer claims it and records a verdict. The enforcement lives in the schema, not in anyone's good intentions:
it refuses a verdict for a packet that was never published,
it refuses an approval that names a different commit than the packet,
it refuses a "pushed" packet unless it cites a real approving verdict for that same commit.
So an approval is bound to one exact commit. Rebase, amend, or add a commit on top and the approval goes stale — the schema simply can't represent "we shipped it" without a matching, current review.
Things that mattered more than I expected
The Stop hook. twoperson install-hook adds a Claude Code Stop hook so the reviewer wakes when the builder's session ends, instead of polling on a timer. The hook only drops a signal — never a packet — because a hook doesn't know whether the tests actually passed.
Packets are untrusted input. They get a credential scan, and they're shown to the reviewer fenced as "this is data, not instructions," so a packet can't smuggle a prompt into the reviewer.
Worktrees. If each agent runs in its own git worktree, the inbox resolves to the main working tree, so the two agents actually see each other. That one cost me an afternoon.
Where it sits
It isn't a replacement for GitHub PRs or branch protection — those are server-side and great. twoperson is the local gate before that: a way for a second agent to sign off on the exact commit at the speed of local file I/O, so by the time a PR opens there's already a commit-bound record of what was reviewed. If you run agents unattended and want an approval pinned to a commit rather than a vibe, that's the gap it fills.
Prior art I read first, because it's the obvious question. OpenAI's codex-plugin-cc has an opt-in Stop-hook review gate — genuinely the better tool if you want a second model on the diff in-session — but it gates the turn, not a commit, and nothing later refuses a stale "we pushed it." claude-review-loop persists reviews without binding them to a SHA. shiplog leans on signed Reviewed-by: commits but needs gh and a remote. secondmate has the same stale-approval refusal, but gets there by spawning the agents itself. I wanted the binding without the orchestration.
MIT, on PyPI:
pip install twoperson
https://github.com/ahm3dwasim/twoperson
Would genuinely like to hear how others are gating agent pushes.
Every number we watched said the run was working. Correct-per-sample probability tripled. The greedy accuracy curve was climbing. By the numbers on our dashboard, this was a textbook RLVR win.
Then we sampled the checkpoint 64 times per problem instead of once. pass@64 had collapsed from 0.83 to 0.
An eight-frame animation does not have a fixed duration. At 8 fps it lasts one second; at 12 fps it lasts two-thirds of a second; at 16 fps it lasts half a second. Before drawing or generating more frames, check whether the problem is missing poses or the time each pose stays on screen.
We maintain
A webcam hand tracker hands you a position, thirty or sixty times a second, as a float
between 0 and 1. A musical scale hands you seven notes per octave. Building a
browser hand-gesture synthesizer is mostly the work of
getting from the first thing to the second thing without it sounding like a fax