@skitterbyte/skitterspec-linear 15.0.0 → 17.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 (44) hide show
  1. package/MIGRATION.md +218 -0
  2. package/README.md +34 -1
  3. package/assets/claude-md-section.md +29 -18
  4. package/assets/commands/spec-remote-review.md +22 -0
  5. package/assets/core/SETUP.md +10 -6
  6. package/assets/core/env.config.json.example +4 -2
  7. package/assets/core/env.config.md +100 -23
  8. package/assets/core/linear.config.json.example +2 -1
  9. package/assets/core/linear.config.md +49 -22
  10. package/assets/review/page.html +1044 -101
  11. package/assets/rules/spec-planning.md +35 -3
  12. package/assets/rules/spec-reports.md +131 -20
  13. package/assets/skills/spec/SKILL.md +161 -4
  14. package/assets/skills/spec-bug/SKILL.md +68 -30
  15. package/assets/skills/spec-cancel/SKILL.md +2 -2
  16. package/assets/skills/spec-claim/SKILL.md +12 -4
  17. package/assets/skills/spec-complete/SKILL.md +2 -2
  18. package/assets/skills/spec-diff/SKILL.md +131 -36
  19. package/assets/skills/spec-hotfix/SKILL.md +61 -25
  20. package/assets/skills/spec-linear-setup/SKILL.md +19 -11
  21. package/assets/skills/spec-next/SKILL.md +121 -58
  22. package/assets/skills/spec-push/SKILL.md +45 -0
  23. package/assets/skills/spec-review/SKILL.md +89 -2
  24. package/assets/skills/spec-reviewed/SKILL.md +31 -5
  25. package/assets/skills/spec-start/SKILL.md +26 -3
  26. package/assets/skills/spec-status/SKILL.md +20 -6
  27. package/assets/skills/spec-sync/SKILL.md +1 -0
  28. package/package.json +2 -2
  29. package/src/cli.js +913 -116
  30. package/src/env/classify.js +87 -2
  31. package/src/env/config.js +214 -17
  32. package/src/env/live.js +94 -0
  33. package/src/env/resolve.js +36 -2
  34. package/src/env/review.js +542 -21
  35. package/src/env/serve.js +298 -19
  36. package/src/env/supervise.js +8 -1
  37. package/src/init.js +60 -9
  38. package/src/vendor/linear/api.js +111 -2
  39. package/src/vendor/linear/cli-sync.js +661 -11
  40. package/src/vendor/linear/config.js +41 -13
  41. package/src/vendor/linear/doctor.js +6 -5
  42. package/src/vendor/sync-core/index.js +11 -3
  43. package/src/vendor/sync-core/src/compare.js +65 -0
  44. package/src/vendor/sync-core/src/normalize.js +26 -0
@@ -154,6 +154,24 @@ Before writing any code for this phase, get the workspace clean:
154
154
  being *"I am moving on"*, which is what keeps this a push rather than a wall —
155
155
  a gate with no exit gets switched off wholesale instead of answered.
156
156
 
157
+ - **Report anything left waiting, and claim none of it.** Ask the engine once:
158
+
159
+ ```
160
+ skitterspec spec-env review waiting
161
+ ```
162
+
163
+ A pass listed here arrived when nothing was watching — a wait that never ran,
164
+ a session cleared, a terminal closed overnight — and no watcher can recover
165
+ those, however good. Relay what it prints: the spec, the code, the verdict and
166
+ the age, with `/spec-reviewed <code>` to pick one up and `--drop <code>` to
167
+ disown it.
168
+
169
+ **It is information, not a gate.** It never refuses, nothing here is
170
+ conditional on it, and **you never claim one** — `/spec-diff` §0 stands
171
+ unchanged, and a pass sitting there when you arrived was not sent to you.
172
+ Silent when nothing is waiting, which is the usual case: say nothing rather
173
+ than reporting that there was nothing to report.
174
+
157
175
  ## 3. Implement the phase
158
176
 
159
177
  Identify the **first unfinished phase** from the `00-overview.md` phase index,
@@ -184,8 +202,8 @@ once it is over. Without a provider this is a no-op and nothing below changes.
184
202
  them. Skip it and a started spec stays mirrored as a phase-less issue.
185
203
  - **Never mint the spec issue.** An unlinked spec is skipped, not created —
