@skitterbyte/skitterspec 21.0.0 → 22.1.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 (43) hide show
  1. package/MIGRATION.md +218 -0
  2. package/README.md +127 -6
  3. package/assets/claude-md-section.md +42 -18
  4. package/assets/commands/allow-main.md +33 -0
  5. package/assets/commands/spec-remote-review.md +22 -0
  6. package/assets/commands/spec-skip.md +24 -0
  7. package/assets/core/env.config.json.example +6 -3
  8. package/assets/core/env.config.md +183 -25
  9. package/assets/hooks/main-guard.cjs +145 -0
  10. package/assets/hooks/review-gate.cjs +8 -3
  11. package/assets/review/page.html +1174 -101
  12. package/assets/rules/offered-unblocks.md +100 -0
  13. package/assets/rules/spec-planning.md +195 -14
  14. package/assets/rules/spec-reports.md +181 -29
  15. package/assets/skills/no-spec/SKILL.md +219 -0
  16. package/assets/skills/spec/SKILL.md +248 -21
  17. package/assets/skills/spec-bug/SKILL.md +34 -24
  18. package/assets/skills/spec-cancel/SKILL.md +29 -0
  19. package/assets/skills/spec-complete/SKILL.md +25 -0
  20. package/assets/skills/spec-diff/SKILL.md +164 -37
  21. package/assets/skills/spec-hotfix/SKILL.md +22 -19
  22. package/assets/skills/spec-init/SKILL.md +4 -2
  23. package/assets/skills/spec-next/SKILL.md +166 -58
  24. package/assets/skills/spec-review/SKILL.md +87 -0
  25. package/assets/skills/spec-reviewed/SKILL.md +31 -5
  26. package/assets/skills/spec-start/SKILL.md +33 -0
  27. package/package.json +1 -1
  28. package/src/cli.js +1612 -172
  29. package/src/env/classify.js +87 -2
  30. package/src/env/config.js +333 -19
  31. package/src/env/hooks.js +102 -33
  32. package/src/env/live.js +124 -1
  33. package/src/env/mainguard.js +240 -0
  34. package/src/env/provision.js +106 -10
  35. package/src/env/registry.js +70 -6
  36. package/src/env/resolve.js +128 -5
  37. package/src/env/review.js +854 -29
  38. package/src/env/reviewers/coderabbit.js +228 -0
  39. package/src/env/reviewers.js +517 -0
  40. package/src/env/serve.js +474 -50
  41. package/src/env/supervise.js +8 -1
  42. package/src/env/teardown.js +12 -1
  43. package/src/init.js +185 -24
@@ -1,6 +1,6 @@
1
1
  ---
2
2
  name: spec-diff
3
- description: See what a spec's worktree changed — render its diff as a page you can mark up, take that review pass back, and act on it. Answers at any point, including half-way through a phase. Use when the user says "/spec-diff", "show me the diff", "what did this phase change", "review this spec's work", wants to read a worktree's changes away from the terminal, or hands back what the review page produced — a six-digit claim code, or the pasted JSON.
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
4
  ---
5
5
 
6
6
  # /spec-diff — see the phase before you commit it
@@ -41,13 +41,19 @@ sole candidate or prints the candidates.
41
41
  The page has marks on it — `✓ accept` per file, notes against a line or a whole
42
42
  file, answers to the checks a written review asked — and it
43
43
  **ends in a decision**: `✓ Approve`, `↺ Request changes` or `… Discuss first`.
44
- A review comes back to you one of two ways, and **both are ordinary**:
44
+ A review comes back to you one of three ways, and **all three are ordinary**:
45
45
 
46
46
  - **A six-digit code** — `418207`, on its own. A *served* page hands its pass
47
47
  straight to the engine, which holds it until someone reads the code out. This
48
48
  is the usual way on a phone.
49
- - **A pasted JSON blob** — a `file://` page has no server to talk to, so it
50
- copies. Not legacy: it is the whole story for a local reader.
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.
51
57
 
52
58
  Either way, **this is not a request to render anything**: it is a review coming
