@skitterbyte/skitterspec 20.0.0 → 22.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.
@@ -208,7 +208,7 @@ is a no-op.
208
208
 
209
209
 
210
210
 
211
- ## 5b. Render the page then offer the review, never write it
211
+ ## 5b. Render the page, arm the gate, then wait for the verdict
212
212
 
213
213
  **Only when the project has per-spec isolation** (`specs/.core/env.config.json`
214
214
  present). Without it there is no worktree to read and this step does not exist —
@@ -224,53 +224,67 @@ skitterspec spec-env review <spec>
224
224
  **This is free.** The engine reads git and splices the patches into a template;
225
225
  the diff never passes through you, so a 266KB patch costs nothing.
226
226
 
227
- **Then offer `/spec-diff`. Do not run it.** The written review is the part that
228
- costs — roughly **700 output tokens**, because writing it means reading the diff
229
- — and that spend is the operator's call, not a default.
230
-
231
- **The offer is the `Review` row of step 6's block** — the counts, the page link
232
- and a question, in one row:
233
-
234
- | **Review** | <N> files, +<a> −<d> · [open the page](<the `open:` URL>) — want a written review before you commit? |
235
-
236
- **It ends in a question, addressed to someone.** It was once a fenced block of
237
- engine output, and it fired on every phase and was never once taken: two quoted
238
- lines under the test counts, addressed to nobody, with the report then closing
239
- on *"commit this first"* — the last instruction the reader got was to move on,
240
- so they did. A row in a labelled table is findable; a question in it is
241
- answerable. Both halves are load-bearing.
242
-
243
- **Never bury it and never split it.** It sits above `Follow-ups` and `Next`, and
244
- the page and the question stay in the same row: two adjacent rows about one page
245
- make the reader resolve a distinction before acting on either. A later edit that
246
- moves it out of the block, or separates the link from the question, undoes this
247
- and should be read as a regression rather than tidying.
227
+ **Then arm the gate**, so the fix now owes a verdict:
248
228
 
229
+ ```
230
+ skitterspec spec-env review arm <spec>
231
+ ```
249
232
 
250
- Relay the **`open:`** line rather than the bare path: a path is not clickable in
251
- any terminal, and a page nobody can open is a page nobody reads.
233
+ **Then wait**, and `/spec-next` §5 owns the sequence follow it there rather
234
+ than reading a second copy here: note the moment, run
235
+ `skitterspec spec-env review wait <spec> --since <that moment>` in the
236
+ background, end your turn, and let `--claim-since` pick the one pass that
237
+ arrived inside the window. **Do not compose a watcher**, and do not give the
238
+ wait a timeout — the reasoning is in `/spec-next` §5 and is not repeated here.
239
+ The banner it describes is what this skill emits in place of a `Review` row,
240
+ and the routing on the verdict is `/spec-diff` §2 and §4, as it is everywhere.
241
+
242
+ **Why this skill arms as well as waits.** Waiting is what any offer does;
243
+ **arming** asserts an obligation that outlives the turn, and belongs only to
244
+ work that is finished. A bug fix is a completed unit — red→green, suite
245
+ passing — so it qualifies, and a wait with nothing owed behind it is a
246
+ suggestion rather than a gate. A mid-run render waits without arming, and
247
+ walking away from that costs nothing.
248
+
249
+ **This is the gap this step existed inside.** It used to render the page, emit a
250
+ row asking *"want a written review before you commit?"*, and finish — with
251
+ nothing watching. A verdict pressed on that page landed in the holding area and
252
+ stayed there, because the run had said the page was **ready** rather than that
253
+ it was **waiting**. Two were pressed on one spec and both were stranded; the
254
+ second existed only because the first appeared to do nothing.
255
+
256
+ **It is user-visible, and that is deliberate.** Once armed, a `git commit` in
257
+ this worktree is refused until a verdict is sent or
258
+ `skitterspec spec-env review skip "<reason>"` records the decision to move on.
259
+ The exit is always one command, and one of them is *"I am moving on"*.
260
+
261
+ Relay the engine's **stack** — the `local:`, `network:` and `remote:` lines, all
262
+ three — rather than the bare `page:` path: a path is not clickable in any
263
+ terminal, and a page nobody can open is a page nobody reads. Relay the `live:`
264
+ line with it where the engine printed one, and nothing where it did not.
265
+ `.claude/rules/spec-reports.md` carries the shape, including why a rigid
266
+ contract took that line.
252
267
 
