@skitterbyte/skitterspec 0.1.0 β†’ 1.0.1

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 (40) hide show
  1. package/README.md +97 -2
  2. package/assets/claude-md-section.md +11 -0
  3. package/assets/core/env.config.json.example +24 -0
  4. package/assets/core/env.config.md +83 -0
  5. package/assets/core/linear.config.json.example +39 -0
  6. package/assets/core/linear.config.md +121 -0
  7. package/assets/rules/spec-planning.md +29 -11
  8. package/assets/skills/spec/SKILL.md +61 -13
  9. package/assets/skills/spec-bug/SKILL.md +0 -4
  10. package/assets/skills/spec-cancel/SKILL.md +8 -3
  11. package/assets/skills/spec-complete/SKILL.md +10 -10
  12. package/assets/skills/spec-env/SKILL.md +57 -0
  13. package/assets/skills/spec-env-down/SKILL.md +56 -0
  14. package/assets/skills/spec-go/SKILL.md +39 -3
  15. package/assets/skills/spec-init/SKILL.md +0 -8
  16. package/assets/skills/spec-pull/SKILL.md +46 -0
  17. package/assets/skills/spec-push/SKILL.md +53 -0
  18. package/assets/skills/spec-ready/SKILL.md +0 -2
  19. package/assets/skills/spec-review/SKILL.md +2 -2
  20. package/assets/skills/spec-status/SKILL.md +46 -0
  21. package/bin/skitterspec.js +0 -0
  22. package/package.json +6 -6
  23. package/src/cli.js +497 -2
  24. package/src/env/config.js +152 -0
  25. package/src/env/provision.js +76 -0
  26. package/src/env/registry.js +95 -0
  27. package/src/env/render.js +26 -0
  28. package/src/env/resolve.js +184 -0
  29. package/src/env/teardown.js +94 -0
  30. package/src/init.js +82 -27
  31. package/src/prompts.js +23 -12
  32. package/src/sync/apply.js +66 -0
  33. package/src/sync/base.js +83 -0
  34. package/src/sync/compare.js +99 -0
  35. package/src/sync/config.js +198 -0
  36. package/src/sync/mcp.js +112 -0
  37. package/src/sync/normalize.js +249 -0
  38. package/src/sync/pull.js +84 -0
  39. package/src/sync/push.js +106 -0
  40. package/src/sync/write.js +86 -0
package/README.md CHANGED
@@ -38,8 +38,6 @@ never clobbers customised files. It writes:
38
38
  .claude/rules/spec-planning.md # governing rule (the single source of truth)
39
39
  .claude/rules/commit-messages.md # commit message + release-note grammar
40
40
  specs/{.core,backlog,in-progress,complete,cancelled}/
41
- specs/backlog/00-index.md # live backlog view (skill-maintained)
42
- specs/complete/00-index.md # append-only completion log
43
41
  CLAUDE.md # adds a "## Spec workflow" section (created if absent)
44
42
  ```
45
43
 
@@ -60,6 +58,7 @@ npx @skitterbyte/skitterspec init ./path/to/project # target a dir (default: c
60
58
  npx @skitterbyte/skitterspec init --yes # accept defaults, skip the prompts
61
59
  npx @skitterbyte/skitterspec init --force # overwrite existing skill/rule/script files
62
60
  npx @skitterbyte/skitterspec init --no-claude-md # don't touch CLAUDE.md
61
+ npx @skitterbyte/skitterspec init --isolation # adopt per-spec isolation (worktree per spec)
63
62
  npx @skitterbyte/skitterspec update # re-copy skills + rule + scripts, leave specs/ + config alone
64
63
  ```
65
64
 
@@ -135,6 +134,102 @@ phase file with its status (`⬜`/`πŸ”„`/`βœ…`). **Each phase is its own file**
135
134
  easy to dive into one phase without wading through the whole spec. The lifecycle
136
135
  skills keep the index and phase files in sync.
137
136
 