53
59
  back, and these steps replace §3–§5 below.
@@ -151,6 +157,17 @@ back, and these steps replace §3–§5 below.
151
157
  absent verdict has always meant "report it and wait" — which is why that is
152
158
  what it still means.
153
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
+
154
171
  3. **Say what you read, then ask what's up.** Report the accepted count, then
155
172
  each open comment as `file:line — note`, then the files you would touch.
156
173
  **Wait — unless the verdict already said otherwise.** Pasting on its own is
@@ -335,6 +352,77 @@ it — and the paste was the one place it broke. The *work* either authorises is
335
352
  ordinary phase-sized cost, and step 3 is where the operator decides whether to
336
353
  spend it.
337
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 --set on` | 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
+ **It re-renders `--branch` afterwards**, because the working view is now empty:
389
+ the commit just happened, and the branch range is what still answers "what am I
390
+ looking at". §4's clean-tree fallback reaches the same place on its own, so
391
+ passing `--branch` is belt and braces rather than a separate rule.
392
+
393
+ **A mid-phase page does not get the commit.** Where the render took
394
+ `--buttons midrun`, the work is half a phase — committing it to look at it
395
+ running splits one phase across two commits and leaves a mess nobody asked for.
396
+ Say the phase needs to land first, re-render, and wait.
397
+
398
+ **Relay every refusal, and work around none of them.** `live take` refuses a
399
+ workbench another spec holds, a hotfix, a stateful spec, a branch touching
400
+ migrations, and a tree with no dev server listening. Each refusal names its own
401
+ way out. **Never park another spec's live session** to make room — that is
402
+ someone else's work, and freeing it is their decision. Re-render and wait, so
403
+ the reader can choose something else.
404
+
405
+ **A refusal may declare the way out of itself.** Ask with
406
+ `skitterspec spec-env live take <spec> --json` and read its `offer` field:
407
+ where one is present, relay the refusal and **then** offer it, exactly as
408
+ `.claude/rules/offered-unblocks.md` describes. Do not re-decide from the reason
409
+ text, and do not infer an offer that is not there — the boundary that keeps
410
+ another spec's workbench out of this lives in that field rather than in prose
411
+ here.
412
+
413
+ **`allow` writes a committed file, and that is worth a sentence.** It edits
414
+ `specs/.core/env.config.json` in the **primary checkout**, so it changes
415
+ behaviour for everyone who pulls and leaves that tree dirty — unlike the live
416
+ actions, which move a branch and write a gitignored receipt. The engine prints
417
+ the absolute path and whose tree it dirtied; relay that rather than letting a
418
+ shared setting change land silently. And it is **enable-only**: there is no
419
+ action that turns a tier off, because turning `network` off from a page reached
420
+ over the network kills the page doing the turning.
421
+
422
+ **`allow remote` permits publishing. It does not publish.** Publishing stays an
423
+ explicit ask in every case (§6), because the page it leaves behind is one this
424
+ tooling cannot remove.
425
+
338
426
  ## 3. Gate it on nothing
339
427
 
340
428
  **This skill has no preconditions and must never grow one.** Not tests passing,
@@ -364,18 +452,37 @@ help someone give one.
364
452
  ```
365
453
  skitterspec spec-env review <spec> # uncommitted work (the default)
366
454
  skitterspec spec-env review <spec> --branch # everything since the base branch
455
+ skitterspec spec-env review <spec> --run-reviewers # …and run review.reviewers
367
456
  skitterspec spec-env review serve # every spec, on localhost
