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