137
+ ## Per-spec isolation β€” worktree by default, Docker on demand
138
+
139
+ Work several specs in parallel without them stepping on each other. **Adopt it
140
+ once** with `npx @skitterbyte/skitterspec init --isolation` (or copy
141
+ `specs/.core/env.config.json.example` β†’ `specs/.core/env.config.json`; every field
142
+ is documented in `specs/.core/env.config.md`). While the config is absent the
143
+ feature is simply unused.
144
+
145
+ Once adopted it's the **default policy**, not a per-spec chore:
146
+
147
+ - **Worktree β€” automatic for every in-progress spec.** `/spec-go` gives each spec
148
+ its own sibling git worktree on its own branch, so you never stash or rebuild to
149
+ switch specs and `main` stays free for hotfixes. All housekeeping (the
150
+ backlog→in-progress move, header edits, the code) happens on that branch and
151
+ lands in one PR; `main` changes only when it merges.
152
+ - **Docker β€” a per-spec escalation.** `/spec` records `> **Stack:** worktree`
153
+ (default) or `worktree + docker` when the spec touches the DB / stateful
154
+ services. Only an escalated spec gets a **namespaced stack** β€” a per-spec
155
+ `COMPOSE_PROJECT_NAME` isolates containers, networks, and **named volumes**, and
156
+ a `PORT_OFFSET` reserves a distinct port block, so N stacks run at once with no
157
+ clashes. A worktree-only spec takes **no** slot, port block, or `.env`.
158
+ - an optional **opener** β€” a single, editor/terminal-agnostic `open.command`
159
+ (e.g. `code {worktreePath}`, a `tmux` command, or a `warp://` deeplink).
160
+
161
+ The machine-local slot registry and volume backups live under `/.spec-env/`
162
+ (gitignored). `docker.enabled` in the config is the project **master switch**
163
+ ("is Docker escalation available?"), not "always run Docker".
164
+
165
+ `/spec-env` Β· `/spec-env-down` remain the **manual engine** behind the automation
166
+ β€” use them to escalate Docker onto an existing worktree, re-attach, or tear down:
167
+
168
+ ```
169
+ /spec-env <spec> # worktree (+ stack iff Stack: worktree + docker) + opener
170
+ # (idempotent; re-run attaches)
171
+ /spec-env-down <spec> # stop stack, drop volumes (backed up first), remove worktree,
172
+ # free the slot. Guards refuse a dirty/unpushed worktree
173
+ # unless --force; --keep-volumes preserves data.
174
+ ```
175
+
176
+ Your `docker-compose.yml` must reference `${PORT_OFFSET}` on each published port
177
+ so services land in the spec's reserved block. Two adoption modes:
178
+
179
+ - **Standalone** (`linkLinear: false`) β€” plain `{type}/{slug}` branch names; pure
180
+ worktree + Docker + opener. No Linear needed.
181
+ - **Linear-linked** (`linkLinear: true` + `specs/.core/linear.config.json`) β€”
182
+ branch names follow Linear's pattern so pushing fires Linear's GitHub
183
+ automation.
184
+
185
+ `/spec-complete` and `/spec-cancel` will *offer* to tear down when the config is
186
+ present β€” never forced.
187
+
188
+ ## Linear hybrid sync β€” git-like `/spec-status` Β· `/spec-pull` Β· `/spec-push`
189
+
190
+ Let **Linear own status and discussion** while the repo stays the **co-authoring
191
+ surface for spec content**. The sync is bidirectional but git-like: explicit
192
+ commands, a committed **base sidecar** for three-way merge, and no blind
193
+ overwrites. It's **opt-in** β€” everything below is inert until
194
+ `specs/.core/linear.config.json` exists (copy `linear.config.json.example` and
195
+ fill in your team / initiative IDs; every field is documented in
196
+ `specs/.core/linear.config.md`). Without it, `/spec`, `/spec-go`, and the CLI
197
+ behave exactly as before.
198
+
199
+ **Mapping** (config-driven): a spec folder β†’ Linear **Project**; each phase
200
+ (`01-…`, `02-…`) β†’ a **Milestone**; tasks β†’ **Issues**; an optional **Initiative**
201
+ groups specs. When linked, `/spec` creates the project + a milestone per phase and
202
+ writes the linking frontmatter into `00-overview.md`.
203
+
204
+ **The git-like lifecycle:**
205
+
206
+ ```
207
+ /spec-status # read-only β€” per-field divergence (local-only / remote-only
208
+ # / conflict / in-sync). Changes nothing.
209
+ /spec-pull [--force] # Linear β†’ repo. Applies remote-only fields; refuses to
210
+ # clobber a conflicting local edit unless --force.
211
+ …refine the spec locally (the repo is the co-authoring surface)…
212
+ /spec-push [--force] # repo β†’ Linear. Ownership-respecting, concurrency-checked;
213
+ # refuses if Linear moved since base unless --force.
214
+ ```
215
+
216
+ **Field ownership** collapses conflicts: each field is `both` (co-authored,
217
+ can conflict), `pull` (Linear owns it β€” e.g. status/priority/labels), or `push`
218
+ (the repo owns it). Only a `both` field that moved on **both** sides is a real
219
+ conflict. `--force` never destroys blindly β€” it backs up the losing side into
220
+ `sync.backupDir` (a local reflog) first, then wins. After any successful
221
+ pull/push the engine **rewrites the base** so the next compare starts clean.
222
+
223
+ `/spec-go` on a linked spec runs `/spec-pull` first, so you always build against
224
+ the current shared state. **Base sidecars** (`sync.baseDir`, default
225
+ `specs/.core/linear-base/`) are **committed** β€” each worktree carries its own
226
+ base. **Backups** (`sync.backupDir`, default `specs/.core/linear-backups/`) are
227
+ local recovery and **gitignored**.
228
+
229
+ This supersedes the earlier one-way `/spec-from-issue` intake design (cancelled):
230
+ because both sides author, the sync had to be bidirectional and three-way, not a
231
+ blind import.
232
+
138
233
  ## After install β€” tailor it
