| name | stacked-prs |
| description | Stacked PR workflow with vanilla Git โ creating and managing PR stacks, parallel stacks with git worktrees, propagating fixes up a stack, and safe merging bottom-to-top |
Why Stacked PRs
- Keeps PRs small (150-400 lines target, 800 hard limit).
- Each branch = one reviewable unit, merged bottom-up.
- Uses only vanilla Git +
gh CLI โ no external stacking tools.
- Preserves atomic commits; squash-merge cleans history at merge time.
Before Starting Any New Stack (Mandatory)
Always sync with origin before creating branches for a new issue or stack.
Each line is a separate Bash tool call โ never chain with &&:
git fetch origin
git checkout main
git reset --hard origin/main
Never branch from a stale local main. Work started from an outdated base
produces unnecessary merge conflicts and may duplicate work already merged.
This applies to every new stack, including parallel stacks in new worktrees.
Single Stack: Creating and Working
Each line is a separate Bash tool call โ never chain with &&:
git checkout -b feat/X-models origin/main
git add src/models/base.py
git commit -m "refactor(models): extract base class"
git add src/models/validation.py
git commit -m "feat(models): add field validation"
git push -u origin feat/X-models
gh pr create --base main --fill
git checkout -b feat/X-api feat/X-models
git add src/api/create.py
git commit -m "feat(api): add create endpoint"
git add tests/test_api.py
git commit -m "test(api): add endpoint tests"
git push -u origin feat/X-api
gh pr create --base feat/X-models --fill
Addressing Review Comments Within a Stack โ Push-Minimizing Strategy
Push is expensive. Every push triggers CI runs and bot reviews (Claude, Gemini).
Pushing intermediate work wastes CI minutes and generates review noise on code you
know is incomplete. The rule: batch all local work, push once per PR, bottom-up.
Setup: One Worktree per Branch in the Stack
Before starting review resolution, set up a worktree for each branch in the
stack. This eliminates all git checkout calls โ each branch has its own
working directory, ready to go.
Each line is a separate Bash tool call:
git worktree add ../project-P2 feat/X-api
git worktree add ../project-P3 feat/X-tests
Now you have:
~/project/ โ feat/X-models (P1)
~/project-P2/ โ feat/X-api (P2)
~/project-P3/ โ feat/X-tests (P3)
No branch switching needed for the rest of the workflow. Use absolute paths
or git -C for all operations.
Workflow: Resolve One PR at a Time, Bottom-Up
Given a stack P1 โ P2 โ P3, each in its own worktree:
Phase 1: Fully resolve P1 locally before pushing
Each line is a separate Bash tool call:
git -C ~/project add src/models/base.py
git -C ~/project commit -m "fix(models): rename field per review"
git -C ~/project add src/models/validation.py
git -C ~/project commit -m "fix(models): add input check per review"
While working on P1, you MAY rebase dependent branches locally to keep them
current, but do not push them:
git -C ~/project-P2 merge feat/X-models
Phase 2: Push P1 once, wait for green
git -C ~/project push
Now wait for CI + bot reviews. If new comments come back, repeat Phase 1-2 on P1.
Do not start pushing P2 until P1 is either merged or fully green with approvals.
Phase 3: After P1 is merged (or green), prepare P2
Before pushing P2, do ALL of the following in P2's worktree:
- Rebase P2 onto updated main (or merged P1)
- Address ALL existing review comments on P2
- Run tests/lints locally to verify
Each line is a separate Bash tool call:
git -C ~/project-P2 fetch origin main
git -C ~/project-P2 rebase origin/main
git -C ~/project-P2 add src/api/create.py
git -C ~/project-P2 commit -m "fix(api): address review feedback"
git -C ~/project-P2 push --force-with-lease
Then retarget the PR base:
gh pr edit <PR2-number> --base main
Phase 4: Repeat for P3
Same pattern in P3's worktree: wait for P2 to go green, then rebase + fix
reviews + verify + push once + retarget base.
Cleanup After Stack is Merged
git worktree remove ~/project-P2
git worktree remove ~/project-P3
git worktree prune
Why This Matters
| Naive approach (push every fix) | Push-minimizing approach |
|---|
| 3 fixes on P1 = 3 CI runs + 3 bot reviews | 3 fixes on P1 = 1 CI run + 1 bot review |
| Rebase P2 after each P1 fix = wasted CI | Rebase P2 locally, push once when ready |
| Bot reviews intermediate code = noise | Bot reviews final code = actionable feedback |
Rules
- Never push dependent PRs before their turn. P2 stays local until P1 is resolved.
- Never rebase a branch that has an open PR (except after its parent is squash-merged, or as part of the single push-with-rebase in Phase 3).
- Never
git push --force on branches with open PRs โ use --force-with-lease only after parent squash-merge or when combining rebase + fixes in a single push.
- Never amend commits on branches with open PRs โ always new commits.
Merging a Stack (Bottom-Up) โ CRITICAL Pattern
gh pr edit <PR2-number> --base main
gh pr merge <PR1-number> --squash
git fetch origin main
git checkout feat/X-api
git rebase origin/main
git push --force-with-lease
Danger Zone
- NEVER use
gh pr merge --admin without explicit user approval. If a merge is blocked, investigate why and fix the root cause โ don't bypass branch protection.
- NEVER use
--delete-branch during stack merges. Branch deletion before retarget auto-closes dependent PRs (lost PR #72 โ recreated as #77). Clean up branches after the entire stack is merged.
- CI not triggering? Do NOT close/reopen โ this desynchronizes third-party GitHub App webhooks (Codecov, CI bots) and can permanently break required status checks. Try a no-op push (
git commit --allow-empty -m "chore: trigger CI" + git push). If CI still doesn't trigger, escalate to the user.
Parallel Stacks with git worktrees
When working on multiple independent stacks simultaneously โ for example when a
planner spawns parallel work streams โ use one git worktree per stack. This
avoids stashing, branch-switching overhead, and accidental cross-stack edits.
Setup: One Worktree per Stack
Each line is a separate Bash tool call:
git fetch origin
git checkout main
git reset --hard origin/main
git worktree add ../project-feat-Y -b feat/Y-schema origin/main
git worktree add ../project-feat-Z feat/Z-pipeline
Each worktree has its own working tree and index. You can build, test, and commit
in each independently. They share the same .git object store and refs.
Naming Convention
Keep worktree directory names and branch names in sync:
../project-feat-X/ โ feat/X-* branches
../project-feat-Y/ โ feat/Y-* branches
../project-feat-Z/ โ feat/Z-* branches
This makes it unambiguous which worktree owns which stack.
Working Across Parallel Stacks
Use absolute paths or git -C โ cd does not persist between Bash calls:
git -C /home/user/project-feat-X checkout feat/X-api
git -C /home/user/project-feat-Y checkout feat/Y-schema
git -C /home/user/project-feat-X fetch origin main
git -C /home/user/project-feat-X merge origin/main
git -C /home/user/project-feat-Y fetch origin main
git -C /home/user/project-feat-Y merge origin/main
Conflict Avoidance Between Parallel Stacks
Before starting parallel stacks, identify shared files. If two stacks will both
modify the same module:
- Prefer sequencing โ make one stack depend on the other (stack them)
- Partition the work โ one stack takes the interface, the other the implementation
- If truly independent โ proceed with parallel worktrees; resolve conflicts when
the second stack merges to main after the first
Never let two parallel worktrees edit the same file with the intent to merge both
to main independently โ this guarantees a conflict.
Cleaning Up Worktrees After Merge
After a stack is fully merged (all PRs squash-merged to main):
git worktree remove ../project-feat-Y
git worktree prune
git push origin --delete feat/Y-schema feat/Y-api
Worktree Status Overview
git worktree list
/home/user/project abc1234 [feat/X-api]
/home/user/project-feat-Y def5678 [feat/Y-schema]
/home/user/project-feat-Z ghi9012 [feat/Z-pipeline]
Stack Topology Reference
main
โโโ feat/X-models PR #10 (base: main)
โ โโโ feat/X-api PR #11 (base: feat/X-models)
โโโ feat/Y-schema PR #20 (base: main) โ separate worktree
โโโ feat/Y-api PR #21 (base: feat/Y-schema)
Merge order: #10 โ retarget #11 โ merge #10 โ rebase #11 โ merge #11.
Stack Y proceeds independently and merges after stack X lands.