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,17 +1,17 @@
1
- // `plumbbob agent <subcommand>` — the doorway to user-authored agents: anything
1
+ // `plumbbob agent <subcommand>`: the doorway to user-authored agents: anything
2
2
  // executable that speaks the JSON envelope contract (JSON in on stdin, JSON out
3
- // on stdout, prose streamed on stderr). `agent list` walks the two agent tiers —
4
- // the repo's tracked `.plumbbob/agents/<name>/`, then the personal
5
- // `~/.plumbbob/agents/<name>/` — and prints each resolvable agent. `agent run
3
+ // on stdout, prose streamed on stderr). `agent list` walks the two agent tiers
4
+ // (the repo's tracked `.plumbbob/agents/<name>/`, then the personal
5
+ // `~/.plumbbob/agents/<name>/`) and prints each resolvable agent. `agent run
6
6
  // <name> [--step N] [--mode before|build|after]` composes the StepContext,
7
7
  // spawns the manifest command, streams its stderr live, captures and validates
8
8
  // the child's envelope, re-emits it on this verb's own stdout (machine) with the
9
9
  // human summary on stderr, lands `parked[]` through the build-log, and appends
10
- // the envelope to the handoff ledger (`builds/<slug>/handoff.json` — untracked,
10
+ // the envelope to the handoff ledger (`builds/<slug>/handoff.json`: untracked,
11
11
  // step-scoped, cleared at checkpoint) so later runs can thread it back in.
12
- // There is deliberately no code path here to checkpoint, flip a step, or chain
13
- // agents — the subprocess boundary keeps the human as the clock by
14
- // construction, not by policy. A thin read-write shell: resolution,
12
+ // No code path here can checkpoint, flip a step, or chain agents, and that
13
+ // absence is deliberate; the subprocess boundary keeps the human as the clock
14
+ // by construction, not by policy. A thin read-write shell: resolution,
15
15
  // composition, and spawn mechanics live in lib/agents.ts.
16
16
  import { readFileSync, writeFileSync } from 'node:fs';
17
17
  import { join } from 'node:path';
