Agentic code review, stabilized

Every review loop lives between two failure modes.

Approve too easily and defects ship. Review too hard and the loop never ends. Fredrik's severity-gated design finds the narrow middle without a crude max-iterations cutoff — this page shows how it works, where it can still spin, and four upgrades that make convergence a property instead of a hope.

Rubber stamp LGTM on everything.
Defects ship quietly.
The narrow middle Blockers are scarce by construction.
Zero is reachable. The loop ends.
Forever loop Something is always findable.
Round 47 and counting.

Part 1 · As designed

Fredrik's system, in one pass

Three sequential roles, three sequential reviewers, and one termination rule. Plans stay in context, never in the repo. The commit hook checks form, not truth — attribution belongs to reviewers who sign verdicts.

Product ManagerTriage against specs & ownership
PlannerPlan held in context, never committed
MaintainerExecutes the plan
Reviewers, strictly in sequence Standards → Annotations → Prose
Fresh context, no hints, never parallel
CommitHook checks form only; verdicts carry names

The severity buckets

MAJORblocks · replan

"The work is wrong, or carries a severe flaw. An incremental fix is unlikely to reach the right answer." Graded by wrongness, not fix size — a one-line behaviour change can't hide here as a nit.

MODERATEblocks · rework

"The outcome is right, the execution is not." Usually a standards violation. Needs rework and another look.

MINORnever blocks

"Fixable without another look." Recorded, fixed, no restart. The pressure valve that keeps nits from fueling the loop.

Termination rule Iterate until MAJOR + MODERATE = 0. Both blocking categories mean "work remains unseen" — when neither exists, the loop ends cleanly. No iteration cap anywhere.

What's already right about it

Severity beats scores. "Report any issues" always finds something; numeric scores let strong dimensions hide weak ones. Severity forces an explicit maturity judgment per finding.
Bounds as design detectors. Comment ratios and character caps force stack-ranking — a function that can't fit its explanation needs extraction, not a longer comment.
Fresh context, strict sequence. "Naming what changed tells a reviewer what counts — and it stops looking." Later reviewers never inherit earlier verdicts.
A claim with a name on it. The hook can't check truth, so attribution does: a false attestation passes the hook but becomes an attributed false claim.

Part 2 · The stress test

Where it can still spin

The termination proof rests entirely on reviewer calibration. Three forces can push the loop back out of the narrow middle — the doc names severity inflation as a risk and leaves oscillation unaddressed.

01

Calibration drift

A reviewer that can always surface a MODERATE recreates the forever-loop with extra steps. Zero is only reachable if blocking findings are scarce by construction — nothing in the base design makes them scarce.

02

Plausible-but-wrong findings

The main loop fuel. A confident, articulate, incorrect MODERATE costs a full rework round and a full re-review — and fresh-context reviewers regenerate them independently, round after round.

03

Oscillation

The fix for the Standards reviewer's finding trips the Annotations reviewer, whose fix trips Standards again. A-fixes-break-B cycles can hold blocking count constant forever while everyone works hard.

Part 3 · The upgrades

Four rules that force convergence

None of these is an iteration cap. Each one removes a specific source of non-termination while keeping the severity system intact — battle-tested in production agent fleets.

U1

Falsifiability gate

A MAJOR or MODERATE must carry a concrete failure scenario — an input/state that produces a wrong outcome, or the exact clause violated. No scenario, it demotes to MINOR.

Why it terminates: vague unease can no longer block. Blockers become scarce by construction, so zero becomes reachable. Kills failure mode 01.

U2

Adversarial refuter

Before a finding blocks, a fresh-context skeptic is prompted to kill it. Survivors block; refuted findings are logged with the refutation.

Why it terminates: plausible-but-wrong findings are filtered before they cost a rework round. Kills failure mode 02, cheaply.

U3

Delta-scoped re-review

Once a reviewer passes the work, it may only raise new blockers on lines touched since its verdict. Everything else it already signed.

Why it terminates: the reviewable surface shrinks monotonically. A-fixes-break-B cycles lose their fuel. Kills failure mode 03.

U4

Divergence detector

If the blocking count fails to strictly decrease for two consecutive rounds, stop iterating — escalate to replan or human. Rate-based, not count-based.

Why it terminates: it never fires on a healthy loop (those strictly decrease), but bounds every unhealthy one. The safety net max-iters pretends to be.

