@skitterbyte/skitterspec-linear 12.0.0 → 14.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 (58) hide show
  1. package/MIGRATION.md +296 -10
  2. package/README.md +32 -2
  3. package/assets/claude-md-section.md +38 -2
  4. package/assets/commands/spec-connect.md +2 -2
  5. package/assets/commands/spec-live.md +2 -2
  6. package/assets/core/SETUP.md +21 -3
  7. package/assets/core/env.config.json.example +7 -3
  8. package/assets/core/env.config.md +90 -30
  9. package/assets/core/linear.config.md +58 -0
  10. package/assets/hooks/review-gate.js +141 -0
  11. package/assets/review/page.html +1787 -0
  12. package/assets/rules/spec-planning.md +250 -10
  13. package/assets/rules/spec-reports.md +321 -0
  14. package/assets/skills/spec/SKILL.md +33 -5
  15. package/assets/skills/spec-bug/SKILL.md +172 -9
  16. package/assets/skills/spec-cancel/SKILL.md +98 -21
  17. package/assets/skills/spec-claim/SKILL.md +114 -0
  18. package/assets/skills/spec-complete/SKILL.md +94 -25
  19. package/assets/skills/spec-diff/SKILL.md +678 -0
  20. package/assets/skills/spec-hotfix/SKILL.md +172 -11
  21. package/assets/skills/spec-init/SKILL.md +56 -7
  22. package/assets/skills/spec-linear-setup/SKILL.md +55 -1
  23. package/assets/skills/spec-list/SKILL.md +218 -0
  24. package/assets/skills/spec-next/SKILL.md +419 -7
  25. package/assets/skills/spec-push/SKILL.md +32 -8
  26. package/assets/skills/spec-review/SKILL.md +40 -5
  27. package/assets/skills/spec-reviewed/SKILL.md +258 -0
  28. package/assets/skills/spec-start/SKILL.md +386 -106
  29. package/assets/skills/spec-status/SKILL.md +24 -2
  30. package/assets/skills/spec-sync/SKILL.md +40 -4
  31. package/assets/skills/spec-to-main/SKILL.md +28 -6
  32. package/package.json +11 -7
  33. package/src/cli.js +1808 -89
  34. package/src/env/building.js +143 -0
  35. package/src/env/commitcmd.js +108 -0
  36. package/src/env/config.js +58 -9
  37. package/src/env/hooks.js +117 -0
  38. package/src/env/provision.js +54 -15
  39. package/src/env/proxy.js +34 -1
  40. package/src/env/render.js +3 -12
  41. package/src/env/resolve.js +295 -9
  42. package/src/env/review.js +1536 -0
  43. package/src/env/serve.js +573 -0
  44. package/src/env/teardown.js +13 -6
  45. package/src/init.js +150 -1
  46. package/src/vendor/linear/api.js +104 -1
  47. package/src/vendor/linear/cli-sync.js +854 -17
  48. package/src/vendor/linear/config.js +8 -0
  49. package/src/vendor/linear/credentials.js +94 -0
  50. package/src/vendor/linear/doctor.js +35 -0
  51. package/src/vendor/linear/identity.js +105 -0
  52. package/src/vendor/linear/mcp.js +26 -0
  53. package/src/vendor/sync-core/index.js +6 -2
  54. package/src/vendor/sync-core/src/compare.js +49 -3
  55. package/src/vendor/sync-core/src/normalize.js +30 -0
  56. package/src/vendor/sync-core/src/push.js +11 -1
  57. package/src/vendor/sync-core/src/write.js +38 -0
  58. 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:** …`):
@@ -13,6 +13,7 @@ and lifecycle stay consistent. Each sets a status on the spec header
13
13
  | `/spec-review` | Re-validate a spec against the codebase; refresh stale parts | `—` | (unchanged) |
14
14
  | `/spec-start` | Put a spec in flight — provision its branch, then build phase 1 | `In Progress` | `specs/in-progress/` |
15
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/` |
@@ -26,6 +27,20 @@ finding one. They are marked `disable-model-invocation`, meaning
26
27
  **only you can run them** — a skill that wants one will tell you to type it
27
28
  rather than invoking it.
28
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. Because the model cannot invoke this
33
+ skill, a pass named this way is only ever picked up because a person typed the
34
+ command, and typing it **is** the human signal. Prose alone did not hold that
35
+ line once already.
36
+
37
+ It is **no longer the only way in**, and the difference is worth stating
38
+ precisely. A phase that ends now *waits* on its page, and a pass arriving inside
39
+ that wait is claimed by the engine (`--claim-since`) without anyone typing
40
+ anything — scoped to the window, refusing when two arrive. `/spec-reviewed` is
41
+ what answers everything outside it: a pass sent when nobody was waiting, two
42
+ passes to choose between, and every harness with no file-watch to wait with.
43
+
29
44
  `/spec-to-main`, `/spec-status` and `/spec-sync` stay **skills** — each carries
