The plumbbob CLI is the mechanical layer the /plumbbob:* skills shell out to — in
normal use you never type it by hand. This page documents the full surface anyway, for
power users, for driving PlumbBob from another harness, and for understanding what each
skill actually runs.
plumbbob <verb> [args] # also available as `pb`
It is a lean CLI — node builtins plus one deliberate dependency,
checkride (C2, amended; D32) — that runs
natively on Node ≥ 22.18. Every verb is a
pure function that writes to stdout/stderr and returns an exit code; the only
process.exit is the bin entry.
| Verb | Synopsis | Effect |
|---|---|---|
start |
start <title> [--slug <name>] [--local] [--allow-dirty] |
scaffold builds/<slug>/, record baseline, open the session |
status |
status [--build <slug>] |
print the orientation dashboard (or NO ACTIVE SESSION) |
build |
build [<n>] [--build <slug>] |
write step n's seam + STEP (goes in-flight) |
handoff |
handoff [<n>] [--build <slug>] |
print the standardized hand-off block; read-only |
check |
check [--bail] [--changed] [--all] [--only a,b] [--skip a,b] [--include a,b] |
run the heavy gate; no state change |
checkpoint |
checkpoint [<n>] [--plan] [-m <msg>] [--body <<'BODY'…] |
gate on green, commit, record SHA, mark step done |
revert |
revert [--to <n>] [--build <slug>] |
git reset --hard to a checkpoint SHA |
park |
park <text> |
append a line to the park list |
spike |
spike <slug> [opt…] [--build <slug>] | spike report <slug> | spike done |
throwaway worktree experiment |
agent |
agent list | agent run <name> [--step N] [--mode …] [--agent <path>] [--build <slug>] |
list user-authored agents, or run one through the doorway |
use |
use <slug> |
re-point the active-build cursor and resume that build |
finish |
finish [--body <<'BODY'…] [--build <slug>] |
append checkpoints to the report, make the final commit, close the session |
init |
init [--uninstall] [--force] |
link plumbbob into Claude Code as the skills-dir plugin |
doctor |
doctor [--migrate] |
diagnose the plugin link; migrate a legacy flat sidecar |
turn |
turn |
UserPromptSubmit hook machinery; not a user verb |
help |
help [<verb>] | --help | -h |
print the verb table, or one verb's flags |
version |
version | --version | -v |
print the CLI version |
status, build, handoff, revert, spike, finish, and agent accept --build <slug>
to target a specific build; without it, the verb resolves the active build from the cursor
(D28, see the layout). The other verbs act on
the cursor's build only.
Every verb answers --help (or -h) with its own synopsis, arguments, and flags —
plumbbob checkpoint --help, or equivalently plumbbob help checkpoint. The flags a verb
declares are the only ones it accepts: an unrecognized flag is a refusal (exit 1), never a
silently-ignored token, so a typo cannot fall through into a commit. turn and park are the
two exceptions to the refusal — turn is a hook that must never wedge a prompt, and park's
argument is free text.
plumbbob start "<title>" [--slug <name>] [--local] [--allow-dirty]
Scaffolds the sidecar, records the baseline HEAD, and opens the session. By default it
plants a tracked build folder at .plumbbob/builds/<slug>/ — the slug derived from the title
as YYYY-MM-DD-<title-slug>, date-prefixed so builds/ lists chronologically (--slug
overrides it verbatim, no prefix) — holding intent.md, build-log.md, and checkpoints
(baseline <sha>); it writes the tracked settings.json — seeded empty as {}, which is
exactly "all defaults", since absent check already means checkride is the gate and absent
auto already means false (D24/D32); the file is
yours once it exists, so a re-start never touches it — and the untracked
STATE sentinel — whose content is the active-build cursor, pointed at the new build (D28) — and narrows the
repo's info/exclude to the control-plane patterns (D17/D26). --local opts out into
the old fully-untracked flat layout — everything under .plumbbob/ excluded (D26).
Refuses (exit 1) on an empty title, a slug that collides with an existing build, a non-git
directory, a repo with no commits, an already-active session, or a dirty tree —
--allow-dirty overrides the dirty-tree refusal and records the current HEAD as the
baseline (D22).
plumbbob status [--build <slug>]
Prints the orientation dashboard — title, the derived phase, the step list with the next
step's done-when and seam, the last checkpoint, and the parked / open-question counts — then a
single suggested next move (D8 / D15). Read-only; prints NO ACTIVE SESSION and
exits 0 when there is no session.
plumbbob build [<n>] [--build <slug>]
Reads step n's seam from intent.md and writes SEAM (the path list) and STEP (the
number) — the STEP file is what makes the dashboard read BUILD. The seam is
orientation, not a lock. Refuses (exit 1)
with no session, a non-numeric or < 1 step, or a seam it cannot parse (seams are exact
paths or dir/ grants, never globs — D23).
plumbbob handoff [<n>] [--build <slug>]
Prints the standardized hand-off block — the "state / choice / what's next" the human sees at
each step boundary. Read-only, no state change. The moment is derived, not passed: a step in
flight yields the pause block, none yields the post-checkpoint boundary block. An explicit
<n> overrides which step it reports on; otherwise it uses the in-flight step, else the last
checkpointed one. Owning the block here rather than as prose in the build skill keeps the
skill from drifting out of sync with status, which renders the same next-step detail.
Refuses (exit 1) only with no session.
plumbbob check [--bail] [--changed] [--all] [--only a,b] [--skip a,b] [--include a,b]
Runs the heavy gate with no state change (D16 / D24 / D32). The gate is
checkride, run in-process: each slot (types,
lint, struct, dead, test, docs, links, spell) resolves to the tool the repo already
configures, raw output lands in .check/, and a red run names the failing slots with
their .check/<slot> pointers. A check key in the settings ladder
(settings.local.json → settings.json, D27) overrides checkride with a shell
command, spawned exactly as before — that is how non-checkride repos gate.
The flags narrow a checkride run for the iteration loop (--bail --only types,lint);
they map straight onto checkride's own flags and are warned-and-ignored on the override
path. checkpoint's gate takes no flags — the commit gate is always the full run.
Exits with the check's code: 0 green, 1 red — including a run where every slot
skipped, which refuses rather than passing vacuously — and 2 when the gate itself
broke (e.g. a malformed checkride.config.json). Refuses (exit 1) with no session.
plumbbob checkpoint [<n>] [-m <msg>] [--body <<'BODY' … BODY]
plumbbob checkpoint --plan [--body …]
The executor-agnostic commit tick (D3). Resolves the step — explicit <n>, else the
in-flight STEP, else the first undone step in intent.md — then gates on a green check,
commits any pending work (or records the existing HEAD if the tree is already clean) with a
CLI-owned Conventional subject <type>(<scope>): <description> — scope from the build slug, type
from the step title (author prefix honored, else feat) — appends step <n> <sha> to checkpoints,
flips the step to [x], and clears SEAM/STEP — dropping the dashboard back to the
DESIGN boundary. -m <msg> overrides the subject. The commit body leads with a
plumbbob step N marker, then a --body heredoc on stdin (skill-composed,
proportional); without it a deterministic fallback carries done-when + seam + diffstat
(D68/D35). --plan instead commits only the build's artifact folder as
chore(<scope>): plan and records a plan <sha> line, giving the plan its own commit so
the first step's diff doesn't absorb the scaffold (D36). Refuses (exit 1) with no session,
no resolvable step, or a red check.
plumbbob revert [--to <n>] [--build <slug>]
git reset --hard to a recorded checkpoint SHA: the last step by default, --to <n> for a
specific step, or the baseline as the fallback. The build folder is now tracked (D26),
so before the reset revert snapshots builds/<slug>/ to a temp dir and restores it after —
park lines and intent edits survive even when reverting to a baseline that predates the folder
(C4). Untracked files inside the seam are removed, files outside it are left alone.
Clears SEAM/STEP, dropping back to the DESIGN boundary. Refuses (exit 1) with no session,
an invalid --to, or a step with no recorded checkpoint.
plumbbob park "<text>"
Appends <text> as a raw line under ## Park list in build-log.md and prints
parked: <text> (D7). This is the dumb capture path — composing the tidy tagged line
is the /plumbbob:park skill's job. Refuses (exit 1) with no session, empty text, or no
## Park list section.
plumbbob spike "<slug>" [opt…] [--build <slug>] # open
plumbbob spike report "<slug>" # scaffold a report, no worktrees
plumbbob spike done # close
Opens a throwaway experiment for a genuine fork (D18): a sibling git worktree and
spike/<slug>-<opt> branch per option (default options a and b), created outside
the repo root, and drops the SPIKE marker. spike done removes every spike worktree and
branch and clears the marker. Refuses (exit 1) with no session, a step already in flight, an
empty slug, or a worktree path that already exists; done refuses when no spike is open.
plumbbob agent list
plumbbob agent run <name> [--step N] [--mode before|build|after] [--agent <path>] [--build <slug>]
plumbbob agent run --mode before|build|after [--step N]
The doorway to user-authored agents (D39 — the full author contract is
agents.md). An agent is any executable that speaks a versioned
JSON-stdin / JSON-stdout / prose-stderr envelope; these two subcommands list and run them,
with no code path to advance the loop — no checkpoint, no step flip, no chaining
(C6, the identity invariant).
agent list prints every resolvable agent — name, origin tier (project /personal),
slots, and description — walking .plumbbob/agents/<name>/ (tracked) then
~/.plumbbob/agents/<name>/ (personal), project shadowing personal (D41). A malformed
agent.json lists as an ✗ … invalid: line rather than hiding. Refuses (exit 1) outside a
git repository.
agent run composes the step's StepContext from intent.md + settings, spawns the
manifest command via sh -c at the repo root with the agent's own directory in
PLUMBBOB_AGENT_DIR (D49) and the context JSON on stdin, streams the child's stderr
live, and — on a clean run (exit 0 + a valid envelope) — lands any parked[] through the
park verb (D44), appends the envelope to the step-scoped builds/<slug>/handoff.json
ledger (untracked, cleared at checkpoint — D47), prints the human summary on stderr, and
re-emits the machine envelope on its own stdout for the calling skill (D46).
--step Npicks the step (else the in-flightSTEP; without either it refuses).- A name runs exactly that agent and fails loud on a miss or an undeclared
--modeslot (D54 — you asked for it by name). No name +--mode <slot>runs the step's harness-bound agents for that slot (D42); a bound agent a teammate lacks degrades to a warning and is skipped (D54). --modenames the slot; it must be one the manifest declares. A single-slot agent infers it; a multi-slot agent requires it.--agent <path>points at an explicit agent directory (top of the ladder); it still needs a name for the run label.- Ctrl-C kills the child and reports rather than orphaning it (D58); a positive
agentTimeout(below) arms a kill timer (D51).
Exits 0 on a clean run, 1 on a failed one — a non-zero child exit (reported verbatim, the envelope of a failed child is not trusted), garbage on stdout (out of contract), a timeout kill, an interrupt, an explicit-name miss, or an undeclared-slot refusal. A batch of bound agents returns non-zero if any that actually ran failed.
plumbbob use <slug>
Re-points the active-build cursor at the named build and resumes it — the one verb for both
switching between builds and picking one back up (D30). Validates that
builds/<slug>/ exists, then rewrites the cursor in STATE (leaving the session sentinel intact). It warns (but
allows) leaving a build that still has a step in flight — that surviving STEP/SEAM is the
payoff of per-build markers. Refuses (exit 1) with an empty slug or a slug with no build
folder; status with no cursor lists the available builds instead of refusing.
plumbbob finish [--body <<'BODY' … BODY] [--build <slug>]
The close-out (D9/D29). Appends the checkpoint SHAs to the build's report.md
(the report itself is written by the /plumbbob:finish skill; a missing one is noted, never a
refusal), makes the final
commit (subject chore(<scope>): finish, mirroring the step-checkpoint Conventional shape; body
leads with a plumbbob finish marker, then an optional --body heredoc), and clears
the control state (STATE, the cursor, the in-flight markers). No separate archive copy — the
tracked build folder already is the record and merges into main with the branch, so it
rides into the PR (D26). There is no refuse-without-report gate. Refuses (exit 1) only
with no session.
plumbbob turn
Hook machinery, not a user verb. Registered as the UserPromptSubmit hook, it reads the hook
payload from stdin — it takes no arguments — ticks the human-turn ledger that the
checkpoint latch reads, and emits any nudge as additionalContext JSON on stdout. It always
exits 0 and never refuses an unrecognized flag: a hook that failed would wedge every prompt in
the session.
PlumbBob has two co-equal install paths: the marketplace plugin (Claude Code installs
the published npm package for you — skills and this CLI on PATH via bin/ — so it needs no
init) and the skills-dir link these verbs manage. See install.md for the
choice; the two are mutually exclusive.
plumbbob init [--uninstall] [--force]
Links plumbbob into Claude Code as the skills-dir plugin: it symlinks the package into
~/.claude/skills/plumbbob, where Claude Code loads it in place (skills as /plumbbob:*,
the post-edit hook auto-registered from hooks/hooks.json). Idempotent, global-only, and it
never writes settings.json. --uninstall drops the link. Refuses (exit 1) if the path
exists and is not a plumbbob link, or if a marketplace plumbbob plugin is already
installed — the two register the same plugin name and collide over the /plumbbob:*
namespace (skills can drop to flat names like /plumbbob:status); --force overrides that guard
(the dev-install path uses it). Restart Claude Code (or /reload-plugins) to activate.
plumbbob doctor [--migrate]
Three diagnostics under one verb.
Plugin link (read-only). Across both install paths it verifies the skills-dir link
resolves to a package carrying the manifest, the skills, and the hook; it also recognizes a
marketplace-only install as a valid, passing state, and flags the double-install
collision when both are present, printing the exact fix for anything missing. Run it first
if a /plumbbob:* skill opens an empty dashboard. Also available in-session as /plumbbob:doctor —
the only way to reach it on a marketplace install, where the CLI is on PATH only inside a
Claude Code session.
Sidecar layout. When run inside a repo carrying a legacy flat sidecar — the
pre-restructure layout with a config file, an archive/ folder, or a flat active session —
doctor reports it and offers --migrate. plumbbob doctor --migrate moves the archive
entries and the active session into tracked builds/<slug>/ folders (the active one becomes
the cursor; the rest are "done" simply by not being it), turns config into settings.json,
narrows the excludes, and stages the whole move without committing — the commit is yours
(D31).
Check gate (D32). Reports how the heavy gate will resolve in this repo: a configured
check override is named as-is; otherwise checkride's own doctor prints the slot/adapter
table (✓ types ← tsc, ○ spell — no tool detected, …) so you can see what a green gate
actually covers before trusting it.
Exits 0 when everything passes, 1 when a check fails or an un-migrated legacy sidecar is present.
The sidecar splits into a tracked artifact plane and an untracked control plane
(D17/D26). The artifact plane — settings.json and every builds/<slug>/ folder —
is committed, so a build's record (intent, log, checkpoints, report) rides its branch into the
PR. The control plane — STATE, settings.local.json, and each build's in-flight markers —
stays git-excluded; a session is live iff STATE is present.
.plumbbob/
STATE # untracked — session sentinel (presence = live) AND the active-build cursor (its content, D28)
settings.json # tracked — project defaults: {"check": "…", "auto": false}
settings.local.json # untracked — personal overlay only: {"auto": true, …} (no cursor — that lives in STATE)
agents/ # tracked — optional: user-authored agents, one dir per agent (D41; see agents.md)
<name>/agent.json # — the manifest; personal agents live under ~/.plumbbob/agents/
builds/
<slug>/ # derived slugs are YYYY-MM-DD-<title-slug>, so ls sorts chronologically
intent.md # tracked — canonical intent (rides the branch into the PR)
build-log.md # tracked — live ledger + park list
checkpoints # tracked — "baseline <sha>", "plan <sha>", "step N <sha>"
report.md # tracked — written at finish
harness.json # tracked — optional: per-step agent slot bindings (D42; see agents.md)
STEP # untracked — the in-flight step number (its presence is the BUILD phase)
SEAM # untracked — the in-flight step's declared paths (awareness, not a lock)
SPIKE # untracked — marker, present while a spike fork is open
handoff.json # untracked — step-scoped agent-run ledger, cleared at checkpoint (D47)
Which build a verb acts on resolves --build <slug> → the active-build cursor in STATE → the sole
build in builds/ → a refusal with a hint (D28). plumbbob start --local opts back into
the old fully-untracked flat layout — intent.md/build-log.md/checkpoints at the sidecar
root, the whole .plumbbob/ excluded — for repos that will not track tool folders (D26).
A repo scaffolded by a pre-restructure plumbbob keeps that legacy flat layout until
plumbbob doctor --migrate moves it here (D31).
The control plane also carries the latch's flat files at the sidecar root — TURN (the
human-turn counter) and GRANT (a one-turn self-approval) — plus each build's TICK entry
stamp. What every one of them holds, how they are git-excluded, and why they are not in
.gitignore or your home directory: state-and-git.md.
Settings resolve through a four-rung ladder (D27): a CLI flag → settings.local.json
(untracked personal overlay) → settings.json (tracked project defaults) → a built-in
default. The known keys:
check overrides the heavy gate (a shell command run in the repo root; its exit code is
the result); absent, the gate is checkride (D24/D32). auto is whether the
agent approves in your place. The per-worktree active-build cursor is not a setting — it
is STATE's content, so plumbbob never rewrites this hand-editable overlay (D28). agents sets
project-wide slot bindings for user-authored agents — the bottom rung under a
build's harness.json and the --agent flag (D57). agentTimeout (seconds) arms a
kill timer for a spawned agent; absent or 0 means no timeout, since the human is present
and Ctrl-C works (D51). Both files are optional JSON — a missing or malformed one
contributes nothing rather than wedging the tool.
- 0 — success. For
check(andcheckpoint's gate), 0 means the heavy check was green. - 1 — a refusal or failure: a guard tripped (no session, a step in flight, bad
argument), a red check, or an unknown verb.
checkpropagates the underlying command's non-zero code. - 2 — the gate itself broke (D32): checkride couldn't run at all (e.g. a
malformed
checkride.config.json). Fix the harness before trusting green or red.
techniques.md— what each verb is for and how the methods fit.troubleshooting.md— what to do when a verb refuses.decisions.md— theD#/C#tags referenced above.