@skitterbyte/skitterspec 21.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.
@@ -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
@@ -428,21 +503,29 @@ committing verdict or a recorded skip.
428
503
 
429
504
  **On `--page-only`, stop here** and report the path.
430
505
 
431
- ## 4a. Read the `reader:` line — never sniff for it yourself
432
-
433
- `spec-env review` reports where it believes the reader is, and
434
- **that is the only place this question is answered.** Three states:
435
-
436
- | `reader:` | What to offer |
437
- |-----------|---------------|
438
- | absent (`unknown`) | the `file://` URL, exactly as always. **Do not warn** — unknown is the ordinary state of a local machine |
439
- | `local` | the `file://` URL |
440
- | `remote` | the `open:` line as printed — the engine already served it. Pass on any `also:` lines too |
441
-
442
- **Never read an environment variable to decide this.** Not `SSH_CONNECTION`, not
443
- `CLAUDE_CODE_*`, not a tty check the engine already did it, reports the answer
444
- on that line and in `--json`, and a second implementation here could not be
445
- tested and would drift from the first. The ranking and the traps
506
+ ## 4a. Relay the stackevery 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 itor `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
446
529
  (`CLAUDE_CODE_ENTRYPOINT` describes the *process*, not the reader; stdin is never
447
530
  a tty under Claude Code) live in `review.js` beside the code, which is where they
448
531
  belong.
@@ -461,8 +544,9 @@ answer left.
461
544
 
462
545
  `review.reader` in `env.config.json` (`local` · `remote` · `detect`) lets the
463
546
  operator settle where they are reading, and an explicit value is believed without
464
- sniffing. `review.serveOnRemote` (default `true`) settles whether the engine may
465
- 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.
466
550
 
467
551
  ## 4b. Wait for the verdict — because asking for one means waiting for it
468
552
 
@@ -483,8 +567,8 @@ said the page was **ready** rather than that it was **waiting**.
483
567
  **So the question is not "can I watch a file".** It is "am I asking?" — and the
484
568
  transport only decides what carries the answer back:
485
569
 
486
- - **A served page** posts to the local store; a file-watch sees it, and the
487
- steps below are that watch.
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.
488
572
  - **A `file://` page** has nothing to post to, so the reader pastes the pass and
489
573
  their next message carries it. Say you are holding and **end the turn** — that
490
574
  is the same wait, carried by the conversation. It is not a lesser one.
@@ -498,9 +582,20 @@ already arrived. Those get the `Review` row, no question, and nothing is owed.
498
582
 
499
583
  1. **Note the moment**, as an ISO timestamp, before you start. That instant is
500
584
  the entire scope of what may be claimed without a person naming it.
501
- 2. **Watch** `.spec-env/reviews/<spec>.pending.json` in the primary checkout,
502
- and **end the turn**. Not a poll and not a held-open turn: the operator has
503
- 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.
504
599
  3. **On waking, let the engine choose:**
505
600
 
506
601
  ```
@@ -711,8 +806,8 @@ consumed rather than stored.
711
806
  purpose; someone who forgot they had voted has not, and only the report tells
712
807
  them apart.
713
808
 
714
- `Review` carries the files and `+`/`−`, the page's `open:` line, and the
715
- 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
716
811
  carries the three totals — files accepted, comments open, comments answered.
717
812
  `--json` reports those under `notes.totals`; read that, never the diff.
718
813
 
@@ -274,30 +274,33 @@ this worktree is refused until a verdict is sent or
274
274
  `skitterspec spec-env review skip "<reason>"` records the decision to move on.
275
275
  The exit is always one command, and one of them is *"I am moving on"*.
276
276
 
277
- Relay the **`open:`** line rather than the bare path: a path is not clickable in
278
- any terminal, and a page nobody can open is a page nobody reads.
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.
279
283
 
280
284
  - **Never write the review unasked**, and **never publish**. Publishing leaves
281
285
  something behind that this tooling cannot remove, so it is always an ask. A
282
286
  `file://` link is no use on a phone, and saying so **is** the ask —
283
287
  `/spec-diff` §6 owns how.
284
- **Follow the `reader:` line the engine printed do not sniff for it.** It
285
- answers where the person reading this is sitting, and the offer changes with it:
286
-
287
- - **absent** (`unknown`) the `file://` URL, exactly as always. **Do not warn:**
288
- unknown is the ordinary state of a local machine, and a warning there is an
289
- accusation against a healthy session.
290
- - **`local`** the `file://` URL.
291
- - **`remote`** — the engine has already stood its local server up and put a URL
292
- the reader can open on `open:`. So there is **nothing special to say**: relay
293
- that line like any other. Any `also:` lines under it are the other addresses
294
- this machine has, offered because the best-guess one can be wrong — pass them
295
- on rather than editing them out.
296
-
297
- **Never read an environment variable to decide this** not `SSH_CONNECTION`,
298
- not `CLAUDE_CODE_*`, not a tty check. The engine did it, reports it on that line
299
- and in `--json`, and a second implementation here could not be tested and would
300
- 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.
301
304
 