186
204
  `/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
205
+ - **Backfill a missing assignee silently; never ask.** Only when the repo owns
206
+ `assignee` (it does by default; `"none"` declines it). If the spec records nobody and
189
207
  `skitterspec spec-sync whoami --json` answers, stamp it with
190
208
  `skitterspec spec-sync assign <spec> --to <id> --name "<name>"` and let the
191
209
  push above carry it. If identity is unknown, skip it in silence.
@@ -353,30 +371,51 @@ so one subject lives in one place:
353
371
 
354
372
  ## ⏸ Review ready — <N> files, +<a> −<d>
355
373
 
356
- **[Open the page](<the `open:` URL>)** · I'm holding here until you send a verdict.
374
+ - **local** <the `local:` URL>
375
+ - **network** — <the `network:` URL, or off with the command that turns it on>
376
+ - **remote** — <the `remote:` URL, or off with the command that turns it on>
357
377
 
358
- `/spec-reviewed` picks it up · `spec-env review skip "<reason>"` moves on
378
+ <the `live:` line, exactly as the engine printed it omitted when it printed none>
359
379
 
360
- **One link, and the engine has already chosen it.** Where it served, the
361
- banner carries the served URL and the wait is real. Where it could not serve —
362
- a busy port, a machine with no network address that is the case publishing
363
- exists for, and then the banner carries the published URL with what is true of
364
- it: *press a verdict, then type `/spec-reviewed`*, because nothing pushes from
365
- the artifact store into this conversation.
380
+ I'm holding here until you send a verdict the wait covers local and network.
381
+
382
+ `/spec-reviewed` picks it up · `spec-env review skip "<reason>"` moves on
366
383
 
367
- **Never offer both.** Publishing while the server is reachable adds a second
368
- door the reader cannot tell apart from the first, and the wait only stands
369
- behind one of them that was done, and three verdicts were pressed on the
370
- published page while each sat unread under a line saying I was holding. The
371
- published page is for the reader the server cannot reach, and for nobody
372
- else.
384
+ **The stack is the engine's copy it, do not compose it.** `spec-env review`
385
+ printed one line per tier, in that order, and `--json` carries the same thing as
386
+ `tiers`. Relay all three whatever they say, including the ones that are off with
387
+ the command that turns them on: a reader who has left the house cannot ask for a
388
+ remote page they were never shown.
389
+
390
+ **Do not pick one for them.** The engine used to, and the guess failed three
391
+ separate ways in one day. `.claude/rules/spec-reports.md` carries the shape and
392
+ why the *one link, never two* rule now reads
393
+ **one link per reachable store, each labelled** —
394
+ local and network are two doors into one room, so the wait
395
+ covers both. `remote` is a second store, so its own line carries its own caveat:
396
+ *a verdict here needs `/spec-reviewed`*, because nothing pushes from
397
+ the artifact store into this conversation. Never let the holding line promise a
398
+ wait over that tier.
399
+
400
+ **Publishing is still never yours to do.** `remote` reading `off` is the normal
401
+ state, and the fix for it is the command on that line — typed by the operator,
402
+ or pressed on the page, because a published page is one this tooling cannot
403
+ remove and permitting it is not publishing it.
404
+
405
+ **The `live:` line goes with the stack**, and it is the engine's line too —
406
+ copied as printed, and **absent when the engine printed none**. The page can act
407
+ on it: a press puts the spec live or takes it down and hands the reader back the
408
+ same page, which is `/spec-diff` §2b's routing and not this skill's. What
409
+ matters here is that an action **never clears the gate this phase armed** — the
410
+ reader has looked at it running and concluded nothing, so the phase still owes a
411
+ verdict.
373
412
 
374
413
  ---
375
414
 
376
415
  **Where you are not waiting, it stays the `Review` row** — the counts and the
377
416
  page link, and **no question**:
378
417
 
379
- | **Review** | <N> files, +<a> −<d> · [open the page](<the `open:` URL>) |
418
+ | **Review** | <N> files, +<a> −<d> · **local** <URL> · **network** <URL> · **remote** off |
380
419
 
381
420
  At the end of a phase you are always waiting, so this shape belongs to the
382
421
  renders that are not this step: a mid-phase `/spec-diff`, a page produced
@@ -406,16 +445,25 @@ whether the two places are adjacent rows or a row and a banner.
406
445
 
407
446
  The row is findable, but a row cannot make the continuation follow from the
408
447
  reading — and that is the gap the whole gate exists to close.
409
- **Where this harness can watch a file and wake the session on a change, use it.** The pass
410
- arrives at the engine's holding area, the watch fires, and the verdict the
411
- reader pressed is what carries the work on.
448
+
449
+ **The wait is a command. Do not write one.**
412
450
 
413
451
  1. **Note the moment you start waiting**, as an ISO timestamp. That instant is
414
452
  the whole scope of what you may claim.
415
- 2. **Watch the pending store** for the spec
416
- `.spec-env/reviews/<spec>.pending.json` in the primary checkout — and
417
- **end your turn**. Do not poll, and do not hold the turn open: the point is that
418
- the reader has the terminal back while they read.
453
+ 2. **Run the engine's wait in the background, and end your turn:**
454
+
455
+ ```
456
+ skitterspec spec-env review wait <spec> --since <the timestamp>
457
+ ```
458
+
459
+ It returns when a pass arrives inside that window, and
460
+ **it takes no timeout unless you pass one** —
461
+ the wait lasts as long as your session, because a reader who walks away from
462
+ a diff is the normal case rather than the edge one. Do not give it a duration
463
+ of your own.
464
+
465
+ Then **end your turn**. Do not poll it and do not hold the turn open: the
466
+ point is that the reader has the terminal back while they read.
419
467
  3. **On waking, let the engine pick**:
420
468
 
421
469
  ```
