@skitterbyte/skitterspec-linear 12.0.0 → 14.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 +296 -10
- package/README.md +32 -2
- package/assets/claude-md-section.md +38 -2
- package/assets/commands/spec-connect.md +2 -2
- package/assets/commands/spec-live.md +2 -2
- package/assets/core/SETUP.md +21 -3
- package/assets/core/env.config.json.example +7 -3
- package/assets/core/env.config.md +90 -30
- package/assets/core/linear.config.md +58 -0
- package/assets/hooks/review-gate.js +141 -0
- package/assets/review/page.html +1787 -0
- package/assets/rules/spec-planning.md +250 -10
- package/assets/rules/spec-reports.md +321 -0
- package/assets/skills/spec/SKILL.md +33 -5
- package/assets/skills/spec-bug/SKILL.md +172 -9
- package/assets/skills/spec-cancel/SKILL.md +98 -21
- package/assets/skills/spec-claim/SKILL.md +114 -0
- package/assets/skills/spec-complete/SKILL.md +94 -25
- package/assets/skills/spec-diff/SKILL.md +678 -0
- package/assets/skills/spec-hotfix/SKILL.md +172 -11
- package/assets/skills/spec-init/SKILL.md +56 -7
- package/assets/skills/spec-linear-setup/SKILL.md +55 -1
- package/assets/skills/spec-list/SKILL.md +218 -0
- package/assets/skills/spec-next/SKILL.md +419 -7
- package/assets/skills/spec-push/SKILL.md +32 -8
- package/assets/skills/spec-review/SKILL.md +40 -5
- package/assets/skills/spec-reviewed/SKILL.md +258 -0
- package/assets/skills/spec-start/SKILL.md +386 -106
- package/assets/skills/spec-status/SKILL.md +24 -2
- package/assets/skills/spec-sync/SKILL.md +40 -4
- package/assets/skills/spec-to-main/SKILL.md +28 -6
- package/package.json +11 -7
- package/src/cli.js +1808 -89
- package/src/env/building.js +143 -0
- package/src/env/commitcmd.js +108 -0
- package/src/env/config.js +58 -9
- package/src/env/hooks.js +117 -0
- package/src/env/provision.js +54 -15
- package/src/env/proxy.js +34 -1
- package/src/env/render.js +3 -12
- package/src/env/resolve.js +295 -9
- package/src/env/review.js +1536 -0
- package/src/env/serve.js +573 -0
- package/src/env/teardown.js +13 -6
- package/src/init.js +150 -1
- package/src/vendor/linear/api.js +104 -1
- package/src/vendor/linear/cli-sync.js +854 -17
- package/src/vendor/linear/config.js +8 -0
- package/src/vendor/linear/credentials.js +94 -0
- package/src/vendor/linear/doctor.js +35 -0
- package/src/vendor/linear/identity.js +105 -0
- package/src/vendor/linear/mcp.js +26 -0
- package/src/vendor/sync-core/index.js +6 -2
- package/src/vendor/sync-core/src/compare.js +49 -3
- package/src/vendor/sync-core/src/normalize.js +30 -0
- package/src/vendor/sync-core/src/push.js +11 -1
- package/src/vendor/sync-core/src/write.js +38 -0
- package/LICENSE +0 -21
|
@@ -0,0 +1,678 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: spec-diff
|
|
3
|
+
description: See what a spec's worktree changed — render its diff as a page you can mark up, take that review pass back, and act on it. Answers at any point, including half-way through a phase. Use when the user says "/spec-diff", "show me the diff", "what did this phase change", "review this spec's work", wants to read a worktree's changes away from the terminal, or hands back what the review page produced — a six-digit claim code, or the pasted JSON.
|
|
4
|
+
---
|
|
5
|
+
|
|
6
|
+
# /spec-diff — see the phase before you commit it
|
|
7
|
+
|
|
8
|
+
> Stay silent while this runs — speak only to ask something you cannot answer
|
|
9
|
+
> yourself, or to report a failure at the moment it happens. Read
|
|
10
|
+
> `.claude/rules/spec-reports.md` before reporting; it defines the block this
|
|
11
|
+
> skill ends with.
|
|
12
|
+
|
|
13
|
+
A phase is built in its own worktree, and the terminal is somewhere else — so
|
|
14
|
+
`git diff` answers about the wrong branch and a 350-line diff read as terminal
|
|
15
|
+
text is scrolling, not review. This renders the diff as a page instead: the
|
|
16
|
+
engine collects it with `git -C`, writes a self-contained HTML file, and you open
|
|
17
|
+
it locally or publish it and read it on a phone.
|
|
18
|
+
|
|
19
|
+
**The diff never passes through the model.** git writes the patches and the
|
|
20
|
+
engine splices them into the page. A 266KB patch costs **zero** context tokens,
|
|
21
|
+
and a 289KB page costs the same to publish as a 5KB one. Do not "simplify" this
|
|
22
|
+
by reading the diff and emitting HTML — that is the one change that would undo
|
|
23
|
+
the whole design.
|
|
24
|
+
|
|
25
|
+
## 1. Resolve the spec — three rules, in order
|
|
26
|
+
|
|
27
|
+
1. **The name argument**, when given. An unknown name **refuses** and says so:
|
|
28
|
+
falling back would review a different spec's work under the name you typed,
|
|
29
|
+
which looks exactly like a correct answer.
|
|
30
|
+
2. **The spec in flight for this session** — `skitterspec spec-env live status`
|
|
31
|
+
names the spec whose branch is checked out in the primary checkout.
|
|
32
|
+
3. **The worktree this session is standing in** — if cwd is inside a spec's
|
|
33
|
+
worktree, that is the spec.
|
|
34
|
+
|
|
35
|
+
If none answers and several specs have worktrees, **list them and stop**. The
|
|
36
|
+
engine does this for you: run the verb with no name and it either resolves the
|
|
37
|
+
sole candidate or prints the candidates.
|
|
38
|
+
|
|
39
|
+
## 2. Were you handed a review pass? Then that is the job
|
|
40
|
+
|
|
41
|
+
The page has marks on it — `✓ accept` per file, notes against a line or a whole
|
|
42
|
+
file, answers to the checks a written review asked — and it
|
|
43
|
+
**ends in a decision**: `✓ Approve`, `↺ Request changes` or `… Discuss first`.
|
|
44
|
+
A review comes back to you one of two ways, and **both are ordinary**:
|
|
45
|
+
|
|
46
|
+
- **A six-digit code** — `418207`, on its own. A *served* page hands its pass
|
|
47
|
+
straight to the engine, which holds it until someone reads the code out. This
|
|
48
|
+
is the usual way on a phone.
|
|
49
|
+
- **A pasted JSON blob** — a `file://` page has no server to talk to, so it
|
|
50
|
+
copies. Not legacy: it is the whole story for a local reader.
|
|
51
|
+
|
|
52
|
+
Either way, **this is not a request to render anything**: it is a review coming
|
|
53
|
+
back, and these steps replace §3–§5 below.
|
|
54
|
+
|
|
55
|
+
0. **Never claim a pass you were not asked to claim.** And never go looking
|
|
56
|
+
for one.
|
|
57
|
+
|
|
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.
|
|
69
|
+
|
|
70
|
+
**Do not read the code out of `.spec-env/reviews/<spec>.pending.json`.** The
|
|
71
|
+
store is a file you can open, so nothing stops you; that is precisely why the
|
|
72
|
+
rule has to be stated rather than assumed. It has already happened once — an
|
|
73
|
+
agent found a waiting approval, read its code off disk, claimed it, and
|
|
74
|
+
reported the round-trip working. The operator had pressed the button, so the
|
|
75
|
+
outcome was harmless and the reasoning was wrong.
|
|
76
|
+
|
|
77
|
+
The render tells you everything a decision needs, so there is nothing to go
|
|
78
|
+
looking for:
|
|
79
|
+
|
|
80
|
+
```
|
|
81
|
+
pending: 1 waiting
|
|
82
|
+
792969 · commit · 1 min ago
|
|
83
|
+
```
|
|
84
|
+
|
|
85
|
+
**Offer it, naming the code**, and wait: *"an approval is waiting, code
|
|
86
|
+
792969, sent a minute ago — does that match your phone?"* The digits travel
|
|
87
|
+
**to** the operator, not from them — they verify rather than transcribe, and
|
|
88
|
+
the code is the only part they can check, so
|
|
89
|
+
**name it rather than describing the pass** (a stranger's approval and theirs
|
|
90
|
+
read identically otherwise).
|
|
91
|
+
|
|
92
|
+
**Two or more waiting is a refusal to guess.** Name them all and ask which.
|
|
93
|
+
Never take the newest, the oldest, or the only `commit` — that is exactly
|
|
94
|
+
the case where someone else's pass is sitting beside the operator's, and it
|
|
95
|
+
is the only case where reading six digits out is worth anyone's time.
|
|
96
|
+
|
|
97
|
+
**On "that isn't mine"**, leave it and offer to drop it:
|
|
98
|
+
|
|
99
|
+
```
|
|
100
|
+
skitterspec spec-env review <spec> --drop <code>
|
|
101
|
+
```
|
|
102
|
+
|
|
103
|
+
A pass that stays is reported on every render until the operator stops
|
|
104
|
+
reading the line — which is how the real one gets missed.
|
|
105
|
+
|
|
106
|
+
1. **Take it in through the engine**, whichever way it arrived.
|
|
107
|
+
|
|
108
|
+
**A code** — claim it, once the operator has confirmed it is theirs (step 0):
|
|
109
|
+
|
|
110
|
+
```
|
|
111
|
+
skitterspec spec-env review <spec> --claim <code>
|
|
112
|
+
```
|
|
113
|
+
|
|
114
|
+
**A pasted blob** — write the JSON to a scratch file
|
|
115
|
+
verbatim — never retype it, never "tidy" it — and merge it:
|
|
116
|
+
|
|
117
|
+
```
|
|
118
|
+
skitterspec spec-env review <spec> --notes <file>
|
|
119
|
+
```
|
|
120
|
+
|
|
121
|
+
Both validate wholesale and write nothing if the blob is malformed or names a
|
|
122
|
+
different spec; a claim additionally refuses a code that matches nothing, and
|
|
123
|
+
**names nothing when it does** — listing the waiting codes would hand a
|
|
124
|
+
guesser the answer. **Relay a refusal as it is written** and stop; every
|
|
125
|
+
message says which entry was wrong, so there is nothing to guess.
|
|
126
|
+
|
|
127
|
+
**A claim is a delivery mechanism, not a second kind of review.** Everything
|
|
128
|
+
below reads the same merged pass and the same verdict; nothing may behave
|
|
129
|
+
differently because of how it arrived.
|
|
130
|
+
|
|
131
|
+
2. **Read the verdict the engine judged.** The pass says what it CONCLUDED, and
|
|
132
|
+
the engine prints it — `committing with <skill>`, `committing with <skill>,
|
|
133
|
+
then the next phase`, `changes requested`, `discuss first`, or
|
|
134
|
+
`commit refused — <reason>`. It is judged, not taken on trust: a committing
|
|
135
|
+
verdict arriving with an open comment is refused and routed to discuss,
|
|
136
|
+
because you asked for something and it therefore cannot also be fine.
|
|
137
|
+
**Never re-judge it yourself, and never count anything** — read the engine's
|
|
138
|
+
answer and route on it.
|
|
139
|
+
|
|
140
|
+
Three routes, and every pass takes exactly one:
|
|
141
|
+
|
|
142
|
+
| Verdict | What it means | Where to go |
|
|
143
|
+
|---------|---------------|-------------|
|
|
144
|
+
| `commit` (honoured) | this is fine, commit it | §2a — commit, then stop |
|
|
145
|
+
| `commit-continue` (honoured) | this is fine, keep going | §2a — commit, then `/spec-next` |
|
|
146
|
+
| `changes` | do these, now | step 4 — **skip the wait**, this is the go-ahead |
|
|
147
|
+
| `discuss` | I have a question | step 3 — report, then **ask what's up** |
|
|
148
|
+
|
|
149
|
+
**A refused commit and a pass with no verdict both mean `discuss`.**
|
|
150
|
+
Neither is a special case: the engine routes the refusal there itself, and an
|
|
151
|
+
absent verdict has always meant "report it and wait" — which is why that is
|
|
152
|
+
what it still means.
|
|
153
|
+
|
|
154
|
+
3. **Say what you read, then ask what's up.** Report the accepted count, then
|
|
155
|
+
each open comment as `file:line — note`, then the files you would touch.
|
|
156
|
+
**Wait — unless the verdict already said otherwise.** Pasting on its own is
|
|
157
|
+
not a go-ahead: this skill is read-only everywhere else, a misread comment
|
|
158
|
+
costs a revert, and the operator may only have wanted it recorded. A `changes`
|
|
159
|
+
verdict **is** that go-ahead, given deliberately on the page, so asking again
|
|
160
|
+
is asking someone to decide twice. The reasoning is unchanged; what changed is
|
|
161
|
+
that the page can now answer it in advance.
|
|
162
|
+
|
|
163
|
+
**`discuss` means "ask me what's up"** — an opening move, not a stopping
|
|
164
|
+
place. A reader who pressed it has a question, and a summary that ends in
|
|
165
|
+
silence leaves them to ask it themselves. Report what you read and then
|
|
166
|
+
**open the conversation**: name what you would do next and ask whether that
|
|
167
|
+
is what they wanted. The same wording has to work for a pass that chose
|
|
168
|
+
nothing at all, since an absent verdict means this too — so ask about the
|
|
169
|
+
review, never about the button.
|
|
170
|
+
|
|
171
|
+
4. **On the go-ahead, work only the commented files.** Read those; do **not**
|
|
172
|
+
open the accepted ones. That is the whole saving the marks buy, and it is
|
|
173
|
+
only worth anything if it is true — so say plainly which files you did not
|
|
174
|
+
open. Make the changes, then run the project's typecheck and test commands.
|
|
175
|
+
|
|
176
|
+
**First, compare the worktree against where you are standing.** This is the
|
|
177
|
+
one step here that writes, and §1's first rule resolves by **name** — so
|
|
178
|
+
editing a tree this session is not standing in is the ordinary case, not an
|
|
179
|
+
edge one. Take the `worktree:` line from
|
|
180
|
+
`skitterspec spec-env resolve <spec>` and compare it with cwd, resolving both
|
|
181
|
+
paths first so a symlinked or trailing-slash spelling of one tree does not
|
|
182
|
+
read as two. Same tree, and everything below is inert.
|
|
183
|
+
|
|
184
|
+
**Different trees, and the discipline applies.** Record the baseline before
|
|
185
|
+
the first edit:
|
|
186
|
+
|
|
187
|
+
```
|
|
188
|
+
skitterspec spec-env resolve <spec> --record-primary
|
|
189
|
+
```
|
|
190
|
+
|
|
191
|
+
Then every write takes an absolute path under the worktree and every command
|
|
192
|
+
is prefixed `cd "<worktreePath>" &&` — typecheck and tests included. A single
|
|
193
|
+
relative path lands the fix in the primary checkout, on the base branch, and
|
|
194
|
+
nothing about it looks wrong at the time.
|
|
195
|
+
|
|
196
|
+
**This is a write discipline, not a precondition.** It changes *how* this
|
|
197
|
+
step writes, never *whether* it runs — §3 below still holds in full, and a
|
|
198
|
+
later edit reading this as a gate would undo the rule it exists beside.
|
|
199
|
+
|
|
200
|
+
5. **Write back what you did**, one entry per comment you acted on:
|
|
201
|
+
|
|
202
|
+
```json
|
|
203
|
+
[{ "id": "2026-01-01T00:00:00.000Z-1", "note": "keyed the accept on the blob sha" }]
|
|
204
|
+
```
|
|
205
|
+
|
|
206
|
+
```
|
|
207
|
+
skitterspec spec-env review <spec> --resolve <file>
|
|
208
|
+
```
|
|
209
|
+
|
|
210
|
+
The note is the load-bearing half: it is what lets the next read **verify**
|
|
211
|
+
the fix rather than trust it. An id that matches nothing is reported and
|
|
212
|
+
skipped, so one bad id never costs you the rest. Then re-render (§4) so the
|
|
213
|
+
page shows each note struck through with its account.
|
|
214
|
+
|
|
215
|
+
6. **Before the re-render, prove nothing leaked** — only when step 4 found two
|
|
216
|
+
trees. Nothing should be reported fixed before it is known to be fixed in the
|
|
217
|
+
right one:
|
|
218
|
+
|
|
219
|
+
```
|
|
220
|
+
skitterspec spec-env resolve <spec> --assert-primary-clean
|
|
221
|
+
```
|
|
222
|
+
|
|
223
|
+
- **Exit 0, "primary checkout clean"** — carry on.
|
|
224
|
+
- **Non-zero** — stop and relay the engine's message unchanged. It names the
|
|
225
|
+
paths and both readings: this run wrote them and they belong in the
|
|
226
|
+
worktree, or something else did and the baseline wants re-recording.
|
|
227
|
+
**Do not guess which, and do not delete anything.** A path that appeared is
|
|
228
|
+
not proof of who put it there.
|
|
229
|
+
- **"cannot tell"** — no baseline, or one from another spec. It exits 0 and
|
|
230
|
+
claims nothing; say so in one line and carry on. An absence is not evidence.
|
|
231
|
+
|
|
232
|
+
WHAT WOULD FOOL THIS CHECK: it watches the **primary checkout** and nothing
|
|
233
|
+
else, so a fix written into *another* spec's worktree would leak there unseen.
|
|
234
|
+
Left unhandled deliberately — the cost of the gap is a missed leak, never a
|
|
235
|
+
false accusation.
|
|
236
|
+
|
|
237
|
+
**Never commit on a `changes` pass.** It authorises the work, not a commit —
|
|
238
|
+
only `commit` and `commit-continue` reach §2a. The fixes sit in the worktree
|
|
239
|
+
where the operator can read them on the next render, which is the whole point of
|
|
240
|
+
sending them back rather than approving.
|
|
241
|
+
|
|
242
|
+
## 2a. A committing pass commits — through the project's own skill
|
|
243
|
+
|
|
244
|
+
Only on an **honoured** `commit` or `commit-continue`. A refused one did not
|
|
245
|
+
happen.
|
|
246
|
+
|
|
247
|
+
The engine names the skill to use on the verdict's `commitWith` — the
|
|
248
|
+
`review.commitWith` config key, `/commit` by default. Do not read the config
|
|
249
|
+
yourself; one answer, from the engine that owns it. **There is no off switch:**
|
|
250
|
+
`"none"` existed and was removed, because a verdict that records itself and does
|
|
251
|
+
nothing is the one thing a review page must not offer.
|
|
252
|
+
|
|
253
|
+
- **A skill you have** — invoke it, and say which one. **Never vendor it.**
|
|
254
|
+
`/commit` ships with **skittership**, a different package: it stages the
|
|
255
|
+
task's files, runs the project's checks, and writes the release-note footers
|
|
256
|
+
this repo's changelog is built from. A copy of it living here would be a fork
|
|
257
|
+
of someone else's skill that drifts silently.
|
|
258
|
+
- **A skill you do not have** — commit it yourself: stage only the files this
|
|
259
|
+
work touched, run the project's typecheck and test commands, and write a
|
|
260
|
+
conventional commit. **Say that you did, every time.** A commit made under
|
|
261
|
+
rules nobody configured must never be reported as one made under `/commit`.
|
|
262
|
+
|
|
263
|
+
**Decide availability from the skill list you already have**, never by testing
|
|
264
|
+
for a file. A skill can legitimately live in several places, so `.claude/skills/
|
|
265
|
+
commit/SKILL.md` missing is an absence that proves nothing
|
|
266
|
+
(`.claude/rules/negative-checks.md` rule 1) — and being wrong about it means
|
|
267
|
+
committing by hand while reporting a hand-off. You are told which skills you
|
|
268
|
+
have; that list is the answer.
|
|
269
|
+
|
|
270
|
+
**Let the commit's own failure be the answer.** If typecheck or the tests fail,
|
|
271
|
+
there is no commit — report the failure and stop. Do not fix the tests to get
|
|
272
|
+
the commit through, and do not commit around them.
|
|
273
|
+
**An approval judges the change; it never promises that it builds**, and the two
|
|
274
|
+
must not be conflated by a skill acting on someone's behalf.
|
|
275
|
+
|
|
276
|
+
**Then record what it produced**, so the page shows the outcome rather than the
|
|
277
|
+
intent:
|
|
278
|
+
|
|
279
|
+
```
|
|
280
|
+
skitterspec spec-env review <spec> --outcome "committed <sha> via <skill|by hand>"
|
|
281
|
+
```
|
|
282
|
+
|
|
283
|
+
That writes the outcome onto the decision the engine already logged and
|
|
284
|
+
re-renders the page, where it reads as history beneath the verdict bar. On a
|
|
285
|
+
failed commit there is no outcome to record — say what failed instead.
|
|
286
|
+
|
|
287
|
+
**Nothing is pushed.** The commit is local, exactly as `/commit` leaves it.
|
|
288
|
+
|
|
289
|
+
### `commit-continue` — then the next phase, and no further
|
|
290
|
+
|
|
291
|
+
On `commit-continue`, once the commit is in, carry on into **`/spec-next`**.
|
|
292
|
+
That is the whole of the chaining, and both halves of that sentence matter:
|
|
293
|
+
|
|
294
|
+
- **A failed commit is the end of the chain.** If typecheck or the tests fail
|
|
295
|
+
there is no commit, and therefore no continue — report where it broke and
|
|
296
|
+
stop. The continue is downstream of the commit, not beside it.
|
|
297
|
+
- **Never `/spec-complete`.** When there is no unfinished phase left, say the
|
|
298
|
+
spec has none and stop. Completing
|
|
299
|
+
**lands the branch and tears the worktree down**, and that must not fall out
|
|
300
|
+
of a button labelled *continue* — the
|
|
301
|
+
distance between "build the next phase" and "delete the worktree" is one skill
|
|
302
|
+
name, and a person pressing a button on a phone cannot see which one you
|
|
303
|
+
picked.
|
|
304
|
+
|
|
305
|
+
**This overturns a recorded Non-goal, and cites it rather than contradicting it.**
|
|
306
|
+
`feat-review-verdict` ruled chaining out: *"'Commit what I just read' and
|
|
307
|
+
'go build the next thing unattended' are different sizes of decision, and the
|
|
308
|
+
second stays a keystroke."* That conflated two meanings of unattended — nobody
|
|
309
|
+
choosing, and nobody watching. A distinctly-labelled fourth button is chosen,
|
|
310
|
+
deliberately, by the person who just read the diff. The ban was right about an
|
|
311
|
+
*automatic* chain and caught a chosen one by accident.
|
|
312
|
+
|
|
313
|
+
**Passes waiting are information — and worth raising.** A render lists them
|
|
314
|
+
when the holding area is not empty: code, verdict, age, one per line. Nothing
|
|
315
|
+
counts them and nothing refuses over them; a pass nobody claims simply sits
|
|
316
|
+
there. But **say so** — an operator who pressed a button on their phone and hears
|
|
317
|
+
nothing has no way to tell a pass that never arrived from one waiting to be
|
|
318
|
+
confirmed, and both look like silence. Raising it is step 0's offer; never
|
|
319
|
+
treating it as a task is the rule that survives.
|
|
320
|
+
|
|
321
|
+
**A mark is information, never a gate.** Nothing counts the ticks or requires
|
|
322
|
+
them: a phase may end with comments open, `/spec-complete` never learns about
|
|
323
|
+
them, and this skill refuses nothing on their account. If a project ever wants
|
|
324
|
+
otherwise that is a config key defaulting to off — not a tidy-up here.
|
|
325
|
+
|
|
326
|
+
**What the intake costs, and why a code costs less.** A pasted blob is file
|
|
327
|
+
paths and the operator's own words, which you need in context to act on them —
|
|
328
|
+
so the paste is not overhead. But it does scale with the review: a marked-up
|
|
329
|
+
60-file pass is kilobytes of context before any work starts.
|
|
330
|
+
|
|
331
|
+
**A claimed pass never enters the context at all.** The engine holds it, merges
|
|
332
|
+
it and reports the counts; six digits is what reaches you. That is the same rule
|
|
333
|
+
the diff already follows — git writes it, the engine splices it, you never read
|
|
334
|
+
it — and the paste was the one place it broke. The *work* either authorises is
|
|
335
|
+
ordinary phase-sized cost, and step 3 is where the operator decides whether to
|
|
336
|
+
spend it.
|
|
337
|
+
|
|
338
|
+
## 3. Gate it on nothing
|
|
339
|
+
|
|
340
|
+
**This skill has no preconditions and must never grow one.** Not tests passing,
|
|
341
|
+
not the phase being finished, not the spec being this session's, not a clean
|
|
342
|
+
tree. Reviewing work in progress is the *common* case — half a phase, a hand
|
|
343
|
+
edit, a colleague's branch — and a gate would refuse at exactly the moment
|
|
344
|
+
someone wants to look.
|
|
345
|
+
|
|
346
|
+
If a later edit is tempted to add "only when the phase is complete", the answer
|
|
347
|
+
is no. The page is free to produce and changes nothing. The same goes for the
|
|
348
|
+
marks: a spec with unread files or open comments is an ordinary spec, and
|
|
349
|
+
nothing here may start counting them.
|
|
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
|
+
|
|
362
|
+
## 4. Render the page — or serve it
|
|
363
|
+
|
|
364
|
+
```
|
|
365
|
+
skitterspec spec-env review <spec> # uncommitted work (the default)
|
|
366
|
+
skitterspec spec-env review <spec> --branch # everything since the base branch
|
|
367
|
+
skitterspec spec-env review serve # every spec, on localhost
|
|
368
|
+
```
|
|
369
|
+
|
|
370
|
+
**The engine handles the switch.** A file when the reader is at this machine, a
|
|
371
|
+
served URL when they are not: on a `remote` reader it stands its own server up
|
|
372
|
+
and puts a URL the reader can open on the `open:` line. Both are free and neither
|
|
373
|
+
publishes anything. You are not choosing between them; you are relaying whichever
|
|
374
|
+
one the engine printed.
|
|
375
|
+
|
|
376
|
+
The operator who does not want a LAN listener started for them sets
|
|
377
|
+
`review.serveOnRemote: false` in `env.config.json`, and the `file://` link with
|
|
378
|
+
its *will not open where you are reading* marker comes back.
|
|
379
|
+
|
|
380
|
+
`serve` renders **per request**, so nothing it shows can be stale, and it lists
|
|
381
|
+
every spec with a worktree rather than one. `--host 0.0.0.0` binds beyond
|
|
382
|
+
loopback and prints a URL a phone on the same network can open, guarded by an
|
|
383
|
+
unguessable path token. Bare, it binds loopback only. It is a process:
|
|
384
|
+
`--status` says whether one is up, `--stop` takes it down.
|
|
385
|
+
|
|
386
|
+
Default to the working tree — "what did this phase just do". Use `--branch` when
|
|
387
|
+
the question is about the whole spec.
|
|
388
|
+
|
|
389
|
+
**You do not have to reach for `--branch` after a commit.** A clean working tree
|
|
390
|
+
is the state a phase *ends* in, so the engine falls back to the branch range by
|
|
391
|
+
itself and says which it is showing — `(working tree clean — since main)` in the
|
|
392
|
+
header line, and `(working tree clean)` on the page. Report that wording as it
|
|
393
|
+
came rather than calling it the working tree.
|
|
394
|
+
|
|
395
|
+
The fallback fires on exactly one state — no `--branch`, and nothing uncommitted
|
|
396
|
+
to show. An explicit `--branch` always means what it says, a tree with real
|
|
397
|
+
changes is never swapped out from under you, and a branch with no work at all
|
|
398
|
+
still reports `nothing to review` exactly as before.
|
|
399
|
+
|
|
400
|
+
Add `--json` to get the file list, totals and the page path back as data. The
|
|
401
|
+
page is written to `.spec-env/reviews/<spec>.html`, which is gitignored — it
|
|
402
|
+
leaves no trace in the branch under review.
|
|
403
|
+
|
|
404
|
+
**On `--page-only`, stop here** and report the path.
|
|
405
|
+
|
|
406
|
+
## 4a. Read the `reader:` line — never sniff for it yourself
|
|
407
|
+
|
|
408
|
+
`spec-env review` reports where it believes the reader is, and
|
|
409
|
+
**that is the only place this question is answered.** Three states:
|
|
410
|
+
|
|
411
|
+
| `reader:` | What to offer |
|
|
412
|
+
|-----------|---------------|
|
|
413
|
+
| absent (`unknown`) | the `file://` URL, exactly as always. **Do not warn** — unknown is the ordinary state of a local machine |
|
|
414
|
+
| `local` | the `file://` URL |
|
|
415
|
+
| `remote` | the `open:` line as printed — the engine already served it. Pass on any `also:` lines too |
|
|
416
|
+
|
|
417
|
+
**Never read an environment variable to decide this.** Not `SSH_CONNECTION`, not
|
|
418
|
+
`CLAUDE_CODE_*`, not a tty check — the engine already did it, reports the answer
|
|
419
|
+
on that line and in `--json`, and a second implementation here could not be
|
|
420
|
+
tested and would drift from the first. The ranking and the traps
|
|
421
|
+
(`CLAUDE_CODE_ENTRYPOINT` describes the *process*, not the reader; stdin is never
|
|
422
|
+
a tty under Claude Code) live in `review.js` beside the code, which is where they
|
|
423
|
+
belong.
|
|
424
|
+
|
|
425
|
+
**It authorises serving, never publishing.** A `remote` reader authorises a
|
|
426
|
+
local server — one process, ended by one flag, leaving nothing behind — and
|
|
427
|
+
nothing more. Publishing leaves a page this tooling cannot remove, so it is an
|
|
428
|
+
ask, in every case, always. The two were once one rule, and lumping them together
|
|
429
|
+
is what left a remote reader holding a dead link: see
|
|
430
|
+
`specs/complete/bug-remote-reader-gets-a-dead-link/`.
|
|
431
|
+
|
|
432
|
+
When serving fails — a busy port, or a machine with no network address — the
|
|
433
|
+
engine falls back to the `file://` URL with its marker and nothing breaks. That
|
|
434
|
+
is the one case where naming publishing is worth doing, because it is the only
|
|
435
|
+
answer left.
|
|
436
|
+
|
|
437
|
+
`review.reader` in `env.config.json` (`local` · `remote` · `detect`) lets the
|
|
438
|
+
operator settle where they are reading, and an explicit value is believed without
|
|
439
|
+
sniffing. `review.serveOnRemote` (default `true`) settles whether the engine may
|
|
440
|
+
act on it.
|
|
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
|
+
|
|
499
|
+
## 5. Offer the written review — say what it costs first
|
|
500
|
+
|
|
501
|
+
The page is free. The **written review is not**, and it costs in two separate
|
|
502
|
+
ways. Quote the one that actually applies rather than a single number:
|
|
503
|
+
|
|
504
|
+
- **Writing it: ~700 output tokens.** The review JSON, near enough regardless of
|
|
505
|
+
how big the diff is. This is unavoidable and it is what was measured.
|
|
506
|
+
- **Reading the diff: input, and it scales.** Only paid when the diff is not
|
|
507
|
+
already in front of you. A 350-line diff is easily 10–15k input tokens.
|
|
508
|
+
|
|
509
|
+
**If you just built this phase, you already have the diff — do not re-read it.**
|
|
510
|
+
That is the common case (`/spec-next` offers this skill the moment a phase ends)
|
|
511
|
+
and the one the ~700 was measured in. Running `git diff` over code you wrote
|
|
512
|
+
three tool calls ago buys nothing and is not free.
|
|
513
|
+
|
|
514
|
+
**When you genuinely do not have it** — a fresh session, half a phase from
|
|
515
|
+
yesterday, a colleague's branch — read *selectively*. `--json` returns the file
|
|
516
|
+
list with each file's `+`/`−`, `status` and `noise` flag and **no patches**, so
|
|
517
|
+
use it to choose: skip everything marked `noise: true`, and skip files the review
|
|
518
|
+
will not have anything to say about. Pulling the whole diff in when three files
|
|
519
|
+
matter is the avoidable half of this cost.
|
|
520
|
+
|
|
521
|
+
Say what it will cost, then let the operator decide. Do not write it unasked when
|
|
522
|
+
the diff is large.
|
|
523
|
+
|
|
524
|
+
When asked, write JSON to a scratch file:
|
|
525
|
+
|
|
526
|
+
```json
|
|
527
|
+
{
|
|
528
|
+
"summary": "Two or three sentences on what this phase actually did, read from the diff.",
|
|
529
|
+
"checks": [
|
|
530
|
+
{ "level": "flag", "file": "src/env/review.js", "note": "…" },
|
|
531
|
+
{ "level": "confirm", "file": "test/env-review.test.js", "note": "…" },
|
|
532
|
+
{ "level": "good", "file": "src/cli.js", "note": "…" }
|
|
533
|
+
]
|
|
534
|
+
}
|
|
535
|
+
```
|
|
536
|
+
|
|
537
|
+
- **`flag`** — something you believe is wrong. **`confirm`** — something only the
|
|
538
|
+
author can settle. **`good`** — a decision worth keeping, said once.
|
|
539
|
+
- Every check names the file it is about.
|
|
540
|
+
- **Review the diff, not the spec.** Keep to what the change shows; do not
|
|
541
|
+
re-derive the plan or restate the spec's reasoning back at its author.
|
|
542
|
+
|
|
543
|
+
Then re-render with it spliced in — the engine renders it, you never emit HTML:
|
|
544
|
+
|
|
545
|
+
```
|
|
546
|
+
skitterspec spec-env review <spec> --review <file>
|
|
547
|
+
```
|
|
548
|
+
|
|
549
|
+
## 6. Publish only when asked
|
|
550
|
+
|
|
551
|
+
**Never publish unprompted.** Publishing leaves something behind that this
|
|
552
|
+
tooling cannot remove, and the page already works as a local file. Offer it;
|
|
553
|
+
do not assume it.
|
|
554
|
+
|
|
555
|
+
When the user asks:
|
|
556
|
+
|
|
557
|
+
- **One spec is one page.** `--json` reports `url` when this spec has been
|
|
558
|
+
published before. If it has, **update that URL** rather than creating a
|
|
559
|
+
second entry, and label the new version for the phase (`phase-3`).
|
|
560
|
+
- **On the first publish**, write the returned URL to the path `--json` reports
|
|
561
|
+
as `urlFile` — one line, no formatting. That file is how every later phase
|
|
562
|
+
finds the same page. Never construct the path yourself.
|
|
563
|
+
- **Say, once, that it is theirs now.** When you report a URL, say in the same
|
|
564
|
+
breath that skitterspec cannot remove the page and that `/artifacts` (or the
|
|
565
|
+
gallery at `claude.ai/code/artifacts`) is where it goes. This is the moment the
|
|
566
|
+
decision is being made, so it is the moment worth saying it — `spec-env down`
|
|
567
|
+
repeats it at teardown, by which point the page has outlived the spec.
|
|
568
|
+
- **Degrade in one line.** If the harness cannot publish — no capability, an
|
|
569
|
+
error — say so, report the local file path, and carry on. That is a working
|
|
570
|
+
outcome, not a failure.
|
|
571
|
+
- **Reach for `--publish-copy`, never a hand transform.** The engine writes the
|
|
572
|
+
page as a complete HTML document and an artifact host wraps page *content*, so
|
|
573
|
+
publishing the page as-written nests two documents. `spec-env review <spec>
|
|
574
|
+
--publish-copy` emits the body-only copy and names its path on a `publish:`
|
|
575
|
+
line. Do not split the document yourself: a rendered page contains the diff,
|
|
576
|
+
and a diff of this project contains `<!doctype html>` as ordinary patch text.
|
|
577
|
+
|
|
578
|
+
The engine knows nothing about publishing and cannot do it. It writes a file and
|
|
579
|
+
reads a URL back as an opaque string; everything about what that string means
|
|
580
|
+
lives here.
|
|
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
|
+
|
|
622
|
+
## 7. Report
|
|
623
|
+
|
|
624
|
+
End with the block defined in `.claude/rules/spec-reports.md`. That file carries
|
|
625
|
+
the shape; this section carries only what is specific here.
|
|
626
|
+
|
|
627
|
+
**Verdicts**
|
|
628
|
+
|
|
629
|
+
- `✅` — the page is rendered, and any review pass handed back was stored and
|
|
630
|
+
acted on.
|
|
631
|
+
- `⚠️` — rendered, with something worth knowing: accepts that lapsed because the
|
|
632
|
+
file changed, comments left unworked because the operator did not say go.
|
|
633
|
+
- `❌` — a render or a fix failed part-way. Quote it.
|
|
634
|
+
- `⏸` — the spec has no worktree. Say that plainly: a spec that has not been
|
|
635
|
+
started has nothing to diff, which is an ordinary state and not an error.
|
|
636
|
+
|
|
637
|
+
**Fields:** `Built` · `Tests` · `Notes` · `Review` · `Follow-ups` · `Next`
|
|
638
|
+
|
|
639
|
+
## 7a. End in a picker
|
|
640
|
+
|
|
641
|
+
Offer the same four endings `/spec-next` §6a defines — `Reviewed` · `Commit` ·
|
|
642
|
+
`Commit & Continue` · `Discuss` — under the same conditions, including
|
|
643
|
+
`Reviewed` only when a pass is waiting, and the rule that nothing claims a pass
|
|
644
|
+
without a pick. That section owns the wording; do not restate it.
|
|
645
|
+
|
|
646
|
+
**This is where changing your mind is handled, and that is why it belongs here.**
|
|
647
|
+
`/spec-diff` renders the page, so it is the command someone runs when they look
|
|
648
|
+
again — often at a review they have already voted on. A pass they sent is still
|
|
649
|
+
sitting **unclaimed**, so a pick made here **supersedes** it:
|
|
650
|
+
|
|
651
|
+
- act on the pick,
|
|
652
|
+
- drop the waiting pass (`--drop <code>`),
|
|
653
|
+
- and say both happened, naming the verdict that was dropped.
|
|
654
|
+
|
|
655
|
+
**Never carry both.** A stored verdict alongside a fresh one is two standing
|
|
656
|
+
conclusions about one review, and whichever a later run picked up would be a
|
|
657
|
+
coin toss. One ending, most recently chosen — the same rule that makes a verdict
|
|
658
|
+
consumed rather than stored.
|
|
659
|
+
|
|
660
|
+
**Say what was dropped rather than dropping it quietly.** Someone who pressed
|
|
661
|
+
`Commit` on their phone and then picks `Discuss` here has changed their mind on
|
|
662
|
+
purpose; someone who forgot they had voted has not, and only the report tells
|
|
663
|
+
them apart.
|
|
664
|
+
|
|
665
|
+
`Review` carries the files and `+`/`−`, the page's `open:` line, and the
|
|
666
|
+
published URL when there is one. Where the page holds a review pass, it also
|
|
667
|
+
carries the three totals — files accepted, comments open, comments answered.
|
|
668
|
+
`--json` reports those under `notes.totals`; read that, never the diff.
|
|
669
|
+
|
|
670
|
+
`Built` appears only when this run actually changed code — the commented files
|
|
671
|
+
it worked on your go-ahead. A render on its own built nothing, and an empty
|
|
672
|
+
`Built` line claiming otherwise is worse than no field.
|
|
673
|
+
|
|
674
|
+
`Tests` and a commit appear only on a committing verdict (§2a).
|
|
675
|
+
**Say which path made the commit** — the configured skill by name, or by hand —
|
|
676
|
+
in the same row as the sha. A reader cannot tell a `/commit` from a hand-rolled one after the
|
|
677
|
+
fact, so the run that made it is the only place that distinction can be
|
|
678
|
+
recorded.
|