@skitterbyte/skitterspec 8.0.1 → 8.3.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/assets/core/env.config.json.example +4 -0
- package/assets/core/env.config.md +22 -0
- package/assets/skills/spec-bug/SKILL.md +55 -8
- package/assets/skills/spec-go/SKILL.md +16 -6
- package/package.json +1 -1
- package/src/cli.js +5 -2
- package/src/env/config.js +42 -0
- package/src/env/provision.js +37 -3
|
@@ -52,6 +52,28 @@ no live `env.config.json` was found.
|
|
|
52
52
|
// empty = no backup, volumes dropped directly.
|
|
53
53
|
},
|
|
54
54
|
|
|
55
|
+
// Gitignored files seeded from the primary checkout into a fresh worktree by
|
|
56
|
+
// `spec-env up`, right after `git worktree add` and BEFORE `setup` runs — so a
|
|
57
|
+
// fresh linked worktree (which starts with none of the repo's gitignored files)
|
|
58
|
+
// has the .env / local secret overrides / local config that setup steps and
|
|
59
|
+
// git hooks depend on. Without this a step like `prisma generate` hard-fails in
|
|
60
|
+
// the new worktree because .env (its datasource URL) isn't there.
|
|
61
|
+
// mode "symlink" (default) points the worktree file at the main file, so it
|
|
62
|
+
// stays in sync; "copy" makes an independent copy.
|
|
63
|
+
// files repo-relative paths to seed. A source absent in main is a printed
|
|
64
|
+
// no-op (not an error); a target that already exists is left untouched
|
|
65
|
+
// (idempotent — safe when `spec-env up` re-attaches an existing
|
|
66
|
+
// worktree). The main checkout is resolved robustly at run time via
|
|
67
|
+
// `git rev-parse --git-common-dir` — no hardcoded repo name or path.
|
|
68
|
+
// Shorthand: `"seedFiles": [".env", …]` == `{ "mode": "symlink", "files": […] }`.
|
|
69
|
+
// Seeded files are gitignored, so they never make the worktree "dirty" and
|
|
70
|
+
// never block teardown; they vanish with the worktree at `spec-env down`.
|
|
71
|
+
// [] (or absent) = seed nothing (current behaviour).
|
|
72
|
+
"seedFiles": {
|
|
73
|
+
"mode": "symlink",
|
|
74
|
+
"files": [".env"]
|
|
75
|
+
},
|
|
76
|
+
|
|
55
77
|
// Bootstrap commands `spec-env up <spec>` runs IN the worktree, right after
|
|
56
78
|
// `git worktree add` (before Docker/dev), on every provision including
|
|
57
79
|
// re-attach — so a fresh worktree's dependencies exist and git hooks,
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
---
|
|
2
2
|
name: spec-bug
|
|
3
|
-
description: Investigate a bug, capture it as a Bug-type spec, and drive it red→green. ALWAYS starts by reproducing the bug with a failing test, then writes the spec and works the test to green. Creates specs/in-progress/bug-<name>/00-overview.md. Use when the user reports a bug, says "/spec-bug", "investigate this bug", "this is broken — find and fix it", or pastes an error/stack trace.
|
|
3
|
+
description: Investigate a bug, capture it as a Bug-type spec, and drive it red→green. When per-spec isolation is enabled it provisions a worktree first, so the failing test and fix land on the bug's own branch, never on main. ALWAYS starts by reproducing the bug with a failing test, then writes the spec and works the test to green. Creates specs/in-progress/bug-<name>/00-overview.md. Use when the user reports a bug, says "/spec-bug", "investigate this bug", "this is broken — find and fix it", or pastes an error/stack trace.
|
|
4
4
|
---
|
|
5
5
|
|
|
6
6
|
# /spec-bug — investigate a bug, prove it with a failing test, fix it
|
|
@@ -24,7 +24,49 @@ Bugs are concrete — confirm, don't over-grill. Establish:
|
|
|
24
24
|
against the broken one (the bug usually lives in the differential). Do NOT
|
|
25
25
|
patch a symptom before you understand the cause.
|
|
26
26
|
|
|
27
|
-
## 2.
|
|
27
|
+
## 2. Isolate the fix in a worktree — when isolation is enabled
|
|
28
|
+
|
|
29
|
+
**Only when per-spec isolation is enabled** (`specs/.core/env.config.json`
|
|
30
|
+
exists). Skip this whole section otherwise — the fix happens in place, on the
|
|
31
|
+
current branch.
|
|
32
|
+
|
|
33
|
+
**Opt-out:** if the user passes `--no-worktree` (or explicitly asks to work in
|
|
34
|
+
place), skip this whole section and fix on the current branch — same as when
|
|
35
|
+
isolation is off. Warn that the fix will land wherever you currently are (usually
|
|
36
|
+
`main`); reserve it for a trivial one-liner or an explicit request.
|
|
37
|
+
|
|
38
|
+
A bug fix changes real source, so — exactly like `/spec-go` — it belongs on the
|
|
39
|
+
bug's **own branch**, never directly on `main`. Provision the worktree **now**,
|
|
40
|
+
before the failing test, so the test, the fix, and the spec all land together and
|
|
41
|
+
arrive as one reviewable PR.
|
|
42
|
+
|
|
43
|
+
The engine resolves a spec by its folder, so seed a **minimal stub** for it to
|
|
44
|
+
provision from — you'll flesh it out in §4:
|
|
45
|
+
|
|
46
|
+
- From the base branch (`main`), create
|
|
47
|
+
`specs/in-progress/bug-<name>/00-overview.md` with just the header block and the
|
|
48
|
+
`## Symptom` you established above.
|
|
49
|
+
- Run `skitterspec spec-env up bug-<name>` (the `spec-env` CLI engine). It prints
|
|
50
|
+
the `git worktree add … -b bug/<name>` command (a branch forked from `main`),
|
|
51
|
+
the worktree path, the opener, and any `in the worktree, run:` bootstrap steps.
|
|
52
|
+
- Run the printed `git worktree add`. **The worktree forks from `main`'s last
|
|
53
|
+
commit, so your uncommitted stub doesn't travel with it** — move it across so
|
|
54
|
+
`main` is left pristine:
|
|
55
|
+
`mv specs/in-progress/bug-<name> <worktreePath>/specs/in-progress/`.
|
|
56
|
+
- **Bootstrap the worktree.** A fresh worktree has no installed dependencies and
|
|
57
|
+
none of the repo's gitignored files (`.env`, local overrides). Run the printed
|
|
58
|
+
`in the worktree, run:` steps (file seeding, then `setup`) in order, before
|
|
59
|
+
anything else.
|
|
60
|
+
- **Trust the worktree for this session.** The engine wrote the printed
|
|
61
|
+
`trusted:` root into `.claude/settings.local.json`, but it won't hot-reload now
|
|
62
|
+
— run `/add-dir <trusted root>` before editing into the worktree, or the first
|
|
63
|
+
edits will prompt.
|
|
64
|
+
- **Do everything below in the worktree**, on the branch — the red test, the fix,
|
|
65
|
+
and the rest of the spec. Act on the worktree with absolute paths /
|
|
66
|
+
`git -C <worktreePath>`, or open a fresh session rooted there (the printed
|
|
67
|
+
opener). `main` changes only when the branch merges (at `/spec-complete`).
|
|
68
|
+
|
|
69
|
+
## 3. Write the failing test FIRST (RED) — mandatory
|
|
28
70
|
|
|
29
71
|
Encode the **correct** (expected) behaviour as a test, then run it and confirm it
|
|
30
72
|
**fails for the right reason**:
|
|
@@ -36,10 +78,13 @@ Encode the **correct** (expected) behaviour as a test, then run it and confirm i
|
|
|
36
78
|
passes before the fix proves nothing — keep refining the assertion until it
|
|
37
79
|
genuinely captures the bug.
|
|
38
80
|
|
|
39
|
-
##
|
|
81
|
+
## 4. Write the Bug spec
|
|
40
82
|
|
|
41
|
-
|
|
42
|
-
|
|
83
|
+
Fill in the spec's entry point `00-overview.md`. **When isolated**, you already
|
|
84
|
+
seeded this stub in §2 and moved it into the worktree — flesh it out there.
|
|
85
|
+
**When not isolated**, create the spec **folder**
|
|
86
|
+
`specs/in-progress/bug-<kebab-name>/` with its entry point `00-overview.md` now
|
|
87
|
+
(every spec is a folder — never a bare file). A bug is
|
|
43
88
|
usually a single-pass fix, so the `## Fix` block can live directly in
|
|
44
89
|
`00-overview.md`. **If the fix needs phasing** (large/uncertain root cause),
|
|
45
90
|
split it into phase files (`01-<slug>.md`, `02-…`) with a phase index in
|
|
@@ -90,7 +135,7 @@ The **State log** is the folder/status audit trail; later transitions
|
|
|
90
135
|
(`/spec-complete`, `/spec-cancel`) append a row. The **Changelog** is for the
|
|
91
136
|
fix narrative and decisions — keep them separate.
|
|
92
137
|
|
|
93
|
-
##
|
|
138
|
+
## 5. Drive to GREEN
|
|
94
139
|
|
|
95
140
|
- Implement the **minimal, root-cause** fix. Match surrounding code; honour all
|
|
96
141
|
project rules (see `.claude/rules/`).
|
|
@@ -103,8 +148,10 @@ test, split the fix into phase files (`01-<slug>.md` …) with a phase index in
|
|
|
103
148
|
`00-overview.md`, and leave the spec in `in-progress` for `/spec-go` to continue.
|
|
104
149
|
Say so explicitly — don't fake green.
|
|
105
150
|
|
|
106
|
-
##
|
|
151
|
+
## 6. Report
|
|
107
152
|
|
|
108
153
|
Summarise: root cause, the failing→passing test, the fix, and the full test
|
|
109
154
|
result. The spec stays in `in-progress`; suggest `/spec-complete` to verify and
|
|
110
|
-
archive it
|
|
155
|
+
archive it (**when isolated**, the fix lives on the bug's branch, and
|
|
156
|
+
`/spec-complete` merges it back to `main`). Do **not** `git commit` unless the
|
|
157
|
+
user asks.
|
|
@@ -26,17 +26,27 @@ Diverting your browser to the spec is a separate explicit step — `/spec-connec
|
|
|
26
26
|
the spec doesn't already have a worktree, provision it **first**, so all the
|
|
27
27
|
housekeeping below lands on the spec's branch and never on `main`:
|
|
28
28
|
|
|
29
|
+
**Opt-out:** if the user passes `--no-worktree` (or explicitly asks to work in
|
|
30
|
+
place), skip the provisioning bullets below and build on the current branch — the
|
|
31
|
+
same "in place otherwise" path used when isolation is off. Warn that the work
|
|
32
|
+
will land wherever you currently are (usually `main`); reserve it for a trivial
|
|
33
|
+
change or an explicit request.
|
|
34
|
+
|
|
29
35
|
- Run `skitterspec spec-env up <name>` (the `spec-env` CLI engine). It adds a git
|
|
30
36
|
worktree on a branch forked from `main`, and — only when the spec's
|
|
31
37
|
`> **Stack:**` header is `worktree + docker` — also brings up its Docker stack.
|
|
32
38
|
Print the worktree path and the opener command it emits.
|
|
33
39
|
- **Bootstrap the worktree's dependencies.** A fresh worktree has an empty
|
|
34
|
-
working tree — no installed dependencies
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
+
working tree — no installed dependencies, and none of the repo's gitignored
|
|
41
|
+
files (`.env`, local secret/config overrides) — so git hooks, typechecks,
|
|
42
|
+
builds and tests fail until they're in place. `spec-env up` prints the
|
|
43
|
+
project's configured **`in the worktree, run:`** commands — run them in the
|
|
44
|
+
worktree, in order, before doing anything else. Those commands are: first any
|
|
45
|
+
**file seeding** (from `env.config.json` → `seedFiles`), which symlinks or
|
|
46
|
+
copies the configured gitignored files from the main checkout into the fresh
|
|
47
|
+
worktree so setup can rely on them; then the **`setup`** commands (e.g. an
|
|
48
|
+
install command). With neither configured there's nothing to run; add
|
|
49
|
+
`seedFiles`/`setup` if agents keep stalling on a missing `.env` or dependencies.
|
|
40
50
|
- **Trust the worktree for this session.** The engine wrote the printed
|
|
41
51
|
`trusted:` root into `.claude/settings.local.json` (gitignored) so future
|
|
42
52
|
sessions trust it automatically — but that file likely won't hot-reload now,
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@skitterbyte/skitterspec",
|
|
3
|
-
"version": "8.0
|
|
3
|
+
"version": "8.3.0",
|
|
4
4
|
"description": "Spec-driven development for Claude Code — a tracker-free filesystem workflow: lifecycle skills and per-spec isolation. For Linear sync, install @skitterbyte/skitterspec-linear instead.",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"claude",
|
package/src/cli.js
CHANGED
|
@@ -212,10 +212,13 @@ function specEnvUp(dir, config, specArg) {
|
|
|
212
212
|
out.push(' run these:')
|
|
213
213
|
for (const cmd of plan.commands) out.push(` ${cmd}`)
|
|
214
214
|
if (plan.openCommand) out.push(` ${plan.openCommand}`)
|
|
215
|
-
|
|
215
|
+
// Seed files first (setup may depend on them), then the setup commands —
|
|
216
|
+
// both run in the worktree, under one heading.
|
|
217
|
+
const worktreeSteps = [...plan.seedCommands, ...plan.setupCommands]
|
|
218
|
+
if (worktreeSteps.length) {
|
|
216
219
|
out.push('')
|
|
217
220
|
out.push(' in the worktree, run:')
|
|
218
|
-
for (const cmd of
|
|
221
|
+
for (const cmd of worktreeSteps) out.push(` ${cmd}`)
|
|
219
222
|
}
|
|
220
223
|
if (plan.envContents) {
|
|
221
224
|
out.push('')
|
package/src/env/config.js
CHANGED
|
@@ -16,6 +16,9 @@
|
|
|
16
16
|
* worktree: { root, folderPattern },
|
|
17
17
|
* docker: { enabled, composeFile, projectNamePattern, portBase,
|
|
18
18
|
* portsPerSpec, envFile, backupCommand },
|
|
19
|
+
* seedFiles:{ mode, files } | [ ".env", ... ], // gitignored files copied/
|
|
20
|
+
* // symlinked from the main checkout into a fresh worktree before
|
|
21
|
+
* // setup runs (mode: "symlink" default | "copy"); empty = none
|
|
19
22
|
* setup: [ "cmd", ... ], // bootstrap commands run in the worktree right
|
|
20
23
|
* // after `git worktree add` (e.g. install deps); empty = none
|
|
21
24
|
* dev: [ { name, command, portVar, health?, frontPort? } ], // host dev
|
|
@@ -45,6 +48,12 @@ const DEFAULT_CONFIG = Object.freeze({
|
|
|
45
48
|
envFile: '.env',
|
|
46
49
|
backupCommand: '',
|
|
47
50
|
}),
|
|
51
|
+
// Gitignored files seeded from the main checkout into a fresh worktree by
|
|
52
|
+
// `spec-env up`, right after `git worktree add` and before `setup` runs — so a
|
|
53
|
+
// fresh worktree has the .env / local overrides that setup steps depend on.
|
|
54
|
+
// `mode` is "symlink" (default, stays in sync with main) or "copy" (an
|
|
55
|
+
// independent copy). `files` is a list of repo-relative paths. Default: none.
|
|
56
|
+
seedFiles: Object.freeze({ mode: 'symlink', files: Object.freeze([]) }),
|
|
48
57
|
// Bootstrap commands run in the worktree by `spec-env up`, right after
|
|
49
58
|
// `git worktree add` (before Docker/dev), on every provision. Array of shell
|
|
50
59
|
// strings (e.g. "pnpm install"); {slug}/{branch}/… expand. Default: none.
|
|
@@ -76,6 +85,7 @@ function defaults() {
|
|
|
76
85
|
return {
|
|
77
86
|
worktree: { ...DEFAULT_CONFIG.worktree },
|
|
78
87
|
docker: { ...DEFAULT_CONFIG.docker },
|
|
88
|
+
seedFiles: { mode: DEFAULT_CONFIG.seedFiles.mode, files: [] },
|
|
79
89
|
setup: [],
|
|
80
90
|
dev: [],
|
|
81
91
|
proxy: { ...DEFAULT_CONFIG.proxy },
|
|
@@ -127,6 +137,34 @@ function normalizeDev(parsed) {
|
|
|
127
137
|
return out
|
|
128
138
|
}
|
|
129
139
|
|
|
140
|
+
// Keep only trimmed, non-empty strings from an array of file paths (lenient).
|
|
141
|
+
function normalizeFileList(parsed) {
|
|
142
|
+
const out = []
|
|
143
|
+
for (const raw of parsed) {
|
|
144
|
+
if (typeof raw !== 'string') continue
|
|
145
|
+
const file = raw.trim()
|
|
146
|
+
if (file) out.push(file)
|
|
147
|
+
}
|
|
148
|
+
return out
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
/**
|
|
152
|
+
* Normalise a parsed `seedFiles` value into `{ mode, files }`. Accepts two forms:
|
|
153
|
+
* - the array shorthand `[".env", ...]` → { mode: 'symlink', files: [...] }
|
|
154
|
+
* - the object form `{ mode?, files? }` — `mode` is 'copy' or 'symlink'
|
|
155
|
+
* (anything else falls back to 'symlink'); `files` is a list of paths.
|
|
156
|
+
* Malformed entries are dropped (lenient, like `normalizeSetup`) so a stray
|
|
157
|
+
* value can't crash provisioning.
|
|
158
|
+
*/
|
|
159
|
+
function normalizeSeedFiles(parsed) {
|
|
160
|
+
if (Array.isArray(parsed)) {
|
|
161
|
+
return { mode: 'symlink', files: normalizeFileList(parsed) }
|
|
162
|
+
}
|
|
163
|
+
const mode = parsed.mode === 'copy' ? 'copy' : 'symlink'
|
|
164
|
+
const files = Array.isArray(parsed.files) ? normalizeFileList(parsed.files) : []
|
|
165
|
+
return { mode, files }
|
|
166
|
+
}
|
|
167
|
+
|
|
130
168
|
/**
|
|
131
169
|
* Normalise a parsed `setup` array into bootstrap commands: keep only trimmed,
|
|
132
170
|
* non-empty strings, drop everything else (lenient, like `normalizeDev`) so a
|
|
@@ -164,6 +202,10 @@ function mergeConfig(base, parsed) {
|
|
|
164
202
|
assign(base.docker, parsed.docker, 'backupCommand', 'string?')
|
|
165
203
|
}
|
|
166
204
|
|
|
205
|
+
if (Array.isArray(parsed.seedFiles) || isObject(parsed.seedFiles)) {
|
|
206
|
+
base.seedFiles = normalizeSeedFiles(parsed.seedFiles)
|
|
207
|
+
}
|
|
208
|
+
|
|
167
209
|
if (Array.isArray(parsed.setup)) {
|
|
168
210
|
base.setup = normalizeSetup(parsed.setup)
|
|
169
211
|
}
|
package/src/env/provision.js
CHANGED
|
@@ -15,6 +15,31 @@ const { portOffset } = require('./registry.js')
|
|
|
15
15
|
const { renderEnvFile, expandOpenCommand } = require('./render.js')
|
|
16
16
|
const { expandTokens } = require('./resolve.js')
|
|
17
17
|
|
|
18
|
+
/**
|
|
19
|
+
* Build one idempotent POSIX-sh command that seeds a gitignored file from the
|
|
20
|
+
* main checkout into the current worktree (the cwd when the skill runs it).
|
|
21
|
+
*
|
|
22
|
+
* The main checkout is resolved at run time from inside the worktree via
|
|
23
|
+
* `git rev-parse --git-common-dir` (absolute `<main>/.git` in a linked worktree)
|
|
24
|
+
* and its dirname — never a hardcoded repo name or a `../..` hop. The command is
|
|
25
|
+
* safe to re-run: a source absent in main is a printed no-op, an already-seeded
|
|
26
|
+
* target (real file or symlink) is left untouched, and only a genuinely missing
|
|
27
|
+
* target is created. `mode` is 'symlink' (points at main, stays in sync) or
|
|
28
|
+
* 'copy' (an independent copy). Output mirrors the setup style: `seeded <f> → …`.
|
|
29
|
+
*/
|
|
30
|
+
function seedCommandFor(file, mode) {
|
|
31
|
+
const op =
|
|
32
|
+
mode === 'copy'
|
|
33
|
+
? `cp "$m/${file}" "${file}"`
|
|
34
|
+
: `ln -s "$m/${file}" "${file}"`
|
|
35
|
+
return (
|
|
36
|
+
'm="$(dirname "$(git rev-parse --git-common-dir)")"; ' +
|
|
37
|
+
`if [ ! -e "$m/${file}" ]; then echo "seed ${file}: not in main — skipped"; ` +
|
|
38
|
+
`elif [ -e "${file}" ] || [ -L "${file}" ]; then echo "seed ${file}: exists — skipped"; ` +
|
|
39
|
+
`else mkdir -p "$(dirname "${file}")" && ${op} && echo "seeded ${file} → $m/${file}"; fi`
|
|
40
|
+
)
|
|
41
|
+
}
|
|
42
|
+
|
|
18
43
|
/**
|
|
19
44
|
* Plan a provisioning run.
|
|
20
45
|
*
|
|
@@ -24,8 +49,8 @@ const { expandTokens } = require('./resolve.js')
|
|
|
24
49
|
* existed in the registry (re-run → attach, don't clobber).
|
|
25
50
|
* @param {object} config normalised env config.
|
|
26
51
|
* @returns {object} { worktreePath, branch, projectName, slot, portOffset,
|
|
27
|
-
* envContents, openCommand, commands,
|
|
28
|
-
* attached }
|
|
52
|
+
* envContents, openCommand, commands, seedCommands,
|
|
53
|
+
* setupCommands, attached }
|
|
29
54
|
*/
|
|
30
55
|
function planUp(spec, alloc, config) {
|
|
31
56
|
const { slot, attached } = alloc
|
|
@@ -53,6 +78,14 @@ function planUp(spec, alloc, config) {
|
|
|
53
78
|
|
|
54
79
|
const openCommand = expandOpenCommand(config.open.command, tokens)
|
|
55
80
|
|
|
81
|
+
// File seeding runs *in the worktree* after `git worktree add`, before the
|
|
82
|
+
// setup commands (which may depend on the seeded .env). Each entry becomes an
|
|
83
|
+
// idempotent shell command resolving the main checkout at run time. Absent
|
|
84
|
+
// config ⇒ no commands ⇒ current behaviour.
|
|
85
|
+
const seed = config.seedFiles || { mode: 'symlink', files: [] }
|
|
86
|
+
const seedMode = seed.mode === 'copy' ? 'copy' : 'symlink'
|
|
87
|
+
const seedCommands = (seed.files || []).map((file) => seedCommandFor(file, seedMode))
|
|
88
|
+
|
|
56
89
|
// Bootstrap commands run *in the worktree* after `git worktree add` (before
|
|
57
90
|
// Docker/dev), on every provision including re-attach — deps must exist for
|
|
58
91
|
// the worktree to be usable. Kept separate from `commands` (run from the
|
|
@@ -79,9 +112,10 @@ function planUp(spec, alloc, config) {
|
|
|
79
112
|
envContents,
|
|
80
113
|
openCommand,
|
|
81
114
|
commands,
|
|
115
|
+
seedCommands,
|
|
82
116
|
setupCommands,
|
|
83
117
|
attached,
|
|
84
118
|
}
|
|
85
119
|
}
|
|
86
120
|
|
|
87
|
-
module.exports = { planUp }
|
|
121
|
+
module.exports = { planUp, seedCommandFor }
|