plumbbob 0.8.18 → 0.9.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 (47) hide show
  1. package/.claude-plugin/plugin.json +1 -1
  2. package/README.md +50 -44
  3. package/dist/cli-core.js +35 -15
  4. package/dist/lib/agents.js +230 -112
  5. package/dist/lib/buildlog.js +50 -30
  6. package/dist/lib/buildlogsync.js +21 -12
  7. package/dist/lib/check.js +69 -35
  8. package/dist/lib/commitmsg.js +53 -32
  9. package/dist/lib/git.js +86 -31
  10. package/dist/lib/intent.js +91 -31
  11. package/dist/lib/latch.js +70 -39
  12. package/dist/lib/orient.js +78 -39
  13. package/dist/lib/plugins.js +11 -8
  14. package/dist/lib/settings.js +65 -32
  15. package/dist/lib/sidecar.js +320 -150
  16. package/dist/lib/templates.js +17 -6
  17. package/dist/verbs/agent.js +116 -55
  18. package/dist/verbs/build.js +27 -12
  19. package/dist/verbs/check.js +33 -7
  20. package/dist/verbs/checkpoint.js +153 -78
  21. package/dist/verbs/doctor.js +192 -95
  22. package/dist/verbs/finish.js +86 -45
  23. package/dist/verbs/handoff.js +56 -20
  24. package/dist/verbs/init.js +37 -12
  25. package/dist/verbs/park.js +14 -3
  26. package/dist/verbs/revert.js +64 -24
  27. package/dist/verbs/spike.js +89 -30
  28. package/dist/verbs/start.js +72 -38
  29. package/dist/verbs/status.js +44 -17
  30. package/dist/verbs/turn.js +88 -45
  31. package/dist/verbs/use.js +20 -9
  32. package/package.json +1 -1
  33. package/skills/{pb-build → build}/SKILL.md +25 -25
  34. package/skills/{pb-doctor → doctor}/SKILL.md +1 -1
  35. package/skills/{pb-finish → finish}/SKILL.md +3 -3
  36. package/skills/{pb-harvest → harvest}/SKILL.md +4 -4
  37. package/skills/{pb-park → park}/SKILL.md +2 -2
  38. package/skills/{pb-plan → plan}/SKILL.md +11 -11
  39. package/skills/{pb-refine → refine}/SKILL.md +7 -7
  40. package/skills/{pb-revert → revert}/SKILL.md +2 -2
  41. package/skills/{pb-spike → spike}/SKILL.md +4 -4
  42. package/skills/{pb-status → status}/SKILL.md +3 -3
  43. package/skills/{pb-step → step}/SKILL.md +9 -9
  44. package/skills/{pb-verify → verify}/SKILL.md +8 -8
  45. package/templates/build-log.md +7 -7
  46. package/templates/intent.md +8 -8
  47. package/templates/spike-report.md +1 -1
@@ -1,15 +1,26 @@
1
1
  // Scaffold templates live at `<pkg-root>/templates/*.md` and carry `{{PLACEHOLDER}}`
2
- // tokens the verbs fill in. `start` stamps intent.md / build-log.md; `spike` stamps
3
- // spike-report.md. Shared here so both resolve the template dir the same way (relative
4
- // to the compiled module URL) and substitute with one mechanism. Pure/procedural,
5
- // node-builtins-only (C1/C2).
2
+ // tokens the verbs fill in: `start` stamps intent.md and build-log.md, `spike` stamps
3
+ // spike-report.md. Shared here so every verb resolves the template dir the same way
4
+ // (relative to the compiled module URL) and substitutes with one mechanism. Pure and
5
+ // procedural, node builtins only.
6
6
  import { fileURLToPath } from 'node:url';
7
7
  import { readFileSync } from 'node:fs';
8
+ /**
9
+ * Read a template by filename from the package's `templates/` directory.
10
+ *
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,
13
+ * never with the consumer's repo.
14
+ */
8
15
  export function readTemplate(name) {
9
16
  return readFileSync(fileURLToPath(new URL(`../../templates/${name}`, import.meta.url)), 'utf8');
10
17
  }