139
234
 
140
235
  The shipped skills are **stack-agnostic**. They say things like "run the
@@ -32,3 +32,14 @@ tests, then write a conventional-commit message with a `Release-Note:` footer
32
32
  for user-visible changes (grammar in `.claude/rules/commit-messages.md`). If the
33
33
  release tooling is enabled (`skitterspec.config.json`), those footers feed the
34
34
  generated `CHANGELOG.md`/`RELEASES.md` at `npm version`.
35
+
36
+ **Per-spec isolation (adopt once, then default):** with isolation adopted
37
+ (`skitterspec init --isolation`, or `specs/.core/env.config.json` present),
38
+ **`/spec-go`** gives every in-progress spec its own git worktree automatically β€”
39
+ parallel specs, no stashing, `main` left free. Docker is a **per-spec
40
+ escalation**: `/spec` sets `> **Stack:** worktree` (default) or `worktree +
41
+ docker` when the spec touches the DB / stateful services, and only the latter
42
+ gets a namespaced stack (isolated volumes + reserved port block). **`/spec-env`**
43
+ Β· **`/spec-env-down`** are the manual engine (escalate Docker later, re-attach,
44
+ tear down β€” guarding dirty/unpushed work, backing up volumes first). Independent
45
+ of lifecycle status; inactive when `env.config.json` is absent.
@@ -0,0 +1,24 @@
1
+ {
2
+ "worktree": {
3
+ "root": "../{repo}-wt",
4
+ "folderPattern": "{slug}"
5
+ },
6
+ "docker": {
7
+ "enabled": true,
8
+ "composeFile": "docker-compose.yml",
9
+ "projectNamePattern": "{repoSlug}_{slug}",
10
+ "portBase": 3000,
11
+ "portsPerSpec": 10,
12
+ "envFile": ".env",
13
+ "backupCommand": ""
14
+ },
15
+ "open": {
16
+ "command": ""
17
+ },
18
+ "registry": ".spec-env/registry.json",
19
+ "linkLinear": true,
20
+ "guards": {
21
+ "refuseTeardownIfDirty": true,
22
+ "refuseTeardownIfUnpushed": true
23
+ }
24
+ }
@@ -0,0 +1,83 @@
1
+ # `env.config.json` β€” per-spec isolation config
2
+
3
+ Opt-in config for per-spec isolation (git worktree + optional namespaced Docker
4
+ stack + an optional opener per in-progress spec), driven by `/spec-go` and the
5
+ `/spec-env` Β· `/spec-env-down` skills.
6
+
7
+ **Once this file is present, isolation is the default policy:** `/spec-go` gives
8
+ **every** in-progress spec its own git worktree automatically. Docker is a **per-
9
+ spec escalation** β€” a spec brings up a stack only when its `> **Stack:**` header
10
+ is `worktree + docker` (set at `/spec` when it touches the DB / stateful
11
+ services). A `worktree`-only spec takes no registry slot, no port block, and no
12
+ `.env`.
13
+
14
+ **Adopt it** with `skitterspec init --isolation` (or copy
15
+ `env.config.json.example` β†’ `env.config.json` here) and edit the values. While
16
+ `env.config.json` is absent the feature is simply unused β€” every skill behaves
17
+ exactly as it does today.
18
+
19
+ The loader (`src/env/config.js` β†’ `loadEnvConfig`) merges your file over the
20
+ frozen defaults below and returns `{ config, present }`; `present:false` means
21
+ no live `env.config.json` was found.
22
+
23
+ ## Fields
24
+
25
+ ```jsonc
26
+ {
27
+ // Where sibling worktrees are created and how their dirs are named.
28
+ "worktree": {
29
+ "root": "../{repo}-wt", // dir that holds all spec worktrees; sibling of
30
+ // the primary checkout, never nested inside it.
31
+ "folderPattern": "{slug}" // per-spec worktree dir name.
32
+ },
33
+
34
+ // Per-spec Docker stack. COMPOSE_PROJECT_NAME namespaces containers,
35
+ // networks, and named volumes; PORT_OFFSET shifts the spec's port block.
36
+ "docker": {
37
+ // Master switch: "is Docker escalation available on this project?" β€” NOT
38
+ // "always run Docker". true = specs MAY escalate (a spec still needs
39
+ // `Stack: worktree + docker` to actually get a stack); the default stack is
40
+ // worktree-only. false = every spec is worktree-only and the escalation is
41
+ // hidden. (Was "always provision Docker" in the pre-Stack engine.)
42
+ "enabled": true,
43
+ "composeFile": "docker-compose.yml",
44
+ "projectNamePattern": "{repoSlug}_{slug}", // β†’ COMPOSE_PROJECT_NAME
45
+ "portBase": 3000, // first port of slot 0's block
46
+ "portsPerSpec": 10, // block width; slot n β†’ portBase + n*portsPerSpec
47
+ "envFile": ".env", // written into the worktree
48
+ "backupCommand": "" // optional pre-teardown backup (e.g. pg_dump);
49
+ // empty = no backup, volumes dropped directly.
50
+ },
51
+
52
+ // Optional, editor/terminal-agnostic opener run after `spec-env up`. The
53
+ // template is expanded with {worktreePath}, {slug}, {branch}, {projectName},
54
+ // {portOffset}. Empty = nothing is opened (the path is just printed).
55
+ // Examples: "code {worktreePath}", "tmux new-window -c {worktreePath}",
56
+ // or a "warp://..." deeplink for Warp users.
57
+ "open": {
58
+ "command": ""
59
+ },
60
+
61
+ // Machine-local slot registry (spec β†’ slot index). Resolved against the
62
+ // primary checkout root, shared by all worktrees, gitignored.
63
+ "registry": ".spec-env/registry.json",
64
+
65
+ // When true and specs/.core/linear.config.json is present, derive branch
66
+ // names from Linear's branch.pattern so pushing fires Linear's GitHub
67
+ // automation. Otherwise branches fall back to {type}/{slug}.
68
+ "linkLinear": true,
69
+
70
+ // Teardown safety. --force overrides both.
71
+ "guards": {
72
+ "refuseTeardownIfDirty": true,
73
+ "refuseTeardownIfUnpushed": true
74
+ }
75
+ }
76
+ ```
77
+
78
+ ## Token expansion
79
+
80
+ - `{repo}` β€” primary checkout dir basename (e.g. `skitterspec`).
81
+ - `{repoSlug}` β€” `{repo}` lower-cased, non-alphanumerics collapsed to `-`
82
+ (safe for a `COMPOSE_PROJECT_NAME`).
83
+ - `{slug}` β€” the spec slug (folder name minus its `feat-`/`bug-` prefix).
@@ -0,0 +1,39 @@
1
+ {
2
+ "linear": {
3
+ "teamKey": "",
4
+ "teamId": "",
5
+ "initiativeId": ""
6
+ },
7
+ "mapping": {
8
+ "specFolder": "project",
9
+ "phases": "milestone",
10
+ "tasks": "issue"
11
+ },
12
+ "states": {
13
+ "backlog": "Backlog",
14
+ "in-progress": "In Progress",
15
+ "complete": "Done",
16
+ "cancelled": "Cancelled"
17
+ },
18
+ "snapshot": {
19
+ "overviewFile": "00-overview.md"
20
+ },
21
+ "branch": {
22
+ "pattern": "{type}/{slug}"
23
+ },
24
+ "sync": {
25
+ "baseDir": "specs/.core/linear-base",
26
+ "backupDir": "specs/.core/linear-backups",
27
+ "fieldOwnership": {
28
+ "description": "both",
29
+ "milestones": "both",
30
+ "phaseBodies": "both",
31
+ "acceptanceCriteria": "both",
32
+ "taskBreakdown": "both",
33
+ "workflowState": "pull",
34
+ "priority": "pull",
35
+ "labels": "pull"
36
+ },
37
+ "localOnlySections": ["State log", "Changelog", "Open questions"]
38
+ }
39
+ }
@@ -0,0 +1,121 @@
1
+ # `linear.config.json` β€” Linear hybrid-sync config
2
+
3
+ Opt-in config for the git-like Linear sync (`/spec-status`, `/spec-pull`,
4
+ `/spec-push`, and the Linear-aware paths of `/spec` and `/spec-go`). Linear owns
5
+ **status and discussion**; the repo stays the **co-authoring surface for spec
6
+ content**. Sync is bidirectional but git-like: explicit commands, a committed
7
+ **base sidecar** for three-way merge, and no blind overwrites.
8
+
9
+ **Every Linear step is gated on this file.** While `specs/.core/linear.config.json`
10
+ is absent the feature is simply unused β€” `/spec`, `/spec-go`, and the CLI's
11
+ `spec-sync` subcommands behave exactly as they do today (local-only). Adopt it by
12
+ copying `linear.config.json.example` β†’ `linear.config.json` here and filling in
13
+ your team / initiative IDs.
14
+
15
+ The loader (`src/sync/config.js` β†’ `loadLinearConfig`) merges your file over the
16
+ frozen defaults below and returns `{ config, present }`; `present:false` means no
17
+ live `linear.config.json` was found (the opt-in gate β€” it never throws on
18
+ absence). A `sync.fieldOwnership` value outside `both|pull|push` is a hard error.
19
+
20
+ ## Fields
21
+
22
+ ```jsonc
23
+ {
24
+ // Which Linear team/initiative specs sync into. IDs are read by the Phase 2
25
+ // MCP adapter; leave blank until you connect the `linear` MCP server.
26
+ "linear": {
27
+ "teamKey": "", // human-facing key, e.g. "ENG" (optional)
28
+ "teamId": "", // Linear team UUID (create target)
29
+ "initiativeId": "" // optional Initiative that groups these specs
30
+ },
31
+
32
+ // How a spec's parts map onto Linear objects. Defaults mirror Decision 7:
33
+ // spec folder β†’ Project, phases β†’ Milestones, tasks β†’ Issues. `phases` may be
34
+ // switched to "issue" if your workspace doesn't expose project milestones.
35
+ "mapping": {
36
+ "specFolder": "project",
37
+ "phases": "milestone", // "milestone" | "issue"
38
+ "tasks": "issue"
39
+ },
40
+
41
+ // Map the spec's lifecycle bucket β†’ the Linear workflow-state name. Used when
42
+ // translating workflowState across the boundary (Linear owns status β†’ `pull`).
43
+ "states": {
44
+ "backlog": "Backlog",
45
+ "in-progress": "In Progress",
46
+ "complete": "Done",
47
+ "cancelled": "Cancelled"
48
+ },
49
+
50
+ // The spec's entry-point file the local snapshot + frontmatter live in.
51
+ "snapshot": {
52
+ "overviewFile": "00-overview.md"
53
+ },
54
+
55
+ // Git branch name derived for a linked spec. Tokens: {type}, {slug},
56
+ // {identifier} (the Linear issue/project identifier, e.g. ENG-123). Shared
57
+ // with the isolation engine's branch derivation (src/env/resolve.js).
58
+ "branch": {
59
+ "pattern": "{type}/{slug}"
60
+ },
61
+
62
+ // The three-way merge engine's on-disk state.
63
+ "sync": {
64
+ // Committed base sidecar dir: the last-synced snapshot per spec, as
65
+ // {baseDir}/{identifier}.base.json. Committed so each worktree carries its
66
+ // own base and the divergence check stays accurate.
67
+ "baseDir": "specs/.core/linear-base",
68
+
69
+ // Backup-before-force lands the about-to-be-clobbered side here (the
70
+ // reflog). --force never destroys without first writing a copy.
71
+ "backupDir": "specs/.core/linear-backups",
72
+
73
+ // Per-field sync direction β€” collapses which fields can ever conflict:
74
+ // "both" β€” co-authored: push + pull, may conflict (both moved off base).
75
+ // "pull" — Linear→local only (e.g. status/priority); a local edit never
76
+ // pushes and a conflict resolves to remote-wins.
77
+ // "push" — local→Linear only; a remote edit never pulls and a conflict
78
+ // resolves to local-wins.
79
+ // Any field key you add here joins the compared field set; a value outside
80
+ // both|pull|push is rejected at load time.
81
+ "fieldOwnership": {
82
+ "description": "both",
83
+ "milestones": "both",
84
+ "phaseBodies": "both",
85
+ "acceptanceCriteria": "both",
86
+ "taskBreakdown": "both",
87
+ "workflowState": "pull",
88
+ "priority": "pull",
89
+ "labels": "pull"
90
+ },
91
+
92
+ // Markdown sections of 00-overview.md that are local-only scaffolding and
93
+ // are stripped from the pushed `description` (never sent to Linear).
94
+ "localOnlySections": ["State log", "Changelog", "Open questions"]
95
+ }
96
+ }
97
+ ```
98
+
99
+ ## Field ownership & conflicts
100
+
101
+ The spec is a set of structured fields, most written by only one side. Marking a
102
+ field's owner collapses which fields can genuinely conflict:
103
+
104
+ - A `pull` field (Linear owns it) never reports as **pushable** β€” a stray local
105
+ edit is informational and gets reverted on the next pull.
106
+ - A `push` field (the repo owns it) never reports as **pullable**.
107
+ - Only a `both` field where **both** sides moved off the committed base is a real
108
+ `conflict` β€” `/spec-push` / `/spec-pull` refuse it unless `--force` (which
109
+ backs up the losing side into `sync.backupDir` first).
110
+
111
+ After any successful pull/push/force the engine **rewrites the base** so the next
112
+ three-way compare starts clean.
113
+
114
+ ## What to commit
115
+
116
+ - **`sync.baseDir`** (default `specs/.core/linear-base/`) β€” **commit it.** The base
117
+ sidecar is the last-synced snapshot the three-way merge compares against; each
118
+ worktree carries its own base, so it must travel with the branch.
119
+ - **`sync.backupDir`** (default `specs/.core/linear-backups/`) β€” **gitignore it.**
120
+ These are `--force` recovery copies (a local reflog), per-machine and not shared.
121
+ Add `specs/.core/linear-backups/` to your `.gitignore`.
@@ -20,6 +20,29 @@ state). `/spec-ready` is a grooming gate only β€” it does not move the folder.
20
20
  `/spec-bug` is test-first and starts straight in `In Progress` (work begins
21
21
  immediately), so it skips Draft/Ready.
