Loop protocol / How a change is proven

Gates first, then three reviewers, then a composed gate

The chain every change passes before it lands: the lane's gate, CodeRabbit, a DeepSeek Flash reviewer, an Opus reviewer when it matters, and one composed gate before the push.

A lane's claim that its change works is worth nothing until something else has checked it. Every change passes the same chain, and each step catches a different kind of mistake. The rules are section 5 and 6 of the contract; this page is why they look the way they do.

The chain

  1. The lane proves it red, then green. A worker writes a test that fails for the right reason, makes it pass, and runs the repository's gate (just check) on its worktree. It hands back an uncommitted patch and a return naming the command it ran and its exit status. A real one, from a worker lane in In practice:
    ```lane-return
    {"v":3,"lane":"W2270","task":"<task id>","status":"complete",
     "base":"17c5bdd8...","worktree":"<repo>/codex/loop87/worktrees/loop87-W2270",
     "patch":"<repo>/codex/loop87/W2270/candidate.patch","patch_sha256":"986b5caf...",
     "check":"gaterun-slot W2270 -- gaterun --log <run dir>/W2270/gate3.log -- just check","exit":0,
     "tail":"gr-run: slot=4 base=17c5bdd8... kind=heavy\ngr-run: tree=20c14c04... 11 changed paths\ngr-run: cmd=just check\ngr-run: end=2026-10-21T01:38:00Z exit=0 result=pass",
     "coderabbit":{"ran":true,"major":0,"unreviewed":0},
     "dsreview":{"verdict":"PASS","major":0,"fixed":0,"disputed":0},
     "questions":["The root regenerates the two identity files; they are left out of the patch.",
                  "CodeRabbit minor: wants different wording in one table row; I kept the brief's exact string."]}
    ```
    The root reads exit, tail and the review fields; a non-zero exit, a missing gr-run: ... result=pass line or an unanswered question sends it back.
  2. CodeRabbit, once. The lane runs coderabbit review --agent on its final candidate and fixes every critical and major finding.
  3. dsreview. The lane saves its brief and runs the DeepSeek Flash reviewer on the patch. It checks each major finding against the code: it fixes what holds and reruns once, or disputes the finding with evidence in its return.
  4. An Opus reviewer, when the tier calls for one. See the table below.
  5. A composed gate. One lander lane takes every queued candidate, rebases them onto current main, regenerates shared generated code once and runs the gate on the composed tree. A candidate that conflicts or fails drops out and goes back to its lane; the rest land.
  6. The root pushes, and CI runs. Only the root pushes. A red check on mainstops the line: nothing new lands until the fix for that check does.

Which reviewer, by tier

A task is guarded when it touches auth, secrets, privacy or redaction, tenant scope, migrations, destructive SQL, IaC, CI workflows, release or deploy configuration, or makes a live change. Everything else is routine.

StepRoutineGuarded
Lane gateAlwaysAlways
CodeRabbitOnce per final candidateAfter the reviewer
dsreviewIn place of the Opus reviewerAs well as the reviewer
Opus reviewerOnly when dsreview gives no verdict, a lane disputes one of its majors, on a second repair round, or when CodeRabbit didn't run; runs after landingAlways, with a security brief, before landing
Composed gateAlwaysAlways

A change after a verdict invalidates it, and only the delta is reviewed again. Reviewers report and never edit. Disagreements between the Opus reviewer and dsreview go in the run's decision log.

Why three reviewers

They miss different things. CodeRabbit is fast and good at local defects and convention, but it reads the diff without the task's intent. The two model reviewers get the brief, so they can say the change doesn't do what was asked.

Before dsreview replaced the routine Opus reviewer, I replayed 59 historical reviews whose exact patch was still on disk, graded each against the code with separate judge agents, and compared:

ReviewerRight verdictDefective changes caughtFalse blocks
Opus reviewer lane56 of 598 of 110 of 48
DeepSeek Flash, diff only49 of 595 of 114 of 48
DeepSeek Flash with read tools56 of 598 of 110 of 48

The tool-using version tied the Opus lane at a median of about $0.05 a review against about $1.09, at similar wall time. The two missed different defects, and together they caught all eleven. That's the case for running dsreview on everything and keeping Opus for guarded work and disputes.

