blog The constraint you forgot you agreed to
RSS
2026-08-03-the-constraint-you-forgot-you-agreed-to.md stacked-reviewsgithub

The constraint you forgot you agreed to

GitHub made the branch the unit of review, and that choice still sizes your work. Why it was only a choice, why agents made it expensive, and what changes when a review stops needing its own branch.

updated october 2026

I expanded this post in October 2026, after GitHub made stacked pull requests generally available.

A pull request is a request to merge one branch. GitHub represents it as a pair of pointers, a head branch and a base branch, and a review is the approval to merge the first into the second. The diff view, the comment threads, and the merge button are all built on that pair.

That coupling was a product decision. GitHub shipped pull requests in February 2008 for the open source workflow of the day: fork the repo, push a branch, ask the maintainer to pull it. The first version was little more than a message asking for that pull. In 2010, Pull Requests 2.0 rebuilt them as discussions around a diff, tied to the branch to be pulled and the branch it would be applied to. Nothing in git defines the unit a reviewer looks at, and a branch can hold one change or fifty. GitHub chose the branch, and the choice outlived the workflow it served.

The branch got promoted#

Because the PR merges a branch, the branch became the unit of review. And because nothing reaches main without review, it became the unit of work too. Teams size work to fit a branch. A refactor spotted along the way waits for a PR of its own, since folding it in would bury the feature under unrelated lines. Scope gets trimmed to keep a diff reviewable, and a feature that would naturally go out in five steps goes out as one large PR, or as five branches that someone has to keep in step. The branch became a shipping container with a weight limit, and work gets planned around the container. The convention has lasted long enough to pass for a law of version control, when it is only the design of one review tool.

Other review tools drew the unit smaller. The Linux kernel reviews a patch series one patch at a time, Gerrit reviews a single commit, and Phabricator reviewed diffs that could be stacked and reordered. When LLVM moved from Phabricator to GitHub pull requests in 2023, the project had to allow personal branches in its main repository so stacked reviews would show proper diffs, since GitHub needs a branch to point each review at. The branch-sized review was one tool's choice. While people wrote every line by hand its cost stayed manageable, because a person can only write so much code in a day.

Agents made the limit expensive#

In my own work, and from the engineers I talk to, a large pull request has gone from rare to normal. Essentially every PR I see now adds or changes hundreds or thousands of lines, often with a description an AI generated. I asked friends at small, medium, and large companies about it, and they all described the same thing, multi-thousand-line PRs that nobody was reviewing thoroughly.

The branch-sized PR is where that volume lands. There is no way to get feedback on part of a branch, because the PR shows every line on the branch or none of them. So when an agent's work runs long, the path of least resistance is to keep committing to the same branch, and the PR grows into a two-thousand-line review that will be skimmed rather than read, by default and without anyone deciding it.

The PR also shapes how the work gets written. An agent asked to build a feature will by default one-shot it and iterate until it seems to work, and a single-branch PR accepts that result, since the result only has to arrive as one diff. I think we have stopped asking which changes belong together and what order they should land in, at the moment the amount of code we change has grown the most. Splitting the work is the obvious response, and on GitHub that means more branches.

Stacking on top of branches#

Stacking means splitting a change into a sequence of small reviews, each built on the one below it, so they can be read separately and land in order. On GitHub that has meant one branch and one PR per piece, each based on the branch below. For most of the pull request's history GitHub gave the chain no support, and few teams sustained it. Feedback on a lower branch had to be threaded through every branch above it, by rebase and force-push or by merging each parent down into its child, and each PR's base had to be re-pointed as the pieces landed. The branches had no meaning as lines of development; they existed so GitHub had something to point each PR at. The split turns one line of work into branch administration.

GitHub made stacked pull requests generally available in October 2026, and they take on much of that administration. The gh stack extension creates and rebases the branches, a stack lands as one atomic operation, and a rebase that leaves a PR's diff unchanged keeps its approvals. However, the unit is the same as before. The gh-stack README describes a stack as "an ordered list of branches where each branch builds on the one below it," so a five-review stack is still five branches.

GitHub's stacks automate the branch administration, but they keep the branch as the unit of review. The branches still exist, so they can drift when someone pushes to one outside the tool, and anyone reading the repo outside the stack view sees five branches where there is one line of work, which makes the feature hard to read or search as a unit. It is also hard to have some people stack and others not in the same repo, when every stack adds a branch per review. The administration only goes away when a review doesn't need a branch at all.

A review without its own branch#

