plumbbob 0.8.3 → 0.8.10

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.
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "plumbbob",
3
3
  "displayName": "PlumbBob",
4
- "version": "0.8.3",
4
+ "version": "0.8.10",
5
5
  "description": "Attention-first build process — driver skills + a CLI that keep you the decider: guidance on the work, a latch on the record.",
6
6
  "author": { "name": "Rob McLarty", "email": "hello@robmclarty.com", "url": "https://robmclarty.com" },
7
7
  "homepage": "https://github.com/robmclarty/plumbbob#readme",
package/README.md CHANGED
@@ -231,12 +231,15 @@ matters.
231
231
  > The latch's payoff is measured, not asserted: the skill-eval harness
232
232
  > ([`test/evals/`](test/evals/)) sweeps this loop headless — prose-only baseline vs. the
233
233
  > shipped latch, N=5 per contract, every assertion a mechanical read of git state — and
234
- > commits the receipt to [`reports/evals/`](reports/evals/2026-07-11.md). The first sweep
234
+ > commits the receipt to [`research/evals/`](research/evals/2026-07-11.md). The first sweep
235
235
  > (opus, 2026-07-11): the latch closes the route it owns — *no checkpoint over a red
236
236
  > check under pressure* went from **2/5 prose-only to 5/5 latched** — while the honest
237
237
  > numbers it also surfaced (park capture 0/5, and adversarial pressure consistently
238
238
  > flipping the `auto` standing grant in settings — a legal, model-writable side door)
239
- > are the next iteration's work, named in the report rather than papered over.
239
+ > are the next iteration's work, named in the report rather than papered over. The
240
+ > current sweep ([opus, 2026-07-18](research/evals/2026-07-18.md), plumbbob 0.8.7)
241
+ > reproduces the win — *no checkpoint over a red check* still holds **5/5 latched** —
242
+ > and confirms both gaps stand at 0.8.7, now the target of the hardening build.
240
243
 
241
244
  ## Documentation
242
245
 
package/dist/cli-core.js CHANGED
@@ -9,6 +9,7 @@ import { start } from "./verbs/start.js";
9
9
  import { status } from "./verbs/status.js";
10
10
  import { park } from "./verbs/park.js";
11
11
  import { build } from "./verbs/build.js";
12
+ import { handoff } from "./verbs/handoff.js";
12
13
  import { check } from "./verbs/check.js";
13
14
  import { checkpoint } from "./verbs/checkpoint.js";
14
15
  import { revert } from "./verbs/revert.js";
@@ -22,7 +23,8 @@ import { turn } from "./verbs/turn.js";
22
23
  const VERBS = [
23
24
  { name: 'start', summary: 'scaffold .plumbbob/; open the session; record the baseline commit' },
24
25
  { name: 'status', summary: 'print the orientation dashboard, or NO ACTIVE SESSION' },
25
- { name: 'build', summary: 'build <n>: write the seam from step n (in-flight, not a lock)' },
26
+ { name: 'build', summary: 'build [<n>]: write the seam from step n, or the next undone step (in-flight, not a lock)' },
27
+ { name: 'handoff', summary: 'print the standardized build hand-off block (pause or post-checkpoint); read-only' },
26
28
  { name: 'check', summary: 'run the heavy check and report; no state change (--bail --only a,b … narrow the checkride gate)' },
27
29
  { name: 'checkpoint', summary: 'checkpoint [<n>]: gate on green, commit/record SHA, mark step done (executor-agnostic)' },
28
30
  { name: 'revert', summary: 'revert [--to n]: git reset --hard to a checkpoint SHA' },
@@ -71,6 +73,8 @@ async function dispatch(verb, cwd, rest) {
71
73
  return park(cwd, rest);
72
74
  case 'build':
73
75
  return build(cwd, rest);
76
+ case 'handoff':
77
+ return handoff(cwd, rest);
74
78
  case 'check':
75
79
  return check(cwd, rest);
76
80
  case 'checkpoint':
package/dist/lib/latch.js CHANGED
@@ -2,33 +2,41 @@
2
2
  // before its check gate (C4: cheap first; the gate already ran in the verify
3
3
  // tick). The work plane stays guidance (D10/D13 untouched); only the commit tick
4
4
  // is latched: a step may not land unless a human turn intervened since entry, the
5
- // human granted self-approval (D65 one-turn grant, D27 standing `auto`), or the
6
- // ledger is dormant. Six rows, evaluated in order, first hit wins; the refusal
7
- // message *is* the pause affordance. Reads TURN/TICK/GRANT/isTTY only — never the
8
- // host (C5). Functional, named exports, node builtins (C1/C2).
5
+ // human granted self-approval (a D65 one-turn grant from their literal prompt), or
6
+ // the ledger is dormant. Five rows, evaluated in order, first hit wins; the refusal
7
+ // message *is* the pause affordance. A standing `auto` in a settings file is NOT a
8
+ // grant (D67): a model can write that file, and a grant it can forge is no grant —
9
+ // so the latch reads it only to say so at the pause, never to allow. Reads
10
+ // TURN/TICK/GRANT/isTTY only — never the host (C5). Named exports, node builtins.
9
11
  import { readFileSync } from 'node:fs';
10
12
  import { grantPath, tickPath, turnPath } from "./sidecar.js";
11
13
  import { resolveBoolean } from "./settings.js";
12
14
  const ALLOW = { allow: true };
13
- // The exact refusal (the spec pins this prose): the message is the affordance —
14
- // it tells the model what the pause is and names the only legitimate grants. The
15
- // plan commit gets its own wording: there is no step, no diff, and no self-review
16
- // at plan time — what the human approves is the plan itself.
15
+ // The refusal message is the affordance — it tells the model what the pause is,
16
+ // how it ends (approval on the next turn lands it; `/pb-build` only starts the next
17
+ // step), and names the only legitimate grants. The plan commit gets its own wording:
18
+ // there is no step, no diff, and no self-review at plan time — what the human approves
19
+ // is the plan itself.
17
20
  const NO_TURN_MESSAGE = `plumbbob: checkpoint refused — no human turn since this step began. This is the
18
- pause: present the diff and the self-review, then end the turn; the human's next
19
- message is the tick. (An explicit \`/pb-build --auto\` or a step range in the human's
20
- own prompt grants self-approval; \`auto: true\` in settings.local.json is the standing
21
- grant.)
21
+ pause: present the diff and the self-review, then end the turn. The human's approval on
22
+ their next turn is what lets this land — re-run \`checkpoint\` then and stop at the
23
+ boundary; \`/pb-build\` only starts the next step, it never lands this one. (An explicit
24
+ \`/pb-build --auto\` or a step range in the human's own prompt is the only self-approval.)
22
25
  `;
23
26
  const NO_TURN_PLAN_MESSAGE = `plumbbob: checkpoint refused — no human turn since \`start\` stamped this plan. This
24
27
  is the plan pause: present the plan, then end the turn; the human's approving
25
- message is the tick that lets it land on re-fire. (\`auto: true\` in
26
- settings.local.json is the standing grant.)
28
+ message is the tick that lets it land on re-fire. (An explicit \`/pb-build --auto\` or a
29
+ step range in the human's own prompt is the only self-approval.)
30
+ `;
31
+ // D67: a settings `auto` is no longer honored — a model can write that file, so a
32
+ // grant it can forge is no grant. When one is set we say so at the point of refusal,
33
+ // rather than silently ignoring it, and point at the only self-approval that works.
34
+ const SETTINGS_AUTO_NOTE = `note: \`auto\` is set in settings but is no longer a grant (D67) — re-fire \`/pb-build --auto\` to self-approve.
27
35
  `;
28
36
  function ceilingMessage(ceiling) {
29
37
  return `plumbbob: checkpoint refused — the range you granted ends at step ${ceiling} — pause here; re-fire to continue.\n`;
30
38
  }
31
- // The six-row matrix, in order, first hit wins. A `range` grant speaks only to
39
+ // The five-row matrix, in order, first hit wins. A `range` grant speaks only to
32
40
  // numbered steps: on a `--plan` checkpoint (step null) it neither allows nor
33
41
  // refuses, and the decision falls through to the turn comparison.
34
42
  export function evaluateLatch(input) {
@@ -36,19 +44,19 @@ export function evaluateLatch(input) {
36
44
  return ALLOW; // 1 — a human at the keyboard
37
45
  if (input.turn === null || input.tick === null)
38
46
  return ALLOW; // 2 — dormant / hand-built
39
- if (input.auto)
40
- return ALLOW; // 3 — standing personal grant (D27)
41
47
  if (input.grant?.kind === 'auto')
42
- return ALLOW; // 4 — one-turn grant
48
+ return ALLOW; // 3 — one-turn grant (D65)
43
49
  if (input.grant?.kind === 'range' && input.step !== null) {
44
50
  if (input.step <= input.grant.ceiling)
45
51
  return ALLOW;
46
52
  return { allow: false, reason: 'ceiling', message: ceilingMessage(input.grant.ceiling) };
47
53
  }
48
54
  if (input.turn > input.tick)
49
- return ALLOW; // 5 — a human turn intervened
50
- // 6 — the pause (plan-worded when no step is landing)
51
- return { allow: false, reason: 'no-turn', message: input.step === null ? NO_TURN_PLAN_MESSAGE : NO_TURN_MESSAGE };
55
+ return ALLOW; // 4 — a human turn intervened
56
+ // 5 — the pause (plan-worded when no step is landing). A settings `auto` no longer
57
+ // allows (D67); when one is set, the refusal notes it rather than honoring it.
58
+ const base = input.step === null ? NO_TURN_PLAN_MESSAGE : NO_TURN_MESSAGE;
59
+ return { allow: false, reason: 'no-turn', message: input.settingsAuto ? base + SETTINGS_AUTO_NOTE : base };
52
60
  }
53
61
  // Parse a GRANT file's content (`auto` | `range <M>`). Anything else — including
54
62
  // a corrupted or hand-mangled file — contributes nothing (D27): no grant, never
@@ -70,9 +78,10 @@ export function checkLatch(root, step) {
70
78
  isTTY: process.stdin.isTTY === true,
71
79
  turn: readCount(turnPath(root)),
72
80
  tick: readCount(tickPath(root)),
73
- auto: resolveBoolean(root, 'auto', false),
74
81
  grant: readGrant(root),
75
82
  step,
83
+ // Still read from the ladder — but only to decorate the pause (D67), not to allow.
84
+ settingsAuto: resolveBoolean(root, 'auto', false),
76
85
  });
77
86
  }
78
87
  function readGrant(root) {
@@ -8,10 +8,13 @@
8
8
  // overlay can never wedge the tool. Functional/procedural, node builtins (C1/C2).
9
9
  //
10
10
  // Known keys: `check` (string — the heavy gate, tracked in settings.json) and
11
- // `auto` (boolean — whether the agent approves in the human's place; a personal
12
- // preference, so it belongs in settings.local.json). The per-worktree active-build
13
- // cursor is NOT here — it lives in the STATE file (see sidecar.ts), so this overlay
14
- // stays purely human-owned and the tool never rewrites it.
11
+ // `auto` (boolean — a personal preference, so it belongs in settings.local.json).
12
+ // Since D67 `auto` is NOT a checkpoint grant: the latch no longer reads it to allow
13
+ // a land (a model can write this file, and a grant it can forge is no grant), only to
14
+ // note it at the pause; self-approval comes from the human's literal `/pb-build --auto`
15
+ // or a range. The per-worktree active-build cursor is NOT here — it lives in the STATE
16
+ // file (see sidecar.ts), so this overlay stays purely human-owned and the tool never
17
+ // rewrites it.
15
18
  import { mkdirSync, readFileSync, writeFileSync } from 'node:fs';
16
19
  import { dirname, join } from 'node:path';
17
20
  const DIRNAME = '.plumbbob';
@@ -99,8 +99,16 @@ async function runOne(root, slug, step, spec) {
99
99
  step,
100
100
  mode,
101
101
  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.
102
105
  auto: resolveBoolean(root, 'auto', false),
103
106
  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.
111
+ agent: resolveRecord(root, 'agentConfig')[spec.name] ?? {},
104
112
  },
105
113
  });