302
305
  **Serving is the engine's to do; publishing is never.** A `remote` reader
303
306
  authorises a local server — one process, ended by one flag, leaving nothing
@@ -154,6 +154,24 @@ Before writing any code for this phase, get the workspace clean:
154
154
  being *"I am moving on"*, which is what keeps this a push rather than a wall —
155
155
  a gate with no exit gets switched off wholesale instead of answered.
156
156
 
157
+ - **Report anything left waiting, and claim none of it.** Ask the engine once:
158
+
159
+ ```
160
+ skitterspec spec-env review waiting
161
+ ```
162
+
163
+ A pass listed here arrived when nothing was watching — a wait that never ran,
164
+ a session cleared, a terminal closed overnight — and no watcher can recover
165
+ those, however good. Relay what it prints: the spec, the code, the verdict and
166
+ the age, with `/spec-reviewed <code>` to pick one up and `--drop <code>` to
167
+ disown it.
168
+
169
+ **It is information, not a gate.** It never refuses, nothing here is
170
+ conditional on it, and **you never claim one** — `/spec-diff` §0 stands
171
+ unchanged, and a pass sitting there when you arrived was not sent to you.
172
+ Silent when nothing is waiting, which is the usual case: say nothing rather
173
+ than reporting that there was nothing to report.
174
+
157
175
  ## 3. Implement the phase
158
176
 
159
177
  Identify the **first unfinished phase** from the `00-overview.md` phase index,
@@ -299,30 +317,51 @@ so one subject lives in one place:
299
317
 
300
318
  ## ⏸ Review ready — <N> files, +<a> −<d>
301
319
 
302
- **[Open the page](<the `open:` URL>)** · I'm holding here until you send a verdict.
320
+ - **local** <the `local:` URL>
321
+ - **network** — <the `network:` URL, or off with the command that turns it on>
322
+ - **remote** — <the `remote:` URL, or off with the command that turns it on>
303
323
 
304
- `/spec-reviewed` picks it up · `spec-env review skip "<reason>"` moves on
324
+ <the `live:` line, exactly as the engine printed it omitted when it printed none>
305
325
 
306
- **One link, and the engine has already chosen it.** Where it served, the
307
- banner carries the served URL and the wait is real. Where it could not serve —
308
- a busy port, a machine with no network address that is the case publishing
309
- exists for, and then the banner carries the published URL with what is true of
310
- it: *press a verdict, then type `/spec-reviewed`*, because nothing pushes from
311
- the artifact store into this conversation.
326
+ I'm holding here until you send a verdict the wait covers local and network.
327
+
328
+ `/spec-reviewed` picks it up · `spec-env review skip "<reason>"` moves on
312
329
 
313
- **Never offer both.** Publishing while the server is reachable adds a second
314
- door the reader cannot tell apart from the first, and the wait only stands
315
- behind one of them that was done, and three verdicts were pressed on the
316
- published page while each sat unread under a line saying I was holding. The
317
- published page is for the reader the server cannot reach, and for nobody
318
- else.
330
+ **The stack is the engine's copy it, do not compose it.** `spec-env review`
331
+ printed one line per tier, in that order, and `--json` carries the same thing as
332
+ `tiers`. Relay all three whatever they say, including the ones that are off with
333
+ the command that turns them on: a reader who has left the house cannot ask for a
334
+ remote page they were never shown.
335
+
336
+ **Do not pick one for them.** The engine used to, and the guess failed three
337
+ separate ways in one day. `.claude/rules/spec-reports.md` carries the shape and
338
+ why the *one link, never two* rule now reads
339
+ **one link per reachable store, each labelled** —
340
+ local and network are two doors into one room, so the wait
341
+ covers both. `remote` is a second store, so its own line carries its own caveat:
342
+ *a verdict here needs `/spec-reviewed`*, because nothing pushes from
343
+ the artifact store into this conversation. Never let the holding line promise a
344
+ wait over that tier.
345
+
346
+ **Publishing is still never yours to do.** `remote` reading `off` is the normal
347
+ state, and the fix for it is the command on that line — typed by the operator,
348
+ or pressed on the page, because a published page is one this tooling cannot
349
+ remove and permitting it is not publishing it.
350
+
351
+ **The `live:` line goes with the stack**, and it is the engine's line too —
352
+ copied as printed, and **absent when the engine printed none**. The page can act
353
+ on it: a press puts the spec live or takes it down and hands the reader back the
354
+ same page, which is `/spec-diff` §2b's routing and not this skill's. What
355
+ matters here is that an action **never clears the gate this phase armed** — the
356
+ reader has looked at it running and concluded nothing, so the phase still owes a
357
+ verdict.
319
358
 
