@skitterbyte/skitterspec-linear 14.0.0 → 17.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 (49) hide show
  1. package/MIGRATION.md +304 -4
  2. package/README.md +34 -1
  3. package/assets/claude-md-section.md +29 -18
  4. package/assets/commands/spec-remote-review.md +22 -0
  5. package/assets/core/SETUP.md +10 -6
  6. package/assets/core/env.config.json.example +4 -2
  7. package/assets/core/env.config.md +103 -25
  8. package/assets/core/linear.config.json.example +2 -1
  9. package/assets/core/linear.config.md +49 -22
  10. package/assets/review/page.html +1101 -108
  11. package/assets/rules/spec-planning.md +39 -7
  12. package/assets/rules/spec-reports.md +210 -31
  13. package/assets/skills/spec/SKILL.md +161 -4
  14. package/assets/skills/spec-bug/SKILL.md +102 -49
  15. package/assets/skills/spec-cancel/SKILL.md +2 -2
  16. package/assets/skills/spec-claim/SKILL.md +12 -4
  17. package/assets/skills/spec-complete/SKILL.md +2 -2
  18. package/assets/skills/spec-diff/SKILL.md +183 -39
  19. package/assets/skills/spec-hotfix/SKILL.md +96 -49
  20. package/assets/skills/spec-init/SKILL.md +18 -6
  21. package/assets/skills/spec-linear-setup/SKILL.md +19 -11
  22. package/assets/skills/spec-next/SKILL.md +147 -62
  23. package/assets/skills/spec-push/SKILL.md +45 -0
  24. package/assets/skills/spec-review/SKILL.md +89 -2
  25. package/assets/skills/spec-reviewed/SKILL.md +31 -5
  26. package/assets/skills/spec-start/SKILL.md +26 -3
  27. package/assets/skills/spec-status/SKILL.md +20 -6
  28. package/assets/skills/spec-sync/SKILL.md +1 -0
  29. package/package.json +2 -2
  30. package/src/cli.js +940 -116
  31. package/src/env/classify.js +87 -2
  32. package/src/env/config.js +214 -17
  33. package/src/env/hooks.js +49 -9
  34. package/src/env/live.js +94 -0
  35. package/src/env/resolve.js +36 -2
  36. package/src/env/review.js +581 -21
  37. package/src/env/serve.js +298 -19
  38. package/src/env/supervise.js +8 -1
  39. package/src/init.js +88 -13
  40. package/src/vendor/linear/api.js +111 -2
  41. package/src/vendor/linear/cli-sync.js +661 -11
  42. package/src/vendor/linear/config.js +41 -13
  43. package/src/vendor/linear/doctor.js +6 -5
  44. package/src/vendor/sync-core/index.js +11 -3
  45. package/src/vendor/sync-core/src/compare.js +65 -0
  46. package/src/vendor/sync-core/src/normalize.js +26 -0
  47. package/src/vendor/sync-core/src/retarget.js +1 -1
  48. package/src/vendor/sync-core/src/task-block.js +2 -2
  49. /package/assets/hooks/{review-gate.js → review-gate.cjs} +0 -0
@@ -21,8 +21,27 @@ port block, and no `.env`.
21
21
  exactly as it does today.
22
22
 
23
23
  The loader (`src/env/config.js` → `loadEnvConfig`) merges your file over the
24
- frozen defaults below and returns `{ config, present }`; `present:false` means
25
- no live `env.config.json` was found.
24
+ frozen defaults below and returns `{ config, present, unknown }`;
25
+ `present:false` means no live `env.config.json` was found.
26
+
27
+ **A key not listed below is ignored — and says so.** Every `spec-env` command
28
+ prints one advisory line per unrecognised key, top-level or nested:
29
+
30
+ ```
31
+ spec-env: env.config.json — unknown key "docker.portbase" is ignored.
32
+ ```
33
+
34
+ It is advisory in the strongest sense: the key is dropped exactly as it always
35
+ was, nothing refuses, and the exit status is unchanged. It exists because the
36
+ two things an unknown key can be — a deliberate forward-compat entry and a typo
37
+ — are indistinguishable from here, and only one of them is a mistake you would
38
+ want to hear about. A mis-typed `review.required` leaves the commit gate on and
39
+ a mis-typed `teardown.deleteRemoteBranch` reverts to `prompt`; before this, the
40
+ only signal either gave was that nothing happened.
41
+
42
+ A **known** key whose value is rejected — `mode: "Checkout"`,
43
+ `teardown.deleteRemoteBranch: "yes"` — is not reported here. Each of those falls
44
+ through to a documented conservative default; see the field notes below.
26
45
 