253
268
  - **Never write the review unasked**, and **never publish**. Publishing leaves
254
269
  something behind that this tooling cannot remove, so it is always an ask. A
255
270
  `file://` link is no use on a phone, and saying so **is** the ask —
256
271
  `/spec-diff` §6 owns how.
257
- **Follow the `reader:` line the engine printed do not sniff for it.** It
258
- answers where the person reading this is sitting, and the offer changes with it:
259
-
260
- - **absent** (`unknown`) the `file://` URL, exactly as always. **Do not warn:**
261
- unknown is the ordinary state of a local machine, and a warning there is an
262
- accusation against a healthy session.
263
- - **`local`** the `file://` URL.
264
- - **`remote`** — the engine has already stood its local server up and put a URL
265
- the reader can open on `open:`. So there is **nothing special to say**: relay
266
- that line like any other. Any `also:` lines under it are the other addresses
267
- this machine has, offered because the best-guess one can be wrong — pass them
268
- on rather than editing them out.
269
-
270
- **Never read an environment variable to decide this** not `SSH_CONNECTION`,
271
- not `CLAUDE_CODE_*`, not a tty check. The engine did it, reports it on that line
272
- and in `--json`, and a second implementation here could not be tested and would
273
- drift.
272
+ **The `reader:` line no longer decides anything here, and that is the point.**
273
+ It is still printed, and it is still the only place that question is answered —
274
+ but the offer does not change with it, because the stack lists every tier
275
+ whatever it says. Three reader states used to mean three different offers, and
276
+ that branching is exactly what produced a `file://` page on a session detected
277
+ `unknown`, a LAN URL for a phone off the network, and an address that changed
278
+ underneath a reader when detection flipped mid-session.
279
+
280
+ So: **relay all three tier lines, every time.** Any `also:` lines sit under
281
+ `network` the other addresses this machine has, offered because the
282
+ best-guess one can be wrong — so pass them on rather than editing them out.
283
+
284
+ **Never read an environment variable to decide anything about the offer** — not
285
+ `SSH_CONNECTION`, not `CLAUDE_CODE_*`, not a tty check. There is nothing left
286
+ here for a detection to decide, and a second implementation of one could not be
287
+ tested and would drift.
274
288
 
275
289
  **Serving is the engine's to do; publishing is never.** A `remote` reader
276
290
  authorises a local server — one process, ended by one flag, leaving nothing
@@ -307,9 +321,15 @@ the shape; this section carries only what is specific here.
307
321
  **Fields:** `Tracker` · `Branch` · `Spec` · `Cause` · `Built` · `Tests` ·
308
322
  `Review` · `Follow-ups` · `Next`
309
323
 
324
+ **`Review` is emitted only where step 5b did not run** — a project with no
325
+ isolation, or a render that failed. Where the run is waiting, the banner carries
326
+ the whole subject and the row is dropped, per `.claude/rules/spec-reports.md`.
327
+
310
328
  `Cause` is the root cause in one clause and `Built` is the fix — the
311
329
  failing→passing test belongs in `Tests`, named, so the evidence is a test name
312
330
  rather than an adjective.
313
331
 
314
- Step 5b's offer is the `Review` row, not a paragraph after the block — the
315
- counts, the link and the question in one row. Nothing follows the block.
332
+ Step 5b ends in the **banner**, not a paragraph and not a row — the run is
333
+ waiting on a verdict, and the banner is the shape that says so. Nothing follows
334
+ it. Where the run is not waiting it asks nothing at all: a `Review` row carries
335
+ the counts and the link, and no question (*asking implies waiting*).
@@ -1,6 +1,6 @@
1
1
  ---
2
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.
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, a bare verdict word, or the pasted JSON.
4
4
  ---
5
5
 
6
6
  # /spec-diff — see the phase before you commit it
