@skitterbyte/skitterspec-linear 8.0.4 → 9.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +34 -16
- package/assets/core/SETUP.md +47 -37
- package/assets/core/linear.config.json.example +10 -7
- package/assets/core/linear.config.md +80 -38
- package/assets/rules/spec-planning.md +3 -0
- package/assets/skills/spec/SKILL.md +106 -10
- package/assets/skills/spec-bug/SKILL.md +61 -0
- package/assets/skills/spec-cancel/SKILL.md +18 -3
- package/assets/skills/spec-complete/SKILL.md +26 -8
- package/assets/skills/spec-go/SKILL.md +16 -6
- package/assets/skills/spec-hotfix/SKILL.md +1 -0
- package/assets/skills/spec-push/SKILL.md +71 -28
- package/assets/skills/spec-status/SKILL.md +13 -12
- package/package.json +1 -1
- package/src/cli.js +18 -6
- package/src/env/provision.js +39 -3
- package/src/env/resolve.js +1 -0
- package/src/vendor/linear/cli-sync.js +87 -12
- package/src/vendor/linear/config.js +41 -17
- package/src/vendor/linear/mcp.js +57 -50
- package/src/vendor/sync-core/index.js +4 -3
- package/src/vendor/sync-core/src/base.js +1 -1
- package/src/vendor/sync-core/src/compare.js +29 -41
- package/src/vendor/sync-core/src/normalize.js +69 -47
- package/src/vendor/sync-core/src/push.js +5 -7
- package/src/vendor/sync-core/src/task-block.js +48 -8
- package/src/vendor/sync-core/src/write.js +14 -13
|
@@ -13,6 +13,66 @@ Spec type convention (see `.claude/rules/spec-planning.md`):
|
|
|
13
13
|
- Bug specs are named `bug-<kebab-name>`; feature specs `feat-<kebab-name>`.
|
|
14
14
|
- Every spec header carries `> **Type:** Bug` (or `Feature`).
|
|
15
15
|
|
|
16
|
+
## Phase 0 — start from a Linear issue (only when asked)
|
|
17
|
+
|
|
18
|
+
**Only when `specs/.core/linear.config.json` exists** *and* the invocation names an
|
|
19
|
+
issue. Otherwise skip this phase entirely and grill from the user's own words.
|
|
20
|
+
|
|
21
|
+
Two ways in:
|
|
22
|
+
|
|
23
|
+
- **`<ISSUE-REF>`** (`SKI-123`, an issue URL, or a bare id anywhere in the
|
|
24
|
+
arguments) — adopt that issue.
|
|
25
|
+
- **`--from-issue [query]`** — browse the intake inbox: issues carrying
|
|
26
|
+
`intake.label` (what the web app files under). With a `query`, filter that list
|
|
27
|
+
case-insensitively by title. Show the shortlist and let the user pick one.
|
|
28
|
+
|
|
29
|
+
Then:
|
|
30
|
+
|
|
31
|
+
1. **Exclude what's already adopted.** Run `skitterspec spec-sync linked --json`
|
|
32
|
+
and drop any issue whose identifier is already stamped on a spec. If the user
|
|
33
|
+
named such an issue directly, **stop** and point them at that spec — one issue
|
|
34
|
+
never becomes two specs.
|
|
35
|
+
2. **Read the issue** with the discovered issue-read tool: title, description,
|
|
36
|
+
labels, reporter, URL. If Linear isn't connected, say so and offer to carry on
|
|
37
|
+
without it — a spec written from the user's own description is still a spec.
|
|
38
|
+
3. **Route bugs away — in `/spec` only.** If any of the issue's labels matches
|
|
39
|
+
`intake.bugLabels` (case-insensitive), this is a bug report, not a feature
|
|
40
|
+
request. Say so, name the matching label, and tell the user to run
|
|
41
|
+
**`/spec-bug <ISSUE-REF>`** — then stop, without authoring a Feature spec.
|
|
42
|
+
With `intake.bugLabels` unset nothing is routed and every issue is treated as a
|
|
43
|
+
feature request.
|
|
44
|
+
|
|
45
|
+
**In `/spec-bug` this step is skipped** — you are already in the bug path.
|
|
46
|
+
Adopt the issue, then reproduce it as usual: the issue body is your repro
|
|
47
|
+
material, and the failing test comes before the spec exactly as normal.
|
|
48
|
+
4. **Seed, don't skip, the grill.** The issue's title becomes the working spec
|
|
49
|
+
title and its description the starting material for **Problem** — quote the
|
|
50
|
+
reporter's words rather than paraphrasing them away. Note the reporter and the
|
|
51
|
+
issue URL for context. Everything after this runs exactly as normal: an issue
|
|
52
|
+
is a *request*, not a groomed spec, so grill it as hard as anything else.
|
|
53
|
+
|
|
54
|
+
### Adopting the issue
|
|
55
|
+
|
|
56
|
+
The issue **becomes** the spec's issue — it is not copied and no second issue is
|
|
57
|
+
minted. This is identical in `/spec` and `/spec-bug`; only *when* it happens
|
|
58
|
+
differs (`/spec` writes the spec in Phase B, `/spec-bug` in its step 4). Once the
|
|
59
|
+
spec file exists:
|
|
60
|
+
|
|
61
|
+
- **Stamp `linear_identifier` and `linear_url`** in `00-overview.md` frontmatter
|
|
62
|
+
from the adopted issue. That is the whole link: every later skill
|
|
63
|
+
(`/spec-push`, `/spec-status`, `/spec-go`) keys off it being present.
|
|
64
|
+
- **Do not run the project picker** and never send `project`. The issue was filed
|
|
65
|
+
somewhere deliberately — where it lives is Linear's business, and adoption is
|
|
66
|
+
not a mint.
|
|
67
|
+
- **Do not write a base sidecar.** Leaving `sync.baseDir` empty for this spec is
|
|
68
|
+
what makes the first `/spec-push` push the spec over the reporter's original
|
|
69
|
+
description (an **update** to the existing issue, plus a sub-issue per phase).
|
|
70
|
+
Recording a snapshot here would declare the mirror already in sync and strand
|
|
71
|
+
the issue showing the raw report forever.
|
|
72
|
+
- **Say what will happen** in the finish-up message: the first `/spec-push` will
|
|
73
|
+
overwrite the issue's description with the spec, and the original report is
|
|
74
|
+
preserved in the spec's **Problem** section.
|
|
75
|
+
|
|
16
76
|
## 1. Reproduce & isolate (light investigation)
|
|
17
77
|
|
|
18
78
|
Bugs are concrete — confirm, don't over-grill. Establish:
|
|
@@ -95,6 +155,7 @@ because work is already underway. Keep it lean:
|
|
|
95
155
|
# Bug: <short title>
|
|
96
156
|
|
|
97
157
|
> **Type:** Bug
|
|
158
|
+
> **Name:** bug-<kebab-name> (the spec folder name — the handle you paste into `/spec-go`)
|
|
98
159
|
> **Status:** In Progress — fixing (red test added)
|
|
99
160
|
> **Author:** <git user.name — who reported/captured it>
|
|
100
161
|
> **Developer:** <git user.name — you, since you're fixing it now>
|
|
@@ -25,6 +25,10 @@ it verbatim/condensed for the header.
|
|
|
25
25
|
tick anything that was actually completed before cancelling so the record is
|
|
26
26
|
honest about what landed.
|
|
27
27
|
- Note any partial/abandoned work so it isn't mistaken for unstarted.
|
|
28
|
+
- **Check for pre-existing uncommitted changes — before you touch anything.**
|
|
29
|
+
Run `git status`. Anything already uncommitted is the *user's* work and must not
|
|
30
|
+
be swept into the cancellation commit: offer `/commit` and **stop**. Everything
|
|
31
|
+
this skill writes in steps 4–5 is its own, and step 5 commits that.
|
|
28
32
|
|
|
29
33
|
## 4. Stamp the spec
|
|
30
34
|
|
|
@@ -41,16 +45,27 @@ Append a **State log** row:
|
|
|
41
45
|
Add a **Changelog** entry:
|
|
42
46
|
`- <YYYY-MM-DD> — Cancelled: <reason>.`
|
|
43
47
|
|
|
44
|
-
## 5. Move to cancelled
|
|
48
|
+
## 5. Move to cancelled — and commit it
|
|
45
49
|
|
|
46
50
|
`mkdir -p specs/cancelled` then **`git mv`** the file or folder:
|
|
47
51
|
`git mv "specs/<bucket>/<name>" "specs/cancelled/<name>"` (preserve history;
|
|
48
52
|
move the whole folder).
|
|
49
53
|
|
|
54
|
+
Then **commit the cancellation edits** — this skill wrote them, so it commits
|
|
55
|
+
them:
|
|
56
|
+
|
|
57
|
+
```
|
|
58
|
+
git add specs/ && git commit -m "chore(spec): cancel <name>"
|
|
59
|
+
```
|
|
60
|
+
|
|
61
|
+
**This matters more here than anywhere else.** Teardown (step 7) refuses a dirty
|
|
62
|
+
worktree and offers `--force` as the way through — and forcing would destroy the
|
|
63
|
+
cancellation record this skill just wrote. Committing first means teardown never
|
|
64
|
+
needs `--force`. Do not `git push`.
|
|
65
|
+
|
|
50
66
|
## 6. Report
|
|
51
67
|
|
|
52
|
-
Confirm the cancellation, the reason recorded,
|
|
53
|
-
`git commit` unless the user asks.
|
|
68
|
+
Confirm the cancellation, the reason recorded, the new location, and the commit.
|
|
54
69
|
|
|
55
70
|
## 7. Tear down the environment (opt-in, only if configured)
|
|
56
71
|
|
|
@@ -27,6 +27,12 @@ Before marking complete, confirm the work is actually finished:
|
|
|
27
27
|
- If genuinely incomplete work remains, **stop and tell the user** rather than
|
|
28
28
|
forcing completion. Offer to finish it (`/spec-go`) or to complete with the
|
|
29
29
|
remaining items explicitly listed as deferred.
|
|
30
|
+
- **Check for pre-existing uncommitted changes — before you touch anything.**
|
|
31
|
+
Run `git status`. Anything already uncommitted is the *user's* work (a
|
|
32
|
+
half-finished phase, a stray fix) and must not be swept into the completion
|
|
33
|
+
commit: offer `/commit` and **stop**. Everything this skill writes in steps 3–4
|
|
34
|
+
is its own, and step 4 commits that.
|
|
35
|
+
|
|
30
36
|
|
|
31
37
|
## 3. Update the spec
|
|
32
38
|
|
|
@@ -40,17 +46,28 @@ Before marking complete, confirm the work is actually finished:
|
|
|
40
46
|
`- <YYYY-MM-DD> — Completed; all phases done, tests green.`
|
|
41
47
|
(Note any consciously-deferred items here too.)
|
|
42
48
|
|
|
43
|
-
## 4. Move to complete
|
|
49
|
+
## 4. Move to complete — and commit it
|
|
44
50
|
|
|
45
51
|
`mkdir -p specs/complete` then **`git mv`** the file or folder:
|
|
46
52
|
`git mv "specs/in-progress/<name>" "specs/complete/<name>"` (preserve history;
|
|
47
53
|
move the whole folder). The `specs/complete/` folder is the record of finished
|
|
48
54
|
specs — `git log`/the per-spec State log give the completion order.
|
|
49
55
|
|
|
56
|
+
Then **commit the completion edits** — steps 3–4 are this skill's own output, so
|
|
57
|
+
it finishes its own work rather than handing you a dirty tree:
|
|
58
|
+
|
|
59
|
+
```
|
|
60
|
+
git add specs/ && git commit -m "chore(spec): complete <name>"
|
|
61
|
+
```
|
|
62
|
+
|
|
63
|
+
Step 2 established the tree was otherwise clean, so this commits exactly the
|
|
64
|
+
status flip and the move — nothing of yours rides along. **This is what lets step
|
|
65
|
+
6 land:** `integrate` refuses a dirty worktree, so without committing here the
|
|
66
|
+
skill would block on the very edits it just made. Do not `git push`.
|
|
67
|
+
|
|
50
68
|
## 5. Report
|
|
51
69
|
|
|
52
|
-
Confirm the move, the final test result, and list anything deferred.
|
|
53
|
-
`git commit` unless the user asks.
|
|
70
|
+
Confirm the move, the commit, the final test result, and list anything deferred.
|
|
54
71
|
|
|
55
72
|
## 6. Land the branch (opt-in, only if isolated)
|
|
56
73
|
|
|
@@ -65,8 +82,9 @@ destination in one flow. **How it lands depends on the spec type:**
|
|
|
65
82
|
A hotfix is built on an old release **tag**, so it can't fast-forward onto `main`.
|
|
66
83
|
Use the hotfix landing instead of the integrate steps below:
|
|
67
84
|
|
|
68
|
-
1. **Require a clean worktree
|
|
69
|
-
|
|
85
|
+
1. **Require a clean worktree.** Step 4 already committed the completion edits, so
|
|
86
|
+
this should pass. If the tree is *still* dirty, that's unrelated work — offer
|
|
87
|
+
`/commit` and **stop**.
|
|
70
88
|
2. **Plan + execute.** Run `skitterspec spec-env hotfix land <name>` — add
|
|
71
89
|
`--also <tag>` for each extra release line to patch (test/demo on their own
|
|
72
90
|
versions). Run the printed commands **in order**. It:
|
|
@@ -92,9 +110,9 @@ is finished — e.g. to run a later phase in CI or a shared test env? Use
|
|
|
92
110
|
**`/spec-to-main`**: same rebase + fast-forward, but it leaves the spec
|
|
93
111
|
`In Progress` and the worktree standing, and it's repeatable.)
|
|
94
112
|
|
|
95
|
-
1. **Require a clean worktree.**
|
|
96
|
-
|
|
97
|
-
|
|
113
|
+
1. **Require a clean worktree.** Step 4 already committed the completion edits, so
|
|
114
|
+
integrate's dirty-tree guard should pass. If the tree is *still* dirty, that's
|
|
115
|
+
unrelated work — offer `/commit` and **stop**; don't sweep it in.
|
|
98
116
|
**If the spec is live** (you took the running instance with `/spec-live`):
|
|
99
117
|
`integrate` is live-aware — it ends the live session first (releases the branch
|
|
100
118
|
back to base, re-isolates it into its worktree, clears the receipt), then prints
|
|
@@ -47,16 +47,26 @@ same "in place otherwise" path used when isolation is off. Warn that the work
|
|
|
47
47
|
will land wherever you currently are (usually `main`); reserve it for a trivial
|
|
48
48
|
change or an explicit request.
|
|
49
49
|
|
|
50
|
-
- Run `skitterspec spec-env up <name>` (the `spec-env` CLI engine). It
|
|
51
|
-
|
|
52
|
-
|
|
50
|
+
- Run `skitterspec spec-env up <name>` (the `spec-env` CLI engine). It is a
|
|
51
|
+
**planner: it prints commands and creates nothing itself.** Under
|
|
52
|
+
`to provision, run:` it emits the `git worktree add` on a branch forked from
|
|
53
|
+
`main` and — only when the spec's `> **Stack:**` header is
|
|
54
|
+
`worktree + docker` — the Docker bring-up.
|
|
55
|
+
**Run those commands and confirm they succeeded** before anything below: every
|
|
56
|
+
later step assumes the worktree exists, and the header line says
|
|
57
|
+
`(plan — nothing created yet)` precisely because at that point it doesn't.
|
|
53
58
|
Print the worktree path and the opener command it emits.
|
|
54
59
|
- **Bootstrap the worktree's dependencies.** A fresh worktree has an empty
|
|
55
60
|
working tree — no installed dependencies, and none of the repo's gitignored
|
|
56
61
|
files (`.env`, local secret/config overrides) — so git hooks, typechecks,
|
|
57
62
|
builds and tests fail until they're in place. `spec-env up` prints the
|
|
58
|
-
project's configured **`in the worktree, run:`** commands — run them in
|
|
59
|
-
|
|
63
|
+
project's configured **`then, in the worktree, run:`** commands — run them in
|
|
64
|
+
order, before doing anything else. Each one begins by `cd`-ing into the
|
|
65
|
+
worktree, so it works from any cwd and cannot quietly act on the main
|
|
66
|
+
checkout; if the worktree is missing it prints
|
|
67
|
+
**`no worktree at … — run the provisioning commands first`** and exits
|
|
68
|
+
non-zero. Seeing that means the `git worktree add` above didn't run or didn't
|
|
69
|
+
work — fix that before going on. Those commands are: first any
|
|
60
70
|
**file seeding** (from `env.config.json` → `seedFiles`), which symlinks or
|
|
61
71
|
copies the configured gitignored files from the main checkout into the fresh
|
|
62
72
|
worktree so setup can rely on them; then the **`setup`** commands (e.g. an
|
|
@@ -136,7 +146,7 @@ tracker. Otherwise skip this step — no provider means zero change to the flow
|
|
|
136
146
|
below. Follow the provider's steps below (nothing to do here without one).
|
|
137
147
|
|
|
138
148
|
**Only when `specs/.core/linear.config.json` exists** and the spec carries a
|
|
139
|
-
`
|
|
149
|
+
`linear_identifier`. Otherwise skip this step — no config means zero change.
|
|
140
150
|
|
|
141
151
|
- **No pull.** Linear is a generated mirror in one-way sync, so there is nothing
|
|
142
152
|
to bring down before building — the repo is already the source of truth. (A
|
|
@@ -89,6 +89,7 @@ is usually a single-pass fix, so the `## Fix` block can live directly in
|
|
|
89
89
|
# Hotfix: <short title>
|
|
90
90
|
|
|
91
91
|
> **Type:** Hotfix
|
|
92
|
+
> **Name:** hotfix-<kebab-name> (the spec folder name — the handle you paste into `/spec-go`)
|
|
92
93
|
> **Status:** In Progress — fixing (red test added)
|
|
93
94
|
> **Author:** <git user.name — who reported/captured it>
|
|
94
95
|
> **Developer:** <git user.name — you, since you're fixing it now>
|
|
@@ -1,14 +1,16 @@
|
|
|
1
1
|
---
|
|
2
2
|
name: spec-push
|
|
3
|
-
description: Push a spec up to its linked Linear
|
|
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; tasks are not synced. 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".
|
|
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
|
-
mirror**.
|
|
10
|
-
|
|
11
|
-
|
|
9
|
+
mirror**. A spec is a Linear **issue**; each phase is a **sub-issue** (a child
|
|
10
|
+
issue). Tasks are not synced — they live only in the repo phase files. This skill
|
|
11
|
+
computes what changed since the last push and applies it — it never reads Linear
|
|
12
|
+
content back or merges. A person editing the mirror in Linear will see it
|
|
13
|
+
overwritten on the next push.
|
|
12
14
|
|
|
13
15
|
**Opt-in**: only runs when `specs/.core/linear.config.json` exists. If absent,
|
|
14
16
|
tell the user how to enable Linear sync and stop.
|
|
@@ -27,40 +29,51 @@ The engine prints a JSON **plan** (no network, no remote read):
|
|
|
27
29
|
|
|
28
30
|
```json
|
|
29
31
|
{
|
|
30
|
-
"
|
|
31
|
-
"
|
|
32
|
-
|
|
32
|
+
"issue": { "description": "…", "state": "in-progress" },
|
|
33
|
+
"subIssues": {
|
|
34
|
+
"create": [{ "ref": "01-outbox", "name": "Outbox", "goal": "…", "state": "backlog" }],
|
|
35
|
+
"update": [{ "id": "REU-2", "name": "…", "goal": "…", "state": "in-progress" }]
|
|
36
|
+
}
|
|
33
37
|
}
|
|
34
38
|
```
|
|
35
39
|
|
|
36
|
-
An empty plan (no
|
|
37
|
-
say so and stop.
|
|
40
|
+
An empty plan (no `issue`, no sub-issue create/update) means the mirror is up to
|
|
41
|
+
date — say so and stop. `state` values are local buckets
|
|
42
|
+
(`backlog`/`in-progress`/`complete`/`cancelled`); map each to the Linear
|
|
43
|
+
issue-state NAME via `config.states` at apply time.
|
|
38
44
|
|
|
39
45
|
## 3. Discover the Linear MCP tools
|
|
40
46
|
|
|
41
|
-
Discover
|
|
42
|
-
|
|
43
|
-
**
|
|
47
|
+
Discover the issue **read + create/update** tools at runtime (`get_issue`,
|
|
48
|
+
`save_issue` — a single upsert covers create and update), plus the **project
|
|
49
|
+
list** tool if this push will mint the spec issue (see the picker below — it is
|
|
50
|
+
optional; without it the picker is skipped, not failed). If Linear isn't
|
|
51
|
+
connected or a needed tool is missing, relay the fix and stop, **writing
|
|
52
|
+
nothing**.
|
|
44
53
|
|
|
45
|
-
**Validate the
|
|
54
|
+
**Validate the issue states first.** Fetch the workspace's issue workflow-state
|
|
46
55
|
names and run `skitterspec spec-sync status <spec> --workspace-states <file>`; if
|
|
47
56
|
it errors (a configured `states` name isn't in the workspace), stop and fix the
|
|
48
|
-
config — Linear silently ignores an unknown
|
|
57
|
+
config — Linear silently ignores an unknown issue state.
|
|
49
58
|
|
|
50
59
|
## 4. Apply the plan (order matters)
|
|
51
60
|
|
|
52
|
-
1. **
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
61
|
+
1. **Spec issue** → if the overview has no `linear_identifier`, this push
|
|
62
|
+
**mints** it: run the picker in **Picking the Linear Project** below, then
|
|
63
|
+
create it with `save_issue` (`team` = `linear.teamId`; `project` = the picked
|
|
64
|
+
id; `title` from the spec title; `description` from `plan.issue.description`;
|
|
65
|
+
state from `plan.issue.state` via `config.states`). Stamp the returned
|
|
66
|
+
identifier into `00-overview.md` frontmatter as `linear_identifier` (and
|
|
67
|
+
`linear_url`). If it already exists and `plan.issue` is present, **update it by
|
|
68
|
+
id and send no `project`** — its placement is Linear's from then on.
|
|
69
|
+
2. **Sub-issues create** → for each, `save_issue` with `parentId` = the spec
|
|
70
|
+
issue id (`name` → title, `goal` → description, `state` via `config.states`);
|
|
71
|
+
stamp the returned id into its phase file as `linear_issue_id` (`ref` is the
|
|
72
|
+
phase-file basename).
|
|
73
|
+
3. **Sub-issues update** → `save_issue` by `id` (title/description/state).
|
|
74
|
+
|
|
75
|
+
Priority, labels, cycles and comments are Linear-native triage — do **not** push
|
|
76
|
+
them; they're the PM's.
|
|
64
77
|
|
|
65
78
|
## 5. Record the snapshot
|
|
66
79
|
|
|
@@ -76,5 +89,35 @@ branch so the mirror-link rides in the PR.
|
|
|
76
89
|
|
|
77
90
|
## 6. Report
|
|
78
91
|
|
|
79
|
-
Summarise what was created/updated in Linear
|
|
80
|
-
recorded. There is no pull — Linear is
|
|
92
|
+
Summarise what was created/updated in Linear (the spec issue and its
|
|
93
|
+
sub-issues) and confirm the snapshot was recorded. There is no pull — Linear is
|
|
94
|
+
a generated mirror.
|
|
95
|
+
|
|
96
|
+
### Picking the Linear Project
|
|
97
|
+
|
|
98
|
+
Run this **only when minting a spec issue** — creating it for the first time. On
|
|
99
|
+
an update the issue already has a project (or deliberately has none), and that
|
|
100
|
+
placement is **Linear's to own**: never send `project` on an update, and never
|
|
101
|
+
record the choice in the spec file or the snapshot. A PM re-homing a spec issue
|
|
102
|
+
must not show up as drift or be overwritten on the next push.
|
|
103
|
+
|
|
104
|
+
1. **List the candidates.** Call the discovered project-list tool for
|
|
105
|
+
`linear.teamId`. Drop archived / completed projects — they can't take new work.
|
|
106
|
+
2. **Offer them.** Show the names (most recently updated first is fine), plus an
|
|
107
|
+
explicit **None (team only)** option. Pre-select `linear.projectId` from
|
|
108
|
+
`linear.config.json` when it's set and still in the list; otherwise pre-select
|
|
109
|
+
**None**.
|
|
110
|
+
3. **Narrow on request.** If the user types a fragment rather than choosing, filter
|
|
111
|
+
the list case-insensitively by name and re-offer. Don't re-fetch.
|
|
112
|
+
4. **Never offer to create a project.** Projects are the PM's surface — if none
|
|
113
|
+
fits, that's **None (team only)**, and someone makes the project in Linear.
|
|
114
|
+
5. **Pass it once.** Include `project: <chosen id>` on the issue-create call.
|
|
115
|
+
Chose None → omit the key entirely (do not send an empty string).
|
|
116
|
+
|
|
117
|
+
**Degrade, never block.** If Linear isn't connected, or the server exposes no
|
|
118
|
+
project-list tool, say so in one line — *"project picker unavailable; creating the
|
|
119
|
+
issue without a project"* — and carry on with `linear.projectId` if it's set, else
|
|
120
|
+
no project at all. A missing picker must never fail `/spec` or `/spec-push`.
|
|
121
|
+
|
|
122
|
+
If `linear.projectId` is set but that Project is archived or missing, relay
|
|
123
|
+
Linear's error and stop rather than silently minting an unparented issue.
|
|
@@ -1,15 +1,15 @@
|
|
|
1
1
|
---
|
|
2
2
|
name: spec-status
|
|
3
|
-
description: Show a spec's one-way sync status against Linear — a read-only drift report. Reports whether the spec changed since the last push (there's something to push) and, optionally, whether Linear's workflow-state differs from the spec's. Fetches the Linear
|
|
3
|
+
description: Show a spec's one-way sync status against Linear — a read-only drift report. Reports whether the spec changed since the last push (there's something to push) and, optionally, whether Linear's issue workflow-state differs from the spec's. Fetches the Linear issue over MCP and runs `skitterspec spec-sync status`. Changes nothing. Opt-in — needs specs/.core/linear.config.json. Use when the user says "/spec-status", "is this spec in sync with Linear", "what would push", or "show spec sync status".
|
|
4
4
|
---
|
|
5
5
|
|
|
6
6
|
# /spec-status — one-way sync drift report
|
|
7
7
|
|
|
8
8
|
Read-only. Reports two things and writes nothing:
|
|
9
9
|
|
|
10
|
-
1. **Pending push** — has the spec changed since the last push (
|
|
11
|
-
|
|
12
|
-
2. **State drift** — does Linear's
|
|
10
|
+
1. **Pending push** — has the spec changed since the last push (is there an issue
|
|
11
|
+
description/state or any phase sub-issue to create or update)?
|
|
12
|
+
2. **State drift** — does Linear's issue workflow-state differ from the spec's
|
|
13
13
|
status? (The repo wins on the next push; this is just a heads-up, e.g. a card
|
|
14
14
|
moved in Linear.)
|
|
15
15
|
|
|
@@ -24,24 +24,25 @@ tell the user how to enable Linear sync and stop.
|
|
|
24
24
|
|
|
25
25
|
Use the argument, else the spec in context; ask if unclear.
|
|
26
26
|
|
|
27
|
-
## 2. Fetch the Linear
|
|
27
|
+
## 2. Fetch the Linear issue (optional, for drift)
|
|
28
28
|
|
|
29
|
-
If the spec has a `
|
|
30
|
-
the
|
|
31
|
-
Linear isn't connected, skip the drift line (still
|
|
29
|
+
If the spec has a `linear_identifier`, discover the Linear MCP read tool
|
|
30
|
+
(`get_issue`) and write the issue JSON to a temp file — this lets the report
|
|
31
|
+
compare workflow-state. If Linear isn't connected, skip the drift line (still
|
|
32
|
+
report pending-push).
|
|
32
33
|
|
|
33
|
-
Optionally fetch the workspace
|
|
34
|
+
Optionally fetch the workspace issue-state names to a file to validate the
|
|
34
35
|
configured `states` at the same time.
|
|
35
36
|
|
|
36
37
|
## 3. Run the engine
|
|
37
38
|
|
|
38
39
|
```
|
|
39
|
-
skitterspec spec-sync status <spec> [--remote <
|
|
40
|
+
skitterspec spec-sync status <spec> [--remote <issuefile>] [--workspace-states <statesfile>]
|
|
40
41
|
```
|
|
41
42
|
|
|
42
43
|
- Reports `push: pending — N to create, M to update` or `up to date`.
|
|
43
|
-
- With `--remote`, adds a `drift:` line comparing Linear's workflow-state
|
|
44
|
-
spec's status.
|
|
44
|
+
- With `--remote`, adds a `drift:` line comparing Linear's issue workflow-state
|
|
45
|
+
to the spec's status.
|
|
45
46
|
- With `--workspace-states`, fails loudly if a configured state name isn't in the
|
|
46
47
|
workspace (Linear would silently no-op it).
|
|
47
48
|
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@skitterbyte/skitterspec-linear",
|
|
3
|
-
"version": "
|
|
3
|
+
"version": "9.0.0",
|
|
4
4
|
"description": "Spec-driven development for Claude Code, with one-way Linear sync — a superset of @skitterbyte/skitterspec: the base filesystem workflow plus /spec-status · /spec-push and the spec-sync CLI. The repo is canonical; Linear is a generated mirror. Install this OR the base, not both.",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"claude",
|
package/src/cli.js
CHANGED
|
@@ -60,7 +60,8 @@ Usage:
|
|
|
60
60
|
.core config alone.
|
|
61
61
|
skitterspec spec-env <cmd> Per-spec isolation engine (opt-in; needs
|
|
62
62
|
specs/.core/env.config.json). Subcommands:
|
|
63
|
-
up <spec> plan a worktree +
|
|
63
|
+
up <spec> print the plan to provision a worktree +
|
|
64
|
+
Docker stack (prints commands; creates nothing)
|
|
64
65
|
down <spec> tear down (guards; --keep-volumes, --force)
|
|
65
66
|
prune reap orphaned test-DB volumes (--older-than <days>)
|
|
66
67
|
dev up <spec> start host dev servers on the spec's ports
|
|
@@ -176,8 +177,10 @@ function specEnvStatus(dir, config) {
|
|
|
176
177
|
})
|
|
177
178
|
}
|
|
178
179
|
|
|
179
|
-
//
|
|
180
|
-
// /spec-env skill executes (git worktree add, docker compose up, .env,
|
|
180
|
+
// Plan a provision: allocate the slot, persist the registry, and print the plan
|
|
181
|
+
// the /spec-env skill executes (git worktree add, docker compose up, .env,
|
|
182
|
+
// opener). This creates no worktree and starts no stack — the caller runs the
|
|
183
|
+
// printed commands. Keep the output's verb honest about that.
|
|
181
184
|
function specEnvUp(dir, config, specArg) {
|
|
182
185
|
if (!specArg) {
|
|
183
186
|
process.stdout.write('Usage: skitterspec spec-env up <spec>\n')
|
|
@@ -224,7 +227,16 @@ function specEnvUp(dir, config, specArg) {
|
|
|
224
227
|
const plan = planUp(spec, { slot, attached }, config)
|
|
225
228
|
|
|
226
229
|
const out = []
|
|
227
|
-
|
|
230
|
+
// `up` is a planner: it prints commands for the caller to run and creates no
|
|
231
|
+
// worktree or stack itself (the registry slot and the trust entry, both reported
|
|
232
|
+
// separately below, are its only writes). Say so in the verb — a past-tense
|
|
233
|
+
// "(provisioned)" reads as a completed state change, and a caller that believes
|
|
234
|
+
// it skips the commands and works on `main`, which is what isolation exists to
|
|
235
|
+
// prevent.
|
|
236
|
+
out.push(
|
|
237
|
+
`spec-env up: ${spec.folder} ` +
|
|
238
|
+
(attached ? '(plan — worktree exists; will attach)' : '(plan — nothing created yet)'),
|
|
239
|
+
)
|
|
228
240
|
out.push('')
|
|
229
241
|
out.push(` worktree: ${plan.worktreePath}`)
|
|
230
242
|
out.push(` branch: ${plan.branch}`)
|
|
@@ -247,7 +259,7 @@ function specEnvUp(dir, config, specArg) {
|
|
|
247
259
|
)
|
|
248
260
|
}
|
|
249
261
|
out.push('')
|
|
250
|
-
out.push(' run
|
|
262
|
+
out.push(' to provision, run:')
|
|
251
263
|
for (const cmd of plan.commands) out.push(` ${cmd}`)
|
|
252
264
|
if (plan.openCommand) out.push(` ${plan.openCommand}`)
|
|
253
265
|
// Seed files first (setup may depend on them), then the setup commands —
|
|
@@ -255,7 +267,7 @@ function specEnvUp(dir, config, specArg) {
|
|
|
255
267
|
const worktreeSteps = [...plan.seedCommands, ...plan.setupCommands]
|
|
256
268
|
if (worktreeSteps.length) {
|
|
257
269
|
out.push('')
|
|
258
|
-
out.push(' in the worktree, run:')
|
|
270
|
+
out.push(' then, in the worktree, run:')
|
|
259
271
|
for (const cmd of worktreeSteps) out.push(` ${cmd}`)
|
|
260
272
|
}
|
|
261
273
|
if (plan.envContents) {
|
package/src/env/provision.js
CHANGED
|
@@ -15,6 +15,37 @@ const { portOffset } = require('./registry.js')
|
|
|
15
15
|
const { renderEnvFile, expandOpenCommand } = require('./render.js')
|
|
16
16
|
const { expandTokens } = require('./resolve.js')
|
|
17
17
|
|
|
18
|
+
/**
|
|
19
|
+
* Build the POSIX-sh prefix that puts a command in the spec's worktree, or stops.
|
|
20
|
+
*
|
|
21
|
+
* Every command under the "then, in the worktree, run:" heading carries this.
|
|
22
|
+
* Without it those commands stay silent when run from the wrong place: in the
|
|
23
|
+
* primary checkout `$m` resolves to the checkout itself, so a seed's source and
|
|
24
|
+
* target are the same path and it prints `exists — skipped` — indistinguishable
|
|
25
|
+
* from a correctly-provisioned re-run — while a setup command like an install
|
|
26
|
+
* runs against the main checkout and "succeeds" too. Three layers then agree that
|
|
27
|
+
* nothing is wrong, and the caller carries on committing to `main`.
|
|
28
|
+
*
|
|
29
|
+
* A `cd` is deliberately chosen over comparing `git rev-parse --show-toplevel`
|
|
30
|
+
* against `worktreePath`: the planned path is a lexical `path.resolve` while git
|
|
31
|
+
* reports the symlink-resolved one (`/tmp` vs `/private/tmp` on macOS), so a
|
|
32
|
+
* string compare would refuse a perfectly good worktree. `cd` sidesteps that, and
|
|
33
|
+
* does better than refusing — it *positions* the command, so a caller who is in
|
|
34
|
+
* the wrong directory still gets correct behaviour. When the worktree was never
|
|
35
|
+
* created — the reported case — the `cd` fails and takes the whole command with
|
|
36
|
+
* it, non-zero and loud.
|
|
37
|
+
*
|
|
38
|
+
* Per-command, not once at the top of the block: a single leading `cd` only
|
|
39
|
+
* protects the sequence if the caller chains it with `&&`, and the reported
|
|
40
|
+
* failure is precisely a caller that ran the steps as separate statements.
|
|
41
|
+
*/
|
|
42
|
+
function worktreeCd(worktreePath) {
|
|
43
|
+
return (
|
|
44
|
+
`cd "${worktreePath}" 2>/dev/null || ` +
|
|
45
|
+
`{ echo "no worktree at ${worktreePath} — run the provisioning commands first"; exit 1; }`
|
|
46
|
+
)
|
|
47
|
+
}
|
|
48
|
+
|
|
18
49
|
/**
|
|
19
50
|
* Build one idempotent POSIX-sh command that seeds a gitignored file from the
|
|
20
51
|
* main checkout into the current worktree (the cwd when the skill runs it).
|
|
@@ -84,13 +115,18 @@ function planUp(spec, alloc, config) {
|
|
|
84
115
|
// config ⇒ no commands ⇒ current behaviour.
|
|
85
116
|
const seed = config.seedFiles || { mode: 'symlink', files: [] }
|
|
86
117
|
const seedMode = seed.mode === 'copy' ? 'copy' : 'symlink'
|
|
87
|
-
const
|
|
118
|
+
const guard = worktreeCd(spec.worktreePath)
|
|
119
|
+
const seedCommands = (seed.files || []).map(
|
|
120
|
+
(file) => `${guard}; ${seedCommandFor(file, seedMode)}`,
|
|
121
|
+
)
|
|
88
122
|
|
|
89
123
|
// Bootstrap commands run *in the worktree* after `git worktree add` (before
|
|
90
124
|
// Docker/dev), on every provision including re-attach — deps must exist for
|
|
91
125
|
// the worktree to be usable. Kept separate from `commands` (run from the
|
|
92
126
|
// primary checkout root); the CLI prints them under an "in the worktree" head.
|
|
93
|
-
const setupCommands = (config.setup || []).map(
|
|
127
|
+
const setupCommands = (config.setup || []).map(
|
|
128
|
+
(cmd) => `${guard}; ${expandTokens(cmd, tokens)}`,
|
|
129
|
+
)
|
|
94
130
|
|
|
95
131
|
const commands = []
|
|
96
132
|
// Fresh branch → -b; attach an existing branch/slot → plain form (never clobber).
|
|
@@ -122,4 +158,4 @@ function planUp(spec, alloc, config) {
|
|
|
122
158
|
}
|
|
123
159
|
}
|
|
124
160
|
|
|
125
|
-
module.exports = { planUp, seedCommandFor }
|
|
161
|
+
module.exports = { planUp, seedCommandFor, worktreeCd }
|