30
45
  real judgment (green tests before a land; an MCP fetch and a team-key check; ten
31
46
  subcommands) — but they are marked user-only too, since nobody reaches them
@@ -44,11 +59,15 @@ mid-spec (so the work can run in CI / a shared test env) while the spec stays
44
59
 
45
60
  **Two workspace modes.** `specs/.core/env.config.json` → `mode` decides where a
46
61
  spec's branch is built. **`worktree`** (the default) gives each spec its own
47
- checkout — several specs at once and `main` left free — and `/spec-start` moves
48
- the session you typed into *into* that checkout, so the work carries on in the
49
- same terminal. **`checkout`** builds the branch in the primary checkout instead:
50
- one spec at a time, and nothing to move into. Neither mode costs you a second
51
- terminal; the difference is how many specs can be in flight at once.
62
+ checkout — several specs at once and `main` left free. `/spec-start` builds the
63
+ branch there, **moves your session into it** with a plain `cd`, and then asks:
64
+ build phase 1 now? Say yes and it carries on into a bare `/spec-next`; say no and
65
+ you are already standing in the worktree, so `/spec-next` typed later does the
66
+ same thing. The primary checkout stays on the base branch throughout the
67
+ session followed the spec, nothing was checked out anywhere.
68
+ **`checkout`** builds the branch in the primary checkout instead: one spec at a
69
+ time, nowhere else to stand, and the build always carries straight on.
70
+ The difference is how many specs can be in flight at once.
52
71
  Pick it for how you work rather than for what the project contains — a repo with
53
72
  no dev servers may still want several specs in flight. In `checkout` mode
54
73
  `/spec-connect` and `/spec-live` do not apply and say so: both exist to reach
@@ -68,8 +87,11 @@ your canonical `localhost` ports so you can test it at the normal URL
68
87
  move, header edits, the code) happens on the spec's branch in the worktree; `main`
69
88
  changes only when it merges. Teardown is folded into `/spec-complete` ·
