plumbbob 0.2.3 → 0.3.1

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.
Files changed (43) hide show
  1. package/README.md +69 -141
  2. package/dist/cli.js +22 -37
  3. package/dist/lib/archive.js +6 -3
  4. package/dist/lib/git.js +0 -4
  5. package/dist/lib/intent.js +1 -1
  6. package/dist/lib/orient.js +160 -0
  7. package/dist/lib/settings.js +12 -8
  8. package/dist/lib/sidecar.js +0 -3
  9. package/dist/verbs/build.js +1 -1
  10. package/dist/verbs/check.js +17 -0
  11. package/dist/verbs/checkpoint.js +83 -0
  12. package/dist/verbs/doctor.js +172 -0
  13. package/dist/verbs/reset.js +54 -0
  14. package/dist/verbs/setup.js +31 -9
  15. package/dist/verbs/status.js +23 -4
  16. package/package.json +2 -2
  17. package/skills/pb-build/SKILL.md +40 -10
  18. package/skills/pb-harvest/SKILL.md +47 -0
  19. package/skills/pb-park/SKILL.md +40 -0
  20. package/skills/pb-plan/SKILL.md +38 -0
  21. package/skills/pb-reset/SKILL.md +39 -0
  22. package/skills/pb-revert/SKILL.md +1 -1
  23. package/skills/pb-spike/SKILL.md +1 -1
  24. package/skills/pb-status/SKILL.md +22 -0
  25. package/skills/pb-step/SKILL.md +37 -0
  26. package/skills/pb-verify/SKILL.md +47 -0
  27. package/skills/plumbbob-interrogate/SKILL.md +1 -1
  28. package/dist/verbs/done.js +0 -63
  29. package/dist/verbs/finish.js +0 -54
  30. package/dist/verbs/mode.js +0 -26
  31. package/dist/verbs/review.js +0 -24
  32. package/dist/verbs/wrap.js +0 -28
  33. package/hooks/bash-guard.sh +0 -62
  34. package/hooks/pre-edit.sh +0 -112
  35. package/skills/park/SKILL.md +0 -26
  36. package/skills/pb-done/SKILL.md +0 -18
  37. package/skills/pb-finish/SKILL.md +0 -18
  38. package/skills/pb-review/SKILL.md +0 -18
  39. package/skills/pb-start/SKILL.md +0 -19
  40. package/skills/pb-wrap/SKILL.md +0 -18
  41. package/skills/plumbbob-docs/SKILL.md +0 -25
  42. package/skills/plumbbob-report/SKILL.md +0 -35
  43. package/skills/plumbbob-triage/SKILL.md +0 -38
@@ -0,0 +1,39 @@
1
+ ---
2
+ name: pb-reset
3
+ description: Close out the build — write the report (what shipped, decisions, parked/harvested items, deferred tangents), then archive intent + build-log + report and clear for a fresh goal. Report by default, no gate.
4
+ disable-model-invocation: true
5
+ model: opus
6
+ allowed-tools: Read, Write, Bash(__PLUMBBOB_BIN__ status:*), Bash(__PLUMBBOB_BIN__ reset:*)
7
+ ---
8
+
9
+ # Plumbbob — reset for a new goal (the close-out)
10
+
11
+ Current session state (injected when this skill runs): !`__PLUMBBOB_BIN__ status 2>/dev/null || echo "plumbbob CLI not found - install the dep and re-run: npx plumbbob setup"`
12
+
13
+ `/pb-reset` ends the build: it captures what happened, archives it, and clears the
14
+ sidecar for the next goal. **Report by default** (D9) — no refuse-without-report gate,
15
+ and no separate docs phase.
16
+
17
+ ## What this skill does, in order
18
+
19
+ 1. **Write the report** to `.plumbbob/report.md`, from `intent.md` + `build-log.md`:
20
+ - **What shipped** — the steps completed and what they delivered.
21
+ - **Decisions and why** — the settled calls that shaped the build.
22
+ - **Parked & harvested** — what was captured and how each was classified.
23
+ - **Final status** — done or partial, and what is left.
24
+ - **Deferred tangents** — the harvested items that become future work.
25
+ This is the "yeah, I did that" artifact. Write it by default; the human may edit it.
26
+ 2. **Archive & clear.** Run `__PLUMBBOB_BIN__ reset`, which appends the checkpoint SHAs
27
+ to the report, archives intent + build-log + report to
28
+ `.plumbbob/archive/<date>-<slug>/`, and clears the sidecar (STATE last). Git is not
29
+ touched.
30
+ 3. **Point at the next goal** — `/pb-plan` to frame the next one.
31
+
32
+ ## The hard contracts
33
+
34
+ - **Report by default, never a gate.** Always offer the report; never wall the exit. A
35
+ bug fix's report can be three lines — size it to the work.
36
+ - **Archive-then-clear, never destroy** (C4). The archive is the record; the active
37
+ files only clear once they are safely copied.
38
+ - **No version bump, no docs phase.** Updating real docs is a separate, explicit ask;
39
+ cutting a release is the human's `/version`.
@@ -8,7 +8,7 @@ allowed-tools: Bash(__PLUMBBOB_BIN__ status:*), Bash(__PLUMBBOB_BIN__ revert:*)
8
8
 