27
46
  ## Fields
28
47
 
@@ -74,8 +93,9 @@ no live `env.config.json` was found.
74
93
  // `spec-env up`, right after `git worktree add` and BEFORE `setup` runs — so a
75
94
  // fresh linked worktree (which starts with none of the repo's gitignored files)
76
95
  // has the .env / local secret overrides / local config that setup steps and
77
- // git hooks depend on. Without this a step like `prisma generate` hard-fails in
78
- // the new worktree because .env (its datasource URL) isn't there.
96
+ // git hooks depend on. Without this any setup step that reads .env a schema
97
+ // or client generator, a codegen pass hard-fails in the new worktree
98
+ // because the file it expects isn't there.
79
99
  // mode "symlink" (default) points the worktree file at the main file, so it
80
100
  // stays in sync; "copy" makes an independent copy.
81
101
  // files repo-relative paths to seed. A source absent in main is a printed
@@ -221,13 +241,15 @@ no live `env.config.json` was found.
221
241
 
222
242
  // Reading a spec's diff (`spec-env review`, `/spec-diff`).
223
243
  //
224
- // `reader` decides how the page's LOCATION IS WORDED, and through
225
- // `serveOnRemote` belowwhether the engine stands its local server up so a
226
- // remote reader gets a link that opens. It never decides to PUBLISH. Three
227
- // values:
228
- // "local" — you are at the machine holding the page; a file:// URL opens.
229
- // "remote" — you are not; it does not, so the page is served instead.
230
- // "detect" work it out (the default).
244
+ // `reader` decides how the page's LOCATION IS WORDED. It no longer decides
245
+ // the bind`allowNetwork` does, see below and it no longer decides which
246
+ // tiers are offered, because every tier is now listed whatever it says.
247
+ // WHAT THE SERVER
248
+ // BINDS TO and nothing else. It never decides whether to serve (`serve`
249
+ // does) and never decides to PUBLISH. Three values:
250
+ // "local" you are at the machine holding the page; it binds 127.0.0.1.
251
+ // "remote" — you are not; it binds every interface so the page opens.
252
+ // "detect" — work it out (the default); cannot-tell binds 127.0.0.1.
231
253
  // An explicit "local"/"remote" is BELIEVED WITHOUT SNIFFING: you know where
232
254
  // you are reading, and no signal outranks being told. Detection is only the
233
255
  // default, and it has three outcomes rather than two — local, remote, and
@@ -237,25 +259,81 @@ no live `env.config.json` was found.
237
259
  // unrecognised value falls through to "detect", so a typo cannot become a
238
260
  // confident answer. Default: detect.
239
261
  //
240
- // `servePort` is the default port for `spec-env review serve`, which renders
241
- // every spec's diff per request on one local server. `--port` overrides it per
242
- // run. The server binds 127.0.0.1 unless `--host 0.0.0.0` is passed, which
243
- // mints an unguessable path token and prints the LAN URL including it —
244
- // anyone holding that URL can read every spec's diff while it runs.
245
- // Default: 7777.
262
+ // `servePort` is the port for `spec-env review serve`, which renders every
263
+ // spec's diff per request on one local server. Two forms:
264
+ //
265
+ // "auto" (the default) derive it from this repo's path, as
266
+ // 7700 + hash(realpath(repoRoot)) % 100. Two repos on one machine stop
267
+ // competing for one shared port without anyone configuring anything, and
268
+ // — the part that matters — THE SAME REPO GETS THE SAME PORT EVERY TIME.
269
+ // The derivation reads nothing on disk, so the port survives a restart, a
270
+ // reboot and a `--stop`, which is what lets a link handed out yesterday
271
+ // still resolve. A symlinked spelling of the tree resolves first, so one
272
+ // repo never lands on two ports.
273
+ // <a number> — pin it. An explicit number always wins, and pinning is what
274
+ // you do when you want a port you can memorise, or when two repos derive
275
+ // the same one.
246
276
  //
