@skitterbyte/skitterspec 18.0.0 → 20.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 (38) hide show
  1. package/MIGRATION.md +296 -10
  2. package/README.md +53 -4
  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/env.config.json.example +7 -3
  7. package/assets/core/env.config.md +90 -30
  8. package/assets/hooks/review-gate.js +141 -0
  9. package/assets/review/page.html +1787 -0
  10. package/assets/rules/spec-planning.md +250 -10
  11. package/assets/rules/spec-reports.md +321 -0
  12. package/assets/skills/spec/SKILL.md +32 -4
  13. package/assets/skills/spec-bug/SKILL.md +113 -8
  14. package/assets/skills/spec-cancel/SKILL.md +84 -19
  15. package/assets/skills/spec-complete/SKILL.md +80 -23
  16. package/assets/skills/spec-diff/SKILL.md +678 -0
  17. package/assets/skills/spec-hotfix/SKILL.md +113 -10
  18. package/assets/skills/spec-init/SKILL.md +56 -7
  19. package/assets/skills/spec-next/SKILL.md +408 -7
  20. package/assets/skills/spec-review/SKILL.md +26 -3
  21. package/assets/skills/spec-reviewed/SKILL.md +258 -0
  22. package/assets/skills/spec-start/SKILL.md +283 -106
  23. package/assets/skills/spec-to-main/SKILL.md +28 -6
  24. package/package.json +11 -7
  25. package/src/cli.js +1808 -89
  26. package/src/env/building.js +143 -0
  27. package/src/env/commitcmd.js +108 -0
  28. package/src/env/config.js +58 -9
  29. package/src/env/hooks.js +117 -0
  30. package/src/env/provision.js +54 -15
  31. package/src/env/proxy.js +34 -1
  32. package/src/env/render.js +3 -12
  33. package/src/env/resolve.js +295 -9
  34. package/src/env/review.js +1536 -0
  35. package/src/env/serve.js +573 -0
  36. package/src/env/teardown.js +13 -6
  37. package/src/init.js +150 -1
  38. package/LICENSE +0 -21
