@skitterbyte/skitterspec-linear 9.1.0 → 10.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.
package/MIGRATION.md ADDED
@@ -0,0 +1,156 @@
1
+ # Migration guide
2
+
3
+ ## `@skitterbyte/skitterspec-linear` v8 → v9 (a spec is an Issue, phases are sub-issues)
4
+
5
+ **v9 remaps the Linear mirror.** A spec is now a Linear **issue** (not a Project),
6
+ each phase a **sub-issue** (not a Milestone), and **tasks are no longer synced**
7
+ (they stay in the repo phase files). This collapses a large spec from ~1 project +
8
+ N milestones + dozens of task-issues down to **one issue + one sub-issue per
9
+ phase**. The base `@skitterbyte/skitterspec` is unaffected (still v15).
10
+
11
+ ### Breaking changes
12
+
13
+ | Area | v8 | v9 |
14
+ |------|-----|-----|
15
+ | `linear.config.json` → `mapping` | `{specFolder:"project", phases:"milestone", tasks:"issue"}` | `{specFolder:"issue", phases:"subissue", tasks:"none"}` |
16
+ | `linear.config.json` → `linear` | `initiativeId` | `projectId` (the project picker's default) |
17
+ | `linear.config.json` → `states` | Linear **Project** statuses (e.g. `Completed`) | Linear **issue** workflow states (e.g. `Done`) |
18
+ | `linear.config.json` → `sync.fieldOwnership` | `{description, milestones, tasks, workflowState}` | `{description, subIssues, workflowState}` |
19
+ | Phase frontmatter | `linear_milestone_id` | `linear_issue_id` (the sub-issue id) |
20
+ | Overview frontmatter | `linear_project_id` + `linear_identifier` | `linear_identifier` (the spec issue) |
21
+ | Last-pushed snapshot | `{project, milestones, issues}` | `{issue, subIssues}` |
22
+
23
+ ### What to do
24
+
25
+ 1. **Upgrade and re-run `update`:** `npx @skitterbyte/skitterspec-linear update`.
26
+ It refreshes the skills, the `linear.config.md` / `SETUP.md` docs, and the
27
+ config example.
28
+ 2. **Edit `specs/.core/linear.config.json`** to the new keys above (or delete it
29
+ and re-copy `linear.config.json.example`). Point `states` at your workspace's
30
+ **issue** states; set `linear.projectId` if most specs belong to one Project —
31
+ it pre-selects the picker's default rather than fixing every spec there.
32
+ 3. **Optionally add `intake`** to start specs from issues someone else filed:
33
+
34
+ ```jsonc
35
+ "intake": {
36
+ "label": "web-app", // the inbox `/spec --from-issue` browses
37
+ "bugLabels": ["bug"] // issues with these route to /spec-bug
38
+ }
39
+ ```
40
+
41
+ Without it, `/spec SKI-123` still adopts an issue by id; only the browsable
42
+ inbox and the bug routing need the labels.
43
+ 4. **Existing pushed specs:** the snapshot format changed, so the first
44
+ `/spec-push` after upgrading **re-creates** the mirror (a fresh issue +
45
+ sub-issues). Delete any stale `specs/.core/linear-base/*.base.json` and the old
46
+ `linear_project_id` / `linear_milestone_id` frontmatter first. If you were
47
+ pre-first-push, there's nothing to reconcile.
48
+ 5. **Task-level issues** created under v8 are no longer managed by the sync —
49
+ close or repurpose them in Linear by hand.
50
+
51
+ ## `@skitterbyte/skitterspec` v2 → v3 (slimmer surface + local traffic diversion)
52
+
53
+ **v3 shrinks the everyday command surface to five verbs — `spec → go → connect →
54
+ commit → complete` — by folding provisioning, teardown, and grooming into the
55
+ lifecycle skills, and adds `/spec-connect` for testing a worktree at your normal
56
+ `localhost` URL.** (`@skitterbyte/skitterspec-linear` moves to v2.0.0 in lockstep.)
57
+
58
+ ### Removed skills (breaking) → where they went
59
+
60
+ | Removed skill | Replaced by |
61
+ |---------------|-------------|
62
+ | `/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>`. |
63
+ | `/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. |
64
+ | `/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`. |
65
+
66
+ The **`skitterspec spec-env` CLI engine stays** (`up`, `down`, `dev`, `connect`,
67
+ `integrate`, `status`, `resolve`) — only the three *skills* were removed. Anything
68
+ that scripted those CLI verbs keeps working.
69
+
70
+ ### New — `/spec-connect` and two config blocks
71
+
72
+ - **`/spec-connect <name>`** points your canonical `localhost` ports at a spec's
73
+ running dev servers (so you can test a worktree's UI/API at the normal URL);
74
+ `/spec-connect main` hands the ports back. It's a small bundled Node reverse
75
+ proxy — no external install. Exclusive: one spec exposed at a time.
76
+ - **`env.config.json` gains `dev` and `proxy` blocks.** `dev` lists the host dev
77
+ servers `/spec-go` starts (`{ name, command, portVar, health?, frontPort? }`);
78
+ `proxy` configures the front-door proxy (`{ enabled, host }`). Both default to
79
+ off/empty, so existing projects are unaffected until you fill `dev` in.
80
+
81
+ ### What to do
82
+
83
+ 1. **Upgrade and re-run `init`** (or `update`): `npx @skitterbyte/skitterspec
84
+ update`. It stops installing the three removed skills, installs `/spec-connect`,
85
+ and refreshes the CLAUDE.md section + `spec-planning` rule. Your specs and
86
+ `env.config.json` are untouched.
87
+ 2. **Remove muscle memory for the old commands** — use `/spec-go` to bring a spec
88
+ up, `/spec-complete`/`/spec-cancel` to tear it down, and `/spec` (no separate
89
+ `/spec-ready`) to reach a Ready spec.
90
+ 3. **To test UI/API worktrees:** add a `dev` block to `env.config.json` (see
91
+ `specs/.core/env.config.md`), then `/spec-go` → `/spec-connect <name>`.
92
+
93
+ ## `@skitterbyte/skitterspec` v1 → v2 (tracker-free base)
94
+
95
+ **v2 of the base package is tracker-free.** The Linear sync feature — the
96
+ `/spec-status`, `/spec-push` skills, the `spec-sync` CLI, the
97
+ Linear-aware steps of `/spec` and `/spec-go`, and the `linear.config.*`
98
+ templates — moved out of `@skitterbyte/skitterspec` into a separate **superset**
99
+ distribution, `@skitterbyte/skitterspec-linear`. You now install exactly one:
100
+
101
+ | If you… | Install |
102
+ |---------|---------|
103
+ | don't sync specs to a tracker | `@skitterbyte/skitterspec` (v2) |
104
+ | use (or want) Linear sync | `@skitterbyte/skitterspec-linear` |
105
+
106
+ Everything else — the spec lifecycle and per-spec isolation — is unchanged and
107
+ present in **both**.
108
+
109
+ ### If you did NOT use Linear sync
110
+
111
+ Nothing to do. Upgrade to v2 and re-run `init` (or `update`) as usual. The base
112
+ never installed the Linear skills for you, so there's nothing to remove.
113
+
114
+ ### If you DID use Linear sync
115
+
116
+ Switching is one install plus a re-`init`:
117
+
118
+ 1. **Install the superset** (in place of the base):
119
+
120
+ ```sh
121
+ npm rm @skitterbyte/skitterspec # if it was a dependency
122
+ npx @skitterbyte/skitterspec-linear init
123
+ ```
124
+
125
+ 2. **Re-run `init`.** It re-installs the shared skills (now composed with the
126
+ Linear steps) and the three sync skills, and re-scaffolds the config
127
+ templates. Your existing files are preserved — `init` never overwrites without
128
+ `--force`.
129
+
130
+ 3. **Your config is unchanged.** The live config path is still
131
+ `specs/.core/linear.config.json`, and the committed base sidecars under
132
+ `specs/.core/linear-base/` are read as-is. No re-linking, no re-sync.
133
+
134
+ That's it — `/spec-status`, `/spec-push`, and `skitterspec-linear
135
+ spec-sync …` work exactly as before.
136
+
137
+ ### One config note — branch naming
138
+
139
+ Embedding the Linear identifier in a worktree branch name is now configured in the
140
+ **isolation** config, not the Linear config. In `specs/.core/env.config.json` set:
141
+
142
+ ```jsonc
143
+ "branch": { "pattern": "{identifier}-{slug}", "identifierField": "linear_identifier" }
144
+ ```
145
+
146
+ If you don't need the id in branch names, leave the default `{type}/{slug}` — the
147
+ old implicit Linear-branch behaviour is off unless you opt in this way. (This is
148
+ the only behavioural change beyond the package split.)
149
+
150
+ ## Why the split
151
+
152
+ The base couldn't ship without a specific tracker's fingerprints baked into shared
153
+ skills and a `src/sync/` engine. Extracting the provider makes the base a clean,
154
+ tracker-free workflow and lets a new provider (e.g. Jira) ship as another superset
155
+ over the same base — without re-patching the base. See
156
+ `specs/complete/feat-extract-ticketing-provider/` for the full rationale.
package/README.md CHANGED
@@ -51,17 +51,38 @@ fuller guide):
51
51
  `/spec-status` reports what would push. Sync is **one-way**: the repo is the
52
52
  source of truth and Linear is a generated mirror.
53
53
 
54
+ ## Upgrading
55
+
56
+ ```sh
57
+ npx @skitterbyte/skitterspec-linear update
58
+ ```
59
+
60
+ `update` refreshes the files it manages (skills, rules, `specs/.core` docs) and
61
+ **keeps anything you edited**. A file it kept is listed under
62
+ `customized (kept)` with the change it declined summarised as `+added −removed`:
63
+
64
+ ```
65
+ customized (kept):
66
+ .claude/rules/spec-planning.md +34 −13
67
+ ```
68
+
69
+ Add `--diff` to see those changes as a unified diff before deciding whether to
70
+ re-apply your edits on top, or `--force` to take the package version and lose
71
+ them. Your `specs/` content and live `.core` config are never touched.
72
+
54
73
  ## What the superset adds
55
74
 
56
75
  On top of the base skills (`/spec`, `/spec-go`, isolation, …):
57
76
 
58
77
  - **`/spec-status`** — read-only drift report: what the next push would create /
59
- update, plus any workflow-state drift. Changes nothing.
78
+ update, any workflow-state drift, and any phase whose status signals disagree
79
+ (see **Phase status** below). Changes nothing.
60
80
  - **`/spec-push`** — repo → Linear, one-way. Diffs the spec against a committed
61
81
  last-pushed snapshot and applies only what changed (issue description + state,
62
82
  phase sub-issues), stamping the returned ids back into the spec.
63
83
  - **`spec-sync` CLI** (`skitterspec-linear spec-sync …`) — the deterministic
64
- engine behind the skills, for CI / local runs.
84
+ engine behind the skills, for CI / local runs:
85
+ `normalize` · `push` · `stamp` · `record` · `status` · `linked`.
65
86
 
66
87
  The shared `/spec`, `/spec-bug` and `/spec-go` skills come composed with the
67
88
  Linear steps filled in: `/spec` asks which Linear **Project** the spec belongs to,
@@ -83,8 +104,10 @@ spec, and a smoke test. Per-field docs live in `specs/.core/linear.config.md`.
83
104
  **What pushes:** the spec is one Linear **issue** — the spec body travels as its
84
105
  **`description`**, each phase as a **sub-issue** (phase name → title, `**Goal:**`
85
106
  → description, phase emoji → state), and the spec's lifecycle folder sets the
86
- issue's **workflow state**. Tasks are **not** synced they stay in the repo phase
87
- files. Priority, labels, cycles and comments are **Linear-native triage** — the
107
+ issue's **workflow state**. A phase's **tasks are mirrored** into its sub-issue's
108
+ description as a read-only checklist (`mapping.tasks: "checklist"`, the default;
109
+ `"none"` keeps the Goal line alone) — no issue is created per task, and a box
110
+ ticked in Linear is overwritten by the next push. Priority, labels, cycles and comments are **Linear-native triage** — the
88
111
  PM's to set in Linear; one-way sync neither pushes nor reads them, so they're
89
112
  never clobbered. A workflow-state a teammate moves in Linear is surfaced by
90
113
  `/spec-status` as drift and overwritten on the next push. **Last-pushed snapshots**
@@ -105,6 +128,13 @@ replaces the description with the spec. A bug-labelled issue routes to
105
128
  `/spec-bug`, which adopts it the same way. `skitterspec-linear spec-sync linked`
106
129
  lists what's already adopted, so an issue never becomes two specs.
107
130
 
131
+ **Phase status.** A phase's state in Linear comes from the `⬜`/`🔄`/`✅` on its
132
+ phase-file **heading** — not from its `> **Status:**` line and not from the
133
+ overview's phase-index row, which are the human mirrors of it. A heading carrying
134
+ no emoji reads as *not started*, so `spec-sync normalize|push|status` warn when
135
+ the emoji is missing or when the three disagree, rather than quietly mirroring a
136
+ finished phase as backlog. The warnings never block a push.
137
+
108
138
  Branch naming that embeds the Linear id lives in the isolation config
109
139
  (`env.config.json` → `branch.pattern` with `{identifier}`, `branch.identifierField:
110
140
  "linear_identifier"`), not in `linear.config.json`.
@@ -3,8 +3,9 @@
3
3
  A start-to-finish guide to getting `/spec-status` and `/spec-push` working against
4
4
  a real Linear workspace. Sync is **one-way**: the repo is the source of truth and
5
5
  the linked Linear **issue** is a **generated mirror** — content is pushed up, never
6
- read back or merged. A spec is a Linear issue and each phase a sub-issue; tasks are
7
- not synced. Covers the **Linear side** (connecting the MCP server, finding your
6
+ read back or merged. A spec is a Linear issue and each phase a sub-issue, with the
7
+ phase's tasks mirrored into that sub-issue's description as a read-only
8
+ checklist. Covers the **Linear side** (connecting the MCP server, finding your
8
9
  team) that the config reference (`linear.config.md`) assumes you already have.
9
10
 
10
11
  > The whole feature is **opt-in**: until `specs/.core/linear.config.json` exists,
@@ -13,6 +14,17 @@ team) that the config reference (`linear.config.md`) assumes you already have.
13
14
 
14
15
  ---
15
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.
27
+
16
28
  ## 1. Install the package
17
29
 
18
30
  Install the Linear superset (or, if you already run the base, switch to it — it
@@ -172,10 +184,15 @@ With a linked spec, confirm push end-to-end:
172
184
  this session. Re-run step 2; remember a fresh add needs a Claude Code restart.
173
185
  - **"missing required tools: issueCreate"** — you're on the read-only endpoint
174
186
  (or a restricted API key). Use `https://mcp.linear.app/mcp` for push.
175
- - **A configured state name silently does nothing** — Linear ignores an unknown
176
- issue state. Run `/spec-status` (it validates the `states` names against the
177
- workspace) and fix `linear.config.json` to the real issue-state names
178
- (`Backlog / Todo / In Progress / Done / Canceled`).
187
+ - **"refusing — the configured issue states have not been validated"** — `push`
188
+ requires the workspace's issue-state names (`--workspace-states <file>`), which
189
+ `/spec-push` fetches for you. Run the skill rather than the CLI directly, or
190
+ pass the file yourself.
191
+ - **"refusing — configured state name(s) not in the workspace"** — Linear ignores
192
+ an unknown issue state, so this is caught before the push rather than after.
193
+ Fix `linear.config.json` to the real issue-state names
194
+ (`Backlog / Todo / In Progress / Done / Canceled`). Upgrading from 8.x, the
195
+ value inverts: project status `Completed` → issue state `Done`.
179
196
  - **Reconnecting doesn't switch workspace** — Linear ties the OAuth session to one
180
197
  workspace. Remove and re-add the server to authenticate against another.
181
198
  - **Bold around an inline code span renders oddly in Linear** — Linear moves the
@@ -11,7 +11,7 @@
11
11
  "mapping": {
12
12
  "specFolder": "issue",
13
13
  "phases": "subissue",
14
- "tasks": "none"
14
+ "tasks": "checklist"
15
15
  },
16
16
  "states": {
17
17
  "backlog": "Backlog",
@@ -3,7 +3,8 @@
3
3
  Opt-in config for the Linear sync (`/spec-status`, `/spec-push`, and the
4
4
  Linear-aware paths of `/spec` and `/spec-go`). Sync is **one-way**: the repo is
5
5
  the source of truth and the Linear **issue** is a **generated mirror**. A spec is
6
- a Linear issue and each phase a sub-issue; tasks are not synced. Content is
6
+ a Linear issue and each phase a sub-issue; a phase's tasks ride along inside
7
+ that sub-issue's description as a read-only checklist. Content is
7
8
  pushed up and never read back or merged — `/spec-push` diffs the spec against a
8
9
  committed **last-pushed snapshot** and applies only what changed; `/spec-status`
9
10
  is a read-only drift report. The `sync.fieldOwnership` map now just selects the
@@ -41,16 +42,24 @@ absence). A `sync.fieldOwnership` value outside `both|pull|push` is a hard error
41
42
  },