247
- // `serveOnRemote` is whether a "remote" reader may have that server started
248
- // FOR them. On (the default) the engine brings it up, binds 0.0.0.0, and puts
249
- // the served URL on the `open:` line best-guess network address first, the
250
- // rest listed under it, because the guess reads interface names and a VPN or
251
- // an unusual adapter will fool it. Off, you get the file:// URL with its
252
- // "will not open where you are reading" marker and the command to type.
277
+ // A derived port CAN still collide — a hundred slots is a small chance, not
278
+ // no chance and the server refuses rather than moving itself aside. The fix
279
+ // it names is `servePort`, because that is what the next link is built from;
280
+ // `--port` moves one run and leaves every link already handed out pointing at
281
+ // the busy port. `spec-env review serve --status` prints the port and which
282
+ // of the three chose it.
283
+ //
284
+ // An unrecognised value falls through to "auto", like every other typed key
285
+ // here. Default: "auto".
286
+ //
287
+ // The server binds 127.0.0.1 unless `--host 0.0.0.0` is passed, which mints
288
+ // an unguessable path token and prints the LAN URL including it — anyone
289
+ // holding that URL can read every spec's diff while it runs.
290
+ //
291
+ // `serve` is whether a render stands the local server up at all:
292
+ // "always" — every render does (the default), so `local:` and `network:`
293
+ // are both http URLs the page can POST a verdict back to. The
294
+ // LAN address comes first with the rest listed under it, because
295
+ // the guess reads interface names and a VPN or an unusual
296
+ // adapter will fool it.
297
+ // "never" — `local:` is a file:// URL instead. Note what that costs: a
298
+ // file:// page has no server to POST to, so its verdict buttons
299
+ // copy a command for you to paste rather than sending anything.
300
+ // It replaced `serveOnRemote`, which gated serving on the reader — and so
301
+ // handed a local machine a page it could read and not answer. A legacy
302
+ // `serveOnRemote: false` is still read as `serve: "never"`.
253
303
  // Either way NOTHING IS PUBLISHED on a detection: a server is one process
254
304
  // ended by one flag, while a published page is one this tooling cannot
255
305
  // remove, so that half stays an explicit ask. Teardown names a server that
256
306
  // served the last spec, and `spec-env prune` reaps a pidfile whose process is
257
307
  // gone. Default: true.
258
308
  //
309
+ // `allowNetwork` and `allowRemote` decide WHICH TIERS a render offers, and
310
+ // they replaced the engine guessing where the reader was sitting. It guessed
311
+ // for a while and got it wrong three separate ways in one day: a file:// page
312
+ // on a session detected `unknown`, a LAN URL for a phone that had left the
313
+ // network, and an address that changed underneath a reader mid-session. So
314
+ // every tier is listed, labelled, and either a URL or the one command that
315
+ // turns it on — and you pick the one that reaches you.
316
+ //
317
+ // `allowNetwork` — whether the review server binds EVERY INTERFACE, so the
318
+ // page opens on your phone, or loopback only. THIS IS WHAT
319
+ // CHOOSES THE BIND; `reader` no longer does. Default: true,
320
+ // which matches what the engine already did.
321
+ // `allowRemote` — whether PUBLISHING is permitted at all. It permits it; it
322
+ // publishes nothing. Default: FALSE, because a published
323
+ // page is one skitterspec cannot delete, so it must never
324
+ // happen unasked.
325
+ //
326
+ // `local` and `network` are two doors into ONE ROOM — the page POSTs to
327
+ // `location.pathname`, so both reach the same server and the same waiting
328
+ // verdict, and one wait covers both. `remote` is a second store: a verdict
329
+ // pressed on a published page needs `/spec-reviewed`, because nothing pushes
330
+ // from an artifact's store into a conversation.
331
+ //
332
+ // Both are toggled by `spec-env review allow <tier> --set [on|off]`, where an
333
+ // empty value toggles — which is what `/spec-remote-review` runs. It writes
334
+ // THIS FILE in the primary checkout, so it changes for everyone who pulls and
335
+ // leaves that tree dirty; the engine says so when it does.
336
+ //
259
337
  // `commitWith` names the skill a COMMITTING verdict hands off to. A review
