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