PR Stack Workflow
Use this for any change too large to review as one pull request: deciding
whether to stack at all, where to cut the slices, the mechanics that keep a
stack reviewable, and how to verify each branch instead of only the tip.
Branch names, commit and PR-title conventions, and how to handle bot review
threads live in git-workflow. Read that first; this skill does not restate it.
Decide whether it needs a stack
A stack costs real time, because every landing forces a base move plus a merge
down the rest of the chain. Pay that only when one of these is true.
| Stack it |
Ship it as one PR |
| You cannot describe the diff in one sentence without the word "and" |
One symptom, one cause |
| It changes a default, or a shape users already have persisted, so it needs a migration decision |
The behavior is new and nobody has state in it |
| Someone will ask "why" and the answer is evidence: numbers, screenshots, a competitor's UI |
The bug report is the justification |
| It touches a shared component other surfaces consume |
The blast radius is one screen |
| Verification is a click path, not an assertion |
An existing test proves it |
Two consequences:
- If the fix is one commit, there is no plan to make. Do not manufacture slices.
- If you cannot yet write the acceptance check, you are researching, not
planning. Do the research, publish it, then plan.
Keep the stack short. Every landing costs another round of retarget-and-merge,
so length is a running cost you pay until the last PR is in.
Slice by what a reviewer has to hold in their head
Not by file, not by directory, not by frontend versus backend. A slice is right
when a reviewer can state what it does, and check it, without loading any other
slice. Practical shapes, in the order they should land:
- The bugs first. Anything true on
main regardless of the wider change.
It is mergeable on day one, it carries no judgment, and landing it early
shrinks everything downstream.
- Defaults and data shape next. A default-visible column, a one-time
migration, "stop rendering an empty string". Cheap diffs with expensive
consequences deserve their own review.
- One screen per slice. A whole component replaced is one reviewer concern
even at 30 files.
- Housekeeping in a bag. Several small independent removals nobody will
argue about.
- One question per slice. "Which run do we compare against?" is one slice:
the query, the picker, the auto-selection, and the URL state together.
- The dense one, alone. There is always one genuinely hard slice. Give it
nothing else to carry.
- New capabilities last but one. A new layout or filter reads as a feature
only once the surface under it has settled.
- Instrumentation last. It wires the seams the earlier slices create. Put
it first and you instrument code that is about to change.
This repo's own agent-setup work is a worked example: #16390, #16391 and #16392
shipped a project-structure path guard as three PRs, landed in that order — the
measured baseline first, then the rules the guard's messages cite, then the hook
that enforces them. The later two are based on the first's branch, not on main,
and #16392's body opens by naming the merge order.
What each PR must say
State the position and the dependency in the body, so a reviewer knows what they
may ignore:
2/8, based on #16913. Base retargets to main when #16913 lands.
- What is deliberately not in this slice, and which slice took it. This is
the line that stops an implementer helpfully widening the scope and breaking
the union check below.
- Any unmerged upstream change that would silently break this slice. A change
that makes two mechanisms read identical data makes them go quiet rather
than visibly wrong, and no test catches that.
Open the whole stack as reviewable, not drafts.
Five rules that cost us
- A fix-up commit lands in the PR that contains the thing it fixes, never as
a follow-up PR later in the stack. Otherwise the earlier PR ships a state you
already know is broken, and it is the one that merges first. When you slice an
existing branch, fold each fix-up back into its parent commit's slice.
- Propagate downward by merging parent into child. Never rebase or force-push
a stack that is under review. Review bots hang their threads off commit
shas, so a force-push discards review you already paid for. These PRs are
squash-merged, so merge commits in a child cost nothing in
main's history.
(The one documented force-push in CONTRIBUTING.md is the CLA author-header
fix on a single PR — not a stack under review.)
- Every PR must stand alone. Lint, typecheck and tests green on its own
branch, not only on the stack tip. A slice that compiles only with its
successor is not a slice: merge it into its neighbor.
- Every landing costs a retarget. When the parent merges, its immediate
child becomes the new bottom: that one PR's base moves to
main, main is
merged into it, and the merge is then propagated down the rest of the chain.
Every landing, until the stack is empty. Plan for that cost instead of
meeting it on merge day.
- A branch that has already merged
main cannot be split by cherry-picking
its commits. The reconciled state exists only in the merged tree: upstream
took some of your changes, replaced others, reverted a few. Slice the final
diff instead, then prove the union.
Prove the union
After slicing a branch, git diff <stack-tip> <original-branch> must be empty,
or every remaining difference must be a deliberate, named decision.
Do not skip this because the slices look right. On an eight-PR split the union
delta came to three files and one added, three removed lines — and that delta was
the finding: the original branch had dropped three ESLint disable pragmas and was
carrying a latent lint failure toward main.
Keep the original branch untouched as the arbiter. When a slice's behavior is in
doubt, run both and compare. Never edit the arbiter to match a slice.
The commands for slicing a merged branch and proving the union are in
references/stack-commands.md, together with the
git-to-PR walk for recovering why a surface is the way it is before you slice it.
Verify per branch, not per stack
Run the verification bar from .agents/AGENTS.md on each branch, in a
worktree checked out on that branch, and publish the result as a table of branch
by check. That file owns the commands and the checks that pass without executing;
follow it there rather than from memory. Three failure modes belong to stacking
specifically:
- Worktrees share one turbo cache. Turbo says so on every run
(
using shared worktree cache), which is exactly why a green lint on slice 4
can be a replay of slice 3's result. Force execution when you switch branches.
- Slicing is what provokes the unused-export gate. Split a feature and you
routinely produce an export whose only consumer lives in a later slice: dead
code on this branch, fine on the tip, and
knip is a required check. The fix
is to move the export into the slice that uses it, not to widen an ignore list.
- Somebody has to load the screen, per slice. Green checks and bot reviews
have between them approved a branch that rendered a blank page, and each slice
has its own branch and its own preview — so a look at the tip proves nothing
about slice 3. Who looks is a judgement call
.agents/AGENTS.md covers: drive
it yourself where the outcome is uncertain, hand over the preview URL where it
is not.
1---2name: pr-stack-workflow3description: Split a large change into a chained stack of small, independently reviewable pull requests, and verify each branch on its own. Use when a change is too large for one PR, when splitting a long-lived branch into PRs, when deciding whether work needs a planning stage at all, when landing or propagating a fix through a stack, or when every check is green on a stack and the change is still wrong.4---5
6# PR Stack Workflow
7
8Use this for any change too large to review as one pull request: deciding
9whether to stack at all, where to cut the slices, the mechanics that keep a
10stack reviewable, and how to verify each branch instead of only the tip.
11
12Branch names, commit and PR-title conventions, and how to handle bot review
13threads live in `git-workflow`. Read that first; this skill does not restate it.
14
15## Decide whether it needs a stack
16
17A stack costs real time, because every landing forces a base move plus a merge
18down the rest of the chain. Pay that only when one of these is true.
19
20| Stack it | Ship it as one PR |
21| --- | --- |
22| You cannot describe the diff in one sentence without the word "and" | One symptom, one cause |
23| It changes a default, or a shape users already have persisted, so it needs a migration decision | The behavior is new and nobody has state in it |
24| Someone will ask "why" and the answer is evidence: numbers, screenshots, a competitor's UI | The bug report is the justification |
25| It touches a shared component other surfaces consume | The blast radius is one screen |
26| Verification is a click path, not an assertion | An existing test proves it |
27
28Two consequences:
29
30- If the fix is one commit, there is no plan to make. Do not manufacture slices.
31- If you cannot yet write the acceptance check, you are researching, not
32 planning. Do the research, publish it, then plan.
33
34Keep the stack short. Every landing costs another round of retarget-and-merge,
35so length is a running cost you pay until the last PR is in.
36
37## Slice by what a reviewer has to hold in their head
38
39Not by file, not by directory, not by frontend versus backend. A slice is right
40when a reviewer can state what it does, and check it, without loading any other
41slice. Practical shapes, in the order they should land:
42
431. **The bugs first.** Anything true on `main` regardless of the wider change.
44 It is mergeable on day one, it carries no judgment, and landing it early
45 shrinks everything downstream.
462. **Defaults and data shape next.** A default-visible column, a one-time
47 migration, "stop rendering an empty string". Cheap diffs with expensive
48 consequences deserve their own review.
493. **One screen per slice.** A whole component replaced is one reviewer concern
50 even at 30 files.
514. **Housekeeping in a bag.** Several small independent removals nobody will
52 argue about.
535. **One question per slice.** "Which run do we compare against?" is one slice:
54 the query, the picker, the auto-selection, and the URL state together.
556. **The dense one, alone.** There is always one genuinely hard slice. Give it
56 nothing else to carry.
577. **New capabilities last but one.** A new layout or filter reads as a feature
58 only once the surface under it has settled.
598. **Instrumentation last.** It wires the seams the earlier slices create. Put
60 it first and you instrument code that is about to change.
61
62This repo's own agent-setup work is a worked example: #16390, #16391 and #16392
63shipped a project-structure path guard as three PRs, landed in that order — the
64measured baseline first, then the rules the guard's messages cite, then the hook
65that enforces them. The later two are based on the first's branch, not on `main`,
66and #16392's body opens by naming the merge order.
67
68## What each PR must say
69
70State the position and the dependency in the body, so a reviewer knows what they
71may ignore:
72
73- `2/8, based on #16913. Base retargets to main when #16913 lands.`
74- What is deliberately **not** in this slice, and which slice took it. This is
75 the line that stops an implementer helpfully widening the scope and breaking
76 the union check below.
77- Any unmerged upstream change that would silently break this slice. A change
78 that makes two mechanisms read identical data makes them go *quiet* rather
79 than visibly wrong, and no test catches that.
80
81Open the whole stack as reviewable, not drafts.
82
83## Five rules that cost us
84
851. **A fix-up commit lands in the PR that contains the thing it fixes**, never as
86 a follow-up PR later in the stack. Otherwise the earlier PR ships a state you
87 already know is broken, and it is the one that merges first. When you slice an
88 existing branch, fold each fix-up back into its parent commit's slice.
892. **Propagate downward by merging parent into child. Never rebase or force-push
90 a stack that is under review.** Review bots hang their threads off commit
91 shas, so a force-push discards review you already paid for. These PRs are
92 squash-merged, so merge commits in a child cost nothing in `main`'s history.
93 (The one documented force-push in `CONTRIBUTING.md` is the CLA author-header
94 fix on a single PR — not a stack under review.)
953. **Every PR must stand alone.** Lint, typecheck and tests green on its own
96 branch, not only on the stack tip. A slice that compiles only with its
97 successor is not a slice: merge it into its neighbor.
984. **Every landing costs a retarget.** When the parent merges, its immediate
99 child becomes the new bottom: that one PR's base moves to `main`, `main` is
100 merged into it, and the merge is then propagated down the rest of the chain.
101 Every landing, until the stack is empty. Plan for that cost instead of
102 meeting it on merge day.
1035. **A branch that has already merged `main` cannot be split by cherry-picking
104 its commits.** The reconciled state exists only in the merged tree: upstream
105 took some of your changes, replaced others, reverted a few. Slice the final
106 diff instead, then prove the union.
107
108## Prove the union
109
110After slicing a branch, `git diff <stack-tip> <original-branch>` must be empty,
111or every remaining difference must be a deliberate, named decision.
112
113Do not skip this because the slices look right. On an eight-PR split the union
114delta came to three files and one added, three removed lines — and that delta was
115the finding: the original branch had dropped three ESLint disable pragmas and was
116carrying a latent lint failure toward `main`.
117
118Keep the original branch untouched as the arbiter. When a slice's behavior is in
119doubt, run both and compare. Never edit the arbiter to match a slice.
120
121The commands for slicing a merged branch and proving the union are in
122[`references/stack-commands.md`](references/stack-commands.md), together with the
123git-to-PR walk for recovering why a surface is the way it is before you slice it.
124
125## Verify per branch, not per stack
126
127Run the verification bar from `.agents/AGENTS.md` on **each branch**, in a
128worktree checked out on that branch, and publish the result as a table of branch
129by check. That file owns the commands and the checks that pass without executing;
130follow it there rather than from memory. Three failure modes belong to stacking
131specifically:
132
133- **Worktrees share one turbo cache.** Turbo says so on every run
134 (`using shared worktree cache`), which is exactly why a green lint on slice 4
135 can be a replay of slice 3's result. Force execution when you switch branches.
136- **Slicing is what provokes the unused-export gate.** Split a feature and you
137 routinely produce an export whose only consumer lives in a later slice: dead
138 code on this branch, fine on the tip, and `knip` is a required check. The fix
139 is to move the export into the slice that uses it, not to widen an ignore list.
140- **Somebody has to load the screen, per slice.** Green checks and bot reviews
141 have between them approved a branch that rendered a blank page, and each slice
142 has its own branch and its own preview — so a look at the tip proves nothing
143 about slice 3. Who looks is a judgement call `.agents/AGENTS.md` covers: drive
144 it yourself where the outcome is uncertain, hand over the preview URL where it
145 is not.