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.
- package/.claude-plugin/plugin.json +1 -1
- package/README.md +166 -71
- package/dist/cli-core.js +21 -14
- package/dist/cli.js +1 -1
- package/dist/lib/buildlog.js +2 -2
- package/dist/lib/check.js +89 -20
- package/dist/lib/git.js +35 -9
- package/dist/lib/intent.js +21 -2
- package/dist/lib/orient.js +2 -2
- package/dist/lib/settings.js +71 -0
- package/dist/lib/sidecar.js +142 -32
- package/dist/verbs/build.js +6 -5
- package/dist/verbs/check.js +39 -5
- package/dist/verbs/checkpoint.js +144 -8
- package/dist/verbs/doctor.js +266 -26
- package/dist/verbs/finish.js +101 -0
- package/dist/verbs/revert.js +20 -14
- package/dist/verbs/spike.js +12 -11
- package/dist/verbs/start.js +45 -21
- package/dist/verbs/status.js +18 -7
- package/dist/verbs/use.js +44 -0
- package/hooks/post-edit.sh +9 -3
- package/package.json +12 -11
- package/skills/pb-build/SKILL.md +9 -3
- package/skills/{pb-wrap → pb-finish}/SKILL.md +20 -15
- package/skills/pb-plan/SKILL.md +9 -2
- package/skills/pb-verify/SKILL.md +24 -7
- package/templates/build-log.md +3 -3
- package/templates/intent.md +1 -1
- package/dist/lib/archive.js +0 -72
- package/dist/verbs/wrap.js +0 -55
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "plumbbob",
|
|
3
3
|
"displayName": "PlumbBob",
|
|
4
|
-
"version": "0.
|
|
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
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
for
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
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
|
-
|
|
27
|
-
|
|
28
|
-
|
|
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
|
|
33
|
-
|
|
34
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
##
|
|
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.
|
|
63
|
-
move, and `/pb-status` always names your next one. (
|
|
64
|
-
the
|
|
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 —
|
|
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
|
|
73
|
-
| `/pb-status` | orient — where you are, the next step's done-when
|
|
74
|
-
| `/pb-harvest` | triage parked ideas
|
|
75
|
-
| `/pb-
|
|
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`
|
|
79
|
-
repair a drifted plan) — plus `/pb-doctor` to check
|
|
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
|
|
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
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
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
|
-
|
|
112
|
-
|
|
113
|
-
- [`docs/happy-path.md`](docs/happy-path.md) — one goal walked end to end.
|
|
114
|
-
- [`
|
|
115
|
-
- [`docs/
|
|
116
|
-
- [`docs/
|
|
117
|
-
- [`docs/
|
|
118
|
-
- [`
|
|
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.
|
|
4
|
-
// Functional/procedural: no classes,
|
|
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 {
|
|
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: '
|
|
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
|
|
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
|
-
|
|
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 '
|
|
73
|
-
return
|
|
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
package/dist/lib/buildlog.js
CHANGED
|
@@ -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
|
|
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-
|
|
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 `
|
|
2
|
-
// advance past while red.
|
|
3
|
-
//
|
|
4
|
-
//
|
|
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 {
|
|
7
|
-
import {
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
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
|
-
|
|
18
|
-
return line === undefined ? DEFAULT_CHECK : line.slice('check='.length).trim();
|
|
25
|
+
return runCheckride(root, flags);
|
|
19
26
|
}
|
|
20
|
-
//
|
|
21
|
-
//
|
|
22
|
-
|
|
23
|
-
|
|
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
|
|
2
|
-
//
|
|
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
|
|
21
|
-
|
|
22
|
-
|
|
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.
|
|
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
|
-
|
|
53
|
-
|
|
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
|
}
|