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.