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,11 +1,11 @@
1
1
  // The agent contract. A user-authored agent is anything executable that speaks a
2
- // versioned JSON envelope — a JSON manifest on disk (`agent.json`), JSON on the
3
- // child's stdin, JSON on its stdout, prose on its stderr — so the doorway is
2
+ // versioned JSON envelope (a JSON manifest on disk (`agent.json`), JSON on the
3
+ // child's stdin, JSON on its stdout, prose on its stderr) so the doorway is
4
4
  // runtime-agnostic: any language, any framework, one subprocess boundary. This
5
5
  // module is the contract's validator, resolver, and runner: it type-narrows the
6
6
  // on-disk manifest and the output envelope a child returns (refusing a contract
7
7
  // major-version mismatch with an upgrade hint rather than guessing at a shape
8
- // that is only stable within a major — additions are a minor bump, removals or
8
+ // that is only stable within a major: additions are a minor bump, removals or
9
9
  // renames a major), it walks the agent tiers to resolve an agent by name, it
10
10
  // composes the step context a child reads on stdin, and it spawns the child.
11
11
  // The validators are pure; the resolver reads `agent.json` files off disk with
@@ -28,7 +28,7 @@ export const CONTRACT_VERSION = 1;
28
28
  * The three lifecycle slots an agent may bind to: `before` (context in),
29
29
  * `build` (the diff), `after` (advisory review).
30
30
  *
31
- * Exactly these, never a fourth — "a salient point in the middle" is judgment
31
+ * Exactly these, never a fourth; "a salient point in the middle" is judgment
32
32
  * the host model handles in prose, not a declarative slot.
33
33
  */
34
34
  export const SLOTS = ['before', 'build', 'after'];
