plumbbob 0.2.1 → 0.3.0
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/README.md +63 -143
- package/dist/cli.js +18 -37
- package/dist/lib/archive.js +6 -3
- package/dist/lib/git.js +0 -4
- package/dist/lib/intent.js +1 -1
- package/dist/lib/orient.js +160 -0
- package/dist/lib/settings.js +12 -8
- package/dist/lib/sidecar.js +0 -3
- package/dist/verbs/build.js +1 -1
- package/dist/verbs/check.js +17 -0
- package/dist/verbs/checkpoint.js +83 -0
- package/dist/verbs/reset.js +54 -0
- package/dist/verbs/revert.js +43 -3
- package/dist/verbs/setup.js +1 -1
- package/dist/verbs/status.js +23 -4
- package/package.json +2 -2
- package/skills/pb-build/SKILL.md +39 -9
- package/skills/pb-harvest/SKILL.md +47 -0
- package/skills/pb-park/SKILL.md +40 -0
- package/skills/pb-plan/SKILL.md +38 -0
- package/skills/pb-reset/SKILL.md +39 -0
- package/skills/pb-status/SKILL.md +22 -0
- package/skills/pb-step/SKILL.md +37 -0
- package/skills/pb-verify/SKILL.md +47 -0
- package/dist/verbs/done.js +0 -63
- package/dist/verbs/finish.js +0 -54
- package/dist/verbs/mode.js +0 -26
- package/dist/verbs/review.js +0 -24
- package/dist/verbs/wrap.js +0 -28
- package/hooks/bash-guard.sh +0 -62
- package/hooks/pre-edit.sh +0 -95
- package/skills/park/SKILL.md +0 -26
- package/skills/pb-done/SKILL.md +0 -18
- package/skills/pb-finish/SKILL.md +0 -18
- package/skills/pb-review/SKILL.md +0 -18
- package/skills/pb-start/SKILL.md +0 -19
- package/skills/pb-wrap/SKILL.md +0 -18
- package/skills/plumbbob-docs/SKILL.md +0 -25
- package/skills/plumbbob-report/SKILL.md +0 -35
- package/skills/plumbbob-triage/SKILL.md +0 -38
package/dist/verbs/done.js
DELETED
|
@@ -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
|
-
}
|
package/dist/verbs/finish.js
DELETED
|
@@ -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
|
-
}
|
package/dist/verbs/mode.js
DELETED
|
@@ -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
|
-
}
|
package/dist/verbs/review.js
DELETED
|
@@ -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
|
-
}
|
package/dist/verbs/wrap.js
DELETED
|
@@ -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
|
-
}
|
package/hooks/bash-guard.sh
DELETED
|
@@ -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,95 +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
|
-
# 4. Everything else is code: allowed only in BUILD, confined to the SEAM (D23).
|
|
88
|
-
# SPIKE locks the main tree like DESIGN (D18) — spike edits live in dormant
|
|
89
|
-
# worktrees, so the muzzle never needs to allow SPIKE here.
|
|
90
|
-
if [ "$state" = "BUILD" ]; then
|
|
91
|
-
in_seam "$rel" "$root/.plumbbob/SEAM" && exit 0
|
|
92
|
-
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>\`."
|
|
93
|
-
fi
|
|
94
|
-
|
|
95
|
-
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>\`."
|
package/skills/park/SKILL.md
DELETED
|
@@ -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.
|
package/skills/pb-done/SKILL.md
DELETED
|
@@ -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.
|
package/skills/pb-start/SKILL.md
DELETED
|
@@ -1,19 +0,0 @@
|
|
|
1
|
-
---
|
|
2
|
-
name: pb-start
|
|
3
|
-
description: Human-triggered driver for `plumbbob start` — scaffold a new session (.plumbbob/, STATE=DESIGN, baseline commit) without leaving the chat.
|
|
4
|
-
disable-model-invocation: true
|
|
5
|
-
model: haiku
|
|
6
|
-
allowed-tools: Bash(__PLUMBBOB_BIN__ status:*), Bash(__PLUMBBOB_BIN__ start:*)
|
|
7
|
-
---
|
|
8
|
-
|
|
9
|
-
# Plumbbob — start a 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 start` 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. Read the session title from the way you were invoked (e.g. `/pb-start "fix the widget"` → title `fix the widget`). If no title is present, ask for one and run nothing.
|
|
18
|
-
2. Run `__PLUMBBOB_BIN__ start "<title>"` via Bash.
|
|
19
|
-
3. Report the verb's output verbatim. If it refuses (e.g. a session already exists), relay that and stop.
|
package/skills/pb-wrap/SKILL.md
DELETED
|
@@ -1,18 +0,0 @@
|
|
|
1
|
-
---
|
|
2
|
-
name: pb-wrap
|
|
3
|
-
description: Human-triggered driver for `plumbbob wrap` — set STATE=FINISH so the report and docs skills can run.
|
|
4
|
-
disable-model-invocation: true
|
|
5
|
-
model: haiku
|
|
6
|
-
allowed-tools: Bash(__PLUMBBOB_BIN__ status:*), Bash(__PLUMBBOB_BIN__ wrap:*)
|
|
7
|
-
---
|
|
8
|
-
|
|
9
|
-
# Plumbbob — wrap into FINISH (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 wrap` 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__ wrap` via Bash.
|
|
18
|
-
2. Report the verb's output verbatim. FINISH is what unlocks `/plumbbob-report` and `/plumbbob-docs` — next, run `/plumbbob-report`.
|
|
@@ -1,25 +0,0 @@
|
|
|
1
|
-
---
|
|
2
|
-
name: plumbbob-docs
|
|
3
|
-
description: FINISH-phase, optional docs update — conservatively project canonical intent into real docs/, only when warranted.
|
|
4
|
-
disable-model-invocation: true
|
|
5
|
-
model: opus
|
|
6
|
-
allowed-tools: Read, Edit, Write, Bash(__PLUMBBOB_BIN__ status:*)
|
|
7
|
-
---
|
|
8
|
-
|
|
9
|
-
# Plumbbob — update the docs
|
|
10
|
-
|
|
11
|
-
Current session state (injected when this skill runs): !`__PLUMBBOB_BIN__ status`
|
|
12
|
-
|
|
13
|
-
## Wrong-state refusal
|
|
14
|
-
|
|
15
|
-
`docs/` is writable **only in FINISH** (D19) — the muzzle blocks it everywhere else. Read the state injected above and **refuse if it is not `FINISH`**, naming `plumbbob wrap` as the verb that enters FINISH (the report skill runs first; this docs pass is the optional second Finish step). If the state is `NO ACTIVE SESSION` the hooks are dormant and there is nothing for this skill to gate — just edit docs the ordinary way.
|
|
16
|
-
|
|
17
|
-
## What this skill does
|
|
18
|
-
|
|
19
|
-
**Conservative by default.** Most sessions write no docs at all — **a bug fix usually shouldn't spawn a doc**. Update real documentation under `docs/` only when the work genuinely changed how the system should be described, and only from the **canonical** parts of `intent.md` (the Frame, the Decisions) — never from the chat transcript.
|
|
20
|
-
|
|
21
|
-
## The one hard contract
|
|
22
|
-
|
|
23
|
-
- Default to doing nothing. Open a doc edit only when you can name what changed for a reader of `docs/`.
|
|
24
|
-
- Project only from `intent.md`; the report is for the session, the docs are for the system.
|
|
25
|
-
- Stay inside `docs/` — and only in FINISH. An edit anywhere else, or in any other state, is the muzzle's job to refuse, not yours to attempt.
|
|
@@ -1,35 +0,0 @@
|
|
|
1
|
-
---
|
|
2
|
-
name: plumbbob-report
|
|
3
|
-
description: FINISH-phase report — synthesize intent + build-log into exactly .plumbbob/report.md with the five required sections.
|
|
4
|
-
disable-model-invocation: true
|
|
5
|
-
model: opus
|
|
6
|
-
allowed-tools: Read, Write, Bash(__PLUMBBOB_BIN__ status:*)
|
|
7
|
-
---
|
|
8
|
-
|
|
9
|
-
# Plumbbob — write the report
|
|
10
|
-
|
|
11
|
-
Current session state (injected when this skill runs): !`__PLUMBBOB_BIN__ status`
|
|
12
|
-
|
|
13
|
-
## Wrong-state refusal
|
|
14
|
-
|
|
15
|
-
This skill writes the report **in FINISH only**. Read the state injected above:
|
|
16
|
-
|
|
17
|
-
- `STATE: FINISH` — proceed.
|
|
18
|
-
- Any other active state (`DESIGN`, `BUILD`, `REVIEW`, `SPIKE`) — **stop and tell the human to run `plumbbob wrap` first** (D28). Entering FINISH is what unlocks writing `report.md`, and this skill is the thing that tells you to wrap.
|
|
19
|
-
- `NO ACTIVE SESSION` — there is nothing to report; start a session with `plumbbob start "<title>"`.
|
|
20
|
-
|
|
21
|
-
## What this skill does
|
|
22
|
-
|
|
23
|
-
The first of the three Finish steps. Read `intent.md` and `build-log.md` and synthesize the conclusion — the "yeah, I did that" artifact — into **exactly one file: `.plumbbob/report.md`**. Write nothing else, and touch no other path.
|
|
24
|
-
|
|
25
|
-
## Required sections
|
|
26
|
-
|
|
27
|
-
`.plumbbob/report.md` must contain these five sections, in order:
|
|
28
|
-
|
|
29
|
-
1. **What shipped** — what this session actually built, measured against the Frame's "done looks like".
|
|
30
|
-
2. **The decisions and why** — the decisions taken (the `D#` ledger) and the reasoning behind each.
|
|
31
|
-
3. **Parked items and how each was triaged** — every Park-list item and its triage outcome (blocker / tangent / pivot signal).
|
|
32
|
-
4. **Final status** — done, partial, or abandoned, with the checkpoint SHAs.
|
|
33
|
-
5. **Deferred tangents (future Plumbbobs)** — the tangents worth their own future session.
|
|
34
|
-
|
|
35
|
-
Once `report.md` exists, the human runs `plumbbob finish` (the closing gate refuses without it) to archive and clear the session.
|
|
@@ -1,38 +0,0 @@
|
|
|
1
|
-
---
|
|
2
|
-
name: plumbbob-triage
|
|
3
|
-
description: DESIGN-phase, step-boundary triage — propose one class per parked item, write only after the human confirms each.
|
|
4
|
-
disable-model-invocation: true
|
|
5
|
-
model: opus
|
|
6
|
-
allowed-tools: Read, Edit, Bash(__PLUMBBOB_BIN__ status:*)
|
|
7
|
-
---
|
|
8
|
-
|
|
9
|
-
# Plumbbob — triage the park list
|
|
10
|
-
|
|
11
|
-
Current session state (injected when this skill runs): !`__PLUMBBOB_BIN__ status`
|
|
12
|
-
|
|
13
|
-
## Wrong-state refusal
|
|
14
|
-
|
|
15
|
-
This skill runs in **DESIGN only**, at a **step boundary** (after the step's check has gone green). Read the state injected above and **stop before writing anything** if it is not `DESIGN`:
|
|
16
|
-
|
|
17
|
-
- `STATE: BUILD` or `STATE: REVIEW` — finish the step first with `plumbbob done`, which returns you to DESIGN; then triage.
|
|
18
|
-
- `STATE: SPIKE` — close the experiment with `plumbbob spike done` first.
|
|
19
|
-
- `STATE: FINISH` — too late to triage; the report skill folds the park list in instead.
|
|
20
|
-
- `NO ACTIVE SESSION` — start one with `plumbbob start "<title>"`.
|
|
21
|
-
|
|
22
|
-
When the state is wrong, refuse in one line naming the verb above.
|
|
23
|
-
|
|
24
|
-
## What this skill does
|
|
25
|
-
|
|
26
|
-
Walk the **Park list** in `build-log.md` item by item and, for each, **propose exactly one class**:
|
|
27
|
-
|
|
28
|
-
- **blocker** — the plan was wrong or incomplete; can't proceed. Folds into `intent.md` and is handled now.
|
|
29
|
-
- **tangent** — a different path, not clearly better. **The default** — defer or kill.
|
|
30
|
-
- **pivot signal** — real evidence the whole approach is wrong. Stop and replan deliberately.
|
|
31
|
-
|
|
32
|
-
## The one hard contract
|
|
33
|
-
|
|
34
|
-
You **propose**; the **human calls it** (D13). For each item, state your proposed class and one line of reasoning, then **wait for the human to confirm or override**. Write **only after** per-item confirmation:
|
|
35
|
-
|
|
36
|
-
- Record each confirmed class in `build-log.md`'s `## Triage` section.
|
|
37
|
-
- A confirmed **blocker** also folds its decision into `intent.md`.
|
|
38
|
-
- Never reclassify or resolve an item the human hasn't confirmed, and default every uncertain item to **tangent**, never to blocker.
|