@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 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` · `status` · `linked`.
86
-
87
- The shared `/spec`, `/spec-bug` and `/spec-go` skills come composed with the
88
- Linear steps filled in: `/spec` asks which Linear **Project** the spec belongs to,
89
- then links it to a Linear issue (a sub-issue per phase). `/spec` and `/spec-bug`
90
- can also start **from** an existing issue — `/spec SKI-123`, or `/spec
91
- --from-issue` to browse the ones your web app filed. There is no pull — the repo
92
- is already canonical, so `/spec-go` just builds.
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
- **Adopting on a long backlog.** By default a spec costs one `save_issue` call
125
- plus one per phase, so mirroring a backlog that already runs to dozens of specs
126
- front-loads hundreds of calls for work nobody has started. Set
127
- `mapping.phases: "deferred"` and a spec sitting in `specs/backlog/` mirrors as
128
- **the issue alone**, keeping its phase list in the description; its sub-issues are
129
- created by the push that follows `/spec-go`, when the work actually starts. Phases
130
- that are already linked keep syncing either way, so switching an existing project
131
- over never strands a live sub-issue. See `linear.config.md` for the details.
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.label` / `intake.bugLabels` in the config):
140
- `/spec SKI-123` adopts that issue, `/spec --from-issue [query]` browses the inbox.
141
- The issue *becomes* the spec's issuethe reporter's thread, comments and links
142
- stay put, their words are carried into the spec's **Problem**, and the first push
143
- replaces the description with the spec. A bug-labelled issue routes to
144
- `/spec-bug`, which adopts it the same way. `skitterspec-linear spec-sync linked`
145
- lists what's already adopted, so an issue never becomes two specs.
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
@@ -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. Find your team id
76
+ ## 3. Configure run `/spec-linear-setup`
77
77
 
78
- `linear.config.json` needs your Linear **team UUID**. The easiest way is to just
79
- ask Claude once the MCP server is connected:
78
+ With the MCP server connected, let the skill do it:
80
79
 
81
- > "List my Linear teams with their ids."
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
- It calls the Linear `list_teams` tool and returns rows like:
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). The `key` (e.g. `SKI`) is the short human handle. If most
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
- ## 4. Scaffold the config
136
+ **Then either run the engine directly:**
95
137
 
96
- `init` dropped a `specs/.core/linear.config.json.example`. Copy it and fill in the
97
- ids from step 3 — the team id is the only required field:
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
- Fix `linear.config.json` to the real issue-state names
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. Both optional.
38
+ // browses the inbox. All optional.
39
39
  "intake": {
40
40
  "label": "", // inbox filter — the label the web app files under
41
- "bugLabels": [] // e.g. ["bug"] — these route to /spec-bug instead
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
- ## Deferring sub-issues until a spec starts
179
+ ## How the push reaches Linear — `auth` and `apply`
179
180
 
180
- `mapping.phases` decides *when* a phase becomes a sub-issue:
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 first `/spec-push`
242
- replaces its description with the spec. The reporter's comments, links and
243
- subscribers stay on the one issue everyone is already watching; their original
244
- words are carried into the spec's **Problem** section.
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. An issue labelled with one of
248
- `intake.bugLabels` routes to `/spec-bug` instead, which adopts it identically.
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 two named
70
- **seams** in the shared skills (`/spec` Phase E, `/spec-go` step 3b) and fulfils a
71
- skill-name + CLI contract. Sync is **one-way**: the repo is the source of truth
72
- and the tracker is a **generated mirror**. It ships `/spec-push` (repo→tracker;
73
- computes a create/update plan against a committed last-pushed snapshot and applies
74
- it) and `/spec-status` (read-only drift report — what would push, and whether the
75
- tracker's workflow-state drifted), backed by a `spec-sync` CLI. There is no
76
- content pull — the tracker is never read back or merged. When a provider is
77
- present, `/spec` also links the spec to the tracker. With no provider installed
78
- the seams are empty and every skill behaves as a plain filesystem workflow. See
79
- the provider package's own docs for its config and field reference.
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