@skitterbyte/skitterspec-linear 10.0.1 → 10.2.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.
@@ -1,6 +1,6 @@
1
1
  ---
2
2
  name: spec-hotfix
3
- description: Fix a production bug on the exact released version — fork a worktree from a release tag, drive it red→green like /spec-bug, then land it by tagging a new patch (for CI/CD to deploy) and cherry-picking the fix back onto main. ALWAYS starts from a base tag and works on the hotfix's own branch, never on main. Creates specs/in-progress/hotfix-<name>/00-overview.md. Use when the user says "/spec-hotfix", "hotfix <tag>", "prod is broken on <version>", "patch the released version", or needs a fix shipped against a tagged release rather than main.
3
+ description: Fix a production bug on the exact released version — fork a worktree from a release tag, drive it red→green like /spec-bug, then land it by tagging a new patch (for CI/CD to deploy) and cherry-picking the fix back onto main. ALWAYS starts from a base tag and works on the hotfix's own branch, never on main. Can start from a tracker issue when a provider is installed (`/spec-hotfix <tag> <ISSUE-REF>`), adopting it as the hotfix's issue. Creates specs/in-progress/hotfix-<name>/00-overview.md. Use when the user says "/spec-hotfix", "hotfix <tag>", "prod is broken on <version>", "patch the released version", or needs a fix shipped against a tagged release rather than main.
4
4
  ---
5
5
 
6
6
  # /spec-hotfix — fix a released version, tag it, cherry-pick back to main
@@ -20,11 +20,114 @@ Spec type convention (see `.claude/rules/spec-planning.md`):
20
20
  tag + cherry-pick — it needs the isolation engine (`specs/.core/env.config.json`).
21
21
  If isolation is absent, say so and stop; there is no in-place path.
22
22
 
23
+ ## Phase 0 — start from a Linear issue (only when asked)
24
+
25
+ **Only when `specs/.core/linear.config.json` exists** *and* the invocation names an
26
+ issue. Otherwise skip this phase entirely and grill from the user's own words.
27
+
28
+ Two ways in:
29
+
30
+ - **`<ISSUE-REF>`** (`SKI-123`, an issue URL, or a bare id anywhere in the
31
+ arguments) — adopt that issue.
32
+ - **`--from-issue [query]`** — browse the intake inbox: issues carrying
33
+ `intake.label` (what the web app files under). With a `query`, filter that list
34
+ case-insensitively by title. Show the shortlist and let the user pick one.
35
+
36
+ Then:
37
+
38
+ 1. **Exclude what's already adopted.** Run `skitterspec spec-sync linked --json`
39
+ and drop any issue whose identifier is already stamped on a spec. If the user
40
+ named such an issue directly, **stop** and point them at that spec — one issue
41
+ never becomes two specs.
42
+ 2. **Read the issue** with the discovered issue-read tool: title, description,
43
+ labels, reporter, URL. If Linear isn't connected, say so and offer to carry on
44
+ without it — a spec written from the user's own description is still a spec.
45
+ 3. **Route the issue to the right skill.** Match the issue's labels
46
+ (case-insensitively) against two lists, **checking `hotfixLabels` first**:
47
+
48
+ - **`intake.hotfixLabels`** → this is broken in **production**, so it needs a
49
+ fix against the released version, not `main`. Say so, name the matching
50
+ label, and tell the user to run **`/spec-hotfix <ISSUE-REF>`** — then stop.
51
+ - **`intake.bugLabels`** → a bug report, not a feature request. Say so, name
52
+ the label, and tell the user to run **`/spec-bug <ISSUE-REF>`** — then stop,
53
+ without authoring a Feature spec.
54
+
55
+ **Hotfix wins when an issue carries both**, and deliberately: production is the
56
+ more specific destination, and the two mistakes are not equally costly. Routing
57
+ a prod issue to `/spec-bug` produces a fix that lands on `main` and never
58
+ reaches the running version — discovered only when someone asks why it hasn't
59
+ shipped. The reverse is a hotfix branch for something that could have waited,
60
+ which is merely wasteful.
61
+
62
+ With a list unset, nothing routes through it; with both unset every issue is
63
+ treated as a feature request, exactly as before.
64
+
65
+ **Which checks run depends on where you are**, because a skip is about not
66
+ bouncing someone to the skill they are already in — not about ignoring an
67
+ escalation:
68
+
69
+ - **In `/spec`** — both checks run.
70
+ - **In `/spec-bug`** — the bug check is skipped (it would route you to
71
+ yourself), but the **hotfix check still runs**. A bug report labelled for
72
+ production is not "already handled" by being in the bug path: `/spec-bug`
73
+ fixes on `main`, and prod would stay broken. Say so and hand off.
74
+ - **In `/spec-hotfix`** — both are skipped. It is already the most specific
75
+ destination; there is nowhere left to route.
76
+
77
+ Then adopt the issue and reproduce it as usual: the issue body is your repro
78
+ material, and the failing test comes before the spec exactly as normal.
79
+ 4. **Seed, don't skip, the grill.** The issue's title becomes the working spec
80
+ title and its description the starting material for **Problem** — or
81
+ **Symptom** in a bug or hotfix spec, which is where a report belongs. Quote the
82
+ reporter's words rather than paraphrasing them away. Note the reporter and the
83
+ issue URL for context. Everything after this runs exactly as normal: an issue
84
+ is a *request*, not a groomed spec, so grill it as hard as anything else.
85
+
86
+ **In `/spec-hotfix`, also mine the issue for a version.** Any release-shaped
87
+ string in the report is a *suggestion* for the base tag, offered when the skill
88
+ asks which version prod is running — never used as a default. The reporter's
89
+ version is the one they saw the bug on, which is often not what is deployed.
90
+
91
+ ### Adopting the issue
92
+
93
+ The issue **becomes** the spec's issue — it is not copied and no second issue is
94
+ minted. This is identical in `/spec`, `/spec-bug` and `/spec-hotfix`; only *when*
95
+ it happens differs (`/spec` writes the spec in Phase B, `/spec-bug` in its step 4,
96
+ `/spec-hotfix` in its step 5). Once the spec file exists:
97
+
98
+ - **Stamp `linear_identifier` and `linear_url`** in `00-overview.md` frontmatter
99
+ from the adopted issue. That is the whole link: every later skill
100
+ (`/spec-push`, `/spec-status`, `/spec-go`) keys off it being present.
101
+ - **Do not run the project picker** and never send `project`. The issue was filed
102
+ somewhere deliberately — where it lives is Linear's business, and adoption is
103
+ not a mint.
104
+ - **Do not write a base sidecar.** Leaving `sync.baseDir` empty for this spec is
105
+ what makes the **linking push** send the spec over the reporter's original
106
+ description (an **update** to the existing issue, plus a sub-issue per phase).
107
+ Recording a snapshot here would declare the mirror already in sync and strand
108
+ the issue showing the raw report forever.
109
+ - **Say what will happen** in the finish-up message. The linking step runs right
110
+ after the spec is written, so the issue's description is replaced by the spec
111
+ **then** — not on some later manual push. The reporter's words are not lost:
112
+ they are quoted in the spec's **Problem** (or **Symptom**) section, and Linear
113
+ keeps the original in the issue's history.
114
+
23
115
  ## 1. Establish the base version (the tag)
