Authoring a dev-spec-kit spec
A spec is the source of truth: dev-spec-kit spec tasks derives evidence-bound tasks from it, and every
criterion's @check binding becomes a proof obligation in the graph. A criterion nobody can execute
is a wish, not a requirement.
File shape (.dev-spec-kit/specs/<feature>.md) — Gherkin is the default format
# Feature: <name>
> User story: As a <role>, I want <capability>, so that <benefit>.
## Requirement REQUIREMENT_<AREA>-NN — <short title>
Scenario: <one observable behavior>
Given <precondition>
When <event>
Then <observable outcome>
@check kind=<unit|integration|api|e2e|visual|parity> ref=<runner selector>
Scenario Outline: <behavior over a data table>
Given a note titled "<title>"
When the note is saved
Then the save fails with "<error>"
Examples:
| title | error |
| <empty> | title required |
| a/b | no slashes |
@check kind=unit ref=<one parameterized test that covers EVERY row>
Each Scenario: is ONE bindable criterion. A Scenario Outline: expands to one criterion PER
Examples row, and the @check under it binds ALL rows — worst-of applies: that check red means
every row unproven. EARS sentences (WHEN/IF/WHILE/WHERE … the system SHALL …) remain fully
supported (spec.criteriaFormat: gherkin = default, ears, mixed); off-format criteria lint but
always parse and bind.
Generating criteria from a one-line requirement (the scaffold)
Given a rough requirement, produce the criteria — don't wait to be handed them:
- Clarify first if it's vague (
dev-spec-kit-clarify): resolve the ambiguity before writing scenarios.
- Happy path — one
Scenario: (Given/When/Then) for the intended behavior, with an observable Then.
- Derive the unhappy paths — for that behavior, generate one criterion in EACH of the four edge
categories (the gate requires ≥1): invalid input · empty/boundary · failure-injection. Phrase each
as an
IF … THEN the system SHALL … (or a failure Scenario:).
- Name a
@check for every criterion — the test that would prove it. If you can't name it, the
criterion is still under-specified; sharpen it.
- Bind + draft — add the
@check lines, then dev-spec-kit spec draft-tests emits failing stubs and
dev-spec-kit-test-author fleshes them. For a too-big requirement, dev-spec-kit-analyze scores and splits it.
Worked shape: "users can rename a note" →
Scenario: rename a note # happy
Given a saved note "draft" When it is renamed to "final" Then it is listed as "final"
@check kind=unit ref=test/notes.test.ts::renames a note
IF the new title is empty THEN the system SHALL reject the rename and keep the old title. # boundary
@check kind=unit ref=test/notes.test.ts::rejects an empty rename
IF a note with the new title already exists THEN the system SHALL refuse with a conflict. # invalid
@check kind=unit ref=test/notes.test.ts::refuses a duplicate-title rename
IF the store write fails mid-rename THEN the system SHALL leave the original title intact. # failure
@check kind=unit ref=test/notes.test.ts::keeps the title when the write fails
Rules (RFC-2119)
- Every criterion MUST be a Gherkin Scenario (Given/When/Then, one behavior) or a single EARS
sentence:
WHEN/IF/WHILE/WHERE … the system SHALL …. One behavior per criterion; split
compound sentences.
- The edge-case mandate — 100% behavioral coverage. For EVERY behavior you MUST name its
unhappy paths across four categories: happy path · invalid input · empty/boundary ·
failure-injection (disk full, timeout, dependency down). If an edge case can't be named proven,
it isn't covered. The floor is mechanical: a requirement with zero negative/failure criteria
fails
dev-spec-kit graph build (gates.negativeFloor, on by default) — write the failure Scenario
(or IF-pattern criterion) and bind it, don't argue with the gate.
- The SHALL clause MUST be observable from outside (a response, a state change, an emitted event) —
never an implementation detail ("uses a HashMap") or a vibe ("is fast", "handles gracefully").
- Every criterion MUST carry ≥1
@check binding. If you cannot name the test that would prove it,
the criterion is not done being written. Unbound criteria are flagged UNVERIFIED and create no task.
- Ref syntax MUST match the stack's runner: maven
Class#method · vitest/jest file::test name ·
pytest file.py::test_name · custom stacks per verify.runners in config.
- IDs MUST be fully qualified — they travel without their spec (PR bodies, boards, chat):
REQUIREMENT_<AREA>-NN for functional and NFR_<AREA>-NN for non-functional criteria (both
carry full proof obligations); ADR_<AREA>-NN for decision records (graph nodes, NO check
obligation). Never bare R-… ids — legacy ids still parse but lint (rules.requireQualifiedIds).
- IDs are stable forever — never renumber; deprecate instead.
- Before locking a spec, ask the user at most 5 clarifying questions, ONE at a time, each with a
recommended answer they can accept with "yes" (scope > security > UX > technical detail).
- Hunt for the unhappy paths: for each WHEN, ask "and when it doesn't?" — propose IF-pattern criteria
for invalid input, timeouts, and empty states. Propose; the user approves scope.
Quality check before handing off
Checklist-as-unit-test-for-the-spec: every criterion observable? every criterion bound? every ID
stable? no compound sentences? unhappy paths covered or explicitly deferred (note why)?
1---2name: dev-spec-kit-spec-author3description: Write high-quality dev-spec-kit specs — Gherkin scenarios (default) or EARS acceptance criteria with @check bindings in .dev-spec-kit/specs/*.md. Use when authoring or revising a spec, converting a ticket/idea into requirements, or when criteria are vague, untestable, or unbound.4---56# Authoring a dev-spec-kit spec78A spec is the source of truth: `dev-spec-kit spec tasks` derives evidence-bound tasks from it, and every9criterion's `@check` binding becomes a proof obligation in the graph. A criterion nobody can execute10is a wish, not a requirement.1112## File shape (`.dev-spec-kit/specs/<feature>.md`) — Gherkin is the default format1314```markdown15# Feature: <name>1617> User story: As a <role>, I want <capability>, so that <benefit>.1819## Requirement REQUIREMENT_<AREA>-NN — <short title>2021Scenario: <one observable behavior>22 Given <precondition>23 When <event>24 Then <observable outcome>2526@check kind=<unit|integration|api|e2e|visual|parity> ref=<runner selector>2728Scenario Outline: <behavior over a data table>29 Given a note titled "<title>"30 When the note is saved31 Then the save fails with "<error>"3233Examples:34 | title | error |35 | <empty> | title required |36 | a/b | no slashes |3738@check kind=unit ref=<one parameterized test that covers EVERY row>39```4041Each `Scenario:` is ONE bindable criterion. A `Scenario Outline:` expands to one criterion PER42Examples row, and the `@check` under it binds ALL rows — worst-of applies: that check red means43every row unproven. EARS sentences (`WHEN/IF/WHILE/WHERE … the system SHALL …`) remain fully44supported (`spec.criteriaFormat`: gherkin = default, ears, mixed); off-format criteria lint but45always parse and bind.4647## Generating criteria from a one-line requirement (the scaffold)4849Given a rough requirement, produce the criteria — don't wait to be handed them:50511. **Clarify first** if it's vague (`dev-spec-kit-clarify`): resolve the ambiguity before writing scenarios.522. **Happy path** — one `Scenario:` (Given/When/Then) for the intended behavior, with an observable Then.533. **Derive the unhappy paths** — for that behavior, generate one criterion in EACH of the four edge54 categories (the gate requires ≥1): invalid input · empty/boundary · failure-injection. Phrase each55 as an `IF … THEN the system SHALL …` (or a failure `Scenario:`).564. **Name a `@check` for every criterion** — the test that would prove it. If you can't name it, the57 criterion is still under-specified; sharpen it.585. **Bind + draft** — add the `@check` lines, then `dev-spec-kit spec draft-tests` emits failing stubs and59 `dev-spec-kit-test-author` fleshes them. For a too-big requirement, `dev-spec-kit-analyze` scores and splits it.6061Worked shape: "users can rename a note" →62```63Scenario: rename a note # happy64 Given a saved note "draft" When it is renamed to "final" Then it is listed as "final"65@check kind=unit ref=test/notes.test.ts::renames a note66IF the new title is empty THEN the system SHALL reject the rename and keep the old title. # boundary67@check kind=unit ref=test/notes.test.ts::rejects an empty rename68IF a note with the new title already exists THEN the system SHALL refuse with a conflict. # invalid69@check kind=unit ref=test/notes.test.ts::refuses a duplicate-title rename70IF the store write fails mid-rename THEN the system SHALL leave the original title intact. # failure71@check kind=unit ref=test/notes.test.ts::keeps the title when the write fails72```7374## Rules (RFC-2119)7576- Every criterion MUST be a Gherkin Scenario (Given/When/Then, one behavior) or a single EARS77 sentence: `WHEN/IF/WHILE/WHERE … the system SHALL …`. One behavior per criterion; split78 compound sentences.79- **The edge-case mandate — 100% behavioral coverage.** For EVERY behavior you MUST name its80 unhappy paths across four categories: happy path · invalid input · empty/boundary ·81 failure-injection (disk full, timeout, dependency down). If an edge case can't be named proven,82 it isn't covered. The floor is mechanical: a requirement with zero negative/failure criteria83 fails `dev-spec-kit graph build` (`gates.negativeFloor`, on by default) — write the failure Scenario84 (or IF-pattern criterion) and bind it, don't argue with the gate.85- The SHALL clause MUST be observable from outside (a response, a state change, an emitted event) —86 never an implementation detail ("uses a HashMap") or a vibe ("is fast", "handles gracefully").87- Every criterion MUST carry ≥1 `@check` binding. If you cannot name the test that would prove it,88 the criterion is not done being written. Unbound criteria are flagged UNVERIFIED and create no task.89- Ref syntax MUST match the stack's runner: maven `Class#method` · vitest/jest `file::test name` ·90 pytest `file.py::test_name` · custom stacks per `verify.runners` in config.91- IDs MUST be fully qualified — they travel without their spec (PR bodies, boards, chat):92 `REQUIREMENT_<AREA>-NN` for functional and `NFR_<AREA>-NN` for non-functional criteria (both93 carry full proof obligations); `ADR_<AREA>-NN` for decision records (graph nodes, NO check94 obligation). Never bare `R-…` ids — legacy ids still parse but lint (`rules.requireQualifiedIds`).95- IDs are stable forever — never renumber; deprecate instead.96- Before locking a spec, ask the user at most 5 clarifying questions, ONE at a time, each with a97 recommended answer they can accept with "yes" (scope > security > UX > technical detail).98- Hunt for the unhappy paths: for each WHEN, ask "and when it doesn't?" — propose IF-pattern criteria99 for invalid input, timeouts, and empty states. Propose; the user approves scope.100101## Quality check before handing off102103Checklist-as-unit-test-for-the-spec: every criterion observable? every criterion bound? every ID104stable? no compound sentences? unhappy paths covered or explicitly deferred (note why)?