| name | spec-kit |
| description | Enforce the Problem → Spec → Plan → Tasks progression with human approval gates before any DISCOVER phase work. Use when starting a new feature. |
SKILL: spec-kit
Purpose
Enforce a structured Problem → Spec → Plan → Tasks progression before any agent enters the DISCOVER phase. Each artifact requires explicit human approval before the next begins. TASKS.md is the terminal artifact — it emits the story files that feed the 8-phase pipeline.
State Machine
PROBLEM (draft) → PROBLEM (approved)
→ SPEC (draft) → SPEC (approved)
→ PLAN (draft) → PLAN (approved)
→ TASKS (draft) → TASKS (approved)
→ story files emitted → DISCOVER
No step may begin until the previous step's artifact is approved. Approval = human sets status: approved in the artifact frontmatter, or reacts with ✅ on the linked GitHub Issue.
Artifact Locations
docs/specs/<epic>-<feature-slug>/
├── PROBLEM.md # raw problem statement — user voice, no solution
├── SPEC.md # formal spec — goals, non-goals, acceptance criteria
├── PLAN.md # technical plan — approach, ADR triggers, risks
└── TASKS.md # task decomposition — maps to story files
PROBLEM.md Template
---
epic: "{epic-slug}"
feature: "{feature-slug}"
status: draft # draft | approved | rejected
approved_by: null
approved_at: null
---
## Problem Statement
<!-- Write in user voice. No solution language. Describe the pain, not the fix. -->
**Who is affected:** {user role(s)}
**Current situation:** {what happens today}
**Impact:** {why this matters — quantify if possible}
**Desired outcome:** {what good looks like, from the user's perspective}
## Open Questions
<!-- Things that must be answered before writing SPEC.md -->
- [ ] {question 1}
- [ ] {question 2}
## Out of Scope
<!-- Explicitly list what this problem statement does NOT cover -->
- {out of scope item}
SPEC.md Template
---
epic: "{epic-slug}"
feature: "{feature-slug}"
status: draft # draft | approved | rejected
approved_by: null
approved_at: null
problem_approved: true # must be true before this file is created
---
## Goals
- {measurable goal 1}
- {measurable goal 2}
## Non-Goals
- {explicitly excluded scope item}
## Acceptance Criteria
```gherkin
Feature: {Feature title}
Scenario: {primary success path}
Given {precondition}
When {action}
Then {outcome}
Scenario: {failure / edge case}
...
Constraints
- {technical constraint}
- {business constraint}
Dependencies
- {system or team dependency}
ADR Triggers
## PLAN.md Template
```markdown
---
epic: "{epic-slug}"
feature: "{feature-slug}"
status: draft # draft | approved | rejected
approved_by: null
approved_at: null
spec_approved: true
---
## Technical Approach
{2–4 paragraph summary. Clean Architecture layers affected. SOLID concerns.}
## Component Breakdown
| Component | Layer | Agent | Notes |
|---|---|---|---|
| {name} | {domain/infra/ui} | {agent} | {notes} |
## ADRs Required
| Decision | Status | File |
|---|---|---|
| {decision} | pending | docs/architecture/{slug}-adr.md |
## Risk Register
| Risk | Likelihood | Impact | Mitigation |
|---|---|---|---|
| {risk} | low/med/high | low/med/high | {mitigation} |
## Test Strategy
- Unit: {what and where}
- Integration: {what and where}
- E2E: {what and where}
- BDD: {Gherkin scenario files}
TASKS.md Template
---
epic: "{epic-slug}"
feature: "{feature-slug}"
status: draft # draft | approved | rejected
approved_by: null
approved_at: null
plan_approved: true
stories_emitted: false
---
## Tasks
<!-- Each task maps to one story file. task_id becomes the story NNNN. -->
| ID | Title | Agent | Priority | ui | adr_required |
|---|---|---|---|---|---|
| 0001 | {story title} | {primary agent} | high | false | false |
| 0002 | {story title} | {primary agent} | medium | true | false |
## Emitted Stories
<!-- Populated automatically when TASKS is approved -->
- [ ] docs/stories/{epic}-{slug}-{timestamp}-0001.md
- [ ] docs/stories/{epic}-{slug}-{timestamp}-0002.md
Validate an Artifact
validate_spec_artifact() {
local file="$1"
local required_status="$2"
if [ ! -f "$file" ]; then
echo "[spec-kit] MISSING $file"
return 1
fi
STATUS=$(python3 -c "
import re
m = re.search(r'^status:\s*(\w+)', open('$file').read(), re.MULTILINE)
print(m.group(1) if m else 'draft')
")
if [ "$STATUS" != "$required_status" ]; then
echo "[spec-kit] BLOCKED $file status=$STATUS required=$required_status"
return 1
fi
echo "[spec-kit] OK $file status=approved"
}
EPIC="user-auth"
SLUG="reset-password"
BASE="docs/specs/${EPIC}-${SLUG}"
validate_spec_artifact "$BASE/PROBLEM.md" "approved" || exit 1
validate_spec_artifact "$BASE/SPEC.md" "approved" || exit 1
validate_spec_artifact "$BASE/PLAN.md" "approved" || exit 1
validate_spec_artifact "$BASE/TASKS.md" "approved" || exit 1
Emit Story Files from TASKS.md
When TASKS.md is approved, emit one story file per task row:
BASE="docs/specs/${EPIC}-${SLUG}"
TS=$(date -u +"%Y%m%d%H%M%S")
python3 - <<'EOF'
import re, os, sys
from datetime import datetime, timezone
tasks_path = sys.argv[1]
epic = sys.argv[2]
ts = sys.argv[3]
text = open(tasks_path).read()
rows = re.findall(
r'^\|\s*(\d{4})\s*\|\s*(.*?)\s*\|\s*(.*?)\s*\|\s*(.*?)\s*\|\s*(true|false)\s*\|\s*(true|false)\s*\|',
text, re.MULTILINE
)
os.makedirs('docs/stories', exist_ok=True)
emitted = []
for task_id, title, agent, priority, ui, adr_req in rows:
slug = re.sub(r'[^a-z0-9]+', '-', title.lower()).strip('-')[:40]
filename = f"docs/stories/{epic}-{slug}-{ts}-{task_id}.md"
if os.path.exists(filename):
print(f"[spec-kit] SKIP {filename} (exists)")
continue
content = f"""---
id: "{epic}-{task_id}"
title: "{title}"
epic: "{epic}"
priority: "{priority}"
ui: {ui}
adr_required: {adr_req}
milestone: null
labels:
- "type:feature"
- "priority:{priority}"
- "phase:discover"
issue_number: null
issue_url: null
---
## Story
**As a** user,
**I want** {title.lower()},
**so that** TODO: define business outcome.
## Acceptance Criteria
```gherkin
@story:{epic}-{task_id} @epic:{epic} @priority:{priority}
Feature: {title}
Scenario: TODO
Given TODO
When TODO
Then TODO
Definition of Done
ADR Links
"""
with open(filename, 'w') as f:
f.write(content)
emitted.append(filename)
print(f"[spec-kit] EMIT {filename}")
Mark stories_emitted in TASKS.md
text2 = open(tasks_path).read()
text2 = text2.replace('stories_emitted: false', 'stories_emitted: true')
open(tasks_path, 'w').write(text2)
EOF
"$BASE/TASKS.md" "$EPIC" "$TS"
## Skip Conditions
Spec-Kit is **skipped** (not required) for:
- Branches matching `spike/*` or `chore/*`
- Stories with `type:bug` label (use bugfix superpower instead)
- When `sdlc.mode: spike` in `project.config.yaml`
```bash
BRANCH=$(git branch --show-current)
if echo "$BRANCH" | grep -qE '^(spike|chore)/'; then
echo "[spec-kit] SKIP branch=$BRANCH — spike/chore exempt"
exit 0
fi
Failure Modes
| Condition | Action |
|---|
| Artifact missing | Create from template. Set status: draft. Halt until approved. |
Artifact status: rejected | Stop. Surface rejection reason to human. Do not create next artifact. |
stories_emitted: true already | Skip emit. Log SKIP — stories already emitted. |
| TASKS table empty | Reject TASKS.md. Require at least one task row. |
| Duplicate task IDs | Reject TASKS.md. IDs must be unique 4-digit values. |
Logging
[spec-kit] VALIDATE docs/specs/user-auth-reset/PROBLEM.md status=approved
[spec-kit] VALIDATE docs/specs/user-auth-reset/SPEC.md status=approved
[spec-kit] BLOCKED docs/specs/user-auth-reset/PLAN.md status=draft
[spec-kit] EMIT docs/stories/user-auth-reset-password-20250416143000-0001.md
[spec-kit] SKIP spike/* branch — spec-kit not required