plumbbob 0.4.14 → 0.5.3

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.
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "plumbbob",
3
3
  "displayName": "PlumbBob",
4
- "version": "0.4.14",
4
+ "version": "0.5.3",
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
@@ -4,47 +4,65 @@
4
4
  <img src="hero.jpg" alt="A row of plumb bobs of varying shapes hanging from strings" width="600">
5
5
  </p>
6
6
 
7
- A [guidance-first build process](docs/attention-first-development.md) for working
8
- *with* an LLM instead of being dragged behind one. It's the layer below [Ridgeline](https://github.com/robmclarty/ridgeline):
9
- where Ridgeline runs autonomously without you, PlumbBob keeps you in the driver's seat
10
- for the small-to-medium work that doesn't justify a full autonomous build — a feature,
11
- a bug, a refactor while staying deliberate rather than vibing. You decide on a surface
12
- outside the chat; PlumbBob orients you, runs each step's labor, and then **stops and
13
- waits for you to advance** the human is the clock, not a lock.
7
+ PlumbBob is a Claude Code plugin — twelve `/pb-*` skills and a small CLI — that runs
8
+ LLM-assisted coding as a loop you control. You and the model settle the plan in a file
9
+ *before* any code; then the model builds one small step at a time and **stops after
10
+ each one**, waiting for you to read the diff and approve it before anything is
11
+ committed. It's built for the small-to-medium work that fills most days a feature, a
12
+ bug, a refactor where a fully autonomous build is overkill but freestyle prompting
13
+ leaves you dragged behind the model, reacting instead of deciding.
14
14
 
15
15
  > Establish *plumb* before you build. The LLM is a hand, not a head.
16
16
 
17
- The name is the method. A plumb bob is a weight on a string — gravity pulls it into
18
- a perfectly straight vertical line, the builder's oldest reference for *true*: the
19
- fixed mark you hold the work against so it never drifts out of square. PlumbBob hangs
20
- your **intent** as that line and keeps every step aligned to it, so the build stays
21
- plumb with what you decided instead of wandering off behind the model.
22
-
23
17
  Its one law is **vibe to execute, never vibe to decide**: the human owns every
24
18
  decision, the LLM owns the labor, and the boundary between them is a **pause you
