@skitterbyte/skitterspec-jira 0.0.0-stage → 2.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 (113) hide show
  1. package/MIGRATION.md +1266 -0
  2. package/README.md +44 -2
  3. package/assets/claude-md-section.md +85 -0
  4. package/assets/commands/allow-main.md +33 -0
  5. package/assets/commands/spec-connect.md +13 -0
  6. package/assets/commands/spec-live.md +14 -0
  7. package/assets/commands/spec-remote-review.md +37 -0
  8. package/assets/commands/spec-skip.md +24 -0
  9. package/assets/core/SETUP.md +192 -0
  10. package/assets/core/env.config.json.example +56 -0
  11. package/assets/core/env.config.md +606 -0
  12. package/assets/core/gating.config.json.example +4 -0
  13. package/assets/core/gating.config.md +81 -0
  14. package/assets/core/jira.config.json.example +18 -0
  15. package/assets/core/jira.config.md +88 -0
  16. package/assets/hooks/main-guard.cjs +162 -0
  17. package/assets/hooks/review-gate.cjs +146 -0
  18. package/assets/review/page.html +3384 -0
  19. package/assets/rules/commit-trailers.md +96 -0
  20. package/assets/rules/negative-checks.md +69 -0
  21. package/assets/rules/offered-unblocks.md +100 -0
  22. package/assets/rules/spec-planning.md +702 -0
  23. package/assets/rules/spec-reports.md +572 -0
  24. package/assets/skills/no-spec/SKILL.md +249 -0
  25. package/assets/skills/spec/SKILL.md +714 -0
  26. package/assets/skills/spec-bug/SKILL.md +585 -0
  27. package/assets/skills/spec-cancel/SKILL.md +261 -0
  28. package/assets/skills/spec-complete/SKILL.md +451 -0
  29. package/assets/skills/spec-diff/SKILL.md +870 -0
  30. package/assets/skills/spec-hotfix/SKILL.md +607 -0
  31. package/assets/skills/spec-init/SKILL.md +178 -0
  32. package/assets/skills/spec-jira-setup/SKILL.md +221 -0
  33. package/assets/skills/spec-list/SKILL.md +173 -0
  34. package/assets/skills/spec-next/SKILL.md +678 -0
  35. package/assets/skills/spec-push/SKILL.md +306 -0
  36. package/assets/skills/spec-review/SKILL.md +256 -0
  37. package/assets/skills/spec-reviewed/SKILL.md +284 -0
  38. package/assets/skills/spec-start/SKILL.md +496 -0
  39. package/assets/skills/spec-status/SKILL.md +114 -0
  40. package/assets/skills/spec-sync/SKILL.md +220 -0
  41. package/assets/skills/spec-to-main/SKILL.md +141 -0
  42. package/bin/skitterspec-jira.js +68 -0
  43. package/package.json +40 -4
  44. package/src/cli.js +5967 -0
  45. package/src/deprecate.js +138 -0
  46. package/src/env/bootstrap.js +188 -0
  47. package/src/env/building.js +143 -0
  48. package/src/env/classify.js +176 -0
  49. package/src/env/commitcmd.js +108 -0
  50. package/src/env/config.js +850 -0
  51. package/src/env/dev.js +63 -0
  52. package/src/env/glob.js +34 -0
  53. package/src/env/hooks.js +226 -0
  54. package/src/env/hotfix.js +413 -0
  55. package/src/env/hotfixland.js +302 -0
  56. package/src/env/integrate.js +167 -0
  57. package/src/env/live.js +479 -0
  58. package/src/env/mainguard.js +299 -0
  59. package/src/env/provision.js +466 -0
  60. package/src/env/proxy.js +225 -0
  61. package/src/env/prune.js +119 -0
  62. package/src/env/registry.js +159 -0
  63. package/src/env/render.js +17 -0
  64. package/src/env/resolve.js +716 -0
  65. package/src/env/review.js +2622 -0
  66. package/src/env/reviewers/coderabbit.js +228 -0
  67. package/src/env/reviewers.js +517 -0
  68. package/src/env/serve.js +1044 -0
  69. package/src/env/supervise.js +157 -0
  70. package/src/env/teardown.js +248 -0
  71. package/src/env/trust.js +87 -0
  72. package/src/gating.js +155 -0
  73. package/src/init.js +1150 -0
  74. package/src/lines-diff.js +114 -0
  75. package/src/prompts.js +129 -0
  76. package/src/vendor/jira/adf.js +414 -0
  77. package/src/vendor/jira/api.js +295 -0
  78. package/src/vendor/jira/cli-sync.js +1252 -0
  79. package/src/vendor/jira/commands.js +44 -0
  80. package/src/vendor/jira/config.js +286 -0
  81. package/src/vendor/jira/credentials.js +108 -0
  82. package/src/vendor/jira/dialect.js +45 -0
  83. package/src/vendor/jira/mcp.js +129 -0
  84. package/src/vendor/provider-kit/index.js +41 -0
  85. package/src/vendor/provider-kit/src/adapter.js +23 -0
  86. package/src/vendor/provider-kit/src/apply.js +207 -0
  87. package/src/vendor/provider-kit/src/args.js +68 -0
  88. package/src/vendor/provider-kit/src/assignment.js +277 -0
  89. package/src/vendor/provider-kit/src/config.js +251 -0
  90. package/src/vendor/provider-kit/src/credentials.js +404 -0
  91. package/src/vendor/provider-kit/src/doctor.js +35 -0
  92. package/src/vendor/provider-kit/src/http.js +63 -0
  93. package/src/vendor/provider-kit/src/identity.js +93 -0
  94. package/src/vendor/provider-kit/src/list-assignee.js +71 -0
  95. package/src/vendor/provider-kit/src/list-rows.js +35 -0
  96. package/src/vendor/provider-kit/src/md-compare.js +39 -0
  97. package/src/vendor/provider-kit/src/md-tables.js +94 -0
  98. package/src/vendor/provider-kit/src/prompt.js +50 -0
  99. package/src/vendor/provider-kit/src/released.js +219 -0
  100. package/src/vendor/provider-kit/src/specs.js +248 -0
  101. package/src/vendor/provider-kit/src/transport.js +22 -0
  102. package/src/vendor/sync-core/index.js +92 -0
  103. package/src/vendor/sync-core/src/base.js +81 -0
  104. package/src/vendor/sync-core/src/compare.js +338 -0
  105. package/src/vendor/sync-core/src/dialect.js +51 -0
  106. package/src/vendor/sync-core/src/fields.js +36 -0
  107. package/src/vendor/sync-core/src/legacy.js +100 -0
  108. package/src/vendor/sync-core/src/normalize.js +971 -0
  109. package/src/vendor/sync-core/src/push.js +85 -0
  110. package/src/vendor/sync-core/src/retarget.js +278 -0
  111. package/src/vendor/sync-core/src/task-block.js +308 -0
  112. package/src/vendor/sync-core/src/verify.js +67 -0
  113. package/src/vendor/sync-core/src/write.js +191 -0