9
9
  # Plumbbob — revert to a checkpoint (driver)
10
10
 
11
- Current session state (injected when this skill runs): !`__PLUMBBOB_BIN__ status`
11
+ Current session state (injected when this skill runs): !`__PLUMBBOB_BIN__ status 2>/dev/null || echo "plumbbob CLI not found - install the dep and re-run: npx plumbbob setup"`
12
12
 
13
13
  This is a **driver skill** — a chat-side trigger for the mechanical `plumbbob revert` verb, so the whole workflow runs from the agent window instead of a terminal. It is `disable-model-invocation: true`: only the human fires it. It carries **no Edit and no Write tool** — its only action is to shell the verb and report the verb's output verbatim, including any refusal. The CLI is the source of truth: never retry a refused transition, and never edit a file to work around one.
14
14
 
@@ -8,7 +8,7 @@ allowed-tools: Bash(__PLUMBBOB_BIN__ status:*), Bash(__PLUMBBOB_BIN__ spike:*)
8
8
 
9
9
  # Plumbbob — spike an experiment (driver)
10
10
 
11
- Current session state (injected when this skill runs): !`__PLUMBBOB_BIN__ status`
11
+ Current session state (injected when this skill runs): !`__PLUMBBOB_BIN__ status 2>/dev/null || echo "plumbbob CLI not found - install the dep and re-run: npx plumbbob setup"`
12
12
 
13
13
  This is a **driver skill** — a chat-side trigger for the mechanical `plumbbob spike` verb, so the whole workflow runs from the agent window instead of a terminal. It is `disable-model-invocation: true`: only the human fires it. It carries **no Edit and no Write tool** — its only action is to shell the verb and report the verb's output verbatim, including any refusal. The CLI is the source of truth: never retry a refused transition, and never edit a file to work around one.
14
14
 
