@skitterbyte/skitterspec 16.3.2 → 16.5.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/rules/spec-planning.md +17 -11
- package/assets/skills/spec-bug/SKILL.md +2 -0
- package/assets/skills/spec-cancel/SKILL.md +2 -0
- package/assets/skills/spec-complete/SKILL.md +38 -15
- package/assets/skills/spec-hotfix/SKILL.md +19 -4
- package/assets/skills/spec-review/SKILL.md +2 -0
- package/bin/skitterspec.js +19 -0
- package/package.json +1 -1
- package/src/cli.js +55 -21
- package/src/env/resolve.js +7 -2
- package/src/env/teardown.js +23 -9
- package/src/init.js +11 -1
|
@@ -66,17 +66,23 @@ Beneath it, `skitterspec spec-env live <take|release|abort|status>` is the engin
|
|
|
66
66
|
|
|
67
67
|
**Ticketing-provider sync (opt-in, a separate package).** The base is
|
|
68
68
|
tracker-free: it knows nothing about any specific ticketing system. A
|
|
69
|
-
ticketing provider is installed as its own distribution that plugs into
|
|
70
|
-
**seams** in the shared skills
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
69
|
+
ticketing provider is installed as its own distribution that plugs into named
|
|
70
|
+
**seams** in the shared skills and fulfils a skill-name + CLI contract.
|
|
71
|
+
Sync is **one-way**: the repo is the source of truth and the tracker is a
|
|
72
|
+
**generated mirror**. It ships `/spec-push` (repo→tracker; computes a create/update plan
|
|
73
|
+
against a committed last-pushed snapshot and applies it) and `/spec-status`
|
|
74
|
+
(read-only drift report — what would push, and whether the tracker's
|
|
75
|
+
workflow-state drifted), backed by a `spec-sync` CLI. There is no content pull —
|
|
76
|
+
the tracker is never read back or merged.
|
|
77
|
+
|
|
78
|
+
**Every skill that moves a spec through the lifecycle carries a seam**, so the
|
|
79
|
+
mirror keeps up without anyone remembering to push: `/spec`, `/spec-bug` and
|
|
80
|
+
`/spec-hotfix` link the spec they create; `/spec-go` refreshes it as work starts;
|
|
81
|
+
`/spec-complete`, `/spec-cancel` and `/spec-review` refresh it after they change
|
|
82
|
+
it. `/spec-to-main` and `/spec-live` carry none — they change no status.
|
|
83
|
+
With no provider installed the seams are empty and every skill behaves as a plain
|
|
84
|
+
filesystem workflow. See the provider package's own docs for its config and field
|
|
85
|
+
reference.
|
|
80
86
|
|
|
81
87
|
## Project conventions (fill this in)
|
|
82
88
|
|
|
@@ -158,6 +158,8 @@ The **State log** is the folder/status audit trail; later transitions
|
|
|
158
158
|
(`/spec-complete`, `/spec-cancel`) append a row. The **Changelog** is for the
|
|
159
159
|
fix narrative and decisions — keep them separate.
|
|
160
160
|
|
|
161
|
+
|
|
162
|
+
|
|
161
163
|
## 5. Drive to GREEN
|
|
162
164
|
|
|
163
165
|
- Implement the **minimal, root-cause** fix. Match surrounding code; honour all
|
|
@@ -53,6 +53,8 @@ Before marking complete, confirm the work is actually finished:
|
|
|
53
53
|
move the whole folder). The `specs/complete/` folder is the record of finished
|
|
54
54
|
specs — `git log`/the per-spec State log give the completion order.
|
|
55
55
|
|
|
56
|
+
|
|
57
|
+
|
|
56
58
|
Then **commit the completion edits** — steps 3–4 are this skill's own output, so
|
|
57
59
|
it finishes its own work rather than handing you a dirty tree:
|
|
58
60
|
|
|
@@ -128,14 +130,14 @@ is finished — e.g. to run a later phase in CI or a shared test env? Use
|
|
|
128
130
|
recover those commits onto the branch, then re-run.
|
|
129
131
|
- *no worktree* — the spec is live but its worktree is gone. Re-isolate it with
|
|
130
132
|
`skitterspec spec-env up <name>`, then re-run.
|
|
131
|
-
Relay the diagnostic to the user and **stop** — do not
|
|
133
|
+
Relay the diagnostic to the user and **stop** — do not tear anything down.
|
|
132
134
|
2. **Plan + execute.** Run `skitterspec spec-env integrate <name>` and run the
|
|
133
135
|
printed commands **in order**:
|
|
134
136
|
- `git -C <worktree> rebase <base>` — replay the branch onto base.
|
|
135
137
|
- `git -C <mainRepoPath> merge --ff-only <branch>` — fast-forward base.
|
|
136
138
|
On a **rebase conflict** (non-zero exit), run
|
|
137
139
|
`git -C <worktree> rebase --abort`, relay the conflict, and **stop** — leave it
|
|
138
|
-
to the user; do not
|
|
140
|
+
to the user; do not tear anything down.
|
|
139
141
|
On a **no-op** ("already landed"), just say so and continue.
|
|
140
142
|
3. **Re-test on base.** Run the project's test command from the primary checkout;
|
|
141
143
|
it must be **green** before you call the landing done.
|
|
@@ -144,9 +146,22 @@ is finished — e.g. to run a later phase in CI or a shared test env? Use
|
|
|
144
146
|
|
|
145
147
|
## 7. Tear down the environment (opt-in, only if configured)
|
|
146
148
|
|
|
147
|
-
**Only when `specs/.core/env.config.json` exists
|
|
148
|
-
|
|
149
|
-
directly (the old `/spec-env-down` skill is gone —
|
|
149
|
+
**Only when `specs/.core/env.config.json` exists.** Reclaiming the environment is
|
|
150
|
+
what completing a spec *is*, so sub-steps 1–3 run **automatically — do not ask**.
|
|
151
|
+
Run the `spec-env` CLI directly (the old `/spec-env-down` skill is gone —
|
|
152
|
+
teardown is folded in here).
|
|
153
|
+
|
|
154
|
+
**The precondition is that the work actually landed.** Only tear down when step 6
|
|
155
|
+
completed: it landed (or reported "already landed") **and** the base suite came
|
|
156
|
+
back green. A rebase conflict, a work-loss abort or a red suite means step 6 told
|
|
157
|
+
you to stop — tear nothing down, because the worktree is where the user picks the
|
|
158
|
+
problem up. That precondition is what makes a confirmation redundant: by the time
|
|
159
|
+
you get here the branch is an ancestor of base and the engine's guards have
|
|
160
|
+
nothing left to protect.
|
|
161
|
+
|
|
162
|
+
**Opt-out:** if the user passed **`--keep-env`**, skip sub-steps 1–3, say the
|
|
163
|
+
worktree and branch are being kept, and go straight to sub-step 4. Mention
|
|
164
|
+
`skitterspec spec-env up <name>` re-attaches it later either way.
|
|
150
165
|
|
|
151
166
|
1. **Disconnect the proxy if this spec is connected.** If `.spec-env/connected`
|
|
152
167
|
names this spec, run `skitterspec spec-env connect main` first so the
|
|
@@ -154,20 +169,28 @@ directly (the old `/spec-env-down` skill is gone — teardown is folded in here)
|
|
|
154
169
|
2. **Stop its host dev servers:** `skitterspec spec-env dev down <name>` (a
|
|
155
170
|
no-op when none are running / configured).
|
|
156
171
|
3. **Remove worktree + stack + slot:** run `skitterspec spec-env down <name>`
|
|
157
|
-
and execute the commands it prints, in order. After a
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
172
|
+
and execute the commands it prints, in order. After a landing — merged into
|
|
173
|
+
base for a **feature/bug**, captured by the deploy tag for a **hotfix** —
|
|
174
|
+
teardown needs **no `--force`** and drops the branch with `git branch -D`,
|
|
175
|
+
which is safe precisely because the commits are already somewhere else. It
|
|
176
|
+
still respects the guards (won't destroy a dirty, or unpushed-and-unlanded,
|
|
177
|
+
worktree without `--force`), so if it *does* refuse, relay that and stop
|
|
178
|
+
rather than reaching for `--force`.
|
|
164
179
|
4. **Reap orphaned test-DB volumes:** run `skitterspec spec-env prune`. It lists
|
|
165
180
|
Docker volumes in the repo namespace that belong to **no live spec** (no
|
|
166
181
|
worktree) — leftovers from declined/aborted teardowns, manual
|
|
167
182
|
`git worktree remove`, or `--keep-volumes`. Show the user the orphan list and,
|
|
168
183
|
**only on their confirmation**, execute the printed `docker volume rm`
|
|
169
|
-
commands.
|
|
170
|
-
|
|
171
|
-
|
|
184
|
+
commands. **This one still asks**, unlike 1–3: it reaps volumes belonging to
|
|
185
|
+
*other* specs, and this spec having landed cleanly says nothing about those.
|
|
186
|
+
Non-fatal: if prune can't run (Docker down) or the user declines, report it and
|
|
187
|
+
finish completing anyway — never block the spec on it. Skip when Docker isn't
|
|
188
|
+
in use (the command self-reports "no orphaned volumes").
|
|
189
|
+
|
|
190
|
+
**Say what you reclaimed.** With no confirmation step the user never saw this
|
|
191
|
+
coming, so the final report must name the worktree path removed and the branch
|
|
192
|
+
deleted (or, under `--keep-env`, that both were kept). A teardown nobody
|
|
193
|
+
authorised and nobody was told about is the one way this step can lose someone's
|
|
194
|
+
place.
|
|
172
195
|
|
|
173
196
|
If `env.config.json` is absent, skip this entirely — behave exactly as before.
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
---
|
|
2
2
|
name: spec-hotfix
|
|
3
|
-
description: Fix a production bug on the exact released version — fork a worktree from a release tag, drive it red→green like /spec-bug, then land it by tagging a new patch (for CI/CD to deploy) and cherry-picking the fix back onto main. ALWAYS starts from a base tag and works on the hotfix's own branch, never on main. Creates specs/in-progress/hotfix-<name>/00-overview.md. Use when the user says "/spec-hotfix", "hotfix <tag>", "prod is broken on <version>", "patch the released version", or needs a fix shipped against a tagged release rather than main.
|
|
3
|
+
description: Fix a production bug on the exact released version — fork a worktree from a release tag, drive it red→green like /spec-bug, then land it by tagging a new patch (for CI/CD to deploy) and cherry-picking the fix back onto main. ALWAYS starts from a base tag and works on the hotfix's own branch, never on main. Can start from a tracker issue when a provider is installed (`/spec-hotfix <tag> <ISSUE-REF>`), adopting it as the hotfix's issue. Creates specs/in-progress/hotfix-<name>/00-overview.md. Use when the user says "/spec-hotfix", "hotfix <tag>", "prod is broken on <version>", "patch the released version", or needs a fix shipped against a tagged release rather than main.
|
|
4
4
|
---
|
|
5
5
|
|
|
6
6
|
# /spec-hotfix — fix a released version, tag it, cherry-pick back to main
|
|
@@ -20,11 +20,24 @@ Spec type convention (see `.claude/rules/spec-planning.md`):
|
|
|
20
20
|
tag + cherry-pick — it needs the isolation engine (`specs/.core/env.config.json`).
|
|
21
21
|
If isolation is absent, say so and stop; there is no in-place path.
|
|
22
22
|
|
|
23
|
+
|
|
24
|
+
|
|
23
25
|
## 1. Establish the base version (the tag)
|
|
24
26
|
|
|
25
|
-
-
|
|
26
|
-
`/spec-hotfix v33.16.4 login-crash`).
|
|
27
|
-
|
|
27
|
+
- **Read the arguments.** `/spec-hotfix <tag> <name>` (e.g.
|
|
28
|
+
`/spec-hotfix v33.16.4 login-crash`). An argument shaped like an issue
|
|
29
|
+
reference — letters, a hyphen, digits (`SKI-123`) —
|
|
30
|
+
is **always a reference, never a name**, so `/spec-hotfix v33.16.4 SKI-123` and
|
|
31
|
+
`/spec-hotfix SKI-123`
|
|
32
|
+
both mean "adopt that issue". Release tags don't take that shape, so the two
|
|
33
|
+
can't be confused. With no name and no reference, ask what to call it.
|
|
34
|
+
- Take the release tag from the argument. If it's missing,
|
|
35
|
+
**ask which version prod is running** — don't guess. When an issue was adopted
|
|
36
|
+
above, **offer any versions it mentions** as suggestions — clearly labelled as
|
|
37
|
+
the reporter's words, not a default — and still wait for the answer. A reporter
|
|
38
|
+
usually names
|
|
39
|
+
the version they *saw* the bug on, which is not necessarily what is deployed,
|
|
40
|
+
and a hotfix forked from the wrong tag fails late.
|
|
28
41
|
- **Verify the tag exists** before anything else:
|
|
29
42
|
`git rev-parse --verify <tag>^{commit}`. If it doesn't resolve, stop and ask.
|
|
30
43
|
|
|
@@ -156,6 +169,8 @@ place of the table when the spec touches no external surface.>
|
|
|
156
169
|
Keep the **State log** (state transitions) separate from the **Changelog** (fix
|
|
157
170
|
narrative and decisions).
|
|
158
171
|
|
|
172
|
+
|
|
173
|
+
|
|
159
174
|
## 6. Drive to GREEN
|
|
160
175
|
|
|
161
176
|
- Implement the **minimal, root-cause** fix on the branch. Match surrounding code;
|
package/bin/skitterspec.js
CHANGED
|
@@ -1,6 +1,25 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
'use strict'
|
|
3
3
|
|
|
4
|
+
// This package's bin/, src/ and assets/ are COMPOSED by scripts/build-dist.js and
|
|
5
|
+
// gitignored, not committed — so a checkout linked before a build (or after a
|
|
6
|
+
// `git clean`) has a working binary with nothing behind it. Say that, instead of
|
|
7
|
+
// letting `require` raise MODULE_NOT_FOUND on an internal path the caller has no
|
|
8
|
+
// way to interpret.
|
|
9
|
+
//
|
|
10
|
+
// Inline rather than shared: a helper would have to live in src/, which is
|
|
11
|
+
// exactly what may be missing. In the workspace packages src/ always exists, so
|
|
12
|
+
// this is inert there.
|
|
13
|
+
const { existsSync } = require('node:fs')
|
|
14
|
+
const { join } = require('node:path')
|
|
15
|
+
if (!existsSync(join(__dirname, '..', 'src'))) {
|
|
16
|
+
console.error(
|
|
17
|
+
'skitterspec: no build output — this package\'s bin/src/assets are composed, not committed.\n' +
|
|
18
|
+
' run "npm run build" in the skitterspec repo, then try again.',
|
|
19
|
+
)
|
|
20
|
+
process.exit(1)
|
|
21
|
+
}
|
|
22
|
+
|
|
4
23
|
const { run } = require('../src/cli.js')
|
|
5
24
|
|
|
6
25
|
run(process.argv.slice(2)).catch((err) => {
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@skitterbyte/skitterspec",
|
|
3
|
-
"version": "16.
|
|
3
|
+
"version": "16.5.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
|
@@ -48,6 +48,29 @@ const { renderRoutes, portsInUse, waitListening } = require('./env/proxy.js')
|
|
|
48
48
|
|
|
49
49
|
const pkg = require('../package.json')
|
|
50
50
|
|
|
51
|
+
// Commands this (tracker-free) base does NOT ship, and the distribution that
|
|
52
|
+
// does. Without this the base says only "unknown command: spec-sync", which a
|
|
53
|
+
// user correctly reads as "no such feature" — nothing anywhere named the
|
|
54
|
+
// distribution that has it, so they were stranded. Naming Linear here is a
|
|
55
|
+
// diagnostic string, not provider machinery: `init.js` already knows
|
|
56
|
+
// `linear.config.json` and `linear-base/` by name in order to protect them.
|
|
57
|
+
const PROVIDER_COMMANDS = {
|
|
58
|
+
'spec-sync': '@skitterbyte/skitterspec-linear',
|
|
59
|
+
'spec-sanitise': '@skitterbyte/skitterspec-linear',
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
function unknownCommand(cmd) {
|
|
63
|
+
const dist = PROVIDER_COMMANDS[cmd]
|
|
64
|
+
if (dist) {
|
|
65
|
+
return (
|
|
66
|
+
`unknown command: ${cmd} — this is the base distribution, which does not ` +
|
|
67
|
+
`ship it.\n ${cmd} comes from ${dist} (a superset of this package): ` +
|
|
68
|
+
`install that instead.`
|
|
69
|
+
)
|
|
70
|
+
}
|
|
71
|
+
return `unknown command: ${cmd} (try --help)`
|
|
72
|
+
}
|
|
73
|
+
|
|
51
74
|
const HELP = `skitterspec — spec-driven-development for Claude Code
|
|
52
75
|
|
|
53
76
|
Usage:
|
|
@@ -190,7 +213,7 @@ function specEnvUp(dir, config, specArg) {
|
|
|
190
213
|
process.stdout.write('Usage: skitterspec spec-env up <spec>\n')
|
|
191
214
|
return
|
|
192
215
|
}
|
|
193
|
-
const spec =
|
|
216
|
+
const spec = resolveSpecWithWorktree(dir, config, specArg)
|
|
194
217
|
|
|
195
218
|
// Live-safe: if this spec is already live on the primary checkout (its branch was
|
|
196
219
|
// branch-switched in by `live take`), a `git worktree add` would fail — the branch
|
|
@@ -357,7 +380,7 @@ function specEnvDown(dir, config, specArg, flags) {
|
|
|
357
380
|
process.stdout.write('Usage: skitterspec spec-env down <spec> [--keep-volumes] [--force]\n')
|
|
358
381
|
return
|
|
359
382
|
}
|
|
360
|
-
const spec =
|
|
383
|
+
const spec = resolveSpecWithWorktree(dir, config, specArg)
|
|
361
384
|
|
|
362
385
|
// A worktree-only spec never held a slot but its worktree still needs removing,
|
|
363
386
|
// so "nothing to do" means neither a slot nor a worktree exists.
|
|
@@ -459,6 +482,31 @@ function liveWorktreePaths(dir) {
|
|
|
459
482
|
return paths
|
|
460
483
|
}
|
|
461
484
|
|
|
485
|
+
// Resolve a spec argument the ONE way every spec-env subcommand resolves it:
|
|
486
|
+
// against the primary checkout first, then the spec's own worktree, then every
|
|
487
|
+
// other checkout git knows about. An in-progress spec is git-mv'd into
|
|
488
|
+
// specs/in-progress/ **on its own branch**, so it exists only in its worktree —
|
|
489
|
+
// a primary-checkout-only lookup fails for exactly the specs these commands
|
|
490
|
+
// serve. The first fallback is the worktree path the config derives for this
|
|
491
|
+
// spec (cheap, no git); the rest come from `git worktree list`, so a worktree
|
|
492
|
+
// provisioned under an older `worktree.root` still resolves. Identity and
|
|
493
|
+
// coordinate tokens always expand against `dir` (the primary checkout), so the
|
|
494
|
+
// answer is identical whether the command was run from main or a worktree.
|
|
495
|
+
function resolveSpecWithWorktree(dir, config, specArg) {
|
|
496
|
+
const { slug } = splitPrefix(path.basename(specArg))
|
|
497
|
+
const { repo, repoSlug } = repoInfo(dir)
|
|
498
|
+
const wtTokens = { repo, repoSlug, slug }
|
|
499
|
+
const worktreeGuess = path.resolve(
|
|
500
|
+
dir,
|
|
501
|
+
expandTokens(config.worktree.root, wtTokens),
|
|
502
|
+
expandTokens(config.worktree.folderPattern, wtTokens),
|
|
503
|
+
)
|
|
504
|
+
const searchDirs = [...new Set([worktreeGuess, ...liveWorktreePaths(dir)])].filter(
|
|
505
|
+
(p) => p !== dir,
|
|
506
|
+
)
|
|
507
|
+
return resolveSpec(specArg, dir, config, { searchDirs })
|
|
508
|
+
}
|
|
509
|
+
|
|
462
510
|
// Every spec folder name found under specs/* across the given checkout roots.
|
|
463
511
|
// An in-progress spec lives on its *worktree branch*, not the primary checkout,
|
|
464
512
|
// so we must scan the worktrees too — otherwise a live spec's DB looks orphaned.
|
|
@@ -771,7 +819,7 @@ function specEnvResolve(dir, config, specArg) {
|
|
|
771
819
|
process.stdout.write('Usage: skitterspec spec-env resolve <spec>\n')
|
|
772
820
|
return
|
|
773
821
|
}
|
|
774
|
-
const r =
|
|
822
|
+
const r = resolveSpecWithWorktree(dir, config, specArg)
|
|
775
823
|
process.stdout.write(
|
|
776
824
|
`spec: ${r.folder} (${r.bucket})\n` +
|
|
777
825
|
`type/slug: ${r.type} / ${r.slug}\n` +
|
|
@@ -792,7 +840,7 @@ async function specEnvDev(dir, config, positional) {
|
|
|
792
840
|
process.stdout.write('Usage: skitterspec spec-env dev <up|down> <spec>\n')
|
|
793
841
|
return
|
|
794
842
|
}
|
|
795
|
-
const spec =
|
|
843
|
+
const spec = resolveSpecWithWorktree(dir, config, specArg)
|
|
796
844
|
if (!config.dev.length) {
|
|
797
845
|
process.stdout.write(
|
|
798
846
|
'spec-env dev: no dev processes configured — set "dev": [...] in env.config.json.\n',
|
|
@@ -888,7 +936,7 @@ async function specEnvConnect(dir, config, specArg) {
|
|
|
888
936
|
return
|
|
889
937
|
}
|
|
890
938
|
|
|
891
|
-
const spec =
|
|
939
|
+
const spec = resolveSpecWithWorktree(dir, config, target)
|
|
892
940
|
const registry = readRegistry(dir, config)
|
|
893
941
|
if (!Object.prototype.hasOwnProperty.call(registry.slots, spec.folder)) {
|
|
894
942
|
process.stdout.write(
|
|
@@ -984,20 +1032,6 @@ async function specEnvLive(dir, config, positional) {
|
|
|
984
1032
|
}
|
|
985
1033
|
}
|
|
986
1034
|
|
|
987
|
-
// Resolve a spec, offering its worktree as a fallback search dir — a spec authored
|
|
988
|
-
// on its own branch may not exist in the primary checkout's specs/**.
|
|
989
|
-
function resolveSpecWithWorktree(dir, config, specArg) {
|
|
990
|
-
const { slug } = splitPrefix(path.basename(specArg))
|
|
991
|
-
const { repo, repoSlug } = repoInfo(dir)
|
|
992
|
-
const wtTokens = { repo, repoSlug, slug }
|
|
993
|
-
const worktreeGuess = path.resolve(
|
|
994
|
-
dir,
|
|
995
|
-
expandTokens(config.worktree.root, wtTokens),
|
|
996
|
-
expandTokens(config.worktree.folderPattern, wtTokens),
|
|
997
|
-
)
|
|
998
|
-
return resolveSpec(specArg, dir, config, { searchDirs: [worktreeGuess] })
|
|
999
|
-
}
|
|
1000
|
-
|
|
1001
1035
|
// Take the running instance: rebase the spec's branch onto base, free it from its
|
|
1002
1036
|
// worktree, and check it out in the primary checkout so the dev server reloads it.
|
|
1003
1037
|
async function specEnvLiveTake(dir, config, specArg) {
|
|
@@ -1373,8 +1407,8 @@ async function run(argv) {
|
|
|
1373
1407
|
await cleanupReleaseTooling(dir, opts)
|
|
1374
1408
|
break
|
|
1375
1409
|
default:
|
|
1376
|
-
throw new Error(
|
|
1410
|
+
throw new Error(unknownCommand(cmd))
|
|
1377
1411
|
}
|
|
1378
1412
|
}
|
|
1379
1413
|
|
|
1380
|
-
module.exports = { run, parse }
|
|
1414
|
+
module.exports = { run, parse, HELP, unknownCommand }
|
package/src/env/resolve.js
CHANGED
|
@@ -217,9 +217,14 @@ function assertPrimaryOnMain(config, git) {
|
|
|
217
217
|
* (the primary checkout), so a worktree-only spec resolves to the right base.
|
|
218
218
|
*/
|
|
219
219
|
function resolveSpec(specArg, dir, config, opts = {}) {
|
|
220
|
-
const
|
|
220
|
+
const searchDirs = opts.searchDirs || []
|
|
221
|
+
const found = findSpecFolder(specArg, dir, searchDirs)
|
|
221
222
|
if (!found) {
|
|
222
|
-
|
|
223
|
+
// Name the roots we looked under: the usual cause is a spec that only exists
|
|
224
|
+
// on its own branch, and the message should say where we didn't find it.
|
|
225
|
+
throw new Error(
|
|
226
|
+
`spec not found under specs/**: ${specArg} (searched: ${[dir, ...searchDirs].join(', ')})`,
|
|
227
|
+
)
|
|
223
228
|
}
|
|
224
229
|
|
|
225
230
|
const { type, slug } = splitPrefix(found.folder)
|
package/src/env/teardown.js
CHANGED
|
@@ -33,7 +33,10 @@ function planDown(spec, config, flags, ctx) {
|
|
|
33
33
|
// A hotfix lands by tag + cherry-pick, so its branch is never an ancestor of
|
|
34
34
|
// base — but once its head is captured by a tag (the deploy tag from
|
|
35
35
|
// `hotfix land`), the commits are recoverable and the branch is safe to drop.
|
|
36
|
-
// Treat "reachable from a tag" as landed, alongside merged.
|
|
36
|
+
// Treat "reachable from a tag" as landed, alongside merged. Read twice below:
|
|
37
|
+
// it decides whether the unpushed guard blocks, and whether the branch delete
|
|
38
|
+
// can use `-D` — the same question ("are these commits recoverable?"), so the
|
|
39
|
+
// two must never answer it differently.
|
|
37
40
|
const landed = Boolean(worktreeState.merged || worktreeState.reachableFromTag)
|
|
38
41
|
|
|
39
42
|
// --- guards (overridable with --force) ---
|
|
@@ -88,15 +91,26 @@ function planDown(spec, config, flags, ctx) {
|
|
|
88
91
|
)
|
|
89
92
|
|
|
90
93
|
// --- delete the branch ---
|
|
91
|
-
// Runs after the worktree remove frees the branch.
|
|
92
|
-
//
|
|
93
|
-
//
|
|
94
|
-
//
|
|
95
|
-
//
|
|
96
|
-
//
|
|
94
|
+
// Runs after the worktree remove frees the branch. `-D` exactly when we have
|
|
95
|
+
// PROVEN the commits survive the delete, `-d` otherwise.
|
|
96
|
+
//
|
|
97
|
+
// `-d` looks like the safe default and mostly is, but its refusal answers a
|
|
98
|
+
// different question from ours: it also declines a branch that is ahead of its
|
|
99
|
+
// upstream ref, reporting `not yet merged to refs/remotes/origin/<branch>,
|
|
100
|
+
// even though it is merged to HEAD`. That fires on the ordinary spec flow —
|
|
101
|
+
// `/spec-go` pushes the branch when it provisions, and the phase commits after
|
|
102
|
+
// it are landed locally rather than pushed — so teardown meets a branch whose
|
|
103
|
+
// every commit is on `main` and `-d` refuses it. `merged` (HEAD is an ancestor
|
|
104
|
+
// of base) already establishes what we actually care about, and establishes it
|
|
105
|
+
// more strongly than `-d` checks.
|
|
106
|
+
//
|
|
107
|
+
// `reachableFromTag` is the same argument for a hotfix: never an ancestor of
|
|
108
|
+
// base, but its head is captured by the deploy tag.
|
|
109
|
+
//
|
|
110
|
+
// Everything else keeps `-d`, so a forced teardown of a genuinely unlanded
|
|
111
|
+
// branch fails loudly and the skill relays it rather than -D-ing.
|
|
97
112
|
if (spec.branch) {
|
|
98
|
-
|
|
99
|
-
commands.push(`git branch ${tagLanded ? '-D' : '-d'} ${spec.branch}`)
|
|
113
|
+
commands.push(`git branch ${landed ? '-D' : '-d'} ${spec.branch}`)
|
|
100
114
|
}
|
|
101
115
|
|
|
102
116
|
return { blocked: false, reason: null, commands, backupCommand, backupPath, volumesDropped }
|
package/src/init.js
CHANGED
|
@@ -558,12 +558,22 @@ function printReport(dir, mode, { diff = false } = {}) {
|
|
|
558
558
|
' at /spec-go (Docker is a per-spec escalation — set > **Stack:** in the spec).\n'
|
|
559
559
|
: 'Per-spec isolation is opt-in: re-run with --isolation (or copy' +
|
|
560
560
|
' specs/.core/env.config.json.example → env.config.json) to enable it.\n'
|
|
561
|
+
// A provider superset ships its own `spec-<provider>-setup` skill; the base
|
|
562
|
+
// ships none. Discovering it from what was actually installed keeps this file
|
|
563
|
+
// tracker-free — it never has to know which tracker (if any) is in the box.
|
|
564
|
+
const setupSkill = SKILLS.find((s) => /^spec-.+-setup$/.test(s))
|
|
565
|
+
const trackerNote = setupSkill
|
|
566
|
+
? `Tracker sync is opt-in: run /${setupSkill} to configure it` +
|
|
567
|
+
' (it discovers your workspace and writes the config), or see' +
|
|
568
|
+
' specs/.core/SETUP.md.\n'
|
|
569
|
+
: ''
|
|
561
570
|
process.stdout.write(
|
|
562
571
|
'\nDone. Skills resolve as /spec, /spec-go, /spec-complete, /spec-cancel,' +
|
|
563
572
|
' /spec-bug, /spec-review, /spec-init, /spec-connect.\n' +
|
|
564
573
|
'Next: tailor .claude/rules/spec-planning.md + the CLAUDE.md section to this' +
|
|
565
574
|
" project's stack, then run /spec.\n" +
|
|
566
|
-
isolationNote
|
|
575
|
+
isolationNote +
|
|
576
|
+
trackerNote,
|
|
567
577
|
)
|
|
568
578
|
}
|
|
569
579
|
|