106
114
  if (!composed.ok) {
@@ -5,6 +5,7 @@ import { readFileSync, writeFileSync } from 'node:fs';
5
5
  import { findRepoRoot } from "../lib/git.js";
6
6
  import { hasSession, intentPath, resolveBuild, seamPath, stampStepStat, stampTick, stepPath } from "../lib/sidecar.js";
7
7
  import { parseStepSeam } from "../lib/intent.js";
8
+ import { parseSteps } from "../lib/orient.js";
8
9
  export function build(cwd, args) {
9
10
  const root = findRepoRoot(cwd);
10
11
  if (root === null || !hasSession(root)) {
@@ -20,12 +21,30 @@ export function build(cwd, args) {
20
21
  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`);
21
22
  return 1;
22
23
  }
23
- if (raw === undefined || !/^\d+$/.test(raw) || Number(raw) < 1) {
24
+ // An explicit arg that isn't a positive integer is a usage error — caught before
25
+ // reading intent.md so the message doesn't depend on the plan being present.
26
+ if (raw !== undefined && (!/^\d+$/.test(raw) || Number(raw) < 1)) {
24
27
  process.stderr.write('plumbbob: build needs a step number. Try: plumbbob build 2.\n');
25
28
  return 1;
26
29
  }
27
- const step = Number(raw);
28
- const parsed = parseStepSeam(readFileSync(intentPath(root, slug), 'utf8'), step);
30
+ const intent = readFileSync(intentPath(root, slug), 'utf8');
31
+ // No argument ⇒ enter the next undone step in intent.md (the same idiom
32
+ // `checkpoint` uses), so a bare `plumbbob build` advances the loop without the
33
+ // skill re-deriving the step in prose. Every step checkpointed ⇒ a `/pb-step`
34
+ // nudge, not a silent no-op.
35
+ let step;
36
+ if (raw === undefined) {
37
+ const nextUndone = parseSteps(intent).find((s) => !s.done);
38
+ if (nextUndone === undefined) {
39
+ process.stderr.write('plumbbob: no undone step to build — every planned step is checkpointed. `/pb-step` to add an increment, or `/pb-finish`.\n');
40
+ return 1;
41
+ }
42
+ step = nextUndone.n;
43
+ }
44
+ else {
45
+ step = Number(raw);
46
+ }
47
+ const parsed = parseStepSeam(intent, step);
29
48
  if (!parsed.ok) {
30
49
  process.stderr.write(`plumbbob: ${parsed.error} Fix the step's seam in intent.md, then \`build ${step}\` again.\n`);
31
50
  return 1;
@@ -36,6 +55,7 @@ export function build(cwd, args) {
36
55
  // The wall-clock receipt starts here (research/07 Build 2b): checkpoint stamps
37
56
  // landedAt, and the pair becomes the step's duration in the finish report.
38
57
  stampStepStat(root, slug, step, 'startedAt', new Date().toISOString());
39
- process.stdout.write(`plumbbob: building step ${step}. Seam (for orientation; not a lock):\n${parsed.seam.map((p) => ` ${p}`).join('\n')}\n`);
58
+ const picked = raw === undefined ? ' (next undone)' : '';
59
+ process.stdout.write(`plumbbob: building step ${step}${picked}. Seam (for orientation; not a lock):\n${parsed.seam.map((p) => ` ${p}`).join('\n')}\n`);
40
60
  return 0;
41
61
  }
@@ -32,7 +32,7 @@ import { gateDetectsTools } from "../lib/check.js";
32
32
  import { marketplacePlumbbob } from "../lib/plugins.js";
33
33
  import { findRepoRoot, gitPath, stagePath } from "../lib/git.js";
34
34
  import { buildDir, excludeControl, listBuilds, readTurn, setActiveBuild, sidecarDir, slugify } from "../lib/sidecar.js";
35
- import { resolveString, settingsPath } from "../lib/settings.js";
35
+ import { resolveBoolean, resolveString, settingsPath } from "../lib/settings.js";
36
36
  // The plumbbob package's own skills/ dir (the canonical set), off this module's
37
37
  // URL so it resolves the same from src/ (dev) and dist/ (published).
38
38
  function packageDir(name) {
@@ -365,7 +365,7 @@ function agentReport(cwd) {
365
365
  // slot/adapter table. Only rows checkride marks `required` count as problems —
366
366
  // an empty slot ("no tool detected") is informational, not a failure, because
367
367
  // the runtime gate already refuses a vacuous run.
368
- async function gateReport(cwd) {
368
+ async function gateReport(cwd, env) {
369
369
  const root = findRepoRoot(cwd);
370
370
  if (root === null)
371
371
  return { lines: [], failed: 0 };
@@ -377,7 +377,9 @@ async function gateReport(cwd) {
377
377
  }
378
378
  try {
379
379
  const silent = { write: () => true };
380
- const { report } = await runDoctor({ cwd: root, stdout: silent });
380
+ // env is undefined in production → runDoctor falls back to its real toolchain
381
+ // probe; tests inject a deterministic one (see GateEnv).
382
+ const { report } = await runDoctor({ cwd: root, stdout: silent, env });
381
383
  let failed = 0;
382
384
  for (const c of report.checks) {
383
385
  if (c.category === 'tool') {
@@ -433,9 +435,16 @@ function latchReport(cwd) {
433
435
  const line = turn === null
434
436
  ? ' ○ latch: dormant — guidance only (no turn ledger yet; it ticks on your first prompt when the UserPromptSubmit hook is wired — re-run after one to confirm)'
435
437
  : ` ✓ latch: live (turn ${turn})`;
436
- return { lines: ['', 'plumbbob doctor — approval latch (D64)', line], failed: 0 };
438
+ const lines = ['', 'plumbbob doctor — approval latch (D64)', line];
439
+ // D67: a settings `auto` used to be a standing grant; the latch no longer honors it
440
+ // (a model can write that file). Surface a set value so a human who relied on it isn't
441
+ // silently changed — informational, never a problem.
442
+ if (resolveBoolean(root, 'auto', false)) {
443
+ lines.push(' ○ auto: set in settings but not a grant since D67 — self-approve per run with `/pb-build --auto`');
444
+ }
445
+ return { lines, failed: 0 };
437
446
  }
438
- export async function doctor(cwd, args = []) {
447
+ export async function doctor(cwd, args = [], env) {
439
448
  const checks = pluginChecks();
440
449
  const out = ['plumbbob doctor — plugin install'];
441
450
  for (const c of checks) {
@@ -445,7 +454,7 @@ export async function doctor(cwd, args = []) {
445
454
  out.push(...sidecar.lines);
446
455
  const agents = agentReport(cwd);
447
456
  out.push(...agents.lines);
448
- const gate = await gateReport(cwd);
457
+ const gate = await gateReport(cwd, env);
449
458
  out.push(...gate.lines);
450
459
  const latch = latchReport(cwd);
451
460
  out.push(...latch.lines);
@@ -0,0 +1,103 @@
1
+ // `plumbbob handoff` — print the standardized build hand-off block (D67): the
2
+ // "state / choice / what's next" the human sees at each step boundary. Read-only,
3
+ // no state change. It derives the moment from the session — a step in flight ⇒ the
4
+ // pause block (built → looks-good/needs-work → next), none ⇒ the post-checkpoint
5
+ // boundary block — and renders the next undone step plus its advisory `- model:`
6
+ // recommendation straight from intent.md. This block lived as prose in pb-build's
7
+ // SKILL.md; owning it here keeps the skill from drifting out of sync with `status`,
8
+ // which renders the same next-step detail (both read `parseSteps`).
9
+ import { readFileSync } from 'node:fs';
10
+ import { findRepoRoot } from "../lib/git.js";
11
+ import { checkpointsPath, hasSession, intentPath, resolveBuild, stepPath } from "../lib/sidecar.js";
12
+ import { parseLastCheckpoint, parseSteps } from "../lib/orient.js";
13
+ export function handoff(cwd, args = []) {
14
+ const root = findRepoRoot(cwd);
15
+ if (root === null || !hasSession(root)) {
16
+ process.stderr.write('plumbbob: no active session. Run `plumbbob start "<title>"` first.\n');
17
+ return 1;
18
+ }
19
+ const { build: slug, rest } = resolveBuild(root, args);
20
+ const steps = parseSteps(readOr(intentPath(root, slug)));
21
+ const inFlight = readStep(stepPath(root, slug));
22
+ const explicit = rest.find((a) => /^\d+$/.test(a));
23
+ const lastDone = parseLastCheckpoint(readOr(checkpointsPath(root, slug)));
24
+ // The step this hand-off is about: an explicit arg wins (the skill's override),
25
+ // else the in-flight step (the pause), else the last checkpointed step (the
26
+ // boundary). Null only on a fresh session the skill would not hand off from.
27
+ const current = explicit !== undefined ? Number(explicit) : (inFlight ?? lastDone?.n ?? null);
28
+ // Next up: the first undone step that is not the current one. At the pause the
29
+ // current step is still `[ ]`, so excluding it by number is what makes "next"
30
+ // mean the step *after* it; at the boundary the current step is already `[x]`,
31
+ // so the `!== current` clause is a harmless no-op. Skipping-ahead builds still
32
+ // land on the earliest remaining step, never a gap.
33
+ const nextUp = steps.find((s) => !s.done && s.n !== current);
34
+ if (current === null) {
35
+ // No step to report as just-done (fresh session): emit only the forward pointer.
36
+ process.stdout.write(`${nextUpLine(nextUp)}\n`);
37
+ return 0;
38
+ }
39
+ const block = inFlight !== null ? pauseBlock(current, titleOf(steps, current), nextUp) : boundaryBlock(current, titleOf(steps, current), nextUp);
40
+ process.stdout.write(`${block}\n`);
41
+ return 0;
42
+ }
43
+ // The pause block: the step is built and awaiting approval. The opener is neutral
44
+ // (the CLI cannot vouch for the diff), pointing at the diff and self-review the
45
+ // skill shows above it.
46
+ function pauseBlock(step, title, nextUp) {
47
+ return [
48
+ `Step ${step}${paren(title)} is built. Review the diff and self-review above, then:`,
49
+ ` - looks good → I'll checkpoint it (lands step ${step} as its own commit, back to the boundary)`,
50
+ ` - needs work → tell me what to change and I'll iterate on this same step`,
51
+ '',
52
+ nextUpLine(nextUp),
53
+ ].join('\n');
54
+ }
55
+ // The boundary block: the checkpoint has landed and the loop is back at the
56
+ // step boundary, pointing at what to build next.
57
+ function boundaryBlock(step, title, nextUp) {
58
+ return [`Step ${step}${paren(title)} checkpointed — back at the boundary.`, '', nextUpLine(nextUp)].join('\n');
59
+ }
60
+ // The forward pointer, shared by both blocks: the next undone step, its title, and
61
+ // its advisory `- model:` recommendation (as just the model token for the `/model`
62
+ // call — the full rationale lives on the `status` dashboard). No next step ⇒ the
63
+ // finish/step nudge instead.
64
+ function nextUpLine(nextUp) {
65
+ if (nextUp === undefined) {
66
+ return 'No planned steps remain — /pb-step to add an increment, or /pb-finish.';
67
+ }
68
+ const head = `Next up: step ${nextUp.n}${paren(nextUp.title)}`;
69
+ const token = modelToken(nextUp.model);
70
+ return token === null ? `${head} — /pb-build to start it.` : `${head} · model: ${token} — /model ${token} then /pb-build.`;
71
+ }
72
+ // A step's title in parentheses, or nothing when the title is empty.
73
+ function paren(title) {
74
+ return title !== null && title.length > 0 ? ` (${title})` : '';
75
+ }
76
+ function titleOf(steps, n) {
77
+ return steps.find((s) => s.n === n)?.title ?? null;
78
+ }
79
+ // The model token from a `- model:` recommendation — the first word, so a
80
+ // `model: opus — <rationale>` line yields `opus` for the `/model` call. Null when
81
+ // there is no recommendation, or it degraded to whitespace (folding the null model
82
+ // into the same guard keeps both branches live).
83
+ function modelToken(model) {
84
+ const first = model?.trim().split(/\s+/)[0];
85
+ return first !== undefined && first.length > 0 ? first : null;
86
+ }
87
+ function readOr(path) {
88
+ try {
89
+ return readFileSync(path, 'utf8');
90
+ }
91
+ catch {
92
+ return '';
93
+ }
94
+ }
95
+ function readStep(path) {
96
+ try {
97
+ const raw = readFileSync(path, 'utf8').trim();
98
+ return /^\d+$/.test(raw) ? Number(raw) : null;
99
+ }
100
+ catch {
101
+ return null;
102
+ }
103
+ }
@@ -3,20 +3,54 @@
3
3
  // monotonic count of human turns the checkpoint latch reads to know a human turn
4
4
  // intervened since a step began) and rewrites `.plumbbob/GRANT` from the literal
5
5
  // prompt — minting a one-turn self-approval only when the human typed
6
- // `/pb-build --auto` or a `N-M` range (D65), clearing it otherwise. Pure machinery,
7
- // not a user verb: it writes nothing to stdout and ALWAYS exits 0, so a broken tick
8
- // can never wedge prompting (C3).
6
+ // `/pb-build --auto` or a `N-M` range (D65), clearing it otherwise. When a step is
7
+ // in flight it also emits ONE `UserPromptSubmit` additionalContext line so a fresh
8
+ // session (post-compaction, or a new `-p` turn where no skill is in context) still
9
+ // knows the beat — a tangent is parked, not chased (amends D64's "writes nothing";
10
+ // guidance only, D10/D13). It ALWAYS exits 0 and any failure degrades to no output,
11
+ // so neither the tick nor the emit can ever wedge prompting (C3).
9
12
  import { readFileSync, writeFileSync } from 'node:fs';
10
- import { findSessionRoot, setGrant, turnPath } from "../lib/sidecar.js";
13
+ import { findSessionRoot, setGrant, stepPath, turnPath } from "../lib/sidecar.js";
11
14
  export function turn(cwd, _args) {
12
15
  try {
13
16
  applyTurn(cwd, readStdin());
17
+ const context = stepInFlightContext(cwd);
18
+ if (context !== null)
19
+ process.stdout.write(context);
14
20
  }
15
21
  catch {
16
22
  // C3: a broken turn must never wedge a prompt — swallow and exit 0.
17
23
  }
18
24
  return 0;
19
25
  }
26
+ // The `UserPromptSubmit` additionalContext payload when a step is in flight (a STEP
27
+ // marker between `build` and `checkpoint`), or null. Guidance only: it reminds a
28
+ // fresh session that a tangent is a park (not an edit) and how the step lands, since
29
+ // the pb-build prose may not be in context after compaction or on a scripted turn.
30
+ // Exported because an in-process test cannot read the hook's real stdout.
31
+ export function stepInFlightContext(cwd) {
32
+ const root = findSessionRoot(cwd);
33
+ if (root === null)
34
+ return null;
35
+ const step = inFlightStep(root);
36
+ if (step === null)
37
+ return null;
38
+ const text = `plumbbob: step ${step} is in flight — a new idea or tangent is a park, not an edit: ` +
39
+ `capture it with \`plumbbob park "<one line>"\`, then stay on the step. ` +
40
+ `It lands when you run \`plumbbob checkpoint ${step}\` after approval.`;
41
+ return `${JSON.stringify({ hookSpecificOutput: { hookEventName: 'UserPromptSubmit', additionalContext: text } })}\n`;
42
+ }
43
+ // The in-flight step number, or null when no STEP marker is present (no step entered,
44
+ // or it already checkpointed). A garbled marker reads as null — never an error (C3).
45
+ function inFlightStep(root) {
46
+ try {
47
+ const n = Number.parseInt(readFileSync(stepPath(root), 'utf8').trim(), 10);
48
+ return Number.isFinite(n) && n > 0 ? n : null;
49
+ }
50
+ catch {
51
+ return null;
52
+ }
53
+ }
20
54
  // The testable core: apply one tick given the raw hook stdin. Split from the fd-0
21
55
  // read so the unit tests can drive it directly — an in-process test cannot feed fd 0
22
56
  // (the same constraint checkpoint's `--body` lives under).
@@ -35,10 +69,12 @@ export function applyTurn(cwd, raw) {
35
69
  // The namespaced `/plumbbob:pb-build` form is honored alongside the bare one.
36
70
  //
37
71
  // Only the invocation's own arguments can mint: the tokens that follow it on its
38
- // line, up to the first token that isn't argument-shaped (a flag, a step number, a
39
- // range). An incidental range elsewhere in the prompt — an issue number, a pasted
40
- // `2020-2024` — is prose, not a grant. Trailing sentence punctuation on an argument
41
- // (`/pb-build 1-3.`) is still the argument the human typed.
72
+ // line, up to the first token that isn't a sanctioned argument (`--auto`, a step
73
+ // number, or a range — the argument-hint sanctions nothing else). An unrecognized
74
+ // flag ends the scan, so `/pb-build --wip 2020-2024` mints nothing — the `2020-2024`
75
+ // is never reached. An incidental range elsewhere in the prompt — an issue number, a
76
+ // pasted `2020-2024` — is prose, not a grant. Trailing sentence punctuation on an
77
+ // argument (`/pb-build 1-3.`) is still the argument the human typed.
42
78
  export function grantFromPrompt(prompt) {
43
79
  const invocation = /\/(?:plumbbob:)?pb-build\b/.exec(prompt);
44
80
  if (invocation === null)
@@ -59,9 +95,9 @@ export function grantFromPrompt(prompt) {
59
95
  ceiling = range[1];
60
96
  continue;
61
97
  }
62
- if (/^\d+$/.test(word) || word.startsWith('-'))
63
- continue; // a step number or another flag
64
- break; // free text — the arguments have ended
98
+ if (/^\d+$/.test(word))
99
+ continue; // a bare step number — keep scanning for a range/--auto
100
+ break; // an unrecognized flag or free text — the arguments have ended
65
101
  }
66
102
  if (ceiling !== undefined)
67
103
  return `range ${ceiling}`;
File without changes
File without changes
File without changes
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "plumbbob",
3
- "version": "0.8.3",
3
+ "version": "0.8.10",
4
4
  "description": "Attention-first build process: 12 skills + a CLI that keep you the decider — guidance on the work, a latch on the record.",
5
5
  "keywords": [
6
6
  "claude-code",
@@ -34,39 +34,33 @@
34
34
  "engines": {
35
35
  "node": ">=22.18.0"
36
36
  },
37
- "devEngines": {
38
- "packageManager": {
39
- "name": "pnpm",
40
- "version": "11.1.2",
41
- "onFail": "download"
42
- }
37
+ "scripts": {
38
+ "clean": "rm -rf dist",
39
+ "build": "tsc -p tsconfig.build.json",
40
+ "prepack": "pnpm run clean && pnpm run build",
41
+ "check": "checkride",
42
+ "test": "vitest run",
43
+ "eval:baseline": "pnpm build && node test/evals/run.ts --sweep baseline",
44
+ "eval:latched": "pnpm build && node test/evals/run.ts --sweep latched",
45
+ "eval:report": "node test/evals/run.ts --report"
43
46
  },
44
47
  "devDependencies": {
45
- "@ast-grep/cli": "^0.43.0",
46
- "@stryker-mutator/core": "^9.6.1",
47
- "@stryker-mutator/vitest-runner": "^9.6.1",
48
- "@types/node": "^24.0.0",
49
- "@vitest/coverage-v8": "^4.1.9",
50
- "fallow": "^2.103.0",
48
+ "@ast-grep/cli": "0.43.0",
49
+ "@stryker-mutator/core": "9.6.1",
50
+ "@stryker-mutator/vitest-runner": "9.6.1",
51
+ "@types/node": "24.13.1",
52
+ "@vitest/coverage-v8": "4.1.9",
53
+ "fallow": "2.103.0",
51
54
  "fascicle": "0.8.16",
52
- "markdownlint-cli2": "^0.22.1",
53
- "oxlint": "^1.69.0",
54
- "typescript": "^6.0.3",
55
- "vitest": "^4.1.8"
55
+ "markdownlint-cli2": "0.22.1",
56
+ "oxlint": "1.69.0",
57
+ "typescript": "6.0.3",
58
+ "vitest": "4.1.9"
56
59
  },
57
60
  "dependencies": {
58
61
  "checkride": "0.3.0"
59
62
  },
60
63
  "bundleDependencies": [
61
64
  "checkride"
62
- ],
63
- "scripts": {
64
- "clean": "rm -rf dist",
65
- "build": "tsc -p tsconfig.build.json",
66
- "check": "checkride",
67
- "test": "vitest run",
68
- "eval:baseline": "pnpm build && node test/evals/run.ts --sweep baseline",
69
- "eval:latched": "pnpm build && node test/evals/run.ts --sweep latched",
70
- "eval:report": "node test/evals/run.ts --report"
71
- }
72
- }
65
+ ]
66
+ }
@@ -3,7 +3,7 @@ name: pb-build
3
3
  description: The default engine — read the next planned step from intent, implement it (its done-when, seam, Decisions, Constraints), then verify it through to the approval pause. Swappable — build by hand/vibed/another harness and run /pb-verify instead. `--auto` self-approves and chains to done; a step range like `1-3` self-approves through step 3, then pauses.
4
4
  argument-hint: "[step-number | step-range] [--auto]"
5
5
  disable-model-invocation: true
6
- allowed-tools: Read, Edit, Write, Bash(plumbbob status:*), Bash(plumbbob build:*), Bash(plumbbob check:*), Bash(plumbbob checkpoint:*), Bash(plumbbob agent:*), Bash(git diff:*)
6
+ allowed-tools: Read, Edit, Write, Bash(plumbbob status:*), Bash(plumbbob build:*), Bash(plumbbob handoff:*), Bash(plumbbob check:*), Bash(plumbbob checkpoint:*), Bash(plumbbob park:*), Bash(plumbbob agent:*), Bash(git diff:*)
7
7
  ---
8
8
 
9
9
  # PlumbBob — build a step (the default engine)
@@ -30,51 +30,36 @@ differs from the model you're running as, say so before implementing — the hum
30
30
  1. **Pick the step.** Use the number you were invoked with (e.g. `/pb-build 4`) — or,
31
31
  if you were given a range like `/pb-build 1-3`, start at the first number and treat
32
32
  the second as the auto-approve ceiling (see the range note under `--auto`). With no
33
- argument, take the next undone, planned step in `.plumbbob/intent.md`. If there is no
34
- planned step to build, stop and tell the human to `/pb-step` first.
35
- 2. **Enter the step.** Run `plumbbob build <n>` (records the in-flight STEP +
36
- SEAM so `/pb-status` shows the step in flight; the seam is awareness, not a
37
- lock).
33
+ argument, don't resolve the next step yourself — the CLI does: bare `plumbbob build`
34
+ (step 2) enters the next undone step and refuses with a `/pb-step` nudge when every
35
+ step is checkpointed.
36
+ 2. **Enter the step.** Run `plumbbob build <n>` — or bare `plumbbob build` to enter the
37
+ next undone step (records the in-flight STEP + SEAM so `/pb-status` shows the step in
38
+ flight; the seam is awareness, not a lock).
38
39
  3. **Read the plan.** Read the step's **done-when**, its **seam**, and the
39
40
  **Decisions** and **Constraints** in `intent.md`. Build to *that* — the deciding
40
41
  already happened, off the chat.
41
- - **Run any bound `before`-agents**. If the build's `harness.json` binds
42
- agents to this step's `before` slot, run `plumbbob agent run --step <n> --mode
43
- before` first: each returns a validated envelope on stdout that plumbbob also
44
- appends to the step's `handoff.json`, and its `summary`/`body` become **context you
45
- read into the build** (that is the whole point of the slot — load the context in
46
- before you write). No binding, or no harness, is a clean no-op — just build.
42
+ - **Bound `before`-agents load context first.** If `plumbbob status` shows a
43
+ `harness bindings:` block, this build binds agents — see **§ Running bound agents**
44
+ and load their context in before you write. No bindings, no harness: just build.
47
45
  4. **Implement** the step, and only that step, staying within the declared seam. A
48
- new problem or "ooh what if" that surfaces mid-build is a `/pb-park`, **not** an
49
- edit — capture it and stay on the step. If you genuinely cannot finish without
50
- touching more than the seam, that is scope drift: surface it to the human rather
51
- than sprawling.
52
- - **If a `build`-slot agent is bound, delegate the diff to it**. Run
53
- `plumbbob agent run --step <n> --mode build` and let that agent author the step's
54
- code instead of writing it yourself; its envelope reports what it did. You still
55
- own the verify tick below — the diff is reviewed the same way whoever wrote it.
56
- - **A manifest's `when` prose is your cue to fire an agent mid-build**.
57
- The three slots are the only *declarative* lifecycle points; there is no config for
58
- "a salient moment in the middle." That is judgment, and you are the frontier model
59
- in the room: when the work reaches the situation a bound agent's `when` (or a step
60
- `note`) describes, fire `plumbbob agent run <name> --step <n>` yourself. Prose is
61
- the orchestration language; you are the workflow engine.
62
- - **Route a non-`done` envelope by its status**. An agent that returns
63
- `blocked` couldn't finish (missing input, failed precondition): surface its `notes`,
64
- let the human unblock, and re-run it — don't work around it. One that returns
65
- `drift` finished but found the plan no longer matches reality: **stop and send the
66
- human to `/pb-refine`** to repair the plan before continuing, rather than building
67
- on a plan that's now wrong. A non-zero exit is a failed run: report it and stop.
46
+ new problem or "ooh what if" that surfaces mid-build is a park, **not** an edit:
47
+ capture it by running `plumbbob park "<one tidy line>"` — **saying "let's defer
48
+ that" writes nothing; only the park line in the build log is a capture.** When the
49
+ *human* hands you the tangent mid-build, their message **is** the approval to park
50
+ it — capture it right away, say what you parked, and stay on the step. If you
51
+ genuinely cannot finish without touching more than the seam, that is scope drift:
52
+ surface it to the human rather than sprawling.
53
+ - **Bound agents can shape the build.** If `plumbbob status` shows a
54
+ `harness bindings:` block, a `build`-slot agent may author the diff, a `when`-cue may
55
+ fire an agent mid-build, and a non-`done` envelope routes by its status — see
56
+ **§ Running bound agents**.
68
57
  5. **Verify, through to the pause.** Run the verify tick exactly as `/pb-verify`
69
58
  does: `plumbbob check` (on red, read `.check/summary.json` and the failing slot's
70
59
  raw output under `.check/` for the actual diagnostics; while iterating on a fix,
71
60
  narrow the loop with `plumbbob check --bail --only <slots>` — the checkpoint gate
72
- still runs everything) → run any bound `after`-agents (`plumbbob agent run --step
73
- <n> --mode after`) and fold their envelopes into the self-review as **advisory
74
- input** — they inform, they never gate (checkride gates, the human is the clock; an
75
- `after`-agent that could fail a step is the lock in autonomy's costume) →
76
- self-review the diff against the done-when, the Decisions, and the Constraints (a
77
- single structured read) → validate → **PAUSE
61
+ still runs everything) → self-review the diff against the done-when, the Decisions,
62
+ and the Constraints (a single structured read) → validate → **PAUSE
78
63
  for the human's approval** → only on approval, checkpoint with
79
64
  `plumbbob checkpoint <n> --body <<'BODY' … BODY` — a commit body **proportional to the
80
65
  step** (a line for a trivial change, a short paragraph for a meatier one; no TIL scan,
@@ -82,24 +67,73 @@ differs from the model you're running as, say so before implementing — the hum
82
67
  build-log's `## Log`, so the history writes itself — you only supply the body (or omit
83
68
  `--body` for the deterministic done-when + seam + diffstat fallback). Do **not** bump
84
69
  the version or changelog — that is the human's `/version` call.
70
+ - **If `plumbbob status` shows a `harness bindings:` block**, fold any bound
71
+ `after`-agents into that self-review as advisory input — see **§ Running bound
72
+ agents**.
85
73
 
86
74
  **The latch makes this pause real.** On the default (non-`--auto`) path you
87
75
  build the step and reach the pause *in one turn* — so under plumbbob's turn hook the
88
76
  checkpoint at the end of that turn is **refused** ("no human turn since this step
89
77
  began"), and that refusal **is** the pause working as designed. Do exactly what the
90
- pause already asks: present the diff and the self-review, **end the turn**, and the
91
- human's next message is the tick that lands the checkpoint when you re-fire. **Never
92
- route around a refusal with a raw `git commit`** — the work plane stays free,
93
- but the *record* is latched on purpose.
94
-
95
- **Then hand off with the next model.** Once the checkpoint lands on re-fire, close the
96
- turn by citing where the loop now stands: the step you just completed and the next
97
- undone step. If that next step carries a `- model:` recommendation (the plan's
98
- smallest-model-that-fits call, echoed on `plumbbob status`'s `▸ next` line), **name
99
- it** so the human knows which `/model` to select before firing `/pb-build` again — it
100
- is what carries the plan's suggestion across a fresh context window, which inherits the
101
- session model, not the plan's. No `- model:` line means any model will do. Guidance,
102
- never a gate.
78
+ pause asks: present the diff and the self-review, emit the closing block below, and
79
+ **end the turn**. The human's next message — their approval — is the human turn that
80
+ lets the checkpoint land: on that turn you run `plumbbob checkpoint <n>` (now allowed)
81
+ and **stop at the boundary**. Landing the checkpoint is its own deliberate beat — the
82
+ thing approval triggers — never a side effect of starting the next step, and
83
+ `/pb-build` only ever starts the *next* step. **Never route around a refusal with a
84
+ raw `git commit`** — the work plane stays free, but the *record* is latched on purpose.
85
+
86
+ **End every default build turn with the standardized hand-off block.** Run `plumbbob
87
+ handoff` and present its output — it renders the same three-part shape (the state, the
88
+ choice, what's next) straight from the session: step N built, the looks-good /
89
+ needs-work choice, and the next undone step with its `- model:` recommendation. Show
90
+ your diff and self-review above it. The CLI owns the block so it can't drift from what
91
+ `/pb-status` reports; you supply only the judgment above it. It already drops the model
92
+ clause when the next step has none, and says so when no planned step remains — you
93
+ don't reproduce the template by hand.
94
+
95
+ **Then hand off with the next model.** Once the checkpoint lands in the approval turn,
96
+ run `plumbbob handoff` again and relay its boundary block — with the step gone from
97
+ in-flight it renders "step N checkpointed" and points at the next undone step, carrying
98
+ that step's `- model:` recommendation (the plan's smallest-model-that-fits call) so the
99
+ human knows which `/model` to select before firing `/pb-build` again. It is what
100
+ carries the plan's suggestion across a fresh context window, which inherits the session
101
+ model, not the plan's. No `- model:` line means any model will do. Guidance, never a
102
+ gate.
103
+
104
+ ## Running bound agents
105
+
106
+ Skip this section unless `plumbbob status` shows a `harness bindings:` block. Nearly every
107
+ build binds **no** agents — for them the default path above is the whole story. When a
108
+ harness *is* bound, this is what each affected step gains, in lifecycle order.
109
+
110
+ - **`before` — load context in (step 3).** If the build's `harness.json` binds agents to
111
+ this step's `before` slot, run `plumbbob agent run --step <n> --mode before` first: each
112
+ returns a validated envelope on stdout that plumbbob also appends to the step's
113
+ `handoff.json`, and its `summary`/`body` become **context you read into the build** (that
114
+ is the whole point of the slot — load the context in before you write). No binding, or no
115
+ harness, is a clean no-op — just build.
116
+ - **`build` — delegate the diff (step 4).** If a `build`-slot agent is bound, run
117
+ `plumbbob agent run --step <n> --mode build` and let that agent author the step's code
118
+ instead of writing it yourself; its envelope reports what it did. You still own the verify
119
+ tick — the diff is reviewed the same way whoever wrote it.
120
+ - **A manifest's `when` prose is your cue to fire an agent mid-build (step 4).** The three
121
+ slots are the only *declarative* lifecycle points; there is no config for "a salient
122
+ moment in the middle." That is judgment, and you are the frontier model in the room: when
123
+ the work reaches the situation a bound agent's `when` (or a step `note`) describes, fire
124
+ `plumbbob agent run <name> --step <n>` yourself. Prose is the orchestration language; you
125
+ are the workflow engine.
126
+ - **Route a non-`done` envelope by its status (step 4).** An agent that returns `blocked`
127
+ couldn't finish (missing input, failed precondition): surface its `notes`, let the human
128
+ unblock, and re-run it — don't work around it. One that returns `drift` finished but found
129
+ the plan no longer matches reality: **stop and send the human to `/pb-refine`** to repair
130
+ the plan before continuing, rather than building on a plan that's now wrong. A non-zero
131
+ exit is a failed run: report it and stop.
132
+ - **`after` — advisory input to the verify tick (step 5).** After `plumbbob check`, run any
133
+ bound `after`-agents (`plumbbob agent run --step <n> --mode after`) and fold their
134
+ envelopes into the self-review as **advisory input** — they inform, they never gate
135
+ (checkride gates, the human is the clock; an `after`-agent that could fail a step is the
136
+ lock in autonomy's costume).
103
137
 
104
138
  ## `--auto` — let the agent be the clock (opt-in)
105
139
 
@@ -107,8 +141,8 @@ differs from the model you're running as, say so before implementing — the hum
107
141
  progress instead of approving each step. It does the same work, but **the agent reviews
108
142
  and approves in the human's place**, and it **chains**:
109
143
 
110
- - Build the next step, running its slots in the same order as the default
111
- path: bound `before`-agents → implement (or the bound `build`-agent) → bound
144
+ - Build the next step, running its slots (see **§ Running bound agents**) in the same
145
+ order as the default path: bound `before`-agents → implement (or the bound `build`-agent) → bound
112
146
  `after`-agents → `check` → self-review → **if the check is green AND the
113
147
  self-review finds no done-when / Decision / Constraint mismatch, checkpoint** and move
114
148
  straight on to the next planned step. Repeat. `--auto` adds no new machinery — the
@@ -118,9 +152,10 @@ and approves in the human's place**, and it **chains**:
118
152
  bound agent returns `blocked` or `drift` (unblock-and-re-run, or `/pb-refine` — an
119
153
  agent cannot advance the loop), a new decision is needed, no planned steps
120
154
  remain, or the top of a requested range is reached. **When `--auto` halts back to the
121
- human, give the same hand-off the default pause does** — the step just completed, the
122
- next undone step, and that next step's `- model:` recommendation if it has one, so a
123
- fresh context window knows which `/model` to select.
155
+ human, give the same hand-off the default pause does** — run `plumbbob handoff` and
156
+ relay its block: the step just completed, the next undone step, and that next step's
157
+ `- model:` recommendation if it has one, so a fresh context window knows which `/model`
158
+ to select.
124
159
 
125
160
  `--auto` and a step range are the only paths that checkpoint without a human pause, and
126
161
  only because the human asked for it by name; a range re-imposes the pause at its top.
@@ -153,7 +188,8 @@ just the one more entry already in the halt list above.
153
188
  - **Swappable, never required.** The loop works without this skill; `/pb-verify`
154
189
  checkpoints a hand-built or vibed diff just the same.
155
190
  - **Build the decided step, not a new one.** Implement what `intent.md` settled. A
156
- new idea mid-build is a `/pb-park`, not an edit.
191
+ new idea mid-build is captured with `plumbbob park`, not an edit — a promise to
192
+ defer writes nothing; the park line does.
157
193
  - **Default ends at the pause.** Implement → verify → wait for approval; never
158
194
  checkpoint without it. Only an explicit `--auto` or a step range lets the agent approve
159
195
  in your place, and it still halts on a red check or any mismatch — a range also stops
@@ -161,13 +197,17 @@ just the one more entry already in the halt list above.
161
197
  - **Agents feed the beat; they never advance it**. `before` loads context,
162
198
  `build` writes the diff, `after` is advisory — none can checkpoint, flip a step, or
163
199
  chain. `blocked` → unblock and re-run; `drift` → `/pb-refine`. You are still the one
164
- who verifies and (bar `--auto`) the human is still the clock.
200
+ who verifies and (bar `--auto`) the human is still the clock. See **§ Running bound
201
+ agents** for the mechanics.
165
202
  - **A refused checkpoint is the pause, not an error.** Under plumbbob's turn
166
- hook a same-turn `checkpoint` is refused by design — present the diff, **end the
167
- turn**, and the human's next message is the tick that lands it on re-fire. Never route
168
- around it with a raw `git commit`; `--auto`, a typed range, or `auto: true` are the
169
- only self-approvals.
170
- - **Close with the next model.** When a step lands, cite the completed step and the next
171
- undone step, and name that next step's `- model:` recommendation if it has one — it's
172
- what a fresh context window needs to pick the right `/model` before re-firing.
173
- Guidance, never a gate.
203
+ hook a same-turn `checkpoint` is refused by design — present the diff and the closing
204
+ block, **end the turn**; the human's approval on their next turn is what lets you land
205
+ it, and landing it is a deliberate beat, not a side effect of the next `/pb-build`.
206
+ Never route around it with a raw `git commit`. An explicit `/pb-build --auto` or a typed
207
+ step range in the human's own prompt are the only self-approvals — **never write `auto`
208
+ into a settings file yourself to unlock a checkpoint. A grant you mint is no grant (the
209
+ latch ignores it since D67); ask the human to re-fire `/pb-build --auto` instead.**
210
+ - **Close with the next model.** When a step lands, run `plumbbob handoff` and relay its
211
+ block — it cites the completed step and the next undone step, and names that next step's
212
+ `- model:` recommendation if it has one, which is what a fresh context window needs to
213
+ pick the right `/model` before re-firing. Guidance, never a gate.
@@ -8,7 +8,7 @@ allowed-tools: Bash(plumbbob doctor:*)
8
8
 
9
9
  # PlumbBob — doctor (the is-it-installed-right move)
10
10
 
11
- Install diagnostic (injected when this skill runs): !`if command -v plumbbob >/dev/null 2>&1; then plumbbob doctor 2>&1; else echo "plumbbob CLI not on PATH in this session. Marketplace install: confirm the plugin is enabled in /plugin, then /reload-plugins. Skills-dir/global install: npm i -g plumbbob && plumbbob init."; fi`
11
+ Install diagnostic (injected when this skill runs): !`plumbbob doctor 2>&1 || true`
12
12
 
13
13
  This is a **thin driver** for `plumbbob doctor`. The report above is read-only — `doctor`
14
14
  inspects, it never writes. This skill carries **no Edit and no Write tool**: never edit a file
@@ -34,6 +34,10 @@ and if you are reading this, that is not your situation.
34
34
  ## What it does
35
35
 
36
36
  1. Surface the injected `doctor` report verbatim — every `✓` and any `✗` with its `→ fix`.
37
+ If nothing rendered above (a few headless `-p` sessions skip the injection), just run
38
+ `plumbbob doctor` yourself — it is in your tools — and surface that.
37
39
  2. If all checks passed, say so; if a skill still misbehaves, the fix is a restart, not a re-run.
38
40
  3. If a check failed, apply its named `→ fix`, then restart Claude Code (or `/reload-plugins`).
39
- 4. If the line above says the CLI is not on PATH, follow the install-path branch it printed.
41
+ 4. If the report is empty or shows `command not found`, the CLI is not on PATH: for a
42
+ skills-dir / global install run `npm i -g plumbbob && plumbbob init`; for a marketplace
43
+ install, confirm the plugin is enabled in `/plugin`, then `/reload-plugins`.
@@ -38,4 +38,7 @@ later. Then:
38
38
  The capture itself is the **dumb CLI**, never an edit. This skill carries **no Edit and
39
39
  no Write tool** on purpose: you may not append to `build-log.md` yourself. Compose, get
40
40
  approval, then shell `plumbbob park` — that is the only write path. If approval
41
- never comes, capture nothing.
41
+ never comes, capture nothing. (When the human's **own message** states the idea — a
42
+ tangent they raise mid-build — that message **is** the approval: capture it directly
43
+ with `plumbbob park`, don't ask again. Deferring in words without the CLI line captures
44
+ nothing.)
@@ -1,6 +1,6 @@
1
1
  ---
2
2
  name: pb-plan
3
- description: "Frame a fresh goal and author the whole plan — Frame, Decisions, Constraints, and all Steps — before any code. Three input modes: no arg interviews you; a file path absorbs a spec; any other text expands your inline intent."
3
+ description: "Frame a fresh goal and author the whole plan — Frame, Decisions, Constraints, and all Steps — before any code. Three input modes: no arg interviews you; a file path (or @-mention) absorbs a spec; any other text expands your inline intent."
4
4
  argument-hint: "[spec-path | intent]"
5
5
  disable-model-invocation: true
6
6
  allowed-tools: Read, Edit, Write, Bash(plumbbob status:*), Bash(plumbbob start:*), Bash(plumbbob checkpoint:*), Bash(plumbbob agent list:*)
@@ -27,11 +27,15 @@ Look at the argument the human gave and pick the mode yourself:
27
27
 
28
28
  1. **No argument → interview.** Walk the human through a short, friendly Q&A to draw
29
29
  the plan out of their head (see *The interview* below).
30
- 2. **The argument is a path to a file that exists → absorb the spec.** Read that file
31
- and distill it into `intent.md`, **retaining enough detail that `intent.md` stands
32
- on its own** — don't just link to the source. Add a one-line provenance
33
- (`*Source: <path>*`) and, for anything sizable, a `## Source` appendix preserving
34
- the original text. (Probe with the `Read` tool; if it isn't a real file, fall to
30
+ 2. **The argument points at a file → absorb the spec.** A bare path, an `@spec.md`
31
+ mention, or a path wrapped in a sentence all count. Strip any leading `@` and probe
32
+ the token with the `Read` tool; if Claude Code has already injected the referenced
33
+ file's contents into your context, treat that as the spec rather than re-fetching.
34
+ Read the spec and distill it into `intent.md`, **retaining enough detail that
35
+ `intent.md` stands on its own** — don't just link to the source. Add a one-line
36
+ provenance (`*Source: <path>*`) and, for anything sizable, a `## Source` appendix
37
+ preserving the original text. Any prose wrapped around the reference (e.g. "absorb
38
+ @spec.md") is extra intent — fold it in. (If the token isn't a real file, fall to
35
39
  mode 3.)
36
40
  3. **Any other text → expand the inline intent.** Treat the text as the human's
37
41
  rough plan, expand it into the full `intent.md`, and ask only about what is
@@ -2,7 +2,7 @@
2
2
  name: pb-verify
3
3
  description: "The verify tick — run the check, self-review the diff against intent, validate the step's done-when, pause for your approval, then checkpoint. Executor-agnostic: it reads the diff, not who wrote it."
4
4
  disable-model-invocation: true
5
- allowed-tools: Read, Bash(plumbbob status:*), Bash(plumbbob check:*), Bash(plumbbob checkpoint:*), Bash(plumbbob agent:*), Bash(git diff:*), Bash(git status:*)
5
+ allowed-tools: Read, Bash(plumbbob status:*), Bash(plumbbob handoff:*), Bash(plumbbob check:*), Bash(plumbbob checkpoint:*), Bash(plumbbob agent:*), Bash(git diff:*), Bash(git status:*)
6
6
  ---
7
7
 
8
8
  # PlumbBob — verify a step (the tick)
@@ -63,15 +63,14 @@ this skill verifies it the same way: **it reads the diff, not the author**.
63
63
  (done-when + seam + diffstat) on its own. Either way, do **not** bump the version or
64
64
  touch the changelog — that is the human's `/version` call.
65
65
  7. **Hand off with the next model** *(once the checkpoint lands)*. `plumbbob checkpoint`
66
- prints `step N checkpointed … Back at the boundary` and returns to DESIGN. Close the
67
- turn by citing where the loop now stands: the step you just completed and the **next
68
- undone step** — its title and done-when. If that next step carries a `- model:`
69
- recommendation (the plan's smallest-model-that-fits call, which `plumbbob status`
70
- echoes on the `▸ next` line), **name it in the hand-off** so the human knows which
71
- `/model` to select before firing `/pb-build` again. This matters most across a context
72
- boundary: a fresh window inherits the *session's* model, not the plan's suggestion, so
73
- this line is what carries the recommendation over. No `- model:` line means any model
74
- will do — say nothing about it. Guidance, never a gate.
66
+ returns to DESIGN; then run `plumbbob handoff` and relay its boundary block. With the
67
+ step gone from in-flight it renders `step N checkpointed — back at the boundary` and
68
+ points at the **next undone step**, carrying that step's `- model:` recommendation (the
69
+ plan's smallest-model-that-fits call) so the human knows which `/model` to select before
70
+ firing `/pb-build` again. This matters most across a context boundary: a fresh window
71
+ inherits the *session's* model, not the plan's suggestion, so this line is what carries
72
+ the recommendation over. The CLI owns the block, so it can't drift from what `plumbbob
73
+ status` reports; no `- model:` line means any model will do. Guidance, never a gate.
75
74
 
76
75
  ## The latch makes the pause real
77
76
 
@@ -84,8 +83,9 @@ the checkpoint land when you re-fire. **Never reach for a raw `git commit` to fo
84
83
  land** — that forges the very record the latch exists to keep honest, and the
85
84
  commit-ask hook asks the human about it anyway. The refusal is a healthy latch doing its
86
85
  job on the *record* while the *work* plane stays free; a `/pb-build --auto` or
87
- a typed step range in the human's own prompt is the only self-approval, and `auto: true`
88
- in settings is the standing one.
86
+ a typed step range in the human's own prompt is the only self-approval — **never write
87
+ `auto` into a settings file to unlock the land; the latch ignores a model-minted grant
88
+ (D67), so ask the human to re-fire `/pb-build --auto` instead.**
89
89
 
90
90
  ## The hard contracts
91
91
 
@@ -104,7 +104,7 @@ in settings is the standing one.
104
104
  hook a same-turn checkpoint is refused *by design* — present the diff, **end the
105
105
  turn**, and let the human's next message re-tick it. Never route around it with a raw
106
106
  `git commit`; the latch guards the record, not the work.
107
- - **Close with the next model.** After the checkpoint lands, cite the completed step
108
- and the next undone step, and surface that next step's `- model:` recommendation when
109
- it has one — it is the plan's smallest-model-that-fits call, and it's what a fresh
110
- context window needs to pick the right `/model`. Guidance, never a gate.
107
+ - **Close with the next model.** After the checkpoint lands, run `plumbbob handoff` and
108
+ relay its block — it cites the completed step and the next undone step, and surfaces that
109
+ next step's `- model:` recommendation when it has one, which is what a fresh context
110
+ window needs to pick the right `/model`. Guidance, never a gate.