plumbbob 0.4.4 → 0.4.11
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 +2 -2
- package/README.md +74 -56
- package/bin/pb +15 -0
- package/bin/plumbbob +26 -0
- package/dist/cli-core.js +25 -6
- package/dist/cli.js +0 -0
- package/dist/lib/buildlog.js +34 -0
- package/dist/lib/orient.js +28 -35
- package/dist/lib/plugins.js +24 -0
- package/dist/lib/sidecar.js +26 -12
- package/dist/verbs/build.js +4 -5
- package/dist/verbs/check.js +1 -1
- package/dist/verbs/checkpoint.js +38 -10
- package/dist/verbs/doctor.js +22 -4
- package/dist/verbs/init.js +25 -9
- package/dist/verbs/park.js +3 -23
- 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 +5 -4
- package/package.json +4 -3
- package/skills/{build → pb-build}/SKILL.md +16 -15
- package/skills/pb-doctor/SKILL.md +39 -0
- package/skills/{harvest → pb-harvest}/SKILL.md +10 -9
- package/skills/{park → pb-park}/SKILL.md +5 -5
- package/skills/{plan → pb-plan}/SKILL.md +10 -10
- package/skills/{refine → pb-refine}/SKILL.md +7 -7
- package/skills/{revert → pb-revert}/SKILL.md +3 -3
- package/skills/{spike → pb-spike}/SKILL.md +2 -2
- package/skills/{status → pb-status}/SKILL.md +2 -2
- package/skills/{step → pb-step}/SKILL.md +8 -8
- package/skills/{verify → pb-verify}/SKILL.md +6 -6
- package/skills/{wrap → pb-wrap}/SKILL.md +12 -6
- package/templates/build-log.md +14 -12
- package/templates/intent.md +8 -9
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "plumbbob",
|
|
3
|
-
"displayName": "
|
|
4
|
-
"version": "0.4.
|
|
3
|
+
"displayName": "PlumbBob",
|
|
4
|
+
"version": "0.4.11",
|
|
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
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
#
|
|
1
|
+
# PlumbBob
|
|
2
2
|
|
|
3
3
|
<p align="center">
|
|
4
4
|
<img src="hero.jpg" alt="A row of plumb bobs of varying shapes hanging from strings" width="600">
|
|
@@ -66,55 +66,55 @@ and it cannot be skipped.
|
|
|
66
66
|
|
|
67
67
|
You drive the whole loop from your IDE with eight `/plumbbob:*` skills — no step numbers to
|
|
68
68
|
remember, no raw CLI to type. Each is `disable-model-invocation`, so *you* fire
|
|
69
|
-
every move, and `/plumbbob:status` always names your next one.
|
|
69
|
+
every move, and `/plumbbob:pb-status` always names your next one.
|
|
70
70
|
|
|
71
71
|
| Skill | Does |
|
|
72
72
|
|-------|------|
|
|
73
|
-
| `/plumbbob:plan` | plan the whole goal — scaffold the session + author intent's Frame, Decisions, Constraints, **and all Steps** |
|
|
74
|
-
| `/plumbbob:step` | revise/sharpen the next step (empty input auto-syncs it to reality) |
|
|
75
|
-
| `/plumbbob:build` | *(optional)* implement the next planned step, then verify it to the pause — `--auto` self-approves and chains to done |
|
|
76
|
-
| `/plumbbob:verify` | the tick — check → self-review → validate → **PAUSE** → checkpoint |
|
|
77
|
-
| `/plumbbob:park` | capture an idea without chasing it |
|
|
78
|
-
| `/plumbbob:status` | orient — where you are, the next step's done-when + seam, and the next move |
|
|
79
|
-
| `/plumbbob:harvest` | triage parked ideas at a boundary (blocker / tangent / pivot) |
|
|
80
|
-
| `/plumbbob:wrap` | wrap up — write the report, archive safely, clear for a fresh goal |
|
|
81
|
-
|
|
82
|
-
Three optional power moves survive for when you need them: `/plumbbob:revert` (recover to
|
|
83
|
-
a checkpoint), `/plumbbob:spike` (throwaway worktree experiment), and `/plumbbob:refine` (attack
|
|
73
|
+
| `/plumbbob:pb-plan` | plan the whole goal — scaffold the session + author intent's Frame, Decisions, Constraints, **and all Steps** |
|
|
74
|
+
| `/plumbbob:pb-step` | revise/sharpen the next step (empty input auto-syncs it to reality) |
|
|
75
|
+
| `/plumbbob:pb-build` | *(optional)* implement the next planned step, then verify it to the pause — `--auto` self-approves and chains to done |
|
|
76
|
+
| `/plumbbob:pb-verify` | the tick — check → self-review → validate → **PAUSE** → checkpoint |
|
|
77
|
+
| `/plumbbob:pb-park` | capture an idea without chasing it |
|
|
78
|
+
| `/plumbbob:pb-status` | orient — where you are, the next step's done-when + seam, and the next move |
|
|
79
|
+
| `/plumbbob:pb-harvest` | triage parked ideas at a boundary (blocker / tangent / pivot) |
|
|
80
|
+
| `/plumbbob:pb-wrap` | wrap up — write the report, archive safely, clear for a fresh goal |
|
|
81
|
+
|
|
82
|
+
Three optional power moves survive for when you need them: `/plumbbob:pb-revert` (recover to
|
|
83
|
+
a checkpoint), `/plumbbob:pb-spike` (throwaway worktree experiment), and `/plumbbob:pb-refine` (attack
|
|
84
84
|
the frame for holes, or repair the plan when it drifts — usable at any point).
|
|
85
85
|
|
|
86
86
|
## The loop
|
|
87
87
|
|
|
88
|
-
The happy path is **plan the whole thing up front, then drive `/plumbbob:build` until
|
|
88
|
+
The happy path is **plan the whole thing up front, then drive `/plumbbob:pb-build` until
|
|
89
89
|
done** — approving each step at its verify pause:
|
|
90
90
|
|
|
91
91
|
```text
|
|
92
|
-
/plumbbob:plan author the whole plan (incl. all steps) (once)
|
|
92
|
+
/plumbbob:pb-plan author the whole plan (incl. all steps) (once)
|
|
93
93
|
└ per step:
|
|
94
|
-
/plumbbob:status review the next step (done-when + seam)
|
|
95
|
-
/plumbbob:step (optional) sharpen/revise it first if needed
|
|
96
|
-
/plumbbob:build (or DIY) implement it → verify → PAUSE → checkpoint
|
|
97
|
-
/plumbbob:park capture strays mid-build
|
|
98
|
-
/plumbbob:harvest triage them at a boundary
|
|
99
|
-
/plumbbob:wrap report + archive + clear (once)
|
|
94
|
+
/plumbbob:pb-status review the next step (done-when + seam)
|
|
95
|
+
/plumbbob:pb-step (optional) sharpen/revise it first if needed
|
|
96
|
+
/plumbbob:pb-build (or DIY) implement it → verify → PAUSE → checkpoint
|
|
97
|
+
/plumbbob:pb-park capture strays mid-build
|
|
98
|
+
/plumbbob:pb-harvest triage them at a boundary
|
|
99
|
+
/plumbbob:pb-wrap report + archive + clear (once)
|
|
100
100
|
```
|
|
101
101
|
|
|
102
|
-
Each `/plumbbob:build` builds the next undone step and stops at the pause for your
|
|
103
|
-
approval — re-firing it is itself the clock tick. (`/plumbbob:build --auto` is the opt-in
|
|
102
|
+
Each `/plumbbob:pb-build` builds the next undone step and stops at the pause for your
|
|
103
|
+
approval — re-firing it is itself the clock tick. (`/plumbbob:pb-build --auto` is the opt-in
|
|
104
104
|
that lets the agent self-approve and chain to done, halting on a red check or any
|
|
105
105
|
mismatch.) For a worked example that walks one goal end to end — planning, building
|
|
106
106
|
each step, wrapping up, archiving, and starting the next task — see
|
|
107
107
|
[`docs/happy-path.md`](docs/happy-path.md).
|
|
108
108
|
|
|
109
|
-
**Three ways to plan.** `/plumbbob:plan` produces the same artifact — a complete, standalone
|
|
109
|
+
**Three ways to plan.** `/plumbbob:pb-plan` produces the same artifact — a complete, standalone
|
|
110
110
|
`intent.md` — from whichever seed you give it: **no argument** runs a short interview;
|
|
111
111
|
**a file path** absorbs an out-of-band spec (retaining its detail so the plan stands on
|
|
112
112
|
its own); **any other text** expands your inline intent. No quotes required — it
|
|
113
113
|
disambiguates the mode itself.
|
|
114
114
|
|
|
115
|
-
**The pluggable executor.** `/plumbbob:build` is one way to turn a planned step into code
|
|
115
|
+
**The pluggable executor.** `/plumbbob:pb-build` is one way to turn a planned step into code
|
|
116
116
|
— it is *optional*. Implement by hand, in a vibe session, or with another harness,
|
|
117
|
-
and run `/plumbbob:verify` instead: it reads the *diff, not the author*. Plumbbob is the
|
|
117
|
+
and run `/plumbbob:pb-verify` instead: it reads the *diff, not the author*. Plumbbob is the
|
|
118
118
|
harness-agnostic spine; how the diff appears is a slot you fill however you like.
|
|
119
119
|
|
|
120
120
|
## Calibration: size everything to the work
|
|
@@ -123,7 +123,7 @@ The fastest way to abandon this is ceremony on a one-liner. The discipline is
|
|
|
123
123
|
*decisions before code*, not *always produce three files*.
|
|
124
124
|
|
|
125
125
|
- **Tiny** (typo, one-liner): no session. Just fix it.
|
|
126
|
-
- **Small** (a contained bug/change): `/plumbbob:plan` a frame + 2–3 decisions; one or two
|
|
126
|
+
- **Small** (a contained bug/change): `/plumbbob:pb-plan` a frame + 2–3 decisions; one or two
|
|
127
127
|
steps; build → verify → checkpoint.
|
|
128
128
|
- **Medium** (a feature touching a few modules): the full loop above.
|
|
129
129
|
- **Large / architectural**: that's Ridgeline's job, not Plumbbob's.
|
|
@@ -134,15 +134,16 @@ Calibration is the skill. When in doubt, smaller.
|
|
|
134
134
|
|
|
135
135
|
- A `plumbbob` CLI (TypeScript, run natively by Node ≥ 22.18, zero runtime
|
|
136
136
|
dependencies) — the dumb mechanical verbs the skills shell out to. You never type
|
|
137
|
-
it by hand.
|
|
137
|
+
it by hand (beyond `plumbbob --help` and `plumbbob --version`). The marketplace
|
|
138
|
+
plugin carries it on PATH via `bin/` shims; `npm i -g` installs it globally.
|
|
138
139
|
- The eight `/plumbbob:*` skills plus the optional power moves, each
|
|
139
140
|
`disable-model-invocation` so *you* fire every move.
|
|
140
141
|
- One session-gated Claude Code hook — `post-edit.sh`, a non-blocking light-feedback
|
|
141
142
|
pass that injects file-scoped lint into the model's context so it self-corrects in
|
|
142
143
|
flow. (v1's pre-edit muzzle, seam-guard, and bash-guard are gone — guidance, not
|
|
143
144
|
enforcement.)
|
|
144
|
-
- A `.plumbbob/` sidecar of flat files: `STATE` (
|
|
145
|
-
`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/`.
|
|
146
147
|
|
|
147
148
|
## Gates — two tiers, different jobs
|
|
148
149
|
|
|
@@ -150,17 +151,19 @@ Calibration is the skill. When in doubt, smaller.
|
|
|
150
151
|
blocks an edit. It exists only because Claude can't see your editor's LSP, so the
|
|
151
152
|
light tier *serves the model*.
|
|
152
153
|
- **Heavy** — the full `pnpm check` (tsc, oxlint, ast-grep, vitest, knip,
|
|
153
|
-
markdownlint). Not a hook: it runs *inside* `/plumbbob:verify`, which refuses to
|
|
154
|
+
markdownlint). Not a hook: it runs *inside* `/plumbbob:pb-verify`, which refuses to
|
|
154
155
|
checkpoint while red. The hard gate lives on the deliberate boundary, not the
|
|
155
156
|
keystroke.
|
|
156
157
|
|
|
157
|
-
##
|
|
158
|
+
## Position is derived, not stored
|
|
158
159
|
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
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.
|
|
164
167
|
|
|
165
168
|
## Git footprint — additive only
|
|
166
169
|
|
|
@@ -174,10 +177,12 @@ archives plain markdown under `.plumbbob/archive/` and never touches git.
|
|
|
174
177
|
|
|
175
178
|
```text
|
|
176
179
|
.plumbbob/
|
|
177
|
-
STATE #
|
|
180
|
+
STATE # session sentinel — its presence means a session is live
|
|
178
181
|
SEAM # the in-flight step's declared paths (awareness, not a lock)
|
|
179
|
-
STEP # the in-flight step number
|
|
180
|
-
|
|
182
|
+
STEP # the in-flight step number (its presence is the BUILD phase)
|
|
183
|
+
SPIKE # marker — present while a spike fork is open
|
|
184
|
+
config # key=value; check=<heavy-check command> (defaults to pnpm run check)
|
|
185
|
+
checkpoints # "baseline <sha>" then "step N <sha>", one per verified step
|
|
181
186
|
intent.md # canonical intent
|
|
182
187
|
build-log.md # live ledger
|
|
183
188
|
archive/
|
|
@@ -189,24 +194,34 @@ archives plain markdown under `.plumbbob/archive/` and never touches git.
|
|
|
189
194
|
|
|
190
195
|
## Install
|
|
191
196
|
|
|
192
|
-
Plumbbob
|
|
193
|
-
|
|
194
|
-
|
|
197
|
+
Plumbbob installs **once, globally** — like `gh` or your dotfiles. There are two co-equal,
|
|
198
|
+
mutually-exclusive ways to do it (both register a Claude Code plugin named `plumbbob`;
|
|
199
|
+
running both collides over the `/plumbbob:*` namespace).
|
|
200
|
+
|
|
201
|
+
**npm global + `init`** — the npm package ships the CLI, the skills, and the hook; `plumbbob
|
|
202
|
+
init` links them into Claude Code as an in-place plugin:
|
|
195
203
|
|
|
196
204
|
```sh
|
|
197
205
|
npm i -g plumbbob # the CLI (also a `pb` shorthand)
|
|
198
206
|
plumbbob init # link it into Claude Code; --uninstall to undo
|
|
199
207
|
```
|
|
200
208
|
|
|
201
|
-
`init` symlinks the package into `~/.claude/skills/plumbbob`, where Claude Code
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
209
|
+
`init` symlinks the package into `~/.claude/skills/plumbbob`, where Claude Code loads it as a
|
|
210
|
+
plugin; the post-edit hook auto-registers from `hooks.json`. Because it's a symlink, a later
|
|
211
|
+
`npm i -g plumbbob@latest` stays live with no re-link.
|
|
212
|
+
|
|
213
|
+
**The marketplace plugin** — self-contained: it ships the skills *and* the `plumbbob`/`pb`
|
|
214
|
+
CLI on PATH (via its `bin/` shims), so it needs neither `npm i -g` nor `plumbbob init`:
|
|
215
|
+
|
|
216
|
+
```text
|
|
217
|
+
/plugin install plumbbob@<marketplace>
|
|
218
|
+
```
|
|
219
|
+
|
|
220
|
+
Either way, Claude Code namespaces the skills as `/plumbbob:<skill>`, so they appear as
|
|
221
|
+
`/plumbbob:pb-plan`, `/plumbbob:pb-status`, and the rest. Nothing else under `~` is touched
|
|
222
|
+
and `settings.json` is left alone — restart Claude Code (or `/reload-plugins`) to activate.
|
|
223
|
+
If a marketplace plumbbob is already installed, `plumbbob init` refuses rather than create
|
|
224
|
+
the collision (`--force` overrides), and `plumbbob doctor` flags a double-install.
|
|
210
225
|
|
|
211
226
|
**Sessions are per-project.** Install scope is not session scope: you install the
|
|
212
227
|
tool once, but each goal lives in its own repo — `plumbbob start "<goal>"` writes a
|
|
@@ -222,10 +237,13 @@ look. Claude Code is the first, first-class target.
|
|
|
222
237
|
plumbbob doctor
|
|
223
238
|
```
|
|
224
239
|
|
|
225
|
-
`doctor`
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
240
|
+
`doctor` works for either install path: it confirms a marketplace plugin, or checks that the
|
|
241
|
+
skills-dir link resolves to the plugin manifest, the skills, and the hook — flagging a
|
|
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;
|
|
246
|
+
[`docs/troubleshooting.md`](docs/troubleshooting.md) covers the rest.
|
|
229
247
|
|
|
230
248
|
## Development
|
|
231
249
|
|
package/bin/pb
ADDED
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
#!/bin/sh
|
|
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)
|
|
15
|
+
exec node "$here/../dist/cli.js" "$@"
|
package/bin/plumbbob
ADDED
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
#!/bin/sh
|
|
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)
|
|
26
|
+
exec node "$here/../dist/cli.js" "$@"
|
package/dist/cli-core.js
CHANGED
|
@@ -2,6 +2,8 @@
|
|
|
2
2
|
// entry (cli.ts) so the routing/help logic is unit-testable in-process without
|
|
3
3
|
// the top-level process.exit. Zero runtime deps, node: builtins only (D1/C2).
|
|
4
4
|
// Functional/procedural: no classes, no `this`, no default export (C1).
|
|
5
|
+
import { readFileSync } from 'node:fs';
|
|
6
|
+
import { fileURLToPath } from 'node:url';
|
|
5
7
|
import { start } from "./verbs/start.js";
|
|
6
8
|
import { status } from "./verbs/status.js";
|
|
7
9
|
import { park } from "./verbs/park.js";
|
|
@@ -14,15 +16,15 @@ import { wrap } from "./verbs/wrap.js";
|
|
|
14
16
|
import { init } from "./verbs/init.js";
|
|
15
17
|
import { doctor } from "./verbs/doctor.js";
|
|
16
18
|
const VERBS = [
|
|
17
|
-
{ name: 'start', summary: 'scaffold .plumbbob/;
|
|
18
|
-
{ name: 'status', summary: 'print the
|
|
19
|
-
{ 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)' },
|
|
20
22
|
{ name: 'check', summary: 'run the heavy check and report; no state change' },
|
|
21
|
-
{ name: 'checkpoint', summary: 'checkpoint [<n>]: gate on green, commit/record SHA, mark step done
|
|
22
|
-
{ 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' },
|
|
23
25
|
{ name: 'park', summary: 'park "<text>": append a raw line to the park list' },
|
|
24
26
|
{ name: 'spike', summary: 'spike "<slug>" | spike done: throwaway worktree experiment' },
|
|
25
|
-
{ 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' },
|
|
26
28
|
{ name: 'init', summary: 'link plumbbob into Claude Code as an in-place plugin (~/.claude/skills/plumbbob); --uninstall to undo' },
|
|
27
29
|
{ name: 'doctor', summary: 'diagnose the plugin link (manifest, skills, hook) and print the fix for anything broken' },
|
|
28
30
|
];
|
|
@@ -36,6 +38,19 @@ export function formatHelp() {
|
|
|
36
38
|
const rows = VERBS.map((v) => ` ${v.name.padEnd(width)} ${v.summary}`);
|
|
37
39
|
return ['plumbbob — attention-first build process', '', 'Usage: plumbbob <verb> [args]', '', 'Verbs:', ...rows, ''].join('\n');
|
|
38
40
|
}
|
|
41
|
+
// The package version, read from the package.json that ships one level above the
|
|
42
|
+
// compiled CLI (dist/cli-core.js → ../package.json; in tests src/cli-core.ts →
|
|
43
|
+
// ../package.json, the repo root). Builtins only (C2); an absent or malformed
|
|
44
|
+
// manifest degrades to 'unknown' rather than throwing, so `--version` never errors.
|
|
45
|
+
export function readVersion() {
|
|
46
|
+
try {
|
|
47
|
+
const raw = readFileSync(fileURLToPath(new URL('../package.json', import.meta.url)), 'utf8');
|
|
48
|
+
return JSON.parse(raw).version ?? 'unknown';
|
|
49
|
+
}
|
|
50
|
+
catch {
|
|
51
|
+
return 'unknown';
|
|
52
|
+
}
|
|
53
|
+
}
|
|
39
54
|
function dispatch(verb, cwd, rest) {
|
|
40
55
|
switch (verb) {
|
|
41
56
|
case 'start':
|
|
@@ -72,6 +87,10 @@ export function run(argv) {
|
|
|
72
87
|
process.stdout.write(`${formatHelp()}\n`);
|
|
73
88
|
return 0;
|
|
74
89
|
}
|
|
90
|
+
if (verb === 'version' || verb === '--version' || verb === '-v') {
|
|
91
|
+
process.stdout.write(`plumbbob ${readVersion()}\n`);
|
|
92
|
+
return 0;
|
|
93
|
+
}
|
|
75
94
|
try {
|
|
76
95
|
return dispatch(verb, process.cwd(), rest);
|
|
77
96
|
}
|
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
|
@@ -77,7 +77,7 @@ export function parseOpenQuestions(intent) {
|
|
|
77
77
|
.length;
|
|
78
78
|
}
|
|
79
79
|
// Open parked items: `- [ ]` lines under `## Park list` (the `park` verb's format).
|
|
80
|
-
// A harvested item is flipped to `- [x]` by `/plumbbob:harvest` and no longer counts; the
|
|
80
|
+
// A harvested item is flipped to `- [x]` by `/plumbbob:pb-harvest` and no longer counts; the
|
|
81
81
|
// `(none yet)` placeholder and the blockquote instructions never match.
|
|
82
82
|
export function parseParked(buildLog) {
|
|
83
83
|
return sectionLines(buildLog, '## Park list').filter((l) => /^-\s+\[ \]\s+\S/.test(l.trim())).length;
|
|
@@ -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:step`';
|
|
117
|
-
}
|
|
118
|
-
// Batch-default: the steps were planned up front, so finishing them usually
|
|
119
|
-
// means "wrap up" — but `/plumbbob:step` can still add an increment if reality grew.
|
|
120
|
-
const harvest = parked > 0 ? `harvest ${parked} parked idea${parked === 1 ? '' : 's'} — \`/plumbbob:harvest\`; then ` : '';
|
|
121
|
-
return `${harvest}wrap up — \`/plumbbob:wrap\` (or \`/plumbbob:step\` to add another increment)`;
|
|
122
|
-
}
|
|
123
|
-
return nextUndone.planned
|
|
124
|
-
? `build step ${nextUndone.n} — \`/plumbbob:build\` (or \`/plumbbob:step\` to revise it first)`
|
|
125
|
-
: `plan step ${nextUndone.n} — \`/plumbbob: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
|
};
|
|
@@ -153,7 +146,7 @@ export function formatOrientation(o) {
|
|
|
153
146
|
if (s !== nextUndone) {
|
|
154
147
|
return head;
|
|
155
148
|
}
|
|
156
|
-
// Surface the next step's detail so the human can review it (and `/plumbbob:step`-
|
|
149
|
+
// Surface the next step's detail so the human can review it (and `/plumbbob:pb-step`-
|
|
157
150
|
// revise) before building. Only what's present — a rough step shows neither.
|
|
158
151
|
const detail = [];
|
|
159
152
|
if (o.nextDoneWhen !== null) {
|
|
@@ -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
|
'',
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
// Detect a marketplace-installed plumbbob plugin from Claude Code's plugin state.
|
|
2
|
+
// Used to keep the skills-dir install (`plumbbob init`) from colliding with a
|
|
3
|
+
// marketplace install: two plugins both named `plumbbob` fight over the
|
|
4
|
+
// `/plumbbob:*` namespace and the skills can drop to flat names (`/pb-status`).
|
|
5
|
+
// Read-only, node builtins only (C1/C2): a missing / unreadable / malformed file
|
|
6
|
+
// means "none installed".
|
|
7
|
+
import { readFileSync } from 'node:fs';
|
|
8
|
+
import { join } from 'node:path';
|
|
9
|
+
// Marketplace plugin ids for plumbbob recorded in installed_plugins.json
|
|
10
|
+
// (e.g. "plumbbob@robmclarty"). Empty when none, or the file is absent/unreadable.
|
|
11
|
+
// `plumbbob@<marketplace>` matches; `plumbbob-spike@x` and the like do not.
|
|
12
|
+
export function marketplacePlumbbob(home) {
|
|
13
|
+
try {
|
|
14
|
+
const raw = readFileSync(join(home, '.claude', 'plugins', 'installed_plugins.json'), 'utf8');
|
|
15
|
+
const plugins = JSON.parse(raw).plugins;
|
|
16
|
+
if (plugins === undefined || plugins === null) {
|
|
17
|
+
return [];
|
|
18
|
+
}
|
|
19
|
+
return Object.keys(plugins).filter((id) => id === 'plumbbob' || id.startsWith('plumbbob@'));
|
|
20
|
+
}
|
|
21
|
+
catch {
|
|
22
|
+
return [];
|
|
23
|
+
}
|
|
24
|
+
}
|
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/check.js
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
// `plumbbob check` — run the heavy gate (D16/D24) and report, with NO state
|
|
2
|
-
// change. The read-only half of the verify tick: `/plumbbob:verify` runs this before the
|
|
2
|
+
// change. The read-only half of the verify tick: `/plumbbob:pb-verify` runs this before the
|
|
3
3
|
// pause so the human approves on a known-green check. Exits with the check's own
|
|
4
4
|
// code (0 = green).
|
|
5
5
|
import { findRepoRoot } from "../lib/git.js";
|