spec · git:20261014.fea376c · 2026-10-14 · sha256 f6a0590ef8cf7eab

spec git:20261014.fea376cA

Immutable. This exact content never changes and is served at /api/v1/blob/f6a0590ef8cf7eab.

---
name: spec
description: Take a rough intent to a reviewed, implementation-ready OpenSpec change — explore first, generate proposal, design, tasks and delta specs via the OpenSpec CLI, then gate them through an artifact review. Stops at the human checkpoint, having written nothing but specs. Use when starting a new feature, refactor, or bug fix.
license: MIT
compatibility: Requires the openspec CLI. Node.js >= 18 for the bundled interlock and interlock-graph CLIs.
argument-hint: "[what you want to build] [--no-explore] [--brief <path>] [--continue] [--force-checkpoint] [--tdd] [--no-tdd]"
allowed-tools: Bash(openspec *) Bash(interlock *) Bash(interlock-graph *) Read Write Glob Grep AskUserQuestion
metadata:
  type: planning
  outputs:
    - openspec/changes/<name>/proposal.md
    - openspec/changes/<name>/design.md
    - openspec/changes/<name>/tasks.md
    - openspec/changes/<name>/decisions.md
    - openspec/changes/<name>/specs/**
---

Turn an intent into a reviewed change that is ready to implement — and then **stop**.

This skill writes specifications. It does not write code. By default it does not run `/interlock:ship` either: the gap between this skill and that one is the deliberate human checkpoint, because a spec is the cheapest place to catch a wrong idea, so that is where a person looks. `--continue` is an opt-in that trades that read for speed on changes a machine can prove are narrow — if passed, Read `${CLAUDE_SKILL_DIR}/continuity.md`. Without the flag, nothing here bridges the gap.

**This skill composes stock OpenSpec rather than replacing it.** Artifact scaffolding, ordering, templates and validation all come from the `openspec` CLI, which is the stable contract. What this adds is the discipline around it: evidence gates, explore handoff, invariant sweeps, a decision ledger, and a review before anyone writes code.

| Flag | Effect |
|------|--------|
| `--no-explore` | Skip the explore phase (intent is already sharp, or a brief exists) |
| `--brief <path>` | Use this explore brief; skip brief matching |
| `--tdd` | Force test-first task shape: `tasks.md` leads with a failing-test section, later sections make it green |
| `--no-tdd` | Force standard implementation-first shape; no leading red section. Never relaxes a bug fix's repro-first task |
| `--continue` | Advanced. After a clean artifact review, Read `${CLAUDE_SKILL_DIR}/continuity.md` — ask `interlock ready` whether the checkpoint may be skipped, and ship if it says yes |
| `--continue --force-checkpoint` | Opt back out mid-flight. Always stops at the checkpoint, whatever readiness would have said |

---

## 0. Pre-flight: the openspec CLI

```bash
openspec --version
```

If it prints `command not found` or exits non-zero, **stop here**: before asking for intent, before explore, before `openspec new change`, before any Write. Say in one line that the openspec CLI is missing, give the install command `npm install -g @fission-ai/openspec@latest` (Node.js 20.19.0 or newer), and point at `interlock doctor` for the full preflight. Do not install it yourself, and do not scaffold `openspec/changes/<name>/` by hand — the CLI owns those formats. `--no-explore` and `--brief` do not skip this step.

---

## 1. Establish intent

If the request is not clear enough to name a change, ask — open-ended, no preset options:

> "What change do you want to work on? Describe what you want to build or fix."

Derive a kebab-case name from the answer ("add user authentication" → `add-user-auth`). **Do not proceed without understanding what the user wants.**

### 1a. Classify: feature or bug fix

**bug-fix** when the request matches `fix`, `regression`, `broken`, `crash`, `500`, `error`, `hang`, `leak`, a stack trace, or an issue reference. **feature** otherwise.

**If bug-fix, gate the proposal on evidence.** Before creating any artifact, require both:

1. **Real log output or error message** — pasted, or a path to it.
2. **A reproduction** — a failing test path, or a command that triggers the bug.

Do not run `openspec new change` until both exist. If the user pushes back: *"A repro is required so the fix has a pass/fail signal — without it we're fixing a symptom."*

A bug-fix `tasks.md` must:
- Land the failing repro test as its **first** task.
- Constrain later tasks to the root cause only. No opportunistic renames, refactors or tidy-ups — those are a separate change.
- **"Root cause only" excludes unrelated cleanup. It does not exclude other readers of the same invariant.** If the root cause is a shared or transformed value — a normalized identity, a cache or dedup key, a canonicalized field — then every consumer still reading the raw form is part of this fix. Enumerate them per `${CLAUDE_PLUGIN_ROOT}/shared/INVARIANT-SWEEP.md` and give each a task. Fixing one call site and leaving its siblings on the raw form is exactly how the original bug survives its own fix.

#### Decide the task shape

Two shapes exist, and this picks the one §3 writes:

- **standard** — tests live inside the section that implements the behaviour they cover.
- **TDD** — a leading section of failing tests, then implementation that makes them pass.

Resolve in this order:

1. **`--tdd` or `--no-tdd` wins.** Both passed → ask which one. Do not guess between two explicit instructions that contradict each other.
2. **Neither passed → behaviour-critical work gets TDD, everything else gets standard.** Behaviour-critical means the change has a pass/fail core a test can pin before the code exists: a pure function, an algorithm, a parser, a transform, a state machine, or a shared or derived invariant of the kind `${CLAUDE_PLUGIN_ROOT}/shared/INVARIANT-SWEEP.md` sweeps. Plumbing, wiring, config, prose and scaffolding are not — those get standard. When a change is part core and part plumbing, judge the part the change is *about*, not the part with the most files.
3. **A bug fix is already test-first and stays that way.** §1a's repro-first task is not the TDD shape and is not negotiable. `--no-tdd` suppresses the leading red section for the rest of the fix and **never relaxes the repro**; `--tdd` on a bug fix adds nothing the repro rule did not already require.

Announce the call in one line before creating any artifact — the shape, and the reason:

> `task shape: TDD (pure transform with a pass/fail core) — section 1 is failing tests.`

**The shape is transient.** It is not written into `openspec/changes/<name>/` as a field and no later skill reads it back. What survives the run is the shape of `tasks.md` itself, which is the only thing ship consumes.

### 1b. Load the explore brief

Prefer a durable handoff over re-deriving discovery from chat. See `${CLAUDE_PLUGIN_ROOT}/shared/EXPLORE-BRIEF.md`.

1. `--brief <path>` given → read that file, skip matching.
2. Otherwise: `ls -t .claude/handoff/explore-*.md 2>/dev/null | head -5`
3. Pick the newest brief whose `## Meta` topic, slug, or `suggested_change_name` overlaps the request (or the newest one if only a single recent brief exists and the topic is vague).
4. **Read the matched brief first.** Prefer its recommendations over re-exploring; reopen code only for gaps or contradictions.
5. From `## Pending Clarifications`, ask only what genuinely blocks a correct proposal. Skip anything already covered by `## Assumptions Made` that the user does not contest.
6. If `suggested_change_name` is set and the user gave no name, use it.

No matching brief is fine — but if none exists and `--no-explore` was not passed, **run `/interlock:explore --autonomous` now** and require a decision-oriented brief: conclusions, a recommended direction, options with a winner, assumptions, pending clarifications, and only cited `file:line` spans. Do not ask for an inventory of every file touched. Use the brief it writes. A spec built on unexplored ground is the most expensive kind to get wrong.

### 1c. Check the ground is current

```bash
interlock drift --json
```

**Never blocks, never branches.** It exits 0 whatever it finds; read it and move on.

Two findings matter before exploring, because both mean `openspec/specs/` describes something other than the code you are about to spec against:

- **`unarchived`** — changes that finished but were never archived, so the living specs never received their deltas. Name them and the command (`openspec archive <name>`).
- **`stale.broken`** — a living spec cites a file that no longer exists. Evidence, not inference, and worth one line even when it looks unrelated: a spec pointing at a deleted file is the clearest sign the area you are about to spec has already moved.

Then continue. Do not stop, do not ask, and do not archive on the user's behalf: archiving rewrites the living specs, which is a decision that belongs to whoever merged the change.

`stale.aging` is inferred from dates and rarely worth interrupting a spec run for — mention it only if it names a spec directly relevant to this change. Orphan detection needs a diff, which this skill does not have; ignore that field here.

---

## 2. Create the change

```bash
openspec new change "<name>"
```

If a change with that name already exists, ask whether to continue it or start a new one.

---

## 3. Generate artifacts, CLI-driven

```bash
openspec status --change "<name>" --json
```

Parse `applyRequires` (artifacts needed before implementation), `artifacts` (each with status and dependencies), and the path context — `planningHome`, `changeRoot`, `artifactPaths`, `actionContext`. **Use those paths; never assume repo-relative locations.**

Then loop, in dependency order, over every artifact that is `ready`:

```bash
openspec instructions <artifact-id> --change "<name>" --json | node "${CLAUDE_PLUGIN_ROOT}/skills/spec/scripts/compact-instructions.mjs"
```

Filter each ready artifact independently; do not dump the full instructions response or prefetch every artifact. The compact response carries `instruction` (schema guidance), `template` (the structure to fill), `resolvedOutputPath`, and non-empty `context` / `rules`.

- Read completed dependency artifacts before writing a new one.
- Write to `resolvedOutputPath`, using `template` as the structure.
- **`context` and `rules` constrain you; they are not content.** Never copy `<context>`, `<rules>` or `<project_context>` blocks into the artifact.
- Re-run `openspec status --change "<name>" --json` after each write; stop when every `applyRequires` artifact is `done`.

### Gates while generating

- **Invariant sweep.** If the change touches a shared or derived value, `tasks.md` must include a task per consumer. Use `interlock-graph consumers <symbol>` for the structural pass, then grep for string-keyed readers. See `${CLAUDE_PLUGIN_ROOT}/shared/INVARIANT-SWEEP.md`.
- **Pinned versions.** If the design names a dependency without a version, ask for it. **Do not gap-fill a version number** — a plausible-looking wrong pin is worse than an open question.
- **Genuine ambiguity.** Ask when it blocks a correct proposal. Otherwise take a reasonable default to keep momentum. Either way, write the row — see below.

### Task shape for ship

`tasks.md` is the wave plan. A numbered `## N.` section is one wave. Follow these four rules when writing it:

1. Default grouping is the numbered section (`1.x` share a wave, `2.x` share the next).
2. Sequential same-file work is one checkbox — not one checkbox per TDD beat, comment move, or trap on the same production path.
3. A task that needs another task's output when the two edit **different files** is a `dependsOn` edge, not a new section. The classifier reads the task text and emits that edge; write the dependency into the task so it can. Incrementing the section to order one cross-file dependency serializes every task in the new section that is independent of it — the edge orders only the dependent task, so its independent siblings keep sharing a batch.
4. Start a new numbered section only when a later set of tasks needs the previous section's output to already exist. Independent files that do not need each other belong in the same section so they can share a batch.

These rules apply even when `openspec instructions tasks` injects no `rules.tasks`. Path collisions inside a section are the planner's problem (`paths`), and a cross-file dependency is an edge (`dependsOn`) — neither is a reason to increment the section number.

**Never split a section to buy parallelism.** The planner packs low-tier siblings in one section into a single lane run by one agent, and a small change may ship solo — the whole change implemented in order by one agent — so a section written wide does not become a spawn per checkbox. Write the sections the work actually has; how many agents run them is the planner's decision, made under caps `interlock limits` publishes.

#### Where the tests go

The four rules above are about grouping and hold under either shape. The shape decided in §1a decides only where the tests sit inside that grouping.

- **standard** — unchanged from the four rules: a test checkbox lives in the section that implements the behaviour it covers.
- **TDD** — `## 1.` is the failing-test wave. Title it **`Failing tests first`**, and have each later section that satisfies it say so: `(make §1 green)`. Cover the transform boundary the invariant sweep found, not just the happy path.

**Rule 2 still binds inside the red wave: the failing suite is one checkbox, not one checkbox per TDD beat.** A leading test section is a wave, not a licence to split. One checkbox per assertion serializes the change and buys nothing — the anti-split rule was never about disliking TDD, it was about the planner packing what you wrote.

**The heading is the contract with ship, so write it exactly.** `/interlock:ship` reads `Failing tests first` off the `## 1.` heading and plans that section as a leading red wave: its tests run **first** instead of deferring to the trailing test wave, in solo mode too. A reworded heading is not a smaller signal, it is no signal — the change plans as an ordinary one, and nothing fails to tell you so except the plan preview, which will not say `RED`.

Two things follow from how ship treats that wave:

- **The red wave's own inter-wave check is skipped, by design.** The suite is supposed to be failing there, so checking it would spend the run's fix attempts trying to repair tests that were just written on purpose. The **next** section's check is the gate: by then the implementation exists, and a still-red suite is a real failure. So the section that makes §1 green must be a section, not a later batch of §1 — which the numbered-section rule already gives you.
- **A dependency from an implementation task onto the red wave is now legal**, where it used to fail the plan outright. Prefer to leave it out anyway: `## 1.` already runs before `## 2.`, so `(make §1 green)` as prose is enough, and an edge buys ordering the sections had already.

One consequence worth stating at the checkpoint: because the red wave's check is skipped, a run cannot tell a genuinely failing suite from one that already passed. Tests that pass before the implementation exists are not pinning new behaviour — that is a review question, and §6 flags it as one.

### 3a. The decision ledger

Write `openspec/changes/<name>/decisions.md`. The format, the two classes, and what makes a row invalid are the contract in `${CLAUDE_PLUGIN_ROOT}/shared/DECISION-LEDGER.md` — read it and follow it; do not restate it here or invent a third class.

What this skill owes the ledger:

1. **Every ambiguity that surfaces mid-flight becomes a row, before it is acted on.** Never resolve an ambiguity silently in the design prose. A decision that exists only in chat does not exist.
2. **Carry forward what explore wrote.** Transcribe the rows from the brief's `Pending Clarifications` and `Assumptions Made` — ids, questions and classes as explore assigned them — as the first write to this file. Do not start a fresh ledger and do not renumber.
3. **`agent_resolved` is a claim, and `interlock ledger` audits all three parts of it.** It requires a written resolution, evidence a reader can follow, *and* the row's id appearing in `design.md`. Each is checked: an empty resolution, an empty evidence cell, a hedge that asserts nothing (`obvious`, `standard practice`, `see above` — the whole cell, not a word inside a real citation), or an id `design.md` never mentions all make the row invalid, and an invalid row blocks exactly like `needs_human`. A missing `design.md` makes every reference unresolvable rather than resolving all of them.
4. **When in doubt, `needs_human`.** A wrong `agent_resolved` ships an unreviewed product decision; a needless `needs_human` costs one question.

Check the file rather than trusting that you wrote it well:

```bash
interlock ledger "<name>" --json
```

It exits non-zero when the ledger blocks — a `needs_human` row remains, a row is invalid, or **the ledger is missing entirely**. An absent `decisions.md` is a failure of the audit, not an empty result: "no rows" reads as "nothing needs a human", which is the one conclusion a file that was never written cannot support. The output distinguishes *missing* from *present and empty* from *unparseable*. In the JSON, `blocking` is the verdict; report the `needs_human` entries of `rows` by `id` and `question` and the entries of `invalidRows` by `reason`, and read `missing` or `unparseable` as why a ledger with no rows still blocks.

A blocking ledger with rows in it is not a failure of this skill: an honest blocking ledger is the correct output for a change that hit a real product question. It is only a failure if the questions never got written down.

---

## 4. Validate

```bash
openspec validate
interlock validate "<name>" --json
```

`openspec validate` checks schema conformance. `interlock validate` checks the flow's own preconditions — all three artifacts present and non-empty, `tasks.md` actually containing checkbox tasks — and exits non-zero when the change is not implementable. Fix anything it reports before continuing. In the JSON, `ready` is the verdict and `problems` is the list to fix; an output carrying `error` and `candidates` instead of `ready` means the name did not resolve.

---

## 5. Gate: artifact review

Run `/interlock:review-artifacts` against the change.

- **Blockers → HALT.** Report them and stop. Do not proceed to ship, and do not "fix and continue" silently: a blocker at this gate means the spec was wrong, and the user should see that.
- **Clean → record it and stop at the checkpoint.**

```bash
# blockers found
interlock autonomy record review-artifacts --blockers <n>
# clean
interlock autonomy clean review-artifacts explore spec
```

A clean gate credits `explore` and `spec` too; a blocker resets them. That is what stops this skill earning autonomy by emitting shallow specs — the downstream gate blames whoever produced the bad artifact.

**`<n>` is `autonomyOutcome.blockers` from the gate's JSON**, carried over from the review — never a count made in prose, including the review's own `SUMMARY:` line. Continuity takes the findings file itself, so keep its path.

---

## 6. Hand off to the human

This is where the default run ends. Close with:

- Change name and artifact paths
- What the change does, in two or three lines
- Assumptions made and any pending clarifications
- Task count, and the wave shape if it is obvious
- The task shape, and — **when it is TDD** — that ship will run section 1's failing tests first and skip that wave's check, so nobody is told whether the suite was genuinely red. Ask the reviewer to confirm the leading tests actually fail against the current code; a test that passes before the implementation exists pins nothing.
- Anything the artifact review flagged below blocker severity

Then say plainly: **review the spec, and run `/interlock:ship` when it looks right.**

If the user passed `--continue`, Read `${CLAUDE_SKILL_DIR}/continuity.md` and follow it. Do not print a spec `GOAL MET` line on that path — if readiness launches ship, `finish()` prints `GOAL MET: interlock ship`.

Without `--continue`, the run ended here. The checkpoint waits for a human, so tell them it is waiting:

```bash
interlock notify checkpoint "<change-name>"
```

That is a no-op that prints `push: not configured` and exits 0 when no topic is set, so run it unconditionally and never branch on the result.

Then print this greppable line so a session `/goal` can stop instead of wandering into implementation:

`GOAL MET: interlock spec stopped at the checkpoint.`

Do not run ship. Do not offer to "just start on the first task". The checkpoint is the point. Do not call `/goal` from this skill — a `/goal` does not skip the checkpoint and does not start ship. Continuity's only opt-in skip is `--continue` plus `interlock ready`.