@skitterbyte/skitterspec 22.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 (37) hide show
  1. package/README.md +14 -0
  2. package/assets/claude-md-section.md +13 -0
  3. package/assets/commands/allow-main.md +33 -0
  4. package/assets/commands/spec-skip.md +24 -0
  5. package/assets/core/env.config.json.example +2 -1
  6. package/assets/core/env.config.md +83 -2
  7. package/assets/hooks/main-guard.cjs +145 -0
  8. package/assets/hooks/review-gate.cjs +8 -3
  9. package/assets/review/page.html +132 -2
  10. package/assets/rules/offered-unblocks.md +100 -0
  11. package/assets/rules/spec-planning.md +160 -11
  12. package/assets/rules/spec-reports.md +50 -9
  13. package/assets/skills/no-spec/SKILL.md +219 -0
  14. package/assets/skills/spec/SKILL.md +135 -36
  15. package/assets/skills/spec-bug/SKILL.md +5 -0
  16. package/assets/skills/spec-cancel/SKILL.md +29 -0
  17. package/assets/skills/spec-complete/SKILL.md +25 -0
  18. package/assets/skills/spec-diff/SKILL.md +33 -1
  19. package/assets/skills/spec-init/SKILL.md +4 -2
  20. package/assets/skills/spec-next/SKILL.md +47 -2
  21. package/assets/skills/spec-reviewed/SKILL.md +3 -3
  22. package/assets/skills/spec-start/SKILL.md +14 -0
  23. package/package.json +1 -1
  24. package/src/cli.js +702 -59
  25. package/src/env/config.js +121 -4
  26. package/src/env/hooks.js +102 -33
  27. package/src/env/live.js +30 -1
  28. package/src/env/mainguard.js +240 -0
  29. package/src/env/provision.js +106 -10
  30. package/src/env/registry.js +70 -6
  31. package/src/env/resolve.js +92 -3
  32. package/src/env/review.js +324 -20
  33. package/src/env/reviewers/coderabbit.js +228 -0
  34. package/src/env/reviewers.js +517 -0
  35. package/src/env/serve.js +189 -44
  36. package/src/env/teardown.js +12 -1
  37. package/src/init.js +125 -15
@@ -0,0 +1,100 @@
1
+ # Offered Unblocks
2
+
3
+ A refusal says what is wrong. Some refusals can also say **what would fix it**,
4
+ and where the engine declares that, a skill may offer it rather than leaving the
5
+ operator at a dead end.
6
+
7
+ This file is the whole contract. A skill's own text says only *that* it acts on
8
+ an offer, never how.
9
+
10
+ ## The engine declares; the skill relays
11
+
12
+ An offer is a field on a refusal, never a judgement a caller makes from the
13
+ reason text:
14
+
15
+ ```json
16
+ {
17
+ "blocked": true,
18
+ "reason": "feat-x's worktree has uncommitted changes — commit or stash them…",
19
+ "offer": { "kind": "satisfy", "label": "Commit first, then go live", "command": "/commit" }
20
+ }
21
+ ```
22
+
23
+ **Absent stays absent.** Most refusals carry no `offer` at all, and that is the
24
+ correct, common answer. Where there is none, relay the refusal and stop.
25
+
26
+ **Never infer one.** If the engine did not declare an offer, there is not one —
27
+ however obviously the reason seems to name a fix. The boundary that matters most
28
+ lives in that decision, and it must not be re-litigated per skill:
29
+ **work that is not the operator's is never offered up.** A live workbench another
30
+ spec holds is that spec's operator's to free, so it declares nothing, and a
31
+ helpful picker is exactly how that would creep back in.
32
+
33
+ ## Two kinds
34
+
35
+ | `kind` | Means | Recorded |
36
+ |--------|-------|----------|
37
+ | `satisfy` | do what the guard asked for | no — nothing was weakened |
38
+ | `bypass` | step past a guard still unsatisfied | **yes** |
39
+
40
+ ## Relay first, then offer
41
+
42
+ In that order, and never instead. A picker that replaced the refusal would hide
43
+ what is wrong behind a choice about what to do next — and the refusal text is
44
+ where every exit is named, including the ones the picker does not offer.
45
+
46
+ Then, only where `offer` is present, put **two** options to the operator: the
47
+ offer's `label`, and Cancel.
48
+
49
+ - **`satisfy`** — run the command, then retry the original operation.
50
+ - **`bypass`** — run the command, then carry on.
51
+ - **Cancel** — stop. Record nothing: declining to bypass is not a decision
52
+ anyone needs to audit.
53
+
54
+ **The label is the engine's, shown verbatim.** Never reword a bypass into
55
+ something easier-sounding, never pre-select it, and never add a third option. A
56
+ bypass that arrives pre-selected is a bypass by default wearing a question mark.
57
+
58
+ ## A bypass is offered once per obligation
59
+
60
+ The engine enforces this; a skill does not count anything. Spend the offer by
61
+ recording it **when you raise the picker**:
62
+
63
+ ```
64
+ skitterspec spec-env review gate <spec> --offered
65
+ ```
66
+
67
+ After that the gate declares no offer for the same arming, so the next refused
68
+ commit is a plain refusal with its exits named in the text. A new phase ending
69
+ arms the gate again and is asked in its own right.
70
+
71
+ **Spent by being made, not by being taken.** An operator who declines still saw
72
+ it, and re-offering on every attempt is how a deliberate step becomes the
73
+ default.
74
+
75
+ **Do not spend it by reading.** `review gate --json` is asked on every
76
+ `/spec-next` run; only raising the picker spends the offer.
77
+
78
+ ## What this is not
79
+
80
+ **Not a `--force`.** One was asked for and rejected: a reasonless lift is
81
+ indistinguishable from nobody having looked, where
82
+ `none: additive, nothing to revert` is a decision a reviewer can argue with. The
83
+ gate's exits are unchanged — a committing verdict, or a recorded skip — and a
84
+ bypass is the second of those at an address a person will reach.
85
+
86
+ **Not a replacement for the refusal.** A picker exists only in a harness. The
87
+ engine and the `PreToolUse` hook keep refusing regardless, so this is a layer on
88
+ top of a guard and never the guard itself.
89
+
90
+ **Not Claude lifting a guard aimed at Claude.** `spec-planning.md` draws that
91
+ line for `/allow-main` and `/spec-skip`, and a bypass offer survives it because
92
+ three things hold at once, where a `--force` had none of them:
93
+
94
+ 1. **The operator answers.** Claude may raise the question and cannot answer it.
95
+ 2. **The choice is recorded** — a fixed reason, distinct from anything typed,
96
+ which shows on the next render as history.
97
+ 3. **It is offered once**, held by the sidecar rather than by prose, so it
98
+ survives a `/clear`, a compaction and a fresh session.
99
+
100
+ Remove any one of those and the objection returns.
@@ -7,9 +7,10 @@ and lifecycle stay consistent. Each sets a status on the spec header
7
7
 