22
22
 
23
+ **Per-spec isolation (opt-in to adopt, then the default policy).** When a project
24
+ adopts isolation (`skitterspec init --isolation`, or `specs/.core/env.config.json`
25
+ present), `/spec-go` gives **every** in-progress spec its own git worktree
26
+ automatically β€” several specs run side by side without stashing or clashing, and
27
+ `main` stays free. Docker is a **per-spec escalation**: `/spec` records
28
+ `> **Stack:** worktree` (default) or `worktree + docker` when the spec touches the
29
+ DB / stateful services, and `/spec-go` brings up a namespaced stack only for the
30
+ latter. All housekeeping (the backlog→in-progress move, header edits, the code)
31
+ happens on the spec's branch in the worktree; `main` changes only when it merges.
32
+ `/spec-env` Β· `/spec-env-down` remain the manual engine (escalate Docker later,
33
+ re-attach, tear down). Isolation is **orthogonal to lifecycle status** and
34
+ inactive when `env.config.json` is absent β€” every skill then behaves as it does
35
+ today.
36
+
37
+ **Linear hybrid sync (opt-in).** When `specs/.core/linear.config.json` is present,
38
+ three git-like skills sync a spec with its linked Linear project: `/spec-status`
39
+ (read-only per-field divergence), `/spec-pull` (Linear→repo), and `/spec-push`
40
+ (repo→Linear) — three-way merged against a committed base sidecar, with
41
+ field-ownership collapsing conflicts and a `--force` + backup escape hatch. Linked
42
+ `/spec` also creates the Linear Project + a Milestone per phase, and `/spec-go`
43
+ pulls first. All of it is inert without the config β€” every skill then behaves as
44
+ it does today. Full model + field reference: `specs/.core/linear.config.md`.
45
+
23
46
  ## Project conventions (fill this in)