70
89
  `/spec-cancel`. Beneath the skills, `skitterspec spec-env
71
- <up|down|prune|dev|connect|integrate|hotfix|live|status|resolve>` is the CLI
72
- engine (omit the spec name and it uses the worktree you are standing in). Teardown drops
90
+ <up|down|prune|dev|connect|integrate|hotfix|live|review|stage|status|resolve>` is the CLI
91
+ engine. **Omit the spec name anywhere and it uses the worktree you are standing in**,
92
+ else the sole provisioned spec — with no exceptions left: a bare
93
+ `/spec-connect` connects, and a bare `/spec-live` takes, where both once meant
94
+ `main`. Teardown drops
73
95
  the finished spec's own test-DB volume; `spec-env prune` additionally reaps
74
96
  **orphaned** volumes left by declined/aborted teardowns, so `/spec-complete` and
75
97
  `/spec-cancel` also sweep orphans (confirm-first). A **hotfix** is the one
@@ -81,9 +103,12 @@ when `env.config.json` is absent — every skill then behaves as it does today.
81
103
  **Live overlay (`/spec-live`, a command) — the light way to test a spec.** `/spec-connect`
82
104
  runs a spec's *own* dev stack and proxies the canonical ports to it (one stack per
83
105
  spec). **Live overlay** instead reuses the one dev server you already have running:
84
- `/spec-live <spec>` rebases the branch onto base, frees it from its worktree, and
106
+ `/spec-live <spec>` or a bare `/spec-live`, which takes the spec you are
107
+ standing on — rebases the branch onto base, frees it from its worktree, and
85
108
  checks it out **in the primary checkout**, so your running server hot-reloads the
86
- feature at the normal URL — no second stack, no proxy. The branch checked out in
109
+ feature at the normal URL — no second stack, no proxy. Bare only acts when there
110
+ is exactly one answer *and* the workbench is free; anything else prints the
111
+ status report rather than guessing. The branch checked out in
87
112
  the primary checkout **is** the lock: exactly one spec is live at a time, and
88
113
  `/spec-live main` hands the instance back (fixes you make while live commit
89
114
  straight onto the branch; `/spec-complete` is live-aware and lands them). Rule of
@@ -93,6 +118,203 @@ stateful ones (`Stack: worktree + docker`, or a branch touching migrations) —
93
118
  `/spec-connect` + a Docker stack for those, and for genuinely parallel testing.
94
119
  Beneath it, `skitterspec spec-env live <take|release|abort|status>` is the engine.
95
120
 
121
+ **Staging one spec and not another (`spec-env stage`).** Several specs are
122
+ authored at once — that is what the worktree mode is for — and they all land in
123
+ the same `specs/` folder until they are committed. A `git add specs/` there
124
+ stages a *directory*, sweeping whatever another session happens to have written
125
+ into this spec's commit, under this spec's ticket trailer.
126
+ `skitterspec spec-env stage [<spec>] [--json]` is how a skill asks instead of
127
+ guessing: it splits the uncommitted tree into the paths that are this spec's —
128
+ its folder in **any** bucket, so a tree mid-`git mv` is handled, plus each
129
+ `spec.companionPaths` entry the project declares — and the paths that are not.
130
+ It reads **the tree you are standing in**, which is the worktree inside one and
131
+ the primary checkout outside, and prints which. The `owned` half is the spec's
132
+ **documents**, never its code: a phase's own implementation is `foreign`, because
133
+ the commits this bounds are the lifecycle ones. Nothing here refuses, writes, or
134
+ exits non-zero — `foreign` is a list of paths to leave alone, not an accusation.
135
+
136
+ **Naming the paths is necessary and not sufficient**, which is why every spec
137
+ commit is also pathspec-limited: `git add -- <paths>` **and**
138
+ `git commit -m "…" -- <paths>`. A checkout has one `.git/index` and every session
139
+ standing in it shares that index, so a bare `git commit` takes whatever another
140
+ session has already staged no matter how carefully this one staged its own. The
141
+ `add` is still needed — `git commit` cannot take a path git has never seen, which
142
+ is every brand-new spec folder — but it is the `--` on the **commit** that bounds
143
+ what lands. The two failures are different and the fix needs both halves.
144
+
145
+ **Reading a spec's diff (`/spec-diff`) — seeing the work, not running it.**
146
+ `/spec-connect` and `/spec-live` both exist to reach a *running app*.
147
+ **`/spec-diff`** answers the other question: what did this actually change? A
148
+ phase is built in its own worktree, so `git diff` in your terminal answers about
149
+ the base branch — and a 350-line diff read as terminal text is scrolling, not
150
+ review. The engine collects the worktree's changes with `git -C` and writes a
151
+ self-contained HTML page (`.spec-env/reviews/<spec>.html`, gitignored) with
152
+ whole-file context that folds away, a file tree, and new files included. You open
153
+ it locally, or publish it and read it on a phone.
154
+ **The diff never passes through the model**, so the page is free to produce
155
+ however large it is; the *written*
156
+ review — a short read plus `flag`/`confirm`/`good` notes — is the part that costs
157
+ tokens, and it is offered rather than assumed. It is a **skill**, model-invocable,
158
+ and it is gated on nothing: half a phase, a hand edit and a colleague's branch are
159
+ all ordinary inputs. `/spec-next` renders the page at the end of a phase and
160
+ offers the review; `skitterspec spec-env review <spec> [--branch]` is the engine.
161
+
162
+ **The page is not read-only — it takes a review pass back.** Tick `✓ accept` per
163
+ file as you read, write notes against a line or a whole file, answer the
164
+ questions a written review asked, then **end it in a decision** and hand it
165
+ back. Three buttons, each carrying its own verdict:
166
+ **`✓ Approve`** (commit it), **`↺ Request changes`** (work them now),
167
+ **`… Discuss first`** (report and talk). `/spec-diff` stores it (`--notes`),
168
+ plays back what it read, and routes on the verdict — `changes` **is** the
169
+ go-ahead, so it works the commented files immediately, while `discuss`, a
170
+ refused approval and a pass carrying no verdict at all report and wait, which
171
+ is what a bare paste has always done. Accepted files stay unopened either way.
172
+
173
+ **Two ways back, and both are ordinary.** A **served** page POSTs the pass to
174
+ its own URL; the engine holds it in a gitignored `.pending.json` beside the
175
+ notes sidecar and answers with a **six-digit code**. Read the code out and
176
+ `/spec-diff` claims it (`--claim`), merging exactly the pass that code names.
177
+ A `file://` page has no server to talk to — the File System Access API is
178
+ Chrome-only and absent on iOS Safari — so it copies to the clipboard and you
179
+ paste, exactly as before. The clipboard path is **not legacy**: it is the whole
180
+ story for a local reader.
181
+
182
+ The code is **not a secret** — it is printed on the page, and it cannot be a
183
+ gate against Claude either, because the store is a file Claude can read.
184
+
185
+ It was once true that a device reaching your page
186
+ **could not reach your conversation**, and that fact was the whole guard: a pass sat in the holding
187
+ area until you typed `/spec-reviewed`. It is no longer true, deliberately —
188
+ a phase that ends **waits** for its verdict, so the button you press on the page
189
+ is what carries the work on. What replaced the guard is two mechanisms and one
190
+ rule. The **serve token** — 48 bits of randomness in the URL path, minted per
191
+ server — decides who can POST at all. The **wait window** decides which pass may
192
+ be claimed without you naming it: the engine's `--claim-since` takes the one
193
+ pass that arrived *while this session was waiting*, acts on nothing when none
194
+ did, and refuses to choose when two did. And outside that window the rule stands
195
+ unchanged and absolute: **Claude never claims a pass it was not asked to**
196
+ (`/spec-diff` step 0, because nothing enforces it).
197
+
198
+ What you give up is real and worth naming: the page can now act. What you get is
199
+ the loop closing without anyone remembering a command — which is the failure the
200
+ old design traded it for.
201
+
202
+ What the code *does* is let you **tell two passes apart** — it is an address,
203
+ never a password. One waiting pass is claimed and acted on the moment you type
204
+ `/spec-reviewed`, with nothing read back: the round-trip verified *which* pass
205
+ in a design where the page pushed and the agent went looking, and that design is
206
+ gone. Two waiting is a refusal to guess — the one case where a stranger's pass
207
+ sits beside yours, and the one case where six digits are worth anyone's time.
208
+ Pasting the code (`/spec-reviewed 324199`) skips even that. A pass you disown is `--drop <code>`; left there it is reported on every
209
+ render until you stop reading the line.
210
+
211
+ A code that matches nothing refuses, **names nothing**, and never falls back to
212
+ "the only one" — that fallback is precisely what would let an unread pass
213
+ through. Claiming **consumes**: a code works once. A second pass from the same
214
+ page render supersedes the first, so the code on screen is always the pass on
215
+ screen; passes from different renders stand alongside each other.
216
+
217
+ **And a claimed pass never enters the model's context.** The engine holds it,
218
+ merges it, and reports the counts — six digits is what reaches Claude, where a
219
+ pasted blob costs context in proportion to how much you wrote. That is the same
220
+ rule the diff already follows, applied to the one place it used to break. What it did comes back as a **resolution** (`--resolve`),
221
+ so the next render shows each note struck through with a one-line account and you
222
+ verify the fix instead of trusting it. An accept is keyed to the file's
223
+ **content hash**, so it survives the commit that ends the phase and lapses by
224
+ itself when that file changes again — announced as `accepted earlier — changed since`, never
225
+ silently. All of it lives beside the page in gitignored `.spec-env/`, and
226
+ **the marks are information, never a gate**: nothing counts them and nothing
227
+ refuses on them.
228
+
229
+ **The verdict is chosen, never derived.** A verdict is one person's conclusion,
230
+ sent once per pass; a counting gate is a refusal computed from how many boxes
231
+ are ticked, and it stays forbidden. The one refusal here is not a count of
232
+ ticks: **committing** is unavailable while any **comment** is unresolved,
233
+ because you asked for something and it therefore cannot also be fine. Files you
234
+ never ticked block nothing — an unticked file is something you said nothing
235
+ about, and requiring every one of them would be the tally this design exists to
236
+ avoid.
237
+
238
+ **The gate — a phase that ended owes an answer.** That is the second refusal,
239
+ and it is a different kind: not a count, and not about the marks at all.
240
+ `/spec-next` **arms** it when a phase ends and renders its page, and exactly two
241
+ things clear it — a **committing verdict**, or
242
+ `skitterspec spec-env review skip "<reason>"`. Until one of them happens,
243
+ `/spec-next` refuses to build the next phase and (where the hook is installed)
244
+ `git commit` refuses in that worktree. `skitterspec spec-env review gate
245
+ [--check] [--json]` is what both ask.
246
+
247
+ **The hook is the half that cannot be talked past.** `skitterspec init` installs
248
+ `.claude/hooks/review-gate.js` and registers it in the project's committed
249
+ `.claude/settings.json`, so a `git commit` is refused by the harness rather than
250
+ by prose — which is what covers a bare `git commit`, a chained command, and
251
+ skittership's own `/commit` without skitterspec ever editing it. It decides
252
+ nothing itself: it hands the command line to
253
+ `spec-env review gate --check --for-command` and turns one exit status into an
254
+ answer. It refuses only where the commit is running
255
+ **inside that spec's own worktree** — a commit on the base branch, or in
256
+ another spec's tree, is not this obligation's business — and it fails **open** on everything else: no engine, an
257
+ unreadable payload, a crash, a timeout, a repo with no isolation.
258
+
259
+ Three things keep it a push rather than a wall. It is armed
260
+ **only by a phase ending**, so reading your own half-finished work mid-phase
261
+ owes nothing.
262
+ It has an **exit that is always one command**, and one of them is
263
+ *"I am moving on"* — with a reason, because `none: additive, nothing to revert` is a decision
264
+ a reviewer can argue with while silence is an oversight. And it accuses
265
+ **only on a positive signal**: an unreadable sidecar, a project that set
266
+ `review.required: false`, a spec the engine could not resolve — every
267
+ cannot-tell exits 0 and says nothing (`.claude/rules/negative-checks.md`).
268
+
269
+ It is on by default wherever isolation is configured. That is the point: the
270
+ push toward reading the diff is the normal path, and stepping off it is the
271
+ thing you have to do deliberately.
272
+
273
+ **The verdict names the action**, and that is why it is `commit` rather than
274
+ `approve`: a review is the guard in front of an action, and an approval that
275
+ only recorded itself was the one control on the page that did not describe what
276
+ it does. Both committing verdicts hand off to the project's own commit skill
277
+ (`review.commitWith` in `env.config.json`, `/commit` by default) — skitterspec
278
+ never vendors one, because `/commit` belongs to skittership.
279
+ **There is no off switch**: `"none"` existed and was removed, since it produced
280
+ exactly the record-and-do-nothing verdict this design is against.
281
+ `Commit & Continue` then runs `/spec-next` and **stops there** — it never
282
+ completes, lands or tears anything down.
283
+
284
+ A verdict is **consumed**, not stored: a commit verdict is spent by the commit
285
+ and a changes one by the work, so neither can go stale and later act on
286
+ something nobody read. What survives is a one-line outcome log, shown on the
287
+ next render as history.
288
+
289
+ **One ending, every skill (`.claude/rules/spec-reports.md`).** Every skill in
290
+ the table above finishes with the same block — a verdict, then a table of only
291
+ the fields that skill declares — and says nothing while it runs beyond a
292
+ question it cannot answer itself or a failure at the moment it happens. The
293
+ shape is defined once in that rule; each skill's `## Report` section names only
294
+ its verdicts and its fields.
295
+
296
+ ✅ **Phase 2 built** — `feat-orders`, 2 of 4
297
+
298
+ | | |
299
+ |---|---|
300
+ | **Tracker** | [ABC-88](https://example.invalid/ABC-88) · `feat-orders` · phase 2 moved |
301
+ | **Branch** | `spec/feat-orders` · 3 commits, clean |
302
+ | **Built** | POST /orders handler, orders schema |
303
+ | **Tests** | 128 passed · npm test |
304
+ | **Review** | 7 files, +212 −18 · [open the page](file:///…) — want a written review before you commit? |
305
+ | **Follow-ups** | none |
306
+ | **Next** | `/spec-next` → phase 3 (Auth) |
307
+
308
+ Four verdicts, and the last two are different facts about the repo: `✅` done ·
309
+ `⚠️` done with caveats · `❌` failed part-way, so there is a mess to clear ·
310
+ `⏸` refused before acting, so nothing changed.
311
+ **A refusal emits the block too** — "nothing happened" is a reported outcome
312
+ rather than an absent one.
313
+ `Follow-ups` is always present, because a recorded `none` is a decision where a
314
+ missing line is an oversight. And the block covers **that run only**: what else
315
+ is in flight is another question, and volunteering it here leaves the reader
316
+ unable to tell what followed from the run they just watched.
317
+
96
318
  **Ticketing-provider sync (opt-in, a separate package).** The base is
97
319
  tracker-free: it knows nothing about any specific ticketing system. A
98
320
  ticketing provider is installed as its own distribution that plugs into named
@@ -111,6 +333,24 @@ against a committed last-pushed snapshot and applies it) and `/spec-status`
111
333
  workflow-state drifted), backed by a `spec-sync` CLI. There is no content pull —