@@ -0,0 +1,678 @@
1
+ ---
2
+ name: spec-diff
3
+ description: See what a spec's worktree changed — render its diff as a page you can mark up, take that review pass back, and act on it. Answers at any point, including half-way through a phase. Use when the user says "/spec-diff", "show me the diff", "what did this phase change", "review this spec's work", wants to read a worktree's changes away from the terminal, or hands back what the review page produced — a six-digit claim code, or the pasted JSON.
4
+ ---
5
+
6
+ # /spec-diff — see the phase before you commit it
7
+
8
+ > Stay silent while this runs — speak only to ask something you cannot answer
9
+ > yourself, or to report a failure at the moment it happens. Read
10
+ > `.claude/rules/spec-reports.md` before reporting; it defines the block this
11
+ > skill ends with.
12
+
13
+ A phase is built in its own worktree, and the terminal is somewhere else — so
14
+ `git diff` answers about the wrong branch and a 350-line diff read as terminal
15
+ text is scrolling, not review. This renders the diff as a page instead: the
16
+ engine collects it with `git -C`, writes a self-contained HTML file, and you open
17
+ it locally or publish it and read it on a phone.
18
+
19
+ **The diff never passes through the model.** git writes the patches and the
20
+ engine splices them into the page. A 266KB patch costs **zero** context tokens,
21
+ and a 289KB page costs the same to publish as a 5KB one. Do not "simplify" this
22
+ by reading the diff and emitting HTML — that is the one change that would undo
23
+ the whole design.
24
+
25
+ ## 1. Resolve the spec — three rules, in order
26
+
27
+ 1. **The name argument**, when given. An unknown name **refuses** and says so:
28
+ falling back would review a different spec's work under the name you typed,
29
+ which looks exactly like a correct answer.
30
+ 2. **The spec in flight for this session** — `skitterspec spec-env live status`
31
+ names the spec whose branch is checked out in the primary checkout.
32
+ 3. **The worktree this session is standing in** — if cwd is inside a spec's
33
+ worktree, that is the spec.
34
+
35
+ If none answers and several specs have worktrees, **list them and stop**. The
36
+ engine does this for you: run the verb with no name and it either resolves the
37
+ sole candidate or prints the candidates.
38
+
39
+ ## 2. Were you handed a review pass? Then that is the job
40
+
41
+ The page has marks on it — `✓ accept` per file, notes against a line or a whole
42
+ file, answers to the checks a written review asked — and it
43
+ **ends in a decision**: `✓ Approve`, `↺ Request changes` or `… Discuss first`.
44
+ A review comes back to you one of two ways, and **both are ordinary**:
45
+
46
+ - **A six-digit code** — `418207`, on its own. A *served* page hands its pass
47
+ straight to the engine, which holds it until someone reads the code out. This
48
+ is the usual way on a phone.
49
+ - **A pasted JSON blob** — a `file://` page has no server to talk to, so it
50
+ copies. Not legacy: it is the whole story for a local reader.
51
+
52
+ Either way, **this is not a request to render anything**: it is a review coming
53
+ back, and these steps replace §3–§5 below.
54
+
55
+ 0. **Never claim a pass you were not asked to claim.** And never go looking
56
+ for one.
57
+
58
+ A device that reaches the page can POST all day. What decides whether one of
59
+ those passes reaches the operator's review is this rule, and — since the wait
60
+ in §4b — two mechanical facts beside it: the **serve token**, 48 unguessable
61
+ bits minted per server, which decides who can POST at all; and the wait
62
+ **window**, which decides which pass a watch may claim. There is exactly one
63
+ way a pass is taken without a person naming it, `--claim-since`, it is
64
+ described in §4b, and it acts on nothing unless precisely one pass arrived
65
+ while this session was waiting for it.
66
+
67
+ Everything else here is unchanged. A pass that was already sitting there when
68
+ you arrived is never yours to take.
69
+
70
+ **Do not read the code out of `.spec-env/reviews/<spec>.pending.json`.** The
71
+ store is a file you can open, so nothing stops you; that is precisely why the
72
+ rule has to be stated rather than assumed. It has already happened once — an
73
+ agent found a waiting approval, read its code off disk, claimed it, and
74
+ reported the round-trip working. The operator had pressed the button, so the
75
+ outcome was harmless and the reasoning was wrong.
76
+
77
+ The render tells you everything a decision needs, so there is nothing to go
78
+ looking for:
79
+
80
+ ```
81
+ pending: 1 waiting
82
+ 792969 · commit · 1 min ago
83
+ ```
84
+
85
+ **Offer it, naming the code**, and wait: *"an approval is waiting, code
86
+ 792969, sent a minute ago — does that match your phone?"* The digits travel
87
+ **to** the operator, not from them — they verify rather than transcribe, and
88
+ the code is the only part they can check, so
89
+ **name it rather than describing the pass** (a stranger's approval and theirs
90
+ read identically otherwise).
91
+
92
+ **Two or more waiting is a refusal to guess.** Name them all and ask which.
93
+ Never take the newest, the oldest, or the only `commit` — that is exactly
94
+ the case where someone else's pass is sitting beside the operator's, and it
95
+ is the only case where reading six digits out is worth anyone's time.
96
+
97
+ **On "that isn't mine"**, leave it and offer to drop it:
98
+
99
+ ```
100
+ skitterspec spec-env review <spec> --drop <code>
101
+ ```
102
+
103
+ A pass that stays is reported on every render until the operator stops
104
+ reading the line — which is how the real one gets missed.
105
+
106
+ 1. **Take it in through the engine**, whichever way it arrived.
107
+
108
+ **A code** — claim it, once the operator has confirmed it is theirs (step 0):
109
+
110
+ ```
111
+ skitterspec spec-env review <spec> --claim <code>
112
+ ```
113
+
114
+ **A pasted blob** — write the JSON to a scratch file
115
+ verbatim — never retype it, never "tidy" it — and merge it:
116
+
117
+ ```
118
+ skitterspec spec-env review <spec> --notes <file>
119
+ ```
120
+
121
+ Both validate wholesale and write nothing if the blob is malformed or names a
122
+ different spec; a claim additionally refuses a code that matches nothing, and
123
+ **names nothing when it does** — listing the waiting codes would hand a
124
+ guesser the answer. **Relay a refusal as it is written** and stop; every
125
+ message says which entry was wrong, so there is nothing to guess.
126
+
127
+ **A claim is a delivery mechanism, not a second kind of review.** Everything
128
+ below reads the same merged pass and the same verdict; nothing may behave
129
+ differently because of how it arrived.
130
+
131
+ 2. **Read the verdict the engine judged.** The pass says what it CONCLUDED, and
132
+ the engine prints it — `committing with <skill>`, `committing with <skill>,
133
+ then the next phase`, `changes requested`, `discuss first`, or
134
+ `commit refused — <reason>`. It is judged, not taken on trust: a committing
135
+ verdict arriving with an open comment is refused and routed to discuss,
136
+ because you asked for something and it therefore cannot also be fine.
137
+ **Never re-judge it yourself, and never count anything** — read the engine's
138
+ answer and route on it.
139
+
140
+ Three routes, and every pass takes exactly one:
141
+
142
+ | Verdict | What it means | Where to go |
143
+ |---------|---------------|-------------|
144
+ | `commit` (honoured) | this is fine, commit it | §2a — commit, then stop |
145
+ | `commit-continue` (honoured) | this is fine, keep going | §2a — commit, then `/spec-next` |
146
+ | `changes` | do these, now | step 4 — **skip the wait**, this is the go-ahead |
147
+ | `discuss` | I have a question | step 3 — report, then **ask what's up** |
148
+
149
+ **A refused commit and a pass with no verdict both mean `discuss`.**
150
+ Neither is a special case: the engine routes the refusal there itself, and an
151
+ absent verdict has always meant "report it and wait" — which is why that is
152
+ what it still means.
153
+
154
+ 3. **Say what you read, then ask what's up.** Report the accepted count, then
155
+ each open comment as `file:line — note`, then the files you would touch.
156
+ **Wait — unless the verdict already said otherwise.** Pasting on its own is
157
+ not a go-ahead: this skill is read-only everywhere else, a misread comment
158
+ costs a revert, and the operator may only have wanted it recorded. A `changes`
159
+ verdict **is** that go-ahead, given deliberately on the page, so asking again
160
+ is asking someone to decide twice. The reasoning is unchanged; what changed is
161
+ that the page can now answer it in advance.
162
+
163
+ **`discuss` means "ask me what's up"** — an opening move, not a stopping
164
+ place. A reader who pressed it has a question, and a summary that ends in
165
+ silence leaves them to ask it themselves. Report what you read and then
166
+ **open the conversation**: name what you would do next and ask whether that
167
+ is what they wanted. The same wording has to work for a pass that chose
168
+ nothing at all, since an absent verdict means this too — so ask about the
169
+ review, never about the button.
170
+
171
+ 4. **On the go-ahead, work only the commented files.** Read those; do **not**
172
+ open the accepted ones. That is the whole saving the marks buy, and it is
173
+ only worth anything if it is true — so say plainly which files you did not
174
+ open. Make the changes, then run the project's typecheck and test commands.
175
+
176
+ **First, compare the worktree against where you are standing.** This is the
177
+ one step here that writes, and §1's first rule resolves by **name** — so
178
+ editing a tree this session is not standing in is the ordinary case, not an
179
+ edge one. Take the `worktree:` line from
180
+ `skitterspec spec-env resolve <spec>` and compare it with cwd, resolving both
181
+ paths first so a symlinked or trailing-slash spelling of one tree does not
182
+ read as two. Same tree, and everything below is inert.
183
+
184
+ **Different trees, and the discipline applies.** Record the baseline before
185
+ the first edit:
186
+
187
+ ```
188
+ skitterspec spec-env resolve <spec> --record-primary
189
+ ```
190
+
191
+ Then every write takes an absolute path under the worktree and every command
192
+ is prefixed `cd "<worktreePath>" &&` — typecheck and tests included. A single
193
+ relative path lands the fix in the primary checkout, on the base branch, and
194
+ nothing about it looks wrong at the time.
195
+
196
+ **This is a write discipline, not a precondition.** It changes *how* this
197
+ step writes, never *whether* it runs — §3 below still holds in full, and a
198
+ later edit reading this as a gate would undo the rule it exists beside.
199
+
200
+ 5. **Write back what you did**, one entry per comment you acted on:
201
+
202
+ ```json
203
+ [{ "id": "2026-01-01T00:00:00.000Z-1", "note": "keyed the accept on the blob sha" }]
204
+ ```
205
+
206
+ ```
207
+ skitterspec spec-env review <spec> --resolve <file>
208
+ ```
209
+
210
+ The note is the load-bearing half: it is what lets the next read **verify**
211
+ the fix rather than trust it. An id that matches nothing is reported and
212
+ skipped, so one bad id never costs you the rest. Then re-render (§4) so the
213
+ page shows each note struck through with its account.
214
+
215
+ 6. **Before the re-render, prove nothing leaked** — only when step 4 found two
216
+ trees. Nothing should be reported fixed before it is known to be fixed in the
217
+ right one:
218
+
219
+ ```
220
+ skitterspec spec-env resolve <spec> --assert-primary-clean
221
+ ```
222
+
223
+ - **Exit 0, "primary checkout clean"** — carry on.
224
+ - **Non-zero** — stop and relay the engine's message unchanged. It names the
225
+ paths and both readings: this run wrote them and they belong in the
226
+ worktree, or something else did and the baseline wants re-recording.
227
+ **Do not guess which, and do not delete anything.** A path that appeared is
228
+ not proof of who put it there.
229
+ - **"cannot tell"** — no baseline, or one from another spec. It exits 0 and
230
+ claims nothing; say so in one line and carry on. An absence is not evidence.
231
+
232
+ WHAT WOULD FOOL THIS CHECK: it watches the **primary checkout** and nothing
233
+ else, so a fix written into *another* spec's worktree would leak there unseen.
234
+ Left unhandled deliberately — the cost of the gap is a missed leak, never a
235
+ false accusation.
236
+
237
+ **Never commit on a `changes` pass.** It authorises the work, not a commit —
238
+ only `commit` and `commit-continue` reach §2a. The fixes sit in the worktree
239
+ where the operator can read them on the next render, which is the whole point of
240
+ sending them back rather than approving.
241
+
242
+ ## 2a. A committing pass commits — through the project's own skill
243
+
244
+ Only on an **honoured** `commit` or `commit-continue`. A refused one did not
245
+ happen.
246
+
247
+ The engine names the skill to use on the verdict's `commitWith` — the
248
+ `review.commitWith` config key, `/commit` by default. Do not read the config
249
+ yourself; one answer, from the engine that owns it. **There is no off switch:**
250
+ `"none"` existed and was removed, because a verdict that records itself and does
251
+ nothing is the one thing a review page must not offer.
252
+
253
+ - **A skill you have** — invoke it, and say which one. **Never vendor it.**
254
+ `/commit` ships with **skittership**, a different package: it stages the
255
+ task's files, runs the project's checks, and writes the release-note footers
256
+ this repo's changelog is built from. A copy of it living here would be a fork
257
+ of someone else's skill that drifts silently.
258
+ - **A skill you do not have** — commit it yourself: stage only the files this
259
+ work touched, run the project's typecheck and test commands, and write a
260
+ conventional commit. **Say that you did, every time.** A commit made under
261
+ rules nobody configured must never be reported as one made under `/commit`.
262
+
263
+ **Decide availability from the skill list you already have**, never by testing
264
+ for a file. A skill can legitimately live in several places, so `.claude/skills/
265
+ commit/SKILL.md` missing is an absence that proves nothing
266
+ (`.claude/rules/negative-checks.md` rule 1) — and being wrong about it means
267
+ committing by hand while reporting a hand-off. You are told which skills you
268
+ have; that list is the answer.
269
+
270
+ **Let the commit's own failure be the answer.** If typecheck or the tests fail,
271
+ there is no commit — report the failure and stop. Do not fix the tests to get
272
+ the commit through, and do not commit around them.
273
+ **An approval judges the change; it never promises that it builds**, and the two
274
+ must not be conflated by a skill acting on someone's behalf.
275
+
276
+ **Then record what it produced**, so the page shows the outcome rather than the
277
+ intent:
278
+
279
+ ```
280
+ skitterspec spec-env review <spec> --outcome "committed <sha> via <skill|by hand>"
281
+ ```
282
+
283
+ That writes the outcome onto the decision the engine already logged and
284
+ re-renders the page, where it reads as history beneath the verdict bar. On a
285
+ failed commit there is no outcome to record — say what failed instead.
286
+
287
+ **Nothing is pushed.** The commit is local, exactly as `/commit` leaves it.
288
+
289
+ ### `commit-continue` — then the next phase, and no further
290
+
291
+ On `commit-continue`, once the commit is in, carry on into **`/spec-next`**.
292
+ That is the whole of the chaining, and both halves of that sentence matter:
293
+
294
+ - **A failed commit is the end of the chain.** If typecheck or the tests fail
295
+ there is no commit, and therefore no continue — report where it broke and
296
+ stop. The continue is downstream of the commit, not beside it.
297
+ - **Never `/spec-complete`.** When there is no unfinished phase left, say the
298
+ spec has none and stop. Completing
299
+ **lands the branch and tears the worktree down**, and that must not fall out
300
+ of a button labelled *continue* — the
301
+ distance between "build the next phase" and "delete the worktree" is one skill
302
+ name, and a person pressing a button on a phone cannot see which one you
303
+ picked.
304
+
305
+ **This overturns a recorded Non-goal, and cites it rather than contradicting it.**
306
+ `feat-review-verdict` ruled chaining out: *"'Commit what I just read' and
307
+ 'go build the next thing unattended' are different sizes of decision, and the
308
+ second stays a keystroke."* That conflated two meanings of unattended — nobody
309
+ choosing, and nobody watching. A distinctly-labelled fourth button is chosen,
310
+ deliberately, by the person who just read the diff. The ban was right about an
311
+ *automatic* chain and caught a chosen one by accident.
312
+
313
+ **Passes waiting are information — and worth raising.** A render lists them
314
+ when the holding area is not empty: code, verdict, age, one per line. Nothing
315
+ counts them and nothing refuses over them; a pass nobody claims simply sits
316
+ there. But **say so** — an operator who pressed a button on their phone and hears
317
+ nothing has no way to tell a pass that never arrived from one waiting to be
318
+ confirmed, and both look like silence. Raising it is step 0's offer; never
319
+ treating it as a task is the rule that survives.
320
+
321
+ **A mark is information, never a gate.** Nothing counts the ticks or requires
322
+ them: a phase may end with comments open, `/spec-complete` never learns about
323
+ them, and this skill refuses nothing on their account. If a project ever wants
324
+ otherwise that is a config key defaulting to off — not a tidy-up here.
325
+
326
+ **What the intake costs, and why a code costs less.** A pasted blob is file
327
+ paths and the operator's own words, which you need in context to act on them —
328
+ so the paste is not overhead. But it does scale with the review: a marked-up
329
+ 60-file pass is kilobytes of context before any work starts.
330
+
331
+ **A claimed pass never enters the context at all.** The engine holds it, merges
332
+ it and reports the counts; six digits is what reaches you. That is the same rule
333
+ the diff already follows — git writes it, the engine splices it, you never read
334
+ it — and the paste was the one place it broke. The *work* either authorises is
335
+ ordinary phase-sized cost, and step 3 is where the operator decides whether to
336
+ spend it.
337
+
338
+ ## 3. Gate it on nothing
339
+
340
+ **This skill has no preconditions and must never grow one.** Not tests passing,
341
+ not the phase being finished, not the spec being this session's, not a clean
342
+ tree. Reviewing work in progress is the *common* case — half a phase, a hand
343
+ edit, a colleague's branch — and a gate would refuse at exactly the moment
344
+ someone wants to look.
345
+
346
+ If a later edit is tempted to add "only when the phase is complete", the answer
347
+ is no. The page is free to produce and changes nothing. The same goes for the
348
+ marks: a spec with unread files or open comments is an ordinary spec, and
349
+ nothing here may start counting them.
350
+
351
+ **And this skill never arms the gate.** Arming is `/spec-next`'s, at the one
352
+ moment that means something: a phase ended. Rendering mid-phase — the common
353
+ case this skill exists for — must not create an obligation, because then
354
+ looking at your own work halfway through would owe you a verdict on it. Reading
355
+ is free; ending a phase is what is answerable.
356
+
357
+ An **already-armed** gate is a different matter, and it is not this skill's to
358
+ enforce either: `/spec-next` §2 is where that refusal lives. Here it is only
359
+ context — the render says a verdict is owed, and this skill's whole job is to
360
+ help someone give one.
361
+
362
+ ## 4. Render the page — or serve it
363
+
364
+ ```
365
+ skitterspec spec-env review <spec> # uncommitted work (the default)
366
+ skitterspec spec-env review <spec> --branch # everything since the base branch
367
+ skitterspec spec-env review serve # every spec, on localhost
368
+ ```
369
+
370
+ **The engine handles the switch.** A file when the reader is at this machine, a
371
+ served URL when they are not: on a `remote` reader it stands its own server up
372
+ and puts a URL the reader can open on the `open:` line. Both are free and neither
373
+ publishes anything. You are not choosing between them; you are relaying whichever
374
+ one the engine printed.
375
+
376
+ The operator who does not want a LAN listener started for them sets
377
+ `review.serveOnRemote: false` in `env.config.json`, and the `file://` link with
378
+ its *will not open where you are reading* marker comes back.
379
+
380
+ `serve` renders **per request**, so nothing it shows can be stale, and it lists
381
+ every spec with a worktree rather than one. `--host 0.0.0.0` binds beyond
382
+ loopback and prints a URL a phone on the same network can open, guarded by an
383
+ unguessable path token. Bare, it binds loopback only. It is a process:
384
+ `--status` says whether one is up, `--stop` takes it down.
385
+
386
+ Default to the working tree — "what did this phase just do". Use `--branch` when
387
+ the question is about the whole spec.
388
+
389
+ **You do not have to reach for `--branch` after a commit.** A clean working tree
390
+ is the state a phase *ends* in, so the engine falls back to the branch range by
391
+ itself and says which it is showing — `(working tree clean — since main)` in the
392
+ header line, and `(working tree clean)` on the page. Report that wording as it
393
+ came rather than calling it the working tree.
394
+
395
+ The fallback fires on exactly one state — no `--branch`, and nothing uncommitted
396
+ to show. An explicit `--branch` always means what it says, a tree with real
397
+ changes is never swapped out from under you, and a branch with no work at all
398
+ still reports `nothing to review` exactly as before.
399
+
400
+ Add `--json` to get the file list, totals and the page path back as data. The
401
+ page is written to `.spec-env/reviews/<spec>.html`, which is gitignored — it
402
+ leaves no trace in the branch under review.
403
+
404
+ **On `--page-only`, stop here** and report the path.
405
+
406
+ ## 4a. Read the `reader:` line — never sniff for it yourself
407
+
408
+ `spec-env review` reports where it believes the reader is, and
409
+ **that is the only place this question is answered.** Three states:
410
+
411
+ | `reader:` | What to offer |
412
+ |-----------|---------------|
413
+ | absent (`unknown`) | the `file://` URL, exactly as always. **Do not warn** — unknown is the ordinary state of a local machine |
414
+ | `local` | the `file://` URL |
415
+ | `remote` | the `open:` line as printed — the engine already served it. Pass on any `also:` lines too |
416
+
417
+ **Never read an environment variable to decide this.** Not `SSH_CONNECTION`, not
418
+ `CLAUDE_CODE_*`, not a tty check — the engine already did it, reports the answer
419
+ on that line and in `--json`, and a second implementation here could not be
420
+ tested and would drift from the first. The ranking and the traps
421
+ (`CLAUDE_CODE_ENTRYPOINT` describes the *process*, not the reader; stdin is never
422
+ a tty under Claude Code) live in `review.js` beside the code, which is where they
423
+ belong.
424
+
425
+ **It authorises serving, never publishing.** A `remote` reader authorises a
426
+ local server — one process, ended by one flag, leaving nothing behind — and
427
+ nothing more. Publishing leaves a page this tooling cannot remove, so it is an
428
+ ask, in every case, always. The two were once one rule, and lumping them together
429
+ is what left a remote reader holding a dead link: see
430
+ `specs/complete/bug-remote-reader-gets-a-dead-link/`.
431
+
432
+ When serving fails — a busy port, or a machine with no network address — the
433
+ engine falls back to the `file://` URL with its marker and nothing breaks. That
434
+ is the one case where naming publishing is worth doing, because it is the only
435
+ answer left.
436
+
437
+ `review.reader` in `env.config.json` (`local` · `remote` · `detect`) lets the
438
+ operator settle where they are reading, and an explicit value is believed without
439
+ sniffing. `review.serveOnRemote` (default `true`) settles whether the engine may
440
+ act on it.
441
+
442
+ ## 4b. Wait for the verdict, where the harness can watch a file
443
+
444
+ A served page can hand its pass back the moment it is pressed. Without a wait,
445
+ that pass lands in the holding area and stops — nothing happens until someone
446
+ types `/spec-reviewed`, so the review and the work carrying on are two separate
447
+ acts joined only by the operator remembering.
448
+ **This is the step that joins them**, and it is this skill's, so `/spec-next` can point here rather than
449
+ keeping a second copy.
450
+
451
+ **Wait when the page was served and the reader is going to read it now.** That
452
+ is the phase-end case, and an operator who says they will look later. Do not
453
+ wait on a bare `--page-only`, on a render nobody asked to be woken about, or
454
+ when the pass has already arrived — there is nothing to wait for.
455
+
456
+ 1. **Note the moment**, as an ISO timestamp, before you start. That instant is
457
+ the entire scope of what may be claimed without a person naming it.
458
+ 2. **Watch** `.spec-env/reviews/<spec>.pending.json` in the primary checkout,
459
+ and **end the turn**. Not a poll and not a held-open turn: the operator has
460
+ their terminal back, and the session costs nothing while they read.
461
+ 3. **On waking, let the engine choose:**
462
+
463
+ ```
464
+ skitterspec spec-env review <spec> --claim-since <timestamp> --json
465
+ ```
466
+
467
+ Three answers, and only one of them acts. One pass in the window — that is
468
+ the pass, claimed and merged exactly as `--claim` would. **None** — ordinary;
469
+ the file changed for some other reason, so say nothing and wait again or
470
+ stop. **Two or more** — it refuses and names the count, never the codes; that
471
+ is §2 step 0's two-passes case, so offer them from the render and ask.
472
+ 4. **Route on the verdict** through §2 step 2 onward. A claim is a delivery
473
+ mechanism; nothing downstream may behave differently because a watch woke
474
+ you rather than a person typing.
475
+
476
+ **What holds this up.** Not "the page cannot reach the conversation" — after
477
+ this step it can, deliberately. Two things replace it. The **serve token** is
478
+ 48 bits of `crypto` randomness in the URL path, minted per server, and it
479
+ decides who can POST at all. The **window** decides which pass is yours: a pass
480
+ sitting there before you started waiting is never swept up, which is precisely
481
+ the stranger's pass the old rule was written about, and two arrivals refuse
482
+ rather than pick. What is left of the old rule is unchanged and still absolute —
483
+ outside this window, a pass is claimed because a person named it.
484
+
485
+ **The wait covers the SERVED page and nothing else.** It watches the engine's
486
+ local store, which is where a served page POSTs. A **published** page writes to
487
+ the artifact's own store instead, and no watch of any kind reaches that — so a
488
+ verdict pressed there is invisible until someone asks for it. Never start a
489
+ wait and then hand over a published link under it: that reads as a promise to
490
+ notice, and it was made three times in a row while three verdicts sat unread.
491
+ Where the page is published, say plainly that `/spec-reviewed` is what picks it
492
+ up.
493
+
494
+ **`/spec-reviewed` is not replaced by this.** It stays the way in for a pass
495
+ that arrived when nobody was waiting, for the two-passes case, for every
496
+ published page, and for every harness with no file-watch at all. It is user-only, and that is still the
497
+ enforcement that makes a named claim a person's decision.
498
+
499
+ ## 5. Offer the written review — say what it costs first
500
+
501
+ The page is free. The **written review is not**, and it costs in two separate
502
+ ways. Quote the one that actually applies rather than a single number:
503
+
504
+ - **Writing it: ~700 output tokens.** The review JSON, near enough regardless of
505
+ how big the diff is. This is unavoidable and it is what was measured.
506
+ - **Reading the diff: input, and it scales.** Only paid when the diff is not
507
+ already in front of you. A 350-line diff is easily 10–15k input tokens.
508
+
509
+ **If you just built this phase, you already have the diff — do not re-read it.**
510
+ That is the common case (`/spec-next` offers this skill the moment a phase ends)
511
+ and the one the ~700 was measured in. Running `git diff` over code you wrote
512
+ three tool calls ago buys nothing and is not free.
513
+
514
+ **When you genuinely do not have it** — a fresh session, half a phase from
515
+ yesterday, a colleague's branch — read *selectively*. `--json` returns the file
516
+ list with each file's `+`/`−`, `status` and `noise` flag and **no patches**, so
517
+ use it to choose: skip everything marked `noise: true`, and skip files the review
518
+ will not have anything to say about. Pulling the whole diff in when three files
519
+ matter is the avoidable half of this cost.
520
+
521
+ Say what it will cost, then let the operator decide. Do not write it unasked when
522
+ the diff is large.
523
+
524
+ When asked, write JSON to a scratch file:
525
+
526
+ ```json
527
+ {
528
+ "summary": "Two or three sentences on what this phase actually did, read from the diff.",
529
+ "checks": [
530
+ { "level": "flag", "file": "src/env/review.js", "note": "…" },
531
+ { "level": "confirm", "file": "test/env-review.test.js", "note": "…" },
532
+ { "level": "good", "file": "src/cli.js", "note": "…" }
533
+ ]
534
+ }
535
+ ```
536
+
537
+ - **`flag`** — something you believe is wrong. **`confirm`** — something only the
538
+ author can settle. **`good`** — a decision worth keeping, said once.
539
+ - Every check names the file it is about.
540
+ - **Review the diff, not the spec.** Keep to what the change shows; do not
541
+ re-derive the plan or restate the spec's reasoning back at its author.
542
+
543
+ Then re-render with it spliced in — the engine renders it, you never emit HTML:
544
+
545
+ ```
546
+ skitterspec spec-env review <spec> --review <file>
547
+ ```
548
+
549
+ ## 6. Publish only when asked
550
+
551
+ **Never publish unprompted.** Publishing leaves something behind that this
552
+ tooling cannot remove, and the page already works as a local file. Offer it;
553
+ do not assume it.
554
+
555
+ When the user asks:
556
+
557
+ - **One spec is one page.** `--json` reports `url` when this spec has been
558
+ published before. If it has, **update that URL** rather than creating a
559
+ second entry, and label the new version for the phase (`phase-3`).
560
+ - **On the first publish**, write the returned URL to the path `--json` reports
561
+ as `urlFile` — one line, no formatting. That file is how every later phase
562
+ finds the same page. Never construct the path yourself.
563
+ - **Say, once, that it is theirs now.** When you report a URL, say in the same
564
+ breath that skitterspec cannot remove the page and that `/artifacts` (or the
565
+ gallery at `claude.ai/code/artifacts`) is where it goes. This is the moment the
566
+ decision is being made, so it is the moment worth saying it — `spec-env down`
567
+ repeats it at teardown, by which point the page has outlived the spec.
568
+ - **Degrade in one line.** If the harness cannot publish — no capability, an
569
+ error — say so, report the local file path, and carry on. That is a working
570
+ outcome, not a failure.
571
+ - **Reach for `--publish-copy`, never a hand transform.** The engine writes the
572
+ page as a complete HTML document and an artifact host wraps page *content*, so
573
+ publishing the page as-written nests two documents. `spec-env review <spec>
574
+ --publish-copy` emits the body-only copy and names its path on a `publish:`
575
+ line. Do not split the document yourself: a rendered page contains the diff,
576
+ and a diff of this project contains `<!doctype html>` as ordinary patch text.
577
+
578
+ The engine knows nothing about publishing and cannot do it. It writes a file and
579
+ reads a URL back as an opaque string; everything about what that string means
580
+ lives here.
581
+
582
+ ### Publish it so the buttons still work
583
+
584
+ A published page is the answer for a reader **no local server can reach** — a
585
+ phone on mobile data, anyone away from the LAN the engine serves on. It is also
586
+ the one surface where the verdict buttons have nowhere to POST: the page's own
587
+ URL is on claude.ai, so a POST fails and the reader gets "could not reach the
588
+ server" after pressing a verdict they meant.
589
+
590
+ So **publish it with a store**, and the page uses it:
591
+
592
+ - Declare `capabilities: {db: {}}`. The page checks for `window.claude.use` and
593
+ writes the pass into the artifact's own `passes` collection instead of
594
+ POSTing. Nothing else about it changes — same marks, same verdicts, same
595
+ refusal to commit over an open comment.
596
+ - **Do not declare `user`.** The pass is not per-viewer private state, a shared
597
+ collection is what it wants, and a declaration nothing uses is a grant asked
598
+ for nothing.
599
+ - **Same file path every time**, so a redeploy reuses the URL. That is what
600
+ answers the clear-down worry: one page per spec, replaced at each render,
601
+ rather than an artifact per phase accumulating in the gallery.
602
+ - **Send a push notification with the URL** when you publish. The reader is by
603
+ definition somewhere else — that is why it was published.
604
+
605
+ ### Taking a stored pass back
606
+
607
+ A pass in the store is claimed the same way in spirit and a different way in
608
+ mechanism, because the engine cannot see it:
609
+
610
+ 1. Read the `passes` collection with the Artifact tool's `read_db`.
611
+ 2. **Apply step 0 unchanged.** One waiting pass, offered by what it says and
612
+ when it was sent; two is a refusal to guess. Nothing here is automatic —
613
+ there is no wait window on this transport, because nothing pushes from the
614
+ store to this conversation.
615
+ 3. Write the pass's `blob` to a scratch file **verbatim** and merge it with
616
+ `spec-env review <spec> --notes <file>`. It rejoins the ordinary path at §2
617
+ step 1, so the verdict is judged and routed exactly as any other.
618
+ 4. **Delete the document** with `write_db` once it is merged. A claim consumes:
619
+ a pass left in the store is claimable twice, which is the one property the
620
+ six-digit code has always had and this transport must not lose.
621
+
622
+ ## 7. Report
623
+
624
+ End with the block defined in `.claude/rules/spec-reports.md`. That file carries
625
+ the shape; this section carries only what is specific here.
626
+
627
+ **Verdicts**
628
+
629
+ - `✅` — the page is rendered, and any review pass handed back was stored and
630
+ acted on.
631
+ - `⚠️` — rendered, with something worth knowing: accepts that lapsed because the
632
+ file changed, comments left unworked because the operator did not say go.
633
+ - `❌` — a render or a fix failed part-way. Quote it.
634
+ - `⏸` — the spec has no worktree. Say that plainly: a spec that has not been
635
+ started has nothing to diff, which is an ordinary state and not an error.
636
+
637
+ **Fields:** `Built` · `Tests` · `Notes` · `Review` · `Follow-ups` · `Next`
638
+
639
+ ## 7a. End in a picker
640
+
641
+ Offer the same four endings `/spec-next` §6a defines — `Reviewed` · `Commit` ·
642
+ `Commit & Continue` · `Discuss` — under the same conditions, including
643
+ `Reviewed` only when a pass is waiting, and the rule that nothing claims a pass
644
+ without a pick. That section owns the wording; do not restate it.
645
+
646
+ **This is where changing your mind is handled, and that is why it belongs here.**
647
+ `/spec-diff` renders the page, so it is the command someone runs when they look
648
+ again — often at a review they have already voted on. A pass they sent is still
649
+ sitting **unclaimed**, so a pick made here **supersedes** it:
650
+
651
+ - act on the pick,
652
+ - drop the waiting pass (`--drop <code>`),
653
+ - and say both happened, naming the verdict that was dropped.
654
+
655
+ **Never carry both.** A stored verdict alongside a fresh one is two standing
656
+ conclusions about one review, and whichever a later run picked up would be a
657
+ coin toss. One ending, most recently chosen — the same rule that makes a verdict
658
+ consumed rather than stored.
659
+
660
+ **Say what was dropped rather than dropping it quietly.** Someone who pressed
661
+ `Commit` on their phone and then picks `Discuss` here has changed their mind on
662
+ purpose; someone who forgot they had voted has not, and only the report tells
663
+ them apart.
664
+
665
+ `Review` carries the files and `+`/`−`, the page's `open:` line, and the
666
+ published URL when there is one. Where the page holds a review pass, it also
667
+ carries the three totals — files accepted, comments open, comments answered.
668
+ `--json` reports those under `notes.totals`; read that, never the diff.
669
+
670
+ `Built` appears only when this run actually changed code — the commented files
671
+ it worked on your go-ahead. A render on its own built nothing, and an empty
672
+ `Built` line claiming otherwise is worse than no field.
673
+
674
+ `Tests` and a commit appear only on a committing verdict (§2a).
675
+ **Say which path made the commit** — the configured skill by name, or by hand —
676
+ in the same row as the sha. A reader cannot tell a `/commit` from a hand-rolled one after the
677
+ fact, so the run that made it is the only place that distinction can be
678
+ recorded.