The unreviewable pull request
Let it grow, then have an agent carve it up
Seeded 2019 Written November 2026
It starts with a task that fits on a sticky note. Somewhere along the way I notice that the code I'm touching is a mess, and since I'm in there anyway, I clean it up. The cleanup exposes an edge case nobody handled, so I handle it. Handling it properly needs tests that don't exist, so I write them. The tests find two bugs. I fix them. Then I remember a feature that was part of the original ask and somehow fell off the plan, and I add that too.
Eventually I open a pull request with 400 files and 20,000 lines changed.
That's on a good day. On a bad day, I've also found a better name for one of the core concepts along the way. What everyone has been calling a "user" is really an "account", so I fix it, and a codebase-wide search and replace changes a line here and there in a thousand files. Add the generated code and the test snapshots that get regenerated because the word appears in them, and the PR isn't 20,000 lines any more. It's 400,000. Something microscopic did that. One word.
Every one of those lines is, as far as I can tell, an improvement. Together, they're unreviewable.
Nobody reads 20,000 lines, let alone 400,000. A reviewer handed a PR like that has two honest options, both bad: spend a week on it, or skim it, leave a comment about a variable name so it looks read, and approve. The research on this is old and the metric is crude, but it points the same way. In SmartBear's study of a team at Cisco, reviewers stopped finding much after 200 to 400 lines. My good-day PR was fifty of those, stapled together. The bad-day one was a thousand.
A manager I had years ago was hostile to PRs like that. Hard-core hostile. A monster didn't get reviewed. It got sent back to be split.
I didn't like it at the time. Naturally.
GitHub had no concept of a stack back then. A chain of PRs was a chain of branches, each based on the one before it, and every fix to the bottom one had to be rebased up through the rest by hand. The same went for every time the master branch moved under me: I had to propagate the merge up through every PR in the chain, again and again, until the reviewer deigned them worth his time. Skip a round, and the drift quickly became unmanageable. Not that it wasn't tough enough already.
All of that was tedious, and it was the smallest of my problems.
The big one was that the split can't be planned up front. With a large task, you only know exactly what you're doing in retrospect, after you've done it. En route, you have a broad idea, and a broad idea doesn't tell you where the seams are. So I'd finish the work, then pluck it apart, hunk by hunk, git add -p until my eyes glazed over. The job was to take a week's worth of exploratory work, with its detours, dead ends and second thoughts, and retrofit it into a perfect string of pearls: the refactor, then the tests, then the two bug fixes, then the feature. Each pearl in its place, lined up as if there had never been anything but 100% certainty about the path and the destination.
That was the horror.
It wasted a lot of time. I learned quickly that his way was better anyway.
What surprised me was that splitting didn't just make the review easier. It made the work better. Each PR in the chain was a milestone, and a milestone has to stand on its own: it builds, its tests pass, and its description says what it does without leaning on the next eleven. When a refactor is cut loose from the feature that motivated it, you notice the parts of the refactor that only made sense for a version of the feature that later changed shape. When the tests have to stand alone, you notice they're half-finished. Debug leftovers, dead code, the abandoned first attempt that's still half in there: a monster hides all of that, and a milestone doesn't. Every PR came out more correct, more maintainable, more beautiful. More all sorts of good things.
And it took load off the reviewer, which matters more than it sounds. In a review process that's actually worth something (not the rubber stamp practiced in most places), the reviewers are the more experienced engineers. They're overloaded, their time is limited, and what they know is some of the most valuable knowledge in the building. Handing them 20,000 lines wastes the scarcest thing you have.
He was right. It just took me a while to stop resenting the hours it cost.
Today, Opus generally has more knowledge, more time and more skill than those reviewers did. You'd think that makes the problem go away. It doesn't. Opus also does a better job on a focused PR than on a multi-hundred-file monstrosity. Its attention runs out later than a human's, but it runs out, and it runs out quietly. An overwhelmed human knows they're overwhelmed. An overwhelmed model writes a confident review of whichever parts it happened to look at closely.
So the rule stands. What changed is who pays for it.
I still let the task grow. I've stopped fighting that. Refactoring along the way, handling the edge cases, adding the feature everyone forgot: that's how I find out what the task actually was. Then, once I know what I did, I tell an agent to split it into a stack. It strings the pearls without complaint, and it has no trouble pretending we knew where we were going all along. Then I have an agent review each PR in the stack. Then an agent resolves the merge conflicts the review fixes cause, and merges the stack, bottom up.
GitHub now supports stacked pull requests as a first-class concept, in public preview since July. That helps, but the tooling was never the hard part. The hard part was the retrofitting, and that's exactly what I delegate.
The best side effect is the one I didn't expect. With the work cut into tractable bites, my own valuable time and inferior mind can provide a much better review of the agents' work. I can follow a 600-line PR that does one thing. I could never follow the monster, and I wrote it. Well, obviously I didn't write it. I asked Claude to.
The rest of this is the parts that took me a while to get right.
1. The monster is the specification
Don't delete the monster branch after the split. Until the stack is merged, it's the source of truth, and it makes the split cheap to check: the top of the stack must produce exactly the same tree as the monster. git diff monster stack-top should print nothing.
That one command turns "did the agent drop a hunk somewhere in 400 files?" from a question I'd have to read my way to into one a machine answers in a second. Divergence from the monster is allowed only on purpose, as review fixes made after the split, in their own commits.
2. Every layer builds, passes and ships
A stack isn't a pile of diffs cut at arbitrary points. Each layer must compile, pass its tests, and be safe to merge and deploy without the layers above it. CI runs on every layer, not just the top. That's what makes a layer reviewable on its own, and it's what lets the bottom of the stack land while the top is still being argued over.
If something can't ship half-done, it goes behind a flag, or it gets the parallel change treatment: expand, migrate, contract. That used to be the extra work everybody skipped. Agents don't skip it, and they don't sigh.
The payoff comes later. When something breaks in production three weeks on, git bisect lands on a PR that does one thing, and the revert takes out one thing, not a feature, a refactor and two unrelated bug fixes.
Not that anybody in their right mind reaches for git bisect unless they're truly desperate. Humans, that is. Agents don't care.
3. A refactor or a behavior change, never both
This is the most useful seam in any split. Kent Beck put it best: "make the change easy (warning: this may be hard), then make the easy change." The stack should read in that order.
A refactor PR changes no behavior, and proves it: the existing tests pass unchanged. If a refactor touches a test's assertions, it isn't a refactor. Mechanical changes (renames, moves, formatting, generated code) get their own PRs, because they're reviewed differently. Nobody should read a mechanical change line by line. You check how it was produced and that the tests still pass. That leaves the PR that changes behavior small, and that's where the real review goes.
4. A rename gets a PR of its own
The rename from the bad day is the extreme case, and the most instructive one. By line count, it's the biggest change in the monster. By content, it's the smallest: one word. Inside the monster, it's poison. The one line in file 612 that changes behavior sits between two lines that only changed a word, and nobody, human or model, reliably spots it in 400,000 lines of noise.
Pulled out on its own, the same thousand files make one of the easiest PRs there is to review, as long as nobody reads it. The PR says exactly how it was made: the search, the replacement, the command that regenerated the code and the snapshots. The reviewer (usually an agent) reruns all of that on the base branch and diffs the result against the PR. If the diff is empty, it's done. What's left is the interesting part: the places where the search got it wrong. A "user interface" is not an "account interface". Those hand-made exceptions go in a commit of their own, listed in the description, and they're the only part anyone reads line by line.
The rename goes at the bottom of the stack, so every layer above it is written and reviewed in the final vocabulary. The reviewer of layer 7 shouldn't have to translate. I found the name last. It goes first.
It merges first, too, and fast. A thousand-file PR conflicts with everything anyone else is working on, so every day it waits, it rots a little more, and so does every other branch in the repository.
The exception is a rename that crosses a boundary. A database column, a field in a public API, a key in serialized data, a setting in someone else's config: those are behavior changes wearing a rename's clothes. They get the parallel change treatment from section 2, and a real review.
Take the rename out, and the 400,000-line monster is a 20,000-line monster again. Still a monster, but one that can be carved.
5. Not everything belongs in the chain
A monster is several things that grew together, and not all of them depend on each other. The two bug fixes the new tests found don't need the feature. Usually they don't even need the tests. Those come out as independent PRs against main and merge first, today, where they're worth something.
A stack should be a chain only where there's a real dependency. Everywhere else it's a tree, or a few unrelated PRs. Every layer taken out of the chain is one less layer to restack when something below it changes, and one less thing waiting on the slowest review in the line.
6. One idea per PR, not N lines per PR
Line count is a proxy. Two to four hundred lines is a decent smell test, but a 380,000-line rename is easier to review than a 40-line change to a locking scheme. What I ask for is one idea per PR, a title that states it without an "and", and a description that says what the PR does, what it deliberately doesn't do, and where it sits in the stack. If the agent can't write the title without an "and", it's two PRs.
Too small is a failure too. A stack of sixty trivial PRs has lost the story just as thoroughly as one PR with sixty ideas in it. A reviewer should be able to read the stack's titles from bottom to top and see the string of pearls: the change, told in the order it should have been built, even though it wasn't. If they can't, the split is wrong, whatever the line counts say.
7. A fresh reviewer for every layer
Each layer gets its own review session, one that didn't write the code and didn't do the split. It gets the plan or design document, so it knows where the stack is headed, and it gets its own diff, which is what it judges.
"This gets fixed in PR 9" is not an acceptable answer in the review of PR 4. If PR 4 is only correct because of PR 9, the split is wrong, and that's a finding against the split, not a reason to approve.
8. Someone still has to look at the whole thing
This is what splitting costs. Twelve reviewers, each judging one slice, can each be right and still miss how slice 3 and slice 9 interact. Cross-cutting concerns (consistency, naming, error handling that's done three different ways in three layers) don't belong to any one layer.
So once the layers are reviewed, one more review goes over the whole stack against the design. That sounds like reviewing the monster after all, but it isn't. Every piece in it has already been vetted, so the reviewer only has to think about the joints. It's a smaller, sharper question, and both agents and humans answer it better.
9. Fixes flow up, merges flow down
A review fix in layer 3 conflicts with layers 4 through 12. That cascade was the smallest of my problems when I maintained stacks by hand, but it was still a problem, and every cascade cost an afternoon. Now restacking is the agent's job (with gh stack sync doing most of the rebasing), and the rule I give it is simple: fix the problem in the lowest layer where it exists. Never patch it at the top. A fix at the top leaves every layer in between lying about what it does.
Merges go bottom up, and a layer merges as soon as it's approved. Every day an approved layer sits unmerged is a day main moves under it, and that drift has to be propagated up through every layer above. It used to be my job. It's an agent's now, but the drift is no smaller for it.
10. Where my own attention goes
Not evenly. That's the point of having layers.
For mechanical layers, I check the evidence: the tree diff came back empty, the rename reproduces from its own command, the tests didn't change. Then I skim. For refactor layers, I check that the tests didn't change, and I read the parts the review flagged. The layer that changes behavior gets a real read, line by line, and so does anything that touches money, security or deleting data, however mechanical it claims to be.
I also read the reviews before the diffs, and I read the author agent's reasons for dismissing a finding more carefully than either. "This is intentional" is the most expensive sentence in the whole process when it isn't true.
These days I let my tasks grow into monsters without guilt, because I know what happens to them next. They get sent back to be split. Every single one.
I've turned into that manager. The difference is that my reports are agents, and they don't take it personally.