@skitterbyte/skitterspec 15.0.5 → 16.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -113,6 +113,9 @@ status.
113
113
 
114
114
  Every spec header carries:
115
115
 
116
+ - `> **Name:**` — the spec's folder name (`feat-`/`bug-`/`hotfix-<kebab-name>`).
117
+ It's the handle you pass to `/spec-go` and the other lifecycle skills, surfaced
118
+ in the header so it's copy-pasteable without digging for the folder name.
116
119
  - `> **Author:**` — who created the spec (set at `/spec` / `/spec-bug`, defaults
117
120
  to `git config user.name`).
118
121
  - `> **Developer:**` — who implements it (`—` until `/spec-go` starts work, then
@@ -14,6 +14,8 @@ Lifecycle (the governing skills) — status in parentheses:
14
14
  `/spec-cancel` (Cancelled). See `.claude/rules/spec-planning.md`. (There is no
15
15
  separate grooming command — `/spec` grills to a Ready spec directly.)
16
16
 
17
+
18
+
17
19
  ## Phase A — reach a clear shared understanding (grill first)
18
20
 
19
21
  Interview the user until requirement AND proposed solution are unambiguous. Do
@@ -73,6 +75,7 @@ the codebase, link rather than duplicate):
73
75
  # <Feature title>
74
76
 
75
77
  > **Type:** Feature
78
+ > **Name:** feat-<kebab-name> (the spec folder name — the handle you paste into `/spec-go`)
76
79
  > **Status:** Ready — not started
77
80
  > **Author:** <git user.name — `git config user.name`>
78
81
  > **Developer:** —
@@ -225,3 +228,5 @@ while the repo stays the source of truth — follow the provider's link steps
225
228
  below (nothing to do here without one).
226
229
 
227
230
 
231
+
232
+
@@ -13,6 +13,8 @@ Spec type convention (see `.claude/rules/spec-planning.md`):
13
13
  - Bug specs are named `bug-<kebab-name>`; feature specs `feat-<kebab-name>`.
14
14
  - Every spec header carries `> **Type:** Bug` (or `Feature`).
15
15
 
16
+
17
+
16
18
  ## 1. Reproduce & isolate (light investigation)
17
19
 
18
20
  Bugs are concrete — confirm, don't over-grill. Establish:
@@ -95,6 +97,7 @@ because work is already underway. Keep it lean:
95
97
  # Bug: <short title>
96
98
 
97
99
  > **Type:** Bug
100
+ > **Name:** bug-<kebab-name> (the spec folder name — the handle you paste into `/spec-go`)
98
101
  > **Status:** In Progress — fixing (red test added)
99
102
  > **Author:** <git user.name — who reported/captured it>
100
103
  > **Developer:** <git user.name — you, since you're fixing it now>
@@ -25,6 +25,10 @@ it verbatim/condensed for the header.
25
25
  tick anything that was actually completed before cancelling so the record is
26
26
  honest about what landed.
27
27
  - Note any partial/abandoned work so it isn't mistaken for unstarted.
28
+ - **Check for pre-existing uncommitted changes — before you touch anything.**
29
+ Run `git status`. Anything already uncommitted is the *user's* work and must not
30
+ be swept into the cancellation commit: offer `/commit` and **stop**. Everything
31
+ this skill writes in steps 4–5 is its own, and step 5 commits that.
28
32
 
29
33
  ## 4. Stamp the spec
30
34
 
@@ -41,16 +45,27 @@ Append a **State log** row:
41
45
  Add a **Changelog** entry:
42
46
  `- <YYYY-MM-DD> — Cancelled: <reason>.`
43
47
 
44
- ## 5. Move to cancelled
48
+ ## 5. Move to cancelled — and commit it
45
49
 
46
50
  `mkdir -p specs/cancelled` then **`git mv`** the file or folder:
47
51
  `git mv "specs/<bucket>/<name>" "specs/cancelled/<name>"` (preserve history;
48
52
  move the whole folder).
