Turn an approved story (issues/NNN-*.md) plus an optional research dump into a technical spec sidecar at issues/NNN-*.spec.md — data model, API, file-by-file change list per lane (as fenced ```paths blocks), behavior-list of tests required, and risks. The spec drives /feature's lane allowlist; lane H3 headings must match labels from CLAUDE.md ## Lane boundaries. Sidecar only — does not modify source files. Use when the user wants to write a technical brief from a story or issue, or whenever /feature dispatches the Spec Writer phase. Triggers: /write-a-spec, "write a spec", "technical spec", "spec from this issue", "draft a technical brief".
75
94%
Does it follow best practices?
Run evals on this skill
Adds up to 20 points to the overall score
View guide
Passed
No findings from the security scan
A spec is the technical-brief sidecar to an issue. It sits between the approved story (what the user wants) and the lane builders (/work-issues --lane … via /feature), and it must answer two questions:
File-by-file change list is the source work-issues-lib.sh::allowlist_for parses (via /feature's orchestrator) to scope each lane's /work-issues invocation.A vague spec produces a confused builder and a broken lane scope. This skill exists to keep specs sharp enough that the parser can deterministically extract per-lane allowlists and a builder can implement without re-asking.
Source-of-truth for the protocol: docs/plans/feature-factory.md §C ("Spec → allowlist format") + ADR 0001-fenced-paths-blocks-for-lane-allowlists. Project glossary: CONTEXT.md.
/feature chain — orchestrator hands the approved story + research dump in./write-a-spec issues/NNN-foo.md works the same way; missing research dump is handled (see below)./feature's Checkpoint 1 but before the user reviews the spec at Checkpoint 2.issues/NNN-*.md file (the approved story). Required.issues/research/NNN-*.md (the research dump from /feature's Explore subagent pass). Optional. If missing, proceed and add a Risks bullet — see "Missing-research handling" below.CLAUDE.md at the repo root — required to look up lane vocabulary. Read its ## Lane boundaries section if present (see "Lane discovery").ARCHITECTURE.md at the repo root — optional, read if present to ground module / dependency claims.If invoked from an issue, also read issues/prd.md for parent context if it exists.
| Source | Output |
|---|---|
issues/NNN-<slug>.md | issues/NNN-<slug>.spec.md (sidecar; mirrors filename) |
The output is a new file. The skill does not modify the source issue, the PRD, or any other existing file. The only Write-tool use is to create the spec at the output path above.
Before drafting the File-by-file change list, read CLAUDE.md and look for a ## Lane boundaries section. That section is the canonical lane vocabulary for the project (e.g. Backend, Frontend, CLI, Worker). Lane H3 headings in the spec MUST match labels defined there — the orchestrator's parser compares H3 text to allowlist names character-for-character.
If CLAUDE.md has no ## Lane boundaries section, ask the user once for the project's lane vocabulary, then proceed. Don't invent lanes silently.
AFK fallback (no user available — /feature running headlessly, claude -p invocation, etc.): instead of stalling on "ask the user," write a sibling issues/NNN-<slug>.QUESTIONS.md file listing the lane-vocabulary question explicitly. Use a best-guess lane vocabulary derived from top-level directory names in the repo (e.g. src/api → Backend, web/ → Frontend, cmd/ → CLI) AND clearly mark this guess in the spec's Risks section: "Lane vocabulary inferred from top-level dirs; CLAUDE.md ## Lane boundaries is missing. Confirm before any lane build runs."
If CLAUDE.md is missing entirely, treat it the same as "has no Lane boundaries section" — write QUESTIONS.md, infer from top-level dirs, mark in Risks.
If the feature only touches one lane (e.g. a backend cron job, a frontend-only UI tweak, a CLI subcommand), the spec omits the unused lane's H3 entirely — do not emit an empty paths block. The orchestrator's parser treats a missing lane as "skip this lane's invocation," which is the correct behavior for single-lane features. Empty paths blocks would be parsed as "this lane exists but has no edits," which is a different (and confusing) signal.
If the expected issues/research/NNN-*.md file is missing (e.g. the user invoked the skill directly without running /feature's Explore pass), proceed anyway. Add an explicit Risks bullet:
No research pass performed; consider running
/featurefor fuller context. The file/module assumptions in this spec are based on the issue text and CLAUDE.md only.
Direct user invocation must work — this skill is not exclusively coupled to /feature. Do not refuse to draft a spec because the research dump is absent.
CLAUDE.md, ARCHITECTURE.md.CLAUDE.md ## Lane boundaries._N/A — <one-sentence reason>_ (forces conscious consideration; doesn't silently skip a category the builder might have missed).paths line: is this an actual literal path the builder will touch, or a placeholder? For each Tests required bullet: is this a testable behavior with an observable outcome, or a wish?paths block; every path is literal (no globs); every test bullet names an observable behavior.issues/NNN-<slug>.spec.md.Next: review the spec at issues/NNN-<slug>.spec.md, then run write-a-rubric and feature.sh continue <id> --accept (or feed to /feature's Checkpoint 2 if orchestrated).Use this template verbatim. All seven H2 sections must appear in the produced spec, in this order.
# <Feature title> — Spec
Source story: `issues/NNN-<slug>.md`
Source research: `issues/research/NNN-<slug>.md` (or `_N/A — not run_`)
## Deliverable
- <What artifact is produced, where it lives, in what format. Name a concrete path / object / command output.>
## Data model
- <Schema changes: new columns, new tables, new indexes, migrations. One bullet per change.>
_If genuinely no data model change: `_N/A — <one-sentence reason>_`._
## API
- <New / changed endpoints. For each: method + path + request shape + response shape + status codes.>
_If genuinely no API change: `_N/A — <one-sentence reason>_`._
## File-by-file change list
### <Lane 1, e.g. Backend>
```paths
src/api/handlers/<name>.ts
src/services/<name>.ts
tests/services/<name>.test.ts
```
### <Lane 2, e.g. Frontend>
```paths
web/components/<name>.tsx
web/hooks/<name>.ts
tests/components/<name>.test.tsx
```
_If the feature is single-lane, omit the unused lane's H3 entirely — do not emit an empty `paths` block._
## Tests required
- <one bullet per testable behavior — verb + observable outcome>
- <e.g. `over-limit-minute returns 429 + Retry-After header`>
- <e.g. `enterprise-plan key bypasses rate limiter — verified by 100 reqs without 429`>
_Each bullet becomes one criterion in the downstream rubric (`write-a-rubric`). NOT test file paths and NOT coverage percentages — behavior-level only._
## Risks & open questions
- <Known unknowns, things that need a human call, anything that could change scope mid-build.>
## Tenant/timezone concerns
- <Multi-tenant isolation: does this leak data across tenants? Timezone: does this break in non-UTC?>
_If genuinely no concerns: `_N/A — <one-sentence reason>_`._ Most features have at least one concern worth pinning here, even if "no — single-tenant only" or "no — internal admin tool, no user-facing TZ."The File-by-file change list section's lane subsections must follow this format strictly — work-issues-lib.sh::allowlist_for parses it character-by-character. The parser fails silently (empty output + exit 0) on most mismatches, treating them as "lane not found / skip" — so a typo or formatting deviation produces a silently-skipped lane, not an error:
### <Bareword> — a single bareword from CLAUDE.md's ## Lane boundaries, with no markdown styling. Specifically:
### **Backend** is wrong; parser sees lane name **Backend**, never matches lookup for Backend.### [Backend](https://...) is wrong for the same reason.### (CommonMark ATX-close): ### Backend ### puts ### inside the lane name. Use ### Backend only. ### Backend is not recognised as an H3 by the parser.### Backend (cron) becomes lane name Backend (cron) — fails the lookup.CLAUDE.md verbatim. The parser does exact-string match against the lowercased lane name feature.sh passes in (per the recent lane_normalize fix). If ## Lane boundaries declares backend (lowercase) and you write ### Backend (capitalized), the parser sees no match — lane is skipped silently with status empty. Use the same case as the bareword in ## Lane boundaries, character-for-character. When in doubt, copy-paste from the source.paths (lowercase, no attributes). Variants the parser silently rejects: \``paths bash, ```Paths, ```paths-list, ~~~paths` (tilde fence), indented fences.paths block is one literal file path — no globs (*, **, ?, [), no brace expansion ({a,b}), no shell metacharacters. The parser rejects all of these with exit 1 and stderr names the offender.crap.py) when the file lives at plugins/agentic-engineering/skills/crap/crap.py. The lane builder concatenates the path to the repo root and tries to edit; a bare basename either fails to locate the file or creates a new file at the wrong place. Even if the upstream issue/PRD used a shorthand basename, the spec MUST expand it to the full repo-root-relative path. Self-check: for each line in a paths block, the file should exist at <repo_root>/<that-line> today (or be a path you're explicitly creating — note "new" in surrounding prose so a reader knows).route_findings fails loud on cross-lane duplicates (per plan §C and ADR 0001).File-by-file change list H2 with their paths block following (blank lines OK, prose OK between H3 and fence).grep -c '^```paths$' <spec> — the count MUST equal the number of lanes you intended. If you intended 2 lanes but the count is 1, you have a fence typo. Run grep '^### ' <spec> — every line must be exactly ### <BarewordLabel> with no styling. Run awk '/^\``paths$/,/^```$/' | grep -v '^```'to dump every path line, andtest -e <repo_root>/` each one — anything that fails the existence check is either misnamed or genuinely new (which you should call out in surrounding prose).Assumes the project's CLAUDE.md ## Lane boundaries declares backend and frontend (lowercase). If your repo declares them as Backend/Frontend (or api/web, or any other case/name), substitute accordingly — the H3 must match CLAUDE.md character-for-character, including case.
## File-by-file change list
### backend
```paths
src/api/handlers/invoices.ts
src/services/invoice-reminder.ts
src/jobs/reminder-job.ts
tests/services/invoice-reminder.test.ts
```
### frontend
```paths
web/components/billing/ReminderCard.tsx
web/hooks/useInvoiceReminders.ts
tests/components/ReminderCard.test.tsx
```What this does right:
paths block.src/api/**/*.ts).### backend, frontend tests under ### frontend.CLAUDE.md ## Lane boundaries character-for-character (here: lowercase).paths blocks. src/api/**/*.ts is not a literal path. The parser will reject the spec. List every file explicitly.src/shared.ts cannot appear under both ### Backend and ### Frontend. If you need a shared file, that's a design problem — the file probably wants splitting, or one lane owns the file and the other reads from it.Tests required. "looks good", "is clean", "well-structured", "is elegant" — unscorable. Replace with an observable: "function X returns Y given Z."Tests required. That's the builder's call. The spec says what behavior must be tested; the builder picks the file layout.Tests required. "80% coverage" doesn't tell the builder what to test. Behavior list does.lane-not-found and skip the lane silently. Match the vocabulary.issues/NNN-<slug>.spec.md (sidecar to issues/NNN-<slug>.md).Deliverable, Data model, API, File-by-file change list, Tests required, Risks & open questions, Tenant/timezone concerns) must appear in the output. Sections that are genuinely N/A use _N/A — <one-sentence reason>_ as the body.CLAUDE.md's ## Lane boundaries. If no such section exists, ask the user once for the project's lane vocabulary; don't invent.QUESTIONS.md listing what you'd have asked, then produce a best-guess spec with assumptions clearly marked.77a9e6b
If you maintain this skill, you can claim it as your own. Once claimed, you can manage eval scenarios, bundle related skills, attach documentation or rules, and ensure cross-agent compatibility.