AI & ML
Shipping AI-Generated Features as Stacked PRs: A Complete Spec Kit + gh stack Tutorial
Sohail Ahmad DEV Community
2 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
← Previous
Approximate Inference: Sampling the Posterior vs Optimizing an Approximation
Next →
5 Verbal Corrections Didn't Stick, So a 46-Line Stop Hook Made the Mistake Physically Impossible
Related
Open WebUI: Chat Interface & Context Options
AI & ML
0
Dev.to (EN Zone)
GPT-6 Astra Just Changed the AI Skill Game — Here’s What AIML Students Should Learn Next
AI & ML
0
Dev.to (EN Zone)
Why Important Ideas Get Harder to Find as Your AI Chat History Grows
AI & ML
0
Dev.to (EN Zone)
Treat the Model Like Any Other Flaky Network Call
AI & ML
0
Dev.to (EN Zone)
Comments0
No comments yet — be the first