49
53
 
54
+ Then **commit the cancellation edits** — this skill wrote them, so it commits
55
+ them:
56
+
57
+ ```
58
+ git add specs/ && git commit -m "chore(spec): cancel <name>"
59
+ ```
60
+
61
+ **This matters more here than anywhere else.** Teardown (step 7) refuses a dirty
62
+ worktree and offers `--force` as the way through — and forcing would destroy the
63
+ cancellation record this skill just wrote. Committing first means teardown never
64
+ needs `--force`. Do not `git push`.
65
+
50
66
  ## 6. Report
51
67
 
52
- Confirm the cancellation, the reason recorded, and the new location. Do **not**
53
- `git commit` unless the user asks.
68
+ Confirm the cancellation, the reason recorded, the new location, and the commit.
54
69
 
55
70
  ## 7. Tear down the environment (opt-in, only if configured)
56
71
 
@@ -27,6 +27,12 @@ Before marking complete, confirm the work is actually finished:
27
27
  - If genuinely incomplete work remains, **stop and tell the user** rather than
28
28
  forcing completion. Offer to finish it (`/spec-go`) or to complete with the
29
29
  remaining items explicitly listed as deferred.
30
+ - **Check for pre-existing uncommitted changes — before you touch anything.**
31
+ Run `git status`. Anything already uncommitted is the *user's* work (a
32
+ half-finished phase, a stray fix) and must not be swept into the completion
33
+ commit: offer `/commit` and **stop**. Everything this skill writes in steps 3–4
34
+ is its own, and step 4 commits that.
35
+
30
36
 
31
37
  ## 3. Update the spec
32
38
 
@@ -40,17 +46,28 @@ Before marking complete, confirm the work is actually finished:
40
46
  `- <YYYY-MM-DD> — Completed; all phases done, tests green.`
41
47
  (Note any consciously-deferred items here too.)
42
48
 
43
- ## 4. Move to complete
49
+ ## 4. Move to complete — and commit it
44
50
 
45
51
  `mkdir -p specs/complete` then **`git mv`** the file or folder:
46
52
  `git mv "specs/in-progress/<name>" "specs/complete/<name>"` (preserve history;
47
53
  move the whole folder). The `specs/complete/` folder is the record of finished
48
54
  specs — `git log`/the per-spec State log give the completion order.
49
55
 
56
+ Then **commit the completion edits** — steps 3–4 are this skill's own output, so
57
+ it finishes its own work rather than handing you a dirty tree:
58
+
59
+ ```
60
+ git add specs/ && git commit -m "chore(spec): complete <name>"
61
+ ```
62
+
63
+ Step 2 established the tree was otherwise clean, so this commits exactly the
64
+ status flip and the move — nothing of yours rides along. **This is what lets step
65
+ 6 land:** `integrate` refuses a dirty worktree, so without committing here the
66
+ skill would block on the very edits it just made. Do not `git push`.
67
+
50
68
  ## 5. Report
51
69
 
52
- Confirm the move, the final test result, and list anything deferred. Do **not**
53
- `git commit` unless the user asks.
70
+ Confirm the move, the commit, the final test result, and list anything deferred.
54
71
 
55
72
  ## 6. Land the branch (opt-in, only if isolated)
56
73
 
@@ -65,8 +82,9 @@ destination in one flow. **How it lands depends on the spec type:**
65
82
  A hotfix is built on an old release **tag**, so it can't fast-forward onto `main`.
66
83
  Use the hotfix landing instead of the integrate steps below:
67
84
 
68
- 1. **Require a clean worktree** the completion edits (status flip, `git mv` to
69
- `complete/`) must be committed first. If dirty, offer `/commit` and **stop**.
85
+ 1. **Require a clean worktree.** Step 4 already committed the completion edits, so
86
+ this should pass. If the tree is *still* dirty, that's unrelated work — offer
87
+ `/commit` and **stop**.
70
88
  2. **Plan + execute.** Run `skitterspec spec-env hotfix land <name>` — add
