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.
- package/.claude-plugin/plugin.json +1 -1
- package/README.md +5 -2
- package/dist/lib/latch.js +23 -16
- package/dist/lib/settings.js +7 -4
- package/dist/verbs/agent.js +3 -0
- package/dist/verbs/doctor.js +15 -6
- package/dist/verbs/turn.js +47 -11
- package/hooks/post-edit.sh +0 -0
- package/hooks/pre-bash-commit.sh +0 -0
- package/node_modules/checkride/dist/cli.js +0 -0
- package/package.json +12 -18
- package/skills/pb-build/SKILL.md +14 -8
- package/skills/pb-doctor/SKILL.md +6 -2
- package/skills/pb-park/SKILL.md +4 -1
- package/skills/pb-plan/SKILL.md +10 -6
- package/skills/pb-verify/SKILL.md +3 -2
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "plumbbob",
|
|
3
3
|
"displayName": "PlumbBob",
|
|
4
|
-
"version": "0.8.
|
|
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 [`
|
|
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
|
|
6
|
-
// ledger is dormant.
|
|
7
|
-
// message *is* the pause affordance.
|
|
8
|
-
//
|
|
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
|
|
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. (
|
|
28
|
-
|
|
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
|
|
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; //
|
|
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; //
|
|
52
|
-
//
|
|
53
|
-
|
|
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) {
|
package/dist/lib/settings.js
CHANGED
|
@@ -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 —
|
|
12
|
-
//
|
|
13
|
-
//
|
|
14
|
-
//
|
|
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';
|
package/dist/verbs/agent.js
CHANGED
|
@@ -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`
|
package/dist/verbs/doctor.js
CHANGED
|
@@ -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
|
-
|
|
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
|
-
|
|
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);
|
package/dist/verbs/turn.js
CHANGED
|
@@ -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.
|
|
7
|
-
//
|
|
8
|
-
//
|
|
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
|
|
39
|
-
//
|
|
40
|
-
//
|
|
41
|
-
//
|
|
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)
|
|
63
|
-
continue; // a step number
|
|
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}`;
|
package/hooks/post-edit.sh
CHANGED
|
File without changes
|
package/hooks/pre-bash-commit.sh
CHANGED
|
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
|
+
"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
|
-
"
|
|
38
|
-
"
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
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
|
-
|
|
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
|
+
}
|
package/skills/pb-build/SKILL.md
CHANGED
|
@@ -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
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
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
|
|
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
|
|
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): !`
|
|
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
|
|
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`.
|
package/skills/pb-park/SKILL.md
CHANGED
|
@@ -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.)
|
package/skills/pb-plan/SKILL.md
CHANGED
|
@@ -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
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
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
|
|
87
|
-
|
|
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
|
|