· 3 min read

The review needs a suspect

An LLM can catch bugs in its own patch because the first draft gives the review pass concrete evidence to inspect.

A hand holds a magnifying glass over a sheet of old postage stamps, bringing a single olive-green stamp into sharp focus.
Heptagon, Public domain

You watch the model write a race condition. Then you ask it to review the patch, and it points at the exact await that causes the race. The obvious question feels almost insulting: if you knew that, why did you write it?

It didn't know, at least not in the way you mean. There was no finished patch sitting inside the model, waiting for the right prompt to fish it out. The review is another inference pass, and it gets to look at something the first pass never had: an actual candidate, with a control-flow path you can trace, and an instruction to distrust it.

Which is a fine reason for the model to fumble the first draft, and a bad reason for a tool to hand you that draft as the answer.

cache.ts

+const cache = new Map<string, User>();
+
+async function getUser(id: string) {
+  if (!cache.has(id)) {
+    cache.set(id, await fetchUser(id));

Model as author (14:02)

Looks consistent with the surrounding code.

Model as reviewer (14:03)

Two calls can cross the await and duplicate the fetch.

+  }
+  return cache.get(id)!;
+}
Same weights one minute apart on the same patch; only the objective changed.

The bug gets an address#

When the model generates, you're asking for one plausible implementation out of thousands. When it reviews, you're asking what can fail in these twelve lines. The second question is smaller, and it comes with better nouns.

cache.tsdiff
- const cache = new Map<string, User>();+ const cache = new Map<string, Promise<User>>();  async function getUser(id: string) {-   if (!cache.has(id)) {-     cache.set(id, await fetchUser(id));-   }-   return cache.get(id)!;+   const pending = cache.get(id) ?? fetchUser(id);+   cache.set(id, pending);+   void pending.catch(() => cache.delete(id));+   return pending;  }

In the removed version, two callers can both miss the cache before either fetch completes, and each one goes off and fetches. That interleaving is hard to ask about in the abstract, but once the code exists the reviewer only has to trace it. The repaired version caches the in-flight promise instead and evicts it if the fetch rejects.

This split is useful enough that people train for it directly. The CriticGPT research found that model-written critiques of model-written code were preferred over human critiques in 63 percent of cases, and that the critics caught more bugs than the paid reviewers in the study — while also hallucinating defects that weren't there. Suspicion is a skill, not an oracle.

You don't even need a second model. The Self-Refine paper used one model as generator, critic, and refiner, and reported roughly twenty percentage points of average improvement across seven tasks. Nothing latent got uncovered along the way; the model spent one pass creating feedback and another acting on it.

Stop calling draft one the answer#

So the unit worth building around is the whole loop — generate, verify, critique, repair — and the first call is only its opening move.

agent-loop.tsts
let patch = await generate(task, repository);for (let pass = 0; pass < 2; pass++) {  const evidence = await verify(patch);  const critique = await review(task, patch, evidence);  if (!critique.blocking.length) break;  patch = await repair(patch, critique, evidence);}

The verify step matters more than another confident paragraph, because a failing test or a profiler trace introduces facts the model can't manufacture by changing tone. The EvalPlus study expanded HumanEval's tests by eighty times and exposed enough hidden failures to drop measured pass rates by 19.3 to 28.9 percentage points. A candidate that survives weak tests has merely exhausted the questions you happened to ask.

Give the reviewer the original task, the patch, and the tool output, and ask for blocking defects and counterexamples rather than a ceremonial “make this better.” Then repair against those findings and run the checks again. A second model can add some diversity of opinion, but as far as I can tell the tool output moves the needle more than a change of personality does.

So yes, the LLM can often write the code its review expects — after the review, because the expectation came from inspecting the draft. The part that bugs me is the interfaces. Draft one gets presented as finished work, and the pass that would actually catch the race sits behind another button I have to remember to press.

Make the system read what it wrote before it hands you the mess. The model never needed to be clairvoyant for that.