@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.
Files changed (58) hide show
  1. package/MIGRATION.md +296 -10
  2. package/README.md +32 -2
  3. package/assets/claude-md-section.md +38 -2
  4. package/assets/commands/spec-connect.md +2 -2
  5. package/assets/commands/spec-live.md +2 -2
  6. package/assets/core/SETUP.md +21 -3
  7. package/assets/core/env.config.json.example +7 -3
  8. package/assets/core/env.config.md +90 -30
  9. package/assets/core/linear.config.md +58 -0
  10. package/assets/hooks/review-gate.js +141 -0
  11. package/assets/review/page.html +1787 -0
  12. package/assets/rules/spec-planning.md +250 -10
  13. package/assets/rules/spec-reports.md +321 -0
  14. package/assets/skills/spec/SKILL.md +33 -5
  15. package/assets/skills/spec-bug/SKILL.md +172 -9
  16. package/assets/skills/spec-cancel/SKILL.md +98 -21
  17. package/assets/skills/spec-claim/SKILL.md +114 -0
  18. package/assets/skills/spec-complete/SKILL.md +94 -25
  19. package/assets/skills/spec-diff/SKILL.md +678 -0
  20. package/assets/skills/spec-hotfix/SKILL.md +172 -11
  21. package/assets/skills/spec-init/SKILL.md +56 -7
  22. package/assets/skills/spec-linear-setup/SKILL.md +55 -1
  23. package/assets/skills/spec-list/SKILL.md +218 -0
  24. package/assets/skills/spec-next/SKILL.md +419 -7
  25. package/assets/skills/spec-push/SKILL.md +32 -8
  26. package/assets/skills/spec-review/SKILL.md +40 -5
  27. package/assets/skills/spec-reviewed/SKILL.md +258 -0
  28. package/assets/skills/spec-start/SKILL.md +386 -106
  29. package/assets/skills/spec-status/SKILL.md +24 -2
  30. package/assets/skills/spec-sync/SKILL.md +40 -4
  31. package/assets/skills/spec-to-main/SKILL.md +28 -6
  32. package/package.json +11 -7
  33. package/src/cli.js +1808 -89
  34. package/src/env/building.js +143 -0
  35. package/src/env/commitcmd.js +108 -0
  36. package/src/env/config.js +58 -9
  37. package/src/env/hooks.js +117 -0
  38. package/src/env/provision.js +54 -15
  39. package/src/env/proxy.js +34 -1
  40. package/src/env/render.js +3 -12
  41. package/src/env/resolve.js +295 -9
  42. package/src/env/review.js +1536 -0
  43. package/src/env/serve.js +573 -0
  44. package/src/env/teardown.js +13 -6
  45. package/src/init.js +150 -1
  46. package/src/vendor/linear/api.js +104 -1
  47. package/src/vendor/linear/cli-sync.js +854 -17
  48. package/src/vendor/linear/config.js +8 -0
  49. package/src/vendor/linear/credentials.js +94 -0
  50. package/src/vendor/linear/doctor.js +35 -0
  51. package/src/vendor/linear/identity.js +105 -0
  52. package/src/vendor/linear/mcp.js +26 -0
  53. package/src/vendor/sync-core/index.js +6 -2
  54. package/src/vendor/sync-core/src/compare.js +49 -3
  55. package/src/vendor/sync-core/src/normalize.js +30 -0
  56. package/src/vendor/sync-core/src/push.js +11 -1
  57. package/src/vendor/sync-core/src/write.js +38 -0
  58. package/LICENSE +0 -21
@@ -1,10 +1,15 @@
1
1
  ---
2
2
  name: spec-next
3
- description: Build the next unfinished phase of the spec in flight for this session — pre-flight, implement with tests, record progress and refresh the tracker. Refuses when no spec is in flight rather than guessing one, and never builds a spec it is not standing in. Use when the user says "/spec-next", "build the next phase", "continue the spec", or "carry on with this spec".
3
+ description: Build the next unfinished phase of the spec in flight for this session — pre-flight, implement with tests, record progress and refresh the tracker. Refuses when no spec is in flight rather than guessing one, and builds wherever that spec resolves rather than wherever the session happens to stand. Use when the user says "/spec-next", "build the next phase", "continue the spec", or "carry on with this spec".
4
4
  ---
5
5
 
6
6
  # /spec-next — build the next phase of the spec in flight
7
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
+
8
13
  It assumes the workbench is already set up: a spec is **in flight** on this
