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 | ||
|---|---|---|---|---|---|---|
| spec-driven-dev |
|
Spec-driven development for PMs | Writing specs and requirements | Write a structured specification before writing any code. Use when starting a new project, feature, or significant change. Adapted for a PM-first workflow where why comes before how. |
Spec-Driven Development
Overview
Write a structured specification before writing any code. The spec is the shared source of truth — it defines what we're building, why it matters, and how we'll know it's done.
Code without a spec is guessing.
A spec doesn't need to be long. A two-paragraph spec beats no spec. The value is in forcing clarity before code is written, not in the length of the document.
Mathias PM Context
As a digital product manager building software:
- Why before how: The spec must capture the business context and user need before technical decisions. Agents reading the spec should understand why this matters, not just what to build.
- Explicit success criteria: Vague requirements produce vague results. Every spec must have testable success criteria.
- Surfaces assumptions: The spec's primary job is to surface misunderstandings before they become expensive code.
- Living document: Update the spec when decisions change. An outdated spec is still better than no spec.
When to Use
Always create a spec when:
- Starting a new project or feature
- Requirements are ambiguous or incomplete
- The change touches multiple files or modules
- You're about to make an architectural decision
- The task would take more than a day to implement
When NOT to use: Single-line fixes, typos, or changes where requirements are unambiguous and self-contained.
The Gated Workflow
Do not advance to the next phase without validation at each gate.
SPECIFY → [review] → PLAN → [review] → TASKS → [review] → IMPLEMENT
Each gate is a deliberate pause: does the next phase make sense given what we know?
Phase 1: SPECIFY
Surface Assumptions Immediately
Before writing spec content, list what you're assuming:
ASSUMPTIONS I'M MAKING:
1. This is a Go backend service (no frontend changes)
2. Authentication is handled by the existing middleware
3. The database is PostgreSQL (matching the rest of the stack)
4. The feature is used by authenticated users only, not public
→ Confirm or correct before I proceed.
Don't silently fill in ambiguous requirements.
Write the Spec
A spec covers six areas:
1. Objective — WHY are we building this?
This is the most important section. It must answer:
- What user problem does this solve?
- Who is the user?
- What does success look like from the user's perspective?
- Why now?
## Objective
Invoice importers at small accounting firms manually copy payment details
from PDF invoices into their banking system, taking 10–20 minutes per invoice.
**User:** Invoice processor at an accounting firm (10–50 invoices/day)
**Problem:** Manual data entry is slow, error-prone, and creates compliance risk
**Goal:** Reduce per-invoice processing time from ~15 minutes to < 2 minutes
Success: Invoice processor can extract and queue a payment from a PDF in under 2 minutes,
with confidence the data is correct.
2. Commands — Full executable commands
Build: task build
Test: task test (or: go test ./...)
Lint: task lint (or: golangci-lint run)
Dev: task dev
Deploy: task deploy:staging
3. Project Structure — Where things live
internal/
domain/ → Core types and interfaces
service/ → Business logic
store/ → Database implementations
handler/ → HTTP handlers
cmd/
server/ → Main entry point
4. Code Style — One real example beats three paragraphs
// Error handling: always wrap with context
if err != nil {
return fmt.Errorf("parse invoice PDF: %w", err)
}
// Dependency injection: accept interfaces
func NewInvoiceService(store InvoiceStore, parser PDFParser) *InvoiceService { ... }
// Context: always first parameter for I/O operations
func (s *InvoiceService) ProcessPDF(ctx context.Context, r io.Reader) (Invoice, error) { ... }
5. Testing Strategy
Framework: testing package + testify
Locations: *_test.go files in same package
Unit tests: table-driven, in-memory implementations for stores
Fast path: go test ./... (unit tests only)
Full suite: go test -tags=integration ./... (includes DB tests)
Coverage: >80% for business logic packages
6. Boundaries
Always:
- Run go test ./... before committing
- Wrap errors with fmt.Errorf("context: %w", err)
- Pass ctx as first parameter to any I/O function
- Run govulncheck before adding new dependencies
Ask first:
- Schema changes
- Adding new external dependencies
- Changing public API contracts
- Performance changes that affect existing behavior
Never:
- Commit secrets or API keys
- Remove or skip failing tests
- Send client data to external APIs
- Use naked returns on errors
Spec Template
# Spec: [Feature/Project Name]
## Objective
[What we're building and why. User story or problem statement.]
[Who is the user? What does success look like from their perspective?]
## Tech Stack
[Language, key libraries, relevant existing infrastructure]
## Commands
Build: [full command]
Test: [full command]
Lint: [full command]
Dev: [full command]
## Project Structure
[Directory layout with descriptions]
## Code Style
[One real code example showing the patterns to follow]
## Testing Strategy
[Framework, test locations, what to unit test vs integration test]
## Boundaries
- Always: [...]
- Ask first: [...]
- Never: [...]
## Success Criteria
[Specific, testable conditions that define "done"]
- [ ] [Condition 1: metric/threshold/method]
- [ ] [Condition 2: ...]
## Open Questions
[Anything unresolved that needs input before implementation begins]
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.
For the executor-facing handoff, feature-spec also carries a Context snapshot
section — inline the architecture facts the implementer cannot look up.
Reframing Vague Requirements
When you receive a vague requirement, translate it into specific success criteria before writing any spec content:
Vague: "Make the invoice parser more reliable"
Reframed success criteria:
- Parser correctly extracts IBAN from 95% of Swedish invoice formats
- Parser correctly extracts total amount from 98% of tested invoices
- Parser returns a structured error (not a panic) for unrecognized formats
- Processing time < 2 seconds for PDFs up to 10MB
→ Are these the right targets?
Phase 2: PLAN
With a validated spec, create a technical implementation plan:
- Identify major components and their dependencies
- Determine implementation order (foundations first)
- Note risks and unknowns
- Identify what can be built in parallel vs. what must be sequential
- Define verification checkpoints between phases
The plan should be reviewable: anyone should be able to read it and say "yes, that's the right approach" or "no, change X."
Load the planning skill for detailed task breakdown.
Phase 3: TASKS
Break the plan into discrete, implementable tasks. Load the planning skill for the full task breakdown methodology.
Each task must have:
- Acceptance criteria
- Verification step (test command, build, manual check)
- File count estimate (no task should touch more than ~5 files)
Phase 4: IMPLEMENT
Execute tasks one at a time. For each task:
- Load
tddskill — write failing tests first - Implement minimal code to pass
- Load
clean-codeskill — refactor
Keeping the Spec Alive
- Update when decisions change — spec first, then code
- Update when scope changes
- Commit the spec — it belongs in version control
- Reference the spec in PRs — link to the section each PR implements
Common Rationalizations
| Rationalization | Reality |
|---|---|
| "This is simple, I don't need a spec" | Simple tasks still need acceptance criteria. A two-line spec is fine. |
| "I'll write the spec after" | That's documentation, not specification. The spec's value is forcing clarity before code. |
| "The spec will slow us down" | A 15-minute spec prevents hours of rework. |
| "Requirements will change anyway" | That's why the spec is a living document. |
| "The user knows what they want" | Even clear requests have implicit assumptions. The spec surfaces those. |
Verification
Before starting implementation:
- Spec covers all six core areas
- Mathias has reviewed and approved the spec
- Success criteria are specific and testable
- Boundaries (Always/Ask First/Never) are defined
- Open questions are resolved or accepted as unknowns
- Spec is saved to a file in the repository
Brain MCP Integration
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: "spec-driven-dev", phase: "<phase-name>", final_status: "pass" | "fail" | "skip", message: "<one-line summary>", project_root: "<absolute path>"}
Phases for this skill: specify, plan, tasks, implement
Status semantics:
pass— the phase's intended outcome was reached (gate passed).fail— the phase's intended outcome was NOT reached (gate blocked, rework required).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
problem-analysisskill for deep requirement understanding before speccing - Load
user-storiesskill to decompose the spec into stories - Load
planningskill for task breakdown - Load
feature-specskill once implementation begins, to scope individual features inside the project - Load
tddskill during implementation