@skitterbyte/skitterspec-linear 11.0.0 → 13.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 (60) hide show
  1. package/MIGRATION.md +260 -10
  2. package/README.md +32 -2
  3. package/assets/claude-md-section.md +48 -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 +9 -3
  8. package/assets/core/env.config.md +102 -30
  9. package/assets/core/gating.config.json.example +4 -0
  10. package/assets/core/gating.config.md +81 -0
  11. package/assets/core/linear.config.md +67 -8
  12. package/assets/review/page.html +1501 -0
  13. package/assets/rules/spec-planning.md +224 -15
  14. package/assets/rules/spec-reports.md +269 -0
  15. package/assets/skills/spec/SKILL.md +64 -13
  16. package/assets/skills/spec-bug/SKILL.md +193 -27
  17. package/assets/skills/spec-cancel/SKILL.md +99 -8
  18. package/assets/skills/spec-claim/SKILL.md +114 -0
  19. package/assets/skills/spec-complete/SKILL.md +123 -22
  20. package/assets/skills/spec-diff/SKILL.md +564 -0
  21. package/assets/skills/spec-hotfix/SKILL.md +202 -22
  22. package/assets/skills/spec-init/SKILL.md +49 -9
  23. package/assets/skills/spec-linear-setup/SKILL.md +86 -7
  24. package/assets/skills/spec-list/SKILL.md +218 -0
  25. package/assets/skills/spec-next/SKILL.md +300 -7
  26. package/assets/skills/spec-push/SKILL.md +45 -22
  27. package/assets/skills/spec-review/SKILL.md +59 -11
  28. package/assets/skills/spec-reviewed/SKILL.md +241 -0
  29. package/assets/skills/spec-start/SKILL.md +426 -66
  30. package/assets/skills/spec-status/SKILL.md +24 -2
  31. package/assets/skills/spec-sync/SKILL.md +47 -11
  32. package/assets/skills/spec-to-main/SKILL.md +42 -20
  33. package/package.json +11 -7
  34. package/src/cli.js +1710 -80
  35. package/src/env/building.js +143 -0
  36. package/src/env/classify.js +91 -0
  37. package/src/env/config.js +57 -9
  38. package/src/env/provision.js +192 -19
  39. package/src/env/proxy.js +34 -1
  40. package/src/env/render.js +3 -12
  41. package/src/env/resolve.js +296 -9
  42. package/src/env/review.js +1329 -0
  43. package/src/env/serve.js +549 -0
  44. package/src/env/teardown.js +13 -6
  45. package/src/gating.js +155 -0
  46. package/src/init.js +124 -2
  47. package/src/prompts.js +10 -1
  48. package/src/vendor/linear/api.js +104 -1
  49. package/src/vendor/linear/cli-sync.js +874 -17
  50. package/src/vendor/linear/config.js +8 -0
  51. package/src/vendor/linear/credentials.js +94 -0
  52. package/src/vendor/linear/doctor.js +35 -0
  53. package/src/vendor/linear/identity.js +105 -0
  54. package/src/vendor/linear/mcp.js +26 -0
  55. package/src/vendor/sync-core/index.js +6 -2
  56. package/src/vendor/sync-core/src/compare.js +74 -5
  57. package/src/vendor/sync-core/src/normalize.js +30 -0
  58. package/src/vendor/sync-core/src/push.js +11 -1
  59. package/src/vendor/sync-core/src/write.js +38 -0
  60. package/LICENSE +0 -21
@@ -5,6 +5,11 @@ description: Create a new spec-driven-development spec. Grills the user to a cle
5
5
 
6
6
  # /spec — author a new spec
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
  Produce ONE concise spec in `specs/backlog/`. Do not start coding — this skill
9
14
  plans only. Implementation happens later via `/spec-start`.
10
15
 
@@ -139,7 +144,17 @@ not write the spec until this is resolved.
139
144
  `worktree + docker` only when it must. This sets the `> **Stack:**` header
140
145
  that `/spec-start` acts on (it can be escalated later). Skip when isolation
141
146
  isn't enabled — leave the default `worktree`.