@@ -41,13 +41,19 @@ sole candidate or prints the candidates.
41
41
  The page has marks on it — `✓ accept` per file, notes against a line or a whole
42
42
  file, answers to the checks a written review asked — and it
43
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**:
44
+ A review comes back to you one of three ways, and **all three are ordinary**:
45
45
 
46
46
  - **A six-digit code** — `418207`, on its own. A *served* page hands its pass
47
47
  straight to the engine, which holds it until someone reads the code out. This
48
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.
49
+ - **A verdict word** — `commit`, `commit-continue`, `continue`, `changes` or
50
+ `discuss`. A `file://` page cannot send anything, so where the reader marked
51
+ nothing it copies a command carrying the conclusion on its own. Send it with
52
+ `skitterspec spec-env review <spec> --verdict <word>`; it joins the same merge
53
+ a claimed pass does, so everything below is unchanged.
54
+ - **A pasted JSON blob** — the same `file://` page, once the reader has marked
55
+ something up: accepts and notes do not fit on a command line, so the whole
56
+ pass travels. Not legacy: it is the whole story for a local reader.
51
57
 
52
58
  Either way, **this is not a request to render anything**: it is a review coming
53
59
  back, and these steps replace §3–§5 below.
@@ -151,6 +157,10 @@ back, and these steps replace §3–§5 below.
151
157
  absent verdict has always meant "report it and wait" — which is why that is
152
158
  what it still means.
153
159
 
160
+ **A pass may carry an `action:` instead**, and then none of the above
161
+ applies: it concluded nothing. Go to **§2b**. The engine refuses a pass
162
+ carrying both, so there is never a choice to make between them.
163
+
154
164
  3. **Say what you read, then ask what's up.** Report the accepted count, then
155
165
  each open comment as `file:line — note`, then the files you would touch.
156
166
  **Wait — unless the verdict already said otherwise.** Pasting on its own is
@@ -335,6 +345,69 @@ it — and the paste was the one place it broke. The *work* either authorises is
335
345
  ordinary phase-sized cost, and step 3 is where the operator decides whether to
336
346
  spend it.
337
347
 
348
+ ## 2b. An action changes something, then hands the page back
349
+
350
+ Only on a pass carrying an **action**. Three of them, and
351
+ **all three end the same way**:
352
+ do the thing, re-render (§4), and wait again (§4b) — because the reader
353
+ still has a decision in front of them and has not made it.
354
+
355
+ | Action | Do | Then |
356
+ |--------|----|------|
357
+ | `live-on` | commit first (below), then `skitterspec spec-env live take <spec>` | re-render `--branch`, wait again |
358
+ | `allow-network` | `skitterspec spec-env review allow network` | re-render, wait again |
359
+ | `allow-remote` | `skitterspec spec-env review allow remote --set on` | re-render, wait again |
360
+
361
+ **THERE IS NO `live-off`, and its absence is deliberate.** Putting *this* change
362
+ live is about the diff on screen; handing the whole instance back to `main` is a
363
+ workspace decision with nothing to do with this review. The `live:` line names
364
+ `/spec-live main` for it — a command the operator types, which is also why no
365
+ skill runs it.
366
+
367
+ **AN ACTION IS NOT A VERDICT, and the gate is untouched by all three.** A phase
368
+ that ended armed the gate, and it is discharged by a committing verdict or a
369
+ recorded skip and by nothing else — so after any of these the phase still owes
370
+ an answer, and that is exactly why the run waits again rather than finishing.
371
+ The engine enforces this rather than trusting it: `ACTIONS` is disjoint from
372
+ `VERDICTS` and `COMMITTING`, so nothing that routes on a verdict can see one.
373
+
374
+ **`live-on` commits first, and the commit is a precondition rather than an answer.**
375
+ `live take` refuses a dirty worktree, and even without that guard
376
+ uncommitted work stays behind in the worktree — so what went live would be the
377
+ *previous* commit while the page claimed to be showing this one. Hand off to
378
+ `review.commitWith` exactly as §2a does (do not restate its rules here), then
379
+ take the instance. The reader has not approved anything by pressing it.
380
+
381
+ **It re-renders `--branch` afterwards**, because the working view is now empty:
382
+ the commit just happened, and the branch range is what still answers "what am I
383
+ looking at". §4's clean-tree fallback reaches the same place on its own, so
384
+ passing `--branch` is belt and braces rather than a separate rule.
385
+
386
+ **A mid-phase page does not get the commit.** Where the render took
387
+ `--buttons midrun`, the work is half a phase — committing it to look at it
388
+ running splits one phase across two commits and leaves a mess nobody asked for.
389
+ Say the phase needs to land first, re-render, and wait.
390
+
391
+ **Relay every refusal, and work around none of them.** `live take` refuses a
392
+ workbench another spec holds, a hotfix, a stateful spec, a branch touching
393
+ migrations, and a tree with no dev server listening. Each refusal names its own
394
+ way out. **Never park another spec's live session** to make room — that is
395
+ someone else's work, and freeing it is their decision. Re-render and wait, so
396
+ the reader can choose something else.
397
+
398
+ **`allow` writes a committed file, and that is worth a sentence.** It edits
399
+ `specs/.core/env.config.json` in the **primary checkout**, so it changes
400
+ behaviour for everyone who pulls and leaves that tree dirty — unlike the live
401
+ actions, which move a branch and write a gitignored receipt. The engine prints
402
+ the absolute path and whose tree it dirtied; relay that rather than letting a
403
+ shared setting change land silently. And it is **enable-only**: there is no
404
+ action that turns a tier off, because turning `network` off from a page reached
405
+ over the network kills the page doing the turning.
406
+
407
+ **`allow remote` permits publishing. It does not publish.** Publishing stays an
408
+ explicit ask in every case (§6), because the page it leaves behind is one this
409
+ tooling cannot remove.
410
+
338
411
  ## 3. Gate it on nothing