112
334
  the tracker is never read back or merged.
113
335
 
336
+ A provider may also ship a **read-only listing** of what the tracker holds. That
337
+ is not a pull: nothing is merged back and no spec file is written. It exists
338
+ because the folder buckets are only the truth *on the branch you are standing
339
+ on* — `/spec-start` moves a spec to
340
+ `specs/in-progress/` on that spec's own branch, so on the base branch an
341
+ in-flight spec still reads `backlog` and a teammate's unlanded spec is not on
342
+ disk at all. The tracker knows both, and each row carries the spec's folder name
343
+ so `/spec-start <name>` is a copy-paste away. See the provider's own docs for
344
+ what it calls the command.
345
+
346
+ A provider may also mirror **who is building a spec**, as one more field the repo
347
+ owns. Where it does, `/spec-start` records the developer and the tracker's ticket
348
+ is assigned to them, the assignment is released when the spec completes, and
349
+ `/spec-claim` moves ownership mid-flight — take it, hand it back, or hand it to a
350
+ teammate. Like everything else here it is opt-in and one-way: the spec file is
351
+ the record, and the ticket is the mirror. See the provider's own docs for how to
352
+ turn it on.
353
+
114
354
  **Every skill that moves a spec through the lifecycle carries a seam**, so the
115
355
  mirror keeps up without anyone remembering to push: `/spec`, `/spec-bug` and
