# 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-042](./docs/adr/042-sdd-adoption.md) for the why.
- **The rules** live in [`.specify/constitution.md`](./.specify/constitution.md) (humans) and
[`.specify/AGENTS.md`](./.specify/AGENTS.md) (AI agents, every invocation).
- **The templates** live in [`.specify/templates/`](./.specify/templates/).
- **The worked example** is [`.specify/features/_example/`](./.specify/features/_example/) — read it first.
---
## 0. The whole workflow at a glance
```mermaid
flowchart TD
idea([Feature idea]):::start --> draft
subgraph author["✍️ Author"]
draft[/"/draft-spec
(Requirements Engineer)"/]:::skill --> issue[("Gitea issue = the SPEC
EARS REQ-NNN + acceptance criteria")]:::spec
end
issue --> ri[/"/review-issue"/]:::skill
ri --> g1{"GATE 1 · spec review
6 personas APPROVE?
Open Questions empty?"}:::gate
g1 -- "FAIL / question" --> amend["Amend the issue body"]:::work --> ri
g1 -- "APPROVE" --> rtm["Seed RTM rows
REQ-ID → issue #"]:::work
rtm --> wt["Create git worktree
(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
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?
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
all personas APPROVE?
every REQ implemented + tested?
no Do-Not-Touch violation?"}:::gate
g3 -- "changes requested" --> fixpr["Fix on branch"]:::work --> rp
g3 -- "APPROVE" --> merge([Merge → main
closed issue = archived spec]):::start
rules["📐 constitution.md + AGENTS.md
(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-issue` runs **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-required` and `needs-review` exist (the feature template + `/draft-spec` attach them; the `labels` create-param is ignored, so they must pre-exist).
- [ ] **Gitea MCP** server configured (`gitea`) — the skills read/write issues and PRs through it.
- [ ] **`.spectral.yaml`** at the repo root (extends `spectral:oas`) — the CI contract check needs it.
- [ ] **Personas present**: identities in [`.claude/personas/`](./.claude/personas/) + checklists in [`.specify/personas/`](./.specify/personas/).
- [ ] **`.specify/constitution.md` + `AGENTS.md`** committed on `main` (so every branch inherits them).
- [ ] **Worktrees + hooks**: new feature work goes in a `git worktree` (plus-free name); run `npm install` in `frontend/` 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`](./.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.md` to 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/`](./.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/avatar`
> with a valid image, the user service shall store it and return a profile view with a
> non-null `avatarUrl`.
> - **REQ-008** (Unwanted-behavior) — If the uploaded file exceeds 2 MB, then the user service
> shall return `400 ErrorCode.AVATAR_TOO_LARGE` and store nothing.
> - **REQ-009** (Unwanted-behavior) — If a caller without `Permission.ADMIN_USER` targets
> another user's avatar, then the system shall return `403 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`](./.specify/features/_example/spec.md).
## 3. How to run a persona review
Each persona reads the spec, walks its checklist in `.specify/personas/.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 `If` clauses | PASS | REQ-006 (401), REQ-009 (403) |
> | 3 | Audit fields server-set, forbidden in body | **FAIL** | `avatarObjectKey` is bound from the request body → mass-assignment (CWE-639). Make it server-set in `UserService`. |
> | 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 `APPROVE`d and tasks are seeded, the implementer points the agent at the
artifacts. Example prompt:
> Implement Gitea issue #142 (profile picture upload). Read `.specify/AGENTS.md` and 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 run `npm run generate:api`. Do not mark a REQ done until its test is green; flip its
> row in `.specify/rtm.md` to 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`](./.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 the `constitution-diff` CI job list the files to reconcile. Record the bump
in ADR-042'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/`](./docs/adr/) (next free
`NNN`, verify on disk). Immutable once `Accepted`; 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 #n` on merge). The closed issue + the RTM
rows are the record of what shipped.
- **RTM** ([`.specify/rtm.md`](./.specify/rtm.md)) — append one row per `REQ-NNN` when a spec
is approved, each pointing at its issue (`#n`); flip `Status` as tests go green; never delete
a shipped requirement's row.
- **Personas** — update `.specify/personas/*.md` checklists 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 shall .` |
| Event-driven | `When , the shall .` |
| State-driven | `While , the shall .` |
| Optional-feature | `Where , the shall .` |
| Unwanted-behavior | `If , then the shall .` |
**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:**
```bash
# validate an OpenAPI contract locally (if you drafted one — same as CI)
npx @stoplight/spectral-cli lint .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
```