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.
- package/.claude-plugin/plugin.json +1 -1
- package/README.md +5 -2
- package/dist/cli-core.js +5 -1
- package/dist/lib/latch.js +31 -22
- package/dist/lib/settings.js +7 -4
- package/dist/verbs/agent.js +8 -0
- package/dist/verbs/build.js +24 -4
- package/dist/verbs/doctor.js +15 -6
- package/dist/verbs/handoff.js +103 -0
- 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 +22 -28
- package/skills/pb-build/SKILL.md +106 -66
- 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 +16 -16
|
@@ -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/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
|
|
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
|
|
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";
|
|
12
14
|
const ALLOW = { allow: true };
|
|
13
|
-
// The
|
|
14
|
-
// it
|
|
15
|
-
//
|
|
16
|
-
// at plan time — what the human approves
|
|
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
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
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. (
|
|
26
|
-
|
|
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
|
|
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; //
|
|
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; //
|
|
50
|
-
//
|
|
51
|
-
|
|
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) {
|
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,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) {
|
package/dist/verbs/build.js
CHANGED
|
@@ -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
|
-
|
|
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
|
|
28
|
-
|
|
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
|
-
|
|
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
|
}
|
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);
|
|
@@ -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
|
+
}
|
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,39 +34,33 @@
|
|
|
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
|
-
"@ast-grep/cli": "
|
|
46
|
-
"@stryker-mutator/core": "
|
|
47
|
-
"@stryker-mutator/vitest-runner": "
|
|
48
|
-
"@types/node": "
|
|
49
|
-
"@vitest/coverage-v8": "
|
|
50
|
-
"fallow": "
|
|
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": "
|
|
53
|
-
"oxlint": "
|
|
54
|
-
"typescript": "
|
|
55
|
-
"vitest": "
|
|
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
|
-
|
|
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 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,
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
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
|
-
- **
|
|
42
|
-
|
|
43
|
-
|
|
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
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
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) →
|
|
73
|
-
|
|
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
|
|
91
|
-
human's next message
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
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
|
|
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** —
|
|
122
|
-
next undone step, and that next step's
|
|
123
|
-
fresh context window knows which `/model`
|
|
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
|
|
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
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
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): !`
|
|
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
|
|
@@ -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
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
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
|
|
88
|
-
|
|
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,
|
|
108
|
-
and the next undone step, and
|
|
109
|
-
|
|
110
|
-
|
|
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.
|