42
43
 
43
44
  // How a spec's parts map onto Linear objects: a spec is an Issue, each phase a
44
- // sub-issue (a child issue), tasks are not synced. These are the defaults.
45
+ // sub-issue (a child issue). `tasks` selects how a phase's checkboxes reach
46
+ // that sub-issue: "checklist" mirrors them into its description (default),
47
+ // "none" leaves the description as the phase's Goal line alone. Either way no
48
+ // issue is created per task. These are the defaults.
45
49
  "mapping": {
46
50
  "specFolder": "issue",
47
51
  "phases": "subissue",
48
- "tasks": "none"
52
+ "tasks": "checklist"
49
53
  },
50
54
 
51
55
  // Map the spec's lifecycle bucket → the Linear ISSUE workflow-state name. Used
52
56
  // for the spec issue's state (from its folder) AND each sub-issue's state (from
53
- // the phase emoji). Names must match the workspace's issue states exactly.
57
+ // the phase emoji). Names must match the workspace's issue states exactly
58
+ // Linear silently IGNORES an unknown state, so a typo pushes clean and the
59
+ // issue never moves. `/spec-push` fetches the workspace's names and `push`
60
+ // refuses to run without them, so a wrong name here fails loudly rather than
61
+ // quietly. (Upgrading from 8.x? The right value inverts: the project status
62
+ // `Completed` becomes the issue state `Done`.)
54
63
  "states": {
55
64
  "backlog": "Backlog",
56
65
  "in-progress": "In Progress",
@@ -112,8 +121,18 @@ recreates:
112
121
  - **Phases → sub-issues.** Each phase file maps to a child issue (`parentId` = the
113
122
  spec issue). The link id lives in the phase file's frontmatter
114
123
  (`linear_issue_id`); its title ← the phase h1, its description ← the phase
115
- `**Goal:**` line, its state ← the phase heading emoji (⬜/🔄/✅).
116
- - **Tasks are not synced.** Task checkboxes stay in the repo phase files only.
124
+ `**Goal:**` line plus its task checklist (see below), its state ← the phase
125
+ heading emoji (⬜/🔄/✅).
126
+ - **Tasks are mirrored, not synced.** With `mapping.tasks: "checklist"` (the
127
+ default) a phase's checkboxes are rendered into its sub-issue's description as
128
+ a markdown checklist — nesting and `[x]` state preserved, any legacy inline
129
+ `(KEY-123)` stripped. No issue is created per task and nothing is read back, so
130
+ a box ticked in Linear is overwritten by the next push. Set `"none"` to keep
131
+ the description as the Goal line alone.
132
+
133
+ Turning this on (or off) changes every sub-issue's description, so the first
134
+ `/spec-push` afterwards reports every already-linked sub-issue as an update.
135
+ That is expected — they update in place and no duplicates are minted.
117
136
 
118
137
  Unlinked local items (a spec with no `linear_identifier`, a phase with no
119
138
  `linear_issue_id`) are created in Linear on the next `/spec-push`, which stamps
@@ -289,7 +289,8 @@ below (nothing to do here without one).
289
289
  If it's absent, skip this phase entirely — the spec stays local-only and `/spec`
290
290
  behaves exactly as above. When present, after writing the spec, link it to Linear
291
291
  so status and discussion live there while the repo stays the co-authoring surface.
292
- A spec is a Linear **issue**; each phase is a **sub-issue**; tasks are not synced:
292
+ A spec is a Linear **issue**; each phase is a **sub-issue**, carrying that
293
+ phase's tasks in its description as a read-only checklist:
293
294
 
294
295
  - **Discover the Linear MCP tools at runtime** (don't hardcode names). If Linear
295
296
  isn't connected/authed, relay the fix and stop — leave the spec written and
@@ -301,19 +302,20 @@ A spec is a Linear **issue**; each phase is a **sub-issue**; tasks are not synce
301
302
  - **Create a sub-issue per phase** (the `mapping.phases` target — `subissue` by
302
303
  default): a child issue with `parentId` = the spec issue, named from each phase
303
304
  file, in execution order.
304
- - **Add the frontmatter block** to `00-overview.md` (above the `#` title) so the
305
- spec is linkable:
306
-
307
- ```yaml
308
- ---
309
- linear_identifier: "<TEAM-123>"
310
- linear_url: "https://linear.app/..."
311
- last_synced_at: "<ISO-8601 now>"
312
- ---
305
+ - **Stamp the ids** so the spec is linkable one call, no hand-edited
306
+ frontmatter:
307
+
308
+ ```
309
+ skitterspec spec-sync stamp <spec> \
310
+ --issue TEAM-123 --url https://linear.app/… \
311
+ --sub 01-<slug>=TEAM-124 --sub 02-<slug>=TEAM-125
313
312
  ```
314
313
 
315
- Stamp each phase file's `linear_issue_id` with its sub-issue id. (Status is not
316
- stored in frontmatter it comes from the spec's lifecycle folder.)
314
+ It writes `linear_identifier`/`linear_url` onto `00-overview.md` and each phase
315
+ file's `linear_issue_id`, validating every ref and id **before** touching a
316
+ file — on any problem it changes nothing and exits non-zero, so a typo can't
317
+ leave the spec pointing at an issue that isn't there. (Status is not stored in
318
+ frontmatter — it comes from the spec's lifecycle folder.)
317
319
  - **Write the initial base sidecar** so the spec starts clean and non-diverged —
318
320
  run `skitterspec spec-sync record <spec>` to capture the local snapshot as the
319
321
  committed base (`sync.baseDir`). `/spec-status` should report in-sync right after.
@@ -1,13 +1,14 @@
1
1
  ---
2
2
  name: spec-push
3
- description: Push a spec up to its linked Linear issue (repo → Linear, one-way). The repo is the source of truth; Linear is a generated mirror. A spec is a Linear issue and each phase a sub-issue; tasks are not synced. Runs `skitterspec spec-sync push` to get a create/update plan, applies it over MCP (issue description/state, phase sub-issues), stamps the returned ids back into the spec, then records the snapshot. Never reads Linear content back. Opt-in — needs specs/.core/linear.config.json. Use when the user says "/spec-push", "push to Linear", "update the Linear issue from this spec".
3
+ description: Push a spec up to its linked Linear issue (repo → Linear, one-way). The repo is the source of truth; Linear is a generated mirror. A spec is a Linear issue and each phase a sub-issue, with the phase's tasks mirrored read-only into that sub-issue's description. Runs `skitterspec spec-sync push` to get a create/update plan, applies it over MCP (issue description/state, phase sub-issues), stamps the returned ids back into the spec, then records the snapshot. Never reads Linear content back. Opt-in — needs specs/.core/linear.config.json. Use when the user says "/spec-push", "push to Linear", "update the Linear issue from this spec".
4
4
  ---
5
5
 
6
6
  # /spec-push — send a spec up to Linear (one-way)
7
7
 
8
8
  Repo → Linear. The repo is the **source of truth**; Linear is a **generated
9
9
  mirror**. A spec is a Linear **issue**; each phase is a **sub-issue** (a child
10
- issue). Tasks are not synced they live only in the repo phase files. This skill
10
+ issue), and a phase's tasks are mirrored into that sub-issue's description as a
11
+ read-only checklist — never as issues of their own. This skill
11
12
  computes what changed since the last push and applies it — it never reads Linear
12
13
  content back or merges. A person editing the mirror in Linear will see it
13
14
  overwritten on the next push.
@@ -19,10 +20,36 @@ tell the user how to enable Linear sync and stop.
19
20
 
20
21
  Use the argument, else the spec in context; ask if unclear.
21
22
 
22
- ## 2. Get the plan from the engine
23
+ ## 2. Connect, and validate the issue states
24
+
25
+ Discover the issue **read + create/update** tools at runtime (`get_issue`,
26
+ `save_issue` — a single upsert covers create and update), plus the **project
27
+ list** tool if this push will mint the spec issue (see the picker below — it is
28
+ optional; without it the picker is skipped, not failed). If Linear isn't
29
+ connected or a needed tool is missing, relay the fix and stop, **writing
30
+ nothing**.
31
+
32
+ Then fetch the workspace's issue workflow-state **names** and write them to a
33
+ file as a JSON array (e.g. `["Backlog","In Progress","Done","Canceled"]`). Step 3
34
+ requires that file: `push` **refuses to run** without it, because Linear silently
35
+ ignores an unknown issue state — the description lands, the issue never moves,
36
+ and nothing errors. If the check reports a name that isn't in the workspace, stop
37
+ and fix `specs/.core/linear.config.json`.
38
+
39
+ **If the check refuses, offer to fix it.** The refusal lists every configured
40
+ name the workspace lacks, the workspace's real state names, and — where the
41
+ bucket makes it unambiguous — which one to use instead. Relay that, then offer to
42
+ apply it to `specs/.core/linear.config.json` → `states`, and do so on the user's
43
+ confirmation. Never edit their config without asking, and never guess a bucket
44
+ the refusal made no suggestion for — ask which state they want.
45
+
46
+ `--skip-state-check` exists for the deliberate exception; do not reach for it to
47
+ get past a failing check.
48
+
49
+ ## 3. Get the plan from the engine
23
50
 
24
51
  ```
25
- skitterspec spec-sync push <spec> --json
52
+ skitterspec spec-sync push <spec> --workspace-states <file> --json
26
53
  ```
27
54
 
28
55
  The engine prints a JSON **plan** (no network, no remote read):
@@ -42,19 +69,15 @@ date — say so and stop. `state` values are local buckets
42
69
  (`backlog`/`in-progress`/`complete`/`cancelled`); map each to the Linear
43
70
  issue-state NAME via `config.states` at apply time.
44
71
 
45
- ## 3. Discover the Linear MCP tools
72
+ ### Stop if the plan reports a pre-9.0 mirror
46
73
 
47
- Discover the issue **read + create/update** tools at runtime (`get_issue`,
48
- `save_issue` a single upsert covers create and update), plus the **project
49
- list** tool if this push will mint the spec issue (see the picker below — it is
50
- optional; without it the picker is skipped, not failed). If Linear isn't
51
- connected or a needed tool is missing, relay the fix and stop, **writing
52
- nothing**.
53
-
54
- **Validate the issue states first.** Fetch the workspace's issue workflow-state
55
- names and run `skitterspec spec-sync status <spec> --workspace-states <file>`; if
56
- it errors (a configured `states` name isn't in the workspace), stop and fix the
57
- config — Linear silently ignores an unknown issue state.
74
+ If the plan carries a **`legacy`** field, this spec was linked under the pre-9.0
75
+ model (`linear_project_id` / `linear_milestone_id`). v9 reads the new keys, finds
76
+ none, and the plan above is therefore **all-creates** applying it mints a fresh
77
+ mirror and **abandons** the existing one. **Stop.** Relay `legacy.keys`,
78
+ `legacy.files` and `legacy.orphanCount` ("this would orphan N live objects"),
79
+ point at `MIGRATION.md` → "v8 → v9", and apply nothing until the user has
80
+ migrated or explicitly confirms they want a new mirror.
58
81
 
59
82
  ## 4. Apply the plan (order matters)
60
83
 
@@ -62,22 +85,37 @@ config — Linear silently ignores an unknown issue state.
62
85
  **mints** it: run the picker in **Picking the Linear Project** below, then
63
86
  create it with `save_issue` (`team` = `linear.teamId`; `project` = the picked
64
87
  id; `title` from the spec title; `description` from `plan.issue.description`;
65
- state from `plan.issue.state` via `config.states`). Stamp the returned
66
- identifier into `00-overview.md` frontmatter as `linear_identifier` (and
67
- `linear_url`). If it already exists and `plan.issue` is present, **update it by
68
- id and send no `project`** — its placement is Linear's from then on.
88
+ state from `plan.issue.state` via `config.states`). Keep the returned
89
+ identifier and url for step 5 — don't hand-edit frontmatter. If it already
90
+ exists and `plan.issue` is present, **update it by id and send no `project`**
91
+ — its placement is Linear's from then on.
69
92
  2. **Sub-issues create** → for each, `save_issue` with `parentId` = the spec
70
- issue id (`name` → title, `goal` → description, `state` via `config.states`);
71
- stamp the returned id into its phase file as `linear_issue_id` (`ref` is the
72
- phase-file basename).
93
+ issue id (`name` → title, `goal` → description, `state` via `config.states`).
94
+ Keep each returned id against its `ref` (the phase-file basename).
73
95
  3. **Sub-issues update** → `save_issue` by `id` (title/description/state).
74
96
 
75
97
  Priority, labels, cycles and comments are Linear-native triage — do **not** push
76
98
  them; they're the PM's.
77
99
 
78
- ## 5. Record the snapshot
100
+ ## 5. Stamp the ids, then record the snapshot
101
+
102
+ Write every id you collected back into the spec in **one** call — the engine
103
+ does the file edits, so there is no hand-editing of frontmatter:
104
+
105
+ ```
106
+ skitterspec spec-sync stamp <spec> \
107
+ --issue SKI-11 --url https://linear.app/… \
108
+ --sub 01-outbox=SKI-12 --sub 02-api=SKI-13
109
+ ```
110
+
111
+ Pass `--issue`/`--url` only on the push that minted the spec issue; pass one
112
+ `--sub <ref>=<id>` for every sub-issue **created** in step 4.2 (updates already
113
+ have their id). It validates every ref and id **before** writing anything and
114
+ exits non-zero having changed nothing if any is wrong — so a typo can't leave the
115
+ spec half-stamped, pointing at an issue that isn't there. Fix what it reports and
116
+ re-run; it is safe to repeat.
79
117
 
80
- After everything applied and the ids are stamped into the files:
118
+ Then record what was pushed:
81
119
 
82
120
  ```
83
121
  skitterspec spec-sync record <spec>
@@ -60,6 +60,19 @@ reading the code, do that instead of asking.
60
60
  creating a `0N-<slug>.md` file **and** a matching overview index row, or drop a
61
61
  dead phase by removing both. Keep the index and files in sync; **preserve
62
62
  completed `[x]` history**.
63
+ - **A phase's status is the `⬜`/`🔄`/`✅` on its H1** — `# Phase 2 — <goal> 🔄`.
64
+ It is mirrored in that file's `> **Status:**` line and in the matching
65
+ `00-overview.md` phase-index row, but the **heading is the authoritative one**:
66
+ tooling reads it, and a heading carrying no emoji reads as *not started*. So a
67
+ finished phase marked only by a `**Status:** done` line is silently wrong
68
+ everywhere except the file itself. Set all three together.
69
+ - **Especially when you create phase files.** Migrating a legacy spec (a bare
70
+ `<name>.md`, or a `00-overview.md` with inline phases) into the folder +
71
+ phase-file form means authoring `0N-<slug>.md` headings from scratch — carry
72
+ each phase's real status onto its H1 as you split it out, rather than
73
+ defaulting every new file to `⬜`. A ticketing provider projects that emoji as
74
+ the phase's state in the tracker, so getting it wrong mirrors a
75
+ complete phase as not-started.
63
76
  - Tick tasks already satisfied by the code; re-open `## Open questions` for
64
77
  anything still undecided.
65
78
  - Add a dated **Changelog** entry summarising the review (e.g. `- <date> —
@@ -15,7 +15,11 @@ const { specSanitise } = require('../src/vendor/linear/cli-sanitise.js')
15
15
  async function main(argv) {
16
16
  const [cmd, ...rest] = argv
17
17
  if (cmd === 'spec-sync') {
18
- await specSync(rest)
18
+ // Propagate the exit code, like spec-sanitise below. Dropping it made
19
+ // `status --workspace-states` (a bad state name) and `stamp` (a refused
20
+ // write) both look successful to any caller checking $?, which is exactly
21
+ // what the /spec-push skill does before it applies a plan.
22
+ process.exitCode = await specSync(rest)
19
23
  return
20
24
  }
21
25
  if (cmd === 'spec-sanitise') {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@skitterbyte/skitterspec-linear",
3
- "version": "9.1.0",
3
+ "version": "10.0.0",
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",
@@ -21,7 +21,8 @@
21
21
  "files": [
22
22
  "bin",
23
23
  "src",
24
- "assets"
24
+ "assets",
25
+ "MIGRATION.md"
25
26
  ],
26
27
  "engines": {
27
28
  "node": ">=18"
package/src/cli.js CHANGED
@@ -79,6 +79,8 @@ Options (init / update):
79
79
  --reset (init) Start again: reset managed scaffolding fresh
80
80
  (needs --yes; never touches your specs or config)
81
81
  --force Overwrite skill/rule/script files that already exist
82
+ --diff (update) Show the upstream changes each customized
83
+ file declined, as a unified diff
82
84
  --dir <path> Target project dir (default: positional arg or cwd)
83
85
  --no-claude-md Skip creating/patching CLAUDE.md
84
86
  --yes, -y Accept defaults; skip the interactive setup prompts
@@ -104,6 +106,7 @@ function parse(argv) {
104
106
  removeReleaseTooling: false,
105
107
  resync: false,
106
108
  reset: false,
109
+ diff: false,
107
110
  }
108
111
  const positional = []
109
112
  for (let i = 0; i < argv.length; i++) {
@@ -116,6 +119,7 @@ function parse(argv) {
116
119
  else if (a === '--remove-release-tooling') opts.removeReleaseTooling = true
117
120
  else if (a === '--resync') opts.resync = true
118
121
  else if (a === '--reset') opts.reset = true
122
+ else if (a === '--diff') opts.diff = true
119
123
  else if (a === '--dir') opts.dir = argv[++i]
120
124
  else if (a.startsWith('--')) throw new Error(`unknown option: ${a}`)
121
125
  else positional.push(a)
@@ -1346,7 +1350,7 @@ async function run(argv) {
1346
1350
  break
1347
1351
  }
1348
1352
  if (action === 'resync') {
1349
- resync(dir, { claudeMd: opts.claudeMd, force: opts.force })
1353
+ resync(dir, { claudeMd: opts.claudeMd, force: opts.force, diff: opts.diff })
1350
1354
  break
1351
1355
  }
1352
1356
  // action === 'create-missing' → fall through to a normal (skip-existing) init.
@@ -1365,7 +1369,7 @@ async function run(argv) {
1365
1369
  case 'update':
1366
1370
  // `update` is a resync — refresh managed files, keep customized ones
1367
1371
  // (--force to overwrite). Leaves specs/ and live .core config alone.
1368
- resync(dir, { claudeMd: opts.claudeMd, force: opts.force })
1372
+ resync(dir, { claudeMd: opts.claudeMd, force: opts.force, diff: opts.diff })
1369
1373
  await cleanupReleaseTooling(dir, opts)
1370
1374
  break
1371
1375
  default: