@skitterbyte/skitterspec 20.0.0 → 21.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.
- package/MIGRATION.md +86 -4
- package/assets/core/env.config.md +3 -2
- package/assets/review/page.html +58 -8
- package/assets/rules/spec-planning.md +5 -5
- package/assets/rules/spec-reports.md +84 -16
- package/assets/skills/spec-bug/SKILL.md +38 -23
- package/assets/skills/spec-diff/SKILL.md +56 -7
- package/assets/skills/spec-hotfix/SKILL.md +31 -20
- package/assets/skills/spec-init/SKILL.md +18 -6
- package/assets/skills/spec-next/SKILL.md +32 -10
- package/package.json +1 -1
- package/src/cli.js +29 -2
- package/src/env/hooks.js +49 -9
- package/src/env/review.js +42 -3
- package/src/init.js +28 -4
- /package/assets/hooks/{review-gate.js → review-gate.cjs} +0 -0
package/MIGRATION.md
CHANGED
|
@@ -1,5 +1,85 @@
|
|
|
1
1
|
# Migration guide
|
|
2
2
|
|
|
3
|
+
## `@skitterbyte/skitterspec` v20 → v21 (the gate installs, and asking implies waiting)
|
|
4
|
+
|
|
5
|
+
If you upgraded to v20 and the commit gate never once fired, this is why. Two
|
|
6
|
+
independent bugs, either of which alone made it a no-op.
|
|
7
|
+
|
|
8
|
+
### Breaking change
|
|
9
|
+
|
|
10
|
+
**The hook ships as `.claude/hooks/review-gate.cjs`**, renamed from
|
|
11
|
+
`review-gate.js`. The script is CommonJS and it is copied *into your* project,
|
|
12
|
+
where your `package.json` decides how node parses a `.js` — so in any
|
|
13
|
+
`"type": "module"` project it died on its own first `require`, printing a stack
|
|
14
|
+
trace on **every Bash tool call**. `.cjs` settles the parse mode at the file,
|
|
15
|
+
which is the only place independent of the one file skitterspec does not
|
|
16
|
+
control. An ESM rewrite would have inverted the same problem onto CommonJS
|
|
17
|
+
projects, which are still the default for anything with no `"type"` set.
|
|
18
|
+
|
|
19
|
+
The upgrade migrates you: your existing `PreToolUse` entry has its path
|
|
20
|
+
**rewritten in place** — so a wrapper, a flag or a different interpreter you
|
|
21
|
+
added all survive — and the retired `review-gate.js` is deleted. If you edited
|
|
22
|
+
that file yourself it is **kept**, with a warning, and left for you to remove.
|
|
23
|
+
|
|
24
|
+
### Bug fix
|
|
25
|
+
|
|
26
|
+
**`skitterspec update` now registers the hook.** v20's notes said `init` and
|
|
27
|
+
`update` both did; only `init` did. `update` copied the script, reported
|
|
28
|
+
`created: .claude/hooks/review-gate.js`, and wired nothing — so every project
|
|
29
|
+
that upgraded into v20 got a hook file and no hook, with nothing saying so.
|
|
30
|
+
|
|
31
|
+
### What to do
|
|
32
|
+
|
|
33
|
+
1. **Upgrade** — `npx @skitterbyte/skitterspec update`.
|
|
34
|
+
2. **Commit `.claude/settings.json` and `.claude/hooks/review-gate.cjs`**, and
|
|
35
|
+
the deletion of `.claude/hooks/review-gate.js`. A hook only a fraction of the
|
|
36
|
+
team has is a gate that holds for a fraction of the team.
|
|
37
|
+
3. **Check it is actually on** — the update reports
|
|
38
|
+
`updated: .claude/settings.json (review-gate hook)` the first time, and
|
|
39
|
+
`unchanged` afterwards. If it says neither, your settings file could not be
|
|
40
|
+
parsed; it was left untouched and the hook is not registered.
|
|
41
|
+
|
|
42
|
+
### Behaviour change — a bug fix and a hotfix now owe a verdict
|
|
43
|
+
|
|
44
|
+
**`/spec-bug` and `/spec-hotfix` now arm the review gate**, as `/spec-next`
|
|
45
|
+
already did. Both render the page at the end of red→green work, and both now
|
|
46
|
+
wait for your verdict instead of finishing — so in that spec's worktree a
|
|
47
|
+
`git commit` is **refused** until one of two things happens:
|
|
48
|
+
|
|
49
|
+
- you send a verdict from the page (or `/spec-reviewed` picks up one already
|
|
50
|
+
waiting), or
|
|
51
|
+
- you run `skitterspec spec-env review skip "<reason>"`, which records the
|
|
52
|
+
decision to move on.
|
|
53
|
+
|
|
54
|
+
The gate is still only armed by work that **finished**, it still fails open on
|
|
55
|
+
every cannot-tell, and `review.required: false` in `specs/.core/env.config.json`
|
|
56
|
+
still turns it off for a project.
|
|
57
|
+
|
|
58
|
+
**Why.** Those two skills used to render the page, ask *"want a written review
|
|
59
|
+
before you commit?"* and finish with nothing watching. A verdict pressed on that
|
|
60
|
+
page sat in the holding area until someone typed `/spec-reviewed` — which they
|
|
61
|
+
had no reason to do, because the run had said the page was *ready* rather than
|
|
62
|
+
that it was *waiting*. Two verdicts were stranded that way on one spec, and the
|
|
63
|
+
second existed only because the first appeared to do nothing.
|
|
64
|
+
|
|
65
|
+
### New, and not breaking — the `Continue` verdict
|
|
66
|
+
|
|
67
|
+
**A page rendered part-way through a run offers `Continue`** — *I have read it,
|
|
68
|
+
carry on* — instead of the committing buttons, via
|
|
69
|
+
`spec-env review <spec> --buttons midrun`. The default is unchanged, so a caller
|
|
70
|
+
that passes nothing renders exactly the page it rendered before. `Continue` can
|
|
71
|
+
never clear an armed gate: that still takes a committing verdict or a recorded
|
|
72
|
+
skip.
|
|
73
|
+
|
|
74
|
+
Nothing else changes: `review.required` still defaults to `true`, and the engine
|
|
75
|
+
and `/spec-next` held the gate throughout regardless of the hook.
|
|
76
|
+
|
|
77
|
+
## `@skitterbyte/skitterspec-linear` v14 → v15 (the gate installs, and asking implies waiting)
|
|
78
|
+
|
|
79
|
+
The same change as `@skitterbyte/skitterspec` v20 → v21 above — this
|
|
80
|
+
distribution composes the same lifecycle skills. Read that entry; nothing here
|
|
81
|
+
is Linear-specific.
|
|
82
|
+
|
|
3
83
|
## `@skitterbyte/skitterspec` v19 → v20 (a phase owes a verdict)
|
|
4
84
|
|
|
5
85
|
### Breaking change
|
|
@@ -41,8 +121,10 @@ cannot-tell lets the commit through.
|
|
|
41
121
|
### Breaking change
|
|
42
122
|
|
|
43
123
|
**`.claude/settings.json` is now written by the installer.** `skitterspec init`
|
|
44
|
-
and `skitterspec update` copy
|
|
45
|
-
|
|
124
|
+
and `skitterspec update` copy the hook script and register it as a `PreToolUse`
|
|
125
|
+
hook in your project's **committed** settings file. (In v20 `update` copied
|
|
126
|
+
without registering, and the script was named `review-gate.js` — both fixed in
|
|
127
|
+
v21; read that entry above if you are landing on the current release.) That is a
|
|
46
128
|
tracked file in most repos, so expect it in `git status` after upgrading — and
|
|
47
129
|
commit it, because a hook only a fraction of the team has is a gate that holds
|
|
48
130
|
for a fraction of the team.
|
|
@@ -76,8 +158,8 @@ as before.
|
|
|
76
158
|
### What to do
|
|
77
159
|
|
|
78
160
|
1. **Upgrade** — `npx @skitterbyte/skitterspec update`.
|
|
79
|
-
2. **Commit `.claude/settings.json` and
|
|
80
|
-
|
|
161
|
+
2. **Commit `.claude/settings.json` and the hook script.** Both are new in your
|
|
162
|
+
working tree after the update.
|
|
81
163
|
3. **Nothing else to configure.** `review.required` defaults to `true` and
|
|
82
164
|
`review.commitWith` defaults to `/commit`; neither needs adding unless you
|
|
83
165
|
are changing it.
|
|
@@ -74,8 +74,9 @@ no live `env.config.json` was found.
|
|
|
74
74
|
// `spec-env up`, right after `git worktree add` and BEFORE `setup` runs — so a
|
|
75
75
|
// fresh linked worktree (which starts with none of the repo's gitignored files)
|
|
76
76
|
// has the .env / local secret overrides / local config that setup steps and
|
|
77
|
-
// git hooks depend on. Without this
|
|
78
|
-
//
|
|
77
|
+
// git hooks depend on. Without this any setup step that reads .env — a schema
|
|
78
|
+
// or client generator, a codegen pass — hard-fails in the new worktree
|
|
79
|
+
// because the file it expects isn't there.
|
|
79
80
|
// mode "symlink" (default) points the worktree file at the main file, so it
|
|
80
81
|
// stays in sync; "copy" makes an independent copy.
|
|
81
82
|
// files repo-relative paths to seed. A source absent in main is a printed
|
package/assets/review/page.html
CHANGED
|
@@ -386,7 +386,13 @@ tr.note-row td {
|
|
|
386
386
|
.verdict-title { font-size: .8rem; color: var(--muted); }
|
|
387
387
|
.verdict-count { font-size: .8rem; color: var(--muted); }
|
|
388
388
|
.v-commit { color: var(--good-fg); background: var(--good-bg); }
|
|
389
|
+
/* Continue borrows the same affirmative pair: it is the mid-run reader saying
|
|
390
|
+
carry on, and it never appears beside a commit button to be confused with. */
|
|
391
|
+
.v-continue { color: var(--good-fg); background: var(--good-bg); }
|
|
389
392
|
.v-changes { color: var(--flag-fg); background: var(--flag-bg); }
|
|
393
|
+
/* A button the render did not ask for is GONE, not dimmed — a disabled control
|
|
394
|
+
invites a reader to work out how to enable it, and there is nothing to do. */
|
|
395
|
+
.verdict button[hidden] { display: none; }
|
|
390
396
|
/* A blocked control is dashed as well as dimmed, so the block survives a
|
|
391
397
|
greyscale screen — and the reason rides in the label, never in a tooltip
|
|
392
398
|
nobody hovers on a phone. */
|
|
@@ -570,6 +576,12 @@ button:disabled:hover { color: var(--muted); border-color: var(--line); }
|
|
|
570
576
|
<span class="verdict-title">Your verdict</span>
|
|
571
577
|
<button id="verdict-commit" class="v-commit" type="button" aria-live="polite">✓ Commit</button>
|
|
572
578
|
<button id="verdict-commit-continue" class="v-commit" type="button" aria-live="polite">✓ Commit & Continue</button>
|
|
579
|
+
<!-- THE MID-RUN VERDICT, hidden unless the render asked for it. "Commit"
|
|
580
|
+
is the wrong verb for unfinished work, so a page rendered part-way
|
|
581
|
+
through a run offers this instead of the committing pair: I have
|
|
582
|
+
read it, carry on. It is not the removed `none` verdict — that
|
|
583
|
+
recorded itself and did nothing, while this one resumes the run. -->
|
|
584
|
+
<button id="verdict-continue" class="v-continue" type="button" hidden>→ Continue</button>
|
|
573
585
|
<button id="verdict-changes" class="v-changes" type="button">↺ Request changes</button>
|
|
574
586
|
<button id="verdict-discuss" type="button">… Discuss first</button>
|
|
575
587
|
<span class="verdict-count" id="verdict-count" aria-live="polite"></span>
|
|
@@ -1216,6 +1228,7 @@ button:disabled:hover { color: var(--muted); border-color: var(--line); }
|
|
|
1216
1228
|
var verdictBtns = {
|
|
1217
1229
|
commit: document.getElementById('verdict-commit'),
|
|
1218
1230
|
'commit-continue': document.getElementById('verdict-commit-continue'),
|
|
1231
|
+
continue: document.getElementById('verdict-continue'),
|
|
1219
1232
|
changes: document.getElementById('verdict-changes'),
|
|
1220
1233
|
discuss: document.getElementById('verdict-discuss'),
|
|
1221
1234
|
}
|
|
@@ -1223,12 +1236,34 @@ button:disabled:hover { color: var(--muted); border-color: var(--line); }
|
|
|
1223
1236
|
// here, and the shared block below follows for free — the same reason the
|
|
1224
1237
|
// engine keeps a `COMMITTING` list rather than a second condition.
|
|
1225
1238
|
var COMMITTERS = ['commit', 'commit-continue']
|
|
1226
|
-
|
|
1239
|
+
|
|
1240
|
+
/*
|
|
1241
|
+
* WHICH BUTTONS THIS PAGE OFFERS, and it comes from the render rather than
|
|
1242
|
+
* from the gate. Deriving it — mid-run iff nothing is armed — would take the
|
|
1243
|
+
* committing buttons away from every page in a project running
|
|
1244
|
+
* `review.required: false`, which never arms and whose readers must still be
|
|
1245
|
+
* able to commit from the page. The engine's `BUTTON_SETS` is the same
|
|
1246
|
+
* decision on the other side of the wire.
|
|
1247
|
+
*
|
|
1248
|
+
* ABSENT IS THE COMMITTING SET. A page rendered before any of this existed,
|
|
1249
|
+
* and a caller that did not ask, both land here — so nothing about their page
|
|
1250
|
+
* changes.
|
|
1251
|
+
*/
|
|
1252
|
+
var MIDRUN = data.buttons === 'midrun'
|
|
1253
|
+
var OFFERED = MIDRUN ? ['continue', 'changes', 'discuss'] : ['commit', 'commit-continue', 'changes', 'discuss']
|
|
1254
|
+
Object.keys(verdictBtns).forEach(function (key) {
|
|
1255
|
+
verdictBtns[key].hidden = OFFERED.indexOf(key) === -1
|
|
1256
|
+
})
|
|
1257
|
+
|
|
1258
|
+
// EVERY VERDICT, because this is the one place a verdict gets a reader-facing
|
|
1227
1259
|
// name: `refresh` relabels only the committing pair, but the decided panel
|
|
1228
|
-
// has to name whichever verdict was actually sent
|
|
1260
|
+
// has to name whichever verdict was actually sent — including one this page
|
|
1261
|
+
// never offered, since a decision restored from storage outlives the render
|
|
1262
|
+
// that made it.
|
|
1229
1263
|
var LABELS = {
|
|
1230
1264
|
commit: '✓ Commit',
|
|
1231
1265
|
'commit-continue': '✓ Commit & Continue',
|
|
1266
|
+
continue: '→ Continue',
|
|
1232
1267
|
changes: '↺ Request changes',
|
|
1233
1268
|
discuss: '… Discuss first',
|
|
1234
1269
|
}
|
|
@@ -1237,6 +1272,9 @@ button:disabled:hover { color: var(--muted); border-color: var(--line); }
|
|
|
1237
1272
|
// Says what it will NOT do: a reader must not press this expecting the
|
|
1238
1273
|
// spec to be finished and landed.
|
|
1239
1274
|
'commit-continue': 'Commit what you just read, then build the next phase — nothing is landed',
|
|
1275
|
+
// Says what it will NOT do, for the same reason: nothing is committed by
|
|
1276
|
+
// this, so the work it resumes is still uncommitted afterwards.
|
|
1277
|
+
continue: 'You have read it — carry on with the run; nothing is committed',
|
|
1240
1278
|
}
|
|
1241
1279
|
|
|
1242
1280
|
var verdictCount = document.getElementById('verdict-count')
|
|
@@ -1286,7 +1324,9 @@ button:disabled:hover { color: var(--muted); border-color: var(--line); }
|
|
|
1286
1324
|
var open = openNotes()
|
|
1287
1325
|
verdictCount.textContent = n
|
|
1288
1326
|
? n + (n === 1 ? ' mark to send' : ' marks to send')
|
|
1289
|
-
|
|
1327
|
+
// Names the action this page actually offers. A mid-run page saying "a
|
|
1328
|
+
// clean read still commits" would promise the one thing it cannot do.
|
|
1329
|
+
: MIDRUN ? 'Nothing marked — a clean read still carries on' : 'Nothing marked — a clean read still commits'
|
|
1290
1330
|
// ONE COUNT, BOTH BUTTONS. Two committing controls blocked by two separate
|
|
1291
1331
|
// reads is how a page ends up with one disabled and the other not — which
|
|
1292
1332
|
// would be a way around the single refusal this page makes.
|
|
@@ -1315,8 +1355,15 @@ button:disabled:hover { color: var(--muted); border-color: var(--line); }
|
|
|
1315
1355
|
: TITLES[key]
|
|
1316
1356
|
})
|
|
1317
1357
|
// Said rather than left unset: the committing pair is the ONLY thing this
|
|
1318
|
-
// page ever blocks, and the
|
|
1319
|
-
//
|
|
1358
|
+
// page ever blocks, and the others are always available on purpose — a note
|
|
1359
|
+
// you want acted on, and a question, are never the wrong thing to send.
|
|
1360
|
+
//
|
|
1361
|
+
// CONTINUE IS NOT BLOCKED, and that mirrors the engine rather than softening
|
|
1362
|
+
// it: `judgeVerdict` refuses a COMMITTING verdict against an open note, and
|
|
1363
|
+
// `continue` is deliberately not one. A disabled button here would be a
|
|
1364
|
+
// second opinion about a refusal the engine does not make, which is the one
|
|
1365
|
+
// thing this bar must never be.
|
|
1366
|
+
verdictBtns.continue.disabled = false
|
|
1320
1367
|
verdictBtns.changes.disabled = false
|
|
1321
1368
|
verdictBtns.discuss.disabled = false
|
|
1322
1369
|
save()
|
|
@@ -1400,6 +1447,7 @@ button:disabled:hover { color: var(--muted); border-color: var(--line); }
|
|
|
1400
1447
|
approve: 'committed',
|
|
1401
1448
|
commit: 'committed',
|
|
1402
1449
|
'commit-continue': 'committed, then carried on',
|
|
1450
|
+
continue: 'read it and carried on',
|
|
1403
1451
|
changes: 'changes requested',
|
|
1404
1452
|
discuss: 'discussed',
|
|
1405
1453
|
skip: 'moved on without a verdict',
|
|
@@ -1716,9 +1764,11 @@ button:disabled:hover { color: var(--muted); border-color: var(--line); }
|
|
|
1716
1764
|
|
|
1717
1765
|
Object.keys(verdictBtns).forEach(function (verdict) {
|
|
1718
1766
|
verdictBtns[verdict].addEventListener('click', function () {
|
|
1719
|
-
// A disabled button does not fire in a browser; the guard is
|
|
1720
|
-
// other caller, and it keeps the block
|
|
1721
|
-
|
|
1767
|
+
// A disabled or hidden button does not fire in a browser; the guard is
|
|
1768
|
+
// for every other caller, and it keeps both the block and the button set
|
|
1769
|
+
// facts rather than styles — a verdict this render never offered must not
|
|
1770
|
+
// be reachable by dispatching at it.
|
|
1771
|
+
if (verdictBtns[verdict].disabled || verdictBtns[verdict].hidden) return
|
|
1722
1772
|
send(verdict)
|
|
1723
1773
|
})
|
|
1724
1774
|
})
|
|
@@ -244,10 +244,10 @@ things clear it — a **committing verdict**, or
|
|
|
244
244
|
`git commit` refuses in that worktree. `skitterspec spec-env review gate
|
|
245
245
|
[--check] [--json]` is what both ask.
|
|
246
246
|
|
|
247
|
-
**The hook is the half that cannot be talked past.** `skitterspec init`
|
|
248
|
-
`.claude/hooks/review-gate.
|
|
249
|
-
`.claude/settings.json`, so a `git commit` is refused by
|
|
250
|
-
by prose — which is what covers a bare `git commit`, a chained command, and
|
|
247
|
+
**The hook is the half that cannot be talked past.** `skitterspec init` and
|
|
248
|
+
`skitterspec update` install `.claude/hooks/review-gate.cjs` and register it in
|
|
249
|
+
the project's committed `.claude/settings.json`, so a `git commit` is refused by
|
|
250
|
+
the harness rather than by prose — which is what covers a bare `git commit`, a chained command, and
|
|
251
251
|
skittership's own `/commit` without skitterspec ever editing it. It decides
|
|
252
252
|
nothing itself: it hands the command line to
|
|
253
253
|
`spec-env review gate --check --for-command` and turns one exit status into an
|
|
@@ -301,7 +301,7 @@ its verdicts and its fields.
|
|
|
301
301
|
| **Branch** | `spec/feat-orders` · 3 commits, clean |
|
|
302
302
|
| **Built** | POST /orders handler, orders schema |
|
|
303
303
|
| **Tests** | 128 passed · npm test |
|
|
304
|
-
| **Review** | 7 files, +212 −18 · [open the page](file:///…)
|
|
304
|
+
| **Review** | 7 files, +212 −18 · [open the page](file:///…) |
|
|
305
305
|
| **Follow-ups** | none |
|
|
306
306
|
| **Next** | `/spec-next` → phase 3 (Auth) |
|
|
307
307
|
|
|
@@ -36,7 +36,7 @@ A verdict sentence, a blank line, then a two-column table:
|
|
|
36
36
|
| **Branch** | `spec/feat-foo` · 3 commits, clean |
|
|
37
37
|
| **Built** | POST /orders handler, orders schema |
|
|
38
38
|
| **Tests** | 128 passed · npm test |
|
|
39
|
-
| **Review** | 7 files, +212 −18 · [open the page](file:///…)
|
|
39
|
+
| **Review** | 7 files, +212 −18 · [open the page](file:///…) |
|
|
40
40
|
| **Follow-ups** | none |
|
|
41
41
|
| **Next** | `/spec-next` → phase 3 (Auth) |
|
|
42
42
|
|
|
@@ -97,15 +97,54 @@ A rule, a heading naming the state and the size, the link in bold, one line
|
|
|
97
97
|
saying the run is stopped, and the two exits. Nothing else, and never a
|
|
98
98
|
paragraph explaining it.
|
|
99
99
|
|
|
100
|
-
**Only promise a wait the transport can deliver.**
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
100
|
+
**Only promise a wait the transport can deliver.** The wait itself is real in
|
|
101
|
+
every case; what varies is what carries it back, not whether the run is
|
|
102
|
+
stopped. Three transports, three true sentences:
|
|
103
|
+
|
|
104
|
+
- **A page the engine SERVES** hands its pass to the local store, which a
|
|
105
|
+
file-watch sees. The run is woken by the press:
|
|
106
|
+
*"I'm holding here until you send a verdict."*
|
|
107
|
+
- **A `file://` page** has no server to POST to, so the pass is copied and
|
|
108
|
+
pasted — the reader's next message is what carries it. The run is stopped just
|
|
109
|
+
the same, and says so:
|
|
110
|
+
*"I'm holding here — paste the pass when you have it."*
|
|
111
|
+
This is the same wait, carried by the conversation instead of a watch. It is
|
|
112
|
+
not a lesser one, and it is not an excuse to ask without waiting.
|
|
113
|
+
- **A PUBLISHED page** writes to the artifact's own store, and nothing pushes
|
|
114
|
+
from there into the conversation. That is the one case where a verdict is
|
|
115
|
+
genuinely invisible until someone asks for it, so the banner says that instead
|
|
116
|
+
of claiming a watch:
|
|
106
117
|
|
|
107
118
|
**[Open the page](https://…)** · press a verdict, then type `/spec-reviewed` — I cannot see it until you do.
|
|
108
119
|
|
|
120
|
+
**A harness with no file-watch does not get an exemption.** It once read
|
|
121
|
+
"change nothing" — keep the row, keep the question, do not wait — and that
|
|
122
|
+
escape hatch is what let a whole class of runs go on asking questions nobody was
|
|
123
|
+
listening for. Where a watch is unavailable the wait is the turn ending, which
|
|
124
|
+
every harness can do.
|
|
125
|
+
|
|
126
|
+
**The `Continue` ending, and why the banner's exits differ mid-run.** A page
|
|
127
|
+
rendered part-way through a run offers `Continue` — *I have read it, carry on* —
|
|
128
|
+
in place of the committing verdicts, because "commit" is the wrong verb for
|
|
129
|
+
unfinished work. Its banner names the same state and different exits:
|
|
130
|
+
|
|
131
|
+
**[Open the page](http://…)** · I'm holding here until you send a verdict.
|
|
132
|
+
|
|
133
|
+
`Continue` carries on · `Request changes` works them now
|
|
134
|
+
|
|
135
|
+
**It is not the `none` verdict that was removed.** `none` recorded itself and
|
|
136
|
+
did nothing, which is the record-and-do-nothing ending this contract exists
|
|
137
|
+
against. `Continue` **resumes the run** — it names an action, which is the bar
|
|
138
|
+
every verdict has to clear. What it does not do is commit, and it can never
|
|
139
|
+
clear the gate a finished phase armed: that gate is discharged by a committing
|
|
140
|
+
verdict or a recorded skip, and by nothing else.
|
|
141
|
+
|
|
142
|
+
**So waiting and arming are separate**, and only one of them is about this
|
|
143
|
+
block. *Waiting* is what any offer does, and it is what this section governs.
|
|
144
|
+
*Arming* asserts an obligation that outlives the turn, and belongs only to work
|
|
145
|
+
that is finished. A mid-run render waits without arming — walking away from it
|
|
146
|
+
costs nothing, which is exactly why `Continue` is safe to offer there.
|
|
147
|
+
|
|
109
148
|
**Exactly one link, never two.** The banner offers the page the reader can
|
|
110
149
|
actually use, and the engine has already decided which that is — a served URL
|
|
111
150
|
when it could serve, a published one only when it could not. Handing over both
|
|
@@ -124,11 +163,34 @@ stopped until they answer. A row cannot carry that, because a row is scanned at
|
|
|
124
163
|
the same weight as every other row. So the state gets the loudest shape on
|
|
125
164
|
screen, at the end, where reading finishes.
|
|
126
165
|
|
|
127
|
-
**
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
166
|
+
**ASKING IMPLIES WAITING — one rule, and the two shapes follow from it.** Any
|
|
167
|
+
render that asks the reader for a verdict ends the turn watching for one. A run
|
|
168
|
+
that does not intend to wait does not ask: its `Review` row keeps the counts and
|
|
169
|
+
the link and **loses the question**.
|
|
170
|
+
|
|
171
|
+
So there are two shapes and no third:
|
|
172
|
+
|
|
173
|
+
- **Asking** → the banner above, and the run waits.
|
|
174
|
+
- **Not asking** → the `Review` row: counts and link, no question.
|
|
175
|
+
|
|
176
|
+
**What this guards against is a reader taught that the button is decorative.**
|
|
177
|
+
`/spec-bug` and `/spec-hotfix` used to render the page, emit a row
|
|
178
|
+
asking *"want a written review before you commit?"*, and finish — with nothing
|
|
179
|
+
watching. A verdict pressed on that page landed in the holding area and stayed
|
|
180
|
+
there, because the run had told the reader the page was **ready** rather than
|
|
181
|
+
that it was **waiting**. It happened twice on one spec, and the second press
|
|
182
|
+
existed only because the first appeared to do nothing. An offer that cannot be
|
|
183
|
+
answered costs more than never having offered.
|
|
184
|
+
|
|
185
|
+
**Rejected: keeping the question and adding a caveat** — *"press a verdict, but
|
|
186
|
+
I will not see it until you ask"*. That is exactly the shape that stranded those
|
|
187
|
+
two passes, and a truthful caveat does not make an unanswerable question worth
|
|
188
|
+
asking. Either watch for the answer, or do not ask.
|
|
189
|
+
|
|
190
|
+
**A render nobody is being held for is still worth reporting** — a mid-phase
|
|
191
|
+
`/spec-diff`, a page produced alongside other work. It gets the row, because
|
|
192
|
+
giving it a banner teaches the reader to scroll past banners. The shape marks
|
|
193
|
+
the difference between *here is a page* and *nothing proceeds until you answer*.
|
|
132
194
|
|
|
133
195
|
**Never fence a message to the reader.** A fenced block is for a command to
|
|
134
196
|
run, code, or engine output quoted verbatim — things the reader copies or
|
|
@@ -185,7 +247,7 @@ behind it — say nothing rather than reporting that there was nothing to report
|
|
|
185
247
|
| `Landed` | A fast-forward, a tag, a cherry-pick. |
|
|
186
248
|
| `Worktree` | A worktree provisioned, entered, or torn down. |
|
|
187
249
|
| `Untouched` | Uncommitted work the run deliberately left alone — whose, and how much. |
|
|
188
|
-
| `Review` | The rendered diff page: files, `+`/`−`, the link —
|
|
250
|
+
| `Review` | The rendered diff page: files, `+`/`−`, the link. **No question** — a row cannot be waited on, so a question in one is unanswerable by construction (see *asking implies waiting*). **Omitted entirely when the run is waiting on a verdict**: the banner after the block carries it instead, and a row saying the same thing beside it splits the reader's attention across two places. |
|
|
189
251
|
| `Follow-ups` | **Always.** `none`, or one line each. |
|
|
190
252
|
| `Next` | **Last.** The single next action for this work — runnable from the state the run leaves behind. |
|
|
191
253
|
|
|
@@ -204,11 +266,17 @@ silent, because "I provisioned, and your four files are still sitting there" is
|
|
|
204
266
|
a different sentence from "I provisioned".
|
|
205
267
|
|
|
206
268
|
**`Review` is one row, not two** — and where the run is waiting, it is no rows
|
|
207
|
-
at all. The page and
|
|
208
|
-
|
|
209
|
-
|
|
269
|
+
at all. The page and what to do with it are the same subject, and splitting them
|
|
270
|
+
made the reader resolve a distinction before acting on either. A waiting run
|
|
271
|
+
promotes the whole subject into the banner; keeping the row as well would
|
|
210
272
|
recreate that split with the two halves further apart than ever.
|
|
211
273
|
|
|
274
|
+
**The row carries no question, and that is the rule above applied here.** It
|
|
275
|
+
once ended *"want a written review before you commit?"* — a question addressed
|
|
276
|
+
to a reader the run was not waiting for. Putting it in a findable row fixed
|
|
277
|
+
where it was; it did not fix that nothing was listening. A run that wants that
|
|
278
|
+
question answered waits for it, and waiting means the banner.
|
|
279
|
+
|
|
212
280
|
## The four verdicts
|
|
213
281
|
|
|
214
282
|
| State | Means |
|
|
@@ -208,7 +208,7 @@ is a no-op.
|
|
|
208
208
|
|
|
209
209
|
|
|
210
210
|
|
|
211
|
-
## 5b. Render the page
|
|
211
|
+
## 5b. Render the page, arm the gate, then wait for the verdict
|
|
212
212
|
|
|
213
213
|
**Only when the project has per-spec isolation** (`specs/.core/env.config.json`
|
|
214
214
|
present). Without it there is no worktree to read and this step does not exist —
|
|
@@ -224,28 +224,37 @@ skitterspec spec-env review <spec>
|
|
|
224
224
|
**This is free.** The engine reads git and splices the patches into a template;
|
|
225
225
|
the diff never passes through you, so a 266KB patch costs nothing.
|
|
226
226
|
|
|
227
|
-
**Then
|
|
228
|
-
costs — roughly **700 output tokens**, because writing it means reading the diff
|
|
229
|
-
— and that spend is the operator's call, not a default.
|
|
227
|
+
**Then arm the gate**, so the fix now owes a verdict:
|
|
230
228
|
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
| **Review** | <N> files, +<a> −<d> · [open the page](<the `open:` URL>) — want a written review before you commit? |
|
|
235
|
-
|
|
236
|
-
**It ends in a question, addressed to someone.** It was once a fenced block of
|
|
237
|
-
engine output, and it fired on every phase and was never once taken: two quoted
|
|
238
|
-
lines under the test counts, addressed to nobody, with the report then closing
|
|
239
|
-
on *"commit this first"* — the last instruction the reader got was to move on,
|
|
240
|
-
so they did. A row in a labelled table is findable; a question in it is
|
|
241
|
-
answerable. Both halves are load-bearing.
|
|
242
|
-
|
|
243
|
-
**Never bury it and never split it.** It sits above `Follow-ups` and `Next`, and
|
|
244
|
-
the page and the question stay in the same row: two adjacent rows about one page
|
|
245
|
-
make the reader resolve a distinction before acting on either. A later edit that
|
|
246
|
-
moves it out of the block, or separates the link from the question, undoes this
|
|
247
|
-
and should be read as a regression rather than tidying.
|
|
229
|
+
```
|
|
230
|
+
skitterspec spec-env review arm <spec>
|
|
231
|
+
```
|
|
248
232
|
|
|
233
|
+
**Then wait**, and `/spec-next` §5 owns the sequence — follow it there rather
|
|
234
|
+
than reading a second copy here: note the moment you start waiting, watch the
|
|
235
|
+
pending store, end your turn, and let `--claim-since` pick the one pass that
|
|
236
|
+
arrived inside the window. The banner it describes is what this skill emits in
|
|
237
|
+
place of a `Review` row, and the routing on the verdict is `/spec-diff` §2 and
|
|
238
|
+
§4, as it is everywhere.
|
|
239
|
+
|
|
240
|
+
**Why this skill arms as well as waits.** Waiting is what any offer does;
|
|
241
|
+
**arming** asserts an obligation that outlives the turn, and belongs only to
|
|
242
|
+
work that is finished. A bug fix is a completed unit — red→green, suite
|
|
243
|
+
passing — so it qualifies, and a wait with nothing owed behind it is a
|
|
244
|
+
suggestion rather than a gate. A mid-run render waits without arming, and
|
|
245
|
+
walking away from that costs nothing.
|
|
246
|
+
|
|
247
|
+
**This is the gap this step existed inside.** It used to render the page, emit a
|
|
248
|
+
row asking *"want a written review before you commit?"*, and finish — with
|
|
249
|
+
nothing watching. A verdict pressed on that page landed in the holding area and
|
|
250
|
+
stayed there, because the run had said the page was **ready** rather than that
|
|
251
|
+
it was **waiting**. Two were pressed on one spec and both were stranded; the
|
|
252
|
+
second existed only because the first appeared to do nothing.
|
|
253
|
+
|
|
254
|
+
**It is user-visible, and that is deliberate.** Once armed, a `git commit` in
|
|
255
|
+
this worktree is refused until a verdict is sent or
|
|
256
|
+
`skitterspec spec-env review skip "<reason>"` records the decision to move on.
|
|
257
|
+
The exit is always one command, and one of them is *"I am moving on"*.
|
|
249
258
|
|
|
250
259
|
Relay the **`open:`** line rather than the bare path: a path is not clickable in
|
|
251
260
|
any terminal, and a page nobody can open is a page nobody reads.
|
|
@@ -307,9 +316,15 @@ the shape; this section carries only what is specific here.
|
|
|
307
316
|
**Fields:** `Tracker` · `Branch` · `Spec` · `Cause` · `Built` · `Tests` ·
|
|
308
317
|
`Review` · `Follow-ups` · `Next`
|
|
309
318
|
|
|
319
|
+
**`Review` is emitted only where step 5b did not run** — a project with no
|
|
320
|
+
isolation, or a render that failed. Where the run is waiting, the banner carries
|
|
321
|
+
the whole subject and the row is dropped, per `.claude/rules/spec-reports.md`.
|
|
322
|
+
|
|
310
323
|
`Cause` is the root cause in one clause and `Built` is the fix — the
|
|
311
324
|
failing→passing test belongs in `Tests`, named, so the evidence is a test name
|
|
312
325
|
rather than an adjective.
|
|
313
326
|
|
|
314
|
-
Step 5b
|
|
315
|
-
|
|
327
|
+
Step 5b ends in the **banner**, not a paragraph and not a row — the run is
|
|
328
|
+
waiting on a verdict, and the banner is the shape that says so. Nothing follows
|
|
329
|
+
it. Where the run is not waiting it asks nothing at all: a `Review` row carries
|
|
330
|
+
the counts and the link, and no question (*asking implies waiting*).
|
|
@@ -401,6 +401,31 @@ Add `--json` to get the file list, totals and the page path back as data. The
|
|
|
401
401
|
page is written to `.spec-env/reviews/<spec>.html`, which is gitignored — it
|
|
402
402
|
leaves no trace in the branch under review.
|
|
403
403
|
|
|
404
|
+
**Declare the button set when the work is unfinished.** A page rendered part-way
|
|
405
|
+
through a run takes `--buttons midrun`, and offers `Continue` — *I have read it,
|
|
406
|
+
carry on* — in place of `Commit` and `Commit & Continue`:
|
|
407
|
+
|
|
408
|
+
```
|
|
409
|
+
skitterspec spec-env review <spec> --buttons midrun
|
|
410
|
+
```
|
|
411
|
+
|
|
412
|
+
**It is a statement about the work, never a reading of the gate.** Ask whether
|
|
413
|
+
the thing you just rendered is *finished*: a phase that ended, a bug fix that is
|
|
414
|
+
green, a spec about to land — committing set, which is the default, so pass
|
|
415
|
+
nothing. Half a phase, a hand edit, a colleague's branch mid-flight, anything
|
|
416
|
+
the operator asked to look at while it is still moving — `midrun`.
|
|
417
|
+
|
|
418
|
+
**Do not derive it from whether the gate is armed.** That is tidier and wrong: a
|
|
419
|
+
project running `review.required: false` never arms at all, so every one of its
|
|
420
|
+
pages would lose the committing buttons and its reader could never commit from
|
|
421
|
+
the page. The caller knows what it rendered; the gate only knows whether the
|
|
422
|
+
project opted into gating.
|
|
423
|
+
|
|
424
|
+
`Continue` is not the removed `none` verdict. `none` recorded itself and did
|
|
425
|
+
nothing; `Continue` **resumes the run**, so it still names an action. What it
|
|
426
|
+
cannot do is commit, or clear a gate a finished phase armed — that takes a
|
|
427
|
+
committing verdict or a recorded skip.
|
|
428
|
+
|
|
404
429
|
**On `--page-only`, stop here** and report the path.
|
|
405
430
|
|
|
406
431
|
## 4a. Read the `reader:` line — never sniff for it yourself
|
|
@@ -439,7 +464,7 @@ operator settle where they are reading, and an explicit value is believed withou
|
|
|
439
464
|
sniffing. `review.serveOnRemote` (default `true`) settles whether the engine may
|
|
440
465
|
act on it.
|
|
441
466
|
|
|
442
|
-
## 4b. Wait for the verdict
|
|
467
|
+
## 4b. Wait for the verdict — because asking for one means waiting for it
|
|
443
468
|
|
|
444
469
|
A served page can hand its pass back the moment it is pressed. Without a wait,
|
|
445
470
|
that pass lands in the holding area and stops — nothing happens until someone
|
|
@@ -448,10 +473,28 @@ acts joined only by the operator remembering.
|
|
|
448
473
|
**This is the step that joins them**, and it is this skill's, so `/spec-next` can point here rather than
|
|
449
474
|
keeping a second copy.
|
|
450
475
|
|
|
451
|
-
**
|
|
452
|
-
|
|
453
|
-
wait
|
|
454
|
-
|
|
476
|
+
**The rule is `.claude/rules/spec-reports.md`'s: asking implies waiting.** If
|
|
477
|
+
this render asks the reader for a verdict, it waits for one. If it is not going
|
|
478
|
+
to wait, it does not ask — the report names the page and its size and stops
|
|
479
|
+
there. There is no third option, and in particular there is no *ask now, notice
|
|
480
|
+
later*: that is the shape that stranded two passes on one spec, where the run
|
|
481
|
+
said the page was **ready** rather than that it was **waiting**.
|
|
482
|
+
|
|
483
|
+
**So the question is not "can I watch a file".** It is "am I asking?" — and the
|
|
484
|
+
transport only decides what carries the answer back:
|
|
485
|
+
|
|
486
|
+
- **A served page** posts to the local store; a file-watch sees it, and the
|
|
487
|
+
steps below are that watch.
|
|
488
|
+
- **A `file://` page** has nothing to post to, so the reader pastes the pass and
|
|
489
|
+
their next message carries it. Say you are holding and **end the turn** — that
|
|
490
|
+
is the same wait, carried by the conversation. It is not a lesser one.
|
|
491
|
+
- **A published page** writes to the artifact's own store, which nothing reaches
|
|
492
|
+
from here. That is the one case where the honest sentence is *press a verdict,
|
|
493
|
+
then type `/spec-reviewed`* — see the published-page paragraph below.
|
|
494
|
+
|
|
495
|
+
**Do not ask, and therefore do not wait, on a render nobody is waiting behind** —
|
|
496
|
+
a bare `--page-only`, a page produced alongside other work, a pass that has
|
|
497
|
+
already arrived. Those get the `Review` row, no question, and nothing is owed.
|
|
455
498
|
|
|
456
499
|
1. **Note the moment**, as an ISO timestamp, before you start. That instant is
|
|
457
500
|
the entire scope of what may be claimed without a person naming it.
|
|
@@ -492,10 +535,16 @@ Where the page is published, say plainly that `/spec-reviewed` is what picks it
|
|
|
492
535
|
up.
|
|
493
536
|
|
|
494
537
|
**`/spec-reviewed` is not replaced by this.** It stays the way in for a pass
|
|
495
|
-
that arrived when nobody was waiting, for the two-passes case, for every
|
|
496
|
-
published page
|
|
538
|
+
that arrived when nobody was waiting, for the two-passes case, and for every
|
|
539
|
+
published page. It is user-only, and that is still the
|
|
497
540
|
enforcement that makes a named claim a person's decision.
|
|
498
541
|
|
|
542
|
+
**A harness with no file-watch is no longer on that list**, and that is the
|
|
543
|
+
change. It used to be — the row and `/spec-reviewed` were "the whole story",
|
|
544
|
+
which read as permission to ask without waiting. Now the turn ending is the
|
|
545
|
+
wait, and every harness can end a turn; `/spec-reviewed` remains available
|
|
546
|
+
there, as it is everywhere, for a pass nobody was holding for.
|
|
547
|
+
|
|
499
548
|
## 5. Offer the written review — say what it costs first
|
|
500
549
|
|
|
501
550
|
The page is free. The **written review is not**, and it costs in two separate
|
|
@@ -230,7 +230,7 @@ is a no-op.
|
|
|
230
230
|
|
|
231
231
|
|
|
232
232
|
|
|
233
|
-
## 6b. Render the page
|
|
233
|
+
## 6b. Render the page, arm the gate, then wait for the verdict
|
|
234
234
|
|
|
235
235
|
**Only when the project has per-spec isolation** (`specs/.core/env.config.json`
|
|
236
236
|
present). Without it there is no worktree to read and this step does not exist —
|
|
@@ -246,28 +246,33 @@ skitterspec spec-env review <spec>
|
|
|
246
246
|
**This is free.** The engine reads git and splices the patches into a template;
|
|
247
247
|
the diff never passes through you, so a 266KB patch costs nothing.
|
|
248
248
|
|
|
249
|
-
**Then
|
|
250
|
-
costs — roughly **700 output tokens**, because writing it means reading the diff
|
|
251
|
-
— and that spend is the operator's call, not a default.
|
|
249
|
+
**Then arm the gate**, so the fix now owes a verdict:
|
|
252
250
|
|
|
253
|
-
|
|
254
|
-
|
|
251
|
+
```
|
|
252
|
+
skitterspec spec-env review arm <spec>
|
|
253
|
+
```
|
|
255
254
|
|
|
256
|
-
|
|
255
|
+
**Then wait**, and `/spec-next` §5 owns the sequence — follow it there rather
|
|
256
|
+
than reading a second copy here: note the moment you start waiting, watch the
|
|
257
|
+
pending store, end your turn, and let `--claim-since` pick the one pass that
|
|
258
|
+
arrived inside the window. The banner it describes is what this skill emits in
|
|
259
|
+
place of a `Review` row, and the routing on the verdict is `/spec-diff` §2 and
|
|
260
|
+
§4, as it is everywhere.
|
|
257
261
|
|
|
258
|
-
**
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
answerable. Both halves are load-bearing.
|
|
262
|
+
**Why this skill arms as well as waits.** Waiting is what any offer does;
|
|
263
|
+
**arming** asserts an obligation that outlives the turn, and belongs only to
|
|
264
|
+
work that is finished. A hotfix is a completed unit — red→green against a
|
|
265
|
+
released tag — so it qualifies, and a wait with nothing owed behind it is a
|
|
266
|
+
suggestion rather than a gate.
|
|
264
267
|
|
|
265
|
-
**
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
moves it out of the block, or separates the link from the question, undoes this
|
|
269
|
-
and should be read as a regression rather than tidying.
|
|
268
|
+
**A hotfix is the case where reading it matters most.** This change is about to
|
|
269
|
+
be tagged and shipped to production from a release line, not merged into a
|
|
270
|
+
branch someone else will read first. The verdict is the only review it gets.
|
|
270
271
|
|
|
272
|
+
**It is user-visible, and that is deliberate.** Once armed, a `git commit` in
|
|
273
|
+
this worktree is refused until a verdict is sent or
|
|
274
|
+
`skitterspec spec-env review skip "<reason>"` records the decision to move on.
|
|
275
|
+
The exit is always one command, and one of them is *"I am moving on"*.
|
|
271
276
|
|
|
272
277
|
Relay the **`open:`** line rather than the bare path: a path is not clickable in
|
|
273
278
|
any terminal, and a page nobody can open is a page nobody reads.
|
|
@@ -335,10 +340,16 @@ the shape; this section carries only what is specific here.
|
|
|
335
340
|
**Fields:** `Tracker` · `Branch` · `Spec` · `Cause` · `Built` · `Tests` ·
|
|
336
341
|
`Review` · `Follow-ups` · `Next`
|
|
337
342
|
|
|
343
|
+
**`Review` is emitted only where step 6b did not run** — a project with no
|
|
344
|
+
isolation, or a render that failed. Where the run is waiting, the banner carries
|
|
345
|
+
the whole subject and the row is dropped, per `.claude/rules/spec-reports.md`.
|
|
346
|
+
|
|
338
347
|
**The base tag goes in the verdict clause** — `✅ /spec-hotfix · hotfix-foo ·
|
|
339
348
|
green on v2.3.1`. Which released version this was fixed against is the first
|
|
340
349
|
thing anyone needs, and it is not a field: the clause is where the run says
|
|
341
350
|
where it got to.
|
|
342
351
|
|
|
343
|
-
Step 6b
|
|
344
|
-
|
|
352
|
+
Step 6b ends in the **banner**, not a paragraph and not a row — the run is
|
|
353
|
+
waiting on a verdict, and the banner is the shape that says so. Nothing follows
|
|
354
|
+
it. Where the run is not waiting it asks nothing at all: a `Review` row carries
|
|
355
|
+
the counts and the link, and no question (*asking implies waiting*).
|
|
@@ -105,12 +105,24 @@ Ensure it exists. If missing, create it documenting:
|
|
|
105
105
|
Read a sibling spec skill (e.g. `spec`, `spec-next`) for the canonical shapes
|
|
106
106
|
rather than inventing them. If the rule already exists, leave it unless stale.
|
|
107
107
|
|
|
108
|
-
## 4a. The review-gate hook (`.claude/hooks/review-gate.
|
|
109
|
-
|
|
110
|
-
`skitterspec init`
|
|
111
|
-
**committed** `.claude/settings.json` as a
|
|
112
|
-
one engine call per Bash tool call and
|
|
113
|
-
whose phase is still awaiting a verdict.
|
|
108
|
+
## 4a. The review-gate hook (`.claude/hooks/review-gate.cjs`)
|
|
109
|
+
|
|
110
|
+
`skitterspec init` **and `skitterspec update`** install the hook script and
|
|
111
|
+
register it in the project's **committed** `.claude/settings.json` as a
|
|
112
|
+
`PreToolUse` hook on `Bash`. It runs one engine call per Bash tool call and
|
|
113
|
+
refuses a `git commit` in a worktree whose phase is still awaiting a verdict.
|
|
114
|
+
|
|
115
|
+
**Both commands, and that is not a detail.** Copying the script and registering
|
|
116
|
+
it are one operation, and they were once split across two code paths — so
|
|
117
|
+
`update` landed the file, reported `created:`, and wired nothing, on every
|
|
118
|
+
project that upgraded. Landing the script is not installing the hook.
|
|
119
|
+
|
|
120
|
+
**`.cjs`, and the extension is load-bearing.** The script lands inside the
|
|
121
|
+
target project, where *that* project's `package.json` decides how node parses a
|
|
122
|
+
`.js` — so CommonJS shipped as `.js` crashes in any `"type": "module"` project,
|
|
123
|
+
on every Bash tool call. `.cjs` settles it at the file. A registration left over
|
|
124
|
+
from the release that named `.js` is rewritten in place, keeping any wrapping
|
|
125
|
+
the operator added, rather than gaining a second entry beside it.
|
|
114
126
|
|
|
115
127
|
**Committed, not machine-local**, and the difference is the point: the trusted
|
|
116
128
|
worktree root is one machine's absolute path, while *a phase that ended owes an
|
|
@@ -269,6 +269,12 @@ skitterspec spec-env review <spec>
|
|
|
269
269
|
the diff never passes through you, so a 266KB patch costs nothing. Report the
|
|
270
270
|
path it prints and move on.
|
|
271
271
|
|
|
272
|
+
**This render takes the committing button set**, which is the default — so
|
|
273
|
+
`--buttons` is not passed. That is a statement about the work, not about the
|
|
274
|
+
gate: the phase is finished, so `Commit` and `Commit & Continue` are the right
|
|
275
|
+
verbs for it. A render part-way through a run takes `--buttons midrun` and
|
|
276
|
+
offers `Continue` instead; `/spec-diff` owns that case.
|
|
277
|
+
|
|
272
278
|
**Then arm the gate**, so the phase now owes a verdict:
|
|
273
279
|
|
|
274
280
|
```
|
|
@@ -313,21 +319,31 @@ else.
|
|
|
313
319
|
|
|
314
320
|
---
|
|
315
321
|
|
|
316
|
-
**Where you are not waiting, it stays the `Review` row** — the counts
|
|
317
|
-
link and
|
|
322
|
+
**Where you are not waiting, it stays the `Review` row** — the counts and the
|
|
323
|
+
page link, and **no question**:
|
|
324
|
+
|
|
325
|
+
| **Review** | <N> files, +<a> −<d> · [open the page](<the `open:` URL>) |
|
|
318
326
|
|
|
319
|
-
|
|
327
|
+
At the end of a phase you are always waiting, so this shape belongs to the
|
|
328
|
+
renders that are not this step: a mid-phase `/spec-diff`, a page produced
|
|
329
|
+
alongside other work. A row cannot be waited on, so a question in one is
|
|
330
|
+
unanswerable however findable it is.
|
|
320
331
|
|
|
321
332
|
**Both shapes are addressed to someone, and that is the constraint.** The offer
|
|
322
333
|
was once a fenced block of engine output: two quoted lines under the test
|
|
323
334
|
counts, addressed to nobody, with the report then closing on *"commit this
|
|
324
335
|
first"* — the last instruction the reader got was to move on, so they did. A row
|
|
325
|
-
in a labelled table is findable; a
|
|
326
|
-
|
|
327
|
-
|
|
336
|
+
in a labelled table is findable; a banner says the work has stopped. What must
|
|
337
|
+
never come back is something unaddressed, unfindable, or fenced.
|
|
338
|
+
|
|
339
|
+
**The row asks nothing, and that is not a weakening of the above.** A row cannot
|
|
340
|
+
be waited on, so a question in one is unanswerable however findable it is —
|
|
341
|
+
which is the failure `spec-reports.md` records under *asking implies waiting*.
|
|
342
|
+
The row names the page; the banner is what asks, because the banner is the shape
|
|
343
|
+
the run is standing behind.
|
|
328
344
|
|
|
329
345
|
**Never bury it and never split it.** The row sits above the last two rows of
|
|
330
|
-
the block, and the page and
|
|
346
|
+
the block, and the page and its counts stay in the same row; the banner
|
|
331
347
|
replaces the row rather than joining it. Two places naming one page make the reader
|
|
332
348
|
resolve a distinction before acting on either — which is the same failure
|
|
333
349
|
whether the two places are adjacent rows or a row and a banner.
|
|
@@ -371,9 +387,15 @@ is that the page can now act, so the token has become a credential rather than
|
|
|
371
387
|
a convenience — and `--claim-since` refusing to choose between two passes is
|
|
372
388
|
what stops a race becoming a wrong commit.
|
|
373
389
|
|
|
374
|
-
**Where the harness cannot watch a file,
|
|
375
|
-
|
|
376
|
-
|
|
390
|
+
**Where the harness cannot watch a file, you still wait** — the turn ending is
|
|
391
|
+
the wait. Say the page is rendered and that you are holding for the pass, then end
|
|
392
|
+
your turn; the reader's next message is what carries it, and `/spec-diff` picks
|
|
393
|
+
it up from the paste exactly as it always has. The gate holds either way: it is
|
|
394
|
+
the engine's, not the watch's.
|
|
395
|
+
|
|
396
|
+
This once read *"change nothing"* — keep the row, keep the question, do not
|
|
397
|
+
wait — and that exemption is the hatch a whole class of unanswerable questions
|
|
398
|
+
came through. Every harness can end a turn.
|
|
377
399
|
|
|
378
400
|
**It still does not break a chained run.** `/commit && /spec-next` is typed as
|
|
379
401
|
one line; by the time this step is reached the chain has finished, so waiting
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@skitterbyte/skitterspec",
|
|
3
|
-
"version": "
|
|
3
|
+
"version": "21.0.0",
|
|
4
4
|
"description": "Spec-driven development for Claude Code \u2014 a tracker-free filesystem workflow: lifecycle skills and per-spec isolation. For Linear sync, install @skitterbyte/skitterspec-linear instead.",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"claude",
|
package/src/cli.js
CHANGED
|
@@ -76,6 +76,8 @@ const {
|
|
|
76
76
|
mergeNotes,
|
|
77
77
|
applyResolutions,
|
|
78
78
|
COMMITTING,
|
|
79
|
+
BUTTON_SETS,
|
|
80
|
+
DEFAULT_BUTTON_SET,
|
|
79
81
|
reviewGatePath,
|
|
80
82
|
readGate,
|
|
81
83
|
writeGate,
|
|
@@ -1633,6 +1635,9 @@ function verdictSaid(v) {
|
|
|
1633
1635
|
// thing that will happen to the repo and the reader should see it coming.
|
|
1634
1636
|
if (v.effective === 'commit') return `committing with ${v.commitWith}`
|
|
1635
1637
|
if (v.effective === 'commit-continue') return `committing with ${v.commitWith}, then the next phase`
|
|
1638
|
+
// Names what it does NOT do, because the reader of a mid-run page has just
|
|
1639
|
+
// pressed a green button and must not read it as a commit.
|
|
1640
|
+
if (v.effective === 'continue') return 'read — carrying on, nothing committed'
|
|
1636
1641
|
if (v.effective === 'changes') return 'changes requested'
|
|
1637
1642
|
return 'discuss first'
|
|
1638
1643
|
}
|
|
@@ -1827,6 +1832,20 @@ function specEnvReviewSkip(dir, config, reason, flags) {
|
|
|
1827
1832
|
}
|
|
1828
1833
|
|
|
1829
1834
|
async function specEnvReview(dir, config, specArg, flags) {
|
|
1835
|
+
// REFUSED BY NAME, never coerced to the default. A typo'd button set silently
|
|
1836
|
+
// rendering the committing page is the same failure the verdict validator
|
|
1837
|
+
// refuses for the same reason: a caller asking for the mid-run page and
|
|
1838
|
+
// getting the committing one would offer a reader a commit on unfinished
|
|
1839
|
+
// work, and nothing would have said so.
|
|
1840
|
+
if (flags.buttons !== null && !BUTTON_SETS.includes(flags.buttons)) {
|
|
1841
|
+
process.stdout.write(
|
|
1842
|
+
`spec-env review: --buttons ${JSON.stringify(flags.buttons)} is not one of ` +
|
|
1843
|
+
`${BUTTON_SETS.join(', ')} — nothing rendered.\n`,
|
|
1844
|
+
)
|
|
1845
|
+
return
|
|
1846
|
+
}
|
|
1847
|
+
const buttons = flags.buttons || DEFAULT_BUTTON_SET
|
|
1848
|
+
|
|
1830
1849
|
// An unknown name throws here rather than falling back to the branch: a review
|
|
1831
1850
|
// of the wrong spec looks exactly like a review of the right one.
|
|
1832
1851
|
const spec = resolveSpecWithWorktree(dir, config, specArg)
|
|
@@ -2166,7 +2185,7 @@ async function specEnvReview(dir, config, specArg, flags) {
|
|
|
2166
2185
|
const gate = gateNow.corrupt ? null : gateNow.gate
|
|
2167
2186
|
|
|
2168
2187
|
const now = new Date().toISOString()
|
|
2169
|
-
let data = collectReview({ spec, git, mode, ref, base, now, notes, gate })
|
|
2188
|
+
let data = collectReview({ spec, git, mode, ref, base, now, notes, gate, buttons })
|
|
2170
2189
|
|
|
2171
2190
|
// A CLEAN WORKING TREE IS NOT "NOTHING TO REVIEW". It is the state a phase
|
|
2172
2191
|
// ends in: the page is rendered before the commit, the commit happens
|
|
@@ -2199,6 +2218,7 @@ async function specEnvReview(dir, config, specArg, flags) {
|
|
|
2199
2218
|
now,
|
|
2200
2219
|
notes,
|
|
2201
2220
|
gate,
|
|
2221
|
+
buttons,
|
|
2202
2222
|
fellBack: true,
|
|
2203
2223
|
})
|
|
2204
2224
|
if (wider.totals.files > 0) {
|
|
@@ -2291,6 +2311,10 @@ async function specEnvReview(dir, config, specArg, flags) {
|
|
|
2291
2311
|
urlFile,
|
|
2292
2312
|
url,
|
|
2293
2313
|
notesFile: reviewNotesPath(out),
|
|
2314
|
+
// Absent stays absent, exactly as it is in the page payload: the
|
|
2315
|
+
// committing set is what a caller that did not ask always got, so
|
|
2316
|
+
// reporting it would make every existing consumer see a new key.
|
|
2317
|
+
...(data.buttons ? { buttons: data.buttons } : {}),
|
|
2294
2318
|
reviewed: Boolean(data.review),
|
|
2295
2319
|
totals: data.totals,
|
|
2296
2320
|
notes: data.notes,
|
|
@@ -3444,6 +3468,7 @@ async function specEnv(rest) {
|
|
|
3444
3468
|
outcome: null,
|
|
3445
3469
|
claim: null,
|
|
3446
3470
|
drop: null,
|
|
3471
|
+
buttons: null,
|
|
3447
3472
|
json: false,
|
|
3448
3473
|
}
|
|
3449
3474
|
for (let i = 0; i < args.length; i++) {
|
|
@@ -3458,6 +3483,7 @@ async function specEnv(rest) {
|
|
|
3458
3483
|
else if (args[i] === '--port') flags.port = args[++i]
|
|
3459
3484
|
else if (args[i] === '--host') flags.host = args[++i]
|
|
3460
3485
|
else if (args[i] === '--publish-copy') flags.publishCopy = true
|
|
3486
|
+
else if (args[i] === '--buttons') flags.buttons = args[++i]
|
|
3461
3487
|
else if (args[i] === '--out') flags.out = args[++i]
|
|
3462
3488
|
else if (args[i] === '--review') flags.review = args[++i]
|
|
3463
3489
|
else if (args[i] === '--notes') flags.notes = args[++i]
|
|
@@ -3556,13 +3582,14 @@ async function specEnv(rest) {
|
|
|
3556
3582
|
break
|
|
3557
3583
|
default:
|
|
3558
3584
|
process.stdout.write(
|
|
3559
|
-
'Usage: skitterspec spec-env <up|down|prune|dev|connect|integrate|hotfix|live|review|stage|status|resolve> [spec] [--keep-volumes] [--force] [--also <tag>] [--older-than <days>] [--branch] [--out <file>] [--review <json>] [--notes <json>] [--resolve <json>] [--outcome <text>] [--claim <code>] [--drop <code>] [--json] [--record-primary] [--assert-primary-clean]\n' +
|
|
3585
|
+
'Usage: skitterspec spec-env <up|down|prune|dev|connect|integrate|hotfix|live|review|stage|status|resolve> [spec] [--keep-volumes] [--force] [--also <tag>] [--older-than <days>] [--branch] [--out <file>] [--review <json>] [--notes <json>] [--resolve <json>] [--outcome <text>] [--claim <code>] [--drop <code>] [--buttons <set>] [--json] [--record-primary] [--assert-primary-clean]\n' +
|
|
3560
3586
|
' review serve [--port <n>] [--host <addr>] [--stop] [--status] serve every diff locally\n' +
|
|
3561
3587
|
' review arm [spec] [--phase <n>] a phase ended — its diff now owes a verdict\n' +
|
|
3562
3588
|
' review gate [spec] [--check] [--json] is one owed? --check exits non-zero if so\n' +
|
|
3563
3589
|
' [--for-command <cmdline>] ...but only when that command is a git commit\n' +
|
|
3564
3590
|
' review skip "<reason>" move on without one, on the record\n' +
|
|
3565
3591
|
' review [spec] --claim-since <iso> claim the one pass that arrived since <iso>\n' +
|
|
3592
|
+
' review [spec] --buttons midrun the page offers Continue, not a commit\n' +
|
|
3566
3593
|
' [spec] is optional everywhere: omit it and the worktree you are standing\n' +
|
|
3567
3594
|
' in is used, else the sole provisioned spec (several -> it lists them).\n' +
|
|
3568
3595
|
' A bare `live` takes that spec when the workbench is free, and prints the\n' +
|
package/src/env/hooks.js
CHANGED
|
@@ -18,8 +18,21 @@
|
|
|
18
18
|
const fs = require('node:fs')
|
|
19
19
|
const path = require('node:path')
|
|
20
20
|
|
|
21
|
-
|
|
21
|
+
// `.cjs`, and the extension is load-bearing. This file is copied INTO the target
|
|
22
|
+
// project, where that project's `package.json` decides how node parses a `.js` —
|
|
23
|
+
// so the CommonJS script shipped as `review-gate.js` died on its own first
|
|
24
|
+
// `require` in every `"type": "module"` project, printing a stack trace over
|
|
25
|
+
// every Bash tool call. `.cjs` settles the parse mode at the file, independently
|
|
26
|
+
// of the one file skitterspec does not control.
|
|
27
|
+
const HOOK_STEM = '.claude/hooks/review-gate'
|
|
28
|
+
const HOOK_SCRIPT = `${HOOK_STEM}.cjs`
|
|
22
29
|
const HOOK_COMMAND = `node "\${CLAUDE_PROJECT_DIR}/${HOOK_SCRIPT}"`
|
|
30
|
+
|
|
31
|
+
// Our script under ANY extension, so a registration naming the retired `.js` is
|
|
32
|
+
// recognised as ours and migrated rather than duplicated. The trailing lookahead
|
|
33
|
+
// is the boundary: without it `review-gate-extra.js` — somebody else's hook —
|
|
34
|
+
// matches on the stem and gets silently rewritten.
|
|
35
|
+
const HOOK_SCRIPT_RE = /[.]claude[/\\]hooks[/\\]review-gate(?:[.][A-Za-z0-9]+)?(?![\w.-])/
|
|
23
36
|
// Seconds. The engine call behind this is one git-free read of a small JSON
|
|
24
37
|
// file, so anything approaching this is a wedge rather than slow work — and a
|
|
25
38
|
// hook that times out fails OPEN, which is the answer we want for a wedge.
|
|
@@ -55,13 +68,21 @@ function hookEntry() {
|
|
|
55
68
|
* run it twice and look like a bug in the gate.
|
|
56
69
|
*/
|
|
57
70
|
function alreadyRegistered(preToolUse) {
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
)
|
|
71
|
+
return registeredHooks(preToolUse).length > 0
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
// Every hook object under `PreToolUse` whose command names our script, whatever
|
|
75
|
+
// extension it names it under. Pure.
|
|
76
|
+
function registeredHooks(preToolUse) {
|
|
77
|
+
if (!Array.isArray(preToolUse)) return []
|
|
78
|
+
const out = []
|
|
79
|
+
for (const group of preToolUse) {
|
|
80
|
+
if (!isObject(group) || !Array.isArray(group.hooks)) continue
|
|
81
|
+
for (const h of group.hooks) {
|
|
82
|
+
if (isObject(h) && typeof h.command === 'string' && HOOK_SCRIPT_RE.test(h.command)) out.push(h)
|
|
83
|
+
}
|
|
84
|
+
}
|
|
85
|
+
return out
|
|
65
86
|
}
|
|
66
87
|
|
|
67
88
|
/**
|
|
@@ -69,6 +90,7 @@ function alreadyRegistered(preToolUse) {
|
|
|
69
90
|
* Idempotent and non-destructive. Returns `{ changed, reason }`:
|
|
70
91
|
* - `created` — no settings file; one was written
|
|
71
92
|
* - `added` — merged into an existing file
|
|
93
|
+
* - `migrated` — registered under a retired path; the path was rewritten
|
|
72
94
|
* - `present` — already registered (no write)
|
|
73
95
|
* - `malformed` — the file exists but is not parseable JSON (left untouched)
|
|
74
96
|
*/
|
|
@@ -98,7 +120,22 @@ function ensureReviewGateHook(dir) {
|
|
|
98
120
|
|
|
99
121
|
const hooks = isObject(parsed.hooks) ? parsed.hooks : {}
|
|
100
122
|
const preToolUse = Array.isArray(hooks.PreToolUse) ? hooks.PreToolUse : []
|
|
101
|
-
|
|
123
|
+
|
|
124
|
+
// Registered already — but possibly under a path that no longer exists, since
|
|
125
|
+
// the script's extension changed. Rewrite the path IN PLACE rather than
|
|
126
|
+
// replacing the command: an operator who wrapped our hook, added a flag or
|
|
127
|
+
// changed the interpreter has registered it their way, and the thing that
|
|
128
|
+
// moved is the file, not their command. Adding a fresh entry beside theirs
|
|
129
|
+
// would run the gate twice and read as a bug in the gate; leaving the old one
|
|
130
|
+
// alone would aim the harness at a file this same upgrade retires.
|
|
131
|
+
const registered = registeredHooks(preToolUse)
|
|
132
|
+
if (registered.length) {
|
|
133
|
+
const stale = registered.filter((h) => !h.command.includes(HOOK_SCRIPT))
|
|
134
|
+
if (!stale.length) return { changed: false, reason: 'present' }
|
|
135
|
+
for (const h of stale) h.command = h.command.replace(HOOK_SCRIPT_RE, HOOK_SCRIPT)
|
|
136
|
+
writeSettings(file, parsed)
|
|
137
|
+
return { changed: true, reason: 'migrated' }
|
|
138
|
+
}
|
|
102
139
|
|
|
103
140
|
writeSettings(file, {
|
|
104
141
|
...parsed,
|
|
@@ -110,8 +147,11 @@ function ensureReviewGateHook(dir) {
|
|
|
110
147
|
module.exports = {
|
|
111
148
|
ensureReviewGateHook,
|
|
112
149
|
alreadyRegistered,
|
|
150
|
+
registeredHooks,
|
|
113
151
|
hookEntry,
|
|
114
152
|
settingsPath,
|
|
153
|
+
HOOK_STEM,
|
|
115
154
|
HOOK_SCRIPT,
|
|
155
|
+
HOOK_SCRIPT_RE,
|
|
116
156
|
HOOK_COMMAND,
|
|
117
157
|
}
|
package/src/env/review.js
CHANGED
|
@@ -230,8 +230,11 @@ function resolveReader(config, env = {}) {
|
|
|
230
230
|
* `fellBack` records that `branch` was reached because the working tree was
|
|
231
231
|
* clean, not because the caller asked for it — see the fallback in
|
|
232
232
|
* `specEnvReview` (`cli.js`).
|
|
233
|
+
*
|
|
234
|
+
* `buttons` is the button set the page renders — see `BUTTON_SETS`. It is the
|
|
235
|
+
* caller's declaration about the work, not a reading of the gate.
|
|
233
236
|
*/
|
|
234
|
-
function collectReview({ spec, git, mode = 'working', ref, base = null, now, notes = null, gate = null, fellBack = false }) {
|
|
237
|
+
function collectReview({ spec, git, mode = 'working', ref, base = null, now, notes = null, gate = null, fellBack = false, buttons = null }) {
|
|
235
238
|
const files = []
|
|
236
239
|
for (const f of trackedFiles(git, ref)) {
|
|
237
240
|
const { patch, whole } = patchFor(git, ref, f, false)
|
|
@@ -301,6 +304,10 @@ function collectReview({ spec, git, mode = 'working', ref, base = null, now, not
|
|
|
301
304
|
// Same rule again: a gate that was never armed and never skipped adds no
|
|
302
305
|
// key at all.
|
|
303
306
|
...(gateForPage(gate) ? { gate: gateForPage(gate) } : {}),
|
|
307
|
+
// THE DEFAULT ADDS NO KEY, so a caller that did not ask for a button set —
|
|
308
|
+
// and a caller that asked for the default by name — renders the payload it
|
|
309
|
+
// rendered before this existed. Opting in is the only thing that shows.
|
|
310
|
+
...(buttons && buttons !== DEFAULT_BUTTON_SET ? { buttons } : {}),
|
|
304
311
|
// WHICH ENGINE DREW THIS PAGE. The render is always current — the git reads
|
|
305
312
|
// happen per request — so a page rendered by a stale process looks entirely
|
|
306
313
|
// right: the counts move, `generatedAt` moves, the diff is correct. Only the
|
|
@@ -399,16 +406,46 @@ const NOTES_VERSION = 1
|
|
|
399
406
|
* `discuss` is the default because it is the behaviour that existed before any
|
|
400
407
|
* verdict did. So a blob from an older page, or one a reader sent without
|
|
401
408
|
* choosing, keeps doing exactly what it always did.
|
|
409
|
+
*
|
|
410
|
+
* `continue` is the mid-run verdict: *I have read it, carry on*. It names an
|
|
411
|
+
* action, which is what separates it from the `none` verdict that was removed —
|
|
412
|
+
* `none` recorded itself and did nothing, while this one resumes the run. What
|
|
413
|
+
* it does NOT do is commit, so it is deliberately absent from `COMMITTING`
|
|
414
|
+
* below and is therefore structurally incapable of clearing an armed gate: a
|
|
415
|
+
* phase that ended still owes a committing verdict or a recorded skip.
|
|
402
416
|
*/
|
|
403
|
-
const VERDICTS = ['commit', 'commit-continue', 'changes', 'discuss']
|
|
417
|
+
const VERDICTS = ['commit', 'commit-continue', 'continue', 'changes', 'discuss']
|
|
404
418
|
const DEFAULT_VERDICT = 'discuss'
|
|
405
419
|
|
|
406
420
|
// The verdicts that COMMIT, and are therefore blocked by an open comment. One
|
|
407
|
-
// list, so a
|
|
421
|
+
// list, so a fifth verdict cannot become a way around the single refusal this
|
|
408
422
|
// engine makes — adding a committing verdict means adding it here, and the
|
|
409
423
|
// block follows for free.
|
|
424
|
+
//
|
|
425
|
+
// `continue` IS DELIBERATELY NOT HERE, and that omission is the whole of
|
|
426
|
+
// decision 2: waiting is what any offer does, while arming asserts an
|
|
427
|
+
// obligation that outlives the turn. A mid-run reader saying "carry on" has
|
|
428
|
+
// answered the offer in front of them and nothing else, so the gate a finished
|
|
429
|
+
// phase armed must survive it untouched.
|
|
410
430
|
const COMMITTING = ['commit', 'commit-continue']
|
|
411
431
|
|
|
432
|
+
/**
|
|
433
|
+
* Which set of buttons a rendered page shows.
|
|
434
|
+
*
|
|
435
|
+
* DECLARED BY THE CALLER, NEVER DERIVED FROM THE GATE. Deriving it — mid-run
|
|
436
|
+
* iff the gate is unarmed — is tidier and wrong: a project running
|
|
437
|
+
* `review.required: false` never arms at all, so every one of its pages would
|
|
438
|
+
* lose the committing buttons and the reader could never commit from the page.
|
|
439
|
+
* The caller knows whether the work it just rendered is finished; the gate only
|
|
440
|
+
* knows whether this project opted into gating.
|
|
441
|
+
*
|
|
442
|
+
* `committing` is the default, so a caller that says nothing keeps today's page
|
|
443
|
+
* exactly — the key is left off the payload entirely rather than written out as
|
|
444
|
+
* the default, so an unchanged caller renders an unchanged page.
|
|
445
|
+
*/
|
|
446
|
+
const BUTTON_SETS = ['committing', 'midrun']
|
|
447
|
+
const DEFAULT_BUTTON_SET = 'committing'
|
|
448
|
+
|
|
412
449
|
/**
|
|
413
450
|
* What an older sidecar's `approve` means now. Pure.
|
|
414
451
|
*
|
|
@@ -1465,6 +1502,8 @@ module.exports = {
|
|
|
1465
1502
|
NOTES_VERSION,
|
|
1466
1503
|
VERDICTS,
|
|
1467
1504
|
COMMITTING,
|
|
1505
|
+
BUTTON_SETS,
|
|
1506
|
+
DEFAULT_BUTTON_SET,
|
|
1468
1507
|
readVerdict,
|
|
1469
1508
|
DEFAULT_VERDICT,
|
|
1470
1509
|
DELETED_HASH,
|
package/src/init.js
CHANGED
|
@@ -39,14 +39,23 @@ function listCommands() {
|
|
|
39
39
|
}
|
|
40
40
|
}
|
|
41
41
|
|
|
42
|
-
// Hook scripts shipped as `assets/hooks/*.
|
|
43
|
-
// Discovered from the bundled tree like everything else, so a
|
|
44
|
-
// installs precisely what it ships and a hook can be retired by
|
|
42
|
+
// Hook scripts shipped as `assets/hooks/*.cjs` (or `*.mjs`), installed to
|
|
43
|
+
// `.claude/hooks/`. Discovered from the bundled tree like everything else, so a
|
|
44
|
+
// distribution installs precisely what it ships and a hook can be retired by
|
|
45
|
+
// deleting it.
|
|
46
|
+
//
|
|
47
|
+
// A BARE `.js` IS DELIBERATELY NOT DISCOVERED. These files are copied into the
|
|
48
|
+
// TARGET project, where that project's `package.json` — the one file skitterspec
|
|
49
|
+
// does not control — decides how node parses a `.js`. Shipping CommonJS as `.js`
|
|
50
|
+
// crashed the review-gate hook in every `"type": "module"` project, on every
|
|
51
|
+
// Bash tool call. The extension is the only thing that settles it at the file,
|
|
52
|
+
// so both accepted forms pin it; `env-review-hook.test.js` asserts the rule over
|
|
53
|
+
// the whole directory rather than over this filter.
|
|
45
54
|
function listHooks() {
|
|
46
55
|
try {
|
|
47
56
|
return fs
|
|
48
57
|
.readdirSync(path.join(ASSETS, 'hooks'))
|
|
49
|
-
.filter((f) => f.endsWith('.
|
|
58
|
+
.filter((f) => f.endsWith('.cjs') || f.endsWith('.mjs'))
|
|
50
59
|
.sort()
|
|
51
60
|
} catch {
|
|
52
61
|
return [] // a distribution may ship no hooks
|
|
@@ -412,6 +421,12 @@ function registerReviewGateHook(dir) {
|
|
|
412
421
|
report.created.push(label)
|
|
413
422
|
} else if (res.reason === 'added') {
|
|
414
423
|
report.updated.push(label)
|
|
424
|
+
} else if (res.reason === 'migrated') {
|
|
425
|
+
// A WRITE, so it may not fall through to `skipped`. It was doing exactly
|
|
426
|
+
// that: rewriting the registered path and then reporting "already
|
|
427
|
+
// registered" — a run that acts and says it did not, which is the shape of
|
|
428
|
+
// the bug this whole change exists to fix.
|
|
429
|
+
report.updated.push('.claude/settings.json (review-gate hook repointed at the renamed script)')
|
|
415
430
|
} else {
|
|
416
431
|
report.skipped.push('.claude/settings.json (review-gate hook already registered)')
|
|
417
432
|
}
|
|
@@ -728,6 +743,15 @@ function resync(dir, { force = false, claudeMd = true, diff = false } = {}) {
|
|
|
728
743
|
installFolders(dir)
|
|
729
744
|
removeRetiredFiles(dir)
|
|
730
745
|
pruneRetiredManaged(dir, manifest)
|
|
746
|
+
// The hook SCRIPT arrives above, as one more managed target; registering it is
|
|
747
|
+
// a separate write to a file we do not manage, so it has to be asked for here.
|
|
748
|
+
// It is easy to read this as duplication of `installHooks()` and delete it —
|
|
749
|
+
// it is not. `installHooks()` is unreachable from this path, and the two
|
|
750
|
+
// halves being in different functions is exactly how they drifted apart once:
|
|
751
|
+
// `update` copied `.claude/hooks/` and registered nothing, for every upgrading
|
|
752
|
+
// project, while reporting a file created. `init-review-gate-hook.test.js`
|
|
753
|
+
// asserts over the entry points rather than over this call.
|
|
754
|
+
registerReviewGateHook(dir)
|
|
731
755
|
if (claudeMd) installClaudeMd(dir, { mode: 'update' })
|
|
732
756
|
flushManifest(dir)
|
|
733
757
|
printReport(dir, 'resync', { diff })
|
|
File without changes
|