24
116
 
25
- - Take the release tag from the argument — `/spec-hotfix <tag> <name>` (e.g.
26
- `/spec-hotfix v33.16.4 login-crash`). If it's missing, **ask which version prod
27
- is running** don't guess.
117
+ - **Read the arguments.** `/spec-hotfix <tag> <name>` (e.g.
118
+ `/spec-hotfix v33.16.4 login-crash`). An argument shaped like an issue
119
+ referenceletters, a hyphen, digits (`SKI-123`) —
120
+ is **always a reference, never a name**, so `/spec-hotfix v33.16.4 SKI-123` and
121
+ `/spec-hotfix SKI-123`
122
+ both mean "adopt that issue". Release tags don't take that shape, so the two
123
+ can't be confused. With no name and no reference, ask what to call it.
124
+ - Take the release tag from the argument. If it's missing,
125
+ **ask which version prod is running** — don't guess. When an issue was adopted
126
+ above, **offer any versions it mentions** as suggestions — clearly labelled as
127
+ the reporter's words, not a default — and still wait for the answer. A reporter
128
+ usually names
129
+ the version they *saw* the bug on, which is not necessarily what is deployed,
130
+ and a hotfix forked from the wrong tag fails late.
28
131
  - **Verify the tag exists** before anything else:
29
132
  `git rev-parse --verify <tag>^{commit}`. If it doesn't resolve, stop and ask.
30
133
 
@@ -156,6 +259,56 @@ place of the table when the spec touches no external surface.>
156
259
  Keep the **State log** (state transitions) separate from the **Changelog** (fix
157
260
  narrative and decisions).
158
261
 
262
+ **Only when `specs/.core/linear.config.json` exists** (Linear sync is opted in).
263
+ If it's absent, skip this entirely — the spec stays local-only and the skill
264
+ behaves exactly as above. When present, link the spec you just wrote so status
265
+ and discussion live in Linear while the repo stays the source of truth. A spec is
266
+ a Linear **issue**; each phase is a **sub-issue**, carrying that phase's tasks in
267
+ its description as a read-only checklist.
268
+
269
+ **Linking is just the first push**, so it runs the same engine path `/spec-push`
270
+ does — you never create the issue by hand:
271
+
272
+ 1. **Pick the transport.** `skitterspec spec-sync states --json`. With a Linear
273
+ API key set it answers `api` and prints the workspace's state names; without
274
+ one it answers `mcp` and you do the MCP work `/spec-push` describes. Write the
275
+ state names to a file for step 3.
276
+ 2. **Pick the Project** — run the picker in **Picking the Linear Project** below.
277
+ Keep the chosen id for step 4.
278
+ 3. **Get the plan.**
279
+ `skitterspec spec-sync push <spec> --workspace-states <file> --json > plan.json`
280
+ — the spec is unlinked, so this plan is all-creates: the issue and one
281
+ sub-issue per phase.
282
+ 4. **Apply it.**
283
+ `skitterspec spec-sync apply <spec> --plan plan.json --project <chosen id>`.
284
+ That creates the issue and its sub-issues and checks what Linear stored. It
285
+ then **stamps the ids into the spec** — `linear_identifier`/`linear_url` on the
286
+ overview, `linear_issue_id` on each phase — and **records the base snapshot**,
287
+ so `/spec-status` reports in-sync immediately. There is no hand-editing of
288
+ frontmatter and no separate `stamp` or `record` call.
289
+
290
+ If it prints `transport = mcp`, it wrote nothing: apply the plan over MCP as
291
+ `/spec-push` steps 4a–5 describe, ending with `spec-sync stamp` and
292
+ `spec-sync record`. That path is fully supported — it is what anyone without
293
+ an API key uses.
294
+ 5. **Echo the branch name** from `branch.pattern` so the user knows what
295
+ `/spec-go` will fork.
296
+
297
+ **If Linear can't be reached**, say so in one line and leave the spec written and
298
+ local — it is still a perfectly good spec, and `/spec-push` links it later. Do
299
+ nothing destructive.
300
+
301
+ **A spec that adopted an existing issue** (see the intake step) is already
302
+ stamped, so its plan is an **update**, not a create: applying it replaces the
303
+ reporter's description with the spec. That is the one-way rule working as
304
+ intended — the repo is canonical and the original text stays in Linear's history.
305
+ Skip the project picker for an adopted issue: its placement is Linear's.
306
+
307
+ Leave committing to the existing convention (the user commits the spec as usual)
308
+ and **never auto-push git** — Linear's own automation reacts to real branch/PR
309
+ events later. Report the Linear issue URL as part of the skill's finish-up
310
+ message.
311
+
159
312
  ## 6. Drive to GREEN
160
313
 
161
314
  - Implement the **minimal, root-cause** fix on the branch. Match surrounding code;