@@ -0,0 +1,870 @@
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, and takes --reviewers to run the project's configured code reviewers. Use when the user says "/spec-diff", "show me the diff", "what did this phase change", "review this spec's work", "run the reviewers", or hands back what the review page produced — a six-digit claim code, a bare verdict word, 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 three ways, and **all three 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 verdict word** — `commit`, `commit-continue`, `continue`, `changes` or
50
+ `discuss`. A `file://` page cannot send anything, so where the reader marked
51
+ nothing it copies a command carrying the conclusion on its own. Send it with
52
+ `skitterspec spec-env review <spec> --verdict <word>`; it joins the same merge
53
+ a claimed pass does, so everything below is unchanged.
54
+ - **A pasted JSON blob** — the same `file://` page, once the reader has marked
55
+ something up: accepts and notes do not fit on a command line, so the whole
56
+ pass travels. Not legacy: it is the whole story for a local reader.
57
+
58
+ Either way, **this is not a request to render anything**: it is a review coming
59
+ back, and these steps replace §3–§5 below.
60
+
61
+ 0. **Never claim a pass you were not asked to claim.** And never go looking
62
+ for one.
63
+
64
+ A device that reaches the page can POST all day. What decides whether one of
65
+ those passes reaches the operator's review is this rule, and — since the wait
66
+ in §4b — two mechanical facts beside it: the **serve token**, 48 unguessable
67
+ bits minted per server, which decides who can POST at all; and the wait
68
+ **window**, which decides which pass a watch may claim. There is exactly one
69
+ way a pass is taken without a person naming it, `--claim-since`, it is
70
+ described in §4b, and it acts on nothing unless precisely one pass arrived
71
+ while this session was waiting for it.
72
+
73
+ Everything else here is unchanged. A pass that was already sitting there when
74
+ you arrived is never yours to take.
75
+
76
+ **Do not read the code out of `.spec-env/reviews/<spec>.pending.json`.** The
77
+ store is a file you can open, so nothing stops you; that is precisely why the
78
+ rule has to be stated rather than assumed. It has already happened once — an
79
+ agent found a waiting approval, read its code off disk, claimed it, and
80
+ reported the round-trip working. The operator had pressed the button, so the
81
+ outcome was harmless and the reasoning was wrong.
82
+
83
+ The render tells you everything a decision needs, so there is nothing to go
84
+ looking for:
85
+
86
+ ```
87
+ pending: 1 waiting
88
+ 792969 · commit · 1 min ago
89
+ ```
90
+
91
+ **Offer it, naming the code**, and wait: *"an approval is waiting, code
92
+ 792969, sent a minute ago — does that match your phone?"* The digits travel
93
+ **to** the operator, not from them — they verify rather than transcribe, and
94
+ the code is the only part they can check, so
95
+ **name it rather than describing the pass** (a stranger's approval and theirs
96
+ read identically otherwise).
97
+
98
+ **Two or more waiting is a refusal to guess.** Name them all and ask which.
99
+ Never take the newest, the oldest, or the only `commit` — that is exactly
100
+ the case where someone else's pass is sitting beside the operator's, and it
101
+ is the only case where reading six digits out is worth anyone's time.
102
+
103
+ **On "that isn't mine"**, leave it and offer to drop it:
104
+
105
+ ```
106
+ skitterspec spec-env review <spec> --drop <code>
107
+ ```
108
+
109
+ A pass that stays is reported on every render until the operator stops
110
+ reading the line — which is how the real one gets missed.
111
+
112
+ 1. **Take it in through the engine**, whichever way it arrived.
113
+
114
+ **A code** — claim it, once the operator has confirmed it is theirs (step 0):
115
+
116
+ ```
117
+ skitterspec spec-env review <spec> --claim <code>
118
+ ```
119
+
120
+ **A pasted blob** — write the JSON to a scratch file
121
+ verbatim — never retype it, never "tidy" it — and merge it:
122
+
123
+ ```
124
+ skitterspec spec-env review <spec> --notes <file>
125
+ ```
126
+
127
+ Both validate wholesale and write nothing if the blob is malformed or names a
128
+ different spec; a claim additionally refuses a code that matches nothing, and
129
+ **names nothing when it does** — listing the waiting codes would hand a
130
+ guesser the answer. **Relay a refusal as it is written** and stop; every
131
+ message says which entry was wrong, so there is nothing to guess.
132
+
133
+ **A claim is a delivery mechanism, not a second kind of review.** Everything
134
+ below reads the same merged pass and the same verdict; nothing may behave
135
+ differently because of how it arrived.
136
+
137
+ 2. **Read the verdict the engine judged.** The pass says what it CONCLUDED, and
138
+ the engine prints it — `committing with <skill>`, `committing with <skill>,
139
+ then the next phase`, `changes requested`, `discuss first`, or
140
+ `commit refused — <reason>`. It is judged, not taken on trust: a committing
141
+ verdict arriving with an open comment is refused and routed to discuss,
142
+ because you asked for something and it therefore cannot also be fine.
143
+ **Never re-judge it yourself, and never count anything** — read the engine's
144
+ answer and route on it.
145
+
146
+ Three routes, and every pass takes exactly one:
147
+
148
+ | Verdict | What it means | Where to go |
149
+ |---------|---------------|-------------|
150
+ | `commit` (honoured) | this is fine, commit it | §2a — commit, then stop |
151
+ | `commit-continue` (honoured) | this is fine, keep going | §2a — commit, then `/spec-next` |
152
+ | `changes` | do these, now | step 4 — **skip the wait**, this is the go-ahead |
153
+ | `discuss` | I have a question | step 3 — report, then **ask what's up** |
154
+
155
+ **A refused commit and a pass with no verdict both mean `discuss`.**
156
+ Neither is a special case: the engine routes the refusal there itself, and an
157
+ absent verdict has always meant "report it and wait" — which is why that is
158
+ what it still means.
159
+
160
+ **A pass may carry an `action:` instead**, and then none of the above
161
+ applies: it concluded nothing. Go to **§2b**. The engine refuses a pass
162
+ carrying both, so there is never a choice to make between them.
163
+
164
+ **It is on the `claimed:` line** — `· action: live-on` at the end of it, and
165
+ `claimed.action` under `--json`. Read it there rather than inferring one
166
+ from a pass that merged nothing: a claim reporting no verdict and no counts
167
+ used to be the only trace a press left, which made every action look like a
168
+ reader who had concluded nothing. If the line carries no action, there was
169
+ none.
170
+
171
+ 3. **Say what you read, then ask what's up.** Report the accepted count, then
172
+ each open comment as `file:line — note`, then the files you would touch.
173
+ **Wait — unless the verdict already said otherwise.** Pasting on its own is
174
+ not a go-ahead: this skill is read-only everywhere else, a misread comment
175
+ costs a revert, and the operator may only have wanted it recorded. A `changes`
176
+ verdict **is** that go-ahead, given deliberately on the page, so asking again
177
+ is asking someone to decide twice. The reasoning is unchanged; what changed is
178
+ that the page can now answer it in advance.
179
+
180
+ **`discuss` means "ask me what's up"** — an opening move, not a stopping
181
+ place. A reader who pressed it has a question, and a summary that ends in
182
+ silence leaves them to ask it themselves. Report what you read and then
183
+ **open the conversation**: name what you would do next and ask whether that
184
+ is what they wanted. The same wording has to work for a pass that chose
185
+ nothing at all, since an absent verdict means this too — so ask about the
186
+ review, never about the button.
187
+
188
+ 4. **On the go-ahead, work only the commented files.** Read those; do **not**
189
+ open the accepted ones. That is the whole saving the marks buy, and it is
190
+ only worth anything if it is true — so say plainly which files you did not
191
+ open. Make the changes, then run the project's typecheck and test commands.
192
+
193
+ **First, compare the worktree against where you are standing.** This is the
194
+ one step here that writes, and §1's first rule resolves by **name** — so
195
+ editing a tree this session is not standing in is the ordinary case, not an
196
+ edge one. Take the `worktree:` line from
197
+ `skitterspec spec-env resolve <spec>` and compare it with cwd, resolving both
198
+ paths first so a symlinked or trailing-slash spelling of one tree does not
199
+ read as two. Same tree, and everything below is inert.
200
+
201
+ **Different trees, and the discipline applies.** Record the baseline before
202
+ the first edit:
203
+
204
+ ```
205
+ skitterspec spec-env resolve <spec> --record-primary
206
+ ```
207
+
208
+ Then every write takes an absolute path under the worktree and every command
209
+ is prefixed `cd "<worktreePath>" &&` — typecheck and tests included. A single
210
+ relative path lands the fix in the primary checkout, on the base branch, and
211
+ nothing about it looks wrong at the time.
212
+
213
+ **This is a write discipline, not a precondition.** It changes *how* this
214
+ step writes, never *whether* it runs — §3 below still holds in full, and a
215
+ later edit reading this as a gate would undo the rule it exists beside.
216
+
217
+ 5. **Write back what you did**, one entry per comment you acted on:
218
+
219
+ ```json
220
+ [{ "id": "2026-01-01T00:00:00.000Z-1", "note": "keyed the accept on the blob sha" }]
221
+ ```
222
+
223
+ ```
224
+ skitterspec spec-env review <spec> --resolve <file>
225
+ ```
226
+
227
+ The note is the load-bearing half: it is what lets the next read **verify**
228
+ the fix rather than trust it. An id that matches nothing is reported and
229
+ skipped, so one bad id never costs you the rest. Then re-render (§4) so the
230
+ page shows each note struck through with its account.
231
+
232
+ 6. **Before the re-render, prove nothing leaked** — only when step 4 found two
233
+ trees. Nothing should be reported fixed before it is known to be fixed in the
234
+ right one:
235
+
236
+ ```
237
+ skitterspec spec-env resolve <spec> --assert-primary-clean
238
+ ```
239
+
240
+ - **Exit 0, "primary checkout clean"** — carry on.
241
+ - **Non-zero** — stop and relay the engine's message unchanged. It names the
242
+ paths and both readings: this run wrote them and they belong in the
243
+ worktree, or something else did and the baseline wants re-recording.
244
+ **Do not guess which, and do not delete anything.** A path that appeared is
245
+ not proof of who put it there.
246
+ - **"cannot tell"** — no baseline, or one from another spec. It exits 0 and
247
+ claims nothing; say so in one line and carry on. An absence is not evidence.
248
+
249
+ WHAT WOULD FOOL THIS CHECK: it watches the **primary checkout** and nothing
250
+ else, so a fix written into *another* spec's worktree would leak there unseen.
251
+ Left unhandled deliberately — the cost of the gap is a missed leak, never a
252
+ false accusation.
253
+
254
+ **Never commit on a `changes` pass.** It authorises the work, not a commit —
255
+ only `commit` and `commit-continue` reach §2a. The fixes sit in the worktree
256
+ where the operator can read them on the next render, which is the whole point of
257
+ sending them back rather than approving.
258
+
259
+ ## 2a. A committing pass commits — through the project's own skill
260
+
261
+ Only on an **honoured** `commit` or `commit-continue`. A refused one did not
262
+ happen.
263
+
264
+ The engine names the skill to use on the verdict's `commitWith` — the
265
+ `review.commitWith` config key, `/commit` by default. Do not read the config
266
+ yourself; one answer, from the engine that owns it. **There is no off switch:**
267
+ `"none"` existed and was removed, because a verdict that records itself and does
268
+ nothing is the one thing a review page must not offer.
269
+
270
+ - **A skill you have** — invoke it, and say which one. **Never vendor it.**
271
+ `/commit` ships with **skittership**, a different package: it stages the
272
+ task's files, runs the project's checks, and writes the release-note footers
273
+ this repo's changelog is built from. A copy of it living here would be a fork
274
+ of someone else's skill that drifts silently.
275
+ - **A skill you do not have** — commit it yourself: stage only the files this
276
+ work touched, run the project's typecheck and test commands, and write a
277
+ conventional commit. **Say that you did, every time.** A commit made under
278
+ rules nobody configured must never be reported as one made under `/commit`.
279
+
280
+ **Decide availability from the skill list you already have**, never by testing
281
+ for a file. A skill can legitimately live in several places, so `.claude/skills/
282
+ commit/SKILL.md` missing is an absence that proves nothing
283
+ (`.claude/rules/negative-checks.md` rule 1) — and being wrong about it means
284
+ committing by hand while reporting a hand-off. You are told which skills you
285
+ have; that list is the answer.
286
+
287
+ **Let the commit's own failure be the answer.** If typecheck or the tests fail,
288
+ there is no commit — report the failure and stop. Do not fix the tests to get
289
+ the commit through, and do not commit around them.
290
+ **An approval judges the change; it never promises that it builds**, and the two
291
+ must not be conflated by a skill acting on someone's behalf.
292
+
293
+ **Then record what it produced**, so the page shows the outcome rather than the
294
+ intent:
295
+
296
+ ```
297
+ skitterspec spec-env review <spec> --outcome "committed <sha> via <skill|by hand>"
298
+ ```
299
+
300
+ That writes the outcome onto the decision the engine already logged and
301
+ re-renders the page, where it reads as history beneath the verdict bar. On a
302
+ failed commit there is no outcome to record — say what failed instead.
303
+
304
+ **Nothing is pushed.** The commit is local, exactly as `/commit` leaves it.
305
+
306
+ ### `commit-continue` — then the next phase, and no further
307
+
308
+ On `commit-continue`, once the commit is in, carry on into **`/spec-next`**.
309
+ That is the whole of the chaining, and both halves of that sentence matter:
310
+
311
+ - **A failed commit is the end of the chain.** If typecheck or the tests fail
312
+ there is no commit, and therefore no continue — report where it broke and
313
+ stop. The continue is downstream of the commit, not beside it.
314
+ - **Never `/spec-complete`.** When there is no unfinished phase left, say the
315
+ spec has none and stop. Completing
316
+ **lands the branch and tears the worktree down**, and that must not fall out
317
+ of a button labelled *continue* — the
318
+ distance between "build the next phase" and "delete the worktree" is one skill
319
+ name, and a person pressing a button on a phone cannot see which one you
320
+ picked.
321
+
322
+ **This overturns a recorded Non-goal, and cites it rather than contradicting it.**
323
+ `feat-review-verdict` ruled chaining out: *"'Commit what I just read' and
324
+ 'go build the next thing unattended' are different sizes of decision, and the
325
+ second stays a keystroke."* That conflated two meanings of unattended — nobody
326
+ choosing, and nobody watching. A distinctly-labelled fourth button is chosen,
327
+ deliberately, by the person who just read the diff. The ban was right about an
328
+ *automatic* chain and caught a chosen one by accident.
329
+
330
+ **Passes waiting are information — and worth raising.** A render lists them
331
+ when the holding area is not empty: code, verdict, age, one per line. Nothing
332
+ counts them and nothing refuses over them; a pass nobody claims simply sits
333
+ there. But **say so** — an operator who pressed a button on their phone and hears
334
+ nothing has no way to tell a pass that never arrived from one waiting to be
335
+ confirmed, and both look like silence. Raising it is step 0's offer; never
336
+ treating it as a task is the rule that survives.
337
+
338
+ **A mark is information, never a gate.** Nothing counts the ticks or requires
339
+ them: a phase may end with comments open, `/spec-complete` never learns about
340
+ them, and this skill refuses nothing on their account. If a project ever wants
341
+ otherwise that is a config key defaulting to off — not a tidy-up here.
342
+
343
+ **What the intake costs, and why a code costs less.** A pasted blob is file
344
+ paths and the operator's own words, which you need in context to act on them —
345
+ so the paste is not overhead. But it does scale with the review: a marked-up
346
+ 60-file pass is kilobytes of context before any work starts.
347
+
348
+ **A claimed pass never enters the context at all.** The engine holds it, merges
349
+ it and reports the counts; six digits is what reaches you. That is the same rule
350
+ the diff already follows — git writes it, the engine splices it, you never read
351
+ it — and the paste was the one place it broke. The *work* either authorises is
352
+ ordinary phase-sized cost, and step 3 is where the operator decides whether to
353
+ spend it.
354
+
355
+ ## 2b. An action changes something, then hands the page back
356
+
357
+ Only on a pass carrying an **action**. Three of them, and
358
+ **all three end the same way**:
359
+ do the thing, re-render (§4), and wait again (§4b) — because the reader
360
+ still has a decision in front of them and has not made it.
361
+
362
+ | Action | Do | Then |
363
+ |--------|----|------|
364
+ | `live-on` | commit first (below), then `skitterspec spec-env live take <spec>` | re-render `--branch`, wait again |
365
+ | `allow-network` | `skitterspec spec-env review allow network` | re-render, wait again |
366
+ | `allow-remote` | `skitterspec spec-env review allow remote <spec> --set on` | publish (§6), re-render, wait again |
367
+
368
+ **THERE IS NO `live-off`, and its absence is deliberate.** Putting *this* change
369
+ live is about the diff on screen; handing the whole instance back to `main` is a
370
+ workspace decision with nothing to do with this review. The `live:` line names
371
+ `/spec-live main` for it — a command the operator types, which is also why no
372
+ skill runs it.
373
+
374
+ **AN ACTION IS NOT A VERDICT, and the gate is untouched by all three.** A phase
375
+ that ended armed the gate, and it is discharged by a committing verdict or a
376
+ recorded skip and by nothing else — so after any of these the phase still owes
377
+ an answer, and that is exactly why the run waits again rather than finishing.
378
+ The engine enforces this rather than trusting it: `ACTIONS` is disjoint from
379
+ `VERDICTS` and `COMMITTING`, so nothing that routes on a verdict can see one.
380
+
381
+ **`live-on` commits first, and the commit is a precondition rather than an answer.**
382
+ `live take` refuses a dirty worktree, and even without that guard
383
+ uncommitted work stays behind in the worktree — so what went live would be the
384
+ *previous* commit while the page claimed to be showing this one. Hand off to
385
+ `review.commitWith` exactly as §2a does (do not restate its rules here), then
386
+ take the instance. The reader has not approved anything by pressing it.
387
+
388
+ **The gate permits that one commit, and nothing more.** A phase that ended
389
+ armed the gate, so the commit this action needs is the very commit the gate
390
+ refuses — which made the press unachievable in the only state it is offered in.
391
+ Claiming a `live-on` pass therefore records a **permit** on the gate, bound to
392
+ the worktree's HEAD, and the commit hook honours it. It dies the moment that
393
+ commit lands, so a second one is refused exactly as before, and the gate stays
394
+ **armed** throughout: the phase still owes a verdict and `/spec-next` still
395
+ refuses to build the next one. Nothing is typed to get it — if a commit here is
396
+ still refused, that is the ordinary gate and the exits are the ordinary three.
397
+
398
+ **It re-renders `--branch` afterwards**, because the working view is now empty:
399
+ the commit just happened, and the branch range is what still answers "what am I
400
+ looking at". §4's clean-tree fallback reaches the same place on its own, so
401
+ passing `--branch` is belt and braces rather than a separate rule.
402
+
403
+ **A mid-phase page does not get the commit.** Where the render took
404
+ `--buttons midrun`, the work is half a phase — committing it to look at it
405
+ running splits one phase across two commits and leaves a mess nobody asked for.
406
+ Say the phase needs to land first, re-render, and wait.
407
+
408
+ **Relay every refusal, and work around none of them.** `live take` refuses a
409
+ workbench another spec holds, a hotfix, a stateful spec, a branch touching
410
+ migrations, and a tree with no dev server listening. Each refusal names its own
411
+ way out. **Never park another spec's live session** to make room — that is
412
+ someone else's work, and freeing it is their decision. Re-render and wait, so
413
+ the reader can choose something else.
414
+
415
+ **A refusal may declare the way out of itself.** Ask with
416
+ `skitterspec spec-env live take <spec> --json` and read its `offer` field:
417
+ where one is present, relay the refusal and **then** offer it, exactly as
418
+ `.claude/rules/offered-unblocks.md` describes. Do not re-decide from the reason
419
+ text, and do not infer an offer that is not there — the boundary that keeps
420
+ another spec's workbench out of this lives in that field rather than in prose
421
+ here.
422
+
423
+ **`allow network` writes a committed file, and that is worth a sentence.** It
424
+ edits `specs/.core/env.config.json` in the **primary checkout**, so it changes
425
+ behaviour for everyone who pulls and leaves that tree dirty — one server binds
426
+ for every spec, so the bind cannot be narrower. The engine prints the absolute
427
+ path; relay that rather than letting a shared setting change land silently.
428
+ **`allow remote` is per spec** and writes only a gitignored sidecar beside the
429
+ page, so the base branch stays clean and no other spec may publish. And it is **enable-only**: there is no
430
+ action that turns a tier off, because turning `network` off from a page reached
431
+ over the network kills the page doing the turning.
432
+
433
+ **`allow remote` permits publishing. It does not publish by itself.**
434
+ Publishing stays an explicit ask (§6), because the page it leaves behind is one
435
+ this tooling cannot remove. A reader who presses `Allow remote` on the page, or
436
+ types `/spec-remote-review` beside a waiting review, **has** asked: the engine
437
+ names the page on its `review:`/`publish:` lines, and that page is published
438
+ and the stack re-shown. Nothing it does not name is published.
439
+
440
+ ## 3. Gate it on nothing
441
+
442
+ **This skill has no preconditions and must never grow one.** Not tests passing,
443
+ not the phase being finished, not the spec being this session's, not a clean
444
+ tree. Reviewing work in progress is the *common* case — half a phase, a hand
445
+ edit, a colleague's branch — and a gate would refuse at exactly the moment
446
+ someone wants to look.
447
+
448
+ If a later edit is tempted to add "only when the phase is complete", the answer
449
+ is no. The page is free to produce and changes nothing. The same goes for the
450
+ marks: a spec with unread files or open comments is an ordinary spec, and
451
+ nothing here may start counting them.
452
+
453
+ **And this skill never arms the gate.** Arming is `/spec-next`'s, at the one
454
+ moment that means something: a phase ended. Rendering mid-phase — the common
455
+ case this skill exists for — must not create an obligation, because then
456
+ looking at your own work halfway through would owe you a verdict on it. Reading
457
+ is free; ending a phase is what is answerable.
458
+
459
+ An **already-armed** gate is a different matter, and it is not this skill's to
460
+ enforce either: `/spec-next` §2 is where that refusal lives. Here it is only
461
+ context — the render says a verdict is owed, and this skill's whole job is to
462
+ help someone give one.
463
+
464
+ ## 4. Render the page — or serve it
465
+
466
+ ```
467
+ skitterspec spec-env review <spec> # uncommitted work (the default)
468
+ skitterspec spec-env review <spec> --branch # everything since the base branch
469
+ skitterspec spec-env review <spec> --run-reviewers # …and run review.reviewers
470
+ skitterspec spec-env review serve # every spec, on localhost
471
+ ```
472
+
473
+ **`--run-reviewers` is opt-in here, and a BARE `/spec-diff` MUST NOT PASS IT.**
474
+ `/spec-next` runs the project's configured reviewers once per phase, where the
475
+ cost is paid against work that is finished. This skill answers at any point,
476
+ including several times in one phase — and each run spends a review against
477
+ whatever hourly limit the reviewer has, which on the tiers this was built for is
478
+ measured in single figures.
479
+
480
+ So pass it when the operator asked for it: `/spec-diff --reviewers`, or they said
481
+ in words that they want the second opinion. Otherwise render without it. The
482
+ cached findings from the phase-end run still show on the page whenever the diff
483
+ has not moved, so the usual mid-phase read loses nothing by not asking.
484
+
485
+ Where it does run, say what it cost — a reviewer takes 30s–3min — and nothing
486
+ else. The page's own strip reports each reviewer's outcome, and a reviewer that
487
+ could not run is that strip's business rather than yours.
488
+
489
+ **There is no switch left to handle.** The engine serves and prints a **stack** —
490
+ one line per tier, `local`, `network`, `remote`, each either a URL or the one
491
+ command that turns it on. All of it is free and none of it publishes anything.
492
+ You are not choosing between them; you are relaying every line it printed.
493
+ `.claude/rules/spec-reports.md` carries the shape and why.
494
+
495
+ The operator who does not want a LAN listener sets `review.allowNetwork: false`
496
+ in `env.config.json` — `network` then reads `off` with the command that turns it
497
+ back on, and `local` is the loopback page. Turning serving off entirely is
498
+ `review.serve: "never"`, and then `local` is the `file://` page, which says on
499
+ its own line that it cannot send a verdict.
500
+
501
+ `serve` renders **per request**, so nothing it shows can be stale, and it lists
502
+ every spec with a worktree rather than one. `--host 0.0.0.0` binds beyond
503
+ loopback and prints a URL a phone on the same network can open, guarded by an
504
+ unguessable path token. Bare, it binds loopback only. It is a process:
505
+ `--status` says whether one is up, `--stop` takes it down.
506
+
507
+ Default to the working tree — "what did this phase just do". Use `--branch` when
508
+ the question is about the whole spec.
509
+
510
+ **You do not have to reach for `--branch` after a commit.** A clean working tree
511
+ is the state a phase *ends* in, so the engine falls back to the branch range by
512
+ itself and says which it is showing — `(working tree clean — since main)` in the
513
+ header line, and `(working tree clean)` on the page. Report that wording as it
514
+ came rather than calling it the working tree.
515
+
516
+ The fallback fires on exactly one state — no `--branch`, and nothing uncommitted
517
+ to show. An explicit `--branch` always means what it says, a tree with real
518
+ changes is never swapped out from under you, and a branch with no work at all
519
+ still reports `nothing to review` exactly as before.
520
+
521
+ Add `--json` to get the file list, totals and the page path back as data. The
522
+ page is written to `.spec-env/reviews/<spec>.html`, which is gitignored — it
523
+ leaves no trace in the branch under review.
524
+
525
+ **Declare the button set when the work is unfinished.** A page rendered part-way
526
+ through a run takes `--buttons midrun`, and offers `Continue` — *I have read it,
527
+ carry on* — in place of `Commit` and `Commit & Continue`:
528
+
529
+ ```
530
+ skitterspec spec-env review <spec> --buttons midrun
531
+ ```
532
+
533
+ **It is a statement about the work, never a reading of the gate.** Ask whether
534
+ the thing you just rendered is *finished*: a phase that ended, a bug fix that is
535
+ green, a spec about to land — committing set, which is the default, so pass
536
+ nothing. Half a phase, a hand edit, a colleague's branch mid-flight, anything
537
+ the operator asked to look at while it is still moving — `midrun`.
538
+
539
+ **Do not derive it from whether the gate is armed.** That is tidier and wrong: a
540
+ project running `review.required: false` never arms at all, so every one of its
541
+ pages would lose the committing buttons and its reader could never commit from
542
+ the page. The caller knows what it rendered; the gate only knows whether the
543
+ project opted into gating.
544
+
545
+ `Continue` is not the removed `none` verdict. `none` recorded itself and did
546
+ nothing; `Continue` **resumes the run**, so it still names an action. What it
547
+ cannot do is commit, or clear a gate a finished phase armed — that takes a
548
+ committing verdict or a recorded skip.
549
+
550
+ **On `--page-only`, stop here** and report the path.
551
+
552
+ ## 4a. Relay the stack — every tier, never one you picked
553
+
554
+ `spec-env review` prints one line per tier and `--json` carries the same thing
555
+ as `tiers`. **Relay all of them, in that order**, whatever the `reader:` line
556
+ says:
557
+
558
+ | Tier | Carries |
559
+ |------|---------|
560
+ | `local` | the loopback URL — or the `file://` page when nothing served, which says it cannot send a verdict |
561
+ | `network` | the LAN URL and any `also:` alternates under it — or `off` with the command that turns it on |
562
+ | `remote` | the published URL and *a verdict here needs `/spec-reviewed`* — or `off` with the command that turns it on |
563
+
564
+ **The `reader:` line decides nothing here any more.** It is still printed and
565
+ still the only place that question is answered, but the offer no longer branches
566
+ on it — because branching on it is what produced a `file://` page for a session
567
+ detected `unknown`, a LAN URL for a phone off the network, and an address that
568
+ changed underneath a reader mid-session. A tier that is off keeps its line, so a
569
+ reader who has left the house can see the surface exists and ask for it.
570
+
571
+ **Never read an environment variable to decide anything about the offer.** Not
572
+ `SSH_CONNECTION`, not `CLAUDE_CODE_*`, not a tty check — there is nothing left
573
+ for a detection to decide, and a second implementation of one could not be
574
+ tested and would drift. The ranking and the traps
575
+ (`CLAUDE_CODE_ENTRYPOINT` describes the *process*, not the reader; stdin is never
576
+ a tty under Claude Code) live in `review.js` beside the code, which is where they
577
+ belong.
578
+
579
+ **It authorises serving, never publishing.** A `remote` reader authorises a
580
+ local server — one process, ended by one flag, leaving nothing behind — and
581
+ nothing more. Publishing leaves a page this tooling cannot remove, so it is an
582
+ ask, in every case, always. The two were once one rule, and lumping them together
583
+ is what left a remote reader holding a dead link: see
584
+ `specs/complete/bug-remote-reader-gets-a-dead-link/`.
585
+
586
+ When serving fails — a busy port, or a machine with no network address — the
587
+ engine falls back to the `file://` URL with its marker and nothing breaks. That
588
+ is the one case where naming publishing is worth doing, because it is the only
589
+ answer left.
590
+
591
+ `review.reader` in `env.config.json` (`local` · `remote` · `detect`) lets the
592
+ operator settle where they are reading, and an explicit value is believed without
593
+ sniffing. What the engine may *do* is settled by the tier settings instead —
594
+ `review.serve` (`always` · `never`), `review.allowNetwork` and
595
+ `review.allowRemote` — not by the reader.
596
+
597
+ ## 4b. Wait for the verdict — because asking for one means waiting for it
598
+
599
+ A served page can hand its pass back the moment it is pressed. Without a wait,
600
+ that pass lands in the holding area and stops — nothing happens until someone
601
+ types `/spec-reviewed`, so the review and the work carrying on are two separate
602
+ acts joined only by the operator remembering.
603
+ **This is the step that joins them**, and it is this skill's, so `/spec-next` can point here rather than
604
+ keeping a second copy.
605
+
606
+ **The rule is `.claude/rules/spec-reports.md`'s: asking implies waiting.** If
607
+ this render asks the reader for a verdict, it waits for one. If it is not going
608
+ to wait, it does not ask — the report names the page and its size and stops
609
+ there. There is no third option, and in particular there is no *ask now, notice
610
+ later*: that is the shape that stranded two passes on one spec, where the run
611
+ said the page was **ready** rather than that it was **waiting**.
612
+
613
+ **So the question is not "can I watch a file".** It is "am I asking?" — and the
614
+ transport only decides what carries the answer back:
615
+
616
+ - **A served page** posts to the local store; the engine's own wait returns when
617
+ it lands, and the steps below run it.
618
+ - **A `file://` page** has nothing to post to, so the reader pastes the pass and
619
+ their next message carries it. Say you are holding and **end the turn** — that
620
+ is the same wait, carried by the conversation. It is not a lesser one.
621
+ - **A published page** writes to the artifact's own store, which nothing reaches
622
+ from here. That is the one case where the honest sentence is *press a verdict,
623
+ then type `/spec-reviewed`* — see the published-page paragraph below.
624
+
625
+ **Do not ask, and therefore do not wait, on a render nobody is waiting behind** —
626
+ a bare `--page-only`, a page produced alongside other work, a pass that has
627
+ already arrived. Those get the `Review` row, no question, and nothing is owed.
628
+
629
+ 1. **Note the moment**, as an ISO timestamp, before you start. That instant is
630
+ the entire scope of what may be claimed without a person naming it.
631
+ 2. **Run the engine's wait, and end your turn** — under the harness's
632
+ persistent watch primitive where one exists, else in the background
633
+ (`.claude/rules/spec-reports.md` carries the preference):
634
+
635
+ ```
636
+ skitterspec spec-env review wait <spec> --since <timestamp>
637
+ ```
638
+
639
+ **Never a watcher of your own, and never a timeout.** It lasts as long as the
640
+ session, because a reader who walks away from a diff is the normal case — and
641
+ a loop composed here would be proven by nothing, which is how one written as
642
+ `[ "$x" \> "$y" ]` spun for five minutes in zsh and looked exactly like
643
+ patience. `/spec-next` §5 carries the full account.
644
+
645
+ Not a poll and not a held-open turn: the operator has their terminal back,
646
+ and the session costs nothing while they read.
647
+ 3. **On waking, let the engine choose:**
648
+
649
+ ```
650
+ skitterspec spec-env review <spec> --claim-since <timestamp> --json
651
+ ```
652
+
653
+ Three answers, and only one of them acts. One pass in the window — that is
654
+ the pass, claimed and merged exactly as `--claim` would. **None** — ordinary;
655
+ the file changed for some other reason, so say nothing and wait again or
656
+ stop. **Two or more** — it refuses and names the count, never the codes; that
657
+ is §2 step 0's two-passes case, so offer them from the render and ask.
658
+ 4. **Route on the verdict** through §2 step 2 onward. A claim is a delivery
659
+ mechanism; nothing downstream may behave differently because a watch woke
660
+ you rather than a person typing.
661
+
662
+ **What holds this up.** Not "the page cannot reach the conversation" — after
663
+ this step it can, deliberately. Two things replace it. The **serve token** is
664
+ 48 bits of `crypto` randomness in the URL path, minted per server, and it
665
+ decides who can POST at all. The **window** decides which pass is yours: a pass
666
+ sitting there before you started waiting is never swept up, which is precisely
667
+ the stranger's pass the old rule was written about, and two arrivals refuse
668
+ rather than pick. What is left of the old rule is unchanged and still absolute —
669
+ outside this window, a pass is claimed because a person named it.
670
+
671
+ **The wait covers the SERVED page and nothing else.** It watches the engine's
672
+ local store, which is where a served page POSTs. A **published** page writes to
673
+ the artifact's own store instead, and no watch of any kind reaches that — so a
674
+ verdict pressed there is invisible until someone asks for it. Never start a
675
+ wait and then hand over a published link under it: that reads as a promise to
676
+ notice, and it was made three times in a row while three verdicts sat unread.
677
+ Where the page is published, say plainly that `/spec-reviewed` is what picks it
678
+ up.
679
+
680
+ **`/spec-reviewed` is not replaced by this.** It stays the way in for a pass
681
+ that arrived when nobody was waiting, for the two-passes case, and for every
682
+ published page. It is user-only, and that is still the
683
+ enforcement that makes a named claim a person's decision.
684
+
685
+ **A harness with no file-watch is no longer on that list**, and that is the
686
+ change. It used to be — the row and `/spec-reviewed` were "the whole story",
687
+ which read as permission to ask without waiting. Now the turn ending is the
688
+ wait, and every harness can end a turn; `/spec-reviewed` remains available
689
+ there, as it is everywhere, for a pass nobody was holding for.
690
+
691
+ ## 5. Offer the written review — say what it costs first
692
+
693
+ The page is free. The **written review is not**, and it costs in two separate
694
+ ways. Quote the one that actually applies rather than a single number:
695
+
696
+ - **Writing it: ~700 output tokens.** The review JSON, near enough regardless of
697
+ how big the diff is. This is unavoidable and it is what was measured.
698
+ - **Reading the diff: input, and it scales.** Only paid when the diff is not
699
+ already in front of you. A 350-line diff is easily 10–15k input tokens.
700
+
701
+ **If you just built this phase, you already have the diff — do not re-read it.**
702
+ That is the common case (`/spec-next` offers this skill the moment a phase ends)
703
+ and the one the ~700 was measured in. Running `git diff` over code you wrote
704
+ three tool calls ago buys nothing and is not free.
705
+
706
+ **When you genuinely do not have it** — a fresh session, half a phase from
707
+ yesterday, a colleague's branch — read *selectively*. `--json` returns the file
708
+ list with each file's `+`/`−`, `status` and `noise` flag and **no patches**, so
709
+ use it to choose: skip everything marked `noise: true`, and skip files the review
710
+ will not have anything to say about. Pulling the whole diff in when three files
711
+ matter is the avoidable half of this cost.
712
+
713
+ Say what it will cost, then let the operator decide. Do not write it unasked when
714
+ the diff is large.
715
+
716
+ When asked, write JSON to a scratch file:
717
+
718
+ ```json
719
+ {
720
+ "summary": "Two or three sentences on what this phase actually did, read from the diff.",
721
+ "checks": [
722
+ { "level": "flag", "file": "src/env/review.js", "note": "…" },
723
+ { "level": "confirm", "file": "test/env-review.test.js", "note": "…" },
724
+ { "level": "good", "file": "src/cli.js", "note": "…" }
725
+ ]
726
+ }
727
+ ```
728
+
729
+ - **`flag`** — something you believe is wrong. **`confirm`** — something only the
730
+ author can settle. **`good`** — a decision worth keeping, said once.
731
+ - Every check names the file it is about.
732
+ - **Review the diff, not the spec.** Keep to what the change shows; do not
733
+ re-derive the plan or restate the spec's reasoning back at its author.
734
+
735
+ Then re-render with it spliced in — the engine renders it, you never emit HTML:
736
+
737
+ ```
738
+ skitterspec spec-env review <spec> --review <file>
739
+ ```
740
+
741
+ ## 6. Publish only when asked
742
+
743
+ **Never publish unprompted.** Publishing leaves something behind that this
744
+ tooling cannot remove, and the page already works as a local file. Offer it;
745
+ do not assume it.
746
+
747
+ When the user asks:
748
+
749
+ - **One spec is one page.** `--json` reports `url` when this spec has been
750
+ published before. If it has, **update that URL** rather than creating a
751
+ second entry, and label the new version for the phase (`phase-3`).
752
+ - **On the first publish**, write the returned URL to the path `--json` reports
753
+ as `urlFile` — one line, no formatting. That file is how every later phase
754
+ finds the same page. Never construct the path yourself.
755
+ - **Say, once, that it is theirs now.** When you report a URL, say in the same
756
+ breath that skitterspec cannot remove the page and that `/artifacts` (or the
757
+ gallery at `claude.ai/code/artifacts`) is where it goes. This is the moment the
758
+ decision is being made, so it is the moment worth saying it — `spec-env down`
759
+ repeats it at teardown, by which point the page has outlived the spec.
760
+ - **Degrade in one line.** If the harness cannot publish — no capability, an
761
+ error — say so, report the local file path, and carry on. That is a working
762
+ outcome, not a failure.
763
+ - **Reach for `--publish-copy`, never a hand transform.** The engine writes the
764
+ page as a complete HTML document and an artifact host wraps page *content*, so
765
+ publishing the page as-written nests two documents. `spec-env review <spec>
766
+ --publish-copy` emits the body-only copy and names its path on a `publish:`
767
+ line. Do not split the document yourself: a rendered page contains the diff,
768
+ and a diff of this project contains `<!doctype html>` as ordinary patch text.
769
+
770
+ The engine knows nothing about publishing and cannot do it. It writes a file and
771
+ reads a URL back as an opaque string; everything about what that string means
772
+ lives here.
773
+
774
+ ### Publish it so the buttons still work
775
+
776
+ A published page is the answer for a reader **no local server can reach** — a
777
+ phone on mobile data, anyone away from the LAN the engine serves on. It is also
778
+ the one surface where the verdict buttons have nowhere to POST: the page's own
779
+ URL is on claude.ai, so a POST fails and the reader gets "could not reach the
780
+ server" after pressing a verdict they meant.
781
+
782
+ So **publish it with a store**, and the page uses it:
783
+
784
+ - Declare `capabilities: {db: {}}`. The page checks for `window.claude.use` and
785
+ writes the pass into the artifact's own `passes` collection instead of
786
+ POSTing. Nothing else about it changes — same marks, same verdicts, same
787
+ refusal to commit over an open comment.
788
+ - **Do not declare `user`.** The pass is not per-viewer private state, a shared
789
+ collection is what it wants, and a declaration nothing uses is a grant asked
790
+ for nothing.
791
+ - **Same file path every time**, so a redeploy reuses the URL. That is what
792
+ answers the clear-down worry: one page per spec, replaced at each render,
793
+ rather than an artifact per phase accumulating in the gallery.
794
+ - **Send a push notification with the URL** when you publish. The reader is by
795
+ definition somewhere else — that is why it was published.
796
+
797
+ ### Taking a stored pass back
798
+
799
+ A pass in the store is claimed the same way in spirit and a different way in
800
+ mechanism, because the engine cannot see it:
801
+
802
+ 1. Read the `passes` collection with the Artifact tool's `read_db`.
803
+ 2. **Apply step 0 unchanged.** One waiting pass, offered by what it says and
804
+ when it was sent; two is a refusal to guess. Nothing here is automatic —
805
+ there is no wait window on this transport, because nothing pushes from the
806
+ store to this conversation.
807
+ 3. Write the pass's `blob` to a scratch file **verbatim** and merge it with
808
+ `spec-env review <spec> --notes <file>`. It rejoins the ordinary path at §2
809
+ step 1, so the verdict is judged and routed exactly as any other.
810
+ 4. **Delete the document** with `write_db` once it is merged. A claim consumes:
811
+ a pass left in the store is claimable twice, which is the one property the
812
+ six-digit code has always had and this transport must not lose.
813
+
814
+ ## 7. Report
815
+
816
+ End with the block defined in `.claude/rules/spec-reports.md`. That file carries
817
+ the shape; this section carries only what is specific here.
818
+
819
+ **Verdicts**
820
+
821
+ - `✅` — the page is rendered, and any review pass handed back was stored and
822
+ acted on.
823
+ - `⚠️` — rendered, with something worth knowing: accepts that lapsed because the
824
+ file changed, comments left unworked because the operator did not say go.
825
+ - `❌` — a render or a fix failed part-way. Quote it.
826
+ - `⏸` — the spec has no worktree. Say that plainly: a spec that has not been
827
+ started has nothing to diff, which is an ordinary state and not an error.
828
+
829
+ **Fields:** `Built` · `Tests` · `Notes` · `Review` · `Follow-ups` · `Next`
830
+
831
+ ## 7a. End in a picker
832
+
833
+ Offer the same four endings `/spec-next` §6a defines — `Reviewed` · `Commit` ·
834
+ `Commit & Continue` · `Discuss` — under the same conditions, including
835
+ `Reviewed` only when a pass is waiting, and the rule that nothing claims a pass
836
+ without a pick. That section owns the wording; do not restate it.
837
+
838
+ **This is where changing your mind is handled, and that is why it belongs here.**
839
+ `/spec-diff` renders the page, so it is the command someone runs when they look
840
+ again — often at a review they have already voted on. A pass they sent is still
841
+ sitting **unclaimed**, so a pick made here **supersedes** it:
842
+
843
+ - act on the pick,
844
+ - drop the waiting pass (`--drop <code>`),
845
+ - and say both happened, naming the verdict that was dropped.
846
+
847
+ **Never carry both.** A stored verdict alongside a fresh one is two standing
848
+ conclusions about one review, and whichever a later run picked up would be a
849
+ coin toss. One ending, most recently chosen — the same rule that makes a verdict
850
+ consumed rather than stored.
851
+
852
+ **Say what was dropped rather than dropping it quietly.** Someone who pressed
853
+ `Commit` on their phone and then picks `Discuss` here has changed their mind on
854
+ purpose; someone who forgot they had voted has not, and only the report tells
855
+ them apart.
856
+
857
+ `Review` carries the files and `+`/`−`, and the tier stack — labelled, run
858
+ together with `·` because a row is one cell. Where the page holds a review pass, it also
859
+ carries the three totals — files accepted, comments open, comments answered.
860
+ `--json` reports those under `notes.totals`; read that, never the diff.
861
+
862
+ `Built` appears only when this run actually changed code — the commented files
863
+ it worked on your go-ahead. A render on its own built nothing, and an empty
864
+ `Built` line claiming otherwise is worse than no field.
865
+
866
+ `Tests` and a commit appear only on a committing verdict (§2a).
867
+ **Say which path made the commit** — the configured skill by name, or by hand —
868
+ in the same row as the sha. A reader cannot tell a `/commit` from a hand-rolled one after the
869
+ fact, so the run that made it is the only place that distinction can be
870
+ recorded.