25
- advance**, not a wall that refuses you. The *why* behind that — attention as the
26
- scarce resource — is the subject of the guidance-first article above; the *how* of
27
- each method is in [`docs/techniques.md`](docs/techniques.md). This repository was
28
- built using PlumbBob, dogfooded on its own build under its own loop.
19
+ advance**, not a wall that refuses you. The *why* behind that — attention as the scarce
20
+ resource — is in [`docs/attention-first-development.md`](docs/attention-first-development.md).
21
+ PlumbBob is the layer below [Ridgeline](https://github.com/robmclarty/ridgeline): where
22
+ Ridgeline runs large, settled work autonomously without you, PlumbBob keeps you in the
23
+ driver's seat for the work your judgment has to steer. This repository was built using
24
+ PlumbBob, dogfooded on its own loop.
25
+
26
+ ## The loop in one picture
27
+
28
+ <p align="center">
29
+ <img src="demo.svg" alt="An animated terminal session: /pb-plan writes the plan, /pb-build implements step 1 and stops at the PAUSE, the human approves, the step is checkpointed" width="760">
30
+ </p>
31
+
32
+ Everything the model must stay true to lives in one file, the build's `intent.md`
33
+ (under `.plumbbob/builds/<slug>/`), written before any code: the **Frame** (the problem, what done looks like, what you're
34
+ explicitly *not* doing), the **Decisions** and **Constraints** (the settled calls, each
35
+ with its *because*), and the **Steps** — a numbered list where every step carries a
36
+ **done-when** (a checkable finish line, ideally a test) and a **seam** (the files it's
37
+ expected to touch).
38
+
39
+ ```text
40
+ /pb-plan decide everything on paper — frame, decisions, all steps (once)
41
+ per step:
42
+ /pb-build implement the next step → run checks → self-review → PAUSE
43
+ (approve) → the step is committed as a checkpoint; fire /pb-build again
44
+ /pb-finish report what shipped and why, final commit, clear (once)
45
+ ```
46
+
47
+ Nothing is locked and nothing refuses you — the loop does a step's labor, pulls up to
48
+ the line, and waits for you to advance it. **You are the clock.**
29
49
 
30
50
  ## Install
31
51
 
32
- PlumbBob installs **once, globally** — like `gh` or your dotfiles in one of two
33
- co-equal, mutually-exclusive ways (both register a Claude Code plugin named
34
- `plumbbob`).
52
+ PlumbBob installs **once, globally** — like `gh` or your dotfiles. There are two ways;
53
+ pick one (both register a Claude Code plugin named `plumbbob`, so running both
54
+ collides).
35
55
 
36
- **Marketplace plugin** — Claude Code installs the published `plumbbob` package for
37
- you (skills, hook, and the CLI on PATH), so you run no `npm i -g` and no `init`. It's
38
- published in the [`agent-tools`](https://github.com/robmclarty/agent-tools)
39
- marketplace — add that once, then install:
56
+ **Marketplace plugin** — Claude Code installs the published `plumbbob` package for you
57
+ (skills, hook, and the CLI on PATH), so you run no `npm i -g` and no `init`:
40
58
 
41
59
  ```text
42
60
  /plugin marketplace add robmclarty/agent-tools
43
61
  /plugin install plumbbob@robmclarty
44
62
  ```
45
63
 
46
- **npm global + `init`** — installs the CLI, then links the skills and hook into
47
- Claude Code in place:
64
+ **npm global + `init`** — installs the CLI, then links the skills and hook into Claude
65
+ Code in place:
48
66
 
49
67
  ```sh
50
68
  npm i -g plumbbob # the CLI (also a `pb` shorthand)
@@ -56,66 +74,143 @@ Restart Claude Code (or `/reload-plugins`) to activate, then run `plumbbob docto
56
74
  per-project sessions, the agent-neutral roadmap — is in
57
75
  [`docs/install.md`](docs/install.md).
58
76
 
59
- ## Features
77
+ ## Your first session
78
+
79
+ In Claude Code, inside any git repo with a clean tree:
80
+
81
+ 1. **Plan.** Fire `/pb-plan` and give it whatever you have — nothing (it interviews
82
+ you), a rough line (`/pb-plan rate-limit POST /login, 5/min/IP, return 429`), or a
83
+ path to a spec file. Together you fill `.plumbbob/intent.md`. No code is written
84
+ yet.
85
+ 2. **Build.** Fire `/pb-build`. It implements the next undone step, runs the
86
+ heavy check gate, reviews its own diff against the plan, and stops:
87
+
88
+ ```text
89
+ PAUSE — read the diff as an editor. Approve to checkpoint, or send fixes.
90
+ ```
91
+
92
+ (The check gate is [checkride](https://www.npmjs.com/package/checkride) — one
93
+ run across the tools your repo already configures: types, lint, tests, dead
94
+ code, docs. To gate through your own command instead, set the `"check"` key in
95
+ `.plumbbob/settings.json`, e.g. `"check": "npm test"`.)
96
+ 3. **Approve** — or send fixes. On your OK it commits the step as a checkpoint, marks
97
+ it done, and returns to the boundary. Fire `/pb-build` again for the next step —
98
+ re-firing it *is* the clock tick. Whenever you lose the thread, `/pb-status` shows
99
+ where you are and names the next move.
100
+ 4. **Finish.** When the last step is done, `/pb-finish` writes a report of what shipped
101
+ and why into the build's tracked folder, makes the final commit, and clears the slate
102
+ for the next goal. The folder rides the branch into the PR — the build's record merges
103
+ into `main` instead of dying with the worktree.
104
+
105
+ A mid-build "ooh, what if…" never derails the step: `/pb-park` captures it to a list
106
+ in one line, and `/pb-harvest` triages the list between steps. One goal walked end to
107
+ end, with the real output at every stage, is in
108
+ [`docs/happy-path.md`](docs/happy-path.md).
109
+
110
+ ## The skills
60
111
 
61
112
  You drive the whole loop from your IDE with `/plumbbob:*` skills — no step numbers to
62
- remember, no raw CLI to type. Each is `disable-model-invocation`, so *you* fire every
63
- move, and `/pb-status` always names your next one. (For readability these docs write
64
- the short form: `/pb-plan` means `/plumbbob:pb-plan`.)
113
+ remember, no raw CLI to type. Every skill is `disable-model-invocation`, so *you* fire
114
+ every move, and `/pb-status` always names your next one. (Claude Code namespaces them
115
+ under the plugin the real command is `/plumbbob:pb-plan`; for readability these docs
116
+ write the short form `/pb-plan`.)
65
117
 
66
118
  | Skill | Does |
67
119
  |-------|------|
68
- | `/pb-plan` | plan the whole goal — scaffold the session and author intent's Frame, Decisions, Constraints, **and all Steps** |
120
+ | `/pb-plan` | plan the whole goal — open the session and author intent's Frame, Decisions, Constraints, **and all Steps** |
69
121
  | `/pb-step` | revise/sharpen the next step (empty input auto-syncs it to reality) |
70
122
  | `/pb-build` | *(optional)* implement the next planned step, then verify it to the pause — `--auto` self-approves and chains to done |
71
123
  | `/pb-verify` | the tick — check → self-review → validate → **PAUSE** → checkpoint |
72
- | `/pb-park` | capture an idea without chasing it |
73
- | `/pb-status` | orient — where you are, the next step's done-when + seam, and the next move |
74
- | `/pb-harvest` | triage parked ideas at a boundary (blocker / tangent / pivot) |
75
- | `/pb-wrap` | wrap up — write the report, archive safely, clear for a fresh goal |
124
+ | `/pb-park` | capture a mid-build idea without chasing it |
125
+ | `/pb-status` | orient — where you are, the next step's done-when and seam, and the next move |
126
+ | `/pb-harvest` | triage parked ideas between steps (blocker / tangent / pivot) |
127
+ | `/pb-finish` | finish up — write the report, make the final commit, clear for a fresh goal |
76
128
 
77
129
  Three power moves round it out — `/pb-revert` (recover to a checkpoint), `/pb-spike`
78
- (throwaway worktree experiment), and `/pb-refine` (attack the frame for holes or
79
- repair a drifted plan) — plus `/pb-doctor` to check your install.
130
+ (throwaway worktree experiment for a fork the plan can't settle), and `/pb-refine`
131
+ (attack the frame for holes or repair a drifted plan) — plus `/pb-doctor` to check
132
+ your install. All twelve, with inputs and effects, are in
133
+ [`docs/skills-reference.md`](docs/skills-reference.md).
80
134
 
81
- Under the skills ships a zero-dependency `plumbbob` CLI (the mechanical verbs the
135
+ Under the skills ships a lean `plumbbob` CLI (the mechanical verbs the
82
136
  skills shell out to), one session-gated post-edit hook (non-blocking lint feedback in
83
- flow), and a `.plumbbob/` sidecar of flat files (`intent.md`, `build-log.md`,
84
- checkpoints, archive). `/pb-build` is just one executor implement a step by hand or
85
- in any other harness and run `/pb-verify` instead; it reads the *diff, not the author*.
86
-
87
- ## Getting started
88
-
89
- The happy path is **plan the whole thing up front, then drive `/pb-build` until
90
- done** approving each step at its verify pause:
91
-
92
- ```text
93
- /pb-plan author the whole plan (incl. all steps) (once)
94
- └ per step:
95
- /pb-status review the next step (done-when + seam)
96
- /pb-step (optional) sharpen/revise it first if needed
97
- /pb-build (or DIY) implement it verify PAUSE checkpoint
98
- /pb-park capture strays mid-build
99
- /pb-harvest triage them at a boundary
100
- /pb-wrap report + archive + clear (once)
101
- ```
102
-
103
- Each `/pb-build` builds the next undone step and stops at the pause for your
104
- approval — re-firing it is itself the clock tick. (`/pb-build --auto` is the opt-in
105
- that lets the agent self-approve and chain to done, halting on a red check or any
106
- mismatch.) For one goal walked end to end planning, building each step, wrapping
107
- up, and starting the next see [`docs/happy-path.md`](docs/happy-path.md).
137
+ flow), and a `.plumbbob/` sidecar you can open and edit by hand at any time — a tracked
138
+ `builds/<slug>/` folder per build (`intent.md`, `build-log.md`, `checkpoints`, `report.md`)
139
+ that rides the branch into the PR, plus an untracked control plane (`settings.local.json`,
140
+ the session sentinel, the in-flight markers).
141
+
142
+ **`/pb-build` is optional.** It's one executor, not the loop. Implement a step by
143
+ hand or vibe it in another session, or in another harness entirely — and run
144
+ `/pb-verify` instead: same checks, same pause, same checkpoint. It reads the *diff,
145
+ not the author*.
146
+
147
+ ## Why "PlumbBob"
148
+
149
+ The name is the method. A plumb bob is a weight on a string — gravity pulls it into a
150
+ perfectly straight vertical line, the builder's oldest reference for *true*: the fixed
151
+ mark you hold the work against so it never drifts out of square. PlumbBob hangs your
152
+ **intent** as that line and keeps every step aligned to it, so the build stays plumb
153
+ with what you decided instead of wandering off behind the model.
154
+
155
+ ## Why not just…?
156
+
157
+ The questions a skeptic should ask, answered straight.
158
+
159
+ **…use plan mode?** Plan mode is the same instinct decide before executing
160
+ scoped to one session. The plan lives in the chat, so it fades with the context
161
+ window, and the approval gates the *start* of the work, not each increment of it.
162
+ PlumbBob puts the plan in a file in your repo that survives compaction, restarts,
163
+ and next week; moves the approval to **every step** — each checked, reviewed
164
+ against the plan, and committed as its own checkpoint, so a wrong turn costs one
165
+ step, not the afternoon; and records the *whys* alongside the decisions, so the
166
+ archive can still answer "what did we decide, and why" long after the session is
167
+ gone. The two compose fine: plan mode — or anything else — can produce a step's
168
+ diff, and `/pb-verify` reads it like any other.
169
+
170
+ **…use git and a TODO file?** That's the honest skeleton, and if your discipline
171
+ holds by willpower alone, it may be all you need. What the CLI adds is the
172
+ mechanical half of the discipline, so willpower is spent only at the pause: a
173
+ recorded baseline, one SHA per verified step, a revert that returns exactly to
174
+ those SHAs while preserving your notes, an archive that never destroys, and a
175
+ dashboard re-injected into every skill so the model re-orients on each move
176
+ without you re-explaining the state.
177
+
178
+ **…wait for a vendor to ship this natively?** They're converging on it — plan
179
+ modes, spec kits, native checkpoints — and that convergence is evidence *for* the
180
+ premise. PlumbBob is deliberately thin against that future: the artifacts are
181
+ plain markdown and ordinary git commits in your own repo, the CLI is lean
182
+ (node builtins plus one deliberate dependency), and nothing locks you in. The
183
+ investment is small — twelve
184
+ skills over a plain-markdown sidecar, learnable in an afternoon — and what you're
185
+ actually learning is the method: decisions before code, one verified step at a
186
+ time, capture instead of chase. That transfers to whatever tool wins. If
187
+ something better ships, you walk away with your archives and your habits intact.
188
+
189
+ **…admit the pause is unenforceable?** It is — on purpose (D10/D13 in
190
+ [`docs/decisions.md`](docs/decisions.md)). A hard lock buys ritual, not control:
191
+ a determined model routes around it. So PlumbBob enforces deterministically where
192
+ it can — `checkpoint` refuses on a red check, `revert` restores only recorded
193
+ SHAs — and leaves human what must be human: you, reading the diff at the pause.
194
+ When guidance does get blown through, the checkpoint record makes recovery one
195
+ command. Cheap recovery, not prevention, is the control that actually matters.
108
196
 
109
197
  ## Documentation
110
198
 
111
- - [`docs/install.md`](docs/install.md)the full install guide and the agent-neutral roadmap.
112
- - [`docs/techniques.md`](docs/techniques.md) — the methods behind the loop, each on its own.
113
- - [`docs/happy-path.md`](docs/happy-path.md) — one goal walked end to end.
114
- - [`docs/cli-reference.md`](docs/cli-reference.md) — every verb, flag, exit code, and the `.plumbbob/` sidecar.
115
- - [`docs/troubleshooting.md`](docs/troubleshooting.md) — fixes for the common snags.
116
- - [`docs/decisions.md`](docs/decisions.md) — the `D#` / `C#` design-decision key.
117
- - [`docs/attention-first-development.md`](docs/attention-first-development.md) — the philosophy: attention as the scarce resource.
118
- - [`CONTRIBUTING.md`](CONTRIBUTING.md) — setup, conventions, and how to submit changes.
199
+ Each doc answers one question in rough reading order for a new user:
200
+
201
+ - *What does a session actually look like?* → [`docs/happy-path.md`](docs/happy-path.md) — one goal walked end to end; read this first.
202
+ - *Show me the artifacts it leaves behind.* → [`examples/`](examples/) — that same session's finished build folder, file by file.
203
+ - *Should I / can I / what about…?* → [`docs/faq.md`](docs/faq.md) — the adoption questions, answered straight.
204
+ - *What is each method for?* → [`docs/techniques.md`](docs/techniques.md) — steps, seams, the pause, park/harvest, spikes.
205
+ - *What does each skill do?* → [`docs/skills-reference.md`](docs/skills-reference.md) — all twelve `/pb-*` skills: inputs, effects, when to reach for each.
206
+ - *How do I install it, exactly?* → [`docs/install.md`](docs/install.md) — the full guide and the agent-neutral roadmap.
207
+ - *What does the CLI underneath do?* → [`docs/cli-reference.md`](docs/cli-reference.md) — every verb, flag, exit code, and the `.plumbbob/` sidecar.
208
+ - *Something's broken.* → [`docs/troubleshooting.md`](docs/troubleshooting.md) — fixes for the common snags.
209
+ - *Why is it built this way?* → [`docs/decisions.md`](docs/decisions.md) — the `D#` / `C#` design-decision key the source cites.
210
+ - *How does it hang together inside?* → [`docs/architecture.md`](docs/architecture.md) — the layers and planes, for contributors (in progress).
211
+ - *Why does this exist at all?* → [`docs/attention-first-development.md`](docs/attention-first-development.md) — the philosophy: attention as the scarce resource.
212
+ - *What does it run on my machine?* → [`SECURITY.md`](SECURITY.md) — execution surface and how to report a vulnerability.
213
+ - *How do I contribute?* → [`CONTRIBUTING.md`](CONTRIBUTING.md) — setup, conventions, and how to submit changes.
119
214
 
120
215
  ## License
121
216
 
package/dist/cli-core.js CHANGED
@@ -1,7 +1,8 @@
1
1
  // plumbbob CLI core — argv dispatch and the help table, separated from the bin
2
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).
3
+ // the top-level process.exit. Node builtins plus the deliberate few (D1/C2
4
+ // currently checkride alone, via check.ts). Functional/procedural: no classes,
5
+ // no `this`, no default export (C1).
5
6
  import { readFileSync } from 'node:fs';
6
7
  import { fileURLToPath } from 'node:url';
7
8
  import { start } from "./verbs/start.js";
@@ -12,21 +13,23 @@ import { check } from "./verbs/check.js";
12
13
  import { checkpoint } from "./verbs/checkpoint.js";
13
14
  import { revert } from "./verbs/revert.js";
14
15
  import { spike } from "./verbs/spike.js";
15
- import { wrap } from "./verbs/wrap.js";
16
+ import { use } from "./verbs/use.js";
17
+ import { finish } from "./verbs/finish.js";
16
18
  import { init } from "./verbs/init.js";
17
19
  import { doctor } from "./verbs/doctor.js";
18
20
  const VERBS = [
19
21
  { name: 'start', summary: 'scaffold .plumbbob/; open the session; record the baseline commit' },
20
22
  { name: 'status', summary: 'print the orientation dashboard, or NO ACTIVE SESSION' },
21
23
  { name: 'build', summary: 'build <n>: write the seam from step n (in-flight, not a lock)' },
22
- { name: 'check', summary: 'run the heavy check and report; no state change' },
24
+ { name: 'check', summary: 'run the heavy check and report; no state change (--bail --only a,b … narrow the checkride gate)' },
23
25
  { name: 'checkpoint', summary: 'checkpoint [<n>]: gate on green, commit/record SHA, mark step done (executor-agnostic)' },
24
26
  { name: 'revert', summary: 'revert [--to n]: git reset --hard to a checkpoint SHA' },
25
27
  { name: 'park', summary: 'park "<text>": append a raw line to the park list' },
26
28
  { name: 'spike', summary: 'spike "<slug>" | spike done: throwaway worktree experiment' },
27
- { name: 'wrap', summary: 'close-out: archive intent+log+report (no gate), clear the sidecar, close the session' },
29
+ { name: 'use', summary: 'use <slug>: re-point the active-build cursor and resume that build' },
30
+ { name: 'finish', summary: 'close-out: report + final commit (no gate), clear the control state, close the session' },
28
31
  { name: 'init', summary: 'link plumbbob into Claude Code as an in-place plugin (~/.claude/skills/plumbbob); --uninstall to undo' },
29
- { name: 'doctor', summary: 'diagnose the plugin link (manifest, skills, hook) and print the fix for anything broken' },
32
+ { name: 'doctor', summary: 'diagnose the plugin link + detect a legacy flat sidecar; doctor --migrate moves it into builds/ (staged, not committed)' },
30
33
  ];
31
34
  // PlumbBob (D1/D10/D13): the deciding/executing boundary is a pause, not a lock,
32
35
  // so there is nothing to defend — every verb runs the same whether a human or the
@@ -51,36 +54,40 @@ export function readVersion() {
51
54
  return 'unknown';
52
55
  }
53
56
  }
54
- function dispatch(verb, cwd, rest) {
57
+ // Async because the checkride gate (D32) is: `check` and `checkpoint` await it;
58
+ // every other verb returns synchronously through the same Promise-typed seam.
59
+ async function dispatch(verb, cwd, rest) {
55
60
  switch (verb) {
56
61
  case 'start':
57
62
  return start(cwd, rest);
58
63
  case 'status':
59
- return status(cwd);
64
+ return status(cwd, rest);
60
65
  case 'park':
61
66
  return park(cwd, rest);
62
67
  case 'build':
63
68
  return build(cwd, rest);
64
69
  case 'check':
65
- return check(cwd);
70
+ return check(cwd, rest);
66
71
  case 'checkpoint':
67
72
  return checkpoint(cwd, rest);
68
73
  case 'revert':
69
74
  return revert(cwd, rest);
70
75
  case 'spike':
71
76
  return spike(cwd, rest);
72
- case 'wrap':
73
- return wrap(cwd);
77
+ case 'use':
78
+ return use(cwd, rest);
79
+ case 'finish':
80
+ return finish(cwd, rest);
74
81
  case 'init':
75
82
  return init(rest);
76
83
  case 'doctor':
77
- return doctor();
84
+ return doctor(cwd, rest);
78
85
  default:
79
86
  process.stderr.write(`plumbbob: unknown verb '${verb}'. Run 'plumbbob help' for the verb table.\n`);
80
87
  return 1;
81
88
  }
82
89
  }
83
- export function run(argv) {
90
+ export async function run(argv) {
84
91
  const verb = argv[0] ?? 'help';
85
92
  const rest = argv.slice(1);
86
93
  if (verb === 'help' || verb === '--help' || verb === '-h') {
@@ -92,7 +99,7 @@ export function run(argv) {
92
99
  return 0;
93
100
  }
94
101
  try {
95
- return dispatch(verb, process.cwd(), rest);
102
+ return await dispatch(verb, process.cwd(), rest);
96
103
  }
97
104
  catch (err) {
98
105
  const message = err instanceof Error ? err.message : String(err);
package/dist/cli.js CHANGED
@@ -5,4 +5,4 @@
5
5
  // Keeping the single process.exit here is what lets cli-core.ts be imported by a
6
6
  // test without tearing down the worker.
7
7
  import { run } from "./cli-core.js";
8
- process.exit(run(process.argv.slice(2)));
8
+ process.exit(await run(process.argv.slice(2)));
@@ -1,6 +1,6 @@
1
1
  // Mutations to the active build-log.md — the human-facing ledger. `park` appends to
2
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).
3
+ // line-by-line *as it happens* rather than being reconstructed at finish (D-history).
4
4
  // Pure string transforms (no fs, no classes — C1/C2); callers own the IO so these
5
5
  // stay trivially testable.
6
6
  // Append `line` after the last non-blank line of the `## <heading>` section — i.e.
@@ -27,7 +27,7 @@ export function appendToSection(content, heading, line) {
27
27
  }
28
28
  // The Log line `checkpoint` writes for a landed step: dated, names the step (its
29
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.
30
+ // build's history — `/plumbbob:pb-finish` reads these instead of re-narrating the build.
31
31
  export function checkpointLogLine(date, step, sha, title) {
32
32
  const head = `- ${date} — step ${step} checkpointed · ${sha.slice(0, 9)}`;
33
33
  return title !== null && title.length > 0 ? `${head} — ${title}` : head;
package/dist/lib/check.js CHANGED
@@ -1,26 +1,95 @@
1
- // The heavy check (D16/D24): the full gate that `review` and `done` refuse to
2
- // advance past while red. The command is read from `.plumbbob/config` (`check=`)
3
- // so non-pnpm repos gate too; tests point it at a stub (`true`/`false`) since a
4
- // real `pnpm check` would recurse into vitest (D14).
1
+ // The heavy check (D16/D24/D32): the full gate that `verify` and `checkpoint`
2
+ // refuse to advance past while red. Two paths through one seam:
3
+ // - a `check` setting resolved through the ladder (flag settings.local.json
4
+ // settings.json, D27) is a shell command spawn it exactly as before, so
5
+ // any repo can gate through anything (tests point it at `true`/`false`, D14);
6
+ // - no setting at all → checkride, our sibling package, imported
7
+ // programmatically (D32): the typed summary comes back in-process, failing
8
+ // slots are reported with their `.check/` raw-output pointers, and an
9
+ // all-slots-skipped run REFUSES rather than green-lighting a repo checkride
10
+ // can't see (a vacuous pass is not a pass).
11
+ // Checkride's stream discipline holds: human progress goes to stderr. Exit 0 is
12
+ // green, 1 is red, 2 means the harness itself broke — reported distinctly,
13
+ // because a misconfigured gate must not read as broken code (both still block).
5
14
  import { spawnSync } from 'node:child_process';
6
- import { readFileSync } from 'node:fs';
7
- import { configPath } from "./sidecar.js";
8
- const DEFAULT_CHECK = 'pnpm run check';
9
- function readCheckCommand(root) {
10
- let config = '';
11
- try {
12
- config = readFileSync(configPath(root), 'utf8');
13
- }
14
- catch {
15
- return DEFAULT_CHECK;
15
+ import { runChecks } from 'checkride';
16
+ import { resolveString } from "./settings.js";
17
+ // Runs the gate in `root` and returns the exit code (0 green, 1 red, 2 harness
18
+ // error). `commandFlag` is the optional CLI override at the top of the settings
19
+ // ladder; a resolved command means the spawn path, no setting means checkride.
20
+ export async function runCheck(root, flags = {}, commandFlag) {
21
+ const command = resolveString(root, 'check', '', commandFlag);
22
+ if (command.length > 0) {
23
+ return runCommand(root, command, flags);
16
24
  }
17
- const line = config.split('\n').find((l) => l.startsWith('check='));
18
- return line === undefined ? DEFAULT_CHECK : line.slice('check='.length).trim();
25
+ return runCheckride(root, flags);
19
26
  }
20
- // Runs the configured check in `root`, streaming its output to the terminal so
21
- // the human sees what failed. Returns the exit code (0 = green).
22
- export function runCheck(root) {
23
- const command = readCheckCommand(root);
27
+ // The spawn override, byte-for-byte the pre-D32 behavior: stream the command's
28
+ // own output to the terminal and return its exit code.
29
+ function runCommand(root, command, flags) {
30
+ if (hasFlags(flags)) {
31
+ process.stderr.write(`plumbbob: check flags only narrow the checkride gate — ignored for the configured command '${command}'.\n`);
32
+ }
24
33
  const result = spawnSync(command, { cwd: root, shell: true, stdio: 'inherit' });
25
34
  return result.status ?? 1;
26
35
  }
36
+ async function runCheckride(root, flags) {
37
+ let summary;
38
+ let exitCode;
39
+ try {
40
+ const result = await runChecks({
41
+ cwd: root,
42
+ bail: flags.bail ?? false,
43
+ changed: flags.changed ?? false,
44
+ all: flags.all ?? false,
45
+ only: flags.only !== undefined ? [...flags.only] : null,
46
+ skip: flags.skip !== undefined ? [...flags.skip] : null,
47
+ include: flags.include !== undefined ? [...flags.include] : null,
48
+ });
49
+ summary = result.summary;
50
+ exitCode = result.exitCode;
51
+ }
52
+ catch (err) {
53
+ // The harness broke, not the code (D32): a malformed checkride.config.json
54
+ // or the like. Checkride's own CLI maps this to exit 2; so do we.
55
+ const message = err instanceof Error ? err.message : String(err);
56
+ process.stderr.write(`plumbbob: the check gate itself failed — ${message}\n` +
57
+ ` Fix checkride.config.json, or set the "check" key in .plumbbob/settings.json to gate through another command.\n`);
58
+ return 2;
59
+ }
60
+ if (summary.checks.every((c) => c.skipped === true || c.name === 'links')) {
61
+ // Zero-config checkride skips slots with no detected tool; a repo it can't
62
+ // see must refuse, not vacuously green-light the checkpoint (D32). The
63
+ // built-in `links` slot is always-on (empty detect list), so it alone
64
+ // proves nothing about the code — a links-only run is still vacuous.
65
+ process.stderr.write('plumbbob: checkride found nothing to check in this repo — refusing to call that green.\n' +
66
+ ' Add tool configs (tsconfig, vitest, …) or a checkride.config.json custom check,\n' +
67
+ ' or set the "check" key in .plumbbob/settings.json to gate through another command.\n');
68
+ return 1;
69
+ }
70
+ if (exitCode !== 0) {
71
+ reportFailingSlots(summary);
72
+ }
73
+ return exitCode;
74
+ }
75
+ // Name the failing slots and where their raw diagnostics landed, so the agent
76
+ // (pb-verify) reads the tool's own JSON instead of scraping scrollback. The
77
+ // summary is the canonical pointer — `output_file` is the adapter's preferred
78
+ // name, but non-JSON output falls back to `.check/<slot>.stdout.txt`.
79
+ function reportFailingSlots(summary) {
80
+ const failing = summary.checks.filter((c) => !c.ok);
81
+ const lines = failing.map((c) => {
82
+ const adapter = c.adapter === null ? '' : ` (${c.adapter})`;
83
+ const output = c.output_file === null ? `.check/${c.name}.stdout.txt` : `.check/${c.output_file}`;
84
+ return ` ✘ ${c.name}${adapter} — raw output: ${output}`;
85
+ });
86
+ process.stderr.write(`plumbbob: failing slots:\n${lines.join('\n')}\n Full report: .check/summary.json\n`);
87
+ }
88
+ function hasFlags(flags) {
89
+ return (flags.bail === true ||
90
+ flags.changed === true ||
91
+ flags.all === true ||
92
+ flags.only !== undefined ||
93
+ flags.skip !== undefined ||
94
+ flags.include !== undefined);
95
+ }
package/dist/lib/git.js CHANGED
@@ -1,7 +1,8 @@
1
- // Thin git wrapper over `node:child_process` (C2: node builtins only, zero
2
- // runtime deps). Functional/procedural, no classes (C1). Plumbbob's git
1
+ // Thin git wrapper over `node:child_process` (C2: node builtins plus the
2
+ // deliberate few). Functional/procedural, no classes (C1). Plumbbob's git
3
3
  // footprint is additive (C5); these helpers only read and locate.
4
4
  import { execFileSync } from 'node:child_process';
5
+ import { isAbsolute, join } from 'node:path';
5
6
  function runGit(root, args) {
6
7
  return execFileSync('git', ['-C', root, ...args], {
7
8
  encoding: 'utf8',
@@ -17,9 +18,13 @@ export function findRepoRoot(cwd) {
17
18
  return null;
18
19
  }
19
20
  }
20
- // Absolute path to the .git directory (handles worktrees/linked git dirs).
21
- export function gitDir(root) {
22
- return runGit(root, ['rev-parse', '--absolute-git-dir']);
21
+ // Absolute path to a file inside the repo's *common* git dir. `--git-path` maps
22
+ // common-dir entries like `info/exclude` to the shared file even from a linked
23
+ // worktree, whose per-worktree gitdir has no `info/` and which git never reads
24
+ // for excludes (D33). The result is relative to `root` unless already absolute.
25
+ export function gitPath(root, relative) {
26
+ const out = runGit(root, ['rev-parse', '--git-path', relative]);
27
+ return isAbsolute(out) ? out : join(root, out);
23
28
  }
24
29
  export function headSha(root) {
25
30
  return runGit(root, ['rev-parse', 'HEAD']);
@@ -33,8 +38,7 @@ export function hasCommit(root) {
33
38
  return false;
34
39
  }
35
40
  }
36
- // Dirty = any tracked change or non-ignored untracked file. The sidecar is
37
- // git-excluded (D17), so an active session never reads as dirty.
41
+ // Dirty = any tracked change or non-ignored untracked file.
38
42
  export function isDirty(root) {
39
43
  return runGit(root, ['status', '--porcelain']).length > 0;
40
44
  }
@@ -43,16 +47,38 @@ export function isDirty(root) {
43
47
  export function stageAll(root) {
44
48
  runGit(root, ['add', '-A']);
45
49
  }
50
+ // Stage a single path (vs `stageAll`'s `-A`): the plan-approval commit stages only
51
+ // the build's artifact folder so the first step's diff can't absorb the plan
52
+ // scaffold (D36). `path` may be absolute or repo-relative — git resolves it against
53
+ // `root`. The `--` guards a path that could look like a flag.
54
+ export function stagePath(root, path) {
55
+ runGit(root, ['add', '--', path]);
56
+ }
46
57
  export function untrackedPaths(root) {
47
58
  const out = runGit(root, ['ls-files', '--others', '--exclude-standard']);
48
59
  return out.length === 0 ? [] : out.split('\n');
49
60
  }
50
61
  // Commit whatever is staged as a checkpoint and return its SHA. --allow-empty so
51
62
  // a step that touched only ignored files still gets a checkpoint to revert to.
52
- export function commit(root, message) {
53
- runGit(root, ['commit', '--allow-empty', '-m', message]);
63
+ // An optional `body` becomes the commit message body: git joins the two `-m`
64
+ // paragraphs with a blank line, so the subject stays the first line (D34).
65
+ export function commit(root, subject, body) {
66
+ const message = body ? ['-m', subject, '-m', body] : ['-m', subject];
67
+ runGit(root, ['commit', '--allow-empty', ...message]);
54
68
  return headSha(root);
55
69
  }
70
+ // The `--stat` summary of what is currently staged (vs HEAD) — the diffstat the
71
+ // deterministic checkpoint body carries (D35). Empty when nothing is staged.
72
+ export function stagedStat(root) {
73
+ return runGit(root, ['diff', '--cached', '--stat']);
74
+ }
75
+ // The repo-relative paths currently staged (vs HEAD) — the set `checkpoint`
76
+ // checks against the step's seam to warn about scope drift. Empty when nothing is
77
+ // staged.
78
+ export function stagedPaths(root) {
79
+ const out = runGit(root, ['diff', '--cached', '--name-only']);
80
+ return out.length === 0 ? [] : out.split('\n');
81
+ }
56
82
  export function resetHard(root, sha) {
57
83
  runGit(root, ['reset', '--hard', sha]);
58
84
  }