@skitterbyte/skitterspec-linear 8.0.5 → 9.1.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/README.md +34 -16
- package/assets/core/SETUP.md +47 -37
- package/assets/core/linear.config.json.example +10 -7
- package/assets/core/linear.config.md +80 -38
- package/assets/rules/spec-planning.md +3 -0
- package/assets/skills/spec/SKILL.md +106 -10
- package/assets/skills/spec-bug/SKILL.md +61 -0
- package/assets/skills/spec-cancel/SKILL.md +18 -3
- package/assets/skills/spec-complete/SKILL.md +26 -8
- package/assets/skills/spec-go/SKILL.md +16 -6
- package/assets/skills/spec-hotfix/SKILL.md +1 -0
- package/assets/skills/spec-push/SKILL.md +71 -28
- package/assets/skills/spec-status/SKILL.md +13 -12
- package/package.json +1 -1
- package/src/cli.js +18 -6
- package/src/env/provision.js +39 -3
- package/src/env/resolve.js +1 -0
- package/src/vendor/linear/cli-sync.js +87 -12
- package/src/vendor/linear/config.js +41 -17
- package/src/vendor/linear/mcp.js +57 -50
- package/src/vendor/sync-core/index.js +4 -3
- package/src/vendor/sync-core/src/base.js +1 -1
- package/src/vendor/sync-core/src/compare.js +29 -41
- package/src/vendor/sync-core/src/normalize.js +61 -43
- package/src/vendor/sync-core/src/push.js +5 -7
- package/src/vendor/sync-core/src/write.js +14 -13
package/README.md
CHANGED
|
@@ -4,7 +4,7 @@ Spec-driven development for [Claude Code](https://claude.com/claude-code), **wit
|
|
|
4
4
|
one-way Linear sync**. A strict **superset** of
|
|
5
5
|
[`@skitterbyte/skitterspec`](https://www.npmjs.com/package/@skitterbyte/skitterspec):
|
|
6
6
|
everything in the base filesystem workflow, plus one-way sync from a spec up to
|
|
7
|
-
its linked Linear
|
|
7
|
+
its linked Linear issue — the repo is canonical, Linear is a generated mirror.
|
|
8
8
|
|
|
9
9
|
```sh
|
|
10
10
|
npx @skitterbyte/skitterspec-linear init
|
|
@@ -46,7 +46,7 @@ fuller guide):
|
|
|
46
46
|
That file is the opt-in gate — until it exists, everything below is inert and
|
|
47
47
|
the package behaves exactly like the base.
|
|
48
48
|
|
|
49
|
-
4. **Link and push** — `/spec` creates a linked Linear
|
|
49
|
+
4. **Link and push** — `/spec` creates a linked Linear issue (a sub-issue per
|
|
50
50
|
phase) and stamps the id; then `/spec-push` publishes the spec up and
|
|
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.
|
|
@@ -58,14 +58,17 @@ On top of the base skills (`/spec`, `/spec-go`, isolation, …):
|
|
|
58
58
|
- **`/spec-status`** — read-only drift report: what the next push would create /
|
|
59
59
|
update, plus any workflow-state drift. Changes nothing.
|
|
60
60
|
- **`/spec-push`** — repo → Linear, one-way. Diffs the spec against a committed
|
|
61
|
-
last-pushed snapshot and applies only what changed (
|
|
62
|
-
|
|
61
|
+
last-pushed snapshot and applies only what changed (issue description + state,
|
|
62
|
+
phase sub-issues), stamping the returned ids back into the spec.
|
|
63
63
|
- **`spec-sync` CLI** (`skitterspec-linear spec-sync …`) — the deterministic
|
|
64
64
|
engine behind the skills, for CI / local runs.
|
|
65
65
|
|
|
66
|
-
The shared `/spec` and `/spec-go` skills come composed with the
|
|
67
|
-
filled in: `/spec`
|
|
68
|
-
|
|
66
|
+
The shared `/spec`, `/spec-bug` and `/spec-go` skills come composed with the
|
|
67
|
+
Linear steps filled in: `/spec` asks which Linear **Project** the spec belongs to,
|
|
68
|
+
then links it to a Linear issue (a sub-issue per phase). `/spec` and `/spec-bug`
|
|
69
|
+
can also start **from** an existing issue — `/spec SKI-123`, or `/spec
|
|
70
|
+
--from-issue` to browse the ones your web app filed. There is no pull — the repo
|
|
71
|
+
is already canonical, so `/spec-go` just builds.
|
|
69
72
|
|
|
70
73
|
## Opt-in
|
|
71
74
|
|
|
@@ -77,15 +80,30 @@ behaves exactly like the base.
|
|
|
77
80
|
setup guide — connecting the `linear` MCP server, finding your team id, linking a
|
|
78
81
|
spec, and a smoke test. Per-field docs live in `specs/.core/linear.config.md`.
|
|
79
82
|
|
|
80
|
-
**What pushes:** the spec
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
**workflow state**.
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
sends only what
|
|
83
|
+
**What pushes:** the spec is one Linear **issue** — the spec body travels as its
|
|
84
|
+
**`description`**, each phase as a **sub-issue** (phase name → title, `**Goal:**`
|
|
85
|
+
→ 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
|
|
88
|
+
PM's to set in Linear; one-way sync neither pushes nor reads them, so they're
|
|
89
|
+
never clobbered. A workflow-state a teammate moves in Linear is surfaced by
|
|
90
|
+
`/spec-status` as drift and overwritten on the next push. **Last-pushed snapshots**
|
|
91
|
+
(`specs/.core/linear-base/`, content hashes) are committed so push sends only what
|
|
92
|
+
changed.
|
|
93
|
+
|
|
94
|
+
**Which Project a spec lands in** is asked once, when the issue is first created
|
|
95
|
+
— a filterable list of your team's projects, defaulting to `linear.projectId` and
|
|
96
|
+
always offering *None*. It's passed on the create call only and never stored, so
|
|
97
|
+
re-homing a spec issue in Linear sticks: it won't read as drift and won't be moved
|
|
98
|
+
back on the next push.
|
|
99
|
+
|
|
100
|
+
**Starting from an issue** (`intake.label` / `intake.bugLabels` in the config):
|
|
101
|
+
`/spec SKI-123` adopts that issue, `/spec --from-issue [query]` browses the inbox.
|
|
102
|
+
The issue *becomes* the spec's issue — the reporter's thread, comments and links
|
|
103
|
+
stay put, their words are carried into the spec's **Problem**, and the first push
|
|
104
|
+
replaces the description with the spec. A bug-labelled issue routes to
|
|
105
|
+
`/spec-bug`, which adopts it the same way. `skitterspec-linear spec-sync linked`
|
|
106
|
+
lists what's already adopted, so an issue never becomes two specs.
|
|
89
107
|
|
|
90
108
|
Branch naming that embeds the Linear id lives in the isolation config
|
|
91
109
|
(`env.config.json` → `branch.pattern` with `{identifier}`, `branch.identifierField:
|
package/assets/core/SETUP.md
CHANGED
|
@@ -2,10 +2,10 @@
|
|
|
2
2
|
|
|
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
|
-
the linked Linear
|
|
6
|
-
read back or merged.
|
|
7
|
-
|
|
8
|
-
already have.
|
|
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
|
|
8
|
+
team) that the config reference (`linear.config.md`) assumes you already have.
|
|
9
9
|
|
|
10
10
|
> The whole feature is **opt-in**: until `specs/.core/linear.config.json` exists,
|
|
11
11
|
> everything below is inert and the package behaves exactly like the base
|
|
@@ -65,9 +65,10 @@ It calls the Linear `list_teams` tool and returns rows like:
|
|
|
65
65
|
Skitterspec — e07c2b54-dcf6-4b6e-81bd-175a9bc79868 (key: SKI)
|
|
66
66
|
```
|
|
67
67
|
|
|
68
|
-
Copy the `id` (the UUID). The `key` (e.g. `SKI`) is the short human handle. If
|
|
69
|
-
|
|
70
|
-
|
|
68
|
+
Copy the `id` (the UUID). The `key` (e.g. `SKI`) is the short human handle. If most
|
|
69
|
+
of your specs belong to one **Project**, ask "list my Linear projects" and copy
|
|
70
|
+
that id into `linear.projectId` — it becomes the *default* the project picker
|
|
71
|
+
pre-selects, not a fixed destination.
|
|
71
72
|
|
|
72
73
|
## 4. Scaffold the config
|
|
73
74
|
|
|
@@ -80,7 +81,11 @@ ids from step 3 — the team id is the only required field:
|
|
|
80
81
|
"linear": {
|
|
81
82
|
"teamKey": "SKI",
|
|
82
83
|
"teamId": "e07c2b54-dcf6-4b6e-81bd-175a9bc79868",
|
|
83
|
-
"
|
|
84
|
+
"projectId": "" // optional: the project picker's default
|
|
85
|
+
},
|
|
86
|
+
"intake": { // optional: starting a spec from an issue
|
|
87
|
+
"label": "web-app", // the inbox `/spec --from-issue` browses
|
|
88
|
+
"bugLabels": ["bug"] // issues with these route to /spec-bug
|
|
84
89
|
}
|
|
85
90
|
}
|
|
86
91
|
```
|
|
@@ -89,20 +94,25 @@ Everything else (state names, field ownership) has sensible defaults — see
|
|
|
89
94
|
`linear.config.md` to customise. The moment this file exists, the Linear steps in
|
|
90
95
|
`/spec` and `/spec-go` and the three sync skills switch on.
|
|
91
96
|
|
|
92
|
-
## 5. Link a spec to a Linear
|
|
97
|
+
## 5. Link a spec to a Linear issue
|
|
93
98
|
|
|
94
|
-
A spec syncs once its `00-overview.md` frontmatter carries a `
|
|
99
|
+
A spec syncs once its `00-overview.md` frontmatter carries a `linear_identifier`.
|
|
95
100
|
Two ways to get there:
|
|
96
101
|
|
|
97
|
-
- **New spec:** run `/spec` — with Linear configured it
|
|
98
|
-
Linear **
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
`
|
|
102
|
+
- **New spec:** run `/spec` — with Linear configured it asks which Project the
|
|
103
|
+
spec belongs to, creates a linked Linear **issue** (one sub-issue per phase) and
|
|
104
|
+
stamps the id for you.
|
|
105
|
+
- **From an existing issue:** run `/spec SKI-123` to adopt that issue, or
|
|
106
|
+
`/spec --from-issue` to browse the `intake.label` inbox and pick one. The issue
|
|
107
|
+
becomes the spec's issue — nothing is duplicated. A bug-labelled issue routes to
|
|
108
|
+
`/spec-bug` instead.
|
|
109
|
+
- **Existing spec / existing Linear issue:** add the id by hand. Ask Claude to
|
|
110
|
+
"create a Linear issue for this spec" (or find an existing one's id), then set
|
|
111
|
+
the frontmatter:
|
|
102
112
|
|
|
103
113
|
```yaml
|
|
104
114
|
---
|
|
105
|
-
|
|
115
|
+
linear_identifier: "SKI-123"
|
|
106
116
|
---
|
|
107
117
|
```
|
|
108
118
|
|
|
@@ -111,7 +121,7 @@ Two ways to get there:
|
|
|
111
121
|
| Command | Direction | What it does |
|
|
112
122
|
|---------|-----------|--------------|
|
|
113
123
|
| `/spec-status` | — | Read-only drift report: what would push (create/update), and whether Linear's workflow-state drifted from the spec. Writes nothing. |
|
|
114
|
-
| `/spec-push` | repo → Linear | Computes a create/update plan vs the last-pushed snapshot and applies it (
|
|
124
|
+
| `/spec-push` | repo → Linear | Computes a create/update plan vs the last-pushed snapshot and applies it (issue description/state, phase sub-issues), stamping new ids back into the spec. |
|
|
115
125
|
|
|
116
126
|
Typical loop: edit the spec in-repo → `/spec-status` (what's pending) →
|
|
117
127
|
`/spec-push` (send it up). There is no pull — Linear is a generated mirror.
|
|
@@ -120,20 +130,20 @@ Typical loop: edit the spec in-repo → `/spec-status` (what's pending) →
|
|
|
120
130
|
|
|
121
131
|
| Field | What |
|
|
122
132
|
|-------|------|
|
|
123
|
-
| `description` | the spec body (problem, solution
|
|
124
|
-
| `
|
|
125
|
-
| `
|
|
126
|
-
| `workflowState` → project status | the spec's lifecycle bucket, mapped via `states` |
|
|
133
|
+
| `description` | the spec body (problem, solution) as the issue description |
|
|
134
|
+
| `subIssues` | one per phase — phase name as **title**, `**Goal:**` as the **description**, phase emoji → **state** |
|
|
135
|
+
| `workflowState` → issue state | the spec's lifecycle folder bucket, mapped via `states` |
|
|
127
136
|
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
137
|
+
Tasks, priority, labels, cycles and comments are **Linear-native triage** (or
|
|
138
|
+
repo-only) — never pushed, so they're never clobbered. A workflow-state a teammate
|
|
139
|
+
moves in Linear is surfaced by `/spec-status` as drift and overwritten on the next
|
|
140
|
+
push.
|
|
132
141
|
|
|
133
|
-
|
|
134
|
-
live in
|
|
135
|
-
|
|
136
|
-
creates each object, so later pushes update instead of recreate.
|
|
142
|
+
The spec is one issue and each phase a **sub-issue** (`parentId` = the spec
|
|
143
|
+
issue). The link ids live in frontmatter — `linear_identifier` on the overview,
|
|
144
|
+
`linear_issue_id` on each phase file; `/spec-push` stamps them the first time it
|
|
145
|
+
creates each object, so later pushes update instead of recreate. Set
|
|
146
|
+
`linear.projectId` to add every spec issue to a grouping Linear Project.
|
|
137
147
|
|
|
138
148
|
Sections listed in `sync.localOnlySections` (default: **State log**, **Changelog**,
|
|
139
149
|
**Open questions**) are stripped from the pushed description — they never leave
|
|
@@ -150,22 +160,22 @@ the repo.
|
|
|
150
160
|
With a linked spec, confirm push end-to-end:
|
|
151
161
|
|
|
152
162
|
1. `/spec-status` → shows what would push (`pending — N to create, M to update`).
|
|
153
|
-
2. `/spec-push` → creates the
|
|
154
|
-
description/
|
|
163
|
+
2. `/spec-push` → creates the spec issue and its phase sub-issues and sets the
|
|
164
|
+
description/state; ids are stamped back into the spec.
|
|
155
165
|
3. `/spec-status` again → **up to date** (nothing changed since the last push).
|
|
156
|
-
4. Edit a
|
|
166
|
+
4. Edit a phase goal locally, `/spec-push` → the matching sub-issue updates;
|
|
157
167
|
`/spec-status` returns to **up to date**.
|
|
158
168
|
|
|
159
169
|
## Troubleshooting
|
|
160
170
|
|
|
161
171
|
- **"connect the `linear` MCP server"** — the server isn't connected/authed for
|
|
162
172
|
this session. Re-run step 2; remember a fresh add needs a Claude Code restart.
|
|
163
|
-
- **"missing required tools:
|
|
173
|
+
- **"missing required tools: issueCreate"** — you're on the read-only endpoint
|
|
164
174
|
(or a restricted API key). Use `https://mcp.linear.app/mcp` for push.
|
|
165
|
-
- **A configured
|
|
166
|
-
|
|
167
|
-
workspace) and fix `linear.config.json` to the real
|
|
168
|
-
(`Backlog /
|
|
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`).
|
|
169
179
|
- **Reconnecting doesn't switch workspace** — Linear ties the OAuth session to one
|
|
170
180
|
workspace. Remove and re-add the server to authenticate against another.
|
|
171
181
|
- **Bold around an inline code span renders oddly in Linear** — Linear moves the
|
|
@@ -2,17 +2,21 @@
|
|
|
2
2
|
"linear": {
|
|
3
3
|
"teamKey": "",
|
|
4
4
|
"teamId": "",
|
|
5
|
-
"
|
|
5
|
+
"projectId": ""
|
|
6
|
+
},
|
|
7
|
+
"intake": {
|
|
8
|
+
"label": "",
|
|
9
|
+
"bugLabels": []
|
|
6
10
|
},
|
|
7
11
|
"mapping": {
|
|
8
|
-
"specFolder": "
|
|
9
|
-
"phases": "
|
|
10
|
-
"tasks": "
|
|
12
|
+
"specFolder": "issue",
|
|
13
|
+
"phases": "subissue",
|
|
14
|
+
"tasks": "none"
|
|
11
15
|
},
|
|
12
16
|
"states": {
|
|
13
17
|
"backlog": "Backlog",
|
|
14
18
|
"in-progress": "In Progress",
|
|
15
|
-
"complete": "
|
|
19
|
+
"complete": "Done",
|
|
16
20
|
"cancelled": "Canceled"
|
|
17
21
|
},
|
|
18
22
|
"snapshot": {
|
|
@@ -25,8 +29,7 @@
|
|
|
25
29
|
"baseDir": "specs/.core/linear-base",
|
|
26
30
|
"fieldOwnership": {
|
|
27
31
|
"description": "push",
|
|
28
|
-
"
|
|
29
|
-
"tasks": "push",
|
|
32
|
+
"subIssues": "push",
|
|
30
33
|
"workflowState": "push"
|
|
31
34
|
},
|
|
32
35
|
"localOnlySections": ["State log", "Changelog", "Open questions"],
|
|
@@ -2,7 +2,8 @@
|
|
|
2
2
|
|
|
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
|
-
the source of truth and the Linear
|
|
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
7
|
pushed up and never read back or merged — `/spec-push` diffs the spec against a
|
|
7
8
|
committed **last-pushed snapshot** and applies only what changed; `/spec-status`
|
|
8
9
|
is a read-only drift report. The `sync.fieldOwnership` map now just selects the
|
|
@@ -12,7 +13,7 @@ projection field set (every field is repo-owned and pushed).
|
|
|
12
13
|
is absent the feature is simply unused — `/spec`, `/spec-go`, and the CLI's
|
|
13
14
|
`spec-sync` subcommands behave exactly as they do today (local-only). Adopt it by
|
|
14
15
|
copying `linear.config.json.example` → `linear.config.json` here and filling in
|
|
15
|
-
your team
|
|
16
|
+
your team ID (and an optional grouping project).
|
|
16
17
|
|
|
17
18
|
The loader (`src/sync/config.js` → `loadLinearConfig`) merges your file over the
|
|
18
19
|
frozen defaults below and returns `{ config, present }`; `present:false` means no
|
|
@@ -23,30 +24,38 @@ absence). A `sync.fieldOwnership` value outside `both|pull|push` is a hard error
|
|
|
23
24
|
|
|
24
25
|
```jsonc
|
|
25
26
|
{
|
|
26
|
-
// Which Linear team
|
|
27
|
-
// MCP adapter; leave blank until you connect the `linear`
|
|
27
|
+
// Which Linear team specs sync into, and an optional Project to group them.
|
|
28
|
+
// IDs are read by the MCP adapter; leave blank until you connect the `linear`
|
|
29
|
+
// MCP server.
|
|
28
30
|
"linear": {
|
|
29
31
|
"teamKey": "", // human-facing key, e.g. "ENG" (optional)
|
|
30
|
-
"teamId": "", // Linear team UUID (
|
|
31
|
-
"
|
|
32
|
+
"teamId": "", // Linear team UUID (the issue's team)
|
|
33
|
+
"projectId": "" // DEFAULT for the project picker (see below)
|
|
32
34
|
},
|
|
33
35
|
|
|
34
|
-
//
|
|
35
|
-
//
|
|
36
|
-
|
|
36
|
+
// Issue intake: `/spec <ISSUE-REF>` adopts any issue; `/spec --from-issue`
|
|
37
|
+
// browses the inbox. Both optional.
|
|
38
|
+
"intake": {
|
|
39
|
+
"label": "", // inbox filter — the label the web app files under
|
|
40
|
+
"bugLabels": [] // e.g. ["bug"] — these route to /spec-bug instead
|
|
41
|
+
},
|
|
42
|
+
|
|
43
|
+
// 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.
|
|
37
45
|
"mapping": {
|
|
38
|
-
"specFolder": "
|
|
39
|
-
"phases": "
|
|
40
|
-
"tasks": "
|
|
46
|
+
"specFolder": "issue",
|
|
47
|
+
"phases": "subissue",
|
|
48
|
+
"tasks": "none"
|
|
41
49
|
},
|
|
42
50
|
|
|
43
|
-
// Map the spec's lifecycle bucket → the Linear workflow-state name. Used
|
|
44
|
-
//
|
|
51
|
+
// Map the spec's lifecycle bucket → the Linear ISSUE workflow-state name. Used
|
|
52
|
+
// 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.
|
|
45
54
|
"states": {
|
|
46
55
|
"backlog": "Backlog",
|
|
47
56
|
"in-progress": "In Progress",
|
|
48
57
|
"complete": "Done",
|
|
49
|
-
"cancelled": "
|
|
58
|
+
"cancelled": "Canceled"
|
|
50
59
|
},
|
|
51
60
|
|
|
52
61
|
// The spec's entry-point file the local snapshot + frontmatter live in.
|
|
@@ -69,15 +78,14 @@ absence). A `sync.fieldOwnership` value outside `both|pull|push` is a hard error
|
|
|
69
78
|
"baseDir": "specs/.core/linear-base",
|
|
70
79
|
|
|
71
80
|
// The pushed projection field set (repo → Linear, one-way). The `push` marker
|
|
72
|
-
// is retained for shape; there is no pull. The default set is the
|
|
73
|
-
// `description`, `
|
|
74
|
-
// the lifecycle `workflowState`.
|
|
75
|
-
//
|
|
76
|
-
//
|
|
81
|
+
// is retained for shape; there is no pull. The default set is the spec
|
|
82
|
+
// issue's `description`, its `subIssues` (one per phase — name + goal +
|
|
83
|
+
// state), and the lifecycle `workflowState`. Tasks, priority, labels, cycles
|
|
84
|
+
// and comments are NOT here, so a PM's triage is never touched. Any key you
|
|
85
|
+
// add joins the pushed projection.
|
|
77
86
|
"fieldOwnership": {
|
|
78
87
|
"description": "push",
|
|
79
|
-
"
|
|
80
|
-
"tasks": "push",
|
|
88
|
+
"subIssues": "push",
|
|
81
89
|
"workflowState": "push"
|
|
82
90
|
},
|
|
83
91
|
|
|
@@ -85,31 +93,65 @@ absence). A `sync.fieldOwnership` value outside `both|pull|push` is a hard error
|
|
|
85
93
|
// are stripped from the pushed `description` (never sent to Linear).
|
|
86
94
|
"localOnlySections": ["State log", "Changelog", "Open questions"],
|
|
87
95
|
|
|
88
|
-
// Reserved.
|
|
89
|
-
//
|
|
90
|
-
// validated but unused. Leave it `{}`.
|
|
96
|
+
// Reserved. Sub-issues are always projected per phase, so this no longer
|
|
97
|
+
// needs setting; it is validated but unused. Leave it `{}`.
|
|
91
98
|
"keyedFields": {}
|
|
92
99
|
}
|
|
93
100
|
}
|
|
94
101
|
```
|
|
95
102
|
|
|
96
|
-
##
|
|
103
|
+
## Spec → Issue, phases → sub-issues
|
|
97
104
|
|
|
98
105
|
Push maps the spec's structure to Linear's, keyed by id so it updates rather than
|
|
99
106
|
recreates:
|
|
100
107
|
|
|
101
|
-
- **
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
- **
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
108
|
+
- **Spec → Issue.** The spec is one Linear issue. Its id lives in the overview
|
|
109
|
+
frontmatter (`linear_identifier`); its `description` ← the overview plan (with
|
|
110
|
+
the `Phases` index and local-only sections stripped); its workflow-state ← the
|
|
111
|
+
spec's lifecycle folder (`specs/<bucket>/`).
|
|
112
|
+
- **Phases → sub-issues.** Each phase file maps to a child issue (`parentId` = the
|
|
113
|
+
spec issue). The link id lives in the phase file's frontmatter
|
|
114
|
+
(`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.
|
|
117
|
+
|
|
118
|
+
Unlinked local items (a spec with no `linear_identifier`, a phase with no
|
|
119
|
+
`linear_issue_id`) are created in Linear on the next `/spec-push`, which stamps
|
|
120
|
+
the new id back so they link from then on.
|
|
121
|
+
|
|
122
|
+
## Which Project a spec issue belongs to
|
|
123
|
+
|
|
124
|
+
`linear.projectId` is the **default**, not a mandate. When a spec issue is first
|
|
125
|
+
created — by `/spec`, or by the first `/spec-push` if the spec was authored
|
|
126
|
+
offline — you're offered the team's projects, filterable by name, with an explicit
|
|
127
|
+
**None (team only)** option and this id pre-selected.
|
|
128
|
+
|
|
129
|
+
The choice is passed on the **create call only**. It is never written into the
|
|
130
|
+
spec, never recorded in the snapshot, and never sent on an update. So once the
|
|
131
|
+
issue exists, where it lives is Linear's business: move it between projects and
|
|
132
|
+
`/spec-status` will not call it drift and `/spec-push` will not move it back.
|
|
133
|
+
|
|
134
|
+
A spec that **adopted** an existing issue (see below) skips the picker entirely —
|
|
135
|
+
it was filed somewhere deliberately.
|
|
136
|
+
|
|
137
|
+
## Starting a spec from an existing issue
|
|
138
|
+
|
|
139
|
+
With `intake` configured, a spec can begin life as a Linear issue someone else
|
|
140
|
+
filed:
|
|
141
|
+
|
|
142
|
+
- `/spec SKI-123` — adopt that issue.
|
|
143
|
+
- `/spec --from-issue [query]` — browse issues labelled `intake.label` (what your
|
|
144
|
+
web app or feedback form files under), optionally filtered by title.
|
|
145
|
+
|
|
146
|
+
The issue **becomes** the spec's issue: its identifier is stamped as
|
|
147
|
+
`linear_identifier`, phases become its sub-issues, and the first `/spec-push`
|
|
148
|
+
replaces its description with the spec. The reporter's comments, links and
|
|
149
|
+
subscribers stay on the one issue everyone is already watching; their original
|
|
150
|
+
words are carried into the spec's **Problem** section.
|
|
151
|
+
|
|
152
|
+
An issue already stamped on a spec can't be adopted twice — `skitterspec spec-sync
|
|
153
|
+
linked` is the list that's checked. An issue labelled with one of
|
|
154
|
+
`intake.bugLabels` routes to `/spec-bug` instead, which adopts it identically.
|
|
113
155
|
|
|
114
156
|
## One direction — nothing to reconcile
|
|
115
157
|
|
|
@@ -113,6 +113,9 @@ status.
|
|
|
113
113
|
|
|
114
114
|
Every spec header carries:
|
|
115
115
|
|
|
116
|
+
- `> **Name:**` — the spec's folder name (`feat-`/`bug-`/`hotfix-<kebab-name>`).
|
|
117
|
+
It's the handle you pass to `/spec-go` and the other lifecycle skills, surfaced
|
|
118
|
+
in the header so it's copy-pasteable without digging for the folder name.
|
|
116
119
|
- `> **Author:**` — who created the spec (set at `/spec` / `/spec-bug`, defaults
|
|
117
120
|
to `git config user.name`).
|
|
118
121
|
- `> **Developer:**` — who implements it (`—` until `/spec-go` starts work, then
|
|
@@ -14,6 +14,66 @@ Lifecycle (the governing skills) — status in parentheses:
|
|
|
14
14
|
`/spec-cancel` (Cancelled). See `.claude/rules/spec-planning.md`. (There is no
|
|
15
15
|
separate grooming command — `/spec` grills to a Ready spec directly.)
|
|
16
16
|
|
|
17
|
+
## Phase 0 — start from a Linear issue (only when asked)
|
|
18
|
+
|
|
19
|
+
**Only when `specs/.core/linear.config.json` exists** *and* the invocation names an
|
|
20
|
+
issue. Otherwise skip this phase entirely and grill from the user's own words.
|
|
21
|
+
|
|
22
|
+
Two ways in:
|
|
23
|
+
|
|
24
|
+
- **`<ISSUE-REF>`** (`SKI-123`, an issue URL, or a bare id anywhere in the
|
|
25
|
+
arguments) — adopt that issue.
|
|
26
|
+
- **`--from-issue [query]`** — browse the intake inbox: issues carrying
|
|
27
|
+
`intake.label` (what the web app files under). With a `query`, filter that list
|
|
28
|
+
case-insensitively by title. Show the shortlist and let the user pick one.
|
|
29
|
+
|
|
30
|
+
Then:
|
|
31
|
+
|
|
32
|
+
1. **Exclude what's already adopted.** Run `skitterspec spec-sync linked --json`
|
|
33
|
+
and drop any issue whose identifier is already stamped on a spec. If the user
|
|
34
|
+
named such an issue directly, **stop** and point them at that spec — one issue
|
|
35
|
+
never becomes two specs.
|
|
36
|
+
2. **Read the issue** with the discovered issue-read tool: title, description,
|
|
37
|
+
labels, reporter, URL. If Linear isn't connected, say so and offer to carry on
|
|
38
|
+
without it — a spec written from the user's own description is still a spec.
|
|
39
|
+
3. **Route bugs away — in `/spec` only.** If any of the issue's labels matches
|
|
40
|
+
`intake.bugLabels` (case-insensitive), this is a bug report, not a feature
|
|
41
|
+
request. Say so, name the matching label, and tell the user to run
|
|
42
|
+
**`/spec-bug <ISSUE-REF>`** — then stop, without authoring a Feature spec.
|
|
43
|
+
With `intake.bugLabels` unset nothing is routed and every issue is treated as a
|
|
44
|
+
feature request.
|
|
45
|
+
|
|
46
|
+
**In `/spec-bug` this step is skipped** — you are already in the bug path.
|
|
47
|
+
Adopt the issue, then reproduce it as usual: the issue body is your repro
|
|
48
|
+
material, and the failing test comes before the spec exactly as normal.
|
|
49
|
+
4. **Seed, don't skip, the grill.** The issue's title becomes the working spec
|
|
50
|
+
title and its description the starting material for **Problem** — quote the
|
|
51
|
+
reporter's words rather than paraphrasing them away. Note the reporter and the
|
|
52
|
+
issue URL for context. Everything after this runs exactly as normal: an issue
|
|
53
|
+
is a *request*, not a groomed spec, so grill it as hard as anything else.
|
|
54
|
+
|
|
55
|
+
### Adopting the issue
|
|
56
|
+
|
|
57
|
+
The issue **becomes** the spec's issue — it is not copied and no second issue is
|
|
58
|
+
minted. This is identical in `/spec` and `/spec-bug`; only *when* it happens
|
|
59
|
+
differs (`/spec` writes the spec in Phase B, `/spec-bug` in its step 4). Once the
|
|
60
|
+
spec file exists:
|
|
61
|
+
|
|
62
|
+
- **Stamp `linear_identifier` and `linear_url`** in `00-overview.md` frontmatter
|
|
63
|
+
from the adopted issue. That is the whole link: every later skill
|
|
64
|
+
(`/spec-push`, `/spec-status`, `/spec-go`) keys off it being present.
|
|
65
|
+
- **Do not run the project picker** and never send `project`. The issue was filed
|
|
66
|
+
somewhere deliberately — where it lives is Linear's business, and adoption is
|
|
67
|
+
not a mint.
|
|
68
|
+
- **Do not write a base sidecar.** Leaving `sync.baseDir` empty for this spec is
|
|
69
|
+
what makes the first `/spec-push` push the spec over the reporter's original
|
|
70
|
+
description (an **update** to the existing issue, plus a sub-issue per phase).
|
|
71
|
+
Recording a snapshot here would declare the mirror already in sync and strand
|
|
72
|
+
the issue showing the raw report forever.
|
|
73
|
+
- **Say what will happen** in the finish-up message: the first `/spec-push` will
|
|
74
|
+
overwrite the issue's description with the spec, and the original report is
|
|
75
|
+
preserved in the spec's **Problem** section.
|
|
76
|
+
|
|
17
77
|
## Phase A — reach a clear shared understanding (grill first)
|
|
18
78
|
|
|
19
79
|
Interview the user until requirement AND proposed solution are unambiguous. Do
|
|
@@ -73,6 +133,7 @@ the codebase, link rather than duplicate):
|
|
|
73
133
|
# <Feature title>
|
|
74
134
|
|
|
75
135
|
> **Type:** Feature
|
|
136
|
+
> **Name:** feat-<kebab-name> (the spec folder name — the handle you paste into `/spec-go`)
|
|
76
137
|
> **Status:** Ready — not started
|
|
77
138
|
> **Author:** <git user.name — `git config user.name`>
|
|
78
139
|
> **Developer:** —
|
|
@@ -227,36 +288,71 @@ below (nothing to do here without one).
|
|
|
227
288
|
**Only when `specs/.core/linear.config.json` exists** (Linear sync is opted in).
|
|
228
289
|
If it's absent, skip this phase entirely — the spec stays local-only and `/spec`
|
|
229
290
|
behaves exactly as above. When present, after writing the spec, link it to Linear
|
|
230
|
-
so status and discussion live there while the repo stays the co-authoring surface
|
|
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:
|
|
231
293
|
|
|
232
294
|
- **Discover the Linear MCP tools at runtime** (don't hardcode names). If Linear
|
|
233
295
|
isn't connected/authed, relay the fix and stop — leave the spec written and
|
|
234
296
|
local; the user can link it later with `/spec-push`. Do nothing destructive.
|
|
235
|
-
- **
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
297
|
+
- **Pick the Project** — run the picker in **Picking the Linear Project** below,
|
|
298
|
+
then **create the Issue** from the spec: `title` from the spec title,
|
|
299
|
+
`description` from the `00-overview.md` plan, `team` = `linear.teamId`, and
|
|
300
|
+
`project` = the picked id (omitted when the user chose None).
|
|
301
|
+
- **Create a sub-issue per phase** (the `mapping.phases` target — `subissue` by
|
|
302
|
+
default): a child issue with `parentId` = the spec issue, named from each phase
|
|
303
|
+
file, in execution order.
|
|
240
304
|
- **Add the frontmatter block** to `00-overview.md` (above the `#` title) so the
|
|
241
305
|
spec is linkable:
|
|
242
306
|
|
|
243
307
|
```yaml
|
|
244
308
|
---
|
|
245
|
-
linear_project_id: "<uuid>"
|
|
246
309
|
linear_identifier: "<TEAM-123>"
|
|
247
310
|
linear_url: "https://linear.app/..."
|
|
248
|
-
spec_status: "backlog"
|
|
249
311
|
last_synced_at: "<ISO-8601 now>"
|
|
250
312
|
---
|
|
251
313
|
```
|
|
252
314
|
|
|
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.)
|
|
253
317
|
- **Write the initial base sidecar** so the spec starts clean and non-diverged —
|
|
254
|
-
run `skitterspec spec-sync
|
|
318
|
+
run `skitterspec spec-sync record <spec>` to capture the local snapshot as the
|
|
255
319
|
committed base (`sync.baseDir`). `/spec-status` should report in-sync right after.
|
|
320
|
+
(`record` is the writer; `normalize` only *prints* the projection.) Skip this
|
|
321
|
+
when the spec **adopted** an existing issue — see Phase 0 — because the issue's
|
|
322
|
+
description is still the reporter's, not the spec.
|
|
256
323
|
- **Echo the branch name** from `branch.pattern` so the user knows what `/spec-go`
|
|
257
324
|
will fork.
|
|
258
325
|
|
|
259
326
|
Leave committing to the existing convention (the user commits the spec as usual)
|
|
260
327
|
and **never auto-push git** — Linear's own automation reacts to real branch/PR
|
|
261
|
-
events later. Report the Linear
|
|
328
|
+
events later. Report the Linear issue URL and the base as part of Phase C's
|
|
262
329
|
finish-up message.
|
|
330
|
+
|
|
331
|
+
### Picking the Linear Project
|
|
332
|
+
|
|
333
|
+
Run this **only when minting a spec issue** — creating it for the first time. On
|
|
334
|
+
an update the issue already has a project (or deliberately has none), and that
|
|
335
|
+
placement is **Linear's to own**: never send `project` on an update, and never
|
|
336
|
+
record the choice in the spec file or the snapshot. A PM re-homing a spec issue
|
|
337
|
+
must not show up as drift or be overwritten on the next push.
|
|
338
|
+
|
|
339
|
+
1. **List the candidates.** Call the discovered project-list tool for
|
|
340
|
+
`linear.teamId`. Drop archived / completed projects — they can't take new work.
|
|
341
|
+
2. **Offer them.** Show the names (most recently updated first is fine), plus an
|
|
342
|
+
explicit **None (team only)** option. Pre-select `linear.projectId` from
|
|
343
|
+
`linear.config.json` when it's set and still in the list; otherwise pre-select
|
|
344
|
+
**None**.
|
|
345
|
+
3. **Narrow on request.** If the user types a fragment rather than choosing, filter
|
|
346
|
+
the list case-insensitively by name and re-offer. Don't re-fetch.
|
|
347
|
+
4. **Never offer to create a project.** Projects are the PM's surface — if none
|
|
348
|
+
fits, that's **None (team only)**, and someone makes the project in Linear.
|
|
349
|
+
5. **Pass it once.** Include `project: <chosen id>` on the issue-create call.
|
|
350
|
+
Chose None → omit the key entirely (do not send an empty string).
|
|
351
|
+
|
|
352
|
+
**Degrade, never block.** If Linear isn't connected, or the server exposes no
|
|
353
|
+
project-list tool, say so in one line — *"project picker unavailable; creating the
|
|
354
|
+
issue without a project"* — and carry on with `linear.projectId` if it's set, else
|
|
355
|
+
no project at all. A missing picker must never fail `/spec` or `/spec-push`.
|
|
356
|
+
|
|
357
|
+
If `linear.projectId` is set but that Project is archived or missing, relay
|
|
358
|
+
Linear's error and stop rather than silently minting an unparented issue.
|