@@ -0,0 +1,172 @@
1
+ ---
2
+ name: spec-linear-setup
3
+ description: Configure Linear sync for this repo by interview rather than by hand. Discovers the workspace over MCP (teams, projects, labels, issue workflow states), asks how the work is organised — which team this repo files into, whether products are split by team or by project, which labels drive intake — then hands the answers to `skitterspec spec-sync init-config`, which validates them and writes specs/.core/linear.config.json. Checks the configured workflow-state names against the workspace, so a renamed state is caught now instead of silently producing a mirror that never moves. Safe to re-run: an existing config is reviewed, never overwritten without consent. Use when the user says "/spec-linear-setup", "set up Linear sync", "connect this repo to Linear", "configure linear.config.json", or "check my Linear setup".
4
+ ---
5
+
6
+ # /spec-linear-setup — configure Linear sync by interview
7
+
8
+ Writes `specs/.core/linear.config.json`, the file that switches one-way Linear
9
+ sync on for this repo. You **discover and interview**; the engine
10
+ (`spec-sync init-config`) **validates and writes**. Keep that split: composing the
11
+ JSON yourself is how a malformed config gets committed, and the engine already
12
+ rejects everything the loader would later reject.
13
+
14
+ The gap this closes is not "I can't find my team's UUID" — it's that nothing ever
15
+ asks how the workspace is *organised*, so the config ends up describing a shape
16
+ nobody confirmed. Ask about structure first; the ids fall out of the answers.
17
+
18
+ **Prerequisite:** the Linear MCP server connected and authenticated (`SETUP.md`
19
+ step 2). Without it, see step 1's degrade rule — never half-write a config.
20
+
21
+ ## 1. Check what already exists
22
+
23
+ Read `specs/.core/linear.config.json`.
24
+
25
+ - **Absent** — this is a first-time setup. Carry on.
26
+ - **Present** — say so and default to **reviewing** it, not replacing it: run the
27
+ discovery in step 2 and report where the live config disagrees with the
28
+ workspace (a team that no longer exists, a state name that was renamed, an
29
+ intake label that's gone). That is the most useful thing a re-run can do. Only
30
+ write when the user asks for it, and then pass `--force`.
31
+
32
+ Then confirm MCP is reachable by making the first discovery call in step 2.
33
+ **Degrade, never block:** if Linear isn't connected or the tools are missing, say
34
+ so in one line — *"Linear MCP not connected — can't discover the workspace"* —
35
+ point at the manual path in `SETUP.md` ("Configure by hand"), and
36
+ **stop without writing anything**. A half-written config is worse than none:
37
+ every other command switches on the moment the file exists.
38
+
39
+ ## 2. Discover the workspace
40
+
41
+ Discover the Linear MCP read tools at runtime and call them before asking
42
+ anything. The interview offers real lists; it never prompts for a raw id.
43
+
44
+ | Tool | What you need from it |
45
+ |------|----------------------|
46
+ | `list_teams` | every team's name, **key** (e.g. `SKI`) and **id** (UUID) |
47
+ | `list_projects` | the projects, per candidate team — names + ids, minus archived/completed |
48
+ | `list_issue_statuses` | the team's **issue workflow-state names**, exactly as spelled |
49
+ | `list_issue_labels` | the label names available for intake routing |
50
+
51
+ Projects, labels and statuses are all **team-scoped** — fetch them for the team
52
+ once step 3 has settled it, not for the whole workspace up front.
53
+
54
+ ## 3. Which team does this repo file into?
55
+
56
+ **This config pins exactly one team for the whole repo.** Say that plainly
57
+ whenever there is more than one, because it reframes the question: it isn't
58
+ "which team do you like", it's *"which product's work does this repo hold"*.
59
+
60
+ - **One team** — confirm it and move on; there is nothing to decide.
61
+ - **Several teams** — list them (name + key), and ask which one this repo's specs
62
+ belong to. **Recommend** the team whose key or name matches the repo, and say
63
+ why. If the user has one repo per product and a team per product, that mapping
64
+ is the answer.
65
+ - **The repo genuinely spans two teams** — sync can't express that today. Say so,
66
+ and offer the two honest options: pick the team that owns most of the work, or
67
+ split the repo's specs across two checkouts. Don't invent a workaround.
68
+
69
+ ## 4. How is the work split — by team, or by project?
70
+
71
+ The question the old setup never asked. Both shapes are normal in Linear:
72
+
73
+ - **Team per product** — each product gets its own team, its own issue prefix and
74
+ its own cycles. Usual when the products have different people or cadences.
75
+ Then `teamId` *is* the product, and `projectId` stays empty: every spec files
76
+ straight into the team.
77
+ - **Project per product, inside one team** — one team, each product a project.
78
+ Usual when the same people work across products. Then `teamId` is that one
79
+ team, and `projectId` is the **default** project the picker pre-selects.
80
+ - **Neither — projects are per-milestone/quarter** — leave `projectId` empty and
81
+ let the picker ask each time.
82
+
83
+ Ask which shape this workspace uses, offering the discovered project list as
84
+ evidence. **Recommend** based on what you actually see: several teams whose names
85
+ read as products ⇒ team-per-product; one team with product-shaped projects ⇒
86
+ project-per-product.
87
+
88
+ Then set the default: offer the team's live projects plus an explicit
89
+ **None (team only)**, and say what the choice means — `projectId` is the picker's
90
+ *pre-selection*, not a mandate. `/spec` still offers the full list on every spec,
91
+ and a PM re-homing an issue in Linear is never overwritten.
92
+
93
+ > **Initiatives are not used for placement.** A spec issue attaches to a team and
94
+ > optionally a project; if this workspace groups projects under initiatives, pick
95
+ > the project inside the initiative and the grouping keeps working in Linear.
96
+
97
+ ## 5. Which labels drive intake? (optional)
98
+
99
+ Only relevant if the user wants to *start* specs from Linear issues
100
+ (`/spec <ISSUE-REF>`, `/spec --from-issue`). Ask, offering the discovered labels;
101
+ "none" is a fine answer and leaves intake off entirely — a bare issue ref still
102
+ works.
103
+
104
+ - **`intake.label`** — the inbox `/spec --from-issue` browses.
105
+ - **`bugLabels`** — issues carrying one of these route to `/spec-bug`.
106
+ - **`hotfixLabels`** — route to `/spec-hotfix` instead: a bug that must be patched
107
+ on the *released* version. Takes precedence over `bugLabels` on an issue
108
+ carrying both.
109
+
110
+ Recommend the obvious matches from the label list (a `bug` label for `bugLabels`,
111
+ a `production`/`hotfix` label for `hotfixLabels`) rather than asking cold.
112
+
113
+ ## 6. Check the workflow-state names
114
+
115
+ Write the `list_issue_statuses` names to a temp file as a JSON array, exactly as
116
+ Linear spells them:
117
+
118
+ ```json
119
+ ["Backlog", "Todo", "In Progress", "Done", "Canceled"]
120
+ ```
121
+
122
+ Pass it as `--states`. This is the check worth having:
123
+ **Linear silently ignores an unknown issue state**, so a workspace that renamed
124
+ `Done` would push clean and produce a mirror that never moves. Catching it here
125
+ — while the user is still in the setup conversation — is the whole reason the
126
+ engine validates rather than just writes.
127
+
128
+ If the engine refuses, it names each bad state *and the flag that fixes it*:
129
+
130
+ ```
131
+ states.complete: "Done" is not an issue state in this workspace
132
+ pass --state complete="Shipped"
133
+ ```
134
+
135
+ Relay that, confirm the mapping with the user (the suggestion is a suggestion —
136
+ never apply one they didn't agree to), and re-run with the `--state` flags. If it
137
+ made no suggestion for a bucket, **ask** which state means "finished" here rather
138
+ than guessing.
139
+
140
+ ## 7. Write it
141
+
142
+ ```
143
+ skitterspec spec-sync init-config \
144
+ --team-id <uuid> [--team-key KEY] [--project-id <uuid>] \
145
+ [--intake-label <name>] [--bug-labels a,b] [--hotfix-labels a,b] \
146
+ [--state <bucket>=<name> …] \
147
+ --states <statesfile> [--force] [--json]
148
+ ```
149
+
150
+ The engine writes **only the keys that differ from the defaults**, so the file
151
+ shows this repo's choices and keeps inheriting everything else. `--force` is
152
+ required to replace an existing config — never pass it without the user having
153
+ asked for a rewrite in step 1.
154
+
155
+ **Relay the engine's report as printed.** It names the team, the project (or
156
+ "team only"), the intake labels, and how many state names were checked against
157
+ the workspace — that report is the evidence the setup is right, so don't
158
+ paraphrase it into "done".
159
+
160
+ ## 8. Report and hand off
161
+
162
+ Confirm the file written and what it says, then name the next step:
163
+
164
+ - `/spec` — write a spec; with Linear configured it offers the project picker,
165
+ creates the linked issue and stamps the id.
166
+ - `/spec-status` — read-only drift report, the safe way to prove the link works.
167
+ - `/spec-push` — send a spec up.
168
+
169
+ Mention what setup did **not** configure, so the defaults aren't mistaken for
170
+ decisions: phase mapping (`mapping.phases` — sub-issue per phase by default),
171
+ field ownership, and the API key (`LINEAR_API_KEY`, which makes pushes take the
172
+ fast path and never lives in the config). Point at `linear.config.md` for those.
@@ -1,14 +1,17 @@
1
1
  ---
2
2
  name: spec-push
3
- description: Push a spec up to its linked Linear issue (repo → Linear, one-way). The repo is the source of truth; Linear is a generated mirror. A spec is a Linear issue and each phase a sub-issue, with the phase's tasks mirrored read-only into that sub-issue's description. Runs `skitterspec spec-sync push` to get a create/update plan, applies it over MCP (issue description/state, phase sub-issues), stamps the returned ids back into the spec, then records the snapshot. Never reads Linear content back. Opt-in — needs specs/.core/linear.config.json. Use when the user says "/spec-push", "push to Linear", "update the Linear issue from this spec".
3
+ description: Push a spec up to its linked Linear issue (repo → Linear, one-way). The repo is the source of truth; Linear is a generated mirror. A spec is a Linear issue and each phase a sub-issue, with the phase's tasks mirrored read-only into that sub-issue's description. Runs `skitterspec spec-sync push` to get a create/update plan, then applies it with `spec-sync apply` — straight to Linear's API when a key is set (descriptions never pass through the model), or over MCP when it isn't — stamping the returned ids back into the spec and recording the snapshot. Never merges Linear content back. Opt-in — needs specs/.core/linear.config.json. Use when the user says "/spec-push", "push to Linear", "update the Linear issue from this spec".
4
4
  ---
5
5
 
6
6
  # /spec-push — send a spec up to Linear (one-way)
7
7
 
8
8
  Repo → Linear. The repo is the **source of truth**; Linear is a **generated
9
9
  mirror**. A spec is a Linear **issue**; each phase is a **sub-issue** (a child
10
- issue), and a phase's tasks are mirrored into that sub-issue's description as a
11
- read-only checklist never as issues of their own. This skill
10
+ issue), and the phase file is mirrored into that sub-issue's description as
11
+ writtenits prose, its sections, and its tasks as a read-only checklist, never
12
+ as issues of their own. Only the phase's h1 and `> **Status:**` line are left
13
+ out, because both are pushed as fields of their own (the sub-issue's title and
14
+ state). This skill
12
15
  computes what changed since the last push and applies it — it never reads Linear
