@skitterbyte/skitterspec 19.0.0 → 20.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 +88 -0
- package/assets/core/env.config.json.example +2 -1
- package/assets/core/env.config.md +14 -1
- package/assets/hooks/review-gate.js +141 -0
- package/assets/review/page.html +310 -24
- package/assets/rules/spec-planning.md +64 -12
- package/assets/rules/spec-reports.md +66 -14
- package/assets/skills/spec-diff/SKILL.md +122 -8
- package/assets/skills/spec-init/SKILL.md +22 -0
- package/assets/skills/spec-next/SKILL.md +144 -25
- package/assets/skills/spec-reviewed/SKILL.md +33 -16
- package/package.json +1 -1
- package/src/cli.js +298 -3
- package/src/env/commitcmd.js +108 -0
- package/src/env/config.js +17 -1
- package/src/env/hooks.js +117 -0
- package/src/env/review.js +208 -1
- package/src/env/serve.js +28 -4
- package/src/init.js +54 -0
|
@@ -69,15 +69,66 @@ where it buried the rows it was meant to sit beside. A missing destination reads
|
|
|
69
69
|
as a rule that does not fit the work, and a rule that does not fit gets ignored.
|
|
70
70
|
So: **if it is worth telling the reader, it is a row.**
|
|
71
71
|
**If it is not a row, it is not worth telling them.**
|
|
72
|
-
`
|
|
72
|
+
`Notes` is the row most of that content belongs in.
|
|
73
73
|
|
|
74
|
-
**
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
74
|
+
**Two controls may follow the block, and nothing else ever.** The ban exists
|
|
75
|
+
because *prose* after the block competes with it for the reader's attention and
|
|
76
|
+
loses them the rows. A control does not compete: it is a row made actionable, in
|
|
77
|
+
the place the reader's eye already finishes. Prose after either of them is the
|
|
78
|
+
same violation wearing a control's clothes.
|
|
79
|
+
|
|
80
|
+
**The first is a picker.** Where the run ends in a choice the reader must make,
|
|
81
|
+
the options may be offered after the table.
|
|
82
|
+
|
|
83
|
+
**The second is the review call-to-action**, and only where the run is actually
|
|
84
|
+
**waiting** on a verdict. It is a banner, not a sentence:
|
|
85
|
+
|
|
86
|
+
---
|
|
87
|
+
|
|
88
|
+
## ⏸ Review ready — 7 files, +212 −18
|
|
89
|
+
|
|
90
|
+
**[Open the page](http://…)** · I'm holding here until you send a verdict.
|
|
91
|
+
|
|
92
|
+
`/spec-reviewed` picks it up · `spec-env review skip "<reason>"` moves on
|
|
93
|
+
|
|
94
|
+
---
|
|
95
|
+
|
|
96
|
+
A rule, a heading naming the state and the size, the link in bold, one line
|
|
97
|
+
saying the run is stopped, and the two exits. Nothing else, and never a
|
|
98
|
+
paragraph explaining it.
|
|
99
|
+
|
|
100
|
+
**Only promise a wait the transport can deliver.** A page the engine SERVES
|
|
101
|
+
hands its pass to the local store, which a file-watch sees — that is the one
|
|
102
|
+
case where *"I'm holding here"* is true. A **published** page writes to the
|
|
103
|
+
artifact's own store, and nothing pushes from there into the conversation, so a
|
|
104
|
+
verdict pressed on it is invisible until someone asks for it. Its banner says
|
|
105
|
+
so instead:
|
|
106
|
+
|
|
107
|
+
**[Open the page](https://…)** · press a verdict, then type `/spec-reviewed` — I cannot see it until you do.
|
|
108
|
+
|
|
109
|
+
**Exactly one link, never two.** The banner offers the page the reader can
|
|
110
|
+
actually use, and the engine has already decided which that is — a served URL
|
|
111
|
+
when it could serve, a published one only when it could not. Handing over both
|
|
112
|
+
asks the reader to know which door the run is standing behind, and they cannot:
|
|
113
|
+
it was done, and a verdict was pressed on the published page three times while
|
|
114
|
+
each one sat unread under a line claiming the run was waiting. Two links is not
|
|
115
|
+
a convenience with a caveat; it is the caveat existing at all.
|
|
116
|
+
|
|
117
|
+
**This is not the old failure returning**, and the difference is the whole
|
|
118
|
+
justification. The offer used to be two quoted lines in the tail of a long
|
|
119
|
+
report, addressed to nobody, under a closing line that told the reader to move
|
|
120
|
+
on — and it was never once taken. It was moved into a labelled row to make it
|
|
121
|
+
findable, and that worked. What changed since is that the run now *waits*: the
|
|
122
|
+
reader is not being offered something optional, they are being told the work has
|
|
123
|
+
stopped until they answer. A row cannot carry that, because a row is scanned at
|
|
124
|
+
the same weight as every other row. So the state gets the loudest shape on
|
|
125
|
+
screen, at the end, where reading finishes.
|
|
126
|
+
|
|
127
|
+
**Where the run is not waiting, it stays a `Review` row.** A render nobody is
|
|
128
|
+
being held for — a mid-phase `/spec-diff`, a page produced alongside other work
|
|
129
|
+
— is a convenience, and giving it a banner teaches the reader to scroll past
|
|
130
|
+
banners. The shape marks the difference between *here is a page* and *nothing
|
|
131
|
+
proceeds until you answer*.
|
|
81
132
|
|
|
82
133
|
**Never fence a message to the reader.** A fenced block is for a command to
|
|
83
134
|
run, code, or engine output quoted verbatim — things the reader copies or
|
|
@@ -130,11 +181,11 @@ behind it — say nothing rather than reporting that there was nothing to report
|
|
|
130
181
|
| `Cause` | The root cause, for work that diagnosed one. |
|
|
131
182
|
| `Built` | What the run produced — the code, the edits, the spec written. |
|
|
132
183
|
| `Tests` | The result and the command that produced it. |
|
|
133
|
-
| `
|
|
184
|
+
| `Notes` | What this run hit and handled — one short paragraph. Named `Snags` once, which announced a problem before the reader had read one; most of what belongs here is neither good nor bad, just worth knowing. A wrong turn, a guard that fired, a check of yours that turned out to be a false negative. Not a caveat on the outcome (`⚠️` and `Why` carry those) and not future work (`Follow-ups` carries that): this is what happened on the way. |
|
|
134
185
|
| `Landed` | A fast-forward, a tag, a cherry-pick. |
|
|
135
186
|
| `Worktree` | A worktree provisioned, entered, or torn down. |
|
|
136
187
|
| `Untouched` | Uncommitted work the run deliberately left alone — whose, and how much. |
|
|
137
|
-
| `Review` | The rendered diff page: files, `+`/`−`, the link — and the offer of a written review, in the same row. |
|
|
188
|
+
| `Review` | The rendered diff page: files, `+`/`−`, the link — and the offer of a written review, in the same row. **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. |
|
|
138
189
|
| `Follow-ups` | **Always.** `none`, or one line each. |
|
|
139
190
|
| `Next` | **Last.** The single next action for this work — runnable from the state the run leaves behind. |
|
|
140
191
|
|
|
@@ -152,10 +203,11 @@ is how that verdict stops meaning anything. It is still reported rather than
|
|
|
152
203
|
silent, because "I provisioned, and your four files are still sitting there" is
|
|
153
204
|
a different sentence from "I provisioned".
|
|
154
205
|
|
|
155
|
-
**`Review` is one row, not two
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
206
|
+
**`Review` is one row, not two** — and where the run is waiting, it is no rows
|
|
207
|
+
at all. The page and the offer to read it are the same subject, and splitting
|
|
208
|
+
them made the reader resolve a distinction before acting on either. A waiting
|
|
209
|
+
run promotes the whole subject into the banner; keeping the row as well would
|
|
210
|
+
recreate that split with the two halves further apart than ever.
|
|
159
211
|
|
|
160
212
|
## The four verdicts
|
|
161
213
|
|
|
@@ -52,14 +52,20 @@ A review comes back to you one of two ways, and **both are ordinary**:
|
|
|
52
52
|
Either way, **this is not a request to render anything**: it is a review coming
|
|
53
53
|
back, and these steps replace §3–§5 below.
|
|
54
54
|
|
|
55
|
-
0. **Never claim a pass you were not asked to claim.**
|
|
55
|
+
0. **Never claim a pass you were not asked to claim.** And never go looking
|
|
56
|
+
for one.
|
|
56
57
|
|
|
57
|
-
|
|
58
|
-
reaches the
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
58
|
+
A device that reaches the page can POST all day. What decides whether one of
|
|
59
|
+
those passes reaches the operator's review is this rule, and — since the wait
|
|
60
|
+
in §4b — two mechanical facts beside it: the **serve token**, 48 unguessable
|
|
61
|
+
bits minted per server, which decides who can POST at all; and the wait
|
|
62
|
+
**window**, which decides which pass a watch may claim. There is exactly one
|
|
63
|
+
way a pass is taken without a person naming it, `--claim-since`, it is
|
|
64
|
+
described in §4b, and it acts on nothing unless precisely one pass arrived
|
|
65
|
+
while this session was waiting for it.
|
|
66
|
+
|
|
67
|
+
Everything else here is unchanged. A pass that was already sitting there when
|
|
68
|
+
you arrived is never yours to take.
|
|
63
69
|
|
|
64
70
|
**Do not read the code out of `.spec-env/reviews/<spec>.pending.json`.** The
|
|
65
71
|
store is a file you can open, so nothing stops you; that is precisely why the
|
|
@@ -342,6 +348,17 @@ is no. The page is free to produce and changes nothing. The same goes for the
|
|
|
342
348
|
marks: a spec with unread files or open comments is an ordinary spec, and
|
|
343
349
|
nothing here may start counting them.
|
|
344
350
|
|
|
351
|
+
**And this skill never arms the gate.** Arming is `/spec-next`'s, at the one
|
|
352
|
+
moment that means something: a phase ended. Rendering mid-phase — the common
|
|
353
|
+
case this skill exists for — must not create an obligation, because then
|
|
354
|
+
looking at your own work halfway through would owe you a verdict on it. Reading
|
|
355
|
+
is free; ending a phase is what is answerable.
|
|
356
|
+
|
|
357
|
+
An **already-armed** gate is a different matter, and it is not this skill's to
|
|
358
|
+
enforce either: `/spec-next` §2 is where that refusal lives. Here it is only
|
|
359
|
+
context — the render says a verdict is owed, and this skill's whole job is to
|
|
360
|
+
help someone give one.
|
|
361
|
+
|
|
345
362
|
## 4. Render the page — or serve it
|
|
346
363
|
|
|
347
364
|
```
|
|
@@ -422,6 +439,63 @@ operator settle where they are reading, and an explicit value is believed withou
|
|
|
422
439
|
sniffing. `review.serveOnRemote` (default `true`) settles whether the engine may
|
|
423
440
|
act on it.
|
|
424
441
|
|
|
442
|
+
## 4b. Wait for the verdict, where the harness can watch a file
|
|
443
|
+
|
|
444
|
+
A served page can hand its pass back the moment it is pressed. Without a wait,
|
|
445
|
+
that pass lands in the holding area and stops — nothing happens until someone
|
|
446
|
+
types `/spec-reviewed`, so the review and the work carrying on are two separate
|
|
447
|
+
acts joined only by the operator remembering.
|
|
448
|
+
**This is the step that joins them**, and it is this skill's, so `/spec-next` can point here rather than
|
|
449
|
+
keeping a second copy.
|
|
450
|
+
|
|
451
|
+
**Wait when the page was served and the reader is going to read it now.** That
|
|
452
|
+
is the phase-end case, and an operator who says they will look later. Do not
|
|
453
|
+
wait on a bare `--page-only`, on a render nobody asked to be woken about, or
|
|
454
|
+
when the pass has already arrived — there is nothing to wait for.
|
|
455
|
+
|
|
456
|
+
1. **Note the moment**, as an ISO timestamp, before you start. That instant is
|
|
457
|
+
the entire scope of what may be claimed without a person naming it.
|
|
458
|
+
2. **Watch** `.spec-env/reviews/<spec>.pending.json` in the primary checkout,
|
|
459
|
+
and **end the turn**. Not a poll and not a held-open turn: the operator has
|
|
460
|
+
their terminal back, and the session costs nothing while they read.
|
|
461
|
+
3. **On waking, let the engine choose:**
|
|
462
|
+
|
|
463
|
+
```
|
|
464
|
+
skitterspec spec-env review <spec> --claim-since <timestamp> --json
|
|
465
|
+
```
|
|
466
|
+
|
|
467
|
+
Three answers, and only one of them acts. One pass in the window — that is
|
|
468
|
+
the pass, claimed and merged exactly as `--claim` would. **None** — ordinary;
|
|
469
|
+
the file changed for some other reason, so say nothing and wait again or
|
|
470
|
+
stop. **Two or more** — it refuses and names the count, never the codes; that
|
|
471
|
+
is §2 step 0's two-passes case, so offer them from the render and ask.
|
|
472
|
+
4. **Route on the verdict** through §2 step 2 onward. A claim is a delivery
|
|
473
|
+
mechanism; nothing downstream may behave differently because a watch woke
|
|
474
|
+
you rather than a person typing.
|
|
475
|
+
|
|
476
|
+
**What holds this up.** Not "the page cannot reach the conversation" — after
|
|
477
|
+
this step it can, deliberately. Two things replace it. The **serve token** is
|
|
478
|
+
48 bits of `crypto` randomness in the URL path, minted per server, and it
|
|
479
|
+
decides who can POST at all. The **window** decides which pass is yours: a pass
|
|
480
|
+
sitting there before you started waiting is never swept up, which is precisely
|
|
481
|
+
the stranger's pass the old rule was written about, and two arrivals refuse
|
|
482
|
+
rather than pick. What is left of the old rule is unchanged and still absolute —
|
|
483
|
+
outside this window, a pass is claimed because a person named it.
|
|
484
|
+
|
|
485
|
+
**The wait covers the SERVED page and nothing else.** It watches the engine's
|
|
486
|
+
local store, which is where a served page POSTs. A **published** page writes to
|
|
487
|
+
the artifact's own store instead, and no watch of any kind reaches that — so a
|
|
488
|
+
verdict pressed there is invisible until someone asks for it. Never start a
|
|
489
|
+
wait and then hand over a published link under it: that reads as a promise to
|
|
490
|
+
notice, and it was made three times in a row while three verdicts sat unread.
|
|
491
|
+
Where the page is published, say plainly that `/spec-reviewed` is what picks it
|
|
492
|
+
up.
|
|
493
|
+
|
|
494
|
+
**`/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, and for every harness with no file-watch at all. It is user-only, and that is still the
|
|
497
|
+
enforcement that makes a named claim a person's decision.
|
|
498
|
+
|
|
425
499
|
## 5. Offer the written review — say what it costs first
|
|
426
500
|
|
|
427
501
|
The page is free. The **written review is not**, and it costs in two separate
|
|
@@ -505,6 +579,46 @@ The engine knows nothing about publishing and cannot do it. It writes a file and
|
|
|
505
579
|
reads a URL back as an opaque string; everything about what that string means
|
|
506
580
|
lives here.
|
|
507
581
|
|
|
582
|
+
### Publish it so the buttons still work
|
|
583
|
+
|
|
584
|
+
A published page is the answer for a reader **no local server can reach** — a
|
|
585
|
+
phone on mobile data, anyone away from the LAN the engine serves on. It is also
|
|
586
|
+
the one surface where the verdict buttons have nowhere to POST: the page's own
|
|
587
|
+
URL is on claude.ai, so a POST fails and the reader gets "could not reach the
|
|
588
|
+
server" after pressing a verdict they meant.
|
|
589
|
+
|
|
590
|
+
So **publish it with a store**, and the page uses it:
|
|
591
|
+
|
|
592
|
+
- Declare `capabilities: {db: {}}`. The page checks for `window.claude.use` and
|
|
593
|
+
writes the pass into the artifact's own `passes` collection instead of
|
|
594
|
+
POSTing. Nothing else about it changes — same marks, same verdicts, same
|
|
595
|
+
refusal to commit over an open comment.
|
|
596
|
+
- **Do not declare `user`.** The pass is not per-viewer private state, a shared
|
|
597
|
+
collection is what it wants, and a declaration nothing uses is a grant asked
|
|
598
|
+
for nothing.
|
|
599
|
+
- **Same file path every time**, so a redeploy reuses the URL. That is what
|
|
600
|
+
answers the clear-down worry: one page per spec, replaced at each render,
|
|
601
|
+
rather than an artifact per phase accumulating in the gallery.
|
|
602
|
+
- **Send a push notification with the URL** when you publish. The reader is by
|
|
603
|
+
definition somewhere else — that is why it was published.
|
|
604
|
+
|
|
605
|
+
### Taking a stored pass back
|
|
606
|
+
|
|
607
|
+
A pass in the store is claimed the same way in spirit and a different way in
|
|
608
|
+
mechanism, because the engine cannot see it:
|
|
609
|
+
|
|
610
|
+
1. Read the `passes` collection with the Artifact tool's `read_db`.
|
|
611
|
+
2. **Apply step 0 unchanged.** One waiting pass, offered by what it says and
|
|
612
|
+
when it was sent; two is a refusal to guess. Nothing here is automatic —
|
|
613
|
+
there is no wait window on this transport, because nothing pushes from the
|
|
614
|
+
store to this conversation.
|
|
615
|
+
3. Write the pass's `blob` to a scratch file **verbatim** and merge it with
|
|
616
|
+
`spec-env review <spec> --notes <file>`. It rejoins the ordinary path at §2
|
|
617
|
+
step 1, so the verdict is judged and routed exactly as any other.
|
|
618
|
+
4. **Delete the document** with `write_db` once it is merged. A claim consumes:
|
|
619
|
+
a pass left in the store is claimable twice, which is the one property the
|
|
620
|
+
six-digit code has always had and this transport must not lose.
|
|
621
|
+
|
|
508
622
|
## 7. Report
|
|
509
623
|
|
|
510
624
|
End with the block defined in `.claude/rules/spec-reports.md`. That file carries
|
|
@@ -520,7 +634,7 @@ the shape; this section carries only what is specific here.
|
|
|
520
634
|
- `⏸` — the spec has no worktree. Say that plainly: a spec that has not been
|
|
521
635
|
started has nothing to diff, which is an ordinary state and not an error.
|
|
522
636
|
|
|
523
|
-
**Fields:** `Built` · `Tests` · `
|
|
637
|
+
**Fields:** `Built` · `Tests` · `Notes` · `Review` · `Follow-ups` · `Next`
|
|
524
638
|
|
|
525
639
|
## 7a. End in a picker
|
|
526
640
|
|
|
@@ -105,6 +105,28 @@ 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.js`)
|
|
109
|
+
|
|
110
|
+
`skitterspec init` installs the hook script and registers it in the project's
|
|
111
|
+
**committed** `.claude/settings.json` as a `PreToolUse` hook on `Bash`. It runs
|
|
112
|
+
one engine call per Bash tool call and refuses a `git commit` in a worktree
|
|
113
|
+
whose phase is still awaiting a verdict.
|
|
114
|
+
|
|
115
|
+
**Committed, not machine-local**, and the difference is the point: the trusted
|
|
116
|
+
worktree root is one machine's absolute path, while *a phase that ended owes an
|
|
117
|
+
answer* is the project's policy and should reach everyone who clones it. The
|
|
118
|
+
command is written with `${CLAUDE_PROJECT_DIR}`, so it holds in worktrees too.
|
|
119
|
+
|
|
120
|
+
**Say it is there, once.** A hook that blocks a commit with the operator not
|
|
121
|
+
knowing a hook exists reads as a broken git, so name it in the report when it is
|
|
122
|
+
newly registered — and say what turns it off (`review.required: false` in
|
|
123
|
+
`env.config.json`, which the hook defers to entirely).
|
|
124
|
+
|
|
125
|
+
**Never fatal, and never rewritten.** A settings file that is not parseable JSON
|
|
126
|
+
is reported and left exactly as it is — it is the operator's config, and
|
|
127
|
+
everything else in it would be lost. A settings file that already names this
|
|
128
|
+
script, however it was wrapped, is left alone rather than gaining a second copy.
|
|
129
|
+
|
|
108
130
|
## 5. CLAUDE.md
|
|
109
131
|
|
|
110
132
|
Ensure a `## Spec workflow` section exists. If absent, add one with the
|
|
@@ -116,7 +116,7 @@ A **name argument** is accepted, but it must *match* the spec in flight — it
|
|
|
116
116
|
narrows a re-run, it does not select a different spec. A mismatch refuses,
|
|
117
117
|
naming both.
|
|
118
118
|
|
|
119
|
-
## 2. Pre-flight —
|
|
119
|
+
## 2. Pre-flight — the last phase is committed, and was answered
|
|
120
120
|
|
|
121
121
|
Before writing any code for this phase, get the workspace clean:
|
|
122
122
|
|
|
@@ -128,6 +128,32 @@ Before writing any code for this phase, get the workspace clean:
|
|
|
128
128
|
next phase on top of an uncommitted one. (Skip if this is the first phase —
|
|
129
129
|
there's nothing prior to commit.)
|
|
130
130
|
|
|
131
|
+
- **Confirm the last phase's review was answered.** Ask the engine, never the
|
|
132
|
+
sidecar:
|
|
133
|
+
|
|
134
|
+
```
|
|
135
|
+
skitterspec spec-env review gate <spec> --json
|
|
136
|
+
```
|
|
137
|
+
|
|
138
|
+
`state: "armed"` means a phase ended, its page was rendered, and nobody has
|
|
139
|
+
said what they concluded. **Refuse, with the `⏸` block**, and name the three
|
|
140
|
+
ways out in the `Next` row: read the page and send a verdict, type
|
|
141
|
+
`/spec-reviewed` if one is already waiting, or
|
|
142
|
+
`skitterspec spec-env review skip "<reason>"` to move on with the reason on
|
|
143
|
+
the record.
|
|
144
|
+
|
|
145
|
+
`state: "clear"` carries on.
|
|
146
|
+
**`state: "unknown"` also carries on, in silence** — it is the project opting out, a sidecar that could not be read, or
|
|
147
|
+
a spec the engine could not resolve, and none of those is evidence that
|
|
148
|
+
anything is owed (`.claude/rules/negative-checks.md`). Do not mention it: a
|
|
149
|
+
line about a gate nobody armed is an accusation against a healthy repo.
|
|
150
|
+
|
|
151
|
+
**This refusal counts nothing.** It is not a tally of ticked boxes — those
|
|
152
|
+
still gate nothing and still are not counted. It asserts one thing: a phase
|
|
153
|
+
that ended has an answer. And the exit is always **one command**, one of them
|
|
154
|
+
being *"I am moving on"*, which is what keeps this a push rather than a wall —
|
|
155
|
+
a gate with no exit gets switched off wholesale instead of answered.
|
|
156
|
+
|
|
131
157
|
## 3. Implement the phase
|
|
132
158
|
|
|
133
159
|
Identify the **first unfinished phase** from the `00-overview.md` phase index,
|
|
@@ -226,7 +252,7 @@ skitterspec spec-env resolve <spec> --assert-primary-clean
|
|
|
226
252
|
- **"cannot tell"** — no baseline, or one from another spec. It exits 0 and
|
|
227
253
|
claims nothing; say so in one line and carry on. An absence is not evidence.
|
|
228
254
|
|
|
229
|
-
## 5. Render the page
|
|
255
|
+
## 5. Render the page, arm the gate, then wait for the verdict
|
|
230
256
|
|
|
231
257
|
**Only when the project has per-spec isolation** (`specs/.core/env.config.json`
|
|
232
258
|
present). Without it there is no worktree to read and this step does not exist.
|
|
@@ -243,31 +269,115 @@ skitterspec spec-env review <spec>
|
|
|
243
269
|
the diff never passes through you, so a 266KB patch costs nothing. Report the
|
|
244
270
|
path it prints and move on.
|
|
245
271
|
|
|
272
|
+
**Then arm the gate**, so the phase now owes a verdict:
|
|
273
|
+
|
|
274
|
+
```
|
|
275
|
+
skitterspec spec-env review arm <spec> --phase <n>
|
|
276
|
+
```
|
|
277
|
+
|
|
278
|
+
It is idempotent within a phase, so a re-render does not restart the clock, and
|
|
279
|
+
it is **never fatal**: a project that opted out, or an engine that could not
|
|
280
|
+
resolve the spec, says so and the phase is still built. Nothing here counts
|
|
281
|
+
anything — the gate asserts that a phase which ended has an answer, and that is
|
|
282
|
+
all it asserts.
|
|
283
|
+
|
|
246
284
|
**Then offer `/spec-diff`. Do not run it.** The written review is the part that
|
|
247
285
|
costs — roughly **700 output tokens**, because writing it means reading the diff
|
|
248
286
|
— and that spend is the operator's call, not a default.
|
|
249
287
|
|
|
250
|
-
**
|
|
251
|
-
and
|
|
288
|
+
**Where you are going to wait, the offer is the banner after the block** —
|
|
289
|
+
defined in `.claude/rules/spec-reports.md`, and **the `Review` row is dropped**
|
|
290
|
+
so one subject lives in one place:
|
|
291
|
+
|
|
292
|
+
---
|
|
293
|
+
|
|
294
|
+
## ⏸ Review ready — <N> files, +<a> −<d>
|
|
295
|
+
|
|
296
|
+
**[Open the page](<the `open:` URL>)** · I'm holding here until you send a verdict.
|
|
297
|
+
|
|
298
|
+
`/spec-reviewed` picks it up · `spec-env review skip "<reason>"` moves on
|
|
299
|
+
|
|
300
|
+
**One link, and the engine has already chosen it.** Where it served, the
|
|
301
|
+
banner carries the served URL and the wait is real. Where it could not serve —
|
|
302
|
+
a busy port, a machine with no network address — that is the case publishing
|
|
303
|
+
exists for, and then the banner carries the published URL with what is true of
|
|
304
|
+
it: *press a verdict, then type `/spec-reviewed`*, because nothing pushes from
|
|
305
|
+
the artifact store into this conversation.
|
|
306
|
+
|
|
307
|
+
**Never offer both.** Publishing while the server is reachable adds a second
|
|
308
|
+
door the reader cannot tell apart from the first, and the wait only stands
|
|
309
|
+
behind one of them — that was done, and three verdicts were pressed on the
|
|
310
|
+
published page while each sat unread under a line saying I was holding. The
|
|
311
|
+
published page is for the reader the server cannot reach, and for nobody
|
|
312
|
+
else.
|
|
313
|
+
|
|
314
|
+
---
|
|
315
|
+
|
|
316
|
+
**Where you are not waiting, it stays the `Review` row** — the counts, the page
|
|
317
|
+
link and a question, in one row:
|
|
252
318
|
|
|
253
319
|
| **Review** | <N> files, +<a> −<d> · [open the page](<the `open:` URL>) — want a written review before you commit? |
|
|
254
320
|
|
|
255
|
-
**
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
321
|
+
**Both shapes are addressed to someone, and that is the constraint.** The offer
|
|
322
|
+
was once a fenced block of engine output: two quoted lines under the test
|
|
323
|
+
counts, addressed to nobody, with the report then closing on *"commit this
|
|
324
|
+
first"* — the last instruction the reader got was to move on, so they did. A row
|
|
325
|
+
in a labelled table is findable; a question in it is answerable; a banner says
|
|
326
|
+
the work has stopped. What must never come back is something unaddressed,
|
|
327
|
+
unfindable, or fenced.
|
|
328
|
+
|
|
329
|
+
**Never bury it and never split it.** The row sits above the last two rows of
|
|
330
|
+
the block, and the page and the question stay in the same row; the banner
|
|
331
|
+
replaces the row rather than joining it. Two places naming one page make the reader
|
|
332
|
+
resolve a distinction before acting on either — which is the same failure
|
|
333
|
+
whether the two places are adjacent rows or a row and a banner.
|
|
334
|
+
|
|
335
|
+
### Then wait for the verdict, where the harness can
|
|
336
|
+
|
|
337
|
+
The row is findable, but a row cannot make the continuation follow from the
|
|
338
|
+
reading — and that is the gap the whole gate exists to close.
|
|
339
|
+
**Where this harness can watch a file and wake the session on a change, use it.** The pass
|
|
340
|
+
arrives at the engine's holding area, the watch fires, and the verdict the
|
|
341
|
+
reader pressed is what carries the work on.
|
|
342
|
+
|
|
343
|
+
1. **Note the moment you start waiting**, as an ISO timestamp. That instant is
|
|
344
|
+
the whole scope of what you may claim.
|
|
345
|
+
2. **Watch the pending store** for the spec —
|
|
346
|
+
`.spec-env/reviews/<spec>.pending.json` in the primary checkout — and
|
|
347
|
+
**end your turn**. Do not poll, and do not hold the turn open: the point is that
|
|
348
|
+
the reader has the terminal back while they read.
|
|
349
|
+
3. **On waking, let the engine pick**:
|
|
261
350
|
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
moves it out of the block, or separates the link from the question, undoes this
|
|
266
|
-
and should be read as a regression rather than tidying.
|
|
351
|
+
```
|
|
352
|
+
skitterspec spec-env review <spec> --claim-since <the timestamp> --json
|
|
353
|
+
```
|
|
267
354
|
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
355
|
+
It claims the one pass that arrived inside the window, and acts on nothing
|
|
356
|
+
at all when none did (the watch can fire on a write that was not a pass) or
|
|
357
|
+
when two did (two sittings, or two people — the operator has the codes).
|
|
358
|
+
4. **Route on the verdict** exactly as `/spec-diff` §2 and §4 describe. Do not
|
|
359
|
+
restate that routing here.
|
|
360
|
+
|
|
361
|
+
**WHY THIS IS SAFE, AND WHAT IT COSTS.** It was once true that a device
|
|
362
|
+
reaching your page could not reach your conversation, and that fact was the
|
|
363
|
+
whole guard: a pass sat in the holding area until a person typed
|
|
364
|
+
`/spec-reviewed`. This replaces that guard rather than weakening it by
|
|
365
|
+
accident, and the replacement is two things together — **the serve token**,
|
|
366
|
+
48 unguessable bits minted per server, which is what decides who can POST at
|
|
367
|
+
all; and **the window**, which is what decides which pass is yours. A pass
|
|
368
|
+
already waiting when the wait began is never claimed by it, which is exactly
|
|
369
|
+
the stranger's pass the old rule was written about. What is genuinely given up
|
|
370
|
+
is that the page can now act, so the token has become a credential rather than
|
|
371
|
+
a convenience — and `--claim-since` refusing to choose between two passes is
|
|
372
|
+
what stops a race becoming a wrong commit.
|
|
373
|
+
|
|
374
|
+
**Where the harness cannot watch a file, change nothing.** The `Review` row and
|
|
375
|
+
`/spec-reviewed` are the whole story, exactly as before. The gate still holds
|
|
376
|
+
either way: it is the engine's, not the watch's.
|
|
377
|
+
|
|
378
|
+
**It still does not break a chained run.** `/commit && /spec-next` is typed as
|
|
379
|
+
one line; by the time this step is reached the chain has finished, so waiting
|
|
380
|
+
here stops nothing that was still going to happen.
|
|
271
381
|
|
|
272
382
|
Relay the **`open:`** line the engine prints, not the bare path: a path is not
|
|
273
383
|
clickable in any terminal, and a page nobody can open is a page nobody reads.
|
|
@@ -296,7 +406,8 @@ drift.
|
|
|
296
406
|
|
|
297
407
|
**Serving is the engine's to do; publishing is never.** A `remote` reader
|
|
298
408
|
authorises a local server — one process, ended by one flag, leaving nothing
|
|
299
|
-
behind — and authorises nothing else.
|
|
409
|
+
behind — and authorises nothing else. A reader the server CAN reach is not a
|
|
410
|
+
reason to publish as well: the page they can already open is the page to name. Publishing leaves a page this tooling
|
|
300
411
|
cannot remove, so it stays an ask in every case, always. If the engine could not
|
|
301
412
|
serve (a busy port, a machine with no network address) it falls back to the
|
|
302
413
|
`file://` URL with its marker, and that is when publishing is worth naming.
|
|
@@ -321,7 +432,7 @@ the shape; this section carries only what is specific here.
|
|
|
321
432
|
- `❌` — the phase's tests are red, or it stopped part-way. Quote the failure.
|
|
322
433
|
- `⏸` — no spec in flight, or the name given does not match the one that is.
|
|
323
434
|
|
|
324
|
-
**Fields:** `Tracker` · `Branch` · `Built` · `Tests` · `
|
|
435
|
+
**Fields:** `Tracker` · `Branch` · `Built` · `Tests` · `Notes` · `Review` ·
|
|
325
436
|
`Follow-ups` · `Next`
|
|
326
437
|
|
|
327
438
|
## 6a. End in a picker
|
|
@@ -330,6 +441,12 @@ The block says what happened; this is what to do about it. Offer the same four
|
|
|
330
441
|
endings the review page carries, so a review finishes the same way wherever the
|
|
331
442
|
reader is standing — the page, a pasted code, or here.
|
|
332
443
|
|
|
444
|
+
**Not when you are waiting.** Where §5 set a watch and ended the turn, the
|
|
445
|
+
verdict is coming from the page and the picker would be a second way to answer
|
|
446
|
+
a question already asked — so the block ends on `Next`, which names the page.
|
|
447
|
+
The picker is for the run that did not wait: no watch available, or a render
|
|
448
|
+
the reader is expected to come back to in their own time.
|
|
449
|
+
|
|
333
450
|
| Option | Does |
|
|
334
451
|
|--------|------|
|
|
335
452
|
| `Reviewed` | Claims the waiting pass and routes on its verdict |
|
|
@@ -373,8 +490,10 @@ top of an uncommitted one — so a `Next` that names only `/spec-next` sends the
|
|
|
373
490
|
reader straight into that refusal. The two halves are four hundred lines apart,
|
|
374
491
|
which is exactly how they drifted.
|
|
375
492
|
|
|
376
|
-
**Step 5's offer
|
|
377
|
-
|
|
378
|
-
|
|
379
|
-
|
|
380
|
-
|
|
493
|
+
**Step 5's offer lands in one of two shapes, and never both.** Waiting on a
|
|
494
|
+
verdict → the **banner** after the block, and no `Review` row. Not waiting → the
|
|
495
|
+
`Review` row, and no banner. Neither is a paragraph: the ban on prose after the
|
|
496
|
+
block is untouched, and the banner is a control the contract names
|
|
497
|
+
(`.claude/rules/spec-reports.md`). Step 5 renders before the commit and this
|
|
498
|
+
step is where its offer lands, so the two must not disagree about which shape it
|
|
499
|
+
takes.
|
|
@@ -37,16 +37,25 @@ of `/spec-diff` step 0's central rule:
|
|
|
37
37
|
**never claim a pass you were not asked to claim**.
|
|
38
38
|
|
|
39
39
|
That rule exists because a review pass can be POSTed by anything that reaches
|
|
40
|
-
the page
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
40
|
+
the page. So a stranger's pass sits in the holding area — unless an agent goes
|
|
41
|
+
and fetches it, which is exactly what happened once: an agent found a waiting
|
|
42
|
+
approval, read its code off disk, claimed it, and reported the round-trip
|
|
43
|
+
working.
|
|
44
44
|
|
|
45
45
|
Prose alone did not prevent that. The harness does —
|
|
46
|
-
**the model cannot invoke this skill** — so a pass
|
|
47
|
-
because a person typed the command. Typing it *is* the human
|
|
48
|
-
model-invocable does not make it more
|
|
49
|
-
standing between a stray approval and
|
|
46
|
+
**the model cannot invoke this skill** — so a pass picked up *this way* is only
|
|
47
|
+
ever picked up because a person typed the command. Typing it *is* the human
|
|
48
|
+
signal. A later edit that makes this skill model-invocable does not make it more
|
|
49
|
+
convenient — it removes the only thing standing between a stray approval and
|
|
50
|
+
someone's repo.
|
|
51
|
+
|
|
52
|
+
**There is exactly one other way in, and it is bounded rather than trusting.**
|
|
53
|
+
A phase that ends waits on its page, and a pass arriving *during that wait* is
|
|
54
|
+
claimed by the engine (`--claim-since`, `/spec-diff` §4b): scoped to the window,
|
|
55
|
+
acting on nothing when no pass arrived, refusing when two did. A pass already
|
|
56
|
+
sitting there when the wait began is never swept up by it — which is the
|
|
57
|
+
stranger's pass this whole rule was written about. Everything outside that
|
|
58
|
+
window still comes through here.
|
|
50
59
|
|
|
51
60
|
## 1. Resolve the spec
|
|
52
61
|
|
|
@@ -141,10 +150,17 @@ everything a decision needs, so there is nothing to go looking for, and going
|
|
|
141
150
|
looking is the bypass step 0 forbids. The file is right there and readable; that
|
|
142
151
|
is precisely why the rule is written down rather than assumed.
|
|
143
152
|
|
|
144
|
-
**
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
153
|
+
**Check the store before concluding nothing is waiting.** Only when this spec
|
|
154
|
+
has a published page (`--json` reports a `url`): a reader off the LAN sent their
|
|
155
|
+
pass to the artifact's own `passes` collection rather than to the engine, so an
|
|
156
|
+
empty holding area is not an empty answer yet. Read it with the Artifact tool's
|
|
157
|
+
`read_db`, merge the `blob` through `--notes`, and **delete the document** —
|
|
158
|
+
`/spec-diff` §6 owns the whole sequence, including that a claim consumes.
|
|
159
|
+
|
|
160
|
+
**Nothing waiting in either is an ordinary answer.** Say so and stop. Do not hunt
|
|
161
|
+
through other specs, and do not treat an empty holding area as a problem — a
|
|
162
|
+
`file://` page copies to the clipboard instead of sending, so mention that the
|
|
163
|
+
pass may be on their clipboard waiting to be pasted, and finish.
|
|
148
164
|
|
|
149
165
|
## 3. One pass waiting? Act on it
|
|
150
166
|
|
|
@@ -155,10 +171,11 @@ manufactured, because the model cannot invoke it.
|
|
|
155
171
|
|
|
156
172
|
**The code was never an authorisation.** A later edit must not restore it as
|
|
157
173
|
one. It earned its keep when the *page* pushed: the agent went looking, found
|
|
158
|
-
a pass, and had to prove which one it had.
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
174
|
+
a pass, and had to prove which one it had. What stops a stranger's approval
|
|
175
|
+
reaching your repo is not those six digits — it is that this command cannot be
|
|
176
|
+
typed by anything but a person, and that the one automatic path (`--claim-since`)
|
|
177
|
+
is bounded by a window a stranger's pass falls outside of. The confirmation step
|
|
178
|
+
added nothing to either, and cost a round-trip on every review.
|
|
162
179
|
|
|
163
180
|
So the code has exactly one job left: **telling two passes apart**. That is
|
|
164
181
|
disambiguation, not a gate.
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@skitterbyte/skitterspec",
|
|
3
|
-
"version": "
|
|
3
|
+
"version": "20.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",
|