@skitterbyte/skitterspec-linear 10.0.0 → 10.0.1

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.
package/MIGRATION.md CHANGED
@@ -1,5 +1,68 @@
1
1
  # Migration guide
2
2
 
3
+ ## `@skitterbyte/skitterspec-linear` v9 → v10 (`push` validates your issue states)
4
+
5
+ **`spec-sync push` now refuses to run until the configured `states` names have
6
+ been checked against your Linear workspace.** The check itself is not new — it
7
+ already existed on `spec-sync status --workspace-states` — but it was advisory,
8
+ and skipping it sent a state name Linear **silently ignores**: the description
9
+ lands, the issue never moves, and nothing errors. The base
10
+ `@skitterbyte/skitterspec` is unaffected.
11
+
12
+ ### Breaking change
13
+
14
+ | | v9 | v10 |
15
+ |---|-----|-----|
16
+ | `spec-sync push <spec>` | runs | **exits 1** unless `--workspace-states <file>` or `--skip-state-check` is passed |
17
+ | A configured state absent from the workspace | pushed, silently no-op | **exits 1**, naming the workspace's real states |
18
+
19
+ `/spec-push` handles this for you — it fetches the workspace's issue
20
+ workflow-state names over MCP and passes them on. **Nothing changes if you drive
21
+ sync through the skill.** Only a direct CLI caller needs updating.
22
+
23
+ ### What to do
24
+
25
+ 1. **Using `/spec-push`?** Nothing. Run `update` and carry on.
26
+ 2. **Calling `spec-sync push` from CI or a script?** Supply the workspace's issue
27
+ workflow-state names as a JSON array and pass the file:
28
+
29
+ ```sh
30
+ # names come from your Linear workspace, e.g. via the MCP server or the API
31
+ echo '["Backlog","Todo","In Progress","Done","Canceled"]' > states.json
32
+ skitterspec spec-sync push my-spec --workspace-states states.json --json
33
+ ```
34
+
35
+ Or opt out deliberately with `--skip-state-check`. Don't reach for it to get
36
+ past a *failing* check — that check is the only thing standing between you and
37
+ a push that moves nothing.
38
+ 3. **If the check refuses,** it tells you what your workspace actually has and,
39
+ where the lifecycle bucket makes it unambiguous, which name to use:
40
+
41
+ ```
42
+ spec-sync push: refusing — configured state name(s) not in the workspace
43
+
44
+ states.complete: "Done" is not an issue state in this workspace
45
+ use "Completed" instead
46
+
47
+ available: Backlog, Todo, In Progress, Completed, Canceled
48
+ ```
49
+
50
+ Fix `specs/.core/linear.config.json` → `states`. `/spec-push` will offer to
51
+ apply the fix for you.
52
+
53
+ ### Also in v10 (not breaking)
54
+
55
+ - **A pre-9.0 mirror is detected before it can be orphaned.** A spec still
56
+ carrying `linear_project_id` / `linear_milestone_id` reads as unlinked to v9+,
57
+ so `push` would emit an all-creates plan and abandon the live mirror. The plan
58
+ now carries a `legacy` field naming how many objects that would strand, and
59
+ `/spec-push` stops. **If you skipped the v8 → v9 migration below, read it now** —
60
+ this is the guard that catches you, not a substitute for it.
61
+ - **`update` says what it skipped** — each `customized (kept)` file now reports
62
+ `+added −removed`, with `--diff` to see the upstream changes you declined.
63
+ - **This guide now ships inside the package** (it wasn't in the published tarball
64
+ before v10 — `files` listed only `bin`/`src`/`assets`).
65
+
3
66
  ## `@skitterbyte/skitterspec-linear` v8 → v9 (a spec is an Issue, phases are sub-issues)
4
67
 
5
68
  **v9 remaps the Linear mirror.** A spec is now a Linear **issue** (not a Project),
@@ -48,6 +111,79 @@ phase**. The base `@skitterbyte/skitterspec` is unaffected (still v15).
48
111
  5. **Task-level issues** created under v8 are no longer managed by the sync —
49
112
  close or repurpose them in Linear by hand.
50
113
 
