plumbbob 0.8.5 → 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.5",
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/lib/latch.js CHANGED
@@ -2,10 +2,12 @@
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";
@@ -19,18 +21,22 @@ const NO_TURN_MESSAGE = `plumbbob: checkpoint refused — no human turn since th
19
21
  pause: present the diff and the self-review, then end the turn. The human's approval on
20
22
  their next turn is what lets this land — re-run \`checkpoint\` then and stop at the
21
23
  boundary; \`/pb-build\` only starts the next step, it never lands this one. (An explicit
22
- \`/pb-build --auto\` or a step range in the human's own prompt grants self-approval;
23
- \`auto: true\` in settings.local.json is the standing grant.)
24
+ \`/pb-build --auto\` or a step range in the human's own prompt is the only self-approval.)
24
25
  `;
25
26
  const NO_TURN_PLAN_MESSAGE = `plumbbob: checkpoint refused — no human turn since \`start\` stamped this plan. This
26
27
  is the plan pause: present the plan, then end the turn; the human's approving
27
- message is the tick that lets it land on re-fire. (\`auto: true\` in
28
- 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.
29
35
  `;
30
36
  function ceilingMessage(ceiling) {
31
37
  return `plumbbob: checkpoint refused — the range you granted ends at step ${ceiling} — pause here; re-fire to continue.\n`;
32
38
  }
33
- // 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
34
40
  // numbered steps: on a `--plan` checkpoint (step null) it neither allows nor
35
41
  // refuses, and the decision falls through to the turn comparison.
36
42
  export function evaluateLatch(input) {
@@ -38,19 +44,19 @@ export function evaluateLatch(input) {
38
44
  return ALLOW; // 1 — a human at the keyboard
39
45
  if (input.turn === null || input.tick === null)
40
46
  return ALLOW; // 2 — dormant / hand-built
41
- if (input.auto)
42
- return ALLOW; // 3 — standing personal grant (D27)
43
47
  if (input.grant?.kind === 'auto')
44
- return ALLOW; // 4 — one-turn grant
48
+ return ALLOW; // 3 — one-turn grant (D65)
45
49
  if (input.grant?.kind === 'range' && input.step !== null) {
46
50
  if (input.step <= input.grant.ceiling)
47
51
  return ALLOW;
48
52
  return { allow: false, reason: 'ceiling', message: ceilingMessage(input.grant.ceiling) };
49
53
  }
50
54
  if (input.turn > input.tick)
51
- return ALLOW; // 5 — a human turn intervened
52
- // 6 — the pause (plan-worded when no step is landing)
53
- 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 };
54
60
  }
55
61
  // Parse a GRANT file's content (`auto` | `range <M>`). Anything else — including
56
62
  // a corrupted or hand-mangled file — contributes nothing (D27): no grant, never
@@ -72,9 +78,10 @@ export function checkLatch(root, step) {
72
78
  isTTY: process.stdin.isTTY === true,
73
79
  turn: readCount(turnPath(root)),
74
80
  tick: readCount(tickPath(root)),
75
- auto: resolveBoolean(root, 'auto', false),
76
81
  grant: readGrant(root),
77
82
  step,
83
+ // Still read from the ladder — but only to decorate the pause (D67), not to allow.
84
+ settingsAuto: resolveBoolean(root, 'auto', false),
78
85
  });
79
86
  }
80
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,6 +99,9 @@ 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),
104
107
  // Hand this agent its own config block over the frozen envelope `settings`
@@ -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);
@@ -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.5",
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,12 +34,15 @@
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
48
  "@ast-grep/cli": "0.43.0",
@@ -59,14 +62,5 @@
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 handoff:*), 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)
@@ -43,10 +43,13 @@ differs from the model you're running as, say so before implementing — the hum
43
43
  `harness bindings:` block, this build binds agents — see **§ Running bound agents**
44
44
  and load their context in before you write. No bindings, no harness: just build.
45
45
  4. **Implement** the step, and only that step, staying within the declared seam. A
46
- new problem or "ooh what if" that surfaces mid-build is a `/pb-park`, **not** an
47
- edit — capture it and stay on the step. If you genuinely cannot finish without
48
- touching more than the seam, that is scope drift: surface it to the human rather
49
- than sprawling.
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.
50
53
  - **Bound agents can shape the build.** If `plumbbob status` shows a
51
54
  `harness bindings:` block, a `build`-slot agent may author the diff, a `when`-cue may
52
55
  fire an agent mid-build, and a non-`done` envelope routes by its status — see
@@ -185,7 +188,8 @@ just the one more entry already in the halt list above.
185
188
  - **Swappable, never required.** The loop works without this skill; `/pb-verify`
186
189
  checkpoints a hand-built or vibed diff just the same.
187
190
  - **Build the decided step, not a new one.** Implement what `intent.md` settled. A
188
- 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.
189
193
  - **Default ends at the pause.** Implement → verify → wait for approval; never
190
194
  checkpoint without it. Only an explicit `--auto` or a step range lets the agent approve
191
195
  in your place, and it still halts on a red check or any mismatch — a range also stops
@@ -199,8 +203,10 @@ just the one more entry already in the halt list above.
199
203
  hook a same-turn `checkpoint` is refused by design — present the diff and the closing
200
204
  block, **end the turn**; the human's approval on their next turn is what lets you land
201
205
  it, and landing it is a deliberate beat, not a side effect of the next `/pb-build`.
202
- Never route around it with a raw `git commit`; `--auto`, a typed range, or `auto: true`
203
- are the only self-approvals.
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.**
204
210
  - **Close with the next model.** When a step lands, run `plumbbob handoff` and relay its
205
211
  block — it cites the completed step and the next undone step, and names that next step's
206
212
  `- model:` recommendation if it has one, which is what a fresh context window needs to
@@ -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
@@ -83,8 +83,9 @@ the checkpoint land when you re-fire. **Never reach for a raw `git commit` to fo
83
83
  land** — that forges the very record the latch exists to keep honest, and the
84
84
  commit-ask hook asks the human about it anyway. The refusal is a healthy latch doing its
85
85
  job on the *record* while the *work* plane stays free; a `/pb-build --auto` or
86
- a typed step range in the human's own prompt is the only self-approval, and `auto: true`
87
- 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.**
88
89
 
89
90
  ## The hard contracts
90
91