@skitterbyte/skitterspec 17.0.0 → 19.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (40) hide show
  1. package/MIGRATION.md +260 -10
  2. package/README.md +53 -4
  3. package/assets/claude-md-section.md +48 -2
  4. package/assets/commands/spec-connect.md +2 -2
  5. package/assets/commands/spec-live.md +2 -2
  6. package/assets/core/env.config.json.example +9 -3
  7. package/assets/core/env.config.md +102 -30
  8. package/assets/core/gating.config.json.example +4 -0
  9. package/assets/core/gating.config.md +81 -0
  10. package/assets/review/page.html +1501 -0
  11. package/assets/rules/spec-planning.md +224 -15
  12. package/assets/rules/spec-reports.md +269 -0
  13. package/assets/skills/spec/SKILL.md +63 -12
  14. package/assets/skills/spec-bug/SKILL.md +134 -26
  15. package/assets/skills/spec-cancel/SKILL.md +85 -6
  16. package/assets/skills/spec-complete/SKILL.md +109 -20
  17. package/assets/skills/spec-diff/SKILL.md +564 -0
  18. package/assets/skills/spec-hotfix/SKILL.md +143 -21
  19. package/assets/skills/spec-init/SKILL.md +49 -9
  20. package/assets/skills/spec-next/SKILL.md +289 -7
  21. package/assets/skills/spec-review/SKILL.md +45 -9
  22. package/assets/skills/spec-reviewed/SKILL.md +241 -0
  23. package/assets/skills/spec-start/SKILL.md +323 -66
  24. package/assets/skills/spec-to-main/SKILL.md +42 -20
  25. package/package.json +11 -7
  26. package/src/cli.js +1710 -80
  27. package/src/env/building.js +143 -0
  28. package/src/env/classify.js +91 -0
  29. package/src/env/config.js +57 -9
  30. package/src/env/provision.js +192 -19
  31. package/src/env/proxy.js +34 -1
  32. package/src/env/render.js +3 -12
  33. package/src/env/resolve.js +296 -9
  34. package/src/env/review.js +1329 -0
  35. package/src/env/serve.js +549 -0
  36. package/src/env/teardown.js +13 -6
  37. package/src/gating.js +155 -0
  38. package/src/init.js +124 -2
  39. package/src/prompts.js +10 -1
  40. package/LICENSE +0 -21
@@ -1,6 +1,6 @@
1
1
  # Spec Planning
2
2
 