260
338
  // page ends in a verdict — commit, commit & continue, request changes,
261
339
  // discuss — and the point of the first two is that the commit follows from
@@ -288,8 +366,8 @@ no live `env.config.json` was found.
288
366
  // place rather than quietly disabling it. Default: true.
289
367
  "review": {
290
368
  "reader": "detect",
291
- "servePort": 7777,
292
- "serveOnRemote": true,
369
+ "servePort": "auto",
370
+ "serve": "always",
293
371
  "commitWith": "/commit",
294
372
  "required": true
295
373
  }
@@ -41,7 +41,8 @@
41
41
  "fieldOwnership": {
42
42
  "description": "push",
43
43
  "subIssues": "push",
44
- "workflowState": "push"
44
+ "workflowState": "push",
45
+ "assignee": "push"
45
46
  },
46
47
  "localOnlySections": ["State log", "Changelog", "Open questions"],
47
48
  "keyedFields": {}
@@ -19,7 +19,9 @@ your team ID (and an optional grouping project).
19
19
  The loader (`src/sync/config.js` → `loadLinearConfig`) merges your file over the
20
20
  frozen defaults below and returns `{ config, present }`; `present:false` means no
21
21
  live `linear.config.json` was found (the opt-in gate — it never throws on
22
- absence). A `sync.fieldOwnership` value outside `both|pull|push` is a hard error.
22
+ absence). A `sync.fieldOwnership` value outside `both|pull|push|none` is a hard error.
23
+ `none` is how a repo **declines** a field the defaults own — see
24
+ **Assignment** below, which is the field that needs it.
23
25
 
24
26
  ## Fields
25
27
 
@@ -40,7 +42,10 @@ absence). A `sync.fieldOwnership` value outside `both|pull|push` is a hard error
40
42
  "intake": {
41
43
  "label": "", // inbox filter — the label the web app files under
42
44
  "bugLabels": [], // e.g. ["bug"] — these route to /spec-bug instead
43
- "hotfixLabels": [] // e.g. ["production"] — these route to /spec-hotfix
45
+ "hotfixLabels": [], // e.g. ["production"] — these route to /spec-hotfix
46
+ "preserveOriginal": true // keep the reporter's description as a comment
47
+ // before the linking push replaces it. ON by
48
+ // default; false declines.
44
49
  },
45
50
 
46
51
  // How a spec's parts map onto Linear objects: a spec is an Issue, each phase a
@@ -119,7 +124,8 @@ absence). A `sync.fieldOwnership` value outside `both|pull|push` is a hard error
119
124
  "fieldOwnership": {
120
125
  "description": "push",
121
126
  "subIssues": "push",
122
- "workflowState": "push"
127
+ "workflowState": "push",
128
+ "assignee": "push"
123
129
  },
124
130
 
125
131
  // Markdown sections of 00-overview.md that are local-only scaffolding and
@@ -135,37 +141,41 @@ absence). A `sync.fieldOwnership` value outside `both|pull|push` is a hard error
135
141
 
136
142
  ## Assignment (`sync.fieldOwnership.assignee`)
137
143
 
138
- Off by default. Add one key and the spec issue is assigned to whoever is
139
- building it:
144
+ **On by default since v16.** The spec issue is assigned to whoever is building
145
+ it: `/spec-start` records them, and the issue is released when the spec
146
+ completes. To decline it, say so:
140
147
 
141
148
  ```jsonc
142
149
  "fieldOwnership": {
143
- "description": "push",
144
- "subIssues": "push",
145
- "workflowState": "push",
146
- "assignee": "push" // <- the whole opt-in
150
+ "assignee": "none" // <- the whole opt-out
147
151
  }
148
152
  ```
149
153
 
150
154
  It is deliberately **not** a config key of its own. `fieldOwnership` is already
151
155
  the documented extension point — "any key you add joins the pushed projection" —