320
359
  ---
321
360
 
322
361
  **Where you are not waiting, it stays the `Review` row** — the counts and the
323
362
  page link, and **no question**:
324
363
 
325
- | **Review** | <N> files, +<a> −<d> · [open the page](<the `open:` URL>) |
364
+ | **Review** | <N> files, +<a> −<d> · **local** <URL> · **network** <URL> · **remote** off |
326
365
 
327
366
  At the end of a phase you are always waiting, so this shape belongs to the
328
367
  renders that are not this step: a mid-phase `/spec-diff`, a page produced
@@ -352,16 +391,25 @@ whether the two places are adjacent rows or a row and a banner.
352
391
 
353
392
  The row is findable, but a row cannot make the continuation follow from the
354
393
  reading — and that is the gap the whole gate exists to close.
355
- **Where this harness can watch a file and wake the session on a change, use it.** The pass
356
- arrives at the engine's holding area, the watch fires, and the verdict the
357
- reader pressed is what carries the work on.
394
+
395
+ **The wait is a command. Do not write one.**
358
396
 
359
397
  1. **Note the moment you start waiting**, as an ISO timestamp. That instant is
360
398
  the whole scope of what you may claim.
361
- 2. **Watch the pending store** for the spec
362
- `.spec-env/reviews/<spec>.pending.json` in the primary checkout — and
363
- **end your turn**. Do not poll, and do not hold the turn open: the point is that
364
- the reader has the terminal back while they read.
399
+ 2. **Run the engine's wait in the background, and end your turn:**
400
+
401
+ ```
402
+ skitterspec spec-env review wait <spec> --since <the timestamp>
403
+ ```
404
+
405
+ It returns when a pass arrives inside that window, and
406
+ **it takes no timeout unless you pass one** —
407
+ the wait lasts as long as your session, because a reader who walks away from
408
+ a diff is the normal case rather than the edge one. Do not give it a duration
409
+ of your own.
410
+
411
+ Then **end your turn**. Do not poll it and do not hold the turn open: the
412
+ point is that the reader has the terminal back while they read.
365
413
  3. **On waking, let the engine pick**:
366
414
 
367
415
  ```
@@ -369,11 +417,26 @@ reader pressed is what carries the work on.
369
417
  ```
370
418
 
371
419
  It claims the one pass that arrived inside the window, and acts on nothing
372
- at all when none did (the watch can fire on a write that was not a pass) or
373
- when two did (two sittings, or two people — the operator has the codes).
420
+ at all when none did (the wait can be woken by something that was not a
421
+ pass) or when two did (two sittings, or two people — the operator has the
422
+ codes).
374
423
  4. **Route on the verdict** exactly as `/spec-diff` §2 and §4 describe. Do not
375
424
  restate that routing here.
376
425
 
426
+ **WHY A COMMAND RATHER THAN A LOOP YOU COMPOSE.** This step used to say "watch
427
+ the pending store" and stop, so every run invented its own watcher in shell —
428
+ and three failed in two days, each reaching the operator as *"I pressed the
429
+ button and nothing happened"*. The worst wrote
430
+ `until [ -f "$P" ] && [ "$x" \> "$y" ]`: valid bash, a syntax error in zsh, a
431
+ predicate that could never be true. It spun for five minutes writing to a
432
+ stderr nobody reads.
433
+
434
+ **The bug is not the lesson — the silence is.** A watcher that can never fire
435
+ and one patiently working are indistinguishable from outside, so nothing about
436
+ that run looked wrong until the operator asked. `review wait` says it has
437
+ started, is written once, and is tested against a store that gains a pass
438
+ mid-flight. A predicate composed fresh each time is proven by nothing.
439
+
377
440
  **WHY THIS IS SAFE, AND WHAT IT COSTS.** It was once true that a device
378
441
  reaching your page could not reach your conversation, and that fact was the
379
442
  whole guard: a pass sat in the holding area until a person typed
@@ -387,11 +450,11 @@ is that the page can now act, so the token has become a credential rather than
387
450
  a convenience — and `--claim-since` refusing to choose between two passes is
388
451
  what stops a race becoming a wrong commit.
389
452
 
