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.md

GitHub

Spec

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) or github:<owner>/<repo>[/<subpath>] — never interpret it yourself; spec_dir.py --resolve does (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

  1. The repository does not answer it — and you say what you checked.
  2. The two answers lead to different documents, not different wording.
  3. 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:

  1. 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.
  2. 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.
  3. 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.
  4. Every slot is independently reviewable and revertible. A slot that cannot be merged on its own is not a slot.
  5. State what cannot be parallelised. If this is honestly one slot, say so.
  6. 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] — is opus. A sonnet slot is a claim that this spec is complete enough to execute without inventing. If you cannot make it honestly, the slot is opus and the reason goes in Risks.

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

  1. 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.

  2. 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). Fill i<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.

  3. 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 specDir set, 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, url and separate. Exit 4 is a credential: give the person the gh auth … command from message and stop this step. Then, in that repository — the code repository when separate is 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 switch in <checkout>: a sibling documentation clone is the person's working copy and may hold their own changes. The issue stays in the code repository; put url (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.

  4. Queue it only on "Publish and queue": add the ready label (orchestrator.readyLabel, default cs: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 from origin/<branch> only, so an unmerged spec is NO SPEC to 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.