152
- and assignment is exactly one more field the repo owns.
153
-
154
- - **Absent = inert.** No writes, no prompts, no drift line, and no assignee hash
155
- in any snapshot. A project that never opts in cannot tell the feature exists,
156
- which is why it is missing from `linear.config.json.example`: that file is
157
- copied verbatim into new projects, and shipping the key there would opt
158
- everyone in by default.
156
+ and assignment is exactly one more field the repo owns. `none` is a value rather
157
+ than an omitted key because the map merges **per key** onto the defaults: once a
158
+ field is owned by default there is nothing an absent key can subtract.
159
+
160
+ - **`none` = inert.** No writes, no prompts, no drift line, and no assignee hash
161
+ in any snapshot identical in every respect to a field the config never
162
+ listed. A repo that declines cannot tell the feature exists.
163
+ - **It was opt-in until v16**, and the cost of that was silent in the wrong
164
+ direction: a repo that never added the line looked exactly like one that had
165
+ decided against it, so the only signal was noticing an unassigned issue weeks
166
+ later. Declining is now a decision on the record; forgetting is not a state.
159
167
  - **The bucket decides.** The spec's `linear_assignee_id` is pushed while the
160
168
  spec is live (`backlog`, `in-progress`) and cleared once it reaches `complete`
161
169
  or `cancelled` — so finishing a spec hands the issue back with no unassign
162
170
  step for anyone to remember. The stamp stays in the file, and so does
163
171
  `> **Developer:**`: they record who *actioned* the work, which outlives who is
164
172
  holding it.
165
- - **Unset means don't touch.** A spec that records nobody sends no assignee at
166
- all, so an issue a PM assigned in Linear is never overwritten. Only an assignee
167
- the repo itself pushed is ever cleared and a snapshot written before you
168
- opted in counts as "never pushed", not as "was nobody".
173
+ - **Unset means don't touch, and this is what makes on-by-default safe.** A
174
+ spec that records nobody sends no assignee at all, so an issue a PM assigned in
175
+ Linear is never overwritten. Only an assignee the repo itself pushed is ever
176
+ cleared and a snapshot written before the field was owned counts as "never
177
+ pushed", not as "was nobody". Turning the default on widens who gets
178
+ **assigned**; it cannot widen who gets **unassigned**.
169
179
  - **Only the spec issue.** Phase sub-issues are never assigned: one person builds
170
180
  a spec, and N assigned sub-issues is N notifications for one piece of work.
171
181
  They stay independently assignable in Linear.
@@ -605,8 +615,25 @@ The issue **becomes** the spec's issue: its identifier is stamped as
605
615
  `linear_identifier`, phases become its sub-issues, and the **linking push**
606
616
  — which runs as the spec is created — replaces its description with the spec. The
607
617
  reporter's comments, links and subscribers stay on the one issue everyone is
608
- already watching; their original words are carried into the spec's **Problem**
609
- (or **Symptom**) section, and Linear keeps the original in the issue's history.
618
+ already watching.
619
+
620
+ **What happens to what they wrote.** Two things, and neither is the issue's
621
+ history:
622
+
623
+ - **Adoption posts it back as a comment**, verbatim, before the push replaces
624
+ the description (`skitterspec spec-sync preserve <spec>`, which the adoption
625
+ skills run for you). A comment is outside `sync.fieldOwnership` by design —
626
+ Linear-native triage that one-way sync never touches — which is exactly what
627
+ makes it safe: no later push can clobber it. It carries a marker so a second
628
+ run posts nothing, and it never fails the adoption: an unreachable Linear, an
629
+ issue with no description or `intake.preserveOriginal: false` all exit 0 and
630
+ say so. Set that key to `false` to turn the whole thing off.
631
+ - **The spec quotes them** in its **Problem** (or **Symptom**) section, which
632
+ *is* pushed as part of the generated description — so the reporter's words are
633
+ in the description too, and they round-trip like everything else.
634
+
635
+ The issue's history does technically hold the pre-adoption text, and it is not
636
+ the answer: a diff viewer is not quotable and nobody opens one.
610
637
 
611
638
  An issue already stamped on a spec can't be adopted twice — `skitterspec spec-sync
612
639
  linked` is the list that's checked.