plumbbob 0.8.0 → 0.8.5
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/dist/cli-core.js +5 -1
- package/dist/lib/latch.js +10 -8
- package/dist/lib/settings.js +3 -9
- package/dist/lib/sidecar.js +53 -18
- package/dist/verbs/agent.js +5 -0
- package/dist/verbs/build.js +24 -4
- package/dist/verbs/doctor.js +8 -4
- package/dist/verbs/finish.js +4 -10
- package/dist/verbs/handoff.js +103 -0
- package/dist/verbs/start.js +16 -5
- package/dist/verbs/use.js +4 -5
- package/hooks/post-edit.sh +7 -7
- package/hooks/pre-bash-commit.sh +7 -8
- package/package.json +11 -11
- package/skills/pb-build/SKILL.md +98 -48
- package/skills/pb-finish/SKILL.md +2 -1
- package/skills/pb-plan/SKILL.md +1 -1
- package/skills/pb-verify/SKILL.md +17 -4
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "plumbbob",
|
|
3
3
|
"displayName": "PlumbBob",
|
|
4
|
-
"version": "0.8.
|
|
4
|
+
"version": "0.8.5",
|
|
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/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
|
@@ -10,15 +10,17 @@ import { readFileSync } from 'node:fs';
|
|
|
10
10
|
import { grantPath, tickPath, turnPath } from "./sidecar.js";
|
|
11
11
|
import { resolveBoolean } from "./settings.js";
|
|
12
12
|
const ALLOW = { allow: true };
|
|
13
|
-
// The
|
|
14
|
-
// it
|
|
15
|
-
//
|
|
16
|
-
// at plan time — what the human approves
|
|
13
|
+
// The refusal message is the affordance — it tells the model what the pause is,
|
|
14
|
+
// how it ends (approval on the next turn lands it; `/pb-build` only starts the next
|
|
15
|
+
// step), and names the only legitimate grants. The plan commit gets its own wording:
|
|
16
|
+
// there is no step, no diff, and no self-review at plan time — what the human approves
|
|
17
|
+
// is the plan itself.
|
|
17
18
|
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
|
-
|
|
19
|
+
pause: present the diff and the self-review, then end the turn. The human's approval on
|
|
20
|
+
their next turn is what lets this land — re-run \`checkpoint\` then and stop at the
|
|
21
|
+
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.)
|
|
22
24
|
`;
|
|
23
25
|
const NO_TURN_PLAN_MESSAGE = `plumbbob: checkpoint refused — no human turn since \`start\` stamped this plan. This
|
|
24
26
|
is the plan pause: present the plan, then end the turn; the human's approving
|
package/dist/lib/settings.js
CHANGED
|
@@ -9,9 +9,9 @@
|
|
|
9
9
|
//
|
|
10
10
|
// Known keys: `check` (string — the heavy gate, tracked in settings.json) and
|
|
11
11
|
// `auto` (boolean — whether the agent approves in the human's place; a personal
|
|
12
|
-
// preference, so it belongs in settings.local.json).
|
|
13
|
-
//
|
|
14
|
-
//
|
|
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.
|
|
15
15
|
import { mkdirSync, readFileSync, writeFileSync } from 'node:fs';
|
|
16
16
|
import { dirname, join } from 'node:path';
|
|
17
17
|
const DIRNAME = '.plumbbob';
|
|
@@ -70,12 +70,6 @@ export function resolveRecord(root, key) {
|
|
|
70
70
|
const value = resolveSetting(root, key, undefined);
|
|
71
71
|
return typeof value === 'object' && value !== null && !Array.isArray(value) ? value : {};
|
|
72
72
|
}
|
|
73
|
-
// Read one key from the untracked local overlay ONLY — no project or built-in
|
|
74
|
-
// fallback. The `activeBuild` cursor lives here and must never resolve from the
|
|
75
|
-
// tracked settings.json (it is per-worktree state, not a shared default).
|
|
76
|
-
export function localSetting(root, key) {
|
|
77
|
-
return readSettings(localSettingsPath(root))[key];
|
|
78
|
-
}
|
|
79
73
|
// Merge one key into settings.local.json, preserving the other keys and creating
|
|
80
74
|
// the file when absent. Pretty-printed so the overlay stays hand-editable. A
|
|
81
75
|
// malformed existing file contributes nothing (readSettings yields {}) and is
|
package/dist/lib/sidecar.js
CHANGED
|
@@ -2,14 +2,18 @@
|
|
|
2
2
|
// read it with a grep and no markdown parsing (D7). Functional/procedural,
|
|
3
3
|
// node builtins only (C1/C2).
|
|
4
4
|
//
|
|
5
|
-
// STATE is
|
|
6
|
-
//
|
|
7
|
-
//
|
|
8
|
-
//
|
|
5
|
+
// STATE is the session sentinel AND the active-build cursor (D28): its EXISTENCE
|
|
6
|
+
// means "a session is active", and its CONTENT names the build that session is on —
|
|
7
|
+
// the two reinforce each other (a session is always on some build; the cursor is
|
|
8
|
+
// meaningless without a session). Content is empty under --local / no build.
|
|
9
|
+
// hasSession stays existence-only; activeBuild reads the content. Homing the cursor
|
|
10
|
+
// here — not in settings.local.json — keeps that overlay purely human-owned
|
|
11
|
+
// (check/auto): the tool only ever reads it, never rewrites it. The phase the
|
|
12
|
+
// dashboard shows (DESIGN/BUILD/SPIKE) is derived, not stored — BUILD ⇔ a STEP is
|
|
13
|
+
// in flight, SPIKE ⇔ the SPIKE marker is present, otherwise DESIGN.
|
|
9
14
|
import { existsSync, readdirSync, readFileSync, writeFileSync, appendFileSync, rmSync, mkdirSync } from 'node:fs';
|
|
10
15
|
import { dirname, join } from 'node:path';
|
|
11
16
|
import { gitPath } from "./git.js";
|
|
12
|
-
import { localSetting } from "./settings.js";
|
|
13
17
|
const DIRNAME = '.plumbbob';
|
|
14
18
|
export function sidecarDir(root) {
|
|
15
19
|
return join(root, DIRNAME);
|
|
@@ -50,19 +54,35 @@ export function listBuilds(root) {
|
|
|
50
54
|
}
|
|
51
55
|
}
|
|
52
56
|
// Resolve which build a verb acts on (D28): an explicit `--build <slug>` flag →
|
|
53
|
-
// the
|
|
54
|
-
//
|
|
55
|
-
//
|
|
56
|
-
// never point at two builds (D28).
|
|
57
|
+
// the cursor in STATE → the sole build in `builds/` → null (the caller then refuses
|
|
58
|
+
// with a hint). One-active-per-worktree holds by construction: the cursor is STATE's
|
|
59
|
+
// single-line content, so it can never point at two builds (D28).
|
|
57
60
|
export function activeBuild(root, flag) {
|
|
58
61
|
if (flag !== undefined && flag.length > 0)
|
|
59
62
|
return flag;
|
|
60
|
-
const cursor =
|
|
61
|
-
if (
|
|
63
|
+
const cursor = readCursor(root);
|
|
64
|
+
if (cursor !== null)
|
|
62
65
|
return cursor;
|
|
63
66
|
const builds = listBuilds(root);
|
|
64
67
|
return builds.length === 1 ? (builds[0] ?? null) : null;
|
|
65
68
|
}
|
|
69
|
+
// STATE's content is the active-build cursor. Empty means "no build" (--local, or a
|
|
70
|
+
// session whose cursor is unset) → null, so activeBuild falls through to the
|
|
71
|
+
// sole-build rule. A pre-STATE-cursor session wrote the literal `active` sentinel
|
|
72
|
+
// here (the cursor lived in settings.local.json then); treat that legacy value as
|
|
73
|
+
// unset so an in-flight upgrade degrades gracefully instead of chasing a build named
|
|
74
|
+
// "active". `active` is thus reserved and never a valid cursor — no derived slug
|
|
75
|
+
// collides (they carry a YYYY-MM-DD- prefix).
|
|
76
|
+
function readCursor(root) {
|
|
77
|
+
let content;
|
|
78
|
+
try {
|
|
79
|
+
content = readFileSync(statePath(root), 'utf8').trim();
|
|
80
|
+
}
|
|
81
|
+
catch {
|
|
82
|
+
return null;
|
|
83
|
+
}
|
|
84
|
+
return content.length > 0 && content !== 'active' ? content : null;
|
|
85
|
+
}
|
|
66
86
|
// The build a verb should act on, plus its argv with the `--build <slug>` pair
|
|
67
87
|
// stripped (D28). Every verb resolves through this: an explicit `--build <slug>`
|
|
68
88
|
// wins, else the cursor / sole-build fallback of `activeBuild`. `rest` matters
|
|
@@ -197,12 +217,26 @@ function patchStepStat(root, slug, step, patch) {
|
|
|
197
217
|
}
|
|
198
218
|
// A session exists iff STATE exists. Deleting STATE (at finish) is what flips the
|
|
199
219
|
// repo back to "no session" — so it is the single source of truth for "is there
|
|
200
|
-
// a session". `start` calls beginSession; `finish` removes the file.
|
|
220
|
+
// a session". `start` calls beginSession; `finish` removes the file. Existence is
|
|
221
|
+
// the whole session signal; STATE's content is a separate axis (the cursor).
|
|
201
222
|
export function hasSession(root) {
|
|
202
223
|
return existsSync(statePath(root));
|
|
203
224
|
}
|
|
204
|
-
|
|
205
|
-
|
|
225
|
+
// Open the session and point its cursor at `slug` — null under --local / no build,
|
|
226
|
+
// which leaves STATE present but empty so activeBuild falls through to the sole-build
|
|
227
|
+
// rule. `finish` removes the file, closing the session and clearing the cursor in
|
|
228
|
+
// one delete.
|
|
229
|
+
export function beginSession(root, slug = null) {
|
|
230
|
+
writeCursor(root, slug);
|
|
231
|
+
}
|
|
232
|
+
// Re-point the cursor at an existing build (`use`) — a plain content rewrite that
|
|
233
|
+
// leaves the session sentinel (STATE's existence) intact. Callers guard hasSession
|
|
234
|
+
// first, so this never resurrects a finished session.
|
|
235
|
+
export function setActiveBuild(root, slug) {
|
|
236
|
+
writeCursor(root, slug);
|
|
237
|
+
}
|
|
238
|
+
function writeCursor(root, slug) {
|
|
239
|
+
writeFileSync(statePath(root), slug === null ? '' : `${slug}\n`);
|
|
206
240
|
}
|
|
207
241
|
// SPIKE marker helpers — existence is the whole signal (content is irrelevant).
|
|
208
242
|
export function inSpike(root, slug) {
|
|
@@ -309,10 +343,11 @@ function addExcludes(root, patterns) {
|
|
|
309
343
|
appendFileSync(exclude, `${prefix}${missing.join('\n')}\n`);
|
|
310
344
|
}
|
|
311
345
|
// The narrowed control plane (D17): with `builds/<slug>/` now tracked, only the
|
|
312
|
-
// per-worktree control files stay git-excluded — the
|
|
313
|
-
//
|
|
314
|
-
// inside every build. Everything else under `.plumbbob/`
|
|
315
|
-
// build's intent/build-log/checkpoints/report) rides the
|
|
346
|
+
// per-worktree control files stay git-excluded — the session sentinel STATE (whose
|
|
347
|
+
// content is the active-build cursor, D28), the personal settings overlay, and the
|
|
348
|
+
// in-flight step markers inside every build. Everything else under `.plumbbob/`
|
|
349
|
+
// (settings.json, and each build's intent/build-log/checkpoints/report) rides the
|
|
350
|
+
// branch into the PR.
|
|
316
351
|
export function excludeControl(root) {
|
|
317
352
|
addExcludes(root, [
|
|
318
353
|
`${DIRNAME}/STATE`,
|
package/dist/verbs/agent.js
CHANGED
|
@@ -101,6 +101,11 @@ async function runOne(root, slug, step, spec) {
|
|
|
101
101
|
settings: {
|
|
102
102
|
auto: resolveBoolean(root, 'auto', false),
|
|
103
103
|
agentTimeout: resolveNumber(root, 'agentTimeout', 0),
|
|
104
|
+
// Hand this agent its own config block over the frozen envelope `settings`
|
|
105
|
+
// field (D5/D6): settings.json → agentConfig[name], with the local overlay
|
|
106
|
+
// shadowing the project rung whole (D7 — no deep merge), and {} when neither
|
|
107
|
+
// defines it. No new envelope field, no new verb — the config just rides here.
|
|
108
|
+
agent: resolveRecord(root, 'agentConfig')[spec.name] ?? {},
|
|
104
109
|
},
|
|
105
110
|
});
|
|
106
111
|
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
|
@@ -31,8 +31,8 @@ import { listAgents } from "../lib/agents.js";
|
|
|
31
31
|
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
|
-
import { buildDir, excludeControl, listBuilds, readTurn, sidecarDir, slugify } from "../lib/sidecar.js";
|
|
35
|
-
import { resolveString, settingsPath
|
|
34
|
+
import { buildDir, excludeControl, listBuilds, readTurn, setActiveBuild, sidecarDir, slugify } from "../lib/sidecar.js";
|
|
35
|
+
import { 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) {
|
|
@@ -206,8 +206,12 @@ export function migrateSidecar(root) {
|
|
|
206
206
|
const configPath = join(dir, 'config');
|
|
207
207
|
if (existsSync(configPath)) {
|
|
208
208
|
if (!existsSync(settingsPath(root))) {
|
|
209
|
+
// Carry forward ONLY what the legacy config actually held — a `check` line,
|
|
210
|
+
// nothing else. No invented `auto` (the config never had one, it defaults to
|
|
211
|
+
// false, and it belongs in settings.local.json): migration yields exactly what
|
|
212
|
+
// a fresh `start` would, plus the check. An empty config → an empty `{}`.
|
|
209
213
|
const check = configCheck(configPath);
|
|
210
|
-
const settings = check === null ? {
|
|
214
|
+
const settings = check === null ? {} : { check };
|
|
211
215
|
writeFileSync(settingsPath(root), `${JSON.stringify(settings, null, 2)}\n`);
|
|
212
216
|
actions.push(check === null ? 'config → settings.json' : `config → settings.json (check: ${check})`);
|
|
213
217
|
}
|
|
@@ -220,7 +224,7 @@ export function migrateSidecar(root) {
|
|
|
220
224
|
const slug = uniqueSlug(slugify(titleFromIntent(flatIntent)), taken);
|
|
221
225
|
taken.add(slug);
|
|
222
226
|
moveInto(dir, buildDir(root, slug), ['intent.md', 'build-log.md', 'checkpoints', 'STEP', 'SEAM', 'SPIKE']);
|
|
223
|
-
|
|
227
|
+
setActiveBuild(root, slug);
|
|
224
228
|
actions.push(`active session → builds/${slug} (the cursor)`);
|
|
225
229
|
}
|
|
226
230
|
// archive/<slug> → builds/<slug>. These are "done" by simply not being the cursor.
|
package/dist/verbs/finish.js
CHANGED
|
@@ -9,7 +9,6 @@ import { appendFileSync, existsSync, readFileSync, rmSync } from 'node:fs';
|
|
|
9
9
|
import { join } from 'node:path';
|
|
10
10
|
import { commit, findRepoRoot, isDirty, stageAll } from "../lib/git.js";
|
|
11
11
|
import { checkpointsPath, clearTick, hasSession, intentPath, readStats, reportPath, resolveBuild, seamPath, setGrant, sidecarDir, spikePath, stepPath, } from "../lib/sidecar.js";
|
|
12
|
-
import { setLocalSetting } from "../lib/settings.js";
|
|
13
12
|
import { parseTitle } from "../lib/orient.js";
|
|
14
13
|
export function finish(cwd, args = []) {
|
|
15
14
|
const root = findRepoRoot(cwd);
|
|
@@ -35,9 +34,10 @@ export function finish(cwd, args = []) {
|
|
|
35
34
|
stageAll(root);
|
|
36
35
|
}
|
|
37
36
|
const sha = commit(root, subject(root, slug), bodyArg(args) ?? undefined);
|
|
38
|
-
// Clear the control state: the in-flight markers, the
|
|
39
|
-
//
|
|
40
|
-
//
|
|
37
|
+
// Clear the control state: the in-flight markers first, then the session sentinel
|
|
38
|
+
// last (so "no session" flips exactly at the end). Deleting STATE also drops the
|
|
39
|
+
// cursor (D28) — cursor and session share the one file now, so a single delete does
|
|
40
|
+
// both. The tracked artifacts stay in place — only the ephemera go.
|
|
41
41
|
rmSync(seamPath(root, slug), { force: true });
|
|
42
42
|
rmSync(stepPath(root, slug), { force: true });
|
|
43
43
|
rmSync(spikePath(root, slug), { force: true });
|
|
@@ -47,12 +47,6 @@ export function finish(cwd, args = []) {
|
|
|
47
47
|
// the next session's first landing.
|
|
48
48
|
clearTick(root, slug);
|
|
49
49
|
setGrant(root, null);
|
|
50
|
-
if (slug !== null) {
|
|
51
|
-
// Drop the activeBuild key — JSON.stringify omits an `undefined` value, so the
|
|
52
|
-
// cursor is removed while the other local settings (auto, …) survive. Skipped
|
|
53
|
-
// under `--local`, where there is no cursor to clear.
|
|
54
|
-
setLocalSetting(root, 'activeBuild', undefined);
|
|
55
|
-
}
|
|
56
50
|
rmSync(join(sidecarDir(root), 'STATE'), { force: true });
|
|
57
51
|
const where = slug === null ? '.plumbbob/' : `.plumbbob/builds/${slug}/`;
|
|
58
52
|
process.stdout.write(`plumbbob: finished — ${sha.slice(0, 9)}. ${where} rides your branch into the PR. ` +
|
|
@@ -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/start.js
CHANGED
|
@@ -1,12 +1,12 @@
|
|
|
1
1
|
// `plumbbob start "<title>"` — scaffold the sidecar, record the baseline, open
|
|
2
2
|
// the session. Refuses on a dirty tree (D22), an existing session, or a non-git dir.
|
|
3
3
|
import { fileURLToPath } from 'node:url';
|
|
4
|
-
import { readFileSync, writeFileSync, mkdirSync } from 'node:fs';
|
|
4
|
+
import { existsSync, readFileSync, writeFileSync, mkdirSync } from 'node:fs';
|
|
5
5
|
import { join } from 'node:path';
|
|
6
6
|
import { detectGate } from "../lib/check.js";
|
|
7
7
|
import { findRepoRoot, hasCommit, headSha, isDirty } from "../lib/git.js";
|
|
8
8
|
import { sidecarDir, buildDir, listBuilds, slugify, checkpointsPath, intentPath, buildLogPath, beginSession, hasSession, excludeControl, excludeSidecar, setGrant, stampTick, } from "../lib/sidecar.js";
|
|
9
|
-
import { settingsPath
|
|
9
|
+
import { settingsPath } from "../lib/settings.js";
|
|
10
10
|
// D24/D32: the gate is checkride unless a `check` setting overrides it, so
|
|
11
11
|
// settings.json seeds with no `check` key at all — absence IS the default. The
|
|
12
12
|
// templates' {{CHECK}} line is a human-readable echo of that.
|
|
@@ -51,8 +51,20 @@ export async function start(cwd, args) {
|
|
|
51
51
|
}
|
|
52
52
|
const sha = headSha(root);
|
|
53
53
|
mkdirSync(sidecarDir(root), { recursive: true });
|
|
54
|
-
beginSession
|
|
55
|
-
|
|
54
|
+
// beginSession writes STATE — the session sentinel and, in its content, the cursor
|
|
55
|
+
// (D28). Point it at this build (null under --local, which has no cursor). The
|
|
56
|
+
// cursor lives in STATE, not settings.local.json, so that overlay stays human-owned.
|
|
57
|
+
beginSession(root, local ? null : slug);
|
|
58
|
+
// Scaffold the tracked settings.json ONLY when absent, and seed it EMPTY — the
|
|
59
|
+
// human owns this file once it exists (their `check` gate lives here), so a
|
|
60
|
+
// re-start must never touch it. We inject no opinions even on first create:
|
|
61
|
+
// absence of `check` already means checkride and absence of `auto` already means
|
|
62
|
+
// false (see settings.ts), so `{}` is exactly "all defaults". `auto` in
|
|
63
|
+
// particular is a personal preference that belongs in settings.local.json, not
|
|
64
|
+
// this tracked file — seeding it here was the bug that clobbered custom checks.
|
|
65
|
+
if (!existsSync(settingsPath(root))) {
|
|
66
|
+
writeFileSync(settingsPath(root), `${JSON.stringify({}, null, 2)}\n`);
|
|
67
|
+
}
|
|
56
68
|
// D26: `--local` keeps today's fully-untracked flat layout (whole `.plumbbob/`
|
|
57
69
|
// excluded); the default plants a tracked, PR-riding `builds/<slug>/` folder
|
|
58
70
|
// (D26) and points the per-worktree cursor at it (D28), excluding only control
|
|
@@ -72,7 +84,6 @@ export async function start(cwd, args) {
|
|
|
72
84
|
writeFileSync(join(dir, 'checkpoints'), `baseline ${sha}\n`);
|
|
73
85
|
writeFileSync(join(dir, 'intent.md'), stamp(readTemplate('intent.md'), title, CHECK_ECHO));
|
|
74
86
|
writeFileSync(join(dir, 'build-log.md'), stamp(readTemplate('build-log.md'), title, CHECK_ECHO));
|
|
75
|
-
setLocalSetting(root, 'activeBuild', slug);
|
|
76
87
|
excludeControl(root);
|
|
77
88
|
intentLocation = `.plumbbob/builds/${slug}/intent.md`;
|
|
78
89
|
}
|
package/dist/verbs/use.js
CHANGED
|
@@ -1,14 +1,13 @@
|
|
|
1
1
|
// `plumbbob use <slug>` (D30) — re-point the per-worktree cursor at an existing
|
|
2
2
|
// build and resume it. Both switching and resuming are the same one word (Q10,
|
|
3
|
-
// `nvm use`-shaped): the cursor is
|
|
4
|
-
//
|
|
3
|
+
// `nvm use`-shaped): the cursor is STATE's single-line content, so pointing it
|
|
4
|
+
// elsewhere IS the switch and one-active-per-worktree holds by
|
|
5
5
|
// construction (D28). It validates the target folder exists, and warns — but
|
|
6
6
|
// allows — leaving a build with a step in flight: that surviving in-flight state
|
|
7
7
|
// is D26's whole payoff, resumed the next time you `use` that build.
|
|
8
8
|
import { existsSync } from 'node:fs';
|
|
9
9
|
import { findRepoRoot } from "../lib/git.js";
|
|
10
|
-
import { activeBuild, hasSession, intentPath, listBuilds, stepPath } from "../lib/sidecar.js";
|
|
11
|
-
import { setLocalSetting } from "../lib/settings.js";
|
|
10
|
+
import { activeBuild, hasSession, intentPath, listBuilds, setActiveBuild, stepPath } from "../lib/sidecar.js";
|
|
12
11
|
export function use(cwd, args) {
|
|
13
12
|
const root = findRepoRoot(cwd);
|
|
14
13
|
if (root === null || !hasSession(root)) {
|
|
@@ -32,7 +31,7 @@ export function use(cwd, args) {
|
|
|
32
31
|
process.stderr.write(`plumbbob: note — build "${leaving}" has a step in flight; its in-flight state is preserved ` +
|
|
33
32
|
`and resumes when you \`use ${leaving}\` again.\n`);
|
|
34
33
|
}
|
|
35
|
-
|
|
34
|
+
setActiveBuild(root, target);
|
|
36
35
|
const resuming = existsSync(stepPath(root, target)) ? ' (a step is in flight — `status` shows where)' : '';
|
|
37
36
|
process.stdout.write(`plumbbob: now on build "${target}"${resuming}. \`status\` to orient.\n`);
|
|
38
37
|
return 0;
|
package/hooks/post-edit.sh
CHANGED
|
@@ -5,16 +5,16 @@
|
|
|
5
5
|
# the tools are absent or there is no session. tsc is deferred to the heavy tier
|
|
6
6
|
# (D25): it has no true single-file mode and would tax every keystroke.
|
|
7
7
|
|
|
8
|
-
# The repo root is the nearest ancestor whose .plumbbob/
|
|
9
|
-
#
|
|
10
|
-
#
|
|
11
|
-
#
|
|
12
|
-
#
|
|
8
|
+
# The repo root is the nearest ancestor whose .plumbbob/STATE is NON-EMPTY — the
|
|
9
|
+
# per-worktree signal that a tracked build is live here (D28). STATE's existence
|
|
10
|
+
# means "a session is active"; its content is the active-build cursor, so a non-empty
|
|
11
|
+
# STATE is exactly "a tracked build is live" (the `--local` layout leaves STATE empty,
|
|
12
|
+
# and this file-scoped check is a tracked-build affordance). `-s` tests both existence
|
|
13
|
+
# and non-emptiness — no JSON parse, no cursor file to read.
|
|
13
14
|
find_root() {
|
|
14
15
|
d=$(pwd -P)
|
|
15
16
|
while [ -n "$d" ]; do
|
|
16
|
-
if [ -
|
|
17
|
-
grep -q '"activeBuild"' "$d/.plumbbob/settings.local.json" 2>/dev/null; then
|
|
17
|
+
if [ -s "$d/.plumbbob/STATE" ]; then
|
|
18
18
|
printf '%s' "$d"
|
|
19
19
|
return 0
|
|
20
20
|
fi
|
package/hooks/pre-bash-commit.sh
CHANGED
|
@@ -8,8 +8,8 @@
|
|
|
8
8
|
|
|
9
9
|
# The repo root is the nearest ancestor with an active session — `.plumbbob/STATE`,
|
|
10
10
|
# the sentinel `start` writes and `finish` removes. Present in both the tracked and
|
|
11
|
-
# the `--local` layout (
|
|
12
|
-
#
|
|
11
|
+
# the `--local` layout (post-edit.sh only fires when STATE is non-empty, i.e. a
|
|
12
|
+
# tracked build; this hook must guard `--local` steps too, so it probes existence).
|
|
13
13
|
find_root() {
|
|
14
14
|
d=$(pwd -P)
|
|
15
15
|
while [ -n "$d" ]; do
|
|
@@ -26,12 +26,11 @@ find_root() {
|
|
|
26
26
|
root=$(find_root) || exit 0 # no active session here: nothing to guard
|
|
27
27
|
|
|
28
28
|
# The in-flight signal is STEP (cleared when the step checkpoints): inside the
|
|
29
|
-
# active build's folder when
|
|
30
|
-
#
|
|
31
|
-
#
|
|
32
|
-
|
|
33
|
-
slug
|
|
34
|
-
[ -z "$slug" ] && slug=$(sed -n 's/.*"activeBuild"[[:space:]]*:[[:space:]]*"\([^"]*\)".*/\1/p' "$settings" 2>/dev/null | head -n1)
|
|
29
|
+
# active build's folder when STATE names one, else the flat sidecar STEP of the
|
|
30
|
+
# `--local` layout. STATE's content IS the cursor (D28) — read the first line, trim
|
|
31
|
+
# whitespace, and treat the legacy `active` sentinel (pre-STATE-cursor) as no cursor.
|
|
32
|
+
slug=$(head -n1 "$root/.plumbbob/STATE" 2>/dev/null | tr -d '[:space:]')
|
|
33
|
+
[ "$slug" = "active" ] && slug=""
|
|
35
34
|
if [ -n "$slug" ]; then
|
|
36
35
|
step_file="$root/.plumbbob/builds/$slug/STEP"
|
|
37
36
|
else
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "plumbbob",
|
|
3
|
-
"version": "0.8.
|
|
3
|
+
"version": "0.8.5",
|
|
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",
|
|
@@ -42,17 +42,17 @@
|
|
|
42
42
|
}
|
|
43
43
|
},
|
|
44
44
|
"devDependencies": {
|
|
45
|
-
"@ast-grep/cli": "
|
|
46
|
-
"@stryker-mutator/core": "
|
|
47
|
-
"@stryker-mutator/vitest-runner": "
|
|
48
|
-
"@types/node": "
|
|
49
|
-
"@vitest/coverage-v8": "
|
|
50
|
-
"fallow": "
|
|
45
|
+
"@ast-grep/cli": "0.43.0",
|
|
46
|
+
"@stryker-mutator/core": "9.6.1",
|
|
47
|
+
"@stryker-mutator/vitest-runner": "9.6.1",
|
|
48
|
+
"@types/node": "24.13.1",
|
|
49
|
+
"@vitest/coverage-v8": "4.1.9",
|
|
50
|
+
"fallow": "2.103.0",
|
|
51
51
|
"fascicle": "0.8.16",
|
|
52
|
-
"markdownlint-cli2": "
|
|
53
|
-
"oxlint": "
|
|
54
|
-
"typescript": "
|
|
55
|
-
"vitest": "
|
|
52
|
+
"markdownlint-cli2": "0.22.1",
|
|
53
|
+
"oxlint": "1.69.0",
|
|
54
|
+
"typescript": "6.0.3",
|
|
55
|
+
"vitest": "4.1.9"
|
|
56
56
|
},
|
|
57
57
|
"dependencies": {
|
|
58
58
|
"checkride": "0.3.0"
|
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 agent:*), Bash(git diff:*)
|
|
7
7
|
---
|
|
8
8
|
|
|
9
9
|
# PlumbBob — build a step (the default engine)
|
|
@@ -30,51 +30,33 @@ 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
46
|
new problem or "ooh what if" that surfaces mid-build is a `/pb-park`, **not** an
|
|
49
47
|
edit — capture it and stay on the step. If you genuinely cannot finish without
|
|
50
48
|
touching more than the seam, that is scope drift: surface it to the human rather
|
|
51
49
|
than sprawling.
|
|
52
|
-
- **
|
|
53
|
-
`
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
- **A manifest's `when` prose is your cue to fire an agent mid-build**.
|
|
57
|
-
The three slots are the only *declarative* lifecycle points; there is no config for
|
|
58
|
-
"a salient moment in the middle." That is judgment, and you are the frontier model
|
|
59
|
-
in the room: when the work reaches the situation a bound agent's `when` (or a step
|
|
60
|
-
`note`) describes, fire `plumbbob agent run <name> --step <n>` yourself. Prose is
|
|
61
|
-
the orchestration language; you are the workflow engine.
|
|
62
|
-
- **Route a non-`done` envelope by its status**. An agent that returns
|
|
63
|
-
`blocked` couldn't finish (missing input, failed precondition): surface its `notes`,
|
|
64
|
-
let the human unblock, and re-run it — don't work around it. One that returns
|
|
65
|
-
`drift` finished but found the plan no longer matches reality: **stop and send the
|
|
66
|
-
human to `/pb-refine`** to repair the plan before continuing, rather than building
|
|
67
|
-
on a plan that's now wrong. A non-zero exit is a failed run: report it and stop.
|
|
50
|
+
- **Bound agents can shape the build.** If `plumbbob status` shows a
|
|
51
|
+
`harness bindings:` block, a `build`-slot agent may author the diff, a `when`-cue may
|
|
52
|
+
fire an agent mid-build, and a non-`done` envelope routes by its status — see
|
|
53
|
+
**§ Running bound agents**.
|
|
68
54
|
5. **Verify, through to the pause.** Run the verify tick exactly as `/pb-verify`
|
|
69
55
|
does: `plumbbob check` (on red, read `.check/summary.json` and the failing slot's
|
|
70
56
|
raw output under `.check/` for the actual diagnostics; while iterating on a fix,
|
|
71
57
|
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
|
|
58
|
+
still runs everything) → self-review the diff against the done-when, the Decisions,
|
|
59
|
+
and the Constraints (a single structured read) → validate → **PAUSE
|
|
78
60
|
for the human's approval** → only on approval, checkpoint with
|
|
79
61
|
`plumbbob checkpoint <n> --body <<'BODY' … BODY` — a commit body **proportional to the
|
|
80
62
|
step** (a line for a trivial change, a short paragraph for a meatier one; no TIL scan,
|
|
@@ -82,15 +64,73 @@ differs from the model you're running as, say so before implementing — the hum
|
|
|
82
64
|
build-log's `## Log`, so the history writes itself — you only supply the body (or omit
|
|
83
65
|
`--body` for the deterministic done-when + seam + diffstat fallback). Do **not** bump
|
|
84
66
|
the version or changelog — that is the human's `/version` call.
|
|
67
|
+
- **If `plumbbob status` shows a `harness bindings:` block**, fold any bound
|
|
68
|
+
`after`-agents into that self-review as advisory input — see **§ Running bound
|
|
69
|
+
agents**.
|
|
85
70
|
|
|
86
|
-
**The latch makes this pause real
|
|
71
|
+
**The latch makes this pause real.** On the default (non-`--auto`) path you
|
|
87
72
|
build the step and reach the pause *in one turn* — so under plumbbob's turn hook the
|
|
88
73
|
checkpoint at the end of that turn is **refused** ("no human turn since this step
|
|
89
74
|
began"), and that refusal **is** the pause working as designed. Do exactly what the
|
|
90
|
-
pause
|
|
91
|
-
human's next message
|
|
92
|
-
|
|
93
|
-
|
|
75
|
+
pause asks: present the diff and the self-review, emit the closing block below, and
|
|
76
|
+
**end the turn**. The human's next message — their approval — is the human turn that
|
|
77
|
+
lets the checkpoint land: on that turn you run `plumbbob checkpoint <n>` (now allowed)
|
|
78
|
+
and **stop at the boundary**. Landing the checkpoint is its own deliberate beat — the
|
|
79
|
+
thing approval triggers — never a side effect of starting the next step, and
|
|
80
|
+
`/pb-build` only ever starts the *next* step. **Never route around a refusal with a
|
|
81
|
+
raw `git commit`** — the work plane stays free, but the *record* is latched on purpose.
|
|
82
|
+
|
|
83
|
+
**End every default build turn with the standardized hand-off block.** Run `plumbbob
|
|
84
|
+
handoff` and present its output — it renders the same three-part shape (the state, the
|
|
85
|
+
choice, what's next) straight from the session: step N built, the looks-good /
|
|
86
|
+
needs-work choice, and the next undone step with its `- model:` recommendation. Show
|
|
87
|
+
your diff and self-review above it. The CLI owns the block so it can't drift from what
|
|
88
|
+
`/pb-status` reports; you supply only the judgment above it. It already drops the model
|
|
89
|
+
clause when the next step has none, and says so when no planned step remains — you
|
|
90
|
+
don't reproduce the template by hand.
|
|
91
|
+
|
|
92
|
+
**Then hand off with the next model.** Once the checkpoint lands in the approval turn,
|
|
93
|
+
run `plumbbob handoff` again and relay its boundary block — with the step gone from
|
|
94
|
+
in-flight it renders "step N checkpointed" and points at the next undone step, carrying
|
|
95
|
+
that step's `- model:` recommendation (the plan's smallest-model-that-fits call) so the
|
|
96
|
+
human knows which `/model` to select before firing `/pb-build` again. It is what
|
|
97
|
+
carries the plan's suggestion across a fresh context window, which inherits the session
|
|
98
|
+
model, not the plan's. No `- model:` line means any model will do. Guidance, never a
|
|
99
|
+
gate.
|
|
100
|
+
|
|
101
|
+
## Running bound agents
|
|
102
|
+
|
|
103
|
+
Skip this section unless `plumbbob status` shows a `harness bindings:` block. Nearly every
|
|
104
|
+
build binds **no** agents — for them the default path above is the whole story. When a
|
|
105
|
+
harness *is* bound, this is what each affected step gains, in lifecycle order.
|
|
106
|
+
|
|
107
|
+
- **`before` — load context in (step 3).** If the build's `harness.json` binds agents to
|
|
108
|
+
this step's `before` slot, run `plumbbob agent run --step <n> --mode before` first: each
|
|
109
|
+
returns a validated envelope on stdout that plumbbob also appends to the step's
|
|
110
|
+
`handoff.json`, and its `summary`/`body` become **context you read into the build** (that
|
|
111
|
+
is the whole point of the slot — load the context in before you write). No binding, or no
|
|
112
|
+
harness, is a clean no-op — just build.
|
|
113
|
+
- **`build` — delegate the diff (step 4).** If a `build`-slot agent is bound, run
|
|
114
|
+
`plumbbob agent run --step <n> --mode build` and let that agent author the step's code
|
|
115
|
+
instead of writing it yourself; its envelope reports what it did. You still own the verify
|
|
116
|
+
tick — the diff is reviewed the same way whoever wrote it.
|
|
117
|
+
- **A manifest's `when` prose is your cue to fire an agent mid-build (step 4).** The three
|
|
118
|
+
slots are the only *declarative* lifecycle points; there is no config for "a salient
|
|
119
|
+
moment in the middle." That is judgment, and you are the frontier model in the room: when
|
|
120
|
+
the work reaches the situation a bound agent's `when` (or a step `note`) describes, fire
|
|
121
|
+
`plumbbob agent run <name> --step <n>` yourself. Prose is the orchestration language; you
|
|
122
|
+
are the workflow engine.
|
|
123
|
+
- **Route a non-`done` envelope by its status (step 4).** An agent that returns `blocked`
|
|
124
|
+
couldn't finish (missing input, failed precondition): surface its `notes`, let the human
|
|
125
|
+
unblock, and re-run it — don't work around it. One that returns `drift` finished but found
|
|
126
|
+
the plan no longer matches reality: **stop and send the human to `/pb-refine`** to repair
|
|
127
|
+
the plan before continuing, rather than building on a plan that's now wrong. A non-zero
|
|
128
|
+
exit is a failed run: report it and stop.
|
|
129
|
+
- **`after` — advisory input to the verify tick (step 5).** After `plumbbob check`, run any
|
|
130
|
+
bound `after`-agents (`plumbbob agent run --step <n> --mode after`) and fold their
|
|
131
|
+
envelopes into the self-review as **advisory input** — they inform, they never gate
|
|
132
|
+
(checkride gates, the human is the clock; an `after`-agent that could fail a step is the
|
|
133
|
+
lock in autonomy's costume).
|
|
94
134
|
|
|
95
135
|
## `--auto` — let the agent be the clock (opt-in)
|
|
96
136
|
|
|
@@ -98,8 +138,8 @@ differs from the model you're running as, say so before implementing — the hum
|
|
|
98
138
|
progress instead of approving each step. It does the same work, but **the agent reviews
|
|
99
139
|
and approves in the human's place**, and it **chains**:
|
|
100
140
|
|
|
101
|
-
- Build the next step, running its slots
|
|
102
|
-
path: bound `before`-agents → implement (or the bound `build`-agent) → bound
|
|
141
|
+
- Build the next step, running its slots (see **§ Running bound agents**) in the same
|
|
142
|
+
order as the default path: bound `before`-agents → implement (or the bound `build`-agent) → bound
|
|
103
143
|
`after`-agents → `check` → self-review → **if the check is green AND the
|
|
104
144
|
self-review finds no done-when / Decision / Constraint mismatch, checkpoint** and move
|
|
105
145
|
straight on to the next planned step. Repeat. `--auto` adds no new machinery — the
|
|
@@ -108,7 +148,11 @@ and approves in the human's place**, and it **chains**:
|
|
|
108
148
|
the self-review finds a mismatch (surface exactly what, and do not checkpoint it), a
|
|
109
149
|
bound agent returns `blocked` or `drift` (unblock-and-re-run, or `/pb-refine` — an
|
|
110
150
|
agent cannot advance the loop), a new decision is needed, no planned steps
|
|
111
|
-
remain, or the top of a requested range is reached.
|
|
151
|
+
remain, or the top of a requested range is reached. **When `--auto` halts back to the
|
|
152
|
+
human, give the same hand-off the default pause does** — run `plumbbob handoff` and
|
|
153
|
+
relay its block: the step just completed, the next undone step, and that next step's
|
|
154
|
+
`- model:` recommendation if it has one, so a fresh context window knows which `/model`
|
|
155
|
+
to select.
|
|
112
156
|
|
|
113
157
|
`--auto` and a step range are the only paths that checkpoint without a human pause, and
|
|
114
158
|
only because the human asked for it by name; a range re-imposes the pause at its top.
|
|
@@ -149,9 +193,15 @@ just the one more entry already in the halt list above.
|
|
|
149
193
|
- **Agents feed the beat; they never advance it**. `before` loads context,
|
|
150
194
|
`build` writes the diff, `after` is advisory — none can checkpoint, flip a step, or
|
|
151
195
|
chain. `blocked` → unblock and re-run; `drift` → `/pb-refine`. You are still the one
|
|
152
|
-
who verifies and (bar `--auto`) the human is still the clock.
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
196
|
+
who verifies and (bar `--auto`) the human is still the clock. See **§ Running bound
|
|
197
|
+
agents** for the mechanics.
|
|
198
|
+
- **A refused checkpoint is the pause, not an error.** Under plumbbob's turn
|
|
199
|
+
hook a same-turn `checkpoint` is refused by design — present the diff and the closing
|
|
200
|
+
block, **end the turn**; the human's approval on their next turn is what lets you land
|
|
201
|
+
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.
|
|
204
|
+
- **Close with the next model.** When a step lands, run `plumbbob handoff` and relay its
|
|
205
|
+
block — it cites the completed step and the next undone step, and names that next step's
|
|
206
|
+
`- model:` recommendation if it has one, which is what a fresh context window needs to
|
|
207
|
+
pick the right `/model` before re-firing. Guidance, never a gate.
|
|
@@ -31,7 +31,8 @@ record.
|
|
|
31
31
|
2. **Finish.** Run `plumbbob finish`, which appends the checkpoint SHAs to the
|
|
32
32
|
report and makes the final commit — subject `plumbbob: finish — <title>`,
|
|
33
33
|
with an optional proportional body via `--body` (the stdin heredoc) — then
|
|
34
|
-
clears the control state (markers,
|
|
34
|
+
clears the control state (the in-flight markers, then `STATE` last — one delete
|
|
35
|
+
drops both the session sentinel and the active-build cursor it carries). The
|
|
35
36
|
build folder stays in place, committed, and rides the branch into the PR.
|
|
36
37
|
3. **Point at the next goal** — `/pb-plan` to frame the next one.
|
|
37
38
|
|
package/skills/pb-plan/SKILL.md
CHANGED
|
@@ -107,7 +107,7 @@ an agent can follow with `/pb-build`. The argument only seeds how you get there.
|
|
|
107
107
|
baseline → plan → steps. Pass a proportional `--body` (the single-quoted stdin
|
|
108
108
|
heredoc) when the rationale is worth carrying; skip it for a small plan. Do this
|
|
109
109
|
only on the human's approval — the plan is their convergence.
|
|
110
|
-
- **The plan commit is latched too
|
|
110
|
+
- **The plan commit is latched too.** Once the turn ledger exists, `checkpoint
|
|
111
111
|
--plan` refuses to land in the same turn `start` stamped it: present the plan,
|
|
112
112
|
**end the turn**, and the human's approving message is the tick that lets it
|
|
113
113
|
commit on re-fire — the refusal *is* the plan pause. Never route around it with a
|
|
@@ -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)
|
|
@@ -62,8 +62,17 @@ this skill verifies it the same way: **it reads the diff, not the author**.
|
|
|
62
62
|
write in one breath. Omit `--body` entirely and the CLI writes a deterministic body
|
|
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
|
+
7. **Hand off with the next model** *(once the checkpoint lands)*. `plumbbob checkpoint`
|
|
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.
|
|
65
74
|
|
|
66
|
-
## The latch makes the pause real
|
|
75
|
+
## The latch makes the pause real
|
|
67
76
|
|
|
68
77
|
When this session runs under plumbbob's turn hook, `plumbbob checkpoint` **refuses to
|
|
69
78
|
land a step in the same turn it was entered** — and that refusal **is** this pause, not
|
|
@@ -73,7 +82,7 @@ the self-review, **end the turn**, and the human's next message is the tick that
|
|
|
73
82
|
the checkpoint land when you re-fire. **Never reach for a raw `git commit` to force the
|
|
74
83
|
land** — that forges the very record the latch exists to keep honest, and the
|
|
75
84
|
commit-ask hook asks the human about it anyway. The refusal is a healthy latch doing its
|
|
76
|
-
job on the *record* while the *work* plane stays free
|
|
85
|
+
job on the *record* while the *work* plane stays free; a `/pb-build --auto` or
|
|
77
86
|
a typed step range in the human's own prompt is the only self-approval, and `auto: true`
|
|
78
87
|
in settings is the standing one.
|
|
79
88
|
|
|
@@ -90,7 +99,11 @@ in settings is the standing one.
|
|
|
90
99
|
- **`after`-agents advise; they never gate**. Their output feeds the
|
|
91
100
|
self-review — checkride gates, the human approves. `blocked` → unblock and re-run;
|
|
92
101
|
`drift` → `/pb-refine` before checkpointing. No code path makes them blocking.
|
|
93
|
-
- **A refused checkpoint is the pause, never a workaround
|
|
102
|
+
- **A refused checkpoint is the pause, never a workaround.** Under the turn
|
|
94
103
|
hook a same-turn checkpoint is refused *by design* — present the diff, **end the
|
|
95
104
|
turn**, and let the human's next message re-tick it. Never route around it with a raw
|
|
96
105
|
`git commit`; the latch guards the record, not the work.
|
|
106
|
+
- **Close with the next model.** After the checkpoint lands, run `plumbbob handoff` and
|
|
107
|
+
relay its block — it cites the completed step and the next undone step, and surfaces that
|
|
108
|
+
next step's `- model:` recommendation when it has one, which is what a fresh context
|
|
109
|
+
window needs to pick the right `/model`. Guidance, never a gate.
|