The PRs didn't get small. They got separable.
One scaffold commit: four app directories with almost nothing in them, a lockfile, and a CI workflow that did very little. Eight days later the same repository was at v1.6.1 — nine release cuts, ninety-three merged pull requests, a monorepo with something running inside it. Somewhere in the middle of that week I stopped and told the agent that had written nearly all of it to change the way it worked. What I got wrong was how I checked whether it had listened.
How this gets built
I build most things this way now. An agent does the typing; I read the diffs, make the architecture calls, and decide what lands. Ninety-three merged pull requests across eight days is a shade under a dozen a day, and every one of them passed under my eyes before it went in.
The writing half of that loop stopped being the constraint a while ago. The constraint is review — how fast I can responsibly say yes to something. And review has a property that writing doesn’t: it degrades with size, and not gracefully. A reviewer reading a large diff doesn’t review it uniformly badly; they review the first part properly and the rest progressively less so, which is worse, because it feels like having read the whole thing.
The commit log below won’t mean much without a rough picture of the system, so: a browser front end, an Angular app rendered on the server, holds a live audio connection open while the person using it — the commit subjects call them the candidate — works through a coding problem. Their microphone streams to a speech-to-text pipeline; what they type goes into an editor on the page; and the problem itself comes from a bank of them, authored as files and seeded into the database. That is as much of it as any of this needs.
The diff that broke it
The fourth pull request of the week was 3,285 changed lines across 26 files. It added the problem-bank schema, a YAML content format, and an idempotent seeder, and I reviewed it exactly the way I’ve just described. I found three things worth acting on. One of them was that the content directory never made it into the Docker images, so the seeder had nothing to seed anywhere except my laptop.
That was the same class of bug as the PR I’d merged the night before — fix(docker): copy workspace packages into runtime stage for module resolution (#3) — something that works on the machine it was written on and is missing from the artifact that actually runs. The fix went in as #5 about four minutes after #4 merged, so review did catch it.
That is the part that bothered me. Review caught it, review had caught the same shape of thing the night before, and review was going to have to keep catching it, because nothing about a 3,285-line diff makes that class of mistake visible except a human reading carefully enough to notice an absence. The repo was under a day old at that point — a Bun workspaces monorepo, most of its app directories still placeholders, TypeScript throughout. If review was already the bottleneck on day one, it was going to be a wall by day eight.
So I told the agent to stop:
I have noticed your tendency to produce huge PRs. These are difficult to review and end up being error-prone. Going forward, no matter how large a problem I give you, you should Always break it down…
And then, because a correction that lives in one conversation dies with it, I opened my global CLAUDE.md and wrote the rule down. The timestamp on that file puts it 46 minutes after #4 merged. Here is what I wrote, in full:
Decompose every piece of work into the barest reviewable units, no matter how large the problem I hand you. Big PRs make review difficult and error-prone, and errors get through.
The seam to look for first: separate the mechanism from the behaviour change. Land the new plumbing, data, or instrumentation in a PR that reads nothing and changes nothing observable, then land the small PR that switches behaviour onto it. The risky diff ends up tiny and reviewable.
Hold onto that second paragraph. I did not.
The number I went looking for
A week later, with the repo at v1.6.1, I wanted to know whether the instruction had done anything. It had felt different to review, and a feeling is not evidence, so I measured the obvious thing. Insertions plus deletions per squashed commit on main, release-please commits excluded, split at the PR where I’d written the rule:
| PRs | Median churn | Mean churn | Over 3,000 lines | |
|---|---|---|---|---|
Scaffold through #4 | 5 | 1,120 | 1,682 | 2 |
#5 through #137 | 80 | 651 | 832 | 2 |
Median churn fell from 1,120 lines to 651. That’s a real improvement, and it is a long way short of what I’d been telling myself I got. A 651-line diff is not a small diff. Two PRs over 3,000 lines landed after the rule, exactly as many as before it — feat(orchestrator): transcribe a connected browser with Pipecat and Deepgram (#32) at 4,593 lines and feat(web): render an Angular SSR app configured from the server (#40) at 3,048. The largest PR of the entire week is on the wrong side of the line.
Five PRs is a thin baseline, and two of those five were the scaffold and the CI setup, so the comparison is doing less work than it looks like. Read strictly, the size data says the instruction half-worked. I was ready to file the whole thing under told the agent something, the agent agreed with me, nothing much changed, which is the most common outcome of telling an agent anything.
The axis I wasn’t measuring
Then I read the commit subjects in order, and the shape was sitting right there.
feat(web): add a capture-loss watcher, wired to nothing (#105)
fix(web): notice when the microphone stops being the candidate's mid-session (#106)
The first of those is 875 lines across two files, and it changes nothing. It adds a watcher that can detect a media track dying, with tests, connected to no part of the running application. The second, 1,482 lines, wires it into the running session so that when it happens, someone is actually told.
Neither of those is a small PR. But they ask completely different questions of a reviewer. Reviewing #105 asks: is this watcher correct on its own terms? Nothing observable changes, so nothing can regress — the worst case is dead code that ships and sits there. Reviewing #106 asks: is this the right moment to react, and is the reaction right? By then the mechanism is already on main, already reviewed, already under test. I never had to hold both questions in my head at once.
The same seam again, three deep:
feat(api): persist append-only code snapshots for a session (#108)
feat(web): give the candidate somewhere to write code (#109)
feat(web): persist what the candidate writes in the editor (#111)
Storage first, with nothing writing to it. Then an editor that writes nowhere. Then the one PR, at 1,618 lines, that connects them — the only one of the three where a mistake reaches a user. It isn’t a pattern I found by picking the two chains that flattered me, either: #63 → #69 → #70, and #93 → #94 → #99 → #100, run the same way.
That is the seam. It is also, word for word, the thing I had written into my own config file the night I wrote the instruction — land the new plumbing in a PR that reads nothing and changes nothing observable, then land the small PR that switches behaviour onto it. The agent did what I asked. I spent the following week grading it on diff size, which is a side effect of the rule rather than the rule itself, and nearly concluded it hadn’t worked.
Size is a lagging indicator of separability. It moves when you find a seam, because half the diff goes into a different PR — which is why the median dropped at all. It moves much less than you’d expect, because a mechanism landed on its own is still a whole mechanism, tests and all. If you grade the rule on the median you will underrate it, every time.
Where the seam doesn’t exist
#32 and #40 are the counter-example that makes the point rather than breaking it. There is no way to land half an Angular SSR application wired to nothing; the wiring is the change. Same for standing up a Pipecat pipeline that transcribes a live browser connection — until it’s connected end to end there is nothing to assert about it. Both are scaffolding, both had to arrive in one piece, and both are honestly hard to review. I read them slowly and I still don’t think I read them well.
A rule phrased as every PR under N lines would have forced a fake split there: three PRs that each pass CI and together mean nothing until the last one lands. That is worse than one big diff, because it looks like discipline. The seam has to exist in the problem. When it doesn’t, the right move is to admit the PR is large and review it differently — not to slice it into pieces that only make the size column look better.
What I’d tell myself
Eight days, empty repository to v1.6.1. That’s the headline the throughput data wants, and it’s the least interesting thing here. I shipped a lot is a measurement, not a finding.
The finding is smaller and more annoying: I wrote the right rule, in the right words, on the right night, and then spent a week checking whether it had worked by looking at a number it was never going to move much. The instruction wasn’t wrong and the agent didn’t ignore it. My verification was pointed at the wrong axis, and if the commit log hadn’t been sitting there in an order I could read, I’d have concluded the whole thing was noise and gone back to shipping 3,000-line pull requests.
Don’t ask for smaller pull requests. Ask for the mechanism and the behaviour change to land separately, then go and check whether they did — by reading what landed, not by measuring how much of it there was.