@@ -423,11 +471,26 @@ reader pressed is what carries the work on.
423
471
  ```
424
472
 
425
473
  It claims the one pass that arrived inside the window, and acts on nothing
426
- at all when none did (the watch can fire on a write that was not a pass) or
427
- when two did (two sittings, or two people — the operator has the codes).
474
+ at all when none did (the wait can be woken by something that was not a
475
+ pass) or when two did (two sittings, or two people — the operator has the
476
+ codes).
428
477
  4. **Route on the verdict** exactly as `/spec-diff` §2 and §4 describe. Do not
429
478
  restate that routing here.
430
479
 
480
+ **WHY A COMMAND RATHER THAN A LOOP YOU COMPOSE.** This step used to say "watch
481
+ the pending store" and stop, so every run invented its own watcher in shell —
482
+ and three failed in two days, each reaching the operator as *"I pressed the
483
+ button and nothing happened"*. The worst wrote
484
+ `until [ -f "$P" ] && [ "$x" \> "$y" ]`: valid bash, a syntax error in zsh, a
485
+ predicate that could never be true. It spun for five minutes writing to a
486
+ stderr nobody reads.
487
+
488
+ **The bug is not the lesson — the silence is.** A watcher that can never fire
489
+ and one patiently working are indistinguishable from outside, so nothing about
490
+ that run looked wrong until the operator asked. `review wait` says it has
491
+ started, is written once, and is tested against a store that gains a pass
492
+ mid-flight. A predicate composed fresh each time is proven by nothing.
493
+
431
494
  **WHY THIS IS SAFE, AND WHAT IT COSTS.** It was once true that a device
432
495
  reaching your page could not reach your conversation, and that fact was the
433
496
  whole guard: a pass sat in the holding area until a person typed
@@ -441,11 +504,11 @@ is that the page can now act, so the token has become a credential rather than
441
504
  a convenience — and `--claim-since` refusing to choose between two passes is
442
505
  what stops a race becoming a wrong commit.
443
506
 
444
- **Where the harness cannot watch a file, you still wait** — the turn ending is
445
- the wait. Say the page is rendered and that you are holding for the pass, then end
446
- your turn; the reader's next message is what carries it, and `/spec-diff` picks
447
- it up from the paste exactly as it always has. The gate holds either way: it is
448
- the engine's, not the watch's.
507
+ **Where the harness cannot run something in the background, you still wait** —
508
+ the turn ending is the wait. Say the page is rendered and that you are holding
509
+ for the pass, then end your turn; the reader's next message is what carries it,
510
+ and `/spec-diff` picks it up from the paste exactly as it always has. The gate
511
+ holds either way: it is the engine's, not the wait's.
449
512
 
450
513
  This once read *"change nothing"* — keep the row, keep the question, do not
451
514
  wait — and that exemption is the hatch a whole class of unanswerable questions
@@ -455,38 +518,38 @@ came through. Every harness can end a turn.
455
518
  one line; by the time this step is reached the chain has finished, so waiting
456
519
  here stops nothing that was still going to happen.
457
520
 
458
- Relay the **`open:`** line the engine prints, not the bare path: a path is not
459
- clickable in any terminal, and a page nobody can open is a page nobody reads.
521
+ Relay the engine's **stack** the `local:`, `network:` and `remote:` lines
522
+ never the bare `page:` path: a path is not clickable in any terminal, and a page
523
+ nobody can open is a page nobody reads.
460
524
 
461
525
  - **Never write the review unasked**, and **never publish**. Publishing leaves
462
526
  something behind that this tooling cannot remove, so it is always something
463
527
  someone asks for. A `file://` link is no use on a phone, and saying so **is**
