General
Shipping AI-Generated Features as Stacked PRs: A Complete Spec Kit + gh stack Tutorial
0 views

A step-by-step guide to pairing spec-driven development with stacked pull requests — from specify init all the way to a fully merged feature.
Why this tutorial exists
AI coding agents are fast. Dangerously fast. Point an agent at a feature request and ten minutes later you're staring at a pull request with 180 changed files. Nobody reviews that. It sits for days, goes stale, and gets merged with a LGTM and a prayer.
The fix is two tools working together:
Spec Kit (specify) gives the agent structure — a spec, a plan, a task list, and gates it must pass before writing code.
gh stack gives the output structure — instead of one monster PR, the agent ships a stack of small PRs (each ≤ 100 files), each reviewable in minutes.
This tutorial walks through the entire lifecycle with a real example. By the end you'll have:
✅ A working Spec Kit + gh stack setup
✅ A plan that is literally a stack layout
✅ An implement loop where gh stack only fires after unit tests pass
✅ Three open PRs stacked on each other
✅ A review/merge workflow that takes the stack from open → merged → cleaned up
What we're building
We're an e-commerce team. The business wants payments: charge cards, refund, webhooks. Estimated scope: ~135 files. That's one unreviewable PR — or a clean 3-PR stack:
main ── 001-payments ── 001-payments-u2 ── 001-payments-u3
PR #1 PR #2 PR #3
Domain (~25f) API (~40f) Webhooks+UI (~70f)
each ≤ 100 files — hard cap
One feature. One stack. Three small reviews. Let's go.
Part 0 — Prerequisites
Tool
Check
Notes
GitHub CLI ≥ 2.x
gh --version
Authenticated: gh auth login
Git
git --version
uv (for Spec Kit)
uv --version
Or use pipx/pip
An AI agent
claude --version
Works with codex, copilot, gemini, cursor-agent, etc.
Node/Python + test runner
project-specific
We use npm run test:unit below — swap in yours
Install the stack extension:
gh extension install hmarr/gh-stack
gh stack --help # should print submit / sync / checkout / view
Part 1 — Project setup
# Fresh project with Claude Code as the agent
uvx --from git+https://github.com/github/spec-kit.git specify init payments-service --ai claude
cd payments-service
# Existing repo instead?
# uvx --from git+https://github.com/github/spec-kit.git specify init --here --ai claude
Spec Kit drops its templates into .specify/ and configures your agent's command slash-commands (/speckit.*).
Now create the cap-enforcement gate — the script that makes the 100-file rule real rather than a suggestion the agent can ignore:
scripts/check-stack-cap.sh
#!/usr/bin/env bash
# Hard cap: max changed files per stack unit. Usage:
# scripts/check-stack-cap.sh <parent-branch> e.g. main, or the unit below
set -euo pipefail
CAP="${STACK_FILE_CAP:-100}"
PARENT="${1:?Usage: check-stack-cap.sh <parent-branch>}"
BASE="$(git merge-base "$PARENT" HEAD)"
COUNT="$(git diff --name-only "$BASE" HEAD | sed '/^$/d' | wc -l | tr -d ' ')"
echo "Files changed vs '$PARENT': $COUNT / $CAP"
if (( COUNT > CAP )); then
echo "❌ FILE CAP EXCEEDED — do NOT push or run gh stack. Split this unit."
exit 1
fi
echo "✅ Within cap — safe to push and submit."
chmod +x scripts/check-stack-cap.sh
git add scripts/ && git commit -m "chore: stack file-cap gate" && git push
🔑 Key idea: the gate runs before anything touches GitHub. If tests are red or the cap is blown, gh stack never runs. Bake this into your agent's rules in Part 2.
Part 2 — Teach the agent the contract
Run the constitution command and paste this addendum (it becomes project law the agent reads on every task):
/speckit.constitution
Add to the generated file:
## Delivery — Stacked PRs (non-negotiable)
1. One feature = one gh stack of units U1..Un, implemented strictly in order.
2. HARD CAP: each unit ≤ 100 changed files, verified by
scripts/check-stack-cap.sh <parent-branch>. Exceeding it ⇒ split the unit.
3. Unit sequence: implement → unit tests PASS → commit → cap check →
git push -u → gh stack submit → create next unit branch.
4. NEVER run gh stack with failing tests or a failed cap check.
5. U1 branch = the spec-kit feature branch (parent: main).
U(k) branch = <feature>-uk (parent: U(k-1)). No forward dependencies.
6. Merge bottom-up. Run gh stack sync after every merge.
That's the whole contract. Everything below is this loop, repeated.
Part 3 — /speckit.specify (and clarify)
/speckit.specify Build payments: card charge, refund, and webhook ingestion.
This branch is the bottom of a gh stack — spec docs ship in PR #1.
Spec Kit creates the branch and commits the spec:
⏺ Created branch 001-payments
⏺ Created specs/001-payments/spec.md
That branch — 001-payments — is now unit U1, the bottom of your stack. Add clarifications with /speckit.clarify if the spec has open questions, then commit any changes.
Part 4 — /speckit.plan: plan the feature as a stack
This is where the two tools fuse. Don't let the plan be prose — make it a stack layout:
/speckit.plan Use PostgreSQL + NestJS + Stripe SDK.
STACK CONSTRAINTS (mandatory):
- Organize implementation as stacked units U1..Un.
- U1 must build and test green alone; each U(k) depends only on U1..U(k-1).
- Each unit ≤ 100 changed files — estimate files per unit, split proactively.
- Add a "## Stack Layout" table: Unit | Branch | Purpose | Depends on | Est. files.
The agent writes back into plan.md:
## Stack Layout
| Unit | Branch | Purpose | Depends on | Est. files |
|------|--------------------|----------------------------|------------|------------|
| U1 | 001-payments | Domain model + migrations | main | ~25 |
| U2 | 001-payments-u2 | REST API + contract tests | U1 | ~40 |
| U3 | 001-payments-u3 | Webhooks + admin UI | U2 | ~70 |
💡 If any unit estimates > 100 files at this stage, the agent should split it now, in the plan — not mid-implementation.
Part 5 — /speckit.tasks + /speckit.analyze
Group every task under a unit, and give each unit an exit definition:
/speckit.tasks
STACK CONSTRAINTS (mandatory):
- Group tasks under "## Unit N" headings; each task belongs to exactly one unit.
- Per unit define: unit-test command and exit criteria:
tests green + scripts/check-stack-cap.sh <parent> exit 0.
- Estimated files > 100 for any unit ⇒ split it before finalizing.
Resulting tasks.md (excerpt):
## Unit 1 — Domain Core (001-payments, parent: main)
- [ ] T001 Payment schema migration
- [ ] T002 Payment aggregate + state machine
- [ ] T003 Unit tests
**Exit:** `npm run test:unit` → commit → `scripts/check-stack-cap.sh main`
→ push → `gh stack submit` → checkout `001-payments-u2`
## Unit 2 — API (001-payments-u2, parent: 001-payments)
- [ ] T004 POST /payments endpoint
- [ ] T005 GET /payments/:id
- [ ] T006 Contract tests
**Exit:** same pattern, parent = `001-payments`
Then sanity-check the structure:
/speckit.analyze
Also verify: every task maps to one unit; no gaps or forward deps in the
unit chain; no unit's file estimate exceeds 100.
Part 6 — /speckit.implement: the unit loop
This is the heart of the tutorial. Kick off:
/speckit.implement
EXECUTION RULES (mandatory):
- One unit at a time, in order U1..Un.
- Unit complete ONLY when: unit tests pass AND scripts/check-stack-cap.sh
<parent> exits 0.
- Then: commit → push -u → gh stack submit → git checkout -b <next-unit>.
- Never run gh stack with red tests or a failed cap check.
- Cap exceeded mid-unit ⇒ stop, split remaining tasks into a new unit in
tasks.md, finish the loop, continue.
🔁 Unit 1
The agent implements T001–T003. Then you (or the agent) run the loop:
npm run test:unit
# ✔ 42 tests passing ← GATE 1: tests green
git add -A
git commit -m "feat(u1): payment domain model + migrations"
scripts/check-stack-cap.sh main
# Files changed vs 'main': 25 / 100
# ✅ Within cap — safe to push and submit. ← GATE 2: cap green
git push -u origin 001-payments
gh stack submit --draft --fill ← NOW the stack command runs
# ✔ Created https://github.com/acme/payments-service/pull/1
git checkout -b 001-payments-u2 ← stack the next unit on top
PR #1 exists. Both gates were green first — that's the discipline.
🔁 Unit 2
You're now on 001-payments-u2, branched from U1. Implement T004–T006, then:
npm run test:unit # ✔ 57 tests passing
git add -A && git commit -m "feat(u2): payments REST API"
scripts/check-stack-cap.sh 001-payments
# Files changed vs '001-payments': 40 / 100
# ✅ Within cap.
git push -u origin 001-payments-u2
gh stack submit --draft --fill
# ✔ Created https://github.com/acme/payments-service/pull/2
git checkout -b 001-payments-u3
Note: gh stack submit is idempotent. Each run creates missing PRs and updates existing ones — so the whole stack stays fresh and CI keeps running on every PR while you build upward.
🔁 Unit 3
npm run test:unit
git add -A && git commit -m "feat(u3): stripe webhooks + admin UI"
scripts/check-stack-cap.sh 001-payments-u2
# Files changed vs '001-payments-u2': 70 / 100
# ✅ Within cap.
git push -u origin 001-payments-u3
gh stack submit --fill
# ✔ Created https://github.com/acme/payments-service/pull/3
The moment of truth — view the stack
gh stack view
main
└─ 001-payments (#1, draft)
└─ 001-payments-u2 (#2, draft)
└─ 001-payments-u3 (#3, draft)
Three PRs. Largest is 70 files. Every one had green tests before it was pushed. 🎉
Unit Exit Checklist (before touching the next unit):
[ ] tasks.md boxes ticked for this unit
[ ] npm run test:unit green
[ ] check-stack-cap.sh <parent> → ✅
[ ] Committed & pushed
[ ] gh stack submit ran clean
[ ] Next unit branch created
Part 7 — You have PRs. Now what?
This is where most stacked-PR guides stop. Ours doesn't.
7.1 Move PR #1 out of draft when review starts
gh pr ready 1
7.2 Handling review feedback — mid-stack edits are normal
A reviewer requests a change to the domain model… in U1, the bottom PR. With stacked PRs this is cheap:
gh stack checkout # interactive picker:
# ? Select a branch (…): 001-payments ← pick the bottom PR's branch
# …apply the fix, with tests…
npm run test:unit && git add -A && git commit -m "fix(u1): review feedback"
scripts/check-stack-cap.sh main && git push
gh stack sync # ✨ rebases U2 and U3 on top of your fix automatically
Open PR #2 and #3 — GitHub now shows them updated with the fix. One commit, one push, one sync. No cherry-pick hell.
7.3 Merging: strictly bottom-up
Never merge U2 before U1. The rhythm is merge → sync → repeat:
# Merge the bottom PR
gh pr merge 1 --squash
gh stack sync # re-parents U2 onto main; PR #2 now targets main
# Reviewers approve PR #2 (now reviewable standalone!)
gh pr merge 2 --squash
gh stack sync # PR #3 re-targets main
gh pr merge 3 --squash
gh stack sync # stack fully landed 🏁
Each PR gets smaller and more standalone as the stack dissolves from the bottom — reviewers see shrinking diffs, not growing ones.
7.4 Cleanup
git checkout main && git pull
git branch -D 001-payments 001-payments-u2 001-payments-u3
git push origin --delete 001-payments 001-payments-u2 001-payments-u3 # if not auto-deleted
Feature landed. Next feature starts the cycle again at Part 3.
Part 8 — When the 100-file cap bites mid-unit
It will happen eventually. The agent is deep into U3, and:
scripts/check-stack-cap.sh 001-payments-u2
# Files changed vs '001-payments-u2': 112 / 100
# ❌ FILE CAP EXCEEDED — do NOT push or run gh stack. Split this unit.
The recovery procedure (the agent follows this automatically if you adopted the constitution):
Stop. Do not push, do not run gh stack.
If the overflow is in the last commit: git reset --soft HEAD~1 (keep the work staged).
Open tasks.md: move unfinished/overflow tasks into a new unit (e.g., split U3 → U3a "Webhooks" / U3b "Admin UI").
Commit what fits within the cap → run the exit loop → push → submit.
git checkout -b 001-payments-u3b and continue the loop.
The stack becomes four PRs instead of three. That's the system working, not failing.
Part 9 — Troubleshooting
Symptom
Cause
Fix
gh stack submit only creates one PR
Child branch wasn't created from the parent — broken ancestry
git checkout -b <child> <parent> from scratch; never open stacked PRs manually
PR targets main instead of the unit below
PR created outside gh stack submit, or ancestry broken
Delete PR, fix ancestry, re-submit
Cap fails at push time
Files added after planning
Part 8 split procedure
Conflicts during gh stack sync
main or a lower unit moved
Fix conflicts branch-by-branch → git rebase --continue → rerun gh stack sync
Fixed U1, but U2's CI broke
Legit stacked coupling — U2 built on old U1
gh stack checkout U2, fix, push, gh stack sync
Agent runs gh stack with red tests
Constitution rules not loaded
Re-paste contract into the prompt; verify .specify/ constitution contains rule #4
Part 10 — Recap & cheat sheet
The entire methodology in one line:
Plan the stack → build a unit → tests green → cap green → gh stack submit → stack the next unit → merge bottom-up → sync.
# ── Stack lifecycle ──────────────────────────────
gh stack submit --draft --fill # create/update PRs (idempotent — run every unit)
gh stack view # visualize the stack
gh stack checkout # hop to any branch (for review fixes)
gh stack sync # rebase/re-parent after fixes and merges
gh pr merge <n> --squash # merge bottom PR, then…
# ── The gate (before EVERY submit) ───────────────
npm run test:unit # gate 1: tests
scripts/check-stack-cap.sh <parent> # gate 2: ≤100 files
# ── Overrides ────────────────────────────────────
STACK_FILE_CAP=50 scripts/check-stack-cap.sh main # tighter temporary cap
Why this combination wins
Problem
Spec Kit's answer
gh stack's answer
Agent builds the wrong thing
Spec + plan + clarify gates
Small PRs get early feedback
Unreviewable mega-PRs
Tasks grouped into ≤100-file units
Stacked PRs, auto-managed
Agent "finishes" broken code
Constitution: tests before anything
gh stack literally cannot run until gates pass
Merge-day pain
Ordered units
Bottom-up merge + gh stack sync
Next steps: try it on your smallest real feature first. Tighten STACK_FILE_CAP to 50 and watch your review times drop. And put the Part 2 contract in your constitution today — it's the difference between an agent that follows the loop and one that ships you another 180-file PR.
Read original: https://dev.to/sohailrajput/shipping-ai-generated-features-as-stacked-prs-a-complete-spec-kit-gh-stack-tutorial-gie
PreviousShift Left Code Review: How Qodo Turns Your Coding Agent Into Its Own First ReviewerNextShipping an image SaaS on Cloudflare Workers: nine things that broke
Related posts
The Trump Administration Is Wreaking Havoc on the Electoral System Before the Midterms
0Wired

YouTuber Druski is going to make exclusive content for Netflix
0The Verge
Why are Soviet math textbooks so hardcore in comparison to US textbooks? (2017)
0Hacker News
Technofeudalism
0Hacker News
The Linkerd Story: The people Behind The Service Mesh — A new documentary premieres at KubeCon + CloudNativeCon North America 2026
0CNCF Blog
Londres en otoño: barrios, mercados y museos a tu ritmo
0dev.to Watercooler