24
47
 
25
48
  The spec skills tell you to run "your project's typecheck and test commands" and
@@ -107,18 +130,13 @@ Keep the index and the phase files in sync (`⬜`/`πŸ”„`/`βœ…`). Legacy specs ma
107
130
  bare `<name>.md`, or a `00-overview.md` with inline phases β€” the skills read
108
131
  those, but new specs always use the folder + phase-file form.
109
132
 
110
- ## Folder indexes (`00-index.md`)
111
-
112
- Two folders keep a skill-maintained `00-index.md` file (never hand-edit):
113
-
114
- - `specs/backlog/00-index.md` β€” **live view** of the backlog (`Added | Spec | Type |
115
- Status`, newest first). `/spec` prepends a row; `/spec-ready` updates its
116
- status; `/spec-go` and `/spec-cancel` remove the row when the spec leaves.
117
- - `specs/complete/00-index.md` β€” **append-only completion log** (`Completed | Spec |
118
- Type`, newest first). `/spec-complete` prepends a row β€” use it to find the
119
- latest completed specs.
133
+ ## Finding specs
120
134
 
121
- `/spec-init` ensures both exist. No index for `in-progress` or `cancelled`.
135
+ The **folder buckets are the source of truth** β€” a spec's bucket is its status.
136
+ To see the backlog, list `specs/backlog/`; for the latest completed specs, use
137
+ `git log`/mtime on `specs/complete/` or each spec's dated **State log**. Live
138
+ status also lives in Linear when it's linked. (There are no `00-index.md`
139
+ summary files β€” the folder tree, headers, and State logs are queried directly.)
122
140
 