3
- Spec-driven development is driven by nine lifecycle skills (plus the
3
+ Spec-driven development is driven by ten lifecycle skills (plus the
4
4
  `/spec-connect` and `/spec-live` **commands** when isolation is on) — use them rather than hand-rolling specs so the structure
5
5
  and lifecycle stay consistent. Each sets a status on the spec header
6
6
  (`> **Status:** …`):
@@ -11,8 +11,9 @@ and lifecycle stay consistent. Each sets a status on the spec header
11
11
  | `/spec-bug` | (Bug) Reproduce with a failing test, capture spec, drive red→green | `In Progress` | `specs/in-progress/` |
12
12
  | `/spec-hotfix` | (Hotfix) Fork a worktree from a release tag, red→green, land by tag + cherry-pick | `In Progress` | `specs/in-progress/` |
13
13
  | `/spec-review` | Re-validate a spec against the codebase; refresh stale parts | `—` | (unchanged) |
14
- | `/spec-start` | Put a spec in flight on this checkout, then build phase 1 | `In Progress` | `specs/in-progress/` |
15
- | `/spec-next` | Build the next phase of the spec in flight (re-run per phase) | `In Progress` (unchanged) | (unchanged) |
14
+ | `/spec-start` | Put a spec in flight provision its branch, then build phase 1 | `In Progress` | `specs/in-progress/` |
15
+ | `/spec-next` | Build the next phase of the spec this session is in (re-run per phase) | `In Progress` (unchanged) | (unchanged) |
16
+ | `/spec-reviewed` | Pick up a review approved on the page — offered by its code, claimed on your word | (unchanged) | (unchanged) |
16
17
  | `/spec-to-main` | Land the branch on the base (rebase + ff) **without** finishing — for running the work in CI / a shared env mid-spec; repeatable | `In Progress` (unchanged) | (unchanged) |
17
18
  | `/spec-complete` | Verify all phases done + tests green; land + tear down | `Complete` | `specs/complete/` |
18
19
  | `/spec-cancel` | Record progress, stamp a reason on the header; tear down | `Cancelled` | `specs/cancelled/` |
@@ -22,9 +23,17 @@ and lifecycle stay consistent. Each sets a status on the spec header
22
23
  exercises judgment. `/spec-connect` and `/spec-live` are instead **slash commands**
23
24
  (`.claude/commands/`): each pre-executes one `skitterspec spec-env` verb and
24
25
  relays its output, so there is no judgment to apply and no model turn spent
25
- finding one. They are marked `disable-model-invocation`, meaning **only you can
26
- run them** — a skill that wants one will tell you to type it rather than invoking
27
- it.
26
+ finding one. They are marked `disable-model-invocation`, meaning
27
+ **only you can run them** — a skill that wants one will tell you to type it
28
+ rather than invoking it.
29
+
30
+ `/spec-reviewed` is a **skill** and **user-only**, and there the marking is not
31
+ convenience — it is the enforcement of `/spec-diff` step 0's rule that a waiting
32
+ review pass is never claimed unasked. A pass can be POSTed by anything that
33
+ reaches the page; what it cannot reach is the conversation. Because the model
34
+ cannot invoke this skill, a pass is only ever picked up because a person typed
35
+ the command, and typing it **is** the human signal. Prose alone did not hold
36
+ that line once already.
28
37
 
29
38
  `/spec-to-main`, `/spec-status` and `/spec-sync` stay **skills** — each carries
30
39
  real judgment (green tests before a land; an MCP fetch and a team-key check; ten
@@ -44,10 +53,15 @@ mid-spec (so the work can run in CI / a shared test env) while the spec stays
44
53
 
45
54
  **Two workspace modes.** `specs/.core/env.config.json` → `mode` decides where a
46
55
  spec's branch is built. **`worktree`** (the default) gives each spec its own
47
- checkout — several specs at once and `main` left free, at the cost of one
48
- terminal session per spec, which `/spec-start` sets up for you. **`checkout`** builds
49
- the branch in the primary checkout instead: one spec at a time, but no second
50
- session and no hand-off, so the terminal you are already in follows the work.
56
+ checkout — several specs at once and `main` left free. `/spec-start` builds the
57
+ branch there, **moves your session into it** with a plain `cd`, and then asks:
58
+ build phase 1 now? Say yes and it carries on into a bare `/spec-next`; say no and
59
+ you are already standing in the worktree, so `/spec-next` typed later does the
60
+ same thing. The primary checkout stays on the base branch throughout — the
61
+ session followed the spec, nothing was checked out anywhere.
62
+ **`checkout`** builds the branch in the primary checkout instead: one spec at a
63
+ time, nowhere else to stand, and the build always carries straight on.
64
+ The difference is how many specs can be in flight at once.
51
65
  Pick it for how you work rather than for what the project contains — a repo with
52
66
  no dev servers may still want several specs in flight. In `checkout` mode
53
67
  `/spec-connect` and `/spec-live` do not apply and say so: both exist to reach
@@ -67,8 +81,11 @@ your canonical `localhost` ports so you can test it at the normal URL
67
81
  move, header edits, the code) happens on the spec's branch in the worktree; `main`
68
82
  changes only when it merges. Teardown is folded into `/spec-complete` ·
69
83
  `/spec-cancel`. Beneath the skills, `skitterspec spec-env
70
- <up|down|prune|dev|connect|integrate|hotfix|live|status|resolve>` is the CLI
71
- engine (omit the spec name and it uses the worktree you are standing in). Teardown drops
84
+ <up|down|prune|dev|connect|integrate|hotfix|live|review|stage|status|resolve>` is the CLI
85
+ engine. **Omit the spec name anywhere and it uses the worktree you are standing in**,
86
+ else the sole provisioned spec — with no exceptions left: a bare
87
+ `/spec-connect` connects, and a bare `/spec-live` takes, where both once meant
88
+ `main`. Teardown drops
72
89
  the finished spec's own test-DB volume; `spec-env prune` additionally reaps
73
90
  **orphaned** volumes left by declined/aborted teardowns, so `/spec-complete` and
74
91
  `/spec-cancel` also sweep orphans (confirm-first). A **hotfix** is the one
@@ -80,17 +97,172 @@ when `env.config.json` is absent — every skill then behaves as it does today.
80
97
  **Live overlay (`/spec-live`, a command) — the light way to test a spec.** `/spec-connect`
81
98
  runs a spec's *own* dev stack and proxies the canonical ports to it (one stack per
82
99
  spec). **Live overlay** instead reuses the one dev server you already have running:
83
- `/spec-live <spec>` rebases the branch onto base, frees it from its worktree, and
100
+ `/spec-live <spec>` or a bare `/spec-live`, which takes the spec you are
101
+ standing on — rebases the branch onto base, frees it from its worktree, and
84
102
  checks it out **in the primary checkout**, so your running server hot-reloads the
85
- feature at the normal URL — no second stack, no proxy. The branch checked out in
103
+ feature at the normal URL — no second stack, no proxy. Bare only acts when there
104
+ is exactly one answer *and* the workbench is free; anything else prints the
105
+ status report rather than guessing. The branch checked out in
86
106
  the primary checkout **is** the lock: exactly one spec is live at a time, and
87
107
  `/spec-live main` hands the instance back (fixes you make while live commit
88
108
  straight onto the branch; `/spec-complete` is live-aware and lands them). Rule of
89
- thumb: **live overlay is the light default for code-only specs**; it *refuses*
109
+ thumb: **live overlay is the light default for *testing* a code-only spec** it
110
+ is not how work gets started, and no lifecycle skill invokes it; it *refuses*
90
111
  stateful ones (`Stack: worktree + docker`, or a branch touching migrations) — keep
91
112
  `/spec-connect` + a Docker stack for those, and for genuinely parallel testing.
92
113
  Beneath it, `skitterspec spec-env live <take|release|abort|status>` is the engine.
93
114
 
115
+ **Staging one spec and not another (`spec-env stage`).** Several specs are
116
+ authored at once — that is what the worktree mode is for — and they all land in
117
+ the same `specs/` folder until they are committed. A `git add specs/` there
118
+ stages a *directory*, sweeping whatever another session happens to have written
119
+ into this spec's commit, under this spec's ticket trailer.
120
+ `skitterspec spec-env stage [<spec>] [--json]` is how a skill asks instead of
121
+ guessing: it splits the uncommitted tree into the paths that are this spec's —
122
+ its folder in **any** bucket, so a tree mid-`git mv` is handled, plus each
123
+ `spec.companionPaths` entry the project declares — and the paths that are not.
124
+ It reads **the tree you are standing in**, which is the worktree inside one and
125
+ the primary checkout outside, and prints which. The `owned` half is the spec's
126
+ **documents**, never its code: a phase's own implementation is `foreign`, because
127
+ the commits this bounds are the lifecycle ones. Nothing here refuses, writes, or
128
+ exits non-zero — `foreign` is a list of paths to leave alone, not an accusation.
129
+
130
+ **Naming the paths is necessary and not sufficient**, which is why every spec
131
+ commit is also pathspec-limited: `git add -- <paths>` **and**
132
+ `git commit -m "…" -- <paths>`. A checkout has one `.git/index` and every session
133
+ standing in it shares that index, so a bare `git commit` takes whatever another
134
+ session has already staged no matter how carefully this one staged its own. The
135
+ `add` is still needed — `git commit` cannot take a path git has never seen, which
136
+ is every brand-new spec folder — but it is the `--` on the **commit** that bounds
137
+ what lands. The two failures are different and the fix needs both halves.
138
+
139
+ **Reading a spec's diff (`/spec-diff`) — seeing the work, not running it.**
140
+ `/spec-connect` and `/spec-live` both exist to reach a *running app*.
141
+ **`/spec-diff`** answers the other question: what did this actually change? A
142
+ phase is built in its own worktree, so `git diff` in your terminal answers about
143
+ the base branch — and a 350-line diff read as terminal text is scrolling, not
144
+ review. The engine collects the worktree's changes with `git -C` and writes a
145
+ self-contained HTML page (`.spec-env/reviews/<spec>.html`, gitignored) with
146
+ whole-file context that folds away, a file tree, and new files included. You open
147
+ it locally, or publish it and read it on a phone.
148
+ **The diff never passes through the model**, so the page is free to produce
149
+ however large it is; the *written*
150
+ review — a short read plus `flag`/`confirm`/`good` notes — is the part that costs
151
+ tokens, and it is offered rather than assumed. It is a **skill**, model-invocable,
152
+ and it is gated on nothing: half a phase, a hand edit and a colleague's branch are
153
+ all ordinary inputs. `/spec-next` renders the page at the end of a phase and
154
+ offers the review; `skitterspec spec-env review <spec> [--branch]` is the engine.
155
+
156
+ **The page is not read-only — it takes a review pass back.** Tick `✓ accept` per
157
+ file as you read, write notes against a line or a whole file, answer the
158
+ questions a written review asked, then **end it in a decision** and hand it
159
+ back. Three buttons, each carrying its own verdict:
160
+ **`✓ Approve`** (commit it), **`↺ Request changes`** (work them now),
161
+ **`… Discuss first`** (report and talk). `/spec-diff` stores it (`--notes`),
162
+ plays back what it read, and routes on the verdict — `changes` **is** the
163
+ go-ahead, so it works the commented files immediately, while `discuss`, a
164
+ refused approval and a pass carrying no verdict at all report and wait, which
165
+ is what a bare paste has always done. Accepted files stay unopened either way.
166
+
167
+ **Two ways back, and both are ordinary.** A **served** page POSTs the pass to
168
+ its own URL; the engine holds it in a gitignored `.pending.json` beside the
169
+ notes sidecar and answers with a **six-digit code**. Read the code out and
170
+ `/spec-diff` claims it (`--claim`), merging exactly the pass that code names.
171
+ A `file://` page has no server to talk to — the File System Access API is
172
+ Chrome-only and absent on iOS Safari — so it copies to the clipboard and you
173
+ paste, exactly as before. The clipboard path is **not legacy**: it is the whole
174
+ story for a local reader.
175
+
176
+ The code is **not a secret** — it is printed on the page, and it cannot be a
177
+ gate against Claude either, because the store is a file Claude can read. What
178
+ actually holds is narrower and stronger:
179
+ **a device that reaches your page cannot reach your conversation.**
180
+ So a pass it queues sits in the holding area
181
+ forever, and the rule that keeps it there is that
182
+ **Claude never claims a pass it was not asked to** — stated in `/spec-diff` step
183
+ 0, because nothing enforces it.
184
+
185
+ What the code *does* is let you **tell two passes apart** — it is an address,
186
+ never a password. One waiting pass is claimed and acted on the moment you type
187
+ `/spec-reviewed`, with nothing read back: the round-trip verified *which* pass
188
+ in a design where the page pushed and the agent went looking, and that design is
189
+ gone. Two waiting is a refusal to guess — the one case where a stranger's pass
190
+ sits beside yours, and the one case where six digits are worth anyone's time.
191
+ Pasting the code (`/spec-reviewed 324199`) skips even that. A pass you disown is `--drop <code>`; left there it is reported on every
192
+ render until you stop reading the line.
193
+
194
+ A code that matches nothing refuses, **names nothing**, and never falls back to
195
+ "the only one" — that fallback is precisely what would let an unread pass
196
+ through. Claiming **consumes**: a code works once. A second pass from the same
197
+ page render supersedes the first, so the code on screen is always the pass on
198
+ screen; passes from different renders stand alongside each other.
199
+
200
+ **And a claimed pass never enters the model's context.** The engine holds it,
201
+ merges it, and reports the counts — six digits is what reaches Claude, where a
202
+ pasted blob costs context in proportion to how much you wrote. That is the same
203
+ rule the diff already follows, applied to the one place it used to break. What it did comes back as a **resolution** (`--resolve`),
204
+ so the next render shows each note struck through with a one-line account and you
205
+ verify the fix instead of trusting it. An accept is keyed to the file's
206
+ **content hash**, so it survives the commit that ends the phase and lapses by
207
+ itself when that file changes again — announced as `accepted earlier — changed since`, never
208
+ silently. All of it lives beside the page in gitignored `.spec-env/`, and
209
+ **the marks are information, never a gate**: nothing counts them and nothing
210
+ refuses on them.
211
+
212
+ **The verdict is chosen, never derived.** A verdict is one person's conclusion,
213
+ sent once per pass; a counting gate is a refusal computed from how many boxes
214
+ are ticked, and it stays forbidden. The one refusal here is not a count of
215
+ ticks: **committing** is unavailable while any **comment** is unresolved,
216
+ because you asked for something and it therefore cannot also be fine. Files you
217
+ never ticked block nothing — an unticked file is something you said nothing
218
+ about, and requiring every one of them would be the tally this design exists to
219
+ avoid.
220
+
221
+ **The verdict names the action**, and that is why it is `commit` rather than
222
+ `approve`: a review is the guard in front of an action, and an approval that
223
+ only recorded itself was the one control on the page that did not describe what
224
+ it does. Both committing verdicts hand off to the project's own commit skill
225
+ (`review.commitWith` in `env.config.json`, `/commit` by default) — skitterspec
226
+ never vendors one, because `/commit` belongs to skittership.
227
+ **There is no off switch**: `"none"` existed and was removed, since it produced
228
+ exactly the record-and-do-nothing verdict this design is against.
229
+ `Commit & Continue` then runs `/spec-next` and **stops there** — it never
230
+ completes, lands or tears anything down.
231
+
232
+ A verdict is **consumed**, not stored: a commit verdict is spent by the commit
233
+ and a changes one by the work, so neither can go stale and later act on
234
+ something nobody read. What survives is a one-line outcome log, shown on the
235
+ next render as history.
236
+
237
+ **One ending, every skill (`.claude/rules/spec-reports.md`).** Every skill in
238
+ the table above finishes with the same block — a verdict, then a table of only
239
+ the fields that skill declares — and says nothing while it runs beyond a
240
+ question it cannot answer itself or a failure at the moment it happens. The
241
+ shape is defined once in that rule; each skill's `## Report` section names only
242
+ its verdicts and its fields.
243
+
244
+ ✅ **Phase 2 built** — `feat-orders`, 2 of 4
245
+
246
+ | | |
247
+ |---|---|
248
+ | **Tracker** | [ABC-88](https://example.invalid/ABC-88) · `feat-orders` · phase 2 moved |
249
+ | **Branch** | `spec/feat-orders` · 3 commits, clean |
250
+ | **Built** | POST /orders handler, orders schema |
251
+ | **Tests** | 128 passed · npm test |
252
+ | **Review** | 7 files, +212 −18 · [open the page](file:///…) — want a written review before you commit? |
253
+ | **Follow-ups** | none |
254
+ | **Next** | `/spec-next` → phase 3 (Auth) |
255
+
256
+ Four verdicts, and the last two are different facts about the repo: `✅` done ·
257
+ `⚠️` done with caveats · `❌` failed part-way, so there is a mess to clear ·
258
+ `⏸` refused before acting, so nothing changed.
259
+ **A refusal emits the block too** — "nothing happened" is a reported outcome
260
+ rather than an absent one.
261
+ `Follow-ups` is always present, because a recorded `none` is a decision where a
262
+ missing line is an oversight. And the block covers **that run only**: what else
263
+ is in flight is another question, and volunteering it here leaves the reader
264
+ unable to tell what followed from the run they just watched.
265
+
94
266
  **Ticketing-provider sync (opt-in, a separate package).** The base is
95
267
  tracker-free: it knows nothing about any specific ticketing system. A
96
268
  ticketing provider is installed as its own distribution that plugs into named
@@ -109,6 +281,24 @@ against a committed last-pushed snapshot and applies it) and `/spec-status`
109
281
  workflow-state drifted), backed by a `spec-sync` CLI. There is no content pull —
110
282
  the tracker is never read back or merged.
111
283
 
284
+ A provider may also ship a **read-only listing** of what the tracker holds. That
285
+ is not a pull: nothing is merged back and no spec file is written. It exists
286
+ because the folder buckets are only the truth *on the branch you are standing
287
+ on* — `/spec-start` moves a spec to
288
+ `specs/in-progress/` on that spec's own branch, so on the base branch an
289
+ in-flight spec still reads `backlog` and a teammate's unlanded spec is not on
290
+ disk at all. The tracker knows both, and each row carries the spec's folder name
291
+ so `/spec-start <name>` is a copy-paste away. See the provider's own docs for
292
+ what it calls the command.
293
+
294
+ A provider may also mirror **who is building a spec**, as one more field the repo
295
+ owns. Where it does, `/spec-start` records the developer and the tracker's ticket
296
+ is assigned to them, the assignment is released when the spec completes, and
297
+ `/spec-claim` moves ownership mid-flight — take it, hand it back, or hand it to a
298
+ teammate. Like everything else here it is opt-in and one-way: the spec file is
299
+ the record, and the ticket is the mirror. See the provider's own docs for how to
300
+ turn it on.
301
+
112
302
  **Every skill that moves a spec through the lifecycle carries a seam**, so the
113
303
  mirror keeps up without anyone remembering to push: `/spec`, `/spec-bug` and
114
304
  `/spec-hotfix` link the spec they create; `/spec-next` refreshes it as work starts;
@@ -118,6 +308,21 @@ With no provider installed the seams are empty and every skill behaves as a plai
118
308
  filesystem workflow. See the provider package's own docs for its config and field
119
309
  reference.
120
310
 
311
+ **Release gating (opt-in, config-gated).** With
312
+ `specs/.core/gating.config.json` present, every spec `/spec`, `/spec-bug` and
313
+ `/spec-hotfix` write carries a `> **Gating:**` header recording one decision:
314
+ does this ship behind a feature flag, or land live? The value is a **flag name**,
315
+ or **`none: <one-line reason>`** — and the reason half is the load-bearing part,
316
+ because `none: additive, nothing to revert` is a decision a reviewer can argue
317
+ with while a bare `none` is a shrug and a missing line is an oversight.
318
+
319
+ Skitterspec bakes in **the offer, never the mechanism**: it asks, cites the
320
+ project's own doc (`guidance` in that config), and records the answer. It never
321
+ reads your flag code. `skitterspec gating check` reports specs with no decision
322
+ and **always exits 0** — it reads only `backlog/` and `in-progress/`, so specs
323
+ finished before you adopted gating are out of range by construction. With the
324
+ config absent nothing appears at all: no question, no header, no check.
325
+
121
326
  ## Project conventions (fill this in)
122
327
 
123
328
  The spec skills tell you to run "your project's typecheck and test commands" and
@@ -160,6 +365,10 @@ Every spec header carries:
160
365
  to `git config user.name`).
