@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,678 @@
1
+ ---
2
+ name: spec-next
3
+ description: Build the next unfinished phase of the spec in flight for this session — pre-flight, implement with tests, record progress and refresh the tracker. Refuses when no spec is in flight rather than guessing one, and builds wherever that spec resolves rather than wherever the session happens to stand. Use when the user says "/spec-next", "build the next phase", "continue the spec", or "carry on with this spec".
4
+ ---
5
+
6
+ # /spec-next — build the next phase of the spec in flight
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
+ It assumes the workbench is already set up: a spec is **in flight** on this
14
+ checkout, and this skill implements its next unfinished phase. Putting a spec in
15
+ flight — provisioning, moving it to `in-progress`, getting its branch here — is
16
+ `/spec-start`'s job. Re-run this per phase until the spec is done, then
17
+ `/spec-complete`.
18
+
19
+ ## 1. Identify the spec in flight
20
+
21
+ **`--worktree <path>` answers before anything else.** When the invocation names a
22
+ worktree, that is the spec to build and that is where it is built — cwd is not
23
+ consulted. It is how `/spec-start` carries on into phase 1 without moving your
24
+ session, and you can type it yourself.
25
+
26
+ **This is not a loosening of the refusal below.** That refusal exists against
27
+ *guessing* which spec to build, and a path someone typed is not a guess. A bare
28
+ `/spec-next` still refuses exactly as it does today.
29
+
30
+ **Validate the path before writing a line into it.** Run the resolver *from* the
31
+ path, so the answer comes from where you are about to write:
32
+
33
+ ```
34
+ cd "<path>" && skitterspec spec-env resolve
35
+ ```
36
+
37
+ Check the `worktree:` line it prints is that same path. If it is not — or the
38
+ command reports that isolation is not enabled — refuse and stop, naming what you
39
+ were given. A path that is not a provisioned worktree must never become a place
40
+ to write code.
41
+ **Read the output, not the exit status** — it exits 0 even when it
42
+ cannot resolve anything.
43
+
44
+ **Not `--dir <path>`.** That flag sets the **repo root**, not the worktree to
45
+ resolve from, so it answers a different question: on a repo with two or more
46
+ worktrees it refuses with *"no spec given, and N specs have worktrees"* and
47
+ validates nothing at all. The `cd` form is what makes the path itself the
48
+ evidence.
49
+
50
+ Otherwise resolve **in this order**, and stop at the first that answers:
51
+
52
+ 1. **The live spec of this checkout** — run
53
+ `skitterspec spec-env live status` and read its `live:` line. `live: yes`
54
+ names the spec whose branch is checked out here; that is the one to build.
55
+ 2. **The worktree you are standing in** — if this session's cwd is inside a
56
+ spec's worktree, that spec is in flight *for this session*. This is the
57
+ manual-parallel path: several specs may be provisioned, and a terminal tab
58
+ opened in one is its own workbench.
59
+ 3. **The current branch, in `checkout` mode** — no worktrees exist, so the
60
+ branch the checkout is on names the spec.
61
+ 4. **The only spec provisioned in this repo** — ask the engine, with no spec
62
+ named, from wherever you happen to be standing:
63
+
64
+ ```
65
+ skitterspec spec-env resolve
66
+ ```
67
+
68
+ Take its `spec:` line **only when it names exactly one spec**.
69
+
70
+ This is the **durable** rung, and that is the whole reason it exists. Rungs 1
71
+ to 3 all read *session* state, and session state does not survive a `/clear`,
72
+ a new terminal tab, or coming back tomorrow — while the provisioned worktree
73
+ they are each a proxy for is **on disk** and survives all three. `/spec-start`
74
+ does leave the session standing in the worktree and that `cd` is real; what it
75
+ is not is durable. Without this rung, a repo holding exactly one answer sends
76
+ the operator away to re-supply something it already knew.
77
+
78
+ It is also the resolution every other bare command in this workflow already
79
+ uses — the worktree you are standing in, else the sole provisioned spec — so
80
+ this rung is what stops `/spec-next` being a silent exception to a rule
81
+ `.claude/rules/spec-planning.md` states has none left.
82
+
83
+ **Say which spec you resolved and how**, before writing a line of it:
84
+ *"not standing in a worktree — `<spec>` is the only spec provisioned"*. Rungs
85
+ 1 to 3 are self-evident to whoever typed the command; this one is not, and
86
+ the operator cannot see from where they sit what you picked.
87
+
88
+ **Several worktrees stay a refusal.** The engine names them and resolves
89
+ nothing — exactly the ambiguity the refusal below exists for. Relay its list
90
+ unchanged and stop; never pick from it.
91
+
92
+ WHAT WOULD FOOL THIS: a worktree left behind by a declined teardown is still
93
+ a worktree, so a finished spec can go on counting as provisioned. That widens
94
+ the candidate set, so the failure it produces is an extra candidate — an
95
+ ambiguity the engine refuses on — and never a wrong spec built. It cannot
96
+ manufacture an *absence*, which is why the absence below is still worth
97
+ refusing on.
98
+
99
+ **If none answers, refuse and stop:**
100
+ `no spec in flight — run /spec-start <name> to start one`.
101
+
102
+ That now answers a real absence — no worktree anywhere, the engine included —
103
+ rather than a session that merely lost track of where it was standing.
104
+
105
+ **Never fall back to the spec "in context".** A spec discussed in conversation
106
+ is not a spec in flight, and this skill writes real code: building the wrong
107
+ spec's phase produces commits on a branch nobody asked for. The refusal is
108
+ cheap; the mistake is not.
109
+
110
+ **Rung 4 is not that fallback wearing a hat.** A provisioned worktree is a
111
+ record that someone ran `/spec-start`: it is on disk, the engine reads it, and it
112
+ either names one spec or refuses. A spec named in conversation is a guess about
113
+ intent with nothing underneath it, and no number of them ever resolves to one.
114
+
115
+ A **name argument** is accepted, but it must *match* the spec in flight — it
116
+ narrows a re-run, it does not select a different spec. A mismatch refuses,
117
+ naming both.
118
+
119
+ ## 2. Pre-flight — the last phase is committed, and was answered
120
+
121
+ Before writing any code for this phase, get the workspace clean:
122
+
123
+ - **Confirm the last-worked phase is committed.** Run `git status` and
124
+ `git log --oneline -5`. The most recently *implemented* phase (not necessarily
125
+ the numerically previous one) should already be committed. If prior-phase work
126
+ is still uncommitted, **stop and suggest committing it first** (e.g. via
127
+ `/commit`) so each phase lands as its own reviewable commit — don't build the
128
+ next phase on top of an uncommitted one. (Skip if this is the first phase —
129
+ there's nothing prior to commit.)
130
+
131
+ - **Confirm the last phase's review was answered.** Ask the engine, never the
132
+ sidecar:
133
+
134
+ ```
135
+ skitterspec spec-env review gate <spec> --json
136
+ ```
137
+
138
+ `state: "armed"` means a phase ended, its page was rendered, and nobody has
139
+ said what they concluded. **Refuse, with the `⏸` block**, and name the three
140
+ ways out in the `Next` row: read the page and send a verdict, type
141
+ `/spec-reviewed` if one is already waiting, or
142
+ `skitterspec spec-env review skip "<reason>"` to move on with the reason on
143
+ the record.
144
+
145
+ `state: "clear"` carries on.
146
+ **`state: "unknown"` also carries on, in silence** — it is the project opting out, a sidecar that could not be read, or
147
+ a spec the engine could not resolve, and none of those is evidence that
148
+ anything is owed (`.claude/rules/negative-checks.md`). Do not mention it: a
149
+ line about a gate nobody armed is an accusation against a healthy repo.
150
+
151
+ **It may also declare the way out.** An `armed` answer can carry an `offer`,
152
+ and where it does, relay the refusal and **then** offer it — once — exactly as
153
+ `.claude/rules/offered-unblocks.md` describes, spending it with
154
+ `skitterspec spec-env review gate <spec> --offered` as you raise it. Where
155
+ there is no `offer`, the three exits in the `Next` row are the whole answer.
156
+
157
+ **This refusal counts nothing.** It is not a tally of ticked boxes — those
158
+ still gate nothing and still are not counted. It asserts one thing: a phase
159
+ that ended has an answer. And the exit is always **one command**, one of them
160
+ being *"I am moving on"*, which is what keeps this a push rather than a wall —
161
+ a gate with no exit gets switched off wholesale instead of answered.
162
+
163
+ - **Report anything left waiting, and claim none of it.** Ask the engine once:
164
+
165
+ ```
166
+ skitterspec spec-env review waiting
167
+ ```
168
+
169
+ A pass listed here arrived when nothing was watching — a wait that never ran,
170
+ a session cleared, a terminal closed overnight — and no watcher can recover
171
+ those, however good. Relay what it prints: the spec, the code, the verdict and
172
+ the age, with `/spec-reviewed <code>` to pick one up and `--drop <code>` to
173
+ disown it.
174
+
175
+ **It is information, not a gate.** It never refuses, nothing here is
176
+ conditional on it, and **you never claim one** — `/spec-diff` §0 stands
177
+ unchanged, and a pass sitting there when you arrived was not sent to you.
178
+ Silent when nothing is waiting, which is the usual case: say nothing rather
179
+ than reporting that there was nothing to report.
180
+
181
+ ## 3. Implement the phase
182
+
183
+ Identify the **first unfinished phase** from the `00-overview.md` phase index,
184
+ then open its phase file (`0N-<slug>.md`) — that file holds the tasks. Mark it
185
+ started: set the phase-file heading to `🔄` and its `> **Status:**` to
186
+ `In progress`, and flip the matching row in the overview phase index to `🔄`.
187
+
188
+ **Then sync with the tracker (only if a provider is installed).** The phase has
189
+ just changed state, so refresh the mirror before the build starts — that is what
190
+ makes the phase show as in progress *while* it is being built rather than only
191
+ once it is over. Without a provider this is a no-op and nothing below changes.
192
+
193
+ **Only when `specs/.core/jira.config.json` exists** and the spec carries a
194
+ `jira_identifier`. Otherwise skip this step — no config means zero change.
195
+
196
+ - **No pull.** Jira is a generated mirror in one-way sync, so there is nothing
197
+ to bring down before building — the repo is already the source of truth. (A
198
+ status a teammate moved in Jira is advisory only; `/spec-status` surfaces it.
199
+ It is overwritten on the next push.)
200
+ - **Refresh the mirror now, without asking.** Run `/spec-push`. The spec has just
201
+ moved to `in-progress` and its phase to `🔄` — both real state changes, and the
202
+ tracker is a generated mirror of them. This holds under **both**
203
+ `mapping.phases` modes, for different reasons:
204
+ - `"subissue"` (the default) — the phase subtasks already exist, and this
205
+ push is what moves the current one into its in-progress status. Skip it and
206
+ every subtask sits in To Do until the spec completes.
207
+ - `"deferred"` — the subtasks do not exist yet, and this push is what mints
208
+ them. Skip it and a started spec stays mirrored as a phase-less issue.
209
+ - **Never mint the spec issue.** An unlinked spec is skipped, not created —
210
+ `/spec-push` is how someone opts in.
211
+ - **No assignee to backfill.** Assignment sync is not yet supported by the Jira
212
+ provider — there is nothing to stamp and nothing to ask, mid-build or ever.
213
+ - **Never fatal.** If the push fails — offline, no credentials, a Jira error —
214
+ say so and **carry on with the build**. The repo is correct regardless; the
215
+ mirror is disposable and the next push repairs it.
216
+ - **Expect a dirty `specs/.core/` afterwards.** The push writes a base snapshot
217
+ and stamps any new keys, and `/spec-next` does not commit. The next `/commit`
218
+ sweeps it up with the phase's own work.
219
+ - Jira automation (or the GitHub for Jira app) may drive status transitions off
220
+ the branch/PR you pushed in step 2; that's expected and the repo still wins on
221
+ the next `/spec-push`.
222
+
223
+ **Before building, compare the worktree against where you are standing.** Take
224
+ the `worktree:` line from `skitterspec spec-env resolve <spec>` and compare it
225
+ with this session's cwd, resolving both paths first so a symlinked or
226
+ trailing-slash spelling of one tree does not read as two.
227
+
228
+ Same tree — the ordinary case, since `/spec-start` leaves the session standing
229
+ in it — and the rest of this step is inert.
230
+ **Different trees, and the discipline below applies however the spec was resolved.**
231
+ `--worktree <path>` is one way to get here and §1's rung 4 is another: a bare
232
+ `/spec-next` typed from the primary checkout resolves the sole provisioned spec
233
+ and builds it somewhere this session is not. What makes the discipline necessary
234
+ is the two trees, so that is what it is conditioned on — not the shape of the
235
+ invocation, which cannot see rung 4 at all.
236
+
237
+ Record the baseline before you write anything:
238
+
239
+ ```
240
+ skitterspec spec-env resolve <spec> --record-primary
241
+ ```
242
+
243
+ Then build as below, with one discipline on top.
244
+ **The session is not standing in the worktree**, so every write takes an
245
+ absolute path under it and every command
246
+ is prefixed `cd "<worktreePath>" &&` — typecheck and tests included. A single
247
+ relative path lands the work in the primary checkout, on the base branch, and
248
+ nothing about it looks wrong at the time. Step 4b is what catches it.
249
+
250
+ Then build it, following the project rules in `.claude/rules/*.md` and `CLAUDE.md`:
251
+
252
+ - Work task by task through the phase file. Make focused edits that match
253
+ surrounding code.
254
+ - Honour the project's conventions (see `.claude/rules/spec-planning.md` and the
255
+ rules it links).
256
+ - **Tests are part of the phase, not after it.** Create/extend tests for the
257
+ work, then run the project's typecheck and test commands. Do not declare the
258
+ phase done until green.
259
+ - Never hardcode dates in tests; never run destructive commands against a real
260
+ database — use the project's test database only.
261
+
262
+ ## 4. Record progress
263
+
264
+ - In the **phase file**: tick completed tasks (`- [x]`), flip its heading to `✅`,
265
+ and set its `> **Status:**` to `Done`.
266
+ - In **`00-overview.md`**: flip the matching phase-index row to `✅`.
267
+ - If anything changed from the plan (a decision, a deviation, a discovered
268
+ constraint), add a dated **Changelog** entry in `00-overview.md`.
269
+ - If new work surfaced, add it as tasks to the appropriate phase file (or add a
270
+ new phase file + index row) rather than doing it silently.
271
+
272
+ **Then refresh the mirror (only if a provider is installed).** The phase is done
273
+ in the repo now; leaving the tracker to catch up at `/spec-complete` is what makes
274
+ a mirror lag a whole spec behind. Without a provider this is a no-op.
275
+
276
+ **Only when `specs/.core/jira.config.json` exists** and the spec carries a
277
+ `jira_identifier`. Either missing → **skip**, in one line
278
+ (`not linked to Jira — /spec-push to mirror it`), and carry on.
279
+
280
+ **Refresh the mirror now, without asking.** Run `/spec-push`. The repo has just
281
+ become the truth about this phase's progress, and progress is what the mirror
282
+ exists to show. Deferring it to `/spec-complete` is what makes every phase
283
+ subtask jump from To Do straight to Done, with nothing visible in between.
284
+
285
+ - **Never mint.** An unlinked spec is skipped, not created.
286
+ - **Never fatal.** If the push fails — offline, no credentials, a Jira error —
287
+ say so and **finish the operation anyway**. The phase is done in the repo
288
+ regardless; the mirror is disposable and the next push repairs it.
289
+ - **Expect a dirty `specs/.core/` afterwards.** The push writes a base snapshot
290
+ and stamps any new keys, and these skills do not commit. The next `/commit`
291
+ sweeps it up with the phase's own work.
292
+ - **Say what happened** in the skill's report: mirror updated, skipped as
293
+ unlinked, or failed with the reason.
294
+
295
+ ## 4b. Prove nothing leaked into the primary checkout
296
+
297
+ **Only when the resolved worktree is not this session's cwd** — the same
298
+ comparison step 3 made, and it holds however the spec was resolved. Standing in
299
+ the worktree there is no second tree to have written into, so this step does not
300
+ apply and there is nothing to check.
301
+
302
+ WHAT WOULD FOOL THIS CHECK: it watches the **primary checkout** and nothing
303
+ else, so a build run from inside *another* spec's worktree would leak there
304
+ unseen. That is left unhandled deliberately rather than overlooked — reaching it
305
+ takes an explicit `--worktree` typed from a second worktree — and the cost of the
306
+ gap is a missed leak, never a false accusation.
307
+
308
+ The phase is built and its progress recorded — all of it written into a tree this
309
+ session is not standing in. Before reporting any of it as done:
310
+
311
+ ```
312
+ skitterspec spec-env resolve <spec> --assert-primary-clean
313
+ ```
314
+
315
+ - **Exit 0, "primary checkout clean"** — carry on.
316
+ - **Non-zero** — stop and relay the engine's message unchanged. It names the
317
+ paths and both readings: this build wrote them and they belong in the worktree,
318
+ or something else did and the baseline wants re-recording.
319
+ **Do not guess which, and do not delete anything.**
320
+ A path that appeared is not proof of who put it there.
321
+ - **"cannot tell"** — no baseline, or one from another spec. It exits 0 and
322
+ claims nothing; say so in one line and carry on. An absence is not evidence.
323
+
324
+ ## 5. Render the page, arm the gate, then wait for the verdict
325
+
326
+ **Only when the project has per-spec isolation** (`specs/.core/env.config.json`
327
+ present). Without it there is no worktree to read and this step does not exist.
328
+
329
+ The phase is built, its tests are green, and nothing is committed yet. That is
330
+ the moment the page is about, so render it now — **after** the tests pass and
331
+ **before** the commit:
332
+
333
+ ```
334
+ skitterspec spec-env review <spec> --run-reviewers
335
+ ```
336
+
337
+ **This is free.** It is the engine reading git and splicing text into a template;
338
+ the diff never passes through you, so a 266KB patch costs nothing. Report the
339
+ path it prints and move on.
340
+
341
+ **`--run-reviewers` is free to *you* as well, and costs wall-clock.** Where the
342
+ project configured any (`review.reviewers` — empty in most projects, and then
343
+ this flag does nothing at all), the engine runs each one against the diff before
344
+ rendering, so the page is already populated when it is opened. Their findings
345
+ come back as JSON and land on the page as **checks**; the patch still never
346
+ passes through you. What it spends is 30s–3min once, after the tests are green —
347
+ which is the cheap half of a step that is about to wait on a person for far
348
+ longer. A re-render of unchanged work reuses the cached findings rather than
349
+ spending another review.
350
+
351
+ **Findings never gate.** A check is not a comment, so a page carrying twenty of
352
+ them still offers `Commit`; a reader who replies to one creates a comment, and
353
+ that does gate, because then a person has asked for something. Nothing here
354
+ counts them, and nothing here acts on them — you render and wait exactly as you
355
+ would have.
356
+
357
+ **A reviewer that could not run is reported by the page**, not by you. Do not
358
+ re-state its outcome, do not investigate it, and never treat it as a failure of
359
+ the phase: the strip on the page says `clean` or `did not run — <why>` for each
360
+ one, which is where a reader deciding whether to commit will look.
361
+
362
+ **This render takes the committing button set**, which is the default — so
363
+ `--buttons` is not passed. That is a statement about the work, not about the
364
+ gate: the phase is finished, so `Commit` and `Commit & Continue` are the right
365
+ verbs for it. A render part-way through a run takes `--buttons midrun` and
366
+ offers `Continue` instead; `/spec-diff` owns that case.
367
+
368
+ **Then arm the gate**, so the phase now owes a verdict:
369
+
370
+ ```
371
+ skitterspec spec-env review arm <spec> --phase <n>
372
+ ```
373
+
374
+ It is idempotent within a phase, so a re-render does not restart the clock, and
375
+ it is **never fatal**: a project that opted out, or an engine that could not
376
+ resolve the spec, says so and the phase is still built. Nothing here counts
377
+ anything — the gate asserts that a phase which ended has an answer, and that is
378
+ all it asserts.
379
+
380
+ **Then offer `/spec-diff`. Do not run it.** The written review is the part that
381
+ costs — roughly **700 output tokens**, because writing it means reading the diff
382
+ — and that spend is the operator's call, not a default.
383
+
384
+ **Where you are going to wait, the offer is the banner after the block** —
385
+ defined in `.claude/rules/spec-reports.md`, and **the `Review` row is dropped**
386
+ so one subject lives in one place:
387
+
388
+ ---
389
+
390
+ ## ⏸ Review ready — <N> files, +<a> −<d><, <n> flagged by <reviewer> — only where one found something>
391
+
392
+ - **local** — <the `local:` URL>
393
+ - **network** — <the `network:` URL, or off with the command that turns it on>
394
+ - **remote** — <the `remote:` URL, or off with the command that turns it on>
395
+
396
+ <the `live:` line, exactly as the engine printed it — omitted when it printed none>
397
+
398
+ I'm holding here until you send a verdict — the wait covers local and network.
399
+
400
+ `/spec-reviewed` picks it up · `spec-env review skip "<reason>"` moves on
401
+
402
+ **The findings count goes in the COUNTS, and nowhere else.** Where `--json`
403
+ reported a reviewer in the `findings` state, its count joins the heading — the
404
+ slot that already says `7 files, +212 −18` — and that is the whole of it. No
405
+ extra line, no extra row, and nothing at all when every reviewer was `clean`,
406
+ `cached` or could not run: the page's own strip is where the per-reviewer
407
+ outcomes belong, and repeating them here would be the second place naming one
408
+ subject that `.claude/rules/spec-reports.md` exists to prevent.
409
+
410
+ **This is not a third amendment to that contract.** The counts slot is already
411
+ there and already carries numbers about the same diff; nothing about the
412
+ banner's shape, its lines or its exits changes. A reader who has configured no
413
+ reviewer sees the heading they have always seen.
414
+
415
+ **The stack is the engine's — copy it, do not compose it.** `spec-env review`
416
+ printed one line per tier, in that order, and `--json` carries the same thing as
417
+ `tiers`. Relay all three whatever they say, including the ones that are off with
418
+ the command that turns them on: a reader who has left the house cannot ask for a
419
+ remote page they were never shown.
420
+
421
+ **Do not pick one for them.** The engine used to, and the guess failed three
422
+ separate ways in one day. `.claude/rules/spec-reports.md` carries the shape and
423
+ why the *one link, never two* rule now reads
424
+ **one link per reachable store, each labelled** —
425
+ local and network are two doors into one room, so the wait
426
+ covers both. `remote` is a second store, so its own line carries its own caveat:
427
+ *a verdict here needs `/spec-reviewed`*, because nothing pushes from
428
+ the artifact store into this conversation. Never let the holding line promise a
429
+ wait over that tier.
430
+
431
+ **Publishing is still never yours to do.** `remote` reading `off` is the normal
432
+ state, and the fix for it is the command on that line — typed by the operator,
433
+ or pressed on the page, because a published page is one this tooling cannot
434
+ remove and permitting it is not publishing it.
435
+
436
+ **The `live:` line goes with the stack**, and it is the engine's line too —
437
+ copied as printed, and **absent when the engine printed none**. The page can act
438
+ on it: a press puts the spec live or takes it down and hands the reader back the
439
+ same page, which is `/spec-diff` §2b's routing and not this skill's. What
440
+ matters here is that an action **never clears the gate this phase armed** — the
441
+ reader has looked at it running and concluded nothing, so the phase still owes a
442
+ verdict.
443
+
444
+ ---
445
+
446
+ **Where you are not waiting, it stays the `Review` row** — the counts and the
447
+ page link, and **no question**:
448
+
449
+ | **Review** | <N> files, +<a> −<d> · **local** <URL> · **network** <URL> · **remote** off |
450
+
451
+ At the end of a phase you are always waiting, so this shape belongs to the
452
+ renders that are not this step: a mid-phase `/spec-diff`, a page produced
453
+ alongside other work. A row cannot be waited on, so a question in one is
454
+ unanswerable however findable it is.
455
+
456
+ **Both shapes are addressed to someone, and that is the constraint.** The offer
457
+ was once a fenced block of engine output: two quoted lines under the test
458
+ counts, addressed to nobody, with the report then closing on *"commit this
459
+ first"* — the last instruction the reader got was to move on, so they did. A row
460
+ in a labelled table is findable; a banner says the work has stopped. What must
461
+ never come back is something unaddressed, unfindable, or fenced.
462
+
463
+ **The row asks nothing, and that is not a weakening of the above.** A row cannot
464
+ be waited on, so a question in one is unanswerable however findable it is —
465
+ which is the failure `spec-reports.md` records under *asking implies waiting*.
466
+ The row names the page; the banner is what asks, because the banner is the shape
467
+ the run is standing behind.
468
+
469
+ **Never bury it and never split it.** The row sits above the last two rows of
470
+ the block, and the page and its counts stay in the same row; the banner
471
+ replaces the row rather than joining it. Two places naming one page make the reader
472
+ resolve a distinction before acting on either — which is the same failure
473
+ whether the two places are adjacent rows or a row and a banner.
474
+
475
+ ### Then wait for the verdict, where the harness can
476
+
477
+ The row is findable, but a row cannot make the continuation follow from the
478
+ reading — and that is the gap the whole gate exists to close.
479
+
480
+ **The wait is a command. Do not write one.**
481
+
482
+ 1. **Note the moment you start waiting**, as an ISO timestamp. That instant is
483
+ the whole scope of what you may claim.
484
+ 2. **Run the engine's wait, and end your turn** — under the harness's
485
+ persistent watch primitive where one exists, else in the background
486
+ (`.claude/rules/spec-reports.md` carries the preference):
487
+
488
+ ```
489
+ skitterspec spec-env review wait <spec> --since <the timestamp>
490
+ ```
491
+
492
+ It returns when a pass arrives inside that window, and
493
+ **it takes no timeout unless you pass one** —
494
+ the wait lasts as long as your session, because a reader who walks away from
495
+ a diff is the normal case rather than the edge one. Do not give it a duration
496
+ of your own.
497
+
498
+ **A watch that dies is re-armed on the same window**, silently and bounded by
499
+ the age of that window — `.claude/rules/spec-reports.md` carries the contract,
500
+ including when to stop and what the banner says instead. Do not restate it
501
+ here.
502
+
503
+ Then **end your turn**. Do not poll it and do not hold the turn open: the
504
+ point is that the reader has the terminal back while they read.
505
+ 3. **On waking, let the engine pick**:
506
+
507
+ ```
508
+ skitterspec spec-env review <spec> --claim-since <the timestamp> --json
509
+ ```
510
+
511
+ It claims the one pass that arrived inside the window, and acts on nothing
512
+ at all when none did (the wait can be woken by something that was not a
513
+ pass) or when two did (two sittings, or two people — the operator has the
514
+ codes).
515
+ 4. **Route on the verdict** exactly as `/spec-diff` §2 and §4 describe. Do not
516
+ restate that routing here.
517
+
518
+ **WHY A COMMAND RATHER THAN A LOOP YOU COMPOSE.** This step used to say "watch
519
+ the pending store" and stop, so every run invented its own watcher in shell —
520
+ and three failed in two days, each reaching the operator as *"I pressed the
521
+ button and nothing happened"*. The worst wrote
522
+ `until [ -f "$P" ] && [ "$x" \> "$y" ]`: valid bash, a syntax error in zsh, a
523
+ predicate that could never be true. It spun for five minutes writing to a
524
+ stderr nobody reads.
525
+
526
+ **The bug is not the lesson — the silence is.** A watcher that can never fire
527
+ and one patiently working are indistinguishable from outside, so nothing about
528
+ that run looked wrong until the operator asked. `review wait` says it has
529
+ started, is written once, and is tested against a store that gains a pass
530
+ mid-flight. A predicate composed fresh each time is proven by nothing.
531
+
532
+ **WHY THIS IS SAFE, AND WHAT IT COSTS.** It was once true that a device
533
+ reaching your page could not reach your conversation, and that fact was the
534
+ whole guard: a pass sat in the holding area until a person typed
535
+ `/spec-reviewed`. This replaces that guard rather than weakening it by
536
+ accident, and the replacement is two things together — **the serve token**,
537
+ 48 unguessable bits minted per server, which is what decides who can POST at
538
+ all; and **the window**, which is what decides which pass is yours. A pass
539
+ already waiting when the wait began is never claimed by it, which is exactly
540
+ the stranger's pass the old rule was written about. What is genuinely given up
541
+ is that the page can now act, so the token has become a credential rather than
542
+ a convenience — and `--claim-since` refusing to choose between two passes is
543
+ what stops a race becoming a wrong commit.
544
+
545
+ **Where the harness cannot run something in the background, you still wait** —
546
+ the turn ending is the wait. Say the page is rendered and that you are holding
547
+ for the pass, then end your turn; the reader's next message is what carries it,
548
+ and `/spec-diff` picks it up from the paste exactly as it always has. The gate
549
+ holds either way: it is the engine's, not the wait's.
550
+
551
+ This once read *"change nothing"* — keep the row, keep the question, do not
552
+ wait — and that exemption is the hatch a whole class of unanswerable questions
553
+ came through. Every harness can end a turn.
554
+
555
+ **It still does not break a chained run.** `/commit && /spec-next` is typed as
556
+ one line; by the time this step is reached the chain has finished, so waiting
557
+ here stops nothing that was still going to happen.
558
+
559
+ Relay the engine's **stack** — the `local:`, `network:` and `remote:` lines —
560
+ never the bare `page:` path: a path is not clickable in any terminal, and a page
561
+ nobody can open is a page nobody reads.
562
+
563
+ - **Never write the review unasked**, and **never publish**. Publishing leaves
564
+ something behind that this tooling cannot remove, so it is always something
565
+ someone asks for. A `file://` link is no use on a phone, and saying so **is**
566
+ the ask — publishing is the answer to it, and `/spec-diff` §6 owns how.
567
+ **The `reader:` line no longer decides anything here, and that is the point.**
568
+ It is still printed, and it is still the only place that question is answered —
569
+ but the offer does not change with it, because the stack lists every tier
570
+ whatever it says. Three reader states used to mean three different offers, and
571
+ that branching is exactly what produced a `file://` page on a session detected
572
+ `unknown`, a LAN URL for a phone off the network, and an address that changed
573
+ underneath a reader when detection flipped mid-session.
574
+
575
+ So: **relay all three tier lines, every time.** Any `also:` lines sit under
576
+ `network` — the other addresses this machine has, offered because the
577
+ best-guess one can be wrong — so pass them on rather than editing them out: in
578
+ the banner they are indented sub-bullets under **network**, labels
579
+ (`(tailnet)`) included, exactly as `.claude/rules/spec-reports.md` shows.
580
+
581
+ **Never read an environment variable to decide anything about the offer** — not
582
+ `SSH_CONNECTION`, not `CLAUDE_CODE_*`, not a tty check. There is nothing left
583
+ here for a detection to decide, and a second implementation of one could not be
584
+ tested and would drift.
585
+
586
+ **Serving is the engine's to do; publishing is never.** The server is one
587
+ process, ended by one flag, leaving nothing behind, and `review.allowNetwork`
588
+ decides whether it binds wide — no detection involved. A reader the server CAN
589
+ reach is not a reason to publish as well: the tiers they can already open are on
590
+ the stack. Publishing leaves a page this tooling cannot remove, so it stays an
591
+ ask in every case, always — which is why `remote` shows the command rather than
592
+ a URL until someone types it.
593
+
594
+ - **Never fatal.** A failed render — no worktree, a git error — is one line and
595
+ the phase is still done. The page is a convenience; the repo is the record.
596
+ - If the project has no isolation config, skip the whole step in silence rather
597
+ than explaining an absence.
598
+
599
+ ## 6. Report
600
+
601
+ Do **not** `git commit` unless the user asks — finish, verify, and wait.
602
+
603
+ End with the block defined in `.claude/rules/spec-reports.md`. That file carries
604
+ the shape; this section carries only what is specific here.
605
+
606
+ **Verdicts**
607
+
608
+ - `✅` — the phase is built and its tests are green.
609
+ - `⚠️` — built and green, with something worth knowing (a mirror that did not
610
+ refresh, a deviation from the plan).
611
+ - `❌` — the phase's tests are red, or it stopped part-way. Quote the failure.
612
+ - `⏸` — no spec in flight, or the name given does not match the one that is.
613
+
614
+ **Fields:** `Tracker` · `Branch` · `Built` · `Tests` · `Notes` · `Review` ·
615
+ `Follow-ups` · `Next`
616
+
617
+ ## 6a. End in a picker
618
+
619
+ The block says what happened; this is what to do about it. Offer the same four
620
+ endings the review page carries, so a review finishes the same way wherever the
621
+ reader is standing — the page, a pasted code, or here.
622
+
623
+ **Not when you are waiting.** Where §5 set a watch and ended the turn, the
624
+ verdict is coming from the page and the picker would be a second way to answer
625
+ a question already asked — so the block ends on `Next`, which names the page.
626
+ The picker is for the run that did not wait: no watch available, or a render
627
+ the reader is expected to come back to in their own time.
628
+
629
+ | Option | Does |
630
+ |--------|------|
631
+ | `Reviewed` | Claims the waiting pass and routes on its verdict |
632
+ | `Commit` | Runs the project's commit skill, and stops |
633
+ | `Commit & Continue` | Commits, then `/spec-next` — and **stops there** |
634
+ | `Discuss` | Asks what is up; changes nothing |
635
+
636
+ **`Reviewed` only when a pass is actually waiting.** The render's `pending:`
637
+ block already says. Offering a pickup with nothing to pick up is the empty
638
+ gesture this exists against — the other three stand on their own.
639
+
640
+ **Do not restate the routing.** `/spec-diff` §2, §2a and §4 own it, including
641
+ the commit hand-off through `review.commitWith` and what `commit-continue` does
642
+ after. Two copies of a routing rule is how the two come to disagree.
643
+
644
+ **It does not break a chained run.** `/commit && /spec-next` is typed as one
645
+ line and the picker appears at the **end**, by which point the chain has already
646
+ finished. The cost that was feared here — a question stopping a run mid-way — is
647
+ not a cost this placement has.
648
+
649
+ **Nothing may claim a pass without a pick.** `/spec-reviewed` is user-only by
650
+ *harness enforcement*, because prose alone once failed to stop an agent claiming
651
+ a pass nobody asked it to. A pick keeps the **property** that makes that safe —
652
+ a person in the conversation chose, and a device that reaches the page cannot —
653
+ while routing around the **mechanism**, since the claim runs downstream of the
654
+ pick. That trade is deliberate and it has exactly one condition: a run that
655
+ shows no picker claims nothing, and a picker nobody answered claims nothing.
656
+
657
+ WHAT WOULD FOOL THIS: a picker shown reflexively at the end of every run trains
658
+ the reader to dismiss it, and a dismissed picker is indistinguishable from a
659
+ considered decline. So offer it where there is a real choice, and let the `Next`
660
+ row carry the rest.
661
+
662
+ `Next` names the commit and then the phase, as
663
+ `/commit, then /spec-next → phase 3 (Auth)`, so the block says what to do and
664
+ which phase is next without a line of prose for either.
665
+
666
+ **The commit is not optional politeness.** Step 6 above deliberately leaves the
667
+ phase uncommitted, and §2 of this very skill refuses to build the next phase on
668
+ top of an uncommitted one — so a `Next` that names only `/spec-next` sends the
669
+ reader straight into that refusal. The two halves are four hundred lines apart,
670
+ which is exactly how they drifted.
671
+
672
+ **Step 5's offer lands in one of two shapes, and never both.** Waiting on a
673
+ verdict → the **banner** after the block, and no `Review` row. Not waiting → the
674
+ `Review` row, and no banner. Neither is a paragraph: the ban on prose after the
675
+ block is untouched, and the banner is a control the contract names
676
+ (`.claude/rules/spec-reports.md`). Step 5 renders before the commit and this
677
+ step is where its offer lands, so the two must not disagree about which shape it
678
+ takes.