339
412
 
340
413
  **This skill has no preconditions and must never grow one.** Not tests passing,
@@ -367,15 +440,17 @@ skitterspec spec-env review <spec> --branch # everything since the base bran
367
440
  skitterspec spec-env review serve # every spec, on localhost
368
441
  ```
369
442
 
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.
443
+ **There is no switch left to handle.** The engine serves and prints a **stack** —
444
+ one line per tier, `local`, `network`, `remote`, each either a URL or the one
445
+ command that turns it on. All of it is free and none of it publishes anything.
446
+ You are not choosing between them; you are relaying every line it printed.
447
+ `.claude/rules/spec-reports.md` carries the shape and why.
375
448
 
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.
449
+ The operator who does not want a LAN listener sets `review.allowNetwork: false`
450
+ in `env.config.json` `network` then reads `off` with the command that turns it
451
+ back on, and `local` is the loopback page. Turning serving off entirely is
452
+ `review.serve: "never"`, and then `local` is the `file://` page, which says on
453
+ its own line that it cannot send a verdict.
379
454
 
380
455
  `serve` renders **per request**, so nothing it shows can be stale, and it lists
381
456
  every spec with a worktree rather than one. `--host 0.0.0.0` binds beyond
@@ -401,23 +476,56 @@ Add `--json` to get the file list, totals and the page path back as data. The
401
476
  page is written to `.spec-env/reviews/<spec>.html`, which is gitignored — it
402
477
  leaves no trace in the branch under review.
403
478
 
404
- **On `--page-only`, stop here** and report the path.
479
+ **Declare the button set when the work is unfinished.** A page rendered part-way
480
+ through a run takes `--buttons midrun`, and offers `Continue` — *I have read it,
481
+ carry on* — in place of `Commit` and `Commit & Continue`:
482
+
483
+ ```
484
+ skitterspec spec-env review <spec> --buttons midrun
485
+ ```
486
+
487
+ **It is a statement about the work, never a reading of the gate.** Ask whether
488
+ the thing you just rendered is *finished*: a phase that ended, a bug fix that is
489
+ green, a spec about to land — committing set, which is the default, so pass
490
+ nothing. Half a phase, a hand edit, a colleague's branch mid-flight, anything
491
+ the operator asked to look at while it is still moving — `midrun`.
405
492
 
