The app's spec says what to build, and every piece of work can prove which part of it it serves: each requirement, screen, decision and non-negotiable has a stable ID in a markdown spec folder; the first failing test, the code comment, the end-to-end flow, the commit body and the report quote that ID; and two scripts print any entry verbatim and prove that every citation in the repo and its history resolves. Works the same for iOS and Android; an app without a spec gets a clean NOT APPLICABLE.
Rules that must hold
- The spec decides what; its non-negotiables never bend. If a task, a design, a library or another
requirement would break a non-negotiable (
NN-entries), stop and ask before writing code. Why: a silently traded rule passes every test of the new work and is caught by nobody. - Quote the spec, never paraphrase it from memory. Print the entry with
spec-lookup.mjs <id>and put the ID and the rule in the first failing test's title or the comment above it, and in the commit body. Why: a misremembered number (4 seconds instead of 5, 4 free lists instead of 3) is the bug no test catches, because the test asserts the same wrong number. - Cite IDs exactly as the spec defines them, and name screens by title and ID together:
"Task list (SCR-1)". The prefixes come from the app's
contracts/spec.json(defaultNN-,SCR-,REQ-,DEC-,OOS-). Why: a cited ID that does not exist, or a name paired with the wrong ID, sends the reader to the wrong requirement (dangling-id,unknown-spec-section,screen-name-mismatch). - When the spec is silent, ambiguous or contradicts itself about anything a user would see, stop and ask: quote the lines, give two or three options with a recommended default, and keep working on everything else. Never invent product behaviour; never ask what the spec already answers. Why: an agent's guess ships as a product decision nobody made.
- Use the documented default of an open decision and say so in the report; ask only when the
default stops working. Every open decision carries
Default:, a settled oneDecided:, and a settled decision is never asked again. Why: an open decision without a default blocks work, and a re-asked one wastes the reviewer's time (entry-field-missing). - Build nothing the spec puts out of scope, and treat its numbers as limits, not targets. A test
or flow that cites an
OOS-entry or a dropped requirement fails. Why: agents over-deliver, and every extra is code to test, translate, declare to the stores and maintain (out-of-scope-traced). - IDs are stable forever. A new entry takes the next free number; an entry is never renamed,
renumbered or reused, and a dropped one stays with
Status: dropped. Why: old commits, tests and reports cite it (duplicate-spec-id,malformed-heading,invalid-field). - A built requirement is traced by a test or flow. Mark a requirement or screen
Status: builtin the commit that adds its green test citing it, and a built screen'sRoute:names a file that exists. Why: "built" with no test is a claim, not evidence (untraced-requirement,route-missing). - Traced IDs fit the commit and test reference patterns. The commit-msg hook
(
contracts/commits.jsonrequirementRef, elsescripts/git/requirement-ref.json) and tdd-workflow'srequirementIdPatternmust accept every traced ID. Why: an ID the hook rejects makes the commit that cites it impossible (ref-pattern-mismatch). - Platform differences are written down, never assumed. When iOS and Android behave differently
(a permission prompt, back navigation, a widget), the entry says so with
Platforms:or splits into one child per platform, each traced by its own test or flow; a spec that describes one platform only is silent about the other (rule 4). Why: the platform nobody specified ships whatever the library does by default. - Behaviour several features need is built once, in the shared layer, and cited where that shared piece is tested, never copied inside one feature. Why: copies drift, and the spec describes the behaviour once for the same reason.
Workflow
- Find out whether the app keeps a spec:
node ${CLAUDE_SKILL_DIR}/scripts/spec-lookup.mjs . --list.NOT APPLICABLE: the app keeps no requirement specmeans requirements come from the issue or the acceptance criteria: quote them with their key (TASKS-12,#41) and skip to step 6. Start a spec (step 9) only when the developer wants one. - Print the entries the task serves:
node ${CLAUDE_SKILL_DIR}/scripts/spec-lookup.mjs . REQ-4 SCR-2(--list --kind screenlists one kind,--briefprints headings only,--from <file>reads IDs from a file). An entry marked dropped or out-of-scope prints a NOTE: do not build it. - Read the non-negotiables the task touches (
--list --kind non-negotiable): any network, data, layout, text, accessibility or permission work touches at least one. Plan so none bends (rule 1); references/questions-and-decisions.md lists the typical collisions. - Check decisions and scope:
--list --kind decisionand--kind out-of-scope. Use each open decision's default and note it for the report; build nothing out of scope. - If the spec is silent, ambiguous or contradicts itself on anything a user would see, or covers one platform only, write the requirement question (quoted lines, options, default) as references/questions-and-decisions.md describes, send it in git-commits-and-reporting's request format, and keep working on the rest.
- Write the IDs into the work, as examples/spec-citations.md shows:
the first failing test's title or comment quotes the rule (
it('moves the task to Done (REQ-2)')), a constant that holds a decision's default cites it, flows name screens as "Task list (SCR-1)", the commit body names the IDs, the report names screens by title and ID. - Update the spec in the same commit: set
Status: in-progresswhen a slice starts andStatus: builtwhen its citing test is green. When a changed requirement changes a test expectation, that commit updates the entry, says why in its body and carriesSpec-Change: <ID> <what changed>(git-commits-and-reporting owns the format); a spec edit with no test change carries no trailer. - Run the check from the repo root:
node ${CLAUDE_SKILL_DIR}/scripts/check-spec-refs.mjs ., and for the session's commitsnode ${CLAUDE_SKILL_DIR}/scripts/check-spec-refs.mjs . --range <base>..HEAD(--traceadds the requirement-to-test matrix for the report). Fix everyFAILline and rerun until it printsRESULT: PASS. - Starting a spec (once per app): copy templates/docs/spec/ to
docs/spec/and templates/contracts/spec.json with its schema tocontracts/, replace the Tasks examples with the app's own entries (allplanned), and keep the default prefixes unless the app already has IDs. references/spec-format.md has the heading grammar, the fields, every contract key and what counts as a citation. If the commit contract sets its ownrequirementRefpattern, make it accept the spec's IDs (rule 9). When the app had nodocs/before, the root tooling treats it as pre-existing: in the same commit adddocs/to.prettierignoreand'docs/**'toPRE_EXISTINGineslint.config.mjs(with aGate-Change:trailer), or declare"docs"inrn-skills.config.json"layoutEntries"when the team wants Prettier to format the spec.
Definition of done
- The entries the work serves were printed with
spec-lookup.mjsand are quoted in the first failing test (title or comment) and in the commit body. - No non-negotiable was traded away; any collision was raised before building.
- Every open decision the work relied on is named in the report with the default used, and every spec gap was asked about with quoted lines and a default.
- Nothing out of scope was built; numbers from the spec were used as limits.
- Entries finished by this work are
Status: built, each cited by a test or flow; behaviour that differs by platform is specified and traced per platform. - The check prints no SKIP line; an app with no spec prints
NOT APPLICABLE: the app keeps no requirement spec (...), which passes. -
node ${CLAUDE_SKILL_DIR}/scripts/check-spec-refs.mjs . --range <base>..HEADprintsRESULT: PASS(before the session's first commit the range is empty: it prints oneNOTE --range ... holds no commit yetline and checks the files only)
Anti-patterns
- Citing from memory ("REQ-2 says Undo lasts 4 seconds"). Print the entry: the time lives in DEC-1, and its default is 5 seconds.
- Inventing an ID for a new idea (
// Leaderboard (SCR-16)). A new screen is a product decision: the entry is added to the spec with the reviewer's agreement first; until then the checker reportsdangling-id. - Renumbering the spec to tidy it up. Every old commit, test and report now points at the wrong entry. Add, drop, never reuse.
- Marking an entry built because the code exists. Built means a test or flow cites it and passes.
- Asking the reviewer how to build something. Architecture and libraries are engineering questions; the reviewer answers what the user gets.
- Asking about something the spec or a default already answers. Read the entry first; ask only about real gaps.
- "Helpful" extras (a widget, a sign-in for later sync, sample data beyond the stated count). Out of scope means not built.
- Writing the iOS behaviour into the spec and letting Android do whatever the library does. Split the entry per platform and trace each.
- Silencing a real citation with
spec-refs-ignore. The marker is for test data that names a missing ID on purpose. - Bare short codes as IDs (
S7,D4). They collide with crypto names, model numbers and data; keep a prefix that ends in-.
Files in this skill
| File | What it is | Read/run when |
|---|---|---|
| references/spec-format.md | The spec folder, heading grammar, fields and statuses, every contracts/spec.json key, what counts as a citation, tracing, agreement with the commit and test patterns, every rule, adopting a spec, when multi-package |
Workflow step 9, and whenever the checker reports something unexpected |
| references/questions-and-decisions.md | What the spec decides, non-negotiables and their collisions, when and how to ask, open decisions and defaults, out of scope and content caps, changing the spec, shared behaviour | Workflow steps 3 to 5 |
| examples/spec-citations.md | One requirement followed from spec-lookup through the test, a constant, a flow, the commit, the report and a question, with what the checker flags |
Once, before the first slice in an app with a spec |
| templates/docs/spec/README.md | docs/spec/README.md: how the spec folder is organized and changed |
Workflow step 9 |
| templates/docs/spec/non-negotiables.md | docs/spec/non-negotiables.md: four Tasks-app non-negotiables, each with Enforced by: |
Workflow step 9 |
| templates/docs/spec/screens.md | docs/spec/screens.md: three screens with actions, states and routes |
Workflow step 9 |
| templates/docs/spec/requirements.md | docs/spec/requirements.md: Tasks requirements, including a per-platform child (iOS and Android notification permission) |
Workflow step 9 |
| templates/docs/spec/decisions.md | docs/spec/decisions.md: an open decision with options and default, and a settled one |
Workflow step 9 |
| templates/docs/spec/out-of-scope.md | docs/spec/out-of-scope.md: what this version does not build |
Workflow step 9 |
| templates/contracts/spec.json | contracts/spec.json: the ID scheme, filled with the defaults |
Workflow step 9, or when the app uses other prefixes |
| templates/contracts/spec.schema.json | contracts/spec.schema.json: JSON Schema of the contract |
With the contract |
scripts/spec-lookup.mjs |
Prints spec entries verbatim (REQ-4 SCR-2 spec 1.1), --list [--kind], --brief, --from; fails on unknown IDs with the nearest ones |
Workflow steps 1 to 4, whenever a spec line is quoted |
scripts/check-spec-refs.mjs |
Checks the spec (headings, unique IDs, fields, routes, defaults, reference patterns) and every citation in files and commits (dangling IDs, sections, name pairs, tracing, excluded work) | Workflow step 8 and the definition of done |
scripts/lib/spec-model.mjs |
Reads the contract and the spec entries (shared by both scripts) | When changing a script |
scripts/selftest.mjs |
Proves both scripts pass the shipped templates and catch each planted bug | After changing a script or a template |
scripts/check-lib.mjs |
Shared script helper, synced from the library (do not edit here) | Never by hand |
assets/requirement-ref.json |
The default requirement-reference pattern of the commit rules, synced from git-commits-and-reporting's scripts/git/requirement-ref.json; used when the app has no copy and no requirementRef |
Never by hand (synced) |
assets/shared.json |
Declares the shared files this skill copies in | When adding a shared file |
tests/bases/app/ |
A minimal Tasks app the fixtures add to the shipped templates | When changing the fixtures |
tests/fixtures/ |
Overlay cases on the templates: good, pass, bad (one per rule) and error | When adding a rule to a script |
Related skills
tdd-workflow- turns a quoted entry into the first failing test; its history check reads the same IDs.git-commits-and-reporting- commit bodies,Spec-Change:trailers, reports and the request format for questions.screen-contracts- how each screen named in the spec is built (states, testIDs).design-visual-parity- proves a built screen matches its approved design baseline; the spec says what it shows.navigation-and-routing- the route files that screen entries name.e2e-maestro- the flows that trace screens and requirements on both platforms.react-native-conventions-index- which skill to load for a task.