I am building GitStack to make it easier to ship high quality code, reviewed well, at the pace agents now write it. Removing the branch-per-review coupling is the center of that. In GitStack a review is a slice of commits on the branch where the work already lives. A manifest in the repo records which commits belong to which review, so the branch stays a branch and no refs exist for review's sake. The gs CLI is compatible with the git workflow you already have, and commits get grouped into reviews wherever the change divides naturally:

bash
gs switch -c search-filters

gs add internal/search/filter.go
gs commit -m "search: filter model"
gs review create -t "search: filter model"

gs add internal/api/search.go
gs commit -m "search: filter params on /api/search"
gs review create -t "search: filter params on /api/search"

That is two reviews on one branch, with no refs invented for the tool's benefit. gs stack prints the state:

> R2  wip    search: filter params on /api/search  ps1
  R1  ready  search: filter model                  ps1  LGTM 1
next: gs submit R1

R1 has an approval, so it can land now, while R2 is still under review and new commits continue on the same branch. When feedback arrives on a lower review after more work has accumulated above it, the fix goes into that review directly: check out the review, amend the commit, push, and everything above it rebases automatically. A review above it that was only rebased keeps its approval. Anyone who doesn't use gs sees an ordinary branch, through a stock git clone or through the GitHub mirror, where they can keep opening pull requests. The branch goes back to being a line of development, and the reviews are a layer on top of it.

What the slices change about review itself#

A review sized to fit in a reader's head gets read instead of skimmed. The title states what the slice is for, the diff is small enough to hold in mind, and the comments address the change rather than its size. Reviewers rubber-stamp diffs too large to hold, and a small slice removes the reason to.

Small slices can go to the right reviewer. A PR that touches the schema, the API, and the UI goes to whoever has time to read all three, and that person is rarely the best judge of all three. Split into slices, each change can be targeted at the reviewer who knows that code best. Each review in GitStack keeps its own reviewer list, so in the stack above, the filter model and the API change can go to different people:

bash
gs review reviewers R1 --add dana
gs review reviewers R2 --add sam

Agent reviewers get the same benefit in a stronger form, as context. An agent pointed at an enormous diff spends most of its attention reconstructing what the change is before it can judge whether the change is right. A titled slice supplies the intent along with a diff small enough to reason about. I expect the review it produces to be more accurate for the same model, though I don't have an eval that measures it yet.

Slices change how an agent writes, too. An agent that builds its work as a stack of reviews has to decide what each change is for and what order the changes land in, instead of one-shotting the feature and iterating until it seems to work.

Slices also let review run in parallel. Five reviews in a stack can be read by five reviewers, human or agent, at once, and the bottom of the stack lands the moment it is approved while the top is still under discussion. A single large PR serializes everything behind the slowest open question on any line of it.

What a slice costs#

Every review needs its own approval. In a repo that requires approval to land, a five-review stack asks for five approvals where one PR asked for one. Each approval covers a much smaller diff, which is the trade I would make, but reviewers approve more often. Those approvals don't all fall on one person, though, since each review in the stack can go to a different reviewer, and I expect agents to approve more of the low-risk reviews over time.

Choosing where to split takes judgment. GitStack records the slices you make, and it doesn't decide where they go. A stack divided in the wrong places, where one review only makes sense after reading the next, is harder to review than the PR it replaced.

Another server holds your code. GitStack becomes the system of record, and GitHub stays connected as a mirror that syncs both ways. Stock git clone works with no gs installed, so the code doesn't depend on GitStack staying around.

gs mirrors git's daily verbs and leaves the rest to git. gs add, gs commit, gs switch, and gs push mirror their git counterparts, and git stays available for stash, tag, bisect, and the rest. Committing or rebasing with raw git goes around the stack tracking, though, so the daily loop has to run through gs. That is also what gs skill install teaches a coding agent.

Try it on one branch#

GitHub made the branch the unit of review to fit the fork-and-pull workflow it was built around, and a choice like that can be made again. The kernel, Gerrit, and Phabricator each chose a smaller unit, and the cost of GitHub's choice grows with every line an agent writes. If your branches have outgrown your PRs, try working in slices for one branch and see how it reads. Install the CLI:

bash
curl -fsSL https://gitstack.sh/install.sh | sh

Or from Homebrew:

bash
brew install alethi-dev/tap/gs

The getting started guide walks from install to your first landed stack, and the launch post covers the rest of the product, including how the GitHub mirror keeps your CI and issues where they are.

You can find me on GitHub, LinkedIn, and Bluesky, and the blog has an RSS feed.