11
- // Replace every `{{KEY}}` with its value. Generic over the placeholder set so a new
12
- // template can add tokens (spike-report's `{{VIA}}`/`{{DATE}}`) without touching this.
18
+ /**
19
+ * Replace every `{{KEY}}` in the template with its value from `subs`.
20
+ *
21
+ * Generic over the placeholder set so a new template can add tokens
22
+ * (spike-report's `{{VIA}}`/`{{DATE}}`) without touching this.
23
+ */
13
24
  export function stampTemplate(template, subs) {
14
25
  let out = template;
15
26
  for (const [key, value] of Object.entries(subs)) {
@@ -1,12 +1,17 @@
1
- // `plumbbob agent <subcommand>` — the doorway to user-authored agents (D39/D41).
2
- // `agent list` walks the two tiers and prints each resolvable agent. `agent run
3
- // <name> [--step N] [--mode before|build|after]` (D60) composes the StepContext,
1
+ // `plumbbob agent <subcommand>` — the doorway to user-authored agents: anything
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
6
+ // <name> [--step N] [--mode before|build|after]` composes the StepContext,
4
7
  // spawns the manifest command, streams its stderr live, captures and validates
5
8
  // the child's envelope, re-emits it on this verb's own stdout (machine) with the
6
- // human summary on stderr (D46/D47), lands `parked[]` through the build-log (D44),
7
- // and appends the envelope to the step-scoped handoff ledger (D47). There is no
8
- // code path here to checkpoint, flip a step, or chain agents — the identity
9
- // invariant (C6) holds by construction. A thin read-write shell: resolution,
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,
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,
10
15
  // composition, and spawn mechanics live in lib/agents.ts.
11
16
  import { readFileSync, writeFileSync } from 'node:fs';
12
17
  import { join } from 'node:path';
@@ -15,6 +20,9 @@ import { SLOTS, composeStepContext, formatAgentList, isSlot, listAgents, parseSl
15
20
  import { appendToSection } from "../lib/buildlog.js";
16
21
  import { resolveBoolean, resolveNumber, resolveRecord } from "../lib/settings.js";
17
22
  import { appendHandoff, buildFolder, buildLogPath, hasSession, intentPath, resolveBuild, stepPath } from "../lib/sidecar.js";
23
+ /**
24
+ * Dispatch `plumbbob agent list|run`, refusing an unknown subcommand with a hint.
25
+ */
18
26
  export async function agent(cwd, args = []) {
19
27
  const [sub, ...rest] = args;
20
28
  if (sub === 'list')
@@ -26,6 +34,9 @@ export async function agent(cwd, args = []) {
26
34
  process.stderr.write(`${message}\n`);
27
35
  return 1;
28
36
  }
37
+ /**
38
+ * Print every resolvable agent across the project and personal tiers.
39
+ */
29
40
  function list(cwd, _args) {
30
41
  const root = findRepoRoot(cwd);
31
42
  if (root === null) {
@@ -35,10 +46,15 @@ function list(cwd, _args) {
35
46
  process.stdout.write(`${formatAgentList(listAgents(root))}\n`);
36
47
  return 0;
37
48
  }
38
- // `agent run`: with a name (or `--agent` flag) run exactly that agent, failing
39
- // loud on a miss (D54); with no name run the step's harness-bound agents for the
40
- // requested slot (D42). Either way this composes the StepContext, spawns, and
41
- // applies side effects — never advancing the loop.
49
+ /**
50
+ * Run one named agent, or a slot's harness-bound agents, against the step in flight.
51
+ *
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
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.
57
+ */
42
58
  async function run(cwd, args) {
43
59
  const root = findRepoRoot(cwd);
44
60
  if (root === null || !hasSession(root)) {
@@ -62,8 +78,8 @@ async function run(cwd, args) {
62
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');
63
79
  return 1;
64
80
  }
65
- // Explicit ask (a name, above the bindings — D57): run exactly it, fail loud on
66
- // a miss. No name: resolve the harness bindings for the slot and run them.
81
+ // An explicit name outranks every binding: run exactly it, fail loud on a
82
+ // miss. No name: resolve the harness bindings for the slot and run them.
67
83
  if (parsed.name !== undefined) {
68
84
  return runOne(root, slug, step, {
69
85
  name: parsed.name,
@@ -74,6 +90,16 @@ async function run(cwd, args) {
74
90
  }
75
91
  return runBound(root, slug, step, parsed.mode);
76
92
  }
93
+ /**
94
+ * One agent's full run: resolve it, pick its slot, compose the StepContext, spawn, report.
95
+ *
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
98
+ * configuration the loop must survive without; an explicit ask
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
101
+ * softening covers a *missing* agent, never a broken one.
102
+ */
77
103
  async function runOne(root, slug, step, spec) {
78
104
  const resolution = resolveAgent(root, spec.name, spec.flagPath !== undefined ? { flagPath: spec.flagPath } : {});
79
105
  if (!resolution.ok) {
@@ -99,15 +125,18 @@ async function runOne(root, slug, step, spec) {
99
125
  step,
100
126
  mode,
101
127
  settings: {
102
- // The frozen envelope still reports settings `auto` to agents (doorway freeze —
103
- // no envelope changes). Note it is no longer a checkpoint grant (D67): the latch
104
- // stopped reading it, so a `true` here informs an agent, it does not self-approve.
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
130
+ // file, so a standing `auto` cannot be a self-approval grant; approval
131
+ // comes only from what the human literally typed. A `true` here informs
132
+ // an agent, it does not self-approve.
105
133
  auto: resolveBoolean(root, 'auto', false),
106
134
  agentTimeout: resolveNumber(root, 'agentTimeout', 0),
107
- // Hand this agent its own config block over the frozen envelope `settings`
108
- // field (D5/D6): settings.json → agentConfig[name], with the local overlay
109
- // shadowing the project rung whole (D7 — no deep merge), and {} when neither
110
- // defines it. No new envelope field, no new verb — the config just rides here.
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
137
+ // rides here. Resolution: settings.json's agentConfig[name], with the
138
+ // untracked personal overlay (settings.local.json) replacing the project
139
+ // entry whole — no deep merge — and {} when neither defines it.
111
140
  agent: resolveRecord(root, 'agentConfig')[spec.name] ?? {},
112
141
  },
113
142
  });
@@ -127,11 +156,16 @@ async function runOne(root, slug, step, spec) {
127
156
  });
128
157
  return report(root, slug, spec.name, mode, step, result);
129
158
  }
130
- // No name given: run the harness-bound agents for the requested slot (D42). The
131
- // bindings merge the ladder — per-step over harness defaults over settings-level
132
- // defaults (D57). A missing bound agent degrades to a warning (D54); an absent
133
- // harness, or one that binds nothing to this slot, is a clean no-op. Each bound
134
- // agent runs in turn; the batch exits 1 if any agent that actually ran failed.
159
+ /**
160
+ * No name given: run the agents the build's harness.json binds to the requested slot.
161
+ *
162
+ * Bindings merge as a ladder — the per-step harness entry beats the harness
163
+ * `defaults`, which beat the settings-level `agents` key; the first level that
164
+ * names the slot wins, replace not append. A missing bound agent degrades to a
165
+ * warning; an absent harness, or one that binds nothing to this slot, is a
166
+ * clean no-op. Each bound agent runs in turn; the batch exits 1 if any agent
167
+ * that actually ran failed.
168
+ */
135
169
  async function runBound(root, slug, step, modeFlag) {
136
170
  if (modeFlag === undefined) {
137
171
  process.stderr.write('plumbbob: agent run needs an agent name, or --mode <slot> to run the step\'s bound agents.\n');
@@ -165,18 +199,25 @@ async function runBound(root, slug, step, modeFlag) {
165
199
  }
166
200
  return worst;
167
201
  }
168
- // Emit the loud error or the soft warning by whether this run was an explicit ask
169
- // (name/flag) or an ambient harness binding (D54/D54), and return the matching
170
- // code — a hard miss stops with 1, a degraded one warns and returns 0 so a batch
171
- // of bound agents carries on.
202
+ /**
203
+ * Emit the loud error or the soft warning by whether this run was an explicit
204
+ * ask (name/flag) or an ambient harness binding, and return the matching code.
205
+ *
206
+ * A hard miss stops with 1; a degraded one warns and returns 0 so a batch of
207
+ * bound agents carries on.
208
+ */
172
209
  function degrade(ambient, hard, soft) {
173
210
  process.stderr.write(`${ambient ? soft : hard}\n`);
174
211
  return ambient ? 0 : 1;
175
212
  }
176
- // Split `run`'s argv (with `--build` already stripped by resolveBuild) into the
177
- // agent name and the value flags. A value flag missing its value, or `--step`
178
- // given a non-number, is a loud error (returned as a string) rather than a silent
179
- // default. Unknown `--flags` are ignored — the point is the named agent.
213
+ /**
214
+ * Split `run`'s argv (with `--build` already stripped by resolveBuild) into the
215
+ * agent name and the value flags.
216
+ *
217
+ * A value flag missing its value, or `--step` given a non-number, is a loud
218
+ * error (returned as a string) rather than a silent default. Unknown `--flags`
219
+ * are ignored — the point is the named agent.
220
+ */
180
221
  function parseRunArgs(args) {
181
222
  const positionals = [];
182
223
  let step;
@@ -209,11 +250,15 @@ function parseRunArgs(args) {
209
250
  }
210
251
  return { name: positionals[0], step, mode, flagPath };
211
252
  }
212
- // Resolve the slot the agent runs in. An explicit `--mode` must name a real slot
213
- // AND one the manifest declares — an undeclared slot is refused loud (D54). With
214
- // no `--mode`, a single-slot agent uses its only slot; a multi-slot agent must be
215
- // told which one (step 5's harness bindings pick the slot from the lifecycle
216
- // point; a bare `run` cannot guess).
253
+ /**
254
+ * Resolve the slot the agent runs in.
255
+ *
256
+ * An explicit `--mode` must name a real slot AND one the manifest declares —
257
+ * an undeclared slot is refused loud, because the user asked for that exact
258
+ * run. With no `--mode`, a single-slot agent uses its only slot; a multi-slot
259
+ * agent must be told which one (harness bindings pick the slot from the
260
+ * lifecycle point; a bare `run` cannot guess).
261
+ */
217
262
  function resolveMode(flag, manifest) {
218
263
  if (flag !== undefined) {
219
264
  if (!isSlot(flag))
@@ -233,13 +278,16 @@ function resolveMode(flag, manifest) {
233
278
  error: `plumbbob: agent "${manifest.name}" declares multiple slots (${manifest.slots.join(', ')}) — pass --mode <slot>.`,
234
279
  };
235
280
  }
236
- // Map the run outcome to reporting and side effects (D46). A failed run — non-zero
237
- // exit, out-of-contract stdout, timeout, interrupt, or a shell that never
238
- // started — reports on stderr and stops with NO side effects (the envelope of a
239
- // failed child is not authoritative). A clean run lands `parked[]` (D44), records
240
- // the envelope in the handoff ledger (D47), prints the human summary on stderr,
241
- // and re-emits the machine envelope on stdout — nothing else, ever (the stream
242
- // discipline: stdout carries the envelope alone).
281
+ /**
282
+ * Map the run outcome to reporting and side effects.
283
+ *
284
+ * A failed run — non-zero exit, out-of-contract stdout, timeout, interrupt, or a
285
+ * shell that never started — reports on stderr and stops with NO side effects:
286
+ * the envelope of a failed child is not authoritative. A clean run lands
287
+ * `parked[]`, records the envelope in the handoff ledger, prints the human
288
+ * summary on stderr, and re-emits the machine envelope on stdout — nothing else,
289
+ * ever (the stream discipline: stdout carries the envelope alone).
290
+ */
243
291
  function report(root, slug, name, mode, step, result) {
244
292
  if (!result.ok) {
245
293
  process.stderr.write(`${failureLine(name, result)}\n`);
@@ -252,6 +300,9 @@ function report(root, slug, name, mode, step, result) {
252
300
  process.stdout.write(`${JSON.stringify(envelope)}\n`);
253
301
  return 0;
254
302
  }
303
+ /**
304
+ * One stderr line naming exactly how a failed run failed.
305
+ */
255
306
  function failureLine(name, result) {
256
307
  switch (result.reason) {
257
308
  case 'exit':
@@ -266,9 +317,13 @@ function failureLine(name, result) {
266
317
  return `plumbbob: could not spawn agent "${name}" — ${result.error}`;
267
318
  }
268
319
  }
269
- // Land each parked concern through the build-log's Park list (D44 — the agent
270
- // never writes .plumbbob/ itself). A build-log with no "## Park list" section, or
271
- // no build-log at all, warns once rather than losing the lines silently.
320
+ /**
321
+ * Land each parked concern through the build-log's Park list.
322
+ *
323
+ * The agent never writes .plumbbob/ itself — parked lines only reach the
324
+ * build-log through this verb. A build-log with no "## Park list" section, or no
325
+ * build-log at all, warns once rather than losing the lines silently.
326
+ */
272
327
  function applyParked(root, slug, parked) {
273
328
  if (parked.length === 0)
274
329
  return;
@@ -292,10 +347,13 @@ function applyParked(root, slug, parked) {
292
347
  }
293
348
  writeFileSync(path, content);
294
349
  }
295
- // The human-facing summary on stderr (D47): a headline plus, for a halt, the
296
- // route the skills name (D52) — `blocked` unblocks and re-runs, `drift` sends the
297
- // plan to /pb-refine. The machine envelope on stdout carries the same status for
298
- // the calling skill; this is the terminal read.
350
+ /**
351
+ * The human-facing summary printed on stderr.
352
+ *
353
+ * A headline plus, for a halt, the route out — `blocked` unblocks and re-runs,
354
+ * `drift` sends the plan to /plumbbob:refine. The machine envelope on stdout carries
355
+ * the same status for the calling skill; this stderr copy is the terminal read.
356
+ */
299
357
  function humanSummary(name, mode, envelope) {
300
358
  const head = `plumbbob: agent "${name}" (${mode}) — ${envelope.status}: ${envelope.summary}\n`;
301
359
  const notes = envelope.notes.length > 0 ? ` notes: ${envelope.notes}\n` : '';
@@ -303,12 +361,15 @@ function humanSummary(name, mode, envelope) {
303
361
  return `${head} blocked — the agent couldn't finish; unblock and re-run.\n${notes}`;
304
362
  }
305
363
  if (envelope.status === 'drift') {
306
- return `${head} drift — the plan no longer matches reality; /pb-refine before continuing.\n${notes}`;
364
+ return `${head} drift — the plan no longer matches reality; /plumbbob:refine before continuing.\n${notes}`;
307
365
  }
308
366
  return head;
309
367
  }
310
- // The in-flight step for this build (the STEP marker `build` wrote), or null when
311
- // none is set — the caller then requires an explicit `--step`.
368
+ /**
369
+ * The in-flight step for this build (the STEP marker `build` wrote), or null.
370
+ *
371
+ * With no marker set, the caller requires an explicit `--step`.
372
+ */
312
373
  function readStep(root, slug) {
313
374
  try {
314
375
  const raw = readFileSync(stepPath(root, slug), 'utf8').trim();
@@ -1,12 +1,23 @@
1
- // `plumbbob build <n>` — read step n's seam from intent.md, write the normalized
2
- // SEAM + STEP. The STEP file is the in-flight signal (the dashboard derives the
3
- // BUILD phase from it); it never checkpoints (only `checkpoint` commits).
1
+ // `plumbbob build <n>` — enter a step: read step n's seam from intent.md and
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
4
+ // step expects to touch, for orientation, never a lock. The STEP file is the
5
+ // in-flight signal (the dashboard derives the BUILD phase from it); this verb
6
+ // never checkpoints — only `checkpoint` commits.
4
7
  import { readFileSync, writeFileSync } from 'node:fs';
5
8
  import { findRepoRoot } from "../lib/git.js";
6
9
  import { hasSession, intentPath, resolveBuild, seamPath, stampStepStat, stampTick, stepPath } from "../lib/sidecar.js";
7
10
  import { parseStepSeam } from "../lib/intent.js";
8
11
  import { parseSteps } from "../lib/orient.js";
9
12
  import { stepLabel, syncBuildLogState } from "../lib/buildlogsync.js";
13
+ /**
14
+ * Enter step n — or the next undone step — and write its SEAM/STEP markers.
15
+ *
16
+ * Refuses a missing session, a malformed step argument, an `N-M` range (a
17
+ * `/plumbbob:build` skill feature, not a CLI one), and a seam that fails to parse.
18
+ * On entry it stamps the turn ledger and the step's start time, and re-renders
19
+ * the build-log's Current step line.
20
+ */
10
21
  export function build(cwd, args) {
11
22
  const root = findRepoRoot(cwd);
12
23
  if (root === null || !hasSession(root)) {
@@ -15,11 +26,11 @@ export function build(cwd, args) {
15
26
  }
16
27
  const { build: slug, rest } = resolveBuild(root, args);
17
28
  const raw = rest.find((a) => !a.startsWith('--'));
18
- // A step range like `1-3` is a `/pb-build` skill affordance (auto-approve
29
+ // A step range like `1-3` is a `/plumbbob:build` skill affordance (auto-approve
19
30
  // through the range, then pause), not a CLI capability — the CLI records one
20
31
  // in-flight step at a time. Name it rather than bounce off the generic usage.
21
32
  if (raw !== undefined && /^\d+-\d*$/.test(raw)) {
22
- process.stderr.write(`plumbbob: build takes one step number; \`${raw}\` step ranges are a \`/pb-build\` feature (auto-approve through the range, then pause). Try \`plumbbob build ${raw.split('-')[0]}\`.\n`);
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`);
23
34
  return 1;
24
35
  }
25
36
  // An explicit arg that isn't a positive integer is a usage error — caught before
@@ -31,13 +42,13 @@ export function build(cwd, args) {
31
42
  const intent = readFileSync(intentPath(root, slug), 'utf8');
32
43
  // No argument ⇒ enter the next undone step in intent.md (the same idiom
33
44
  // `checkpoint` uses), so a bare `plumbbob build` advances the loop without the
34
- // skill re-deriving the step in prose. Every step checkpointed ⇒ a `/pb-step`
45
+ // skill re-deriving the step in prose. Every step checkpointed ⇒ a `/plumbbob:step`
35
46
  // nudge, not a silent no-op.
36
47
  let step;
37
48
  if (raw === undefined) {
38
49
  const nextUndone = parseSteps(intent).find((s) => !s.done);
39
50
  if (nextUndone === undefined) {
40
- process.stderr.write('plumbbob: no undone step to build — every planned step is checkpointed. `/pb-step` to add an increment, or `/pb-finish`.\n');
51
+ process.stderr.write('plumbbob: no undone step to build — every planned step is checkpointed. `/plumbbob:step` to add an increment, or `/plumbbob:finish`.\n');
41
52
  return 1;
42
53
  }
43
54
  step = nextUndone.n;
@@ -52,12 +63,16 @@ export function build(cwd, args) {
52
63
  }
53
64
  writeFileSync(seamPath(root, slug), `${parsed.seam.join('\n')}\n`);
54
65
  writeFileSync(stepPath(root, slug), `${step}\n`);
55
- stampTick(root, slug); // the entry stamp (D64): TICK = TURN; skipped when the ledger is dormant.
56
- // The wall-clock receipt starts here (research/07 Build 2b): checkpoint stamps
57
- // landedAt, and the pair becomes the step's duration in the finish report.
66
+ // Stamp the turn ledger on entry (TICK = TURN): the checkpoint latch demands
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.
69
+ stampTick(root, slug);
70
+ // The wall-clock receipt starts here: checkpoint stamps landedAt, and the
71
+ // pair becomes the step's duration in the finish report.
58
72
  stampStepStat(root, slug, step, 'startedAt', new Date().toISOString());
59
- // The build-log's top half now shows this step in flight (D69): Current step +
60
- // the ☐/☑ mirror, re-rendered from intent.md. Best-effort — never blocks the build.
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.
61
76
  const title = parseSteps(intent).find((s) => s.n === step)?.title ?? null;
62
77
  syncBuildLogState(root, slug, stepLabel(step, title));
63
78
  const picked = raw === undefined ? ' (next undone)' : '';
@@ -1,14 +1,24 @@
1
- // `plumbbob check` — run the heavy gate (D16/D24/D32) and report, with NO state
2
- // change. The read-only half of the verify tick: `/pb-verify` runs this before the
3
- // pause so the human approves on a known-green check. Exits with the check's own
4
- // code (0 = green, 1 = red, 2 = the gate itself broke).
1
+ // `plumbbob check` — run the heavy gate and report, with NO state change. This
2
+ // is the read-only half of the verify tick: `/plumbbob:verify` runs it before the
3
+ // pause so the human approves on a known-green check. The gate itself lives in
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
7
+ // that gate through something else. Exits with the check's own code (0 = green,
8
+ // 1 = red, 2 = the gate itself broke).
5
9
  //
6
10
  // Narrowing flags for the iteration loop (`check --bail --only types,lint`) map
7
- // straight onto checkride's RunFlags (D32). Only this verb takes them — the
11
+ // straight onto checkride's run flags. Only this verb takes them — the
8
12
  // checkpoint gate is always full-fat.
9
13
  import { findRepoRoot } from "../lib/git.js";
10
14
  import { hasSession } from "../lib/sidecar.js";
11
15
  import { runCheck } from "../lib/check.js";
16
+ /**
17
+ * Run the heavy check gate for the active session and print the verdict line.
18
+ *
19
+ * Refuses without a session; otherwise returns the gate's own exit code so a
20
+ * caller can branch on green (0), red (1), or a broken gate (2) directly.
21
+ */
12
22
  export async function check(cwd, args = []) {
13
23
  const root = findRepoRoot(cwd);
14
24
  if (root === null || !hasSession(root)) {
@@ -19,8 +29,12 @@ export async function check(cwd, args = []) {
19
29
  process.stdout.write(verdictLine(code));
20
30
  return code;
21
31
  }
22
- // argv → CheckFlags: bare booleans plus comma-separated slot lists. Unknown
23
- // args are ignored rather than refused — the gate itself is the point.
32
+ /**
33
+ * Map argv onto checkride's flags: bare booleans plus comma-separated slot lists.
34
+ *
35
+ * Unknown args are ignored rather than refused — running the gate is the point,
36
+ * not flag hygiene.
37
+ */
24
38
  function parseFlags(args) {
25
39
  return {
26
40
  ...(args.includes('--bail') ? { bail: true } : {}),
@@ -31,6 +45,12 @@ function parseFlags(args) {
31
45
  ...slotList(args, '--include'),
32
46
  };
33
47
  }
48
+ /**
49
+ * Read one `--only a,b`-style flag into its slot-list entry.
50
+ *
51
+ * Returns {} when the flag is absent, valueless, or followed by another flag,
52
+ * so the spread in parseFlags simply contributes nothing.
53
+ */
34
54
  function slotList(args, flag) {
35
55
  const i = args.indexOf(flag);
36
56
  const value = i >= 0 ? args[i + 1] : undefined;
@@ -42,6 +62,12 @@ function slotList(args, flag) {
42
62
  .filter((s) => s.length > 0);
43
63
  return names.length > 0 ? { [flag.slice(2)]: names } : {};
44
64
  }
65
+ /**
66
+ * The one-line human verdict for a check exit code.
67
+ *
68
+ * A broken gate (exit 2) reports distinctly from red — a misconfigured harness
69
+ * must never read as broken code, though both block a checkpoint.
70
+ */
45
71
  function verdictLine(code) {
46
72
  if (code === 0)
47
73
  return '\nplumbbob: check green.\n';