@skitterbyte/skitterspec-linear 14.0.0 → 17.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 (49) hide show
  1. package/MIGRATION.md +304 -4
  2. package/README.md +34 -1
  3. package/assets/claude-md-section.md +29 -18
  4. package/assets/commands/spec-remote-review.md +22 -0
  5. package/assets/core/SETUP.md +10 -6
  6. package/assets/core/env.config.json.example +4 -2
  7. package/assets/core/env.config.md +103 -25
  8. package/assets/core/linear.config.json.example +2 -1
  9. package/assets/core/linear.config.md +49 -22
  10. package/assets/review/page.html +1101 -108
  11. package/assets/rules/spec-planning.md +39 -7
  12. package/assets/rules/spec-reports.md +210 -31
  13. package/assets/skills/spec/SKILL.md +161 -4
  14. package/assets/skills/spec-bug/SKILL.md +102 -49
  15. package/assets/skills/spec-cancel/SKILL.md +2 -2
  16. package/assets/skills/spec-claim/SKILL.md +12 -4
  17. package/assets/skills/spec-complete/SKILL.md +2 -2
  18. package/assets/skills/spec-diff/SKILL.md +183 -39
  19. package/assets/skills/spec-hotfix/SKILL.md +96 -49
  20. package/assets/skills/spec-init/SKILL.md +18 -6
  21. package/assets/skills/spec-linear-setup/SKILL.md +19 -11
  22. package/assets/skills/spec-next/SKILL.md +147 -62
  23. package/assets/skills/spec-push/SKILL.md +45 -0
  24. package/assets/skills/spec-review/SKILL.md +89 -2
  25. package/assets/skills/spec-reviewed/SKILL.md +31 -5
  26. package/assets/skills/spec-start/SKILL.md +26 -3
  27. package/assets/skills/spec-status/SKILL.md +20 -6
  28. package/assets/skills/spec-sync/SKILL.md +1 -0
  29. package/package.json +2 -2
  30. package/src/cli.js +940 -116
  31. package/src/env/classify.js +87 -2
  32. package/src/env/config.js +214 -17
  33. package/src/env/hooks.js +49 -9
  34. package/src/env/live.js +94 -0
  35. package/src/env/resolve.js +36 -2
  36. package/src/env/review.js +581 -21
  37. package/src/env/serve.js +298 -19
  38. package/src/env/supervise.js +8 -1
  39. package/src/init.js +88 -13
  40. package/src/vendor/linear/api.js +111 -2
  41. package/src/vendor/linear/cli-sync.js +661 -11
  42. package/src/vendor/linear/config.js +41 -13
  43. package/src/vendor/linear/doctor.js +6 -5
  44. package/src/vendor/sync-core/index.js +11 -3
  45. package/src/vendor/sync-core/src/compare.js +65 -0
  46. package/src/vendor/sync-core/src/normalize.js +26 -0
  47. package/src/vendor/sync-core/src/retarget.js +1 -1
  48. package/src/vendor/sync-core/src/task-block.js +2 -2
  49. /package/assets/hooks/{review-gate.js → review-gate.cjs} +0 -0