464
528
  the ask — publishing is the answer to it, and `/spec-diff` §6 owns how.
465
- **Follow the `reader:` line the engine printed do not sniff for it.** It
466
- answers where the person reading this is sitting, and the offer changes with it:
467
-
468
- - **absent** (`unknown`) the `file://` URL, exactly as always. **Do not warn:**
469
- unknown is the ordinary state of a local machine, and a warning there is an
470
- accusation against a healthy session.
471
- - **`local`** the `file://` URL.
472
- - **`remote`** — the engine has already stood its local server up and put a URL
473
- the reader can open on `open:`. So there is **nothing special to say**: relay
474
- that line like any other. Any `also:` lines under it are the other addresses
475
- this machine has, offered because the best-guess one can be wrong — pass them
476
- on rather than editing them out.
477
-
478
- **Never read an environment variable to decide this** not `SSH_CONNECTION`,
479
- not `CLAUDE_CODE_*`, not a tty check. The engine did it, reports it on that line
480
- and in `--json`, and a second implementation here could not be tested and would
481
- drift.
482
-
483
- **Serving is the engine's to do; publishing is never.** A `remote` reader
484
- authorises a local serverone process, ended by one flag, leaving nothing
485
- behind and authorises nothing else. A reader the server CAN reach is not a
486
- reason to publish as well: the page they can already open is the page to name. Publishing leaves a page this tooling
487
- cannot remove, so it stays an ask in every case, always. If the engine could not
488
- serve (a busy port, a machine with no network address) it falls back to the
489
- `file://` URL with its marker, and that is when publishing is worth naming.
529
+ **The `reader:` line no longer decides anything here, and that is the point.**
530
+ It is still printed, and it is still the only place that question is answered —
531
+ but the offer does not change with it, because the stack lists every tier
532
+ whatever it says. Three reader states used to mean three different offers, and
533
+ that branching is exactly what produced a `file://` page on a session detected
534
+ `unknown`, a LAN URL for a phone off the network, and an address that changed
535
+ underneath a reader when detection flipped mid-session.
536
+
537
+ So: **relay all three tier lines, every time.** Any `also:` lines sit under
538
+ `network` the other addresses this machine has, offered because the
539
+ best-guess one can be wrong — so pass them on rather than editing them out.
540
+
541
+ **Never read an environment variable to decide anything about the offer** — not
542
+ `SSH_CONNECTION`, not `CLAUDE_CODE_*`, not a tty check. There is nothing left
543
+ here for a detection to decide, and a second implementation of one could not be
544
+ tested and would drift.
545
+
546
+ **Serving is the engine's to do; publishing is never.** The server is one
547
+ process, ended by one flag, leaving nothing behind, and `review.allowNetwork`
548
+ decides whether it binds wide no detection involved. A reader the server CAN
549
+ reach is not a reason to publish as well: the tiers they can already open are on
550
+ the stack. Publishing leaves a page this tooling cannot remove, so it stays an
551
+ ask in every case, always which is why `remote` shows the command rather than
552
+ a URL until someone types it.
490
553
 
491
554
  - **Never fatal.** A failed render — no worktree, a git error — is one line and
492
555
  the phase is still done. The page is a convenience; the repo is the record.
@@ -155,6 +155,24 @@ the chosen project through as `--project <id>`.
155
155
 
156
156
  ## 4a. Apply it yourself — the MCP path (order matters)
157
157
 
158
+ **First, check whether you are about to overwrite someone.** On the API path
159
+ `apply` does this for itself, off the read-back it already makes. On the MCP
160
+ path nothing has read the issue yet, so ask before you write:
161
+
162
+ ```
163
+ skitterspec spec-sync status <spec> --remote <issuefile>
164
+ ```
165
+
166
+ using the same issue JSON step 2 fetched — it must carry the `description`. A
167
+ `drift: … description was edited on Linear since the last push` line means a
168
+ person wrote something there that step 1 below replaces.
169
+
170
+ **It is a warning and never a gate.** The repo is the source of truth and the
171
+ push still goes ahead; relay the line, name the issue, and carry on. What is
172
+ forbidden is doing it *silently* — someone's paragraph disappearing with nothing
173
+ said about it is the failure this exists to end. Nothing printed means nothing to
174
+ say, which includes every case the engine could not tell.
175
+
158
176
  1. **Spec issue** → if the overview has no `linear_identifier`, this push
