@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 +136 -0
- package/assets/core/SETUP.md +19 -10
- package/assets/core/linear.config.md +7 -0
- package/package.json +1 -1
- package/src/vendor/sync-core/src/normalize.js +55 -3
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 →
|
package/assets/core/SETUP.md
CHANGED
|
@@ -14,16 +14,25 @@ team) that the config reference (`linear.config.md`) assumes you already have.
|
|
|
14
14
|
|
|
15
15
|
---
|
|
16
16
|
|
|
17
|
-
## Upgrading
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
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.
|
|
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
|
|
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
|
-
|
|
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
|
|