@skitterbyte/skitterspec-linear 10.6.0 → 10.8.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.
Files changed (34) hide show
  1. package/assets/claude-md-section.md +19 -9
  2. package/assets/commands/spec-connect.md +13 -0
  3. package/assets/commands/spec-live.md +14 -0
  4. package/assets/core/ci-stages.md +110 -0
  5. package/assets/core/env.config.md +18 -0
  6. package/assets/core/linear.config.json.example +4 -0
  7. package/assets/core/linear.config.md +88 -0
  8. package/assets/rules/commit-trailers.md +37 -5
  9. package/assets/rules/spec-planning.md +19 -4
  10. package/assets/skills/spec/SKILL.md +20 -0
  11. package/assets/skills/spec-bug/SKILL.md +48 -2
  12. package/assets/skills/spec-cancel/SKILL.md +9 -0
  13. package/assets/skills/spec-complete/SKILL.md +12 -1
  14. package/assets/skills/spec-go/SKILL.md +10 -8
  15. package/assets/skills/spec-hotfix/SKILL.md +49 -3
  16. package/assets/skills/spec-linear-setup/SKILL.md +38 -2
  17. package/assets/skills/spec-status/SKILL.md +1 -0
  18. package/assets/skills/spec-sync/SKILL.md +9 -0
  19. package/assets/skills/spec-to-main/SKILL.md +2 -1
  20. package/bin/skitterspec-linear.js +10 -0
  21. package/package.json +1 -1
  22. package/src/cli.js +211 -44
  23. package/src/env/config.js +19 -0
  24. package/src/env/teardown.js +62 -4
  25. package/src/init.js +120 -2
  26. package/src/vendor/linear/cli-sync.js +454 -34
  27. package/src/vendor/linear/config.js +158 -1
  28. package/src/vendor/linear/doctor.js +67 -1
  29. package/src/vendor/linear/released.js +149 -5
  30. package/src/vendor/sync-core/index.js +4 -1
  31. package/src/vendor/sync-core/src/compare.js +59 -3
  32. package/src/vendor/sync-core/src/normalize.js +65 -2
  33. package/assets/skills/spec-connect/SKILL.md +0 -59
  34. package/assets/skills/spec-live/SKILL.md +0 -73
@@ -1,9 +1,9 @@
1
1
  ## Spec workflow
2
2
 
3
- Spec-driven development runs through seven lifecycle skills (plus `/spec-connect`
4
- when isolation is on) — use them so structure and lifecycle stay consistent (see
5
- `.claude/rules/spec-planning.md`). The everyday loop is
6
- **`spec go connect commit complete`**:
3
+ Spec-driven development runs through the lifecycle **skills** below — use them so
4
+ structure and lifecycle stay consistent (see `.claude/rules/spec-planning.md`).
5
+ The everyday loop is **`spec → go → commit → complete`**, with `/spec-connect`
6
+ in the middle when you want to test the spec in a browser.
7
7
 
8
8
  | Skill | Action | Status | Folder |
9
9
  |-------|--------|--------|--------|