9
14
  checkout, and this skill implements its next unfinished phase. Putting a spec in
10
15
  flight — provisioning, moving it to `in-progress`, getting its branch here — is
@@ -13,7 +18,36 @@ flight — provisioning, moving it to `in-progress`, getting its branch here —
13
18
 
14
19
  ## 1. Identify the spec in flight
15
20
 
16
- Resolve **in this order**, and stop at the first that answers:
21
+ **`--worktree <path>` answers before anything else.** When the invocation names a
22
+ worktree, that is the spec to build and that is where it is built — cwd is not
23
+ consulted. It is how `/spec-start` carries on into phase 1 without moving your
24
+ session, and you can type it yourself.
25
+
26
+ **This is not a loosening of the refusal below.** That refusal exists against
27
+ *guessing* which spec to build, and a path someone typed is not a guess. A bare
28
+ `/spec-next` still refuses exactly as it does today.
29
+
30
+ **Validate the path before writing a line into it.** Run the resolver *from* the
31
+ path, so the answer comes from where you are about to write:
32
+
33
+ ```
34
+ cd "<path>" && skitterspec spec-env resolve
35
+ ```
36
+
37
+ Check the `worktree:` line it prints is that same path. If it is not — or the
38
+ command reports that isolation is not enabled — refuse and stop, naming what you
39
+ were given. A path that is not a provisioned worktree must never become a place
40
+ to write code.
41
+ **Read the output, not the exit status** — it exits 0 even when it
42
+ cannot resolve anything.
43
+
44
+ **Not `--dir <path>`.** That flag sets the **repo root**, not the worktree to
45
+ resolve from, so it answers a different question: on a repo with two or more
46
+ worktrees it refuses with *"no spec given, and N specs have worktrees"* and
47
+ validates nothing at all. The `cd` form is what makes the path itself the
48
+ evidence.
49
+
50
+ Otherwise resolve **in this order**, and stop at the first that answers:
17
51
 
18
52
  1. **The live spec of this checkout** — run
19
53
  `skitterspec spec-env live status` and read its `live:` line. `live: yes`
@@ -24,20 +58,65 @@ Resolve **in this order**, and stop at the first that answers:
24
58
  opened in one is its own workbench.
25
59
  3. **The current branch, in `checkout` mode** — no worktrees exist, so the
26
60
  branch the checkout is on names the spec.
61
+ 4. **The only spec provisioned in this repo** — ask the engine, with no spec
62
+ named, from wherever you happen to be standing:
63
+
64
+ ```
65
+ skitterspec spec-env resolve
66
+ ```
67
+
68
+ Take its `spec:` line **only when it names exactly one spec**.
69
+
70
+ This is the **durable** rung, and that is the whole reason it exists. Rungs 1
71
+ to 3 all read *session* state, and session state does not survive a `/clear`,
72
+ a new terminal tab, or coming back tomorrow — while the provisioned worktree
73
+ they are each a proxy for is **on disk** and survives all three. `/spec-start`
74
+ does leave the session standing in the worktree and that `cd` is real; what it
75
+ is not is durable. Without this rung, a repo holding exactly one answer sends
76
+ the operator away to re-supply something it already knew.
77
+
78
+ It is also the resolution every other bare command in this workflow already
79
+ uses — the worktree you are standing in, else the sole provisioned spec — so
80
+ this rung is what stops `/spec-next` being a silent exception to a rule
81
+ `.claude/rules/spec-planning.md` states has none left.
82
+
83
+ **Say which spec you resolved and how**, before writing a line of it:
84
+ *"not standing in a worktree — `<spec>` is the only spec provisioned"*. Rungs
85
+ 1 to 3 are self-evident to whoever typed the command; this one is not, and
86
+ the operator cannot see from where they sit what you picked.
87
+
88
+ **Several worktrees stay a refusal.** The engine names them and resolves
89
+ nothing — exactly the ambiguity the refusal below exists for. Relay its list
90
+ unchanged and stop; never pick from it.
91
+
92
+ WHAT WOULD FOOL THIS: a worktree left behind by a declined teardown is still
93
+ a worktree, so a finished spec can go on counting as provisioned. That widens
94
+ the candidate set, so the failure it produces is an extra candidate — an
95
+ ambiguity the engine refuses on — and never a wrong spec built. It cannot
96
+ manufacture an *absence*, which is why the absence below is still worth
97
+ refusing on.
27
98
 
28
99
  **If none answers, refuse and stop:**