159
177
  **mints** it: run the picker in **Picking the Linear Project** below, then
160
178
  create it with `save_issue` (`team` = `linear.teamId`; `project` = the picked
@@ -233,6 +251,33 @@ This writes the last-pushed snapshot from the now-stamped files, so the next
233
251
  `/spec-push` produces an empty plan. Commit the stamped spec + snapshot into the
234
252
  branch so the mirror-link rides in the PR.
235
253
 
254
+ ## 5b. A refused mint, and the way back
255
+
256
+ Two refusals reach you from `apply`, and neither is a failure to retry.
257
+
258
+ **`N already exists and no spec claims it`** — the push was about to mint, and
259
+ an issue with **exactly** this title is sitting there unheld. That is almost
260
+ always the spec's own issue, whose link was lost: a bad merge, a hand edit, a
261
+ stray `sed` over the frontmatter. Adopt it rather than creating a twin:
262
+
263
+ ```
264
+ skitterspec spec-sync reattach <spec> --to <ISSUE-REF>
265
+ ```
266
+
267
+ Bare, `reattach` searches by title itself and stamps the one unclaimed match;
268
+ it refuses when several match and names them, because choosing between two is
269
+ a guess. `--force-new` is the escape when the duplicate title is genuine.
270
+
271
+ **`N exists in Linear but could not be recorded`** — the create landed and the
272
+ stamp did not, so Linear holds an issue the repo does not point at.
273
+ **Do not re-run**:
274
+ the plan still reads the spec as unlinked, so a re-run mints a second.
275
+ `reattach --to <the named id>` is the fix.
276
+
277
+ Everything else — a rate limit, a refusal that says waiting will not help — is
278
+ reported with Linear's own explanation and whether retrying can help. Relay it;
279
+ do not paraphrase.
280
+
236
281
  ## 6. Report
237
282
 
238
283
  End with the block defined in `.claude/rules/spec-reports.md`. That file carries
@@ -120,8 +120,8 @@ Linear API key is set (see `apply.transport` in `linear.config.md`).
120
120
  Do not roll anything back, and do not stop to ask.
121
121
  - **Say what happened** in the skill's report: mirror updated, skipped as
122
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
123
+ - **There is no unassign step, and that is not an omission.** Where the repo owns
124
+ `assignee`, the projection derives it from the spec's lifecycle
125
125
  bucket — so moving the folder to `complete`/`cancelled` is *itself* what
126
126
  releases the issue, and the push above carries it like any other field. The
127
127
  `linear_assignee_id` stamp and the `> **Developer:**` header both stay put:
@@ -146,6 +146,67 @@ names it. This is why the commit no longer stages `specs/` wholesale: the one
146
146
  file outside the spec's folder that had to be swept up is addressable, so the
147
147
  breadth bought nothing and cost another session's work.
148
148
 
149
+ ## 4b. Hand the refresh back on a page, then wait
150
+
151
+ **Only when the project has per-spec isolation** (`specs/.core/env.config.json`
152
+ present) **and this review actually changed something.** Either missing → skip
153
+ the whole step in silence.
154
+
155
+ A refreshed spec is a rewrite of a document someone is about to work from, so it
156
+ gets read before it is committed — the same ending `/spec` now has:
157
+
158
+ ```
159
+ skitterspec spec-env review <spec> --docs --buttons refresh
160
+ ```
161
+
162
+ `--docs` reads the spec's own documents from the tree you are standing in, so it
163
+ wants no worktree and works for a spec in any bucket. These documents are
164
+ **tracked**, so the diff against `HEAD` is a patch rather than a set of new
165
+ files — which is exactly what the reader wants here: what drifted, and what you
166
+ rewrote.
167
+
168
+ **Render nothing when nothing changed.** `/spec-review` frequently concludes a
169
+ spec is still accurate, and that is a success rather than a gap. A page showing
170
+ an empty diff under a commit button asks for a verdict on nothing, and the
171
+ engine refuses it for the same reason — `nothing to review`. Take that refusal
172
+ as the answer and report the `⏸`/`✅` you already had.
173
+
174
+ **Arm nothing.** No `review arm` here: the gate asserts that a phase which ended
175
+ owes an answer, and a re-validated spec has ended no phase. Walking away leaves
176
+ an uncommitted spec, which is the state this skill has always finished in.
177
+
178
+ ### The three endings
179
+
180
+ `--buttons refresh` offers `Commit`, `Request changes` and `Discuss`.
181
+
182
+ **No start verdict, deliberately.** The spec this skill refreshed may already be
183
+ in progress — that is half of what it is used for — so `Commit & Start` would
184
+ offer to provision a worktree for a spec that already has one.
185
+
186
+ - **`commit`** — hand off to `review.commitWith` (`/commit` by default) with the
187
+ pathspec the render reported on `docs.paths`, then finish. The spec keeps
188
+ whatever status step 4 left it in.
189
+ - **`changes`** — work the notes into the spec, record a resolution for each so
190
+ the next render strikes it through with what changed, re-render, and wait
191
+ again.
192
+ - **`discuss`** — report and talk. Claim nothing, change nothing.
193
+
194
+ ### Then wait for it
195
+
196
+ **The wait is a command. Do not write one.** Note the moment you start waiting
197
+ as an ISO timestamp, run the engine's wait in the background, and end your turn:
198
+
199
+ ```
200
+ skitterspec spec-env review wait <spec> --since <the timestamp>
201
+ ```
202
+
203
+ Pass no timeout. On waking, let the engine pick the pass —
204
+ `skitterspec spec-env review <spec> --docs --claim-since <the timestamp> --json`
205
+ — and route on the verdict above.
206
+
207
+ **Where the page is `file://`** the pass is copied and pasted, so the wait is the
208
+ turn ending and the banner says so. Do not start a watch that cannot fire.
209
+
149
210
  ## 5. Report
150
211
 
151
212
  Do **not** `git commit` unless the user asks.
@@ -166,6 +227,32 @@ the shape; this section carries only what is specific here.
166
227
  API that moved, a task the code already does. `Spec` carries the status the
167
228
  review leaves it in, which is the answer to "can I start this?".
168
229
 
230
+ **Where step 4b rendered a page the run is waiting**, so the block omits the
231
+ `Review` row and ends on the banner from `.claude/rules/spec-reports.md` — the
232
+ tier stack copied from the engine's own lines, never one link chosen for the
233
+ reader:
234
+
235
+ ---
236
+
237
+ ## ⏸ Review ready — &lt;N&gt; files, +&lt;a&gt; −&lt;d&gt;
238
+
239
+ - **local** — &lt;the `local:` URL&gt;
240
+ - **network** — &lt;the `network:` URL, or off with the command that turns it on&gt;
241
+ - **remote** — &lt;the `remote:` URL, or off with the command that turns it on&gt;
242
+
243
+ I'm holding here until you send a verdict — the wait covers local and network.
244
+
245
+ `Commit` commits the refresh · `Request changes` works them now
246
+
247
+ ---
248
+
249
+ `Next` then names the page rather than a command, because the button is what
250
+ carries the work on.
251
+
252
+ Where nothing was rendered — no isolation, or no drift to show — `Next` is
253
+ `/commit, then /spec-start <name>`, since this skill does not commit and
254
+ `/spec-start` refuses a tree holding anything that is not the spec's.
255
+
169
256
  **Drift found and not fixed is a `Follow-up`, not a silence.** A spec this skill
170
257
  declared reviewed is one nobody will re-read; something it noticed and left
171
258
  undone has to leave the session in writing or it did not happen.
@@ -1,6 +1,6 @@
1
1
  ---
2
2
  name: spec-reviewed
3
- description: Pick up the review you approved on the page — run it bare to pick up the single waiting pass, paste the six-digit code off the page ("/spec-reviewed 608223") to name one exactly, and be asked which only when two are waiting. Use when the user says "/spec-reviewed", "I approved it", "I've reviewed it", "pick up my review", "I pressed approve", or otherwise says they have finished reviewing a rendered diff.
3
+ description: Pick up the review you approved on the page — run it bare to pick up the single waiting pass, paste the six-digit code off the page ("/spec-reviewed 608223") to name one exactly, or pass the verdict itself ("/spec-reviewed commit") when the page could not send and copied you a command instead. Use when the user says "/spec-reviewed", "I approved it", "I've reviewed it", "pick up my review", "I pressed approve", or otherwise says they have finished reviewing a rendered diff.
4
4
  disable-model-invocation: true
5
5
  ---
6
6
 
@@ -70,16 +70,42 @@ Several provisioned and none resolved is a refusal — relay its list and stop,
70
70
  never pick from it. See `.claude/rules/spec-planning.md`; do not restate the
71
71
  rule here.
72
72
 
73
- **Three argument shapes, and they cannot collide.** A **six-digit code**
74
- matches `^\d{6}$`; a **tracker id** carries a letter and a hyphen; a
75
- **spec name** is neither. So the parse needs no flag, and nothing has to be
76
- guessed at from context.
73
+ **Four argument shapes, and they cannot collide.** A **six-digit code**
74
+ matches `^\d{6}$`; a **verdict** is one of a closed list of six words;
75
+ a **tracker id** carries a letter and a hyphen;
76
+ a **spec name** is none of them and always carries a lifecycle prefix
77
+ (`feat-`, `bug-`, `hotfix-`), which no verdict does. So the parse needs no flag,
78
+ and nothing has to be guessed at from context.
77
79
 
78
80
  A **six-digit code** is a pass the operator read off their own page. It says
79
81
  **which pass**, not which spec — so resolve the spec exactly as a bare
80
82
  invocation does, above, and claim the code *there*. Then go to step 4 — a named
81
83
  pass has nothing to disambiguate.
82
84
 
85
+ A **verdict word** — `commit`, `commit-continue`, `commit-start`, `continue`,
86
+ `changes`, `discuss` — is the conclusion itself, arriving without a pass behind
87
+ it. It is
88
+ what a **`file://` page** hands over: that page has no server to POST to and no
89
+ store to write to, so it copies a command instead of sending anything, and this
90
+ is the command. Resolve the spec as a bare invocation does, then send the word
91
+ through the engine:
92
+
93
+ ```
94
+ skitterspec spec-env review <spec> --verdict <word>
95
+ ```
96
+
97
+ It joins the same merge a claimed pass goes through, so the routing in step 4
98
+ is unchanged: a commit over open notes is refused exactly as it would be, the
99
+ outcome log records it, and the gate a phase armed is cleared. Then go to step
100
+ 4 — there is nothing to disambiguate.
101
+
102
+ **A word carries a verdict and nothing else**, and that is a property of the
103
+ transport, not a shortcut. Accepts and comments do not fit on a command line,
104
+ so the page only ever offers the words while the reader has marked nothing —
105
+ the moment they tick an accept or write a note it goes back to handing over the
106
+ blob. If someone tells you they marked things up *and* gives you a word, ask
107
+ for the pass: the word would land a verdict with their notes silently dropped.
108
+
83
109
  A **name** targets that spec instead. A **tracker id** does too, but only
84
110
  through a **provider seam**: the base knows nothing about tracker ids, so
85
111
  resolve one by asking whichever provider is installed for its listing —
@@ -233,10 +233,14 @@ branch is already here.
233
233
  - Set the **Status** header: `> **Status:** In Progress — Phase 1 (started <YYYY-MM-DD>)`.
234
234
  - Set **Developer** if it is still `—` (`git config user.name`).
235
235
 
236
- **Only when all three hold**: `specs/.core/linear.config.json` exists, its
237
- `sync.fieldOwnership` includes `assignee`, and the spec carries a
236
+ **Only when all three hold**: `specs/.core/linear.config.json` exists, the repo
237
+ **owns** `assignee` in its `sync.fieldOwnership`, and the spec carries a
238
238
  `linear_identifier`. Any one missing → skip this step silently and carry on; a
239
- project that has not opted in must see no trace of assignment.
239
+ project that declined must see no trace of assignment.
240
+
241
+ Ownership is the **value**, not the key: `assignee` is owned by default, and
242
+ `"none"` is how a project declines it. Testing whether the key is *present* would
243
+ read as opted-in everywhere, since the default puts it in every config.
240
244
 
241
245
  **Never blocks, never fails the skill.** Everything below is best-effort: the
242
246
  branch is provisioned and the spec is moving either way, and an unassigned issue
@@ -371,6 +375,25 @@ here beyond knowing it is no longer dead code.
371
375
  A spec ideally arrives `Ready` from `/spec`; a `Draft` works too — sanity-check
372
376
  it is well-formed first.
373
377
 
378
+ - **Report anything left waiting, and claim none of it.** Ask the engine once:
379
+
380
+ ```
381
+ skitterspec spec-env review waiting
382
+ ```
383
+
384
+ A pass listed here arrived when nothing was watching — a wait that never ran,
385
+ a session cleared, a terminal closed overnight — and no watcher can recover
386
+ those, however good. Relay what it prints: the spec, the code, the verdict and
387
+ the age, with `/spec-reviewed <code>` to pick one up and `--drop <code>` to
388
+ disown it.
389
+
390
+ **It is information, not a gate.** It never refuses, nothing here is
391
+ conditional on it, and **you never claim one** — `/spec-diff` §0 stands
392
+ unchanged, and a pass sitting there when you arrived was not sent to you.
393
+ Silent when nothing is waiting, which is the usual case: say nothing rather
394
+ than reporting that there was nothing to report.
395
+
396
+
374
397
  ## 4b. Note a missing gating decision (only if configured)
375
398
 
376
399
  **Only when `specs/.core/gating.config.json` exists.** Run
@@ -33,9 +33,20 @@ Use the argument, else the spec in context; ask if unclear.
33
33
  ## 2. Fetch the Linear issue (optional, for drift)
34
34
 
35
35
  If the spec has a `linear_identifier`, discover the Linear MCP read tool
36
- (`get_issue`) and write the issue JSON to a temp file this lets the report
37
- compare workflow-state. If Linear isn't connected, skip the drift line (still
38
- report pending-push).
36
+ (`get_issue`) and write the issue JSON to a temp file. If Linear isn't connected,
37
+ skip the drift lines (still report pending-push).
38
+
39
+ **Write the whole issue, including its `description`.** The engine reports two
40
+ independent kinds of drift off that one file: the workflow-state someone moved,
41
+ and the **description someone edited**. Write only the state fields and the
42
+ second check has nothing to look at — and it says nothing rather than guessing,
43
+ so the loss is silent. Keep `url` too: it is what the description line points
44
+ the reader at.
45
+
46
+ **The description itself never enters the conversation.** The engine hashes it
47
+ and compares hashes; what it prints is *that* the text changed, plus where to
48
+ read it. Do not open the temp file, and do not paste the description into your
49
+ report.
39
50
 
