Spec-Driven, Part 10: Review the Batch, Not the Diff

aicode-reviewtoolingworkflow
June 7, 2026·9 min read
Part 10 of Spec-Driven →

Fourteen stories finished overnight is fourteen diffs waiting for me in the morning. The first time that happened I did what everyone does: opened the branch, scrolled the combined diff, and nodded at anything that compiled and looked tidy.

That's not a review. That's reviewing agent-written code as if a person had written it in one sitting, and a loop doesn't write like that. It writes in small, separate, confident pieces, and the way you review them has to match.

This is Part 10 of the series. Earlier parts covered the spec and the gates around each story. This one is about the human at the end, and what I changed about how I review once the volume went up.

The diff is the wrong unit

A combined diff of a whole run shows you the end state. It hides the order things happened in, and the order is where the information is.

A story that adds a field, a later story that renames it and a third that quietly works around the rename all look fine in the final diff. Read in sequence, as three commits, they're a small story about an agent changing its mind. That story is exactly what you want to see.

So the first change is mechanical. The loop commits locally, one commit per story, and I review in that order, in dependency order, one commit at a time. The commit is the unit. I read the message first, because a commit message is a claim: "adds the speed control to each tile" tells me what to go and check.

A commit message you can check

The weak version of a story commit is the one agents write by default: "Improve tile layout and controls". It's true of almost any change to a UI, so it tells me nothing to check.

The better version names the criterion it closes: "Add per-tile speed control; value shown in whole pounds and pence; control disabled while a transaction is running". Now I have three things to go and look at, and a way to tell whether the story did what it said or something nearby.

I put that in the story's rules, so the agent writes it every time. The instruction is one line: the commit message states which acceptance criteria it satisfies. It costs the loop nothing, and it turns the log into a checklist I can review against.

Three passes, and the diff comes last

Here's the weak version, the one I used to run. Open the PR, read the files top to bottom, comment on naming, approve.

The better version has three passes, and the diff is the last of them.

Pass one: the board. Before I read a line of code, I check that every story is closed and every story's spec update was actually made. This sounds like admin. It caught me out on the 27-story run, where nine stories shipped with no written spec at all, because I hadn't made "update the spec" part of each story's criteria. I only found out when I went looking. If the record is missing, the review is already compromised, because I'm about to judge code against a document that doesn't describe it.

Pass two: the criteria. For each story I take its acceptance criteria and its failure-mode criterion and check them by hand, against something the agent didn't write. Where a story deals with values, I check them against the backend's real units, not against the tests. Part 7 is the long version of why: the suite was green while an earlier story had the wrong divisor, because the tests checked what the code did, not whether the pounds and pence were right.

Here's what that looks like in practice. The story's criterion says the cash figure on a tile is shown in pounds and pence. I don't ask whether the formatting function looks sensible. I find where the value comes from, check which unit the backend sends it in, and put one known amount through by hand. If the story handles money, the expected values were written into the criteria before the agent started, from the contract, so I'm comparing against a number the agent never touched.

This pass is slow and unglamorous, and it's the one that would have caught the wrong divisors on the 27-story run before the later stories had to fix them.

Pass three: the diff. Only now do I read the code, and by then I'm reading it with a question in mind: does this do what the story said, and nothing else? Scope creep is easy to spot when you know the story's boundary. It's invisible when you're just reading a diff.

Why the in-loop review isn't enough

Each story in my loop already has a code-review step, and the story can't close until its critical and high findings are resolved. It earns its place. It keeps findings from piling up into one enormous pull request.

But it reviews the code against good practice. It can't review it against a contract it hasn't been shown, and it can't tell me the story was the wrong story. A model reviewing a diff sees an amount divided by a number and a nicely formatted result. Nothing about that line is wrong unless you know what unit the backend sends.

The human pass is the one place where someone holds the spec, the contract and the intent at once. That's the job I'm keeping.

The findings that matter aren't about the code

What my own review is for isn't what I expected. Very little of it is catching bugs in the usual sense. The compiler, the linter, the tests and the in-loop review had already taken most of those.

What's left falls into three groups, and these are the shapes to look for.

A story that did something nearby. The criterion says "disable the control while a transaction is running". The agent disables it, and also changes the colour of the whole tile, because that seemed sensible. Nothing breaks. But the spec didn't ask for it, nobody decided it, and the next story now builds on a choice no human made.

A story that was right but in the wrong place. The logic works, and it sits in the view because that was the nearest file. Everything is green. In six months someone needs it in a second view and copies it.

A story that quietly depended on a later one. It passes only because of a helper added two commits on. Read in order, that's obvious. In a combined diff it's invisible, which is the strongest argument I know for reviewing by commit.

The comment I leave has changed too. The weak version is "this should be a service". The better version cites the story: "Story 12 says only the speed control changes. The tile colour change isn't in the criteria. Either add it to the spec or take it out." It goes back into the spec, not just into the code, and that's what keeps the next run honest.

What goes in the pull request

The pull request description used to be a summary of the feature. Now it's the list of stories, in order, and against each one a short note of what I checked by hand and what I only read.

That does two things. It tells the human reviewer, who may well be someone else on the team, where the cold spots are, so they can put their attention there instead of rediscovering them. And it keeps me honest. A line that says "read only" next to a story that touches money is an uncomfortable thing to leave in a description, and usually enough to make me go back and check it properly.

Keep the batch small enough to hold in your head

I mentioned in Part 3 that I'd started capping runs at five or six stories. That cap is a review decision more than a loop decision.

Past a handful of stories I stop being able to hold the spec in my head while I read, and the passes above turn into skimming. A run that ends at a natural boundary, with the whole branch building locally, is a run I can review properly. A run that goes on because the loop could is one I'll review badly.

None of this skips the normal route. Nothing is pushed until the whole branch builds. It then goes up as an ordinary pull request, with human review and CI, like any other change. The loop gets no shortcut past review because it wrote a lot of code quickly.

Where it falls over

It's slower than the loop makes it feel. The writing took hours and the review still takes a proper sitting, and I haven't timed the whole thing against building it by hand, so I can't tell you it's a net win on every feature.

It also depends on discipline I don't always have. The board pass only works if the stories were written well in the first place. If the criteria are vague, pass two collapses into "looks right to me", which is the thing I was trying to leave behind.

And it can't review what was never recorded. Those nine stories with no spec had to be reconstructed afterwards from commit messages. That worked, but it's the next post for a reason: a reconstructed record is a poor substitute for one written as you go.

Review is where your leverage went

My position: the loop moves your leverage to the two ends, the spec at the front and the review at the back. If you spend all your care on the first and wave the second through, you've built a fast way to ship confident mistakes.

On the next batch you run, review it story by story in order. Read the commit messages as claims, check the board before the code, check the values against the real contract, and read the diff last. Cap the run so you can actually do that.

Part 11 covers what to do when a loop outruns its plan and the spec has to be written back from what was built. The earlier parts are on the blog if you're catching up.