29
100
  `no spec in flight — run /spec-start <name> to start one`.
30
101
 
102
+ That now answers a real absence — no worktree anywhere, the engine included —
103
+ rather than a session that merely lost track of where it was standing.
104
+
31
105
  **Never fall back to the spec "in context".** A spec discussed in conversation
32
106
  is not a spec in flight, and this skill writes real code: building the wrong
33
107
  spec's phase produces commits on a branch nobody asked for. The refusal is
34
108
  cheap; the mistake is not.
35
109
 
110
+ **Rung 4 is not that fallback wearing a hat.** A provisioned worktree is a
111
+ record that someone ran `/spec-start`: it is on disk, the engine reads it, and it
112
+ either names one spec or refuses. A spec named in conversation is a guess about
113
+ intent with nothing underneath it, and no number of them ever resolves to one.
114
+
36
115
  A **name argument** is accepted, but it must *match* the spec in flight — it
37
116
  narrows a re-run, it does not select a different spec. A mismatch refuses,
38
117
  naming both.
39
118
 
40
- ## 2. Pre-flight — commit prior work
119
+ ## 2. Pre-flight — the last phase is committed, and was answered
41
120
 
42
121
  Before writing any code for this phase, get the workspace clean:
43
122
 
@@ -49,6 +128,32 @@ Before writing any code for this phase, get the workspace clean:
49
128
  next phase on top of an uncommitted one. (Skip if this is the first phase —
50
129
  there's nothing prior to commit.)
51
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
+
52
157
  ## 3. Implement the phase
53
158
 
54
159
  Identify the **first unfinished phase** from the `00-overview.md` phase index,
@@ -79,6 +184,17 @@ once it is over. Without a provider this is a no-op and nothing below changes.
79
184
  them. Skip it and a started spec stays mirrored as a phase-less issue.
80
185
  - **Never mint the spec issue.** An unlinked spec is skipped, not created —
81
186
  `/spec-push` is how someone opts in.
187
+ - **Backfill a missing assignee silently; never ask.** Only when
188
+ `sync.fieldOwnership` includes `assignee`. If the spec records nobody and
189
+ `skitterspec spec-sync whoami --json` answers, stamp it with
190
+ `skitterspec spec-sync assign <spec> --to <id> --name "<name>"` and let the
191
+ push above carry it. If identity is unknown, skip it in silence.
192
+ **Do not prompt**: an assignment question in the middle of a build is an
193
+ interruption with no deadline, and `/spec-claim` answers it whenever the
194
+ operator likes.
195
+ - **A spec assigned to someone else is left alone.** Say so once
196
+ (`assigned to <name> — /spec-claim to take it`) and change nothing. Picking up
197
+ a colleague's spec is a decision, not a side effect of running the next phase.
82
198
  - **Never fatal.** If the push fails — offline, no key, a Linear error — say so
83
199
  and **carry on with the build**. The repo is correct regardless; the mirror is
84
200
  disposable and the next push repairs it.
@@ -89,6 +205,33 @@ once it is over. Without a provider this is a no-op and nothing below changes.
89
205
  branch/PR you pushed in step 2; that's expected and the repo still wins on the
90
206
  next `/spec-push`.
91
207
 
208
+ **Before building, compare the worktree against where you are standing.** Take
209
+ the `worktree:` line from `skitterspec spec-env resolve <spec>` and compare it
210
+ with this session's cwd, resolving both paths first so a symlinked or
211
+ trailing-slash spelling of one tree does not read as two.
212
+
213
+ Same tree — the ordinary case, since `/spec-start` leaves the session standing
214
+ in it — and the rest of this step is inert.
215
+ **Different trees, and the discipline below applies however the spec was resolved.**
216
+ `--worktree <path>` is one way to get here and §1's rung 4 is another: a bare
217
+ `/spec-next` typed from the primary checkout resolves the sole provisioned spec
218
+ and builds it somewhere this session is not. What makes the discipline necessary
219
+ is the two trees, so that is what it is conditioned on — not the shape of the
220
+ invocation, which cannot see rung 4 at all.
221
+
222
+ Record the baseline before you write anything:
223
+
224
+ ```
225
+ skitterspec spec-env resolve <spec> --record-primary
226
+ ```
227
+
228
+ Then build as below, with one discipline on top.
229
+ **The session is not standing in the worktree**, so every write takes an
230
+ absolute path under it and every command
231
+ is prefixed `cd "<worktreePath>" &&` — typecheck and tests included. A single
232
+ relative path lands the work in the primary checkout, on the base branch, and
233
+ nothing about it looks wrong at the time. Step 4b is what catches it.
234
+
92
235
  Then build it, following the project rules in `.claude/rules/*.md` and `CLAUDE.md`:
93
236
 
94
237
  - Work task by task through the phase file. Make focused edits that match
@@ -134,8 +277,277 @@ sub-issue jump from Backlog straight to Done, with nothing visible in between.
134
277
  - **Say what happened** in the skill's report: mirror updated, skipped as
135
278
  unlinked, or failed with the reason.
136
279
 
137
- ## 5. Report
280
+ ## 4b. Prove nothing leaked into the primary checkout
281
+
282
+ **Only when the resolved worktree is not this session's cwd** — the same
283
+ comparison step 3 made, and it holds however the spec was resolved. Standing in
284
+ the worktree there is no second tree to have written into, so this step does not
285
+ apply and there is nothing to check.
286
+
287
+ WHAT WOULD FOOL THIS CHECK: it watches the **primary checkout** and nothing
288
+ else, so a build run from inside *another* spec's worktree would leak there
289
+ unseen. That is left unhandled deliberately rather than overlooked — reaching it
290
+ takes an explicit `--worktree` typed from a second worktree — and the cost of the
291
+ gap is a missed leak, never a false accusation.
292
+
293
+ The phase is built and its progress recorded — all of it written into a tree this
294
+ session is not standing in. Before reporting any of it as done:
295
+
296
+ ```
297
+ skitterspec spec-env resolve <spec> --assert-primary-clean
298
+ ```
299
+
300
+ - **Exit 0, "primary checkout clean"** — carry on.
301
+ - **Non-zero** — stop and relay the engine's message unchanged. It names the
302
+ paths and both readings: this build wrote them and they belong in the worktree,
303
+ or something else did and the baseline wants re-recording.
304
+ **Do not guess which, and do not delete anything.**
305
+ A path that appeared is not proof of who put it there.
306
+ - **"cannot tell"** — no baseline, or one from another spec. It exits 0 and
307
+ claims nothing; say so in one line and carry on. An absence is not evidence.
308
+
309
+ ## 5. Render the page, arm the gate, then wait for the verdict
310
+
311
+ **Only when the project has per-spec isolation** (`specs/.core/env.config.json`
312
+ present). Without it there is no worktree to read and this step does not exist.
313
+
314
+ The phase is built, its tests are green, and nothing is committed yet. That is
315
+ the moment the page is about, so render it now — **after** the tests pass and
316
+ **before** the commit:
317
+
318
+ ```
319
+ skitterspec spec-env review <spec>
320
+ ```
321
+
322
+ **This is free.** It is the engine reading git and splicing text into a template;
323
+ the diff never passes through you, so a 266KB patch costs nothing. Report the
324
+ path it prints and move on.
325
+
326
+ **Then arm the gate**, so the phase now owes a verdict:
327
+
328
+ ```
329
+ skitterspec spec-env review arm <spec> --phase <n>
330
+ ```
331
+
332
+ It is idempotent within a phase, so a re-render does not restart the clock, and
333
+ it is **never fatal**: a project that opted out, or an engine that could not
334
+ resolve the spec, says so and the phase is still built. Nothing here counts
335
+ anything — the gate asserts that a phase which ended has an answer, and that is
336
+ all it asserts.
337
+
338
+ **Then offer `/spec-diff`. Do not run it.** The written review is the part that
339
+ costs — roughly **700 output tokens**, because writing it means reading the diff
340
+ — and that spend is the operator's call, not a default.
341
+
342
+ **Where you are going to wait, the offer is the banner after the block** —
343
+ defined in `.claude/rules/spec-reports.md`, and **the `Review` row is dropped**
344
+ so one subject lives in one place:
345
+
346
+ ---
347
+
348
+ ## ⏸ Review ready — <N> files, +<a> −<d>
349
+
350
+ **[Open the page](<the `open:` URL>)** · I'm holding here until you send a verdict.
351
+
352
+ `/spec-reviewed` picks it up · `spec-env review skip "<reason>"` moves on
353
+
354
+ **One link, and the engine has already chosen it.** Where it served, the
355
+ banner carries the served URL and the wait is real. Where it could not serve —
356
+ a busy port, a machine with no network address — that is the case publishing
357
+ exists for, and then the banner carries the published URL with what is true of
358
+ it: *press a verdict, then type `/spec-reviewed`*, because nothing pushes from
359
+ the artifact store into this conversation.
360
+
361
+ **Never offer both.** Publishing while the server is reachable adds a second
362
+ door the reader cannot tell apart from the first, and the wait only stands
363
+ behind one of them — that was done, and three verdicts were pressed on the
364
+ published page while each sat unread under a line saying I was holding. The
365
+ published page is for the reader the server cannot reach, and for nobody
366
+ else.
367
+
368
+ ---
369
+
370
+ **Where you are not waiting, it stays the `Review` row** — the counts, the page
371
+ link and a question, in one row:
372
+
373
+ | **Review** | <N> files, +<a> −<d> · [open the page](<the `open:` URL>) — want a written review before you commit? |
374
+
375
+ **Both shapes are addressed to someone, and that is the constraint.** The offer
376
+ was once a fenced block of engine output: two quoted lines under the test
377
+ counts, addressed to nobody, with the report then closing on *"commit this
378
+ first"* — the last instruction the reader got was to move on, so they did. A row
379
+ in a labelled table is findable; a question in it is answerable; a banner says
380
+ the work has stopped. What must never come back is something unaddressed,
381
+ unfindable, or fenced.
382
+
383
+ **Never bury it and never split it.** The row sits above the last two rows of
384
+ the block, and the page and the question stay in the same row; the banner
385
+ replaces the row rather than joining it. Two places naming one page make the reader
386
+ resolve a distinction before acting on either — which is the same failure
387
+ whether the two places are adjacent rows or a row and a banner.
388
+
389
+ ### Then wait for the verdict, where the harness can
390
+
391
+ The row is findable, but a row cannot make the continuation follow from the
392
+ reading — and that is the gap the whole gate exists to close.
393
+ **Where this harness can watch a file and wake the session on a change, use it.** The pass
394
+ arrives at the engine's holding area, the watch fires, and the verdict the
395
+ reader pressed is what carries the work on.
396
+
397
+ 1. **Note the moment you start waiting**, as an ISO timestamp. That instant is
398
+ the whole scope of what you may claim.
399
+ 2. **Watch the pending store** for the spec —
400
+ `.spec-env/reviews/<spec>.pending.json` in the primary checkout — and
401
+ **end your turn**. Do not poll, and do not hold the turn open: the point is that
402
+ the reader has the terminal back while they read.
403
+ 3. **On waking, let the engine pick**:
404
+
405
+ ```
406
+ skitterspec spec-env review <spec> --claim-since <the timestamp> --json
407
+ ```
408
+
409
+ It claims the one pass that arrived inside the window, and acts on nothing
410
+ at all when none did (the watch can fire on a write that was not a pass) or
411
+ when two did (two sittings, or two people — the operator has the codes).
412
+ 4. **Route on the verdict** exactly as `/spec-diff` §2 and §4 describe. Do not
413
+ restate that routing here.
414
+
415
+ **WHY THIS IS SAFE, AND WHAT IT COSTS.** It was once true that a device
416
+ reaching your page could not reach your conversation, and that fact was the
417
+ whole guard: a pass sat in the holding area until a person typed
418
+ `/spec-reviewed`. This replaces that guard rather than weakening it by
419
+ accident, and the replacement is two things together — **the serve token**,
420
+ 48 unguessable bits minted per server, which is what decides who can POST at
421
+ all; and **the window**, which is what decides which pass is yours. A pass
422
+ already waiting when the wait began is never claimed by it, which is exactly
423
+ the stranger's pass the old rule was written about. What is genuinely given up
424
+ is that the page can now act, so the token has become a credential rather than
425
+ a convenience — and `--claim-since` refusing to choose between two passes is
426
+ what stops a race becoming a wrong commit.
427
+
428
+ **Where the harness cannot watch a file, change nothing.** The `Review` row and
429
+ `/spec-reviewed` are the whole story, exactly as before. The gate still holds
430
+ either way: it is the engine's, not the watch's.
431
+
432
+ **It still does not break a chained run.** `/commit && /spec-next` is typed as
433
+ one line; by the time this step is reached the chain has finished, so waiting
434
+ here stops nothing that was still going to happen.
435
+
436
+ Relay the **`open:`** line the engine prints, not the bare path: a path is not
437
+ clickable in any terminal, and a page nobody can open is a page nobody reads.
438
+
439
+ - **Never write the review unasked**, and **never publish**. Publishing leaves
440
+ something behind that this tooling cannot remove, so it is always something
441
+ someone asks for. A `file://` link is no use on a phone, and saying so **is**
442
+ the ask — publishing is the answer to it, and `/spec-diff` §6 owns how.
443
+ **Follow the `reader:` line the engine printed — do not sniff for it.** It
444
+ answers where the person reading this is sitting, and the offer changes with it:
445
+
446
+ - **absent** (`unknown`) — the `file://` URL, exactly as always. **Do not warn:**
447
+ unknown is the ordinary state of a local machine, and a warning there is an
448
+ accusation against a healthy session.
449
+ - **`local`** — the `file://` URL.
450
+ - **`remote`** — the engine has already stood its local server up and put a URL
451
+ the reader can open on `open:`. So there is **nothing special to say**: relay
452
+ that line like any other. Any `also:` lines under it are the other addresses
453
+ this machine has, offered because the best-guess one can be wrong — pass them
454
+ on rather than editing them out.
455
+
456
+ **Never read an environment variable to decide this** — not `SSH_CONNECTION`,
457
+ not `CLAUDE_CODE_*`, not a tty check. The engine did it, reports it on that line
458
+ and in `--json`, and a second implementation here could not be tested and would
459
+ drift.
460
+
461
+ **Serving is the engine's to do; publishing is never.** A `remote` reader
462
+ authorises a local server — one process, ended by one flag, leaving nothing
463
+ behind — and authorises nothing else. A reader the server CAN reach is not a
464
+ reason to publish as well: the page they can already open is the page to name. Publishing leaves a page this tooling
465
+ cannot remove, so it stays an ask in every case, always. If the engine could not
466
+ serve (a busy port, a machine with no network address) it falls back to the
467
+ `file://` URL with its marker, and that is when publishing is worth naming.
468
+
469
+ - **Never fatal.** A failed render — no worktree, a git error — is one line and
470
+ the phase is still done. The page is a convenience; the repo is the record.
471
+ - If the project has no isolation config, skip the whole step in silence rather
472
+ than explaining an absence.
473
+
474
+ ## 6. Report
475
+
476
+ Do **not** `git commit` unless the user asks — finish, verify, and wait.
477
+
478
+ End with the block defined in `.claude/rules/spec-reports.md`. That file carries
479
+ the shape; this section carries only what is specific here.
480
+
481
+ **Verdicts**
482
+
483
+ - `✅` — the phase is built and its tests are green.
484
+ - `⚠️` — built and green, with something worth knowing (a mirror that did not
485
+ refresh, a deviation from the plan).
486
+ - `❌` — the phase's tests are red, or it stopped part-way. Quote the failure.
487
+ - `⏸` — no spec in flight, or the name given does not match the one that is.
488
+
489
+ **Fields:** `Tracker` · `Branch` · `Built` · `Tests` · `Notes` · `Review` ·
490
+ `Follow-ups` · `Next`
491
+
492
+ ## 6a. End in a picker
493
+
494
+ The block says what happened; this is what to do about it. Offer the same four
495
+ endings the review page carries, so a review finishes the same way wherever the
496
+ reader is standing — the page, a pasted code, or here.
497
+
498
+ **Not when you are waiting.** Where §5 set a watch and ended the turn, the
499
+ verdict is coming from the page and the picker would be a second way to answer
500
+ a question already asked — so the block ends on `Next`, which names the page.
501
+ The picker is for the run that did not wait: no watch available, or a render
502
+ the reader is expected to come back to in their own time.
503
+
504
+ | Option | Does |
505
+ |--------|------|
506
+ | `Reviewed` | Claims the waiting pass and routes on its verdict |
507
+ | `Commit` | Runs the project's commit skill, and stops |
508
+ | `Commit & Continue` | Commits, then `/spec-next` — and **stops there** |
509
+ | `Discuss` | Asks what is up; changes nothing |
510
+
511
+ **`Reviewed` only when a pass is actually waiting.** The render's `pending:`
512
+ block already says. Offering a pickup with nothing to pick up is the empty
513
+ gesture this exists against — the other three stand on their own.
514
+
515
+ **Do not restate the routing.** `/spec-diff` §2, §2a and §4 own it, including
516
+ the commit hand-off through `review.commitWith` and what `commit-continue` does
517
+ after. Two copies of a routing rule is how the two come to disagree.
518
+
519
+ **It does not break a chained run.** `/commit && /spec-next` is typed as one
520
+ line and the picker appears at the **end**, by which point the chain has already
521
+ finished. The cost that was feared here — a question stopping a run mid-way — is
522
+ not a cost this placement has.
523
+
524
+ **Nothing may claim a pass without a pick.** `/spec-reviewed` is user-only by
525
+ *harness enforcement*, because prose alone once failed to stop an agent claiming
526
+ a pass nobody asked it to. A pick keeps the **property** that makes that safe —
527
+ a person in the conversation chose, and a device that reaches the page cannot —
528
+ while routing around the **mechanism**, since the claim runs downstream of the
529
+ pick. That trade is deliberate and it has exactly one condition: a run that
530
+ shows no picker claims nothing, and a picker nobody answered claims nothing.
531
+
532
+ WHAT WOULD FOOL THIS: a picker shown reflexively at the end of every run trains
533
+ the reader to dismiss it, and a dismissed picker is indistinguishable from a
534
+ considered decline. So offer it where there is a real choice, and let the `Next`
535
+ row carry the rest.
536
+
537
+ `Next` names the commit and then the phase, as
538
+ `/commit, then /spec-next → phase 3 (Auth)`, so the block says what to do and
539
+ which phase is next without a line of prose for either.
540
+
541
+ **The commit is not optional politeness.** Step 6 above deliberately leaves the
542
+ phase uncommitted, and §2 of this very skill refuses to build the next phase on
543
+ top of an uncommitted one — so a `Next` that names only `/spec-next` sends the
544
+ reader straight into that refusal. The two halves are four hundred lines apart,
545
+ which is exactly how they drifted.
138
546
 
139
- Summarise what was implemented, the test result (quote failures if any), and
140
- which phase is next. Do **not** `git commit` unless the user asks — finish,
141
- verify, and wait.
547
+ **Step 5's offer lands in one of two shapes, and never both.** Waiting on a
548
+ verdict the **banner** after the block, and no `Review` row. Not waiting the
549
+ `Review` row, and no banner. Neither is a paragraph: the ban on prose after the
550
+ block is untouched, and the banner is a control the contract names
551
+ (`.claude/rules/spec-reports.md`). Step 5 renders before the commit and this
552
+ step is where its offer lands, so the two must not disagree about which shape it
553
+ takes.
@@ -5,6 +5,11 @@ description: Push a spec up to its linked Linear issue (repo → Linear, one-way
5
5
 
6
6
  # /spec-push — send a spec up to Linear (one-way)
7
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
+
8
13
  Repo → Linear. The repo is the **source of truth**; Linear is a
9
14
  **generated mirror**. A spec is a Linear **issue**; each phase is a
10
15
  **sub-issue** (a child issue), and the phase file is mirrored into that
@@ -67,7 +72,7 @@ get past a failing check.
67
72
  ## 3. Get the plan from the engine
68
73
 
69
74
  ```
70
- skitterspec spec-sync push <spec> --workspace-states <file> --json
75
+ skitterspec spec-sync plan <spec> --workspace-states <file> --json
71
76
  ```
72
77
 
73
78
  The engine prints a JSON **plan** (no network, no remote read):
@@ -230,14 +235,33 @@ branch so the mirror-link rides in the PR.
230
235
 
231
236
  ## 6. Report
232
237
 
233
- Summarise what was created/updated in Linear (the spec issue and its
234
- sub-issues), **say which transport was used**, and confirm the snapshot was
235
- recorded. There is no pull — Linear is a generated mirror.
238
+ End with the block defined in `.claude/rules/spec-reports.md`. That file carries
239
+ the shape; this section carries only what is specific here.
240
+
241
+ **Verdicts**
242
+
243
+ - `✅` — the mirror matches the repo and the snapshot is recorded.
244
+ - `⚠️` — pushed, with something worth knowing: Linear stored different text, a
245
+ project picker that could not be offered, `phasesDeferred` phases not yet
246
+ minted.
247
+ - `❌` — it wrote some objects and failed on another. Name what exists in Linear
248
+ now; `apply` stamps each id as it goes, so the re-run is an update, never a
249
+ duplicate.
250
+ - `⏸` — no config, no `linear_identifier`, an unvalidated state list, or a
251
+ pre-9.0 mirror. Nothing was written.
252
+
253
+ **Fields:** `Tracker` · `Follow-ups` · `Next`
254
+
255
+ `Tracker` carries what was created and updated — the spec issue and its
256
+ sub-issues — **the transport that did it**, and that the snapshot was recorded.
257
+
258
+ **Say the transport, every time.** On the API path you never saw the
259
+ descriptions, so "pushed 12 sub-issues" is the engine's report rather than your
260
+ observation, and that distinction is the difference between a fact and a
261
+ paraphrase. If it warned that Linear stored different text, relay it: the repo
262
+ is still correct and a re-push overwrites the mirror.
236
263
 
237
- Saying the transport matters: on the API path you never saw the descriptions, so
238
- "pushed 12 sub-issues" is the engine's report, not your observation. If it warned
239
- that Linear stored different text, relay that — the repo is still correct, and a
240
- re-push overwrites the mirror.
264
+ There is no pull Linear is a generated mirror.
241
265
 
242
266
  ### Picking the Linear Project
243
267
 
@@ -5,6 +5,11 @@ description: Re-validate an existing spec against the current codebase — detec
5
5
 
6
6
  # /spec-review — bring a spec back in sync with the codebase
7
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
+
8
13
  Specs rot: the code moves on while a spec sits in the backlog or pauses
9
14
  mid-build. This skill re-validates a spec against the **current** code and
10
15
  rewrites the stale parts so it's safe to act on. It plans only — it does not
@@ -115,6 +120,12 @@ Linear API key is set (see `apply.transport` in `linear.config.md`).
115
120
  Do not roll anything back, and do not stop to ask.
116
121
  - **Say what happened** in the skill's report: mirror updated, skipped as
117
122
  unlinked, or failed with the reason.
123
+ - **There is no unassign step, and that is not an omission.** With `assignee` in
124
+ `sync.fieldOwnership`, the projection derives it from the spec's lifecycle
125
+ bucket — so moving the folder to `complete`/`cancelled` is *itself* what
126
+ releases the issue, and the push above carries it like any other field. The
127
+ `linear_assignee_id` stamp and the `> **Developer:**` header both stay put:
128
+ they record who actioned the work, which outlives who is holding it.
118
129
 
119
130
  ### Why it sits here
120
131
 
@@ -125,12 +136,36 @@ In `/spec-complete` and `/spec-cancel` this step is deliberately pinned
125
136
  its folder bucket. Push while the folder is still in `in-progress/` and the
126
137
  issue is set to the state the spec is *leaving*.
127
138
  - **Before the commit**, because the push stamps ids into the spec and writes a
128
- snapshot under `specs/.core/`. The `git add specs/` that follows sweeps both up
129
- with the status change; push after it instead and those files are left
139
+ snapshot under `specs/.core/`. Push after the commit instead and both are left
130
140
  uncommitted, which makes `spec-env integrate` refuse to land the branch.
131
141
 
142
+ The snapshot is reached **by name**, not by breadth. It is a declared
143
+ `spec.companionPaths` entry (`specs/.core/linear-base/{identifier}.base.json`),
144
+ so `spec-env stage` returns it among this spec's owned paths and the commit below
145
+ names it. This is why the commit no longer stages `specs/` wholesale: the one
146
+ file outside the spec's folder that had to be swept up is addressable, so the
147
+ breadth bought nothing and cost another session's work.
148
+
132
149
  ## 5. Report
133
150
 
134
- Summarise the drift found, what you changed, any questions still open, and
135
- whether the spec is now safe to `/spec-start` (or should drop back to `Draft` until
136
- the open questions are resolved). Do **not** `git commit` unless the user asks.
151
+ Do **not** `git commit` unless the user asks.
152
+
153
+ End with the block defined in `.claude/rules/spec-reports.md`. That file carries
154
+ the shape; this section carries only what is specific here.
155
+
156
+ **Verdicts**
157
+
158
+ - `✅` — checked against the code and brought up to date; safe to `/spec-start`.
159
+ - `⚠️` — updated, but dropped back to `Draft`: questions are open that only the
160
+ operator can answer. Name them.
161
+ - `⏸` — no such spec, or nothing to review. Nothing changed.
162
+
163
+ **Fields:** `Tracker` · `Spec` · `Built` · `Follow-ups` · `Next`
164
+
165
+ `Built` is the drift found and what you changed about it — a renamed file, an
166
+ API that moved, a task the code already does. `Spec` carries the status the
167
+ review leaves it in, which is the answer to "can I start this?".
168
+
169
+ **Drift found and not fixed is a `Follow-up`, not a silence.** A spec this skill
170
+ declared reviewed is one nobody will re-read; something it noticed and left
171
+ undone has to leave the session in writing or it did not happen.