406
- ## 4a. Read the `reader:` line never sniff for it yourself
493
+ **Do not derive it from whether the gate is armed.** That is tidier and wrong: a
494
+ project running `review.required: false` never arms at all, so every one of its
495
+ pages would lose the committing buttons and its reader could never commit from
496
+ the page. The caller knows what it rendered; the gate only knows whether the
497
+ project opted into gating.
407
498
 
408
- `spec-env review` reports where it believes the reader is, and
409
- **that is the only place this question is answered.** Three states:
499
+ `Continue` is not the removed `none` verdict. `none` recorded itself and did
500
+ nothing; `Continue` **resumes the run**, so it still names an action. What it
501
+ cannot do is commit, or clear a gate a finished phase armed — that takes a
502
+ committing verdict or a recorded skip.
410
503
 
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 |
504
+ **On `--page-only`, stop here** and report the path.
416
505
 
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
506
+ ## 4a. Relay the stack every tier, never one you picked
507
+
508
+ `spec-env review` prints one line per tier and `--json` carries the same thing
509
+ as `tiers`. **Relay all of them, in that order**, whatever the `reader:` line
510
+ says:
511
+
512
+ | Tier | Carries |
513
+ |------|---------|
514
+ | `local` | the loopback URL — or the `file://` page when nothing served, which says it cannot send a verdict |
515
+ | `network` | the LAN URL and any `also:` alternates under it — or `off` with the command that turns it on |
516
+ | `remote` | the published URL and *a verdict here needs `/spec-reviewed`* — or `off` with the command that turns it on |
517
+
518
+ **The `reader:` line decides nothing here any more.** It is still printed and
519
+ still the only place that question is answered, but the offer no longer branches
520
+ on it — because branching on it is what produced a `file://` page for a session
521
+ detected `unknown`, a LAN URL for a phone off the network, and an address that
522
+ changed underneath a reader mid-session. A tier that is off keeps its line, so a
523
+ reader who has left the house can see the surface exists and ask for it.
524
+
525
+ **Never read an environment variable to decide anything about the offer.** Not
526
+ `SSH_CONNECTION`, not `CLAUDE_CODE_*`, not a tty check — there is nothing left
527
+ for a detection to decide, and a second implementation of one could not be
528
+ tested and would drift. The ranking and the traps
421
529
  (`CLAUDE_CODE_ENTRYPOINT` describes the *process*, not the reader; stdin is never
422
530
  a tty under Claude Code) live in `review.js` beside the code, which is where they
423
531
  belong.
@@ -436,10 +544,11 @@ answer left.
436
544
 
437
545
  `review.reader` in `env.config.json` (`local` · `remote` · `detect`) lets the
438
546
  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.
547
+ sniffing. What the engine may *do* is settled by the tier settings instead —
548
+ `review.serve` (`always` · `never`), `review.allowNetwork` and
549
+ `review.allowRemote` — not by the reader.
441
550
 
442
- ## 4b. Wait for the verdict, where the harness can watch a file
551
+ ## 4b. Wait for the verdict because asking for one means waiting for it
443
552
 
444
553
  A served page can hand its pass back the moment it is pressed. Without a wait,
445
554
  that pass lands in the holding area and stops — nothing happens until someone
@@ -448,16 +557,45 @@ acts joined only by the operator remembering.
448
557
  **This is the step that joins them**, and it is this skill's, so `/spec-next` can point here rather than
449
558
  keeping a second copy.
450
559
 
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.
560
+ **The rule is `.claude/rules/spec-reports.md`'s: asking implies waiting.** If
561
+ this render asks the reader for a verdict, it waits for one. If it is not going
562
+ to wait, it does not ask the report names the page and its size and stops
563
+ there. There is no third option, and in particular there is no *ask now, notice
564
+ later*: that is the shape that stranded two passes on one spec, where the run
565
+ said the page was **ready** rather than that it was **waiting**.
566
+
567
+ **So the question is not "can I watch a file".** It is "am I asking?" — and the
568
+ transport only decides what carries the answer back:
569
+
570
+ - **A served page** posts to the local store; the engine's own wait returns when
571
+ it lands, and the steps below run it.
572
+ - **A `file://` page** has nothing to post to, so the reader pastes the pass and
573
+ their next message carries it. Say you are holding and **end the turn** — that
574
+ is the same wait, carried by the conversation. It is not a lesser one.
575
+ - **A published page** writes to the artifact's own store, which nothing reaches
576
+ from here. That is the one case where the honest sentence is *press a verdict,
577
+ then type `/spec-reviewed`* — see the published-page paragraph below.
578
+
579
+ **Do not ask, and therefore do not wait, on a render nobody is waiting behind** —
580
+ a bare `--page-only`, a page produced alongside other work, a pass that has
581
+ already arrived. Those get the `Review` row, no question, and nothing is owed.
455
582
 