40
51
  Optionally fetch the workspace issue-state names to a file to validate the
41
52
  configured `states` at the same time.
@@ -81,9 +92,12 @@ the shape; this section carries only what is specific here.
81
92
  **Verdicts**
82
93
 
83
94
  - `✅` — in sync; nothing would push.
84
- - `⚠️` — drift: N objects would push, or Linear's workflow state was moved by
85
- hand. Neither is an error the repo wins on the next push — but both are the
86
- reason someone ran this.
95
+ - `⚠️` — drift: N objects would push, Linear's workflow state was moved by hand,
96
+ or the issue's **description** was edited on Linear since the last push. None
97
+ is an error — the repo wins on the next push — but all are the reason someone
98
+ ran this. **Say which**, since the three call for different things: a pending
99
+ push is routine, a moved state is usually someone else's automation, and an
100
+ edited description means a person wrote something that the next push replaces.
87
101
  - `⏸` — no config, or the spec is not linked. Nothing to compare.
88
102
 
89
103
  **Fields:** `Tracker` · `Follow-ups` · `Next`
@@ -42,6 +42,7 @@ In a project that installs the Linear superset the binary is
42
42
  | "what specs are there?", "what's in the backlog?", "what is Jane on?" | `list` — or hand to `/spec-list` |
43
43
  | "did the mirror survive the push?" | `verify <spec> --stored <file>` |