142
- 10. **Open questions** anything still undecided.
147
+ 10. **Release gating** *(only when `specs/.core/gating.config.json` exists)* —
148
+ should this ship behind a feature flag, or land live?
149
+ **Offer, don't impose**: the user decides and you raise it, so a spec
150
+ never reaches
151
+ `/spec-complete` with the question unasked. Cite the project's own
152
+ `guidance` path from that config when it names one — skitterspec knows
153
+ nothing about how this project does flags, and must not guess. Record the
154
+ answer **either way**: a flag name, or `none: <one-line reason>`. "No" is a
155
+ decision and belongs in the header; silence is not. Skip entirely when the
156
+ config is absent — that project does not use flags.
157
+ 11. **Open questions** — anything still undecided.
143
158
 
144
159
  Stop grilling when there are no unresolved branches that would change the spec.
145
160
  Briefly play back the agreed understanding before writing.
@@ -198,6 +213,9 @@ the codebase, link rather than duplicate):
198
213
  > **Area:** <comma-separated files/modules this touches>
199
214
  > **Stack:** <worktree — or "worktree + docker" if it touches the DB/stateful
200
215
  > services; only acted on when isolation is enabled — see Phase A item 9>
216
+ > **Gating:** <flag name — or "none: <one-line reason>". Only when release gating
217
+ > is configured; omit the line entirely otherwise. An empty value or a bare
218
+ > "none" is not a valid outcome — see Phase A item 10>
201
219
 
202
220
  ## Problem
203
221
 
@@ -301,13 +319,14 @@ Rules for the spec body:
301
319
  phase index.
302
320
  - **Honour project conventions** when writing tasks — reference the relevant
303
321
  `.claude/rules/*.md` rather than re-explaining them.
304
- - **The `## Impact` table is derived from Phase A items 3 (Affected areas) & 5
305
- (Data/API impact)** a structured place to record what those already surface,
306
- not new grilling. It is the scannable substitute for spelling impact out in
307
- prose: name the surfaces (endpoints, schemas, DB tables, domain objects,
308
- routes, business rules) instead of describing them, keep `Detail` terse, and
309
- let it not paragraphs carry the blast radius. It complements the `Area:`
310
- header (files) by naming behavioural surfaces.
322
+ -
323
+ **The `## Impact` table is derived from Phase A items 3 (Affected areas) & 5 (Data/API impact)**
324
+ a structured place to record what those already surface, not new grilling.
325
+ It is the scannable substitute for spelling impact out in prose: name the
326
+ surfaces (endpoints, schemas, DB tables, domain objects, routes, business
327
+ rules) instead of describing them, keep `Detail` terse, and let it not
328
+ paragraphs carry the blast radius. It complements the `Area:` header
329
+ (files) by naming behavioural surfaces.
311
330
  - **Changelog** is mandatory and lives in the spec. Every later decision or
312
331
  course-correction gets a dated one-line entry. Convert relative dates to
313
332
  absolute.
@@ -315,10 +334,10 @@ Rules for the spec body:
315
334
 
316
335
  ## Phase C — finish up
317
336
 
318
- After writing, tell the user the path and that it's **`Ready`** in `backlog`
319
- (grilling in Phase A resolved the open questions). If you deliberately left open
320
- questions unresolved, write `Draft` instead and say what still needs deciding.
321
- Either way, the next step is `/spec-start` to start building.
337
+ Decide the status: **`Ready`** in `backlog` when grilling in Phase A resolved the
338
+ open questions, `Draft` when you deliberately left some unresolved. Either way
339
+ the next step is `/spec-start`. The Report section below is where all of that
340
+ reaches the user do not narrate it here as well.
322
341
 
323
342
  ## Phase D — record the isolation stack (only if configured)
324
343
 
@@ -332,6 +351,15 @@ run `skitterspec spec-env up <name>` to add Docker to an existing worktree). If
332
351
  `env.config.json` is absent, isolation is off — leave the default `worktree` and
333
352
  finish as above.
334
353
 
354
+ ## Phase D2 — record the gating decision (only if configured)
355
+
356
+ **Only when `specs/.core/gating.config.json` exists.** Make sure the
357
+ `> **Gating:**` header carries the Phase A item 10 answer — a flag name, or
358
+ `none: <reason>` using the config's `default` wording if it sets one. Nothing is
359
+ provisioned or enforced by this: the header exists so the decision is
360
+ **on the record and reviewable**, and `skitterspec gating check` reports a spec that has
361
+ none. It never blocks. If the config is absent, do not write the line at all.
362
+
335
363
  ## Phase E — link to a ticketing provider (only if one is installed)
336
364
 
