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.
Files changed (144) hide show
  1. package/.claude-plugin/plugin.json +1 -1
  2. package/README.md +89 -87
  3. package/dist/cli-core.js +18 -8
  4. package/dist/cli.js +1 -1
  5. package/dist/lib/agents.js +26 -26
  6. package/dist/lib/buildlog.js +21 -11
  7. package/dist/lib/buildlogsync.js +8 -8
  8. package/dist/lib/check.js +14 -14
  9. package/dist/lib/commitbody.js +72 -0
  10. package/dist/lib/commitmsg.js +7 -7
  11. package/dist/lib/git.js +44 -15
  12. package/dist/lib/intent.js +20 -19
  13. package/dist/lib/latch.js +16 -16
  14. package/dist/lib/orient.js +36 -17
  15. package/dist/lib/reentry.js +3 -3
  16. package/dist/lib/settings.js +16 -16
  17. package/dist/lib/sidecar.js +70 -54
  18. package/dist/lib/templates.js +1 -1
  19. package/dist/verbs/abandon.js +122 -0
  20. package/dist/verbs/agent.js +25 -25
  21. package/dist/verbs/build.js +9 -9
  22. package/dist/verbs/check.js +6 -6
  23. package/dist/verbs/checkpoint.js +48 -47
  24. package/dist/verbs/doctor.js +35 -29
  25. package/dist/verbs/finish.js +27 -40
  26. package/dist/verbs/handoff.js +9 -8
  27. package/dist/verbs/init.js +9 -9
  28. package/dist/verbs/park.js +3 -3
  29. package/dist/verbs/recover.js +12 -12
  30. package/dist/verbs/revert.js +12 -12
  31. package/dist/verbs/spike.js +21 -21
  32. package/dist/verbs/start.js +49 -19
  33. package/dist/verbs/status.js +8 -8
  34. package/dist/verbs/turn.js +16 -16
  35. package/dist/verbs/use.js +4 -4
  36. package/node_modules/checkride/.claude-plugin/plugin.json +1 -1
  37. package/node_modules/checkride/CHANGELOG.md +401 -0
  38. package/node_modules/checkride/README.md +27 -9
  39. package/node_modules/checkride/dist/adapters.d.ts +12 -4
  40. package/node_modules/checkride/dist/adapters.d.ts.map +1 -1
  41. package/node_modules/checkride/dist/adapters.js +21 -1
  42. package/node_modules/checkride/dist/adapters.js.map +1 -1
  43. package/node_modules/checkride/dist/agent-setup/cursor.d.ts +9 -1
  44. package/node_modules/checkride/dist/agent-setup/cursor.d.ts.map +1 -1
  45. package/node_modules/checkride/dist/agent-setup/cursor.js +59 -1
  46. package/node_modules/checkride/dist/agent-setup/cursor.js.map +1 -1
  47. package/node_modules/checkride/dist/agent-setup/harness.d.ts +1 -1
  48. package/node_modules/checkride/dist/agent-setup/harness.d.ts.map +1 -1
  49. package/node_modules/checkride/dist/agent-setup/harness.js +10 -1
  50. package/node_modules/checkride/dist/agent-setup/harness.js.map +1 -1
  51. package/node_modules/checkride/dist/agent-setup/scripts.d.ts +23 -0
  52. package/node_modules/checkride/dist/agent-setup/scripts.d.ts.map +1 -1
  53. package/node_modules/checkride/dist/agent-setup/scripts.js +387 -82
  54. package/node_modules/checkride/dist/agent-setup/scripts.js.map +1 -1
  55. package/node_modules/checkride/dist/baseline/fallow.js +2 -2
  56. package/node_modules/checkride/dist/baseline/fingerprint.d.ts +14 -5
  57. package/node_modules/checkride/dist/baseline/fingerprint.d.ts.map +1 -1
  58. package/node_modules/checkride/dist/baseline/fingerprint.js +58 -5
  59. package/node_modules/checkride/dist/baseline/fingerprint.js.map +1 -1
  60. package/node_modules/checkride/dist/baseline/history.d.ts +98 -0
  61. package/node_modules/checkride/dist/baseline/history.d.ts.map +1 -0
  62. package/node_modules/checkride/dist/baseline/history.js +232 -0
  63. package/node_modules/checkride/dist/baseline/history.js.map +1 -0
  64. package/node_modules/checkride/dist/baseline/index.d.ts +4 -2
  65. package/node_modules/checkride/dist/baseline/index.d.ts.map +1 -1
  66. package/node_modules/checkride/dist/baseline/index.js +2 -1
  67. package/node_modules/checkride/dist/baseline/index.js.map +1 -1
  68. package/node_modules/checkride/dist/baseline/store.d.ts +30 -7
  69. package/node_modules/checkride/dist/baseline/store.d.ts.map +1 -1
  70. package/node_modules/checkride/dist/baseline/store.js +50 -16
  71. package/node_modules/checkride/dist/baseline/store.js.map +1 -1
  72. package/node_modules/checkride/dist/cli.d.ts +5 -1
  73. package/node_modules/checkride/dist/cli.d.ts.map +1 -1
  74. package/node_modules/checkride/dist/cli.js +98 -25
  75. package/node_modules/checkride/dist/cli.js.map +1 -1
  76. package/node_modules/checkride/dist/config.d.ts +33 -4
  77. package/node_modules/checkride/dist/config.d.ts.map +1 -1
  78. package/node_modules/checkride/dist/config.js +21 -6
  79. package/node_modules/checkride/dist/config.js.map +1 -1
  80. package/node_modules/checkride/dist/digest/digest.js +1 -1
  81. package/node_modules/checkride/dist/doctor.d.ts +2 -0
  82. package/node_modules/checkride/dist/doctor.d.ts.map +1 -1
  83. package/node_modules/checkride/dist/doctor.js +39 -1
  84. package/node_modules/checkride/dist/doctor.js.map +1 -1
  85. package/node_modules/checkride/dist/gate.d.ts +49 -4
  86. package/node_modules/checkride/dist/gate.d.ts.map +1 -1
  87. package/node_modules/checkride/dist/gate.js +143 -16
  88. package/node_modules/checkride/dist/gate.js.map +1 -1
  89. package/node_modules/checkride/dist/init.d.ts +8 -1
  90. package/node_modules/checkride/dist/init.d.ts.map +1 -1
  91. package/node_modules/checkride/dist/init.js +68 -7
  92. package/node_modules/checkride/dist/init.js.map +1 -1
  93. package/node_modules/checkride/dist/node-pin.d.ts +1 -1
  94. package/node_modules/checkride/dist/node-pin.js +1 -1
  95. package/node_modules/checkride/dist/orchestrator.d.ts +13 -0
  96. package/node_modules/checkride/dist/orchestrator.d.ts.map +1 -1
  97. package/node_modules/checkride/dist/orchestrator.js +118 -13
  98. package/node_modules/checkride/dist/orchestrator.js.map +1 -1
  99. package/node_modules/checkride/dist/pm/index.d.ts +1 -0
  100. package/node_modules/checkride/dist/pm/index.d.ts.map +1 -1
  101. package/node_modules/checkride/dist/pm/index.js.map +1 -1
  102. package/node_modules/checkride/dist/pm/launch.d.ts +23 -2
  103. package/node_modules/checkride/dist/pm/launch.d.ts.map +1 -1
  104. package/node_modules/checkride/dist/pm/launch.js +16 -0
  105. package/node_modules/checkride/dist/pm/launch.js.map +1 -1
  106. package/node_modules/checkride/dist/pm/translate.js +1 -1
  107. package/node_modules/checkride/dist/pm/translate.js.map +1 -1
  108. package/node_modules/checkride/dist/qa/mutation.d.ts +4 -4
  109. package/node_modules/checkride/dist/qa/mutation.js +4 -4
  110. package/node_modules/checkride/dist/recover.d.ts +48 -0
  111. package/node_modules/checkride/dist/recover.d.ts.map +1 -0
  112. package/node_modules/checkride/dist/recover.js +220 -0
  113. package/node_modules/checkride/dist/recover.js.map +1 -0
  114. package/node_modules/checkride/dist/smoke.d.ts +1 -1
  115. package/node_modules/checkride/dist/smoke.js +1 -1
  116. package/node_modules/checkride/dist/snippets.d.ts +1 -1
  117. package/node_modules/checkride/package.json +3 -2
  118. package/node_modules/checkride/schema/checkride.config.schema.json +9 -1
  119. package/node_modules/checkride/skills/check/SKILL.md +9 -1
  120. package/node_modules/checkride/templates/shared/styles/Repo/Drift.yml +41 -0
  121. package/node_modules/checkride/templates/shared/styles/Repo/Latin.yml +14 -0
  122. package/node_modules/checkride/templates/shared/styles/Repo/LyHyphen.yml +28 -0
  123. package/node_modules/checkride/templates/shared/styles/Repo/Minted.yml +33 -0
  124. package/node_modules/checkride/templates/shared/styles/Repo/ThereIs.yml +16 -0
  125. package/node_modules/checkride/templates/shared/styles/Repo/Weasel.yml +26 -0
  126. package/node_modules/checkride/templates/shared/vale.ini +34 -0
  127. package/package.json +13 -11
  128. package/skills/abandon/SKILL.md +20 -0
  129. package/skills/build/SKILL.md +69 -64
  130. package/skills/doctor/SKILL.md +11 -11
  131. package/skills/finish/SKILL.md +20 -19
  132. package/skills/harvest/SKILL.md +13 -12
  133. package/skills/park/SKILL.md +10 -10
  134. package/skills/plan/SKILL.md +63 -54
  135. package/skills/recover/SKILL.md +7 -7
  136. package/skills/refine/SKILL.md +18 -16
  137. package/skills/revert/SKILL.md +6 -6
  138. package/skills/spike/SKILL.md +9 -9
  139. package/skills/status/SKILL.md +5 -5
  140. package/skills/step/SKILL.md +17 -12
  141. package/skills/verify/SKILL.md +45 -40
  142. package/templates/build-log.md +7 -7
  143. package/templates/intent.md +49 -39
  144. package/templates/spike-report.md +10 -10