44
44
  | "link this spec to KEY-1 by hand" | `stamp <spec> --issue KEY-1` |
45
+ | "this spec lost its link", "a push made a duplicate issue" | `reattach <spec>` — or `--to KEY-1` to name one |
45
46
  | "mirror the whole backlog / every complete spec" | `apply --all <bucket>` — **confirm first** |
46
47
  | "is the team key stale?", "did Linear get renamed?" | `retarget` |
47
48
  | "what ticket am I on?", writing a commit | `ref` |
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@skitterbyte/skitterspec-linear",
3
- "version": "15.0.0",
4
- "description": "Spec-driven development for Claude Code, with one-way Linear sync \u2014 a superset of @skitterbyte/skitterspec: the base filesystem workflow plus /spec-status \u00b7 /spec-push and the spec-sync CLI. The repo is canonical; Linear is a generated mirror. Install this OR the base, not both.",
3
+ "version": "17.0.0",
4
+ "description": "Spec-driven development for Claude Code, with one-way Linear sync a superset of @skitterbyte/skitterspec: the base filesystem workflow plus /spec-status · /spec-push and the spec-sync CLI. The repo is canonical; Linear is a generated mirror. Install this OR the base, not both.",
5
5
  "keywords": [
6
6
  "claude",
7
7
  "claude-code",