13
16
  content back or merges. A person editing the mirror in Linear will see it
14
17
  overwritten on the next push.
@@ -20,21 +23,37 @@ tell the user how to enable Linear sync and stop.
20
23
 
21
24
  Use the argument, else the spec in context; ask if unclear.
22
25
 
23
- ## 2. Connect, and validate the issue states
26
+ ## 2. Pick the transport, and get the workspace states
24
27
 
25
- Discover the issue **read + create/update** tools at runtime (`get_issue`,
26
- `save_issue` — a single upsert covers create and update), plus the **project
27
- list** tool if this push will mint the spec issue (see the picker below — it is
28
- optional; without it the picker is skipped, not failed). If Linear isn't
29
- connected or a needed tool is missing, relay the fix and stop, **writing
30
- nothing**.
28
+ **Ask the engine first it decides, not you:**
31
29
 
32
- Then fetch the workspace's issue workflow-state **names** and write them to a
33
- file as a JSON array (e.g. `["Backlog","In Progress","Done","Canceled"]`). Step 3
34
- requires that file: `push` **refuses to run** without it, because Linear silently
35
- ignores an unknown issue state — the description lands, the issue never moves,
36
- and nothing errors. If the check reports a name that isn't in the workspace, stop
37
- and fix `specs/.core/linear.config.json`.
30
+ ```
31
+ skitterspec spec-sync states --json
32
+ ```
33
+
34
+ With a Linear API key set (`LINEAR_API_KEY`, or whatever `auth.keyEnv` names) the
35
+ engine talks to Linear directly. That is the **fast path**, and it is the default
36
+ whenever a key is present: descriptions never pass through you, in either
37
+ direction. Without a key it falls back to MCP and everything below works as it
38
+ always has.
39
+
40
+ - **`transport = api`** → the command prints the workspace's state names as a
41
+ JSON array. Write it to a file and carry on to step 3.
42
+ **Skip the MCP tool discovery entirely** — you make no Linear calls here.
43
+ - **`transport = mcp`** → do the MCP work: discover the issue
44
+ **read + create/update** tools at runtime (`get_issue`, `save_issue` — a single
45
+ upsert covers create and update), plus the **project list** tool if this push will
46
+ mint the spec issue (see the picker below — optional; without it the picker is
47
+ skipped, not failed). If Linear isn't connected or a needed tool is missing,
48
+ relay the fix and stop, **writing nothing**. Then fetch the workspace's issue
49
+ workflow-state **names** yourself and write them to a file as a JSON array
50
+ (e.g. `["Backlog","In Progress","Done","Canceled"]`).
51
+
52
+ Either way you end up with a states file. Step 3 requires it:
53
+ `push` **refuses to run** without one, because Linear silently ignores an unknown
54
+ issue state — the description lands, the issue never moves, and nothing errors.
55
+ If the check reports a name that isn't in the workspace, stop and fix
56
+ `specs/.core/linear.config.json`.
38
57
 
39
58
  **If the check refuses, offer to fix it.** The refusal lists every configured
40
59
  name the workspace lacks, the workspace's real state names, and — where the
@@ -69,6 +88,27 @@ date — say so and stop. `state` values are local buckets
69
88
  (`backlog`/`in-progress`/`complete`/`cancelled`); map each to the Linear
70
89
  issue-state NAME via `config.states` at apply time.
71
90
 
91
+ A **`phasesDeferred`** field means `mapping.phases` is `"deferred"` and this spec
92
+ has not started, so its phases are deliberately absent from the plan — the issue
93
+ pushes alone and the sub-issues are minted by the push that follows `/spec-go`.
94
+ Relay the count; it is not a sign the phase files failed to parse. Nothing else
95
+ about applying the plan changes.
96
+
97
+ The plan always carries a **`phaseMode`** field — `subissue`, `deferred` or
98
+ `inline` — the mode that resolved for THIS spec's lifecycle bucket.
99
+ `mapping.phases` may be a per-bucket map, so the config alone no longer tells you
100
+ which mode a given spec got. Relay it whenever it is not `subissue`:
101
+
102
+ - **`inline`** — no sub-issues are created at all. Each unlinked phase is a
103
+ section of the spec issue's own description, and the `## Phases` index stays as
104
+ its table of contents. A plan with no sub-issue creates is the expected shape,
105
+ not a parse failure. A phase that already carries an id keeps its sub-issue and
106
+ still appears under `subIssues` — apply those normally.
107
+ - **`deferred`** — as above; the `phasesDeferred` count says how many are waiting.
108
+
109
+ Nothing about how you apply the plan changes in either mode: apply exactly the
110
+ `issue` and `subIssues` the plan lists.
111
+
72
112
  ### Stop if the plan reports a pre-9.0 mirror
73
113
 
74
114
  If the plan carries a **`legacy`** field, this spec was linked under the pre-9.0
@@ -79,7 +119,37 @@ mirror and **abandons** the existing one. **Stop.** Relay `legacy.keys`,
79
119
  point at `MIGRATION.md` → "v8 → v9", and apply nothing until the user has
80
120
  migrated or explicitly confirms they want a new mirror.
81
121
 
82
- ## 4. Apply the plan (order matters)
122
+ ## 4. Apply the plan
123
+
124
+ **On `transport = api`, this is one command:**
125
+
126
+ ```
127
+ skitterspec spec-sync apply <spec> --plan plan.json
128
+ ```
129
+
130
+ It creates and updates the issue and its sub-issues, reads each description back
131
+ and runs the same check as step 4b, stamps every returned id into the spec, and
132
+ records the snapshot. **Steps 4a–5 below are then already done** — skip to step 6
133
+ and report what it printed, including the transport.
134
+
135
+ Two things it guarantees, so you don't have to manage them:
136
+
137
+ - **It writes nothing until everything checkable has been checked** — a legacy
138
+ plan, a missing key, or a `config.states` name the workspace lacks all fail
139
+ before the first write.
140
+ - **It stamps each id the moment its object exists.** If a run is interrupted,
141
+ re-run the same command: the objects it already created are linked, so the new
142
+ plan sees them as updates and no duplicate is minted. Never "start again" by
143
+ hand.
144
+
145
+ If it prints **`transport = mcp`** it has written nothing and is telling you to
146
+ apply the plan yourself — do steps 4a, 4b and 5 below. Pass `--via mcp` to force
147
+ that deliberately.
148
+
149
+ If the picker is needed (a first push minting the issue), run it first and pass
150
+ the chosen project through as `--project <id>`.
151
+
152
+ ## 4a. Apply it yourself — the MCP path (order matters)
83
153
 