@@ -1,22 +1,22 @@
1
- // The .plumbbob/ sidecar — plumbbob's control directory beside the code. Control
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 — intent, build-log, checkpoints, report),
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 — the
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 — not in settings.local.json — keeps that overlay purely
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 — BUILD ⇔
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) — the
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 — the directory names under `builds/`, sorted.
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 — the active-build cursor — or null when it names no build.
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 — scanning positionals on the raw argv would let it masquerade
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 — session sentinel and active-build cursor in one.
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 — the path reads stay stable even before a tracked
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 — the `builds/<slug>/` dir for the active
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 — a single-purpose presence flag, like SEAM/STEP.
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 — the in-flight step's edit grant: a plain list of the paths the step may
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
- * STEP — the number of the step in flight, a bare number in a flat file.
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
- * The build's `checkpoints` ledger — the recorded SHAs of landed plan and step
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 — the build's plan: steps, seams, decisions, done-when.
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 — the human-facing build ledger (the Steps mirror and the Log).
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 — the close-out report, beside intent.md / build-log.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 — an explicit `/plumbbob:spike` or a planned `spike:` step —
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 — the caller writes the stamped template there.
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 — the agent-run ledger.
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[]` — the CLI itself is
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 — a corrupt ledger must never wedge a run.
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 — the step-scoped ledger clears when the step
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 — the per-build stats receipt, beside checkpoints.
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 — malformed contributes
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 — the stats are a
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 — startedAt at `build <n>`, landedAt at
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" — the
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`) — a plain content rewrite
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 — the SPIKE marker exists (content is irrelevant).
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 — existence is the whole signal.
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 — both flat, per-worktree control (never per-build, never committed).
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 — the human-turn count the UserPromptSubmit hook maintains.
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 — the one-turn self-approval minted from the human's literal prompt.
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 — minted on a match, cleared
441
- * otherwise — so a grant lives exactly one turn by construction. Pairing the
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 — the per-build entry stamp: the TURN value recorded when work was
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 — `checkpoint` clears it when a step (or the plan)
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 — never an error.
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 — a re-`start` after finish must not double-add. `gitPath` resolves
523
- * to the *common* gitdir's exclude — the only one git reads — so this works from
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 — the session sentinel STATE (whose content is the active-build
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 — checkpoint's
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 — it must never ride a step commit into the PR.
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-untracked sidecar —
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" — the caller's job is to write it, not to
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 — TICK, GRANT and
607
- * handoff.json all arrived that way — and checkpoint/finish stage with `-A`,
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-untracked sidecar is already covered by its blanket `.plumbbob/` line;
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" — and calling excludeSidecar on the tracked
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) {
@@ -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 — the templates ship with the package,
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
+ }