@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
@@ -1,7 +1,7 @@
1
1
  # Spec Planning
2
2
 
3
3
  Spec-driven development is driven by ten lifecycle skills (plus the
4
- `/spec-connect` and `/spec-live` **commands** when isolation is on) — use them rather than hand-rolling specs so the structure
4
+ `/spec-connect`, `/spec-live` and `/spec-remote-review` **commands** when isolation is on) — use them rather than hand-rolling specs so the structure
5
5
  and lifecycle stay consistent. Each sets a status on the spec header
6
6
  (`> **Status:** …`):
7
7
 
@@ -20,13 +20,24 @@ and lifecycle stay consistent. Each sets a status on the spec header
20
20
  | `/spec-init` | Bootstrap/repair this workflow in a project (idempotent) | — | — |
21
21
 
22
22
  **Skills vs commands.** The table above lists **skills** — Claude reads them and
23
- exercises judgment. `/spec-connect` and `/spec-live` are instead **slash commands**
23
+ exercises judgment. `/spec-connect`, `/spec-live` and `/spec-remote-review` are
24
+ instead **slash commands**
24
25
  (`.claude/commands/`): each pre-executes one `skitterspec spec-env` verb and
25
26
  relays its output, so there is no judgment to apply and no model turn spent
26
27
  finding one. They are marked `disable-model-invocation`, meaning
27
28
  **only you can run them** — a skill that wants one will tell you to type it
28
29
  rather than invoking it.
29
30
 
31
+ **`/spec-remote-review`** is the newest and the smallest: it toggles
32
+ `review.allowRemote`, and takes `on`/`off` when you would rather say which.
33
+ It exists because a render's `remote:` line has to name something a person can
34
+ act on, and `skitterspec spec-env review allow remote` is not what anyone
35
+ reconstructs from a page they are reading on a phone. Making the line
36
+ *clickable* was considered and rejected — the only clickable thing markdown has
37
+ is a URL, and a URL that acts when it is **fetched** is one a link previewer or
38
+ a prefetcher fires with nobody involved. It **permits** publishing; it publishes
39
+ nothing.
40
+
30
41
  `/spec-reviewed` is a **skill** and **user-only**, and there the marking is not
31
42
  convenience — it is the enforcement of `/spec-diff` step 0's rule that a waiting
32
43
  review pass is never claimed unasked. Because the model cannot invoke this
@@ -244,10 +255,10 @@ things clear it — a **committing verdict**, or
244
255
  `git commit` refuses in that worktree. `skitterspec spec-env review gate
245
256
  [--check] [--json]` is what both ask.
246
257
 
247
- **The hook is the half that cannot be talked past.** `skitterspec init` installs
248
- `.claude/hooks/review-gate.js` and registers it in the project's committed
249
- `.claude/settings.json`, so a `git commit` is refused by the harness rather than
250
- by prose — which is what covers a bare `git commit`, a chained command, and
258
+ **The hook is the half that cannot be talked past.** `skitterspec init` and
259
+ `skitterspec update` install `.claude/hooks/review-gate.cjs` and register it in
260
+ the project's committed `.claude/settings.json`, so a `git commit` is refused by
261
+ the harness rather than by prose — which is what covers a bare `git commit`, a chained command, and
251
262
  skittership's own `/commit` without skitterspec ever editing it. It decides
252
263
  nothing itself: it hands the command line to
253
264
  `spec-env review gate --check --for-command` and turns one exit status into an
@@ -301,7 +312,7 @@ its verdicts and its fields.
301
312
  | **Branch** | `spec/feat-orders` · 3 commits, clean |
302
313
  | **Built** | POST /orders handler, orders schema |
303
314
  | **Tests** | 128 passed · npm test |
304
- | **Review** | 7 files, +212 −18 · [open the page](file:///…) want a written review before you commit? |
315
+ | **Review** | 7 files, +212 −18 · **local** [http://127.0.0.1:7760/…](http://127.0.0.1:7760/…) · **network** [http://192.168.0.136:7760/…](http://192.168.0.136:7760/…) · **remote** off |
305
316
  | **Follow-ups** | none |
306
317
  | **Next** | `/spec-next` → phase 3 (Auth) |
307
318
 
@@ -333,6 +344,27 @@ against a committed last-pushed snapshot and applies it) and `/spec-status`
333
344
  workflow-state drifted), backed by a `spec-sync` CLI. There is no content pull —