@@ -50,10 +50,10 @@ function list(cwd, _args) {
50
50
  * Run one named agent, or a slot's harness-bound agents, against the step in flight.
51
51
  *
52
52
  * With a name (or `--agent` flag) it runs exactly that agent, failing loud on a
53
- * miss — the user who typed the name asked for it specifically. With no name it
53
+ * miss; the user who typed the name asked for it specifically. With no name it
54
54
  * runs whatever the build's harness.json binds to the requested slot. Either
55
- * way it composes the StepContext, spawns, and applies side effects — never
56
- * advancing the loop.
55
+ * way it composes the StepContext, spawns, and applies side effects (never
56
+ * advancing the loop).
57
57
  */
58
58
  async function run(cwd, args) {
59
59
  const root = findRepoRoot(cwd);
@@ -72,7 +72,7 @@ async function run(cwd, args) {
72
72
  process.stderr.write('plumbbob: no step to run against — pass --step N, or `plumbbob build N` first.\n');
73
73
  return 1;
74
74
  }
75
- // A `--agent <path>` flag still needs a name — it labels an explicit run (the
75
+ // A `--agent <path>` flag still needs a name; it labels an explicit run (the
76
76
  // handoff ledger and the human summary key on the name), it does not name one.
77
77
  if (parsed.name === undefined && parsed.flagPath !== undefined) {
78
78
  process.stderr.write('plumbbob: --agent needs an agent name too, for the run label. Try: plumbbob agent run reviewer --agent ./path --step 3.\n');
@@ -94,10 +94,10 @@ async function run(cwd, args) {
94
94
  * One agent's full run: resolve it, pick its slot, compose the StepContext, spawn, report.
95
95
  *
96
96
  * `ambient` marks a harness-bound run whose resolution or slot mismatch
97
- * degrades to a warning so a batch keeps going — a binding is ambient
97
+ * degrades to a warning so a batch keeps going; a binding is ambient
98
98
  * configuration the loop must survive without; an explicit ask
99
99
  * (`ambient: false`) fails loud on the same miss. A run that actually starts
100
- * and fails (non-zero exit, timeout, …) is a hard failure either way — the
100
+ * and fails (non-zero exit, timeout, …) is a hard failure either way: the
101
101
  * softening covers a *missing* agent, never a broken one.
102
102
  */
103
103
  async function runOne(root, slug, step, spec) {
@@ -126,17 +126,17 @@ async function runOne(root, slug, step, spec) {
126
126
  mode,
127
127
  settings: {
128
128
  // The envelope reports the `auto` setting to agents as information only.
129
- // The checkpoint latch never reads it — a model can write a settings
129
+ // The checkpoint latch never reads it; a model can write a settings
130
130
  // file, so a standing `auto` cannot be a self-approval grant; approval
131
131
  // comes only from what the human literally typed. A `true` here informs
132
132
  // an agent, it does not self-approve.
133
133
  auto: resolveBoolean(root, 'auto', false),
134
134
  agentTimeout: resolveNumber(root, 'agentTimeout', 0),
135
135
  // Hand this agent its own config block over the envelope's existing
136
- // `settings` field — no new envelope field, no new verb; the config just
136
+ // `settings` field: no new envelope field, no new verb; the config just
137
137
  // rides here. Resolution: settings.json's agentConfig[name], with the
138
138
  // untracked personal overlay (settings.local.json) replacing the project
139
- // entry whole — no deep merge — and {} when neither defines it.
139
+ // entry whole (no deep merge) and {} when neither defines it.
140
140
  agent: resolveRecord(root, 'agentConfig')[spec.name] ?? {},
141
141
  },
142
142
  });
@@ -159,7 +159,7 @@ async function runOne(root, slug, step, spec) {
159
159
  /**
160
160
  * No name given: run the agents the build's harness.json binds to the requested slot.
161
161
  *
162
- * Bindings merge as a ladder — the per-step harness entry beats the harness
162
+ * Bindings merge as a ladder: the per-step harness entry beats the harness
163
163
  * `defaults`, which beat the settings-level `agents` key; the first level that
164
164
  * names the slot wins, replace not append. A missing bound agent degrades to a
165
165
  * warning; an absent harness, or one that binds nothing to this slot, is a
@@ -216,7 +216,7 @@ function degrade(ambient, hard, soft) {
216
216
  *
217
217
  * A value flag missing its value, or `--step` given a non-number, is a loud
218
218
  * error (returned as a string) rather than a silent default. An undeclared
219
- * `--flag` never arrives here — `run` screens argv against the verb spec in
219
+ * `--flag` never arrives here; `run` screens argv against the verb spec in
220
220
  * cli-core.ts and refuses before dispatch.
221
221
  */
222
222
  function parseRunArgs(args) {
@@ -254,7 +254,7 @@ function parseRunArgs(args) {
254
254
  /**
255
255
  * Resolve the slot the agent runs in.
256
256
  *
257
- * An explicit `--mode` must name a real slot AND one the manifest declares —
257
+ * An explicit `--mode` must name a real slot AND one the manifest declares;
258
258
  * an undeclared slot is refused loud, because the user asked for that exact
259
259
  * run. With no `--mode`, a single-slot agent uses its only slot; a multi-slot
260
260
  * agent must be told which one (harness bindings pick the slot from the
@@ -282,11 +282,11 @@ function resolveMode(flag, manifest) {
282
282
  /**
283
283
  * Map the run outcome to reporting and side effects.
284
284
  *
285
- * A failed run — non-zero exit, out-of-contract stdout, timeout, interrupt, or a
286
- * shell that never started — reports on stderr and stops with NO side effects:
285
+ * A failed run (non-zero exit, out-of-contract stdout, timeout, interrupt, or a
286
+ * shell that never started) reports on stderr and stops with NO side effects:
287
287
  * the envelope of a failed child is not authoritative. A clean run lands
288
288
  * `parked[]`, records the envelope in the handoff ledger, prints the human
289
- * summary on stderr, and re-emits the machine envelope on stdout — nothing else,
289
+ * summary on stderr, and re-emits the machine envelope on stdout, nothing else,
290
290
  * ever (the stream discipline: stdout carries the envelope alone).
291
291
  */
292
292
  function report(root, slug, name, mode, step, result) {
@@ -321,7 +321,7 @@ function failureLine(name, result) {
321
321
  /**
322
322
  * Land each parked concern through the build-log's Park list.
323
323
  *
324
- * The agent never writes .plumbbob/ itself — parked lines only reach the
324
+ * The agent never writes .plumbbob/ itself; parked lines only reach the
325
325
  * build-log through this verb. A build-log with no "## Park list" section, or no
326
326
  * build-log at all, warns once rather than losing the lines silently.
327
327
  */
@@ -351,7 +351,7 @@ function applyParked(root, slug, parked) {
351
351
  /**
352
352
  * The human-facing summary printed on stderr.
353
353
  *
354
- * A headline plus, for a halt, the route out — `blocked` unblocks and re-runs,
354
+ * A headline plus, for a halt, the route out: `blocked` unblocks and re-runs,
355
355
  * `drift` sends the plan to /plumbbob:refine. The machine envelope on stdout carries
356
356
  * the same status for the calling skill; this stderr copy is the terminal read.
357
357
  */
@@ -1,9 +1,9 @@
1
- // `plumbbob build <n>` — enter a step: read step n's seam from intent.md and
1
+ // `plumbbob build <n>`: enter a step: read step n's seam from intent.md and
2
2
  // write the normalized SEAM + STEP control files (flat, untracked, per-build).
3
- // The seam is the step's edit grant — the exact paths and `dir/` prefixes the
3
+ // The seam is the step's edit grant: the exact paths and `dir/` prefixes the
4
4
  // step expects to touch, for orientation, never a lock. The STEP file is the
5
5
  // in-flight signal (the dashboard derives the BUILD phase from it); this verb
6
- // never checkpoints — only `checkpoint` commits.
6
+ // never checkpoints; only `checkpoint` commits.
7
7
  import { readFileSync, writeFileSync } from 'node:fs';
8
8
  import { findRepoRoot } from "../lib/git.js";
9
9
  import { hasSession, intentPath, resolveBuild, seamPath, stampStepStat, stampTick, stepPath } from "../lib/sidecar.js";
@@ -11,7 +11,7 @@ import { parseStepSeam } from "../lib/intent.js";
11
11
  import { parseSteps } from "../lib/orient.js";
12
12
  import { stepLabel, syncBuildLogState } from "../lib/buildlogsync.js";
13
13
  /**
14
- * Enter step n — or the next undone step — and write its SEAM/STEP markers.
14
+ * Enter step n (or the next undone step) and write its SEAM/STEP markers.
15
15
  *
16
16
  * Refuses a missing session, a malformed step argument, an `N-M` range (a
17
17
  * `/plumbbob:build` skill feature, not a CLI one), and a seam that fails to parse.
@@ -27,13 +27,13 @@ export function build(cwd, args) {
27
27
  const { build: slug, rest } = resolveBuild(root, args);
28
28
  const raw = rest.find((a) => !a.startsWith('--'));
29
29
  // A step range like `1-3` is a `/plumbbob:build` skill affordance (auto-approve
30
- // through the range, then pause), not a CLI capability — the CLI records one
30
+ // through the range, then pause), not a CLI capability: the CLI records one
31
31
  // in-flight step at a time. Name it rather than bounce off the generic usage.
32
32
  if (raw !== undefined && /^\d+-\d*$/.test(raw)) {
33
33
  process.stderr.write(`plumbbob: build takes one step number; \`${raw}\` step ranges are a \`/plumbbob:build\` feature (auto-approve through the range, then pause). Try \`plumbbob build ${raw.split('-')[0]}\`.\n`);
34
34
  return 1;
35
35
  }
36
- // An explicit arg that isn't a positive integer is a usage error — caught before
36
+ // An explicit arg that isn't a positive integer is a usage error: caught before
37
37
  // reading intent.md so the message doesn't depend on the plan being present.
38
38
  if (raw !== undefined && (!/^\d+$/.test(raw) || Number(raw) < 1)) {
39
39
  process.stderr.write('plumbbob: build needs a step number. Try: plumbbob build 2.\n');
@@ -65,14 +65,14 @@ export function build(cwd, args) {
65
65
  writeFileSync(stepPath(root, slug), `${step}\n`);
66
66
  // Stamp the turn ledger on entry (TICK = TURN): the checkpoint latch demands
67
67
  // a human turn after this point before the step may land. Skipped when the
68
- // ledger is dormant — a host with no hooks grows no TURN/TICK files.
68
+ // ledger is dormant: a host with no hooks grows no TURN/TICK files.
69
69
  stampTick(root, slug);
70
70
  // The wall-clock receipt starts here: checkpoint stamps landedAt, and the
71
71
  // pair becomes the step's duration in the finish report.
72
72
  stampStepStat(root, slug, step, 'startedAt', new Date().toISOString());
73
73
  // The build-log's top half is CLI-owned so it never lies: show this step in
74
- // flight — Current step plus the ☐/☑ mirror, re-rendered from intent.md.
75
- // Best-effort — a missing or hand-edited build-log never blocks the build.
74
+ // flight: Current step plus the ☐/☑ mirror, re-rendered from intent.md.
75
+ // Best-effort: a missing or hand-edited build-log never blocks the build.
76
76
  const title = parseSteps(intent).find((s) => s.n === step)?.title ?? null;
77
77
  syncBuildLogState(root, slug, stepLabel(step, title));
78
78
  const picked = raw === undefined ? ' (next undone)' : '';
@@ -1,14 +1,14 @@
1
- // `plumbbob check` — run the heavy gate and report, with NO state change. This
1
+ // `plumbbob check`: run the heavy gate and report, with NO state change. This
2
2
  // is the read-only half of the verify tick: `/plumbbob:verify` runs it before the
3
3
  // pause so the human approves on a known-green check. The gate itself lives in
4
4
  // lib/check.ts: checkride (our sibling check-runner package) by default, or a
5
- // spawn command named by the `check` setting — resolved through the settings
6
- // ladder (flag → local overlay → tracked settings.json → default) — for repos
5
+ // spawn command named by the `check` setting (resolved through the settings
6
+ // ladder (flag → local overlay → tracked settings.json → default)) for repos
7
7
  // that gate through something else. Exits with the check's own code (0 = green,
8
8
  // 1 = red, 2 = the gate itself broke).
9
9
  //
10
10
  // Narrowing flags for the iteration loop (`check --bail --only types,lint`) map
11
- // straight onto checkride's run flags. Only this verb takes them — the
11
+ // straight onto checkride's run flags. Only this verb takes them: the
12
12
  // checkpoint gate is always full-fat.
13
13
  import { findRepoRoot } from "../lib/git.js";
14
14
  import { hasSession } from "../lib/sidecar.js";
@@ -35,7 +35,7 @@ export async function check(cwd, args = []) {
35
35
  * Only the declared flags are read here; an undeclared one never reaches this
36
36
  * function, because `run` screens argv against the verb spec in cli-core.ts and
37
37
  * refuses before dispatch. This used to tolerate unknown args on the grounds
38
- * that running the gate mattered more than flag hygiene — but the same tolerance
38
+ * that running the gate mattered more than flag hygiene, but the same tolerance
39
39
  * on the mutating verbs let `checkpoint --help` commit, so the screen is now
40
40
  * central and uniform.
41
41
  */
@@ -69,7 +69,7 @@ function slotList(args, flag) {
69
69
  /**
70
70
  * The one-line human verdict for a check exit code.
71
71
  *
72
- * A broken gate (exit 2) reports distinctly from red — a misconfigured harness
72
+ * A broken gate (exit 2) reports distinctly from red: a misconfigured harness
73
73
  * must never read as broken code, though both block a checkpoint.
74
74
  */
75
75
  function verdictLine(code) {
@@ -1,18 +1,19 @@
1
- // `plumbbob checkpoint [<n>] [-m <msg>]` — the commit tick that lands a step.
1
+ // `plumbbob checkpoint [<n>] [-m <msg>]`: the commit tick that lands a step.
2
2
  // Executor-agnostic and author-blind: it reads the diff, not who wrote it, so a
3
3
  // `/plumbbob:build` run, your own hands, a vibe session, or another harness all
4
4
  // checkpoint identically. It does NOT require a STEP marker (the flat control
5
5
  // file recording the step in flight): the step is whatever you pass, else the
6
6
  // in-flight STEP, else the next undone step in intent.md. It evaluates the
7
7
  // approval latch, gates on a green check, commits any pending work (or records
8
- // the existing HEAD when the tree is already clean — the human's commit skill
8
+ // the existing HEAD when the tree is already clean: the human's commit skill
9
9
  // may have committed first), records the SHA, flips the intent checkbox to
10
- // `[x]`, and clears the STEP/SEAM markers — returning the dashboard to the
10
+ // `[x]`, and clears the STEP/SEAM markers: returning the dashboard to the
11
11
  // DESIGN boundary.
12
12
  import { appendFileSync, readFileSync, rmSync, writeFileSync } from 'node:fs';
13
13
  import { commit, findRepoRoot, headSha, isDirty, stageAll, stagePath, stagedPaths, stagedStat } from "../lib/git.js";
14
14
  import { activeBuild, buildFolder, buildLogPath, buildScope, bumpStepStat, checkpointsPath, clearHandoff, clearTick, hasSession, intentPath, readStats, refreshExcludes, seamPath, stampStepStat, stepPath, } from "../lib/sidecar.js";
15
15
  import { runCheck } from "../lib/check.js";
16
+ import { readCommitBody } from "../lib/commitbody.js";
16
17
  import { checkLatch } from "../lib/latch.js";
17
18
  import { markStepDone, parseSteps } from "../lib/orient.js";
18
19
  import { parseBuildScope, parseStepSeam, scopeDrift } from "../lib/intent.js";
@@ -22,7 +23,7 @@ import { AT_BOUNDARY, syncBuildLogState } from "../lib/buildlogsync.js";
22
23
  /**
23
24
  * Land a step: latch, check gate, commit, record, return to the boundary.
24
25
  *
25
- * Exit 1 on any refusal — no session, no resolvable step, a latched tick, or a
26
+ * Exit 1 on any refusal: no session, no resolvable step, a latched tick, or a
26
27
  * red/broken check; each refusal says what unblocks it.
27
28
  */
28
29
  export async function checkpoint(cwd, args) {
@@ -31,7 +32,7 @@ export async function checkpoint(cwd, args) {
31
32
  process.stderr.write('plumbbob: no active session. Run `plumbbob start "<title>"` first.\n');
32
33
  return 1;
33
34
  }
34
- // Before either staging path — `-A` below, and the plan commit's folder-scoped
35
+ // Before either staging path: `-A` below, and the plan commit's folder-scoped
35
36
  // add, which also leans on the excludes to skip the in-flight markers inside
36
37
  // the build folder.
37
38
  refreshExcludes(root);
@@ -43,10 +44,18 @@ export async function checkpoint(cwd, args) {
43
44
  process.stderr.write('plumbbob: no step to checkpoint — pass a number, or plan a step in intent.md first.\n');
44
45
  return 1;
45
46
  }
46
- // The approval latch runs before the check gate — cheap first. The tick may
47
+ // Checked before the latch and the check gate: a socket stdin can never
48
+ // deliver the requested body, so there is no point latching or running a
49
+ // ~55s check only to discover that afterward.
50
+ const bodyResult = readCommitBody(args);
51
+ if (!bodyResult.ok) {
52
+ process.stderr.write(bodyResult.message);
53
+ return 1;
54
+ }
55
+ // The approval latch runs before the check gate: cheap first. The tick may
47
56
  // not land without a human turn since the step was entered, a one-turn grant
48
57
  // the human typed, or a dormant ledger. A refusal is not an error: the
49
- // message is the pause affordance — exit 1 and hand the turn back.
58
+ // message is the pause affordance: exit 1 and hand the turn back.
50
59
  const latch = checkLatch(root, step);
51
60
  if (!latch.allow) {
52
61
  process.stderr.write(latch.message);
@@ -69,7 +78,7 @@ export async function checkpoint(cwd, args) {
69
78
  if (isDirty(root)) {
70
79
  stageAll(root);
71
80
  warnScopeDrift(root, step);
72
- const body = withMarker(`plumbbob step ${step}`, bodyArg(args) ?? fallbackBody(root, step));
81
+ const body = withMarker(`plumbbob step ${step}`, bodyResult.body ?? fallbackBody(root, step));
73
82
  sha = commit(root, messageArg(args) ?? subjectForStep(root, step), body);
74
83
  }
75
84
  else {
@@ -86,8 +95,8 @@ export async function checkpoint(cwd, args) {
86
95
  syncBuildLogState(root, activeBuild(root), AT_BOUNDARY);
87
96
  rmSync(seamPath(root), { force: true });
88
97
  rmSync(stepPath(root), { force: true });
89
- clearHandoff(root); // the agent-run handoff ledger is step-scoped — clear it with the markers.
90
- clearTick(root); // the step's entry stamp is spent — the next `build <n>` re-stamps.
98
+ clearHandoff(root); // the agent-run handoff ledger is step-scoped; clear it with the markers.
99
+ clearTick(root); // the step's entry stamp is spent; the next `build <n>` re-stamps.
91
100
  process.stdout.write(`plumbbob: step ${step} checkpointed — ${sha.slice(0, 9)}. Back at the boundary.\n`);
92
101
  return 0;
93
102
  }
@@ -97,33 +106,46 @@ export async function checkpoint(cwd, args) {
97
106
  *
98
107
  * Giving the plan its own commit keeps the first step's diff from absorbing the
99
108
  * scaffold, so `git log` reads baseline → plan → steps. No check gate (there is
100
- * no code work to verify yet), no intent flip, no step markers — the plan lives
109
+ * no code work to verify yet), no intent flip, no step markers: the plan lives
101
110
  * entirely in DESIGN. An optional `--body` (stdin heredoc) rides after the
102
111
  * `plumbbob plan` marker line; the folder is whitelisted artifact plane, so
103
112
  * there is no scope drift to warn about.
104
113
  */
105
114
  function checkpointPlan(root, args) {
106
115
  // The latch covers the plan commit too, keyed on the TICK that `start`
107
- // stamped: the plan pause is a real pause. No step number — a step-range
116
+ // stamped: the plan pause is a real pause. No step number: a step-range
108
117
  // grant does not speak to a plan.
109
118
  const latch = checkLatch(root, null);
110
119
  if (!latch.allow) {
111
120
  process.stderr.write(latch.message);
112
121
  return 1;
113
122
  }
114
- stagePath(root, buildFolder(root));
115
- const sha = commit(root, planSubject(root), withMarker('plumbbob plan', bodyArg(args) ?? undefined));
123
+ const bodyResult = readCommitBody(args);
124
+ if (!bodyResult.ok) {
125
+ process.stderr.write(bodyResult.message);
126
+ return 1;
127
+ }
128
+ // A repo that gitignores the sidecar (its `.gitignore`, not plumbbob's own
129
+ // info/exclude) makes this folder-scoped add impossible; stagePath skips it
130
+ // and reports false, and the plan still lands as a valid record-only commit:
131
+ // its body carries the plan, the files stay untracked, and `--allow-empty`
132
+ // (in commit) makes the empty diff legal. History still reads baseline → plan
133
+ // → steps.
134
+ const staged = stagePath(root, buildFolder(root));
135
+ const sha = commit(root, planSubject(root), withMarker('plumbbob plan', bodyResult.body ?? undefined));
116
136
  appendFileSync(checkpointsPath(root), `plan ${sha}\n`);
117
137
  // Landing the plan consumes `start`'s entry stamp: a later hand-built diff
118
138
  // (no `build <n>`) must find no stale TICK and stay guidance-governed.
119
139
  clearTick(root);
120
- process.stdout.write(`plumbbob: plan committed — ${sha.slice(0, 9)}. Baseline → plan → steps.\n`);
140
+ process.stdout.write(staged
141
+ ? `plumbbob: plan committed — ${sha.slice(0, 9)}. Baseline → plan → steps.\n`
142
+ : `plumbbob: plan committed (record-only — sidecar is gitignored; the plan rides the commit message, the files stay untracked) — ${sha.slice(0, 9)}. Baseline → plan → steps.\n`);
121
143
  return 0;
122
144
  }
123
145
  /**
124
146
  * Compose the plan commit's CLI-owned subject: `chore(<scope>): plan`.
125
147
  *
126
- * The scope resolves through the build-default fallback chain — a bare
148
+ * The scope resolves through the build-default fallback chain: a bare
127
149
  * `chore: plan` when none resolves. The `plumbbob plan` identifier rides the
128
150
  * body marker line, never the subject.
129
151
  */
@@ -134,7 +156,7 @@ function planSubject(root) {
134
156
  * Resolve the step being checkpointed, or null when none can be determined.
135
157
  *
136
158
  * Explicit arg > in-flight STEP file > first undone step in intent.md. A `-m`
137
- * value is a message, never a step — `checkpoint -m "2"` must not read as
159
+ * value is a message, never a step: `checkpoint -m "2"` must not read as
138
160
  * step 2.
139
161
  */
140
162
  function resolveStep(root, args) {
@@ -154,10 +176,10 @@ function resolveStep(root, args) {
154
176
  }
155
177
  }
156
178
  /**
157
- * Warn — never refuse — when the staged tree reaches beyond the step's seam.
179
+ * Warn (never refuse) when the staged tree reaches beyond the step's seam.
158
180
  *
159
181
  * Guidance, not a gate: the checkpoint captures the drift and says so. The seam
160
- * (the step's edit grant — exact paths or `dir/` prefixes) comes from the
182
+ * (the step's edit grant: exact paths or `dir/` prefixes) comes from the
161
183
  * in-flight SEAM file when a build is live, else the step's declared seam in
162
184
  * intent.md. Plumbbob's own artifact plane is whitelisted inside `scopeDrift`,
163
185
  * so the `[x]` flip and build-log line this very checkpoint stages never read
@@ -177,7 +199,7 @@ function warnScopeDrift(root, step) {
177
199
  *
178
200
  * The normalized SEAM file `build` wrote is authoritative while a build is
179
201
  * live; fall back to the step's declared seam parsed from intent.md. Empty when
180
- * neither resolves — the caller then skips the drift warning rather than
202
+ * neither resolves: the caller then skips the drift warning rather than
181
203
  * flagging the whole tree.
182
204
  */
183
205
  function seamTokens(root, step) {
@@ -191,14 +213,14 @@ function seamTokens(root, step) {
191
213
  }
192
214
  }
193
215
  catch {
194
- // no SEAM file — fall through to the declared seam.
216
+ // no SEAM file: fall through to the declared seam.
195
217
  }
196
218
  return seamForStep(root, step);
197
219
  }
198
220
  /**
199
221
  * Flip the step's checkbox in intent.md to `[x]`.
200
222
  *
201
- * Best-effort bookkeeping — the checkpoint SHA is the source of truth — but the
223
+ * Best-effort bookkeeping (the checkpoint SHA is the source of truth) but the
202
224
  * dashboard reads intent.md, so a swallowed failure here would make orientation
203
225
  * lie; the catch warns and asks for a hand flip instead.
204
226
  */
@@ -216,7 +238,7 @@ function flipIntent(root, step) {
216
238
  *
217
239
  * The build's history accrues at each checkpoint instead of being reconstructed
218
240
  * at finish; the step's title is lifted from intent.md when still present.
219
- * Best-effort: a missing or odd build-log never blocks a checkpoint — the
241
+ * Best-effort: a missing or odd build-log never blocks a checkpoint; the
220
242
  * `checkpoints` SHA is the source of truth.
221
243
  */
222
244
  function logCheckpoint(root, step, sha) {
@@ -236,7 +258,7 @@ function logCheckpoint(root, step, sha) {
236
258
  /**
237
259
  * The compact stats receipt riding the Log line, or null when nothing accrued.
238
260
  *
239
- * Only what happened is shown — a clean first-try step gets no suffix at all.
261
+ * Only what happened is shown: a clean first-try step gets no suffix at all.
240
262
  * Wall-clock needs both stamps: a hand-built step never ran `build <n>`, so it
241
263
  * has no startedAt.
242
264
  */
@@ -280,7 +302,7 @@ function subjectForStep(root, step) {
280
302
  * The build-default Conventional scope, or null for a bare subject.
281
303
  *
282
304
  * The `**Scope:**` header field in intent.md wins when authored and filled;
283
- * else the build slug with its date prefix stripped; else null — a build with
305
+ * else the build slug with its date prefix stripped; else null: a build with
284
306
  * neither field keeps producing bare subjects.
285
307
  */
286
308
  function buildDefaultScope(root) {
@@ -291,7 +313,7 @@ function buildDefaultScope(root) {
291
313
  }
292
314
  }
293
315
  catch {
294
- // no intent.md yet — fall through to the slug rung.
316
+ // no intent.md yet: fall through to the slug rung.
295
317
  }
296
318
  return buildScope(activeBuild(root));
297
319
  }
@@ -313,27 +335,6 @@ function messageArg(args) {
313
335
  const i = args.indexOf('-m');
314
336
  return i !== -1 && i + 1 < args.length ? (args[i + 1] ?? null) : null;
315
337
  }
316
- /**
317
- * Read the `--body` commit body from stdin, or null when absent or empty.
318
- *
319
- * The stdin heredoc lets a skill compose proportional prose the CLI never
320
- * could; null either way hands over to the deterministic fallback body. Reading
321
- * fd 0 blocks until EOF, which the heredoc supplies; a read error (no stdin
322
- * attached) degrades to the fallback, and an interactive TTY — which would
323
- * never send EOF — skips the read instead of hanging.
324
- */
325
- function bodyArg(args) {
326
- if (!args.includes('--body') || process.stdin.isTTY === true) {
327
- return null;
328
- }
329
- try {
330
- const raw = readFileSync(0, 'utf8').trimEnd();
331
- return raw.length > 0 ? raw : null;
332
- }
333
- catch {
334
- return null;
335
- }
336
- }
337
338
  /**
338
339
  * The deterministic fallback commit body: the step's done-when, its seam, and
339
340
  * the staged diffstat.