Handmade by Devvrat. All rights reserved.
Agents finish a feature in one sitting. You open one pull request. Reviewers get a wall of code. They skim. The PR sits.
GitHub stacked PRs split that wall into a chain. Same repository. Each PR targets the branch below it. Reviewers see one layer.
┌── feat/frontend → PR #3 (base: feat/api-endpoints) ← top
┌── feat/api-endpoints → PR #2 (base: feat/auth-layer)
┌── feat/auth-layer → PR #1 (base: main) ← bottom
main
Bottom PR targets main. Each PR above targets the branch below. Schema first. API next. UI last.
If A depends on B, B lives in the same layer or a lower one. New branch when the concern changes, or when the current layer is already big enough to review.
Public preview. Same repository only. No forks.
gh stack init feat/auth-layer
git add . && git commit -m "Add auth data model"
gh stack add feat/api-endpoints
git add . && git commit -m "Add auth API routes"
gh stack submit
First PR targets main. The next one targets feat/auth-layer. Each diff stays small.
Do not git rebase or git push --force stack branches. Use gh stack rebase and gh stack push.
Stacks merge bottom-up. Use gh stack merge. Never gh pr merge. Never the green Merge button.
gh stack merge 35 --yes --squash # remaining stack up through #35
gh stack merge 23 --yes --squash # #23 and every unmerged PR below it
main.You cannot merge a middle PR on its own.
After a partial merge, restack before you look at the next PR:
sync fetches trunk, skips the squash-merged layer, rebases the rest onto main with --onto, and force-with-lease pushes. Skip it and the next PR shows CONFLICTING.
You squash-merge the bottom PR with a normal merge. GitHub writes a new commit on main. The original layer SHA is gone. GitHub retargets the next PR onto main. That PR still contains the original commits.
Git reports "added in both" on files both layers touched. Same intent, different SHAs. Not a real conflict.
Do not resolve that in the GitHub UI. You replay the merged layer. Run gh stack sync --prune.
gh stack merge is the operation that drops the squashed commits and rebases the next PR onto trunk. A normal merge only retargets the base.
Keep the rules a multi-user trunk needs: required reviews, required status checks, linear history, conversation resolution, no force-push, no deleting the default branch. Those apply to every layer in the stack, not only the bottom PR. gh stack merge evaluates them when the merge runs. It cannot bypass them. Auto-merge is unsupported. Merge queues are: the stack enqueues in order.
Lock branch (classic protection) and a ruleset Restrict updates rule freeze the ref. A single PR squash can still land. gh stack merge writes trunk through the async stack-merge API and fails with Cannot change this locked branch. A repo admin hits this too. Stack merge has no admin override.
Use Lock for a release freeze or an incident. Lift it for the merge window, or wait. Do not squash layers with the green button as a workaround; you get the squash-replay conflict above.
Agents generate a lot of code. A stack gives each change a place to go. One concern, one PR. Merge with gh stack merge. After a partial merge, gh stack sync --prune before the next layer.