368
457
  ```
369
458
 
370
- **The engine handles the switch.** A file when the reader is at this machine, a
371
- served URL when they are not: on a `remote` reader it stands its own server up
372
- and puts a URL the reader can open on the `open:` line. Both are free and neither
373
- publishes anything. You are not choosing between them; you are relaying whichever
374
- one the engine printed.
375
-
376
- The operator who does not want a LAN listener started for them sets
377
- `review.serveOnRemote: false` in `env.config.json`, and the `file://` link with
378
- its *will not open where you are reading* marker comes back.
459
+ **`--run-reviewers` is opt-in here, and a BARE `/spec-diff` MUST NOT PASS IT.**
460
+ `/spec-next` runs the project's configured reviewers once per phase, where the
461
+ cost is paid against work that is finished. This skill answers at any point,
462
+ including several times in one phase — and each run spends a review against
463
+ whatever hourly limit the reviewer has, which on the tiers this was built for is
464
+ measured in single figures.
465
+
466
+ So pass it when the operator asked for it: `/spec-diff --reviewers`, or they said
467
+ in words that they want the second opinion. Otherwise render without it. The
468
+ cached findings from the phase-end run still show on the page whenever the diff
469
+ has not moved, so the usual mid-phase read loses nothing by not asking.
470
+
471
+ Where it does run, say what it cost — a reviewer takes 30s–3min — and nothing
472
+ else. The page's own strip reports each reviewer's outcome, and a reviewer that
473
+ could not run is that strip's business rather than yours.
474
+
475
+ **There is no switch left to handle.** The engine serves and prints a **stack** —
476
+ one line per tier, `local`, `network`, `remote`, each either a URL or the one
477
+ command that turns it on. All of it is free and none of it publishes anything.
478
+ You are not choosing between them; you are relaying every line it printed.
479
+ `.claude/rules/spec-reports.md` carries the shape and why.
480
+
481
+ The operator who does not want a LAN listener sets `review.allowNetwork: false`
482
+ in `env.config.json` — `network` then reads `off` with the command that turns it
483
+ back on, and `local` is the loopback page. Turning serving off entirely is
484
+ `review.serve: "never"`, and then `local` is the `file://` page, which says on
485
+ its own line that it cannot send a verdict.
379
486
 
380
487
  `serve` renders **per request**, so nothing it shows can be stale, and it lists
381
488
  every spec with a worktree rather than one. `--host 0.0.0.0` binds beyond
@@ -428,21 +535,29 @@ committing verdict or a recorded skip.
428
535
 
429
536
  **On `--page-only`, stop here** and report the path.
430
537
 
431
- ## 4a. Read the `reader:` line — never sniff for it yourself
432
-
433
- `spec-env review` reports where it believes the reader is, and
434
- **that is the only place this question is answered.** Three states:
435
-
436
- | `reader:` | What to offer |
437
- |-----------|---------------|
438
- | absent (`unknown`) | the `file://` URL, exactly as always. **Do not warn** — unknown is the ordinary state of a local machine |
439
- | `local` | the `file://` URL |
440
- | `remote` | the `open:` line as printed — the engine already served it. Pass on any `also:` lines too |
441
-
442
- **Never read an environment variable to decide this.** Not `SSH_CONNECTION`, not
443
- `CLAUDE_CODE_*`, not a tty check — the engine already did it, reports the answer
444
- on that line and in `--json`, and a second implementation here could not be
445
- tested and would drift from the first. The ranking and the traps
538
+ ## 4a. Relay the stack — every tier, never one you picked
539
+
540
+ `spec-env review` prints one line per tier and `--json` carries the same thing
541
+ as `tiers`. **Relay all of them, in that order**, whatever the `reader:` line
542
+ says:
543
+
544
+ | Tier | Carries |
545
+ |------|---------|
546
+ | `local` | the loopback URL — or the `file://` page when nothing served, which says it cannot send a verdict |
547
+ | `network` | the LAN URL and any `also:` alternates under it — or `off` with the command that turns it on |
548
+ | `remote` | the published URL and *a verdict here needs `/spec-reviewed`* — or `off` with the command that turns it on |
549
+
550
+ **The `reader:` line decides nothing here any more.** It is still printed and
551
+ still the only place that question is answered, but the offer no longer branches
552
+ on it — because branching on it is what produced a `file://` page for a session
553
+ detected `unknown`, a LAN URL for a phone off the network, and an address that
554
+ changed underneath a reader mid-session. A tier that is off keeps its line, so a
555
+ reader who has left the house can see the surface exists and ask for it.
556
+
557
+ **Never read an environment variable to decide anything about the offer.** Not
558
+ `SSH_CONNECTION`, not `CLAUDE_CODE_*`, not a tty check — there is nothing left
559
+ for a detection to decide, and a second implementation of one could not be
560
+ tested and would drift. The ranking and the traps
446
561
  (`CLAUDE_CODE_ENTRYPOINT` describes the *process*, not the reader; stdin is never
447
562
  a tty under Claude Code) live in `review.js` beside the code, which is where they