71
89
  `--also <tag>` for each extra release line to patch (test/demo on their own
72
90
  versions). Run the printed commands **in order**. It:
@@ -92,9 +110,9 @@ is finished — e.g. to run a later phase in CI or a shared test env? Use
92
110
  **`/spec-to-main`**: same rebase + fast-forward, but it leaves the spec
93
111
  `In Progress` and the worktree standing, and it's repeatable.)
94
112
 
95
- 1. **Require a clean worktree.** The completion edits (status flip, the
96
- `git mv` to `complete/`) must be committed first integrate refuses a dirty
97
- tree. If it's dirty, offer `/commit` and **stop**; don't auto-commit.
113
+ 1. **Require a clean worktree.** Step 4 already committed the completion edits, so
114
+ integrate's dirty-tree guard should pass. If the tree is *still* dirty, that's
115
+ unrelated work offer `/commit` and **stop**; don't sweep it in.
98
116
  **If the spec is live** (you took the running instance with `/spec-live`):
99
117
  `integrate` is live-aware — it ends the live session first (releases the branch
100
118
  back to base, re-isolates it into its worktree, clears the receipt), then prints
@@ -47,16 +47,26 @@ same "in place otherwise" path used when isolation is off. Warn that the work
47
47
  will land wherever you currently are (usually `main`); reserve it for a trivial
48
48
  change or an explicit request.
49
49
 
50
- - Run `skitterspec spec-env up <name>` (the `spec-env` CLI engine). It adds a git
51
- worktree on a branch forked from `main`, and only when the spec's
52
- `> **Stack:**` header is `worktree + docker` also brings up its Docker stack.
50
+ - Run `skitterspec spec-env up <name>` (the `spec-env` CLI engine). It is a
51
+ **planner: it prints commands and creates nothing itself.** Under
52
+ `to provision, run:` it emits the `git worktree add` on a branch forked from
53
+ `main` and — only when the spec's `> **Stack:**` header is
54
+ `worktree + docker` — the Docker bring-up.
55
+ **Run those commands and confirm they succeeded** before anything below: every
56
+ later step assumes the worktree exists, and the header line says
57
+ `(plan — nothing created yet)` precisely because at that point it doesn't.
53
58
  Print the worktree path and the opener command it emits.
54
59
  - **Bootstrap the worktree's dependencies.** A fresh worktree has an empty
55
60
  working tree — no installed dependencies, and none of the repo's gitignored
56
61
  files (`.env`, local secret/config overrides) — so git hooks, typechecks,
57
62
  builds and tests fail until they're in place. `spec-env up` prints the
58
- project's configured **`in the worktree, run:`** commands — run them in the
59
- worktree, in order, before doing anything else. Those commands are: first any
63
+ project's configured **`then, in the worktree, run:`** commands — run them in
64
+ order, before doing anything else. Each one begins by `cd`-ing into the
65
+ worktree, so it works from any cwd and cannot quietly act on the main
66
+ checkout; if the worktree is missing it prints
67
+ **`no worktree at … — run the provisioning commands first`** and exits
68
+ non-zero. Seeing that means the `git worktree add` above didn't run or didn't
69
+ work — fix that before going on. Those commands are: first any
60
70
  **file seeding** (from `env.config.json` → `seedFiles`), which symlinks or
61
71
  copies the configured gitignored files from the main checkout into the fresh
