Consolidates the full SDD workflow into one view at the top of the guide: a Mermaid flowchart (skills, the three gates, the TDD loop), a one-time prerequisites checklist, and a gates table. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
14 KiB
Spec-Driven Development (SDD)
How we turn a feature idea into merged, traceable code in this repo. SDD layers a uniform, machine-readable front-end onto the workflow we already run (Gitea issues → branch/PR → multi-persona review → red/green TDD). It does not replace any of that — see ADR-041 for the why.
- The rules live in
.specify/constitution.md(humans) and.specify/AGENTS.md(AI agents, every invocation). - The templates live in
.specify/templates/. - The worked example is
.specify/features/_example/— read it first.
0. The whole workflow at a glance
flowchart TD
idea([Feature idea]):::start --> draft
subgraph author["✍️ Author"]
draft[/"/draft-spec<br/>(Requirements Engineer)"/]:::skill --> issue[("Gitea issue = the SPEC<br/>EARS REQ-NNN + acceptance criteria")]:::spec
end
issue --> ri[/"/review-issue"/]:::skill
ri --> g1{"GATE 1 · spec review<br/>6 personas APPROVE?<br/>Open Questions empty?"}:::gate
g1 -- "FAIL / question" --> amend["Amend the issue body"]:::work --> ri
g1 -- "APPROVE" --> rtm["Seed RTM rows<br/>REQ-ID → issue #"]:::work
rtm --> wt["Create git worktree<br/>(pull main first)"]:::work --> impl[/"/implement"/]:::skill
subgraph build["🔁 Build · TDD per REQ-NNN"]
impl --> red["Red: failing test"]:::work --> green["Green: minimal code"]:::work --> sync["Refactor + sync<br/>generate:api · flip RTM → Done"]:::work --> commit["Commit · Refs #n"]:::work
commit -- "next REQ" --> red
end
build --> pr[["Open PR · Closes #n"]]:::work --> g2{"GATE 2 · CI green?<br/>ci.yml + sdd-gate.yml"}:::gate
g2 -- "red" --> fixci["Fix on branch"]:::work --> g2
g2 -- "green" --> rp[/"/review-pr"/]:::skill
rp --> g3{"GATE 3 · PR review<br/>all personas APPROVE?<br/>every REQ implemented + tested?<br/>no Do-Not-Touch violation?"}:::gate
g3 -- "changes requested" --> fixpr["Fix on branch"]:::work --> rp
g3 -- "APPROVE" --> merge([Merge → main<br/>closed issue = archived spec]):::start
rules["📐 constitution.md + AGENTS.md<br/>(bind every step)"]:::rules -.-> draft
rules -.-> impl
rules -.-> rp
classDef start fill:#1d3b53,color:#fff,stroke:#1d3b53;
classDef skill fill:#e8f5f0,stroke:#3aa884,color:#13352b;
classDef gate fill:#fff3cd,stroke:#d39e00,color:#5a4500;
classDef spec fill:#eef2ff,stroke:#5b6ee1,color:#1e2a5a;
classDef work fill:#f6f6f6,stroke:#bbb,color:#222;
classDef rules fill:#fdecea,stroke:#d9534f,color:#611a15;
/deliver-issueruns GATE 1 → discuss → build → GATE 3 (loop) end-to-end in one go.
Prerequisites (one-time setup)
Before the workflow runs cleanly, confirm these exist (most ship with this repo):
- Gitea labels
spec-requiredandneeds-reviewexist (the feature template +/draft-specattach them; thelabelscreate-param is ignored, so they must pre-exist). - Gitea MCP server configured (
gitea) — the skills read/write issues and PRs through it. .spectral.yamlat the repo root (extendsspectral:oas) — the CI contract check needs it.- Personas present: identities in
.claude/personas/+ checklists in.specify/personas/. .specify/constitution.md+AGENTS.mdcommitted onmain(so every branch inherits them).- Worktrees + hooks: new feature work goes in a
git worktree(plus-free name); runnpm installinfrontend/once per worktree so the pre-commit lint hook works.
The three gates
| Gate | When | Mechanism | Blocks on |
|---|---|---|---|
| 1 · Spec review | after /draft-spec, before any code |
/review-issue (6 persona checklists) |
any persona CHANGES REQUESTED, or an unresolved ## Open Question |
| 2 · CI | on every PR | ci.yml (tests · lint · semgrep) + sdd-gate.yml (rtm-check · contract-validate · constitution-diff) |
ci.yml failure (hard); sdd-gate jobs are non-blocking during adoption — see the workflow TODO |
| 3 · PR review | before merge | /review-pr (7 personas + traceability) |
any persona Changes requested, an unimplemented/untested REQ-NNN, or a constitution Do-Not-Touch violation |
1. The workflow in 8 steps
| # | Step | Who | Artifacts created / touched |
|---|---|---|---|
| 1 | Idea → Gitea issue using the Feature template | author | Gitea issue (labels spec-required, needs-review) from .gitea/ISSUE_TEMPLATE/feature.md |
| 2 | Write the spec in the issue body — Context, User Journey, EARS REQ-NNN requirements, measurable acceptance criteria, Out of Scope |
author | the Gitea issue body is the spec (single source of truth — no committed spec.md) |
| 3 | Capture durable design decisions as needed | author | a docs/adr/ ADR for any project-wide/irreversible decision; an OpenAPI contract and a STRIDE threat model inline in the issue (use the .specify/templates/ as the writing aid) |
| 4 | Persona spec review — the six checklists gate the spec | RE, Developer, Security, DevOps, UI/UX, Architect | /review-issue posts each persona's checklist verdict as a Gitea comment; findings folded into the issue body |
| 5 | Resolve Open Questions & blocking FAILs — spec does not proceed while any remain | author | issue body updated; Open Questions emptied |
| 6 | Seed the RTM — one row per REQ-NNN, pointing at the issue |
author | rows added to .specify/rtm.md (Issue: #n, Status: Planned) — committed with the feature branch |
| 7 | Implement in a worktree, TDD per task (failing test → green → refactor → commit); agent reads AGENTS.md + the issue body (the spec) |
implementer (often an AI agent) | code + tests; npm run generate:api after backend changes; RTM Status → Done |
| 8 | PR → multi-persona PR review → merge | reviewers | PR (Closes #n); the closed issue is the archived spec, the RTM rows record what shipped |
The personas at step 4 review the spec (the issue); the same personas at step 8 (via the
existing review-pr / deliver-issue skills) review the code. Step 4 catches at spec time
what used to surface only at step 8.
Skills that drive this: /draft-spec (requirements engineer authors steps 1–2 → creates
the issue) → /review-issue (step 4 gate) → /implement (steps 6–7) → /review-pr (step 8).
/deliver-issue runs review → discuss → implement → review-loop end-to-end.
Why issue-only? The Gitea issue body is the single source of truth for a spec — there is no committed per-feature
spec.mdto drift out of sync with it. The only SDD artifact that lives in git per feature is the RTM row (REQ-ID → issue # → test). The worked example under.specify/features/_example/is a template/reference, not a live feature — it shows the full artifact set in one place; real features keep the spec in the issue.
2. How a Gitea issue becomes a spec
Before (free-form issue):
Title: Add profile pictures Users should be able to upload a picture for their profile. Make sure it's not too big and only admins can remove other people's. Show initials if there's no picture.
Ambiguous: how big? which formats? what status code on rejection? what about unauthenticated callers? No identifiers to trace, no measurable criteria.
After (SDD-structured issue — excerpt):
Title: As a user I want to upload a profile picture so other family members recognise me
## Requirements
- REQ-002 (Event-driven) — When an authenticated user sends
POST /api/users/me/avatarwith a valid image, the user service shall store it and return a profile view with a non-nullavatarUrl.- REQ-008 (Unwanted-behavior) — If the uploaded file exceeds 2 MB, then the user service shall return
400 ErrorCode.AVATAR_TOO_LARGEand store nothing.- REQ-009 (Unwanted-behavior) — If a caller without
Permission.ADMIN_USERtargets another user's avatar, then the system shall return403 ErrorCode.FORBIDDEN.## Acceptance Criteria
- REQ-008 — a 2.1 MB PNG returns
400 AVATAR_TOO_LARGE; bucket object count unchanged.
Every behavior is now a uniquely-identified, testable, EARS-formed requirement with a
measurable acceptance criterion. See the full version in
.specify/features/_example/spec.md.
3. How to run a persona review
Each persona reads the spec, walks its checklist in .specify/personas/<persona>.md, and
posts a Gitea comment with PASS / FAIL / QUESTION per
item and a verdict. A FAIL from Security or Architect is a hard block. Concrete example:
Security — Spec Review
# Item Status Note 1 All mutating endpoints have authn + authz IfclausesPASS REQ-006 (401), REQ-009 (403) 3 Audit fields server-set, forbidden in body FAIL avatarObjectKeyis bound from the request body → mass-assignment (CWE-639). Make it server-set inUserService.6 Upload type allow-list + size PASS REQ-007 / REQ-008 9 threat-model.md present & STRIDE-complete QUESTION Is the avatar URL public or proxied? If public S3, that's information disclosure. Verdict: CHANGES REQUESTED — blocking FAIL: #3. Resolve #9 in the threat model.
The author folds the fix into the spec (here: server-set key + authenticated proxy URL),
empties the finding, and the persona re-reviews until APPROVE. This mirrors the existing
review-issue skill — the persona checklists just make the spec pass/fail explicit.
4. How the AI agent uses the spec
Once the spec is APPROVEd and tasks are seeded, the implementer points the agent at the
artifacts. Example prompt:
Implement Gitea issue #142 (profile picture upload). Read
.specify/AGENTS.mdand obey the constitution it references. The contract is the issue body — its EARS requirements REQ-001…REQ-009 and acceptance criteria. Build a red/green task list from them, write the failing test for each REQ first, confirm it fails, then make it pass. After backend model changes runnpm run generate:api. Do not mark a REQ done until its test is green; flip its row in.specify/rtm.mdto Done as you go.
The agent now has: the rules (AGENTS.md → constitution) and the exact requirements with ids
from the issue — so its output is bounded and verifiable. (The /implement skill fetches the
issue body for you via the Gitea API.)
5. Maintenance rules
- Constitution (
.specify/constitution.md) — change it only when a project-wide rule genuinely changes. Bump the semantic version (MAJOR = rule removed/weakened, MINOR = rule added/tightened, PATCH = wording), run the §6 Sync Impact review, and let theconstitution-diffCI job list the files to reconcile. Record the bump in ADR-041's revision log (or a superseding ADR for MAJOR). - AGENTS.md — keep it under 200 lines. It cross-references the constitution; it must never duplicate or contradict it.
- ADRs — project-wide/irreversible decisions go in
docs/adr/(next freeNNN, verify on disk). Immutable onceAccepted; supersede, don't edit. - Feature specs — the spec is the Gitea issue body; there is no committed
spec.md. "Archiving" is just closing the issue (Closes #non merge). The closed issue + the RTM rows are the record of what shipped. - RTM (
.specify/rtm.md) — append one row perREQ-NNNwhen a spec is approved, each pointing at its issue (#n); flipStatusas tests go green; never delete a shipped requirement's row. - Personas — update
.specify/personas/*.mdchecklists when a recurring blind spot appears; keep them aligned with the richer.claude/personas/.
6. Quick-start cheatsheet
EARS patterns (every requirement is one of these + a REQ-NNN id):
| Pattern | Shape |
|---|---|
| Ubiquitous | The <system> shall <behavior>. |
| Event-driven | When <trigger>, the <system> shall <behavior>. |
| State-driven | While <state>, the <system> shall <behavior>. |
| Optional-feature | Where <feature/permission present>, the <system> shall <behavior>. |
| Unwanted-behavior | If <undesired condition>, then the <system> shall <response>. |
File locations:
| What | Where |
|---|---|
| Non-negotiable rules | .specify/constitution.md |
| Agent rules (read every time) | .specify/AGENTS.md |
| Templates (writing aids) | .specify/templates/{feature-spec,adr,threat-model,api-contract-stub}.md |
| Persona checklists | .specify/personas/*.md |
| In-flight feature spec | the Gitea issue body (not a committed file) |
| Worked example (template/reference) | .specify/features/_example/ |
| Traceability matrix | .specify/rtm.md (REQ-ID → issue # → test) |
| ADR archive | docs/adr/NNN-*.md |
| Issue templates | .gitea/ISSUE_TEMPLATE/{feature,bug}.md |
| CI gate | .gitea/workflows/sdd-gate.yml |
Before you mark a feature done: every REQ-NNN has a green test, the RTM Status is
Done, all six personas APPROVE, npm run lint and the targeted tests pass, and
npm run generate:api has been run if the backend model changed.
Commands:
# validate an OpenAPI contract locally (if you drafted one — same as CI)
npx @stoplight/spectral-cli lint <your-contract>.yaml
# regenerate the TS client after a backend model/endpoint change
cd frontend && npm run generate:api # backend must run with --spring.profiles.active=dev