@skitterbyte/skitterspec-linear 8.0.5 → 9.1.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.
@@ -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, and the new location. Do **not**
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. Do **not**
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** the completion edits (status flip, `git mv` to
69
- `complete/`) must be committed first. If dirty, offer `/commit` and **stop**.
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.** The completion edits (status flip, the
96
- `git mv` to `complete/`) must be committed first integrate refuses a dirty
97
- tree. If it's dirty, offer `/commit` and **stop**; don't auto-commit.
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 adds a git
51
- worktree on a branch forked from `main`, and only when the spec's
52
- `> **Stack:**` header is `worktree + docker` also brings up its Docker stack.
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 the
59
- worktree, in order, before doing anything else. Those commands are: first any
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
- `linear_project_id`. Otherwise skip this step — no config means zero change.
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 project (repo → Linear, one-way). The repo is the source of truth; Linear is a generated mirror. Runs `skitterspec spec-sync push` to get a create/update plan, applies it over MCP (project description/status, milestones, 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 project 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; 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**. This skill computes what changed since the last push and applies it
10
- it never reads Linear content back or merges. A person editing the mirror in
11
- Linear will see it overwritten on the next push.
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
- "project": { "description": "…", "status": "in-progress", "priority": 2, "labels": ["…"] },
31
- "milestones": { "create": [{ "ref": "01-outbox", "name": "…", "goal": "…" }], "update": [{ "id": "…", "name": "…", "goal": "…" }] },
32
- "issues": { "create": [{ "ref": "<task text>", "title": "…", "description": "…", "done": false, "milestoneRef": "01-outbox" }], "update": [{ "id": "SKI-1", "title": "…", "description": "", "done": true }] }
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 project, no create/update) means the mirror is up to date —
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 project + milestone + issue **create/update** tools at runtime. If
42
- Linear isn't connected or a needed tool is missing, relay the fix and stop,
43
- **writing nothing**.
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 project states first.** Fetch the workspace's project-status
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 project status.
57
+ config — Linear silently ignores an unknown issue state.
49
58
 
50
59
  ## 4. Apply the plan (order matters)
51
60
 
52
- 1. **Milestones create** → create each in Linear; for each, stamp the returned id
53
- into its phase file: the `ref` is the phase-file basename.
54
- 2. **Issues create** create each (link to its milestone by `milestoneRef`,
55
- resolving a `create` ref to the id just minted); stamp the returned identifier
56
- back onto the matching task line (`ref` is the task's text).
57
- 3. **Milestones/issues update** save by `id`.
58
- 4. **Project** set description + status (map `status` the local bucket — to
59
- the Linear project-status name via `config.states`). Priority, labels, cycles
60
- and comments are Linear-native triage do **not** push them; they're the PM's.
61
-
62
- Map the local status bucket to Linear's project status through `config.states`
63
- (e.g. `complete → Completed`).
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 and confirm the snapshot was
80
- recorded. There is no pull — Linear is a generated mirror.
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 project 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".
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 (are there
11
- milestones/issues/description to create or update)?
12
- 2. **State drift** — does Linear's project workflow-state differ from the spec'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 project (optional, for drift)
27
+ ## 2. Fetch the Linear issue (optional, for drift)
28
28
 
29
- If the spec has a `linear_project_id`, discover the Linear MCP read tool and write
30
- the project JSON to a temp file — this lets the report compare workflow-state. If
31
- Linear isn't connected, skip the drift line (still report pending-push).
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 project-status names to a file to validate the
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 <projectfile>] [--workspace-states <statesfile>]
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 to the
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": "8.0.5",
3
+ "version": "9.1.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 + Docker stack + opener
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
- // Provision: allocate the slot, persist the registry, and print the plan the
180
- // /spec-env skill executes (git worktree add, docker compose up, .env, opener).
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
- out.push(`spec-env up: ${spec.folder} ${attached ? '(attached existing)' : '(provisioned)'}`)
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 these:')
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) {
@@ -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 seedCommands = (seed.files || []).map((file) => seedCommandFor(file, seedMode))
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((cmd) => expandTokens(cmd, tokens))
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 }
@@ -251,6 +251,7 @@ function resolveSpec(specArg, dir, config, opts = {}) {
251
251
  }
252
252
 
253
253
  module.exports = {
254
+ BUCKETS,
254
255
  resolveSpec,
255
256
  resolveBaseBranch,
256
257
  resolvePrimaryCheckout,