456
583
  1. **Note the moment**, as an ISO timestamp, before you start. That instant is
457
584
  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.
585
+ 2. **Run the engine's wait in the background, and end the turn:**
586
+
587
+ ```
588
+ skitterspec spec-env review wait <spec> --since <timestamp>
589
+ ```
590
+
591
+ **Never a watcher of your own, and never a timeout.** It lasts as long as the
592
+ session, because a reader who walks away from a diff is the normal case — and
593
+ a loop composed here would be proven by nothing, which is how one written as
594
+ `[ "$x" \> "$y" ]` spun for five minutes in zsh and looked exactly like
595
+ patience. `/spec-next` §5 carries the full account.
596
+
597
+ Not a poll and not a held-open turn: the operator has their terminal back,
598
+ and the session costs nothing while they read.
461
599
  3. **On waking, let the engine choose:**
462
600
 
463
601
  ```
@@ -492,10 +630,16 @@ Where the page is published, say plainly that `/spec-reviewed` is what picks it
492
630
  up.
493
631
 
494
632
  **`/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
633
+ that arrived when nobody was waiting, for the two-passes case, and for every
634
+ published page. It is user-only, and that is still the
497
635
  enforcement that makes a named claim a person's decision.
498
636
 
637
+ **A harness with no file-watch is no longer on that list**, and that is the
638
+ change. It used to be — the row and `/spec-reviewed` were "the whole story",
639
+ which read as permission to ask without waiting. Now the turn ending is the
640
+ wait, and every harness can end a turn; `/spec-reviewed` remains available
641
+ there, as it is everywhere, for a pass nobody was holding for.
642
+
499
643
  ## 5. Offer the written review — say what it costs first
500
644
 
501
645
  The page is free. The **written review is not**, and it costs in two separate
@@ -662,8 +806,8 @@ consumed rather than stored.
662
806
  purpose; someone who forgot they had voted has not, and only the report tells
663
807
  them apart.
664
808
 
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
809
+ `Review` carries the files and `+`/`−`, and the tier stack — labelled, run
810
+ together with `·` because a row is one cell. Where the page holds a review pass, it also
667
811
  carries the three totals — files accepted, comments open, comments answered.
668
812
  `--json` reports those under `notes.totals`; read that, never the diff.
669
813
 
@@ -230,7 +230,7 @@ is a no-op.
230
230
 
231
231
 
232
232
 
233
- ## 6b. Render the page then offer the review, never write it
233
+ ## 6b. Render the page, arm the gate, then wait for the verdict
234
234
 
235
235
  **Only when the project has per-spec isolation** (`specs/.core/env.config.json`
236
236
  present). Without it there is no worktree to read and this step does not exist —
@@ -246,53 +246,61 @@ skitterspec spec-env review <spec>
246
246
  **This is free.** The engine reads git and splices the patches into a template;
247
247
  the diff never passes through you, so a 266KB patch costs nothing.
248
248
 
