plumbbob 0.9.5 → 0.10.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.claude-plugin/plugin.json +1 -1
- package/README.md +89 -87
- package/dist/cli-core.js +18 -8
- package/dist/cli.js +1 -1
- package/dist/lib/agents.js +26 -26
- package/dist/lib/buildlog.js +21 -11
- package/dist/lib/buildlogsync.js +8 -8
- package/dist/lib/check.js +14 -14
- package/dist/lib/commitbody.js +72 -0
- package/dist/lib/commitmsg.js +7 -7
- package/dist/lib/git.js +44 -15
- package/dist/lib/intent.js +20 -19
- package/dist/lib/latch.js +16 -16
- package/dist/lib/orient.js +36 -17
- package/dist/lib/reentry.js +3 -3
- package/dist/lib/settings.js +16 -16
- package/dist/lib/sidecar.js +70 -54
- package/dist/lib/templates.js +1 -1
- package/dist/verbs/abandon.js +122 -0
- package/dist/verbs/agent.js +25 -25
- package/dist/verbs/build.js +9 -9
- package/dist/verbs/check.js +6 -6
- package/dist/verbs/checkpoint.js +48 -47
- package/dist/verbs/doctor.js +35 -29
- package/dist/verbs/finish.js +27 -40
- package/dist/verbs/handoff.js +9 -8
- package/dist/verbs/init.js +9 -9
- package/dist/verbs/park.js +3 -3
- package/dist/verbs/recover.js +12 -12
- package/dist/verbs/revert.js +12 -12
- package/dist/verbs/spike.js +21 -21
- package/dist/verbs/start.js +49 -19
- package/dist/verbs/status.js +8 -8
- package/dist/verbs/turn.js +16 -16
- package/dist/verbs/use.js +4 -4
- package/node_modules/checkride/.claude-plugin/plugin.json +1 -1
- package/node_modules/checkride/CHANGELOG.md +401 -0
- package/node_modules/checkride/README.md +27 -9
- package/node_modules/checkride/dist/adapters.d.ts +12 -4
- package/node_modules/checkride/dist/adapters.d.ts.map +1 -1
- package/node_modules/checkride/dist/adapters.js +21 -1
- package/node_modules/checkride/dist/adapters.js.map +1 -1
- package/node_modules/checkride/dist/agent-setup/cursor.d.ts +9 -1
- package/node_modules/checkride/dist/agent-setup/cursor.d.ts.map +1 -1
- package/node_modules/checkride/dist/agent-setup/cursor.js +59 -1
- package/node_modules/checkride/dist/agent-setup/cursor.js.map +1 -1
- package/node_modules/checkride/dist/agent-setup/harness.d.ts +1 -1
- package/node_modules/checkride/dist/agent-setup/harness.d.ts.map +1 -1
- package/node_modules/checkride/dist/agent-setup/harness.js +10 -1
- package/node_modules/checkride/dist/agent-setup/harness.js.map +1 -1
- package/node_modules/checkride/dist/agent-setup/scripts.d.ts +23 -0
- package/node_modules/checkride/dist/agent-setup/scripts.d.ts.map +1 -1
- package/node_modules/checkride/dist/agent-setup/scripts.js +387 -82
- package/node_modules/checkride/dist/agent-setup/scripts.js.map +1 -1
- package/node_modules/checkride/dist/baseline/fallow.js +2 -2
- package/node_modules/checkride/dist/baseline/fingerprint.d.ts +14 -5
- package/node_modules/checkride/dist/baseline/fingerprint.d.ts.map +1 -1
- package/node_modules/checkride/dist/baseline/fingerprint.js +58 -5
- package/node_modules/checkride/dist/baseline/fingerprint.js.map +1 -1
- package/node_modules/checkride/dist/baseline/history.d.ts +98 -0
- package/node_modules/checkride/dist/baseline/history.d.ts.map +1 -0
- package/node_modules/checkride/dist/baseline/history.js +232 -0
- package/node_modules/checkride/dist/baseline/history.js.map +1 -0
- package/node_modules/checkride/dist/baseline/index.d.ts +4 -2
- package/node_modules/checkride/dist/baseline/index.d.ts.map +1 -1
- package/node_modules/checkride/dist/baseline/index.js +2 -1
- package/node_modules/checkride/dist/baseline/index.js.map +1 -1
- package/node_modules/checkride/dist/baseline/store.d.ts +30 -7
- package/node_modules/checkride/dist/baseline/store.d.ts.map +1 -1
- package/node_modules/checkride/dist/baseline/store.js +50 -16
- package/node_modules/checkride/dist/baseline/store.js.map +1 -1
- package/node_modules/checkride/dist/cli.d.ts +5 -1
- package/node_modules/checkride/dist/cli.d.ts.map +1 -1
- package/node_modules/checkride/dist/cli.js +98 -25
- package/node_modules/checkride/dist/cli.js.map +1 -1
- package/node_modules/checkride/dist/config.d.ts +33 -4
- package/node_modules/checkride/dist/config.d.ts.map +1 -1
- package/node_modules/checkride/dist/config.js +21 -6
- package/node_modules/checkride/dist/config.js.map +1 -1
- package/node_modules/checkride/dist/digest/digest.js +1 -1
- package/node_modules/checkride/dist/doctor.d.ts +2 -0
- package/node_modules/checkride/dist/doctor.d.ts.map +1 -1
- package/node_modules/checkride/dist/doctor.js +39 -1
- package/node_modules/checkride/dist/doctor.js.map +1 -1
- package/node_modules/checkride/dist/gate.d.ts +49 -4
- package/node_modules/checkride/dist/gate.d.ts.map +1 -1
- package/node_modules/checkride/dist/gate.js +143 -16
- package/node_modules/checkride/dist/gate.js.map +1 -1
- package/node_modules/checkride/dist/init.d.ts +8 -1
- package/node_modules/checkride/dist/init.d.ts.map +1 -1
- package/node_modules/checkride/dist/init.js +68 -7
- package/node_modules/checkride/dist/init.js.map +1 -1
- package/node_modules/checkride/dist/node-pin.d.ts +1 -1
- package/node_modules/checkride/dist/node-pin.js +1 -1
- package/node_modules/checkride/dist/orchestrator.d.ts +13 -0
- package/node_modules/checkride/dist/orchestrator.d.ts.map +1 -1
- package/node_modules/checkride/dist/orchestrator.js +118 -13
- package/node_modules/checkride/dist/orchestrator.js.map +1 -1
- package/node_modules/checkride/dist/pm/index.d.ts +1 -0
- package/node_modules/checkride/dist/pm/index.d.ts.map +1 -1
- package/node_modules/checkride/dist/pm/index.js.map +1 -1
- package/node_modules/checkride/dist/pm/launch.d.ts +23 -2
- package/node_modules/checkride/dist/pm/launch.d.ts.map +1 -1
- package/node_modules/checkride/dist/pm/launch.js +16 -0
- package/node_modules/checkride/dist/pm/launch.js.map +1 -1
- package/node_modules/checkride/dist/pm/translate.js +1 -1
- package/node_modules/checkride/dist/pm/translate.js.map +1 -1
- package/node_modules/checkride/dist/qa/mutation.d.ts +4 -4
- package/node_modules/checkride/dist/qa/mutation.js +4 -4
- package/node_modules/checkride/dist/recover.d.ts +48 -0
- package/node_modules/checkride/dist/recover.d.ts.map +1 -0
- package/node_modules/checkride/dist/recover.js +220 -0
- package/node_modules/checkride/dist/recover.js.map +1 -0
- package/node_modules/checkride/dist/smoke.d.ts +1 -1
- package/node_modules/checkride/dist/smoke.js +1 -1
- package/node_modules/checkride/dist/snippets.d.ts +1 -1
- package/node_modules/checkride/package.json +3 -2
- package/node_modules/checkride/schema/checkride.config.schema.json +9 -1
- package/node_modules/checkride/skills/check/SKILL.md +9 -1
- package/node_modules/checkride/templates/shared/styles/Repo/Drift.yml +41 -0
- package/node_modules/checkride/templates/shared/styles/Repo/Latin.yml +14 -0
- package/node_modules/checkride/templates/shared/styles/Repo/LyHyphen.yml +28 -0
- package/node_modules/checkride/templates/shared/styles/Repo/Minted.yml +33 -0
- package/node_modules/checkride/templates/shared/styles/Repo/ThereIs.yml +16 -0
- package/node_modules/checkride/templates/shared/styles/Repo/Weasel.yml +26 -0
- package/node_modules/checkride/templates/shared/vale.ini +34 -0
- package/package.json +13 -11
- package/skills/abandon/SKILL.md +20 -0
- package/skills/build/SKILL.md +69 -64
- package/skills/doctor/SKILL.md +11 -11
- package/skills/finish/SKILL.md +20 -19
- package/skills/harvest/SKILL.md +13 -12
- package/skills/park/SKILL.md +10 -10
- package/skills/plan/SKILL.md +63 -54
- package/skills/recover/SKILL.md +7 -7
- package/skills/refine/SKILL.md +18 -16
- package/skills/revert/SKILL.md +6 -6
- package/skills/spike/SKILL.md +9 -9
- package/skills/status/SKILL.md +5 -5
- package/skills/step/SKILL.md +17 -12
- package/skills/verify/SKILL.md +45 -40
- package/templates/build-log.md +7 -7
- package/templates/intent.md +49 -39
- package/templates/spike-report.md +10 -10
package/dist/lib/sidecar.js
CHANGED
|
@@ -1,22 +1,22 @@
|
|
|
1
|
-
// The .plumbbob/ sidecar
|
|
1
|
+
// The .plumbbob/ sidecar: plumbbob's control directory beside the code. Control
|
|
2
2
|
// state lives in flat files (bare numbers, plain path lists, one-line markers) so
|
|
3
3
|
// shell hooks can read it with a grep and no markdown parsing. Functional and
|
|
4
4
|
// procedural, node builtins only.
|
|
5
5
|
//
|
|
6
6
|
// Two planes share the directory: the tracked ARTIFACT plane (settings.json and
|
|
7
|
-
// each build's `builds/<slug>/` folder
|
|
7
|
+
// each build's `builds/<slug>/` folder: intent, build-log, checkpoints, report),
|
|
8
8
|
// which rides its branch into the PR, and the git-excluded CONTROL plane (STATE,
|
|
9
9
|
// the personal settings overlay, the turn ledger, and each build's in-flight
|
|
10
10
|
// markers), which stays per-worktree and never commits.
|
|
11
11
|
//
|
|
12
12
|
// STATE is the session sentinel AND the active-build cursor: its EXISTENCE means
|
|
13
|
-
// "a session is active", and its CONTENT names the build that session is on
|
|
13
|
+
// "a session is active", and its CONTENT names the build that session is on: the
|
|
14
14
|
// two reinforce each other (a session is always on some build; the cursor is
|
|
15
15
|
// meaningless without a session). Content is empty under --local / no build.
|
|
16
16
|
// hasSession stays existence-only; activeBuild reads the content. Homing the
|
|
17
|
-
// cursor here
|
|
17
|
+
// cursor here (not in settings.local.json) keeps that overlay purely
|
|
18
18
|
// human-owned (check/auto): the tool only ever reads it, never rewrites it. The
|
|
19
|
-
// phase the dashboard shows (DESIGN/BUILD/SPIKE) is derived, not stored
|
|
19
|
+
// phase the dashboard shows (DESIGN/BUILD/SPIKE) is derived, not stored: BUILD ⇔
|
|
20
20
|
// a STEP is in flight, SPIKE ⇔ the SPIKE marker is present, otherwise DESIGN.
|
|
21
21
|
import { existsSync, readdirSync, readFileSync, writeFileSync, appendFileSync, rmSync, mkdirSync } from 'node:fs';
|
|
22
22
|
import { dirname, join } from 'node:path';
|
|
@@ -48,7 +48,7 @@ export function buildDir(root, slug) {
|
|
|
48
48
|
* Derive a filesystem-safe slug from a build title.
|
|
49
49
|
*
|
|
50
50
|
* Lowercased, every run of non-alphanumerics collapsed to a single hyphen,
|
|
51
|
-
* trimmed of leading/trailing hyphens. The CLI stays dumb and explicit
|
|
51
|
+
* trimmed of leading/trailing hyphens. The CLI stays dumb and explicit:
|
|
52
52
|
* collision handling belongs to the caller (`start` refuses an existing slug
|
|
53
53
|
* rather than silently suffixing `-2`, and prepends the `YYYY-MM-DD-` date that
|
|
54
54
|
* keeps `listBuilds`' lexical sort chronological). A title with no alphanumerics
|
|
@@ -65,7 +65,7 @@ export function slugify(title) {
|
|
|
65
65
|
* date prefix `start` prepends stripped off.
|
|
66
66
|
*
|
|
67
67
|
* `2026-07-18-escape-hatch` yields the human-meaningful `escape-hatch`. Null
|
|
68
|
-
* when no build resolves (`--local`, or a slug that is nothing but a date)
|
|
68
|
+
* when no build resolves (`--local`, or a slug that is nothing but a date); the
|
|
69
69
|
* caller then omits the `(scope)` segment, which Conventional Commits permit.
|
|
70
70
|
*/
|
|
71
71
|
export function buildScope(slug) {
|
|
@@ -76,7 +76,7 @@ export function buildScope(slug) {
|
|
|
76
76
|
return stripped.length > 0 ? stripped : null;
|
|
77
77
|
}
|
|
78
78
|
/**
|
|
79
|
-
* Existing build slugs
|
|
79
|
+
* Existing build slugs: the directory names under `builds/`, sorted.
|
|
80
80
|
*
|
|
81
81
|
* Empty when `builds/` is absent (a `--local` repo, or before the first tracked
|
|
82
82
|
* `start`).
|
|
@@ -110,7 +110,7 @@ export function activeBuild(root, flag) {
|
|
|
110
110
|
return builds.length === 1 ? (builds[0] ?? null) : null;
|
|
111
111
|
}
|
|
112
112
|
/**
|
|
113
|
-
* STATE's content
|
|
113
|
+
* STATE's content (the active-build cursor) or null when it names no build.
|
|
114
114
|
*
|
|
115
115
|
* Empty means "no build" (--local, or a session whose cursor is unset), so
|
|
116
116
|
* activeBuild falls through to the sole-build rule. The literal value `active`
|
|
@@ -135,7 +135,7 @@ function readCursor(root) {
|
|
|
135
135
|
*
|
|
136
136
|
* Every verb resolves through this: an explicit `--build <slug>` wins, else the
|
|
137
137
|
* cursor / sole-build fallback of `activeBuild`. `rest` matters because the slug
|
|
138
|
-
* is a bare token
|
|
138
|
+
* is a bare token: scanning positionals on the raw argv would let it masquerade
|
|
139
139
|
* as a step number or a spike slug, so callers scan `rest` instead.
|
|
140
140
|
*/
|
|
141
141
|
export function resolveBuild(root, args) {
|
|
@@ -145,7 +145,7 @@ export function resolveBuild(root, args) {
|
|
|
145
145
|
return { build: activeBuild(root, args[i + 1]), rest: [...args.slice(0, i), ...args.slice(i + 2)] };
|
|
146
146
|
}
|
|
147
147
|
/**
|
|
148
|
-
* The STATE file
|
|
148
|
+
* The STATE file: session sentinel and active-build cursor in one.
|
|
149
149
|
*/
|
|
150
150
|
function statePath(root) {
|
|
151
151
|
return join(root, DIRNAME, 'STATE');
|
|
@@ -158,7 +158,7 @@ function statePath(root) {
|
|
|
158
158
|
* omit it and the dir resolves from the cursor (the default the
|
|
159
159
|
* executor-agnostic path reads lean on). Either way a `null` slug falls back to
|
|
160
160
|
* the flat sidecar root, which covers the `--local` layout and any
|
|
161
|
-
* no-cursor/no-build repo
|
|
161
|
+
* no-cursor/no-build repo: the path reads stay stable even before a tracked
|
|
162
162
|
* build exists or when a "no active session" guard is about to fire.
|
|
163
163
|
*/
|
|
164
164
|
function artifactDir(root, slug) {
|
|
@@ -166,7 +166,7 @@ function artifactDir(root, slug) {
|
|
|
166
166
|
return resolved === null ? sidecarDir(root) : buildDir(root, resolved);
|
|
167
167
|
}
|
|
168
168
|
/**
|
|
169
|
-
* The resolved build's artifact folder
|
|
169
|
+
* The resolved build's artifact folder: the `builds/<slug>/` dir for the active
|
|
170
170
|
* build (or the flat sidecar root under `--local`/no-cursor).
|
|
171
171
|
*
|
|
172
172
|
* Public so the plan-approval commit can stage exactly this build's scaffold and
|
|
@@ -176,7 +176,7 @@ export function buildFolder(root, slug) {
|
|
|
176
176
|
return artifactDir(root, slug);
|
|
177
177
|
}
|
|
178
178
|
/**
|
|
179
|
-
* The SPIKE marker
|
|
179
|
+
* The SPIKE marker: a single-purpose presence flag, like SEAM/STEP.
|
|
180
180
|
*
|
|
181
181
|
* Written by `spike` on open, removed on `spike done`. Its existence is the one
|
|
182
182
|
* signal the dashboard and the spike gates read to know "a spike is active".
|
|
@@ -185,39 +185,55 @@ export function spikePath(root, slug) {
|
|
|
185
185
|
return join(artifactDir(root, slug), 'SPIKE');
|
|
186
186
|
}
|
|
187
187
|
/**
|
|
188
|
-
* SEAM
|
|
188
|
+
* SEAM: the in-flight step's edit grant: a plain list of the paths the step may
|
|
189
189
|
* touch, one per line, so the hooks read it with a grep and no markdown parsing.
|
|
190
190
|
*/
|
|
191
191
|
export function seamPath(root, slug) {
|
|
192
192
|
return join(artifactDir(root, slug), 'SEAM');
|
|
193
193
|
}
|
|
194
194
|
/**
|
|
195
|
-
*
|
|
195
|
+
* Remove the SEAM marker: `abandon` drops the in-flight step's edit grant, the
|
|
196
|
+
* same clear `checkpoint` and `revert` do inline. Homed here so a verb not
|
|
197
|
+
* sanctioned to delete on its own can still return to the boundary; absent is a
|
|
198
|
+
* no-op.
|
|
199
|
+
*/
|
|
200
|
+
export function clearSeam(root, slug) {
|
|
201
|
+
rmSync(seamPath(root, slug), { force: true });
|
|
202
|
+
}
|
|
203
|
+
/**
|
|
204
|
+
* STEP: the number of the step in flight, a bare number in a flat file.
|
|
196
205
|
*/
|
|
197
206
|
export function stepPath(root, slug) {
|
|
198
207
|
return join(artifactDir(root, slug), 'STEP');
|
|
199
208
|
}
|
|
200
209
|
/**
|
|
201
|
-
*
|
|
210
|
+
* Remove the STEP marker alongside SEAM, dropping the build back to the DESIGN
|
|
211
|
+
* boundary; absent is a no-op.
|
|
212
|
+
*/
|
|
213
|
+
export function clearStep(root, slug) {
|
|
214
|
+
rmSync(stepPath(root, slug), { force: true });
|
|
215
|
+
}
|
|
216
|
+
/**
|
|
217
|
+
* The build's `checkpoints` ledger: the recorded SHAs of landed plan and step
|
|
202
218
|
* commits, one line each.
|
|
203
219
|
*/
|
|
204
220
|
export function checkpointsPath(root, slug) {
|
|
205
221
|
return join(artifactDir(root, slug), 'checkpoints');
|
|
206
222
|
}
|
|
207
223
|
/**
|
|
208
|
-
* intent.md
|
|
224
|
+
* intent.md: the build's plan: steps, seams, decisions, done-when.
|
|
209
225
|
*/
|
|
210
226
|
export function intentPath(root, slug) {
|
|
211
227
|
return join(artifactDir(root, slug), 'intent.md');
|
|
212
228
|
}
|
|
213
229
|
/**
|
|
214
|
-
* build-log.md
|
|
230
|
+
* build-log.md: the human-facing build ledger (the Steps mirror and the Log).
|
|
215
231
|
*/
|
|
216
232
|
export function buildLogPath(root, slug) {
|
|
217
233
|
return join(artifactDir(root, slug), 'build-log.md');
|
|
218
234
|
}
|
|
219
235
|
/**
|
|
220
|
-
* report.md
|
|
236
|
+
* report.md: the close-out report, beside intent.md / build-log.md.
|
|
221
237
|
*
|
|
222
238
|
* The finish skill writes it and `finish` commits it with the folder; the
|
|
223
239
|
* tracked build folder is the archive, so the report rides the branch into the
|
|
@@ -227,7 +243,7 @@ export function reportPath(root, slug) {
|
|
|
227
243
|
return join(artifactDir(root, slug), 'report.md');
|
|
228
244
|
}
|
|
229
245
|
// --- Spike reports: `spike-NN-<slug>.md` files in the build folder, beside
|
|
230
|
-
// report.md. One per spike
|
|
246
|
+
// report.md. One per spike (an explicit `/plumbbob:spike` or a planned `spike:` step)
|
|
231
247
|
// so a build with several forks tells its decision history through the folder
|
|
232
248
|
// listing. NN is CLI-allocated (the human never numbers them), zero-padded for a
|
|
233
249
|
// stable sort. ---
|
|
@@ -251,7 +267,7 @@ export function listSpikeReports(root, slug) {
|
|
|
251
267
|
* sanitized spike slug.
|
|
252
268
|
*
|
|
253
269
|
* One past the highest existing NN, so a gap never reallocates a number. Pure
|
|
254
|
-
* allocation
|
|
270
|
+
* allocation: the caller writes the stamped template there.
|
|
255
271
|
*/
|
|
256
272
|
export function nextSpikeReportPath(root, slug, spikeSlug) {
|
|
257
273
|
const used = listSpikeReports(root, slug).map((name) => Number(name.slice('spike-'.length, 'spike-'.length + 2)));
|
|
@@ -260,10 +276,10 @@ export function nextSpikeReportPath(root, slug, spikeSlug) {
|
|
|
260
276
|
return join(artifactDir(root, slug), `spike-${nn}-${spikeSlug}.md`);
|
|
261
277
|
}
|
|
262
278
|
/**
|
|
263
|
-
* handoff.json
|
|
279
|
+
* handoff.json: the agent-run ledger.
|
|
264
280
|
*
|
|
265
281
|
* Each `agent run` appends its validated envelope here so a later invocation can
|
|
266
|
-
* thread earlier results into the next call's `context[]
|
|
282
|
+
* thread earlier results into the next call's `context[]`: the CLI itself is
|
|
267
283
|
* memoryless between runs, and the file survives context-window compaction where
|
|
268
284
|
* inline stdout does not. It is in-flight control state like STEP/SEAM:
|
|
269
285
|
* untracked (excludeControl), scoped to the current step, cleared when the step
|
|
@@ -276,7 +292,7 @@ export function handoffPath(root, slug) {
|
|
|
276
292
|
* Append one entry to the build's handoff.json.
|
|
277
293
|
*
|
|
278
294
|
* Creates the file (as a JSON array) when absent and tolerates a malformed
|
|
279
|
-
* existing file by starting fresh
|
|
295
|
+
* existing file by starting fresh: a corrupt ledger must never wedge a run.
|
|
280
296
|
* Pretty-printed so it stays readable.
|
|
281
297
|
*/
|
|
282
298
|
export function appendHandoff(root, slug, entry) {
|
|
@@ -295,7 +311,7 @@ export function appendHandoff(root, slug, entry) {
|
|
|
295
311
|
writeFileSync(path, `${JSON.stringify(entries, null, 2)}\n`);
|
|
296
312
|
}
|
|
297
313
|
/**
|
|
298
|
-
* Remove the build's handoff.json
|
|
314
|
+
* Remove the build's handoff.json: the step-scoped ledger clears when the step
|
|
299
315
|
* checkpoints, the same beat that clears STEP/SEAM.
|
|
300
316
|
*
|
|
301
317
|
* An absent file is a no-op (`force`).
|
|
@@ -304,13 +320,13 @@ export function clearHandoff(root, slug) {
|
|
|
304
320
|
rmSync(handoffPath(root, slug), { force: true });
|
|
305
321
|
}
|
|
306
322
|
/**
|
|
307
|
-
* stats.json
|
|
323
|
+
* stats.json: the per-build stats receipt, beside checkpoints.
|
|
308
324
|
*/
|
|
309
325
|
export function statsPath(root, slug) {
|
|
310
326
|
return join(artifactDir(root, slug), 'stats.json');
|
|
311
327
|
}
|
|
312
328
|
/**
|
|
313
|
-
* The whole stats file, or {} when absent or corrupt
|
|
329
|
+
* The whole stats file, or {} when absent or corrupt: malformed contributes
|
|
314
330
|
* nothing.
|
|
315
331
|
*/
|
|
316
332
|
export function readStats(root, slug) {
|
|
@@ -325,14 +341,14 @@ export function readStats(root, slug) {
|
|
|
325
341
|
/**
|
|
326
342
|
* Increment one counter on one step.
|
|
327
343
|
*
|
|
328
|
-
* Best-effort by contract: a failed write is swallowed
|
|
344
|
+
* Best-effort by contract: a failed write is swallowed; the stats are a
|
|
329
345
|
* receipt, never a gate on the verb that accrues them.
|
|
330
346
|
*/
|
|
331
347
|
export function bumpStepStat(root, slug, step, key) {
|
|
332
348
|
patchStepStat(root, slug, step, (current) => ({ [key]: (current[key] ?? 0) + 1 }));
|
|
333
349
|
}
|
|
334
350
|
/**
|
|
335
|
-
* Stamp one timestamp on one step
|
|
351
|
+
* Stamp one timestamp on one step: startedAt at `build <n>`, landedAt at
|
|
336
352
|
* checkpoint.
|
|
337
353
|
*
|
|
338
354
|
* Same best-effort contract as bumpStepStat.
|
|
@@ -362,7 +378,7 @@ function patchStepStat(root, slug, step, patch) {
|
|
|
362
378
|
/**
|
|
363
379
|
* A session exists iff STATE exists.
|
|
364
380
|
*
|
|
365
|
-
* Deleting STATE (at finish) is what flips the repo back to "no session"
|
|
381
|
+
* Deleting STATE (at finish) is what flips the repo back to "no session": the
|
|
366
382
|
* file is the single source of truth for "is there a session". `start` calls
|
|
367
383
|
* beginSession; `finish` removes the file. Existence is the whole session
|
|
368
384
|
* signal; STATE's content is a separate axis (the cursor).
|
|
@@ -381,7 +397,7 @@ export function beginSession(root, slug = null) {
|
|
|
381
397
|
writeCursor(root, slug);
|
|
382
398
|
}
|
|
383
399
|
/**
|
|
384
|
-
* Re-point the cursor at an existing build (`use`)
|
|
400
|
+
* Re-point the cursor at an existing build (`use`): a plain content rewrite
|
|
385
401
|
* that leaves the session sentinel (STATE's existence) intact.
|
|
386
402
|
*
|
|
387
403
|
* Callers guard hasSession first, so this never resurrects a finished session.
|
|
@@ -396,13 +412,13 @@ function writeCursor(root, slug) {
|
|
|
396
412
|
writeFileSync(statePath(root), slug === null ? '' : `${slug}\n`);
|
|
397
413
|
}
|
|
398
414
|
/**
|
|
399
|
-
* True while a spike is open
|
|
415
|
+
* True while a spike is open: the SPIKE marker exists (content is irrelevant).
|
|
400
416
|
*/
|
|
401
417
|
export function inSpike(root, slug) {
|
|
402
418
|
return existsSync(spikePath(root, slug));
|
|
403
419
|
}
|
|
404
420
|
/**
|
|
405
|
-
* Drop the SPIKE marker
|
|
421
|
+
* Drop the SPIKE marker: existence is the whole signal.
|
|
406
422
|
*/
|
|
407
423
|
export function markSpike(root, slug) {
|
|
408
424
|
writeFileSync(spikePath(root, slug), 'active\n');
|
|
@@ -415,20 +431,20 @@ export function clearSpike(root, slug) {
|
|
|
415
431
|
}
|
|
416
432
|
// --- The turn ledger: the checkpoint latch's inputs. `.plumbbob/TURN` is a count
|
|
417
433
|
// of human prompts the model never writes, with a one-turn `.plumbbob/GRANT`
|
|
418
|
-
// beside it
|
|
434
|
+
// beside it: both flat, per-worktree control (never per-build, never committed).
|
|
419
435
|
// `plumbbob turn` (the UserPromptSubmit hook) ticks TURN once per human prompt
|
|
420
436
|
// and rewrites GRANT from the literal prompt; the checkpoint latch reads them to
|
|
421
437
|
// refuse a land until a human turn has intervened since the step was entered.
|
|
422
438
|
// Kept in the sidecar so the lone GRANT `rmSync` stays where deletions of control
|
|
423
439
|
// state are allowed. ---
|
|
424
440
|
/**
|
|
425
|
-
* TURN
|
|
441
|
+
* TURN: the human-turn count the UserPromptSubmit hook maintains.
|
|
426
442
|
*/
|
|
427
443
|
export function turnPath(root) {
|
|
428
444
|
return join(root, DIRNAME, 'TURN');
|
|
429
445
|
}
|
|
430
446
|
/**
|
|
431
|
-
* GRANT
|
|
447
|
+
* GRANT: the one-turn self-approval minted from the human's literal prompt.
|
|
432
448
|
*/
|
|
433
449
|
export function grantPath(root) {
|
|
434
450
|
return join(root, DIRNAME, 'GRANT');
|
|
@@ -437,8 +453,8 @@ export function grantPath(root) {
|
|
|
437
453
|
* Rewrite (or clear) the one-turn GRANT: a non-null value is the minted grant
|
|
438
454
|
* (`auto` | `range M`), null clears it.
|
|
439
455
|
*
|
|
440
|
-
* The turn hook calls this on every human prompt
|
|
441
|
-
* otherwise
|
|
456
|
+
* The turn hook calls this on every human prompt (minted on a match, cleared
|
|
457
|
+
* otherwise) so a grant lives exactly one turn by construction. Pairing the
|
|
442
458
|
* write with its delete here means a grant can never half-persist, and the
|
|
443
459
|
* delete lives where deletions belong.
|
|
444
460
|
*/
|
|
@@ -451,7 +467,7 @@ export function setGrant(root, grant) {
|
|
|
451
467
|
}
|
|
452
468
|
}
|
|
453
469
|
/**
|
|
454
|
-
* TICK
|
|
470
|
+
* TICK: the per-build entry stamp: the TURN value recorded when work was
|
|
455
471
|
* entered (`build <n>` for a step, `start` for the plan), cleared when
|
|
456
472
|
* `checkpoint` lands.
|
|
457
473
|
*
|
|
@@ -474,7 +490,7 @@ export function stampTick(root, slug) {
|
|
|
474
490
|
writeFileSync(tickPath(root, slug), `${turn}\n`);
|
|
475
491
|
}
|
|
476
492
|
/**
|
|
477
|
-
* Consume the entry stamp
|
|
493
|
+
* Consume the entry stamp: `checkpoint` clears it when a step (or the plan)
|
|
478
494
|
* lands, the same beat that clears STEP/SEAM/handoff.
|
|
479
495
|
*
|
|
480
496
|
* Absent is a no-op; the rmSync lives here with the sidecar's other deletions.
|
|
@@ -486,7 +502,7 @@ export function clearTick(root, slug) {
|
|
|
486
502
|
* The TURN count, or null when the ledger is absent or unreadable.
|
|
487
503
|
*
|
|
488
504
|
* Absence is the "dormant" signal stampTick keys off and the doctor latch probe
|
|
489
|
-
* reports
|
|
505
|
+
* reports: never an error.
|
|
490
506
|
*/
|
|
491
507
|
export function readTurn(root) {
|
|
492
508
|
try {
|
|
@@ -502,7 +518,7 @@ export function readTurn(root) {
|
|
|
502
518
|
* `.plumbbob/STATE` file), or null when there is none.
|
|
503
519
|
*
|
|
504
520
|
* The turn hook runs on every human prompt in every repo; this is the cheap,
|
|
505
|
-
* git-free probe that keeps it a silent no-op outside a live plumbbob session
|
|
521
|
+
* git-free probe that keeps it a silent no-op outside a live plumbbob session:
|
|
506
522
|
* filesystem only, never a host sniff.
|
|
507
523
|
*/
|
|
508
524
|
export function findSessionRoot(cwd) {
|
|
@@ -519,8 +535,8 @@ export function findSessionRoot(cwd) {
|
|
|
519
535
|
/**
|
|
520
536
|
* Append `patterns` to the repo's info/exclude, each at most once.
|
|
521
537
|
*
|
|
522
|
-
* Idempotent
|
|
523
|
-
* to the *common* gitdir's exclude
|
|
538
|
+
* Idempotent: a re-`start` after finish must not double-add. `gitPath` resolves
|
|
539
|
+
* to the *common* gitdir's exclude (the only one git reads) so this works from
|
|
524
540
|
* a linked worktree, whose per-worktree gitdir has no `info/`. info/exclude
|
|
525
541
|
* keeps the exclusion personal machinery, never something imposed on the repo's
|
|
526
542
|
* own tracked ignore file.
|
|
@@ -546,7 +562,7 @@ function addExcludes(root, patterns) {
|
|
|
546
562
|
* Git-exclude the control plane while the artifact plane stays tracked.
|
|
547
563
|
*
|
|
548
564
|
* With `builds/<slug>/` tracked, only the per-worktree control files are
|
|
549
|
-
* excluded
|
|
565
|
+
* excluded: the session sentinel STATE (whose content is the active-build
|
|
550
566
|
* cursor), the personal settings overlay, the turn ledger, and the in-flight
|
|
551
567
|
* markers inside every build. Everything else under `.plumbbob/` (settings.json,
|
|
552
568
|
* and each build's intent/build-log/checkpoints/report) rides the branch into
|
|
@@ -563,11 +579,11 @@ export function excludeControl(root) {
|
|
|
563
579
|
`${DIRNAME}/builds/*/STEP`,
|
|
564
580
|
`${DIRNAME}/builds/*/SEAM`,
|
|
565
581
|
`${DIRNAME}/builds/*/SPIKE`,
|
|
566
|
-
// The entry stamp is in-flight control like STEP/SEAM
|
|
582
|
+
// The entry stamp is in-flight control like STEP/SEAM: checkpoint's
|
|
567
583
|
// stageAll must never sweep it into a step commit.
|
|
568
584
|
`${DIRNAME}/builds/*/TICK`,
|
|
569
585
|
// The agent-run handoff ledger is step-scoped in-flight state, not a
|
|
570
|
-
// tracked artifact
|
|
586
|
+
// tracked artifact: it must never ride a step commit into the PR.
|
|
571
587
|
`${DIRNAME}/builds/*/handoff.json`,
|
|
572
588
|
// The checkride gate writes raw tool output to `.check/`; checkpoint's
|
|
573
589
|
// stageAll must never sweep it into a step commit.
|
|
@@ -575,7 +591,7 @@ export function excludeControl(root) {
|
|
|
575
591
|
]);
|
|
576
592
|
}
|
|
577
593
|
/**
|
|
578
|
-
* `start --local`: opt out of the tracked layout into a fully
|
|
594
|
+
* `start --local`: opt out of the tracked layout into a fully untracked sidecar:
|
|
579
595
|
* some team repos won't accept tool folders in-tree.
|
|
580
596
|
*
|
|
581
597
|
* Excludes the whole `.plumbbob/` directory.
|
|
@@ -586,7 +602,7 @@ export function excludeSidecar(root) {
|
|
|
586
602
|
/**
|
|
587
603
|
* Whether info/exclude already carries `pattern` verbatim.
|
|
588
604
|
*
|
|
589
|
-
* Absent or unreadable reads as "no"
|
|
605
|
+
* Absent or unreadable reads as "no": the caller's job is to write it, not to
|
|
590
606
|
* fail on a repo that has never been excluded.
|
|
591
607
|
*/
|
|
592
608
|
function hasExclude(root, pattern) {
|
|
@@ -603,15 +619,15 @@ function hasExclude(root, pattern) {
|
|
|
603
619
|
* Re-apply the control-plane excludes on the way into a `git add -A`.
|
|
604
620
|
*
|
|
605
621
|
* `start` writes them once, but a plumbbob upgraded mid-build can add a control
|
|
606
|
-
* file the running session's exclude never learned
|
|
607
|
-
* handoff.json all arrived that way
|
|
622
|
+
* file the running session's exclude never learned (TICK, GRANT and
|
|
623
|
+
* handoff.json all arrived that way) and checkpoint/finish stage with `-A`,
|
|
608
624
|
* which would sweep the unexcluded file into the commit and ride it into the PR.
|
|
609
625
|
* Idempotent, so the common path is one read and no write.
|
|
610
626
|
*
|
|
611
|
-
* A fully
|
|
627
|
+
* A fully untracked sidecar is already covered by its blanket `.plumbbob/` line;
|
|
612
628
|
* layering the narrow patterns underneath would only add noise, so skip it. That
|
|
613
629
|
* check reads info/exclude rather than the cursor because a null cursor means
|
|
614
|
-
* "`--local` OR no build named"
|
|
630
|
+
* "`--local` OR no build named", and calling excludeSidecar on the tracked
|
|
615
631
|
* layout would untrack the whole artifact plane.
|
|
616
632
|
*/
|
|
617
633
|
export function refreshExcludes(root) {
|
package/dist/lib/templates.js
CHANGED
|
@@ -9,7 +9,7 @@ import { readFileSync } from 'node:fs';
|
|
|
9
9
|
* Read a template by filename from the package's `templates/` directory.
|
|
10
10
|
*
|
|
11
11
|
* Resolved relative to the compiled module URL, so the same relative hop works
|
|
12
|
-
* from `src/lib/` and `dist/lib/` alike
|
|
12
|
+
* from `src/lib/` and `dist/lib/` alike: the templates ship with the package,
|
|
13
13
|
* never with the consumer's repo.
|
|
14
14
|
*/
|
|
15
15
|
export function readTemplate(name) {
|
|
@@ -0,0 +1,122 @@
|
|
|
1
|
+
// `plumbbob abandon`: the third exit from an in-flight step. Checkpoint lands
|
|
2
|
+
// the step and revert destroys the work; abandon drops the attempt but keeps the
|
|
3
|
+
// working-tree diff exactly where it is. Its only writes are the in-flight
|
|
4
|
+
// control markers it clears (STEP, SEAM, TICK, and the step-scoped handoff
|
|
5
|
+
// ledger), a build-log line, and the stats receipt: it touches neither the
|
|
6
|
+
// working tree nor git nor the intent checkbox, so the step stays planned and
|
|
7
|
+
// re-buildable with its diff still in the tree for the human to keep, rework, or
|
|
8
|
+
// commit by hand.
|
|
9
|
+
//
|
|
10
|
+
// A step exit is a boundary crossing, so abandon honors the same approval latch
|
|
11
|
+
// as checkpoint. That is not decoration: abandon clears the entry stamp the
|
|
12
|
+
// latch reads, so an unlatched abandon followed by a same-turn checkpoint would
|
|
13
|
+
// hand that checkpoint an unlatched land, the same side door the settings-auto
|
|
14
|
+
// case closed. Every deletion routes through the sidecar helpers that own
|
|
15
|
+
// control-state clears; abandon itself never calls the raw fs deleters.
|
|
16
|
+
import { readFileSync, writeFileSync } from 'node:fs';
|
|
17
|
+
import { findRepoRoot } from "../lib/git.js";
|
|
18
|
+
import { bumpStepStat, buildLogPath, clearHandoff, clearSeam, clearStep, clearTick, hasSession, intentPath, resolveBuild, stepPath, } from "../lib/sidecar.js";
|
|
19
|
+
import { checkLatch } from "../lib/latch.js";
|
|
20
|
+
import { abandonLogLine, appendToSection } from "../lib/buildlog.js";
|
|
21
|
+
import { parseSteps } from "../lib/orient.js";
|
|
22
|
+
/**
|
|
23
|
+
* Drop the in-flight step while keeping its work: latch, clear the markers,
|
|
24
|
+
* record the event, and return to the boundary.
|
|
25
|
+
*
|
|
26
|
+
* Exit 1 on any refusal: no session, no step in flight, or a latched boundary
|
|
27
|
+
* (no human turn since the step began). Each refusal says what unblocks it.
|
|
28
|
+
*/
|
|
29
|
+
export function abandon(cwd, args) {
|
|
30
|
+
const root = findRepoRoot(cwd);
|
|
31
|
+
if (root === null || !hasSession(root)) {
|
|
32
|
+
process.stderr.write('plumbbob: no active session. Run `plumbbob start "<title>"` first.\n');
|
|
33
|
+
return 1;
|
|
34
|
+
}
|
|
35
|
+
const { build: slug } = resolveBuild(root, args);
|
|
36
|
+
const step = readInFlightStep(root, slug);
|
|
37
|
+
if (step === null) {
|
|
38
|
+
process.stderr.write('plumbbob: no step in flight to abandon. abandon drops an in-flight step; from the boundary there is nothing to drop.\n');
|
|
39
|
+
return 1;
|
|
40
|
+
}
|
|
41
|
+
// The same latch checkpoint runs, keyed on this step: a step exit is a boundary
|
|
42
|
+
// crossing, and abandon clears the entry stamp, so an unlatched abandon would
|
|
43
|
+
// hand a same-turn checkpoint an unlatched land. Refusal is the pause, not an
|
|
44
|
+
// error: exit 1 and hand the turn back.
|
|
45
|
+
const latch = checkLatch(root, step);
|
|
46
|
+
if (!latch.allow) {
|
|
47
|
+
process.stderr.write(abandonPause(latch.reason, step));
|
|
48
|
+
return 1;
|
|
49
|
+
}
|
|
50
|
+
// Keep the work: clear only the in-flight control markers, never the tree,
|
|
51
|
+
// git, or the intent checkbox. The step stays [ ] and re-buildable.
|
|
52
|
+
clearStep(root, slug);
|
|
53
|
+
clearSeam(root, slug);
|
|
54
|
+
clearTick(root, slug);
|
|
55
|
+
clearHandoff(root, slug);
|
|
56
|
+
bumpStepStat(root, slug, step, 'abandons');
|
|
57
|
+
logAbandon(root, slug, step);
|
|
58
|
+
process.stdout.write(`plumbbob: step ${step} abandoned — work kept in the tree, back at the boundary. The step stays planned.\n`);
|
|
59
|
+
return 0;
|
|
60
|
+
}
|
|
61
|
+
/**
|
|
62
|
+
* The abandon-worded pause affordance.
|
|
63
|
+
*
|
|
64
|
+
* abandon runs checkpoint's latch, but the pause it hits is its own: the wording
|
|
65
|
+
* names the verb the human ran and the one self-approval that lands it, rather
|
|
66
|
+
* than sending them back to `checkpoint`. A `ceiling` reason (a range grant the
|
|
67
|
+
* step overran) keeps the range wording.
|
|
68
|
+
*/
|
|
69
|
+
function abandonPause(reason, step) {
|
|
70
|
+
if (reason === 'ceiling') {
|
|
71
|
+
return `plumbbob: abandon refused — step ${step} is past the range you granted; pause here, then run it again to continue.\n`;
|
|
72
|
+
}
|
|
73
|
+
return `plumbbob: abandon refused — no human turn since this step began. A step exit crosses the same
|
|
74
|
+
boundary as a checkpoint, so abandon honors the same pause: end the turn, and the human's approval on
|
|
75
|
+
their next turn is what lets it land. (An explicit \`/plumbbob:build --auto\` or a step range in the
|
|
76
|
+
human's own prompt is the only self-approval.)
|
|
77
|
+
`;
|
|
78
|
+
}
|
|
79
|
+
/**
|
|
80
|
+
* The in-flight STEP number from the marker, or null when none is in flight.
|
|
81
|
+
*/
|
|
82
|
+
function readInFlightStep(root, slug) {
|
|
83
|
+
try {
|
|
84
|
+
const raw = readFileSync(stepPath(root, slug), 'utf8').trim();
|
|
85
|
+
return /^\d+$/.test(raw) ? Number(raw) : null;
|
|
86
|
+
}
|
|
87
|
+
catch {
|
|
88
|
+
return null;
|
|
89
|
+
}
|
|
90
|
+
}
|
|
91
|
+
/**
|
|
92
|
+
* Append the abandon line to the build-log's `## Log`, the first non-checkpoint
|
|
93
|
+
* event the log records.
|
|
94
|
+
*
|
|
95
|
+
* Best-effort like every build-log write: a missing or odd log never fails an
|
|
96
|
+
* abandon, whose real record is the cleared markers and the stats bump.
|
|
97
|
+
*/
|
|
98
|
+
function logAbandon(root, slug, step) {
|
|
99
|
+
try {
|
|
100
|
+
const path = buildLogPath(root, slug);
|
|
101
|
+
const date = new Date().toISOString().slice(0, 10);
|
|
102
|
+
const line = abandonLogLine(date, step, titleForStep(root, slug, step));
|
|
103
|
+
const updated = appendToSection(readFileSync(path, 'utf8'), 'Log', line);
|
|
104
|
+
if (updated !== null) {
|
|
105
|
+
writeFileSync(path, updated);
|
|
106
|
+
}
|
|
107
|
+
}
|
|
108
|
+
catch {
|
|
109
|
+
// best-effort ledger; never fail an abandon over the build-log.
|
|
110
|
+
}
|
|
111
|
+
}
|
|
112
|
+
/**
|
|
113
|
+
* The step's title from intent.md, or null when absent or unreadable.
|
|
114
|
+
*/
|
|
115
|
+
function titleForStep(root, slug, step) {
|
|
116
|
+
try {
|
|
117
|
+
return parseSteps(readFileSync(intentPath(root, slug), 'utf8')).find((s) => s.n === step)?.title ?? null;
|
|
118
|
+
}
|
|
119
|
+
catch {
|
|
120
|
+
return null;
|
|
121
|
+
}
|
|
122
|
+
}
|