334
345
  the tracker is never read back or merged.
335
346
 
347
+ **One-way means the repo wins, and it now wins *loudly*.** Those are different
348
+ claims and only the first was ever in doubt. A ticket's description is generated
349
+ from the spec, so a person editing it there is writing something the next push
350
+ replaces — which is correct, and which used to happen in silence. The engine now
351
+ compares a hash of what the tracker holds against a hash of what the repo last
352
+ sent, and says *that* it changed and where to read it.
353
+ It **reports and never refuses**: only a person can tell a typo fix from a
354
+ paragraph worth keeping.
355
+
356
+ **This is still not a pull.** Nothing read is merged, written to a spec, or fed
357
+ into the projection, and the text never enters the conversation — the comparison
358
+ is hashes, and the output is one line for a human. It is the same thing
359
+ `compareStored` has always done for a different question.
360
+
361
+ **And adoption no longer costs the reporter their words.** When a provider
362
+ adopts an existing ticket — one a PM or colleague filed — the linking push
363
+ replaces its description with the spec. Before that happens the original is
364
+ posted back onto the ticket as a **comment**: a surface one-way sync never
365
+ touches, so no later push can clobber it. Where a provider supports it, this is
366
+ automatic and has an opt-out. See the provider's own docs.
367
+
336
368
  A provider may also ship a **read-only listing** of what the tracker holds. That
337
369
  is not a pull: nothing is merged back and no spec file is written. It exists
338
370
  because the folder buckets are only the truth *on the branch you are standing
@@ -36,7 +36,7 @@ A verdict sentence, a blank line, then a two-column table:
36
36
  | **Branch** | `spec/feat-foo` · 3 commits, clean |
37
37
  | **Built** | POST /orders handler, orders schema |
38
38
  | **Tests** | 128 passed · npm test |