8
8
  | Skill | Purpose | Status | Folder |
9
9
  |-------|---------|--------|--------|
10
- | `/spec` | (Feature) Grill to a clear shared understanding, then write a groomed spec | `Ready` (or `Draft`) | `specs/backlog/` |
10
+ | `/spec` | (Feature) Grill to a clear shared understanding, write a groomed spec in its own worktree, then land it on the verdict | `Ready` (or `Draft`) | `specs/backlog/` (on the base branch, by a fast-forward) |
11
11
  | `/spec-bug` | (Bug) Reproduce with a failing test, capture spec, drive red→green | `In Progress` | `specs/in-progress/` |
12
12
  | `/spec-hotfix` | (Hotfix) Fork a worktree from a release tag, red→green, land by tag + cherry-pick | `In Progress` | `specs/in-progress/` |
13
+ | `/no-spec` | Work that genuinely has no spec — its own branch, worktree, page and land. Writes no spec and moves nothing through the lifecycle | `—` | (none) |
13
14
  | `/spec-review` | Re-validate a spec against the codebase; refresh stale parts | `—` | (unchanged) |
14
15
  | `/spec-start` | Put a spec in flight — provision its branch, then build phase 1 | `In Progress` | `specs/in-progress/` |
15
16
  | `/spec-next` | Build the next phase of the spec this session is in (re-run per phase) | `In Progress` (unchanged) | (unchanged) |
@@ -52,6 +53,81 @@ anything — scoped to the window, refusing when two arrive. `/spec-reviewed` is
52
53
  what answers everything outside it: a pass sent when nobody was waiting, two
53
54
  passes to choose between, and every harness with no file-watch to wait with.
54
55
 