@@ -0,0 +1,22 @@
1
+ ---
2
+ name: pb-status
3
+ description: Show the orientation dashboard — where you are, what's done, what's parked, and the next move. A thin trigger for `plumbbob status`.
4
+ disable-model-invocation: true
5
+ model: haiku
6
+ allowed-tools: Bash(__PLUMBBOB_BIN__ status:*)
7
+ ---
8
+
9
+ # Plumbbob — orient (the where-am-I move)
10
+
11
+ Current session state (injected when this skill runs): !`__PLUMBBOB_BIN__ status 2>/dev/null || echo "plumbbob CLI not found - install the dep and re-run: npx plumbbob setup"`
12
+
13
+ This is a **thin driver** for `__PLUMBBOB_BIN__ status`. The dashboard above is your
14
+ orientation — the intent, the step list, the parked/open-question counts, and the
15
+ inferred next move. Report it verbatim and point the human at that next move. This
16
+ skill carries **no Edit and no Write tool**: the CLI is the source of truth, so
17
+ **never retry**, and never edit a file to change what the orientation says.
18
+
19
+ ## What it does
20
+
21
+ 1. Surface the injected `status` output — the dashboard and its suggested next move.
22
+ 2. If it reads `NO ACTIVE SESSION`, tell the human to `/pb-plan` to frame a goal.
@@ -0,0 +1,37 @@
1
+ ---
2
+ name: pb-step
3
+ description: Plan the next increment — propose one small step with a done-when criterion and a seam, get the human's OK, and append it to intent's ## Steps. One by default; several only if the human already knows them.
4
+ disable-model-invocation: true
5
+ model: opus
6
+ allowed-tools: Read, Edit, Bash(__PLUMBBOB_BIN__ status:*)
7
+ ---
8
+
9
+ # Plumbbob — plan the next step (the single-increment move)
10
+
11
+ Current session state (injected when this skill runs): !`__PLUMBBOB_BIN__ status 2>/dev/null || echo "plumbbob CLI not found - install the dep and re-run: npx plumbbob setup"`
12
+
13
+ `/pb-step` plans the **next increment** just-in-time (D6) — the smallest verifiable
14
+ piece of the framed goal, planned only when you reach it. (Framing the whole goal is
15
+ the separate `/pb-plan` move.)
16
+
17
+ ## What this skill does
18
+
19
+ 1. **Read the plan.** Read `intent.md`'s Frame, Decisions, Constraints, and the steps
20
+ already done, to see what the *next* increment should be.
21
+ 2. **Propose one step.** Draft a single step:
22
+ - a one-line **title**,
23
+ - a **done-when** criterion — ideally a test or check result, something
24
+ `/pb-verify` can actually validate,
25
+ - a **seam**: the specific files it should touch (exact paths, or a `dir/` grant).
26
+ Keep it small enough to verify in one review pass.
27
+ 3. **Get the human's OK**, then **append** it to `## Steps` in the standard format —
28
+ `N. [ ] <title> — **done when:** <criterion>` with a `- seam:` sub-line. Default
29
+ to **one** step; plan several only when the human already knows them.
30
+
31
+ ## The hard contracts
32
+
33
+ - **One verifiable increment.** Each step carries a done-when `/pb-verify` can check
34
+ and a seam small enough to review in one pass.
35
+ - **Append to `## Steps` only**, in the standard format `status` and `build` parse —
36
+ never the Roadmap, never loose prose.
37
+ - **The human approves the step** before it lands. You propose; they decide.
@@ -0,0 +1,47 @@
1
+ ---
2
+ name: pb-verify
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
+ disable-model-invocation: true
5
+ model: opus
6
+ allowed-tools: Read, Bash(__PLUMBBOB_BIN__ status:*), Bash(__PLUMBBOB_BIN__ check:*), Bash(__PLUMBBOB_BIN__ checkpoint:*), Bash(git diff:*), Bash(git status:*)
7
+ ---
8
+
9
+ # Plumbbob — verify a step (the tick)
10
+
11
+ Current session state (injected when this skill runs): !`__PLUMBBOB_BIN__ status 2>/dev/null || echo "plumbbob CLI not found - install the dep and re-run: npx plumbbob setup"`
12
+
13
+ This is the **tick** — the one beat where the human is the clock. Whatever produced
14
+ the current diff — `/pb-build`, your own hands, a vibe session, another harness —
15
+ this skill verifies it the same way: **it reads the diff, not the author** (D3).
16
+
17
+ ## What this skill does, in order
18
+
19
+ 1. **Check.** Run `__PLUMBBOB_BIN__ check` (the heavy gate). If it comes back
20
+ **red**, stop here: report what failed and do **not** pause for approval — there
21
+ is nothing to approve yet. The human fixes it and re-invokes.
22
+ 2. **Self-review** *(a single structured read, D16)*. Read `git diff` and
23
+ `.plumbbob/intent.md`, then in one pass check the diff against:
24
+ - the current step's **done-when** criterion — is it actually met?
25
+ - the **Decisions** — does anything contradict a settled call?
26
+ - the **Constraints** — are any violated?
27
+ Surface every mismatch plainly. You are reviewing, not building — do not fix anything.
28
+ 3. **Validate.** State, yes or no, whether the step's done-when is met, with the evidence.
29
+ 4. **PAUSE.** Present the check result, the self-review, and the validation, then
30
+ **stop and wait for the human's explicit approval.** This is the convergence beat;
31
+ the human is the clock. Never checkpoint without it.
32
+ 5. **Checkpoint** *(only after approval)*. Commit the work — the human's
33
+ commit-with-TIL skill for a rich message, or let `checkpoint` make the WIP commit
34
+ — then run `__PLUMBBOB_BIN__ checkpoint` to record the SHA, flip the step to done,
35
+ and return to DESIGN. Do **not** bump the version or touch the changelog — that is
36
+ the human's `/version` call.
37
+
38
+ ## The hard contracts
39
+
40
+ - **Never skip the pause.** Check → self-review → validate, then wait. Approval is
41
+ the only thing that triggers the checkpoint.
42
+ - **Read the diff, not the author** (D3). Verify identically whether the code was
43
+ built by `/pb-build`, by hand, vibed, or by another harness.
44
+ - **Red means stop, not pause.** A failing check is not an approval decision; report
45
+ it and end your turn.
46
+ - **You review; you do not build.** If the self-review finds a problem, surface it
47
+ and stop — fixing is a new build beat, not part of verify.
@@ -8,7 +8,7 @@ allowed-tools: Read, Edit, Bash(__PLUMBBOB_BIN__ status:*)
8
8
 
9
9
  # Plumbbob — interrogate the frame
10
10
 
11
- Current session state (injected when this skill runs): !`__PLUMBBOB_BIN__ status`
11
+ Current session state (injected when this skill runs): !`__PLUMBBOB_BIN__ status 2>/dev/null || echo "plumbbob CLI not found - install the dep and re-run: npx plumbbob setup"`
12
12
 
13
13
  ## Wrong-state refusal
14
14
 
