@skitterbyte/skitterspec-jira 0.0.0-stage → 2.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 (113) hide show
  1. package/MIGRATION.md +1266 -0
  2. package/README.md +44 -2
  3. package/assets/claude-md-section.md +85 -0
  4. package/assets/commands/allow-main.md +33 -0
  5. package/assets/commands/spec-connect.md +13 -0
  6. package/assets/commands/spec-live.md +14 -0
  7. package/assets/commands/spec-remote-review.md +37 -0
  8. package/assets/commands/spec-skip.md +24 -0
  9. package/assets/core/SETUP.md +192 -0
  10. package/assets/core/env.config.json.example +56 -0
  11. package/assets/core/env.config.md +606 -0
  12. package/assets/core/gating.config.json.example +4 -0
  13. package/assets/core/gating.config.md +81 -0
  14. package/assets/core/jira.config.json.example +18 -0
  15. package/assets/core/jira.config.md +88 -0
  16. package/assets/hooks/main-guard.cjs +162 -0
  17. package/assets/hooks/review-gate.cjs +146 -0
  18. package/assets/review/page.html +3384 -0
  19. package/assets/rules/commit-trailers.md +96 -0
  20. package/assets/rules/negative-checks.md +69 -0
  21. package/assets/rules/offered-unblocks.md +100 -0
  22. package/assets/rules/spec-planning.md +702 -0
  23. package/assets/rules/spec-reports.md +572 -0
  24. package/assets/skills/no-spec/SKILL.md +249 -0
  25. package/assets/skills/spec/SKILL.md +714 -0
  26. package/assets/skills/spec-bug/SKILL.md +585 -0
  27. package/assets/skills/spec-cancel/SKILL.md +261 -0
  28. package/assets/skills/spec-complete/SKILL.md +451 -0
  29. package/assets/skills/spec-diff/SKILL.md +870 -0
  30. package/assets/skills/spec-hotfix/SKILL.md +607 -0
  31. package/assets/skills/spec-init/SKILL.md +178 -0
  32. package/assets/skills/spec-jira-setup/SKILL.md +221 -0
  33. package/assets/skills/spec-list/SKILL.md +173 -0
  34. package/assets/skills/spec-next/SKILL.md +678 -0
  35. package/assets/skills/spec-push/SKILL.md +306 -0
  36. package/assets/skills/spec-review/SKILL.md +256 -0
  37. package/assets/skills/spec-reviewed/SKILL.md +284 -0
  38. package/assets/skills/spec-start/SKILL.md +496 -0
  39. package/assets/skills/spec-status/SKILL.md +114 -0
  40. package/assets/skills/spec-sync/SKILL.md +220 -0
  41. package/assets/skills/spec-to-main/SKILL.md +141 -0
  42. package/bin/skitterspec-jira.js +68 -0
  43. package/package.json +40 -4
  44. package/src/cli.js +5967 -0
  45. package/src/deprecate.js +138 -0
  46. package/src/env/bootstrap.js +188 -0
  47. package/src/env/building.js +143 -0
  48. package/src/env/classify.js +176 -0
  49. package/src/env/commitcmd.js +108 -0
  50. package/src/env/config.js +850 -0
  51. package/src/env/dev.js +63 -0
  52. package/src/env/glob.js +34 -0
  53. package/src/env/hooks.js +226 -0
  54. package/src/env/hotfix.js +413 -0
  55. package/src/env/hotfixland.js +302 -0
  56. package/src/env/integrate.js +167 -0
  57. package/src/env/live.js +479 -0
  58. package/src/env/mainguard.js +299 -0
  59. package/src/env/provision.js +466 -0
  60. package/src/env/proxy.js +225 -0
  61. package/src/env/prune.js +119 -0
  62. package/src/env/registry.js +159 -0
  63. package/src/env/render.js +17 -0
  64. package/src/env/resolve.js +716 -0
  65. package/src/env/review.js +2622 -0
  66. package/src/env/reviewers/coderabbit.js +228 -0
  67. package/src/env/reviewers.js +517 -0
  68. package/src/env/serve.js +1044 -0
  69. package/src/env/supervise.js +157 -0
  70. package/src/env/teardown.js +248 -0
  71. package/src/env/trust.js +87 -0
  72. package/src/gating.js +155 -0
  73. package/src/init.js +1150 -0
  74. package/src/lines-diff.js +114 -0
  75. package/src/prompts.js +129 -0
  76. package/src/vendor/jira/adf.js +414 -0
  77. package/src/vendor/jira/api.js +295 -0
  78. package/src/vendor/jira/cli-sync.js +1252 -0
  79. package/src/vendor/jira/commands.js +44 -0
  80. package/src/vendor/jira/config.js +286 -0
  81. package/src/vendor/jira/credentials.js +108 -0
  82. package/src/vendor/jira/dialect.js +45 -0
  83. package/src/vendor/jira/mcp.js +129 -0
  84. package/src/vendor/provider-kit/index.js +41 -0
  85. package/src/vendor/provider-kit/src/adapter.js +23 -0
  86. package/src/vendor/provider-kit/src/apply.js +207 -0
  87. package/src/vendor/provider-kit/src/args.js +68 -0
  88. package/src/vendor/provider-kit/src/assignment.js +277 -0
  89. package/src/vendor/provider-kit/src/config.js +251 -0
  90. package/src/vendor/provider-kit/src/credentials.js +404 -0
  91. package/src/vendor/provider-kit/src/doctor.js +35 -0
  92. package/src/vendor/provider-kit/src/http.js +63 -0
  93. package/src/vendor/provider-kit/src/identity.js +93 -0
  94. package/src/vendor/provider-kit/src/list-assignee.js +71 -0
  95. package/src/vendor/provider-kit/src/list-rows.js +35 -0
  96. package/src/vendor/provider-kit/src/md-compare.js +39 -0
  97. package/src/vendor/provider-kit/src/md-tables.js +94 -0
  98. package/src/vendor/provider-kit/src/prompt.js +50 -0
  99. package/src/vendor/provider-kit/src/released.js +219 -0
  100. package/src/vendor/provider-kit/src/specs.js +248 -0
  101. package/src/vendor/provider-kit/src/transport.js +22 -0
  102. package/src/vendor/sync-core/index.js +92 -0
  103. package/src/vendor/sync-core/src/base.js +81 -0
  104. package/src/vendor/sync-core/src/compare.js +338 -0
  105. package/src/vendor/sync-core/src/dialect.js +51 -0
  106. package/src/vendor/sync-core/src/fields.js +36 -0
  107. package/src/vendor/sync-core/src/legacy.js +100 -0
  108. package/src/vendor/sync-core/src/normalize.js +971 -0
  109. package/src/vendor/sync-core/src/push.js +85 -0
  110. package/src/vendor/sync-core/src/retarget.js +278 -0
  111. package/src/vendor/sync-core/src/task-block.js +308 -0
  112. package/src/vendor/sync-core/src/verify.js +67 -0
  113. package/src/vendor/sync-core/src/write.js +191 -0
