plumbbob 0.3.0 → 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.
@@ -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,89 +1,228 @@
1
1
  # Plumbbob
2
2
 
3
- A guidance-first build process for working *with* an LLM instead of being dragged behind one. You decide on a surface outside the chat; plumbbob orients you, runs each step's labor, and then **stops and waits for you to advance** — the human is the clock, not a lock.
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 own loop.
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 (a pre-edit muzzle). v2 replaces the lock with a **clock**: nothing blocks your edits. Instead, the system does a step's work, then pauses at a verify gate for your approval before it checkpoints. You stay the decider by *advancing the clock*, not by fighting a wall.
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
- - The **human** owns convergence you decide, you approve, you advance.
15
- - The **plan** lives on durable surfaces (`intent.md`, `build-log.md`), not in the chat.
16
- - The boundary is a **pause**, not a refusal: the system pulls up to a line and waits.
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 `pb-*` skills — no step numbers to remember, no raw CLI to type. `/pb-status` always names your next move.
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
- | `/pb-plan` | frame a goal — scaffold the session + author intent's Frame, Decisions, Constraints |
25
- | `/pb-step` | plan the next increment a title, a done-when, a seam |
26
- | `/pb-build` | *(optional)* implement the planned step, then verify it to the pause |
27
- | `/pb-verify` | the tick — check → self-review → validate → **PAUSE** → checkpoint |
28
- | `/pb-park` | capture an idea without chasing it |
29
- | `/pb-status` | orient — where you are, what's parked, and the next move |
30
- | `/pb-harvest` | triage parked ideas at a boundary (blocker / tangent / pivot) |
31
- | `/pb-reset` | close out — write the report, archive, clear for a fresh goal |
32
-
33
- Three optional power moves survive for when you need them: `/pb-revert` (recover to a checkpoint), `/pb-spike` (throwaway worktree experiment), and `/plumbbob-interrogate` (attack the frame for holes).
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
- /pb-plan frame the goal (once)
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
- /pb-status "what's next?"
41
- /pb-step plan the next increment
42
- /pb-build (or DIY) implement it
43
- /pb-verify check review → PAUSE → checkpoint
44
- /pb-park capture strays mid-build
45
- /pb-harvest triage them at a boundary
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
- **The pluggable executor.** `/pb-build` is one way to turn a planned step into code — it is *optional*. Implement by hand, in a vibe session, or with another harness, and run `/pb-verify` instead: it reads the *diff, not the author*. Plumbbob is the harness-agnostic spine; how the diff appears is a slot you fill however you like.
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).
108
+
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.
114
+
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.
119
+
120
+ ## Calibration: size everything to the work
121
+
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*.
124
+
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.
130
+
131
+ Calibration is the skill. When in doubt, smaller.
50
132
 
51
133
  ## What ships
52
134
 
53
- - A `plumbbob` CLI (TypeScript, run natively by Node ≥ 22.18, zero runtime dependencies) — the dumb mechanical verbs the skills shell out to. You never type it by hand.
54
- - One session-gated Claude Code hook `post-edit.sh`, a non-blocking light-feedback pass that injects file-scoped lint into the model's context. (v1's pre-edit muzzle and bash-guard are gone — guidance, not enforcement.)
55
- - The eight `pb-*` skills plus the optional power moves, each `disable-model-invocation` so *you* fire every move.
56
- - A `.plumbbob/` sidecar of flat files: `STATE` (orientation only), `intent.md`, `build-log.md`, `checkpoints`, and `archive/`.
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.
57
156
 
58
- ## Install
157
+ ## STATE is orientation, not a gate
59
158
 
60
- The npm package is `plumbbob`; it installs the CLI (`plumbbob` plus a `pb` shorthand) and ships the hook and skills inside the package.
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.
61
164
 
62
- ### Project-level (recommended)
165
+ ## Git footprint — additive only
63
166
 
64
- ```sh
65
- pnpm add -D plumbbob
66
- pnpm exec plumbbob setup --local # or --project to commit it for a team
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
67
188
  ```
68
189
 
69
- This copies the skills into `<repo>/.claude/skills/` (their bin pointed at the project-local binary) and registers the post-edit hook in place under `node_modules`. Nothing is written under `~`.
190
+ ## Install
70
191
 
71
- ### Global
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.
72
195
 
73
196
  ```sh
74
- npm install -g plumbbob
75
- plumbbob setup --global
197
+ npm i -g plumbbob # the CLI (also a `pb` shorthand)
198
+ plumbbob init # link it into Claude Code; --uninstall to undo
76
199
  ```
77
200
 
78
- This copies the hook and skills under `~/.claude/` and registers them in `~/.claude/settings.json`. Restart Claude Code (or reload settings) after install.
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.
79
207
 
80
- ## STATE is orientation, not a gate
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.
81
211
 
82
- The one-word `.plumbbob/STATE` (`DESIGN` / `BUILD` / `SPIKE`) is read by `/pb-status` to tell you where you are and what to do next. It no longer gates edits — a wrong state is a mislabeled position on a map, not a locked door. The post-edit hook is session-gated: a repo with no `.plumbbob/STATE` behaves exactly like plain Claude Code.
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.
83
215
 
84
- ## Git footprint — additive only
216
+ ### Verify
217
+
218
+ ```sh
219
+ plumbbob doctor
220
+ ```
85
221
 
86
- Plumbbob commits cheap checkpoint markers (`plumbbob: step n done`) on your feature branch and reverts to its own recorded SHAs. It never rewrites pushed history; your normal squash-merge collapses the checkpoints at PR time. `reset` archives plain markdown under `.plumbbob/archive/` and never touches git.
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.
87
226
 
88
227
  ## Development
89
228
 
