@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 +156 -0
- package/README.md +34 -4
- package/assets/core/SETUP.md +23 -6
- package/assets/core/linear.config.json.example +1 -1
- package/assets/core/linear.config.md +25 -6
- package/assets/skills/spec/SKILL.md +14 -12
- package/assets/skills/spec-push/SKILL.md +63 -25
- package/assets/skills/spec-review/SKILL.md +13 -0
- package/bin/skitterspec-linear.js +5 -1
- package/package.json +3 -2
- package/src/cli.js +6 -2
- package/src/init.js +45 -11
- package/src/lines-diff.js +114 -0
- package/src/vendor/linear/cli-sync.js +216 -13
- package/src/vendor/linear/config.js +20 -1
- package/src/vendor/linear/mcp.js +2 -1
- package/src/vendor/sync-core/index.js +7 -2
- package/src/vendor/sync-core/src/legacy.js +90 -0
- package/src/vendor/sync-core/src/normalize.js +188 -7
- package/src/vendor/sync-core/src/push.js +7 -0
- package/src/vendor/sync-core/src/task-block.js +87 -33
- package/src/vendor/sync-core/src/write.js +1 -0
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,
|
|
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**.
|
|
87
|
-
|
|
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`.
|
package/assets/core/SETUP.md
CHANGED
|
@@ -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
|
|
7
|
-
|
|
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
|
-
- **
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
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
|
|
@@ -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;
|
|
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)
|
|
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": "
|
|
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
|
|
116
|
-
|
|
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
|
|
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
|
-
- **
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
```
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
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
|
-
|
|
316
|
-
|
|
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
|
|
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)
|
|
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.
|
|
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
|
-
|
|
72
|
+
### Stop if the plan reports a pre-9.0 mirror
|
|
46
73
|
|
|
47
|
-
|
|
48
|
-
`
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
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`).
|
|
66
|
-
identifier
|
|
67
|
-
|
|
68
|
-
|
|
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
|
-
|
|
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.
|
|
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
|
-
|
|
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
|
-
|
|
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": "
|
|
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:
|