84
154
  1. **Spec issue** → if the overview has no `linear_identifier`, this push
85
155
  **mints** it: run the picker in **Picking the Linear Project** below, then
@@ -97,7 +167,41 @@ migrated or explicitly confirms they want a new mirror.
97
167
  Priority, labels, cycles and comments are Linear-native triage — do **not** push
98
168
  them; they're the PM's.
99
169
 
100
- ## 5. Stamp the ids, then record the snapshot
170
+ ## 4b. Verify what Linear actually stored the MCP path
171
+
172
+ *(On the API path `apply` already did this. Skip.)*
173
+
174
+ Linear reserialises markdown on save, and it does not always preserve what you
175
+ sent — a table nested in a list item comes back with characters missing from
176
+ every data cell, silently. Check before you record the push as good.
177
+
178
+ For each issue you created or updated in step 4a, read its `description` back
179
+ (`get_issue`) and write what you got to a JSON file:
180
+
181
+ ```json
182
+ { "issue": "<stored description>", "subIssues": { "01-outbox": "<stored>" } }
183
+ ```
184
+
185
+ Then:
186
+
187
+ ```
188
+ skitterspec spec-sync verify <spec> --stored <file>
189
+ ```
190
+
191
+ It compares against what the engine sent, ignoring the reformatting Linear
192
+ legitimately applies (renumbered ordered lists, `-`→`*`, collapsed table
193
+ separators, checkbox case, whitespace) and reporting only lost or altered **word
194
+ characters**. Relay any divergence — it prints both sides around the first
195
+ difference. It exits 0 either way: the repo is unaffected and still correct, so
196
+ this is a warning, not a failure.
197
+
198
+ This is **not a pull**. Nothing read here is merged, stamped, or written
199
+ anywhere; the repo remains the only source of truth. Do it before step 5 so a
200
+ corrupted push is visible before the snapshot records it as good.
201
+
202
+ ## 5. Stamp the ids, then record the snapshot — the MCP path
203
+
204
+ *(On the API path `apply` already did both. Skip.)*
101
205
 
102
206
  Write every id you collected back into the spec in **one** call — the engine
103
207
  does the file edits, so there is no hand-editing of frontmatter:
@@ -109,7 +213,7 @@ skitterspec spec-sync stamp <spec> \
109
213
  ```
110
214
 
111
215
  Pass `--issue`/`--url` only on the push that minted the spec issue; pass one
112
- `--sub <ref>=<id>` for every sub-issue **created** in step 4.2 (updates already
216
+ `--sub <ref>=<id>` for every sub-issue **created** in step 4a.2 (updates already
113
217
  have their id). It validates every ref and id **before** writing anything and
114
218
  exits non-zero having changed nothing if any is wrong — so a typo can't leave the
115
219
  spec half-stamped, pointing at an issue that isn't there. Fix what it reports and
@@ -128,8 +232,13 @@ branch so the mirror-link rides in the PR.
128
232
  ## 6. Report
129
233
 
130
234
  Summarise what was created/updated in Linear (the spec issue and its
131
- sub-issues) and confirm the snapshot was recorded. There is no pull — Linear is
132
- a generated mirror.
235
+ sub-issues), **say which transport was used**, and confirm the snapshot was
236
+ recorded. There is no pull — Linear is a generated mirror.
237
+
238
+ Saying the transport matters: on the API path you never saw the descriptions, so
239
+ "pushed 12 sub-issues" is the engine's report, not your observation. If it warned
240
+ that Linear stored different text, relay that — the repo is still correct, and a
241
+ re-push overwrites the mirror.
133
242
 
134
243
  ### Picking the Linear Project
135
244
 
@@ -139,8 +248,10 @@ placement is **Linear's to own**: never send `project` on an update, and never
139
248
  record the choice in the spec file or the snapshot. A PM re-homing a spec issue
140
249
  must not show up as drift or be overwritten on the next push.
141
250
 
142
- 1. **List the candidates.** Call the discovered project-list tool for
143
- `linear.teamId`. Drop archived / completed projects they can't take new work.
251
+ 1. **List the candidates.** Ask the engine:
252
+ `skitterspec spec-sync projects --json`. On the API path it returns the team's
253
+ projects; on the MCP path it says so, and you call the discovered project-list
254
+ tool instead. Drop archived / completed projects — they can't take new work.
144
255
  2. **Offer them.** Show the names (most recently updated first is fine), plus an
145
256
  explicit **None (team only)** option. Pre-select `linear.projectId` from
146
257
  `linear.config.json` when it's set and still in the list; otherwise pre-select
@@ -149,13 +260,14 @@ must not show up as drift or be overwritten on the next push.
149
260
  the list case-insensitively by name and re-offer. Don't re-fetch.
150
261
  4. **Never offer to create a project.** Projects are the PM's surface — if none
151
262
  fits, that's **None (team only)**, and someone makes the project in Linear.
152
- 5. **Pass it once.** Include `project: <chosen id>` on the issue-create call.
153
- Chose None omit the key entirely (do not send an empty string).
154
-
155
- **Degrade, never block.** If Linear isn't connected, or the server exposes no
156
- project-list tool, say so in one line*"project picker unavailable; creating the
157
- issue without a project"* and carry on with `linear.projectId` if it's set, else
158
- no project at all. A missing picker must never fail `/spec` or `/spec-push`.
263
+ 5. **Pass it once**, to whichever thing mints the issue: `--project <chosen id>`
264
+ on `spec-sync apply`, or `project: <chosen id>` on the MCP issue-create call.
265
+ Chose None → omit it entirely (do not pass an empty string).
266
+
267
+ **Degrade, never block.** If the list can't be fetched Linear not connected, no
268
+ project-list tool, no API key, or `spec-sync projects` reporting it couldn't ask
269
+ say so in one line *"project picker unavailable"* and carry on with
270
+ `linear.projectId` if it's set, else no project at all. A missing picker must never fail the skill that called it.
159
271
 
160
272
  If `linear.projectId` is set but that Project is archived or missing, relay
161
273
  Linear's error and stop rather than silently minting an unparented issue.
@@ -82,6 +82,40 @@ reading the code, do that instead of asking.
82
82
  re-grooming, knock it back to `Draft` (set `> **Status:** Draft` and append a
83
83
  **State log** row).
84
84
 
85
+ **Only when `specs/.core/linear.config.json` exists** and the spec's overview
86
+ carries a `linear_identifier`. Either missing → **skip**, in one line
87
+ (`not linked to Linear — /spec-push to mirror it`), and carry on. Nothing else in
88
+ this step changes.
89
+
90
+ **Refresh the mirror now, without asking.** Run `/spec-push`. The repo has just
91
+ become the truth about this spec's state, and the tracker is a generated mirror
92
+ of it — leaving them to diverge until someone remembers to push by hand is the
93
+ gap this exists to close. It costs one engine call and no model tokens when a
94
+ Linear API key is set (see `apply.transport` in `linear.config.md`).
95
+
96
+ - **Never mint.** An unlinked spec is skipped, not created. An issue born `Done`
97
+ or `Canceled` is tracker noise, and a spec kept deliberately local should stay
98
+ that way — `/spec-push` is how someone opts in.
99
+ - **Never fatal.** If the push fails — offline, no key, a Linear error — say so
100
+ and **finish the operation anyway**. The spec is complete/cancelled/reviewed in
101
+ the repo regardless; the mirror is disposable and the next push repairs it.
102
+ Do not roll anything back, and do not stop to ask.
103
+ - **Say what happened** in the skill's report: mirror updated, skipped as
104
+ unlinked, or failed with the reason.
105
+
106
+ ### Why it sits here
107
+
108
+ In `/spec-complete` and `/spec-cancel` this step is deliberately pinned
109
+ **after the `git mv` and before the commit**, and both halves matter:
110
+
111
+ - **After the move**, because the projection reads a spec's workflow state from
112
+ its folder bucket. Push while the folder is still in `in-progress/` and the
113
+ issue is set to the state the spec is *leaving*.
114
+ - **Before the commit**, because the push stamps ids into the spec and writes a
115
+ snapshot under `specs/.core/`. The `git add specs/` that follows sweeps both up
116
+ with the status change; push after it instead and those files are left
117
+ uncommitted, which makes `spec-env integrate` refuse to land the branch.
118
+
85
119
  ## 5. Report
86
120
 
87
121
  Summarise the drift found, what you changed, any questions still open, and
@@ -50,3 +50,12 @@ skitterspec spec-sync status <spec> [--remote <issuefile>] [--workspace-states <
50
50
 
51
51
  Relay the engine's output verbatim. Suggest `/spec-push` if a push is pending.
52
52
  Never write to either side.
53
+
54
+ A **`phases: <mode>`** line names the phase mode that resolved for this spec's
55
+ lifecycle bucket, and appears only when it is not the default `subissue`.
56
+ `mapping.phases` may be a per-bucket map, so this is the only place the mode a
57
+ given spec got is stated. Under **`inline`** the phases live in the spec issue's
58
+ description rather than as sub-issues, so "0 to create" is the expected shape
59
+ rather than a sign the phase files failed to parse; under **`deferred`** the
60
+ `N phase(s) deferred` line above it says how many are still waiting on
61
+ `/spec-go`. Relay both lines as printed.