Free bonus "A claim with a name on it" gives you a calibration ledger for free: track each reviewer's MAJOR overturn rate over time and severity inflation shows up in the data before it ever stalls a loop.

Part 4 · Paste-ready

The semantic version

The upgrades as contract language you can drop straight into CONTRIBUTING.md. Each block is the reviewer- or maintainer-facing wording that implements the rule — copy, paste, adjust the voice. This is the actual lever for each idea; the sliders in the Loop Lab below are just its shadow.

U1

Verdict contract

A MAJOR or MODERATE verdict must state a failure scenario: the input or state that produces the wrong outcome, or the exact standard clause violated. A finding that cannot name one is recorded as MINOR and does not block.

Goes in the reviewer instructions. The commit hook can check for the scenario's form, exactly like the existing attestation checks.

U2

Refuter stage

Before rework begins, each blocking finding is handed to a fresh context with only the diff and the claim: "Attempt to refute this finding. If you cannot reproduce the failure or locate the violated clause, mark it refuted." Refuted findings are logged with the refuter's name, under the same attribution rules as verdicts.

A new stage between review and rework. Costs one cheap read per blocker; saves a full rework round per false positive.

U3

Re-review scope

You previously passed revision N. New blocking findings are in scope only on lines changed since your verdict. Re-raising a finding on an unchanged line requires naming what your earlier pass missed.

Goes in the re-review prompt. Keeps fresh context intact — it narrows scope, not memory.

U4

Stop rule

The maintainer records the blocking count each round. If it fails to strictly decrease for two consecutive rounds, stop iterating and escalate with the round log attached.

Goes in the maintainer loop. Bounded without max-iters; never fires on a healthy run.

Part 5 · See the dynamics

Loop Lab

A seeded Monte-Carlo simulation of the review loop — 300 trials per run. Set how hostile the environment is, flip upgrades on and off, and watch the open-blocker curve bend. Baseline (no upgrades) always runs alongside your configuration for comparison.

What this is, honestly: illustrative dynamics, not an empirical claim. The parameters are priors, not measurements from any repo — the sim's only job is to make the failure modes, and which rule kills which one, legible. For the empirical view, bring your own round logs. For the exact model, open the assumptions drawer below the chart.

Environment

Upgrades

Presets

Open blockers by round mean across active trials

Baseline (no upgrades) Your configuration