39
- | **Review** | 7 files, +212 −18 · [open the page](file:///…) want a written review before you commit? |
39
+ | **Review** | 7 files, +212 −18 · **local** [http://127.0.0.1:7760/…](http://127.0.0.1:7760/…) · **network** [http://192.168.0.136:7760/…](http://192.168.0.136:7760/…) · **remote** off |
40
40
  | **Follow-ups** | none |
41
41
  | **Next** | `/spec-next` → phase 3 (Auth) |
42
42
 
@@ -87,32 +87,182 @@ the options may be offered after the table.
87
87
 
88
88
  ## ⏸ Review ready — 7 files, +212 −18
89
89
 
90
- **[Open the page](http://…)** · I'm holding here until you send a verdict.
90
+ - **local** [http://127.0.0.1:7760/7e9e123e7540/feat-orders](http://127.0.0.1:7760/7e9e123e7540/feat-orders)
91
+ - **network** — [http://192.168.0.136:7760/7e9e123e7540/feat-orders](http://192.168.0.136:7760/7e9e123e7540/feat-orders)
92
+ - **remote** — off · `/spec-remote-review` turns it on
91
93
 
92
- `/spec-reviewed` picks it up · `spec-env review skip "<reason>"` moves on
93
-
94
- ---
94
+ live: off /spec-live to put it live
95
95
 
96
- A rule, a heading naming the state and the size, the link in bold, one line
97
- saying the run is stopped, and the two exits. Nothing else, and never a
98
- paragraph explaining it.
96
+ I'm holding here until you send a verdict the wait covers local and network.
99
97
 
100
- **Only promise a wait the transport can deliver.** A page the engine SERVES
101
- hands its pass to the local store, which a file-watch sees — that is the one
102
- case where *"I'm holding here"* is true. A **published** page writes to the
103
- artifact's own store, and nothing pushes from there into the conversation, so a
104
- verdict pressed on it is invisible until someone asks for it. Its banner says
105
- so instead:
98
+ `/spec-reviewed` picks it up · `spec-env review skip "<reason>"` moves on
106
99
 
107
- **[Open the page](https://…)** · press a verdict, then type `/spec-reviewed` — I cannot see it until you do.
100
+ ---
108
101
 
109
- **Exactly one link, never two.** The banner offers the page the reader can
110
- actually use, and the engine has already decided which that is a served URL
111
- when it could serve, a published one only when it could not. Handing over both
112
- asks the reader to know which door the run is standing behind, and they cannot:
113
- it was done, and a verdict was pressed on the published page three times while
114
- each one sat unread under a line claiming the run was waiting. Two links is not
115
- a convenience with a caveat; it is the caveat existing at all.
102
+ A rule, a heading naming the state and the size, **the stack**, the `live:`
103
+ line, one line saying the run is stopped and which tiers it covers, and the two
104
+ exits. Nothing else, and never a paragraph explaining it.
105
+
106
+ **The `live:` line, and why a rigid contract took a second amendment.** Reading
107
+ a diff is not the only way to judge a change often the question is whether it
108
+ *works* and the answer to that lives at the running URL. A reader deciding
109
+ whether to open the page at all wants to know whether it is already running
110
+ somewhere, and finding that out any other way is a trip to a terminal in the
111
+ middle of a review.
112
+
113
+ **Each line names a command a person types**, rather than describing a
114
+ capability. `live: off — the page can put it live` told the reader that a page
115
+ somewhere could do it and left them to find the verb — and the verb is what they
116
+ need when they are in a terminal rather than on the page. So the lines say
117
+ `/spec-live`, `/spec-live main`, `/spec-remote-review`. Making them *clickable*
118
+ was considered and rejected: the only clickable thing markdown has is a URL, and
119
+ a URL that acts when it is **fetched** is one a link previewer or a prefetcher
120
+ fires with nobody involved.
121
+
122
+ It is the engine's line, copied like the stack: `spec-env review` prints it and
123
+ `--json` carries the same answer as `live`, from one function, so the report and
124
+ the command cannot disagree. Four states, and only three of them are a line —
125
+ `on` (with the URL when the project configured one), `off`, `held` (naming the
126
+ spec holding the workbench). The fourth, `unavailable`,
127
+ **prints nothing at all**:
128
+ a project with no isolation and a spec with no worktree both land there,
129
+ and a line about a surface that does not exist is an accusation against a
130
+ healthy repo.
131
+
132
+ **This is the second amendment to this section in two specs**, which is worth
133
+ naming rather than letting pass. The contract earns its rigidity from real
134
+ failures, and two amendments in a row is how a rigid contract quietly becomes a
135
+ negotiable one. Both have carried their reason and both replaced a line rather
136
+ than adding a paragraph.
137
+ **A third should have to argue harder than either did.**
138
+
139
+ **The stack is the engine's, copied rather than composed.** `spec-env review`
140
+ prints one line per tier and `--json` carries the same thing as `tiers`, so take
141
+ it from there in the order it comes — and do not decide which tiers are worth
142
+ mentioning. The whole reason the stack exists is that the engine was being asked
143
+ to guess where the reader was sitting, and it guessed wrong three separate ways
144
+ in one day; a skill re-deciding that in prose is the same guess one layer up.
145
+
146
+ **Three tiers, always all three, in this order.**
147
+
148
+ | Tier | Is | Off when |
149
+ |------|----|----------|
150
+ | `local` | the loopback URL — the machine the run is on | never. With nothing served it is the `file://` page, and says it cannot send a verdict |
151
+ | `network` | the LAN URL — a phone on the same wifi | `review.allowNetwork` is false, or this machine has no network address |
152
+ | `remote` | a published page, for a reader off the network | `review.allowRemote` is false, or nothing has been published |
153
+
154
+ A tier that is off **keeps its line** and names the one command that turns it
155
+ on. Dropping the line is what left a reader who had walked out of the house with
156
+ nothing on screen to tell them a remote page was even possible — they could not
157
+ ask for what they could not see.
158
+
159
+ **Only promise a wait the transport can deliver.** The wait itself is real in
160
+ every case; what varies is what carries it back, not whether the run is
161
+ stopped. Three transports, three true sentences:
162
+
163
+ - **A page the engine SERVES** hands its pass to the local store, and
164
+ `spec-env review wait` returns when it lands. The run is woken by the press:
165
+ *"I'm holding here until you send a verdict."*
166
+
167
+ **That sentence is only true for as long as the wait is actually running**, so
168
+ the wait is the engine's and never one composed for the occasion. A watcher
169
+ written per run is proven by nothing: one was written
170
+ `until [ -f "$P" ] && [ "$x" \> "$y" ]` — valid bash, a syntax error in zsh,
171
+ a predicate that could never be true — and it spun for five minutes while the
172
+ report underneath it said the run was holding.
173
+ **Silence is what made that expensive**:
174
+ a wait that cannot fire and one patiently working look identical,
175
+ so nothing seemed wrong until the reader asked. Give the wait no timeout
176
+ either; it lasts as long as the session, because a reader who walks away from
177
+ a diff is the normal case and a bounded watch once lost a verdict to a lunch
178
+ break.
179
+ - **A `file://` page** has no server to POST to, so the pass is copied and
180
+ pasted — the reader's next message is what carries it. The run is stopped just
181
+ the same, and says so:
182
+ *"I'm holding here — paste the pass when you have it."*
183
+ This is the same wait, carried by the conversation instead of a watch. It is
184
+ not a lesser one, and it is not an excuse to ask without waiting.
185
+ - **A PUBLISHED page** writes to the artifact's own store, and nothing pushes
186
+ from there into the conversation. That is the one case where a verdict is
187
+ genuinely invisible until someone asks for it, so its line in the stack says
188
+ that rather than letting the holding line speak for it:
189
+
190
+ - **remote** — [https://…](https://…) · a verdict here needs `/spec-reviewed`
191
+
192
+ Where `remote` is the only tier a reader can reach, the holding line says so
193
+ too: *press a verdict, then type `/spec-reviewed` — I cannot see it until you
194
+ do.* Where a served tier is reachable as well, the holding line keeps naming
195
+ the tiers the wait covers, and `remote`'s own caveat sits on `remote`'s line
196
+ where it belongs.
197
+
198
+ **A harness with no file-watch does not get an exemption.** It once read
199
+ "change nothing" — keep the row, keep the question, do not wait — and that
200
+ escape hatch is what let a whole class of runs go on asking questions nobody was
201
+ listening for. Where a watch is unavailable the wait is the turn ending, which
202
+ every harness can do.
203
+
204
+ **The `Continue` ending, and why the banner's exits differ mid-run.** A page
205
+ rendered part-way through a run offers `Continue` — *I have read it, carry on* —
206
+ in place of the committing verdicts, because "commit" is the wrong verb for
207
+ unfinished work. Its banner names the same state and the same stack, and
208
+ different exits:
209
+
210
+ …the stack, unchanged…
211
+
212
+ I'm holding here until you send a verdict — the wait covers local and network.
213
+
214
+ `Continue` carries on · `Request changes` works them now
215
+
216
+ The stack is written out once, above, and referred to everywhere else. Two
217
+ copies of it is how the two come to disagree about which tiers exist.
218
+
219
+ **It is not the `none` verdict that was removed.** `none` recorded itself and
220
+ did nothing, which is the record-and-do-nothing ending this contract exists
221
+ against. `Continue` **resumes the run** — it names an action, which is the bar
222
+ every verdict has to clear. What it does not do is commit, and it can never
223
+ clear the gate a finished phase armed: that gate is discharged by a committing
224
+ verdict or a recorded skip, and by nothing else.
225
+
226
+ **So waiting and arming are separate**, and only one of them is about this
227
+ block. *Waiting* is what any offer does, and it is what this section governs.
228
+ *Arming* asserts an obligation that outlives the turn, and belongs only to work
229
+ that is finished. A mid-run render waits without arming — walking away from it
230
+ costs nothing, which is exactly why `Continue` is safe to offer there.
231
+
232
+ **One link per reachable store, each labelled.** This amends
233
+ *"exactly one link, never two"*, and the evidence that rule was written on is
234
+ the reason it still reads the way it does. Two links were handed over, the wait
235
+ stood behind only one of them, and a verdict was pressed on the published page
236
+ three times while each one sat unread under a line claiming the run was waiting.
237
+ Two links was not a convenience with a caveat; the caveat existing was the bug.
238
+
239
+ **What that evidence is about is the store, not the count.** A served page hands
240
+ its pass to the local pending store, which `spec-env review wait` watches; a
241
+ published page writes to the artifact's own store, which nothing here can see.
242
+ Two links into two stores asks the reader to know which door the run is standing
243
+ behind, and they cannot.
244
+
245
+ **Local and network are two doors into one room.** The page POSTs with
246
+ `fetch(location.pathname, …)`, so a page opened at `127.0.0.1:7760/…` and one
247
+ opened at `192.168.0.136:7760/…` reach the same server process and the same
248
+ pending store. One wait covers both, and a verdict pressed on either wakes the
249
+ same run. Between those two the objection does not apply — which is why both are
250
+ offered, and why offering them gives nothing up.
251
+
252
+ **So two things are required, and the second is what the old rule lacked.**
253
+ Every link is **labelled** with its tier, and one line says
254
+ **which tiers the wait covers**.
255
+ Two *unlabelled* links stay forbidden exactly as before, because
256
+ the reader still cannot tell them apart.
257
+
258
+ **`remote` keeps the caveat it always had.** It is listed so a reader off the
259
+ network knows the surface exists and what turns it on — never as a door the wait
260
+ is claimed to cover. Where it is on and published, its line says what is true of
261
+ it: *a verdict here needs `/spec-reviewed`*.
262
+
263
+ **Still forbidden**, in the words of the rule this amends: offering two links
264
+ into **different** stores without saying which one the wait is watching. That is
265
+ the failure that cost three unread verdicts, and nothing here touches it.
116
266
 
117
267
  **This is not the old failure returning**, and the difference is the whole
118
268
  justification. The offer used to be two quoted lines in the tail of a long
@@ -124,11 +274,34 @@ stopped until they answer. A row cannot carry that, because a row is scanned at
124
274
  the same weight as every other row. So the state gets the loudest shape on
125
275
  screen, at the end, where reading finishes.
126
276
 
127
- **Where the run is not waiting, it stays a `Review` row.** A render nobody is
128
- being held for a mid-phase `/spec-diff`, a page produced alongside other work
129
- is a convenience, and giving it a banner teaches the reader to scroll past
130
- banners. The shape marks the difference between *here is a page* and *nothing
131
- proceeds until you answer*.
277
+ **ASKING IMPLIES WAITING one rule, and the two shapes follow from it.** Any
278
+ render that asks the reader for a verdict ends the turn watching for one. A run
279
+ that does not intend to wait does not ask: its `Review` row keeps the counts and
280
+ the link and **loses the question**.
281
+
282
+ So there are two shapes and no third:
283
+
284
+ - **Asking** → the banner above, and the run waits.
285
+ - **Not asking** → the `Review` row: counts and link, no question.
286
+
287
+ **What this guards against is a reader taught that the button is decorative.**
288
+ `/spec-bug` and `/spec-hotfix` used to render the page, emit a row
289
+ asking *"want a written review before you commit?"*, and finish — with nothing
290
+ watching. A verdict pressed on that page landed in the holding area and stayed
291
+ there, because the run had told the reader the page was **ready** rather than
292
+ that it was **waiting**. It happened twice on one spec, and the second press
293
+ existed only because the first appeared to do nothing. An offer that cannot be
294
+ answered costs more than never having offered.
295
+
296
+ **Rejected: keeping the question and adding a caveat** — *"press a verdict, but
297
+ I will not see it until you ask"*. That is exactly the shape that stranded those
298
+ two passes, and a truthful caveat does not make an unanswerable question worth
299
+ asking. Either watch for the answer, or do not ask.
300
+
301
+ **A render nobody is being held for is still worth reporting** — a mid-phase
302
+ `/spec-diff`, a page produced alongside other work. It gets the row, because
303
+ giving it a banner teaches the reader to scroll past banners. The shape marks
304
+ the difference between *here is a page* and *nothing proceeds until you answer*.
132
305
 
133
306
  **Never fence a message to the reader.** A fenced block is for a command to
134
307
  run, code, or engine output quoted verbatim — things the reader copies or
@@ -185,7 +358,7 @@ behind it — say nothing rather than reporting that there was nothing to report
185
358
  | `Landed` | A fast-forward, a tag, a cherry-pick. |
186
359
  | `Worktree` | A worktree provisioned, entered, or torn down. |
187
360
  | `Untouched` | Uncommitted work the run deliberately left alone — whose, and how much. |
188
- | `Review` | The rendered diff page: files, `+`/`−`, the linkand the offer of a written review, in the same row. **Omitted entirely when the run is waiting on a verdict**: the banner after the block carries it instead, and a row saying the same thing beside it splits the reader's attention across two places. |
361
+ | `Review` | The rendered diff page: files, `+`/`−`, and the stackthe same tiers the banner lists, labelled, run together with `·` because a row is one cell, plus `live` where the engine reported one. **No question** — a row cannot be waited on, so a question in one is unanswerable by construction (see *asking implies waiting*). **Omitted entirely when the run is waiting on a verdict**: the banner after the block carries it instead, and a row saying the same thing beside it splits the reader's attention across two places. |
189
362
  | `Follow-ups` | **Always.** `none`, or one line each. |
190
363
  | `Next` | **Last.** The single next action for this work — runnable from the state the run leaves behind. |
191
364
 
@@ -204,11 +377,17 @@ silent, because "I provisioned, and your four files are still sitting there" is
204
377
  a different sentence from "I provisioned".
205
378
 
206
379
  **`Review` is one row, not two** — and where the run is waiting, it is no rows
207
- at all. The page and the offer to read it are the same subject, and splitting
208
- them made the reader resolve a distinction before acting on either. A waiting
209
- run promotes the whole subject into the banner; keeping the row as well would
380
+ at all. The page and what to do with it are the same subject, and splitting them
381
+ made the reader resolve a distinction before acting on either. A waiting run
382
+ promotes the whole subject into the banner; keeping the row as well would
210
383
  recreate that split with the two halves further apart than ever.
211
384
 
385
+ **The row carries no question, and that is the rule above applied here.** It
386
+ once ended *"want a written review before you commit?"* — a question addressed
387
+ to a reader the run was not waiting for. Putting it in a findable row fixed
388
+ where it was; it did not fix that nothing was listening. A run that wants that
389
+ question answered waits for it, and waiting means the banner.
390
+
212
391
  ## The four verdicts
213
392
 
214
393
  | State | Means |
@@ -105,11 +105,40 @@ it happens differs (`/spec` writes the spec in Phase B, `/spec-bug` in its step
105
105
  description (an **update** to the existing issue, plus a sub-issue per phase).
106
106
  Recording a snapshot here would declare the mirror already in sync and strand
107
107
  the issue showing the raw report forever.
108
+ - **Preserve the original, before anything replaces it.** Run this the moment
109
+ the spec file exists and **before the linking push**:
110
+
111
+ ```
112
+ skitterspec spec-sync preserve <spec>
113
+ ```
114
+
115
+ It posts the issue's current description onto the issue as a comment — the one
116
+ surface one-way sync never touches, so no push can clobber it. On the API path
117
+ it reads the description itself; without a key it prints the body and the
118
+ marker, and you post it with the Linear comment tool after checking the
119
+ comments for that marker.
120
+
121
+ **The ordering is the whole correctness condition.** Run before the push, this
122
+ keeps what the reporter filed. Run after it, it would keep the generated spec
123
+ and report success — which is why it is a step of its own here rather than a
124
+ line in the push sequence. The engine warns when it can tell it is late, but it
125
+ cannot always tell.
126
+
127
+ **It never fails the adoption.** A project that set
128
+ `intake.preserveOriginal: false`, an unreachable Linear, an issue with no
129
+ description, a comment Linear refused — every one of them exits 0 and says so.
130
+ Relay what it printed and carry on.
108
131
  - **Say what will happen** in the finish-up message. The linking step runs right
109
132
  after the spec is written, so the issue's description is replaced by the spec
110
- **then** — not on some later manual push. The reporter's words are not lost:
111
- they are quoted in the spec's **Problem** (or **Symptom**) section, and Linear
112
- keeps the original in the issue's history.
133
+ **then** — not on some later manual push. Say both halves: the description
134
+ becomes the spec, **and** the original is preserved as a comment on the same
135
+ issue, so the reporter can still read what they wrote.
136
+
137
+ **Do not send anyone to the issue's history for it.** This used to say Linear
138
+ keeps the original there, and that is true and useless: history is a diff
139
+ viewer nobody opens, it is not quotable, and it degrades to "it is in there
140
+ somewhere". The comment is the answer, and the other half is the spec's own
141
+ **Problem** (or **Symptom**) section quoting the reporter.
113
142
 
114
143
  ## Phase A — reach a clear shared understanding (grill first)
115
144
 
@@ -339,6 +368,104 @@ open questions, `Draft` when you deliberately left some unresolved. Either way
339
368
  the next step is `/spec-start`. The Report section below is where all of that
340
369
  reaches the user — do not narrate it here as well.
341
370
 
371
+ ## Phase C2 — render the spec, then wait for the verdict
372
+
373
+ **Only when the project has per-spec isolation** (`specs/.core/env.config.json`
374
+ present). Without it there is no page and this phase does not exist — skip it in
375
+ silence rather than explaining an absence.
376
+
377
+ A spec is the one artefact whose whole purpose is to be
378
+ **read before work begins**, and until now it was the only one with no reading
379
+ surface: `/spec`
380
+ finished, the spec sat uncommitted, and `/spec-start` had to point that out and
381
+ commit it as a side effect of provisioning. So this phase ends where every other
382
+ phase of the lifecycle ends — on a page, in a verdict.
383
+
384
+ ```
385
+ skitterspec spec-env review <spec> --docs --buttons authoring
386
+ ```
387
+
388
+ `--docs` reads the spec's own documents from the tree you are standing in, so it
389
+ never wants a worktree — a backlog spec has none. It renders
390
+ **this spec's documents and nobody else's**: several specs are routinely
391
+ authored in one
392
+ checkout, and the file set comes from `spec-env stage`'s `owned` half precisely
393
+ so a colleague's spec cannot land on your page and then be committed under your
394
+ verdict.
395
+
396
+ **This is free.** The engine reads git and splices text into a template; the
397
+ diff never passes through you.
398
+
399
+ **Render nothing when nothing was written.** A `⏸` run — grilling that never
400
+ reached a shared understanding — has no spec to show, and must not ask for a
401
+ verdict on one.
402
+
403
+ **Arm nothing.** There is no `spec-env review arm` in this path, and that is a
404
+ decision rather than an omission: the gate asserts that *a phase which ended*
405
+ owes an answer, and a backlog spec owes no phase. Walking away from this page
406
+ costs nothing — the spec is simply still uncommitted, which `/spec-start` already
407
+ handles. A `/spec` run must leave `spec-env review gate --check` exiting 0.
408
+
409
+ ### The two endings this page offers
410
+
411
+ `--buttons authoring` offers `Commit & Start` and `Commit` in place of the
412
+ committing pair, because "commit and build the next phase" is meaningless for a
413
+ spec with no phase in flight.
414
+
415
+ - **`commit-start`** — hand off to `review.commitWith` (`/commit` by default),
416
+ passing the pathspec the render reported on `docs.paths`, then run
417
+ **`/spec-start <name>`** and **stop there**. It never completes, lands or tears
418
+ anything down, exactly as `commit-continue` stops after `/spec-next`. This is
419
+ the `commit && /spec-start` that was typed by hand.
420
+ - **`commit`** — the same commit, then finish. The spec stays `Ready` in
421
+ `backlog`, ready for a `/spec-start` whenever it is wanted.
422
+ - **`changes`** — work the notes into the spec, record a resolution for each one
423
+ so the next render shows it struck through with what changed, then
424
+ **re-render, and wait again**. The reader is still holding a decision, so
425
+ ending the turn and
426
+ making them type `/spec-diff` would reopen the loop this exists to close.
427
+ - **`discuss`** — report and talk. Claim nothing, change nothing.
428
+
429
+ **Take the pathspec from the render, not from the tree.** A checkout is shared,
430
+ so the set of this spec's uncommitted documents can differ between the render and
431
+ the verdict — and the reader's conclusion is about what the page showed them.
432
+
433
+ ### Then wait for it
434
+
435
+ **The wait is a command. Do not write one.**
436
+
437
+ 1. **Note the moment you start waiting**, as an ISO timestamp. That instant is
438
+ the whole scope of what you may claim.
439
+ 2. **Run the engine's wait in the background, and end your turn:**
440
+
441
+ ```
442
+ skitterspec spec-env review wait <spec> --since <the timestamp>
443
+ ```
444
+
445
+ It takes no timeout unless you pass one, and you must not pass one: a reader
446
+ who walks away from a spec is the normal case, and a bounded watch once lost a
447
+ verdict to a lunch break.
448
+ 3. **On waking, let the engine pick:**
449
+
450
+ ```
451
+ skitterspec spec-env review <spec> --docs --claim-since <the timestamp> --json
452
+ ```
453
+
454
+ It claims the one pass that arrived inside the window, acts on nothing when
455
+ none did, and refuses to choose when two did.
456
+ 4. **Route on the verdict** as above.
457
+
458
+ **Where the page is `file://`** — the engine could not serve — there is no server
459
+ to POST to, so the pass is copied and pasted and the reader's next message is
460
+ what carries it. **Do not start a watch that cannot fire**: a wait on the pending
461
+ store would spin forever while the report underneath it claimed to be holding,
462
+ which is the exact failure `.claude/rules/spec-reports.md` records. The wait is
463
+ the turn ending, and the banner says so.
464
+
465
+ Relay the engine's **stack** — the `local:`, `network:` and `remote:` lines, all
466
+ three, in that order — never the bare `page:` path. `.claude/rules/spec-reports.md`
467
+ carries the shape and why every tier is named rather than one being chosen.
468
+
342
469
  ## Phase D — record the isolation stack (only if configured)
343
470
 
344
471
  **Only when `specs/.core/env.config.json` exists** (per-spec isolation is
@@ -468,9 +595,39 @@ the shape; this section carries only what is specific here.
468
595
  **Fields:** `Tracker` · `Spec` · `Built` · `Follow-ups` · `Next`
469
596
 
470
597
  `Built` is the spec's path and phase count; `Spec` is its status and bucket;
471
- `Next` is `/spec-start <name>`, with the name spelled the way it must be typed.
472
598
  `Tracker` appears only when a provider is installed and linked it.
473
599
 
600
+ **`Next` depends on whether Phase C2 rendered a page.** Where it did, the run is
601
+ waiting on a verdict, so the block **omits the `Review` row** and ends on the
602
+ banner defined in `.claude/rules/spec-reports.md` — counts, the tier stack, and
603
+ this page's two exits:
604
+
605
+ ---
606
+
607
+ ## ⏸ Review ready — &lt;N&gt; files, +&lt;a&gt; −&lt;d&gt;
608
+
609
+ - **local** — &lt;the `local:` URL&gt;
610
+ - **network** — &lt;the `network:` URL, or off with the command that turns it on&gt;
611
+ - **remote** — &lt;the `remote:` URL, or off with the command that turns it on&gt;
612
+
613
+ I'm holding here until you send a verdict — the wait covers local and network.
614
+
615
+ `Commit & Start` puts it in flight · `Commit` keeps it for later
616
+
617
+ ---
618
+
619
+ There `Next` names the page rather than a command, because the button is what
620
+ carries the work on.
621
+
622
+ Where no page was rendered — isolation is not configured, or nothing was
623
+ written — `Next` is `/spec-start <name>`, with the name spelled the way it must
624
+ be typed.
625
+
626
+ **A spec written without a page is still uncommitted**, so that `Next` relies on
627
+ `/spec-start` committing the spec when it is all that is uncommitted. Say
628
+ `/commit, then /spec-start <name>` when anything else is uncommitted too, or the
629
+ row sends the reader to a refusal.
630
+
474
631
  **`Follow-ups` is almost always `none` here.** This skill's whole job is to
475
632
  capture work, so anything it surfaced belongs in the spec it just wrote rather
476
633
  than in a follow-up line beneath it.