Writing specs
A spec is plain Markdown with id: frontmatter and <!-- id: ... --> comment markers inline. SpecShip parses every .md file under your specs/ directory tree.
Not sure what the spec should say yet? Start with
/specship:spec new <requirement>— it grounds your idea in the existing code, explores 2–3 approaches with you, and (only once you explicitly confirm) writes a design brief and hands off tospec-authorto produce the spec below. The brief stays linked to the spec for traceability and shows up on the dashboard’s Specs page.
Minimal example
Section titled “Minimal example”---id: AUTH-DOCtitle: Authenticationowner: alicepriority: P0---
<!-- id: REQ-AUTH-005 --># Reject expired tokens with 401
The `checkExpiry` function returns false when the JWT is past its `exp` claim.Expired tokens return HTTP 401 with `code: token_expired` in the body.
## Acceptance
<!-- id: AC-AUTH-005-1 -->- **Token within expiry**: `checkExpiry` returns true.- **Token past expiry**: `checkExpiry` returns false. Response is 401.- **Clock skew tolerance**: a 30s grace window is allowed.That single file produces:
AUTH-DOC (document)└── REQ-AUTH-005 (requirement) └── AC-AUTH-005-1 (acceptance)— three spec nodes in the knowledge graph, each individually queryable, each separately linkable to code.
The three node kinds
Section titled “The three node kinds”| Kind | When to use | ID pattern |
|---|---|---|
| document | A whole-area concern (auth, billing, search). Lives at the top of a file via frontmatter. Holds metadata (owner, priority). | <AREA>-DOC |
| requirement | One testable assertion. Lives under an <!-- id: ... --> comment + # heading. | REQ-<AREA>-<NNN> |
| acceptance | A concrete pass/fail criterion that supports a requirement. Lives under a nested ## Acceptance or a <!-- id: ... --> inside the requirement’s body. | AC-<AREA>-<NNN>-<i> |
The hierarchy is just convention — SpecShip doesn’t enforce it. A flat REQ-* layout works fine if the team prefers it. Two-level (REQ-* + AC-*) is the most common shape we see.
ID rules
Section titled “ID rules”- Stable. Once a spec has an ID, never change it. Code links to the ID. Renames break links.
- Globally unique within the project.
- Conventional but not enforced.
REQ-AUTH-005is convention;widget-001works if you prefer. SpecShip just treats the string as an opaque key. - Embedded. The
<!-- id: ... -->comment must immediately precede a heading. SpecShip uses the heading as the spec’s title.
When a spec is genuinely dead — replaced, scope-cut, never-going-to-ship — don’t delete it. Mark it obsolete:
<!-- id: REQ-AUTH-002 --># [DEPRECATED] OAuth 1.0a flow
Superseded by REQ-AUTH-009.Existing links resolve. Searches still find it. History is preserved.
Document-level frontmatter
Section titled “Document-level frontmatter”The leading --- block on a document spec accepts these fields (all optional except id and title):
---id: AUTH-DOCtitle: Authenticationowner: alice # used by `spec-fix` to route the approval gatepriority: P0 # P0 / P1 / P2 / P3status: active # active / paused / shipped / deprecatedtags: [security, public-api]---owner and priority show up in the desktop UI’s Specs page filters. tags are searchable via specship_search.
Patterns
Section titled “Patterns”One file per area
Section titled “One file per area”The most common shape: specs/auth.md, specs/billing.md, specs/search.md. Each file’s frontmatter is the area document. Requirements and acceptance criteria live inline.
This scales to ~30 requirements per file. Past that, split:
specs/auth/├── README.md ← AUTH-DOC, the area doc + index├── login.md ← REQ-AUTH-001..010├── tokens.md ← REQ-AUTH-020..030└── recovery.md ← REQ-AUTH-040..050Linking from the requirement
Section titled “Linking from the requirement”A spec can declare a soft link to its expected implementation via a @link annotation, which specship init parses and pre-creates a link in drafted state:
<!-- id: REQ-AUTH-005 --># Reject expired tokens with 401
@link src/auth.ts:checkExpiry
The `checkExpiry` function returns false when the JWT is past its `exp` claim.When the agent implements it, that drafted link gets promoted to implemented. This is a useful breadcrumb for new requirements — you tell the agent where to look before it even reads the spec.
Multi-line code samples in the spec
Section titled “Multi-line code samples in the spec”If the spec needs to be precise about the wire format, put it in a fenced block:
<!-- id: REQ-AUTH-005 --># Reject expired tokens with 401
```jsonHTTP/1.1 401 UnauthorizedContent-Type: application/json
{ "code": "token_expired", "exp": 1700000000 }```The fenced block is part of the spec’s content hash. If you change the JSON shape, the hash changes, the link goes drifted, the agent has to re-verify.
Bridging vague specs
Section titled “Bridging vague specs”Sometimes the spec is fuzzy because the requirement is genuinely fuzzy (“the UI should feel snappy”). Two patterns work:
- Make it testable: “the dashboard’s first paint completes in under 800ms on a mid-range device” — now it’s
verifiable. - Use
documentslink kind: link the spec withkind: documentsinstead ofkind: implements. It tells the graph “this code is about this requirement” without claiming testability.
What NOT to put in specs
Section titled “What NOT to put in specs”| Avoid | Why | Where instead |
|---|---|---|
| Architecture diagrams | They aren’t requirements; they’re design decisions. They go stale. | A DESIGN.md outside specs/. |
| Code snippets that ARE the implementation | The spec becomes the source of truth and the code becomes redundant. | The spec describes behaviour; the code does the behaviour. |
| Long backstory (“we tried X first, then Y…”) | The spec is what we’re shipping, not how we got here. | A decisions/ directory or git history. |
| Open questions | ”Maybe we should…” isn’t a spec. | An issue tracker. |
Where they live
Section titled “Where they live”By default specship init indexes any .md file under specs/ at the project root. Customize via the configuration file:
spec: roots: - specs/ - docs/requirements/ ignore: - "**/draft/**"→ Next: Spec links & drift — the link lifecycle and how drift detection works.