448
563
  belong.
@@ -461,8 +576,9 @@ answer left.
461
576
 
462
577
  `review.reader` in `env.config.json` (`local` · `remote` · `detect`) lets the
463
578
  operator settle where they are reading, and an explicit value is believed without
464
- sniffing. `review.serveOnRemote` (default `true`) settles whether the engine may
465
- act on it.
579
+ sniffing. What the engine may *do* is settled by the tier settings instead —
580
+ `review.serve` (`always` · `never`), `review.allowNetwork` and
581
+ `review.allowRemote` — not by the reader.
466
582
 
467
583
  ## 4b. Wait for the verdict — because asking for one means waiting for it
468
584
 
@@ -483,8 +599,8 @@ said the page was **ready** rather than that it was **waiting**.
483
599
  **So the question is not "can I watch a file".** It is "am I asking?" — and the
484
600
  transport only decides what carries the answer back:
485
601
 
486
- - **A served page** posts to the local store; a file-watch sees it, and the
487
- steps below are that watch.
602
+ - **A served page** posts to the local store; the engine's own wait returns when
603
+ it lands, and the steps below run it.
488
604
  - **A `file://` page** has nothing to post to, so the reader pastes the pass and
489
605
  their next message carries it. Say you are holding and **end the turn** — that
490
606
  is the same wait, carried by the conversation. It is not a lesser one.
@@ -498,9 +614,20 @@ already arrived. Those get the `Review` row, no question, and nothing is owed.
498
614
 
499
615
  1. **Note the moment**, as an ISO timestamp, before you start. That instant is
500
616
  the entire scope of what may be claimed without a person naming it.
501
- 2. **Watch** `.spec-env/reviews/<spec>.pending.json` in the primary checkout,
502
- and **end the turn**. Not a poll and not a held-open turn: the operator has
503
- their terminal back, and the session costs nothing while they read.
617
+ 2. **Run the engine's wait in the background, and end the turn:**
618
+
619
+ ```
620
+ skitterspec spec-env review wait <spec> --since <timestamp>
621
+ ```
622
+
623
+ **Never a watcher of your own, and never a timeout.** It lasts as long as the
624
+ session, because a reader who walks away from a diff is the normal case — and
625
+ a loop composed here would be proven by nothing, which is how one written as
626
+ `[ "$x" \> "$y" ]` spun for five minutes in zsh and looked exactly like
627
+ patience. `/spec-next` §5 carries the full account.
628
+
629
+ Not a poll and not a held-open turn: the operator has their terminal back,
630
+ and the session costs nothing while they read.
504
631
  3. **On waking, let the engine choose:**
505
632
 
506
633
  ```
@@ -711,8 +838,8 @@ consumed rather than stored.
711
838
  purpose; someone who forgot they had voted has not, and only the report tells
712
839
  them apart.
713
840
 
714
- `Review` carries the files and `+`/`−`, the page's `open:` line, and the
715
- published URL when there is one. Where the page holds a review pass, it also
841
+ `Review` carries the files and `+`/`−`, and the tier stack — labelled, run
842
+ together with `·` because a row is one cell. Where the page holds a review pass, it also
716
843
  carries the three totals — files accepted, comments open, comments answered.
717
844
  `--json` reports those under `notes.totals`; read that, never the diff.
718
845
 
@@ -274,30 +274,33 @@ this worktree is refused until a verdict is sent or
274
274
  `skitterspec spec-env review skip "<reason>"` records the decision to move on.
275
275
  The exit is always one command, and one of them is *"I am moving on"*.
276
276
 
277
- Relay the **`open:`** line rather than the bare path: a path is not clickable in
278
- any terminal, and a page nobody can open is a page nobody reads.
277
+ Relay the engine's **stack** — the `local:`, `network:` and `remote:` lines, all
278
+ three — rather than the bare `page:` path: a path is not clickable in any
279
+ terminal, and a page nobody can open is a page nobody reads. Relay the `live:`
280
+ line with it where the engine printed one, and nothing where it did not.
281
+ `.claude/rules/spec-reports.md` carries the shape, including why a rigid
282
+ contract took that line.
279
283
 
280
284
  - **Never write the review unasked**, and **never publish**. Publishing leaves
281
285
  something behind that this tooling cannot remove, so it is always an ask. A
282
286
  `file://` link is no use on a phone, and saying so **is** the ask —