114
+ ## `@skitterbyte/skitterspec-linear` v7 → v8 (sync goes one-way; `/spec-pull` removed)
115
+
116
+ **v8 made sync one-way.** The repo became the sole source of truth and Linear a
117
+ **generated mirror**: content is pushed up, never read back or merged. The
118
+ three-way merge engine and everything that fed it were retired.
119
+
120
+ ### Breaking changes
121
+
122
+ | Area | v7 | v8 |
123
+ |------|-----|-----|
124
+ | Skills | `/spec-status`, `/spec-push`, **`/spec-pull`** | `/spec-status`, `/spec-push` |
125
+ | `spec-sync` subcommands | `normalize`, `push`, `status`, **`pull`** | `normalize`, `push`, `status`, **`record`** |
126
+ | Sidecar | a three-way merge base | the **last-pushed snapshot** (`record` writes it) |
127
+ | `sync.fieldOwnership` | `pull` / `both` values were load-bearing | still parsed; nothing is pulled |
128
+
129
+ ### What to do
130
+
131
+ 1. **Drop `/spec-pull` from any workflow that calls it.** There is no
132
+ replacement: editing the mirror in Linear is no longer an input. A person
133
+ editing the issue will see it overwritten by the next push.
134
+ 2. **Replace `spec-sync pull` in scripts with `spec-sync record`** — it writes the
135
+ snapshot from the current repo files after a push is applied.
136
+ 3. **Delete stale merge-base sidecars** under `sync.baseDir`; the first push after
137
+ upgrading writes the new snapshot format.
138
+
139
+ ## `@skitterbyte/skitterspec-linear` v1 → v7 (no breaking changes)
140
+
141
+ Every major in this range was a **routine version bump**, not a breaking
142
+ contract. The skills (`/spec-status`, `/spec-push`, `/spec-pull`) and the
143
+ `spec-sync` subcommands (`normalize`, `push`, `pull`, `status`) were identical at
144
+ v1 and at v7. Upgrading anywhere inside this range needs **no action** beyond
145
+ re-running `update`.
146
+
147
+ One thing did change quietly, at **v4**: `sync.fieldOwnership` lost its
148
+ `milestones`, `phaseBodies`, `acceptanceCriteria` and `taskBreakdown` entries when
149
+ that detail moved inside `description`. A config still listing them does not
150
+ error — unknown keys merge in and *join the compared set* — so remove them if
151
+ you have them, or they will be compared against fields that no longer exist.
152
+
153
+ ## `@skitterbyte/skitterspec` v3 → v16 (no breaking changes)
154
+
155
+ **Nothing in this range requires action.** Thirteen majors sounds like thirteen
156
+ migrations; it was one habit. Every release in this period was cut as a major
157
+ bump regardless of size (see `RELEASING.md`), and the base package's contract
158
+ never broke: **no skill was ever removed and no CLI flag was ever removed** — the
159
+ surface only grew. The spec folder layout
160
+ (`.core`/`backlog`/`in-progress`/`complete`/`cancelled`) is unchanged throughout.
161
+
162
+ The `feat(sync)!` commits that appear in this window changed
163
+ `@skitterbyte/skitterspec-linear`, which ships separately; the base was bumped
164
+ alongside it in lockstep. If you are on the **superset**, read the provider
165
+ entries above — those are the ones with work in them.
166
+
167
+ What each major actually added, so you can see what you gain by upgrading:
168
+
169
+ | Major | What landed |
170
+ |-------|-------------|
171
+ | v4 | `setup` commands bootstrap a fresh worktree's dependencies |
172
+ | v5, v6 | version bumps only |
173
+ | v7 | release docs refreshed; stale scripts dropped |
174
+ | v8 | version bump only |
175
+ | v9 | released alongside the provider's Linear body round-trip |
176
+ | v10, v11 | version bumps only |
177
+ | v12 | install manifest + `update --resync` / `--reset`; **`/spec-live`** overlay |
178
+ | v13 | **`/spec-hotfix`**, **`/spec-to-main`**, `spec-env prune` for orphaned test DBs |
179
+ | v14 | **Impact map** in the spec templates; live-aware `/spec-go`; the docs site |
180
+ | v15 | released alongside the provider's one-way sync switch |
181
+ | v16 | spec `Name:` handle; `/spec-complete` · `/spec-cancel` commit their own edits |
182
+
183
+ **Spec files written under an older version still read.** The template grew
184
+ (the Impact map at v14, the `Name:` header at v16) but the lifecycle skills treat
185
+ both as optional — `/spec-review` adds them if you want them.
186
+
51
187
  ## `@skitterbyte/skitterspec` v2 → v3 (slimmer surface + local traffic diversion)
