From October 1 to 7, the same code change was reviewed fifteen times in a row by an AI. Each review is called a round.
In round 7, the AI found nothing to say: the change held only nine changed lines. In round 8, it held 400, and the AI started hunting for defects again. Nobody had set a maximum number of rounds.
That is the trap: "zero defects" does not mean "done", because the code keeps changing. If nobody writes down when to stop, the review goes on as long as the code changes.
This review AI is a skill of Claude Code. Claude is Anthropic's AI, and Claude Code is the programming assistant you drive from the keyboard. A skill is a folder of instructions that the assistant loads for a type of task. Mine is called senior-review.
Since then, every step of my work has a stop rule you can check. I tell it through a real case, shipped on October 27.
That real case is a ticket: a written request, such as a bug to fix, delivered as one reviewed change. A second skill, ticket, chains five steps for me: plan, code, proof, review, wrap-up.
Each step has an owner. It is often an agent, an instance of Claude launched for one precise task. I describe the method, not the skill file.
One model per step, because none of them reviews itself
Planning, coding and reviewing are three different jobs. I give them to three different agents. Each runs on a model of the Claude family: Fable, Sonnet or Opus.
A comparison: the architect, the mason and the building inspector. The mason does not sign off on his own wall. I explained in another article why the reviewer must never be the author.
The plan lists the steps, the files to touch and the tests to write first. I launch everything with one sentence: "plan fable execution sonnet /senior-review opus". The ticket skill reads it word by word.
| I write | What the skill launches |
|---|---|
| "plan fable" | a Fable agent writes the plan |
| "execution sonnet" | an executing agent, on Sonnet, writes the code |
| "/senior-review opus in a loop" | Opus reviewers, round after round |
If I say nothing more, the skill applies that same trio.
This choice of models is a setting, not a measurement. The diagram shows the five steps and their owner.
The test turns red before the first line of code
My contrast checker tells whether a text stays readable on its background, for example grey on white. Contrast measures the gap in lightness between the two.
It had a defect. A link pasted into a mockup review, where people comment on the drawing of a screen, reopened the tool with its usual colors. The real ticket is called "Share a contrast check through a link".
The two colors are written at the end of the URL, the address of the page: ?fg=767676&bg=ffffff. For this grey on white, the tool must show the contrast ratio, the number it computes: 4.54.
The ticket adds two requirements. Changing a color updates the URL without reloading the page, with history.replaceState. An invalid value such as ?fg=zzz brings back the usual colors, with no error.
A test is a small program that checks that the code does what you expect. TDD means writing that test before the code. It fails first (red), then passes (green) once the code arrives.
My test is e2e, end to end: a real browser opens the real page, like a user. It uses Playwright, a tool that drives a browser from code.
Real result of the first run, before any code (excerpt; the test titles are in French):
✘ 5 […] › 1. ?fg=767676&bg=ffffff : couleurs du lien appliquées, AA normal réussi (8.9s)
Expected: "767676"
Received: "1A1A2E"
5 failed
2 passed (10.9s)
The message names the defect: the field shows 1A1A2E, the usual color, instead of 767676. The test fails for the right reason. It computes the expected ratio itself with the official formula (WCAG).
Two tests already passed. One checks my own ratio calculation. The other is the ?fg=zzz case, which worked even without the feature. It guards against a future break, but proves nothing about reading the URL.
A Sonnet agent then writes the code. It starts from a brief, written instructions that inherit nothing from my session: the plan, the allowed files, the bans.
Undo the fix: if the test stays green, it proved nothing
A green test proves nothing until you have seen it fail without the fix. Reverse mutation means undoing the fix. The test must then go back to red.
If it stays green, it was not checking what you thought. Mutation testing automates this move across a whole project: see my article on mutation testing.
Before the review, on the first draft of the code, I undid the fix in four ways. I removed the reading of the URL, then its writing. I replaced replaceState with pushState, its cousin that adds a page to the history. I removed the check on values.
Each attempt made at least one test fail. More followed during the review, and one made nothing turn red: that is the story of round 2. Real results of two attempts (kept verbatim, in French):
=== M1 : lecture de l'URL retirée
4 failed
3 passed (10.7s)
=== M2 : replaceState retiré
1 failed
6 passed (2.2s)
Then I look at the page like a user, with screenshots at three sizes: mobile (390×844), tablet (820×1180) and desktop (1280×900).
My proof report separates three things, one line each:
- the code is right: the tests seen red then green, and the reverse mutation;
- the effect is visible: a screenshot read the way the user would see it;
- what stays unchecked: what the test really went through, and what it did not touch.
On this ticket, two areas have no test. No test clicks the "Apply" buttons. The language switch links lose the colors of the link.
Before trusting the agent, I rerun the project's automatic checks myself, then I read each screenshot. Below, the same link at the three sizes, taken on the live site.
All three screenshots show 4.54, with no horizontal scroll. The method for catching display bugs with Playwright is in my article on responsive testing.
The review runs until a round changes nothing
On this ticket, two reviewers worked in parallel in rounds 1 and 2, one on the code and one on the tests. Only one remained in round 3. All ran on Opus, five reviewers in total.
They are "blind": they do not know who wrote the code or what the author thinks of it.
A reviewer flags defects. Each flagged defect is called a finding, blocking if it prevents adding the code to the project, important if it must be fixed before moving on.
The rounds form a loop: review, fix, review again. It stops when a round meets three conditions:
- no blocking finding;
- no important finding still open;
- no code change.
A change that only touches tests counts as a change: another round is due. At the fifth round without a conclusion, the loop stops and hands control back to me.
My reviewers were only meant to read, and I had written it in their instructions. In fact they had every right, Edit and Write included: they could modify files. An instruction is only a request. Only the agent's definition really forbids an action.
Round 1: a real bug, three test gaps
In the tool, you pick a color with a picker. When you move it, it signals an event at every small movement. My code rewrote the URL at every event.
Now WebKit, Safari's engine, accepts at most 100 rewrites in 10 seconds. Beyond that it returns an error, SecurityError. The URL then stays frozen on an old color, while the displayed ratio is correct.
Playwright can launch that engine, and a reviewer reproduced the bug with it. Chromium, Chrome's engine, ends on the right URL. The fix: write the URL 250 ms after the last event, not at each one.
The reviewer also found three gaps in my tests:
- swapping the colors and typing the background were not checked in the URL;
- the English version of the tool was not checked after the URL rewrite;
- the reading of the background color had no explicit check.
Round 2: my test did not watch the delay
I replaced 250 ms with 0 in the code. All nine tests stayed green, so none really watched the delay. The reason: my test sent the 150 events at once, too fast for the delay to change anything.
The test now spaces its events by 16 ms, the pace of a drag. Real results, excerpt (kept verbatim, in French):
# M-a : debounce retiré (setTimeout 250 -> 0)
9 passed (10.3s)
# Tour 2, M-c : setTimeout 0 avec rafale espacée de 16 ms
1 failed 8 passed
# Tour 2, code restauré
9 passed (5.2s)
Two more remarks. Leaving the page before 250 ms loses the last color of the URL: rejected, no human copies a URL in 250 ms. The ticket announced "4.54:1", the tool shows "4.54": a badly worded criterion, not a bug.
Round 3: nothing to report
A last reviewer raised nothing. The delay test passed 90 times out of 90, even under load, with 4 CPU loops at 100%. The code did not change: the loop stops.
Total: 9 findings. 5 fixed, 1 rejected, 3 deferred, meaning put off until later. Those 3 are only two topics. The first is the rewrite of the bare URL at load: seen by two rounds, so counted twice. The second is the wording gap in the ticket, which is not a code defect.
My journal said everything was fine
My review skill keeps a journal: one line per review, with the findings raised, confirmed and dropped. As of October 27, it showed 981 confirmed findings. But "confirmed" only meant "passed my filter". The journal did not say whether I then fixed the defect or refused to fix it. I tell that discovery, figures and cost per finding included, in my article on AI code review.
The journal now records the outcome of each confirmed finding: fixed, rejected or deferred. This ticket is the first review to carry it: 5 fixed, 1 rejected and 3 filed as "deferred". That is one real deferral seen twice, plus the wording gap in the ticket.
So I announce no acceptance rate: over a single review, it would mean nothing. Nor can I say that Fable plans better, or that Opus reviews better than Sonnet. It is my setting, not a result.
Last step, the wrap-up. I reread my corrections from the session ("did you test it?", "are you sure?"): each one reveals a missing rule. The skill proposes at most three to me, with their destination. Nothing is written without my approval. This ticket produced one: a test that proves a delay spaces its events.
Conclusion
Every rule in this chain was born from a failure observed from the outside. A loop that ran fifteen times. "Read-only" reviewers who could write. A journal that graded itself with its own filter.
I have no before and after measurement: I promise you neither time savings nor quality gains. I offer two moves for tomorrow. Write the stop condition of your loop before you launch it. Undo your fix before you trust your test.