283
287
  `/spec-diff` §6 owns how.
284
- **Follow the `reader:` line the engine printed — do not sniff for it.** It
285
- answers where the person reading this is sitting, and the offer changes with it:
286
-
287
- - **absent** (`unknown`) — the `file://` URL, exactly as always. **Do not warn:**
288
- unknown is the ordinary state of a local machine, and a warning there is an
289
- accusation against a healthy session.
290
- - **`local`** — the `file://` URL.
291
- - **`remote`** — the engine has already stood its local server up and put a URL
292
- the reader can open on `open:`. So there is **nothing special to say**: relay
293
- that line like any other. Any `also:` lines under it are the other addresses
294
- this machine has, offered because the best-guess one can be wrong — pass them
295
- on rather than editing them out.
296
-
297
- **Never read an environment variable to decide this** — not `SSH_CONNECTION`,
298
- not `CLAUDE_CODE_*`, not a tty check. The engine did it, reports it on that line
299
- and in `--json`, and a second implementation here could not be tested and would
300
- drift.
288
+ **The `reader:` line no longer decides anything here, and that is the point.**
289
+ It is still printed, and it is still the only place that question is answered —
290
+ but the offer does not change with it, because the stack lists every tier
291
+ whatever it says. Three reader states used to mean three different offers, and
292
+ that branching is exactly what produced a `file://` page on a session detected
293
+ `unknown`, a LAN URL for a phone off the network, and an address that changed
294
+ underneath a reader when detection flipped mid-session.
295
+
296
+ So: **relay all three tier lines, every time.** Any `also:` lines sit under
297
+ `network` — the other addresses this machine has, offered because the
298
+ best-guess one can be wrong — so pass them on rather than editing them out.
299
+
300
+ **Never read an environment variable to decide anything about the offer** — not
301
+ `SSH_CONNECTION`, not `CLAUDE_CODE_*`, not a tty check. There is nothing left
302
+ here for a detection to decide, and a second implementation of one could not be
303
+ tested and would drift.
301
304
 
302
305
  **Serving is the engine's to do; publishing is never.** A `remote` reader
303
306
  authorises a local server — one process, ended by one flag, leaving nothing
@@ -27,8 +27,10 @@ clobber customised content. Finish with a summary of created vs already-present.
27
27
  > **Leave alone**. Non-interactively it only adds what's missing; `--resync` /
28
28
  > `--reset` (reset needs `--yes`) drive the stronger actions.
29
29
 
30
- The system is **twelve skills**: `spec` (feature), `spec-bug` (bug),
31
- `spec-hotfix` (a bug fixed on a released tag), `spec-review`, `spec-start`,
30
+ The system is **thirteen skills**: `spec` (feature), `spec-bug` (bug),
31
+ `spec-hotfix` (a bug fixed on a released tag),
32
+ `no-spec` (work that genuinely has none — off the base branch all the same),
33
+ `spec-review`, `spec-start`,
32
34
  `spec-next`, `spec-diff` (read a spec's diff as a page),
33
35
  `spec-reviewed` (pick up a review you approved on that page), `spec-to-main`,
34
36
  `spec-complete`, `spec-cancel`, and this `spec-init`. The lifecycle is `backlog → in-progress → complete / cancelled`,