@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.
- package/README.md +76 -15
- package/assets/core/SETUP.md +73 -13
- package/assets/core/linear.config.json.example +8 -1
- package/assets/core/linear.config.md +243 -8
- 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-go/SKILL.md +10 -3
- 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 +142 -30
- package/assets/skills/spec-review/SKILL.md +34 -0
- package/assets/skills/spec-status/SKILL.md +9 -0
- package/bin/skitterspec-linear.js +19 -0
- package/package.json +1 -1
- package/src/cli.js +30 -19
- 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 +788 -3
- package/src/vendor/linear/config.js +127 -7
- package/src/vendor/sync-core/index.js +8 -1
- package/src/vendor/sync-core/src/normalize.js +291 -82
- package/src/vendor/sync-core/src/push.js +18 -1
- package/src/vendor/sync-core/src/tables.js +102 -0
- package/src/vendor/sync-core/src/task-block.js +18 -7
- package/src/vendor/sync-core/src/verify.js +83 -0
|
@@ -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
|
-
-
|
|
26
|
-
`/spec-hotfix v33.16.4 login-crash`).
|
|
27
|
-
|
|
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
|
+
reference — letters, 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
|
|
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
|
|
11
|
-
|
|
10
|
+
issue), and the phase file is mirrored into that sub-issue's description as
|
|
11
|
+
written — its 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.
|
|
26
|
+
## 2. Pick the transport, and get the workspace states
|
|
24
27
|
|
|
25
|
-
|
|
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
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
and
|
|
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
|
|
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
|
-
##
|
|
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
|
|
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)
|
|
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.**
|
|
143
|
-
`
|
|
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
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
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.
|