@skitterbyte/skitterspec-linear 10.1.0 → 10.3.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 +68 -23
- package/assets/core/SETUP.md +73 -13
- package/assets/core/linear.config.json.example +8 -1
- package/assets/core/linear.config.md +177 -10
- package/assets/rules/spec-planning.md +17 -11
- package/assets/skills/spec/SKILL.md +106 -66
- package/assets/skills/spec-bug/SKILL.md +99 -17
- package/assets/skills/spec-cancel/SKILL.md +34 -0
- package/assets/skills/spec-complete/SKILL.md +70 -15
- package/assets/skills/spec-hotfix/SKILL.md +157 -4
- package/assets/skills/spec-linear-setup/SKILL.md +172 -0
- package/assets/skills/spec-push/SKILL.md +108 -32
- package/assets/skills/spec-review/SKILL.md +34 -0
- package/assets/skills/spec-status/SKILL.md +9 -0
- package/bin/skitterspec-linear.js +46 -13
- package/package.json +1 -1
- package/src/cli.js +55 -21
- package/src/env/resolve.js +7 -2
- package/src/env/teardown.js +23 -9
- package/src/init.js +11 -1
- package/src/vendor/linear/api.js +246 -0
- package/src/vendor/linear/cli-sync.js +714 -11
- package/src/vendor/linear/commands.js +54 -0
- package/src/vendor/linear/config.js +116 -14
- package/src/vendor/sync-core/src/normalize.js +232 -85
- package/src/vendor/sync-core/src/push.js +10 -1
- package/src/vendor/sync-core/src/task-block.js +18 -7
package/README.md
CHANGED
|
@@ -82,14 +82,37 @@ On top of the base skills (`/spec`, `/spec-go`, isolation, …):
|
|
|
82
82
|
phase sub-issues), stamping the returned ids back into the spec.
|
|
83
83
|
- **`spec-sync` CLI** (`skitterspec-linear spec-sync …`) — the deterministic
|
|
84
84
|
engine behind the skills, for CI / local runs:
|
|
85
|
-
`normalize` · `push` · `stamp` · `record` · `
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
85
|
+
`normalize` · `push` · `apply` · `states` · `stamp` · `record` · `verify` ·
|
|
86
|
+
`status` · `linked`.
|
|
87
|
+
- **A direct-API push path.** Set a Linear personal API key and `/spec-push`
|
|
88
|
+
applies its plan through the engine instead of one assistant call per issue —
|
|
89
|
+
descriptions never pass through the model, in either direction:
|
|
90
|
+
|
|
91
|
+
```bash
|
|
92
|
+
export LINEAR_API_KEY=lin_api_… # Linear → Settings → Security & access
|
|
93
|
+
```
|
|
94
|
+
|
|
95
|
+
`auth.keyEnv` names the variable (never the key itself); `apply.transport`
|
|
96
|
+
pins `api`/`mcp`, or leave it empty to use the API whenever a key is present.
|
|
97
|
+
`--via mcp` forces the original path for one run.
|
|
98
|
+
**With no key nothing changes** — the MCP path is fully supported and stays
|
|
99
|
+
the default for anyone who never sets one.
|
|
100
|
+
- **One-command adoption on an existing repo.**
|
|
101
|
+
`skitterspec-linear spec-sync apply --all complete` pushes every spec in a
|
|
102
|
+
lifecycle bucket, reporting what it created, updated, skipped and failed. A
|
|
103
|
+
spec that fails doesn't stop the rest, and re-running retries only those —
|
|
104
|
+
every id is written into the spec as soon as its object exists, so nothing is
|
|
105
|
+
ever duplicated. Needs an API key. See `specs/.core/linear.config.md`.
|
|
106
|
+
|
|
107
|
+
**Every skill that moves a spec through its lifecycle** comes composed with the
|
|
108
|
+
Linear steps filled in, so the mirror keeps up without anyone remembering to
|
|
109
|
+
push: `/spec`, `/spec-bug` and `/spec-hotfix` link the spec they create (asking
|
|
110
|
+
which **Project** it belongs to, and minting a sub-issue per phase);
|
|
111
|
+
`/spec-go` refreshes it as work starts; `/spec-complete`, `/spec-cancel` and
|
|
112
|
+
`/spec-review` refresh it after they change it. All three creating skills can
|
|
113
|
+
also start **from** an existing issue — `/spec SKI-123`,
|
|
114
|
+
`/spec-hotfix v33.16.4 SKI-123`, or `--from-issue` to browse the ones your web
|
|
115
|
+
app filed. There is no pull — the repo is already canonical.
|
|
93
116
|
|
|
94
117
|
## Opt-in
|
|
95
118
|
|
|
@@ -121,14 +144,32 @@ are left alone. Your spec files are never modified. After a push, each stored
|
|
|
121
144
|
description is read back and compared against what was sent, ignoring Linear's
|
|
122
145
|
own reformatting and reporting anything genuinely lost. Both are automatic.
|
|
123
146
|
|
|
124
|
-
**
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
147
|
+
**How phases are mirrored** is `mapping.phases`, and it takes one mode for the
|
|
148
|
+
whole repo *or* one per lifecycle bucket:
|
|
149
|
+
|
|
150
|
+
```json
|
|
151
|
+
"mapping": { "phases": { "backlog": "deferred", "complete": "inline" } }
|
|
152
|
+
```
|
|
153
|
+
|
|
154
|
+
- `"subissue"` (default) — every phase is a sub-issue from the spec's first push,
|
|
155
|
+
so agents can be assigned one each.
|
|
156
|
+
- `"deferred"` — unlinked phases wait until the work starts. A spec sitting in
|
|
157
|
+
`specs/backlog/` mirrors as **the issue alone**, keeping its phase list in the
|
|
158
|
+
description; the sub-issues arrive with the push that follows `/spec-go`. Worth
|
|
159
|
+
it when adopting sync on a backlog of dozens of specs, where the default
|
|
160
|
+
front-loads hundreds of calls for work nobody has started.
|
|
161
|
+
- `"inline"` — phases become **sections of the spec issue's own description**,
|
|
162
|
+
full task lists included, and no sub-issues are minted. For work nobody will
|
|
163
|
+
pick up phase by phase: 250 completed specs are 250 readable issues instead of
|
|
164
|
+
250 issues plus 669 sub-issues.
|
|
165
|
+
|
|
166
|
+
The per-bucket form exists because those answers differ by *when*, not by repo —
|
|
167
|
+
finished work wants one issue, work in flight wants the assignable sub-issues.
|
|
168
|
+
A bucket the map omits defaults to `"subissue"`; a bad key or mode fails loudly at
|
|
169
|
+
load. Phases already carrying an id keep their sub-issue in every mode and are
|
|
170
|
+
never also inlined, so switching an existing project over never strands a live
|
|
171
|
+
sub-issue. `/spec-push` and `/spec-status` print the mode that resolved. See
|
|
172
|
+
`linear.config.md` for the details and the adoption path.
|
|
132
173
|
|
|
133
174
|
**Which Project a spec lands in** is asked once, when the issue is first created
|
|
134
175
|
— a filterable list of your team's projects, defaulting to `linear.projectId` and
|
|
@@ -136,13 +177,17 @@ always offering *None*. It's passed on the create call only and never stored, so
|
|
|
136
177
|
re-homing a spec issue in Linear sticks: it won't read as drift and won't be moved
|
|
137
178
|
back on the next push.
|
|
138
179
|
|
|
139
|
-
**Starting from an issue** (`intake
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
180
|
+
**Starting from an issue** (the `intake` block in the config): `/spec SKI-123`
|
|
181
|
+
adopts that issue, `--from-issue [query]` browses the inbox, and `/spec-bug` and
|
|
182
|
+
`/spec-hotfix` adopt the same way — `/spec-hotfix v33.16.4 SKI-123` starts a
|
|
183
|
+
patch to a released version from the issue that reported it. The issue *becomes*
|
|
184
|
+
the spec's issue — the reporter's thread, comments and links stay put, their
|
|
185
|
+
words are carried into the spec's **Problem** (or **Symptom**), and the linking
|
|
186
|
+
push replaces the description with the spec as it is created. Labels route an
|
|
187
|
+
issue onward: `intake.hotfixLabels` to `/spec-hotfix`, `intake.bugLabels` to
|
|
188
|
+
`/spec-bug`, with hotfix winning when an issue carries both.
|
|
189
|
+
`skitterspec-linear spec-sync linked` lists what's already adopted, so an issue
|
|
190
|
+
never becomes two specs.
|
|
146
191
|
|
|
147
192
|
**Phase status.** A phase's state in Linear comes from the `⬜`/`🔄`/`✅` on its
|
|
148
193
|
phase-file **heading** — not from its `> **Status:**` line and not from the
|
package/assets/core/SETUP.md
CHANGED
|
@@ -73,28 +73,83 @@ claude mcp list
|
|
|
73
73
|
> `/spec-status` (drift report) without granting write access. `/spec-push`
|
|
74
74
|
> needs the full (writable) endpoint.
|
|
75
75
|
|
|
76
|
-
## 3.
|
|
76
|
+
## 3. Configure — run `/spec-linear-setup`
|
|
77
77
|
|
|
78
|
-
|
|
79
|
-
ask Claude once the MCP server is connected:
|
|
78
|
+
With the MCP server connected, let the skill do it:
|
|
80
79
|
|
|
81
|
-
|
|
80
|
+
```
|
|
81
|
+
/spec-linear-setup
|
|
82
|
+
```
|
|
83
|
+
|
|
84
|
+
It discovers your workspace (teams, projects, labels, issue workflow states),
|
|
85
|
+
asks how the work is **organised**, and writes `specs/.core/linear.config.json`
|
|
86
|
+
for you. You pick from real lists — no UUID is ever typed by hand.
|
|
87
|
+
|
|
88
|
+
The questions it asks, and why each one matters:
|
|
89
|
+
|
|
90
|
+
| It asks | Because |
|
|
91
|
+
|---------|---------|
|
|
92
|
+
| Which team does this repo file into? | The config pins **one team per repo**. With several teams the real question is which product's work this repo holds. |
|
|
93
|
+
| Are products split by team, or by project? | Team-per-product ⇒ `teamId` *is* the product and `projectId` stays empty. Project-per-product ⇒ one team, and `projectId` is the picker's default. |
|
|
94
|
+
| Which labels drive intake? | `intake.label` is the inbox `/spec --from-issue` browses; `bugLabels`/`hotfixLabels` route an issue to `/spec-bug` or `/spec-hotfix`. Optional — "none" leaves intake off. |
|
|
95
|
+
|
|
96
|
+
### What setup validates
|
|
97
|
+
|
|
98
|
+
The skill hands your answers to `skitterspec spec-sync init-config`, which
|
|
99
|
+
**checks them before writing** — the config is never composed by the model.
|
|
100
|
+
|
|
101
|
+
The check that earns its keep is on the **workflow-state names**.
|
|
102
|
+
`states` maps each lifecycle bucket to a Linear issue-state name, and
|
|
103
|
+
**Linear silently ignores an issue state it doesn't recognise**: no error, no
|
|
104
|
+
warning. So a workspace that renamed `Done` to `Shipped` would push perfectly
|
|
105
|
+
clean and produce a mirror that *never moves* — and you'd find out weeks later
|
|
106
|
+
wondering why nothing in Linear reflects your specs. Setup compares the
|
|
107
|
+
configured names against the workspace's real ones and refuses, naming the flag
|
|
108
|
+
that fixes each:
|
|
109
|
+
|
|
110
|
+
```
|
|
111
|
+
states.complete: "Done" is not an issue state in this workspace
|
|
112
|
+
pass --state complete="Shipped"
|
|
113
|
+
```
|
|
114
|
+
|
|
115
|
+
It also writes **only the keys that differ from the defaults**, so the file shows
|
|
116
|
+
the handful of choices that are actually yours and keeps inheriting the rest as
|
|
117
|
+
they improve.
|
|
118
|
+
|
|
119
|
+
**Re-running is safe.** With a config already present the skill *reviews* it
|
|
120
|
+
against the live workspace rather than replacing it — the quickest way to find
|
|
121
|
+
out that a team was archived or a state renamed. It only rewrites if you ask.
|
|
122
|
+
|
|
123
|
+
## 4. Configure by hand (if you're not using Claude Code)
|
|
124
|
+
|
|
125
|
+
The skill is the recommended path, not the only one — the CLI works on its own.
|
|
82
126
|
|
|
83
|
-
|
|
127
|
+
**Find your team id.** Ask Claude ("List my Linear teams with their ids"), or
|
|
128
|
+
call Linear's `list_teams` yourself. You'll get rows like:
|
|
84
129
|
|
|
85
130
|
```
|
|
86
131
|
Skitterspec — e07c2b54-dcf6-4b6e-81bd-175a9bc79868 (key: SKI)
|
|
87
132
|
```
|
|
88
133
|
|
|
89
|
-
Copy the `id` (the UUID)
|
|
90
|
-
of your specs belong to one **Project**, ask "list my Linear projects" and copy
|
|
91
|
-
that id into `linear.projectId` — it becomes the *default* the project picker
|
|
92
|
-
pre-selects, not a fixed destination.
|
|
134
|
+
Copy the `id` (the UUID); the `key` (e.g. `SKI`) is the short human handle.
|
|
93
135
|
|
|
94
|
-
|
|
136
|
+
**Then either run the engine directly:**
|
|
95
137
|
|
|
96
|
-
|
|
97
|
-
|
|
138
|
+
```sh
|
|
139
|
+
skitterspec spec-sync init-config \
|
|
140
|
+
--team-id e07c2b54-dcf6-4b6e-81bd-175a9bc79868 --team-key SKI \
|
|
141
|
+
--intake-label web-app --bug-labels bug \
|
|
142
|
+
--states states.json # a JSON array of your issue-state names
|
|
143
|
+
```
|
|
144
|
+
|
|
145
|
+
`--states` is optional here (unlike `/spec-push`'s check, which is mandatory) —
|
|
146
|
+
without a config there is nothing to read a team from, so the names can't be
|
|
147
|
+
fetched first. Pass it if you have them; without it the command writes and tells
|
|
148
|
+
you the names are unverified.
|
|
149
|
+
|
|
150
|
+
**Or copy the example and edit it.** `init` dropped a
|
|
151
|
+
`specs/.core/linear.config.json.example`; the team id is the only required
|
|
152
|
+
field:
|
|
98
153
|
|
|
99
154
|
```jsonc
|
|
100
155
|
// specs/.core/linear.config.json
|
|
@@ -115,6 +170,10 @@ Everything else (state names, field ownership) has sensible defaults — see
|
|
|
115
170
|
`linear.config.md` to customise. The moment this file exists, the Linear steps in
|
|
116
171
|
`/spec` and `/spec-go` and the three sync skills switch on.
|
|
117
172
|
|
|
173
|
+
> Editing by hand skips the state-name check described above. If you go this
|
|
174
|
+
> route and your workspace renamed any state, `/spec-push` catches it at your
|
|
175
|
+
> first push instead.
|
|
176
|
+
|
|
118
177
|
## 5. Link a spec to a Linear issue
|
|
119
178
|
|
|
120
179
|
A spec syncs once its `00-overview.md` frontmatter carries a `linear_identifier`.
|
|
@@ -199,7 +258,8 @@ With a linked spec, confirm push end-to-end:
|
|
|
199
258
|
pass the file yourself.
|
|
200
259
|
- **"refusing — configured state name(s) not in the workspace"** — Linear ignores
|
|
201
260
|
an unknown issue state, so this is caught before the push rather than after.
|
|
202
|
-
|
|
261
|
+
The refusal names the replacement for each; re-run `/spec-linear-setup` to fix
|
|
262
|
+
it, or edit `linear.config.json` → `states` to the real issue-state names
|
|
203
263
|
(`Backlog / Todo / In Progress / Done / Canceled`). Upgrading from 8.x, the
|
|
204
264
|
value inverts: project status `Completed` → issue state `Done`.
|
|
205
265
|
- **Reconnecting doesn't switch workspace** — Linear ties the OAuth session to one
|
|
@@ -6,7 +6,8 @@
|
|
|
6
6
|
},
|
|
7
7
|
"intake": {
|
|
8
8
|
"label": "",
|
|
9
|
-
"bugLabels": []
|
|
9
|
+
"bugLabels": [],
|
|
10
|
+
"hotfixLabels": []
|
|
10
11
|
},
|
|
11
12
|
"mapping": {
|
|
12
13
|
"specFolder": "issue",
|
|
@@ -25,6 +26,12 @@
|
|
|
25
26
|
"branch": {
|
|
26
27
|
"pattern": "{type}/{slug}"
|
|
27
28
|
},
|
|
29
|
+
"auth": {
|
|
30
|
+
"keyEnv": "LINEAR_API_KEY"
|
|
31
|
+
},
|
|
32
|
+
"apply": {
|
|
33
|
+
"transport": ""
|
|
34
|
+
},
|
|
28
35
|
"sync": {
|
|
29
36
|
"baseDir": "specs/.core/linear-base",
|
|
30
37
|
"fieldOwnership": {
|
|
@@ -35,10 +35,11 @@ absence). A `sync.fieldOwnership` value outside `both|pull|push` is a hard error
|
|
|
35
35
|
},
|
|
36
36
|
|
|
37
37
|
// Issue intake: `/spec <ISSUE-REF>` adopts any issue; `/spec --from-issue`
|
|
38
|
-
// browses the inbox.
|
|
38
|
+
// browses the inbox. All optional.
|
|
39
39
|
"intake": {
|
|
40
40
|
"label": "", // inbox filter — the label the web app files under
|
|
41
|
-
"bugLabels": []
|
|
41
|
+
"bugLabels": [], // e.g. ["bug"] — these route to /spec-bug instead
|
|
42
|
+
"hotfixLabels": [] // e.g. ["production"] — these route to /spec-hotfix
|
|
42
43
|
},
|
|
43
44
|
|
|
44
45
|
// How a spec's parts map onto Linear objects: a spec is an Issue, each phase a
|
|
@@ -175,15 +176,103 @@ altered is reported with both sides of the first difference. It warns rather tha
|
|
|
175
176
|
fails — the repo is unaffected and re-pushing overwrites the mirror. This is not
|
|
176
177
|
a pull: nothing read back is merged, stamped or written anywhere.
|
|
177
178
|
|
|
178
|
-
##
|
|
179
|
+
## How the push reaches Linear — `auth` and `apply`
|
|
179
180
|
|
|
180
|
-
`
|
|
181
|
+
By default `/spec-push` applies its plan **over MCP**: the assistant makes one
|
|
182
|
+
`save_issue` call per object. That works with no setup at all — MCP carries the
|
|
183
|
+
Linear session you are already signed into — but it means every issue description
|
|
184
|
+
is generated as output tokens, and read back the same way, so a large push is
|
|
185
|
+
bounded by the model rather than by Linear.
|
|
186
|
+
|
|
187
|
+
Set a **Linear personal API key** and the engine talks to Linear directly
|
|
188
|
+
instead:
|
|
189
|
+
|
|
190
|
+
```bash
|
|
191
|
+
export LINEAR_API_KEY=lin_api_… # from Linear → Settings → Security & access
|
|
192
|
+
```
|
|
193
|
+
|
|
194
|
+
```json
|
|
195
|
+
"auth": { "keyEnv": "LINEAR_API_KEY" },
|
|
196
|
+
"apply": { "transport": "" }
|
|
197
|
+
```
|
|
198
|
+
|
|
199
|
+
- **`auth.keyEnv`** names the environment variable the key is read from.
|
|
200
|
+
It names the **variable, never the key** — nothing secret is ever written to
|
|
201
|
+
this file or to the repo.
|
|
202
|
+
- **`apply.transport`** pins the transport: `"api"`, `"mcp"`, or `""` (the
|
|
203
|
+
default) to decide per run — the API when a key is present, MCP when it isn't.
|
|
204
|
+
`spec-sync apply --via <api|mcp>` overrides it for one run.
|
|
205
|
+
|
|
206
|
+
With a key present, `/spec-push` runs a single `spec-sync apply`, which writes,
|
|
207
|
+
reads back what Linear stored, stamps the ids into your spec and records the
|
|
208
|
+
snapshot. Descriptions never pass through the assistant in either direction.
|
|
209
|
+
|
|
210
|
+
**An interrupted run is safe to repeat.** Each id is written into the spec as soon
|
|
211
|
+
as its object exists, so re-running applies only what is still missing.
|
|
212
|
+
It never mints a second copy of an issue it already created. There is no ledger
|
|
213
|
+
to keep in step; the spec files are the record.
|
|
214
|
+
|
|
215
|
+
Without a key, nothing changes: the MCP path is fully supported and remains the
|
|
216
|
+
default for anyone who never sets one.
|
|
217
|
+
|
|
218
|
+
## How phases are mirrored — `mapping.phases`
|
|
219
|
+
|
|
220
|
+
`mapping.phases` decides *whether and when* a phase becomes a sub-issue:
|
|
181
221
|
|
|
182
222
|
- `"subissue"` (default) — from the spec's first push. A spec costs `1 + N`
|
|
183
223
|
`save_issue` calls to mirror, N being its phase count.
|
|
184
224
|
- `"deferred"` — only once the work starts. A spec sitting in `specs/backlog/`
|
|
185
225
|
mirrors as **the issue alone**; its sub-issues are created by the push that
|
|
186
226
|
follows `/spec-go`.
|
|
227
|
+
- `"inline"` — never. Each phase becomes a **section of the spec issue's own
|
|
228
|
+
description**, with its full task list, and the `## Phases` index stays as the
|
|
229
|
+
table of contents. One issue per spec, however many phases it has.
|
|
230
|
+
|
|
231
|
+
### One mode, or one per bucket
|
|
232
|
+
|
|
233
|
+
Either form is valid:
|
|
234
|
+
|
|
235
|
+
```json
|
|
236
|
+
"mapping": { "phases": "subissue" }
|
|
237
|
+
```
|
|
238
|
+
|
|
239
|
+
```json
|
|
240
|
+
"mapping": {
|
|
241
|
+
"phases": { "backlog": "subissue", "in-progress": "subissue", "complete": "inline" }
|
|
242
|
+
}
|
|
243
|
+
```
|
|
244
|
+
|
|
245
|
+
A **scalar** applies one mode to the whole repo — that is what every config was
|
|
246
|
+
before per-bucket mapping, and it still means exactly what it meant. A **map**
|
|
247
|
+
keys the mode by the spec's lifecycle bucket: `backlog`, `in-progress`,
|
|
248
|
+
`complete`, `cancelled`. A bucket the map **omits defaults to `subissue`**, so a
|
|
249
|
+
partial map adds an exception for the buckets it names rather than quietly
|
|
250
|
+
changing the ones it does not.
|
|
251
|
+
|
|
252
|
+
An unknown key or an unknown mode is a **loud error** at load, not a fallback: a
|
|
253
|
+
misspelt `"completed"` that read as "the map said nothing" would go on minting
|
|
254
|
+
exactly the sub-issues the config was written to stop, and look deliberate doing
|
|
255
|
+
it.
|
|
256
|
+
|
|
257
|
+
Why per bucket rather than per repo: phases became sub-issues so that parallel
|
|
258
|
+
agents could be assigned one each. That reasoning holds for work in flight and
|
|
259
|
+
does not hold for work that finished long ago — a repo with 250 completed specs
|
|
260
|
+
gets 669 sub-issues nobody will ever read. Set `complete: "inline"` and those
|
|
261
|
+
mirror as 250 readable issues, while the backlog keeps the assignable sub-issues
|
|
262
|
+
that made the choice worth it.
|
|
263
|
+
|
|
264
|
+
### Switching modes is non-destructive
|
|
265
|
+
|
|
266
|
+
**A phase already carrying a `linear_issue_id` keeps its sub-issue in every
|
|
267
|
+
mode**, and is never *also* inlined. One-way sync has no delete op, so
|
|
268
|
+
withholding a live sub-issue would not remove it from Linear — it would freeze it
|
|
269
|
+
there, never updated again. So changing `mapping.phases` only ever changes what
|
|
270
|
+
has yet to be minted, and a spec part-way through keeps a coherent mirror.
|
|
271
|
+
|
|
272
|
+
**Adopting on an established repo:** set `complete: "inline"` (and `"deferred"`
|
|
273
|
+
or `"inline"` for `backlog`) **before** the first backfill push. Finished specs
|
|
274
|
+
then never mint sub-issues at all, rather than minting them and stranding them.
|
|
275
|
+
|
|
187
276
|
|
|
188
277
|
Deferral is worth setting when you adopt sync on a project that already has a
|
|
189
278
|
long backlog, where the default means mirroring every phase of every spec nobody
|
|
@@ -208,11 +297,43 @@ What defers and what does not:
|
|
|
208
297
|
- **`/spec-push` and `/spec-status` say so**, printing `N phase(s) deferred`, and
|
|
209
298
|
the JSON plan carries a `phasesDeferred` count — a spec with no sub-issues
|
|
210
299
|
reads as deliberate rather than as phase files that failed to parse.
|
|
300
|
+
- **Both also print the resolved mode** as `phases: <mode>` whenever it is not
|
|
301
|
+
the default, naming the bucket it resolved through, and the JSON plan always
|
|
302
|
+
carries it as `phaseMode`. With a per-bucket map the config alone no longer
|
|
303
|
+
tells you which mode a given spec got, so the report is where that is stated.
|
|
211
304
|
|
|
212
305
|
There is no snapshot state behind this and nothing to migrate: the last-pushed
|
|
213
306
|
snapshot only ever recorded sub-issues that have an id, so a deferred phase is
|
|
214
307
|
simply absent from it and arrives as an ordinary `create` when it projects.
|
|
215
308
|
|
|
309
|
+
### What `inline` renders
|
|
310
|
+
|
|
311
|
+
Each unlinked phase is appended to the description as a `###` section carrying
|
|
312
|
+
the **same body its sub-issue would have had** — the identical composer, so
|
|
313
|
+
`inline` inherits every fidelity guarantee the sub-issue form has rather than
|
|
314
|
+
being a second, thinner projection. The body's own headings are demoted to nest
|
|
315
|
+
under that `###` (a phase's `## Tasks` would otherwise read as a sibling of the
|
|
316
|
+
spec's `## Problem` and drag every later phase under it).
|
|
317
|
+
|
|
318
|
+
```markdown
|
|
319
|
+
## Phases
|
|
320
|
+
|
|
321
|
+
| # | Phase | Status | File |
|
|
322
|
+
| 1 | Mode resolver | ✅ | [01-mode-resolver.md](01-mode-resolver.md) |
|
|
323
|
+
|
|
324
|
+
### Phase 1 — Mode resolver ✅
|
|
325
|
+
|
|
326
|
+
**Goal:** one resolver decides the mode for a spec.
|
|
327
|
+
|
|
328
|
+
#### Tasks
|
|
329
|
+
|
|
330
|
+
- [x] Extend the config loader
|
|
331
|
+
```
|
|
332
|
+
|
|
333
|
+
The phase heading is the phase file's h1 **as written**, emoji included: a
|
|
334
|
+
sub-issue projects its title as `name` and its status emoji as `state`, and
|
|
335
|
+
inlined there are no such fields for either to live in.
|
|
336
|
+
|
|
216
337
|
## Which Project a spec issue belongs to
|
|
217
338
|
|
|
218
339
|
`linear.projectId` is the **default**, not a mandate. When a spec issue is first
|
|
@@ -228,6 +349,28 @@ issue exists, where it lives is Linear's business: move it between projects and
|
|
|
228
349
|
A spec that **adopted** an existing issue (see below) skips the picker entirely —
|
|
229
350
|
it was filed somewhere deliberately.
|
|
230
351
|
|
|
352
|
+
### Known limits — one team per repo, no initiatives
|
|
353
|
+
|
|
354
|
+
Two things this config deliberately cannot express today. Both are limits, not
|
|
355
|
+
oversights; `/spec-linear-setup` says so during setup rather than letting you
|
|
356
|
+
find out later.
|
|
357
|
+
|
|
358
|
+
**One team per repo.** `linear.teamId` is a single value, so every spec in a repo
|
|
359
|
+
files into the same Linear team. If your workspace runs a team per product and a
|
|
360
|
+
repo genuinely spans two of them, sync has no way to say that — pick the team
|
|
361
|
+
that owns most of the work, or split the specs across two checkouts. (Splitting
|
|
362
|
+
products by **project** inside one team has no such limit: that is what
|
|
363
|
+
`projectId` and the picker are for.)
|
|
364
|
+
|
|
365
|
+
**Initiatives are not used for placement.** A spec issue attaches to a team and
|
|
366
|
+
optionally a project — never to an initiative. If your projects are grouped under
|
|
367
|
+
initiatives the grouping still works in Linear; you just pick the project inside
|
|
368
|
+
the initiative, and the initiative follows from it. What's missing is *filtering
|
|
369
|
+
the picker* by initiative, which would matter to a workspace with enough projects
|
|
370
|
+
that the flat list stops being useful. The hook is already there when it does:
|
|
371
|
+
Linear's `list_projects` accepts an `initiative` filter, and the API adapter's
|
|
372
|
+
`listProjects` (`src/api.js`) queries `team(id) { projects }` sending none.
|
|
373
|
+
|
|
231
374
|
## Starting a spec from an existing issue
|
|
232
375
|
|
|
233
376
|
With `intake` configured, a spec can begin life as a Linear issue someone else
|
|
@@ -236,16 +379,40 @@ filed:
|
|
|
236
379
|
- `/spec SKI-123` — adopt that issue.
|
|
237
380
|
- `/spec --from-issue [query]` — browse issues labelled `intake.label` (what your
|
|
238
381
|
web app or feedback form files under), optionally filtered by title.
|
|
382
|
+
- `/spec-bug SKI-123` — same, for a bug.
|
|
383
|
+
- `/spec-hotfix v33.16.4 SKI-123` — same, for a bug that has to be patched on a
|
|
384
|
+
released version. Give the tag or be asked for it; any version the report
|
|
385
|
+
mentions is offered as a suggestion, never used as a default.
|
|
239
386
|
|
|
240
387
|
The issue **becomes** the spec's issue: its identifier is stamped as
|
|
241
|
-
`linear_identifier`, phases become its sub-issues, and the
|
|
242
|
-
replaces its description with the spec. The
|
|
243
|
-
subscribers stay on the one issue everyone is
|
|
244
|
-
words are carried into the spec's **Problem**
|
|
388
|
+
`linear_identifier`, phases become its sub-issues, and the **linking push**
|
|
389
|
+
— which runs as the spec is created — replaces its description with the spec. The
|
|
390
|
+
reporter's comments, links and subscribers stay on the one issue everyone is
|
|
391
|
+
already watching; their original words are carried into the spec's **Problem**
|
|
392
|
+
(or **Symptom**) section, and Linear keeps the original in the issue's history.
|
|
245
393
|
|
|
246
394
|
An issue already stamped on a spec can't be adopted twice — `skitterspec spec-sync
|
|
247
|
-
linked` is the list that's checked.
|
|
248
|
-
|
|
395
|
+
linked` is the list that's checked.
|
|
396
|
+
|
|
397
|
+
### Routing an issue to the right skill
|
|
398
|
+
|
|
399
|
+
Two optional label lists send an issue to a more specific skill, checked in this
|
|
400
|
+
order:
|
|
401
|
+
|
|
402
|
+
| List | Routes to | Means |
|
|
403
|
+
|------|-----------|-------|
|
|
404
|
+
| `intake.hotfixLabels` | `/spec-hotfix` | broken in production — patch the released version |
|
|
405
|
+
| `intake.bugLabels` | `/spec-bug` | a bug, fixed on `main` like any other |
|
|
406
|
+
|
|
407
|
+
**`hotfixLabels` wins when an issue carries both.** The two mistakes are not
|
|
408
|
+
equally costly: routing a production issue to `/spec-bug` produces a fix that
|
|
409
|
+
lands on `main` and never reaches the running version — noticed only when someone
|
|
410
|
+
asks why it hasn't shipped. The reverse is a hotfix branch for something that
|
|
411
|
+
could have waited.
|
|
412
|
+
|
|
413
|
+
`/spec-bug` still checks `hotfixLabels` — being in the bug path is not a reason to
|
|
414
|
+
miss that production is broken. `/spec-hotfix` checks neither; it is already the
|
|
415
|
+
most specific destination. Leave a list empty and nothing routes through it.
|
|
249
416
|
|
|
250
417
|
## One direction — nothing to reconcile
|
|
251
418
|
|
|
@@ -66,17 +66,23 @@ Beneath it, `skitterspec spec-env live <take|release|abort|status>` is the engin
|
|
|
66
66
|
|
|
67
67
|
**Ticketing-provider sync (opt-in, a separate package).** The base is
|
|
68
68
|
tracker-free: it knows nothing about any specific ticketing system. A
|
|
69
|
-
ticketing provider is installed as its own distribution that plugs into
|
|
70
|
-
**seams** in the shared skills
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
69
|
+
ticketing provider is installed as its own distribution that plugs into named
|
|
70
|
+
**seams** in the shared skills and fulfils a skill-name + CLI contract.
|
|
71
|
+
Sync is **one-way**: the repo is the source of truth and the tracker is a
|
|
72
|
+
**generated mirror**. It ships `/spec-push` (repo→tracker; computes a create/update plan
|
|
73
|
+
against a committed last-pushed snapshot and applies it) and `/spec-status`
|
|
74
|
+
(read-only drift report — what would push, and whether the tracker's
|
|
75
|
+
workflow-state drifted), backed by a `spec-sync` CLI. There is no content pull —
|
|
76
|
+
the tracker is never read back or merged.
|
|
77
|
+
|
|
78
|
+
**Every skill that moves a spec through the lifecycle carries a seam**, so the
|
|
79
|
+
mirror keeps up without anyone remembering to push: `/spec`, `/spec-bug` and
|
|
80
|
+
`/spec-hotfix` link the spec they create; `/spec-go` refreshes it as work starts;
|
|
81
|
+
`/spec-complete`, `/spec-cancel` and `/spec-review` refresh it after they change
|
|
82
|
+
it. `/spec-to-main` and `/spec-live` carry none — they change no status.
|
|
83
|
+
With no provider installed the seams are empty and every skill behaves as a plain
|
|
84
|
+
filesystem workflow. See the provider package's own docs for its config and field
|
|
85
|
+
reference.
|
|
80
86
|
|
|
81
87
|
## Project conventions (fill this in)
|
|
82
88
|
|