plumbbob 0.4.8 → 0.4.12
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.claude-plugin/plugin.json +1 -1
- package/README.md +17 -12
- package/bin/pb +13 -4
- package/bin/plumbbob +24 -7
- package/dist/cli-core.js +6 -6
- package/dist/cli.js +0 -0
- package/dist/lib/buildlog.js +34 -0
- package/dist/lib/orient.js +26 -33
- package/dist/lib/sidecar.js +26 -12
- package/dist/verbs/build.js +4 -5
- package/dist/verbs/checkpoint.js +38 -10
- package/dist/verbs/doctor.js +1 -1
- package/dist/verbs/park.js +2 -22
- package/dist/verbs/revert.js +2 -3
- package/dist/verbs/spike.js +15 -17
- package/dist/verbs/start.js +5 -5
- package/dist/verbs/status.js +2 -2
- package/dist/verbs/wrap.js +2 -1
- package/package.json +3 -3
- package/skills/pb-build/SKILL.md +4 -2
- package/skills/pb-doctor/SKILL.md +39 -0
- package/skills/pb-harvest/SKILL.md +7 -6
- package/skills/pb-park/SKILL.md +4 -3
- package/skills/pb-plan/SKILL.md +2 -1
- package/skills/pb-refine/SKILL.md +1 -0
- package/skills/pb-revert/SKILL.md +2 -1
- package/skills/pb-spike/SKILL.md +1 -0
- package/skills/pb-step/SKILL.md +1 -0
- package/skills/pb-verify/SKILL.md +3 -3
- package/skills/pb-wrap/SKILL.md +9 -3
- package/templates/build-log.md +9 -7
- package/templates/intent.md +1 -2
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "plumbbob",
|
|
3
3
|
"displayName": "PlumbBob",
|
|
4
|
-
"version": "0.4.
|
|
4
|
+
"version": "0.4.12",
|
|
5
5
|
"description": "Attention-first build process — driver skills + a CLI that keep you the decider; guidance, not enforcement.",
|
|
6
6
|
"author": { "name": "Rob McLarty", "email": "hello@robmclarty.com", "url": "https://robmclarty.com" },
|
|
7
7
|
"homepage": "https://github.com/robmclarty/plumbbob#readme",
|
package/README.md
CHANGED
|
@@ -142,8 +142,8 @@ Calibration is the skill. When in doubt, smaller.
|
|
|
142
142
|
pass that injects file-scoped lint into the model's context so it self-corrects in
|
|
143
143
|
flow. (v1's pre-edit muzzle, seam-guard, and bash-guard are gone — guidance, not
|
|
144
144
|
enforcement.)
|
|
145
|
-
- A `.plumbbob/` sidecar of flat files: `STATE` (
|
|
146
|
-
`build-log.md`, `checkpoints`, and `archive/`.
|
|
145
|
+
- A `.plumbbob/` sidecar of flat files: `STATE` (the session sentinel — its presence
|
|
146
|
+
means a session is live), `intent.md`, `build-log.md`, `checkpoints`, and `archive/`.
|
|
147
147
|
|
|
148
148
|
## Gates — two tiers, different jobs
|
|
149
149
|
|
|
@@ -155,13 +155,15 @@ Calibration is the skill. When in doubt, smaller.
|
|
|
155
155
|
checkpoint while red. The hard gate lives on the deliberate boundary, not the
|
|
156
156
|
keystroke.
|
|
157
157
|
|
|
158
|
-
##
|
|
158
|
+
## Position is derived, not stored
|
|
159
159
|
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
160
|
+
There is no stored state machine. The dashboard's phase — `DESIGN`, `BUILD`, or `SPIKE` —
|
|
161
|
+
is *derived* from what's on disk: a `STEP` file means a step is in flight (BUILD), the
|
|
162
|
+
`SPIKE` marker means a fork is open (SPIKE), otherwise you're at a boundary (DESIGN). The
|
|
163
|
+
position gates nothing — it's a label on a map, read by `/plumbbob:pb-status` to tell you
|
|
164
|
+
where you are and what to do next, never a locked door. `.plumbbob/STATE` is just the
|
|
165
|
+
session sentinel: its presence means a session is live, and the post-edit hook is gated on
|
|
166
|
+
it, so a repo with no `.plumbbob/STATE` behaves exactly like plain Claude Code.
|
|
165
167
|
|
|
166
168
|
## Git footprint — additive only
|
|
167
169
|
|
|
@@ -175,9 +177,10 @@ archives plain markdown under `.plumbbob/archive/` and never touches git.
|
|
|
175
177
|
|
|
176
178
|
```text
|
|
177
179
|
.plumbbob/
|
|
178
|
-
STATE #
|
|
180
|
+
STATE # session sentinel — its presence means a session is live
|
|
179
181
|
SEAM # the in-flight step's declared paths (awareness, not a lock)
|
|
180
|
-
STEP # the in-flight step number
|
|
182
|
+
STEP # the in-flight step number (its presence is the BUILD phase)
|
|
183
|
+
SPIKE # marker — present while a spike fork is open
|
|
181
184
|
config # key=value; check=<heavy-check command> (defaults to pnpm run check)
|
|
182
185
|
checkpoints # "baseline <sha>" then "step N <sha>", one per verified step
|
|
183
186
|
intent.md # canonical intent
|
|
@@ -236,8 +239,10 @@ plumbbob doctor
|
|
|
236
239
|
|
|
237
240
|
`doctor` works for either install path: it confirms a marketplace plugin, or checks that the
|
|
238
241
|
skills-dir link resolves to the plugin manifest, the skills, and the hook — flagging a
|
|
239
|
-
double-install collision — and prints the exact fix for anything broken.
|
|
240
|
-
|
|
242
|
+
double-install collision — and prints the exact fix for anything broken. The terminal command
|
|
243
|
+
above is for the **global / skills-dir** install; a **marketplace** plugin puts the CLI on
|
|
244
|
+
PATH only *inside a Claude Code session*, so run **`/plumbbob:pb-doctor`** there instead. Run
|
|
245
|
+
it first if a `/plumbbob:*` skill ever opens with an empty dashboard;
|
|
241
246
|
[`docs/troubleshooting.md`](docs/troubleshooting.md) covers the rest.
|
|
242
247
|
|
|
243
248
|
## Development
|
package/bin/pb
CHANGED
|
@@ -1,6 +1,15 @@
|
|
|
1
1
|
#!/bin/sh
|
|
2
|
-
# pb — short alias for `plumbbob` (see bin/plumbbob for the full note). Same
|
|
3
|
-
#
|
|
4
|
-
#
|
|
5
|
-
|
|
2
|
+
# pb — short alias for `plumbbob` (see bin/plumbbob for the full note). Same dual
|
|
3
|
+
# role: the npm `bin` entry for `pb` (so npm ships it 0755) and the plugin PATH shim.
|
|
4
|
+
# Resolves any symlink chain so it works on PATH, globally linked, or as a dep.
|
|
5
|
+
src=$0
|
|
6
|
+
while [ -h "$src" ]; do
|
|
7
|
+
dir=$(CDPATH='' cd -- "$(dirname -- "$src")" && pwd)
|
|
8
|
+
link=$(readlink -- "$src")
|
|
9
|
+
case $link in
|
|
10
|
+
/*) src=$link ;;
|
|
11
|
+
*) src=$dir/$link ;;
|
|
12
|
+
esac
|
|
13
|
+
done
|
|
14
|
+
here=$(CDPATH='' cd -- "$(dirname -- "$src")" && pwd)
|
|
6
15
|
exec node "$here/../dist/cli.js" "$@"
|
package/bin/plumbbob
CHANGED
|
@@ -1,9 +1,26 @@
|
|
|
1
1
|
#!/bin/sh
|
|
2
|
-
# plumbbob —
|
|
3
|
-
#
|
|
4
|
-
#
|
|
5
|
-
#
|
|
6
|
-
#
|
|
7
|
-
#
|
|
8
|
-
|
|
2
|
+
# plumbbob — the package's `bin` entry AND the marketplace/plugin PATH shim.
|
|
3
|
+
#
|
|
4
|
+
# Pointing the npm `bin` at THIS file (not dist/cli.js) is deliberate: npm/pacote
|
|
5
|
+
# rewrites every packed file to 0644 except the ones named in `bin`, which it stamps
|
|
6
|
+
# 0755 — regardless of the mode in git. Claude Code adds this bin/ dir to the Bash
|
|
7
|
+
# tool's PATH while the plugin is enabled, so the shim must arrive executable; naming
|
|
8
|
+
# it in `bin` is what carries the +x bit through an npm-sourced plugin install.
|
|
9
|
+
# (dist/cli.js no longer needs its own bit — it is always run as `node` below.)
|
|
10
|
+
#
|
|
11
|
+
# As the npm bin, this file is also symlinked into a bin dir by `npm i -g` and by a
|
|
12
|
+
# local node_modules/.bin dep, so $0 can reach us through one or more symlinks.
|
|
13
|
+
# Resolve the chain to the real file, then run the bundled CLI relative to it — so it
|
|
14
|
+
# works on PATH (plugin), globally linked, and as a dep. Requires node >=22 on PATH
|
|
15
|
+
# (same as dist/cli.js's shebang).
|
|
16
|
+
src=$0
|
|
17
|
+
while [ -h "$src" ]; do
|
|
18
|
+
dir=$(CDPATH='' cd -- "$(dirname -- "$src")" && pwd)
|
|
19
|
+
link=$(readlink -- "$src")
|
|
20
|
+
case $link in
|
|
21
|
+
/*) src=$link ;;
|
|
22
|
+
*) src=$dir/$link ;;
|
|
23
|
+
esac
|
|
24
|
+
done
|
|
25
|
+
here=$(CDPATH='' cd -- "$(dirname -- "$src")" && pwd)
|
|
9
26
|
exec node "$here/../dist/cli.js" "$@"
|
package/dist/cli-core.js
CHANGED
|
@@ -16,15 +16,15 @@ import { wrap } from "./verbs/wrap.js";
|
|
|
16
16
|
import { init } from "./verbs/init.js";
|
|
17
17
|
import { doctor } from "./verbs/doctor.js";
|
|
18
18
|
const VERBS = [
|
|
19
|
-
{ name: 'start', summary: 'scaffold .plumbbob/;
|
|
20
|
-
{ name: 'status', summary: 'print the
|
|
21
|
-
{ name: 'build', summary: 'build <n>: write the seam from step n
|
|
19
|
+
{ name: 'start', summary: 'scaffold .plumbbob/; open the session; record the baseline commit' },
|
|
20
|
+
{ name: 'status', summary: 'print the orientation dashboard, or NO ACTIVE SESSION' },
|
|
21
|
+
{ name: 'build', summary: 'build <n>: write the seam from step n (in-flight, not a lock)' },
|
|
22
22
|
{ name: 'check', summary: 'run the heavy check and report; no state change' },
|
|
23
|
-
{ name: 'checkpoint', summary: 'checkpoint [<n>]: gate on green, commit/record SHA, mark step done
|
|
24
|
-
{ name: 'revert', summary: 'revert [--to n]: git reset --hard to a checkpoint SHA
|
|
23
|
+
{ name: 'checkpoint', summary: 'checkpoint [<n>]: gate on green, commit/record SHA, mark step done (executor-agnostic)' },
|
|
24
|
+
{ name: 'revert', summary: 'revert [--to n]: git reset --hard to a checkpoint SHA' },
|
|
25
25
|
{ name: 'park', summary: 'park "<text>": append a raw line to the park list' },
|
|
26
26
|
{ name: 'spike', summary: 'spike "<slug>" | spike done: throwaway worktree experiment' },
|
|
27
|
-
{ name: 'wrap', summary: 'v2 close-out: archive intent+log+report (no gate), clear the sidecar,
|
|
27
|
+
{ name: 'wrap', summary: 'v2 close-out: archive intent+log+report (no gate), clear the sidecar, close the session' },
|
|
28
28
|
{ name: 'init', summary: 'link plumbbob into Claude Code as an in-place plugin (~/.claude/skills/plumbbob); --uninstall to undo' },
|
|
29
29
|
{ name: 'doctor', summary: 'diagnose the plugin link (manifest, skills, hook) and print the fix for anything broken' },
|
|
30
30
|
];
|
package/dist/cli.js
CHANGED
|
File without changes
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
// Mutations to the active build-log.md — the human-facing ledger. `park` appends to
|
|
2
|
+
// its Park list; `checkpoint` appends to its Log so the build's history accrues
|
|
3
|
+
// line-by-line *as it happens* rather than being reconstructed at wrap (D-history).
|
|
4
|
+
// Pure string transforms (no fs, no classes — C1/C2); callers own the IO so these
|
|
5
|
+
// stay trivially testable.
|
|
6
|
+
// Append `line` after the last non-blank line of the `## <heading>` section — i.e.
|
|
7
|
+
// just before the next `## ` heading or EOF. Returns null when the section is absent
|
|
8
|
+
// so the caller can report rather than silently corrupt the doc.
|
|
9
|
+
export function appendToSection(content, heading, line) {
|
|
10
|
+
const lines = content.split('\n');
|
|
11
|
+
const headingIdx = lines.findIndex((l) => l.trim() === `## ${heading}`);
|
|
12
|
+
if (headingIdx === -1) {
|
|
13
|
+
return null;
|
|
14
|
+
}
|
|
15
|
+
let nextSection = lines.findIndex((l, i) => i > headingIdx && l.startsWith('## '));
|
|
16
|
+
if (nextSection === -1) {
|
|
17
|
+
nextSection = lines.length;
|
|
18
|
+
}
|
|
19
|
+
let insertAt = headingIdx + 1;
|
|
20
|
+
for (let i = headingIdx + 1; i < nextSection; i++) {
|
|
21
|
+
if ((lines[i] ?? '').trim() !== '') {
|
|
22
|
+
insertAt = i + 1;
|
|
23
|
+
}
|
|
24
|
+
}
|
|
25
|
+
lines.splice(insertAt, 0, line);
|
|
26
|
+
return lines.join('\n');
|
|
27
|
+
}
|
|
28
|
+
// The Log line `checkpoint` writes for a landed step: dated, names the step (its
|
|
29
|
+
// title when intent.md still carries it), and carries the short SHA. One line of the
|
|
30
|
+
// build's history — `/plumbbob:pb-wrap` reads these instead of re-narrating the build.
|
|
31
|
+
export function checkpointLogLine(date, step, sha, title) {
|
|
32
|
+
const head = `- ${date} — step ${step} checkpointed · ${sha.slice(0, 9)}`;
|
|
33
|
+
return title !== null && title.length > 0 ? `${head} — ${title}` : head;
|
|
34
|
+
}
|
package/dist/lib/orient.js
CHANGED
|
@@ -93,52 +93,45 @@ export function parseLastCheckpoint(checkpoints) {
|
|
|
93
93
|
return last;
|
|
94
94
|
}
|
|
95
95
|
// The single primary next move (D15). It suggests; the dashboard prints the full
|
|
96
|
-
// list + counts so the human can always override.
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
default: {
|
|
112
|
-
// DESIGN (and any unknown state): you are at the boundary.
|
|
113
|
-
const nextUndone = steps.find((s) => !s.done);
|
|
114
|
-
if (nextUndone === undefined) {
|
|
115
|
-
if (steps.length === 0) {
|
|
116
|
-
return 'plan the first step — `/plumbbob:pb-step`';
|
|
117
|
-
}
|
|
118
|
-
// Batch-default: the steps were planned up front, so finishing them usually
|
|
119
|
-
// means "wrap up" — but `/plumbbob:pb-step` can still add an increment if reality grew.
|
|
120
|
-
const harvest = parked > 0 ? `harvest ${parked} parked idea${parked === 1 ? '' : 's'} — \`/plumbbob:pb-harvest\`; then ` : '';
|
|
121
|
-
return `${harvest}wrap up — \`/plumbbob:pb-wrap\` (or \`/plumbbob:pb-step\` to add another increment)`;
|
|
122
|
-
}
|
|
123
|
-
return nextUndone.planned
|
|
124
|
-
? `build step ${nextUndone.n} — \`/plumbbob:pb-build\` (or \`/plumbbob:pb-step\` to revise it first)`
|
|
125
|
-
: `plan step ${nextUndone.n} — \`/plumbbob:pb-step\``;
|
|
96
|
+
// list + counts so the human can always override. The phase is derived: a spike
|
|
97
|
+
// in progress and an in-flight step each have one obvious next move; otherwise you
|
|
98
|
+
// are at the boundary and the move follows from the steps.
|
|
99
|
+
function nextMove(spiking, steps, inFlight, parked) {
|
|
100
|
+
if (spiking) {
|
|
101
|
+
return 'close the spike — `plumbbob spike done`';
|
|
102
|
+
}
|
|
103
|
+
if (inFlight !== null) {
|
|
104
|
+
return `finish step ${inFlight} — \`/plumbbob:pb-verify\` (or keep editing, then \`/plumbbob:pb-verify\`)`;
|
|
105
|
+
}
|
|
106
|
+
// At the boundary (DESIGN): the move follows from the steps.
|
|
107
|
+
const nextUndone = steps.find((s) => !s.done);
|
|
108
|
+
if (nextUndone === undefined) {
|
|
109
|
+
if (steps.length === 0) {
|
|
110
|
+
return 'plan the first step — `/plumbbob:pb-step`';
|
|
126
111
|
}
|
|
112
|
+
// Batch-default: the steps were planned up front, so finishing them usually
|
|
113
|
+
// means "wrap up" — but `/plumbbob:pb-step` can still add an increment if reality grew.
|
|
114
|
+
const harvest = parked > 0 ? `harvest ${parked} parked idea${parked === 1 ? '' : 's'} — \`/plumbbob:pb-harvest\`; then ` : '';
|
|
115
|
+
return `${harvest}wrap up — \`/plumbbob:pb-wrap\` (or \`/plumbbob:pb-step\` to add another increment)`;
|
|
127
116
|
}
|
|
117
|
+
return nextUndone.planned
|
|
118
|
+
? `build step ${nextUndone.n} — \`/plumbbob:pb-build\` (or \`/plumbbob:pb-step\` to revise it first)`
|
|
119
|
+
: `plan step ${nextUndone.n} — \`/plumbbob:pb-step\``;
|
|
128
120
|
}
|
|
129
121
|
export function orient(input) {
|
|
130
122
|
const steps = parseSteps(input.intent);
|
|
131
123
|
const parked = parseParked(input.buildLog);
|
|
132
124
|
const nextUndone = steps.find((s) => !s.done);
|
|
133
125
|
const seamParse = nextUndone === undefined ? null : parseStepSeam(input.intent, nextUndone.n);
|
|
126
|
+
const phase = input.spiking ? 'SPIKE' : input.inFlight !== null ? 'BUILD' : 'DESIGN';
|
|
134
127
|
return {
|
|
135
128
|
title: parseTitle(input.intent),
|
|
136
|
-
|
|
129
|
+
phase,
|
|
137
130
|
steps,
|
|
138
131
|
lastCheckpoint: parseLastCheckpoint(input.checkpoints),
|
|
139
132
|
parked,
|
|
140
133
|
openQuestions: parseOpenQuestions(input.intent),
|
|
141
|
-
next: nextMove(input.
|
|
134
|
+
next: nextMove(input.spiking, steps, input.inFlight, parked),
|
|
142
135
|
nextDoneWhen: nextUndone?.doneWhen ?? null,
|
|
143
136
|
nextSeam: seamParse !== null && seamParse.ok ? seamParse.seam : [],
|
|
144
137
|
};
|
|
@@ -168,7 +161,7 @@ export function formatOrientation(o) {
|
|
|
168
161
|
const cp = o.lastCheckpoint;
|
|
169
162
|
const cpLine = cp === null ? 'last checkpoint none yet' : `last checkpoint step ${cp.n} · ${cp.sha.slice(0, 7)}`;
|
|
170
163
|
return [
|
|
171
|
-
`Plumbbob — ${o.title ?? '(untitled)'} [${o.
|
|
164
|
+
`Plumbbob — ${o.title ?? '(untitled)'} [${o.phase}]`,
|
|
172
165
|
'',
|
|
173
166
|
stepsBlock,
|
|
174
167
|
'',
|
package/dist/lib/sidecar.js
CHANGED
|
@@ -1,7 +1,12 @@
|
|
|
1
1
|
// The .plumbbob/ sidecar: control state lives in flat files so the hooks can
|
|
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 a pure session sentinel: its EXISTENCE means "a session is active",
|
|
6
|
+
// and nothing reads its content. The phase the dashboard shows (DESIGN/BUILD/
|
|
7
|
+
// SPIKE) is derived, not stored — BUILD ⇔ a STEP is in flight, SPIKE ⇔ the SPIKE
|
|
8
|
+
// marker is present, otherwise DESIGN.
|
|
9
|
+
import { existsSync, readFileSync, writeFileSync, appendFileSync, rmSync } from 'node:fs';
|
|
5
10
|
import { join } from 'node:path';
|
|
6
11
|
import { gitDir } from "./git.js";
|
|
7
12
|
const DIRNAME = '.plumbbob';
|
|
@@ -11,6 +16,12 @@ export function sidecarDir(root) {
|
|
|
11
16
|
function statePath(root) {
|
|
12
17
|
return join(root, DIRNAME, 'STATE');
|
|
13
18
|
}
|
|
19
|
+
// The SPIKE marker (a single-purpose presence flag, like SEAM/STEP): written by
|
|
20
|
+
// `spike` on open, removed on `spike done`. Its existence is the one signal that
|
|
21
|
+
// the dashboard and the spike gates read to know "a spike is active".
|
|
22
|
+
export function spikePath(root) {
|
|
23
|
+
return join(root, DIRNAME, 'SPIKE');
|
|
24
|
+
}
|
|
14
25
|
// SEAM and STEP carry the in-flight step (D4/D7): a plain path list and a bare
|
|
15
26
|
// number, so the hooks read them with a grep and no markdown parsing.
|
|
16
27
|
export function seamPath(root) {
|
|
@@ -31,21 +42,24 @@ export function intentPath(root) {
|
|
|
31
42
|
export function buildLogPath(root) {
|
|
32
43
|
return join(root, DIRNAME, 'build-log.md');
|
|
33
44
|
}
|
|
34
|
-
// A session exists iff STATE exists. Deleting STATE (at
|
|
35
|
-
//
|
|
45
|
+
// A session exists iff STATE exists. Deleting STATE (at wrap) is what flips the
|
|
46
|
+
// repo back to "no session" — so it is the single source of truth for "is there
|
|
47
|
+
// a session". `start` calls beginSession; `wrap` removes the file.
|
|
36
48
|
export function hasSession(root) {
|
|
37
49
|
return existsSync(statePath(root));
|
|
38
50
|
}
|
|
39
|
-
export function
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
51
|
+
export function beginSession(root) {
|
|
52
|
+
writeFileSync(statePath(root), 'active\n');
|
|
53
|
+
}
|
|
54
|
+
// SPIKE marker helpers — existence is the whole signal (content is irrelevant).
|
|
55
|
+
export function inSpike(root) {
|
|
56
|
+
return existsSync(spikePath(root));
|
|
57
|
+
}
|
|
58
|
+
export function markSpike(root) {
|
|
59
|
+
writeFileSync(spikePath(root), 'active\n');
|
|
46
60
|
}
|
|
47
|
-
export function
|
|
48
|
-
|
|
61
|
+
export function clearSpike(root) {
|
|
62
|
+
rmSync(spikePath(root), { force: true });
|
|
49
63
|
}
|
|
50
64
|
// D17: keep the sidecar untracked by appending `.plumbbob/` to the repo's
|
|
51
65
|
// git/info/exclude. Idempotent — a re-`start` after finish must not double-add.
|
package/dist/verbs/build.js
CHANGED
|
@@ -1,9 +1,9 @@
|
|
|
1
1
|
// `plumbbob build <n>` — read step n's seam from intent.md, write the normalized
|
|
2
|
-
// SEAM + STEP
|
|
3
|
-
//
|
|
2
|
+
// SEAM + STEP. The STEP file is the in-flight signal (the dashboard derives the
|
|
3
|
+
// BUILD phase from it); it never checkpoints (only `checkpoint` commits).
|
|
4
4
|
import { readFileSync, writeFileSync } from 'node:fs';
|
|
5
5
|
import { findRepoRoot } from "../lib/git.js";
|
|
6
|
-
import { hasSession, intentPath, seamPath, stepPath
|
|
6
|
+
import { hasSession, intentPath, seamPath, stepPath } from "../lib/sidecar.js";
|
|
7
7
|
import { parseStepSeam } from "../lib/intent.js";
|
|
8
8
|
export function build(cwd, args) {
|
|
9
9
|
const root = findRepoRoot(cwd);
|
|
@@ -24,7 +24,6 @@ export function build(cwd, args) {
|
|
|
24
24
|
}
|
|
25
25
|
writeFileSync(seamPath(root), `${parsed.seam.join('\n')}\n`);
|
|
26
26
|
writeFileSync(stepPath(root), `${step}\n`);
|
|
27
|
-
|
|
28
|
-
process.stdout.write(`plumbbob: building step ${step} — STATE=BUILD. Seam (for orientation; not a lock in v2):\n${parsed.seam.map((p) => ` ${p}`).join('\n')}\n`);
|
|
27
|
+
process.stdout.write(`plumbbob: building step ${step}. Seam (for orientation; not a lock in v2):\n${parsed.seam.map((p) => ` ${p}`).join('\n')}\n`);
|
|
29
28
|
return 0;
|
|
30
29
|
}
|
package/dist/verbs/checkpoint.js
CHANGED
|
@@ -1,16 +1,18 @@
|
|
|
1
1
|
// `plumbbob checkpoint [<n>] [-m <msg>]` — the executor-agnostic commit tick (D3).
|
|
2
|
-
// Unlike v1 `done`, it does NOT require
|
|
3
|
-
//
|
|
4
|
-
//
|
|
5
|
-
//
|
|
6
|
-
//
|
|
7
|
-
//
|
|
8
|
-
// a vibe session, or another harness all checkpoint
|
|
2
|
+
// Unlike v1 `done`, it does NOT require a STEP file: the step is whatever you pass,
|
|
3
|
+
// else the in-flight STEP, else the next undone step in intent. It gates on a green
|
|
4
|
+
// check, then commits any pending work (or records the existing HEAD when the tree
|
|
5
|
+
// is already clean — the human's commit skill may have committed first), records the
|
|
6
|
+
// SHA, flips the intent checkbox to `[x]`, and clears any STEP/SEAM (which returns
|
|
7
|
+
// the dashboard to the DESIGN boundary). The diff's author is irrelevant:
|
|
8
|
+
// `/plumbbob:pb-build`, your hands, a vibe session, or another harness all checkpoint
|
|
9
|
+
// the same way.
|
|
9
10
|
import { appendFileSync, readFileSync, rmSync, writeFileSync } from 'node:fs';
|
|
10
11
|
import { commit, findRepoRoot, headSha, isDirty, stageAll } from "../lib/git.js";
|
|
11
|
-
import { checkpointsPath, hasSession, intentPath, seamPath, stepPath
|
|
12
|
+
import { buildLogPath, checkpointsPath, hasSession, intentPath, seamPath, stepPath } from "../lib/sidecar.js";
|
|
12
13
|
import { runCheck } from "../lib/check.js";
|
|
13
14
|
import { markStepDone, parseSteps } from "../lib/orient.js";
|
|
15
|
+
import { appendToSection, checkpointLogLine } from "../lib/buildlog.js";
|
|
14
16
|
export function checkpoint(cwd, args) {
|
|
15
17
|
const root = findRepoRoot(cwd);
|
|
16
18
|
if (root === null || !hasSession(root)) {
|
|
@@ -36,10 +38,10 @@ export function checkpoint(cwd, args) {
|
|
|
36
38
|
}
|
|
37
39
|
appendFileSync(checkpointsPath(root), `step ${step} ${sha}\n`);
|
|
38
40
|
flipIntent(root, step);
|
|
41
|
+
logCheckpoint(root, step, sha);
|
|
39
42
|
rmSync(seamPath(root), { force: true });
|
|
40
43
|
rmSync(stepPath(root), { force: true });
|
|
41
|
-
|
|
42
|
-
process.stdout.write(`plumbbob: step ${step} checkpointed — ${sha.slice(0, 9)}. STATE=DESIGN.\n`);
|
|
44
|
+
process.stdout.write(`plumbbob: step ${step} checkpointed — ${sha.slice(0, 9)}. Back at the boundary.\n`);
|
|
43
45
|
return 0;
|
|
44
46
|
}
|
|
45
47
|
// Step resolution (D3): explicit arg > in-flight STEP file > first undone step in
|
|
@@ -68,6 +70,32 @@ function flipIntent(root, step) {
|
|
|
68
70
|
// best-effort bookkeeping; the checkpoint SHA is the source of truth.
|
|
69
71
|
}
|
|
70
72
|
}
|
|
73
|
+
// Append a dated line to the build-log's `## Log` so the build's history accrues at
|
|
74
|
+
// each checkpoint instead of being reconstructed at wrap. The step's title is lifted
|
|
75
|
+
// from intent.md when still present. Best-effort: a missing/odd build-log never blocks
|
|
76
|
+
// a checkpoint — the `checkpoints` SHA is the source of truth.
|
|
77
|
+
function logCheckpoint(root, step, sha) {
|
|
78
|
+
try {
|
|
79
|
+
const path = buildLogPath(root);
|
|
80
|
+
const date = new Date().toISOString().slice(0, 10);
|
|
81
|
+
const line = checkpointLogLine(date, step, sha, titleForStep(root, step));
|
|
82
|
+
const updated = appendToSection(readFileSync(path, 'utf8'), 'Log', line);
|
|
83
|
+
if (updated !== null) {
|
|
84
|
+
writeFileSync(path, updated);
|
|
85
|
+
}
|
|
86
|
+
}
|
|
87
|
+
catch {
|
|
88
|
+
// best-effort ledger; never fail a checkpoint over the build-log.
|
|
89
|
+
}
|
|
90
|
+
}
|
|
91
|
+
function titleForStep(root, step) {
|
|
92
|
+
try {
|
|
93
|
+
return parseSteps(readFileSync(intentPath(root), 'utf8')).find((s) => s.n === step)?.title ?? null;
|
|
94
|
+
}
|
|
95
|
+
catch {
|
|
96
|
+
return null;
|
|
97
|
+
}
|
|
98
|
+
}
|
|
71
99
|
function messageArg(args) {
|
|
72
100
|
const i = args.indexOf('-m');
|
|
73
101
|
return i !== -1 && i + 1 < args.length ? (args[i + 1] ?? null) : null;
|
package/dist/verbs/doctor.js
CHANGED
|
@@ -86,7 +86,7 @@ export function doctor() {
|
|
|
86
86
|
out.push(failed === 0
|
|
87
87
|
? 'plumbbob: all checks passed. If a skill still misbehaves, restart Claude Code (or /reload-plugins).'
|
|
88
88
|
: `plumbbob: ${failed} problem(s) — apply the → fixes, then restart Claude Code.`);
|
|
89
|
-
out.push('plumbbob: skills shell a bare `plumbbob`. The marketplace plugin puts it on PATH via bin
|
|
89
|
+
out.push('plumbbob: skills shell a bare `plumbbob`. The marketplace plugin puts it on PATH only inside a Claude Code session (via bin/) — there is no terminal `plumbbob`; for one (or the skills-dir install) run `npm i -g plumbbob`. In-session you can also run this as `/plumbbob:pb-doctor`. Sessions are per-project via `plumbbob start`.');
|
|
90
90
|
process.stdout.write(`${out.join('\n')}\n`);
|
|
91
91
|
return failed === 0 ? 0 : 1;
|
|
92
92
|
}
|
package/dist/verbs/park.js
CHANGED
|
@@ -4,6 +4,7 @@
|
|
|
4
4
|
import { readFileSync, writeFileSync } from 'node:fs';
|
|
5
5
|
import { findRepoRoot } from "../lib/git.js";
|
|
6
6
|
import { hasSession, buildLogPath } from "../lib/sidecar.js";
|
|
7
|
+
import { appendToSection } from "../lib/buildlog.js";
|
|
7
8
|
export function park(cwd, args) {
|
|
8
9
|
const root = findRepoRoot(cwd);
|
|
9
10
|
if (root === null || !hasSession(root)) {
|
|
@@ -19,7 +20,7 @@ export function park(cwd, args) {
|
|
|
19
20
|
return 1;
|
|
20
21
|
}
|
|
21
22
|
const path = buildLogPath(root);
|
|
22
|
-
const updated =
|
|
23
|
+
const updated = appendToSection(readFileSync(path, 'utf8'), 'Park list', `- [ ] ${text}`);
|
|
23
24
|
if (updated === null) {
|
|
24
25
|
process.stderr.write('plumbbob: could not find a "## Park list" section in build-log.md.\n');
|
|
25
26
|
return 1;
|
|
@@ -28,24 +29,3 @@ export function park(cwd, args) {
|
|
|
28
29
|
process.stdout.write(`parked: ${text}\n`);
|
|
29
30
|
return 0;
|
|
30
31
|
}
|
|
31
|
-
// Append after the last non-blank line of the Park list section (i.e. just before
|
|
32
|
-
// the next `## ` heading). Returns null if there is no Park list to append to.
|
|
33
|
-
function insertParkItem(content, text) {
|
|
34
|
-
const lines = content.split('\n');
|
|
35
|
-
const headingIdx = lines.findIndex((line) => line.trim() === '## Park list');
|
|
36
|
-
if (headingIdx === -1) {
|
|
37
|
-
return null;
|
|
38
|
-
}
|
|
39
|
-
let nextSection = lines.findIndex((line, i) => i > headingIdx && line.startsWith('## '));
|
|
40
|
-
if (nextSection === -1) {
|
|
41
|
-
nextSection = lines.length;
|
|
42
|
-
}
|
|
43
|
-
let insertAt = headingIdx + 1;
|
|
44
|
-
for (let i = headingIdx + 1; i < nextSection; i++) {
|
|
45
|
-
if ((lines[i] ?? '').trim() !== '') {
|
|
46
|
-
insertAt = i + 1;
|
|
47
|
-
}
|
|
48
|
-
}
|
|
49
|
-
lines.splice(insertAt, 0, `- [ ] ${text}`);
|
|
50
|
-
return lines.join('\n');
|
|
51
|
-
}
|
package/dist/verbs/revert.js
CHANGED
|
@@ -13,7 +13,7 @@ import { tmpdir } from 'node:os';
|
|
|
13
13
|
import { join } from 'node:path';
|
|
14
14
|
import { fileURLToPath } from 'node:url';
|
|
15
15
|
import { findRepoRoot, resetHard, untrackedPaths } from "../lib/git.js";
|
|
16
|
-
import { checkpointsPath, hasSession, seamPath, sidecarDir, stepPath
|
|
16
|
+
import { checkpointsPath, hasSession, seamPath, sidecarDir, stepPath } from "../lib/sidecar.js";
|
|
17
17
|
import { matchesSeam } from "../lib/intent.js";
|
|
18
18
|
export function revert(cwd, args) {
|
|
19
19
|
const root = findRepoRoot(cwd);
|
|
@@ -53,8 +53,7 @@ export function revert(cwd, args) {
|
|
|
53
53
|
}
|
|
54
54
|
rmSync(seamPath(root), { force: true });
|
|
55
55
|
rmSync(stepPath(root), { force: true });
|
|
56
|
-
|
|
57
|
-
process.stdout.write(`plumbbob: reverted to ${sha.slice(0, 9)} — STATE=DESIGN. Park lines and intent edits were preserved.\n`);
|
|
56
|
+
process.stdout.write(`plumbbob: reverted to ${sha.slice(0, 9)} — back at the boundary. Park lines and intent edits were preserved.\n`);
|
|
58
57
|
return 0;
|
|
59
58
|
}
|
|
60
59
|
// The repo paths that belong to plumbbob, not to the work being reverted: the
|
package/dist/verbs/spike.js
CHANGED
|
@@ -1,10 +1,10 @@
|
|
|
1
1
|
// `plumbbob spike` (D18) — the spike lifecycle for a genuine fork the design
|
|
2
2
|
// phase couldn't settle. `spike "<slug>" [opt…]` creates a sibling git worktree +
|
|
3
3
|
// `spike/<slug>-<opt>` branch per option OUTSIDE the repo root (default opts a/b)
|
|
4
|
-
// and
|
|
5
|
-
//
|
|
6
|
-
//
|
|
7
|
-
// `spike done` removes every spike worktree + branch and
|
|
4
|
+
// and drops the SPIKE marker; the main tree stays put while you experiment in the
|
|
5
|
+
// worktrees, which are hook-dormant by construction — the untracked sidecar (D17)
|
|
6
|
+
// doesn't exist in a fresh checkout, so the hooks find no STATE there.
|
|
7
|
+
// `spike done` removes every spike worktree + branch and clears the marker.
|
|
8
8
|
//
|
|
9
9
|
// Worktree git calls run directly here rather than via lib/git.ts (which holds the
|
|
10
10
|
// shared additive read/commit helpers): worktree management is spike-local, and
|
|
@@ -13,7 +13,7 @@ import { execFileSync } from 'node:child_process';
|
|
|
13
13
|
import { existsSync } from 'node:fs';
|
|
14
14
|
import { basename, dirname, join } from 'node:path';
|
|
15
15
|
import { findRepoRoot } from "../lib/git.js";
|
|
16
|
-
import { hasSession,
|
|
16
|
+
import { hasSession, inSpike, markSpike, clearSpike, stepPath } from "../lib/sidecar.js";
|
|
17
17
|
const DEFAULT_OPTIONS = ['a', 'b'];
|
|
18
18
|
export function spike(cwd, args) {
|
|
19
19
|
const root = findRepoRoot(cwd);
|
|
@@ -28,14 +28,13 @@ export function spike(cwd, args) {
|
|
|
28
28
|
return spikeStart(root, positionals);
|
|
29
29
|
}
|
|
30
30
|
function spikeStart(root, positionals) {
|
|
31
|
-
|
|
32
|
-
if (state === 'SPIKE') {
|
|
31
|
+
if (inSpike(root)) {
|
|
33
32
|
process.stderr.write('plumbbob: already in a spike. Run `plumbbob spike done` to close it first.\n');
|
|
34
33
|
return 1;
|
|
35
34
|
}
|
|
36
|
-
if (
|
|
37
|
-
process.stderr.write(
|
|
38
|
-
'A spike is a deliberate fork —
|
|
35
|
+
if (existsSync(stepPath(root))) {
|
|
36
|
+
process.stderr.write('plumbbob: spike starts from a settled boundary, but a step is in flight. ' +
|
|
37
|
+
'A spike is a deliberate fork — checkpoint or revert the current step first.\n');
|
|
39
38
|
return 1;
|
|
40
39
|
}
|
|
41
40
|
const slug = sanitize(positionals[0] ?? '');
|
|
@@ -55,16 +54,15 @@ function spikeStart(root, positionals) {
|
|
|
55
54
|
git(root, ['worktree', 'add', '-b', `spike/${slug}-${opt}`, path, 'HEAD']);
|
|
56
55
|
created.push(path);
|
|
57
56
|
}
|
|
58
|
-
|
|
59
|
-
process.stdout.write(`plumbbob:
|
|
57
|
+
markSpike(root);
|
|
58
|
+
process.stdout.write(`plumbbob: spiking — the main tree stays put. Experiment in the throwaway worktrees:\n${created
|
|
60
59
|
.map((p) => ` ${p}`)
|
|
61
60
|
.join('\n')}\nWhen you've decided, record the verdict in intent.md and run \`plumbbob spike done\`.\n`);
|
|
62
61
|
return 0;
|
|
63
62
|
}
|
|
64
63
|
function spikeDone(root) {
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
process.stderr.write(`plumbbob: no active spike (state is ${state ?? 'UNKNOWN'}).\n`);
|
|
64
|
+
if (!inSpike(root)) {
|
|
65
|
+
process.stderr.write('plumbbob: no active spike to close.\n');
|
|
68
66
|
return 1;
|
|
69
67
|
}
|
|
70
68
|
for (const path of spikeWorktrees(root)) {
|
|
@@ -74,8 +72,8 @@ function spikeDone(root) {
|
|
|
74
72
|
for (const branch of spikeBranches(root)) {
|
|
75
73
|
git(root, ['branch', '-D', branch]);
|
|
76
74
|
}
|
|
77
|
-
|
|
78
|
-
process.stdout.write('plumbbob: spike closed —
|
|
75
|
+
clearSpike(root);
|
|
76
|
+
process.stdout.write('plumbbob: spike closed — worktrees and branches removed, back at the boundary. ' +
|
|
79
77
|
'Record the verdict (which option won, and why) in intent.md before you `build`.\n');
|
|
80
78
|
return 0;
|
|
81
79
|
}
|
package/dist/verbs/start.js
CHANGED
|
@@ -1,10 +1,10 @@
|
|
|
1
|
-
// `plumbbob start "<title>"` — scaffold the sidecar, record the baseline,
|
|
2
|
-
//
|
|
1
|
+
// `plumbbob start "<title>"` — scaffold the sidecar, record the baseline, open
|
|
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
4
|
import { readFileSync, writeFileSync, mkdirSync } from 'node:fs';
|
|
5
5
|
import { join } from 'node:path';
|
|
6
6
|
import { findRepoRoot, hasCommit, headSha, isDirty } from "../lib/git.js";
|
|
7
|
-
import { sidecarDir, checkpointsPath, configPath, intentPath, buildLogPath,
|
|
7
|
+
import { sidecarDir, checkpointsPath, configPath, intentPath, buildLogPath, beginSession, hasSession, excludeSidecar, } from "../lib/sidecar.js";
|
|
8
8
|
const DEFAULT_CHECK = 'pnpm run check';
|
|
9
9
|
export function start(cwd, args) {
|
|
10
10
|
const positionals = args.filter((a) => !a.startsWith('--'));
|
|
@@ -37,7 +37,7 @@ export function start(cwd, args) {
|
|
|
37
37
|
const sha = headSha(root);
|
|
38
38
|
const check = detectCheck(root);
|
|
39
39
|
mkdirSync(sidecarDir(root), { recursive: true });
|
|
40
|
-
|
|
40
|
+
beginSession(root);
|
|
41
41
|
writeFileSync(checkpointsPath(root), `baseline ${sha}\n`);
|
|
42
42
|
writeFileSync(configPath(root), `check=${check.command}\n`);
|
|
43
43
|
writeFileSync(intentPath(root), stamp(readTemplate('intent.md'), title, check.command));
|
|
@@ -46,7 +46,7 @@ export function start(cwd, args) {
|
|
|
46
46
|
if (check.warn) {
|
|
47
47
|
process.stderr.write(`plumbbob: WARNING the heavy check '${check.command}' is not defined in this repo's package.json. Edit .plumbbob/config (check=...) to set the real gate before \`review\`/\`done\`.\n`);
|
|
48
48
|
}
|
|
49
|
-
process.stdout.write(`plumbbob: started "${title}" —
|
|
49
|
+
process.stdout.write(`plumbbob: started "${title}" — baseline ${sha.slice(0, 9)}. Frame and decide in .plumbbob/intent.md; \`build\` a step once the decisions are made.\n`);
|
|
50
50
|
return 0;
|
|
51
51
|
}
|
|
52
52
|
// D24: default the heavy check to `pnpm run check`; warn (but still record it)
|
package/dist/verbs/status.js
CHANGED
|
@@ -3,7 +3,7 @@
|
|
|
3
3
|
// behavior, so the `NO ACTIVE SESSION` sentinel is kept exact.
|
|
4
4
|
import { readFileSync } from 'node:fs';
|
|
5
5
|
import { findRepoRoot } from "../lib/git.js";
|
|
6
|
-
import { buildLogPath, checkpointsPath, hasSession,
|
|
6
|
+
import { buildLogPath, checkpointsPath, hasSession, inSpike, intentPath, stepPath } from "../lib/sidecar.js";
|
|
7
7
|
import { formatOrientation, orient } from "../lib/orient.js";
|
|
8
8
|
function readOr(path) {
|
|
9
9
|
try {
|
|
@@ -21,11 +21,11 @@ export function status(cwd) {
|
|
|
21
21
|
}
|
|
22
22
|
const inFlightRaw = readOr(stepPath(root)).trim();
|
|
23
23
|
const orientation = orient({
|
|
24
|
-
state: readState(root) ?? 'UNKNOWN',
|
|
25
24
|
intent: readOr(intentPath(root)),
|
|
26
25
|
buildLog: readOr(buildLogPath(root)),
|
|
27
26
|
checkpoints: readOr(checkpointsPath(root)),
|
|
28
27
|
inFlight: /^\d+$/.test(inFlightRaw) ? Number(inFlightRaw) : null,
|
|
28
|
+
spiking: inSpike(root),
|
|
29
29
|
});
|
|
30
30
|
process.stdout.write(`${formatOrientation(orientation)}\n`);
|
|
31
31
|
return 0;
|
package/dist/verbs/wrap.js
CHANGED
|
@@ -7,7 +7,7 @@
|
|
|
7
7
|
import { appendFileSync, existsSync, readFileSync, rmSync } from 'node:fs';
|
|
8
8
|
import { join, relative } from 'node:path';
|
|
9
9
|
import { findRepoRoot } from "../lib/git.js";
|
|
10
|
-
import { buildLogPath, checkpointsPath, hasSession, intentPath, seamPath, sidecarDir, stepPath } from "../lib/sidecar.js";
|
|
10
|
+
import { buildLogPath, checkpointsPath, hasSession, intentPath, seamPath, sidecarDir, spikePath, stepPath } from "../lib/sidecar.js";
|
|
11
11
|
import { archiveSession, reportPath } from "../lib/archive.js";
|
|
12
12
|
export function wrap(cwd) {
|
|
13
13
|
const root = findRepoRoot(cwd);
|
|
@@ -30,6 +30,7 @@ export function wrap(cwd) {
|
|
|
30
30
|
rmSync(reportPath(root), { force: true });
|
|
31
31
|
rmSync(seamPath(root), { force: true });
|
|
32
32
|
rmSync(stepPath(root), { force: true });
|
|
33
|
+
rmSync(spikePath(root), { force: true });
|
|
33
34
|
rmSync(join(sidecarDir(root), 'STATE'), { force: true });
|
|
34
35
|
process.stdout.write(`plumbbob: wrap — archived to ${relative(root, archived)}. Sidecar cleared. ` +
|
|
35
36
|
'Run `/plumbbob:pb-plan` (or `plumbbob start "<title>"`) to frame the next goal.\n');
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "plumbbob",
|
|
3
|
-
"version": "0.4.
|
|
3
|
+
"version": "0.4.12",
|
|
4
4
|
"description": "Attention-first build process: 11 skills + a CLI that keep you the decider — guidance, not enforcement.",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"claude-code",
|
|
@@ -20,8 +20,8 @@
|
|
|
20
20
|
"license": "Apache-2.0",
|
|
21
21
|
"author": "Rob McLarty <hello@robmclarty.com>",
|
|
22
22
|
"bin": {
|
|
23
|
-
"plumbbob": "
|
|
24
|
-
"pb": "
|
|
23
|
+
"plumbbob": "bin/plumbbob",
|
|
24
|
+
"pb": "bin/pb"
|
|
25
25
|
},
|
|
26
26
|
"files": [
|
|
27
27
|
".claude-plugin",
|
package/skills/pb-build/SKILL.md
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
name: pb-build
|
|
3
3
|
description: The optional engine — read the next planned step from intent, implement it (its done-when, seam, Decisions, Constraints), then verify it through to the approval pause. Skip it to build by hand/vibed/another harness. `--auto` self-approves and chains to done.
|
|
4
|
+
argument-hint: "[step-number] [--auto]"
|
|
4
5
|
disable-model-invocation: true
|
|
5
6
|
model: opus
|
|
6
7
|
allowed-tools: Read, Edit, Write, Bash(plumbbob status:*), Bash(plumbbob build:*), Bash(plumbbob check:*), Bash(plumbbob checkpoint:*), Bash(git diff:*)
|
|
@@ -40,8 +41,9 @@ at the pause for your approval. **Re-firing `/plumbbob:pb-build` is itself the c
|
|
|
40
41
|
does: `plumbbob check` → self-review the diff against the done-when, the
|
|
41
42
|
Decisions, and the Constraints (a single structured read, D16) → validate → **PAUSE
|
|
42
43
|
for the human's approval** → only on approval, checkpoint with
|
|
43
|
-
`plumbbob checkpoint
|
|
44
|
-
|
|
44
|
+
`plumbbob checkpoint` (which also appends this step to the build-log's `## Log` — the
|
|
45
|
+
history writes itself at each checkpoint, so you don't hand-log it). Do **not** bump
|
|
46
|
+
the version or changelog — that is the human's `/version` call.
|
|
45
47
|
|
|
46
48
|
## `--auto` — let the agent be the clock (opt-in)
|
|
47
49
|
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: pb-doctor
|
|
3
|
+
description: Diagnose the plugin install from inside a session — is plumbbob linked, are the skills and hook present, is there a collision. A thin trigger for `plumbbob doctor`. Matters most for a marketplace install, where the CLI is on PATH only inside Claude Code.
|
|
4
|
+
disable-model-invocation: true
|
|
5
|
+
model: haiku
|
|
6
|
+
allowed-tools: Bash(plumbbob doctor:*)
|
|
7
|
+
---
|
|
8
|
+
|
|
9
|
+
# Plumbbob — doctor (the is-it-installed-right move)
|
|
10
|
+
|
|
11
|
+
Install diagnostic (injected when this skill runs): !`if command -v plumbbob >/dev/null 2>&1; then plumbbob doctor 2>&1; else echo "plumbbob CLI not on PATH in this session. Marketplace install: confirm the plugin is enabled in /plugin, then /reload-plugins. Skills-dir/global install: npm i -g plumbbob && plumbbob init."; fi`
|
|
12
|
+
|
|
13
|
+
This is a **thin driver** for `plumbbob doctor`. The report above is read-only — `doctor`
|
|
14
|
+
inspects, it never writes. This skill carries **no Edit and no Write tool**: never edit a file
|
|
15
|
+
to change what the report says. If a check failed, surface its `→ fix` verbatim and apply
|
|
16
|
+
*that* fix, then have the human **restart Claude Code (or `/reload-plugins`)**. **Never retry.**
|
|
17
|
+
|
|
18
|
+
## Why a skill (and not just the terminal)
|
|
19
|
+
|
|
20
|
+
The CLI's reach differs by install path, and that is the whole point of running `doctor` here:
|
|
21
|
+
|
|
22
|
+
- **Marketplace plugin** — `plumbbob` is on PATH **only inside a Claude Code session** (its
|
|
23
|
+
`bin/` shims are injected into the Bash tool while the plugin is enabled). There is **no
|
|
24
|
+
terminal `plumbbob`** to run. So this skill — or letting me run `plumbbob doctor` in-session
|
|
25
|
+
— is the *only* way to reach `doctor`. The fact that this skill loaded at all already proves
|
|
26
|
+
the plugin is enabled and the shim is on PATH.
|
|
27
|
+
- **Skills-dir / global install** (`npm i -g plumbbob` + `plumbbob init`) — `plumbbob` is on
|
|
28
|
+
PATH everywhere, so terminal `plumbbob doctor` works too; this skill is just convenience.
|
|
29
|
+
|
|
30
|
+
The one failure `doctor` **cannot** diagnose: a plugin that never loaded at all (no
|
|
31
|
+
`/plumbbob:*` skill appears). That is a `/plugin` / `/reload-plugins` problem, not doctor's —
|
|
32
|
+
and if you are reading this, that is not your situation.
|
|
33
|
+
|
|
34
|
+
## What it does
|
|
35
|
+
|
|
36
|
+
1. Surface the injected `doctor` report verbatim — every `✓` and any `✗` with its `→ fix`.
|
|
37
|
+
2. If all checks passed, say so; if a skill still misbehaves, the fix is a restart, not a re-run.
|
|
38
|
+
3. If a check failed, apply its named `→ fix`, then restart Claude Code (or `/reload-plugins`).
|
|
39
|
+
4. If the line above says the CLI is not on PATH, follow the install-path branch it printed.
|
|
@@ -13,15 +13,16 @@ Current session state (injected when this skill runs): !`plumbbob status 2>/dev/
|
|
|
13
13
|
`/plumbbob:pb-harvest` is the complement of `/plumbbob:pb-park` (D7): you parked ideas as seeds during a
|
|
14
14
|
build; now, at a boundary, you harvest them — decide what each one is.
|
|
15
15
|
|
|
16
|
-
## When to run it —
|
|
16
|
+
## When to run it — boundary only
|
|
17
17
|
|
|
18
|
-
Harvest at a **step boundary**: after a step is checkpointed and you are back
|
|
19
|
-
DESIGN, not mid-step. Read the
|
|
18
|
+
Harvest at a **step boundary**: after a step is checkpointed and you are back at the
|
|
19
|
+
DESIGN boundary, not mid-step. Read the dashboard injected above:
|
|
20
20
|
|
|
21
21
|
- `NO ACTIVE SESSION` — **refuse**; tell the human to `plumbbob start "<title>"` first.
|
|
22
|
-
-
|
|
23
|
-
|
|
24
|
-
|
|
22
|
+
- A step in flight (the dashboard reads `[BUILD]`, and `next →` points at finishing the
|
|
23
|
+
step) — **refuse** and suggest finishing it with `/plumbbob:pb-verify` before harvesting.
|
|
24
|
+
Chasing parked items mid-step is the disease parking prevents.
|
|
25
|
+
- At the boundary (the dashboard reads `[DESIGN]`) — go ahead.
|
|
25
26
|
|
|
26
27
|
## What this skill does
|
|
27
28
|
|
package/skills/pb-park/SKILL.md
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
name: pb-park
|
|
3
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. The capture half of the park/harvest loop.
|
|
4
|
+
argument-hint: "[idea]"
|
|
4
5
|
disable-model-invocation: true
|
|
5
6
|
model: haiku
|
|
6
7
|
allowed-tools: Bash(plumbbob status:*), Bash(plumbbob park:*)
|
|
@@ -16,10 +17,10 @@ it — is the whole point: it protects the step in flight.
|
|
|
16
17
|
|
|
17
18
|
## Wrong-state refusal
|
|
18
19
|
|
|
19
|
-
Parking needs an **active session** to capture into. Read the
|
|
20
|
+
Parking needs an **active session** to capture into. Read the dashboard injected above: if
|
|
20
21
|
it is `NO ACTIVE SESSION`, **refuse** and tell the human to run `plumbbob start
|
|
21
|
-
"<title>"` first.
|
|
22
|
-
capture is always available, which is the whole point of parking.
|
|
22
|
+
"<title>"` first. Any time the session is live — at the boundary, mid-step, or in a spike —
|
|
23
|
+
is fine; capture is always available, which is the whole point of parking.
|
|
23
24
|
|
|
24
25
|
## What this skill does
|
|
25
26
|
|
package/skills/pb-plan/SKILL.md
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
name: pb-plan
|
|
3
3
|
description: "Frame a fresh goal and author the whole plan — Frame, Decisions, Constraints, and all Steps — before any code. Three input modes: no arg interviews you; a file path absorbs a spec; any other text expands your inline intent."
|
|
4
|
+
argument-hint: "[spec-path | intent]"
|
|
4
5
|
disable-model-invocation: true
|
|
5
6
|
model: opus
|
|
6
7
|
allowed-tools: Read, Edit, Write, Bash(plumbbob status:*), Bash(plumbbob start:*)
|
|
@@ -38,7 +39,7 @@ an agent can follow with `/plumbbob:pb-build`. The argument only seeds how you g
|
|
|
38
39
|
## What this skill does
|
|
39
40
|
|
|
40
41
|
1. **Scaffold.** If there is no active session, run `plumbbob start "<title>"`
|
|
41
|
-
to create `.plumbbob/` (
|
|
42
|
+
to create `.plumbbob/` (baseline recorded, session opened). If a session already
|
|
42
43
|
exists, say so and edit the existing `intent.md` rather than starting over.
|
|
43
44
|
2. **Frame** (`.plumbbob/intent.md`), with the human: the **Problem** in plain words,
|
|
44
45
|
the **smallest thing** that solves it, what **done looks like**, and what you are
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
name: pb-refine
|
|
3
3
|
description: Keep intent.md true — attack the plan for holes (append as Open questions) and refine or repair the Frame, Decisions, Constraints, and Steps to match reality. Usable at any point; you propose, the human approves.
|
|
4
|
+
argument-hint: "[focus]"
|
|
4
5
|
disable-model-invocation: true
|
|
5
6
|
model: opus
|
|
6
7
|
allowed-tools: Read, Edit, Bash(plumbbob status:*)
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
name: pb-revert
|
|
3
|
-
description: Human-triggered driver for `plumbbob revert` — git reset --hard to a checkpoint SHA (discarding the half-done step) and return to
|
|
3
|
+
description: Human-triggered driver for `plumbbob revert` — git reset --hard to a checkpoint SHA (discarding the half-done step) and return to the boundary.
|
|
4
|
+
argument-hint: "[--to <step>]"
|
|
4
5
|
disable-model-invocation: true
|
|
5
6
|
model: haiku
|
|
6
7
|
allowed-tools: Bash(plumbbob status:*), Bash(plumbbob revert:*)
|
package/skills/pb-spike/SKILL.md
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
name: pb-spike
|
|
3
3
|
description: Human-triggered driver for `plumbbob spike` — open a throwaway worktree experiment for a genuine fork, or tear it down with `spike done`.
|
|
4
|
+
argument-hint: "<slug> | done"
|
|
4
5
|
disable-model-invocation: true
|
|
5
6
|
model: haiku
|
|
6
7
|
allowed-tools: Bash(plumbbob status:*), Bash(plumbbob spike:*)
|
package/skills/pb-step/SKILL.md
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
name: pb-step
|
|
3
3
|
description: Revise the next increment just-in-time — sharpen the next undone step against what's now true, or (with input) re-cut, split, or add a step. Empty input runs an automatic sharpen. One at a time; the human approves.
|
|
4
|
+
argument-hint: "[what-changed]"
|
|
4
5
|
disable-model-invocation: true
|
|
5
6
|
model: opus
|
|
6
7
|
allowed-tools: Read, Edit, Bash(plumbbob status:*)
|
|
@@ -31,9 +31,9 @@ this skill verifies it the same way: **it reads the diff, not the author** (D3).
|
|
|
31
31
|
the human is the clock. Never checkpoint without it.
|
|
32
32
|
5. **Checkpoint** *(only after approval)*. Commit the work — the human's
|
|
33
33
|
commit-with-TIL skill for a rich message, or let `checkpoint` make the WIP commit
|
|
34
|
-
— then run `plumbbob checkpoint` to record the SHA, flip the step to done,
|
|
35
|
-
and return to DESIGN. Do **not** bump the version or
|
|
36
|
-
the human's `/version` call.
|
|
34
|
+
— then run `plumbbob checkpoint` to record the SHA, flip the step to done, append the
|
|
35
|
+
step to the build-log's `## Log`, and return to DESIGN. Do **not** bump the version or
|
|
36
|
+
touch the changelog — that is the human's `/version` call.
|
|
37
37
|
|
|
38
38
|
## The hard contracts
|
|
39
39
|
|
package/skills/pb-wrap/SKILL.md
CHANGED
|
@@ -16,9 +16,12 @@ and no separate docs phase.
|
|
|
16
16
|
|
|
17
17
|
## What this skill does, in order
|
|
18
18
|
|
|
19
|
-
1. **Write the report** to `.plumbbob/report.md`, from `intent.md` + `build-log.md
|
|
20
|
-
-
|
|
21
|
-
|
|
19
|
+
1. **Write the report** to `.plumbbob/report.md`, from `intent.md` + `build-log.md`.
|
|
20
|
+
The build-log's `## Log` is already the chronological history — `plumbbob checkpoint`
|
|
21
|
+
wrote a dated line for every step as it landed. **Read it as the spine; do not
|
|
22
|
+
re-narrate it.** The report adds only what the log does not already carry:
|
|
23
|
+
- **What shipped** — summarize from the `## Log` timeline; don't rebuild it step by step.
|
|
24
|
+
- **Decisions and why** — the settled calls that shaped the build (the *why* behind the log).
|
|
22
25
|
- **Parked & harvested** — what was captured and how each was classified.
|
|
23
26
|
- **Final status** — done or partial, and what is left.
|
|
24
27
|
- **Deferred tangents** — the harvested items that become future work.
|
|
@@ -33,6 +36,9 @@ and no separate docs phase.
|
|
|
33
36
|
|
|
34
37
|
- **Report by default, never a gate.** Always offer the report; never wall the exit. A
|
|
35
38
|
bug fix's report can be three lines — size it to the work.
|
|
39
|
+
- **The log is the history; the report is the synthesis.** `checkpoint` already recorded
|
|
40
|
+
what shipped, step by step — wrap applies the *unique additions* (the why, the deferred
|
|
41
|
+
tangents, the final status), it does not rewrite the timeline.
|
|
36
42
|
- **Archive-then-clear, never destroy** (C4). The archive is the record; the active
|
|
37
43
|
files only clear once they are safely copied.
|
|
38
44
|
- **No version bump, no docs phase.** Updating real docs is a separate, explicit ask;
|
package/templates/build-log.md
CHANGED
|
@@ -5,12 +5,13 @@ step boundaries. The antidote to "my plan got lost in the noise."
|
|
|
5
5
|
Steps : where you are. One step in flight at a time.
|
|
6
6
|
Park list : where ideas go so you do not chase them. CAPTURE, never act inline.
|
|
7
7
|
Harvest : the boundary ritual that keeps you on one branch.
|
|
8
|
-
Log : the
|
|
8
|
+
Log : the build's history. `plumbbob checkpoint` appends a line per step as it
|
|
9
|
+
lands; feeds the /plumbbob:pb-wrap report, then gets archived.
|
|
9
10
|
-->
|
|
10
11
|
|
|
11
12
|
# Build log — {{TITLE}}
|
|
12
13
|
|
|
13
|
-
**Current step:** none (
|
|
14
|
+
**Current step:** none (at the boundary)
|
|
14
15
|
**Heavy check:** {{CHECK}}
|
|
15
16
|
|
|
16
17
|
## Steps
|
|
@@ -47,8 +48,9 @@ Harvest results this boundary:
|
|
|
47
48
|
|
|
48
49
|
## Log
|
|
49
50
|
|
|
50
|
-
*(
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
-
|
|
51
|
+
*(The build's history, oldest first. `plumbbob checkpoint` appends a dated line here
|
|
52
|
+
every time a step lands — via `/plumbbob:pb-build` or `/plumbbob:pb-verify` — so this
|
|
53
|
+
fills in as you go, not at the end. Add your own decision/event lines too: this is what
|
|
54
|
+
you point at to say "I did that — the LLM helped, but those were my calls."
|
|
55
|
+
`/plumbbob:pb-wrap` reads this for the report; `plumbbob wrap` archives it under
|
|
56
|
+
`.plumbbob/archive/`.)*
|
package/templates/intent.md
CHANGED
|
@@ -9,8 +9,7 @@ open. If the implementor (you-later, or the LLM) has to guess, the doc failed.
|
|
|
9
9
|
|
|
10
10
|
# {{TITLE}}
|
|
11
11
|
|
|
12
|
-
**
|
|
13
|
-
**Phase** (bookkeeping while in DESIGN): frame
|
|
12
|
+
**Phase** (your own bookkeeping while framing): frame
|
|
14
13
|
**Size:** tiny | small | medium
|
|
15
14
|
|
|
16
15
|
## Frame
|