123
141
  ## Rules
124
142
 
@@ -35,7 +35,13 @@ not write the spec until this is resolved.
35
35
  6. **Security & multi-tenancy** β€” authz, tenant scoping, untrusted input.
36
36
  7. **Edge cases & failure modes.**
37
37
  8. **Testing approach** β€” what proves each phase correct.
38
- 9. **Open questions** β€” anything still undecided.
38
+ 9. **Isolation stack** *(only when `specs/.core/env.config.json` exists)* β€” does
39
+ this spec touch the DB / stateful services (so its worktree needs a Docker
40
+ stack), or is a plain worktree enough? Default `worktree`; escalate to
41
+ `worktree + docker` only when it must. This sets the `> **Stack:**` header
42
+ that `/spec-go` acts on (it can be escalated later). Skip when isolation
43
+ isn't enabled β€” leave the default `worktree`.
44
+ 10. **Open questions** β€” anything still undecided.
39
45
 
40
46
  Stop grilling when there are no unresolved branches that would change the spec.
41
47
  Briefly play back the agreed understanding before writing.
@@ -71,6 +77,8 @@ the codebase, link rather than duplicate):
71
77
  > **Developer:** β€”
72
78
  > **Raised:** <YYYY-MM-DD (today)>
73
79
  > **Area:** <comma-separated files/modules this touches>