337
365
  **Only when a ticketing provider is installed and configured** (it ships the
@@ -359,7 +387,7 @@ does — you never create the issue by hand:
359
387
  2. **Pick the Project** — run the picker in **Picking the Linear Project** below.
360
388
  Keep the chosen id for step 4.
361
389
  3. **Get the plan.**
362
- `skitterspec spec-sync push <spec> --workspace-states <file> --json > plan.json`
390
+ `skitterspec spec-sync plan <spec> --workspace-states <file> --json > plan.json`
363
391
  — the spec is unlinked, so this plan is all-creates: the issue and one
364
392
  sub-issue per phase.
365
393
  4. **Apply it.**
@@ -423,3 +451,26 @@ say so in one line — *"project picker unavailable"* — and carry on with
423
451
 
424
452
  If `linear.projectId` is set but that Project is archived or missing, relay
425
453
  Linear's error and stop rather than silently minting an unparented issue.
454
+
455
+ ## Report
456
+
457
+ End with the block defined in `.claude/rules/spec-reports.md`. That file carries
458
+ the shape; this section carries only what is specific here.
459
+
460
+ **Verdicts**
461
+
462
+ - `✅` — a `Ready` spec is written to `specs/backlog/<name>/`.
463
+ - `⚠️` — written as `Draft`; open questions were deliberately left. Name them.
464
+ - `⏸` — grilling did not reach a shared understanding, so nothing was written.
465
+ That is the skill working: a spec written over an unresolved requirement is
466
+ the outcome Phase A exists to prevent.
467
+
468
+ **Fields:** `Tracker` · `Spec` · `Built` · `Follow-ups` · `Next`
469
+
470
+ `Built` is the spec's path and phase count; `Spec` is its status and bucket;
471
+ `Next` is `/spec-start <name>`, with the name spelled the way it must be typed.
472
+ `Tracker` appears only when a provider is installed and linked it.
473
+
474
+ **`Follow-ups` is almost always `none` here.** This skill's whole job is to
475
+ capture work, so anything it surfaced belongs in the spec it just wrote rather
476
+ than in a follow-up line beneath it.
@@ -5,6 +5,11 @@ description: Investigate a bug, capture it as a Bug-type spec, and drive it red
5
5
 
6
6
  # /spec-bug — investigate a bug, prove it with a failing test, fix it
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
  This is the **bug** counterpart to `/spec` (which is for **features**, plan-only).
9
14
  Unlike `/spec`, this skill is hands-on and test-first: it reproduces the bug as a
10
15
  **failing test (RED)**, captures a lean Bug spec, then works the test to **GREEN**.
@@ -140,25 +145,13 @@ provision from — you'll flesh it out in §4:
140
145
  `## Symptom` you established above.
141
146
  - Run `skitterspec spec-env up bug-<name>` (the `spec-env` CLI engine). It prints
142
147
  the `git worktree add … -b bug/<name>` command (a branch forked from `main`),
143
- the worktree path, the opener, and any `in the worktree, run:` bootstrap steps.
144
- - Run the printed `git worktree add`. **The worktree forks from `main`'s last
145
- commit, so your uncommitted stub doesn't travel with it**move it across so
146
- `main` is left pristine. **Create the destination bucket first:**
147
-
148
- ```
149
- mkdir -p <worktreePath>/specs/in-progress
150
- mv specs/in-progress/bug-<name> <worktreePath>/specs/in-progress/
151
- ```
152
-
153
- The `mkdir -p` is not belt-and-braces. Git does not store empty directories,
154
- so `specs/in-progress/` is **absent** from a fresh worktree whenever that
155
- bucket happens to be empty on `main` — the common case, since it empties every
156
- time the last in-progress spec completes. `mv` into a missing destination
157
- renames your spec folder **to** `specs/in-progress`, silently: the spec's files
158
- end up one level too high, `00-overview.md` sits where the bucket should be,
159
- and every later step still appears to work until something cannot find the
160
- spec. Confirm the result before carrying on — you want
161
- `<worktreePath>/specs/in-progress/bug-<name>/00-overview.md`.
148
+ the worktree path, and any `in the worktree, run:` bootstrap steps.
149
+ - Run the printed commands in order. The plan **commits the stub first** — the
150
+ worktree forks from `main`'s last commit, so the stub has to be in it — and
151
+ then adds the worktree. Nothing to move afterwards: the spec is already there.
152
+ The commit is planned, not silent; it appears in the printed plan above the
153
+ `git worktree add`, and `spec-env up` refuses outright if anything *other* than
154
+ this spec is uncommitted.
162
155
  - **Bootstrap the worktree.** A fresh worktree has no installed dependencies and
163
156
  none of the repo's gitignored files (`.env`, local overrides). Run the printed
164
157
  `in the worktree, run:` steps (file seeding, then `setup`) in order, before
@@ -169,8 +162,8 @@ provision from — you'll flesh it out in §4:
169
162
  edits will prompt.
170
163
  - **Do everything below in the worktree**, on the branch — the red test, the fix,
171
164
  and the rest of the spec. Act on the worktree with absolute paths /
172
- `git -C <worktreePath>`, or open a fresh session rooted there (the printed
173
- opener). `main` changes only when the branch merges (at `/spec-complete`).
165
+ `git -C <worktreePath>`, or open a session rooted at the printed path. `main`
166
+ changes only when the branch merges (at `/spec-complete`).
174
167
 
175
168
  ## 3. Write the failing test FIRST (RED) — mandatory
176
169
 
@@ -207,6 +200,8 @@ because work is already underway. Keep it lean:
207
200
  > **Developer:** <git user.name — you, since you're fixing it now>
208
201
  > **Raised:** <YYYY-MM-DD (today)>
209
202
  > **Area:** <files/modules>
203
+ > **Gating:** <flag name — or "none: <one-line reason>". Only when release gating
204
+ > is configured; omit the line entirely otherwise>
210
205
 
211
206
  ## Symptom
212
207
 
@@ -281,7 +276,7 @@ does — you never create the issue by hand:
281
276
  2. **Pick the Project** — run the picker in **Picking the Linear Project** below.
282
277
  Keep the chosen id for step 4.
283
278
  3. **Get the plan.**
284
- `skitterspec spec-sync push <spec> --workspace-states <file> --json > plan.json`
279
+ `skitterspec spec-sync plan <spec> --workspace-states <file> --json > plan.json`
285
280
  — the spec is unlinked, so this plan is all-creates: the issue and one
286
281
  sub-issue per phase.
287
282
  4. **Apply it.**
@@ -314,6 +309,66 @@ and **never auto-push git** — Linear's own automation reacts to real branch/PR
314
309
  events later. Report the Linear issue URL as part of the skill's finish-up
315
310
  message.
316
311
 
312
+ **Only when all three hold**: `specs/.core/linear.config.json` exists, its
313
+ `sync.fieldOwnership` includes `assignee`, and the spec carries a
314
+ `linear_identifier`. Any one missing → skip this step silently and carry on; a
315
+ project that has not opted in must see no trace of assignment.
316
+
317
+ **Never blocks, never fails the skill.** Everything below is best-effort: the
318
+ branch is provisioned and the spec is moving either way, and an unassigned issue
319
+ is a cosmetic gap that `/spec-claim` closes later.
320
+
321
+ 1. **Work out who you are.** Run `skitterspec spec-sync whoami --json`.
322
+ - `ok: true` → use `id` and `name`. Nothing to ask.
323
+ - `source: "mcp"` or the command reports no API key → call the discovered
324
+ user-read tool with `me`, then cache it:
325
+ `skitterspec spec-sync whoami --set <id> --name "<name>"`.
326
+ - `ok: false` → identity is **unknown**, which is an ordinary state (a shared
327
+ or bot key, an offline machine). Go to step 2.
328
+ 2. **Unknown identity — three states, not two.** Decide by what is actually
329
+ reachable, and route the third to inaction:
330
+ - **Linear reachable *and* this is an interactive session** → offer a short
331
+ user search (`skitterspec spec-sync users <name-or-email>`, or the
332
+ user-list tool on MCP), let the operator pick, and offer to cache it with
333
+ `whoami --set` so this is asked once per machine rather than once per spec.
334
+ - **Not reachable, or not interactive** → say so in one line
335
+ (`assignment skipped — no Linear identity`) and **carry on**. Do not prompt
336
+ for something you could not act on, and never stall a `/spec-start` on it.
337
+ 3. **Record it on the spec** — through the engine, never by hand-editing
338
+ frontmatter:
339
+
340
+ ```
341
+ skitterspec spec-sync assign <spec> --to <user-id> --name "<display name>"
342
+ ```
343
+
344
+ 4. **Leave `> **Developer:**` as `git config user.name`** — the step above this
345
+ seam already set it, and this seam must not overwrite it with the tracker's
346
+ display name.
347
+
348
+ Those two names are the same person and often not the same string, and the
349
+ git one is the one everything else in the spec already uses: `Author:`, every
350
+ **State log** `By` row, and every commit. Writing the tracker's name into this
351
+ one field would leave a spec whose own audit trail contradicts its header —
352
+ a worse problem than the one it would solve. Anyone wanting a different name
353
+ on their specs sets `git config user.name`, and it stays consistent
354
+ everywhere.
355
+
356
+ The header names **who is building this**; the stamp in step 3 names
357
+ **which tracker account it is assigned to**. They answer different questions,
358
+ so they need not be the same string. The one place that reasoning does not hold
359
+ is handing a spec to *someone else* — there is no local git name for them —
360
+ and that case belongs to `/spec-claim --to`, which sets the header from the
361
+ tracker deliberately.
362
+
363
+ **Nothing is pushed here.** `assign` writes the repo only, and the refresh these
364
+ skills already run sends it. Assignment is an ordinary field of the projection,
365
+ not a side errand with its own network call.
366
+
367
+ **There is no unassign step anywhere.** The projection derives the assignee from
368
+ the spec's lifecycle bucket, so `/spec-complete` and `/spec-cancel` release the
369
+ issue through the push they already make. The stamp deliberately stays in the
370
+ file: who actioned the work outlives who is currently holding it.
371
+
317
372
  ### Picking the Linear Project
318
373
 
319
374
  Run this **only when minting a spec issue** — creating it for the first time. On
@@ -346,6 +401,19 @@ say so in one line — *"project picker unavailable"* — and carry on with
346
401
  If `linear.projectId` is set but that Project is archived or missing, relay
347
402
  Linear's error and stop rather than silently minting an unparented issue.
348
403
 
404
+ ### Release gating (only when configured)
405
+
406
+ **Only when `specs/.core/gating.config.json` exists.** Before writing the spec,
407
+ ask: should this fix ship behind a feature flag, or land live?
408
+ **Offer, don't impose** — cite the project's own `guidance` path when the config
409
+ names one, and
410
+ record the answer either way, as a flag name or `none: <one-line reason>`.
411
+
412
+ A bug fix ships in the next release exactly like a feature, so it gets the same
413
+ question rather than an assumption. A risky rewrite of a broken path is precisely
414
+ where a kill-switch earns its keep; a one-line null check is precisely where
415
+ `none:` is the honest answer. Skip entirely when the config is absent.
416
+
349
417
  ## 5. Drive to GREEN
350
418
 
351
419
  - Implement the **minimal, root-cause** fix. Match surrounding code; honour all
@@ -383,10 +451,108 @@ sub-issue jump from Backlog straight to Done, with nothing visible in between.
383
451
  - **Say what happened** in the skill's report: mirror updated, skipped as
384
452
  unlinked, or failed with the reason.
385
453
 
454
+ ## 5b. Render the page — then offer the review, never write it
455
+
456
+ **Only when the project has per-spec isolation** (`specs/.core/env.config.json`
457
+ present). Without it there is no worktree to read and this step does not exist —
458
+ skip it in silence rather than explaining an absence.
459
+
460
+ The fix is green and nothing is committed yet. That is the moment the page is
461
+ about, so render it now — **after** the tests pass and **before** the commit:
462
+
463
+ ```
464
+ skitterspec spec-env review <spec>
465
+ ```
466
+
467
+ **This is free.** The engine reads git and splices the patches into a template;
468
+ the diff never passes through you, so a 266KB patch costs nothing.
469
+
470
+ **Then offer `/spec-diff`. Do not run it.** The written review is the part that
471
+ costs — roughly **700 output tokens**, because writing it means reading the diff
472
+ — and that spend is the operator's call, not a default.
473
+
474
+ **The offer is the `Review` row of step 6's block** — the counts, the page link
475
+ and a question, in one row:
476
+
477
+ | **Review** | <N> files, +<a> −<d> · [open the page](<the `open:` URL>) — want a written review before you commit? |
478
+
479
+ **It ends in a question, addressed to someone.** It was once a fenced block of
480
+ engine output, and it fired on every phase and was never once taken: two quoted
481
+ lines under the test counts, addressed to nobody, with the report then closing
482
+ on *"commit this first"* — the last instruction the reader got was to move on,
483
+ so they did. A row in a labelled table is findable; a question in it is
484
+ answerable. Both halves are load-bearing.
485
+
486
+ **Never bury it and never split it.** It sits above `Follow-ups` and `Next`, and
487
+ the page and the question stay in the same row: two adjacent rows about one page
488
+ make the reader resolve a distinction before acting on either. A later edit that
489
+ moves it out of the block, or separates the link from the question, undoes this
490
+ and should be read as a regression rather than tidying.
491
+
492
+
493
+ Relay the **`open:`** line rather than the bare path: a path is not clickable in
494
+ any terminal, and a page nobody can open is a page nobody reads.
495
+
496
+ - **Never write the review unasked**, and **never publish**. Publishing leaves
497
+ something behind that this tooling cannot remove, so it is always an ask. A
498
+ `file://` link is no use on a phone, and saying so **is** the ask —
499
+ `/spec-diff` §6 owns how.
500
+ **Follow the `reader:` line the engine printed — do not sniff for it.** It
501
+ answers where the person reading this is sitting, and the offer changes with it:
502
+
503
+ - **absent** (`unknown`) — the `file://` URL, exactly as always. **Do not warn:**
504
+ unknown is the ordinary state of a local machine, and a warning there is an
505
+ accusation against a healthy session.
506
+ - **`local`** — the `file://` URL.
507
+ - **`remote`** — the engine has already stood its local server up and put a URL
508
+ the reader can open on `open:`. So there is **nothing special to say**: relay
509
+ that line like any other. Any `also:` lines under it are the other addresses
510
+ this machine has, offered because the best-guess one can be wrong — pass them
511
+ on rather than editing them out.
512
+
513
+ **Never read an environment variable to decide this** — not `SSH_CONNECTION`,
514
+ not `CLAUDE_CODE_*`, not a tty check. The engine did it, reports it on that line
515
+ and in `--json`, and a second implementation here could not be tested and would
516
+ drift.
517
+
518
+ **Serving is the engine's to do; publishing is never.** A `remote` reader
519
+ authorises a local server — one process, ended by one flag, leaving nothing
520
+ behind — and authorises nothing else. Publishing leaves a page this tooling
521
+ cannot remove, so it stays an ask in every case, always. If the engine could not
522
+ serve (a busy port, a machine with no network address) it falls back to the
523
+ `file://` URL with its marker, and that is when publishing is worth naming.
524
+
525
+ - **Never fatal.** A failed render — no worktree, a git error — is one line, and
526
+ the fix is still done. The page is a convenience; the repo is the record.
527
+
386
528
  ## 6. Report
387
529
 
388
- Summarise: root cause, the failing→passing test, the fix, and the full test
389
- result. The spec stays in `in-progress`; suggest `/spec-complete` to verify and
390
- archive it (**when isolated**, the fix lives on the bug's branch, and
391
- `/spec-complete` merges it back to `main`). Do **not** `git commit` unless the
392
- user asks.
530
+ Do **not** `git commit` unless the user asks. The spec stays in `in-progress`;
531
+ `Next` names the commit and then `/spec-complete`, as
532
+ `/commit, then /spec-complete` this skill leaves the fix uncommitted, and
533
+ `/spec-complete` §2 refuses on pre-existing uncommitted changes, so naming only
534
+ the second half sends the reader into a refusal. `/spec-complete` then verifies
535
+ and archives it (**when isolated**, the fix lives on the bug's branch, and it
536
+ merges that back to `main`).
537
+
538
+ End with the block defined in `.claude/rules/spec-reports.md`. That file carries
539
+ the shape; this section carries only what is specific here.
540
+
541
+ **Verdicts**
542
+
543
+ - `✅` — red→green, the fix is in and the suite passes.
544
+ - `⚠️` — green, with something worth knowing (a narrowed repro, a mirror that
545
+ did not refresh).
546
+ - `❌` — the test is still red, or a later test broke. Quote the failure.
547
+ - `⏸` — the bug could not be reproduced, so nothing was written. Say what was
548
+ tried; an unreproduced bug is a finding, not a failed run.
549
+
550
+ **Fields:** `Tracker` · `Branch` · `Spec` · `Cause` · `Built` · `Tests` ·
551
+ `Review` · `Follow-ups` · `Next`
552
+
553
+ `Cause` is the root cause in one clause and `Built` is the fix — the
554
+ failing→passing test belongs in `Tests`, named, so the evidence is a test name
555
+ rather than an adjective.
556
+
557
+ Step 5b's offer is the `Review` row, not a paragraph after the block — the
558
+ counts, the link and the question in one row. Nothing follows the block.
@@ -5,6 +5,11 @@ description: Cancel a spec — capture the reason, record final progress, stamp
5
5
 
6
6
  # /spec-cancel — record, stamp a reason, archive a spec
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
  ## 1. Identify the target spec
9
14
 
10
15
  - Use the name/path argument if given, else the spec **in context**. If unclear,
@@ -71,6 +76,12 @@ Linear API key is set (see `apply.transport` in `linear.config.md`).
71
76
  Do not roll anything back, and do not stop to ask.
72
77
  - **Say what happened** in the skill's report: mirror updated, skipped as
73
78
  unlinked, or failed with the reason.
79
+ - **There is no unassign step, and that is not an omission.** With `assignee` in
80
+ `sync.fieldOwnership`, the projection derives it from the spec's lifecycle
81
+ bucket — so moving the folder to `complete`/`cancelled` is *itself* what
82
+ releases the issue, and the push above carries it like any other field. The
83
+ `linear_assignee_id` stamp and the `> **Developer:**` header both stay put:
84
+ they record who actioned the work, which outlives who is holding it.
74
85
 
75
86
  ### Why it sits here
76
87
 
@@ -81,17 +92,33 @@ In `/spec-complete` and `/spec-cancel` this step is deliberately pinned
81
92
  its folder bucket. Push while the folder is still in `in-progress/` and the
82
93
  issue is set to the state the spec is *leaving*.
83
94
  - **Before the commit**, because the push stamps ids into the spec and writes a
84
- snapshot under `specs/.core/`. The `git add specs/` that follows sweeps both up
85
- with the status change; push after it instead and those files are left
95
+ snapshot under `specs/.core/`. Push after the commit instead and both are left
86
96
  uncommitted, which makes `spec-env integrate` refuse to land the branch.
87
97
 
98
+ The snapshot is reached **by name**, not by breadth. It is a declared
99
+ `spec.companionPaths` entry (`specs/.core/linear-base/{identifier}.base.json`),
100
+ so `spec-env stage` returns it among this spec's owned paths and the commit below
101
+ names it. This is why the commit no longer stages `specs/` wholesale: the one
102
+ file outside the spec's folder that had to be swept up is addressable, so the
103
+ breadth bought nothing and cost another session's work.
104
+
88
105
  Then **commit the cancellation edits** — this skill wrote them, so it commits
89
- them:
106
+ them. Ask the engine which paths are this spec's, then stage and commit
107
+ **exactly those**:
90
108
 
91
109
  ```
92
- git add specs/ && git commit -m "chore(spec): cancel <name>"
110
+ skitterspec spec-env stage <name> # lists them; --json to consume
111
+ git add -- <the owned paths>
112
+ git commit -m "chore(spec): cancel <name>" -- <the owned paths>
93
113
  ```
94
114
 
115
+ **Never `git add specs/`.** That stages a *directory*, so a spec another
116
+ session is part-way through writing lands in this commit under this spec's
117
+ ticket. The `--` on the **commit** is the other half: a checkout has one
118
+ `.git/index`, shared by every session standing in it, so a bare `git commit`
119
+ takes whatever else is staged there however carefully you staged your own.
120
+ `.claude/rules/spec-planning.md` carries the full account.
121
+
95
122
  **This matters more here than anywhere else.** Teardown (step 7) refuses a dirty
96
123
  worktree and offers `--force` as the way through — and forcing would destroy the
97
124
  cancellation record this skill just wrote. Committing first means teardown never
@@ -99,7 +126,31 @@ needs `--force`. Do not `git push`.
99
126
 
100
127
  ## 6. Report
101
128
 
102
- Confirm the cancellation, the reason recorded, the new location, and the commit.
129
+ **The block is emitted when the run ends, not where this section sits.** The
130
+ sections below run after it, so their outcome belongs in the block — write it
131
+ once, at the end, with what actually happened.
132
+
133
+ End with the block defined in `.claude/rules/spec-reports.md`. That file carries
134
+ the shape; this section carries only what is specific here.
135
+
136
+ **Verdicts**
137
+
138
+ - `✅` — stamped, moved to `cancelled`, committed, torn down.
139
+ - `⚠️` — cancelled, with something worth knowing: teardown declined, a tracker
140
+ refresh that failed, unpushed commits the operator chose to let go.
141
+ - `❌` — it acted and stopped part-way. Say what is where.
142
+ - `⏸` — no reason given, or no such spec. Nothing changed.
143
+
144
+ **Fields:** `Tracker` · `Spec` · `Worktree` · `Follow-ups` · `Next`
145
+
146
+ **The reason goes in the verdict clause** — `✅ /spec-cancel · feat-foo ·
147
+ superseded by feat-bar`. It is the one thing anyone reading this later wants,
148
+ and a cancellation is an ordinary successful run, so it must not be mistaken for
149
+ a `Why`: that field is for a run that did not do what it set out to.
150
+
151
+ `Worktree` says what was reclaimed, or that teardown was declined and the
152
+ worktree still stands — on a cancelled spec that worktree may hold the only copy
153
+ of the work, so an unreclaimed one is worth a line rather than a silence.
103
154
 
104
155
  ## 7. Tear down the environment (opt-in, only if configured)
105
156
 
@@ -110,9 +161,23 @@ directly (the old `/spec-env-down` skill is gone — teardown is folded in here)
110
161
  1. If `.spec-env/connected` names this spec, run `skitterspec spec-env connect
111
162
  main` first to free the canonical ports.
112
163
  2. `skitterspec spec-env dev down <name>` — stop its host dev servers.
113
- **Standing in the worktree? Leave it before you tear it down.** If this
114
- session's cwd is inside the spec's own worktree, `cd` to the primary checkout
115
- **first**, then run the teardown commands.
164
+ **Standing in the worktree? Leave it before you tear it down.** If this session's
165
+ cwd is inside the spec's own worktree — the normal case in `worktree` mode, since
166
+ `/spec-start` moves you there — get out **first**, then run the teardown commands.
167
+ One instruction covers it:
168
+
169
+ ```
170
+ cd <primary checkout>
171
+ ```
172
+
173
+ That is the whole mechanism, and it does not matter how you got in: the session
174
+ was moved by a `cd` and it leaves by one. There is no tool to call here, and none
175
+ should be reached for — the move in is a plain `cd` precisely because a tool that
176
+ asks for approval is unusable on a phone.
177
+
178
+ **`spec-env down` stays the single thing that deletes a worktree.** The plan
179
+ below is the only deleter, because a second one is how the teardown guards get
180
+ bypassed.
116
181
 
117
182
  Not because git refuses — it does not. `git worktree remove` **succeeds** on the
118
183
  tree you are standing in, and that is the problem: the directory vanishes under
@@ -126,6 +191,32 @@ only ordering that survives.
126
191
  remove the worktree/stack and free the slot. It respects the teardown guards
127
192
  (won't destroy a dirty/unpushed worktree without `--force`).
128
193
 
194
+ **When it refuses over unpushed commits, relay both ways out.**
195
+ A cancelled spec is normally unlanded, so this is the one moment in the
196
+ lifecycle where that guard genuinely fires — and it fires about real loss.
197
+ The worktree is the only copy of this work: the branch is on no remote and
198
+ not merged into the base branch, so removing it ends it. Say that plainly,
199
+ relay the engine's reason, and give both endings:
200
+
201
+ ```
202
+ publish it first — keeps the work reachable, then re-run /spec-cancel:
203
+ git -C <worktreePath> push -u origin <branch>
204
+
205
+ or accept the loss (the worktree and its commits go):
206
+ skitterspec spec-env down <name> --force
207
+ ```
208
+
209
+ **Print the push; never run it.** Publishing abandoned work to a shared
210
+ remote is the same unasked-for act this workflow took out of `/spec-start`,
211
+ and it is no more wanted here — someone may well want this branch to exist
212
+ nowhere but their own machine. Offer the command and wait for an answer.
213
+
214
+ **Never reach for `--force` yourself either.** The engine's own message names
215
+ only that half, which is the whole reason this step exists: meeting a wall
216
+ labelled *--force to tear down anyway* at the exact moment a backup is still
217
+ cheap is how work gets thrown away. Both options, then stop — the choice is
218
+ a decision about someone's work, and it is theirs.
219
+
129
220
  If the plan prints a `remote branch — confirm with the user first:` section,
130
221
  **ask before running that line** — it is a `git push <remote> --delete`, and
131
222
  the branch is merged, so it loses nothing. Usually there is nothing to ask: