@skitterbyte/skitterspec-linear 12.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 (55) hide show
  1. package/MIGRATION.md +208 -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 +6 -3
  8. package/assets/core/env.config.md +77 -30
  9. package/assets/core/linear.config.md +58 -0
  10. package/assets/review/page.html +1501 -0
  11. package/assets/rules/spec-planning.md +198 -10
  12. package/assets/rules/spec-reports.md +269 -0
  13. package/assets/skills/spec/SKILL.md +33 -5
  14. package/assets/skills/spec-bug/SKILL.md +172 -9
  15. package/assets/skills/spec-cancel/SKILL.md +98 -21
  16. package/assets/skills/spec-claim/SKILL.md +114 -0
  17. package/assets/skills/spec-complete/SKILL.md +94 -25
  18. package/assets/skills/spec-diff/SKILL.md +564 -0
  19. package/assets/skills/spec-hotfix/SKILL.md +172 -11
  20. package/assets/skills/spec-init/SKILL.md +34 -7
  21. package/assets/skills/spec-linear-setup/SKILL.md +55 -1
  22. package/assets/skills/spec-list/SKILL.md +218 -0
  23. package/assets/skills/spec-next/SKILL.md +299 -6
  24. package/assets/skills/spec-push/SKILL.md +32 -8
  25. package/assets/skills/spec-review/SKILL.md +40 -5
  26. package/assets/skills/spec-reviewed/SKILL.md +241 -0
  27. package/assets/skills/spec-start/SKILL.md +386 -106
  28. package/assets/skills/spec-status/SKILL.md +24 -2
  29. package/assets/skills/spec-sync/SKILL.md +40 -4
  30. package/assets/skills/spec-to-main/SKILL.md +28 -6
  31. package/package.json +11 -7
  32. package/src/cli.js +1513 -89
  33. package/src/env/building.js +143 -0
  34. package/src/env/config.js +42 -9
  35. package/src/env/provision.js +54 -15
  36. package/src/env/proxy.js +34 -1
  37. package/src/env/render.js +3 -12
  38. package/src/env/resolve.js +295 -9
  39. package/src/env/review.js +1329 -0
  40. package/src/env/serve.js +549 -0
  41. package/src/env/teardown.js +13 -6
  42. package/src/init.js +96 -1
  43. package/src/vendor/linear/api.js +104 -1
  44. package/src/vendor/linear/cli-sync.js +854 -17
  45. package/src/vendor/linear/config.js +8 -0
  46. package/src/vendor/linear/credentials.js +94 -0
  47. package/src/vendor/linear/doctor.js +35 -0
  48. package/src/vendor/linear/identity.js +105 -0
  49. package/src/vendor/linear/mcp.js +26 -0
  50. package/src/vendor/sync-core/index.js +6 -2
  51. package/src/vendor/sync-core/src/compare.js +49 -3
  52. package/src/vendor/sync-core/src/normalize.js +30 -0
  53. package/src/vendor/sync-core/src/push.js +11 -1
  54. package/src/vendor/sync-core/src/write.js +38 -0
  55. package/LICENSE +0 -21
package/MIGRATION.md CHANGED
@@ -1,5 +1,209 @@
1
1
  # Migration guide
2
2
 
3
+ ## `@skitterbyte/skitterspec` v18 → v19 (starting a spec offers phase 1)
4
+
5
+ ### Breaking change
6
+
7
+ **skitterspec now requires Node 22.13 or newer.** `engines.node` was `>=18`, and
8
+ that floor was a claim nobody was testing: the test suite cannot run without an
9
+ install, and pnpm 11.11 — the package manager this repo pins — itself requires
10
+ 22.13. A floor CI cannot exercise is a promise rather than a guarantee, so it was
11
+ raised to the version the toolchain actually needs.
12
+
13
+ On Node 18 or 20, `npm install` now warns — or fails, under `engine-strict`.
14
+ Upgrade Node, or stay on v18 of skitterspec. Nothing in your config changes
15
+ either way.
16
+
17
+ ### Releases now carry provenance
18
+
19
+ Every release from v19 onwards is built and signed by GitHub Actions through npm
20
+ Trusted Publishing, so no publish token exists anywhere to be leaked. Each
21
+ published version carries a signed attestation you can verify back to the commit
22
+ it was built from:
23
+
24
+ ```
25
+ npm view @skitterbyte/skitterspec@19.0.0 dist.attestations
26
+ ```
27
+
28
+ Nothing to do — it is a property of the package you receive.
29
+
30
+ ### Breaking change
31
+
32
+ **`/spec-start` no longer pushes the spec's branch.** It provisions the worktree
33
+ and commits the spec's move to `in-progress/` exactly as before, and then stops.
34
+ Publishing is yours to do, whenever you want the work somewhere other than your
35
+ machine:
36
+
37
+ ```
38
+ git -C <worktreePath> push -u origin <branch>
39
+ ```
40
+
41
+ Two things change for you, and neither is in your config:
42
+
43
+ - **Spec branches stop appearing on the remote.** Nothing is lost — the branch
44
+ and its commits are in the worktree — but a branch you have not pushed is on
45
+ one machine only, and that is now the default rather than something the
46
+ tooling quietly undid.
47
+ - **Cancelling a spec with unpublished work now refuses.** `/spec-cancel` has
48
+ always respected `guards.refuseTeardownIfUnpushed`, but the guard could never
49
+ fire while provisioning published every branch. It fires now, at the one moment
50
+ it was written for: the work really is about to be destroyed, and the worktree
51
+ is the only copy. `/spec-cancel` names both ways out — publish the branch and
52
+ re-run, or `spec-env down <name> --force` accepting the loss. Nothing was
53
+ removed from your config and nothing needs adding to it.
54
+
55
+ The justification for the old behaviour does not survive reading, which is why
56
+ it went rather than becoming a setting: it claimed to fire the tracker's branch
57
+ automation, and that needs `{identifier}` in `branch.pattern`, which the shipped
58
+ default does not carry. `/spec-bug` never pushed and `/spec-hotfix` forbids it,
59
+ so this also makes the three consistent.
60
+
61
+ **The `open.command` config key is gone.** It was the editor/terminal-agnostic
62
+ opener — `code {worktreePath}`, a `tmux` command, a `warp://` deeplink — that
63
+ `/spec-start` ran when it could not move your session into the worktree.
64
+
65
+ **Leaving it in `env.config.json` is harmless and silent** — which is the part
66
+ to watch. The config merge copies known keys only, so a leftover `open` block is
67
+ ignored rather than rejected: nothing errors, and your editor simply stops
68
+ opening. If you set it deliberately, that absence is the only signal you get.
69
+
70
+ **`/spec-start` no longer moves your session into the worktree either.** It
71
+ provisions the worktree, does the housekeeping there, prints the path, and stops.
72
+ This **supersedes the "opens a session in it" half of v17 → v18 below** — the
73
+ branch still never leaves its worktree, but nothing tries to relocate your shell
74
+ to reach it.
75
+
76
+ | v18 | v19 |
77
+ |-----|-----|
78
+ | Three paths through `/spec-start`: enter the session, or fall back two ways | **One path.** Provision, bootstrap, print the path. |
79
+ | Reaching the work meant getting a shell or a window into the worktree | **`/spec-diff`** renders the worktree's diff as a page you read anywhere |
80
+ | `open.command` opened an editor on the fallback path | Removed. Nothing opens anything. |
81
+
82
+ ### What replaced it
83
+
84
+ **`/spec-diff`** — a new skill, and the reason the opener had nothing left to do.
85
+ A phase is built in its own worktree, so `git diff` in your terminal answers
86
+ about the base branch. `/spec-diff` collects that worktree's changes with
87
+ `git -C` and writes a self-contained HTML page: whole-file context that folds
88
+ away, a file tree, untracked files included. Open it locally, or publish it and
89
+ read it on a phone.
90
+
91
+ The page lands in `.spec-env/reviews/<spec>.html` (gitignored), and
92
+ **the diff never passes through the model** — so it costs no context tokens
93
+ however large it is. The optional *written* review is the part that costs, and it
94
+ is offered rather than assumed. `/spec-next` writes the page at the end of every
95
+ phase. Beneath it, `skitterspec spec-env review <spec> [--branch]` is the engine.
96
+
97
+ ### `/spec-start` lands you in the worktree, and offers phase 1
98
+
99
+ Two changes to the same moment. In `worktree` mode `/spec-start` used to
100
+ provision the branch, print the path and leave your session where it was; opening
101
+ a session in the worktree was then yours to do. It now
102
+ **moves your session into the worktree** as part of bootstrapping it, and asks
103
+ whether to build phase 1:
104
+
105
+ ```
106
+ worktree ready — this session is now in it:
107
+ ../myrepo-wt/sort-inbox
108
+
109
+ build phase 1 now?
110
+ yes -> carries on into /spec-next
111
+ no -> you are already there; type /spec-next whenever you like
112
+ ```
113
+
114
+ **Your shell will not be where it was.** A session that was on `main` in the
115
+ primary checkout is standing in the spec's worktree afterwards, on the spec's
116
+ branch — so the next command you type runs there. That is the point of it, and it
117
+ is still a real change to plan for. The move is a plain `cd`: nothing prompts you
118
+ for approval, nothing opens a new terminal or window, and your primary checkout
119
+ is untouched and still on the base branch.
120
+
121
+ **`/spec-next` needs no argument now.** Say **yes** and `/spec-start` carries on
122
+ into a bare `/spec-next`; say **no** and typing `/spec-next` an hour later does
123
+ the same thing, because you are already standing in the right place. Neither is
124
+ assumed, because provisioning is cheap and reversible while a phase build is
125
+ neither.
126
+
127
+ **This is not a loosened refusal.** A bare `/spec-next` still refuses to build a
128
+ spec it is not standing in, exactly as before — what changed is where you are
129
+ standing, not how weakly the rule reads. `--worktree <path>` survives beside it as
130
+ the explicit way to build a spec you are *not* in, and a path you pass is still
131
+ not a path anything guessed.
132
+
133
+ **Leaving is a `cd` too.** `/spec-complete` and `/spec-cancel` delete the
134
+ worktree, which is now the directory you are standing in, so both tell you to `cd`
135
+ to the primary checkout first. `git worktree remove` **succeeds** on the tree you
136
+ occupy rather than refusing — the teardown looks fine and every command after it
137
+ dies with `Unable to read current working directory`.
138
+
139
+ **The `--worktree` build checks itself.** On that path, `/spec-next` first records
140
+ what your primary checkout looked like, and afterwards reports anything that
141
+ appeared in it — the signature of a relative path that missed the worktree. It
142
+ reports rather than accuses: it cannot know who wrote a file, so it names both
143
+ readings and deletes nothing.
144
+ `skitterspec spec-env resolve <spec> --record-primary` and
145
+ `--assert-primary-clean` are the engine underneath, usable on their own.
146
+
147
+ ### What to do
148
+
149
+ 1. **Upgrade** — `npx @skitterbyte/skitterspec update`.
150
+ 2. **Delete the `open` block from `specs/.core/env.config.json`**, if you have
151
+ one. Optional — it is ignored either way — but leaving it implies a setting
152
+ that no longer does anything.
153
+ 3. **Answer the question `/spec-start` now asks.** In `worktree` mode it offers
154
+ phase 1 before it finishes. Take the offer and it is built there and then;
155
+ decline and you are left standing in the provisioned worktree, free to type
156
+ `/spec-next` whenever you like. Both endings are fully supported — decline when
157
+ the phase is a big one and you would rather spend a fresh context on it.
158
+ 4. **Expect your shell to move, whichever you answer.** Anything you had queued
159
+ for the primary checkout — a `git` command, a script, a relative path — now
160
+ runs in the worktree instead. `cd` back when you want the base branch, and note
161
+ that `/spec-complete` and `/spec-cancel` require exactly that before they tear
162
+ the worktree down.
163
+ 5. **Use `/spec-diff` to read the work** rather than reaching for a terminal in
164
+ the worktree. It is gated on nothing — half a phase, a hand edit, or a
165
+ colleague's branch are all ordinary inputs.
166
+
167
+ `checkout` mode is unchanged.
168
+
169
+ ## `@skitterbyte/skitterspec-linear` v12 → v13 (starting a spec offers phase 1)
170
+
171
+ The same change as `@skitterbyte/skitterspec` v18 → v19 above — this
172
+ distribution composes the same lifecycle skills. Read that entry first.
173
+
174
+ ### Breaking change
175
+
176
+ **`spec-sync push` is now `spec-sync plan`.** The verb computes a create/update
177
+ plan and performs no network I/O; `spec-sync apply` is what writes to Linear.
178
+ Calling it `push` put three unrelated things behind one word — this verb, the
179
+ `/spec-push` skill, and `git push` — and it was the one that pushes nothing.
180
+
181
+ ```
182
+ skitterspec spec-sync plan <spec> --workspace-states <file> --json > plan.json
183
+ skitterspec spec-sync apply <spec> --plan plan.json
184
+ ```
185
+
186
+ The old name is **not** aliased. It is recognised and exits 1 naming its
187
+ replacement, so a script that calls it fails loudly with the fix in the message
188
+ rather than drifting on a name that will be removed later.
189
+
190
+ **Your `linear.config.json` needs no change.** The `"push"` values under
191
+ `sync.fieldOwnership` — `assignee: "push"`, `description: "push"`,
192
+ `workflowState: "push"` — are a different vocabulary: they name a direction of
193
+ ownership, not a subcommand. They are untouched and still mean what they meant.
194
+ Do not search-and-replace `push` in your config.
195
+
196
+ ### What else is here
197
+
198
+ **One thing here is Linear-specific.** `/spec-start` now pushes to Linear itself,
199
+ right after it commits the spec's move to `in-progress/`. It used to push nothing
200
+ and leave the mirror to the refresh `/spec-next` runs — immediate in `checkout`
201
+ mode, but in `worktree` mode hours away or never. Until it came, the issue sat in
202
+ its old workflow state with nobody assigned while the repo read `in-progress`
203
+ with a developer on it. Expect one more Linear call per `/spec-start`, and expect
204
+ the issue to be current the moment the spec is in flight. Nothing else changes:
205
+ sync is still one-way, and an unlinked spec is still skipped rather than minted.
206
+
3
207
  ## `@skitterbyte/skitterspec` v17 → v18 (a spec is built in its own worktree)