56
+ **`/no-spec` is the one row that writes no spec**, and it is in the table
57
+ because it is the same lane: a branch, a worktree, a review page, a verdict, a
58
+ land. What it leaves out is the document, because mechanical work — a version
59
+ bump, a lockfile refresh, a rename — genuinely has none, and a one-phase spec
60
+ written for it is a document nobody reads. What it does **not** leave out is the
61
+ branch. Doing that work in the primary checkout leaves the base branch dirty for
62
+ every in-flight spec to replay over, and renders no page, so it is the one kind
63
+ of change that reaches the base branch unreviewed. It is model-invocable, which
64
+ matters: it is where Claude is sent when it would otherwise write on the base
65
+ branch. Beneath it, `skitterspec spec-env nospec <name>` records the branch —
66
+ and that record is what makes a name with nothing under `specs/**` resolvable by
67
+ `review`, `integrate`, `down` and a bare `resolve`.
68
+
69
+ **`/allow-main` is the fourth command.** It is user-only for a different reason
70
+ from the other three: `/spec-connect`, `/spec-live` and `/spec-remote-review`
71
+ are marked that way because there is no judgment to apply. This one is marked that
72
+ way because **Claude must not be able to lift a guard aimed at Claude.**
73
+
74
+ The guard is `.claude/hooks/main-guard.cjs`, a `PreToolUse` hook on the write
75
+ tools, and it refuses an `Edit` or a `Write` in the **primary checkout** while
76
+ it is on the **base branch**. `main` is where work lands, not where it happens:
77
+ work done there leaves the base branch dirty for every in-flight spec to replay
78
+ over, and renders no review page at all, so it is the one kind of change that
79
+ arrives unreviewed. It refuses the FIRST write rather than the commit, because
80
+ nothing has been written yet and the fix is one command — where a commit-time
81
+ gate fires once the work already exists in a checkout that cannot `switch -c`
82
+ out from under the other worktrees.
83
+
84
+ It fires only on a **positive signal** — isolation configured, the guard not
85
+ switched off, this directory IS the primary checkout, HEAD IS the base branch —
86
+ and every cannot-tell allows the write in silence: no repo, no config, a
87
+ worktree, an unresolvable base, a missing or crashed engine
88
+ (`.claude/rules/negative-checks.md`). `skitterspec spec-env main
89
+ <check|allow|status>` is the engine, and `guards.mainIsLandingZone` in
90
+ `env.config.json` turns it off for a project. It is
91
+ **on by default wherever isolation is configured**, matching the review gate.
92
+
93
+ There is **no allowlist**, and it is empty by construction rather than by
94
+ omission: `/spec` authors into its own worktree and `/no-spec` catches ad-hoc
95
+ work, so no legitimate writer to the base branch is left. `/spec-init` looks
96
+ like the exception and is not one — it bootstraps a repo with no
97
+ `env.config.json` yet, so the guard never fires there.
98
+
99
+ **Two exits, and they are not equal.** `/no-spec <name>` and `/spec-start
100
+ <name>` move the work, and Claude takes those on its own; `/allow-main` lifts
101
+ the guard, and only a person can type it. That asymmetry is the same line
102
+ `/spec-reviewed` draws, and `spec-planning.md` already records that prose alone
103
+ did not hold it once.
104
+
105
+ **`/spec-skip` is the fifth command**, and it is user-only for `/allow-main`'s
106
+ reason rather than the mild one: it lifts the review gate, which is a guard
107
+ aimed at Claude. It pre-executes `skitterspec spec-env review skip "<reason>"`
108
+ and relays it — the same exit that always existed, at an address a person will
109
+ actually type. The long form stays exactly as valid and is what the engine
110
+ prints; what changed is that a refusal now names something reachable, because a
111
+ gate whose exit nobody can find gets switched off wholesale instead of answered.
112
+
113
+ **It is not a `--force`, and one was asked for.** A reasonless lift reopens
114
+ precisely what the reason requirement closed: `none: additive, nothing to
115
+ revert` is a decision a reviewer can argue with, and silence is an oversight.
116
+ So a bare `/spec-skip` refuses and asks for a reason, and the two things that
117
+ discharge a phase's obligation are still a committing verdict and a recorded
118
+ skip — never a flag. Putting the flag on `/commit` was the other suggestion and
119
+ is worse: `/commit` belongs to skittership, which is why the gate is enforced
120
+ by a hook rather than by editing that skill.
121
+
122
+ **A refusal may OFFER its own exit, and that is a different thing again.**
123
+ Where the engine declares one, a skill relays the refusal and **then** offers it
124
+ — once per obligation, with the choice recorded, and answered by the operator
125
+ rather than by Claude. `.claude/rules/offered-unblocks.md` is the whole
126
+ contract. The three properties named there are what let a Claude-raised bypass
127
+ survive the rule above, where a reasonless `--force` did not: the operator
128
+ answers, the choice is on the record, and it is offered once. Remove any one and
129
+ the objection returns.
130
+
55
131
  `/spec-to-main`, `/spec-status` and `/spec-sync` stay **skills** — each carries
56
132
  real judgment (green tests before a land; an MCP fetch and a team-key check; ten
57
133
  subcommands) — but they are marked user-only too, since nobody reaches them
@@ -98,7 +174,7 @@ your canonical `localhost` ports so you can test it at the normal URL
98
174
  move, header edits, the code) happens on the spec's branch in the worktree; `main`
99
175
  changes only when it merges. Teardown is folded into `/spec-complete` ·