52
188
 
53
189
  **v3 shrinks the everyday command surface to five verbs — `spec → go → connect →
@@ -14,16 +14,25 @@ team) that the config reference (`linear.config.md`) assumes you already have.
14
14
 
15
15
  ---
16
16
 
17
- ## Upgrading from 8.x
18
-
19
- **v9 remapped the mirror.** A spec is now an **issue** (was a Project), a phase a
20
- **sub-issue** (was a Milestone), and tasks are no longer objects. The frontmatter
21
- keys moved with it, so a spec linked under 8.x reads as **unlinked** to v9 — the
22
- next `/spec-push` would mint a fresh mirror and abandon the old one.
23
-
24
- `spec-sync push` detects this and refuses to let the plan be applied blind, but
25
- read **`MIGRATION.md`** ("v8 v9", shipped with the package) before upgrading a
26
- repo with a live mirror. Fresh installs can skip this section.
17
+ ## Upgrading an existing install
18
+
19
+ Fresh installs can skip this section. **`MIGRATION.md` ships with the package**
20
+ read the entry for the version you are coming from before upgrading a repo with a
21
+ live mirror.
22
+
23
+ **From 9.x — `push` now validates your issue states.** `spec-sync push` refuses
24
+ to run until the configured `states` names have been checked against the
25
+ workspace. `/spec-push` does that for you, so nothing changes if you drive sync
26
+ through the skill; a script calling the CLI directly must pass
27
+ `--workspace-states <file>` (or `--skip-state-check`). See `MIGRATION.md`
28
+ → "v9 → v10".
29
+
30
+ **From 8.x — the mirror was remapped.** A spec is now an **issue** (was a
31
+ Project), a phase a **sub-issue** (was a Milestone), and tasks are no longer
32
+ objects. The frontmatter keys moved with it, so a spec linked under 8.x reads as
33
+ **unlinked** — the next `/spec-push` would mint a fresh mirror and abandon the old
34
+ one. `push` detects this and refuses to let the plan be applied blind, but read
35
+ `MIGRATION.md` → "v8 → v9" first.
27
36
 
28
37
  ## 1. Install the package
29
38
 
@@ -46,6 +46,13 @@ absence). A `sync.fieldOwnership` value outside `both|pull|push` is a hard error
46
46
  // that sub-issue: "checklist" mirrors them into its description (default),
47
47
  // "none" leaves the description as the phase's Goal line alone. Either way no
48
48
  // issue is created per task. These are the defaults.
49
+ //
50
+ // Under "checklist" the mirror keeps the phase file's OWN section headings: a
51
+ // phase with `## Tasks` and `## Acceptance` arrives as two headed sections, in
52
+ // source order, each heading reproduced as written. Checkboxes written before
53
+ // any heading appear under `## Tasks`. A heading with no checkboxes under it
54
+ // is not mirrored. Nesting, sub-bullets and inline formatting are preserved;
55
+ // a legacy inline `(KEY-123)` on a task line is stripped.
49
56
  "mapping": {
50
57
  "specFolder": "issue",
51
58
  "phases": "subissue",
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@skitterbyte/skitterspec-linear",
3
- "version": "10.0.0",
3
+ "version": "10.0.1",
4
4
  "description": "Spec-driven development for Claude Code, with one-way Linear sync — a superset of @skitterbyte/skitterspec: the base filesystem workflow plus /spec-status · /spec-push and the spec-sync CLI. The repo is canonical; Linear is a generated mirror. Install this OR the base, not both.",
5
5
  "keywords": [
6
6
  "claude",
@@ -319,6 +319,46 @@ function parseTaskLine(line) {
319
319
  return { id, text, done }
320
320
  }
321
321
 
322
+ /**
323
+ * Split a phase's task blocks into the `##` sections they were written under.
324
+ *
325
+ * The checklist used to be one flat list under a hardcoded `## Tasks`, so a
326
+ * criterion written under `## Acceptance` arrived in the mirror as an ordinary
327
+ * open task. Nothing was lost — it was just unreadable.
328
+ *
329
+ * Grouping is done by MAPPING blocks onto headings, not by re-parsing the body
330
+ * section by section: `findTaskBlocks` tracks open task subtrees across a
331
+ * continuous body, and slicing that body at every heading would change what a
332
+ * block claims at a section boundary. A heading inside a fence is not a heading
333
+ * (`fenceMask`), and the phase file's own `#` H1 is not a section.
334
+ *
335
+ * @returns {Array<{heading:string|null, tasks:string[]}>} in source order;
336
+ * `heading` is null for blocks that precede every heading, and a heading with
337
+ * no blocks under it never appears.
338
+ */
339
+ function groupTasksByHeading(lines, blocks, renderTask) {
340
+ const inFence = fenceMask(lines)
341
+ const headings = []
342
+ for (let i = 0; i < lines.length; i++) {
343
+ if (inFence[i]) continue
344
+ const m = /^(#{2,6})\s+(.*\S)\s*$/.exec(lines[i])
345
+ if (m) headings.push({ line: i, heading: `${m[1]} ${m[2]}` })
346
+ }
347
+
348
+ const groups = []
349
+ for (const b of blocks) {
350
+ let heading = null
351
+ for (const h of headings) {
352
+ if (h.line >= b.start) break
353
+ heading = h.heading
354
+ }
355
+ const last = groups[groups.length - 1]
356
+ if (last && last.heading === heading) last.tasks.push(renderTask(b))
357
+ else groups.push({ heading, tasks: [renderTask(b)] })
358
+ }
359
+ return groups
360
+ }
361
+
322
362
  // Read the phase files (01-*.md, 02-*.md …) in execution order. Each yields its
323
363
  // linked milestone id (from optional frontmatter), title, goal and tasks.
324
364
  function readPhaseFiles(snapshotDir) {
@@ -347,7 +387,8 @@ function readPhaseFiles(snapshotDir) {
347
387
  // the marker its author wrote, and any inline `(KEY-123)` stamped on a legacy task
348
388
  // line stripped — those ids were per-task issues we no longer create, and
349
389
  // they read as noise in the mirror.
350
- const tasks = findTaskBlocks(body.split('\n')).map((b) => {
390
+ const lines = body.split('\n')
391
+ const renderTask = (b) => {
351
392
  // `findTaskBlocks` also returns the plain sub-bullets written underneath
352
393
  // a task. They carry no checkbox, so they render as the bullet their
353
394
  // author used — emitting `- [ ]` here would invent a task that does not
@@ -355,7 +396,10 @@ function readPhaseFiles(snapshotDir) {
355
396
  const parsed = parseTaskLine(`[${b.checkbox ? b.mark : ' '}] ${b.text}`)
356
397
  const text = parsed ? parsed.text : b.text
357
398
  return b.checkbox ? `${b.indent}- [${b.mark}] ${text}` : `${b.indent}${b.marker} ${text}`
358
- })
399
+ }
400
+ const blocks = findTaskBlocks(lines)
401
+ const tasks = blocks.map(renderTask)
402
+ const taskGroups = groupTasksByHeading(lines, blocks, renderTask)
359
403
  return {
360
404
  phase: file.replace(/\.md$/, ''),
361
405
  file,
@@ -370,6 +414,7 @@ function readPhaseFiles(snapshotDir) {
370
414
  emoji: headingEmoji(body),
371
415
  statusLine: phaseStatusLine(body),
372
416
  tasks,
417
+ taskGroups,
373
418
  }
374
419
  })
375
420
  }
@@ -497,7 +542,14 @@ function subIssueBody(phase, tasksMode) {
497
542
  if (tasksMode !== 'checklist' || !phase.tasks.length) return phase.goal
498
543
  const parts = []
499
544
  if (phase.goal) parts.push(phase.goal, '')
500
- parts.push('## Tasks', '', ...phase.tasks)
545
+ // One section per source heading, in source order. Checkboxes written before
546
+ // any heading keep the `## Tasks` default, so a phase file with a single task
547
+ // section — every one in this repo's corpus but two — projects unchanged.
548
+ const groups = phase.taskGroups && phase.taskGroups.length ? phase.taskGroups : [{ heading: null, tasks: phase.tasks }]
549
+ groups.forEach((group, i) => {
550
+ if (i) parts.push('')
551
+ parts.push(group.heading || '## Tasks', '', ...group.tasks)
552
+ })
501
553
  return parts.join('\n')
502
554
  }
503
555