package/MIGRATION.md ADDED
@@ -0,0 +1,1266 @@
1
+ # Migration guide
2
+
3
+ ## `@skitterbyte/skitterspec-github` v1 → v2 (tell, don't track)
4
+
5
+ Skitterspec now tells GitHub about **its own** transitions and nothing
6
+ else. It writes nothing about releases or deployments, and it never reads a
7
+ ticket's state back.
8
+
9
+ ### Breaking change 1 — The `release create|deployed|complete|reconcile` verbs are removed
10
+
11
+ They exit 1 with one line naming `spec-sync released <range> --json`. A pipeline
12
+ still calling them goes red rather than silently believing it mirrors
13
+ anything. **To migrate**, replace the step with your own script over the
14
+ release's ticket list:
15
+
16
+ ```sh
17
+ skitterspec-github spec-sync released "<previous-tag>..<tag>" --json
18
+ ```
19
+
20
+ Each ticket row now carries `spec` and `url` (both `null` for a ref no spec has
21
+ linked), so the script needs no second lookup. Set whatever GitHub field,
22
+ state or release object your process uses from that list. The checkout needs
23
+ `fetch-depth: 0` so the previous tag is visible.
24
+
25
+ ### Breaking change 2 — The `releases` block (`deployedField`, `series`) and `release.stages` are retired
26
+
27
+ The keys are ignored, like any unknown key, so a config carrying them still
28
+ loads. `spec-sync doctor` names them as unused config (a `warn`, never a
29
+ failure). Delete them when you remove the CI step.
30
+
31
+ ### Breaking change 3 — no state drift
32
+
33
+ `spec-sync status --remote` and `/spec-status` no longer compare the tracker's
34
+ state with the spec's. A ticket someone moved on GitHub is not reported, and
35
+ push still resends state only when the spec itself moves. The edited-description
36
+ warning is unchanged.
37
+
38
+ ## `@skitterbyte/skitterspec-azure-boards` v1 → v2 (tell, don't track)
39
+
40
+ Skitterspec now tells Azure Boards about **its own** transitions and nothing
41
+ else. It writes nothing about releases or deployments, and it never reads a
42
+ ticket's state back.
43
+
44
+ ### Breaking change 1 — The `release create|deployed|complete` verbs are removed
45
+
46
+ They exit 1 with one line naming `spec-sync released <range> --json`. A pipeline
47
+ still calling them goes red rather than silently believing it mirrors
48
+ anything. **To migrate**, replace the step with your own script over the
49
+ release's ticket list:
50
+
51
+ ```sh
52
+ skitterspec-azure-boards spec-sync released "<previous-tag>..<tag>" --json
53
+ ```
54
+
55
+ Each ticket row now carries `spec` and `url` (both `null` for a ref no spec has
56
+ linked), so the script needs no second lookup. Set whatever Azure Boards field,
57
+ state or release object your process uses from that list. The checkout needs
58
+ `fetch-depth: 0` so the previous tag is visible.
59
+
60
+ ### Breaking change 2 — The `releases` block (`workItemType`, `series`, `ticketTag`, `deployedField`) and `release.stages` are retired
61
+
62
+ The keys are ignored, like any unknown key, so a config carrying them still
63
+ loads. `spec-sync doctor` names them as unused config (a `warn`, never a
64
+ failure). Delete them when you remove the CI step.
65
+
66
+ ### Breaking change 3 — no state drift
67
+
68
+ `spec-sync status --remote` and `/spec-status` no longer compare the tracker's
69
+ state with the spec's. A ticket someone moved on Azure Boards is not reported, and
70
+ push still resends state only when the spec itself moves. The edited-description
71
+ warning is unchanged.
72
+
73
+ ## `@skitterbyte/skitterspec-jira` v1 → v2 (tell, don't track)
74
+
75
+ Skitterspec now tells Jira about **its own** transitions and nothing else. It
76
+ writes nothing about releases or deployments, and it never reads an issue's
77
+ status back.
78
+
79
+ ### Breaking change 1 — no status drift
80
+
81
+ `spec-sync status --remote` and `/spec-status` no longer compare Jira's status
82
+ with the spec's. An issue someone moved on Jira is not reported, and push still
83
+ transitions it only when the spec itself moves. The edited-description warning
84
+ is unchanged.
85
+
86
+ ### Breaking change 2 — `release.stages` retired
87
+
88
+ It was accepted and validated before, and never acted on. It is now ignored
89
+ like any unknown key, and `spec-sync doctor` names it as unused config (a
90
+ `warn`, never a failure). Delete it.
91
+
92
+ ### Also — `released --json` names the spec
93
+
94
+ Each ticket row now carries `spec` and `url` (both `null` for a ref no spec has
95
+ linked), so CI can record a release, for example as a fix version, without a
96
+ second lookup:
97
+
98
+ ```sh
99
+ skitterspec-jira spec-sync released "<previous-tag>..<tag>" --json
100
+ ```
101
+
102
+ ## `@skitterbyte/skitterspec` v23 → v24 (hotfix land runs the release itself)
103
+
104
+ ### Breaking change 1 — `spec-env hotfix land` runs its steps, and holds before the carry
105
+
106
+ `hotfix land` used to print a plan: a `git tag` for the patch-bumped base tag,
107
+ the `--also` cherry-picks, and a `git cherry-pick <base>..<branch>` onto main.
108
+ The skill ran it by hand, and you pushed the tag yourself.
109
+ **It now runs the land itself**, stopping at the first step that fails:
110
+
111
+ 1. **release** — `hotfix.releaseWith` when it is set (see below), otherwise the
112
+ built-in tagger. The release is recorded in `.spec-env/hotfix/<spec>.json`,
113
+ so a re-run reuses it rather than cutting a second one.
114
+ 2. **push** — the branch, then the tag, to `origin`, **only with `--push`**.
115
+ Without it the run **holds** here: exit 0, both push commands printed, and
116
+ nothing carried. A re-run after you push by hand finds the tag on the remote
117
+ and carries on.
118
+ 3. **carry** — the branch's commits onto main, **minus the version commit** and
119
+ anything main already has, so the spec's completion commits arrive and main's
120
+ version line does not move. It runs in place when main is checked out and
121
+ clean, through a throwaway worktree when it isn't checked out, and is
122
+ **refused** with the command to run later when main is checked out and dirty.
123
+ A conflict is aborted and names the commit.
124
+
125
+ `--plan` describes the steps and runs nothing; `--json` reports each step's
126
+ outcome. Refusals and failures now exit 1.
127
+
128
+ **What to change.** Run `skitterspec update` to take the new `/spec-complete`
129
+ and `/spec-hotfix`. `/spec-complete` asks before passing `--push`. Anything of
130
+ your own that ran `hotfix land`'s printed commands should run the verb itself,
131
+ with `--push` once you have decided to deploy.
132
+
133
+ ### Breaking change 2 — a hotfix's worktree waits for `spec-env hotfix finish`
134
+
135
+ `/spec-complete` used to tear a hotfix down as soon as it had tagged. The branch
136
+ is now kept until the deploy is confirmed, because a failed deploy cuts the next
137
+ release from the same branch. Run `/spec-complete` again on the landed hotfix,
138
+ or `skitterspec spec-env hotfix finish <spec>` directly. It removes the worktree
139
+ and deletes the branch only when the tree is clean, a release tag is at HEAD,
140
+ and `origin/<branch>` is at HEAD. It names every check that fails, and has no
141
+ `--force`.
142
+
143
+ ### New — `hotfix.releaseWith` and a real `hotfix.bump`
144
+
145
+ - **`hotfix.releaseWith`** is a command run in the hotfix worktree. It cuts the
146
+ release (a version commit and a tag) and prints one JSON line:
147
+ `{"tag","version","baseTag","branch","versionCommit","pushed"}`. skittership
148
+ 2.1+ ships one:
149
+
150
+ ```jsonc
151
+ "hotfix": { "releaseWith": "node scripts/release-hotfix.cjs --json" }
152
+ ```
153
+
154
+ Call node directly, not `pnpm run`, so no run banner reaches stdout. Anything
155
+ but the JSON line is a refusal. Add `--allow-postversion` yourself if your
156
+ package.json has a `postversion` script. `init` and `update` suggest this when
157
+ the script is present, and never write it.
158
+ - **`hotfix.bump`** was parsed and ignored; it is now read. `"patch"` (the
159
+ default) is the old behaviour. `"hotfix-suffix"` cuts `X.Y.Z-hotfix.N`,
160
+ continues a base that is already a hotfix tag, and is not blocked by a later
161
+ `X.Y.(Z+1)`. Any other value is refused when `hotfix land` runs.
162
+
163
+ Extra targets (`hotfix.targets` / `--also`) stay tag-only: they never go through
164
+ `releaseWith` and are not pushed.
165
+
166
+ ## `@skitterbyte/skitterspec` v22 → v23 (the engine runs the worktree bootstrap)
167
+
168
+ ### Breaking change — `spec-env up` no longer prints the worktree steps
169
+
170
+ `spec-env up` (and `spec-env nospec`) used to end with a
171
+ `then, in the worktree, run:` block of shell commands: one per seeded file, the
172
+ env-file write, each `setup` command, and `docker compose … up -d`. They now end
173
+ with one line instead:
174
+
175
+ ```
176
+ then, run:
177
+ skitterspec spec-env bootstrap <name>
178
+ ```
179
+
180
+ **`spec-env bootstrap [<spec>]` runs those steps itself**, with the worktree
181
+ as its cwd: it seeds the gitignored files, writes `docker.envFile`, runs `setup`
182
+ and brings the stack up, in that order. The behaviour is the same as before: it
183
+ is idempotent, prints `seeded …` / `exists — skipped` / `not in main — skipped`
184
+ per seed, refuses to write through a symlinked env file, and stops at the first
185
+ failing step, naming it. `--docs` seeds only. `--plan` describes the steps
186
+ without running them, and `--json` reports each step and its outcome.
187
+
188
+ **Why.** The printed commands had to be copied word for word, and agents did
189
+ not copy them faithfully. A project's secrets hook refused any Bash call naming
190
+ a `.env` file, so every rewritten seed command was blocked and bootstrap stopped
191
+ half-way. The new command line names no file.
192
+
193
+ **What to change.** The shipped skills already call `bootstrap`; run
194
+ `skitterspec update` to take them. Anything of your own that ran the printed
195
+ block (a script, or a skill you edited) should run
196
+ `skitterspec spec-env bootstrap <name>` after the `git worktree add` instead.
197
+ Nothing in `env.config.json` changes.
198
+
199
+ **If you added a secrets-hook exception for the printed seed commands**, you
200
+ can remove it now. Nothing shipped prints them any more.
201
+
202
+ ## `@skitterbyte/skitterspec-azure-boards` v0 → v1 (first release)
203
+
204
+ Nothing to migrate: this is the distribution's first release. It is a superset
205
+ of `@skitterbyte/skitterspec` — install exactly one of the base or one
206
+ superset. Coming from the base, run `npx @skitterbyte/skitterspec-azure-boards
207
+ update` and run `/spec-azure-setup`
208
+ (or `skitterspec-azure-boards spec-sync init-config --org-url … --project …`).
209
+
210
+ **Coming from `@skitterbyte/skitterspec-linear` or `-jira`** — a repo moving
211
+ trackers: swap the distribution, configure Azure as above, set
212
+ `refs.foreignPatterns` to the old team key (e.g. `"ERQ-\\d+"`), then push each
213
+ backlog and in-progress spec (`/spec-push`, or `spec-sync apply --all
214
+ in-progress` and `--all backlog`) to link it to a new work item. Finished specs
215
+ keep their old stamps (`linear_identifier`, …) as history; nothing rewrites
216
+ them. Old `Refs: ERQ-12` trailers stay in the log, and `spec-sync released`
217
+ counts them as `historical` rather than as missing a ref. New commits carry
218
+ `Refs: AB#n`. Delete the old `linear.config.json` / `jira.config.json` once the
219
+ open specs are linked.
220
+
221
+ ## `@skitterbyte/skitterspec-github` v0 → v1 (first release)
222
+
223
+ Nothing to migrate: this is the distribution's first release. It is a superset
224
+ of `@skitterbyte/skitterspec` — install exactly one of the base or one
225
+ superset. Coming from the base, run `npx @skitterbyte/skitterspec-github
226
+ update`, make a Projects v2 board for the repo, and run `/spec-github-setup`
227
+ (or `skitterspec-github spec-sync init-config --owner … --repo … --project N`).
228
+
229
+ **Coming from `@skitterbyte/skitterspec-linear` or `-jira`** — a repo moving
230
+ trackers: swap the distribution, configure GitHub as above, then push each
231
+ backlog and in-progress spec (`/spec-push`, or `spec-sync apply --all
232
+ in-progress` and `--all backlog`) to link it to a new issue. Finished specs keep
233
+ their old stamps (`linear_identifier`, …) as history; nothing rewrites them.
234
+ Old `Refs: SKS-12` trailers stay in the log, and `spec-sync released` counts
235
+ them as `historical` rather than as missing a ref. New commits carry
236
+ `Refs: #N`. Delete the old `linear.config.json` / `jira.config.json` once the
237
+ open specs are linked.
238
+
239
+ ## `@skitterbyte/skitterspec-jira` v0 → v1 (first release)
240
+
241
+ Nothing to migrate: this is the distribution's first release. It is a superset
242
+ of `@skitterbyte/skitterspec` — install exactly one of the base, the Linear
243
+ superset, or this one. Coming from the base, run `npx
244
+ @skitterbyte/skitterspec-jira update` and then `skitterspec-jira spec-sync
245
+ init-config --site-url … --project-key …` (or the `/spec-jira-setup` skill) to
246
+ opt into Jira sync. Coming from `@skitterbyte/skitterspec-linear`: the two
247
+ supersets share one credentials store and one base workflow, but a repo syncs
248
+ to one tracker — swap distributions only if the repo is moving trackers.
249
+
250
+ ## `@skitterbyte/skitterspec` v21 → v22 (a review port per repo, and every render serves)
251
+
252
+ ### Breaking change 1 — `review.serveOnRemote` is now `review.serve`
253
+
254
+ **Every render now stands the review server up**, and hands back an `http://`
255
+ URL instead of a `file://` one. It used to serve only when it detected that you
256
+ were reading from somewhere else — and that detection reads three environment
257
+ variables, so a plain local terminal got a `file://` page. A `file://` page has
258
+ **no server to POST to**, so the verdict buttons on it had nowhere to go: the
259
+ press-a-button-and-the-work-continues loop was missing from exactly the sessions
260
+ that are easiest to use.
261
+
262
+ **Which interfaces it listens on is `review.allowNetwork`** — see breaking
263
+ change 3, below. The change here is narrower than it looks: `file://` →
264
+ `http://127.0.0.1`, a page that opens on the machine holding it and, unlike
265
+ `file://`, can answer. Serving more never means listening wider; that is one
266
+ setting, and it is not this one.
267
+
268
+ **What to change.** `review.serveOnRemote: false` is still read as the new
269
+ `review.serve: "never"`, so a config that turned serving off keeps working. The
270
+ key is renamed because the old name would now claim to govern remote renders
271
+ while governing every one of them:
272
+
273
+ ```jsonc
274
+ // specs/.core/env.config.json — before
275
+ "review": { "serveOnRemote": false }
276
+ // after
277
+ "review": { "serve": "never" }
278
+ ```
279
+
280
+ `serveOnRemote: true` needs no translation — `"always"` is the default and does
281
+ what it did. Note what `"never"` costs you: the page falls back to `file://`,
282
+ and its buttons copy a command for you to paste rather than sending anything.
283
+
284
+ **A render that could not serve now says why** — `review.serve is "never"`, or
285
+ the failure naming the port to free — on the `open:` line and in `--json` as
286
+ `notServed`. A `file://` link with nothing said about it used to be the ordinary
287
+ outcome; now it can only mean the ask did not land.
288
+
289
+ **A spec with no worktree is served too.** A spec you have only just written has
290
+ no branch of its own, and the served route used to refuse it — so its page
291
+ existed and 404'd. It is now served as its own documents, listed in the index,
292
+ and accepts a verdict.
293
+
294
+ ### Breaking change 2 — a review port per repo
295
+
296
+ **`review.servePort` now defaults to `"auto"`, not `7777`.** A repo that does
297
+ not pin the key moves to a port derived from its own path —
298
+ `7700 + hash(realpath(repoRoot)) % 100` — the first time `spec-env review serve`
299
+ starts after the upgrade.
300
+
301
+ **The one-time break is a link, and only a link.** A review page open on a
302
+ phone at that moment points at `7777`, and `7777` is no longer this repo's
303
+ server. The verdict you press there goes nowhere. **Re-render the page** —
304
+ `/spec-diff`, or `/spec-next`'s own render — and the new link works. Nothing on
305
+ disk is lost, no pass is dropped, and this happens once.
306
+
307
+ **Why not simply keep 7777.** It was shared by every repo on the machine, so the
308
+ second repo to start was refused, whoever hit that passed `--port`, and the
309
+ links they had already handed out pointed at another repo's daemon — a `404`,
310
+ reported in a grey line in the page footer. A derived port is stable for a given
311
+ tree across a restart, a reboot and a `--stop`, which is what a link handed out
312
+ yesterday needs.
313
+
314
+ ### To keep a fixed port
315
+
316
+ Pin it, and nothing derives:
317
+
318
+ ```jsonc
319
+ // specs/.core/env.config.json
320
+ "review": { "servePort": 7777 }
321
+ ```
322
+
323
+ An explicit number always wins, exactly as it did before. Pin one when you want
324
+ a port you can memorise, or when two repos happen to derive the same one — a
325
+ hundred slots is a small chance of that, not no chance, and the server still
326
+ refuses a busy port rather than moving itself aside.
327
+
328
+ `spec-env review serve --status` now prints the port **and which of the three chose it** —
329
+ `--port`, `review.servePort`, or the derivation.
330
+
331
+ ### Breaking change 3 — the reader no longer decides the bind: `review.allowNetwork` does
332
+
333
+ **Two new keys, and one of them changes a default.** The engine used to guess
334
+ where you were reading and pick a surface for you. It guessed wrong three
335
+ separate ways in one day — a `file://` page on a session detected `unknown`, a
336
+ LAN URL for a phone that had left the network, and an address that changed
337
+ underneath a reader when detection flipped mid-session. So it stopped guessing:
338
+ every render now lists every tier, labelled, in a fixed order.
339
+
340
+ ```jsonc
341
+ // specs/.core/env.config.json — the new keys, with their defaults
342
+ "review": {
343
+ "allowNetwork": true, // the server binds every interface, so the page opens on your phone
344
+ "allowRemote": false // publishing is permitted at all — it permits, it never publishes
345
+ }
346
+ ```
347
+
348
+ **`allowNetwork` defaults to `true`, and that is the behaviour change to read before upgrading.**
349
+ Before, a local or unknown reader got a loopback bind and a
350
+ remote one got every interface; now the setting decides, and its default is the
351
+ wide bind — which is what a remote reader already got, applied to every render.
352
+ The URL still carries the **serve token**, 48 random bits minted per server,
353
+ which remains the only thing deciding who can POST a verdict. A machine you
354
+ would rather not expose sets `"allowNetwork": false`, and `network:` then reads
355
+ `off` with the command that turns it back on.
356
+
357
+ **`allowRemote` defaults to `false`** and permits publishing without performing
358
+ it — a published page is one skitterspec cannot delete, so it stays an explicit
359
+ ask either way.
360
+
361
+ **`review.reader` still exists** and now decides only how a page's location is
362
+ *worded*. It no longer chooses the bind and no longer chooses which tiers appear.
363
+
364
+ ### `/spec-remote-review` — a new slash command
365
+
366
+ Toggles `review.allowRemote`, and takes `on`/`off` when you would rather say
367
+ which. It exists because a render's `remote:` line has to name something you can
368
+ act on, and `skitterspec spec-env review allow remote` is not what anyone
369
+ reconstructs from a page they are reading on a phone. `init` and `update`
370
+ install it with the other commands; like them it is user-only, so nothing but you
371
+ can run it.
372
+
373
+ **It writes `specs/.core/env.config.json` in the primary checkout** — a
374
+ committed file, so it changes for everyone who pulls and leaves that tree dirty.
375
+ The engine says so when it does.
376
+
377
+ ## `@skitterbyte/skitterspec-linear` v18 → v19 (tell, don't track; hotfix land runs the release itself)
378
+
379
+ Skitterspec now tells Linear about **its own** transitions and nothing
380
+ else. It writes nothing about releases or deployments, and it never reads a
381
+ ticket's state back.
382
+
383
+ ### Breaking change 1 — The `spec-sync stage` verb is removed
384
+
385
+ It exits 1 with one line naming `spec-sync released <range> --json`. A pipeline
386
+ still calling it goes red rather than silently believing it mirrors
387
+ anything. **To migrate**, replace the step with your own script over the
388
+ release's ticket list:
389
+
390
+ ```sh
391
+ skitterspec-linear spec-sync released "<previous-tag>..<tag>" --json
392
+ ```
393
+
394
+ Each ticket row now carries `spec` and `url` (both `null` for a ref no spec has
395
+ linked), so the script needs no second lookup. Set whatever Linear field,
396
+ state or release object your process uses from that list. The checkout needs
397
+ `fetch-depth: 0` so the previous tag is visible.
398
+
399
+ ### Breaking change 2 — `release.stages` (the deployment ladder) is retired
400
+
401
+ The keys are ignored, like any unknown key, so a config carrying them still
402
+ loads. `spec-sync doctor` names them as unused config (a `warn`, never a
403
+ failure). Delete them when you remove the CI step.
404
+
405
+ ### Breaking change 3 — no state drift
406
+
407
+ `spec-sync status --remote` and `/spec-status` no longer compare the tracker's
408
+ state with the spec's. A ticket someone moved on Linear is not reported, and
409
+ push still resends state only when the spec itself moves. The edited-description
410
+ warning is unchanged.
411
+
412
+ **A ladder rung no longer reads as `complete`.** Where a state was a
413
+ `release.stages` rung, the live `spec-sync list` now shows it as unmapped. To
414
+ keep it reading as finished, list it as an alias:
415
+ `"complete": ["Done", "On Test"]`. `init-config --stage` is gone too, and is
416
+ refused as an unknown flag.
417
+
418
+ ### Also — hotfix land runs the release itself
419
+
420
+ The same change as `@skitterbyte/skitterspec` v23 → v24 above — this
421
+ distribution composes the same engine. Read that entry; nothing here is
422
+ Linear-specific.
423
+
424
+ ## `@skitterbyte/skitterspec-linear` v17 → v18 (the engine runs the worktree bootstrap)
425
+
426
+ The same change as `@skitterbyte/skitterspec` v22 → v23 above — this
427
+ distribution composes the same engine. Read that entry; nothing here is
428
+ Linear-specific.
429
+
430
+ ## `@skitterbyte/skitterspec-linear` v16 → v17 (a review port per repo, and every render serves)
431
+
432
+ The same two changes as `@skitterbyte/skitterspec` v21 → v22 above — this
433
+ distribution composes the same engine. Read that entry; nothing here is
434
+ Linear-specific.
435
+
436
+ **If you are coming from v15, read the v15 → v16 entry below as well.** 16.0.0
437
+ was bumped in the repo and never published, so npm went 15 → 17 and both
438
+ entries apply to you.
439
+
440
+ ## `@skitterbyte/skitterspec-linear` v15 → v16 (assignment is on by default)
441
+
442
+ ### Breaking change
443
+
444
+ **A spec's Linear issue is now assigned to whoever is building it.**
445
+ `sync.fieldOwnership.assignee` defaults to `"push"`; it was absent
446
+ before, and absent meant inert. From this version `/spec-start` stamps the
447
+ developer on the spec and the push assigns the issue, and `/spec-complete`
448
+ releases it — on every repo that has not said otherwise.
449
+
450
+ **To decline it**, one line in `specs/.core/linear.config.json`:
451
+
452
+ ```jsonc
453
+ "sync": {
454
+ "fieldOwnership": {
455
+ "assignee": "none"
456
+ }
457
+ }
458
+ ```
459
+
460
+ `none` is a new value in the ownership enum (`both|pull|push|none`) and means
461
+ *this repo does not own the field*: nothing is written, no hash is recorded in a
462
+ snapshot, and `spec-sync status` prints no assignee line — identical in every
463
+ respect to the old behaviour of never listing it. It has to be a value rather
464
+ than an omitted key, because the map merges **per key** onto the defaults: once
465
+ a field is owned by default, there is nothing an absent key can subtract.
466
+
467
+ **An explicit `"assignee": "push"` you already had is now redundant and harmless.**
468
+ Nothing rewrites it and nothing needs to.
469
+
470
+ ### It cannot overwrite a PM's triage, and that is by construction
471
+
472
+ The concern this default has to answer is whether upgrading re-assigns or
473
+ un-assigns issues someone else owns. It cannot, for two reasons that predate it:
474
+
475
+ - **A spec that records no assignee sends none.** Only a spec the repo itself
476
+ stamped is ever pushed, so an issue a PM assigned in Linear is left alone.
477
+ - **A snapshot with no assignee key means "never pushed", not "was null".** Every
478
+ spec linked before this existed has exactly such a snapshot, and the diff emits
479
+ a *clear* only against an assignee it has a recorded hash for.
480
+
481
+ So the flip widens who gets **assigned** — specs you start from now on — and
482
+ cannot widen who gets **unassigned**. There is a regression test pinning this
483
+ (`sync-assignee-projection.test.js`, "THE UPGRADE CASE").
484
+
485
+ ### `spec-sync init-config --assign` is gone; `--no-assign` replaces it
486
+
487
+ The flag's meaning inverted with the default, so it is removed rather than kept
488
+ as a silent no-op — a flag that still parses and does nothing would read as
489
+ "assignment configured" to anyone who wrote it before v16. `--assign` now fails
490
+ as an unknown flag, which is the louder and more useful answer. `--no-assign`
491
+ writes the `"none"` opt-out above.
492
+
493
+ ### `spec-sync assign` now refuses a field the repo does not own
494
+
495
+ Previously it stamped the frontmatter in any repo and printed
496
+ `next: push it, so Linear agrees` — while `toFieldSet` dropped the field, so the
497
+ push never carried it. It now refuses, naming `sync.fieldOwnership.assignee` and
498
+ the value that enables it. Only `/spec-claim` enforced this before, so a script
499
+ or a direct CLI call got the untrue sentence.
500
+
501
+ ## `@skitterbyte/skitterspec` v20 → v21 (the gate installs, and asking implies waiting)
502
+
503
+ If you upgraded to v20 and the commit gate never once fired, this is why. Two
504
+ independent bugs, either of which alone made it a no-op.
505
+
506
+ ### Breaking change
507
+
508
+ **The hook ships as `.claude/hooks/review-gate.cjs`**, renamed from
509
+ `review-gate.js`. The script is CommonJS and it is copied *into your* project,
510
+ where your `package.json` decides how node parses a `.js` — so in any
511
+ `"type": "module"` project it died on its own first `require`, printing a stack
512
+ trace on **every Bash tool call**. `.cjs` settles the parse mode at the file,
513
+ which is the only place independent of the one file skitterspec does not
514
+ control. An ESM rewrite would have inverted the same problem onto CommonJS
515
+ projects, which are still the default for anything with no `"type"` set.
516
+
517
+ The upgrade migrates you: your existing `PreToolUse` entry has its path
518
+ **rewritten in place** — so a wrapper, a flag or a different interpreter you
519
+ added all survive — and the retired `review-gate.js` is deleted. If you edited
520
+ that file yourself it is **kept**, with a warning, and left for you to remove.
521
+
522
+ **Check whether you already had a `review-gate.cjs` of your own.** `.cjs` is the
523
+ obvious way to work around the v20 crash, so anyone who fixed it by hand most
524
+ likely picked this exact filename — and this upgrade is the moment skitterspec
525
+ starts managing that path. A file already sitting there is **kept**, the real
526
+ hook is therefore never installed, and `review-gate.js` is pruned out from under
527
+ it. A hand-written shim that only `require`d the old script then fails open, so
528
+ the gate is silently absent under an update that reported success.
529
+
530
+ Two lines settle it:
531
+
532
+ ```
533
+ head -3 .claude/hooks/review-gate.cjs # ours opens with a 'use strict' + a doc comment
534
+ npx @skitterbyte/skitterspec update --check # a line naming this path means yours was kept
535
+ ```
536
+
537
+ If it is yours, take ours — `npx @skitterbyte/skitterspec update --force`, or
538
+ delete the file and re-run `update`. Later versions report this case in its own
539
+ words rather than as `your edit — kept`, but the manifest records the path after
540
+ the first upgrade, so that wording cannot reach anyone who has already run one.
541
+
542
+ ### Bug fix
543
+
544
+ **`skitterspec update` now registers the hook.** v20's notes said `init` and
545
+ `update` both did; only `init` did. `update` copied the script, reported
546
+ `created: .claude/hooks/review-gate.js`, and wired nothing — so every project
547
+ that upgraded into v20 got a hook file and no hook, with nothing saying so.
548
+
549
+ ### What to do
550
+
551
+ 1. **Upgrade** — `npx @skitterbyte/skitterspec update`.
552
+ 2. **Commit `.claude/settings.json` and `.claude/hooks/review-gate.cjs`**, and
553
+ the deletion of `.claude/hooks/review-gate.js`. A hook only a fraction of the
554
+ team has is a gate that holds for a fraction of the team.
555
+ 3. **Check it is actually on** — the update reports
556
+ `updated: .claude/settings.json (review-gate hook)` the first time, and
557
+ `unchanged` afterwards. If it says neither, your settings file could not be
558
+ parsed; it was left untouched and the hook is not registered.
559
+
560
+ ### Behaviour change — a bug fix and a hotfix now owe a verdict
561
+
562
+ **`/spec-bug` and `/spec-hotfix` now arm the review gate**, as `/spec-next`
563
+ already did. Both render the page at the end of red→green work, and both now
564
+ wait for your verdict instead of finishing — so in that spec's worktree a
565
+ `git commit` is **refused** until one of two things happens:
566
+
567
+ - you send a verdict from the page (or `/spec-reviewed` picks up one already
568
+ waiting), or
569
+ - you run `skitterspec spec-env review skip "<reason>"`, which records the
570
+ decision to move on.
571
+
572
+ The gate is still only armed by work that **finished**, it still fails open on
573
+ every cannot-tell, and `review.required: false` in `specs/.core/env.config.json`
574
+ still turns it off for a project.
575
+
576
+ **Why.** Those two skills used to render the page, ask *"want a written review
577
+ before you commit?"* and finish with nothing watching. A verdict pressed on that
578
+ page sat in the holding area until someone typed `/spec-reviewed` — which they
579
+ had no reason to do, because the run had said the page was *ready* rather than
580
+ that it was *waiting*. Two verdicts were stranded that way on one spec, and the
581
+ second existed only because the first appeared to do nothing.
582
+
583
+ ### New, and not breaking — the `Continue` verdict
584
+
585
+ **A page rendered part-way through a run offers `Continue`** — *I have read it,
586
+ carry on* — instead of the committing buttons, via
587
+ `spec-env review <spec> --buttons midrun`. The default is unchanged, so a caller
588
+ that passes nothing renders exactly the page it rendered before. `Continue` can
589
+ never clear an armed gate: that still takes a committing verdict or a recorded
590
+ skip.
591
+
592
+ Nothing else changes: `review.required` still defaults to `true`, and the engine
593
+ and `/spec-next` held the gate throughout regardless of the hook.
594
+
595
+ ## `@skitterbyte/skitterspec-linear` v14 → v15 (the gate installs, and asking implies waiting)
596
+
597
+ The same change as `@skitterbyte/skitterspec` v20 → v21 above — this
598
+ distribution composes the same lifecycle skills. Read that entry; nothing here
599
+ is Linear-specific.
600
+
601
+ ## `@skitterbyte/skitterspec` v19 → v20 (a phase owes a verdict)
602
+
603
+ ### Breaking change
604
+
605
+ **A phase that has ended now refuses to go further until you send a verdict.**
606
+ `/spec-next` **arms** a gate when it finishes a phase and renders its review
607
+ page. While it is armed, two things refuse:
608
+
609
+ - `/spec-next` will not build the next phase.
610
+ - `git commit` inside **that spec's own worktree** is blocked by a harness hook
611
+ — which is what covers a bare `git commit`, a chained command, and
612
+ skittership's `/commit` without skitterspec editing any of them.
613
+
614
+ Exactly two things clear it, and both are one command:
615
+
616
+ ```
617
+ # press Commit or Commit & Continue on the review page — or:
618
+ skitterspec spec-env review skip "none: additive, nothing to revert"
619
+ ```
620
+
621
+ The skip is deliberately not silent: the reason goes into the review outcome
622
+ log, on the same reasoning as the `Gating:` header — a reason is a decision a
623
+ reviewer can argue with, where silence is an oversight.
624
+
625
+ **It is on by default** wherever isolation is configured (`env.config.json`
626
+ present). To turn it off for a project, add to `specs/.core/env.config.json`:
627
+
628
+ ```json
629
+ { "review": { "required": false } }
630
+ ```
631
+
632
+ Three things keep it a push rather than a wall, and they are worth knowing
633
+ before you reach for that setting. It is armed **only by a phase ending**, so a
634
+ mid-phase `/spec-diff` owes nothing. The exit is always one command. And it
635
+ accuses only on a positive signal — no engine, an unreadable payload, a commit
636
+ on the base branch or in another spec's tree, a repo with no isolation: every
637
+ cannot-tell lets the commit through.
638
+
639
+ ### Breaking change
640
+
641
+ **`.claude/settings.json` is now written by the installer.** `skitterspec init`
642
+ and `skitterspec update` copy the hook script and register it as a `PreToolUse`
643
+ hook in your project's **committed** settings file. (In v20 `update` copied
644
+ without registering, and the script was named `review-gate.js` — both fixed in
645
+ v21; read that entry above if you are landing on the current release.) That is a
646
+ tracked file in most repos, so expect it in `git status` after upgrading — and
647
+ commit it, because a hook only a fraction of the team has is a gate that holds
648
+ for a fraction of the team.
649
+
650
+ A settings file that cannot be parsed is **reported and left alone**, never
651
+ rewritten. The hook is an extra layer: the engine and `/spec-next` hold the gate
652
+ without it.
653
+
654
+ ### The review page can now reach your session — deliberately
655
+
656
+ This inverts an invariant the docs used to state outright: *a device that
657
+ reaches your page cannot reach your conversation*. It no longer holds, and the
658
+ change is the point. `/spec-next` now ends a phase by rendering the page and
659
+ **waiting** on it, so the button you press is what carries the work on — there
660
+ is no command to remember.
661
+
662
+ What replaced the old guard is two mechanisms and one rule:
663
+
664
+ - **The serve token** — 48 random bits in the URL path, minted per server —
665
+ decides who can POST at all.
666
+ - **The wait window** — only a pass that arrives *while the session is waiting*
667
+ is claimed for you, and two arrivals refuse rather than pick one.
668
+ - Outside that window nothing is claimed unasked. `/spec-reviewed`, or
669
+ `/spec-reviewed 324199` to name one exactly, is still how a pass sent when
670
+ nobody was waiting gets picked up — and it is still user-only, so the model
671
+ cannot claim a pass on its own.
672
+
673
+ A `file://` page has no server to talk to, so it copies and you paste, exactly
674
+ as before.
675
+
676
+ ### What to do
677
+
678
+ 1. **Upgrade** — `npx @skitterbyte/skitterspec update`.
679
+ 2. **Commit `.claude/settings.json` and the hook script.** Both are new in your
680
+ working tree after the update.
681
+ 3. **Nothing else to configure.** `review.required` defaults to `true` and
682
+ `review.commitWith` defaults to `/commit`; neither needs adding unless you
683
+ are changing it.
684
+
685
+ ## `@skitterbyte/skitterspec-linear` v13 → v14 (a phase owes a verdict)
686
+
687
+ The same change as `@skitterbyte/skitterspec` v19 → v20 above — this
688
+ distribution composes the same lifecycle skills. Read that entry; nothing here
689
+ is Linear-specific.
690
+
691
+ ## `@skitterbyte/skitterspec` v18 → v19 (starting a spec offers phase 1)
692
+
693
+ ### Breaking change
694
+
695
+ **skitterspec now requires Node 22.13 or newer.** `engines.node` was `>=18`, and
696
+ that floor was a claim nobody was testing: the test suite cannot run without an
697
+ install, and pnpm 11.11 — the package manager this repo pins — itself requires
698
+ 22.13. A floor CI cannot exercise is a promise rather than a guarantee, so it was
699
+ raised to the version the toolchain actually needs.
700
+
701
+ On Node 18 or 20, `npm install` now warns — or fails, under `engine-strict`.
702
+ Upgrade Node, or stay on v18 of skitterspec. Nothing in your config changes
703
+ either way.
704
+
705
+ ### Releases now carry provenance
706
+
707
+ Every release from v19 onwards is built and signed by GitHub Actions through npm
708
+ Trusted Publishing, so no publish token exists anywhere to be leaked. Each
709
+ published version carries a signed attestation you can verify back to the commit
710
+ it was built from:
711
+
712
+ ```
713
+ npm view @skitterbyte/skitterspec@19.0.0 dist.attestations
714
+ ```
715
+
716
+ Nothing to do — it is a property of the package you receive.
717
+
718
+ ### Breaking change
719
+
720
+ **`/spec-start` no longer pushes the spec's branch.** It provisions the worktree
721
+ and commits the spec's move to `in-progress/` exactly as before, and then stops.
722
+ Publishing is yours to do, whenever you want the work somewhere other than your
723
+ machine:
724
+
725
+ ```
726
+ git -C <worktreePath> push -u origin <branch>
727
+ ```
728
+
729
+ Two things change for you, and neither is in your config:
730
+
731
+ - **Spec branches stop appearing on the remote.** Nothing is lost — the branch
732
+ and its commits are in the worktree — but a branch you have not pushed is on
733
+ one machine only, and that is now the default rather than something the
734
+ tooling quietly undid.
735
+ - **Cancelling a spec with unpublished work now refuses.** `/spec-cancel` has
736
+ always respected `guards.refuseTeardownIfUnpushed`, but the guard could never
737
+ fire while provisioning published every branch. It fires now, at the one moment
738
+ it was written for: the work really is about to be destroyed, and the worktree
739
+ is the only copy. `/spec-cancel` names both ways out — publish the branch and
740
+ re-run, or `spec-env down <name> --force` accepting the loss. Nothing was
741
+ removed from your config and nothing needs adding to it.
742
+
743
+ The justification for the old behaviour does not survive reading, which is why
744
+ it went rather than becoming a setting: it claimed to fire the tracker's branch
745
+ automation, and that needs `{identifier}` in `branch.pattern`, which the shipped
746
+ default does not carry. `/spec-bug` never pushed and `/spec-hotfix` forbids it,
747
+ so this also makes the three consistent.
748
+
749
+ **The `open.command` config key is gone.** It was the editor/terminal-agnostic
750
+ opener — `code {worktreePath}`, a `tmux` command, a `warp://` deeplink — that
751
+ `/spec-start` ran when it could not move your session into the worktree.
752
+
753
+ **Leaving it in `env.config.json` is harmless and silent** — which is the part
754
+ to watch. The config merge copies known keys only, so a leftover `open` block is
755
+ ignored rather than rejected: nothing errors, and your editor simply stops
756
+ opening. If you set it deliberately, that absence is the only signal you get.
757
+
758
+ **`/spec-start` no longer moves your session into the worktree either.** It
759
+ provisions the worktree, does the housekeeping there, prints the path, and stops.
760
+ This **supersedes the "opens a session in it" half of v17 → v18 below** — the
761
+ branch still never leaves its worktree, but nothing tries to relocate your shell
762
+ to reach it.
763
+
764
+ | v18 | v19 |
765
+ |-----|-----|
766
+ | Three paths through `/spec-start`: enter the session, or fall back two ways | **One path.** Provision, bootstrap, print the path. |
767
+ | 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 |
768
+ | `open.command` opened an editor on the fallback path | Removed. Nothing opens anything. |
769
+
770
+ ### What replaced it
771
+
772
+ **`/spec-diff`** — a new skill, and the reason the opener had nothing left to do.
773
+ A phase is built in its own worktree, so `git diff` in your terminal answers
774
+ about the base branch. `/spec-diff` collects that worktree's changes with
775
+ `git -C` and writes a self-contained HTML page: whole-file context that folds
776
+ away, a file tree, untracked files included. Open it locally, or publish it and
777
+ read it on a phone.
778
+
779
+ The page lands in `.spec-env/reviews/<spec>.html` (gitignored), and
780
+ **the diff never passes through the model** — so it costs no context tokens
781
+ however large it is. The optional *written* review is the part that costs, and it
782
+ is offered rather than assumed. `/spec-next` writes the page at the end of every
783
+ phase. Beneath it, `skitterspec spec-env review <spec> [--branch]` is the engine.
784
+
785
+ ### `/spec-start` lands you in the worktree, and offers phase 1
786
+
787
+ Two changes to the same moment. In `worktree` mode `/spec-start` used to
788
+ provision the branch, print the path and leave your session where it was; opening
789
+ a session in the worktree was then yours to do. It now
790
+ **moves your session into the worktree** as part of bootstrapping it, and asks
791
+ whether to build phase 1:
792
+
793
+ ```
794
+ worktree ready — this session is now in it:
795
+ ../myrepo-wt/sort-inbox
796
+
797
+ build phase 1 now?
798
+ yes -> carries on into /spec-next
799
+ no -> you are already there; type /spec-next whenever you like
800
+ ```
801
+
802
+ **Your shell will not be where it was.** A session that was on `main` in the
803
+ primary checkout is standing in the spec's worktree afterwards, on the spec's
804
+ branch — so the next command you type runs there. That is the point of it, and it
805
+ is still a real change to plan for. The move is a plain `cd`: nothing prompts you
806
+ for approval, nothing opens a new terminal or window, and your primary checkout
807
+ is untouched and still on the base branch.
808
+
809
+ **`/spec-next` needs no argument now.** Say **yes** and `/spec-start` carries on
810
+ into a bare `/spec-next`; say **no** and typing `/spec-next` an hour later does
811
+ the same thing, because you are already standing in the right place. Neither is
812
+ assumed, because provisioning is cheap and reversible while a phase build is
813
+ neither.
814
+
815
+ **This is not a loosened refusal.** A bare `/spec-next` still refuses to build a
816
+ spec it is not standing in, exactly as before — what changed is where you are
817
+ standing, not how weakly the rule reads. `--worktree <path>` survives beside it as
818
+ the explicit way to build a spec you are *not* in, and a path you pass is still
819
+ not a path anything guessed.
820
+
821
+ **Leaving is a `cd` too.** `/spec-complete` and `/spec-cancel` delete the
822
+ worktree, which is now the directory you are standing in, so both tell you to `cd`
823
+ to the primary checkout first. `git worktree remove` **succeeds** on the tree you
824
+ occupy rather than refusing — the teardown looks fine and every command after it
825
+ dies with `Unable to read current working directory`.
826
+
827
+ **The `--worktree` build checks itself.** On that path, `/spec-next` first records
828
+ what your primary checkout looked like, and afterwards reports anything that
829
+ appeared in it — the signature of a relative path that missed the worktree. It
830
+ reports rather than accuses: it cannot know who wrote a file, so it names both
831
+ readings and deletes nothing.
832
+ `skitterspec spec-env resolve <spec> --record-primary` and
833
+ `--assert-primary-clean` are the engine underneath, usable on their own.
834
+
835
+ ### What to do
836
+
837
+ 1. **Upgrade** — `npx @skitterbyte/skitterspec update`.
838
+ 2. **Delete the `open` block from `specs/.core/env.config.json`**, if you have
839
+ one. Optional — it is ignored either way — but leaving it implies a setting
840
+ that no longer does anything.
841
+ 3. **Answer the question `/spec-start` now asks.** In `worktree` mode it offers
842
+ phase 1 before it finishes. Take the offer and it is built there and then;
843
+ decline and you are left standing in the provisioned worktree, free to type
844
+ `/spec-next` whenever you like. Both endings are fully supported — decline when
845
+ the phase is a big one and you would rather spend a fresh context on it.
846
+ 4. **Expect your shell to move, whichever you answer.** Anything you had queued
847
+ for the primary checkout — a `git` command, a script, a relative path — now
848
+ runs in the worktree instead. `cd` back when you want the base branch, and note
849
+ that `/spec-complete` and `/spec-cancel` require exactly that before they tear
850
+ the worktree down.
851
+ 5. **Use `/spec-diff` to read the work** rather than reaching for a terminal in
852
+ the worktree. It is gated on nothing — half a phase, a hand edit, or a
853
+ colleague's branch are all ordinary inputs.
854
+
855
+ `checkout` mode is unchanged.
856
+
857
+ ## `@skitterbyte/skitterspec-linear` v12 → v13 (starting a spec offers phase 1)
858
+
859
+ The same change as `@skitterbyte/skitterspec` v18 → v19 above — this
860
+ distribution composes the same lifecycle skills. Read that entry first.
861
+
862
+ ### Breaking change
863
+
864
+ **`spec-sync push` is now `spec-sync plan`.** The verb computes a create/update
865
+ plan and performs no network I/O; `spec-sync apply` is what writes to Linear.
866
+ Calling it `push` put three unrelated things behind one word — this verb, the
867
+ `/spec-push` skill, and `git push` — and it was the one that pushes nothing.
868
+
869
+ ```
870
+ skitterspec spec-sync plan <spec> --workspace-states <file> --json > plan.json
871
+ skitterspec spec-sync apply <spec> --plan plan.json
872
+ ```
873
+
874
+ The old name is **not** aliased. It is recognised and exits 1 naming its
875
+ replacement, so a script that calls it fails loudly with the fix in the message
876
+ rather than drifting on a name that will be removed later.
877
+
878
+ **Your `linear.config.json` needs no change.** The `"push"` values under
879
+ `sync.fieldOwnership` — `assignee: "push"`, `description: "push"`,
880
+ `workflowState: "push"` — are a different vocabulary: they name a direction of
881
+ ownership, not a subcommand. They are untouched and still mean what they meant.
882
+ Do not search-and-replace `push` in your config.
883
+
884
+ ### What else is here
885
+
886
+ **One thing here is Linear-specific.** `/spec-start` now pushes to Linear itself,
887
+ right after it commits the spec's move to `in-progress/`. It used to push nothing
888
+ and leave the mirror to the refresh `/spec-next` runs — immediate in `checkout`
889
+ mode, but in `worktree` mode hours away or never. Until it came, the issue sat in
890
+ its old workflow state with nobody assigned while the repo read `in-progress`
891
+ with a developer on it. Expect one more Linear call per `/spec-start`, and expect
892
+ the issue to be current the moment the spec is in flight. Nothing else changes:
893
+ sync is still one-way, and an unlinked spec is still skipped rather than minted.
894
+
895
+ ## `@skitterbyte/skitterspec` v17 → v18 (a spec is built in its own worktree)
896
+
897
+ ### Breaking change
898
+
899
+ **`/spec-start` no longer moves the branch into your checkout.** In `worktree`
900
+ mode it provisions the spec's worktree, does the housekeeping there, opens a
901
+ session in it, and stops. The spec is built where it was provisioned — which is
902
+ what worktrees are for, and why `main` stays free.
903
+
904
+ This **supersedes the "one checkout holds one spec in flight" rule** described
905
+ under v16 → v17 below. That rule was a consequence of moving the branch into the
906
+ primary checkout; with the move gone, so is the restriction:
907
+
908
+ | v17 | v18 |
909
+ |-----|-----|
910
+ | `/spec-start` refused while another spec held your checkout | In `worktree` mode it only requires a **clean tree**. Several specs in flight is what the mode is for. |
911
+ | Starting a spec took two invocations, with a `/spec-live <name>` you typed in between | **One invocation.** No hand-off command, no re-run. |
912
+ | `/spec-live main` was one of the ways out of the gate | The ways out are `/spec-complete` and `/spec-cancel`. Parking to free a workbench is a one-workbench answer, and only `checkout` mode holds one spec now. |
913
+
914
+ **`/spec-live` is for testing only** — reusing your running dev server to reach a
915
+ spec at the canonical URL. It was never meant to be how work gets started, and no
916
+ lifecycle skill calls it.
917
+
918
+ `checkout` mode is unchanged: the branch is built in the primary checkout, the
919
+ gate still requires the workbench free, and `/spec-start` carries straight on
920
+ into phase 1 in the same session.
921
+
922
+ ### What to do
923
+
924
+ 1. **Upgrade** — `npx @skitterbyte/skitterspec update`.
925
+ 2. **Expect a session, not a swap.** After `/spec-start` in `worktree` mode, run
926
+ `/spec-next` from the session it opens in the worktree. `/spec-next` builds
927
+ the spec it is *standing in* and refuses to build one from elsewhere.
928
+ 3. **Nothing to configure.** `spec.companionPaths` in `env.config.json` is new
929
+ and optional — it names paths that belong to a spec alongside its own folder
930
+ (a tracker's per-spec snapshot), so `/spec-start` can commit them together.
931
+ Empty by default; `/spec-linear-setup` sets it for you.
932
+
933
+ ## `@skitterbyte/skitterspec-linear` v11 → v12 (a spec is built in its own worktree)
934
+
935
+ The same change as `@skitterbyte/skitterspec` v17 → v18 above — this
936
+ distribution composes the same lifecycle skills. Read that entry; nothing here
937
+ is Linear-specific.
938
+
939
+ ## `@skitterbyte/skitterspec` v16 → v17 (`/spec-go` splits in two)
940
+
941
+ ### Breaking change
942
+
943
+ **`/spec-go` is removed.** It did two jobs — set an environment up, and build a
944
+ phase — and the seam between them is where the worktree hand-off hurt: you ran
945
+ the same command twice, once to provision and once, from another session, to
946
+ build. They are now two commands with one job each:
947
+
948
+ | Removed | Use instead |
949
+ |---------|-------------|
950
+ | `/spec-go <name>` (first run — start a spec) | **`/spec-start <name>`** — puts the spec in flight on this checkout, moves it to `in-progress`, then builds phase 1. |
951
+ | `/spec-go` (later runs — build the next phase) | **`/spec-next`** — builds the next phase of whichever spec is in flight. Re-run it per phase. |
952
+ | `/spec-go --here` | **Nothing — `/spec-start` *is* here.** It puts the branch in the checkout you are in, which is what the flag was reaching for. |
953
+
954
+ **One checkout holds one spec in flight.** `/spec-start` refuses while another
955
+ spec holds your checkout, naming it and the three ways to free the workbench
956
+ (`/spec-complete`, `/spec-cancel`, or `/spec-live main` to park it). It will not
957
+ stash, commit or switch on your behalf — moving unfinished work is a decision,
958
+ not a side effect.
959
+
960
+ **`/spec-next` refuses when nothing is in flight** rather than guessing a spec
961
+ from the conversation. It writes real code; a wrong guess produces commits on a
962
+ branch nobody asked for.
963
+
964
+ ### What to do
965
+
966
+ 1. **Upgrade** — `npx @skitterbyte/skitterspec update` removes the retired
967
+ `/spec-go` skill and installs the two replacements. A `/spec-go` you edited
968
+ yourself is kept with a warning rather than deleted; remove it by hand.
969
+ 2. **Retrain the muscle memory** — `/spec-start <name>` to begin, `/spec-next` to
970
+ carry on, unchanged `/spec-complete` to finish.
971
+ 3. **Providers**: the `spec-go-start` seam is now **`spec-next-start`**. A
972
+ provider distribution must rename its fragment file to match, or the build
973
+ fails on an orphaned seam.
974
+
975
+ ## `@skitterbyte/skitterspec-linear` v10 → v11 (`/spec-go` splits in two)
976
+
977
+ The same change as `@skitterbyte/skitterspec` v16 → v17 above — this
978
+ distribution composes the same lifecycle skills, so `/spec-go` is removed here
979
+ too and replaced by `/spec-start` + `/spec-next`. Read that entry; nothing here
980
+ is Linear-specific. (Backfilled: the base entry was written when the change
981
+ landed and this one was missed.)
982
+
983
+ ## `@skitterbyte/skitterspec-linear` v9 → v10 (`push` validates your issue states)
984
+
985
+ **`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
986
+ already existed on `spec-sync status --workspace-states` — but it was advisory,
987
+ and skipping it sent a state name Linear **silently ignores**: the description
988
+ lands, the issue never moves, and nothing errors. The base
989
+ `@skitterbyte/skitterspec` is unaffected.
990
+
991
+ ### Breaking change
992
+
993
+ | | v9 | v10 |
994
+ |---|-----|-----|
995
+ | `spec-sync push <spec>` | runs | **exits 1** unless `--workspace-states <file>` or `--skip-state-check` is passed |
996
+ | A configured state absent from the workspace | pushed, silently no-op | **exits 1**, naming the workspace's real states |
997
+
998
+ `/spec-push` handles this for you — it fetches the workspace's issue
999
+ 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.
1000
+
1001
+ ### What to do
1002
+
1003
+ 1. **Using `/spec-push`?** Nothing. Run `update` and carry on.
1004
+ 2. **Calling `spec-sync push` from CI or a script?** Supply the workspace's issue
1005
+ workflow-state names as a JSON array and pass the file:
1006
+
1007
+ ```sh
1008
+ # names come from your Linear workspace, e.g. via the MCP server or the API
1009
+ echo '["Backlog","Todo","In Progress","Done","Canceled"]' > states.json
1010
+ skitterspec spec-sync push my-spec --workspace-states states.json --json
1011
+ ```
1012
+
1013
+ Or opt out deliberately with `--skip-state-check`. Don't reach for it to get
1014
+ past a *failing* check — that check is the only thing standing between you and
1015
+ a push that moves nothing.
1016
+ 3. **If the check refuses,** it tells you what your workspace actually has and,
1017
+ where the lifecycle bucket makes it unambiguous, which name to use:
1018
+
1019
+ ```
1020
+ spec-sync push: refusing — configured state name(s) not in the workspace
1021
+
1022
+ states.complete: "Done" is not an issue state in this workspace
1023
+ use "Completed" instead
1024
+
1025
+ available: Backlog, Todo, In Progress, Completed, Canceled
1026
+ ```
1027
+
1028
+ Fix `specs/.core/linear.config.json` → `states`. `/spec-push` will offer to
1029
+ apply the fix for you.
1030
+
1031
+ ### Also in v10 (not breaking)
1032
+
1033
+ - **A pre-9.0 mirror is detected before it can be orphaned.** A spec still
1034
+ carrying `linear_project_id` / `linear_milestone_id` reads as unlinked to v9+,
1035
+ so `push` would emit an all-creates plan and abandon the live mirror. The plan
1036
+ now carries a `legacy` field naming how many objects that would strand, and
1037
+ `/spec-push` stops. **If you skipped the v8 → v9 migration below, read it now** —
1038
+ this is the guard that catches you, not a substitute for it.
1039
+ - **`update` says what it skipped** — each `customized (kept)` file now reports
1040
+ `+added −removed`, with `--diff` to see the upstream changes you declined.
1041
+ - **This guide now ships inside the package** (it wasn't in the published tarball
1042
+ before v10 — `files` listed only `bin`/`src`/`assets`).
1043
+
1044
+ ## `@skitterbyte/skitterspec-linear` v8 → v9 (a spec is an Issue, phases are sub-issues)
1045
+
1046
+ **v9 remaps the Linear mirror.** A spec is now a Linear **issue** (not a Project),
1047
+ each phase a **sub-issue** (not a Milestone), and **tasks are no longer synced**
1048
+ (they stay in the repo phase files). This collapses a large spec from ~1 project +
1049
+ N milestones + dozens of task-issues down to **one issue + one sub-issue per phase**. The base `@skitterbyte/skitterspec` is unaffected (still v15).
1050
+
1051
+ ### Breaking changes
1052
+
1053
+ | Area | v8 | v9 |
1054
+ |------|-----|-----|
1055
+ | `linear.config.json` → `mapping` | `{specFolder:"project", phases:"milestone", tasks:"issue"}` | `{specFolder:"issue", phases:"subissue", tasks:"none"}` |
1056
+ | `linear.config.json` → `linear` | `initiativeId` | `projectId` (the project picker's default) |
1057
+ | `linear.config.json` → `states` | Linear **Project** statuses (e.g. `Completed`) | Linear **issue** workflow states (e.g. `Done`) |
1058
+ | `linear.config.json` → `sync.fieldOwnership` | `{description, milestones, tasks, workflowState}` | `{description, subIssues, workflowState}` |
1059
+ | Phase frontmatter | `linear_milestone_id` | `linear_issue_id` (the sub-issue id) |
1060
+ | Overview frontmatter | `linear_project_id` + `linear_identifier` | `linear_identifier` (the spec issue) |
1061
+ | Last-pushed snapshot | `{project, milestones, issues}` | `{issue, subIssues}` |
1062
+
1063
+ ### What to do
1064
+
1065
+ 1. **Upgrade and re-run `update`:** `npx @skitterbyte/skitterspec-linear update`.
1066
+ It refreshes the skills, the `linear.config.md` / `SETUP.md` docs, and the
1067
+ config example.
1068
+ 2. **Edit `specs/.core/linear.config.json`** to the new keys above (or delete it
1069
+ and re-copy `linear.config.json.example`). Point `states` at your workspace's
1070
+ **issue** states; set `linear.projectId` if most specs belong to one Project —
1071
+ it pre-selects the picker's default rather than fixing every spec there.
1072
+ 3. **Optionally add `intake`** to start specs from issues someone else filed:
1073
+
1074
+ ```jsonc
1075
+ "intake": {
1076
+ "label": "web-app", // the inbox `/spec --from-issue` browses
1077
+ "bugLabels": ["bug"] // issues with these route to /spec-bug
1078
+ }
1079
+ ```
1080
+
1081
+ Without it, `/spec SKI-123` still adopts an issue by id; only the browsable
1082
+ inbox and the bug routing need the labels.
1083
+ 4. **Existing pushed specs:** the snapshot format changed, so the first
1084
+ `/spec-push` after upgrading **re-creates** the mirror (a fresh issue +
1085
+ sub-issues). Delete any stale `specs/.core/linear-base/*.base.json` and the old
1086
+ `linear_project_id` / `linear_milestone_id` frontmatter first. If you were
1087
+ pre-first-push, there's nothing to reconcile.
1088
+ 5. **Task-level issues** created under v8 are no longer managed by the sync —
1089
+ close or repurpose them in Linear by hand.
1090
+
1091
+ ## `@skitterbyte/skitterspec-linear` v7 → v8 (sync goes one-way; `/spec-pull` removed)
1092
+
1093
+ **v8 made sync one-way.** The repo became the sole source of truth and Linear a
1094
+ **generated mirror**: content is pushed up, never read back or merged. The
1095
+ three-way merge engine and everything that fed it were retired.
1096
+
1097
+ ### Breaking changes
1098
+
1099
+ | Area | v7 | v8 |
1100
+ |------|-----|-----|
1101
+ | Skills | `/spec-status`, `/spec-push`, **`/spec-pull`** | `/spec-status`, `/spec-push` |
1102
+ | `spec-sync` subcommands | `normalize`, `push`, `status`, **`pull`** | `normalize`, `push`, `status`, **`record`** |
1103
+ | Sidecar | a three-way merge base | the **last-pushed snapshot** (`record` writes it) |
1104
+ | `sync.fieldOwnership` | `pull` / `both` values were load-bearing | still parsed; nothing is pulled |
1105
+
1106
+ ### What to do
1107
+
1108
+ 1. **Drop `/spec-pull` from any workflow that calls it.** There is no
1109
+ replacement: editing the mirror in Linear is no longer an input. A person
1110
+ editing the issue will see it overwritten by the next push.
1111
+ 2. **Replace `spec-sync pull` in scripts with `spec-sync record`** — it writes the
1112
+ snapshot from the current repo files after a push is applied.
1113
+ 3. **Delete stale merge-base sidecars** under `sync.baseDir`; the first push after
1114
+ upgrading writes the new snapshot format.
1115
+
1116
+ ## `@skitterbyte/skitterspec-linear` v1 → v7 (no breaking changes)
1117
+
1118
+ Every major in this range was a **routine version bump**, not a breaking
1119
+ contract. The skills (`/spec-status`, `/spec-push`, `/spec-pull`) and the
1120
+ `spec-sync` subcommands (`normalize`, `push`, `pull`, `status`) were identical at
1121
+ v1 and at v7. Upgrading anywhere inside this range needs **no action** beyond
1122
+ re-running `update`.
1123
+
1124
+ One thing did change quietly, at **v4**: `sync.fieldOwnership` lost its
1125
+ `milestones`, `phaseBodies`, `acceptanceCriteria` and `taskBreakdown` entries when
1126
+ that detail moved inside `description`. A config still listing them does not
1127
+ error — unknown keys merge in and *join the compared set* — so remove them if
1128
+ you have them, or they will be compared against fields that no longer exist.
1129
+
1130
+ ## `@skitterbyte/skitterspec` v3 → v16 (no breaking changes)
1131
+
1132
+ **Nothing in this range requires action.** Thirteen majors sounds like thirteen
1133
+ migrations; it was one habit. Every release in this period was cut as a major
1134
+ bump regardless of size (see `RELEASING.md`), and the base package's contract
1135
+ never broke: **no skill was ever removed and no CLI flag was ever removed** — the
1136
+ surface only grew. The spec folder layout
1137
+ (`.core`/`backlog`/`in-progress`/`complete`/`cancelled`) is unchanged throughout.
1138
+
1139
+ The `feat(sync)!` commits that appear in this window changed
1140
+ `@skitterbyte/skitterspec-linear`, which ships separately; the base was bumped
1141
+ alongside it in lockstep. If you are on the **superset**, read the provider
1142
+ entries above — those are the ones with work in them.
1143
+
1144
+ What each major actually added, so you can see what you gain by upgrading:
1145
+
1146
+ | Major | What landed |
1147
+ |-------|-------------|
1148
+ | v4 | `setup` commands bootstrap a fresh worktree's dependencies |
1149
+ | v5, v6 | version bumps only |
1150
+ | v7 | release docs refreshed; stale scripts dropped |
1151
+ | v8 | version bump only |
1152
+ | v9 | released alongside the provider's Linear body round-trip |
1153
+ | v10, v11 | version bumps only |
1154
+ | v12 | install manifest + `update --resync` / `--reset`; **`/spec-live`** overlay |
1155
+ | v13 | **`/spec-hotfix`**, **`/spec-to-main`**, `spec-env prune` for orphaned test DBs |
1156
+ | v14 | **Impact map** in the spec templates; live-aware `/spec-go`; the docs site |
1157
+ | v15 | released alongside the provider's one-way sync switch |
1158
+ | v16 | spec `Name:` handle; `/spec-complete` · `/spec-cancel` commit their own edits |
1159
+
1160
+ **Spec files written under an older version still read.** The template grew
1161
+ (the Impact map at v14, the `Name:` header at v16) but the lifecycle skills treat
1162
+ both as optional — `/spec-review` adds them if you want them.
1163
+
1164
+ ## `@skitterbyte/skitterspec` v2 → v3 (slimmer surface + local traffic diversion)
1165
+
1166
+ **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.)
1167
+
1168
+ ### Removed skills (breaking) → where they went
1169
+
1170
+ | Removed skill | Replaced by |
1171
+ |---------------|-------------|
1172
+ | `/spec-env` | **Automatic in `/spec-go`** — it provisions the worktree and (with your OK) starts the spec's dev servers. Escalate Docker later with the CLI: `skitterspec spec-env up <name>`. |
1173
+ | `/spec-env-down` | **Folded into `/spec-complete` and `/spec-cancel`** — they tear the environment down (dev servers, worktree, stack, slot) as part of finishing/abandoning a spec. |
1174
+ | `/spec-ready` | **Folded into `/spec`** — grilling now writes a `Ready` spec directly (or `Draft` if you deliberately leave open questions). Go straight to `/spec-go`. |
1175
+
1176
+ The **`skitterspec spec-env` CLI engine stays** (`up`, `down`, `dev`, `connect`,
1177
+ `integrate`, `status`, `resolve`) — only the three *skills* were removed. Anything
1178
+ that scripted those CLI verbs keeps working.
1179
+
1180
+ ### New — `/spec-connect` and two config blocks
1181
+
1182
+ - **`/spec-connect <name>`** points your canonical `localhost` ports at a spec's
1183
+ running dev servers (so you can test a worktree's UI/API at the normal URL);
1184
+ `/spec-connect main` hands the ports back. It's a small bundled Node reverse
1185
+ proxy — no external install. Exclusive: one spec exposed at a time.
1186
+ - **`env.config.json` gains `dev` and `proxy` blocks.** `dev` lists the host dev
1187
+ servers `/spec-go` starts (`{ name, command, portVar, health?, frontPort? }`);
1188
+ `proxy` configures the front-door proxy (`{ enabled, host }`). Both default to
1189
+ off/empty, so existing projects are unaffected until you fill `dev` in.
1190
+
1191
+ ### What to do
1192
+
1193
+ 1. **Upgrade and re-run `init`** (or `update`): `npx @skitterbyte/skitterspec
1194
+ update`. It stops installing the three removed skills, installs `/spec-connect`,
1195
+ and refreshes the CLAUDE.md section + `spec-planning` rule. Your specs and
1196
+ `env.config.json` are untouched.
1197
+ 2. **Remove muscle memory for the old commands** — use `/spec-go` to bring a spec
1198
+ up, `/spec-complete`/`/spec-cancel` to tear it down, and `/spec` (no separate
1199
+ `/spec-ready`) to reach a Ready spec.
1200
+ 3. **To test UI/API worktrees:** add a `dev` block to `env.config.json` (see
1201
+ `specs/.core/env.config.md`), then `/spec-go` → `/spec-connect <name>`.
1202
+
1203
+ ## `@skitterbyte/skitterspec` v1 → v2 (tracker-free base)
1204
+
1205
+ **v2 of the base package is tracker-free.** The Linear sync feature — the
1206
+ `/spec-status`, `/spec-push` skills, the `spec-sync` CLI, the
1207
+ Linear-aware steps of `/spec` and `/spec-go`, and the `linear.config.*`
1208
+ templates — moved out of `@skitterbyte/skitterspec` into a separate **superset**
1209
+ distribution, `@skitterbyte/skitterspec-linear`. You now install exactly one:
1210
+
1211
+ | If you… | Install |
1212
+ |---------|---------|
1213
+ | don't sync specs to a tracker | `@skitterbyte/skitterspec` (v2) |
1214
+ | use (or want) Linear sync | `@skitterbyte/skitterspec-linear` |
1215
+
1216
+ Everything else — the spec lifecycle and per-spec isolation — is unchanged and
1217
+ present in **both**.
1218
+
1219
+ ### If you did NOT use Linear sync
1220
+
1221
+ Nothing to do. Upgrade to v2 and re-run `init` (or `update`) as usual. The base
1222
+ never installed the Linear skills for you, so there's nothing to remove.
1223
+
1224
+ ### If you DID use Linear sync
1225
+
1226
+ Switching is one install plus a re-`init`:
1227
+
1228
+ 1. **Install the superset** (in place of the base):
1229
+
1230
+ ```sh
1231
+ npm rm @skitterbyte/skitterspec # if it was a dependency
1232
+ npx @skitterbyte/skitterspec-linear init
1233
+ ```
1234
+
1235
+ 2. **Re-run `init`.** It re-installs the shared skills (now composed with the
1236
+ Linear steps) and the three sync skills, and re-scaffolds the config
1237
+ templates. Your existing files are preserved — `init` never overwrites without
1238
+ `--force`.
1239
+
1240
+ 3. **Your config is unchanged.** The live config path is still
1241
+ `specs/.core/linear.config.json`, and the committed base sidecars under
1242
+ `specs/.core/linear-base/` are read as-is. No re-linking, no re-sync.
1243
+
1244
+ That's it — `/spec-status`, `/spec-push`, and `skitterspec-linear
1245
+ spec-sync …` work exactly as before.
1246
+
1247
+ ### One config note — branch naming
1248
+
1249
+ Embedding the Linear identifier in a worktree branch name is now configured in the
1250
+ **isolation** config, not the Linear config. In `specs/.core/env.config.json` set:
1251
+
1252
+ ```jsonc
1253
+ "branch": { "pattern": "{identifier}-{slug}", "identifierField": "linear_identifier" }
1254
+ ```
1255
+
1256
+ If you don't need the id in branch names, leave the default `{type}/{slug}` — the
1257
+ old implicit Linear-branch behaviour is off unless you opt in this way. (This is
1258
+ the only behavioural change beyond the package split.)
1259
+
1260
+ ## Why the split
1261
+
1262
+ The base couldn't ship without a specific tracker's fingerprints baked into shared
1263
+ skills and a `src/sync/` engine. Extracting the provider makes the base a clean,
1264
+ tracker-free workflow and lets a new provider (e.g. Jira) ship as another superset
1265
+ over the same base — without re-patching the base. See
1266
+ `specs/complete/feat-extract-ticketing-provider/` for the full rationale.