62
72
  worktree so setup can rely on them; then the **`setup`** commands (e.g. an
@@ -89,6 +89,7 @@ is usually a single-pass fix, so the `## Fix` block can live directly in
89
89
  # Hotfix: <short title>
90
90
 
91
91
  > **Type:** Hotfix
92
+ > **Name:** hotfix-<kebab-name> (the spec folder name — the handle you paste into `/spec-go`)
92
93
  > **Status:** In Progress — fixing (red test added)
93
94
  > **Author:** <git user.name — who reported/captured it>
94
95
  > **Developer:** <git user.name — you, since you're fixing it now>
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@skitterbyte/skitterspec",
3
- "version": "15.0.5",
3
+ "version": "16.1.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
@@ -60,7 +60,8 @@ Usage:
60
60
  .core config alone.
61
61
  skitterspec spec-env <cmd> Per-spec isolation engine (opt-in; needs
62
62
  specs/.core/env.config.json). Subcommands:
63
- up <spec> plan a worktree + Docker stack + opener
63
+ up <spec> print the plan to provision a worktree +
64
+ Docker stack (prints commands; creates nothing)
64
65
  down <spec> tear down (guards; --keep-volumes, --force)
65
66
  prune reap orphaned test-DB volumes (--older-than <days>)
66
67
  dev up <spec> start host dev servers on the spec's ports
@@ -176,8 +177,10 @@ function specEnvStatus(dir, config) {
176
177
  })
177
178
  }
178
179
 
