TL;DR
- Some things can't be checked with a number, like whether an animation feels right.
- So a second, read-only agent grades the first one against a written rubric it is not allowed to edit.
- In my run the reviewer rejected the builder three times, and the most interesting problem it caught was in the test evidence, not the code.
In Part 1 I built a loop that chased a number, frames per second. But most of what we care about in software is not a number. "Does this region switch feel good?" has no assert. You cannot write expect(feelsRight).toBe(true).
So this part is about how you check quality when there is nothing to measure. The approach I used is a second agent that grades the first one against a written rubric. In my run the reviewer turned the builder down three times before it approved anything, and the most interesting problem it found was not in the code at all.
A quick reminder of the definition, since this is Part 2 of 3: a loop is an external script that runs the agent, a separate check the agent cannot edit decides pass or fail, and it repeats until it passes or hits a limit. In Part 1 the check was a Playwright test. Here the check is another agent.
The problem this loop solves
In the browser you can switch regions, say from Tamil to Korean, which swaps out hundreds of posters at once. Done badly, the grid flashes blank and jumps around. Done well, it fades from one set to the next, keeps its layout, shows a loading state, and puts you back at the top.
"Done well" is subjective, which is the kind of thing you cannot unit-test. So I wrote it down as a rubric and had a second agent apply it.
The bar: a rubric a person owns
The rubric is seven plain-English checks in a file, and the first line is the one that matters:
Overall APPROVED requires every item PASS. This file is human-owned.
Only a person changes the bar.
The seven items are things like a crossfade instead of a flash, no layout shift, a visible loading state, posters that stay 2:3, and landing back at the top after a switch. Nothing fancy, just a written version of "good" that a reviewer can apply the same way every time.
Two agents that never touch
The important decision is that the builder and the reviewer are two separate Claude Code agents with different permissions.
The builder can write code:
tools: Read, Edit, Write, Grep, Glob
The reviewer is read-only. It cannot edit anything, including the rubric:
tools: Read, Grep, Glob
Each one runs in its own context. This answers an obvious question: why not let one agent do both jobs? Because an agent that writes code and then reviews the same code tends to approve it. Mine did, almost word for word, as you will see. The reviewer never sees the builder's reasoning or its summary. It only sees the code and the evidence, which is a set of before, during, and after screenshots from an automated capture.
The loop looks like this:
builder makes one change -> capture screenshots -> reviewer scores the rubric
^ |
|______ CHANGES-REQUESTED: feed feedback back ________|
(until VERDICT: APPROVED, or cap = 6)
The builder cannot edit the rubric, the tests, or the capture script, which is the same protected-files rule from Part 1.
What actually happened
The reviewer turned the builder down three times. Here is how it went.
Round 1. The builder replaced the whole grid with a loading skeleton during the swap and summed up its work as "eliminating the hard cut and empty-grid flash." The reviewer looked at the screenshots and disagreed, because replacing every poster with identical skeleton cards is itself a hard cut rather than a crossfade. Verdict: CHANGES-REQUESTED. The builder had claimed it fixed the flash, and the evidence said it had not. If the same agent had been reviewing its own work, that version would have shipped.
Round 2. The builder reworked it into a real crossfade, with the old posters fading out over the new grid. The reviewer agreed the fade was there now, and then caught the next issue: the required loading state never actually appeared, because the local posters decode instantly. Correct, and easy to miss. Still not approved.
Round 3. The builder added the loading state, and six of the seven items clearly passed. The seventh got a verdict I did not expect, though. Not a fail, but BLOCKED:
The after frame sits at the top, but the before frame was already at the top, so a scroll reset from the middle of the list was never actually tested.
The most interesting bug was not in the code
That third verdict is my favourite part of the project. The feature was fine. The code did reset the scroll to the top. What failed was the evidence: the screenshot always started at the top, so there was no way to see the reset happen. Before and after looked the same, and the reviewer refused to give credit for something it could not see.
The fix was in the capture script, not the app. It now scrolls partway down before switching, so the reset is visible:
await grid.evaluate((el) => { el.scrollTop = Math.round(el.scrollHeight * 0.4) })
// ...switch region...
expect(afterScroll).toBeLessThanOrEqual(4) // proves it snapped back to the top
A reviewer is only as good as the evidence you give it. A good reviewer pointed at a blind spot still cannot see anything.
What a gate does not cover
The run stopped at round 4, not because it failed but because I hit a Claude usage limit. These local loops run on a Claude Code subscription rather than a paid API key, which I will come back to in Part 3. So I checked the last rubric items by hand and merged.
One honest detail: the merged code had a couple of lint errors, because the loop only ran the UX rubric, not lint. I cleaned those up myself. A loop checks what its gate checks and nothing else, so it helps to know what your gate leaves out.
What I took from Part 2
The reviewer has to be independent: its own context, read-only, and no ability to edit the code or the rubric. An agent reviewing its own work is close to no review at all. The builder's confidence did not catch the flash. The screenshots did.
In Part 3 the loop changes again. The first two loops you start and watch. The last one runs on a schedule while you are asleep, and its whole job is to do nothing most nights. I will also cover the bug that broke all three loops and the setup that lets this one run for free.
The full project, with the builder and reviewer agents, is on GitHub:
MovieBrowse — a loop-engineering learning project
A small, deliberately-boring regional cinema browser: a fast, virtualized grid of film posters for one "region" (a language / film industry, not a country), with a manual region selector and a geo-IP default.
But the website is not the point. It's a vehicle for learning and demonstrating loop engineering with Claude Code — building automated loops where an AI agent makes a change, a deterministic check decides whether that change is good, and the loop repeats until the check passes. The interesting part is the loops, not the movie site. If the website ever became the hard part, something went wrong.
This repo ships three loops, each a different shape, each anchored in a real config and
a real failure. There's one write-up per loop in blog/.
What is a "loop"?
A loop here means:
run something → check a verifiable…
Feedback and different takes are welcome. Would you trust an AI to review another AI's work, or does a human still need to sign off?
Top comments (0)