@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.
- package/README.md +97 -2
- package/assets/claude-md-section.md +11 -0
- package/assets/core/env.config.json.example +24 -0
- package/assets/core/env.config.md +83 -0
- package/assets/core/linear.config.json.example +39 -0
- package/assets/core/linear.config.md +121 -0
- package/assets/rules/spec-planning.md +29 -11
- package/assets/skills/spec/SKILL.md +61 -13
- package/assets/skills/spec-bug/SKILL.md +0 -4
- package/assets/skills/spec-cancel/SKILL.md +8 -3
- package/assets/skills/spec-complete/SKILL.md +10 -10
- package/assets/skills/spec-env/SKILL.md +57 -0
- package/assets/skills/spec-env-down/SKILL.md +56 -0
- package/assets/skills/spec-go/SKILL.md +39 -3
- package/assets/skills/spec-init/SKILL.md +0 -8
- package/assets/skills/spec-pull/SKILL.md +46 -0
- package/assets/skills/spec-push/SKILL.md +53 -0
- package/assets/skills/spec-ready/SKILL.md +0 -2
- package/assets/skills/spec-review/SKILL.md +2 -2
- package/assets/skills/spec-status/SKILL.md +46 -0
- package/bin/skitterspec.js +0 -0
- package/package.json +6 -6
- package/src/cli.js +497 -2
- package/src/env/config.js +152 -0
- package/src/env/provision.js +76 -0
- package/src/env/registry.js +95 -0
- package/src/env/render.js +26 -0
- package/src/env/resolve.js +184 -0
- package/src/env/teardown.js +94 -0
- package/src/init.js +82 -27
- package/src/prompts.js +23 -12
- package/src/sync/apply.js +66 -0
- package/src/sync/base.js +83 -0
- package/src/sync/compare.js +99 -0
- package/src/sync/config.js +198 -0
- package/src/sync/mcp.js +112 -0
- package/src/sync/normalize.js +249 -0
- package/src/sync/pull.js +84 -0
- package/src/sync/push.js +106 -0
- 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
|
-
##
|
|
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
|
-
|
|
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. **
|
|
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 β
|
|
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.
|