179
- // Provision: allocate the slot, persist the registry, and print the plan the
180
- // /spec-env skill executes (git worktree add, docker compose up, .env, opener).
180
+ // Plan a provision: allocate the slot, persist the registry, and print the plan
181
+ // the /spec-env skill executes (git worktree add, docker compose up, .env,
182
+ // opener). This creates no worktree and starts no stack — the caller runs the
183
+ // printed commands. Keep the output's verb honest about that.
181
184
  function specEnvUp(dir, config, specArg) {
182
185
  if (!specArg) {
183
186
  process.stdout.write('Usage: skitterspec spec-env up <spec>\n')
@@ -224,7 +227,16 @@ function specEnvUp(dir, config, specArg) {
224
227
  const plan = planUp(spec, { slot, attached }, config)
225
228
 
226
229
  const out = []
227
- out.push(`spec-env up: ${spec.folder} ${attached ? '(attached existing)' : '(provisioned)'}`)
230
+ // `up` is a planner: it prints commands for the caller to run and creates no
231
+ // worktree or stack itself (the registry slot and the trust entry, both reported
232
+ // separately below, are its only writes). Say so in the verb — a past-tense
233
+ // "(provisioned)" reads as a completed state change, and a caller that believes
234
+ // it skips the commands and works on `main`, which is what isolation exists to
235
+ // prevent.
236
+ out.push(
237
+ `spec-env up: ${spec.folder} ` +
238
+ (attached ? '(plan — worktree exists; will attach)' : '(plan — nothing created yet)'),
239
+ )
228
240
  out.push('')
229
241
  out.push(` worktree: ${plan.worktreePath}`)
230
242
  out.push(` branch: ${plan.branch}`)
@@ -247,7 +259,7 @@ function specEnvUp(dir, config, specArg) {
247
259
  )
248
260
  }
249
261
  out.push('')
250
- out.push(' run these:')
262
+ out.push(' to provision, run:')
251
263
  for (const cmd of plan.commands) out.push(` ${cmd}`)
252
264
  if (plan.openCommand) out.push(` ${plan.openCommand}`)
253
265
  // Seed files first (setup may depend on them), then the setup commands —
@@ -255,7 +267,7 @@ function specEnvUp(dir, config, specArg) {
255
267
  const worktreeSteps = [...plan.seedCommands, ...plan.setupCommands]
256
268
  if (worktreeSteps.length) {
257
269
  out.push('')
258
- out.push(' in the worktree, run:')
270
+ out.push(' then, in the worktree, run:')
259
271
  for (const cmd of worktreeSteps) out.push(` ${cmd}`)
260
272
  }
261
273
  if (plan.envContents) {
@@ -15,6 +15,37 @@ const { portOffset } = require('./registry.js')
15
15
  const { renderEnvFile, expandOpenCommand } = require('./render.js')
16
16
  const { expandTokens } = require('./resolve.js')
17
17
 
18
+ /**
19
+ * Build the POSIX-sh prefix that puts a command in the spec's worktree, or stops.
20
+ *
21
+ * Every command under the "then, in the worktree, run:" heading carries this.
22
+ * Without it those commands stay silent when run from the wrong place: in the
23
+ * primary checkout `$m` resolves to the checkout itself, so a seed's source and
24
+ * target are the same path and it prints `exists — skipped` — indistinguishable
25
+ * from a correctly-provisioned re-run — while a setup command like an install
26
+ * runs against the main checkout and "succeeds" too. Three layers then agree that
27
+ * nothing is wrong, and the caller carries on committing to `main`.
28
+ *
29
+ * A `cd` is deliberately chosen over comparing `git rev-parse --show-toplevel`
30
+ * against `worktreePath`: the planned path is a lexical `path.resolve` while git
31
+ * reports the symlink-resolved one (`/tmp` vs `/private/tmp` on macOS), so a
32
+ * string compare would refuse a perfectly good worktree. `cd` sidesteps that, and
33
+ * does better than refusing — it *positions* the command, so a caller who is in
34
+ * the wrong directory still gets correct behaviour. When the worktree was never
35
+ * created — the reported case — the `cd` fails and takes the whole command with
36
+ * it, non-zero and loud.
37
+ *
38
+ * Per-command, not once at the top of the block: a single leading `cd` only
39
+ * protects the sequence if the caller chains it with `&&`, and the reported
40
+ * failure is precisely a caller that ran the steps as separate statements.
41
+ */
42
+ function worktreeCd(worktreePath) {
43
+ return (
44
+ `cd "${worktreePath}" 2>/dev/null || ` +
45
+ `{ echo "no worktree at ${worktreePath} — run the provisioning commands first"; exit 1; }`
46
+ )
47
+ }
48
+
18
49
  /**
19
50
  * Build one idempotent POSIX-sh command that seeds a gitignored file from the
20
51
  * main checkout into the current worktree (the cwd when the skill runs it).
@@ -84,13 +115,18 @@ function planUp(spec, alloc, config) {
84
115
  // config ⇒ no commands ⇒ current behaviour.
85
116
  const seed = config.seedFiles || { mode: 'symlink', files: [] }
86
117
  const seedMode = seed.mode === 'copy' ? 'copy' : 'symlink'
87
- const seedCommands = (seed.files || []).map((file) => seedCommandFor(file, seedMode))
118
+ const guard = worktreeCd(spec.worktreePath)
119
+ const seedCommands = (seed.files || []).map(
120
+ (file) => `${guard}; ${seedCommandFor(file, seedMode)}`,
121
+ )
88
122
 
89
123
  // Bootstrap commands run *in the worktree* after `git worktree add` (before
90
124
  // Docker/dev), on every provision including re-attach — deps must exist for
91
125
  // the worktree to be usable. Kept separate from `commands` (run from the
92
126
  // primary checkout root); the CLI prints them under an "in the worktree" head.
93
- const setupCommands = (config.setup || []).map((cmd) => expandTokens(cmd, tokens))
127
+ const setupCommands = (config.setup || []).map(
128
+ (cmd) => `${guard}; ${expandTokens(cmd, tokens)}`,
129
+ )
94
130
 
95
131
  const commands = []
96
132
  // Fresh branch → -b; attach an existing branch/slot → plain form (never clobber).
@@ -122,4 +158,4 @@ function planUp(spec, alloc, config) {
122
158
  }
123
159
  }
124
160
 
125
- module.exports = { planUp, seedCommandFor }
161
+ module.exports = { planUp, seedCommandFor, worktreeCd }
@@ -251,6 +251,7 @@ function resolveSpec(specArg, dir, config, opts = {}) {
251
251
  }
252
252
 
253
253
  module.exports = {
254
+ BUCKETS,
254
255
  resolveSpec,
255
256
  resolveBaseBranch,
256
257
  resolvePrimaryCheckout,