80
+ > **Stack:** <worktree β€” or "worktree + docker" if it touches the DB/stateful
81
+ > services; only acted on when isolation is enabled β€” see Phase A item 9>
74
82
 
75
83
  ## Problem
76
84
 
@@ -159,18 +167,58 @@ Rules for the spec body:
159
167
  absolute.
160
168
  - Keep it tight. If a section adds no information, delete it.
161
169
 
162
- ## Phase C β€” index the spec
163
-
164
- Prepend a row to `specs/backlog/00-index.md` (newest first β€” directly under the table
165
- header row, above existing rows):
166
-
167
- ```
168
- | <YYYY-MM-DD> | <feat-name> | Feature | Draft |
169
- ```
170
-
171
- This is the live view of the backlog; `/spec-go` / `/spec-cancel` remove the row
172
- when the spec leaves. Create `00-index.md` from a header if it's somehow missing
173
- (`/spec-init` normally ensures it).
170
+ ## Phase C β€” finish up
174
171
 
175
172
  After writing, tell the user the path and that it's a `Draft` in `backlog`. Next
176
173
  step is `/spec-ready` once it's groomed, then `/spec-go` to start building.
174
+
175
+ ## Phase D β€” record the isolation stack (only if configured)
176
+
177
+ **Only when `specs/.core/env.config.json` exists** (per-spec isolation is
178
+ enabled), make sure the `> **Stack:**` header reflects the Phase A item 9
179
+ decision β€” `worktree` (default) or `worktree + docker` when it touches the DB /
180
+ stateful services. Nothing to provision now: `/spec-go` gives every in-progress
181
+ spec its own worktree automatically, and brings up Docker only when the Stack
182
+ says so. Mention the operator can escalate the Stack later (edit the header, or
183
+ run `/spec-env <name>` to add Docker to an existing worktree). If
184
+ `env.config.json` is absent, isolation is off β€” leave the default `worktree` and
185
+ finish as above.
186
+
187
+ ## Phase E β€” link to Linear (only if configured)
188
+
189
+ **Only when `specs/.core/linear.config.json` exists** (Linear sync is opted in).
190
+ If it's absent, skip this phase entirely β€” the spec stays local-only and `/spec`
191
+ behaves exactly as above. When present, after writing the spec, link it to Linear
192
+ so status and discussion live there while the repo stays the co-authoring surface:
193
+
194
+ - **Discover the Linear MCP tools at runtime** (don't hardcode names). If Linear
195
+ isn't connected/authed, relay the fix and stop β€” leave the spec written and
196
+ local; the user can link it later with `/spec-push`. Do nothing destructive.
197
+ - **Create the Project** from the spec: name from the title, description from the
198
+ `00-overview.md` plan. Attach the `initiativeId` from `linear.config.json` when
199
+ one is set.
200
+ - **Create a Milestone per phase** (the `mapping.phases` target β€” milestones by
201
+ default), named from each phase file, in execution order.
202
+ - **Add the frontmatter block** to `00-overview.md` (above the `#` title) so the
203
+ spec is linkable:
204
+
205
+ ```yaml
206
+ ---
207
+ linear_project_id: "<uuid>"
208
+ linear_identifier: "<TEAM-123>"
209
+ linear_url: "https://linear.app/..."
210
+ spec_status: "backlog"
211
+ last_synced_at: "<ISO-8601 now>"
212
+ ---
213
+ ```
214
+
215
+ - **Write the initial base sidecar** so the spec starts clean and non-diverged β€”
216
+ run `skitterspec spec-sync normalize <spec>` to capture the local snapshot as the
217
+ committed base (`sync.baseDir`). `/spec-status` should report in-sync right after.
218
+ - **Echo the branch name** from `branch.pattern` so the user knows what `/spec-go`
219
+ will fork.
220
+
221
+ Leave committing to the existing convention (the user commits the spec as usual)
222
+ and **never auto-push git** β€” Linear's own automation reacts to real branch/PR
223
+ events later. Report the Linear project URL and the base as part of Phase C's
224
+ finish-up message.
@@ -103,10 +103,6 @@ test, split the fix into phase files (`01-<slug>.md` …) with a phase index in
103
103
  `00-overview.md`, and leave the spec in `in-progress` for `/spec-go` to continue.
104
104
  Say so explicitly β€” don't fake green.
105
105
 
106
- It starts in `in-progress`, so it does **not** touch `specs/backlog/00-index.md`
107
- (there is no index for in-progress). When `/spec-complete` later finishes it,
108
- that skill logs it to `specs/complete/00-index.md`.
109
-
110
106
  ## 5. Report
111
107
 
112
108
  Summarise: root cause, the failingβ†’passing test, the fix, and the full test
@@ -47,10 +47,15 @@ Add a **Changelog** entry:
47
47
  `git mv "specs/<bucket>/<name>" "specs/cancelled/<name>"` (preserve history;
48
48
  move the whole folder).
49
49
 
50
- If the spec was in `backlog`, **remove its row from `specs/backlog/00-index.md`**
51
- (it has left the backlog). There is no index for `cancelled`.
52
-
53
50
  ## 6. Report
54
51
 
55
52
  Confirm the cancellation, the reason recorded, and the new location. Do **not**
56
53
  `git commit` unless the user asks.
54
+
55
+ ## 7. Offer teardown (opt-in, only if configured)
56
+
57
+ **Only when `specs/.core/env.config.json` exists**, offer β€” don't force β€” to
58
+ reclaim the cancelled spec's environment: "Want me to run `/spec-env-down
59
+ <name>` to remove its worktree, stack, volumes, and free its slot?" It respects
60
+ the teardown guards (won't destroy a dirty/unpushed worktree without `--force`).
61
+ If `env.config.json` is absent, skip this entirely β€” behave exactly as before.
@@ -43,18 +43,18 @@ Before marking complete, confirm the work is actually finished:
43
43
 
44
44
  `mkdir -p specs/complete` then **`git mv`** the file or folder:
45
45
  `git mv "specs/in-progress/<name>" "specs/complete/<name>"` (preserve history;
46
- move the whole folder).
47
-
48
- Then **prepend a row to `specs/complete/00-index.md`** (newest first β€” directly under
49
- the table header, above existing rows):
50
-
51
- ```
52
- | <YYYY-MM-DD> | <name> | Feature|Bug |
53
- ```
54
-
55
- This is the append-only completion log used to find the latest completed specs.
46
+ move the whole folder). The `specs/complete/` folder is the record of finished
47
+ specs β€” `git log`/the per-spec State log give the completion order.
56
48
 
57
49
  ## 5. Report
58
50
 
59
51
  Confirm the move, the final test result, and list anything deferred. Do **not**
60
52
  `git commit` unless the user asks.
53
+
54
+ ## 6. Offer teardown (opt-in, only if configured)
55
+
56
+ **Only when `specs/.core/env.config.json` exists**, offer β€” don't force β€” to
57
+ reclaim the finished spec's environment: "Want me to run `/spec-env-down <name>`
58
+ to remove its worktree, stack, volumes, and free its slot?" It respects the
59
+ teardown guards (won't destroy a dirty/unpushed worktree without `--force`). If
60
+ `env.config.json` is absent, skip this entirely β€” behave exactly as before.