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