249
- **Then offer `/spec-diff`. Do not run it.** The written review is the part that
250
- costs — roughly **700 output tokens**, because writing it means reading the diff
251
- — and that spend is the operator's call, not a default.
252
-
253
- **The offer is the `Review` row of step 6's block** — the counts, the page link
254
- and a question, in one row:
255
-
256
- | **Review** | <N> files, +<a> −<d> · [open the page](<the `open:` URL>) — want a written review before you commit? |
257
-
258
- **It ends in a question, addressed to someone.** It was once a fenced block of
259
- engine output, and it fired on every phase and was never once taken: two quoted
260
- lines under the test counts, addressed to nobody, with the report then closing
261
- on *"commit this first"* — the last instruction the reader got was to move on,
262
- so they did. A row in a labelled table is findable; a question in it is
263
- answerable. Both halves are load-bearing.
264
-
265
- **Never bury it and never split it.** It sits above `Follow-ups` and `Next`, and
266
- the page and the question stay in the same row: two adjacent rows about one page
267
- make the reader resolve a distinction before acting on either. A later edit that
268
- moves it out of the block, or separates the link from the question, undoes this
269
- and should be read as a regression rather than tidying.
249
+ **Then arm the gate**, so the fix now owes a verdict:
270
250
 
251
+ ```
252
+ skitterspec spec-env review arm <spec>
253
+ ```
271
254
 
272
- Relay the **`open:`** line rather than the bare path: a path is not clickable in
273
- any terminal, and a page nobody can open is a page nobody reads.
255
+ **Then wait**, and `/spec-next` §5 owns the sequence follow it there rather
256
+ than reading a second copy here: note the moment you start waiting, watch the
257
+ pending store, end your turn, and let `--claim-since` pick the one pass that
258
+ arrived inside the window. The banner it describes is what this skill emits in
259
+ place of a `Review` row, and the routing on the verdict is `/spec-diff` §2 and
260
+ §4, as it is everywhere.
261
+
262
+ **Why this skill arms as well as waits.** Waiting is what any offer does;
263
+ **arming** asserts an obligation that outlives the turn, and belongs only to
264
+ work that is finished. A hotfix is a completed unit — red→green against a
265
+ released tag — so it qualifies, and a wait with nothing owed behind it is a
266
+ suggestion rather than a gate.
267
+
268
+ **A hotfix is the case where reading it matters most.** This change is about to
269
+ be tagged and shipped to production from a release line, not merged into a
270
+ branch someone else will read first. The verdict is the only review it gets.
271
+
272
+ **It is user-visible, and that is deliberate.** Once armed, a `git commit` in
273
+ this worktree is refused until a verdict is sent or
274
+ `skitterspec spec-env review skip "<reason>"` records the decision to move on.
275
+ The exit is always one command, and one of them is *"I am moving on"*.
276
+
277
+ Relay the engine's **stack** — the `local:`, `network:` and `remote:` lines, all
278
+ three — rather than the bare `page:` path: a path is not clickable in any
279
+ terminal, and a page nobody can open is a page nobody reads. Relay the `live:`
280
+ line with it where the engine printed one, and nothing where it did not.
281
+ `.claude/rules/spec-reports.md` carries the shape, including why a rigid
282
+ contract took that line.
274
283
 
275
284
  - **Never write the review unasked**, and **never publish**. Publishing leaves
276
285
  something behind that this tooling cannot remove, so it is always an ask. A
277
286
  `file://` link is no use on a phone, and saying so **is** the ask —
278
287
  `/spec-diff` §6 owns how.
279
- **Follow the `reader:` line the engine printed do not sniff for it.** It
280
- answers where the person reading this is sitting, and the offer changes with it:
281
-
282
- - **absent** (`unknown`) the `file://` URL, exactly as always. **Do not warn:**
283
- unknown is the ordinary state of a local machine, and a warning there is an
284
- accusation against a healthy session.
285
- - **`local`** the `file://` URL.
286
- - **`remote`** — the engine has already stood its local server up and put a URL
287
- the reader can open on `open:`. So there is **nothing special to say**: relay
288
- that line like any other. Any `also:` lines under it are the other addresses
289
- this machine has, offered because the best-guess one can be wrong — pass them
290
- on rather than editing them out.
291
-
292
- **Never read an environment variable to decide this** not `SSH_CONNECTION`,
293
- not `CLAUDE_CODE_*`, not a tty check. The engine did it, reports it on that line
294
- and in `--json`, and a second implementation here could not be tested and would
295
- drift.
288
+ **The `reader:` line no longer decides anything here, and that is the point.**
289
+ It is still printed, and it is still the only place that question is answered —
290
+ but the offer does not change with it, because the stack lists every tier
291
+ whatever it says. Three reader states used to mean three different offers, and
292
+ that branching is exactly what produced a `file://` page on a session detected
293
+ `unknown`, a LAN URL for a phone off the network, and an address that changed
294
+ underneath a reader when detection flipped mid-session.
295
+
296
+ So: **relay all three tier lines, every time.** Any `also:` lines sit under
297
+ `network` the other addresses this machine has, offered because the
298
+ best-guess one can be wrong — so pass them on rather than editing them out.
299
+
300
+ **Never read an environment variable to decide anything about the offer** — not
301
+ `SSH_CONNECTION`, not `CLAUDE_CODE_*`, not a tty check. There is nothing left
302
+ here for a detection to decide, and a second implementation of one could not be
303
+ tested and would drift.
296
304
 
