Seven skills carried a `## Mode 2 Routing Note` citing a routing pod ("Plan 6")
that does not exist. Several admitted in their own text that nothing about the
skill changes between modes — so it was roughly ten lines of context on every
load for zero behavioural effect. The content is not worthless, just not
per-skill: it now lives as one table in README.md, with the caveat that nothing
depends on it.
The related line inside the session_log sections stays, because that one does
motivate behaviour — but it no longer cites a component that has never shipped.
It now says what is true: pass-rate is what a future routing decision would be
based on, and a skill that never logs contributes no evidence either way.
While in README: the contributing steps were stale enough to fail CI for anyone
who followed them. They said to add a row to SKILLS_INDEX.md by hand — the table
is generated now, and `task check` fails on a hand-edited diff — and listed none
of the four frontmatter keys the gates require. Step 4 now names the
pressure-test bar from #19 rather than leaving it to be discovered.
Closes #36
Bump-Type: patch
12 KiB
name, surfaces, summary, use_when, description
| name | surfaces | summary | use_when | description | ||
|---|---|---|---|---|---|---|
| feature-spec |
|
Implementation-level feature spec (tighter than spec-driven-dev) | Scoping one feature inside an already-specced project | Write a tight implementation-level spec for a single feature or component before coding it. Use after a project-level spec exists (see spec-driven-dev) and you need to nail down one feature inside it. |
Feature Spec
Overview
A feature spec is the implementation-level contract for one feature or component, written by an engineer before code. It is shorter and more focused than a project-level spec (spec-driven-dev) — it assumes the project context, the stack, and the user are already known. Its job is to force one decision: what does done look like, and what is explicitly not in this feature.
Core principle: If you cannot write three measurable success criteria, you do not understand the feature well enough to build it.
When to Use
- About to start implementing a feature inside an existing, already-specced project
- A change that touches more than one file or one package
- A change that introduces a new public API, schema, or external contract
- Any time the requirements feel "obvious" — that feeling usually means assumptions are stacking silently
When NOT to use:
- Project kickoff or first-time architecture decisions — use
spec-driven-devinstead, which covers the broader lifecycle (Objective, Tech Stack, Project Structure, Boundaries, etc.) - Single-line fixes, typos, dependency bumps
- Spike or throwaway prototypes (note them as such; no spec needed)
Iron Laws
- Success criteria must be measurable. "The system is fast" is banned. "p99 < 200ms under 100 RPS" is valid. If you cannot state a measurement, the criterion is not a criterion.
- An "Out of scope" section is mandatory. If you do not draw the boundary, the implementer (you, future-you, or another agent) will guess wrong.
- Every technical decision in "Technical approach" must have a rationale. A decision is any choice where a reasonable alternative existed and was not taken. "Use Postgres" is not a decision; "use Postgres because the rest of the stack uses it and we need transactions across user + invoice" is.
EARS-lite acceptance criteria
Measurable criteria answer "is it done?". They do not always answer "what should it do when X happens?" — and behavioural requirements written as prose are where ambiguity survives review. For any criterion that is conditional or event-driven, use EARS-lite instead of prose:
WHEN <trigger> THE SYSTEM SHALL <observable response>
WHILE <state> THE SYSTEM SHALL <observable response>
IF <unwanted condition> THEN THE SYSTEM SHALL <observable response>
There is a fourth, unconditional form. Use it for invariants — the properties that must hold with no trigger at all, which are usually the ones that matter most:
THE SYSTEM SHALL <observable response>
Worked, deliberately from a domain unlike whatever you are specifying:
THE SYSTEM SHALL reject a firmware image whose signature does not verify
WHEN a sensor reports a temperature above the shutdown limit
THE SYSTEM SHALL open the contactor within 200ms
WHILE the contactor is open
THE SYSTEM SHALL refuse every start command
IF a sensor reading is missing or stale by more than one interval
THEN THE SYSTEM SHALL treat it as above the limit
The example is off-domain on purpose. An on-domain example gets copied: a spec written against an invoice-flavoured example inherited a requirement about extraction confidence that its feature request never mentioned, and the author only noticed on review. Read the shapes, not the content.
Each conditional line names a trigger and one observable response, which usually maps
onto a single test. The WHILE and unconditional forms are universally quantified —
"every start command", "a firmware image" — so they map onto a property or a set of
cases, not one assertion. That is still worth writing: it closes the requirements half
of the audit trail, which is otherwise issue-body prose.
This is an option, not a replacement. Threshold criteria ("p99 < 200ms under 100 RPS") stay as they are — EARS phrasing adds nothing to a number. Use it for conditional behaviour, error handling, and state-dependent rules.
Context snapshot
The executor of this spec will not see the conversation that produced it, and often cannot see the architecture document either — a different agent, a fresh session, a different machine. A spec that says "see the architecture doc" hands them a lookup they may not be able to perform.
So inline the specific facts this spec depends on, not the whole document:
## Context snapshot
Taken from <source> @ <commit-or-date>. Re-check before implementing if stale.
- The mailbox poller writes rows to `invoices` with `state = 'received'`.
- The payment-file writer reads `invoices` and emits one file per batch.
- Money is int64 minor units plus an ISO-4217 code; float is banned repo-wide.
Three rules keep this from becoming a second source of truth, which is the real objection to inlining and a good one:
- Snapshot what the implementer cannot look up and cannot proceed without. That is wider than "facts this spec's decisions rest on" — a table name may underpin no decision here and still be the thing that blocks writing the migration. It is still narrower than the whole document: if a fact changes nothing and blocks nothing, leave it out, and say what you left out so the omission reads as deliberate.
- Always cite source and version. A snapshot without provenance cannot be revalidated, and an unrevalidatable copy is exactly the drift everyone fears.
- If you could not read the source either, say so in those words. Writing a spec
against a document you cannot open is common and is not a reason to skip the section
— it is a reason to mark the whole block
UNVERIFIED — reconstructed, not read, and to make confirming it the implementer's first task, before the first failing test. An unmarked guess is the only genuinely dangerous version. - The snapshot is evidence, not authority. Open the section with one line to that effect — "where this disagrees with the source, the source wins and this spec is stale" — rather than annotating each fact.
Spec Template
# Spec: [Feature Name]
## Problem Statement
What problem does this feature solve? For whom? Why now?
(2-4 sentences. If you cannot answer "why now," ask before writing the rest.)
## Context snapshot
[Facts the implementer cannot look up, with source and version. See "Context snapshot" above.]
## Success Criteria
- [ ] Criterion 1 — measurable and verifiable
- [ ] Criterion 2 — measurable and verifiable
- [ ] Criterion 3 — measurable and verifiable
### Behavioural criteria (EARS-lite)
[Conditional and event-driven behaviour, one trigger and one observable response per
line. Not checkboxes — these are properties, and several need more than one test each.
Omit the subsection entirely if the feature has no conditional behaviour.]
## Constraints
Non-negotiable requirements the solution must satisfy.
Examples: "must not change the public API", "must complete in < 500ms",
"must not require a database migration", "must work offline".
## Out of Scope
What this feature explicitly does NOT do, even if related.
Anything plausibly in scope that you decided to defer goes here.
## Technical Approach
The architecture or design decisions, with rationale for each.
Each bullet should answer "why this choice over the obvious alternative."
## Risks
What could go wrong, and how it would be mitigated or detected.
At least one risk must be listed; "no risks" usually means insufficient analysis.
Worked Example
# Spec: IBAN extraction from Swedish PDF invoices
## Problem Statement
Invoice processors at small accounting firms manually copy IBANs from PDF
invoices into the banking system. The current parser handles ~60% of formats;
40% require manual fallback, defeating the automation premise.
Why now: the pilot customer is onboarding three new firms next month, all of
which use formats currently in the 40%.
## Success Criteria
- [ ] IBAN extracted correctly from ≥ 95% of a 200-invoice test corpus
- [ ] Zero false positives (extracting a non-IBAN string as an IBAN) on the corpus
- [ ] p99 extraction time < 500ms per invoice
- [ ] Returns a structured error (not a panic) for unrecognized formats
## Constraints
- Must not require a new dependency (PDF library is already chosen)
- Must work without network access (no external IBAN validation API)
- Must be callable from the existing `Parser.Extract(ctx, r)` signature
## Out of Scope
- Non-Swedish IBAN formats (separate spec when needed)
- BIC/SWIFT extraction (handled by a separate parser)
- OCR — input is assumed to be text-extractable PDF, not scanned image
- Validating that the IBAN refers to a real account
## Technical Approach
- Layered regex-then-checksum approach: regex narrows candidates, MOD-97
checksum validates. Rationale: pure regex is too permissive; pure
checksum requires too many candidates to test.
- Use the existing `pdfplumber` text layer rather than re-extracting.
Rationale: extraction is already the slow path; reusing the layer
avoids doubling extraction time.
- Return `(iban string, ok bool, err error)` not `(*IBAN, error)`.
Rationale: the caller almost always wants to know "did we find one"
separately from "did extraction itself fail," and a pointer return
invites nil-deref bugs.
## Risks
- Regex tuned to current corpus may overfit; mitigation: corpus is held out
per-firm, so we will see degradation at the next firm onboarding before
it reaches production.
- MOD-97 implementation bugs are subtle; mitigation: cross-check against
the Wikipedia reference implementation in tests.
Common Rationalizations
| Rationalization | Reality |
|---|---|
| "It's a small feature, I don't need a spec" | Small features still need a measurable definition of done. The spec stays small too. |
| "I'll write the spec after, when I know more" | Then it is documentation, not a spec. The point is forcing clarity before the code locks in your assumptions. |
| "The success criteria are obvious" | If they are obvious, they are easy to write down. If they are hard to write down, they are not obvious. |
| "I don't have time to enumerate out-of-scope items" | You will spend more time arguing over scope creep at review than writing this section now. |
Brain MCP Integration
The brain holds prior feature specs across the project. Use it to keep constraints and out-of-scope decisions coherent across related features.
At spec start:
- Run
brain_queryfor prior specs in the same area or for the same problem domain. Reusing the same constraints and out-of-scope decisions across related features keeps the system coherent.
After spec is approved:
- Run
brain_writewith the spec's success criteria and out-of-scope list. Future related specs can cross-reference rather than re-derive.
Logging
Call session_log once at the end of every phase to record the outcome.
Pass-rate is computed downstream by the /pass-rate HTTP endpoint, which
treats pass as success, fail as failure, skip as neither.
At end of each phase:
session_logwith{session_id: "<this session's id>", skill: "feature-spec", phase: "<phase-name>", final_status: "pass" | "fail" | "skip", message: "<one-line summary>", project_root: "<absolute path>"}
Phases for this skill: draft
Status semantics:
pass— the phase's intended outcome was reached (spec drafted and approved).fail— the phase's intended outcome was NOT reached (spec rejected, blocked on unresolved questions).skip— phase was skipped intentionally.
Why this matters: pass-rate is what a future routing decision would be based on — whether a given call can be served by a local model instead of a cloud one. A skill that never logs contributes no evidence either way.
Cross-References
- Load
spec-driven-devinstead if this is the first spec for a project (broader template, includes tech stack, project structure, boundaries). - Load
problem-analysisskill before this if the problem statement itself feels fuzzy. - Load
tddskill once the spec is approved — each success criterion becomes a test. - Load
planningskill if the spec implies more than ~5 implementation tasks.