@@ -111,11 +111,40 @@ it happens differs (`/spec` writes the spec in Phase B, `/spec-bug` in its step
111
111
  description (an **update** to the existing issue, plus a sub-issue per phase).
112
112
  Recording a snapshot here would declare the mirror already in sync and strand
113
113
  the issue showing the raw report forever.
114
+ - **Preserve the original, before anything replaces it.** Run this the moment
115
+ the spec file exists and **before the linking push**:
116
+
117
+ ```
118
+ skitterspec spec-sync preserve <spec>
119
+ ```
120
+
121
+ It posts the issue's current description onto the issue as a comment — the one
122
+ surface one-way sync never touches, so no push can clobber it. On the API path
123
+ it reads the description itself; without a key it prints the body and the
124
+ marker, and you post it with the Linear comment tool after checking the
125
+ comments for that marker.
126
+
127
+ **The ordering is the whole correctness condition.** Run before the push, this
128
+ keeps what the reporter filed. Run after it, it would keep the generated spec
129
+ and report success — which is why it is a step of its own here rather than a
130
+ line in the push sequence. The engine warns when it can tell it is late, but it
131
+ cannot always tell.
132
+
133
+ **It never fails the adoption.** A project that set
134
+ `intake.preserveOriginal: false`, an unreachable Linear, an issue with no
135
+ description, a comment Linear refused — every one of them exits 0 and says so.
136
+ Relay what it printed and carry on.
114
137
  - **Say what will happen** in the finish-up message. The linking step runs right
115
138
  after the spec is written, so the issue's description is replaced by the spec
116
- **then** — not on some later manual push. The reporter's words are not lost:
117
- they are quoted in the spec's **Problem** (or **Symptom**) section, and Linear
118
- keeps the original in the issue's history.
139
+ **then** — not on some later manual push. Say both halves: the description
140
+ becomes the spec, **and** the original is preserved as a comment on the same
141
+ issue, so the reporter can still read what they wrote.
142
+
143
+ **Do not send anyone to the issue's history for it.** This used to say Linear
144
+ keeps the original there, and that is true and useless: history is a diff
145
+ viewer nobody opens, it is not quotable, and it degrades to "it is in there
146
+ somewhere". The comment is the answer, and the other half is the spec's own
147
+ **Problem** (or **Symptom**) section quoting the reporter.
119
148
 
120
149
  ## 1. Establish the base version (the tag)
121
150
 
@@ -333,10 +362,14 @@ and **never auto-push git** — Linear's own automation reacts to real branch/PR
333
362
  events later. Report the Linear issue URL as part of the skill's finish-up
334
363
  message.
335
364
 
336
- **Only when all three hold**: `specs/.core/linear.config.json` exists, its
337
- `sync.fieldOwnership` includes `assignee`, and the spec carries a
365
+ **Only when all three hold**: `specs/.core/linear.config.json` exists, the repo
366
+ **owns** `assignee` in its `sync.fieldOwnership`, and the spec carries a
338
367
  `linear_identifier`. Any one missing → skip this step silently and carry on; a
339
- project that has not opted in must see no trace of assignment.
368
+ project that declined must see no trace of assignment.
369
+
370
+ Ownership is the **value**, not the key: `assignee` is owned by default, and
371
+ `"none"` is how a project declines it. Testing whether the key is *present* would
372
+ read as opted-in everywhere, since the default puts it in every config.
340
373
 
341
374
  **Never blocks, never fails the skill.** Everything below is best-effort: the
342
375
  branch is provisioned and the spec is moving either way, and an unassigned issue
@@ -473,7 +506,7 @@ sub-issue jump from Backlog straight to Done, with nothing visible in between.
473
506
  - **Say what happened** in the skill's report: mirror updated, skipped as
474
507
  unlinked, or failed with the reason.
475
508
 
476
- ## 6b. Render the page then offer the review, never write it
509
+ ## 6b. Render the page, arm the gate, then wait for the verdict
477
510
 
478
511
  **Only when the project has per-spec isolation** (`specs/.core/env.config.json`
479
512
  present). Without it there is no worktree to read and this step does not exist —
@@ -489,53 +522,61 @@ skitterspec spec-env review <spec>
489
522
  **This is free.** The engine reads git and splices the patches into a template;
490
523
  the diff never passes through you, so a 266KB patch costs nothing.
491
524
 
492
- **Then offer `/spec-diff`. Do not run it.** The written review is the part that
493
- costs — roughly **700 output tokens**, because writing it means reading the diff
494
- — and that spend is the operator's call, not a default.
495
-
496
- **The offer is the `Review` row of step 6's block** — the counts, the page link
497
- and a question, in one row:
498
-
499
- | **Review** | <N> files, +<a> −<d> · [open the page](<the `open:` URL>) — want a written review before you commit? |
500
-
501
- **It ends in a question, addressed to someone.** It was once a fenced block of
502
- engine output, and it fired on every phase and was never once taken: two quoted
503
- lines under the test counts, addressed to nobody, with the report then closing
504
- on *"commit this first"* — the last instruction the reader got was to move on,
505
- so they did. A row in a labelled table is findable; a question in it is
506
- answerable. Both halves are load-bearing.
507
-
508
- **Never bury it and never split it.** It sits above `Follow-ups` and `Next`, and
509
- the page and the question stay in the same row: two adjacent rows about one page
510
- make the reader resolve a distinction before acting on either. A later edit that
511
- moves it out of the block, or separates the link from the question, undoes this
512
- and should be read as a regression rather than tidying.
525
+ **Then arm the gate**, so the fix now owes a verdict:
513
526
 
527
+ ```
528
+ skitterspec spec-env review arm <spec>
529
+ ```
514
530
 
515
- Relay the **`open:`** line rather than the bare path: a path is not clickable in
516
- any terminal, and a page nobody can open is a page nobody reads.
531
+ **Then wait**, and `/spec-next` §5 owns the sequence follow it there rather
532
+ than reading a second copy here: note the moment you start waiting, watch the
533
+ pending store, end your turn, and let `--claim-since` pick the one pass that
534
+ arrived inside the window. The banner it describes is what this skill emits in
535
+ place of a `Review` row, and the routing on the verdict is `/spec-diff` §2 and
536
+ §4, as it is everywhere.
537
+
538
+ **Why this skill arms as well as waits.** Waiting is what any offer does;
539
+ **arming** asserts an obligation that outlives the turn, and belongs only to
540
+ work that is finished. A hotfix is a completed unit — red→green against a
541
+ released tag — so it qualifies, and a wait with nothing owed behind it is a
542
+ suggestion rather than a gate.
543
+
544
+ **A hotfix is the case where reading it matters most.** This change is about to
545
+ be tagged and shipped to production from a release line, not merged into a
546
+ branch someone else will read first. The verdict is the only review it gets.
547
+
548
+ **It is user-visible, and that is deliberate.** Once armed, a `git commit` in
549
+ this worktree is refused until a verdict is sent or
550
+ `skitterspec spec-env review skip "<reason>"` records the decision to move on.
551
+ The exit is always one command, and one of them is *"I am moving on"*.
552
+
553
+ Relay the engine's **stack** — the `local:`, `network:` and `remote:` lines, all
554
+ three — rather than the bare `page:` path: a path is not clickable in any
555
+ terminal, and a page nobody can open is a page nobody reads. Relay the `live:`
556
+ line with it where the engine printed one, and nothing where it did not.
557
+ `.claude/rules/spec-reports.md` carries the shape, including why a rigid
558
+ contract took that line.
517
559
 
518
560
  - **Never write the review unasked**, and **never publish**. Publishing leaves
519
561
  something behind that this tooling cannot remove, so it is always an ask. A
520
562
  `file://` link is no use on a phone, and saying so **is** the ask —
521
563
  `/spec-diff` §6 owns how.
522
- **Follow the `reader:` line the engine printed do not sniff for it.** It
523
- answers where the person reading this is sitting, and the offer changes with it:
524
-
525
- - **absent** (`unknown`) the `file://` URL, exactly as always. **Do not warn:**
526
- unknown is the ordinary state of a local machine, and a warning there is an
527
- accusation against a healthy session.
528
- - **`local`** the `file://` URL.
529
- - **`remote`** — the engine has already stood its local server up and put a URL
530
- the reader can open on `open:`. So there is **nothing special to say**: relay
531
- that line like any other. Any `also:` lines under it are the other addresses
532
- this machine has, offered because the best-guess one can be wrong — pass them
533
- on rather than editing them out.
534
-
535
- **Never read an environment variable to decide this** not `SSH_CONNECTION`,
536
- not `CLAUDE_CODE_*`, not a tty check. The engine did it, reports it on that line
537
- and in `--json`, and a second implementation here could not be tested and would
538
- drift.
564
+ **The `reader:` line no longer decides anything here, and that is the point.**
565
+ It is still printed, and it is still the only place that question is answered —
566
+ but the offer does not change with it, because the stack lists every tier
567
+ whatever it says. Three reader states used to mean three different offers, and
568
+ that branching is exactly what produced a `file://` page on a session detected
569
+ `unknown`, a LAN URL for a phone off the network, and an address that changed
570
+ underneath a reader when detection flipped mid-session.
571
+
572
+ So: **relay all three tier lines, every time.** Any `also:` lines sit under
573
+ `network` the other addresses this machine has, offered because the
574
+ best-guess one can be wrong — so pass them on rather than editing them out.
575
+
576
+ **Never read an environment variable to decide anything about the offer** — not
577
+ `SSH_CONNECTION`, not `CLAUDE_CODE_*`, not a tty check. There is nothing left
578
+ here for a detection to decide, and a second implementation of one could not be
579
+ tested and would drift.
539
580
 
540
581
  **Serving is the engine's to do; publishing is never.** A `remote` reader
541
582
  authorises a local server — one process, ended by one flag, leaving nothing
@@ -578,10 +619,16 @@ the shape; this section carries only what is specific here.
578
619
  **Fields:** `Tracker` · `Branch` · `Spec` · `Cause` · `Built` · `Tests` ·
579
620
  `Review` · `Follow-ups` · `Next`
580
621
 
622
+ **`Review` is emitted only where step 6b did not run** — a project with no
623
+ isolation, or a render that failed. Where the run is waiting, the banner carries
624
+ the whole subject and the row is dropped, per `.claude/rules/spec-reports.md`.
625
+
581
626
  **The base tag goes in the verdict clause** — `✅ /spec-hotfix · hotfix-foo ·
582
627
  green on v2.3.1`. Which released version this was fixed against is the first
583
628
  thing anyone needs, and it is not a field: the clause is where the run says
584
629
  where it got to.
585
630
 
586
- Step 6b's offer is the `Review` row, not a paragraph after the block — the
587
- counts, the link and the question in one row. Nothing follows the block.
631
+ Step 6b ends in the **banner**, not a paragraph and not a row — the run is
632
+ waiting on a verdict, and the banner is the shape that says so. Nothing follows
633
+ it. Where the run is not waiting it asks nothing at all: a `Review` row carries
634
+ 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
@@ -178,19 +178,27 @@ a state from the `list_issue_statuses` names you already have:
178
178
  automation closes them and a problem otherwise — `spec-sync doctor` warns about
179
179
  it either way, so say which it is.
180
180
 
181
- ## 7b. Should a spec's issue be assigned to whoever is building it? (optional)
181
+ ## 7b. Assignment is on does this team want it off?
182
182
 
183
- Ask it plainly, in one line: with this on, `/spec-start` records the developer
184
- and their Linear issue is assigned to them, released again when the spec
185
- completes. Off, nothing about assignment happens at all.
183
+ Say what it does, in one line: `/spec-start` records the developer and their
184
+ Linear issue is assigned to them, released again when the spec completes. Then
185
+ ask whether to turn it **off**.
186
186
 
187
- **Offer once and take "no" for an answer.** It is off by default deliberately —
188
- a team whose PM owns assignment in Linear does not want the repo writing that
189
- field, and the cost of guessing wrong is somebody else's triage being
190
- overwritten.
187
+ **The question inverted at v16, and the reason is which mistake is recoverable.**
188
+ While it was opt-in, a repo that simply never added the line was
189
+ indistinguishable from one that had decided against it so the only signal was
190
+ someone noticing weeks later that nobody was on the issue. On by default, a team
191
+ that does not want it says so once, here, and the answer is on the record.
191
192
 
192
- If they want it, pass **`--assign`** in step 8. Then settle who *they* are, so
193
- `/spec-start` never has to stop and ask:
193
+ **It cannot overwrite a PM's triage either way**, which is what makes the default
194
+ safe rather than merely convenient: a spec that records no assignee sends none,
195
+ and a snapshot with no assignee key means "never pushed" rather than "was null".
196
+ Assignment only ever writes specs *this repo* started.
197
+
198
+ If they want it off, pass **`--no-assign`** in step 8, and skip the rest of this
199
+ step — there is no identity to settle for a repo that does not assign.
200
+
201
+ Otherwise settle who *they* are, so `/spec-start` never has to stop and ask:
194
202
 
195
203
  ```
196
204
  skitterspec spec-sync whoami
@@ -210,7 +218,7 @@ setup.
210
218
  skitterspec spec-sync init-config \
211
219
  --team-id <uuid> [--team-key KEY] [--project-id <uuid>] \
212
220
  [--intake-label <name>] [--bug-labels a,b] [--hotfix-labels a,b] \
213
- [--state <bucket>=<name> …] [--assign] \
221
+ [--state <bucket>=<name> …] [--no-assign] \
214
222
  --states <statesfile> [--force] [--json]
215
223
  ```
216
224
 
@@ -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,
@@ -184,8 +202,8 @@ once it is over. Without a provider this is a no-op and nothing below changes.
184
202
  them. Skip it and a started spec stays mirrored as a phase-less issue.
185
203
  - **Never mint the spec issue.** An unlinked spec is skipped, not created —
186
204
  `/spec-push` is how someone opts in.
187
- - **Backfill a missing assignee silently; never ask.** Only when
188
- `sync.fieldOwnership` includes `assignee`. If the spec records nobody and
205
+ - **Backfill a missing assignee silently; never ask.** Only when the repo owns
206
+ `assignee` (it does by default; `"none"` declines it). If the spec records nobody and
189
207
  `skitterspec spec-sync whoami --json` answers, stamp it with
190
208
  `skitterspec spec-sync assign <spec> --to <id> --name "<name>"` and let the
191
209
  push above carry it. If identity is unknown, skip it in silence.
@@ -323,6 +341,12 @@ skitterspec spec-env review <spec>
323
341
  the diff never passes through you, so a 266KB patch costs nothing. Report the
324
342
  path it prints and move on.
325
343
 
344
+ **This render takes the committing button set**, which is the default — so
345
+ `--buttons` is not passed. That is a statement about the work, not about the
346
+ gate: the phase is finished, so `Commit` and `Commit & Continue` are the right
347
+ verbs for it. A render part-way through a run takes `--buttons midrun` and
348
+ offers `Continue` instead; `/spec-diff` owns that case.
349
+
326
350
  **Then arm the gate**, so the phase now owes a verdict:
327
351
 
328
352
  ```
@@ -347,41 +371,72 @@ so one subject lives in one place:
347
371
 
348
372
  ## ⏸ Review ready — <N> files, +<a> −<d>
349
373
 
350
- **[Open the page](<the `open:` URL>)** · I'm holding here until you send a verdict.
374
+ - **local** <the `local:` URL>
375
+ - **network** — <the `network:` URL, or off with the command that turns it on>
376
+ - **remote** — <the `remote:` URL, or off with the command that turns it on>
351
377
 
352
- `/spec-reviewed` picks it up · `spec-env review skip "<reason>"` moves on
378
+ <the `live:` line, exactly as the engine printed it omitted when it printed none>
353
379
 
354
- **One link, and the engine has already chosen it.** Where it served, the
355
- banner carries the served URL and the wait is real. Where it could not serve —
356
- a busy port, a machine with no network address — that is the case publishing
357
- exists for, and then the banner carries the published URL with what is true of
358
- it: *press a verdict, then type `/spec-reviewed`*, because nothing pushes from
359
- the artifact store into this conversation.
380
+ I'm holding here until you send a verdict the wait covers local and network.
360
381
 
361
- **Never offer both.** Publishing while the server is reachable adds a second
362
- door the reader cannot tell apart from the first, and the wait only stands
363
- behind one of themthat was done, and three verdicts were pressed on the
364
- published page while each sat unread under a line saying I was holding. The
365
- published page is for the reader the server cannot reach, and for nobody
366
- else.
382
+ `/spec-reviewed` picks it up · `spec-env review skip "<reason>"` moves on
383
+
384
+ **The stack is the engine's copy it, do not compose it.** `spec-env review`
385
+ printed one line per tier, in that order, and `--json` carries the same thing as
386
+ `tiers`. Relay all three whatever they say, including the ones that are off with
387
+ the command that turns them on: a reader who has left the house cannot ask for a
388
+ remote page they were never shown.
389
+
390
+ **Do not pick one for them.** The engine used to, and the guess failed three
391
+ separate ways in one day. `.claude/rules/spec-reports.md` carries the shape and
392
+ why the *one link, never two* rule now reads
393
+ **one link per reachable store, each labelled** —
394
+ local and network are two doors into one room, so the wait
395
+ covers both. `remote` is a second store, so its own line carries its own caveat:
396
+ *a verdict here needs `/spec-reviewed`*, because nothing pushes from
397
+ the artifact store into this conversation. Never let the holding line promise a
398
+ wait over that tier.
399
+
400
+ **Publishing is still never yours to do.** `remote` reading `off` is the normal
401
+ state, and the fix for it is the command on that line — typed by the operator,
402
+ or pressed on the page, because a published page is one this tooling cannot
403
+ remove and permitting it is not publishing it.
404
+
405
+ **The `live:` line goes with the stack**, and it is the engine's line too —
406
+ copied as printed, and **absent when the engine printed none**. The page can act
407
+ on it: a press puts the spec live or takes it down and hands the reader back the
408
+ same page, which is `/spec-diff` §2b's routing and not this skill's. What
409
+ matters here is that an action **never clears the gate this phase armed** — the
410
+ reader has looked at it running and concluded nothing, so the phase still owes a
411
+ verdict.
367
412
 
368
413
  ---
369
414
 
370
- **Where you are not waiting, it stays the `Review` row** — the counts, the page
371
- link and a question, in one row:
415
+ **Where you are not waiting, it stays the `Review` row** — the counts and the
416
+ page link, and **no question**:
417
+
418
+ | **Review** | <N> files, +<a> −<d> · **local** <URL> · **network** <URL> · **remote** off |
372
419
 
373
- | **Review** | <N> files, +<a> −<d> · [open the page](<the `open:` URL>) want a written review before you commit? |
420
+ At the end of a phase you are always waiting, so this shape belongs to the
421
+ renders that are not this step: a mid-phase `/spec-diff`, a page produced
422
+ alongside other work. A row cannot be waited on, so a question in one is
423
+ unanswerable however findable it is.
374
424
 
375
425
  **Both shapes are addressed to someone, and that is the constraint.** The offer
376
426
  was once a fenced block of engine output: two quoted lines under the test
377
427
  counts, addressed to nobody, with the report then closing on *"commit this
378
428
  first"* — the last instruction the reader got was to move on, so they did. A row
379
- in a labelled table is findable; a question in it is answerable; a banner says
380
- the work has stopped. What must never come back is something unaddressed,
381
- unfindable, or fenced.
429
+ in a labelled table is findable; a banner says the work has stopped. What must
430
+ never come back is something unaddressed, unfindable, or fenced.
431
+
432
+ **The row asks nothing, and that is not a weakening of the above.** A row cannot
433
+ be waited on, so a question in one is unanswerable however findable it is —
434
+ which is the failure `spec-reports.md` records under *asking implies waiting*.
435
+ The row names the page; the banner is what asks, because the banner is the shape
436
+ the run is standing behind.
382
437
 
383
438
  **Never bury it and never split it.** The row sits above the last two rows of
384
- the block, and the page and the question stay in the same row; the banner
439
+ the block, and the page and its counts stay in the same row; the banner
385
440
  replaces the row rather than joining it. Two places naming one page make the reader
386
441
  resolve a distinction before acting on either — which is the same failure
387
442
  whether the two places are adjacent rows or a row and a banner.
@@ -390,16 +445,25 @@ whether the two places are adjacent rows or a row and a banner.
390
445
 
391
446
  The row is findable, but a row cannot make the continuation follow from the
392
447
  reading — and that is the gap the whole gate exists to close.
393
- **Where this harness can watch a file and wake the session on a change, use it.** The pass
394
- arrives at the engine's holding area, the watch fires, and the verdict the
395
- reader pressed is what carries the work on.
448
+
449
+ **The wait is a command. Do not write one.**
396
450
 
397
451
  1. **Note the moment you start waiting**, as an ISO timestamp. That instant is
398
452
  the whole scope of what you may claim.
399
- 2. **Watch the pending store** for the spec
400
- `.spec-env/reviews/<spec>.pending.json` in the primary checkout — and
401
- **end your turn**. Do not poll, and do not hold the turn open: the point is that
402
- the reader has the terminal back while they read.
453
+ 2. **Run the engine's wait in the background, and end your turn:**
454
+
455
+ ```
456
+ skitterspec spec-env review wait <spec> --since <the timestamp>
457
+ ```
458
+
459
+ It returns when a pass arrives inside that window, and
460
+ **it takes no timeout unless you pass one** —
461
+ the wait lasts as long as your session, because a reader who walks away from
462
+ a diff is the normal case rather than the edge one. Do not give it a duration
463
+ of your own.
464
+
465
+ Then **end your turn**. Do not poll it and do not hold the turn open: the
466
+ point is that the reader has the terminal back while they read.
403
467
  3. **On waking, let the engine pick**:
404
468
 
405
469
  ```
@@ -407,11 +471,26 @@ reader pressed is what carries the work on.
407
471
  ```
408
472
 
409
473
  It claims the one pass that arrived inside the window, and acts on nothing
410
- at all when none did (the watch can fire on a write that was not a pass) or
411
- when two did (two sittings, or two people — the operator has the codes).
474
+ at all when none did (the wait can be woken by something that was not a
475
+ pass) or when two did (two sittings, or two people — the operator has the
476
+ codes).
412
477
  4. **Route on the verdict** exactly as `/spec-diff` §2 and §4 describe. Do not
413
478
  restate that routing here.
414
479
 
480
+ **WHY A COMMAND RATHER THAN A LOOP YOU COMPOSE.** This step used to say "watch
481
+ the pending store" and stop, so every run invented its own watcher in shell —
482
+ and three failed in two days, each reaching the operator as *"I pressed the
483
+ button and nothing happened"*. The worst wrote
484
+ `until [ -f "$P" ] && [ "$x" \> "$y" ]`: valid bash, a syntax error in zsh, a
485
+ predicate that could never be true. It spun for five minutes writing to a
486
+ stderr nobody reads.
487
+
488
+ **The bug is not the lesson — the silence is.** A watcher that can never fire
489
+ and one patiently working are indistinguishable from outside, so nothing about
490
+ that run looked wrong until the operator asked. `review wait` says it has
491
+ started, is written once, and is tested against a store that gains a pass
492
+ mid-flight. A predicate composed fresh each time is proven by nothing.
493
+
415
494
  **WHY THIS IS SAFE, AND WHAT IT COSTS.** It was once true that a device
416
495
  reaching your page could not reach your conversation, and that fact was the
417
496
  whole guard: a pass sat in the holding area until a person typed
@@ -425,46 +504,52 @@ is that the page can now act, so the token has become a credential rather than
425
504
  a convenience — and `--claim-since` refusing to choose between two passes is
426
505
  what stops a race becoming a wrong commit.
427
506
 
428
- **Where the harness cannot watch a file, change nothing.** The `Review` row and
429
- `/spec-reviewed` are the whole story, exactly as before. The gate still holds
430
- either way: it is the engine's, not the watch's.
507
+ **Where the harness cannot run something in the background, you still wait**
508
+ the turn ending is the wait. Say the page is rendered and that you are holding
509
+ for the pass, then end your turn; the reader's next message is what carries it,
510
+ and `/spec-diff` picks it up from the paste exactly as it always has. The gate
511
+ holds either way: it is the engine's, not the wait's.
512
+
513
+ This once read *"change nothing"* — keep the row, keep the question, do not
514
+ wait — and that exemption is the hatch a whole class of unanswerable questions
515
+ came through. Every harness can end a turn.
431
516
 
432
517
  **It still does not break a chained run.** `/commit && /spec-next` is typed as
433
518
  one line; by the time this step is reached the chain has finished, so waiting
434
519
  here stops nothing that was still going to happen.
435
520
 
436
- Relay the **`open:`** line the engine prints, not the bare path: a path is not
437
- clickable in any terminal, and a page nobody can open is a page nobody reads.
521
+ Relay the engine's **stack** the `local:`, `network:` and `remote:` lines
522
+ never the bare `page:` path: a path is not clickable in any terminal, and a page
523
+ nobody can open is a page nobody reads.
438
524
 
439
525
  - **Never write the review unasked**, and **never publish**. Publishing leaves
440
526
  something behind that this tooling cannot remove, so it is always something
441
527
  someone asks for. A `file://` link is no use on a phone, and saying so **is**
442
528
  the ask — publishing is the answer to it, and `/spec-diff` §6 owns how.
443
- **Follow the `reader:` line the engine printed do not sniff for it.** It
444
- answers where the person reading this is sitting, and the offer changes with it:
445
-
446
- - **absent** (`unknown`) the `file://` URL, exactly as always. **Do not warn:**
447
- unknown is the ordinary state of a local machine, and a warning there is an
448
- accusation against a healthy session.
449
- - **`local`** the `file://` URL.
450
- - **`remote`** — the engine has already stood its local server up and put a URL
451
- the reader can open on `open:`. So there is **nothing special to say**: relay
452
- that line like any other. Any `also:` lines under it are the other addresses
453
- this machine has, offered because the best-guess one can be wrong — pass them
454
- on rather than editing them out.
455
-
456
- **Never read an environment variable to decide this** not `SSH_CONNECTION`,
457
- not `CLAUDE_CODE_*`, not a tty check. The engine did it, reports it on that line
458
- and in `--json`, and a second implementation here could not be tested and would
459
- drift.
460
-
461
- **Serving is the engine's to do; publishing is never.** A `remote` reader
462
- authorises a local serverone process, ended by one flag, leaving nothing
463
- behind and authorises nothing else. A reader the server CAN reach is not a
464
- reason to publish as well: the page they can already open is the page to name. Publishing leaves a page this tooling
465
- cannot remove, so it stays an ask in every case, always. If the engine could not
466
- serve (a busy port, a machine with no network address) it falls back to the
467
- `file://` URL with its marker, and that is when publishing is worth naming.
529
+ **The `reader:` line no longer decides anything here, and that is the point.**
530
+ It is still printed, and it is still the only place that question is answered —
531
+ but the offer does not change with it, because the stack lists every tier
532
+ whatever it says. Three reader states used to mean three different offers, and
533
+ that branching is exactly what produced a `file://` page on a session detected
534
+ `unknown`, a LAN URL for a phone off the network, and an address that changed
535
+ underneath a reader when detection flipped mid-session.
536
+
537
+ So: **relay all three tier lines, every time.** Any `also:` lines sit under
538
+ `network` the other addresses this machine has, offered because the
539
+ best-guess one can be wrong — so pass them on rather than editing them out.
540
+
541
+ **Never read an environment variable to decide anything about the offer** — not
542
+ `SSH_CONNECTION`, not `CLAUDE_CODE_*`, not a tty check. There is nothing left
543
+ here for a detection to decide, and a second implementation of one could not be
544
+ tested and would drift.
545
+
546
+ **Serving is the engine's to do; publishing is never.** The server is one
547
+ process, ended by one flag, leaving nothing behind, and `review.allowNetwork`
548
+ decides whether it binds wide no detection involved. A reader the server CAN
549
+ reach is not a reason to publish as well: the tiers they can already open are on
550
+ the stack. Publishing leaves a page this tooling cannot remove, so it stays an
551
+ ask in every case, always which is why `remote` shows the command rather than
552
+ a URL until someone types it.
468
553
 
469
554
  - **Never fatal.** A failed render — no worktree, a git error — is one line and
470
555
  the phase is still done. The page is a convenience; the repo is the record.
@@ -155,6 +155,24 @@ the chosen project through as `--project <id>`.
155
155
 
156
156
  ## 4a. Apply it yourself — the MCP path (order matters)
157
157
 
158
+ **First, check whether you are about to overwrite someone.** On the API path
159
+ `apply` does this for itself, off the read-back it already makes. On the MCP
160
+ path nothing has read the issue yet, so ask before you write:
161
+
162
+ ```
163
+ skitterspec spec-sync status <spec> --remote <issuefile>
164
+ ```
165
+
166
+ using the same issue JSON step 2 fetched — it must carry the `description`. A
167
+ `drift: … description was edited on Linear since the last push` line means a
168
+ person wrote something there that step 1 below replaces.
169
+
170
+ **It is a warning and never a gate.** The repo is the source of truth and the
171
+ push still goes ahead; relay the line, name the issue, and carry on. What is
172
+ forbidden is doing it *silently* — someone's paragraph disappearing with nothing
173
+ said about it is the failure this exists to end. Nothing printed means nothing to
174
+ say, which includes every case the engine could not tell.
175
+
158
176
  1. **Spec issue** → if the overview has no `linear_identifier`, this push
159
177
  **mints** it: run the picker in **Picking the Linear Project** below, then
160
178
  create it with `save_issue` (`team` = `linear.teamId`; `project` = the picked
@@ -233,6 +251,33 @@ This writes the last-pushed snapshot from the now-stamped files, so the next
233
251
  `/spec-push` produces an empty plan. Commit the stamped spec + snapshot into the
234
252
  branch so the mirror-link rides in the PR.
235
253
 
254
+ ## 5b. A refused mint, and the way back
255
+
256
+ Two refusals reach you from `apply`, and neither is a failure to retry.
257
+
258
+ **`N already exists and no spec claims it`** — the push was about to mint, and
259
+ an issue with **exactly** this title is sitting there unheld. That is almost
260
+ always the spec's own issue, whose link was lost: a bad merge, a hand edit, a
261
+ stray `sed` over the frontmatter. Adopt it rather than creating a twin:
262
+
263
+ ```
264
+ skitterspec spec-sync reattach <spec> --to <ISSUE-REF>
265
+ ```
266
+
267
+ Bare, `reattach` searches by title itself and stamps the one unclaimed match;
268
+ it refuses when several match and names them, because choosing between two is
269
+ a guess. `--force-new` is the escape when the duplicate title is genuine.
270
+
271
+ **`N exists in Linear but could not be recorded`** — the create landed and the
272
+ stamp did not, so Linear holds an issue the repo does not point at.
273
+ **Do not re-run**:
274
+ the plan still reads the spec as unlinked, so a re-run mints a second.
275
+ `reattach --to <the named id>` is the fix.
276
+
277
+ Everything else — a rate limit, a refusal that says waiting will not help — is
278
+ reported with Linear's own explanation and whether retrying can help. Relay it;
279
+ do not paraphrase.
280
+
236
281
  ## 6. Report
237
282
 
238
283
  End with the block defined in `.claude/rules/spec-reports.md`. That file carries