The limits: eleven defective changes, mostly from one repository, graded by Opus judges. Treat the tie as "comparable", not "equal". DeepSeek also raises far fewer minor findings and tends to call a real small issue major, which is why a lane may dispute a major with evidence.

In its first live loop, lanes ran dsreview on 104 candidates: 84 passed, 9 failed and 11 got no verdict, which sent them to an Opus reviewer. It raised 29 major findings. Lanes disputed 12 of them with evidence and fixed most of the rest; a lane can do both in one return, so the split is approximate.

Gating the composed tree

Every candidate passed its own gate against an older main, so the composed tree has to be gated again. Two things make that affordable.

In the repository from In practice, a landing goes like this. One lander lane takes everything queued, rebases it onto current main, regenerates the OpenAPI code, the sqlc database code and the code fingerprints once, and commits. Then it gates the composed tree with every leg the touched packages need:

  • just check, the whole local gate;
  • each integration shard the touched packages run in, out of fourteen (four API shards, API capacity, reporting, services, domain, runtime, the rest, two inventory and two jobs shards), each a set of Postgres-backed Go packages under -race in containers;
  • the Playwright browser suite, about 150 tests, when the front end changed.
# from one committed compose worktree, every leg in the background at once
gaterun-slot L17-check   -- gaterun --log L17/check.log   -- just check &
gaterun-slot L17-api-1   -- gaterun --log L17/api-1.log   -- just integration-shard-ci api-1 &
gaterun-slot L17-jobs-2  -- gaterun --log L17/jobs-2.log  -- just integration-shard-ci jobs-2 &
# ... one line per shard the touched packages run in ...
gaterun-slot L17-browser -- gaterun --kind browser --log L17/browser.log -- \
    bash -c 'just playwright-setup && just playwright-comprehensive' &
wait

A candidate that conflicts or fails a leg drops out and goes back to its lane, and the rest land. The root pushes, and the next lander starts at once.

  • Run the legs in parallel. A full gate on a large repository is the unit tests, a dozen integration shards and a browser suite. Run one after another, thirteen legs took about 90 minutes; run side by side, the remaining legs of the next landing took about 20.
  • Only from a committed tree. The remote runner uploads HEAD plus uncommitted files, so parallel runs from a dirty worktree each gated a different partial tree, and a leg could pass without the change in it. Commit the candidates and the regeneration, check git status --porcelain is empty, and edit nothing until every leg exits.

CI runs the same recipes

After the push, CI proves the change again on clean runners, and it calls the same just recipes a lane ran, split across jobs: just check-go on a large arm64 runner, just check-gen, just check-web, the chart render, an image build, the integration shards as a matrix of just integration-shard-ci <shard>, and two Playwright suites. A first job classifies the changed paths so a docs-only push skips the heavy jobs, and a Renovate bump that only changes a pin takes a lighter proof route.

Two aggregator jobs, ci-success and integration-success, need every other job and are the only checks branch protection and LOOP.md's ci-required name. Adding a job never means editing the branch protection. The aggregators run even when a job failed, and a job may be skipped only when the path classifier said the change doesn't touch it; a skip anywhere else fails the check. The CI runners are pinned to an image that carries just, and the workflow fails if the image's version differs from the one it expects, so CI and the justfile never pin the same tool twice.

Traps

  • CodeRabbit's exit 0 isn't a clean review. A run with no complete line failed, and unreviewedFileCount above zero is partial coverage. New files are invisible until staged.
  • dsreview through a Cloudflare AI Gateway: a DLP check on responses buffers the whole reply, so streaming doesn't stream, and a proxied custom domain cuts any call over 100 seconds. Check requests only.
  • A gate that passes on its first run against unchanged code has probably asserted nothing. Make it fail for the right reason first.
  • Never hide a git push error. A push redirected to /dev/null failed silently twice in one loop; check the remote after every push.

Without my setup

The chain works with less. Keep the lane gate and the composed gate whatever else you drop. CodeRabbit is optional, and the lane template says how to remove it. dsreview needs a DeepSeek route; without one, the Opus reviewer takes its place on every routine change at roughly twenty times the cost. Remote gates become any machine with a slot limit, or CI on a pushed branch.

Get dsreview: tools/dsreview in the agent-workflows plugin. Rules: CodeRabbit is the review gate, gate runner.