390
- **Where the harness cannot watch a file, you still wait** — the turn ending is
391
- the wait. Say the page is rendered and that you are holding for the pass, then end
392
- your turn; the reader's next message is what carries it, and `/spec-diff` picks
393
- it up from the paste exactly as it always has. The gate holds either way: it is
394
- the engine's, not the watch's.
453
+ **Where the harness cannot run something in the background, you still wait** —
454
+ the turn ending is the wait. Say the page is rendered and that you are holding
455
+ for the pass, then end your turn; the reader's next message is what carries it,
456
+ and `/spec-diff` picks it up from the paste exactly as it always has. The gate
457
+ holds either way: it is the engine's, not the wait's.
395
458
 
396
459
  This once read *"change nothing"* — keep the row, keep the question, do not
397
460
  wait — and that exemption is the hatch a whole class of unanswerable questions
@@ -401,38 +464,38 @@ came through. Every harness can end a turn.
401
464
  one line; by the time this step is reached the chain has finished, so waiting
402
465
  here stops nothing that was still going to happen.
403
466
 
404
- Relay the **`open:`** line the engine prints, not the bare path: a path is not
405
- clickable in any terminal, and a page nobody can open is a page nobody reads.
467
+ Relay the engine's **stack** the `local:`, `network:` and `remote:` lines
468
+ never the bare `page:` path: a path is not clickable in any terminal, and a page
469
+ nobody can open is a page nobody reads.
406
470
 
407
471
  - **Never write the review unasked**, and **never publish**. Publishing leaves
408
472
  something behind that this tooling cannot remove, so it is always something
409
473
  someone asks for. A `file://` link is no use on a phone, and saying so **is**
410
474
  the ask — publishing is the answer to it, and `/spec-diff` §6 owns how.
411
- **Follow the `reader:` line the engine printed do not sniff for it.** It
412
- answers where the person reading this is sitting, and the offer changes with it:
413
-
414
- - **absent** (`unknown`) the `file://` URL, exactly as always. **Do not warn:**
415
- unknown is the ordinary state of a local machine, and a warning there is an
416
- accusation against a healthy session.
417
- - **`local`** the `file://` URL.
418
- - **`remote`** — the engine has already stood its local server up and put a URL
419
- the reader can open on `open:`. So there is **nothing special to say**: relay
420
- that line like any other. Any `also:` lines under it are the other addresses
421
- this machine has, offered because the best-guess one can be wrong — pass them
422
- on rather than editing them out.
423
-
424
- **Never read an environment variable to decide this** not `SSH_CONNECTION`,
425
- not `CLAUDE_CODE_*`, not a tty check. The engine did it, reports it on that line
426
- and in `--json`, and a second implementation here could not be tested and would
427
- drift.
428
-
429
- **Serving is the engine's to do; publishing is never.** A `remote` reader
430
- authorises a local serverone process, ended by one flag, leaving nothing
431
- behind and authorises nothing else. A reader the server CAN reach is not a
432
- reason to publish as well: the page they can already open is the page to name. Publishing leaves a page this tooling
433
- cannot remove, so it stays an ask in every case, always. If the engine could not
434
- serve (a busy port, a machine with no network address) it falls back to the
435
- `file://` URL with its marker, and that is when publishing is worth naming.
475
+ **The `reader:` line no longer decides anything here, and that is the point.**
476
+ It is still printed, and it is still the only place that question is answered —
477
+ but the offer does not change with it, because the stack lists every tier
478
+ whatever it says. Three reader states used to mean three different offers, and
479
+ that branching is exactly what produced a `file://` page on a session detected
480
+ `unknown`, a LAN URL for a phone off the network, and an address that changed
481
+ underneath a reader when detection flipped mid-session.
482
+
483
+ So: **relay all three tier lines, every time.** Any `also:` lines sit under
484
+ `network` the other addresses this machine has, offered because the
485
+ best-guess one can be wrong — so pass them on rather than editing them out.
486
+
487
+ **Never read an environment variable to decide anything about the offer** — not
488
+ `SSH_CONNECTION`, not `CLAUDE_CODE_*`, not a tty check. There is nothing left
489
+ here for a detection to decide, and a second implementation of one could not be
490
+ tested and would drift.
491
+
492
+ **Serving is the engine's to do; publishing is never.** The server is one
493
+ process, ended by one flag, leaving nothing behind, and `review.allowNetwork`
494
+ decides whether it binds wide no detection involved. A reader the server CAN
495
+ reach is not a reason to publish as well: the tiers they can already open are on
496
+ the stack. Publishing leaves a page this tooling cannot remove, so it stays an
497
+ ask in every case, always which is why `remote` shows the command rather than
498
+ a URL until someone types it.
436
499
 
437
500
  - **Never fatal.** A failed render — no worktree, a git error — is one line and
438
501
  the phase is still done. The page is a convenience; the repo is the record.