100
176
  `/spec-cancel`. Beneath the skills, `skitterspec spec-env
101
- <up|down|prune|dev|connect|integrate|hotfix|live|review|stage|status|resolve>` is the CLI
177
+ <up|nospec|main|down|prune|dev|connect|integrate|hotfix|live|review|stage|status|resolve>` is the CLI
102
178
  engine. **Omit the spec name anywhere and it uses the worktree you are standing in**,
103
179
  else the sole provisioned spec — with no exceptions left: a bare
104
180
  `/spec-connect` connects, and a bare `/spec-live` takes, where both once meant
@@ -129,11 +205,42 @@ stateful ones (`Stack: worktree + docker`, or a branch touching migrations) —
129
205
  `/spec-connect` + a Docker stack for those, and for genuinely parallel testing.
130
206
  Beneath it, `skitterspec spec-env live <take|release|abort|status>` is the engine.
131
207
 
132
- **Staging one spec and not another (`spec-env stage`).** Several specs are
133
- authored at once — that is what the worktree mode is for — and they all land in
134
- the same `specs/` folder until they are committed. A `git add specs/` there
135
- stages a *directory*, sweeping whatever another session happens to have written
136
- into this spec's commit, under this spec's ticket trailer.
208
+ **Authoring a spec is work, so it happens off the base branch too (`/spec`).**
209
+ `main` is where work **lands**, not where it happens, and a spec document is no
210
+ exception. With isolation configured, `/spec` grills first — which puts nothing
211
+ on disk — then provisions the new spec's own worktree with
212
+ `spec-env up <name> --docs`, moves the session into it, and writes the spec
213
+ there. `--docs` skips the `setup` commands and Docker, so a tree to write
214
+ markdown in costs one `git worktree add`; `/spec-start` re-runs `up` without the
215
+ flag and the install happens then, at the first moment the tree holds code.
216
+
217
+ **The committing verdict is what puts it on the base branch.** Phase C2's page
218
+ offers `Commit & Start` and `Commit`, and **both** commit on the branch and then
219
+ `spec-env integrate` — rebase, fast-forward — so the base gains the spec as one
220
+ commit. That is not optional tidiness: `ls specs/backlog/` is how specs are
221
+ found, and a folder bucket is only the truth on the branch you are standing on,
222
+ so a spec left unlanded is invisible to everyone. They differ only in the
223
+ worktree: `Commit & Start` keeps it and carries into `/spec-start`, plain
224
+ `commit` tears it down, because a spec parked in the backlog should hold no
225
+ checkout.
226
+
227
+ **What this bought is the reason it changed.** The page waits for a verdict
228
+ without a timeout — deliberately, because a reader walking away from a diff is
229
+ the normal case — and while the spec was authored in the primary checkout that
230
+ wait held the base branch dirty for as long as it lasted, so a release could not
231
+ be cut through it. Now an unanswered spec sits on its own branch in its own
232
+ worktree, in nothing's way. Phase C2 still **arms nothing**: the gate asserts
233
+ that a phase which ended owes an answer, and a backlog spec owes no phase — and
234
+ walking away from it now costs nobody anything rather than leaving a mess
235
+ someone else has to work around.
236
+
237
+ With isolation **absent** none of this applies: there is no worktree to make and
238
+ no land to do, so `/spec` writes the folder where the session is standing,
239
+ exactly as it always did.
240
+
241
+ **Staging one spec and not another (`spec-env stage`).** A `git add specs/`
242
+ stages a *directory*, sweeping whatever else happens to be sitting in it into
243
+ this spec's commit, under this spec's ticket trailer.
137
244
  `skitterspec spec-env stage [<spec>] [--json]` is how a skill asks instead of
138
245
  guessing: it splits the uncommitted tree into the paths that are this spec's —
139
246
  its folder in **any** bucket, so a tree mid-`git mv` is handled, plus each
@@ -144,6 +251,14 @@ the primary checkout outside, and prints which. The `owned` half is the spec's
144
251
  the commits this bounds are the lifecycle ones. Nothing here refuses, writes, or
145
252
  exits non-zero — `foreign` is a list of paths to leave alone, not an accusation.
146
253
 
254
+ **And it stays load-bearing now that `/spec` authors in its own worktree**, even
255
+ though that tree holds one spec. It was once justified by several specs sharing
256
+ one `specs/` folder, and that justification is gone; what replaces it is smaller
257
+ and just as real. A project's `spec.companionPaths`, a hand-edited
258
+ `specs/.core/`, and a tracker snapshot all land in the same tree as the spec, and
259
+ only the spec's own documents belong under its authoring verdict. Where isolation
260
+ is absent the original reason applies unchanged.
261
+
147
262
  **Naming the paths is necessary and not sufficient**, which is why every spec
148
263
  commit is also pathspec-limited: `git add -- <paths>` **and**
149
264
  `git commit -m "…" -- <paths>`. A checkout has one `.git/index` and every session
@@ -237,6 +352,34 @@ silently. All of it lives beside the page in gitignored `.spec-env/`, and
237
352
  **the marks are information, never a gate**: nothing counts them and nothing
238
353
  refuses on them.
239
354
 
355
+ **A second opinion, from something that did not write the code (`review.reviewers`).**
356
+ The page can also carry findings from code reviewers
357
+ that run from a CLI — CodeRabbit's has a free tier, PR-Agent and Kodus are
358
+ self-hosted — and they arrive as **checks**, the same shape a written review
359
+ uses. `/spec-next` runs them once per phase before the render, so the page is
360
+ populated when it is opened; `/spec-diff --reviewers` is the opt-in mid-phase
361
+ run, and a bare `/spec-diff` never spends one. Results are cached against a hash
362
+ of the diff, so re-reading unchanged work costs nothing.
363
+
364
+ **They inform; they never gate.** A check is not a comment, so a page carrying
365
+ twenty findings still offers `Commit` — and a reader who replies to one creates
366
+ a comment, which does gate, because then a person has asked for something. There
367
+ is deliberately no severity threshold that blocks: that is the counting gate
368
+ this whole design exists against, wearing a vendor's name.
369
+
370
+ **And a reviewer that could not run says so**, which is the one place
371
+ `.claude/rules/negative-checks.md` inverts. Silence is normally the safe branch;
372
+ here a rate-limited or unauthenticated reviewer would render identically to one
373
+ that read the diff and approved of it, on the page a commit decision is made
374
+ from. So each gets a line — `12 findings` · `clean` · `did not run — <why>` —
375
+ and none of it ever refuses a render, a verdict or a commit.
376
+
377
+ The list is **empty by default** and `init` never writes one: configuring a
378
+ hosted reviewer sends the worktree's diff to a third party, which is a real
379
+ change for a tool that otherwise touches nothing outside `.spec-env/`. Every
380
+ finding on the page is badged with the reviewer that produced it. `env.config.md`
381
+ carries the field reference.
382
+
240
383
  **The verdict is chosen, never derived.** A verdict is one person's conclusion,
241
384
  sent once per pass; a counting gate is a refusal computed from how many boxes
242
385
  are ticked, and it stays forbidden. The one refusal here is not a count of
@@ -311,7 +454,7 @@ its verdicts and its fields.
311
454
  | **Tracker** | [ABC-88](https://example.invalid/ABC-88) · `feat-orders` · phase 2 moved |
312
455
  | **Branch** | `spec/feat-orders` · 3 commits, clean |
313
456
  | **Built** | POST /orders handler, orders schema |
314
- | **Tests** | 128 passed · npm test |
457
+ | **Tests** | 128 passed · dotnet test |
315
458
  | **Review** | 7 files, +212 −18 · **local** [http://127.0.0.1:7760/…](http://127.0.0.1:7760/…) · **network** [http://192.168.0.136:7760/…](http://192.168.0.136:7760/…) · **remote** off |
316
459
  | **Follow-ups** | none |
317
460
  | **Next** | `/spec-next` → phase 3 (Auth) |
@@ -413,9 +556,15 @@ The spec skills tell you to run "your project's typecheck and test commands" and
413
556
  to "honour project conventions". Make those concrete here so specs stay
414
557
  consistent with the codebase:
415
558
 
416
- - **Typecheck command:** `<e.g. npm run typecheck>`
417
- - **Test command:** `<e.g. npm test>` (single file/dir: `<e.g. npx vitest run path>`)
418
- - **Lint/format:** `<e.g. npm run lint>`
559
+ Skitterspec never runs these itself and knows nothing about what this project
560
+ is written in — it shells out to `git`, and to whatever you write below. The
561
+ examples span several stacks on purpose: pick the shape that fits, or write one
562
+ none of them describes.
563
+
564
+ - **Typecheck command:** `<e.g. npm run typecheck · dotnet build · mypy . · go vet ./...>`
565
+ - **Test command:** `<e.g. npm test · dotnet test · pytest · go test ./...>`
566
+ (single file/dir: `<e.g. npx vitest run path · dotnet test --filter · pytest path>`)
567
+ - **Lint/format:** `<e.g. npm run lint · dotnet format · ruff check · golangci-lint run>`
419
568
  - **Other rules specs must honour:** link the relevant `.claude/rules/*.md`
420
569
  (architecture, code style, testing, database, etc.) rather than restating them.
421
570
 
@@ -35,7 +35,7 @@ A verdict sentence, a blank line, then a two-column table:
35
35
  | **Tracker** | [ABC-88](https://example.invalid/ABC-88) · `feat-foo` · phase 2 moved |
36
36
  | **Branch** | `spec/feat-foo` · 3 commits, clean |
37
37
  | **Built** | POST /orders handler, orders schema |
38
- | **Tests** | 128 passed · npm test |
38
+ | **Tests** | 128 passed · dotnet test |
39
39
  | **Review** | 7 files, +212 −18 · **local** [http://127.0.0.1:7760/…](http://127.0.0.1:7760/…) · **network** [http://192.168.0.136:7760/…](http://192.168.0.136:7760/…) · **remote** off |
40
40
  | **Follow-ups** | none |
41
41
  | **Next** | `/spec-next` → phase 3 (Auth) |
@@ -201,6 +201,42 @@ escape hatch is what let a whole class of runs go on asking questions nobody was
201
201
  listening for. Where a watch is unavailable the wait is the turn ending, which
202
202
  every harness can do.
203
203
 
204
+ **A watch that dies is re-armed, silently, on the same window.** A backgrounded
205
+ wait does not always survive a long idle: three sessions lost theirs over one
206
+ lunch break, and two in another were killed at 43 and 30 minutes. So a wait that
207
+ ends **without a verdict** and **was not stopped by the operator** is started
208
+ again on the **same** `--since`. Not a fresh one — a pass that arrived during
209
+ the gap is still inside the original window, and re-arming on a new timestamp
210
+ would strand exactly that pass.
211
+
212
+ **Say nothing when you do it.** Nothing happened and the window is unchanged; a
213
+ line on every death turns a working loop into a stream of non-events.
214
+
215
+ **An operator who stopped it is not a death.** Re-arming over a deliberate stop
216
+ makes the wait unstoppable, which is worse than a wait that stops. Where the two
217
+ cannot be told apart, treat it as deliberate and leave it
218
+ (`.claude/rules/negative-checks.md` rule 4): the cost of being wrong is one
219
+ `/spec-reviewed`, against a loop nobody can break.
220
+
221
+ **It is bounded by the age of the window, not by a count of retries.** Keep
222
+ re-arming while the window is younger than **12 hours**, then stop. A count means
223
+ something different depending on whether deaths come five minutes or forty-five
224
+ apart, and the observed interval already varies that much. Twelve hours
225
+ deliberately does not cover an overnight gap — the pass is never lost, and
226
+ `/spec-reviewed` is the designed recovery — because the alternative is waking
227
+ the session every half hour all night for a review nobody is returning to
228
+ before morning.
229
+
230
+ **Past the bound the banner degrades**, to the sentence this file already
231
+ defines for a transport that cannot push into the conversation:
232
+
233
+ *press a verdict, then type `/spec-reviewed` — I cannot see it until you do.*
234
+
235
+ That is a new **state** for an existing sentence, not a new sentence. The
236
+ holding line is only ever printed while a wait is actually running, which is the
237
+ rule this section has always carried: *only promise a wait the transport can
238
+ deliver.*
239
+
204
240
  **The `Continue` ending, and why the banner's exits differ mid-run.** A page
205
241
  rendered part-way through a run offers `Continue` — *I have read it, carry on* —
206
242
  in place of the committing verdicts, because "commit" is the wrong verb for
@@ -422,10 +458,10 @@ fenced block **above** the table, where a fence means what a fence should mean:
422
458
 
423
459
  | | |
424
460
  |---|---|
425
- | **Why** | 3 assertions fail in `orders.test.js`; nothing committed |
461
+ | **Why** | 3 assertions fail in `test_orders.py`; nothing committed |
426
462
  | **Branch** | `spec/feat-foo` · 2 commits, dirty |
427
463
  | **Built** | POST /orders handler, orders schema |
428
- | **Tests** | 125 passed, 3 failed · npm test |
464
+ | **Tests** | 125 passed, 3 failed · pytest |
429
465
  | **Follow-ups** | none |
430
466
  | **Next** | fix the failures, then `/spec-next` to finish phase 2 |
431
467
 
@@ -452,11 +488,16 @@ finish, so it outlives the session:
452
488
  canonical port; not in scope here.
453
489
  ```
454
490
 
455
- **Say where the spec would be written.** These skills run inside the spec's own
456
- worktree, and a spec authored there physically lives on that branch — invisible
457
- from the base branch, and cancelled along with its host if the host is
458
- cancelled. So the offer names the primary checkout as the place to write it.
459
- `spec-planning.md` and `commit-trailers.md` carry the reasoning.
491
+ **The offer needs no caveat about where.** These skills run inside the spec's own
492
+ worktree, and a follow-up spec authored *there* would physically live on that
493
+ branch — invisible from the base branch, and cancelled along with its host. That
494
+ used to make the offer name the primary checkout as the place to write it.
495
+ It no longer does: `/spec` provisions the **new** spec's own worktree before it
496
+ writes anything and moves the session there, so the follow-up lands on its own
497
+ branch and its own ticket without anyone naming a location. Take the offer and
498
+ let `/spec` handle it. Where isolation is absent there is no worktree to move to,
499
+ and the old caveat applies — `spec-planning.md` and `commit-trailers.md` carry
500
+ the reasoning.
460
501
 
461
502
  ## Worked examples
462
503
 
@@ -468,7 +509,7 @@ cancelled. So the offer names the primary checkout as the place to write it.
468
509
  |---|---|
469
510
  | **Branch** | `spec/feat-foo` · 4 commits, clean |
470
511
  | **Built** | Auth middleware, session table |
471
- | **Tests** | 140 passed · npm test |
512
+ | **Tests** | 140 passed · go test ./... |
472
513
  | **Follow-ups** | Session expiry is read from two places; phase 4 leans on one |
473
514
  | **Next** | `/spec-next` → phase 4 (Docs) |
474
515
 
@@ -0,0 +1,219 @@
1
+ ---
2
+ name: no-spec
3
+ description: Do a piece of work that genuinely has no spec — a version bump, a lockfile refresh, a rename — on its own branch in its own worktree, reviewed on a page and landed on the verdict. Never on the base branch. Use when the user says "/no-spec", or asks for mechanical work that does not warrant a spec and should not sit on main.
4
+ ---
5
+
6
+ # /no-spec — work with no spec, off the base branch
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
+ Some work has no spec and should not have one. Bumping four projects to a new
14
+ version, refreshing a lockfile, a rename across a package: writing a one-phase
15
+ document for it produces a spec nobody reads, and `spec --quick` would have been
16
+ that under a friendlier name.
17
+
18
+ **What it is not is an excuse to work on the base branch.** Two things follow
19
+ from doing it there, and both cost someone else something: the tree is left
20
+ dirty, so every in-flight spec has to replay over it and a release cannot be
21
+ cut; and no page is ever rendered, so the work gets no review at all. This skill
22
+ is the same lane every spec uses — branch, worktree, page, verdict, land —
23
+ with the document left out because there is nothing to put in it.
24
+
25
+ **It writes no spec, moves nothing through the lifecycle, and mints no ticket.**
26
+ There is no bucket, no status, no phase index and no `Refs:` trailer. If you
27
+ find yourself wanting any of those, the work wanted `/spec`.
28
+
29
+ ## 1. Name it
30
+
31
+ Take a kebab-case name from the invocation. It becomes the branch
32
+ (`chore/<name>`) and the worktree folder, so it should describe the work:
33
+ `bump-deps`, `rename-widget-props`, `lockfile-refresh`.
34
+
35
+ **Ask if none was given.** Do not invent one — a name nobody chose is a branch
36
+ nobody recognises a week later, and this is the one thing the skill cannot infer
37
+ from the work.
38
+
39
+ **Refuse a name that is already a spec**, which the engine does for you: two
40
+ things answering to one name is how a teardown removes the wrong tree.
41
+
42
+ ## 2. Provision
43
+
44
+ ```
45
+ skitterspec spec-env nospec <name>
46
+ ```
47
+
48
+ It records the name as a **specless branch** and prints the plan. Run the
49
+ commands it prints, then move this session into the worktree with a plain `cd`
50
+ — the Bash working directory persists between calls, so from here on the work
51
+ happens on its own branch.
52
+
53
+ **Confirm the move landed** rather than reading silence as success
54
+ (`.claude/rules/negative-checks.md` rule 1): `skitterspec spec-env resolve` with
55
+ no argument must name this branch, and report `no spec` as its bucket. If it
56
+ names something else, stop — say so and do nothing further, because the failure
57
+ this guards against is a session that writes a branch's worth of work into the
58
+ primary checkout while everything looks normal at the time.
59
+
60
+ **The record is not bookkeeping.** It is the only thing that makes a name with
61
+ nothing under `specs/**` resolvable, so without it `review`, `integrate`, `down`
62
+ and a bare `resolve` would all refuse this branch as a spec that does not exist.
63
+
64
+ **Where isolation is not configured** (`specs/.core/env.config.json` absent)
65
+ there is no worktree to make. Say so in one line and stop: this skill exists to
66
+ move work off the base branch, and without isolation there is nowhere to move it
67
+ to. Do not silently do the work in place — that is the outcome it exists to
68
+ prevent.
69
+
70
+ ## 3. Do the work
71
+
72
+ Follow the project's rules in `.claude/rules/*.md` and `CLAUDE.md`, exactly as a
73
+ phase would. Tests are part of the work and not after it: extend or add whatever
74
+ covers the change, then run the project's typecheck and test commands. Do not
75
+ proceed to the page until they are green — and if they cannot be made green, say
76
+ so where it happens and report `❌`.
77
+
78
+ **Mechanical does not mean untested.** A version bump that breaks a build is the
79
+ commonest thing in this lane, which is why the gate is the same one a phase has.
80
+
81
+ ## 4. Render the page and arm the gate
82
+
83
+ The work is done, the tests are green, and nothing is committed yet:
84
+
85
+ ```
86
+ skitterspec spec-env review <name> --buttons nospec --run-reviewers
87
+ ```
88
+
89
+ **This is free.** The engine reads git and splices text into a template; the
90
+ diff never passes through you however large it is.
91
+
92
+ `--buttons nospec` offers `Commit & Land` and `Commit`. It is its own set
93
+ because neither of the usual pairs fits: there is no next phase to continue and
94
+ nothing to put in flight.
95
+
96
+ **Then arm the gate:**
97
+
98
+ ```
99
+ skitterspec spec-env review arm <name>
100
+ ```
101
+
102
+ **This skill arms, and `/spec` does not.** That asymmetry is the rule in
103
+ `.claude/rules/spec-reports.md` rather than a choice made here: the gate asserts
104
+ that *finished work* owes an answer, and this is finished work. A written spec
105
+ owes no phase, so authoring arms nothing.
106
+
107
+ **Then offer `/spec-diff`. Do not run it.** The written review costs real tokens
108
+ because writing it means reading the diff, and that spend is the user's call.
109
+
110
+ ## 5. Wait for the verdict
111
+
112
+ **The wait is a command. Do not write one.**
113
+
114
+ 1. **Note the moment you start waiting**, as an ISO timestamp. That instant is
115
+ the whole scope of what you may claim.
116
+ 2. **Run the engine's wait in the background, and end your turn:**
117
+
118
+ ```
119
+ skitterspec spec-env review wait <name> --since <the timestamp>
120
+ ```
121
+
122
+ Pass no timeout. A reader who walks away from a diff is the normal case, and
123
+ a bounded watch once lost a verdict to a lunch break.
124
+ 3. **On waking, let the engine pick:**
125
+
126
+ **A watch that dies is re-armed on the same window**, silently and bounded by
127
+ the age of that window — `.claude/rules/spec-reports.md` carries the contract,
128
+ including when to stop and what the banner says instead. Do not restate it
129
+ here.
130
+
131
+ ```
132
+ skitterspec spec-env review <name> --claim-since <the timestamp> --json
133
+ ```
134
+
135
+ It claims the one pass that arrived inside the window, acts on nothing when
136
+ none did, and refuses to choose when two did.
137
+
138
+ Relay the engine's **stack** — the `local:`, `network:` and `remote:` lines, all
139
+ three, in that order — never the bare `page:` path.
140
+ `.claude/rules/spec-reports.md` carries the shape and why every tier is named.
141
+
142
+ ### Route on the verdict
143
+
144
+ - **`commit-land`** — hand off to `review.commitWith` (`/commit` by default),
145
+ then land it, then tear the worktree down:
146
+
147
+ ```
148
+ skitterspec spec-env integrate <name>
149
+ skitterspec spec-env down <name>
150
+ ```
151
+
152
+ `integrate` plans a rebase onto the base branch and a `merge --ff-only`; run
153
+ the commands it prints. `down` removes the worktree, deletes the branch and
154
+ **forgets the specless record**, which is what stops a torn-down name going on
155
+ answering to every verb with a path that is no longer there.
156
+
157
+ **If the rebase conflicts, stop.** `git rebase --abort`, leave the commit on
158
+ the branch, tear nothing down, and report `❌` with the conflict — there is a
159
+ standing worktree, which is what separates `❌` from `⏸`.
160
+
161
+ - **`commit`** — the same commit, and **stop there**. The branch stands with the
162
+ work on it, which is the right ending for work that turned out to be bigger
163
+ than one sitting. Say in `Next` that `/spec-to-main` lands it later.
164
+ - **`changes`** — work the notes, record a resolution for each one so the next
165
+ render shows it struck through with what changed, then
166
+ **re-render and wait again**. The reader is still holding a decision.
167
+ - **`discuss`** — report and talk. Claim nothing, change nothing, tear nothing
168
+ down.
169
+
170
+ **Never tear the worktree down on anything but `commit-land`.** It is the only
171
+ verdict that says the work is finished, and the cost of being wrong is somebody
172
+ else's branch removed from disk.
173
+
174
+ ## Report
175
+
176
+ End with the block defined in `.claude/rules/spec-reports.md`. That file carries
177
+ the shape; this section carries only what is specific here.
178
+
179
+ **Verdicts**
180
+
181
+ - `✅` — the work is done, the tests are green, and the verdict was acted on.
182
+ - `⚠️` — done and green, with something worth knowing.
183
+ - `❌` — the tests are red, or the land failed part-way. Quote the failure and
184
+ say what is standing.
185
+ - `⏸` — refused before acting: no name given, the name is already a spec, the
186
+ `cd` did not take, or the project has no isolation. Nothing changed.
187
+
188
+ **Fields:** `Branch` · `Built` · `Tests` · `Notes` · `Landed` · `Worktree` ·
189
+ `Review` · `Follow-ups` · `Next`
190
+
191
+ **No `Tracker` and no `Spec`, ever.** There is no ticket and no document, and a
192
+ row saying so would report an absence with nothing behind it — which
193
+ `spec-reports.md` forbids for exactly this reason.
194
+
195
+ `Landed` and `Worktree` appear only once a verdict has been acted on; a run
196
+ waiting on the page has neither, and **omits the `Review` row** in favour of the
197
+ banner, which carries the counts, the stack and the two exits:
198
+
199
+ ---
200
+
201
+ ## ⏸ Review ready — &lt;N&gt; files, +&lt;a&gt; −&lt;d&gt;
202
+
203
+ - **local** — &lt;the `local:` URL&gt;
204
+ - **network** — &lt;the `network:` URL, or off with the command that turns it on&gt;
205
+ - **remote** — &lt;the `remote:` URL, or off with the command that turns it on&gt;
206
+
207
+ I'm holding here until you send a verdict — the wait covers local and network.
208
+
209
+ `Commit & Land` finishes it · `Commit` keeps the branch
210
+
211
+ ---
212
+
213
+ There `Next` names the page rather than a command, because the button is what
214
+ carries the work on.
215
+
216
+ **`Follow-ups` is where a spec gets offered.** This lane is for work with no
217
+ spec, so the thing it most often surfaces is work that *does* want one. Offer to
218
+ `/spec` it on the spot — and `/spec` provisions its own worktree, so there is
219
+ no caveat about where to write it.