@@ -122,7 +122,7 @@ export function parseEnvelope(raw) {
122
122
  * True when a string names one of the three slots.
123
123
  *
124
124
  * Exported for the `agent run` verb's `--mode` refusal: a mode the manifest
125
- * doesn't declare is refused loud — the user who typed it asked for that slot
125
+ * doesn't declare is refused loud; the user who typed it asked for that slot
126
126
  * specifically.
127
127
  */
128
128
  export function isSlot(value) {
@@ -169,7 +169,7 @@ function parseSlots(value) {
169
169
  /**
170
170
  * Narrow the envelope's optional `parked[]` (absent = none).
171
171
  *
172
- * Present, it must be an array of non-blank strings — each becomes a park line
172
+ * Present, it must be an array of non-blank strings: each becomes a park line
173
173
  * the CLI lands through the park verb. A malformed list is refused rather than
174
174
  * silently dropped: losing a parked concern is a quiet data loss. Returns the
175
175
  * trimmed lines or an error string.
@@ -219,13 +219,13 @@ function envelopeFail(error) {
219
219
  return { ok: false, error };
220
220
  }
221
221
  /**
222
- * The project agents tier — `.plumbbob/agents/` under the repo root.
222
+ * The project agents tier: `.plumbbob/agents/` under the repo root.
223
223
  */
224
224
  export function projectAgentsDir(root) {
225
225
  return join(root, '.plumbbob', 'agents');
226
226
  }
227
227
  /**
228
- * The personal agents tier — `~/.plumbbob/agents/`, the user's own library.
228
+ * The personal agents tier: `~/.plumbbob/agents/`, the user's own library.
229
229
  */
230
230
  export function personalAgentsDir(home) {
231
231
  return join(home, '.plumbbob', 'agents');
@@ -246,7 +246,7 @@ function agentsHome(home) {
246
246
  * A `flagPath` (from `--agent <path>`) points straight at an agent directory
247
247
  * and takes top priority. A tier "hits" when it holds an `agent.json`: a
248
248
  * malformed one there is surfaced as an error (project still shadows
249
- * personal), never silently skipped in favor of a different agent — an
249
+ * personal), never silently skipped in favor of a different agent; an
250
250
  * explicit ask fails loud. Returns the not-found error only when no tier holds
251
251
  * a manifest at all.
252
252
  */
@@ -273,7 +273,7 @@ export function resolveAgent(root, name, opts = {}) {
273
273
  /**
274
274
  * Every resolvable agent across both tiers, sorted by name.
275
275
  *
276
- * Project shadows personal: a name defined in both resolves — and lists — from
276
+ * Project shadows personal: a name defined in both resolves (and lists) from
277
277
  * project. Each entry carries its origin and resolution; a malformed manifest
278
278
  * lands as an errored resolution rather than crashing the walk.
279
279
  */
@@ -325,7 +325,7 @@ export function formatAgentList(listings) {
325
325
  * Returns null when the directory holds no manifest (a tier miss, so
326
326
  * resolution falls through), an errored resolution when the manifest is
327
327
  * present but unreadable/malformed, else the resolved agent. A missing file
328
- * (ENOENT throw) is the miss signal — one read, no TOCTOU race.
328
+ * (ENOENT throw) is the miss signal: one read, no TOCTOU race.
329
329
  */
330
330
  function loadAgentDir(dir, origin) {
331
331
  const file = join(dir, 'agent.json');
@@ -367,10 +367,10 @@ function agentDirNames(dir) {
367
367
  /**
368
368
  * Compose the StepContext for one step.
369
369
  *
370
- * The strict seam parse is the only refusal path — a missing step or
370
+ * The strict seam parse is the only refusal path: a missing step or
371
371
  * unparseable seam gates git behavior, so it fails loud; everything else is
372
372
  * best-effort and lands whatever it can, returning `warnings` (skipped-bullet
373
- * lines) for the caller to print on stderr rather than writing there itself —
373
+ * lines) for the caller to print on stderr rather than writing there itself;
374
374
  * this module stays pure, the verb does the IO.
375
375
  */
376
376
  export function composeStepContext(params) {
@@ -410,7 +410,7 @@ function skipWarnings(heading, skipped) {
410
410
  * Validate a parsed harness.json.
411
411
  *
412
412
  * Contract first (a major mismatch gets the same upgrade hint as the manifest
413
- * and envelope). Structure is strict — a `steps` that is not an object, a
413
+ * and envelope). Structure is strict: a `steps` that is not an object, a
414
414
  * non-numeric step key, or a step entry that is not an object is the author's
415
415
  * error and is refused loud. Slot *contents* stay lenient: a slot value may be
416
416
  * one name or a list, and blanks/non-strings drop rather than refuse, because
@@ -445,9 +445,9 @@ function parseHarness(raw) {
445
445
  /**
446
446
  * Read and validate a harness.json at `path`.
447
447
  *
448
- * The caller builds the path from the build folder — this module stays
448
+ * The caller builds the path from the build folder; this module stays
449
449
  * ignorant of the sidecar layout, taking the full path. Returns null when the
450
- * file is absent (a build with no bound agents — a clean no-op); an errored
450
+ * file is absent (a build with no bound agents: a clean no-op); an errored
451
451
  * parse when present-but-broken (invalid JSON or a malformed structure the
452
452
  * author must fix); else the validated bindings. Shared so every reader
453
453
  * (`agent run`, `status`) parses the harness the same way.
@@ -474,7 +474,7 @@ export function readHarnessFile(path) {
474
474
  *
475
475
  * Accepts a harness `defaults` block, a per-step entry, or the settings-level
476
476
  * defaults. A slot value may be a single agent name or a list; blanks and
477
- * non-strings drop. Only the three real slots are read — `note` and any
477
+ * non-strings drop. Only the three real slots are read; `note` and any
478
478
  * stranger key are ignored here. A slot present but naming no valid agent
479
479
  * still counts as bound-to-nothing (an explicit override to none), so callers
480
480
  * get clean replace semantics up the ladder.
@@ -493,7 +493,7 @@ export function parseSlotBindings(raw) {
493
493
  * The agents bound to one slot for one step, merged down the bindings ladder.
494
494
  *
495
495
  * A per-step slot entry overrides the harness `defaults`, which override the
496
- * settings-level defaults — the first level that names the slot wins, replace
496
+ * settings-level defaults: the first level that names the slot wins, replace
497
497
  * not append, the same most-specific-wins shape as the settings ladder. The
498
498
  * `--agent` flag and an explicit name sit above all of this, but the verb
499
499
  * takes the single-agent path for those and never reaches here. Returns []
@@ -511,7 +511,7 @@ export function resolveSlotAgents(params) {
511
511
  return [];
512
512
  }
513
513
  /**
514
- * Coerce a slot value — one agent name or a list — to a trimmed name list,
514
+ * Coerce a slot value (one agent name or a list) to a trimmed name list,
515
515
  * dropping blanks and non-strings.
516
516
  */
517
517
  function asNameList(value) {
@@ -519,7 +519,7 @@ function asNameList(value) {
519
519
  return items.filter((v) => typeof v === 'string' && v.trim().length > 0).map((v) => v.trim());
520
520
  }
521
521
  /**
522
- * True when a value is a plain JSON object — not null, not an array.
522
+ * True when a value is a plain JSON object: not null, not an array.
523
523
  */
524
524
  function isObject(value) {
525
525
  return typeof value === 'object' && value !== null && !Array.isArray(value);
@@ -532,13 +532,13 @@ function isObject(value) {
532
532
  * directory rides in `PLUMBBOB_AGENT_DIR` so a root-cwd agent can still reach
533
533
  * its files. The composed StepContext is delivered as JSON on stdin. Stdout is
534
534
  * piped and captured (the envelope); stderr is inherited so the child's prose
535
- * streams live to the terminal — production (narrating) never collides with
535
+ * streams live to the terminal: production (narrating) never collides with
536
536
  * consumption (one structured result at the pause). A SIGINT while the child
537
- * runs kills it and reports `interrupted` rather than orphaning it — the human
537
+ * runs kills it and reports `interrupted` rather than orphaning it; the human
538
538
  * is present, Ctrl-C works. A positive `timeoutSeconds` arms a kill timer
539
539
  * (0 = off; a timeout is the user's explicit opt-in). Async `spawn`, not
540
540
  * `spawnSync`, so the parent stays live to interrupt gracefully. Never
541
- * rejects — every failure mode is a resolved `AgentRunResult`.
541
+ * rejects: every failure mode is a resolved `AgentRunResult`.
542
542
  */
543
543
  export function runAgent(params) {
544
544
  return new Promise((resolve) => {
@@ -576,7 +576,7 @@ export function runAgent(params) {
576
576
  child.stdout.on('data', (chunk) => {
577
577
  stdout += chunk.toString();
578
578
  });
579
- // A shell that never launched (e.g. `sh` itself missing) — distinct from a
579
+ // A shell that never launched (for example `sh` itself missing): distinct from a
580
580
  // command that ran and exited non-zero, which arrives on `close`.
581
581
  child.on('error', (err) => {
582
582
  finish({ ok: false, reason: 'spawn', error: err.message });
@@ -599,9 +599,9 @@ export function runAgent(params) {
599
599
  /**
600
600
  * Parse the child's captured stdout into a validated envelope.
601
601
  *
602
- * JSON first (a non-JSON stdout is out of contract — the envelope must be one
602
+ * JSON first (a non-JSON stdout is out of contract: the envelope must be one
603
603
  * JSON object), then the envelope validator, which carries the
604
- * contract-mismatch hint. Kept private — `runAgent` is the only caller and the
604
+ * contract-mismatch hint. Kept private; `runAgent` is the only caller and the
605
605
  * verb reads the union, not this.
606
606
  */
607
607
  function parseChildEnvelope(stdout) {
@@ -1,14 +1,14 @@
1
- // Mutations to the active build-log.md — the human-facing ledger inside the
1
+ // Mutations to the active build-log.md: the human-facing ledger inside the
2
2
  // build's tracked `builds/<slug>/` folder. `park` appends to its Park list;
3
3
  // `checkpoint` appends to its Log so the build's history accrues line-by-line
4
4
  // *as it happens* rather than being reconstructed at finish. The `## Steps`
5
5
  // mirror and `**Current step:**` line are CLI-owned: `build`, `checkpoint`,
6
6
  // and `revert` re-render them from intent.md so the ledger's top half tracks
7
- // reality without a model turn. Pure string transforms — no fs, no classes;
7
+ // reality without a model turn. Pure string transforms: no fs, no classes;
8
8
  // callers own the IO so these stay trivially testable.
9
9
  /**
10
- * Append `line` after the last non-blank line of the `## <heading>` section —
11
- * i.e. just before the next `## ` heading or EOF.
10
+ * Append `line` after the last non-blank line of the `## <heading>` section:
11
+ * that is, just before the next `## ` heading or EOF.
12
12
  *
13
13
  * Returns null when the section is absent so the caller can report rather than
14
14
  * silently corrupt the doc.
@@ -33,9 +33,9 @@ export function appendToSection(content, heading, line) {
33
33
  return lines.join('\n');
34
34
  }
35
35
  /**
36
- * Replace the `**Current step:**` line with `**Current step:** <label>` —
37
- * `build` sets it to `<n> — <title>` when a step opens; `checkpoint`/`revert`
38
- * reset it to `none (at the boundary)`.
36
+ * Replace the `**Current step:**` line with `**Current step:** <label>`:
37
+ * `build` sets it to the step's number and title joined by an em dash when a
38
+ * step opens; `checkpoint`/`revert` reset it to `none (at the boundary)`.
39
39
  *
40
40
  * Returns null when the line is absent so the caller skips rather than
41
41
  * corrupting the doc.
@@ -50,14 +50,14 @@ export function setCurrentStep(content, label) {
50
50
  return lines.join('\n');
51
51
  }
52
52
  /**
53
- * Regenerate the `## Steps` mirror from intent.md's parsed steps — one
53
+ * Regenerate the `## Steps` mirror from intent.md's parsed steps: one
54
54
  * `- ☑/☐ N. <title>` line each, in order.
55
55
  *
56
56
  * Only the mirror's own `- ☑/☐` list lines are owned; the italic instructions
57
57
  * paragraph (and any other prose) is preserved, and exactly one blank line
58
58
  * separates it from the list. Returns null when the section is absent so the
59
59
  * caller skips rather than corrupting the doc. With no steps the mirror
60
- * renders empty — the plan is not authored yet — rather than inventing a
60
+ * renders empty (the plan is not authored yet) rather than inventing a
61
61
  * placeholder.
62
62
  */
63
63
  export function syncStepsSection(content, steps) {
@@ -92,9 +92,9 @@ function isMirrorLine(line) {
92
92
  * The Log line `checkpoint` writes for a landed step: dated, names the step
93
93
  * (its title when intent.md still carries it), and carries the short SHA.
94
94
  *
95
- * One line of the build's history — `/plumbbob:finish` reads these instead of
95
+ * One line of the build's history: `/plumbbob:finish` reads these instead of
96
96
  * re-narrating the build. `stats` is the optional compact receipt of what the
97
- * step cost (e.g. `2 red, 34m`) — null when nothing accrued, so a clean
97
+ * step cost (for example `2 red, 34m`): null when nothing accrued, so a clean
98
98
  * first-try step reads as a bare line.
99
99
  */
100
100
  export function checkpointLogLine(date, step, sha, title, stats = null) {
@@ -102,3 +102,13 @@ export function checkpointLogLine(date, step, sha, title, stats = null) {
102
102
  const titled = title !== null && title.length > 0 ? `${head} — ${title}` : head;
103
103
  return stats !== null && stats.length > 0 ? `${titled} (${stats})` : titled;
104
104
  }
105
+ /**
106
+ * The Log line `abandon` writes for a dropped-but-kept step: the first
107
+ * non-checkpoint event the log records. Dated, names the step (its title when
108
+ * intent.md still carries it), and states plainly that the work stayed in the
109
+ * tree, so the narrative never has a step go quiet mid-flight.
110
+ */
111
+ export function abandonLogLine(date, step, title) {
112
+ const head = `- ${date} — step ${step} abandoned · work kept in tree`;
113
+ return title !== null && title.length > 0 ? `${head} — ${title}` : head;
114
+ }
@@ -1,22 +1,22 @@
1
- // Keep the build-log's top half — the `**Current step:**` line and the `## Steps`
2
- // mirror — in sync with intent.md, the build's plan document. The build-log is the
1
+ // Keep the build-log's top half (the `**Current step:**` line and the `## Steps`
2
+ // mirror) in sync with intent.md, the build's plan document. The build-log is the
3
3
  // human-facing ledger in the build's tracked `builds/<slug>/` folder; its top half
4
4
  // is CLI-owned so it never lies: `build` sets the current-step label when a step
5
5
  // opens, `checkpoint` and `revert` reset it to the boundary, and all three
6
6
  // re-render the mirror from intent.md's parsed steps so it tracks reality without
7
7
  // a model turn. The write is best-effort (a missing or hand-edited build-log never
8
- // fails a verb — the checkpoints ledger and intent.md stay the source of truth),
8
+ // fails a verb: the checkpoints ledger and intent.md stay the source of truth),
9
9
  // matching how `checkpoint`'s Log append behaves.
10
10
  import { readFileSync, writeFileSync } from 'node:fs';
11
11
  import { buildLogPath, intentPath } from "./sidecar.js";
12
12
  import { parseSteps } from "./orient.js";
13
13
  import { setCurrentStep, syncStepsSection } from "./buildlog.js";
14
- /** The Current-step label for no step in flight — the loop is at the step boundary. */
14
+ /** The Current-step label for no step in flight: the loop is at the step boundary. */
15
15
  export const AT_BOUNDARY = 'none (at the boundary)';
16
16
  /**
17
- * The current-step label for an open step: `<n> — <title>`, or a bare `<n>`
18
- * when the step carries no title. `build` passes this; the boundary verbs pass
19
- * AT_BOUNDARY.
17
+ * The current-step label for an open step: the number and title joined by an
18
+ * em dash, or a bare `<n>` when the step carries no title. `build` passes
19
+ * this; the boundary verbs pass AT_BOUNDARY.
20
20
  */
21
21
  export function stepLabel(n, title) {
22
22
  return title !== null && title.trim().length > 0 ? `${n} — ${title.trim()}` : String(n);
@@ -26,7 +26,7 @@ export function stepLabel(n, title) {
26
26
  * `label` and re-render the `## Steps` mirror from intent.md.
27
27
  *
28
28
  * Each transform is skipped when its target is absent (null), and the whole
29
- * thing swallows any IO error — the ledger must never block the verb.
29
+ * thing swallows any IO error: the ledger must never block the verb.
30
30
  */
31
31
  export function syncBuildLogState(root, slug, label) {
32
32
  try {
package/dist/lib/check.js CHANGED
@@ -11,7 +11,7 @@
11
11
  // pointers, and an all-slots-skipped run REFUSES rather than green-lighting
12
12
  // a repo checkride can't see (a vacuous pass is not a pass).
13
13
  // Checkride's stream discipline holds: human progress goes to stderr. Exit 0 is
14
- // green, 1 is red, 2 means the harness itself broke — reported distinctly,
14
+ // green, 1 is red, 2 means the harness itself broke: reported distinctly,
15
15
  // because a misconfigured gate must not read as broken code (both still block).
16
16
  import { spawnSync } from 'node:child_process';
17
17
  import { runChecks, runDoctor } from 'checkride';
@@ -21,7 +21,7 @@ import { resolveString } from "./settings.js";
21
21
  * Probe whether the check gate would have anything to run in `root`.
22
22
  *
23
23
  * A configured `check` command answers yes on its own; otherwise checkride's
24
- * doctor detection runs in-process — the same pass `plumbbob doctor` reports
24
+ * doctor detection runs in-process: the same pass `plumbbob doctor` reports
25
25
  * as a table. `start` reads this to surface "nothing to check" at plan time,
26
26
  * while the human is still deciding, instead of at the first refused
27
27
  * checkpoint.
@@ -37,19 +37,19 @@ export async function detectGate(root) {
37
37
  }
38
38
  catch {
39
39
  // A broken checkride harness is a different problem with its own reporters
40
- // (doctor's ✗ row, runCheck's exit 2) — the plan-time probe stays quiet
40
+ // (doctor's ✗ row, runCheck's exit 2); the plan-time probe stays quiet
41
41
  // rather than mislabeling it "nothing to check".
42
42
  return { configured: null, detected: true };
43
43
  }
44
44
  }
45
45
  /**
46
46
  * Checkride's always-on repo checks: the adapters that resolve with no tool
47
- * config of their own — the built-in `links` slot, the pnpm-audit security
47
+ * config of their own: the built-in `links` slot, the pnpm-audit security
48
48
  * scan, the publint/attw package-shape probes, and the pack/smoke/snippets
49
49
  * publish-bundle slots. Several are named for their shape rather than because
50
50
  * a bare directory reports them: `publint`/`attw` resolve off a devDependency,
51
- * `build` off a `scripts.build` entry — which says the package can be built,
52
- * not that checkride can see the code that goes into it — and `snippets`
51
+ * `build` off a `scripts.build` entry (which says the package can be built,
52
+ * not that checkride can see the code that goes into it), and `snippets`
53
53
  * carries two adapters since checkride 0.10.2, so the dist-facing
54
54
  * `snippets-dist` is named beside it (this list matches ADAPTER names, not
55
55
  * slot names). Every one of them but `links` is an opt-in slot, so a default
@@ -72,7 +72,7 @@ const ALWAYS_ON_ADAPTERS = [
72
72
  ];
73
73
  /**
74
74
  * The detection rule, shared by `start`'s probe and doctor's callout: some
75
- * tool slot beyond the always-on family has an adapter — i.e. checkride can
75
+ * tool slot beyond the always-on family has an adapter: that is, checkride can
76
76
  * see the CODE (a tsconfig, a test runner, a linter…), not just the repo.
77
77
  */
78
78
  export function gateDetectsTools(checks) {
@@ -90,7 +90,7 @@ export function gateDetectsTools(checks) {
90
90
  */
91
91
  export async function runCheck(root, flags = {}, commandFlag) {
92
92
  if (gateIsRunningFor(root)) {
93
- // This repo re-entering its OWN gate is never a slow gate — it is a
93
+ // This repo re-entering its OWN gate is never a slow gate: it is a
94
94
  // recursion whose next generation forks wider than this one. Exit 2 (the
95
95
  // harness broke), not 1: nothing has been learned about the code. A gate on
96
96
  // a DIFFERENT root is ordinary nested work (the test suite gating its
@@ -105,7 +105,7 @@ export async function runCheck(root, flags = {}, commandFlag) {
105
105
  }
106
106
  /**
107
107
  * The spawn override: stream the configured command's own output to the
108
- * terminal and return its exit code — the command is trusted verbatim, no
108
+ * terminal and return its exit code: the command is trusted verbatim, no
109
109
  * interpretation.
110
110
  */
111
111
  function runCommand(root, command, flags) {
@@ -133,7 +133,7 @@ async function runCheckride(root, flags) {
133
133
  include: flags.include !== undefined ? [...flags.include] : null,
134
134
  // Checkride's own vacuous-green refusal (0.10.2): zero checks executed is
135
135
  // an error, not a pass. It overlaps the refusal below and never reaches
136
- // the human — our predicate is the broader one (it also catches a
136
+ // the human: our predicate is the broader one (it also catches a
137
137
  // links-only run, where a check DID execute) and fires first, so
138
138
  // plumbbob's message and its exit 1 are what a reader sees. Kept anyway:
139
139
  // it is the contract checkride asks a gate to run under, so a future
@@ -145,7 +145,7 @@ async function runCheckride(root, flags) {
145
145
  }
146
146
  catch (err) {
147
147
  // The harness broke, not the code: a malformed checkride.config.json or
148
- // the like. Checkride's own CLI maps this to exit 2; so do we — a
148
+ // the like. Checkride's own CLI maps this to exit 2; so do we: a
149
149
  // misconfigured gate must report distinctly from red.
150
150
  const message = err instanceof Error ? err.message : String(err);
151
151
  process.stderr.write(`plumbbob: the check gate itself failed — ${message}\n` +
@@ -156,7 +156,7 @@ async function runCheckride(root, flags) {
156
156
  // Zero-config checkride skips slots with no detected tool; a repo it can't
157
157
  // see must refuse, not vacuously green-light the checkpoint. The built-in
158
158
  // `links` slot is always-on (empty detect list), so it alone proves
159
- // nothing about the code — a links-only run is still vacuous.
159
+ // nothing about the code: a links-only run is still vacuous.
160
160
  process.stderr.write('plumbbob: checkride found nothing to check in this repo — refusing to call that green.\n' +
161
161
  ' Add tool configs (tsconfig, vitest, …) or a checkride.config.json custom check,\n' +
162
162
  ' or set the "check" key in .plumbbob/settings.json to gate through another command.\n');
@@ -171,7 +171,7 @@ async function runCheckride(root, flags) {
171
171
  * Name the failing slots and where their raw diagnostics landed, so the agent
172
172
  * (verify) reads the tool's own JSON instead of scraping scrollback.
173
173
  *
174
- * The summary is the canonical pointer — `output_file` is the adapter's
174
+ * The summary is the canonical pointer: `output_file` is the adapter's
175
175
  * preferred name, but non-JSON output falls back to `.check/<slot>.stdout.txt`.
176
176
  */
177
177
  function reportFailingSlots(summary) {
@@ -184,7 +184,7 @@ function reportFailingSlots(summary) {
184
184
  process.stderr.write(`plumbbob: failing slots:\n${lines.join('\n')}\n Full report: .check/summary.json\n`);
185
185
  }
186
186
  /**
187
- * True when any narrowing flag was passed — the trigger for the spawn path's
187
+ * True when any narrowing flag was passed: the trigger for the spawn path's
188
188
  * flags-are-ignored warning.
189
189
  */
190
190
  function hasFlags(flags) {
@@ -0,0 +1,72 @@
1
+ // The `--body` stdin reader shared by `checkpoint` and `finish`: both verbs
2
+ // take a commit body on fd 0 via a single-quoted heredoc (`--body <<'BODY'`),
3
+ // never as an argument value. `evaluateCommitBody` is the pure decision over
4
+ // an already-known fd-0 shape, so every shape is a direct unit test with no
5
+ // fd tricks; `readCommitBody` gathers the real one: the mirror of latch.ts's
6
+ // evaluateLatch/checkLatch split.
7
+ //
8
+ // Four fd-0 shapes reach this in practice: a heredoc is a regular file, a
9
+ // pipe is a FIFO, an interactive terminal is a TTY, and an agent harness
10
+ // hands the CLI a socket. The first three already degrade safely: a TTY
11
+ // skips the read (it would never send EOF), a regular file/FIFO/`/dev/null`
12
+ // read to EOF and fall back to the caller's own default body when empty. A
13
+ // socket also never sends EOF on its own, so the same blind `readFileSync(0)`
14
+ // blocks forever and, worse, drops the requested body silently. Refuse
15
+ // instead: a silently dropped body reads as success.
16
+ import { fstatSync, readFileSync } from 'node:fs';
17
+ /**
18
+ * The refusal shown when `--body` is asked to read a socket: names the form
19
+ * that works so the retry succeeds instead of guessing again.
20
+ */
21
+ export const SOCKET_REFUSAL = "plumbbob: --body refuses — stdin is a socket, and a socket never sends EOF the way a heredoc does, so the read would block forever and the body would be lost. Redirect it in with the `--body <<'BODY'` … `BODY` heredoc form instead.\n";
22
+ /**
23
+ * Decide the `--body` outcome from an already-known fd-0 shape.
24
+ *
25
+ * Absent flag or a TTY both mean "don't read": the caller's fallback body
26
+ * applies. `other` covers a heredoc (regular file), a pipe (FIFO), and
27
+ * `/dev/null` (character device) alike: each is read to EOF, and an empty or
28
+ * failed read degrades to the same fallback an absent `--body` would use.
29
+ */
30
+ export function evaluateCommitBody(hasBodyFlag, shape, read) {
31
+ if (!hasBodyFlag || shape === 'tty') {
32
+ return { ok: true, body: null };
33
+ }
34
+ if (shape === 'socket') {
35
+ return { ok: false, message: SOCKET_REFUSAL };
36
+ }
37
+ try {
38
+ const raw = read().trimEnd();
39
+ return { ok: true, body: raw.length > 0 ? raw : null };
40
+ }
41
+ catch {
42
+ return { ok: true, body: null };
43
+ }
44
+ }
45
+ /**
46
+ * Read the `--body` commit body from the real fd 0, or refuse when it can
47
+ * never deliver one.
48
+ *
49
+ * `checkpoint` and `finish` both call this where each used to call its own
50
+ * copy of `bodyArg`: one implementation, one refusal string, so the two
51
+ * verbs cannot drift apart on the wording.
52
+ */
53
+ export function readCommitBody(args) {
54
+ return evaluateCommitBody(args.includes('--body'), stdinShape(), () => readFileSync(0, 'utf8'));
55
+ }
56
+ /**
57
+ * The fd-0 shape the guard cares about: a TTY skips the read (never sends
58
+ * EOF), a socket refuses (the same problem, but silent), everything else is
59
+ * read as before. An unreadable fd (no stdin attached at all) is treated the
60
+ * same as any other shape; the read attempt that follows fails safely.
61
+ */
62
+ function stdinShape() {
63
+ if (process.stdin.isTTY === true) {
64
+ return 'tty';
65
+ }
66
+ try {
67
+ return fstatSync(0).isSocket() ? 'socket' : 'other';
68
+ }
69
+ catch {
70
+ return 'other';
71
+ }
72
+ }
@@ -1,8 +1,8 @@
1
1
  // Conventional-Commit subject assembly. Plumbbob's commit subjects read as
2
- // `type(scope): description` — real feat/fix/chore types and a real parenthesised
3
- // scope — so `git log` speaks the same grammar as the rest of the branch. The
2
+ // `type(scope): description` (real feat/fix/chore types and a real parenthesised
3
+ // scope) so `git log` speaks the same grammar as the rest of the branch. The
4
4
  // `plumbbob`/`step N` identifiers ride a marker line at the head of the body,
5
- // which `withMarker` prepends — so `git log --grep plumbbob` finds every plumbbob
5
+ // which `withMarker` prepends, so `git log --grep plumbbob` finds every plumbbob
6
6
  // commit. Functional and procedural, node builtins only, no gate: these only
7
7
  // shape a string the CLI is about to commit.
8
8
  /**
@@ -10,7 +10,7 @@
10
10
  * prefix on a step title.
11
11
  *
12
12
  * Anchoring on this closed set keeps a prose title like `Note: rewire the
13
- * cache` from being misread as a `Note` type — only these words, immediately
13
+ * cache` from being misread as a `Note` type; only these words, immediately
14
14
  * followed by an optional `(scope)`, an optional `!`, and a colon, count as an
15
15
  * author-supplied prefix.
16
16
  */
@@ -23,7 +23,7 @@ const PREFIX = new RegExp(`^(${TYPES.join('|')})(?:\\(([^)]+)\\))?(!)?:\\s+(.+)$
23
23
  *
24
24
  * `feat(escape-hatch): add the flag` → { type:'feat', scope:'escape-hatch',
25
25
  * breaking:false, description:'add the flag' }. A title with no recognised
26
- * prefix comes back whole as the description with a null type/scope — the
26
+ * prefix comes back whole as the description with a null type/scope; the
27
27
  * caller then defaults the type and fills the scope from the build.
28
28
  */
29
29
  export function parseConventionalTitle(title) {
@@ -46,11 +46,11 @@ export function conventionalSubject(parts) {
46
46
  /**
47
47
  * A Conventional subject from a step title, filling gaps from the build.
48
48
  *
49
- * An author-written prefix (`fix(parser): …`) is honoured verbatim — its type,
49
+ * An author-written prefix (`fix(parser): …`) is honoured verbatim: its type,
50
50
  * scope, breaking marker, and exact wording all win. A bare prose title
51
51
  * defaults to `defaultType` and the build `scope`, and gets its sentence-case
52
52
  * opener de-capitalised (`Add the flag` → `add the flag`) to match
53
- * Conventional style — while an all-caps acronym start (`API`, `OAuth`) is
53
+ * Conventional style, while an all-caps acronym start (`API`, `OAuth`) is
54
54
  * left alone.
55
55
  */
56
56
  export function subjectFromTitle(title, defaultType, scope) {