@@ -93,7 +232,22 @@ pnpm check # tsc, oxlint, ast-grep, vitest, knip, markdownlint
93
232
  pnpm build # emit dist/ (what the published bin runs)
94
233
  ```
95
234
 
96
- The underlying philosophy — attention as the scarce resource — lives in [`docs/attention-first-development.md`](docs/attention-first-development.md).
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.
97
251
 
98
252
  ## License
99
253
 
@@ -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,80 +1,8 @@
1
1
  #!/usr/bin/env node
2
- // plumbbob CLI — hand-rolled argv dispatch, zero runtime deps, node: builtins
3
- // only (D1/C2). Functional/procedural: no classes, no `this`, no default
4
- // export (C1). Verbs are wired up build step by build step.
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 { reset } from "./verbs/reset.js";
14
- import { setup } from "./verbs/setup.js";
15
- const VERBS = [
16
- { name: 'start', summary: 'scaffold .plumbbob/; STATE=DESIGN; record the baseline commit' },
17
- { name: 'status', summary: 'print the session state, or NO ACTIVE SESSION' },
18
- { name: 'build', summary: 'build <n>: write the seam from step n; STATE=BUILD (orientation, not a lock)' },
19
- { name: 'check', summary: 'run the heavy check and report; no state change' },
20
- { name: 'checkpoint', summary: 'checkpoint [<n>]: gate on green, commit/record SHA, mark step done, STATE=DESIGN (executor-agnostic)' },
21
- { name: 'revert', summary: 'revert [--to n]: git reset --hard to a checkpoint SHA; STATE=DESIGN' },
22
- { name: 'park', summary: 'park "<text>": append a raw line to the park list' },
23
- { name: 'spike', summary: 'spike "<slug>" | spike done: throwaway worktree experiment' },
24
- { name: 'reset', summary: 'v2 close-out: archive intent+log+report (no gate), clear the sidecar, STATE off' },
25
- { name: 'setup', summary: 'install hooks + skills; register them (self-contained per-project by default; --global for ~/.claude)' },
26
- ];
27
- // Plumbbob v2 (D1/D10/D13): the deciding/executing boundary is no longer a lock,
28
- // so there is nothing to defend — every verb runs the same whether a human or the
29
- // model triggers it. The old human-only `mode` escape hatch and its CLAUDECODE
30
- // in-session refusal are gone; what keeps the human the decider is the pause at
31
- // the step boundary (the skills), not a refusal here.
32
- function formatHelp() {
33
- const width = Math.max(...VERBS.map((v) => v.name.length));
34
- const rows = VERBS.map((v) => ` ${v.name.padEnd(width)} ${v.summary}`);
35
- return ['plumbbob — attention-first build process', '', 'Usage: plumbbob <verb> [args]', '', 'Verbs:', ...rows, ''].join('\n');
36
- }
37
- function dispatch(verb, cwd, rest) {
38
- switch (verb) {
39
- case 'start':
40
- return start(cwd, rest);
41
- case 'status':
42
- return status(cwd);
43
- case 'park':
44
- return park(cwd, rest);
45
- case 'build':
46
- return build(cwd, rest);
47
- case 'check':
48
- return check(cwd);
49
- case 'checkpoint':
50
- return checkpoint(cwd, rest);
51
- case 'revert':
52
- return revert(cwd, rest);
53
- case 'spike':
54
- return spike(cwd, rest);
55
- case 'reset':
56
- return reset(cwd);
57
- case 'setup':
58
- return setup(cwd, rest);
59
- default:
60
- process.stderr.write(`plumbbob: unknown verb '${verb}'. Run 'plumbbob help' for the verb table.\n`);
61
- return 1;
62
- }
63
- }
64
- function run(argv) {
65
- const verb = argv[0] ?? 'help';
66
- const rest = argv.slice(1);
67
- if (verb === 'help' || verb === '--help' || verb === '-h') {
68
- process.stdout.write(`${formatHelp()}\n`);
69
- return 0;
70
- }
71
- try {
72
- return dispatch(verb, process.cwd(), rest);
73
- }
74
- catch (err) {
75
- const message = err instanceof Error ? err.message : String(err);
76
- process.stderr.write(`plumbbob: ${verb} failed: ${message}\n`);
77
- return 1;
78
- }
79
- }
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";
80
8
  process.exit(run(process.argv.slice(2)));
@@ -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 `reset` does not gate on it (D9), so a
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))}`);
@@ -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').toLowerCase();
49
- return { n: s.n, done: s.done, title: s.title, planned: block.includes('done when') };
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 `/pb-harvest` and no longer counts; the
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 — `/pb-reset`';
102
+ return 'wrap up — `/plumbbob:wrap`';
100
103
  case 'REVIEW':
101
- return 'read the diff cold against intent, then `/pb-verify`';
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 — `/pb-verify`'
106
- : `finish step ${n} — \`/pb-verify\` (or keep editing, then \`/pb-verify\`)`;
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 — `/pb-step`';
116
+ return 'plan the first step — `/plumbbob:step`';
114
117
  }
115
- // Just-in-time (D6): finishing the *planned* steps usually means "plan the
116
- // next," not "done"only the human knows which, so offer both.
117
- const harvest = parked > 0 ? `harvest ${parked} parked idea${parked === 1 ? '' : 's'} — \`/pb-harvest\`; then ` : '';
118
- return `${harvest}plan the next step — \`/pb-step\` (or \`/pb-reset\` to wrap up if you're done)`;
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} — \`/pb-build\``
122
- : `plan step ${nextUndone.n} — \`/pb-step\``;
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
- return ` ${marker} ${s.n} ${s.title}${tail}`;
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;
@@ -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: `/pb-verify` runs this before the
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";
@@ -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: `/pb-build`, your hands,
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";