plumbbob 0.3.1 → 0.4.2
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 +11 -0
- package/README.md +199 -53
- package/dist/cli-core.js +83 -0
- package/dist/cli.js +6 -82
- package/dist/lib/archive.js +1 -1
- package/dist/lib/orient.js +35 -15
- package/dist/verbs/check.js +1 -1
- package/dist/verbs/checkpoint.js +1 -1
- package/dist/verbs/doctor.js +44 -142
- package/dist/verbs/init.js +81 -0
- package/dist/verbs/{reset.js → wrap.js} +7 -7
- package/hooks/hooks.json +12 -0
- package/package.json +4 -3
- package/skills/build/SKILL.md +70 -0
- package/skills/{pb-harvest → harvest}/SKILL.md +6 -6
- package/skills/{pb-park → park}/SKILL.md +6 -6
- package/skills/plan/SKILL.md +85 -0
- package/skills/refine/SKILL.md +45 -0
- package/skills/{pb-revert → revert}/SKILL.md +5 -5
- package/skills/{pb-spike → spike}/SKILL.md +5 -5
- package/skills/{pb-status → status}/SKILL.md +5 -5
- package/skills/step/SKILL.md +49 -0
- package/skills/{pb-verify → verify}/SKILL.md +8 -8
- package/skills/{pb-reset → wrap}/SKILL.md +8 -8
- package/templates/build-log.md +14 -14
- package/templates/intent.md +10 -6
- package/dist/lib/settings.js +0 -83
- package/dist/verbs/setup.js +0 -163
- package/skills/pb-build/SKILL.md +0 -49
- package/skills/pb-plan/SKILL.md +0 -38
- package/skills/pb-step/SKILL.md +0 -37
- package/skills/plumbbob-interrogate/SKILL.md +0 -33
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "plumbbob",
|
|
3
|
+
"displayName": "Plumbbob",
|
|
4
|
+
"version": "0.4.0",
|
|
5
|
+
"description": "Attention-first build process — driver skills + a CLI that keep you the decider; guidance, not enforcement.",
|
|
6
|
+
"author": { "name": "Rob McLarty", "email": "hello@robmclarty.com", "url": "https://robmclarty.com" },
|
|
7
|
+
"homepage": "https://github.com/robmclarty/plumbbob#readme",
|
|
8
|
+
"repository": "https://github.com/robmclarty/plumbbob",
|
|
9
|
+
"license": "Apache-2.0",
|
|
10
|
+
"keywords": ["claude-code", "claude", "hooks", "cli", "agent", "attention-first"]
|
|
11
|
+
}
|
package/README.md
CHANGED
|
@@ -1,97 +1,228 @@
|
|
|
1
1
|
# Plumbbob
|
|
2
2
|
|
|
3
|
-
|
|
3
|
+
<p align="center">
|
|
4
|
+
<img src="hero.jpg" alt="A row of plumb bobs of varying shapes hanging from strings" width="600">
|
|
5
|
+
</p>
|
|
6
|
+
|
|
7
|
+
A guidance-first build process for working *with* an LLM instead of being dragged
|
|
8
|
+
behind one. It's the layer below Ridgeline: where Ridgeline runs autonomously
|
|
9
|
+
without you, Plumbbob keeps you in the driver's seat for the small-to-medium work
|
|
10
|
+
that doesn't justify a full autonomous build — a feature, a bug, a refactor — while
|
|
11
|
+
staying deliberate rather than vibing. You decide on a surface outside the chat;
|
|
12
|
+
Plumbbob orients you, runs each step's labor, and then **stops and waits for you to
|
|
13
|
+
advance** — the human is the clock, not a lock.
|
|
4
14
|
|
|
5
15
|
> Ridgeline is the line. Plumbbob establishes *true* before you build.
|
|
6
16
|
> The LLM is a hand, not a head.
|
|
7
17
|
|
|
8
|
-
This repository was built using Plumbbob v2, dogfooded on its own build under its
|
|
18
|
+
This repository was built using Plumbbob v2, dogfooded on its own build under its
|
|
19
|
+
own loop.
|
|
20
|
+
|
|
21
|
+
## The one law
|
|
22
|
+
|
|
23
|
+
**Vibe to execute, never vibe to decide.**
|
|
24
|
+
|
|
25
|
+
Vibing is fine — *once every decision being carried out was already made on a
|
|
26
|
+
surface outside the chat.* It becomes a slot machine only when the deciding happens
|
|
27
|
+
inside the stream while code is flowing. The whole job of Plumbbob is to keep
|
|
28
|
+
decisions and execution from fusing:
|
|
29
|
+
|
|
30
|
+
- The **human** owns convergence. You decide, choose, pick the branch.
|
|
31
|
+
- The **LLM** owns divergence in design (finding holes, generating options) and
|
|
32
|
+
convergence *only* in build (executing a decided step).
|
|
33
|
+
- The **boundary** between deciding and executing is held by a **pause you
|
|
34
|
+
advance**, not a wall that refuses you.
|
|
35
|
+
|
|
36
|
+
If you feel tired and lost, those two activities have fused again. The fix is never
|
|
37
|
+
"prompt better." It's "stop, leave the chat, go decide, come back."
|
|
38
|
+
|
|
39
|
+
## Why it works: get the plan out of your head
|
|
40
|
+
|
|
41
|
+
The exhaustion is a working-memory problem. You can't *produce* intent and
|
|
42
|
+
*consume* the model's output at once — consuming overwrites producing. So Plumbbob
|
|
43
|
+
externalizes your plan onto durable surfaces that survive the flood:
|
|
44
|
+
|
|
45
|
+
- `intent.md` — what you decided, before any code. Your canonical intent.
|
|
46
|
+
- `build-log.md` — the live ledger of steps, parked ideas, and decisions.
|
|
47
|
+
|
|
48
|
+
When the model floods you, you read the page, not your memory. The chat is
|
|
49
|
+
ephemeral; the docs persist. **The chat is a hand; the docs are the head.**
|
|
9
50
|
|
|
10
51
|
## The shift: a clock, not a lock
|
|
11
52
|
|
|
12
|
-
Plumbbob v1 enforced the deciding/executing boundary with a hard file lock
|
|
53
|
+
Plumbbob v1 enforced the deciding/executing boundary with a hard file lock — a
|
|
54
|
+
pre-edit muzzle that *refused* code edits unless you were in the right state. It
|
|
55
|
+
provided no real security (a determined model routed around it), so its only product
|
|
56
|
+
was forced ritual.
|
|
13
57
|
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
58
|
+
v2 replaces the lock with a **clock**. Nothing blocks your edits. Instead, the
|
|
59
|
+
system does a step's work, then pauses at a verify gate for your approval before it
|
|
60
|
+
checkpoints. You stay the decider not because a wall refuses you, but because the
|
|
61
|
+
loop pulls up to a line — the verify pause — and idles there until you approve.
|
|
62
|
+
Pull, not block. The pause *is* the product; it is the moment your judgment enters,
|
|
63
|
+
and it cannot be skipped.
|
|
17
64
|
|
|
18
65
|
## The eight skills
|
|
19
66
|
|
|
20
|
-
You drive the whole loop from your IDE with eight
|
|
67
|
+
You drive the whole loop from your IDE with eight `/plumbbob:*` skills — no step numbers to
|
|
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.
|
|
21
70
|
|
|
22
71
|
| Skill | Does |
|
|
23
72
|
|-------|------|
|
|
24
|
-
| `/
|
|
25
|
-
| `/
|
|
26
|
-
| `/
|
|
27
|
-
| `/
|
|
28
|
-
| `/
|
|
29
|
-
| `/
|
|
30
|
-
| `/
|
|
31
|
-
| `/
|
|
32
|
-
|
|
33
|
-
Three optional power moves survive for when you need them: `/
|
|
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
|
|
84
|
+
the frame for holes, or repair the plan when it drifts — usable at any point).
|
|
34
85
|
|
|
35
86
|
## The loop
|
|
36
87
|
|
|
37
|
-
|
|
38
|
-
|
|
88
|
+
The happy path is **plan the whole thing up front, then drive `/plumbbob:build` until
|
|
89
|
+
done** — approving each step at its verify pause:
|
|
90
|
+
|
|
91
|
+
```text
|
|
92
|
+
/plumbbob:plan author the whole plan (incl. all steps) (once)
|
|
39
93
|
└ per step:
|
|
40
|
-
/
|
|
41
|
-
/
|
|
42
|
-
/
|
|
43
|
-
/
|
|
44
|
-
/
|
|
45
|
-
|
|
46
|
-
/pb-reset report + archive + clear (once)
|
|
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)
|
|
47
100
|
```
|
|
48
101
|
|
|
49
|
-
|
|
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
|
|
104
|
+
that lets the agent self-approve and chain to done, halting on a red check or any
|
|
105
|
+
mismatch.) For a worked example that walks one goal end to end — planning, building
|
|
106
|
+
each step, wrapping up, archiving, and starting the next task — see
|
|
107
|
+
[`docs/happy-path.md`](docs/happy-path.md).
|
|
50
108
|
|
|
51
|
-
|
|
109
|
+
**Three ways to plan.** `/plumbbob:plan` produces the same artifact — a complete, standalone
|
|
110
|
+
`intent.md` — from whichever seed you give it: **no argument** runs a short interview;
|
|
111
|
+
**a file path** absorbs an out-of-band spec (retaining its detail so the plan stands on
|
|
112
|
+
its own); **any other text** expands your inline intent. No quotes required — it
|
|
113
|
+
disambiguates the mode itself.
|
|
52
114
|
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
-
|
|
115
|
+
**The pluggable executor.** `/plumbbob:build` is one way to turn a planned step into code
|
|
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
|
|
118
|
+
harness-agnostic spine; how the diff appears is a slot you fill however you like.
|
|
57
119
|
|
|
58
|
-
##
|
|
120
|
+
## Calibration: size everything to the work
|
|
59
121
|
|
|
60
|
-
The
|
|
122
|
+
The fastest way to abandon this is ceremony on a one-liner. The discipline is
|
|
123
|
+
*decisions before code*, not *always produce three files*.
|
|
61
124
|
|
|
62
|
-
|
|
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
|
|
127
|
+
steps; build → verify → checkpoint.
|
|
128
|
+
- **Medium** (a feature touching a few modules): the full loop above.
|
|
129
|
+
- **Large / architectural**: that's Ridgeline's job, not Plumbbob's.
|
|
63
130
|
|
|
64
|
-
|
|
65
|
-
pnpm add -D plumbbob
|
|
66
|
-
pnpm exec plumbbob setup --local # or --project to commit it for a team
|
|
67
|
-
```
|
|
131
|
+
Calibration is the skill. When in doubt, smaller.
|
|
68
132
|
|
|
69
|
-
|
|
133
|
+
## What ships
|
|
70
134
|
|
|
71
|
-
|
|
135
|
+
- A `plumbbob` CLI (TypeScript, run natively by Node ≥ 22.18, zero runtime
|
|
136
|
+
dependencies) — the dumb mechanical verbs the skills shell out to. You never type
|
|
137
|
+
it by hand.
|
|
138
|
+
- The eight `/plumbbob:*` skills plus the optional power moves, each
|
|
139
|
+
`disable-model-invocation` so *you* fire every move.
|
|
140
|
+
- One session-gated Claude Code hook — `post-edit.sh`, a non-blocking light-feedback
|
|
141
|
+
pass that injects file-scoped lint into the model's context so it self-corrects in
|
|
142
|
+
flow. (v1's pre-edit muzzle, seam-guard, and bash-guard are gone — guidance, not
|
|
143
|
+
enforcement.)
|
|
144
|
+
- A `.plumbbob/` sidecar of flat files: `STATE` (orientation only), `intent.md`,
|
|
145
|
+
`build-log.md`, `checkpoints`, and `archive/`.
|
|
146
|
+
|
|
147
|
+
## Gates — two tiers, different jobs
|
|
148
|
+
|
|
149
|
+
- **Light** — the non-blocking `post-edit` feedback above. Per changed file. Never
|
|
150
|
+
blocks an edit. It exists only because Claude can't see your editor's LSP, so the
|
|
151
|
+
light tier *serves the model*.
|
|
152
|
+
- **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
|
+
checkpoint while red. The hard gate lives on the deliberate boundary, not the
|
|
155
|
+
keystroke.
|
|
72
156
|
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
plumbbob
|
|
157
|
+
## STATE is orientation, not a gate
|
|
158
|
+
|
|
159
|
+
The current position lives in one word in `.plumbbob/STATE` — `DESIGN`, `BUILD`, or
|
|
160
|
+
`SPIKE`. It no longer gates anything. It is read by `/plumbbob:status` to tell you where
|
|
161
|
+
you are and what to do next; a wrong state is a mislabeled position on a map, not a
|
|
162
|
+
locked door. The post-edit hook is session-gated: a repo with no `.plumbbob/STATE`
|
|
163
|
+
behaves exactly like plain Claude Code.
|
|
164
|
+
|
|
165
|
+
## Git footprint — additive only
|
|
166
|
+
|
|
167
|
+
Plumbbob commits cheap checkpoint markers (`plumbbob: step n done`) on your feature
|
|
168
|
+
branch and reverts to its own recorded SHAs. It **never rewrites pushed history**;
|
|
169
|
+
your normal squash-merge collapses the checkpoints at PR time. `start` records the
|
|
170
|
+
baseline HEAD; `revert [--to n]` does `git reset --hard` to a recorded SHA; `wrap`
|
|
171
|
+
archives plain markdown under `.plumbbob/archive/` and never touches git.
|
|
172
|
+
|
|
173
|
+
## The `.plumbbob/` folder
|
|
174
|
+
|
|
175
|
+
```text
|
|
176
|
+
.plumbbob/
|
|
177
|
+
STATE # one word: DESIGN | BUILD | SPIKE — orientation, not a gate
|
|
178
|
+
SEAM # the in-flight step's declared paths (awareness, not a lock)
|
|
179
|
+
STEP # the in-flight step number
|
|
180
|
+
checkpoints # "step N <git-sha>", one per verified step
|
|
181
|
+
intent.md # canonical intent
|
|
182
|
+
build-log.md # live ledger
|
|
183
|
+
archive/
|
|
184
|
+
<date>-<slug>/
|
|
185
|
+
intent.md
|
|
186
|
+
build-log.md
|
|
187
|
+
report.md
|
|
76
188
|
```
|
|
77
189
|
|
|
78
|
-
|
|
190
|
+
## Install
|
|
79
191
|
|
|
80
|
-
|
|
192
|
+
Plumbbob is a personal tool — install it once, globally, like `gh` or your
|
|
193
|
+
dotfiles. The npm package ships the CLI plus the skills and hook; `plumbbob init`
|
|
194
|
+
links them into Claude Code as an in-place plugin.
|
|
81
195
|
|
|
82
196
|
```sh
|
|
83
|
-
|
|
197
|
+
npm i -g plumbbob # the CLI (also a `pb` shorthand)
|
|
198
|
+
plumbbob init # link it into Claude Code; --uninstall to undo
|
|
84
199
|
```
|
|
85
200
|
|
|
86
|
-
`
|
|
201
|
+
`init` symlinks the package into `~/.claude/skills/plumbbob`, where Claude Code
|
|
202
|
+
loads it as a plugin: the skills appear namespaced as `/plumbbob:*` and the
|
|
203
|
+
post-edit hook auto-registers from `hooks.json`. Nothing else under `~` is touched
|
|
204
|
+
and `settings.json` is left alone — restart Claude Code (or `/reload-plugins`) to
|
|
205
|
+
activate. Because it's a symlink, a later `npm i -g plumbbob@latest` stays live with
|
|
206
|
+
no re-link.
|
|
87
207
|
|
|
88
|
-
|
|
208
|
+
**Sessions are per-project.** Install scope is not session scope: you install the
|
|
209
|
+
tool once, but each goal lives in its own repo — `plumbbob start "<goal>"` writes a
|
|
210
|
+
`.plumbbob/` sidecar there, independent of the one global link.
|
|
89
211
|
|
|
90
|
-
|
|
212
|
+
**Other agents (roadmap).** The npm package is the agent-neutral carrier; a future
|
|
213
|
+
`plumbbob init --host codex|cursor|zed` will place the same skills where those tools
|
|
214
|
+
look. Claude Code is the first, first-class target.
|
|
91
215
|
|
|
92
|
-
|
|
216
|
+
### Verify
|
|
217
|
+
|
|
218
|
+
```sh
|
|
219
|
+
plumbbob doctor
|
|
220
|
+
```
|
|
93
221
|
|
|
94
|
-
|
|
222
|
+
`doctor` checks that the link resolves to the plugin manifest, the skills, and the
|
|
223
|
+
hook, and prints the exact fix for anything broken. Run it first if a `/plumbbob:*`
|
|
224
|
+
skill ever opens with an empty dashboard; [`docs/troubleshooting.md`](docs/troubleshooting.md)
|
|
225
|
+
covers the rest.
|
|
95
226
|
|
|
96
227
|
## Development
|
|
97
228
|
|
|
@@ -101,7 +232,22 @@ pnpm check # tsc, oxlint, ast-grep, vitest, knip, markdownlint
|
|
|
101
232
|
pnpm build # emit dist/ (what the published bin runs)
|
|
102
233
|
```
|
|
103
234
|
|
|
104
|
-
|
|
235
|
+
## Documentation
|
|
236
|
+
|
|
237
|
+
- [`docs/techniques.md`](docs/techniques.md) — the methods behind the loop, each on its own.
|
|
238
|
+
- [`docs/happy-path.md`](docs/happy-path.md) — one goal walked end to end.
|
|
239
|
+
- [`docs/cli-reference.md`](docs/cli-reference.md) — every verb, flag, and exit code.
|
|
240
|
+
- [`docs/troubleshooting.md`](docs/troubleshooting.md) — fixes for the common snags.
|
|
241
|
+
- [`docs/decisions.md`](docs/decisions.md) — the `D#` / `C#` design-decision key.
|
|
242
|
+
- [`docs/attention-first-development.md`](docs/attention-first-development.md) — the philosophy: attention as the scarce resource.
|
|
243
|
+
- [`CONTRIBUTING.md`](CONTRIBUTING.md) — setup, conventions, and how to submit changes.
|
|
244
|
+
|
|
245
|
+
## The shape, in one line
|
|
246
|
+
|
|
247
|
+
The human owns convergence; the LLM owns divergence in design and convergence only
|
|
248
|
+
in implementation; and the boundary between deciding and executing is a **pause you
|
|
249
|
+
advance**, not a lock you fight — the system does the labor and waits for you to be
|
|
250
|
+
the clock.
|
|
105
251
|
|
|
106
252
|
## License
|
|
107
253
|
|
package/dist/cli-core.js
ADDED
|
@@ -0,0 +1,83 @@
|
|
|
1
|
+
// plumbbob CLI core — argv dispatch and the help table, separated from the bin
|
|
2
|
+
// entry (cli.ts) so the routing/help logic is unit-testable in-process without
|
|
3
|
+
// the top-level process.exit. Zero runtime deps, node: builtins only (D1/C2).
|
|
4
|
+
// Functional/procedural: no classes, no `this`, no default export (C1).
|
|
5
|
+
import { start } from "./verbs/start.js";
|
|
6
|
+
import { status } from "./verbs/status.js";
|
|
7
|
+
import { park } from "./verbs/park.js";
|
|
8
|
+
import { build } from "./verbs/build.js";
|
|
9
|
+
import { check } from "./verbs/check.js";
|
|
10
|
+
import { checkpoint } from "./verbs/checkpoint.js";
|
|
11
|
+
import { revert } from "./verbs/revert.js";
|
|
12
|
+
import { spike } from "./verbs/spike.js";
|
|
13
|
+
import { wrap } from "./verbs/wrap.js";
|
|
14
|
+
import { init } from "./verbs/init.js";
|
|
15
|
+
import { doctor } from "./verbs/doctor.js";
|
|
16
|
+
const VERBS = [
|
|
17
|
+
{ name: 'start', summary: 'scaffold .plumbbob/; STATE=DESIGN; record the baseline commit' },
|
|
18
|
+
{ name: 'status', summary: 'print the session state, or NO ACTIVE SESSION' },
|
|
19
|
+
{ name: 'build', summary: 'build <n>: write the seam from step n; STATE=BUILD (orientation, not a lock)' },
|
|
20
|
+
{ 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, STATE=DESIGN (executor-agnostic)' },
|
|
22
|
+
{ name: 'revert', summary: 'revert [--to n]: git reset --hard to a checkpoint SHA; STATE=DESIGN' },
|
|
23
|
+
{ name: 'park', summary: 'park "<text>": append a raw line to the park list' },
|
|
24
|
+
{ 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, STATE off' },
|
|
26
|
+
{ name: 'init', summary: 'link plumbbob into Claude Code as an in-place plugin (~/.claude/skills/plumbbob); --uninstall to undo' },
|
|
27
|
+
{ name: 'doctor', summary: 'diagnose the plugin link (manifest, skills, hook) and print the fix for anything broken' },
|
|
28
|
+
];
|
|
29
|
+
// Plumbbob v2 (D1/D10/D13): the deciding/executing boundary is no longer a lock,
|
|
30
|
+
// so there is nothing to defend — every verb runs the same whether a human or the
|
|
31
|
+
// model triggers it. The old human-only `mode` escape hatch and its CLAUDECODE
|
|
32
|
+
// in-session refusal are gone; what keeps the human the decider is the pause at
|
|
33
|
+
// the step boundary (the skills), not a refusal here.
|
|
34
|
+
export function formatHelp() {
|
|
35
|
+
const width = Math.max(...VERBS.map((v) => v.name.length));
|
|
36
|
+
const rows = VERBS.map((v) => ` ${v.name.padEnd(width)} ${v.summary}`);
|
|
37
|
+
return ['plumbbob — attention-first build process', '', 'Usage: plumbbob <verb> [args]', '', 'Verbs:', ...rows, ''].join('\n');
|
|
38
|
+
}
|
|
39
|
+
function dispatch(verb, cwd, rest) {
|
|
40
|
+
switch (verb) {
|
|
41
|
+
case 'start':
|
|
42
|
+
return start(cwd, rest);
|
|
43
|
+
case 'status':
|
|
44
|
+
return status(cwd);
|
|
45
|
+
case 'park':
|
|
46
|
+
return park(cwd, rest);
|
|
47
|
+
case 'build':
|
|
48
|
+
return build(cwd, rest);
|
|
49
|
+
case 'check':
|
|
50
|
+
return check(cwd);
|
|
51
|
+
case 'checkpoint':
|
|
52
|
+
return checkpoint(cwd, rest);
|
|
53
|
+
case 'revert':
|
|
54
|
+
return revert(cwd, rest);
|
|
55
|
+
case 'spike':
|
|
56
|
+
return spike(cwd, rest);
|
|
57
|
+
case 'wrap':
|
|
58
|
+
return wrap(cwd);
|
|
59
|
+
case 'init':
|
|
60
|
+
return init(rest);
|
|
61
|
+
case 'doctor':
|
|
62
|
+
return doctor();
|
|
63
|
+
default:
|
|
64
|
+
process.stderr.write(`plumbbob: unknown verb '${verb}'. Run 'plumbbob help' for the verb table.\n`);
|
|
65
|
+
return 1;
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
export function run(argv) {
|
|
69
|
+
const verb = argv[0] ?? 'help';
|
|
70
|
+
const rest = argv.slice(1);
|
|
71
|
+
if (verb === 'help' || verb === '--help' || verb === '-h') {
|
|
72
|
+
process.stdout.write(`${formatHelp()}\n`);
|
|
73
|
+
return 0;
|
|
74
|
+
}
|
|
75
|
+
try {
|
|
76
|
+
return dispatch(verb, process.cwd(), rest);
|
|
77
|
+
}
|
|
78
|
+
catch (err) {
|
|
79
|
+
const message = err instanceof Error ? err.message : String(err);
|
|
80
|
+
process.stderr.write(`plumbbob: ${verb} failed: ${message}\n`);
|
|
81
|
+
return 1;
|
|
82
|
+
}
|
|
83
|
+
}
|
package/dist/cli.js
CHANGED
|
@@ -1,84 +1,8 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
|
-
// plumbbob CLI —
|
|
3
|
-
//
|
|
4
|
-
//
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
import {
|
|
8
|
-
import { build } from "./verbs/build.js";
|
|
9
|
-
import { check } from "./verbs/check.js";
|
|
10
|
-
import { checkpoint } from "./verbs/checkpoint.js";
|
|
11
|
-
import { revert } from "./verbs/revert.js";
|
|
12
|
-
import { spike } from "./verbs/spike.js";
|
|
13
|
-
import { reset } from "./verbs/reset.js";
|
|
14
|
-
import { setup } from "./verbs/setup.js";
|
|
15
|
-
import { doctor } from "./verbs/doctor.js";
|
|
16
|
-
const VERBS = [
|
|
17
|
-
{ name: 'start', summary: 'scaffold .plumbbob/; STATE=DESIGN; record the baseline commit' },
|
|
18
|
-
{ name: 'status', summary: 'print the session state, or NO ACTIVE SESSION' },
|
|
19
|
-
{ name: 'build', summary: 'build <n>: write the seam from step n; STATE=BUILD (orientation, not a lock)' },
|
|
20
|
-
{ 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, STATE=DESIGN (executor-agnostic)' },
|
|
22
|
-
{ name: 'revert', summary: 'revert [--to n]: git reset --hard to a checkpoint SHA; STATE=DESIGN' },
|
|
23
|
-
{ name: 'park', summary: 'park "<text>": append a raw line to the park list' },
|
|
24
|
-
{ name: 'spike', summary: 'spike "<slug>" | spike done: throwaway worktree experiment' },
|
|
25
|
-
{ name: 'reset', summary: 'v2 close-out: archive intent+log+report (no gate), clear the sidecar, STATE off' },
|
|
26
|
-
{ name: 'setup', summary: 'install hooks + skills; register them (self-contained per-project by default; --global for ~/.claude)' },
|
|
27
|
-
{ name: 'doctor', summary: 'diagnose the install (skills, bin, hook) and print the fix for anything broken' },
|
|
28
|
-
];
|
|
29
|
-
// Plumbbob v2 (D1/D10/D13): the deciding/executing boundary is no longer a lock,
|
|
30
|
-
// so there is nothing to defend — every verb runs the same whether a human or the
|
|
31
|
-
// model triggers it. The old human-only `mode` escape hatch and its CLAUDECODE
|
|
32
|
-
// in-session refusal are gone; what keeps the human the decider is the pause at
|
|
33
|
-
// the step boundary (the skills), not a refusal here.
|
|
34
|
-
function formatHelp() {
|
|
35
|
-
const width = Math.max(...VERBS.map((v) => v.name.length));
|
|
36
|
-
const rows = VERBS.map((v) => ` ${v.name.padEnd(width)} ${v.summary}`);
|
|
37
|
-
return ['plumbbob — attention-first build process', '', 'Usage: plumbbob <verb> [args]', '', 'Verbs:', ...rows, ''].join('\n');
|
|
38
|
-
}
|
|
39
|
-
function dispatch(verb, cwd, rest) {
|
|
40
|
-
switch (verb) {
|
|
41
|
-
case 'start':
|
|
42
|
-
return start(cwd, rest);
|
|
43
|
-
case 'status':
|
|
44
|
-
return status(cwd);
|
|
45
|
-
case 'park':
|
|
46
|
-
return park(cwd, rest);
|
|
47
|
-
case 'build':
|
|
48
|
-
return build(cwd, rest);
|
|
49
|
-
case 'check':
|
|
50
|
-
return check(cwd);
|
|
51
|
-
case 'checkpoint':
|
|
52
|
-
return checkpoint(cwd, rest);
|
|
53
|
-
case 'revert':
|
|
54
|
-
return revert(cwd, rest);
|
|
55
|
-
case 'spike':
|
|
56
|
-
return spike(cwd, rest);
|
|
57
|
-
case 'reset':
|
|
58
|
-
return reset(cwd);
|
|
59
|
-
case 'setup':
|
|
60
|
-
return setup(cwd, rest);
|
|
61
|
-
case 'doctor':
|
|
62
|
-
return doctor(cwd);
|
|
63
|
-
default:
|
|
64
|
-
process.stderr.write(`plumbbob: unknown verb '${verb}'. Run 'plumbbob help' for the verb table.\n`);
|
|
65
|
-
return 1;
|
|
66
|
-
}
|
|
67
|
-
}
|
|
68
|
-
function run(argv) {
|
|
69
|
-
const verb = argv[0] ?? 'help';
|
|
70
|
-
const rest = argv.slice(1);
|
|
71
|
-
if (verb === 'help' || verb === '--help' || verb === '-h') {
|
|
72
|
-
process.stdout.write(`${formatHelp()}\n`);
|
|
73
|
-
return 0;
|
|
74
|
-
}
|
|
75
|
-
try {
|
|
76
|
-
return dispatch(verb, process.cwd(), rest);
|
|
77
|
-
}
|
|
78
|
-
catch (err) {
|
|
79
|
-
const message = err instanceof Error ? err.message : String(err);
|
|
80
|
-
process.stderr.write(`plumbbob: ${verb} failed: ${message}\n`);
|
|
81
|
-
return 1;
|
|
82
|
-
}
|
|
83
|
-
}
|
|
2
|
+
// plumbbob CLI entry — the published bin. All routing and the help table live in
|
|
3
|
+
// cli-core.ts (unit-tested in-process); this file is only the executable shell:
|
|
4
|
+
// it wires argv to the dispatcher and turns the returned code into the exit code.
|
|
5
|
+
// Keeping the single process.exit here is what lets cli-core.ts be imported by a
|
|
6
|
+
// test without tearing down the worker.
|
|
7
|
+
import { run } from "./cli-core.js";
|
|
84
8
|
process.exit(run(process.argv.slice(2)));
|
package/dist/lib/archive.js
CHANGED
|
@@ -58,7 +58,7 @@ function uniqueArchiveDir(root, base) {
|
|
|
58
58
|
}
|
|
59
59
|
// Copy intent + build-log + report into archive/<date>-<slug>/ and return the
|
|
60
60
|
// directory created. Intent and build-log always exist in an active session; the
|
|
61
|
-
// report is copied only when present — v2 `
|
|
61
|
+
// report is copied only when present — v2 `wrap` does not gate on it (D9), so a
|
|
62
62
|
// close-out without a report still archives the rest.
|
|
63
63
|
export function archiveSession(root) {
|
|
64
64
|
const dir = uniqueArchiveDir(root, `${today()}-${slugify(sessionTitle(root))}`);
|
package/dist/lib/orient.js
CHANGED
|
@@ -3,6 +3,7 @@
|
|
|
3
3
|
// best-effort — it takes raw file contents (no fs), and a malformed doc degrades
|
|
4
4
|
// to fewer fields rather than throwing. Functional/procedural, no classes, no
|
|
5
5
|
// default export (C1).
|
|
6
|
+
import { parseStepSeam } from "./intent.js";
|
|
6
7
|
// The lines of a named `## Section`, from its heading to the next `## ` (or EOF).
|
|
7
8
|
function sectionLines(content, heading) {
|
|
8
9
|
const lines = content.split('\n');
|
|
@@ -45,8 +46,10 @@ export function parseSteps(intent) {
|
|
|
45
46
|
});
|
|
46
47
|
return starts.map((s, i) => {
|
|
47
48
|
const blockEnd = starts[i + 1]?.idx ?? lines.length;
|
|
48
|
-
const block = lines.slice(s.idx, blockEnd).join('\n')
|
|
49
|
-
|
|
49
|
+
const block = lines.slice(s.idx, blockEnd).join('\n');
|
|
50
|
+
const dw = /\*\*done when:\*\*\s*(.+)/i.exec(block);
|
|
51
|
+
const doneWhen = dw ? (dw[1] ?? '').trim() : null;
|
|
52
|
+
return { n: s.n, done: s.done, title: s.title, planned: /done when/i.test(block), doneWhen };
|
|
50
53
|
});
|
|
51
54
|
}
|
|
52
55
|
// Flip step N's `[ ]` checkbox to `[x]` within the `## Steps` section — mechanical
|
|
@@ -74,7 +77,7 @@ export function parseOpenQuestions(intent) {
|
|
|
74
77
|
.length;
|
|
75
78
|
}
|
|
76
79
|
// Open parked items: `- [ ]` lines under `## Park list` (the `park` verb's format).
|
|
77
|
-
// A harvested item is flipped to `- [x]` by `/
|
|
80
|
+
// A harvested item is flipped to `- [x]` by `/plumbbob:harvest` and no longer counts; the
|
|
78
81
|
// `(none yet)` placeholder and the blockquote instructions never match.
|
|
79
82
|
export function parseParked(buildLog) {
|
|
80
83
|
return sectionLines(buildLog, '## Park list').filter((l) => /^-\s+\[ \]\s+\S/.test(l.trim())).length;
|
|
@@ -96,36 +99,38 @@ function nextMove(state, steps, inFlight, parked) {
|
|
|
96
99
|
case 'SPIKE':
|
|
97
100
|
return 'close the spike — `plumbbob spike done`';
|
|
98
101
|
case 'FINISH':
|
|
99
|
-
return 'wrap up — `/
|
|
102
|
+
return 'wrap up — `/plumbbob:wrap`';
|
|
100
103
|
case 'REVIEW':
|
|
101
|
-
return 'read the diff cold against intent, then `/
|
|
104
|
+
return 'read the diff cold against intent, then `/plumbbob:verify`';
|
|
102
105
|
case 'BUILD': {
|
|
103
106
|
const n = inFlight ?? steps.find((s) => !s.done)?.n;
|
|
104
107
|
return n === undefined
|
|
105
|
-
? 'finish the step in flight — `/
|
|
106
|
-
: `finish step ${n} — \`/
|
|
108
|
+
? 'finish the step in flight — `/plumbbob:verify`'
|
|
109
|
+
: `finish step ${n} — \`/plumbbob:verify\` (or keep editing, then \`/plumbbob:verify\`)`;
|
|
107
110
|
}
|
|
108
111
|
default: {
|
|
109
112
|
// DESIGN (and any unknown state): you are at the boundary.
|
|
110
113
|
const nextUndone = steps.find((s) => !s.done);
|
|
111
114
|
if (nextUndone === undefined) {
|
|
112
115
|
if (steps.length === 0) {
|
|
113
|
-
return 'plan the first step — `/
|
|
116
|
+
return 'plan the first step — `/plumbbob:step`';
|
|
114
117
|
}
|
|
115
|
-
//
|
|
116
|
-
//
|
|
117
|
-
const harvest = parked > 0 ? `harvest ${parked} parked idea${parked === 1 ? '' : 's'} — \`/
|
|
118
|
-
return `${harvest}
|
|
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)`;
|
|
119
122
|
}
|
|
120
123
|
return nextUndone.planned
|
|
121
|
-
? `build step ${nextUndone.n} — \`/
|
|
122
|
-
: `plan step ${nextUndone.n} — \`/
|
|
124
|
+
? `build step ${nextUndone.n} — \`/plumbbob:build\` (or \`/plumbbob:step\` to revise it first)`
|
|
125
|
+
: `plan step ${nextUndone.n} — \`/plumbbob:step\``;
|
|
123
126
|
}
|
|
124
127
|
}
|
|
125
128
|
}
|
|
126
129
|
export function orient(input) {
|
|
127
130
|
const steps = parseSteps(input.intent);
|
|
128
131
|
const parked = parseParked(input.buildLog);
|
|
132
|
+
const nextUndone = steps.find((s) => !s.done);
|
|
133
|
+
const seamParse = nextUndone === undefined ? null : parseStepSeam(input.intent, nextUndone.n);
|
|
129
134
|
return {
|
|
130
135
|
title: parseTitle(input.intent),
|
|
131
136
|
state: input.state,
|
|
@@ -134,6 +139,8 @@ export function orient(input) {
|
|
|
134
139
|
parked,
|
|
135
140
|
openQuestions: parseOpenQuestions(input.intent),
|
|
136
141
|
next: nextMove(input.state, steps, input.inFlight, parked),
|
|
142
|
+
nextDoneWhen: nextUndone?.doneWhen ?? null,
|
|
143
|
+
nextSeam: seamParse !== null && seamParse.ok ? seamParse.seam : [],
|
|
137
144
|
};
|
|
138
145
|
}
|
|
139
146
|
export function formatOrientation(o) {
|
|
@@ -142,7 +149,20 @@ export function formatOrientation(o) {
|
|
|
142
149
|
const stepLines = o.steps.map((s) => {
|
|
143
150
|
const marker = s.done ? '✓' : s === nextUndone ? '▸' : ' ';
|
|
144
151
|
const tail = s === nextUndone ? ' ← next' : '';
|
|
145
|
-
|
|
152
|
+
const head = ` ${marker} ${s.n} ${s.title}${tail}`;
|
|
153
|
+
if (s !== nextUndone) {
|
|
154
|
+
return head;
|
|
155
|
+
}
|
|
156
|
+
// Surface the next step's detail so the human can review it (and `/plumbbob:step`-
|
|
157
|
+
// revise) before building. Only what's present — a rough step shows neither.
|
|
158
|
+
const detail = [];
|
|
159
|
+
if (o.nextDoneWhen !== null) {
|
|
160
|
+
detail.push(` done when: ${o.nextDoneWhen}`);
|
|
161
|
+
}
|
|
162
|
+
if (o.nextSeam.length > 0) {
|
|
163
|
+
detail.push(` seam: ${o.nextSeam.join(', ')}`);
|
|
164
|
+
}
|
|
165
|
+
return detail.length > 0 ? [head, ...detail].join('\n') : head;
|
|
146
166
|
});
|
|
147
167
|
const stepsBlock = o.steps.length === 0 ? ' (no steps planned yet)' : ` steps ${doneCount}/${o.steps.length} done\n${stepLines.join('\n')}`;
|
|
148
168
|
const cp = o.lastCheckpoint;
|
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: `/
|
|
2
|
+
// change. The read-only half of the verify tick: `/plumbbob: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";
|
package/dist/verbs/checkpoint.js
CHANGED
|
@@ -4,7 +4,7 @@
|
|
|
4
4
|
// It gates on a green check, then commits any pending work (or records the existing
|
|
5
5
|
// HEAD when the tree is already clean — the human's commit skill may have committed
|
|
6
6
|
// first), records the SHA, flips the intent checkbox to `[x]`, clears any STEP/SEAM,
|
|
7
|
-
// and returns to DESIGN. The diff's author is irrelevant: `/
|
|
7
|
+
// and returns to DESIGN. The diff's author is irrelevant: `/plumbbob:build`, your hands,
|
|
8
8
|
// a vibe session, or another harness all checkpoint the same way.
|
|
9
9
|
import { appendFileSync, readFileSync, rmSync, writeFileSync } from 'node:fs';
|
|
10
10
|
import { commit, findRepoRoot, headSha, isDirty, stageAll } from "../lib/git.js";
|