4
208
 
5
209
  ### Breaking change
@@ -90,8 +294,7 @@ landed and this one was missed.)
90
294
 
91
295
  ## `@skitterbyte/skitterspec-linear` v9 → v10 (`push` validates your issue states)
92
296
 
93
- **`spec-sync push` now refuses to run until the configured `states` names have
94
- been checked against your Linear workspace.** The check itself is not new — it
297
+ **`spec-sync push` now refuses to run until the configured `states` names have been checked against your Linear workspace.** The check itself is not new — it
95
298
  already existed on `spec-sync status --workspace-states` — but it was advisory,
96
299
  and skipping it sent a state name Linear **silently ignores**: the description
97
300
  lands, the issue never moves, and nothing errors. The base
@@ -105,8 +308,7 @@ lands, the issue never moves, and nothing errors. The base
105
308
  | A configured state absent from the workspace | pushed, silently no-op | **exits 1**, naming the workspace's real states |
106
309
 
107
310
  `/spec-push` handles this for you — it fetches the workspace's issue
108
- workflow-state names over MCP and passes them on. **Nothing changes if you drive
109
- sync through the skill.** Only a direct CLI caller needs updating.
311
+ workflow-state names over MCP and passes them on. **Nothing changes if you drive sync through the skill.** Only a direct CLI caller needs updating.
110
312
 