161
366
  - `> **Developer:**` — who implements it (`—` until `/spec-start` starts work, then
162
367
  set to `git config user.name`; `/spec-bug` sets it immediately).
368
+ - `> **Gating:**` — the release-gating decision,
369
+ **only when `specs/.core/gating.config.json` exists**: a flag name, or
370
+ `none: <one-line reason>`. Absent entirely in a project that has not adopted
371
+ gating.
163
372
 
164
373
  Every spec also has a **State log** table — the audit trail of folder/status
165
374
  transitions. Each lifecycle skill appends exactly one row when it changes state:
@@ -0,0 +1,269 @@
1
+ # Spec Reports
2
+
3
+ Every lifecycle skill ends with the same block. This file is its whole
4
+ definition — a skill's `## Report` section names only which fields it emits and
5
+ any behaviour that was always there.
6
+
7
+ Two things it buys: a run's outcome is scannable in one shape rather than
8
+ composed fresh each time, and "nothing happened" becomes a reported outcome
9
+ instead of an absent one.
10
+
11
+ ## Silence during the run
12
+
13
+ **Do not narrate.** Not what you are about to do, not why, not which step you
14
+ are on. The tool calls are already on screen, so a line per step re-states the
15
+ transcript in words that never vary.
16
+
17
+ Speak mid-run for exactly two reasons:
18
+
19
+ - **A question you cannot answer yourself** — a decision the spec left open, a
20
+ confirmation a guard demands, an ambiguity with more than one defensible
21
+ answer.
22
+ - **A failure, at the moment it happens** — a red test, a refused command, a
23
+ conflict. Say it where it occurs; do not hold it back for the block.
24
+
25
+ Everything else waits.
26
+
27
+ ## The block
28
+
29
+ A verdict sentence, a blank line, then a two-column table:
30
+
31
+ ✅ **Phase 2 built** — `feat-foo`, 2 of 4
32
+
33
+ | | |
34
+ |---|---|
35
+ | **Tracker** | [ABC-88](https://example.invalid/ABC-88) · `feat-foo` · phase 2 moved |
36
+ | **Branch** | `spec/feat-foo` · 3 commits, clean |
37
+ | **Built** | POST /orders handler, orders schema |
38
+ | **Tests** | 128 passed · npm test |
39
+ | **Review** | 7 files, +212 −18 · [open the page](file:///…) — want a written review before you commit? |
40
+ | **Follow-ups** | none |
41
+ | **Next** | `/spec-next` → phase 3 (Auth) |
42
+
43
+ **A table, and never a fenced block.** An aligned label column is only
44
+ achievable inside a fence, because markdown collapses runs of spaces outside
45
+ one — so alignment and being recognised as a report are mutually exclusive, and
46
+ the report wins. A fence renders as the same grey box as a code sample, and a
47
+ reader who has been shown code all session reads one more box as code: the first
48
+ person to receive one of these did not register it as output at all, and missed
49
+ the `Next` row inside it. That is the exact scanning failure this contract exists
50
+ to fix, so producing it in the contract's own format is not a cosmetic mistake.
51
+
52
+ **The verdict sentence** is `<state> **<one clause>** — <spec>, <where it got
53
+ to>`. The clause says what happened; the tail says to what, and how far. Name
54
+ the skill in the clause when the run's identity is not obvious from the
55
+ conversation — a refusal, or a skill invoked by another. A run with no spec to
56
+ name drops that segment rather than inventing one.
57
+
58
+ **The header row is empty.** The labels are the left column, and a header over
59
+ them would only be able to say "field", which no one needs told.
60
+
61
+ **The block is the last thing in the message.** Nothing follows it — not a
62
+ closing line, not a next step, not the offer. Everything the reader must act on
63
+ is a row, which is what earns the block its position.
64
+
65
+ **And that is a rule about the destination, not only the prohibition.** It was
66
+ stated plainly and broken repeatedly, by runs that had something worth saying
67
+ and nowhere in the block to say it — so the paragraph went after the table,
68
+ where it buried the rows it was meant to sit beside. A missing destination reads
69
+ as a rule that does not fit the work, and a rule that does not fit gets ignored.
70
+ So: **if it is worth telling the reader, it is a row.**
71
+ **If it is not a row, it is not worth telling them.**
72
+ `Snags` is the row most of that content belongs in.
73
+
74
+ **One control may follow the block: a picker.** Where the run ends in a choice
75
+ the reader must make, the options may be offered after the table. That is not
76
+ an exception squeezed into the rule — the ban exists because *prose* after the
77
+ block competes with it for the reader's attention and loses them the rows. A
78
+ picker does not compete: it is the `Next` row made actionable, in the place the
79
+ reader's eye already finishes. Prose after the block stays banned, and adding a
80
+ sentence alongside the picker is the same violation wearing a control's clothes.
81
+
82
+ **Never fence a message to the reader.** A fenced block is for a command to
83
+ run, code, or engine output quoted verbatim — things the reader copies or
84
+ compares. A question, an offer, a hand-off is prose. The same mechanism that
85
+ made the old report unreadable applies to anything else in a grey box: it is
86
+ read as an artefact to skim rather than as something addressed to someone, so a
87
+ question inside one goes unanswered and an instruction inside one goes unread.
88
+
89
+ **It reports this run and nothing else.** Not what else is in flight, not the
90
+ other worktrees, not the backlog, not the state of the repo at large. A reader
91
+ finishing one piece of work cannot tell whether a line about some other spec is
92
+ a consequence of what just happened or an unrelated aside, and has to stop and
93
+ work it out — which is the cost this block exists to remove. If they want the
94
+ wider picture there is a skill that answers it; volunteering it here muddies the
95
+ one thing they asked about.
96
+
97
+ `Next` is the single next action **for this work**, not a menu of what else
98
+ could be done.
99
+
100
+ **And it must be runnable from the state the run actually leaves behind.** Most
101
+ of these skills deliberately do not commit — they finish, verify, and wait — so
102
+ the tree they hand back is dirty, and several of the commands they would name
103
+ next refuse on a dirty tree. A `Next` that omits the commit therefore sends the
104
+ reader to a refusal: `/spec-next` leaves a phase uncommitted and its own §2
105
+ refuses on exactly that, and `/spec-bug` leaves a fix uncommitted where
106
+ `/spec-complete` refuses the same way. Where the skill does not commit, the row
107
+ says `/commit, then <the thing>`.
108
+
109
+ This is not a style rule. `Next` is the one row a reader is meant to act on, and
110
+ it closes the block because the closing row should be the one that moves the
111
+ work on. A row that cannot be run spends the trust the position was designed to
112
+ earn — and the two skills that got it wrong got it wrong independently, which is
113
+ what makes it a missing constraint rather than two slips.
114
+
115
+ ## Field vocabulary, in this order
116
+
117
+ A skill emits only the fields it declares, in this order, skipping the rest.
118
+ `Follow-ups` is the exception: always present, second to last.
119
+
120
+ `Tracker` is the conditional one: a skill may declare it and still never emit
121
+ it, because no ticketing provider is installed. That is an absence with nothing
122
+ behind it — say nothing rather than reporting that there was nothing to report.
123
+
124
+ | Field | Carries |
125
+ |-------|---------|
126
+ | `Tracker` | **First.** The ticket id, linked where the spec has a url, the spec's folder name beside it, and what changed there. |
127
+ | `Why` | **Non-`✅` only.** What stopped it, in one clause. |
128
+ | `Branch` | Branch name, commit count, clean or dirty. |
129
+ | `Spec` | The spec document's own state: status, bucket, phase. |
130
+ | `Cause` | The root cause, for work that diagnosed one. |
131
+ | `Built` | What the run produced — the code, the edits, the spec written. |
132
+ | `Tests` | The result and the command that produced it. |
133
+ | `Snags` | What this run hit and handled — one short paragraph. A wrong turn, a guard that fired, a check of yours that turned out to be a false negative. Not a caveat on the outcome (`⚠️` and `Why` carry those) and not future work (`Follow-ups` carries that): this is what happened on the way. |
134
+ | `Landed` | A fast-forward, a tag, a cherry-pick. |
135
+ | `Worktree` | A worktree provisioned, entered, or torn down. |
136
+ | `Untouched` | Uncommitted work the run deliberately left alone — whose, and how much. |
137
+ | `Review` | The rendered diff page: files, `+`/`−`, the link — and the offer of a written review, in the same row. |
138
+ | `Follow-ups` | **Always.** `none`, or one line each. |
139
+ | `Next` | **Last.** The single next action for this work — runnable from the state the run leaves behind. |
140
+
141
+ The order runs identity → context → what happened → where it went → what to do.
142
+ **`Tracker` is first** because the id is what addresses this work outside the
143
+ repo, and **`Next` is last** because it is the only row the reader acts on: the
144
+ closing row should be the one that moves the work on.
145
+
146
+ **`Untouched` is a fact, not a caveat.** A run that provisions beside another
147
+ spec's uncommitted work has done nothing wrong and nothing partial — it left
148
+ alone something that was never its business — so it stays `✅` with a row, and
149
+ does not become `⚠️`. Several specs in flight at once is what the worktree mode
150
+ is *for*; spending the "something is worth knowing" verdict on the normal case
151
+ is how that verdict stops meaning anything. It is still reported rather than
152
+ silent, because "I provisioned, and your four files are still sitting there" is
153
+ a different sentence from "I provisioned".
154
+
155
+ **`Review` is one row, not two.** The page and the offer to read it are the same
156
+ subject, and splitting them made the reader resolve a distinction before acting
157
+ on either. The offer does not need the final row once it has a label column to
158
+ be found by.
159
+
160
+ ## The four verdicts
161
+
162
+ | State | Means |
163
+ |-------|-------|
164
+ | `✅` | Done. Everything the skill set out to do happened. |
165
+ | `⚠️` | Done with caveats — it finished, but something is worth knowing. |
166
+ | `❌` | Failed part-way. It acted, and the repo is mid-something. |
167
+ | `⏸` | Refused before acting. Nothing changed. |
168
+
169
+ **`❌` and `⏸` are different facts about the repo**, and the difference decides
170
+ what happens next. A failed landing leaves a conflicted rebase and a standing
171
+ worktree — there is a mess to clear. A refusal leaves nothing at all, and the
172
+ only thing to do is supply what was missing. Collapsing them sends people
173
+ looking for wreckage that is not there, or past wreckage that is.
174
+
175
+ **A refusal still emits the block.** That is the point of having four states:
176
+ every invocation ends the same way, so an outcome is never inferred from
177
+ silence.
178
+
179
+ ## Where the block may grow
180
+
181
+ One place only. A non-`✅` verdict adds a `Why` row, and quotes the output that
182
+ justifies it — the failing assertion, the conflict, the engine's refusal — in a
183
+ fenced block **above** the table, where a fence means what a fence should mean:
184
+
185
+ ```
186
+ ✖ rejects a negative quantity
187
+ expected 422, got 500
188
+ ```
189
+
190
+ ❌ **Phase 2 stopped — tests red** — `feat-foo`, 2 of 4
191
+
192
+ | | |
193
+ |---|---|
194
+ | **Why** | 3 assertions fail in `orders.test.js`; nothing committed |
195
+ | **Branch** | `spec/feat-foo` · 2 commits, dirty |
196
+ | **Built** | POST /orders handler, orders schema |
197
+ | **Tests** | 125 passed, 3 failed · npm test |
198
+ | **Follow-ups** | none |
199
+ | **Next** | fix the failures, then `/spec-next` to finish phase 2 |
200
+
201
+ Everywhere else the block stays the size it is. Prose after it, an extra row of
202
+ your own invention, a second block — all three are the shape drifting back to
203
+ what this replaced.
204
+
205
+ ## Follow-ups
206
+
207
+ **Always present.** A recorded `none` is a decision; a missing line is an
208
+ oversight — the same logic as the `Gating` header.
209
+
210
+ The bar is deliberately high. A follow-up is something *this* work surfaced
211
+ that *this* spec will not fix — a wrong assumption, a missing guard, a decision
212
+ the spec deferred and the code then leaned on. It is **not** "could be tidier",
213
+ and it is **not** anything already sitting in the spec's Open questions.
214
+
215
+ When there is one, **offer to `/spec` it on the spot**. If the user does not
216
+ take the offer, write one dated line into the spec's Changelog before you
217
+ finish, so it outlives the session:
218
+
219
+ ```
220
+ - 2026-01-09 — Follow-up surfaced: connect's port picker assumes a free
221
+ canonical port; not in scope here.
222
+ ```
223
+
224
+ **Say where the spec would be written.** These skills run inside the spec's own
225
+ worktree, and a spec authored there physically lives on that branch — invisible
226
+ from the base branch, and cancelled along with its host if the host is
227
+ cancelled. So the offer names the primary checkout as the place to write it.
228
+ `spec-planning.md` and `commit-trailers.md` carry the reasoning.
229
+
230
+ ## Worked examples
231
+
232
+ **`⚠️` — it finished, with something worth knowing.**
233
+
234
+ ⚠️ **Phase 3 built; tracker not updated** — `feat-foo`, 3 of 4
235
+
236
+ | | |
237
+ |---|---|
238
+ | **Branch** | `spec/feat-foo` · 4 commits, clean |
239
+ | **Built** | Auth middleware, session table |
240
+ | **Tests** | 140 passed · npm test |
241
+ | **Follow-ups** | Session expiry is read from two places; phase 4 leans on one |
242
+ | **Next** | `/spec-next` → phase 4 (Docs) |
243
+
244
+ **`⏸` — refused, nothing changed.**
245
+
246
+ ⏸ **`/spec-next` refused — no spec in flight**
247
+
248
+ | | |
249
+ |---|---|
250
+ | **Why** | Not standing in a worktree, and 2 specs are provisioned |
251
+ | **Follow-ups** | none |
252
+ | **Next** | `/spec-start <name>`, or `cd` into `../repo-wt/feat-orders` or `../repo-wt/feat-auth` |
253
+
254
+ **`✅` — a spec finished and landed.**
255
+
256
+ ✅ **Landed and torn down** — `feat-foo`, complete
257
+
258
+ | | |
259
+ |---|---|
260
+ | **Tracker** | [ABC-88](https://example.invalid/ABC-88) · `feat-foo` · moved to its done state |
261
+ | **Spec** | Complete · `specs/complete/feat-foo` |
262
+ | **Tests** | 131 passed · npm test |
263
+ | **Landed** | base fast-forwarded to 4a1c9e2 |
264
+ | **Worktree** | removed · `../repo-wt/feat-foo` |
265
+ | **Follow-ups** | none |
266
+ | **Next** | pick the next spec from `specs/backlog/` |
267
+
268
+ The `❌` example is in **Where the block may grow** above, since the failing
269
+ output it quotes is the thing that example exists to show.