@skitterbyte/skitterspec 16.10.0 → 17.0.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/MIGRATION.md +36 -0
- package/README.md +3 -3
- package/assets/claude-md-section.md +13 -51
- package/assets/core/env.config.json.example +4 -1
- package/assets/core/env.config.md +26 -6
- package/assets/rules/spec-planning.md +28 -9
- package/assets/skills/spec/SKILL.md +13 -8
- package/assets/skills/spec-bug/SKILL.md +9 -7
- package/assets/skills/spec-cancel/SKILL.md +12 -0
- package/assets/skills/spec-complete/SKILL.md +26 -5
- package/assets/skills/spec-hotfix/SKILL.md +8 -6
- package/assets/skills/spec-init/SKILL.md +16 -6
- package/assets/skills/spec-next/SKILL.md +98 -0
- package/assets/skills/spec-review/SKILL.md +5 -5
- package/assets/skills/spec-start/SKILL.md +146 -0
- package/assets/skills/spec-to-main/SKILL.md +7 -5
- package/package.json +1 -1
- package/src/cli.js +198 -14
- package/src/env/config.js +24 -1
- package/src/env/integrate.js +61 -1
- package/src/env/live.js +27 -3
- package/src/env/provision.js +58 -1
- package/src/env/teardown.js +41 -3
- package/src/init.js +21 -12
- package/src/prompts.js +31 -3
- package/assets/skills/spec-go/SKILL.md +0 -190
package/src/env/config.js
CHANGED
|
@@ -13,6 +13,9 @@
|
|
|
13
13
|
*
|
|
14
14
|
* Shape (see specs/.core/env.config.md for field docs):
|
|
15
15
|
* {
|
|
16
|
+
* mode: "worktree" | "checkout", // where a spec's branch is built —
|
|
17
|
+
* // its own worktree (default), or the primary checkout in place.
|
|
18
|
+
* // NOTE: unrelated to `seedFiles.mode`, which is symlink|copy.
|
|
16
19
|
* worktree: { root, folderPattern },
|
|
17
20
|
* docker: { enabled, composeFile, projectNamePattern, portBase,
|
|
18
21
|
* portsPerSpec, envFile, backupCommand },
|
|
@@ -44,6 +47,16 @@ const { join } = require('node:path')
|
|
|
44
47
|
const CONFIG_FILE = join('specs', '.core', 'env.config.json')
|
|
45
48
|
|
|
46
49
|
const DEFAULT_CONFIG = Object.freeze({
|
|
50
|
+
// Where a spec's branch is built. "worktree" gives every spec its own checkout
|
|
51
|
+
// — parallel specs, `main` left free — at the cost of a terminal session per
|
|
52
|
+
// spec. "checkout" builds the branch in the primary checkout instead: one spec
|
|
53
|
+
// at a time, in the terminal you are already sitting in.
|
|
54
|
+
//
|
|
55
|
+
// Defaults to "worktree" so no installed repo changes behaviour on upgrade.
|
|
56
|
+
// Never inferred from whether `dev`/`docker` are configured: a repo with no
|
|
57
|
+
// dev servers may still want parallel specs, and absence of configuration is
|
|
58
|
+
// not evidence of intent (see .claude/rules/negative-checks.md).
|
|
59
|
+
mode: 'worktree',
|
|
47
60
|
worktree: Object.freeze({ root: '../{repo}-wt', folderPattern: '{slug}' }),
|
|
48
61
|
docker: Object.freeze({
|
|
49
62
|
enabled: true,
|
|
@@ -81,7 +94,7 @@ const DEFAULT_CONFIG = Object.freeze({
|
|
|
81
94
|
baseBranch: '',
|
|
82
95
|
guards: Object.freeze({ refuseTeardownIfDirty: true, refuseTeardownIfUnpushed: true }),
|
|
83
96
|
// Teardown cleanup beyond this machine. `deleteRemoteBranch` decides what
|
|
84
|
-
// `spec-env down` does about the branch `/spec-
|
|
97
|
+
// `spec-env down` does about the branch `/spec-start` pushed: "prompt" (default)
|
|
85
98
|
// plans the delete in its own confirm-first section for the skill to ask about,
|
|
86
99
|
// "never" omits it, "always" folds it into the run-blind command list. Only ever
|
|
87
100
|
// planned for a LANDED branch — see teardown.js.
|
|
@@ -105,6 +118,7 @@ function isObject(value) {
|
|
|
105
118
|
// A fresh, deeply-mutable copy of the defaults to merge onto.
|
|
106
119
|
function defaults() {
|
|
107
120
|
return {
|
|
121
|
+
mode: DEFAULT_CONFIG.mode,
|
|
108
122
|
worktree: { ...DEFAULT_CONFIG.worktree },
|
|
109
123
|
docker: { ...DEFAULT_CONFIG.docker },
|
|
110
124
|
seedFiles: { mode: DEFAULT_CONFIG.seedFiles.mode, files: [] },
|
|
@@ -257,6 +271,15 @@ function mergeConfig(base, parsed) {
|
|
|
257
271
|
assign(base, parsed, 'registry', 'string')
|
|
258
272
|
assign(base, parsed, 'baseBranch', 'string')
|
|
259
273
|
|
|
274
|
+
// Same treatment as `teardown.deleteRemoteBranch` below, for the same reason:
|
|
275
|
+
// an unrecognised value falls through to the default rather than erroring or
|
|
276
|
+
// being taken literally. The fallback direction matters — "worktree" is the
|
|
277
|
+
// conservative one, so a typo ("Checkout", "in-place") costs an extra terminal
|
|
278
|
+
// session, never a spec's work landing somewhere the author did not choose.
|
|
279
|
+
if (parsed.mode === 'worktree' || parsed.mode === 'checkout') {
|
|
280
|
+
base.mode = parsed.mode
|
|
281
|
+
}
|
|
282
|
+
|
|
260
283
|
if (isObject(parsed.guards)) {
|
|
261
284
|
assign(base.guards, parsed.guards, 'refuseTeardownIfDirty', 'boolean')
|
|
262
285
|
assign(base.guards, parsed.guards, 'refuseTeardownIfUnpushed', 'boolean')
|
package/src/env/integrate.js
CHANGED
|
@@ -16,6 +16,11 @@
|
|
|
16
16
|
*
|
|
17
17
|
* @param {object} spec resolved spec: { branch, worktreePath, folder, ... }
|
|
18
18
|
* @param {object} config normalised env config (unused today; kept for symmetry).
|
|
19
|
+
* In CHECKOUT mode there is no worktree: the branch is already checked out in the
|
|
20
|
+
* primary checkout, so landing is a rebase in place, a switch to base, and the
|
|
21
|
+
* fast-forward. `planIntegrateCheckout` below covers that; the shape of what it
|
|
22
|
+
* returns is identical so callers need no second code path.
|
|
23
|
+
*
|
|
19
24
|
* @param {object} ctx { worktreeState: { dirty }, base, aheadOfBase, mainRepoPath }
|
|
20
25
|
* @returns {object} { blocked, noop, reason, commands, base, branch }
|
|
21
26
|
*/
|
|
@@ -43,4 +48,59 @@ function planIntegrate(spec, config, ctx) {
|
|
|
43
48
|
}
|
|
44
49
|
}
|
|
45
50
|
|
|
46
|
-
|
|
51
|
+
/**
|
|
52
|
+
* Pure integrate planner for CHECKOUT mode.
|
|
53
|
+
*
|
|
54
|
+
* The branch lives in the primary checkout, so `git -C <worktree> rebase` — the
|
|
55
|
+
* worktree-mode plan — has nothing to address. Landing is three steps in one
|
|
56
|
+
* repo: rebase onto base, switch to base, fast-forward.
|
|
57
|
+
*
|
|
58
|
+
* The switch is what makes this safe to repeat. Leaving the checkout on the spec
|
|
59
|
+
* branch after landing would mean the next `spec-env up` refuses ("standing on
|
|
60
|
+
* another spec's branch") for a spec that is finished, and the operator would
|
|
61
|
+
* have to know to switch back by hand.
|
|
62
|
+
*
|
|
63
|
+
* ctx: { dirty, base, aheadOfBase, checkoutPath, onBranch }
|
|
64
|
+
*/
|
|
65
|
+
function planIntegrateCheckout(spec, config, ctx) {
|
|
66
|
+
const { dirty, base, aheadOfBase, checkoutPath, onBranch } = ctx || {}
|
|
67
|
+
const branch = spec.branch
|
|
68
|
+
const result = { blocked: false, noop: false, reason: null, commands: [], base, branch }
|
|
69
|
+
|
|
70
|
+
// "Already landed" is answered FIRST, before any refusal. A landed spec needs
|
|
71
|
+
// no action wherever the checkout happens to be standing — and after a
|
|
72
|
+
// successful land it is standing on base, so asking "not on the branch" here
|
|
73
|
+
// would refuse the very spec this just finished landing. /spec-complete calls
|
|
74
|
+
// integrate again on exactly that state.
|
|
75
|
+
if (!aheadOfBase) {
|
|
76
|
+
return { ...result, noop: true }
|
|
77
|
+
}
|
|
78
|
+
if (dirty) {
|
|
79
|
+
return {
|
|
80
|
+
...result,
|
|
81
|
+
blocked: true,
|
|
82
|
+
reason: 'the checkout has uncommitted changes — commit the completion first',
|
|
83
|
+
}
|
|
84
|
+
}
|
|
85
|
+
// There IS something to land, so where you are standing now matters: the
|
|
86
|
+
// branch is the checkout in this mode, and landing from elsewhere would
|
|
87
|
+
// rebase and fast-forward a branch the operator is not looking at.
|
|
88
|
+
if (onBranch === false) {
|
|
89
|
+
return {
|
|
90
|
+
...result,
|
|
91
|
+
blocked: true,
|
|
92
|
+
reason: `the checkout is not on ${branch} — switch to it before landing`,
|
|
93
|
+
}
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
return {
|
|
97
|
+
...result,
|
|
98
|
+
commands: [
|
|
99
|
+
`git -C ${checkoutPath} rebase ${base}`,
|
|
100
|
+
`git -C ${checkoutPath} switch ${base}`,
|
|
101
|
+
`git -C ${checkoutPath} merge --ff-only ${branch}`,
|
|
102
|
+
],
|
|
103
|
+
}
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
module.exports = { planIntegrate, planIntegrateCheckout }
|
package/src/env/live.js
CHANGED
|
@@ -134,7 +134,11 @@ function migrationsHit(files, patterns) {
|
|
|
134
134
|
* ctx:
|
|
135
135
|
* primary { onBase, branch, baseBranch } — the guard result for the primary checkout
|
|
136
136
|
* primaryPath absolute path of the primary checkout (the checkout target)
|
|
137
|
+
* inFlight string|null — folder of the spec holding the primary, from
|
|
138
|
+
* its receipt; null when nothing is recorded (a hand-switched
|
|
139
|
+
* branch), which degrades the message to the branch name
|
|
137
140
|
* clean boolean — primary checkout working tree is clean
|
|
141
|
+
* worktreeClean boolean — the SPEC WORKTREE's tree is clean (the rebase runs there)
|
|
138
142
|
* worktreeExists boolean — the spec's worktree is on disk
|
|
139
143
|
* base resolved base branch name (rebase target)
|
|
140
144
|
* baseMainCommit primary HEAD before the switch (receipt / crash recovery)
|
|
@@ -166,9 +170,16 @@ function planTake(spec, config, ctx) {
|
|
|
166
170
|
// (or you) already holds the live instance.
|
|
167
171
|
if (!c.primary || !c.primary.onBase) {
|
|
168
172
|
const on = c.primary && c.primary.branch ? c.primary.branch : '(detached)'
|
|
173
|
+
// Name the SPEC, not just the branch, and name every way out. `/spec-start`
|
|
174
|
+
// relays this refusal verbatim as its gate, so whatever is missing here is
|
|
175
|
+
// missing from the operator's only explanation of why they are stuck —
|
|
176
|
+
// "release it with /spec-live main" alone reads as the sole option when
|
|
177
|
+
// completing or cancelling the held spec are usually the ones they want.
|
|
178
|
+
const held = c.inFlight ? `${c.inFlight} (branch ${on})` : on
|
|
169
179
|
return block(
|
|
170
|
-
`primary checkout is on ${on}, not ${base} —
|
|
171
|
-
'
|
|
180
|
+
`primary checkout is on ${on}, not ${base} — ${held} already holds it. ` +
|
|
181
|
+
'Free the workbench first: `/spec-complete` if it is finished, ' +
|
|
182
|
+
'`/spec-cancel` if it is not wanted, or `/spec-live main` to park it',
|
|
172
183
|
)
|
|
173
184
|
}
|
|
174
185
|
// 2. Never switch a dirty tree — the checkout is reset back to base on release.
|
|
@@ -177,7 +188,20 @@ function planTake(spec, config, ctx) {
|
|
|
177
188
|
}
|
|
178
189
|
// 3. Need a worktree holding the branch to detach and hand over.
|
|
179
190
|
if (!c.worktreeExists) {
|
|
180
|
-
return block(`${spec.folder} has no worktree — run \`/spec-
|
|
191
|
+
return block(`${spec.folder} has no worktree — run \`/spec-start ${spec.folder}\` first`)
|
|
192
|
+
}
|
|
193
|
+
// 3b. The WORKTREE's own tree, not the primary checkout's. Check 2 above reads
|
|
194
|
+
// the checkout we switch INTO; the rebase runs in the worktree, and git
|
|
195
|
+
// refuses to rebase over uncommitted changes. Checking only the primary
|
|
196
|
+
// let a dirty worktree through to fail at the rebase, where the failure
|
|
197
|
+
// was then reported as a merge conflict — a wrong cause for a real
|
|
198
|
+
// problem, which is worse than no message. Validate the tree the
|
|
199
|
+
// operation actually touches.
|
|
200
|
+
if (c.worktreeClean === false) {
|
|
201
|
+
return block(
|
|
202
|
+
`${spec.folder}'s worktree has uncommitted changes — commit or stash them in ` +
|
|
203
|
+
`${spec.worktreePath} first (the rebase cannot run over them)`,
|
|
204
|
+
)
|
|
181
205
|
}
|
|
182
206
|
// 4. A hotfix is built on an old release tag; checking its branch out under the
|
|
183
207
|
// running dev server risks schema/DB drift breaking the shared instance.
|
package/src/env/provision.js
CHANGED
|
@@ -158,4 +158,61 @@ function planUp(spec, alloc, config) {
|
|
|
158
158
|
}
|
|
159
159
|
}
|
|
160
160
|
|
|
161
|
-
|
|
161
|
+
/**
|
|
162
|
+
* Plan `spec-env up` in CHECKOUT mode — the branch is built in the primary
|
|
163
|
+
* checkout and there is no worktree at all.
|
|
164
|
+
*
|
|
165
|
+
* Pure: every git fact it needs arrives in `ctx`, and it writes nothing.
|
|
166
|
+
*
|
|
167
|
+
* ctx: { current, base, onBase, clean, branchExists }
|
|
168
|
+
*
|
|
169
|
+
* What it refuses, and why each is a refusal rather than a warning:
|
|
170
|
+
* - a dirty tree, because `git switch -c` CARRIES uncommitted changes onto the
|
|
171
|
+
* new branch. That is silent and it is the operator's work, so it is theirs
|
|
172
|
+
* to place, not ours.
|
|
173
|
+
* - standing on another branch, because switching away from it is a decision
|
|
174
|
+
* about someone else's unfinished spec. Being on THIS spec's branch is not a
|
|
175
|
+
* refusal — it is the re-run, and the answer is "already attached".
|
|
176
|
+
*
|
|
177
|
+
* There is no bootstrap and no opener: the primary checkout already has its
|
|
178
|
+
* dependencies, and no new session is being opened.
|
|
179
|
+
*/
|
|
180
|
+
function planCheckoutUp(spec, ctx, config) {
|
|
181
|
+
const base = ctx.base || (config && config.baseBranch) || 'main'
|
|
182
|
+
const result = {
|
|
183
|
+
mode: 'checkout',
|
|
184
|
+
blocked: false,
|
|
185
|
+
reason: null,
|
|
186
|
+
attached: false,
|
|
187
|
+
branch: spec.branch,
|
|
188
|
+
checkoutPath: ctx.checkoutPath || null,
|
|
189
|
+
commands: [],
|
|
190
|
+
}
|
|
191
|
+
const block = (reason) => ({ ...result, blocked: true, reason })
|
|
192
|
+
|
|
193
|
+
// Order matters: report "already attached" before anything else, so a re-run
|
|
194
|
+
// on the spec's own branch is never refused for a dirty tree it legitimately
|
|
195
|
+
// has — you are mid-phase, with the phase's own edits in progress.
|
|
196
|
+
if (ctx.current && ctx.current === spec.branch) {
|
|
197
|
+
return { ...result, attached: true }
|
|
198
|
+
}
|
|
199
|
+
if (!ctx.clean) {
|
|
200
|
+
return block(
|
|
201
|
+
'the primary checkout has uncommitted changes — commit or stash them first ' +
|
|
202
|
+
'(switching would carry them onto the new branch)',
|
|
203
|
+
)
|
|
204
|
+
}
|
|
205
|
+
if (!ctx.onBase) {
|
|
206
|
+
return block(
|
|
207
|
+
`the primary checkout is on ${ctx.current || '(detached)'}, not ${base} — ` +
|
|
208
|
+
'finish or park that branch first; checkout mode holds one spec at a time',
|
|
209
|
+
)
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
result.commands.push(
|
|
213
|
+
ctx.branchExists ? `git switch ${spec.branch}` : `git switch -c ${spec.branch}`,
|
|
214
|
+
)
|
|
215
|
+
return result
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
module.exports = { planUp, planCheckoutUp, seedCommandFor, worktreeCd }
|
package/src/env/teardown.js
CHANGED
|
@@ -105,7 +105,7 @@ function planDown(spec, config, flags, ctx) {
|
|
|
105
105
|
// different question from ours: it also declines a branch that is ahead of its
|
|
106
106
|
// upstream ref, reporting `not yet merged to refs/remotes/origin/<branch>,
|
|
107
107
|
// even though it is merged to HEAD`. That fires on the ordinary spec flow —
|
|
108
|
-
// `/spec-
|
|
108
|
+
// `/spec-start` pushes the branch when it provisions, and the phase commits after
|
|
109
109
|
// it are landed locally rather than pushed — so teardown meets a branch whose
|
|
110
110
|
// every commit is on `main` and `-d` refuses it. `merged` (HEAD is an ancestor
|
|
111
111
|
// of base) already establishes what we actually care about, and establishes it
|
|
@@ -122,7 +122,7 @@ function planDown(spec, config, flags, ctx) {
|
|
|
122
122
|
|
|
123
123
|
// --- delete the branch on the remote (planned, never run here) ---
|
|
124
124
|
//
|
|
125
|
-
// `/spec-
|
|
125
|
+
// `/spec-start` pushes the branch at provision time, so a completed spec otherwise
|
|
126
126
|
// leaves a merged branch on the remote forever. Cleaning that up is the goal;
|
|
127
127
|
// doing it safely is the constraint.
|
|
128
128
|
//
|
|
@@ -185,4 +185,42 @@ function blocked(reason) {
|
|
|
185
185
|
}
|
|
186
186
|
}
|
|
187
187
|
|
|
188
|
-
|
|
188
|
+
/**
|
|
189
|
+
* Pure teardown planner for CHECKOUT mode.
|
|
190
|
+
*
|
|
191
|
+
* There is no worktree to remove, no slot and no volumes — the only thing a
|
|
192
|
+
* finished spec leaves behind is its branch, and the checkout standing on it.
|
|
193
|
+
*
|
|
194
|
+
* Order is load-bearing: git refuses to delete the branch you are on, so the
|
|
195
|
+
* switch to base must come first. It is also what returns the checkout to a
|
|
196
|
+
* state the next `spec-env up` will accept.
|
|
197
|
+
*
|
|
198
|
+
* The same "are these commits recoverable?" question decides `-d` vs `-D`, and
|
|
199
|
+
* it is asked exactly as worktree-mode teardown asks it — a landed branch is
|
|
200
|
+
* safe to force-delete because its commits are on base (or under a tag); an
|
|
201
|
+
* unlanded one is not, and is refused rather than quietly dropped.
|
|
202
|
+
*
|
|
203
|
+
* ctx: { dirty, landed, onBranch, base, checkoutPath }
|
|
204
|
+
*/
|
|
205
|
+
function planDownCheckout(spec, config, flags, ctx) {
|
|
206
|
+
const { dirty, landed, onBranch, base, checkoutPath } = ctx || {}
|
|
207
|
+
const force = Boolean(flags && flags.force)
|
|
208
|
+
const result = { mode: 'checkout', blocked: false, reason: null, commands: [], branch: spec.branch }
|
|
209
|
+
const block = (reason) => ({ ...result, blocked: true, reason })
|
|
210
|
+
|
|
211
|
+
if (!force) {
|
|
212
|
+
if (config.guards.refuseTeardownIfDirty && dirty) {
|
|
213
|
+
return block('the checkout has uncommitted changes')
|
|
214
|
+
}
|
|
215
|
+
if (!landed) {
|
|
216
|
+
return block(`${spec.branch} is not merged into ${base} — landing it first is what makes the delete safe`)
|
|
217
|
+
}
|
|
218
|
+
}
|
|
219
|
+
|
|
220
|
+
const commands = []
|
|
221
|
+
if (onBranch !== false) commands.push(`git -C ${checkoutPath} switch ${base}`)
|
|
222
|
+
commands.push(`git -C ${checkoutPath} branch ${landed ? '-D' : '-d'} ${spec.branch}`)
|
|
223
|
+
return { ...result, commands }
|
|
224
|
+
}
|
|
225
|
+
|
|
226
|
+
module.exports = { planDown, planDownCheckout }
|
package/src/init.js
CHANGED
|
@@ -423,18 +423,24 @@ function installCore(dir, opts) {
|
|
|
423
423
|
}
|
|
424
424
|
|
|
425
425
|
// Activate opt-in per-spec isolation: write specs/.core/env.config.json from the
|
|
426
|
-
// example asset so /spec-
|
|
426
|
+
// example asset so /spec-start provisions a worktree for every in-progress spec.
|
|
427
427
|
// Only called when the operator opts in, and never on `update` (adopting isolation
|
|
428
428
|
// is a deliberate choice, not something a re-sync flips on). Idempotent: writeFile
|
|
429
429
|
// never clobbers an existing env.config.json without --force.
|
|
430
|
-
function installIsolation(dir, { enabled }, opts) {
|
|
430
|
+
function installIsolation(dir, { enabled, workspaceMode }, opts) {
|
|
431
431
|
if (!enabled) return
|
|
432
|
-
|
|
433
|
-
|
|
434
|
-
|
|
435
|
-
|
|
436
|
-
|
|
437
|
-
|
|
432
|
+
const target = path.join(dir, 'specs', '.core', 'env.config.json')
|
|
433
|
+
copyAsset(dir, path.join('core', 'env.config.json.example'), target, opts)
|
|
434
|
+
|
|
435
|
+
// Only 'checkout' is written; 'worktree' is already what the template says and
|
|
436
|
+
// what the loader defaults to, so the common path leaves the file untouched.
|
|
437
|
+
// Guarded by existsSync because copyAsset legitimately declines to overwrite a
|
|
438
|
+
// config the operator already customized — rewriting it here would undo that.
|
|
439
|
+
if (workspaceMode === 'checkout' && fs.existsSync(target)) {
|
|
440
|
+
const parsed = JSON.parse(fs.readFileSync(target, 'utf8'))
|
|
441
|
+
parsed.mode = 'checkout'
|
|
442
|
+
fs.writeFileSync(target, `${JSON.stringify(parsed, null, 2)}\n`)
|
|
443
|
+
}
|
|
438
444
|
trustWorktreeRoot(dir)
|
|
439
445
|
}
|
|
440
446
|
|
|
@@ -673,7 +679,7 @@ function printReport(dir, mode, { diff = false } = {}) {
|
|
|
673
679
|
const isolationOn = fs.existsSync(path.join(dir, 'specs', '.core', 'env.config.json'))
|
|
674
680
|
const isolationNote = isolationOn
|
|
675
681
|
? 'Per-spec isolation is ON: every in-progress spec gets its own git worktree' +
|
|
676
|
-
' at /spec-
|
|
682
|
+
' at /spec-start (Docker is a per-spec escalation — set > **Stack:** in the spec).\n'
|
|
677
683
|
: 'Per-spec isolation is opt-in: re-run with --isolation (or copy' +
|
|
678
684
|
' specs/.core/env.config.json.example → env.config.json) to enable it.\n'
|
|
679
685
|
// A provider superset ships its own `spec-<provider>-setup` skill; the base
|
|
@@ -696,7 +702,7 @@ function printReport(dir, mode, { diff = false } = {}) {
|
|
|
696
702
|
' (it discovers your workspace and writes the config), or see' +
|
|
697
703
|
' specs/.core/SETUP.md.\n'
|
|
698
704
|
process.stdout.write(
|
|
699
|
-
'\nDone. Skills resolve as /spec, /spec-
|
|
705
|
+
'\nDone. Skills resolve as /spec, /spec-start, /spec-next, /spec-complete,' +
|
|
700
706
|
' /spec-bug, /spec-review, /spec-init, /spec-connect.\n' +
|
|
701
707
|
'Next: tailor .claude/rules/spec-planning.md + the CLAUDE.md section to this' +
|
|
702
708
|
" project's stack, then run /spec.\n" +
|
|
@@ -705,7 +711,10 @@ function printReport(dir, mode, { diff = false } = {}) {
|
|
|
705
711
|
)
|
|
706
712
|
}
|
|
707
713
|
|
|
708
|
-
|
|
714
|
+
// `mode` here is the INSTALL mode ('init' | 'update'), long-standing and
|
|
715
|
+
// unrelated to the config's own `mode` key — which arrives as `workspaceMode`
|
|
716
|
+
// precisely so the two cannot be confused at a call site.
|
|
717
|
+
async function init({ dir, force, claudeMd, mode, isolation, workspaceMode }) {
|
|
709
718
|
if (!fs.existsSync(dir)) throw new Error(`target dir does not exist: ${dir}`)
|
|
710
719
|
resetReport()
|
|
711
720
|
|
|
@@ -716,7 +725,7 @@ async function init({ dir, force, claudeMd, mode, isolation }) {
|
|
|
716
725
|
removeRetiredFiles(dir)
|
|
717
726
|
installCore(dir, { force })
|
|
718
727
|
// Adopting isolation writes the live env.config.json — init only, never update.
|
|
719
|
-
if (mode !== 'update') installIsolation(dir, { enabled: isolation }, { force })
|
|
728
|
+
if (mode !== 'update') installIsolation(dir, { enabled: isolation, workspaceMode }, { force })
|
|
720
729
|
if (claudeMd) installClaudeMd(dir, { mode })
|
|
721
730
|
|
|
722
731
|
// Record what we wrote (and migrate a pre-manifest repo) so a later resync can
|
package/src/prompts.js
CHANGED
|
@@ -7,7 +7,8 @@
|
|
|
7
7
|
* test suite never imports the interactive UI.
|
|
8
8
|
*
|
|
9
9
|
* `isolationSeed` pre-fills the per-spec isolation question. Returns
|
|
10
|
-
* `{ isolation }
|
|
10
|
+
* `{ isolation, mode }` — `mode` is only asked when isolation is enabled, and
|
|
11
|
+
* is `'worktree'` otherwise (the value the config defaults to anyway).
|
|
11
12
|
*/
|
|
12
13
|
|
|
13
14
|
async function promptSetup({ isolationSeed = false } = {}) {
|
|
@@ -23,15 +24,42 @@ async function promptSetup({ isolationSeed = false } = {}) {
|
|
|
23
24
|
{
|
|
24
25
|
type: 'confirm',
|
|
25
26
|
name: 'isolation',
|
|
26
|
-
message: 'Enable per-spec isolation —
|
|
27
|
+
message: 'Enable per-spec isolation — build each spec on its own branch?',
|
|
27
28
|
initial: isolationSeed,
|
|
28
29
|
},
|
|
30
|
+
{
|
|
31
|
+
// Only reachable when isolation was accepted: `prev` is the previous
|
|
32
|
+
// answer, and returning null skips the question entirely.
|
|
33
|
+
type: (prev) => (prev ? 'select' : null),
|
|
34
|
+
name: 'mode',
|
|
35
|
+
message: 'Where should a spec be built?',
|
|
36
|
+
hint: '- this is the trade, not a preference',
|
|
37
|
+
initial: 0,
|
|
38
|
+
choices: [
|
|
39
|
+
{
|
|
40
|
+
title: 'Its own worktree (default)',
|
|
41
|
+
value: 'worktree',
|
|
42
|
+
description: 'several specs at once, main left free — one terminal session per spec',
|
|
43
|
+
},
|
|
44
|
+
{
|
|
45
|
+
title: 'The checkout you are in',
|
|
46
|
+
value: 'checkout',
|
|
47
|
+
description: 'one spec at a time, no second session — your terminal follows the work',
|
|
48
|
+
},
|
|
49
|
+
],
|
|
50
|
+
},
|
|
29
51
|
]
|
|
30
52
|
|
|
31
53
|
const ans = await prompts(questions, { onCancel })
|
|
32
54
|
if (cancelled) throw new Error('Setup cancelled')
|
|
33
55
|
|
|
34
|
-
|
|
56
|
+
// Anything other than an explicit 'checkout' resolves to the default, so a
|
|
57
|
+
// skipped or cancelled-into-default answer can never select the mode that
|
|
58
|
+
// puts a spec's work in the primary checkout.
|
|
59
|
+
return {
|
|
60
|
+
isolation: Boolean(ans.isolation),
|
|
61
|
+
mode: ans.mode === 'checkout' ? 'checkout' : 'worktree',
|
|
62
|
+
}
|
|
35
63
|
}
|
|
36
64
|
|
|
37
65
|
/**
|
|
@@ -1,190 +0,0 @@
|
|
|
1
|
-
---
|
|
2
|
-
name: spec-go
|
|
3
|
-
description: Promote a spec into active development and build the next phase — provisions its worktree, brings up its host dev servers (confirm first), then implements the phase with tests. Targets a spec by name (arg) or the spec currently in context. Use when the user says "/spec-go", "start this spec", "begin implementing <spec>", or "let's build the next phase".
|
|
4
|
-
---
|
|
5
|
-
|
|
6
|
-
# /spec-go — start (or continue) implementing a spec
|
|
7
|
-
|
|
8
|
-
The "up" button: it promotes the spec, provisions its worktree, brings its host
|
|
9
|
-
dev servers up on the spec's reserved ports (with your OK), then builds the phase.
|
|
10
|
-
Diverting your browser to the spec is a separate explicit step the **user**
|
|
11
|
-
types — `/spec-connect` (a slash command, not a skill you can invoke).
|
|
12
|
-
|
|
13
|
-
## 1. Identify the target spec
|
|
14
|
-
|
|
15
|
-
- If a name/path is given as an argument, use it.
|
|
16
|
-
- Otherwise use the spec **currently in context** (the one just created or
|
|
17
|
-
discussed). If neither is clear, ask which spec.
|
|
18
|
-
- Locate it by searching `specs/` (check `specs/backlog/` first, then the other
|
|
19
|
-
buckets). A spec is a `<name>/` folder whose entry point is `00-overview.md`,
|
|
20
|
-
with **one file per phase** alongside it (`01-<slug>.md`, `02-…`). Legacy specs
|
|
21
|
-
may be a bare `<name>.md`, or a `00-overview.md` with inline phases — handle
|
|
22
|
-
those too.
|
|
23
|
-
|
|
24
|
-
## 2. Move it into development
|
|
25
|
-
|
|
26
|
-
**Live check first (isolation only).** If per-spec isolation is enabled, before
|
|
27
|
-
provisioning run `skitterspec spec-env live status <name>` and read its `live:`
|
|
28
|
-
line. If it says **`live: yes`**, this spec is already checked out in the
|
|
29
|
-
**primary checkout** (you took it live with `/spec-live`) — **do not provision,
|
|
30
|
-
do not run `spec-env up`, and do not "work in the worktree"**. Its branch lives
|
|
31
|
-
in the primary checkout and its worktree is on a **detached HEAD**, so a commit
|
|
32
|
-
made in the worktree would strand on that detached HEAD and never reach the
|
|
33
|
-
branch. Instead skip the provisioning bullets and step 2b, leave the spec where
|
|
34
|
-
it is, and go straight to **step 4**, implementing the phase **in the primary
|
|
35
|
-
checkout on the branch** — edits and commits there advance the branch, and
|
|
36
|
-
`/spec-complete` lands them. (`spec-env up` refuses while live and says the same.
|
|
37
|
-
To return to an isolated worktree instead, ask the user to type `/spec-live main`
|
|
38
|
-
first, then re-run `/spec-go`.)
|
|
39
|
-
|
|
40
|
-
**If per-spec isolation is enabled** (`specs/.core/env.config.json` exists), the
|
|
41
|
-
spec **isn't already live** (the check above), and it doesn't already have a
|
|
42
|
-
worktree, provision it **first**, so all the housekeeping below lands on the
|
|
43
|
-
spec's branch and never on `main`:
|
|
44
|
-
|
|
45
|
-
**Opt-out:** if the user passes `--no-worktree` (or explicitly asks to work in
|
|
46
|
-
place), skip the provisioning bullets below and build on the current branch — the
|
|
47
|
-
same "in place otherwise" path used when isolation is off. Warn that the work
|
|
48
|
-
will land wherever you currently are (usually `main`); reserve it for a trivial
|
|
49
|
-
change or an explicit request.
|
|
50
|
-
|
|
51
|
-
- Run `skitterspec spec-env up <name>` (the `spec-env` CLI engine). It is a
|
|
52
|
-
**planner: it prints commands and creates nothing itself.** Under
|
|
53
|
-
`to provision, run:` it emits the `git worktree add` on a branch forked from
|
|
54
|
-
`main` and — only when the spec's `> **Stack:**` header is
|
|
55
|
-
`worktree + docker` — the Docker bring-up.
|
|
56
|
-
**Run those commands and confirm they succeeded** before anything below: every
|
|
57
|
-
later step assumes the worktree exists, and the header line says
|
|
58
|
-
`(plan — nothing created yet)` precisely because at that point it doesn't.
|
|
59
|
-
Print the worktree path and the opener command it emits.
|
|
60
|
-
- **Bootstrap the worktree's dependencies.** A fresh worktree has an empty
|
|
61
|
-
working tree — no installed dependencies, and none of the repo's gitignored
|
|
62
|
-
files (`.env`, local secret/config overrides) — so git hooks, typechecks,
|
|
63
|
-
builds and tests fail until they're in place. `spec-env up` prints the
|
|
64
|
-
project's configured **`then, in the worktree, run:`** commands — run them in
|
|
65
|
-
order, before doing anything else. Each one begins by `cd`-ing into the
|
|
66
|
-
worktree, so it works from any cwd and cannot quietly act on the main
|
|
67
|
-
checkout; if the worktree is missing it prints
|
|
68
|
-
**`no worktree at … — run the provisioning commands first`** and exits
|
|
69
|
-
non-zero. Seeing that means the `git worktree add` above didn't run or didn't
|
|
70
|
-
work — fix that before going on. Those commands are: first any
|
|
71
|
-
**file seeding** (from `env.config.json` → `seedFiles`), which symlinks or
|
|
72
|
-
copies the configured gitignored files from the main checkout into the fresh
|
|
73
|
-
worktree so setup can rely on them; then the **`setup`** commands (e.g. an
|
|
74
|
-
install command). With neither configured there's nothing to run; add
|
|
75
|
-
`seedFiles`/`setup` if agents keep stalling on a missing `.env` or dependencies.
|
|
76
|
-
- **Trust the worktree for this session.** The engine wrote the printed
|
|
77
|
-
`trusted:` root into `.claude/settings.local.json` (gitignored) so future
|
|
78
|
-
sessions trust it automatically — but that file likely won't hot-reload now,
|
|
79
|
-
so run `/add-dir <trusted root>` before editing into the worktree, or the
|
|
80
|
-
first edits will prompt.
|
|
81
|
-
- **Do the rest in the worktree**, on the branch: open it (the printed opener, or
|
|
82
|
-
a fresh Claude session rooted there) or, staying in this session, act on the
|
|
83
|
-
worktree path with absolute paths / `git -C <worktreePath>`. The spec move,
|
|
84
|
-
header edits **and** the phase's code all happen on the branch — so the spec's
|
|
85
|
-
evolution travels with the code it describes and lands in one PR. `main` changes
|
|
86
|
-
only when that branch merges (at `/spec-complete`).
|
|
87
|
-
|
|
88
|
-
Then move the spec (in the worktree when isolated, in place otherwise):
|
|
89
|
-
|
|
90
|
-
- If it isn't already under `specs/in-progress/`, move the whole spec folder
|
|
91
|
-
there. **Use `git mv`** to keep history:
|
|
92
|
-
`git mv "specs/backlog/<name>" "specs/in-progress/<name>"`.
|
|
93
|
-
`mkdir -p specs/in-progress` first if needed.
|
|
94
|
-
- Update the **Status** header in the entry point:
|
|
95
|
-
`> **Status:** In Progress — Phase 1 (started <YYYY-MM-DD>)`.
|
|
96
|
-
- Set the **Developer** header field if it's still `—`: use `git config user.name`.
|
|
97
|
-
- Append a **State log** row:
|
|
98
|
-
`| <YYYY-MM-DD> | In Progress | in-progress | <git user.name> |`.
|
|
99
|
-
- **When isolated:** commit the move and **push the branch** now — that records
|
|
100
|
-
the in-progress state for everyone and fires the tracker's automation (when a
|
|
101
|
-
ticketing provider is linked).
|
|
102
|
-
|
|
103
|
-
A spec ideally reaches here already `Ready` (written by `/spec`), but `/spec-go`
|
|
104
|
-
works on a `Draft` too — just sanity-check it's well-formed before building.
|
|
105
|
-
|
|
106
|
-
If the spec is already in `in-progress`, skip the move and implement the **next
|
|
107
|
-
unfinished phase** instead of Phase 1. (When isolated, subsequent `/spec-go` runs
|
|
108
|
-
happen from inside the worktree — where the spec already sits in `in-progress` on
|
|
109
|
-
the branch — and a re-run of `spec-env up` just re-attaches it.)
|
|
110
|
-
|
|
111
|
-
## 2b. Bring the spec's dev servers up — confirm before heavy steps
|
|
112
|
-
|
|
113
|
-
**Only when isolation is enabled and the project configures host dev servers**
|
|
114
|
-
(`env.config.json` → a non-empty `dev` array). This is what makes the spec
|
|
115
|
-
runnable — its UI/API on the spec's reserved port block, isolated from `main`.
|
|
116
|
-
|
|
117
|
-
- **Show the plan and get a yes first.** List what will start: the per-process
|
|
118
|
-
dev commands, the ports they'll bind (the spec's slot block), and any Docker
|
|
119
|
-
stack. Don't start heavy processes silently. If the user passed **`--plan`**,
|
|
120
|
-
print this plan and **stop** (preview only).
|
|
121
|
-
- On confirmation, run `skitterspec spec-env dev up <name>` — it launches each
|
|
122
|
-
dev process detached on its port, logs to `.spec-env/logs/`, and waits on each
|
|
123
|
-
`health` check. With no `dev` configured it's a clean no-op; skip this step.
|
|
124
|
-
- **Diverting your browser is a separate step.** To test the spec at your normal
|
|
125
|
-
`localhost` URL, the **user** types **`/spec-connect <name>`** (exclusive — it
|
|
126
|
-
exposes this spec on the canonical ports; `/spec-connect main` hands them back).
|
|
127
|
-
`/spec-go` never seizes the canonical ports on its own. For a **code-only** spec,
|
|
128
|
-
the lighter **`/spec-live <name>`** reuses the already-running dev server (a
|
|
129
|
-
branch-switch, no second stack) — `/spec-live main` hands it back. Both are
|
|
130
|
-
user-only slash commands: tell the user to run one, never try to invoke it.
|
|
131
|
-
|
|
132
|
-
## 3. Pre-flight — commit prior work
|
|
133
|
-
|
|
134
|
-
Before writing any code for this phase, get the workspace clean:
|
|
135
|
-
|
|
136
|
-
- **Confirm the last-worked phase is committed.** Run `git status` and
|
|
137
|
-
`git log --oneline -5`. The most recently *implemented* phase (not necessarily
|
|
138
|
-
the numerically previous one) should already be committed. If prior-phase work
|
|
139
|
-
is still uncommitted, **stop and suggest committing it first** (e.g. via
|
|
140
|
-
`/commit`) so each phase lands as its own reviewable commit — don't build the
|
|
141
|
-
next phase on top of an uncommitted one. (Skip if this is the first phase —
|
|
142
|
-
there's nothing prior to commit.)
|
|
143
|
-
|
|
144
|
-
## 4. Implement the phase
|
|
145
|
-
|
|
146
|
-
Identify the **first unfinished phase** from the `00-overview.md` phase index,
|
|
147
|
-
then open its phase file (`0N-<slug>.md`) — that file holds the tasks. Mark it
|
|
148
|
-
started: set the phase-file heading to `🔄` and its `> **Status:**` to
|
|
149
|
-
`In progress`, and flip the matching row in the overview phase index to `🔄`.
|
|
150
|
-
|
|
151
|
-
**Then sync with the tracker (only if a provider is installed).** The phase has
|
|
152
|
-
just changed state, so refresh the mirror before the build starts — that is what
|
|
153
|
-
makes the phase show as in progress *while* it is being built rather than only
|
|
154
|
-
once it is over. Without a provider this is a no-op and nothing below changes.
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
Then build it, following the project rules in `.claude/rules/*.md` and `CLAUDE.md`:
|
|
159
|
-
|
|
160
|
-
- Work task by task through the phase file. Make focused edits that match
|
|
161
|
-
surrounding code.
|
|
162
|
-
- Honour the project's conventions (see `.claude/rules/spec-planning.md` and the
|
|
163
|
-
rules it links).
|
|
164
|
-
- **Tests are part of the phase, not after it.** Create/extend tests for the
|
|
165
|
-
work, then run the project's typecheck and test commands. Do not declare the
|
|
166
|
-
phase done until green.
|
|
167
|
-
- Never hardcode dates in tests; never run destructive commands against a real
|
|
168
|
-
database — use the project's test database only.
|
|
169
|
-
|
|
170
|
-
## 5. Record progress
|
|
171
|
-
|
|
172
|
-
- In the **phase file**: tick completed tasks (`- [x]`), flip its heading to `✅`,
|
|
173
|
-
and set its `> **Status:**` to `Done`.
|
|
174
|
-
- In **`00-overview.md`**: flip the matching phase-index row to `✅`.
|
|
175
|
-
- If anything changed from the plan (a decision, a deviation, a discovered
|
|
176
|
-
constraint), add a dated **Changelog** entry in `00-overview.md`.
|
|
177
|
-
- If new work surfaced, add it as tasks to the appropriate phase file (or add a
|
|
178
|
-
new phase file + index row) rather than doing it silently.
|
|
179
|
-
|
|
180
|
-
**Then refresh the mirror (only if a provider is installed).** The phase is done
|
|
181
|
-
in the repo now; leaving the tracker to catch up at `/spec-complete` is what makes
|
|
182
|
-
a mirror lag a whole spec behind. Without a provider this is a no-op.
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
## 6. Report
|
|
187
|
-
|
|
188
|
-
Summarise what was implemented, the test result (quote failures if any), and
|
|
189
|
-
which phase is next. Do **not** `git commit` unless the user asks — finish,
|
|
190
|
-
verify, and wait.
|