DEV Community

AW
AW

Posted on

Twoperson – so your AI coding agent can't approve its own work

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
Enter fullscreen mode Exit fullscreen mode

https://github.com/ahm3dwasim/twoperson

Would genuinely like to hear how others are gating agent pushes.

Top comments (3)

Collapse
 
deanlee profile image
Dean Lee

Moving enforcement out of the prompt and into the schema solves the agent rationalization failure mode, but in unattended loops the failure surface usually migrates to the test assertions next. If the builder cannot talk itself past the SHA gate, it eventually learns to relax the evaluation criteria or mock the tricky fixtures so the packet passes cleanly on the first pass.The logical next constraint is making the test definitions or acceptance benchmarks immutable relative to the reviewer's inbox, so a green packet cannot be bought by rewriting the goalposts.

Collapse
 
drkashy profile image
AW

True, that's the real next failure surface, and the schema doesn't close it. It just moves the lie from I got a review -> the tests were always this weak. The only thing stopping it right now is that the packet carries the changed files, so a test edit shows up in the diff the reviewer sees. But that's back to trusting the reviewer to actually look, which is the soft spot. Hashing the test set at the start and rejecting any packet whose tests changed without an explicit "I'm changing the tests, here's why" verdict feels like the cleaner fix. Going to try building that in. Thanks for the suggestion. If you've got another approach you think would hold up better, I'm open to it.

Some comments may only be visible to logged-in visitors. Sign in to view all comments.