Install
$ agentstack add skill-belchman-claude-skills-write-a-spec ✓ scanned · ✓ verified — works with Claude Code, Cursor, and more.
Security review
✓ PassedNo issues found. Passed automated security review. · v0.1.0 How review works →
- ✓ Prompt-injection patterns
- ✓ Secret / credential exfiltration
- ✓ Dangerous shell & filesystem operations
- ✓ Untrusted network calls
- ✓ Known-malicious package signatures
What it can access
- ✓ Network access No
- ✓ Filesystem access No
- ✓ Shell / process execution No
- ✓ Environment & secrets No
- ✓ Dynamic code execution No
From automated source analysis of v0.1.0. “Used” means the capability is present in the source — more access means more to trust, not that it’s unsafe.
About
write-a-spec
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:
- What changes — data model, API shape, files touched per lane, tests required, risks.
- Where the lane allowlists come from — the spec's
File-by-file change listis the sourcework-issues-lib.sh::allowlist_forparses (via/feature's orchestrator) to scope each lane's/work-issuesinvocation.
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](../../../../docs/plans/feature-factory.md) §C ("Spec → allowlist format") + ADR [0001-fenced-paths-blocks-for-lane-allowlists](../../../../docs/adr/0001-fenced-paths-blocks-for-lane-allowlists.md). Project glossary: [CONTEXT.md](../../../../CONTEXT.md).
When to run
- Step 4 of the
/featurechain — orchestrator hands the approved story + research dump in. - Direct user invocation —
/write-a-spec issues/NNN-foo.mdworks the same way; missing research dump is handled (see below). - After a story is approved at
/feature's Checkpoint 1 but before the user reviews the spec at Checkpoint 2.
Inputs (priority order)
- Path to an
issues/NNN-*.mdfile (the approved story). Required. - Path to
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.mdat the repo root — required to look up lane vocabulary. Read its## Lane boundariessection if present (see "Lane discovery").ARCHITECTURE.mdat 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.
Output
| Source | Output | |---|---| | issues/NNN-.md | issues/NNN-.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.
Lane discovery
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-.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.
Single-lane handling
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.
Missing-research handling
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 /feature for 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.
Process
- Read input. Issue, research dump (if present),
CLAUDE.md,ARCHITECTURE.md. - Resolve lane vocabulary from
CLAUDE.md## Lane boundaries. - Identify the deliverable. What artifact is produced, where it lives, in what shape.
- Draft each template section. Use the template below verbatim — sections that are genuinely N/A still appear with body
_N/A — _(forces conscious consideration; doesn't silently skip a category the builder might have missed). - Self-critique pass. For each
pathsline: is this an actual literal path the builder will touch, or a placeholder? For eachTests requiredbullet: is this a testable behavior with an observable outcome, or a wish? - Stop check. All seven template sections present; every lane that has changes has an H3 + fenced
pathsblock; every path is literal (no globs); every test bullet names an observable behavior. - Write the file at
issues/NNN-.spec.md. - Print the closing one-liner:
Next: review the spec at issues/NNN-.spec.md, then run write-a-rubric and feature.sh continue --accept (or feed to /feature's Checkpoint 2 if orchestrated).
Spec template
Use this template verbatim. All seven H2 sections must appear in the produced spec, in this order.
````markdown
— Spec
Source story: issues/NNN-.md Source research: issues/research/NNN-.md (or _N/A — not run_)
Deliverable
Data model
If genuinely no data model change: _N/A — _.
API
If genuinely no API change: _N/A — _.
File-by-file change list
src/api/handlers/.ts
src/services/.ts
tests/services/.test.ts
web/components/.tsx
web/hooks/.ts
tests/components/.test.tsx
If the feature is single-lane, omit the unused lane's H3 entirely — do not emit an empty paths block.
Tests required
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
Tenant/timezone concerns
If genuinely no concerns: _N/A — _. 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."
````
Paths block convention
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:
- Lane labels are H3 headings of the form
###— a single bareword fromCLAUDE.md's## Lane boundaries, with no markdown styling. Specifically: - No bold:
### **Backend**is wrong; parser sees lane name**Backend**, never matches lookup forBackend. - No links:
### [Backend](https://...)is wrong for the same reason. - No trailing
###(CommonMark ATX-close):### Backend ###puts###inside the lane name. Use### Backendonly. - No leading indentation:
### Backendis not recognised as an H3 by the parser. - No parenthetical annotations:
### Backend (cron)becomes lane nameBackend (cron)— fails the lookup. - Case-sensitive — copy from
CLAUDE.mdverbatim. The parser does exact-string match against the lowercased lane namefeature.shpasses in (per the recentlane_normalizefix). If## Lane boundariesdeclaresbackend(lowercase) and you write### Backend(capitalized), the parser sees no match — lane is skipped silently with statusempty. Use the same case as the bareword in## Lane boundaries, character-for-character. When in doubt, copy-paste from the source. - Trailing whitespace IS tolerated (parser trims) — but leading whitespace anywhere in the heading is not.
- Paths blocks are fenced code blocks whose info string is literally
paths(lowercase, no attributes). Variants the parser silently rejects:\\\paths bash,\\\Paths,\\\paths-list,~~~paths` (tilde fence), indented fences. - Each line inside a
pathsblock 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. - Paths are repo-root-relative. Never write just a basename (
crap.py) when the file lives atplugins/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 apathsblock, the file should exist at/today (or be a path you're explicitly creating — note "new" in surrounding prose so a reader knows). - A path may appear in only one lane within a single spec.
route_findingsfails loud on cross-lane duplicates (per plan §C and ADR 0001). - Lane sub-sections appear directly under the
File-by-file change listH2 with theirpathsblock following (blank lines OK, prose OK between H3 and fence). - Self-check before writing: after drafting, run
grep -c '^`paths$'— 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. Rungrep '^### '— every line must be exactly###with no styling. Runawk '/^\\\paths$/,/^\\\$/' | grep -v '^\\\'to dump every path line, andtest -e /` each one — anything that fails the existence check is either misnamed or genuinely new (which you should call out in surrounding prose).
Worked example
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.
````markdown
File-by-file change list
backend
src/api/handlers/invoices.ts
src/services/invoice-reminder.ts
src/jobs/reminder-job.ts
tests/services/invoice-reminder.test.ts
frontend
web/components/billing/ReminderCard.tsx
web/hooks/useInvoiceReminders.ts
tests/components/ReminderCard.test.tsx
````
What this does right:
- Two distinct H3 lanes, each with its own
pathsblock. - Every line is a literal path (no
src/api/**/*.ts). - Test paths live in the lane that owns them — backend tests under
### backend, frontend tests under### frontend. - No path appears in both lanes.
- H3 labels match
CLAUDE.md ## Lane boundariescharacter-for-character (here: lowercase).
Anti-patterns
- Globs in
pathsblocks.src/api/**/*.tsis not a literal path. The parser will reject the spec. List every file explicitly. - Same path in multiple lanes.
src/shared.tscannot appear under both### Backendand### 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. - Taste-based criteria in
Tests required. "looks good", "is clean", "well-structured", "is elegant" — unscorable. Replace with an observable: "function X returns Y given Z." - Conversation-dependent items. "as we discussed", "the case Sarah mentioned", "what we talked about Monday" — the rubric writer and the builder can't read the conversation. Make it explicit in the spec.
- File paths in
Tests required. That's the builder's call. The spec says what behavior must be tested; the builder picks the file layout. - Coverage percentages in
Tests required. "80% coverage" doesn't tell the builder what to test. Behavior list does. - Inventing lane labels not in CLAUDE.md. The orchestrator's parser will get
lane-not-foundand skip the lane silently. Match the vocabulary. - Refusing on missing research. Direct-user invocation is supported. Add a Risks bullet and proceed.
Hard rules
- One spec file per issue. Output path:
issues/NNN-.spec.md(sidecar toissues/NNN-.md). - Do not modify the source issue or PRD or any existing file.
- This skill produces text only. It does not call any external API or service.
- All seven template H2 sections (
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 — _as the body. - Lane H3 headings must match labels from
CLAUDE.md's## Lane boundaries. If no such section exists, ask the user once for the project's lane vocabulary; don't invent. - If the source story is too thin to produce a meaningful spec (e.g. acceptance criteria are vague placeholders), ask the user — don't fabricate behavior. If no user is available (AFK / autonomous run), write a sibling
QUESTIONS.mdlisting what you'd have asked, then produce a best-guess spec with assumptions clearly marked.
Source & license
This open-source skill is cataloged on AgentStack and links to its original source — we do not rehost the code.
- Author: belchman
- Source: belchman/claude-skills
- License: MIT
Install and usage instructions live in the source repository linked above.
Reviews
No reviews yet — be the first.
Write a review
Versions
- v0.1.0 Imported from the upstream source.