116
356
  `/spec-hotfix` link the spec they create; `/spec-next` refreshes it as work starts;
@@ -0,0 +1,321 @@
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
+ `Notes` is the row most of that content belongs in.
73
+
74
+ **Two controls may follow the block, and nothing else ever.** The ban exists
75
+ because *prose* after the block competes with it for the reader's attention and
76
+ loses them the rows. A control does not compete: it is a row made actionable, in
77
+ the place the reader's eye already finishes. Prose after either of them is the
78
+ same violation wearing a control's clothes.
79
+
80
+ **The first is a picker.** Where the run ends in a choice the reader must make,
81
+ the options may be offered after the table.
82
+
83
+ **The second is the review call-to-action**, and only where the run is actually
84
+ **waiting** on a verdict. It is a banner, not a sentence:
85
+
86
+ ---
87
+
88
+ ## ⏸ Review ready — 7 files, +212 −18
89
+
90
+ **[Open the page](http://…)** · I'm holding here until you send a verdict.
91
+
92
+ `/spec-reviewed` picks it up · `spec-env review skip "<reason>"` moves on
93
+
94
+ ---
95
+
96
+ A rule, a heading naming the state and the size, the link in bold, one line
97
+ saying the run is stopped, and the two exits. Nothing else, and never a
98
+ paragraph explaining it.
99
+
100
+ **Only promise a wait the transport can deliver.** A page the engine SERVES
101
+ hands its pass to the local store, which a file-watch sees — that is the one
102
+ case where *"I'm holding here"* is true. A **published** page writes to the
103
+ artifact's own store, and nothing pushes from there into the conversation, so a
104
+ verdict pressed on it is invisible until someone asks for it. Its banner says
105
+ so instead:
106
+
107
+ **[Open the page](https://…)** · press a verdict, then type `/spec-reviewed` — I cannot see it until you do.
108
+
109
+ **Exactly one link, never two.** The banner offers the page the reader can
110
+ actually use, and the engine has already decided which that is — a served URL
111
+ when it could serve, a published one only when it could not. Handing over both
112
+ asks the reader to know which door the run is standing behind, and they cannot:
113
+ it was done, and a verdict was pressed on the published page three times while
114
+ each one sat unread under a line claiming the run was waiting. Two links is not
115
+ a convenience with a caveat; it is the caveat existing at all.
116
+
117
+ **This is not the old failure returning**, and the difference is the whole
118
+ justification. The offer used to be two quoted lines in the tail of a long
119
+ report, addressed to nobody, under a closing line that told the reader to move
120
+ on — and it was never once taken. It was moved into a labelled row to make it
121
+ findable, and that worked. What changed since is that the run now *waits*: the
122
+ reader is not being offered something optional, they are being told the work has
123
+ stopped until they answer. A row cannot carry that, because a row is scanned at
124
+ the same weight as every other row. So the state gets the loudest shape on
125
+ screen, at the end, where reading finishes.
126
+
127
+ **Where the run is not waiting, it stays a `Review` row.** A render nobody is
128
+ being held for — a mid-phase `/spec-diff`, a page produced alongside other work
129
+ — is a convenience, and giving it a banner teaches the reader to scroll past
130
+ banners. The shape marks the difference between *here is a page* and *nothing
131
+ proceeds until you answer*.
132
+
133
+ **Never fence a message to the reader.** A fenced block is for a command to
134
+ run, code, or engine output quoted verbatim — things the reader copies or
135
+ compares. A question, an offer, a hand-off is prose. The same mechanism that
136
+ made the old report unreadable applies to anything else in a grey box: it is
137
+ read as an artefact to skim rather than as something addressed to someone, so a
138
+ question inside one goes unanswered and an instruction inside one goes unread.
139
+
140
+ **It reports this run and nothing else.** Not what else is in flight, not the
141
+ other worktrees, not the backlog, not the state of the repo at large. A reader
142
+ finishing one piece of work cannot tell whether a line about some other spec is
143
+ a consequence of what just happened or an unrelated aside, and has to stop and
144
+ work it out — which is the cost this block exists to remove. If they want the
145
+ wider picture there is a skill that answers it; volunteering it here muddies the
146
+ one thing they asked about.
147
+
148
+ `Next` is the single next action **for this work**, not a menu of what else
149
+ could be done.
150
+
151
+ **And it must be runnable from the state the run actually leaves behind.** Most
152
+ of these skills deliberately do not commit — they finish, verify, and wait — so
153
+ the tree they hand back is dirty, and several of the commands they would name
154
+ next refuse on a dirty tree. A `Next` that omits the commit therefore sends the
155
+ reader to a refusal: `/spec-next` leaves a phase uncommitted and its own §2
156
+ refuses on exactly that, and `/spec-bug` leaves a fix uncommitted where
157
+ `/spec-complete` refuses the same way. Where the skill does not commit, the row
158
+ says `/commit, then <the thing>`.
159
+
160
+ This is not a style rule. `Next` is the one row a reader is meant to act on, and
161
+ it closes the block because the closing row should be the one that moves the
162
+ work on. A row that cannot be run spends the trust the position was designed to
163
+ earn — and the two skills that got it wrong got it wrong independently, which is
164
+ what makes it a missing constraint rather than two slips.
165
+
166
+ ## Field vocabulary, in this order
167
+
168
+ A skill emits only the fields it declares, in this order, skipping the rest.
169
+ `Follow-ups` is the exception: always present, second to last.
170
+
171
+ `Tracker` is the conditional one: a skill may declare it and still never emit
172
+ it, because no ticketing provider is installed. That is an absence with nothing
173
+ behind it — say nothing rather than reporting that there was nothing to report.
174
+
175
+ | Field | Carries |
176
+ |-------|---------|
177
+ | `Tracker` | **First.** The ticket id, linked where the spec has a url, the spec's folder name beside it, and what changed there. |
178
+ | `Why` | **Non-`✅` only.** What stopped it, in one clause. |
179
+ | `Branch` | Branch name, commit count, clean or dirty. |
180
+ | `Spec` | The spec document's own state: status, bucket, phase. |
181
+ | `Cause` | The root cause, for work that diagnosed one. |
182
+ | `Built` | What the run produced — the code, the edits, the spec written. |
183
+ | `Tests` | The result and the command that produced it. |
184
+ | `Notes` | What this run hit and handled — one short paragraph. Named `Snags` once, which announced a problem before the reader had read one; most of what belongs here is neither good nor bad, just worth knowing. 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. |
185
+ | `Landed` | A fast-forward, a tag, a cherry-pick. |
186
+ | `Worktree` | A worktree provisioned, entered, or torn down. |
187
+ | `Untouched` | Uncommitted work the run deliberately left alone — whose, and how much. |
188
+ | `Review` | The rendered diff page: files, `+`/`−`, the link — and the offer of a written review, in the same row. **Omitted entirely when the run is waiting on a verdict**: the banner after the block carries it instead, and a row saying the same thing beside it splits the reader's attention across two places. |
189
+ | `Follow-ups` | **Always.** `none`, or one line each. |
190
+ | `Next` | **Last.** The single next action for this work — runnable from the state the run leaves behind. |
191
+
192
+ The order runs identity → context → what happened → where it went → what to do.
193
+ **`Tracker` is first** because the id is what addresses this work outside the
194
+ repo, and **`Next` is last** because it is the only row the reader acts on: the
195
+ closing row should be the one that moves the work on.
196
+
197
+ **`Untouched` is a fact, not a caveat.** A run that provisions beside another
198
+ spec's uncommitted work has done nothing wrong and nothing partial — it left
199
+ alone something that was never its business — so it stays `✅` with a row, and
200
+ does not become `⚠️`. Several specs in flight at once is what the worktree mode
201
+ is *for*; spending the "something is worth knowing" verdict on the normal case
202
+ is how that verdict stops meaning anything. It is still reported rather than
203
+ silent, because "I provisioned, and your four files are still sitting there" is
204
+ a different sentence from "I provisioned".
205
+
206
+ **`Review` is one row, not two** — and where the run is waiting, it is no rows
207
+ at all. The page and the offer to read it are the same subject, and splitting
208
+ them made the reader resolve a distinction before acting on either. A waiting
209
+ run promotes the whole subject into the banner; keeping the row as well would
210
+ recreate that split with the two halves further apart than ever.
211
+
212
+ ## The four verdicts
213
+
214
+ | State | Means |
215
+ |-------|-------|
216
+ | `✅` | Done. Everything the skill set out to do happened. |
217
+ | `⚠️` | Done with caveats — it finished, but something is worth knowing. |
218
+ | `❌` | Failed part-way. It acted, and the repo is mid-something. |
219
+ | `⏸` | Refused before acting. Nothing changed. |
220
+
221
+ **`❌` and `⏸` are different facts about the repo**, and the difference decides
222
+ what happens next. A failed landing leaves a conflicted rebase and a standing
223
+ worktree — there is a mess to clear. A refusal leaves nothing at all, and the
224
+ only thing to do is supply what was missing. Collapsing them sends people
225
+ looking for wreckage that is not there, or past wreckage that is.
226
+
227
+ **A refusal still emits the block.** That is the point of having four states:
228
+ every invocation ends the same way, so an outcome is never inferred from
229
+ silence.
230
+
231
+ ## Where the block may grow
232
+
233
+ One place only. A non-`✅` verdict adds a `Why` row, and quotes the output that
234
+ justifies it — the failing assertion, the conflict, the engine's refusal — in a
235
+ fenced block **above** the table, where a fence means what a fence should mean:
236
+
237
+ ```
238
+ ✖ rejects a negative quantity
239
+ expected 422, got 500
240
+ ```
241
+
242
+ ❌ **Phase 2 stopped — tests red** — `feat-foo`, 2 of 4
243
+
244
+ | | |
245
+ |---|---|
246
+ | **Why** | 3 assertions fail in `orders.test.js`; nothing committed |
247
+ | **Branch** | `spec/feat-foo` · 2 commits, dirty |
248
+ | **Built** | POST /orders handler, orders schema |
249
+ | **Tests** | 125 passed, 3 failed · npm test |
250
+ | **Follow-ups** | none |
251
+ | **Next** | fix the failures, then `/spec-next` to finish phase 2 |
252
+
253
+ Everywhere else the block stays the size it is. Prose after it, an extra row of
254
+ your own invention, a second block — all three are the shape drifting back to
255
+ what this replaced.
256
+
257
+ ## Follow-ups
258
+
259
+ **Always present.** A recorded `none` is a decision; a missing line is an
260
+ oversight — the same logic as the `Gating` header.
261
+
262
+ The bar is deliberately high. A follow-up is something *this* work surfaced
263
+ that *this* spec will not fix — a wrong assumption, a missing guard, a decision
264
+ the spec deferred and the code then leaned on. It is **not** "could be tidier",
265
+ and it is **not** anything already sitting in the spec's Open questions.
266
+
267
+ When there is one, **offer to `/spec` it on the spot**. If the user does not
268
+ take the offer, write one dated line into the spec's Changelog before you
269
+ finish, so it outlives the session:
270
+
271
+ ```
272
+ - 2026-01-09 — Follow-up surfaced: connect's port picker assumes a free
273
+ canonical port; not in scope here.
274
+ ```
275
+
276
+ **Say where the spec would be written.** These skills run inside the spec's own
277
+ worktree, and a spec authored there physically lives on that branch — invisible
278
+ from the base branch, and cancelled along with its host if the host is
279
+ cancelled. So the offer names the primary checkout as the place to write it.
280
+ `spec-planning.md` and `commit-trailers.md` carry the reasoning.
281
+
282
+ ## Worked examples
283
+
284
+ **`⚠️` — it finished, with something worth knowing.**
285
+
286
+ ⚠️ **Phase 3 built; tracker not updated** — `feat-foo`, 3 of 4
287
+
288
+ | | |
289
+ |---|---|
290
+ | **Branch** | `spec/feat-foo` · 4 commits, clean |
291
+ | **Built** | Auth middleware, session table |
292
+ | **Tests** | 140 passed · npm test |
293
+ | **Follow-ups** | Session expiry is read from two places; phase 4 leans on one |
294
+ | **Next** | `/spec-next` → phase 4 (Docs) |
295
+
296
+ **`⏸` — refused, nothing changed.**
297
+
298
+ ⏸ **`/spec-next` refused — no spec in flight**
299
+
300
+ | | |
301
+ |---|---|
302
+ | **Why** | Not standing in a worktree, and 2 specs are provisioned |
303
+ | **Follow-ups** | none |
304
+ | **Next** | `/spec-start <name>`, or `cd` into `../repo-wt/feat-orders` or `../repo-wt/feat-auth` |
305
+
306
+ **`✅` — a spec finished and landed.**
307
+
308
+ ✅ **Landed and torn down** — `feat-foo`, complete
309
+
310
+ | | |
311
+ |---|---|
312
+ | **Tracker** | [ABC-88](https://example.invalid/ABC-88) · `feat-foo` · moved to its done state |
313
+ | **Spec** | Complete · `specs/complete/feat-foo` |
314
+ | **Tests** | 131 passed · npm test |
315
+ | **Landed** | base fast-forwarded to 4a1c9e2 |
316
+ | **Worktree** | removed · `../repo-wt/feat-foo` |
317
+ | **Follow-ups** | none |
318
+ | **Next** | pick the next spec from `specs/backlog/` |
319
+
320
+ The `❌` example is in **Where the block may grow** above, since the failing
321
+ output it quotes is the thing that example exists to show.