297
305
  **Serving is the engine's to do; publishing is never.** A `remote` reader
298
306
  authorises a local server — one process, ended by one flag, leaving nothing
@@ -335,10 +343,16 @@ the shape; this section carries only what is specific here.
335
343
  **Fields:** `Tracker` · `Branch` · `Spec` · `Cause` · `Built` · `Tests` ·
336
344
  `Review` · `Follow-ups` · `Next`
337
345
 
346
+ **`Review` is emitted only where step 6b did not run** — a project with no
347
+ isolation, or a render that failed. Where the run is waiting, the banner carries
348
+ the whole subject and the row is dropped, per `.claude/rules/spec-reports.md`.
349
+
338
350
  **The base tag goes in the verdict clause** — `✅ /spec-hotfix · hotfix-foo ·
339
351
  green on v2.3.1`. Which released version this was fixed against is the first
340
352
  thing anyone needs, and it is not a field: the clause is where the run says
341
353
  where it got to.
342
354
 
343
- Step 6b's offer is the `Review` row, not a paragraph after the block — the
344
- counts, the link and the question in one row. Nothing follows the block.
355
+ Step 6b ends in the **banner**, not a paragraph and not a row — the run is
356
+ waiting on a verdict, and the banner is the shape that says so. Nothing follows
357
+ it. Where the run is not waiting it asks nothing at all: a `Review` row carries
358
+ the counts and the link, and no question (*asking implies waiting*).
@@ -105,12 +105,24 @@ Ensure it exists. If missing, create it documenting:
105
105
  Read a sibling spec skill (e.g. `spec`, `spec-next`) for the canonical shapes
106
106
  rather than inventing them. If the rule already exists, leave it unless stale.
107
107
 
108
- ## 4a. The review-gate hook (`.claude/hooks/review-gate.js`)
109
-
110
- `skitterspec init` installs the hook script and registers it in the project's
111
- **committed** `.claude/settings.json` as a `PreToolUse` hook on `Bash`. It runs
112
- one engine call per Bash tool call and refuses a `git commit` in a worktree
113
- whose phase is still awaiting a verdict.
108
+ ## 4a. The review-gate hook (`.claude/hooks/review-gate.cjs`)
109
+
110
+ `skitterspec init` **and `skitterspec update`** install the hook script and
111
+ register it in the project's **committed** `.claude/settings.json` as a
112
+ `PreToolUse` hook on `Bash`. It runs one engine call per Bash tool call and
113
+ refuses a `git commit` in a worktree whose phase is still awaiting a verdict.
114
+
115
+ **Both commands, and that is not a detail.** Copying the script and registering
116
+ it are one operation, and they were once split across two code paths — so
117
+ `update` landed the file, reported `created:`, and wired nothing, on every
118
+ project that upgraded. Landing the script is not installing the hook.
119
+
120
+ **`.cjs`, and the extension is load-bearing.** The script lands inside the
121
+ target project, where *that* project's `package.json` decides how node parses a
122
+ `.js` — so CommonJS shipped as `.js` crashes in any `"type": "module"` project,
123
+ on every Bash tool call. `.cjs` settles it at the file. A registration left over
124
+ from the release that named `.js` is rewritten in place, keeping any wrapping
125
+ the operator added, rather than gaining a second entry beside it.
114
126
 
115
127
  **Committed, not machine-local**, and the difference is the point: the trusted
116
128
  worktree root is one machine's absolute path, while *a phase that ended owes an