111
313
  ### What to do
112
314
 
@@ -156,8 +358,7 @@ sync through the skill.** Only a direct CLI caller needs updating.
156
358
  **v9 remaps the Linear mirror.** A spec is now a Linear **issue** (not a Project),
157
359
  each phase a **sub-issue** (not a Milestone), and **tasks are no longer synced**
158
360
  (they stay in the repo phase files). This collapses a large spec from ~1 project +
159
- N milestones + dozens of task-issues down to **one issue + one sub-issue per
160
- phase**. The base `@skitterbyte/skitterspec` is unaffected (still v15).
361
+ N milestones + dozens of task-issues down to **one issue + one sub-issue per phase**. The base `@skitterbyte/skitterspec` is unaffected (still v15).
161
362
 
162
363
  ### Breaking changes
163
364
 
@@ -274,10 +475,7 @@ both as optional — `/spec-review` adds them if you want them.
274
475
 
275
476
  ## `@skitterbyte/skitterspec` v2 → v3 (slimmer surface + local traffic diversion)
276
477
 
277
- **v3 shrinks the everyday command surface to five verbs — `spec → go → connect →
278
- commit → complete` — by folding provisioning, teardown, and grooming into the
279
- lifecycle skills, and adds `/spec-connect` for testing a worktree at your normal
280
- `localhost` URL.** (`@skitterbyte/skitterspec-linear` moves to v2.0.0 in lockstep.)
478
+ **v3 shrinks the everyday command surface to five verbs — `spec → go → connect → commit → complete` — by folding provisioning, teardown, and grooming into the lifecycle skills, and adds `/spec-connect` for testing a worktree at your normal `localhost` URL.** (`@skitterbyte/skitterspec-linear` moves to v2.0.0 in lockstep.)
281
479
 
282
480
  ### Removed skills (breaking) → where they went
283
481
 
package/README.md CHANGED
@@ -1,7 +1,6 @@
1
1
  # @skitterbyte/skitterspec-linear
2
2
 
