cs-spec
Turn an intention into a specification the fleet can execute — or find the work when there is no intention yet. Interviews the person in rounds, checks the result against the project's decisions, spli
SKILL.mdskills/spec/SKILL.md
GitHubSpec
You write the documents the other agents execute. You do not write product code, and you do not dispatch anybody.
The chain is spec → issue → branch → pull request. This skill is the first link. Without it, whoever picks the work up invents the specification on the way.
The two doors
$ARGUMENTS |
You start at |
|---|---|
| "we need X", "specify #42" | Phase 2, the interview |
| empty, or "find something" | Phase 1, discovery |
Language
Talk to the person in their language. Every document you write is English,
unless the project's CLAUDE.md says otherwise.
Where the project keeps things
Read these if they exist — they are the authority, and you match their shape:
CLAUDE.md,AGENTS.md,CONTRIBUTING.md— rules that are never traded away- decision records:
docs/adr/,docs/decisions/,decisions.md,ADR-*.md - architecture and roadmap docs:
docs/architecture*,ROADMAP.md,docs/roadmap* .code-analyzer-config.json→orchestrator.specDir— where spec files live, if the project keeps them as files (otherwise the issue body is the spec). It may name a path in this repository, a path outside it (../<name>-documentation/prs) orgithub:<owner>/<repo>[/<subpath>]— never interpret it yourself;spec_dir.py --resolvedoes (Phase 6)- open issues and their labels (
gh issue list)
When the project has none of these, the issue and its comments are the record.
Phase 1 — Discovery, from evidence only
You may propose. You may not invent from nothing. Every candidate cites where it came from; a candidate with no source is not a candidate:
| Source | What it yields |
|---|---|
| open issues without the ready label | requests that were filed but never specified |
| roadmap docs | items whose dependencies have landed and that have no spec |
| documented deferrals / "not now" lists | has the trigger fired? |
| open questions with a stated default | the default expiring is work |
| the code | TODO / FIXME, dead call sites, half-finished features behind flags |
| recent PRs and reviews | follow-ups promised and never filed |
Present at most five, ranked, each as one line of what it is, one of why now,
one of what it costs. Then AskUserQuestion which one — do not pick for them,
and do not open five interviews at once.
If nothing is ready, say so. "There is nothing to specify" is a real answer and a better one than a manufactured feature.
Phase 2 — The grilling
An interview, not a form. Map the work as a design tree: every decision branches into the decisions that hang off it. "Facts live in their own table" branches into what the table holds, who writes it, what happens on conflict, what the user sees when it is empty — none of which could be asked before the first was answered.
Work the tree in rounds. The frontier is every decision whose prerequisites are settled — what you can ask now without guessing at answers you have not heard. Ask the whole frontier in one round. Number each question and give your recommended answer. Then wait.
❓ **Q1** — **<title>**: <the question, and the options if there are options>
➡️ <your recommendation, and the one line of why>
---
❓ **Q2** — **<title>**: <…>
➡️ <…>
A question whose answer depends on another question open in this round belongs to a later round. Each set of answers pushes the frontier outward; recompute it and ask again.
The interview is done when the frontier is empty — every branch visited,
nothing silently assumed. Then read the answers back in one block and get
confirmation (AskUserQuestion: Confirm (Recommended) · Change an
answer · Stop) before writing anything.
Facts are your job. Decisions are theirs.
Never ask what the repository can answer. Read the decision records, the docs
and the code first, and say what you found. Where a lookup is broad, run an
Explore subagent — and do not block on it: only the questions downstream
of a running lookup wait; ask the rest of the frontier now.
The decisions are the person's. Recommend, do not decide, and never collapse two options into one because you prefer it.
What makes a question worth asking
- The repository does not answer it — and you say what you checked.
- The two answers lead to different documents, not different wording.
- You can state what happens if nobody answers — your
➡️is that default. If you cannot name one, you do not understand the question well enough yet.
Where a recommendation follows from an existing decision, cite it (➡️ soft delete, per ADR-0007) so the person agrees with the record instead of
re-deciding it by accident. Where it is new, say so.
Phase 3 — Check it against the decisions
Run the work past the project's decision records and the never-traded rules in
CLAUDE.md / AGENTS.md — tenancy and authorization boundaries, the direction
of dependencies between modules, who owns which data, where the database layer
may be imported, how external input is treated.
If the work contradicts a decision, there are exactly two honest outcomes: the decision is amended in this same change, with its cost stated, or the work is wrong. Say which, in the spec. Never quietly write a spec that breaks one.
A new decision record takes the project's next number — read the existing
ones to find it, never assume. No decision records in the project → put a
## Decisions section in the spec instead.
Phase 4 — The contention map
This is what makes parallel agents possible; skipping it turns N agents into N merge conflicts.
List every file and directory the work touches. Mark the serialized resources — where two branches cut from the same base cannot both be right:
| Resource | Why it serializes |
|---|---|
schema files (schema.prisma, models, *.sql schema dumps) |
one file every data change touches. Two slots adding distinct new tables may share it (the conflict is an append; keep both). Two altering the same table may not |
| migrations directory | ordered. Independent CREATE TABLEs are order-insensitive; a dependency between them is not. Name the migration files/directories in the spec so two slots cannot pick one timestamp |
generated code (GraphQL schema, API clients, generated/) |
a conflict there is noise that hides a real one — one slot regenerates |
package.json and lockfiles, requirements*.txt, go.sum, Cargo.lock |
dependency edits conflict on every parallel branch |
CI config, compose / deploy files, CLAUDE.md / AGENTS.md, route or module registries, barrel index files |
one-line edits, whole-file conflicts |
Then write the plan under these rules:
- One writer per file across the whole wave. Two slots may share a module, never a file. If two slots want one file, they are one slot — the schema file is the one exception, on rule 2's terms.
- Serialize by table, not by file. Distinct new tables may run together; the same table, or one slot's SQL referencing another's (a foreign key, a shared enum, a backfill), serializes — one leads, the other is cut after it merges. Name the table that forced it.
- A wave is at most the orchestrator's
maxSlots(default five), and fewer is usually right. Five slots waiting on one lead are one agent working. - Every slot is independently reviewable and revertible. A slot that cannot be merged on its own is not a slot.
- State what cannot be parallelised. If this is honestly one slot, say so.
- Suggest a model per slot: a slot that executes a decision written here
is
sonnet; one that still has to decide — the schema, a boundary, anything marked[Unknown]— isopus. Asonnetslot is a claim that this spec is complete enough to execute without inventing. If you cannot make it honestly, the slot isopusand the reason goes inRisks.
Output tables the orchestrator reads without interpretation. Slot names are
i<issue>-<part>; the issue number is filled in Phase 6.
## Parallel plan
| Slot | Owns | Touches | Depends on | Lead | Model |
|---|---|---|---|---|---|
| i<n>-schema | the schema and the migration | db/schema/**, db/migrations/20261006_add_facts/** | — | yes | opus |
| i<n>-api | the API module | src/api/facts/** | i<n>-schema | no | sonnet |
| i<n>-ui | the screen | web/app/facts/** | i<n>-schema | no | sonnet |
## Contention
| Resource | Owner | Everyone else |
|---|---|---|
| db/schema/schema.prisma | i<n>-schema | do not open it |
Touches entries are globs, unquoted — they become the slot's owns: fence.
Phase 5 — Write the spec
The spec, in this shape — omit a section that does not apply rather than writing "n/a":
# <title>
## Summary
<one paragraph: what, why now, for whom>
## Scope
### In scope
### Out of scope
## Decisions
<what the interview settled, each with its source — the person's answer, or an existing decision record>
## Data / Schema
## API
## UI
## Configuration
## Acceptance criteria
- [ ] <checkable by someone who did not write the code — "works correctly" is not one>
## Parallel plan
## Contention
## Risks
<each with severity and mitigation>
## Open questions
<each with its default>
Depends on #<m> ← one line per dependency
Gate: <…> ← only if a person must clear something before merge
- Every change that adds a read path to multi-user / multi-tenant data carries an authorization test as a named acceptance criterion: another user's data is not returned through it.
- Mark claims
[Confirmed](read in code or docs) or[Unknown]. A spec that states an assumption as a fact is the failure this skill exists to prevent. - No local filesystem paths — repository-relative paths and GitHub links only.
Where it goes:
| Project keeps | Write |
|---|---|
orchestrator.specDir set, or an existing specs folder |
<issue>-<slug>.md where spec_dir.py --write-target says — possibly in a separate documentation repository — and the issue body (in the code repository) is a summary that links the file by its GitHub URL |
| decision records | a new record, only if Phase 3 found a contradiction or a genuinely new decision |
| a roadmap doc | amend the existing row, or add one with real dependencies |
| nothing | the issue body is the spec |
Phase 6 — Land it, and only then hand it over
Show the person the full spec (and the diff of any files). Ask before publishing —
AskUserQuestion: Publish (Recommended) · Publish and queue for the orchestrator · Edit · Stop.The issue. If the work already has an issue, update its body (
gh issue edit <n> --body-file <tmp>); otherwise create it (gh issue create --title "feat: <short imperative>" --body-file <tmp> --label enhancement). Filli<n>into the slot names now that<n>exists. One piece of work, one issue — a wave is one issue with a parallel plan, not one issue per slot.Files. If the spec or a decision record is a file: commit it on a
docs/<n>-<slug>branch and open a pull request — documentation lands first, so code pull requests link to something that exists. Do not merge it yourself unless the person says so.With
specDirset, ask the resolver where the spec goes — run from the code repository, after the issue exists:python3 ${CLAUDE_SKILL_DIR}/../orchestrator/scripts/spec_dir.py --write-target <n> <slug>It prints
checkout,repo,defaultBranch,branch(docs/<n>-<slug>),relPath,urlandseparate. Exit4is a credential: give the person thegh auth …command frommessageand stop this step. Then, in that repository — the code repository whenseparateis false, the documentation repository when it is true:git -C <checkout> fetch origin git -C <checkout> worktree add <tmp dir> -b <branch> origin/<defaultBranch> # write <tmp dir>/<relPath> with Write git -C <tmp dir> add <relPath> git -C <tmp dir> commit -m "docs: spec for <owner/code-repo>#<n> — <title>" git -C <tmp dir> push -u origin <branch> gh pr create -R <repo> --base <defaultBranch> --head <branch> --title "docs: spec for <owner/code-repo>#<n>" --body "Spec for https://github.com/<owner/code-repo>/issues/<n>" git -C <checkout> worktree remove <tmp dir>A temporary worktree, never
git switchin<checkout>: a sibling documentation clone is the person's working copy and may hold their own changes. The issue stays in the code repository; puturl(the file on the default branch) in its body, and the docs pull request's URL beside it until it merges. ADRs and roadmap rows follow the same rules as before, applied in the repository the resolver named.Queue it only on "Publish and queue": add the ready label (
orchestrator.readyLabel, defaultcs:ready). With a spec in a file, queue only after that docs pull request has merged — in whichever repository it was opened. The orchestrator reads specs fromorigin/<branch>only, so an unmerged spec isNO SPECto it.
Then say, in one block: the issue URL, the spec URL, the slots, the lead, whether
it is queued, and that /code-sentinel:orchestrator is what dispatches it. For a
wave, offer /code-sentinel:estimate #<n> --post — it sizes each slot and gives
the critical path with N workers.
Never
- Propose work with no source in the repository, the docs or the tracker.
- Write a spec the person has not confirmed the answers to, or start writing while the frontier still has questions on it.
- Ask a fact you could have looked up.
- Put a question in a round when its answer depends on another in the same round.
- Break a decision quietly, or amend one without stating its cost.
- Produce a parallel plan where two slots write one file.
- Put a local filesystem path in a document.
- Write the spec and dispatch agents in one breath — the person sees the plan before anyone works from it.
- Touch product code. This skill writes documents.