TL;DR
- A "loop" is not an agent grading its own work. It is an external script that re-runs the agent, plus a separate check the agent cannot edit.
- I turned "feels smooth" into an FPS number and let the loop optimize toward it.
- I set the target too high to be reachable on a 60Hz screen. The loop kept failing but never faked the result. The bug was in my number, not the code.
Could I get an AI agent to make my website faster without me sitting there, running it, reading the numbers, and running it again? That is what this series is about. Not how I built a website, because the website is boring on purpose, but how you wrap an agent in a loop that works toward a goal on its own, and how you stop it from cheating along the way.
In this first part I want to explain what a loop actually is, because there is a common misconception, and then walk through a real one. I set this loop a target that was physically impossible to reach and watched what it did. That run taught me more than a passing test would have.
This is Part 1 of 3. All three parts use the same small movie-poster website as the example, but the website is never the point.
What a loop is, and what it is not
I had a wrong idea about this at first, so let me clear it up. A loop is not an agent prompting itself, grading its own work, and deciding when it is done. An agent left to mark its own homework will usually tell you it passed.
A loop is closer to this: an external script runs the agent, a separate check that the agent cannot edit decides whether the result is good, and that repeats until the check passes or you hit a limit.
There are three parts to it that come up again and again:
- The driver: the script that re-runs the agent. This is the thing that removes the manual work, not the agent.
- The gate: the check that decides pass or fail. The agent makes changes, but it never decides when to stop.
- The cap: a limit, so a stuck loop gives up instead of running forever.
One rule matters more than the rest. The thing being checked must never be able to edit the check, and only a person is allowed to change the target. If the agent can edit its own test or lower its own bar, the loop is pointless. So most of the work in a loop is not writing a clever prompt. It is building a check the agent cannot get around. That idea runs through the whole series.
The problem this loop solves
The website is a grid of movie posters you can scroll and zoom through. It holds hundreds of real images and only keeps the visible ones in the page, so it stays light. The one thing it has to do well is feel smooth.
"Feels smooth" is a vibe, and you cannot put a vibe in a loop, so the first job is to turn it into a number. I used two:
- sustained frames per second (FPS) while scrolling and zooming, and
- no single main-thread task longer than 50 milliseconds, since a long task is what makes scrolling stutter.
One quick note on why the data matters. I could have filled the grid with plain coloured boxes as placeholders, and it would have been less work. But coloured boxes are almost free to draw, while real images cost real decode time, memory, and paint. A performance test running against coloured boxes would be measuring something that isn't really there. So I used about 200 real JPEGs, with the same <img> markup I would use with a live movie API. A check is only as honest as the thing it measures.
A Playwright test drives the grid, records frame times, writes them to a file, and asserts:
const FPS_TARGET = 65
const LONG_TASK_MAX_MS = 50
// ...
expect(metrics.avgFps).toBeGreaterThanOrEqual(FPS_TARGET)
expect(metrics.longestTaskMs).toBeLessThanOrEqual(LONG_TASK_MAX_MS)
That test file is the definition of "smooth" for this project. It is the gate, and everything else works toward it.
The loop itself
The driver is a small bash script. This is its core:
for i in $(seq 1 "$MAX_ITERS"); do # cap = 12
if npm run gate; then
echo "PASS — gate green. Loop 1 complete."; exit 0
fi
# gate failed, so ask Claude for one optimization
claude -p "$PROMPT" --permission-mode acceptEdits
# ...safety checks... then commit and loop again
done
The prompt tells Claude to read the failing report, make one targeted optimization, and stop, and not to touch the test or the threshold. A prompt is only words though, so the script also enforces it. Before it commits each round, it checks whether any protected file changed, and if one did, it reverts everything and stops.
if protected_changed; then
echo "ABORT — protected gate files were modified. Reverting."
git checkout -- "${PROTECTED[@]}"; exit 2
fi
So if the agent tried to pass by lowering the target, the loop would undo it before the change ever counted.
The failure
I set FPS_TARGET = 65 and ran it. Here is iteration 1:
FAIL — { "avgFps": 60.1, "longestTaskMs": 0, "longTasksOver50": 0 }
The grid was already doing about 60 FPS with zero long tasks. It only "failed" because 60.1 is below 65.
Claude made a real optimization anyway. It moved a shimmer animation off the main thread and onto the GPU, which is a good change. Iteration 2:
FAIL — { "avgFps": 60.2, "longestTaskMs": 0 }
60.1 became 60.2, and it was never going to climb much higher. The reason is the display, not the code. The browser measures frames against the screen's refresh rate, which is around 60Hz, so measured FPS cannot go past the monitor's ceiling no matter how good the code gets.
The target was the problem, not the code. I had asked for a number the hardware could not produce. The loop could not reach it, and instead of faking the result, it just kept failing, because the protected-files check stopped it from touching the test.
Fixing it was my job, not the agent's, since only a person changes the target. I lowered it to something reachable:
const FPS_TARGET = 58 // stay near 60, don't drop frames
I ran it again and it passed on the first gate.
There is a subtler point here too. On a 60Hz display there is no FPS target that is both currently failing and actually reachable, because you are already sitting at the ceiling. If you wanted a loop that improves its way to green over several rounds, FPS is the wrong thing to measure. You would measure something like p95 frame time or the number of dropped frames instead. Picking the right number to chase is part of the work.
What I took from Part 1
A goal loop is only as good as the number you give it. The run against 65 looked like a failure, but it was useful, because it showed the loop would rather keep failing than fake a pass. That is what you want to see before you trust it with anything.
In Part 2 the check changes. Loop 1 measured a number, but a lot of what we care about is not a number. "Does this animation feel right?" has no assert. So I hand the judgment to a second, separate agent, and it turns the first one down three times before it agrees to a pass.
The full project, with the loop drivers, gates, and 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…
If you would do any of this differently, I would like to hear it. What is the one thing you would have your own loop check?
Top comments (0)