@@ -13,8 +13,15 @@ when isolation is on) — use them so structure and lifecycle stay consistent (s
13
13
  | `/spec-go` | Provision the env, bring dev servers up, implement the next phase | `In Progress` | `specs/in-progress/` |
14
14
  | `/spec-complete` | Verify all phases done + tests green; land + tear down | `Complete` | `specs/complete/` |
15
15
  | `/spec-cancel` | Record progress, stamp a reason; tear down | `Cancelled` | `specs/cancelled/` |
16
+ | `/spec-hotfix` | (Hotfix) Fork a worktree from a release tag, red→green, land by tag | `In Progress` | `specs/in-progress/` |
17
+ | `/spec-to-main` | Land the branch on the base mid-spec, without finishing | (unchanged) | (unchanged) |
16
18
  | `/spec-init` | Bootstrap/repair this workflow in a project (idempotent) | — | — |
17
19
 
20
+ **Skills vs commands.** The table is skills — Claude reads them and exercises
21
+ judgment. `/spec-connect` and `/spec-live` are **slash commands** instead: each
22
+ pre-executes one `spec-env` verb and relays it, so only you can run them. A skill
23
+ that wants one will tell you to type it.
24
+
18
25
  Every spec has a **type** (`> **Type:** Feature\|Bug`) and a filename prefix
19
26
  (`feat-<name>` / `bug-<name>`) — never `[BUG]` brackets (glob hazard). Specs use
20
27
  markdown checkboxes (`- [ ]`) for task tracking and are the single source of
@@ -32,12 +39,15 @@ State log.
32
39
  (`skitterspec init --isolation`, or `specs/.core/env.config.json` present),
33
40
  **`/spec-go`** gives every in-progress spec its own git worktree automatically —
34
41
  parallel specs, no stashing, `main` left free — and brings up the project's host
35
- **dev servers** (`env.config` → `dev`) on the spec's ports. **`/spec-connect
36
- <name>`** then exposes that spec on your canonical `localhost` ports so you can
37
- test it at the normal URL (`/spec-connect main` hands them back). Docker is a
42
+ **dev servers** (`env.config` → `dev`) on the spec's ports. The **`/spec-connect <name>`** command then exposes that spec on your canonical
43
+ `localhost` ports so you can test it at the normal URL (`/spec-connect main`
44
+ hands them back); for a code-only spec, **`/spec-live <name>`** reuses the dev
45
+ server you already have running instead. Docker is a
38
46
  **per-spec escalation**: `/spec` sets `> **Stack:** worktree` (default) or
39
47
  `worktree + docker` when the spec touches the DB / stateful services, and only
40
48
  the latter gets a namespaced stack. Teardown is folded into **`/spec-complete`** ·
41
- **`/spec-cancel`**; beneath the skills, `skitterspec spec-env
42
- <up|down|dev|connect|integrate>` is the CLI engine. Independent of lifecycle
49
+ **`/spec-cancel`**; beneath the skills, `skitterspec spec-env` is the CLI engine — `up`, `down`,
50
+ `prune`, `dev`, `connect`, `integrate`, `hotfix`, `live`, `status` and `resolve`.
51
+ Most are planners the skills run; omit the spec name and it uses the worktree you
52
+ are standing in. Independent of lifecycle
43
53
  status; inactive when `env.config.json` is absent.
@@ -0,0 +1,13 @@
1
+ ---
2
+ description: Expose one spec's dev servers on the canonical localhost ports (or `main` to hand them back)
3
+ argument-hint: "[spec|main]"
4
+ allowed-tools: Bash({{exec}} skitterspec spec-env connect:*)
5
+ disable-model-invocation: true
6
+ ---
7
+ !`{{exec}} skitterspec spec-env connect $ARGUMENTS`
8
+
9
+ Relay the engine output above verbatim. Add nothing and run nothing else.
10
+
11
+ Only if it reports a **canonical port is in use**: say that the main checkout's
12
+ dev server still holds that port, and that stopping it and re-running is the fix
13
+ (the proxy cannot share a port main is bound to).
@@ -0,0 +1,14 @@
1
+ ---
2
+ description: Put one spec live on the already-running dev server — `<spec>` takes it, a bare `take` takes the spec you are on, `main` hands the instance back
3
+ argument-hint: "[<spec> | main | take | release | abort | status]"
4
+ allowed-tools: Bash({{exec}} skitterspec spec-env live:*)
5
+ disable-model-invocation: true
6
+ ---
7
+ !`{{exec}} skitterspec spec-env live $ARGUMENTS`
8
+
9
+ Relay the engine output above verbatim. Add nothing and run nothing else.
10
+
11
+ The engine enforces every refusal itself and prints why — a hotfix, a stateful
12
+ spec (`Stack: worktree + docker`), a branch touching migrations, a dirty tree, a
13
+ rebase conflict, or another spec already holding the instance. Do not re-explain
14
+ or work around any of them.
@@ -0,0 +1,110 @@
1
+ # Wiring a deploy pipeline to Linear
2
+
3
+ How a CI/CD pipeline moves a release's tickets along the ladder declared in
4
+ `linear.config.json` → `release.stages`. Read `linear.config.md` → "The
5
+ deployment ladder" first for the config itself; this page is the pipeline half.
6
+
7
+ Nothing here applies until a ladder is declared. Without one, `spec-sync stage`
8
+ refuses and every other command behaves as it always has.
9
+
10
+ ## The model
11
+
12
+ A spec's lifecycle ends at `complete`. Everything after that is a fact about an
13
+ **environment**, which no folder under `specs/` can derive — so the repo hands
14
+ off. The handoff point is one-way and automatic:
15
+
16
+ ```
17
+ /spec-complete → states.complete (the repo's last word on state)
18
+ CI: spec-sync stage test → release.stages test (the pipeline's, from here on)
19
+ CI: spec-sync stage prod → release.stages prod
20
+ ```
21
+
22
+ Once a spec is complete, pushes stop re-asserting its workflow state, so the
23
+ pipeline is the only writer. Editing a finished spec's prose still updates the
24
+ mirror's description; it will not drag the ticket back out of the pipeline.
25
+
26
+ ## What a stage step runs
27
+
28
+ ```sh
29
+ skitterspec spec-sync stage <key> <previous-tag>..<this-tag> --apply
30
+ ```
31
+
32
+ - **Pass the range explicitly.** With no range it defaults to
33
+ `git describe --tags --abbrev=0`..HEAD, which is a developer convenience — a
34
+ pipeline knows exactly which tag it is deploying, and should say so. The
35
+ resolved range is printed on every run either way.
36
+ - **Drop `--apply` to dry-run.** Same output, no writes. Worth running in a
37
+ pre-deploy job.
38
+ - **`--json`** for a machine-readable result: the moves, the skips by category,
39
+ and the commit counts.
40
+ - **Exit code is 0 unless a write failed**, so a failed move fails the stage.
41
+ A range with nothing to move is a success, not a failure.
42
+
43
+ The tickets come from `Refs:` trailers on the commits in the range (see the
44
+ `commit-trailers` rule). A commit with no trailer is counted and reported, never
45
+ guessed at.
46
+
47
+ ## Setup
48
+
49
+ 1. **A ladder in `linear.config.json`** — via `/spec-linear-setup`, or
50
+ `spec-sync init-config --stage test="On Test" --stage prod="Done"`.
51
+ 2. **An API key as a pipeline secret**, exposed as the env var named by
52
+ `auth.keyEnv` (default `LINEAR_API_KEY`). The config names the *variable*,
53
+ never the key. `--apply` needs the API transport and refuses over MCP.
54
+ 3. **Full history for the range** (CI: `fetch-depth: 0`). Fetching tags is not
55
+ enough on its own: it makes the tag *resolve* without deepening the history,
56
+ so `git log <tag>..HEAD` would return only the commits the clone happens to
57
+ hold. `stage` and `released` check this and **refuse** rather than report a
58
+ partial range, so a shallow checkout fails the step loudly instead of
59
+ deploying a release that quietly leaves tickets behind.
60
+ 4. **Run `spec-sync doctor --check-remote` once** after setup. It reports the
61
+ ladder's rungs against the workspace and warns if the last rung never closes
62
+ an issue.
63
+
64
+ ### Azure Pipelines
65
+
66
+ ```yaml
67
+ - task: Bash@3
68
+ displayName: Mark tickets as deployed to test
69
+ env:
70
+ LINEAR_API_KEY: $(LinearApiKey) # a secret variable, not inline
71
+ inputs:
72
+ targetType: inline
73
+ script: |
74
+ set -euo pipefail
75
+ npx skitterspec spec-sync stage test "$(PreviousTag)..$(Build.SourceBranchName)" --apply
76
+ ```
77
+
78
+ Repeat per environment with a different `<key>`: a post-deploy step on the test
79
+ stage runs `stage test`, the demo approval gate runs `stage demo`, the production
80
+ deploy runs `stage prod`.
81
+
82
+ ## Why not call the API directly
83
+
84
+ A hand-rolled `curl` to Linear's GraphQL endpoint loses three things this command
85
+ has, and the loss is silent in all three cases:
86
+
87
+ - **State-name validation.** Linear **silently ignores** an unknown workflow
88
+ state: the mutation succeeds, and the issue never moves. `stage` resolves the
89
+ target state before its first write and refuses if the workspace lacks it.
90
+ - **The skip rules.** A range carries refs that must not be moved — another
91
+ team's, ones no spec claims, and specs that have not finished. A blanket loop
92
+ over every ref in the log moves all of them.
93
+ - **Retries and error classification.** The adapter retries transient failures;
94
+ a bare `curl` reports a 500 as success if you forget to check.
95
+
96
+ ## What it will not move, and why
97
+
98
+ Every skipped ref is named in the output with its reason — a silent exclusion and
99
+ a successful move look identical in a pipeline log.
100
+
101
+ | Skipped | Reason |
102
+ |---|---|
103
+ | another team's ref | not `linear.teamKey`; writing to an unconfigured team is the worst failure available here |
104
+ | no spec claims it | tracker-only work, or a typo in a trailer — nothing here can tell which |
105
+ | spec not complete | it landed via `/spec-to-main`; push still owns its state and would bounce it back |
106
+ | unreadable | Linear did not return the issue; dropped rather than guessed at |
107
+
108
+ A move that runs against the declared order — backwards, or skipping a rung —
109
+ **warns and proceeds**. A rollback from test and a hotfix going straight to prod
110
+ are both legitimate, and refusing either would be wrong on healthy input.
@@ -148,6 +148,24 @@ no live `env.config.json` was found.
148
148
  "refuseTeardownIfUnpushed": true
149
149
  },
150
150
 
151
+ // What teardown cleans up beyond this machine. `/spec-go` pushes the spec
152
+ // branch when it provisions, so without this a completed spec leaves a merged
153
+ // branch on the remote forever. `deleteRemoteBranch`:
154
+ // "prompt" (default) — plan `git push <remote> --delete <branch>` in its own
155
+ // "confirm with the user first" section; /spec-complete and
156
+ // /spec-cancel ask before running it.
157
+ // "never" — omit it; clean the remote up yourself.
158
+ // "always" — fold it into `run these:` and never ask.
159
+ // Only ever planned for a branch that has LANDED (merged into base, or captured
160
+ // by a hotfix's deploy tag) and whose remote-tracking ref this clone can
161
+ // actually see. Until a branch lands, the remote copy is its only backup, so
162
+ // --force does NOT enable this. A branch pushed from another machine has no
163
+ // local ref here and is simply missed — teardown under-cleans rather than
164
+ // deleting something it cannot see. An unrecognised value means "prompt".
165
+ "teardown": {
166
+ "deleteRemoteBranch": "prompt"
167
+ },
168
+
151
169
  // Live overlay (`spec-env live` / `/spec-live`): test a spec on the already-
152
170
  // running dev server by checking its branch out in the primary checkout.
153
171
  // `migrations` is a list of globs (`**`, `*`, `?`) marking migration files; a
@@ -23,6 +23,10 @@
23
23
  "snapshot": {
24
24
  "overviewFile": "00-overview.md"
25
25
  },
26
+ "release": {
27
+ "stages": [],
28
+ "ignorePaths": ["specs/"]
29
+ },
26
30
  "branch": {
27
31
  "pattern": "{type}/{slug}"
28
32
  },
@@ -85,6 +85,17 @@ absence). A `sync.fieldOwnership` value outside `both|pull|push` is a hard error
85
85
  "overviewFile": "00-overview.md"
86
86
  },
87
87
 
88
+ // The project's OWN deployment ladder — where a ticket goes AFTER its spec is
89
+ // complete. Empty (the default) means no ladder is declared and every
90
+ // stage-aware path is unused. See "The deployment ladder" below.
91
+ //
92
+ // `ignorePaths` is what `released`/`stage` treat as BOOKKEEPING rather than
93
+ // shipped work — see "Bookkeeping commits" below. `[]` opts out.
94
+ "release": {
95
+ "stages": [],
96
+ "ignorePaths": ["specs/"]
97
+ },
98
+
88
99
  // Git branch name derived for a linked spec. Tokens: {type}, {slug},
89
100
  // {identifier} (the Linear issue/project identifier, e.g. ENG-123). Shared
90
101
  // with the isolation engine's branch derivation (src/env/resolve.js).
@@ -122,6 +133,83 @@ absence). A `sync.fieldOwnership` value outside `both|pull|push` is a hard error
122
133
  }
123
134
  ```
124
135
 
136
+ ## The deployment ladder (`release.stages`)
137
+
138
+ A spec's lifecycle stops at `complete`. Where a ticket goes **after** that —
139
+ deployed to test, approved for demo, live in prod — is a fact about an
140
+ **environment**, and nothing under `specs/` can derive it. `release.stages` is
141
+ where a project declares that ladder in its own vocabulary:
142
+
143
+ ```jsonc
144
+ "release": {
145
+ "stages": [
146
+ { "key": "test", "state": "On Test" },
147
+ { "key": "demo", "state": "Ready for Demo" },
148
+ { "key": "prod", "state": "Done" }
149
+ ]
150
+ }
151
+ ```
152
+
153
+ - **`key`** is what CI names (`spec-sync stage test …`); **`state`** is the
154
+ Linear issue state it moves to. Pipelines reference the *key*, never the state
155
+ name, so renaming a Linear column is one edit here rather than a hunt through
156
+ pipeline YAML.
157
+ - **Every `state` is checked against the workspace** alongside `states`, at push
158
+ and setup time. Linear **silently ignores** an unknown issue state, so an
159
+ unchecked typo would deploy cleanly and move nothing, forever.
160
+ - **Order is recorded, not enforced.** It drives reporting and the doctor check;
161
+ a rollback from test, or a hotfix going straight to prod, is legitimate and is
162
+ never refused.
163
+ - **Keys must be unique**, and a malformed entry is a hard error at load — the
164
+ same treatment `sync.fieldOwnership` and `mapping.phases` get.
165
+ - **Absent or empty = the opt-out.** Nothing about the ladder affects a project
166
+ that has not declared one.
167
+
168
+ Unlike `states` and `mapping.phases`, this is **not** keyed by lifecycle bucket:
169
+ the buckets are a closed set the repo derives, and a deployment stage never is.
170
+
171
+ Run `skitterspec spec-sync states` to see the whole configured vocabulary — the
172
+ bucket map and the ladder — against what the workspace actually has.
173
+
174
+
175
+ ## Bookkeeping commits (`release.ignorePaths`)
176
+
177
+ `spec-sync released` and `spec-sync stage` answer "what did this release
178
+ contain?" from the `Refs:` trailers in the range. Left alone, that over-answers:
179
+ a spec's `chore(spec): complete <name>` commit carries the **same ref** as the
180
+ code it describes, but lands **after** the tag that shipped that code — so the
181
+ ticket turns up in two consecutive release ranges, once for its code and once
182
+ for its paperwork. Downstream, a deployment ladder then drags an issue that had
183
+ already reached the top back down a rung.
184
+
185
+ `release.ignorePaths` names the repo-relative prefixes that are paperwork:
186
+
187
+ ```jsonc
188
+ "release": {
189
+ "ignorePaths": ["specs/"]
190
+ }
191
+ ```
192
+
193
+ - **Paths, not subjects.** `chore(spec):` is a convention a mislabelled commit
194
+ escapes; what a commit changed is a fact. A commit touching an ignored path
195
+ **and** a source file still counts — it shipped code.
196
+ - **Prefixes, not globs.** `specs` and `specs/` both mean the directory, and a
197
+ prefix matches only on a path boundary, so `specs/` never swallows
198
+ `specs-archive/`. Name a single file to ignore just that file.
199
+ - **Only a commit whose paths are ALL ignored is dropped**, and only when git
200
+ actually listed paths for it. A merge commit (for which git lists none) counts,
201
+ as does every commit if the path read fails — a lookup that saw nothing knows
202
+ nothing, and dropping a ticket nobody notices is worse than the double-count
203
+ this is fixing.
204
+ - **Every drop is disclosed.** The report says `N commit(s) ignored as
205
+ bookkeeping`, and `--json` carries `ignored` / `ignoredCommits`.
206
+ - **`[]` is the opt-out**; a blank entry is a hard error, since `""` is a prefix
207
+ of every path and would silently empty the release.
208
+
209
+ Default: `["specs/"]`. A project that keeps its paperwork elsewhere names its own
210
+ directories instead.
211
+
212
+
125
213
  ## Spec → Issue, phases → sub-issues
126
214
 
127
215
  Push maps the spec's structure to Linear's, keyed by id so it updates rather than
@@ -6,8 +6,8 @@ installed when a ticketing provider is.
6
6
 
7
7
  ## `Refs:` — the ticket this commit belongs to
8
8
 
9
- A commit made on a spec's branch carries the Linear issue it belongs to, as the
10
- **last** trailer in the message:
9
+ A commit carries the Linear issue **its own changes** belong to, as the **last**
10
+ trailer in the message:
11
11
 
12
12
  ```
13
13
  fix(sync): verify no longer flags every update
@@ -26,15 +26,44 @@ Get the value from the engine rather than reading it off a spec by hand:
26
26
  pnpm exec skitterspec-linear spec-sync ref
27
27
  ```
28
28
 
29
+ Bare, that answers **from the branch** — correct whenever you are committing
30
+ that branch's own implementation work, which is nearly always.
31
+
32
+ ## When the branch and the commit disagree
33
+
34
+ The branch is a proxy for "what this commit is about", and it is a good one
35
+ right up until you commit something that is not this branch's work. The usual
36
+ way in: part-way through a spec, a design question warrants its own spec, so
37
+ `/spec` writes a new one into `specs/backlog/` and links it. That commit is
38
+ **entirely the new spec's**, but you are standing on the old spec's branch, and
39
+ the bare command answers with the old spec's ticket.
40
+
41
+ Name the spec instead — still the engine, never a hand-written id:
42
+
43
+ ```
44
+ pnpm exec skitterspec-linear spec-sync ref feat-distance-prescriptions
45
+ ```
46
+
47
+ The branch is not consulted, so this works from `main` too. An unknown spec name
48
+ fails rather than falling back to the branch, so a typo can't become a
49
+ confidently wrong ref.
50
+
51
+ **Better still, avoid the split:** author backlog specs from the base branch.
52
+ A spec written inside another spec's worktree physically lives on that branch —
53
+ it is not on `main` until that spec lands, and it is cancelled along with it.
54
+
29
55
  ## Rules
30
56
 
57
+ - **The ref names the commit's subject, not your location.** If they disagree,
58
+ the commit's subject wins — resolve it with `spec-sync ref <spec>`.
31
59
  - **Omit the trailer entirely when there is no ref.** `spec-sync ref` exits
32
60
  non-zero and prints nothing on a commit that has no ticket — on `main`, or on
33
61
  a spec kept deliberately local. Never invent one, and never write
34
62
  `Refs: none`: the release report counts unreferenced commits, and a fabricated
35
63
  ref is worse than an honest gap.
36
- - **One ref per commit.** A commit belongs to one spec the spec whose branch
37
- it is on. If work genuinely spans two tickets, that is two commits.
64
+ - **One ref per commit.** A commit belongs to one spec. If work genuinely spans
65
+ two tickets, that is two commits and that is the fix when a single commit
66
+ would mix a branch's own work with another spec's files.
38
67
  - **Never use Linear's magic words** — `Fixes`, `Closes`, `Resolves`. Those
39
68
  close the issue the moment the commit reaches the default branch, which is
40
69
  wrong here: a ticket moves when its work is **released**, not when it merges.
@@ -49,4 +78,7 @@ pnpm exec skitterspec-linear spec-sync ref
49
78
  The repo lands specs with `merge --ff-only`, so history is linear and branch
50
79
  names never reach it. The commit message is the only artefact that survives into
51
80
  the range a release scans, which is what lets
52
- `spec-sync released <range>` report the tickets a release contains.
81
+ `spec-sync released <range>` report the tickets a release contains. A ref that
82
+ names the wrong ticket does not merely lose information — it moves a commit onto
83
+ another ticket's release report, which is worse than the gap it was meant to
84
+ close.
@@ -1,7 +1,7 @@
1
1
  # Spec Planning
2
2
 
3
- Spec-driven development is driven by nine lifecycle skills (plus `/spec-connect`
4
- when isolation is on) — use them rather than hand-rolling specs so the structure
3
+ Spec-driven development is driven by nine lifecycle skills (plus the
4
+ `/spec-connect` and `/spec-live` **commands** when isolation is on) — use them rather than hand-rolling specs so the structure
5
5
  and lifecycle stay consistent. Each sets a status on the spec header
6
6
  (`> **Status:** …`):
7
7
 
@@ -17,6 +17,20 @@ and lifecycle stay consistent. Each sets a status on the spec header
17
17
  | `/spec-cancel` | Record progress, stamp a reason on the header; tear down | `Cancelled` | `specs/cancelled/` |
18
18
  | `/spec-init` | Bootstrap/repair this workflow in a project (idempotent) | — | — |
19
19
 
20
+ **Skills vs commands.** The table above lists **skills** — Claude reads them and
21
+ exercises judgment. `/spec-connect` and `/spec-live` are instead **slash commands**
22
+ (`.claude/commands/`): each pre-executes one `skitterspec spec-env` verb and
23
+ relays its output, so there is no judgment to apply and no model turn spent
24
+ finding one. They are marked `disable-model-invocation`, meaning **only you can
25
+ run them** — a skill that wants one will tell you to type it rather than invoking
26
+ it.
27
+
28
+ `/spec-to-main`, `/spec-status` and `/spec-sync` stay **skills** — each carries
29
+ real judgment (green tests before a land; an MCP fetch and a team-key check; ten
30
+ subcommands) — but they are marked user-only too, since nobody reaches them
31
+ except by typing them. Everything else in the table above stays model-invocable,
32
+ which is what lets `/spec-go` hand off to `/spec-push` as work progresses.
33
+
20
34
  Status flow: `Ready → In Progress → Complete` (or `Cancelled` from any state).
21
35
  `/spec` grills to a **Ready** spec directly — there is no separate grooming
22
36
  command; it writes `Draft` only when open questions are deliberately left.
@@ -41,7 +55,8 @@ your canonical `localhost` ports so you can test it at the normal URL
41
55
  move, header edits, the code) happens on the spec's branch in the worktree; `main`
42
56
  changes only when it merges. Teardown is folded into `/spec-complete` ·
43
57
  `/spec-cancel`. Beneath the skills, `skitterspec spec-env
44
- <up|down|prune|dev|connect|integrate|hotfix>` is the CLI engine. Teardown drops
58
+ <up|down|prune|dev|connect|integrate|hotfix|live|status|resolve>` is the CLI
59
+ engine (omit the spec name and it uses the worktree you are standing in). Teardown drops
45
60
  the finished spec's own test-DB volume; `spec-env prune` additionally reaps
46
61
  **orphaned** volumes left by declined/aborted teardowns, so `/spec-complete` and
47
62
  `/spec-cancel` also sweep orphans (confirm-first). A **hotfix** is the one
@@ -50,7 +65,7 @@ tag and `/spec-complete` lands it via `spec-env hotfix land` (tag + cherry-pick)
50
65
  not a fast-forward. Isolation is **orthogonal to lifecycle status** and inactive
51
66
  when `env.config.json` is absent — every skill then behaves as it does today.
52
67
 
53
- **Live overlay (`/spec-live`) — the light way to test a spec.** `/spec-connect`
68
+ **Live overlay (`/spec-live`, a command) — the light way to test a spec.** `/spec-connect`
54
69
  runs a spec's *own* dev stack and proxies the canonical ports to it (one stack per
55
70
  spec). **Live overlay** instead reuses the one dev server you already have running:
56
71
  `/spec-live <spec>` rebases the branch onto base, frees it from its worktree, and
@@ -143,6 +143,26 @@ Briefly play back the agreed understanding before writing.
143
143
 
144
144
  This skill is for **features**. For bugs, use `/spec-bug` (test-first, red→green).
145
145
 
146
+ ### Write it from the base branch
147
+
148
+ **Check where you are before creating the folder.** A backlog spec belongs on
149
+ the base branch (`main`). If you are on another spec's branch — most likely
150
+ inside its worktree, because a design question came up part-way through
151
+ implementing it — say so before writing, and offer to author the spec from the
152
+ primary checkout instead.
153
+
154
+ It matters for more than tidiness: a spec written inside another spec's worktree
155
+ **physically lives on that branch**. It is not on `main` until that spec lands,
156
+ it is invisible to anyone listing `specs/backlog/` meanwhile, and if that spec is
157
+ cancelled the new spec is cancelled with it. Committing it there also mis-stamps
158
+ the commit's ticket trailer, since that is resolved from the branch (see
159
+ `.claude/rules/commit-trailers.md`, installed with a ticketing provider).
160
+
161
+ **Warn, don't refuse** — doing this deliberately is legitimate, and the user may
162
+ have a reason. If they continue, carry on exactly as normal, and mention that
163
+ the trailer for that commit wants `spec-sync ref <new-spec-name>` rather than the
164
+ bare form.
165
+
146
166
  - **Every spec is a folder** — never a bare file, even for a one-line change:
147
167
  `specs/backlog/feat-<kebab-name>/`. Create it with `mkdir -p`.
148
168
  - The entry point is **always `00-overview.md`** — the index/dashboard for the
@@ -143,8 +143,22 @@ provision from — you'll flesh it out in §4:
143
143
  the worktree path, the opener, and any `in the worktree, run:` bootstrap steps.
144
144
  - Run the printed `git worktree add`. **The worktree forks from `main`'s last
145
145
  commit, so your uncommitted stub doesn't travel with it** — move it across so
146
- `main` is left pristine:
147
- `mv specs/in-progress/bug-<name> <worktreePath>/specs/in-progress/`.
146
+ `main` is left pristine. **Create the destination bucket first:**
147
+
148
+ ```
149
+ mkdir -p <worktreePath>/specs/in-progress
150
+ mv specs/in-progress/bug-<name> <worktreePath>/specs/in-progress/
151
+ ```
152
+
153
+ The `mkdir -p` is not belt-and-braces. Git does not store empty directories,
154
+ so `specs/in-progress/` is **absent** from a fresh worktree whenever that
155
+ bucket happens to be empty on `main` — the common case, since it empties every
156
+ time the last in-progress spec completes. `mv` into a missing destination
157
+ renames your spec folder **to** `specs/in-progress`, silently: the spec's files
158
+ end up one level too high, `00-overview.md` sits where the bucket should be,
159
+ and every later step still appears to work until something cannot find the
160
+ spec. Confirm the result before carrying on — you want
161
+ `<worktreePath>/specs/in-progress/bug-<name>/00-overview.md`.
148
162
  - **Bootstrap the worktree.** A fresh worktree has no installed dependencies and
149
163
  none of the repo's gitignored files (`.env`, local overrides). Run the printed
150
164
  `in the worktree, run:` steps (file seeding, then `setup`) in order, before
@@ -298,6 +312,38 @@ and **never auto-push git** — Linear's own automation reacts to real branch/PR
298
312
  events later. Report the Linear issue URL as part of the skill's finish-up
299
313
  message.
300
314
 
315
+ ### Picking the Linear Project
316
+
317
+ Run this **only when minting a spec issue** — creating it for the first time. On
318
+ an update the issue already has a project (or deliberately has none), and that
319
+ placement is **Linear's to own**: never send `project` on an update, and never
320
+ record the choice in the spec file or the snapshot. A PM re-homing a spec issue
321
+ must not show up as drift or be overwritten on the next push.
322
+
323
+ 1. **List the candidates.** Ask the engine:
324
+ `skitterspec spec-sync projects --json`. On the API path it returns the team's
325
+ projects; on the MCP path it says so, and you call the discovered project-list
326
+ tool instead. Drop archived / completed projects — they can't take new work.
327
+ 2. **Offer them.** Show the names (most recently updated first is fine), plus an
328
+ explicit **None (team only)** option. Pre-select `linear.projectId` from
329
+ `linear.config.json` when it's set and still in the list; otherwise pre-select
330
+ **None**.
331
+ 3. **Narrow on request.** If the user types a fragment rather than choosing, filter
332
+ the list case-insensitively by name and re-offer. Don't re-fetch.
333
+ 4. **Never offer to create a project.** Projects are the PM's surface — if none
334
+ fits, that's **None (team only)**, and someone makes the project in Linear.
335
+ 5. **Pass it once**, to whichever thing mints the issue: `--project <chosen id>`
336
+ on `spec-sync apply`, or `project: <chosen id>` on the MCP issue-create call.
337
+ Chose None → omit it entirely (do not pass an empty string).
338
+
339
+ **Degrade, never block.** If the list can't be fetched — Linear not connected, no
340
+ project-list tool, no API key, or `spec-sync projects` reporting it couldn't ask —
341
+ say so in one line — *"project picker unavailable"* — and carry on with
342
+ `linear.projectId` if it's set, else no project at all. A missing picker must never fail the skill that called it.
343
+
344
+ If `linear.projectId` is set but that Project is archived or missing, relay
345
+ Linear's error and stop rather than silently minting an unparented issue.
346
+
301
347
  ## 5. Drive to GREEN
302
348
 
303
349
  - Implement the **minimal, root-cause** fix. Match surrounding code; honour all
@@ -113,6 +113,15 @@ directly (the old `/spec-env-down` skill is gone — teardown is folded in here)
113
113
  3. `skitterspec spec-env down <name>` — then execute the printed commands to
114
114
  remove the worktree/stack and free the slot. It respects the teardown guards
115
115
  (won't destroy a dirty/unpushed worktree without `--force`).
116
+
117
+ If the plan prints a `remote branch — confirm with the user first:` section,
118
+ **ask before running that line** — it is a `git push <remote> --delete`, and
119
+ the branch is merged, so it loses nothing. Usually there is nothing to ask:
120
+ a cancelled spec is normally **unlanded**, and the planner deliberately never
121
+ offers the remote delete then, because the pushed branch is the only surviving
122
+ copy of abandoned work. Seeing no such section here is the expected case, not
123
+ a fault — and if the user wants the remote branch gone anyway, that is their
124
+ call to make explicitly, not something to tidy away on their behalf.
116
125
  4. `skitterspec spec-env prune` — reap orphaned test-DB volumes that belong to no
117
126
  live spec (leftovers from declined/aborted teardowns or manual worktree
118
127
  removal). Show the orphan list and, **only on the user's confirmation**, run
@@ -152,7 +152,7 @@ is finished — e.g. to run a later phase in CI or a shared test env? Use
152
152
  back to base, re-isolates it into its worktree, clears the receipt), then prints
153
153
  the normal landing plan. Commit any live fixes to the branch first; it refuses
154
154
  if the primary checkout is dirty, or if a *different* spec holds it (release that
155
- one with `/spec-live main`). Teardown (step 7) is unchanged.
155
+ one by typing `/spec-live main`). Teardown (step 7) is unchanged.
156
156
  **Work-loss abort.** Before it ends the live session, `integrate` checks the
157
157
  work is actually landable and **aborts loudly** rather than finalize a spec
158
158
  having landed nothing. Two cases, both leaving the live session intact:
@@ -208,6 +208,17 @@ worktree and branch are being kept, and go straight to sub-step 4. Mention
208
208
  still respects the guards (won't destroy a dirty, or unpushed-and-unlanded,
209
209
  worktree without `--force`), so if it *does* refuse, relay that and stop
210
210
  rather than reaching for `--force`.
211
+
212
+ **If the plan prints a `remote branch — confirm with the user first:`
213
+ section, ask before running it.** `/spec-go` pushed this branch when it
214
+ provisioned, so the remote copy outlives teardown unless someone deletes it.
215
+ The planner only ever offers this for a branch that has **landed**, so say so
216
+ plainly when you ask — the commits are on the base branch (or captured by the
217
+ deploy tag), so deleting the remote branch loses nothing. On a yes, run the
218
+ printed `git push <remote> --delete <branch>`; on a no, leave it and say the
219
+ remote branch is still there. Never fold it into the `run these:` batch — a
220
+ project that wants it unattended sets `teardown.deleteRemoteBranch: "always"`
221
+ in `env.config.json`, and then it appears in `run these:` instead of here.
211
222
  4. **Reap orphaned test-DB volumes:** run `skitterspec spec-env prune`. It lists
212
223
  Docker volumes in the repo namespace that belong to **no live spec** (no
213
224
  worktree) — leftovers from declined/aborted teardowns, manual
@@ -7,7 +7,8 @@ description: Promote a spec into active development and build the next phase —
7
7
 
8
8
  The "up" button: it promotes the spec, provisions its worktree, brings its host
9
9
  dev servers up on the spec's reserved ports (with your OK), then builds the phase.
10
- Diverting your browser to the spec is a separate explicit step `/spec-connect`.
10
+ Diverting your browser to the spec is a separate explicit step the **user**
11
+ types — `/spec-connect` (a slash command, not a skill you can invoke).
11
12
 
12
13
  ## 1. Identify the target spec
13
14
 
@@ -33,8 +34,8 @@ branch. Instead skip the provisioning bullets and step 2b, leave the spec where
33
34
  it is, and go straight to **step 4**, implementing the phase **in the primary
34
35
  checkout on the branch** — edits and commits there advance the branch, and
35
36
  `/spec-complete` lands them. (`spec-env up` refuses while live and says the same.
36
- To return to an isolated worktree instead, run `/spec-live main` first, then
37
- re-run `/spec-go`.)
37
+ To return to an isolated worktree instead, ask the user to type `/spec-live main`
38
+ first, then re-run `/spec-go`.)
38
39
 
39
40
  **If per-spec isolation is enabled** (`specs/.core/env.config.json` exists), the
40
41
  spec **isn't already live** (the check above), and it doesn't already have a
@@ -121,11 +122,12 @@ runnable — its UI/API on the spec's reserved port block, isolated from `main`.
121
122
  dev process detached on its port, logs to `.spec-env/logs/`, and waits on each
122
123
  `health` check. With no `dev` configured it's a clean no-op; skip this step.
123
124
  - **Diverting your browser is a separate step.** To test the spec at your normal
124
- `localhost` URL, run **`/spec-connect <name>`** (exclusive — it exposes this
125
- spec on the canonical ports; `/spec-connect main` hands them back). `/spec-go`
126
- never seizes the canonical ports on its own. For a **code-only** spec, the
127
- lighter **`/spec-live <name>`** reuses your already-running dev server (a
128
- branch-switch, no second stack) — `/spec-live main` hands it back.
125
+ `localhost` URL, the **user** types **`/spec-connect <name>`** (exclusive — it
126
+ exposes this spec on the canonical ports; `/spec-connect main` hands them back).
127
+ `/spec-go` never seizes the canonical ports on its own. For a **code-only** spec,
128
+ the lighter **`/spec-live <name>`** reuses the already-running dev server (a
129
+ branch-switch, no second stack) — `/spec-live main` hands it back. Both are
130
+ user-only slash commands: tell the user to run one, never try to invoke it.
129
131
 
130
132
  ## 3. Pre-flight — commit prior work
131
133