One run, narrated a single seeded trial under your configuration

    Table view of the chart data
    RoundBaselineYour config
    Model assumptions (the exact math)
    • Each hidden real defect is surfaced with p = 0.85 per round; ~1 in 5 seeded defects is a MAJOR.
    • Plausible-but-wrong blockers per round ~ Poisson(1.6 × fp-rate); severity-inflated nits ~ Poisson(1.1 × strictness).
    • U1 demotes 85% of false and 90% of inflated findings to MINOR. U2 refutes 75% of remaining false blockers and wrongly kills a real one 4% of the time (it returns to the hidden pool).
    • Each fix ripples a new defect with p = 0.5 × oscillation; U3 multiplies that by 0.15. U4 escalates after two consecutive non-decreasing rounds.
    • 300 seeded trials per run (mulberry32), capped at 30 rounds; curves show the mean over trials still active at each round.

    Part 6 · The receipts

    One real diff, reviewed for real

    Commit 2c37e1c of annotated-tree — the --githook-guide feature, 5 files, +112/−17 — staged in a worktree exactly as a maintainer leaves work. Judged by fresh-context agents handed the repo's own review-prompt.md gate blocks verbatim, against its own repo-standards.md, with the real binary running the machine gates. U1, U2, and U4 layered on top.

    Round 12 blockers
    • pre-commit RED — two new files break the 200-char annotation bound (277 and 209 chars).
    • standards MAJOR — "the embedded guide's Gates A and C are general commit-governance workflow" outside the root charter; "the AUTO-REJECT's own remedy was not taken."
    • refuter UPHELD — "this is not reviewer taste; the governing docs state the rule explicitly: update the charter first, or don't build it."
    • annotations 3 MINOR · prose 1 MINOR — recorded, never blocking.
    Reworkneutral re-plan
    • A planner that neither wrote the change nor raised the finding chose the remedy: narrow the shipped guide to the annotation gates. "Extending the charter to fit one doc is backwards."
    • An implementer applied it exactly: strict-check green, build clean, re-staged.
    Round 20 blockers
    • standards 0 / 0 / 0 — charter violation resolved, gate-count contradiction gone, tests 19/19.
    • annotations 2 MINOR · prose 1 MINOR, knowingly left.
    • MAJOR + MODERATE = 0 → the loop terminates. Blocking 2 → 0, strictly decreasing; the stop rule never fired.
    Charter at the reviewed commit

    "…enforces the first-line annotation format (strict-check), and teaches it…"

    Charter on main today, after 0.5.0 and 0.6.0

    "maps a workspace an agent can route from without opening files, and keeps it honest"

    The MAJOR flagged a real charter/product drift that the repo's own history later resolved — via the other of the two remedies the finding named. No causal claim; the drift was real, and it got fixed.

    What the run proved beyond the verdict

    The false-green catalog is real. The first strict-check run was piped through tail and reported exit 0 — the exact trap CONTRIBUTING.md documents. Re-run properly: exit 1.
    Fresh context is load-bearing. A resumed-context re-review reported the staged diff "unchanged" and re-issued stale findings while the files had been rewritten. One resumed context, one immediate false claim. A fresh context reviewed correctly.
    Severity held its shape. All eight MINORs stayed MINOR across rounds; the one MAJOR carried a concrete scenario and survived adversarial refutation. No inflation observed — n=1, but the ledger below is how you'd see it at scale.

    The oscillation probe

    Follow-up challenge: scan repo-standards.md for a gray area and try to induce oscillation. Three candidate tensions were identified (annotation bound vs forced file-split, DbC vs Fail Fast, charter concreteness vs altitude) and the sharpest was probed live on real code.

    The probe. A realistic defensive guard was staged at the strict.rs grading seam: re-check the annotation shape after an Ok verdict. Fresh Gate A: MAJOR under DbC, delete it — and it found the guard was dead code with real latent defects (body-relative line numbers, a dropped length bound, a silent fall-through).
    The flip test. The guard was committed as the base and its deletion staged — exactly the demanded fix — for a second fresh Gate A with no knowledge of round one. Result: 0 / 0 / 0, with Fail Fast explicitly graded "none": errors are still caught at the source, and the deleted fallback was the softer path.
    Verdict: no oscillation induced. The theoretical DbC-vs-Fail-Fast ping-pong collapses because reviewers ground findings in the actual code (both traced the shared checker), not in principle labels. The fixed point is stable. Where variance did appear: the intent-neutrality tripwire — one reviewer rejected a nine-word tail clause as "arguing for the change," a flat rewrite passed. The loop's nondeterminism lives at the intent gate, and a sloppy brief costs a full round.

    Part 7 · Bring your own data

    Your loop, measured

    The sim shows dynamics; this shows your repo. Verdicts already carry names, so the loop emits everything needed. Paste per-round logs from a real run and get the empirical view: actual convergence, divergence rounds flagged, and a per-reviewer calibration ledger — the severity-inflation detector, running on data instead of priors.

    Validated on a real repo diff (2026-08-03). Commit 2c37e1c of fredrikolis/annotated-tree — the --githook-guide feature, 5 files, +112/−17 — reviewed by fresh-context agents running the repo's own docs/review-prompt.md gates against docs/repo-standards.md, with U1/U2/U4 layered on. Round 1: the machine gate went red on two annotation bounds, and Gate A raised one MAJOR (the embedded guide's Gates A/C exceed the root charter — an AUTO-REJECT the adversarial refuter upheld with the standard's own clause). Neutral re-plan, rework, round 2: MAJOR + MODERATE = 0, tests 19/19. Two rounds, strictly decreasing, the stop rule never fired. "Load real run" below charts it.
    Log schema
    {
      "rounds": [
        { "findings": [
          { "reviewer": "standards",
            "severity": "MODERATE",
            "overturned": false }
        ]}
      ]
    }

    severity: MAJOR | MODERATE | MINOR. Set overturned: true when the finding was later refuted, withdrawn, or shown wrong. One entry per finding, one object per round, in order.

    How you'd prove stabilization

    1 · Instrument. Log per-round blocking counts and named verdicts — the schema above. The attribution mechanism means this is a logging change, not a process change.
    2 · Replay A/B. Re-run a batch of past tasks with and without the four contract rules, same tasks, same models, seeds fixed where possible.
    3 · Compare. Rounds-to-terminate distribution, stall rate, defect escape rate, and per-reviewer overturn rate between arms.
    4 · Watch drift. Overturn rate per reviewer over time is the severity-inflation detector — it moves in the data before it ever stalls a loop.