@@ -1,63 +0,0 @@
1
- // `plumbbob done` — the step gate. Refuses on a red check, then stages the whole
2
- // step (D8: `git add -A`; the sidecar is git-excluded so it never lands), warns
3
- // about anything committed outside the SEAM, takes the checkpoint commit, records
4
- // its SHA, and returns to DESIGN.
5
- import { appendFileSync, readFileSync, rmSync } from 'node:fs';
6
- import { commit, findRepoRoot, stageAll, stagedPaths } from "../lib/git.js";
7
- import { checkpointsPath, hasSession, readState, seamPath, stepPath, writeState } from "../lib/sidecar.js";
8
- import { runCheck } from "../lib/check.js";
9
- import { matchesSeam } from "../lib/intent.js";
10
- export function done(cwd) {
11
- const root = findRepoRoot(cwd);
12
- if (root === null || !hasSession(root)) {
13
- process.stderr.write('plumbbob: no active session. Run `plumbbob start "<title>"` first.\n');
14
- return 1;
15
- }
16
- const state = readState(root);
17
- if (state !== 'BUILD' && state !== 'REVIEW') {
18
- process.stderr.write(`plumbbob: done runs from BUILD or REVIEW (current state is ${state ?? 'UNKNOWN'}). Run \`plumbbob build <n>\` first.\n`);
19
- return 1;
20
- }
21
- const step = readStepNumber(root);
22
- if (step === null) {
23
- process.stderr.write('plumbbob: no in-flight step — run `plumbbob build <n>` first.\n');
24
- return 1;
25
- }
26
- if (runCheck(root) !== 0) {
27
- process.stderr.write('plumbbob: check failed (red) — done refuses on red. Fix it and re-run `done`.\n');
28
- return 1;
29
- }
30
- stageAll(root);
31
- const seam = readSeamTokens(root);
32
- const outside = stagedPaths(root).filter((p) => !matchesSeam(p, seam));
33
- if (outside.length > 0) {
34
- process.stderr.write(`plumbbob: WARNING committed paths outside the SEAM: ${outside.join(', ')}. The checkpoint captures them, but scope drift may mean the plan needs revising.\n`);
35
- }
36
- const sha = commit(root, `plumbbob: step ${step} done`);
37
- appendFileSync(checkpointsPath(root), `step ${step} ${sha}\n`);
38
- rmSync(seamPath(root), { force: true });
39
- rmSync(stepPath(root), { force: true });
40
- writeState(root, 'DESIGN');
41
- process.stdout.write(`plumbbob: step ${step} done — checkpoint ${sha.slice(0, 9)}. STATE=DESIGN.\n`);
42
- return 0;
43
- }
44
- function readStepNumber(root) {
45
- try {
46
- const raw = readFileSync(stepPath(root), 'utf8').trim();
47
- return /^\d+$/.test(raw) ? Number(raw) : null;
48
- }
49
- catch {
50
- return null;
51
- }
52
- }
53
- function readSeamTokens(root) {
54
- try {
55
- return readFileSync(seamPath(root), 'utf8')
56
- .split('\n')
57
- .map((l) => l.trim())
58
- .filter((l) => l.length > 0);
59
- }
60
- catch {
61
- return [];
62
- }
63
- }
@@ -1,54 +0,0 @@
1
- // `plumbbob finish` (D19/D20) — the closing gate, symmetric with the step gate.
2
- // Refuses unless a report exists, appends the checkpoint SHA list to it, archives
3
- // intent + build-log + report under .plumbbob/archive/, clears the active files,
4
- // and deletes SEAM, STEP, then STATE LAST (deleting STATE is what switches the
5
- // muzzle off, so it happens exactly at session end). Never touches git (C5).
6
- import { appendFileSync, existsSync, readFileSync, rmSync } from 'node:fs';
7
- import { join, relative } from 'node:path';
8
- import { findRepoRoot } from "../lib/git.js";
9
- import { buildLogPath, checkpointsPath, hasSession, intentPath, seamPath, sidecarDir, stepPath, } from "../lib/sidecar.js";
10
- import { archiveSession, reportPath } from "../lib/archive.js";
11
- export function finish(cwd) {
12
- const root = findRepoRoot(cwd);
13
- if (root === null || !hasSession(root)) {
14
- process.stderr.write('plumbbob: no active session. Run `plumbbob start "<title>"` first.\n');
15
- return 1;
16
- }
17
- if (!existsSync(reportPath(root))) {
18
- process.stderr.write('plumbbob: finish refuses without a report — run `/plumbbob-report` first (it writes .plumbbob/report.md). ' +
19
- 'The closing gate is symmetric with the step gate: you do not walk away without capturing what happened.\n');
20
- return 1;
21
- }
22
- appendCheckpointShas(root);
23
- const archived = archiveSession(root);
24
- // Clear the active files — now safely archived.
25
- rmSync(intentPath(root), { force: true });
26
- rmSync(buildLogPath(root), { force: true });
27
- rmSync(reportPath(root), { force: true });
28
- // Delete the control files LAST, STATE the very last: while STATE exists the
29
- // muzzle is live, so it comes off only once everything else is torn down.
30
- rmSync(seamPath(root), { force: true });
31
- rmSync(stepPath(root), { force: true });
32
- rmSync(join(sidecarDir(root), 'STATE'), { force: true });
33
- process.stdout.write(`plumbbob: finished — archived to ${relative(root, archived)}. STATE cleared (muzzle off). ` +
34
- 'Run `plumbbob start "<title>"` to begin the next task.\n');
35
- return 0;
36
- }
37
- // Append the recorded checkpoints (baseline + each `step n <sha>`) to the report,
38
- // so the archived report lists the SHAs (spec: "finish lists the SHAs in the
39
- // report").
40
- function appendCheckpointShas(root) {
41
- let raw = '';
42
- try {
43
- raw = readFileSync(checkpointsPath(root), 'utf8');
44
- }
45
- catch {
46
- raw = '';
47
- }
48
- const lines = raw
49
- .split('\n')
50
- .map((l) => l.trim())
51
- .filter((l) => l.length > 0)
52
- .map((l) => `- ${l}`);
53
- appendFileSync(reportPath(root), ['', '## Checkpoints', '', ...lines, ''].join('\n'));
54
- }
@@ -1,26 +0,0 @@
1
- // `plumbbob mode <x>` — the hidden escape hatch: set STATE directly when reality
2
- // and the machine desync. Not part of the normal flow; the lone human-only verb,
3
- // so the dispatch refuses it under CLAUDECODE and bash-guard blocks it from the
4
- // model's shell (D21 revised).
5
- import { findRepoRoot } from "../lib/git.js";
6
- import { hasSession, readState, writeState, VALID_STATES } from "../lib/sidecar.js";
7
- export function mode(cwd, args) {
8
- const root = findRepoRoot(cwd);
9
- if (root === null || !hasSession(root)) {
10
- process.stderr.write('plumbbob: no active session. Run `plumbbob start "<title>"` first.\n');
11
- return 1;
12
- }
13
- const target = args[0];
14
- if (target === undefined) {
15
- process.stderr.write(`plumbbob: mode needs a state — one of: ${VALID_STATES.join(', ')}.\n`);
16
- return 1;
17
- }
18
- if (!VALID_STATES.includes(target)) {
19
- process.stderr.write(`plumbbob: '${target}' is not a valid state — one of: ${VALID_STATES.join(', ')}.\n`);
20
- return 1;
21
- }
22
- const previous = readState(root) ?? 'UNKNOWN';
23
- writeState(root, target);
24
- process.stdout.write(`STATE: ${previous} -> ${target} (escape hatch — prefer the normal verbs when you can).\n`);
25
- return 0;
26
- }
@@ -1,24 +0,0 @@
1
- // `plumbbob review` — run the heavy check from BUILD. Green flips to REVIEW (the
2
- // muzzle goes back on so reading the diff can't slide into editing). Red stays in
3
- // BUILD. The state only advances on a green check.
4
- import { findRepoRoot } from "../lib/git.js";
5
- import { hasSession, readState, writeState } from "../lib/sidecar.js";
6
- import { runCheck } from "../lib/check.js";
7
- export function review(cwd) {
8
- const root = findRepoRoot(cwd);
9
- if (root === null || !hasSession(root)) {
10
- process.stderr.write('plumbbob: no active session. Run `plumbbob start "<title>"` first.\n');
11
- return 1;
12
- }
13
- if (readState(root) !== 'BUILD') {
14
- process.stderr.write(`plumbbob: review runs from BUILD (current state is ${readState(root) ?? 'UNKNOWN'}). Run \`plumbbob build <n>\` first.\n`);
15
- return 1;
16
- }
17
- if (runCheck(root) !== 0) {
18
- process.stderr.write('plumbbob: check failed (red) — staying in BUILD. Fix it and re-run `review` (or `done` once green).\n');
19
- return 1;
20
- }
21
- writeState(root, 'REVIEW');
22
- process.stdout.write('plumbbob: check green — STATE=REVIEW. Read the diff against intent.md (edits muzzled). `build <n>` to re-enter and fix, or `done` to checkpoint.\n');
23
- return 0;
24
- }
@@ -1,28 +0,0 @@
1
- // `plumbbob wrap` (D19/D28) — the FINISH-entry verb. Sets STATE=FINISH so
2
- // /plumbbob-report can write report.md and /plumbbob-docs can touch docs/.
3
- // `finish` stays the closing gate; wrap just opens the one state where
4
- // documentation may be projected. A transition the human fires — from a terminal
5
- // or, in-session, via the /pb-wrap driver skill (D21 revised).
6
- import { findRepoRoot } from "../lib/git.js";
7
- import { hasSession, readState, writeState } from "../lib/sidecar.js";
8
- export function wrap(cwd) {
9
- const root = findRepoRoot(cwd);
10
- if (root === null || !hasSession(root)) {
11
- process.stderr.write('plumbbob: no active session. Run `plumbbob start "<title>"` first.\n');
12
- return 1;
13
- }
14
- const state = readState(root);
15
- if (state === 'FINISH') {
16
- process.stdout.write('plumbbob: already in FINISH. Run `/plumbbob-report`, then `plumbbob finish` to close.\n');
17
- return 0;
18
- }
19
- if (state !== 'DESIGN') {
20
- process.stderr.write(`plumbbob: wrap enters FINISH from DESIGN (current state is ${state ?? 'UNKNOWN'}). ` +
21
- 'Close the current step first — `done` from BUILD/REVIEW, or `spike done` from SPIKE.\n');
22
- return 1;
23
- }
24
- writeState(root, 'FINISH');
25
- process.stdout.write('plumbbob: STATE=FINISH. Now `/plumbbob-report` writes the report (and `/plumbbob-docs` may touch docs/); ' +
26
- 'then `plumbbob finish` archives and closes.\n');
27
- return 0;
28
- }
@@ -1,62 +0,0 @@
1
- #!/bin/sh
2
- # bash-guard.sh — Plumbbob's assistive fence (D21), not a wall. PreToolUse on
3
- # Bash. Session-gated. Blocks shell commands that poke control state or write
4
- # files outside BUILD/SPIKE, so the muzzle is not trivially bypassed. Full
5
- # shell-write detection is unsolvable; the residual gap is accepted (D21).
6
-
7
- find_root() {
8
- d=$(pwd -P)
9
- while [ -n "$d" ]; do
10
- if [ -f "$d/.plumbbob/STATE" ]; then
11
- printf '%s' "$d"
12
- return 0
13
- fi
14
- [ "$d" = "/" ] && break
15
- d=$(dirname "$d")
16
- done
17
- return 1
18
- }
19
-
20
- deny() {
21
- printf 'plumbbob: %s\n' "$1" >&2
22
- exit 2
23
- }
24
-
25
- root=$(find_root) || exit 0 # no session: allow
26
-
27
- input=$(cat)
28
- command=$(printf '%s' "$input" | jq -r '.tool_input.command // empty' 2>/dev/null)
29
- [ -z "$command" ] && command=$(printf '%s' "$input" | sed -n 's/.*"command"[[:space:]]*:[[:space:]]*"\([^"]*\)".*/\1/p' | head -n1)
30
- [ -z "$command" ] && exit 0
31
-
32
- state=$(tr -d '[:space:]' <"$root/.plumbbob/STATE" 2>/dev/null)
33
-
34
- # Always: keep the model out of the control files and the escape hatch (D21).
35
- case "$command" in
36
- *.plumbbob/STATE* | *.plumbbob/SEAM*)
37
- deny "blocked: do not touch .plumbbob/STATE or SEAM from the shell. Read state with \`plumbbob status\`; transitions are the human's verbs. Do not retry."
38
- ;;
39
- esac
40
- case "$command" in
41
- *"plumbbob mode"* | *"pb mode"* | *"plumbbob mode"*)
42
- deny "blocked: \`plumbbob mode\` is the human's escape hatch, not a model action. Do not retry — ask the human."
43
- ;;
44
- esac
45
-
46
- # Outside BUILD/SPIKE: block obvious file-writing shell patterns (D21).
47
- case "$state" in
48
- BUILD | SPIKE) ;;
49
- *)
50
- # Strip redirects that can't write a real file (stderr merges, /dev/null
51
- # sinks) so read-only commands aren't over-blocked. Any surviving `>` is a
52
- # real write. The residual gap (e.g. `>/dev/nullEVIL`) is accepted (D21).
53
- scrubbed=$(printf '%s' "$command" | sed 's/[0-9]*>&[0-9-]//g; s/&\{0,1\}[0-9]*>>* *\/dev\/null//g')
54
- case "$scrubbed" in
55
- *">"* | *"tee "* | *"sed -i"* | *"git apply"*)
56
- deny "blocked: file-writing shell commands are not allowed in ${state:-?} (code edits happen in BUILD). Do not retry — park it or ask the human to \`plumbbob build <n>\`."
57
- ;;
58
- esac
59
- ;;
60
- esac
61
-
62
- exit 0
package/hooks/pre-edit.sh DELETED
@@ -1,112 +0,0 @@
1
- #!/bin/sh
2
- # pre-edit.sh — Plumbbob muzzle + seam-guard. PreToolUse on
3
- # Edit|Write|MultiEdit|NotebookEdit. Session-gated: a repo with no .plumbbob/
4
- # session behaves exactly like plain Claude Code (C7). The dormant check is pure
5
- # sh (test -f) before any JSON parsing, so a no-session edit pays ~nothing (C3).
6
- # Built from the verified hooks API (D3): input is JSON on stdin
7
- # (tool_input.file_path / notebook_path, absolute); deny is exit 2 + stderr.
8
-
9
- # Walk up from the hook's physical cwd to the session root, like git finds .git.
10
- find_root() {
11
- d=$(pwd -P)
12
- while [ -n "$d" ]; do
13
- if [ -f "$d/.plumbbob/STATE" ]; then
14
- printf '%s' "$d"
15
- return 0
16
- fi
17
- [ "$d" = "/" ] && break
18
- d=$(dirname "$d")
19
- done
20
- return 1
21
- }
22
-
23
- # Seam membership (D23): exact whole-line match, or a `dir/` line by prefix.
24
- in_seam() {
25
- _rel=$1
26
- _seam=$2
27
- [ -f "$_seam" ] || return 1
28
- grep -qFx "$_rel" "$_seam" && return 0
29
- while IFS= read -r _line || [ -n "$_line" ]; do
30
- case "$_line" in
31
- */)
32
- case "$_rel" in
33
- "$_line"*) return 0 ;;
34
- esac
35
- ;;
36
- esac
37
- done <"$_seam"
38
- return 1
39
- }
40
-
41
- deny() {
42
- printf 'plumbbob: %s\n' "$1" >&2
43
- exit 2
44
- }
45
-
46
- root=$(find_root) || exit 0 # no session anywhere above cwd: allow
47
-
48
- input=$(cat)
49
- path=$(printf '%s' "$input" | jq -r '.tool_input.file_path // .tool_input.notebook_path // empty' 2>/dev/null)
50
- if [ -z "$path" ]; then
51
- path=$(printf '%s' "$input" | sed -n 's/.*"file_path"[[:space:]]*:[[:space:]]*"\([^"]*\)".*/\1/p' | head -n1)
52
- [ -z "$path" ] && path=$(printf '%s' "$input" | sed -n 's/.*"notebook_path"[[:space:]]*:[[:space:]]*"\([^"]*\)".*/\1/p' | head -n1)
53
- fi
54
- [ -z "$path" ] && exit 0 # nothing to guard: allow
55
-
56
- state=$(tr -d '[:space:]' <"$root/.plumbbob/STATE" 2>/dev/null)
57
-
58
- # Canonicalize the absolute tool path to repo-relative (D4).
59
- case "$path" in
60
- "$root"/*) rel=${path#"$root"/} ;;
61
- *) rel=$path ;;
62
- esac
63
-
64
- # 1. The archive is read-only history — never writable (D6/D19).
65
- case "$rel" in
66
- .plumbbob/archive/*)
67
- deny "blocked: $rel is archived history (read-only). Do not retry."
68
- ;;
69
- esac
70
-
71
- # 2. The three control docs are writable in every state, so DESIGN and FINISH can
72
- # do their work. Anchored exact paths — never a bare */intent.md suffix (D6).
73
- case "$rel" in
74
- .plumbbob/intent.md | .plumbbob/build-log.md | .plumbbob/report.md)
75
- exit 0
76
- ;;
77
- esac
78
-
79
- # 3. docs/ is writable only in FINISH (D19).
80
- case "$rel" in
81
- docs/*)
82
- [ "$state" = "FINISH" ] && exit 0
83
- deny "blocked: $rel (docs/) is writable only in FINISH — docs are projected at the end. Do not retry; park it or finish the build first."
84
- ;;
85
- esac
86
-
87
- # The seam muzzle below governs repo code only (D23). A path *outside* the repo
88
- # (Claude's own plan-mode scratch in ~/.claude/plans, an editor tmpfile) and a
89
- # git-ignored path *inside* it (fallow data, dist/, coverage/) are none of its
90
- # business — never block them. `.plumbbob/` is itself git-ignored but is
91
- # plumbbob's own control surface, so its non-doc files (STATE/SEAM/...) must stay
92
- # governed by the muzzle, never skipped here.
93
- case "$rel" in
94
- .plumbbob/*) ;; # control state: stays governed by the muzzle, never skipped
95
- *)
96
- # git resolves the path via the repo toplevel (robust to symlinked cwds).
97
- # check-ignore exit: 0 = ignored, 128 = outside the repo — neither is the
98
- # muzzle's business; 1 = in-repo, tracked territory -> fall through to it.
99
- git -C "$root" check-ignore -q -- "$path" 2>/dev/null
100
- [ "$?" -ne 1 ] && exit 0
101
- ;;
102
- esac
103
-
104
- # 4. Everything else is code: allowed only in BUILD, confined to the SEAM (D23).
105
- # SPIKE locks the main tree like DESIGN (D18) — spike edits live in dormant
106
- # worktrees, so the muzzle never needs to allow SPIKE here.
107
- if [ "$state" = "BUILD" ]; then
108
- in_seam "$rel" "$root/.plumbbob/SEAM" && exit 0
109
- deny "blocked: $rel is outside the seam for this step. Do not retry — park it (\`plumbbob park\`), or ask the human to revise the step's seam in intent.md and re-run \`plumbbob build <n>\`."
110
- fi
111
-
112
- deny "blocked: code edits are not allowed in ${state:-?} (they happen in BUILD). Do not retry — if this needs doing it's a decision: park it (\`plumbbob park\`) or ask the human to \`plumbbob build <n>\`."
@@ -1,26 +0,0 @@
1
- ---
2
- name: park
3
- description: Compose one tidy tagged park line, get the human's OK in-turn, then capture it by shelling `plumbbob park` — never by editing a file.
4
- disable-model-invocation: true
5
- model: haiku
6
- allowed-tools: Bash(__PLUMBBOB_BIN__ status:*), Bash(__PLUMBBOB_BIN__ park:*)
7
- ---
8
-
9
- # Park
10
-
11
- Current session state (injected when this skill runs): !`__PLUMBBOB_BIN__ status`
12
-
13
- ## Wrong-state refusal
14
-
15
- Parking needs an **active session** to capture into. Read the state injected above: if it is `NO ACTIVE SESSION`, **refuse** and tell the human to run `plumbbob start "<title>"` first. Every active state (`DESIGN`, `BUILD`, `REVIEW`, `SPIKE`, `FINISH`) is fine — capture is always available, which is the whole point of parking.
16
-
17
- ## What this skill does
18
-
19
- Take the idea, problem, or "ooh what if" the human just had and **compose it into one tidy, tagged line** — short, legible, self-contained, so it still reads cleanly weeks later. Then:
20
-
21
- 1. **Show the composed line to the human** and wait for **in-turn approval** — they confirm it as-is or edit the wording.
22
- 2. **Only after** that approval, capture it by running `__PLUMBBOB_BIN__ park "<the approved line>"` via Bash.
23
-
24
- ## The one hard contract
25
-
26
- The capture itself is the **dumb CLI**, never an edit. This skill carries **no Edit and no Write tool** on purpose (D12): you may not append to `build-log.md` yourself. Compose, get approval, then shell `__PLUMBBOB_BIN__ park` — that is the only write path. If approval never comes, capture nothing.
@@ -1,18 +0,0 @@
1
- ---
2
- name: pb-done
3
- description: Human-triggered driver for `plumbbob done` — ensure the check is green, take the checkpoint commit, record its SHA, and return to DESIGN.
4
- disable-model-invocation: true
5
- model: haiku
6
- allowed-tools: Bash(__PLUMBBOB_BIN__ status:*), Bash(__PLUMBBOB_BIN__ done:*)
7
- ---
8
-
9
- # Plumbbob — finish a step (driver)
10
-
11
- Current session state (injected when this skill runs): !`__PLUMBBOB_BIN__ status`
12
-
13
- This is a **driver skill** — a chat-side trigger for the mechanical `plumbbob done` verb, so the whole workflow runs from the agent window instead of a terminal. It is `disable-model-invocation: true`: only the human fires it. It carries **no Edit and no Write tool** — its only action is to shell the verb and report the verb's output verbatim, including any refusal. The CLI is the source of truth: never retry a refused transition, and never edit a file to work around one.
14
-
15
- ## What it does
16
-
17
- 1. Run `__PLUMBBOB_BIN__ done` via Bash.
18
- 2. Report the verb's output verbatim — the checkpoint SHA and return to DESIGN, or any refusal (e.g. the check is red).
@@ -1,18 +0,0 @@
1
- ---
2
- name: pb-finish
3
- description: Human-triggered driver for `plumbbob finish` — refuse unless a report is archived, then archive, clear the session, and switch the muzzle off.
4
- disable-model-invocation: true
5
- model: haiku
6
- allowed-tools: Bash(__PLUMBBOB_BIN__ status:*), Bash(__PLUMBBOB_BIN__ finish:*)
7
- ---
8
-
9
- # Plumbbob — finish the session (driver)
10
-
11
- Current session state (injected when this skill runs): !`__PLUMBBOB_BIN__ status`
12
-
13
- This is a **driver skill** — a chat-side trigger for the mechanical `plumbbob finish` verb, so the whole workflow runs from the agent window instead of a terminal. It is `disable-model-invocation: true`: only the human fires it. It carries **no Edit and no Write tool** — its only action is to shell the verb and report the verb's output verbatim, including any refusal. The CLI is the source of truth: never retry a refused transition, and never edit a file to work around one.
14
-
15
- ## What it does
16
-
17
- 1. Run `__PLUMBBOB_BIN__ finish` via Bash.
18
- 2. Report the verb's output verbatim. It is the closing gate — it **refuses unless `.plumbbob/report.md` exists**. If it refuses, relay that and tell the human to run `/plumbbob-report` first; do not write the report yourself from here.
@@ -1,18 +0,0 @@
1
- ---
2
- name: pb-review
3
- description: Human-triggered driver for `plumbbob review` — run the heavy check and, if green, flip to REVIEW (muzzle back on) to read the diff cold.
4
- disable-model-invocation: true
5
- model: haiku
6
- allowed-tools: Bash(__PLUMBBOB_BIN__ status:*), Bash(__PLUMBBOB_BIN__ review:*)
7
- ---
8
-
9
- # Plumbbob — review a step (driver)
10
-
11
- Current session state (injected when this skill runs): !`__PLUMBBOB_BIN__ status`
12
-
13
- This is a **driver skill** — a chat-side trigger for the mechanical `plumbbob review` verb, so the whole workflow runs from the agent window instead of a terminal. It is `disable-model-invocation: true`: only the human fires it. It carries **no Edit and no Write tool** — its only action is to shell the verb and report the verb's output verbatim, including any refusal. The CLI is the source of truth: never retry a refused transition, and never edit a file to work around one.
14
-
15
- ## What it does
16
-
17
- 1. Run `__PLUMBBOB_BIN__ review` via Bash.
18
- 2. Report the verb's output verbatim. If the heavy check is red the verb stays in BUILD and prints the failures — relay them and stop; fixing them is the human's call, not an automatic retry.