3
- Spec-driven development for [Claude Code](https://claude.com/claude-code), **with
4
- one-way Linear sync**. A strict **superset** of
3
+ Spec-driven development for [Claude Code](https://claude.com/claude-code), **with one-way Linear sync**. A strict **superset** of
5
4
  [`@skitterbyte/skitterspec`](https://www.npmjs.com/package/@skitterbyte/skitterspec):
6
5
  everything in the base filesystem workflow, plus one-way sync from a spec up to
7
6
  its linked Linear issue — the repo is canonical, Linear is a generated mirror.
@@ -114,6 +113,37 @@ also start **from** an existing issue — `/spec SKI-123`,
114
113
  `/spec-hotfix v33.16.4 SKI-123`, or `--from-issue` to browse the ones your web
115
114
  app filed. There is no pull — the repo is already canonical.
116
115
 
116
+ ## One ending, every skill — `.claude/rules/spec-reports.md`
117
+
118
+ Every skill finishes with the same block, and says nothing while it runs beyond
119
+ a question it cannot answer itself or a failure at the moment it happens:
120
+
121
+ ✅ **Phase 2 built** — `feat-orders`, 2 of 4
122
+
123
+ | | |
124
+ |---|---|
125
+ | **Tracker** | [SKS-88](https://linear.app/…/SKS-88) · `feat-orders` · phase 2 moved |
126
+ | **Branch** | `spec/feat-orders` · 3 commits, clean |
127
+ | **Built** | POST /orders handler, orders schema |
128
+ | **Tests** | 128 passed · npm test |
129
+ | **Review** | 7 files, +212 −18 · [open the page](file:///…) — want a written review before you commit? |
130
+ | **Follow-ups** | none |
131
+ | **Next** | `/spec-next` → phase 3 (Auth) |
132
+
133
+ Four verdicts, and the last two are different facts about your repo: `✅` done ·
134
+ `⚠️` done with caveats · `❌` failed part-way, so there is a mess to clear ·
135
+ `⏸` refused before acting, so nothing changed.
136
+ **A refusal emits the block too**, so "nothing happened" is a reported outcome
137
+ rather than an absent one.
138
+
139
+ Fields come from a fixed vocabulary in a fixed order, and a skill emits only the
140
+ ones it declares — `Tracker` first because the id is how you address the work
141
+ outside the repo, `Next` last because it is the only row you act on.
142
+ `Follow-ups` is always there: a recorded `none` is a decision where a missing
143
+ line is an oversight. The block covers **that run only** — what else is in
144
+ flight is a different question, and answering it here leaves you unable to tell
145
+ what followed from the run you just watched.
146
+
117
147
  ## Opt-in
118
148
 
119
149
  Linear sync is inert until `specs/.core/linear.config.json` exists — copy the
@@ -2,14 +2,50 @@
2
2
 
3
3
  Spec-driven development runs through the lifecycle **skills** — use them so
4
4
  structure and lifecycle stay consistent. The everyday loop is
5
- **`spec → start → next → commit → complete`**, with `/spec-connect` when you want to test
6
- the spec in a browser.
5
+ **`spec → start → next → commit → complete`**, with **`/spec-diff`** to read what
6
+ a phase changed and `/spec-connect` when you want to test the spec in a browser.
7
7
 
8
8
  **Skills vs commands.** The lifecycle skills are read by Claude, which exercises
9
9
  judgment. `/spec-connect` and `/spec-live` are **slash commands** instead — each
10
10
  pre-executes one `spec-env` verb and relays it, so only you can run them; a
11
11
  skill that wants one will tell you to type it.
12
12
 
13
+ **Seeing the work.** A phase is built in its own worktree, so `git diff` in your
14
+ terminal answers about the base branch. **`/spec-diff`** renders that worktree's
15
+ diff as a self-contained HTML page you open locally — or publish, and read on a
16
+ phone. The diff never passes through the model, so the page costs no context
17
+ tokens however large it is; the optional written review is the part that costs,
18
+ and it is offered rather than assumed. `/spec-next` writes the page at the end of
19
+ every phase.
20
+
21
+ **Handing the review back.** The page takes marks: tick `✓ accept` per file as
22
+ you read, note anything against a line or a whole file, answer the questions a
23
+ written review asked — then **end it in a decision**. Three buttons, each
24
+ carrying its own verdict:
25
+ `✓ Commit` commits it, `✓ Commit & Continue` commits and builds the next phase,
26
+ `↺ Request changes` sends it straight back to be worked, `… Discuss first` asks
27
+ you what's up. A **served** page hands the pass to
28
+ the engine, which **holds** it and shows a six-digit code. Type `/spec-reviewed`
29
+ and the waiting pass is picked up and acted on; paste the code after it
30
+ (`/spec-reviewed 324199`) to name one exactly, which matters only when two are
31
+ waiting. **Nothing pushes** — a device that reaches your page cannot reach this
32
+ conversation, which is what keeps a stray approval out of your review, and is
33
+ why the code is an address rather than a password. A `file://` page has no server to talk to, so it
34
+ copies and you paste, as before. Approve is unavailable while a note is open —
35
+ you asked for something, so it cannot also be fine — and it hands off to your
36
+ own commit skill (`review.commitWith`, `/commit` by default) rather than a copy
37
+ living here. Fixes come back as resolutions, so the next render shows each note
38
+ struck through with what changed. An accept remembers the file's content, so it
39
+ lapses by itself when that file changes again. The marks are information —
40
+ nothing gates on them; the verdict is the one thing you choose, once.
41
+
42
+ **One ending.** Every spec skill finishes with the same block — a verdict
43
+ (`✅` · `⚠️` · `❌` · `⏸`), then a table of the fields that skill declares,
44
+ ending on the one thing to do next. A refusal emits it too, so "nothing
45
+ happened" is reported rather than absent, and `Follow-ups` is always there
46
+ because a recorded `none` is a decision. Skills stay quiet while they run. The
47
+ shape lives in **`.claude/rules/spec-reports.md`**.
48
+
13
49
  The skill table, the spec type/folder conventions and the per-spec isolation
14
50
  model all live in **`.claude/rules/spec-planning.md`**, the canonical reference
15
51
  every spec skill points at. Tailor its per-phase test commands to this stack.
@@ -1,6 +1,6 @@
1
1
  ---
2
- description: Expose one spec's dev servers on the canonical localhost ports (or `main` to hand them back)
3
- argument-hint: "[spec|main]"
2
+ description: Expose one spec's dev servers on the canonical localhost ports bare uses the spec you are on, `main` hands them back
3
+ argument-hint: "[<spec> | main]"
4
4
  allowed-tools: Bash({{exec}} skitterspec spec-env connect:*)
5
5
  disable-model-invocation: true
6
6
  ---
@@ -1,6 +1,6 @@
1
1
  ---
2
- description: Put one spec live on the already-running dev server — `<spec>` takes it, a bare `take` takes the spec you are on, `main` hands the instance back
3
- argument-hint: "[<spec> | main | take | release | abort | status]"
2
+ description: Put one spec live on the already-running dev server — bare takes the spec you are on, `<spec>` takes that one, `main` hands the instance back
3
+ argument-hint: "[<spec> | main | status | release | abort]"
4
4
  allowed-tools: Bash({{exec}} skitterspec spec-env live:*)
5
5
  disable-model-invocation: true
6
6
  ---
@@ -20,9 +20,9 @@ Fresh installs can skip this section. **`MIGRATION.md` ships with the package**
20
20
  read the entry for the version you are coming from before upgrading a repo with a
21
21
  live mirror.
22
22
 
23
- **From 9.x — `push` now validates your issue states.** `spec-sync push` refuses
24
- to run until the configured `states` names have been checked against the
25
- workspace. `/spec-push` does that for you, so nothing changes if you drive sync
23
+ **From 9.x — the plan verb validates your issue states.** `spec-sync plan`
24
+ (named `spec-sync push` before v13) refuses to run until the configured `states`
25
+ names have been checked against the workspace. `/spec-push` does that for you, so nothing changes if you drive sync
26
26
  through the skill; a script calling the CLI directly must pass
27
27
  `--workspace-states <file>` (or `--skip-state-check`). See `MIGRATION.md`
28
28
  → "v9 → v10".
@@ -202,9 +202,14 @@ Two ways to get there:
202
202
  |---------|-----------|--------------|
203
203
  | `/spec-status` | — | Read-only drift report: what would push (create/update), and whether Linear's workflow-state drifted from the spec. Writes nothing. |
204
204
  | `/spec-push` | repo → Linear | Computes a create/update plan vs the last-pushed snapshot and applies it (issue description/state, phase sub-issues), stamping new ids back into the spec. |
205
+ | `/spec-claim` | repo → Linear | Take a spec over, `--release` it, or hand it `--to` a teammate. Records the owner in the spec, then pushes. Needs assignment enabled (below). |
206
+ | `/spec-list` | Linear → you | Read-only listing of every spec Linear holds — id, title, state, who holds it and the local spec name to start it by. Filters by state, backlog order, or assignee. Writes nothing. |
205
207
 
206
208
  Typical loop: edit the spec in-repo → `/spec-status` (what's pending) →
207
209
  `/spec-push` (send it up). There is no pull — Linear is a generated mirror.
210
+ `/spec-list` is the read side of that: it asks Linear what exists rather than
211
+ the repo, which is the only way to see a spec someone started on a branch you
212
+ have not got.
208
213
 
209
214
  ### What gets pushed
210
215
 
@@ -229,6 +234,19 @@ Sections listed in `sync.localOnlySections` (default: **State log**, **Changelog
229
234
  **Open questions**) are stripped from the pushed description — they never leave
230
235
  the repo.
231
236
 
237
+ ### Assignment (opt-in)
238
+
239
+ Add `"assignee": "push"` to `sync.fieldOwnership` and the spec issue is assigned
240
+ to whoever is building it: `/spec-start` records them, and the issue is released
241
+ automatically when the spec completes. Left out, nothing about it happens at all
242
+ — see **Assignment** in `linear.config.md` for the full rules.
243
+
244
+ You do not configure *who you are*: it comes from your own API key. Check it
245
+ with `skitterspec spec-sync whoami` (`--set` overrides it if the key is shared or
246
+ a bot's), and `skitterspec spec-sync users <name-or-email>` looks somebody up.
247
+ `skitterspec spec-sync doctor` reports the resolved identity, and stays quiet
248
+ about it in a project that has not opted in.
249
+
232
250
  ## 7. What to commit
233
251
 
234
252
  - **Commit** `specs/.core/linear-base/` — the last-pushed snapshots (content
@@ -25,9 +25,6 @@
25
25
  "enabled": true,
26
26
  "host": "127.0.0.1"
27
27
  },
28
- "open": {
29
- "command": ""
30
- },
31
28
  "registry": ".spec-env/registry.json",
32
29
  "branch": {
33
30
  "pattern": "{type}/{slug}",
@@ -41,6 +38,12 @@
41
38
  "refuseTeardownIfDirty": true,
42
39
  "refuseTeardownIfUnpushed": true
43
40
  },
41
+ "review": {
42
+ "reader": "detect",
43
+ "servePort": 7777,
44
+ "serveOnRemote": true,
45
+ "commitWith": "/commit"
46
+ },
44
47
  "live": {
45
48
  "migrations": []
46
49
  }
@@ -1,11 +1,12 @@
1
1
  # `env.config.json` — per-spec isolation config
2
2
 
3
3
  Opt-in config for per-spec isolation (git worktree + optional namespaced Docker
4
- stack + host dev servers + a front-door proxy + an optional opener per
5
- in-progress spec). Provisioning is folded into `/spec-start`, teardown into
6
- `/spec-complete` · `/spec-cancel`, and traffic diversion is `/spec-connect`; the
7
- `skitterspec spec-env <up|down|prune|dev|connect|integrate>` CLI is the engine
8
- beneath them.
4
+ stack + host dev servers + a front-door proxy per in-progress spec).
5
+ Provisioning is folded into `/spec-start`, teardown into `/spec-complete` ·
6
+ `/spec-cancel`, traffic diversion is `/spec-connect`, and reading a spec's diff
7
+ is `/spec-diff`; the `skitterspec spec-env
8
+ <up|down|prune|dev|connect|integrate|hotfix|live|review|stage|status|resolve>` CLI is
9
+ the engine beneath them.
9
10
 
10
11
  **Once this file is present, isolation is the default policy:** `/spec-start` gives
11
12
  **every** in-progress spec its own git worktree automatically. Docker is a
@@ -31,11 +32,12 @@ no live `env.config.json` was found.
31
32
  // Where a spec's branch gets built.
32
33
  //
33
34
  // "worktree" (default) — every spec gets its own git worktree. Several
34
- // specs run side by side and `main` stays free, at the cost of
35
- // one terminal session per spec (`/spec-start` sets it up for you).
35
+ // specs run side by side and `main` stays free. `/spec-start`
36
+ // moves your session into the spec's worktree with a `cd`, so
37
+ // the terminal you are already in follows the work.
36
38
  // "checkout" — the branch is built in the primary checkout instead. One
37
- // spec at a time, but no second session and no hand-off: the
38
- // terminal you are already in follows the work.
39
+ // spec at a time, and nowhere else to stand: the work comes to
40
+ // your terminal rather than your terminal going to it.
39
41
  //
40
42
  // Pick it for how you work, not for what this repo contains — a project with
41
43
  // no dev servers may still want several specs in flight. An unrecognised
@@ -124,24 +126,6 @@ no live `env.config.json` was found.
124
126
  "host": "127.0.0.1" // bind host for the canonical ports
125
127
  },
126
128
 
127
- // Optional, editor/terminal-agnostic opener — the FALLBACK for reaching a
128
- // worktree. `/spec-start` normally moves the session you typed into into the
129
- // worktree, and then there is nothing to open: it runs this only when it
130
- // could not switch in place (the session is already inside another worktree,
131
- // or the harness cannot move it). Run after provisioning and bootstrap, so
132
- // the session opens onto a tree that is ready to work in. The template is
133
- // expanded with {worktreePath}, {slug}, {branch}, {projectName},
134
- // {portOffset}.
135
- // Empty = nothing is opened (the path is just printed), which is how you turn
136
- // the auto-open off, and the right value unless you actually want a second
137
- // window on the fallback path. A non-interactive run skips it either way — an
138
- // opened window nobody is sitting at helps no one.
139
- // Examples: "code {worktreePath}", "tmux new-window -c {worktreePath}",
140
- // or a "warp://..." deeplink for Warp users.
141
- "open": {
142
- "command": ""
143
- },
144
-
145
129
  // Machine-local slot registry (spec → slot index). Resolved against the
146
130
  // primary checkout root, shared by all worktrees, gitignored.
147
131
  "registry": ".spec-env/registry.json",
@@ -193,9 +177,10 @@ no live `env.config.json` was found.
193
177
  "refuseTeardownIfUnpushed": true
194
178
  },
195
179
 
196
- // What teardown cleans up beyond this machine. `/spec-start` pushes the spec
197
- // branch when it provisions, so without this a completed spec leaves a merged
198
- // branch on the remote forever. `deleteRemoteBranch`:
180
+ // What teardown cleans up beyond this machine. Nothing publishes a spec
181
+ // branch for you, so a remote copy exists only because you pushed it by hand —
182
+ // and without this a completed spec leaves that merged branch on the remote
183
+ // forever. `deleteRemoteBranch`:
199
184
  // "prompt" (default) — plan `git push <remote> --delete <branch>` in its own
200
185
  // "confirm with the user first" section; /spec-complete and
201
186
  // /spec-cancel ask before running it.
@@ -232,6 +217,68 @@ no live `env.config.json` was found.
232
217
  "bump": "patch",
233
218
  "cherryPickMain": true,
234
219
  "targets": []
220
+ },
221
+
222
+ // Reading a spec's diff (`spec-env review`, `/spec-diff`).
223
+ //
224
+ // `reader` decides how the page's LOCATION IS WORDED, and — through
225
+ // `serveOnRemote` below — whether the engine stands its local server up so a
226
+ // remote reader gets a link that opens. It never decides to PUBLISH. Three
227
+ // values:
228
+ // "local" — you are at the machine holding the page; a file:// URL opens.
229
+ // "remote" — you are not; it does not, so the page is served instead.
230
+ // "detect" — work it out (the default).
231
+ // An explicit "local"/"remote" is BELIEVED WITHOUT SNIFFING: you know where
232
+ // you are reading, and no signal outranks being told. Detection is only the
233
+ // default, and it has three outcomes rather than two — local, remote, and
234
+ // unknown. Unknown behaves exactly as the tool did before any of this existed
235
+ // (the file:// URL, no warning), because a wrong "local" prints a dead link
236
+ // and a wrong "remote" warns at someone whose link works fine. An
237
+ // unrecognised value falls through to "detect", so a typo cannot become a
238
+ // confident answer. Default: detect.
239
+ //
240
+ // `servePort` is the default port for `spec-env review serve`, which renders
241
+ // every spec's diff per request on one local server. `--port` overrides it per
242
+ // run. The server binds 127.0.0.1 unless `--host 0.0.0.0` is passed, which
243
+ // mints an unguessable path token and prints the LAN URL including it —
244
+ // anyone holding that URL can read every spec's diff while it runs.
245
+ // Default: 7777.
246
+ //
247
+ // `serveOnRemote` is whether a "remote" reader may have that server started
248
+ // FOR them. On (the default) the engine brings it up, binds 0.0.0.0, and puts
249
+ // the served URL on the `open:` line — best-guess network address first, the
250
+ // rest listed under it, because the guess reads interface names and a VPN or
251
+ // an unusual adapter will fool it. Off, you get the file:// URL with its
252
+ // "will not open where you are reading" marker and the command to type.
253
+ // Either way NOTHING IS PUBLISHED on a detection: a server is one process
254
+ // ended by one flag, while a published page is one this tooling cannot
255
+ // remove, so that half stays an explicit ask. Teardown names a server that
256
+ // served the last spec, and `spec-env prune` reaps a pidfile whose process is
257
+ // gone. Default: true.
258
+ //
259
+ // `commitWith` names the skill a COMMITTING verdict hands off to. A review
260
+ // page ends in a verdict — commit, commit & continue, request changes,
261
+ // discuss — and the point of the first two is that the commit follows from
262
+ // the reading rather than costing a separate decision. Skitterspec never
263
+ // commits through a skill it vendored: `/commit` ships with skittership, a
264
+ // different package, and a copy living here would fork it. Two shapes:
265
+ // "/commit" — the default; hand off to skittership's commit skill.
266
+ // "<name>" — any other skill your project installs.
267
+ // With the named skill unavailable, the committing branch commits directly —
268
+ // stage, typecheck, test, conventional message — and SAYS it took that path,
269
+ // because a commit made under rules nobody configured must not read as one
270
+ // made under /commit.
271
+ //
272
+ // THERE IS NO OFF SWITCH. `"none"` existed and was removed: it produced a
273
+ // verdict that records itself and does nothing, which is the one thing a
274
+ // review page must not offer — a review is the guard in front of an action.
275
+ // Recording an approval for SOMEONE ELSE to act on is a separate mechanism,
276
+ // not a value of this key. Default: "/commit".
277
+ "review": {
278
+ "reader": "detect",
279
+ "servePort": 7777,
280
+ "serveOnRemote": true,
281
+ "commitWith": "/commit"
235
282
  }
236
283
  }
237
284
  ```
@@ -133,6 +133,64 @@ absence). A `sync.fieldOwnership` value outside `both|pull|push` is a hard error
133
133
  }
134
134
  ```
135
135
 
136
+ ## Assignment (`sync.fieldOwnership.assignee`)
137
+
138
+ Off by default. Add one key and the spec issue is assigned to whoever is
139
+ building it:
140
+
141
+ ```jsonc
142
+ "fieldOwnership": {
143
+ "description": "push",
144
+ "subIssues": "push",
145
+ "workflowState": "push",
146
+ "assignee": "push" // <- the whole opt-in
147
+ }
148
+ ```
149
+
150
+ It is deliberately **not** a config key of its own. `fieldOwnership` is already
151
+ the documented extension point — "any key you add joins the pushed projection" —
152
+ and assignment is exactly one more field the repo owns.
153
+
154
+ - **Absent = inert.** No writes, no prompts, no drift line, and no assignee hash
155
+ in any snapshot. A project that never opts in cannot tell the feature exists,
156
+ which is why it is missing from `linear.config.json.example`: that file is
157
+ copied verbatim into new projects, and shipping the key there would opt
158
+ everyone in by default.
159
+ - **The bucket decides.** The spec's `linear_assignee_id` is pushed while the
160
+ spec is live (`backlog`, `in-progress`) and cleared once it reaches `complete`
161
+ or `cancelled` — so finishing a spec hands the issue back with no unassign
162
+ step for anyone to remember. The stamp stays in the file, and so does
163
+ `> **Developer:**`: they record who *actioned* the work, which outlives who is
164
+ holding it.
165
+ - **Unset means don't touch.** A spec that records nobody sends no assignee at
166
+ all, so an issue a PM assigned in Linear is never overwritten. Only an assignee
167
+ the repo itself pushed is ever cleared — and a snapshot written before you
168
+ opted in counts as "never pushed", not as "was nobody".
169
+ - **Only the spec issue.** Phase sub-issues are never assigned: one person builds
170
+ a spec, and N assigned sub-issues is N notifications for one piece of work.
171
+ They stay independently assignable in Linear.
172
+
173
+ **Who you are** is not configured here, and cannot be — this file is committed,
174
+ so a user id in it would follow the repo to every teammate who clones it. It is
175
+ derived from your own API key (`viewer`) and cached per machine in
176
+ `~/.config/skitterspec/credentials.json` beside the key. `spec-sync whoami`
177
+ shows it, `--set` overrides it when the key is shared or a bot's, and
178
+ `spec-sync users` looks somebody up by name or email.
179
+
180
+ Ownership moves with **`/spec-claim`** — take it, `--release` it, or `--to` a
181
+ teammate.
182
+
183
+ **Reading it back** is **`/spec-list`** (engine: `spec-sync list`), the one
184
+ command that asks Linear what exists instead of the repo. It lists parentless
185
+ issues — a phase sub-issue carries a parent, a spec issue does not — joined to
186
+ the local spec folder that owns each one, so every row carries the name you
187
+ paste into `/spec-start`. Scope is the live states by default
188
+ (`--state`/`--all`/`--in-progress` to change it, `--next N` for the top of the
189
+ backlog in Linear's own order), and `--mine`/`--by <user>` filter by assignee,
190
+ reusing the identity above rather than a second copy of it. It writes nothing,
191
+ and it says what it did not show — the count, the archived exclusion, and any
192
+ cap — rather than implying a completeness it never checked.
193
+
136
194
  ## The deployment ladder (`release.stages`)
137
195
 
138
196
  A spec's lifecycle stops at `complete`. Where a ticket goes **after** that —