DEV Community

Sohail Ahmad
Sohail Ahmad

Posted on

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

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
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

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."
Enter fullscreen mode Exit fullscreen mode
chmod +x scripts/check-stack-cap.sh
git add scripts/ && git commit -m "chore: stack file-cap gate" && git push
Enter fullscreen mode Exit fullscreen mode

🔑 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
Enter fullscreen mode Exit fullscreen mode

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.
Enter fullscreen mode Exit fullscreen mode

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.
Enter fullscreen mode Exit fullscreen mode

Spec Kit creates the branch and commits the spec:

⏺ Created branch 001-payments
⏺ Created specs/001-payments/spec.md
Enter fullscreen mode Exit fullscreen mode

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.
Enter fullscreen mode Exit fullscreen mode

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        |
Enter fullscreen mode Exit fullscreen mode

💡 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.
Enter fullscreen mode Exit fullscreen mode

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`
Enter fullscreen mode Exit fullscreen mode

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.
Enter fullscreen mode Exit fullscreen mode

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.
Enter fullscreen mode Exit fullscreen mode

🔁 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
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

The moment of truth — view the stack

gh stack view
Enter fullscreen mode Exit fullscreen mode
main
└─ 001-payments (#1, draft)
   └─ 001-payments-u2 (#2, draft)
      └─ 001-payments-u3 (#3, draft)
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

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 🏁
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

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.
Enter fullscreen mode Exit fullscreen mode

The recovery procedure (the agent follows this automatically if you adopted the constitution):

  1. Stop. Do not push, do not run gh stack.
  2. If the overflow is in the last commit: git reset --soft HEAD~1 (keep the work staged).
  3. Open tasks.md: move unfinished/overflow tasks into a new unit (e.g., split U3 → U3a "Webhooks" / U3b "Admin UI").
  4. Commit what fits within the cap → run the exit loop → push → submit.
  5. 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
Enter fullscreen mode Exit fullscreen mode

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.

Top comments (0)