plumbbob 0.13.1 → 0.13.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 +80 -58
- package/dist/cli-core.js +21 -6
- package/dist/lib/buildlog.js +77 -0
- package/dist/lib/git.js +53 -0
- package/dist/lib/latch.js +23 -0
- package/dist/lib/orient.js +601 -58
- package/dist/lib/sidecar.js +111 -3
- package/dist/verbs/build.js +38 -3
- package/dist/verbs/checkpoint.js +40 -13
- package/dist/verbs/handoff.js +452 -46
- package/dist/verbs/harvest.js +224 -0
- package/dist/verbs/relay.js +15 -2
- package/dist/verbs/status.js +64 -17
- package/dist/verbs/turn.js +108 -27
- package/package.json +1 -1
- package/skills/build/SKILL.md +18 -6
- package/skills/finish/SKILL.md +18 -4
- package/skills/harvest/SKILL.md +48 -14
- package/skills/plan/SKILL.md +32 -16
- package/skills/refine/SKILL.md +5 -2
- package/skills/run/SKILL.md +256 -0
- package/skills/status/SKILL.md +4 -2
- package/skills/step/SKILL.md +12 -3
- package/skills/verify/SKILL.md +5 -1
- package/templates/intent.md +12 -8
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "plumbbob",
|
|
3
3
|
"displayName": "PlumbBob",
|
|
4
|
-
"version": "0.13.
|
|
4
|
+
"version": "0.13.3",
|
|
5
5
|
"description": "Attention-first build process — driver skills + a CLI that keep you the decider: guidance on the work, a latch on the record.",
|
|
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
|
@@ -10,11 +10,12 @@
|
|
|
10
10
|
<a href="https://robmclarty.github.io/plumbbob/api.html">API reference</a>
|
|
11
11
|
</p>
|
|
12
12
|
|
|
13
|
-
PlumbBob is a Claude Code plugin (
|
|
13
|
+
PlumbBob is a Claude Code plugin (sixteen skills and a small CLI) that runs
|
|
14
14
|
LLM-assisted coding as a loop you control. You and the model settle the plan in a file
|
|
15
|
-
*before* any code
|
|
16
|
-
|
|
17
|
-
|
|
15
|
+
*before* any code, down to where the build will stop for you; then the model builds it
|
|
16
|
+
one small step at a time, each checked and committed on its own, and **stops where the
|
|
17
|
+
plan says the call is yours**, waiting for you to read what landed and approve it
|
|
18
|
+
before it goes on. It's built for the small-to-medium work that fills most days (a feature, a
|
|
18
19
|
bug, a refactor) where a fully autonomous build is overkill but freestyle prompting
|
|
19
20
|
leaves you dragged behind the model, reacting instead of deciding.
|
|
20
21
|
|
|
@@ -32,8 +33,9 @@ PlumbBob itself, running its own loop.
|
|
|
32
33
|
## Who it's for
|
|
33
34
|
|
|
34
35
|
- **You already build with a coding agent and end the day reviewing a wall of diff.**
|
|
35
|
-
PlumbBob turns that wall into steps
|
|
36
|
-
read as an
|
|
36
|
+
PlumbBob turns that wall into small steps, each its own commit, and stops for your
|
|
37
|
+
review where the plan says one is due, with every step small enough to read as an
|
|
38
|
+
editor.
|
|
37
39
|
- **You are skeptical of AI-written code.** Nothing lands without a green check, a
|
|
38
40
|
self-review against the plan, and your say-so, and the CLI refuses to record a step
|
|
39
41
|
the model approved for itself.
|
|
@@ -47,26 +49,31 @@ PlumbBob itself, running its own loop.
|
|
|
47
49
|
## The loop in one picture
|
|
48
50
|
|
|
49
51
|
<p align="center">
|
|
50
|
-
<img src="
|
|
52
|
+
<img src="site/media/loop-one-step.svg" alt="One step of the loop, drawn as a wall going up course by course: on the scaffold a person reads a plumb line at the corner, and down the wall a mechanical arm holds the next stone and waits" width="760">
|
|
51
53
|
</p>
|
|
52
54
|
|
|
53
55
|
Everything the model must stay true to lives in one file, the build's `intent.md`
|
|
54
56
|
(under `.plumbbob/builds/<slug>/`), written before any code: the **Frame** (the problem, what done looks like, what you're
|
|
55
57
|
explicitly *not* doing), the **Decisions** and **Constraints** (the settled calls, each
|
|
56
|
-
with its *because*),
|
|
58
|
+
with its *because*), the **Steps**: a numbered list where every step carries a
|
|
57
59
|
**done-when** (a checkable finish line, ideally a test) and a **seam** (the files it's
|
|
58
|
-
expected to touch)
|
|
60
|
+
expected to touch), and the **Build ranges**: where the build stops for your call, before
|
|
61
|
+
a decision the plan left open or after a stretch of steps worth reviewing.
|
|
59
62
|
|
|
60
63
|
```text
|
|
61
|
-
/plumbbob:plan decide everything on paper
|
|
62
|
-
per
|
|
63
|
-
/plumbbob:
|
|
64
|
-
|
|
65
|
-
|
|
64
|
+
/plumbbob:plan decide everything on paper: frame, decisions, steps, ranges (once)
|
|
65
|
+
per range:
|
|
66
|
+
/plumbbob:run each step: implement → check → self-review → checkpoint
|
|
67
|
+
then STOP where the plan says: a step's pause, or the stop card
|
|
68
|
+
(one reply) → settle the stop; fire /plumbbob:run again
|
|
69
|
+
/plumbbob:finish report what shipped and why, final commit, clear (once)
|
|
66
70
|
```
|
|
67
71
|
|
|
68
|
-
|
|
69
|
-
|
|
72
|
+
`/plumbbob:build` is the same loop a step at a time, stopping at every step's pause, for
|
|
73
|
+
a plan without ranges or a stretch you want to walk slowly. Nothing is locked and nothing
|
|
74
|
+
refuses you: the loop does the labor, pulls up to the line, and waits for you to advance
|
|
75
|
+
it. **You are the clock.** [`docs/runs.md`](docs/runs.md) walks one plan through its runs
|
|
76
|
+
and then drives it by hand, the two side by side.
|
|
70
77
|
|
|
71
78
|
## What a prompt can't replicate
|
|
72
79
|
|
|
@@ -132,19 +139,22 @@ In Claude Code, inside any git repo with a clean tree:
|
|
|
132
139
|
1. **Plan.** Fire `/plumbbob:plan` and give it whatever you have: nothing (it interviews
|
|
133
140
|
you), a rough line (`/plumbbob:plan rate-limit POST /login, 5/min/IP, return 429`), or a
|
|
134
141
|
path to a spec file. Together you fill the build's `intent.md`: the steps, and the
|
|
135
|
-
build ranges that say where the build stops for your call.
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
142
|
+
build ranges that say where the build stops for your call. For a three-step rate
|
|
143
|
+
limiter that might be step 1 alone, to settle how the bucket refills, then steps 2
|
|
144
|
+
and 3 together. No code is written yet.
|
|
145
|
+
2. **Run.** Fire `/plumbbob:run`. It runs the range holding the next step, here step 1
|
|
146
|
+
alone, so it implements that step, runs the heavy check gate, reviews its own diff
|
|
147
|
+
against the plan, and stops at the step's pause, where the plan reserved your call.
|
|
148
|
+
What it stops on is one block with a fixed shape (the
|
|
149
|
+
[turn anatomy](docs/presentation.md)): the CLI measures the check, the seam, and the
|
|
150
|
+
diff, the model supplies only the judgment, and nothing is written around it.
|
|
141
151
|
|
|
142
152
|
````markdown
|
|
143
153
|
**Now**: Step 1 of 3 - feat(limiter): add a token-bucket limiter
|
|
144
154
|
|
|
145
|
-
**Summary**: A per-IP token bucket now refuses the 6th attempt inside a minute. (details:
|
|
155
|
+
**Summary**: A per-IP token bucket now refuses the 6th attempt inside a minute. (details: [detail.md](.plumbbob/detail.md))
|
|
146
156
|
|
|
147
|
-
1. The bucket lives in memory, keyed by IP, refilling five tokens a minute.
|
|
157
|
+
1. The bucket lives in memory, keyed by IP, refilling on read: five tokens a minute.
|
|
148
158
|
2. The clock is injected, so the test advances time instead of sleeping on it.
|
|
149
159
|
|
|
150
160
|
**Readout**:
|
|
@@ -161,7 +171,8 @@ In Claude Code, inside any git repo with a clean tree:
|
|
|
161
171
|
|
|
162
172
|
**Verdict**: ● Plumb
|
|
163
173
|
|
|
164
|
-
**Next Up**: Step 2 of 3 - feat(login): wire the limiter into POST /login (model: **Sonnet**, details:
|
|
174
|
+
**Next Up**: Step 2 of 3 - feat(login): wire the limiter into POST /login (model: **Sonnet**, details: [intent.md:28](.plumbbob/builds/2026-07-03-rate-limit-the-login-endpoint/intent.md#L28))
|
|
175
|
+
· range 1 of 2 lands with step 1
|
|
165
176
|
|
|
166
177
|
**Your Call**:
|
|
167
178
|
|
|
@@ -170,10 +181,10 @@ In Claude Code, inside any git repo with a clean tree:
|
|
|
170
181
|
- anything that reads as direction → I take it as what to change; nothing lands until you approve
|
|
171
182
|
- `revert` → I wind the work back to the last checkpoint
|
|
172
183
|
|
|
173
|
-
**Recommendation**: Approve it. The gate is green
|
|
184
|
+
**Recommendation**: Approve it, refilling on read. The gate is green and the seam held, and a bucket that refills when it is read needs no timer for the route to clean up.
|
|
174
185
|
````
|
|
175
186
|
|
|
176
|
-

|
|
177
188
|
|
|
178
189
|
(The check gate is [checkride](https://www.npmjs.com/package/checkride), one
|
|
179
190
|
run across the tools your repo already configures: types, lint, tests, dead
|
|
@@ -181,36 +192,43 @@ In Claude Code, inside any git repo with a clean tree:
|
|
|
181
192
|
`.plumbbob/settings.json`, for example `"check": "npm test"`; in a repo where
|
|
182
193
|
checkride finds no tools at all, `start` says so up front and names that key.)
|
|
183
194
|
3. **Reply.** Say `looks good` and it commits the step as a checkpoint, marks it done,
|
|
184
|
-
and returns to the boundary
|
|
195
|
+
and returns to the boundary, where the line beneath Next Up names the run to type
|
|
196
|
+
next and the steps it will land:
|
|
185
197
|
|
|
186
198
|
```text
|
|
187
|
-
**Checkpoint**: Step 1 complete (a1b2c3d4e, details:
|
|
199
|
+
**Checkpoint**: Step 1 complete (a1b2c3d4e, details: [build-log.md:70](.plumbbob/builds/2026-07-03-rate-limit-the-login-endpoint/build-log.md#L70))
|
|
188
200
|
|
|
189
201
|
**Verdict**: ● Plumb
|
|
190
202
|
|
|
191
|
-
**Next Up**: Step 2 of 3 - feat(login): wire the limiter into POST /login (model: **Sonnet**, details:
|
|
203
|
+
**Next Up**: Step 2 of 3 - feat(login): wire the limiter into POST /login (model: **Sonnet**, details: [intent.md:28](.plumbbob/builds/2026-07-03-rate-limit-the-login-endpoint/intent.md#L28))
|
|
204
|
+
· range 2 of 2: /plumbbob:run (steps 2-3)
|
|
192
205
|
```
|
|
193
206
|
|
|
194
|
-

|
|
195
208
|
|
|
196
209
|
Ask instead (`expand 2`, or any question) and it shows more of what is there and
|
|
197
210
|
changes nothing. The answer closes on one line pointing back at the step, not on the
|
|
198
211
|
whole block again, since you have already read it; say `show the pause` to see the
|
|
199
212
|
block again. Direct it and it takes that as the fix, and nothing lands until you say
|
|
200
|
-
`looks good`.
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
213
|
+
`looks good`.
|
|
214
|
+
4. **Run again.** Fire `/plumbbob:run` again; re-firing it *is* the clock tick. Range 2
|
|
215
|
+
lands steps 2 and 3, each on a green check with no pause between them, and ends on the
|
|
216
|
+
**stop card**: the range's review (a summary of both steps, a readout measured across
|
|
217
|
+
them, the Verdict), with every idea parked along the way listed under `Harvested` and
|
|
218
|
+
a lean proposed for each. One reply settles it: `looks good` records the review and
|
|
219
|
+
every lean, and `make P1 a blocker` changes one first. `/plumbbob:build` stays the
|
|
220
|
+
granular move whenever you want it, one step to its pause. Whenever you lose the
|
|
221
|
+
thread, `/plumbbob:status` shows where you are and names the next move.
|
|
222
|
+
5. **Finish.** When the last range lands, answer its stop card with `/plumbbob:finish`: it
|
|
223
|
+
records the card, writes a report of what shipped and why into the build's tracked
|
|
224
|
+
folder, makes the final commit, and clears the slate for the next goal. The folder
|
|
225
|
+
rides the branch into the PR: the build's record merges into `main` instead of dying
|
|
226
|
+
with the worktree.
|
|
209
227
|
|
|
210
228
|
A mid-build "ooh, what if…" never derails the step: `/plumbbob:park` captures it to a list
|
|
211
|
-
in one line, and
|
|
212
|
-
|
|
213
|
-
[`docs/happy-path.md`](docs/happy-path.md).
|
|
229
|
+
in one line, and the next stop card proposes a class for it, settled with the rest of the
|
|
230
|
+
review (`/plumbbob:harvest` triages the list anywhere else). One goal walked end to end,
|
|
231
|
+
with the real output at every stage, is in [`docs/happy-path.md`](docs/happy-path.md).
|
|
214
232
|
|
|
215
233
|
## The skills
|
|
216
234
|
|
|
@@ -218,16 +236,17 @@ You drive the whole loop from your IDE with `/plumbbob:*` skills: no step number
|
|
|
218
236
|
remember, no raw CLI to type. Every skill is `disable-model-invocation`, so *you* fire
|
|
219
237
|
every move, and `/plumbbob:status` always names your next one. (Claude Code namespaces
|
|
220
238
|
them under the plugin, so the command is `/plumbbob:plan`. The bare `/plan` reaches it
|
|
221
|
-
too, but only where nothing else owns that name, and
|
|
239
|
+
too, but only where nothing else owns that name, and five of the sixteen share one with
|
|
222
240
|
a Claude Code built-in, so these docs write the full form.)
|
|
223
241
|
|
|
224
|
-
**The happy path**:
|
|
225
|
-
|
|
242
|
+
**The happy path**: plan, run each range, finish; many sessions need nothing else, and
|
|
243
|
+
build is the same loop a step at a time:
|
|
226
244
|
|
|
227
245
|
| Skill <img alt="" width="110" height="1"> | Does |
|
|
228
246
|
| ------------------------------------ | ------ |
|
|
229
247
|
| `/plumbbob:plan` | plan the whole goal: open the session and author intent's Frame, Decisions, Constraints, **and all Steps**, with the build ranges that say where the build stops for you |
|
|
230
|
-
| `/plumbbob:
|
|
248
|
+
| `/plumbbob:run` | run what is left of the range holding the next step (or range N) and stop where the plan says: at a step's pause, or on the stop card that reviews what landed and settles every parked idea in one reply |
|
|
249
|
+
| `/plumbbob:build` | the granular move: implement the next planned step, then verify it to the pause; `--auto` self-approves and chains to done, and a range like `1-3`, or a list like `20,5-9`, self-approves exactly the steps it names, then stops |
|
|
231
250
|
| `/plumbbob:finish` | finish up: write the report, make the final commit, clear for a fresh goal |
|
|
232
251
|
|
|
233
252
|
**Plan-shaping moves**: optional, for when the plan needs work mid-flight:
|
|
@@ -236,6 +255,7 @@ nothing else:
|
|
|
236
255
|
| ------------------------------------ | ------ |
|
|
237
256
|
| `/plumbbob:step` | revise/sharpen the next step (empty input auto-syncs it to reality) |
|
|
238
257
|
| `/plumbbob:refine` | attack the frame for holes, or repair a drifted plan (`ranges` re-cuts the build ranges) |
|
|
258
|
+
| `/plumbbob:order` | set the build order: the undone steps in the sequence to build them |
|
|
239
259
|
| `/plumbbob:spike` | throwaway worktree experiment for a fork the plan can't settle |
|
|
240
260
|
|
|
241
261
|
**Helpers**: orient, verify, recover, diagnose:
|
|
@@ -254,16 +274,16 @@ nothing else:
|
|
|
254
274
|
| Skill <img alt="" width="110" height="1"> | Does |
|
|
255
275
|
| ------------------------------------ | ------ |
|
|
256
276
|
| `/plumbbob:park` | capture a mid-build idea without chasing it |
|
|
257
|
-
| `/plumbbob:harvest` | triage parked ideas between steps (blocker / tangent / pivot) |
|
|
277
|
+
| `/plumbbob:harvest` | triage parked ideas between steps (blocker / tangent / pivot); a run's stop card carries its own |
|
|
258
278
|
|
|
259
|
-
All
|
|
279
|
+
All sixteen, with inputs and effects, are in
|
|
260
280
|
[`docs/skills-reference.md`](docs/skills-reference.md).
|
|
261
281
|
|
|
262
282
|
Under the skills ships a lean `plumbbob` CLI (the mechanical verbs the
|
|
263
283
|
skills shell out to), four hooks (a non-blocking post-edit lint, a `git commit`
|
|
264
284
|
ask-hook while a step is in flight, the per-prompt turn ledger the approval latch
|
|
265
|
-
reads, and a check at each turn's end that the turn
|
|
266
|
-
printed), and a `.plumbbob/` sidecar you can open and edit by hand at any time: a tracked
|
|
285
|
+
reads, which also mints a run's grant, and a check at each turn's end that the turn
|
|
286
|
+
relayed the ending plumbbob printed), and a `.plumbbob/` sidecar you can open and edit by hand at any time: a tracked
|
|
267
287
|
`builds/<slug>/` folder per build (`intent.md`, `build-log.md`, `checkpoints`, `report.md`)
|
|
268
288
|
that rides the branch into the PR, plus an untracked control plane (`settings.local.json`,
|
|
269
289
|
the session sentinel, the in-flight markers).
|
|
@@ -297,9 +317,9 @@ The questions a skeptic should ask, answered straight.
|
|
|
297
317
|
scoped to one session. The plan lives in the chat, so it fades with the context
|
|
298
318
|
window, and the approval gates the *start* of the work, not each increment of it.
|
|
299
319
|
PlumbBob puts the plan in a file in your repo that survives compaction, restarts,
|
|
300
|
-
and next week; moves the approval to **
|
|
301
|
-
|
|
302
|
-
step, not the afternoon; and records the *whys* alongside the decisions, so the
|
|
320
|
+
and next week; moves the approval to **the stops the plan names**, down to every step
|
|
321
|
+
when you want it, with each step checked, reviewed against the plan, and committed as
|
|
322
|
+
its own checkpoint, so a wrong turn costs one step, not the afternoon; and records the *whys* alongside the decisions, so the
|
|
303
323
|
archive can still answer "what did we decide, and why" long after the session is
|
|
304
324
|
gone. The two compose fine: plan mode (or anything else) can produce a step's
|
|
305
325
|
diff, and `/plumbbob:verify` reads it like any other.
|
|
@@ -316,7 +336,7 @@ modes, spec kits, native checkpoints), and that convergence is evidence *for* th
|
|
|
316
336
|
premise. PlumbBob is deliberately thin against that future: the artifacts are
|
|
317
337
|
plain markdown and ordinary git commits in your own repo, the CLI is lean
|
|
318
338
|
(node builtins plus one deliberate dependency), and nothing locks you in. The
|
|
319
|
-
investment is small (
|
|
339
|
+
investment is small (sixteen
|
|
320
340
|
skills over a plain-markdown sidecar, learnable in an afternoon), and what you're
|
|
321
341
|
actually learning is the method: decisions before code, one verified step at a
|
|
322
342
|
time, capture instead of chase. That transfers to whatever tool wins. If
|
|
@@ -341,19 +361,21 @@ that matters.
|
|
|
341
361
|
> under [`docs/evals/`](docs/evals/): the route the latch owns (*no checkpoint over a red
|
|
342
362
|
> check under pressure*) went from **2/5 prose-only to 5/5 latched** on the first sweep
|
|
343
363
|
> ([2026-07-11](docs/evals/2026-07-11.md)), every sweep since has held it and closed the
|
|
344
|
-
> two gaps that one surfaced,
|
|
345
|
-
>
|
|
364
|
+
> two gaps that one surfaced, [2026-09-17](docs/evals/2026-09-17.md) began scoring the
|
|
365
|
+
> shape of the turn itself, and the latest ([2026-10-19](docs/evals/2026-10-19.md)) holds a
|
|
366
|
+
> run to its stop card, 5/5.
|
|
346
367
|
|
|
347
368
|
## Documentation
|
|
348
369
|
|
|
349
370
|
Each doc answers one question (in rough reading order for a new user):
|
|
350
371
|
|
|
351
372
|
- *What does a session actually look like?* → [`docs/happy-path.md`](docs/happy-path.md): one goal walked end to end; read this first.
|
|
373
|
+
- *How do I drive a plan with runs, and what do they replace?* → [`docs/runs.md`](docs/runs.md): ranges, the build order, and the stop card's harvest, walked through three runs and set beside the manual build, harvest, and order moves.
|
|
352
374
|
- *What do the words mean?* → [`docs/glossary.md`](docs/glossary.md): every term the loop mints, one line each, with where to read more.
|
|
353
375
|
- *Show me the artifacts it leaves behind.* → [`examples/`](examples/): that same session's finished build folder, file by file.
|
|
354
376
|
- *Should I / can I / what about…?* → [`docs/faq.md`](docs/faq.md): the adoption questions, answered straight.
|
|
355
377
|
- *What is each method for?* → [`docs/techniques.md`](docs/techniques.md): steps, seams, the pause, park/harvest, spikes.
|
|
356
|
-
- *What does each skill do?* → [`docs/skills-reference.md`](docs/skills-reference.md): all
|
|
378
|
+
- *What does each skill do?* → [`docs/skills-reference.md`](docs/skills-reference.md): all sixteen skills: inputs, effects, when to reach for each.
|
|
357
379
|
- *What does a turn look like, and who renders which part?* → [`docs/presentation.md`](docs/presentation.md): the anatomy of the pause, the boundary, and the driver turns.
|
|
358
380
|
- *How do I plug in my own agent?* → [`docs/agents.md`](docs/agents.md): the subprocess envelope, the manifest, `harness.json`, and working examples (including a local-model reviewer via Ollama).
|
|
359
381
|
- *How do I get a local model reviewing my steps?* → [`docs/local-model-review.md`](docs/local-model-review.md): the ollama-reviewer example walked end to end, install to every-pause review.
|
package/dist/cli-core.js
CHANGED
|
@@ -14,6 +14,7 @@ import { fileURLToPath } from 'node:url';
|
|
|
14
14
|
import { start } from "./verbs/start.js";
|
|
15
15
|
import { status } from "./verbs/status.js";
|
|
16
16
|
import { park } from "./verbs/park.js";
|
|
17
|
+
import { harvest } from "./verbs/harvest.js";
|
|
17
18
|
import { order } from "./verbs/order.js";
|
|
18
19
|
import { build } from "./verbs/build.js";
|
|
19
20
|
import { handoff } from "./verbs/handoff.js";
|
|
@@ -60,7 +61,7 @@ const VERBS = [
|
|
|
60
61
|
{
|
|
61
62
|
name: 'status',
|
|
62
63
|
description: 'print the orientation dashboard, or NO ACTIVE SESSION',
|
|
63
|
-
synopsis: ['status [--build <slug>] [--invoked "<args>"]'],
|
|
64
|
+
synopsis: ['status [--build <slug>] [--invoked "<args>"] [--run "<args>"]'],
|
|
64
65
|
flags: [
|
|
65
66
|
BUILD_FLAG,
|
|
66
67
|
{
|
|
@@ -68,6 +69,11 @@ const VERBS = [
|
|
|
68
69
|
value: '"<args>"',
|
|
69
70
|
gloss: 'the raw skill invocation; an explicit step number in it repoints the dashboard at that step',
|
|
70
71
|
},
|
|
72
|
+
{
|
|
73
|
+
name: '--run',
|
|
74
|
+
value: '"<args>"',
|
|
75
|
+
gloss: 'the raw /plumbbob:run invocation; prints the run it names above the dashboard, or the refusal as its pointer',
|
|
76
|
+
},
|
|
71
77
|
],
|
|
72
78
|
notes: 'Read-only; always exits 0. With no cursor it lists the available builds.',
|
|
73
79
|
},
|
|
@@ -77,20 +83,21 @@ const VERBS = [
|
|
|
77
83
|
synopsis: ['build [<n>] [--build <slug>]'],
|
|
78
84
|
args: [{ name: '<n>', gloss: 'step to build (default: the next undone step)' }],
|
|
79
85
|
flags: [BUILD_FLAG],
|
|
80
|
-
notes:
|
|
86
|
+
notes: "Refuses (exit 1) with no session, a non-numeric or < 1 step, a seam it cannot parse, or a stop card still waiting for its reply, since the step's pause would write over the range's review; the remedy names the reply that records the card.",
|
|
81
87
|
},
|
|
82
88
|
{
|
|
83
89
|
name: 'handoff',
|
|
84
|
-
description: 'print the footer card that ends a turn (pause, boundary, plan, or
|
|
85
|
-
synopsis: ['handoff [<n>] [--full] [--plan] [--driver] [--build <slug>]'],
|
|
90
|
+
description: 'print the footer card that ends a turn (pause, boundary, plan, driver, or stop), or a reminder for a pause or a stop already shown',
|
|
91
|
+
synopsis: ['handoff [<n>] [--full] [--plan] [--driver] [--stop] [--build <slug>]'],
|
|
86
92
|
args: [{ name: '<n>', gloss: 'step to report on (default: the in-flight step, else the last checkpointed one)' }],
|
|
87
93
|
flags: [
|
|
88
|
-
{ name: '--full', gloss: 'print the whole pause card even when
|
|
94
|
+
{ name: '--full', gloss: 'print the whole pause card, or with --stop the whole stop card, even when it was already shown' },
|
|
89
95
|
{ name: '--plan', gloss: 'render the plan-pause card instead of a step tier' },
|
|
90
96
|
{ name: '--driver', gloss: "render a driver turn's pointer back at the open step" },
|
|
97
|
+
{ name: '--stop', gloss: "render the stop card for the range the last landing completed, from the range's detail file" },
|
|
91
98
|
BUILD_FLAG,
|
|
92
99
|
],
|
|
93
|
-
notes:
|
|
100
|
+
notes: "Writes only the detail file (a blank line around each heading), the PAUSE stamp, and with --stop the STOP stamp; refuses (exit 1) with no session, with two of --plan, --driver, and --stop together, and with --stop when a step is in flight, no range that lands its steps just landed, or the detail file is not that range's.",
|
|
94
101
|
},
|
|
95
102
|
{
|
|
96
103
|
name: 'check',
|
|
@@ -156,6 +163,12 @@ const VERBS = [
|
|
|
156
163
|
flags: [{ name: '--reset', gloss: 'drop the line, so the plan reads in document order again' }],
|
|
157
164
|
notes: 'Writes the one numeric line under `## Build order` in intent.md, which status, build, checkpoint, and handoff all read. A span is spelled out on the line. Refuses (exit 1) with no session, no numbers, a span that runs backward, a number the plan lacks, a repeat, or a step already checkpointed.',
|
|
158
165
|
},
|
|
166
|
+
{
|
|
167
|
+
name: 'harvest',
|
|
168
|
+
description: "record a harvest from the detail file, and at a run's stop the range's review",
|
|
169
|
+
synopsis: ['harvest'],
|
|
170
|
+
notes: "Reads the detail file's `## Harvest` proposals (`- park 6 → tangent: <reason>`), flips each settled park in build-log.md, appends the dated Harvest lines, and, when the detail is a range's, writes the range's review beneath a dated Log line; then clears the detail file and the STOP stamp, and commits nothing. Refuses (exit 1) with no session, a detail file that is no range's or harvest's, a range's detail in the turn its stop card printed, one no stop card showed, one whose range is open again, or one whose range a step joined after it landed, and a proposal naming a park the list lacks or a class other than blocker, tangent, or pivot.",
|
|
171
|
+
},
|
|
159
172
|
{
|
|
160
173
|
name: 'spike',
|
|
161
174
|
description: 'worktree experiment + report for a genuine fork',
|
|
@@ -365,6 +378,8 @@ async function dispatch(verb, cwd, rest) {
|
|
|
365
378
|
return park(cwd, rest);
|
|
366
379
|
case 'order':
|
|
367
380
|
return order(cwd, rest);
|
|
381
|
+
case 'harvest':
|
|
382
|
+
return harvest(cwd, rest);
|
|
368
383
|
case 'build':
|
|
369
384
|
return build(cwd, rest);
|
|
370
385
|
case 'handoff':
|
package/dist/lib/buildlog.js
CHANGED
|
@@ -150,3 +150,80 @@ export function abandonLogLine(date, step, title) {
|
|
|
150
150
|
const head = `- ${date} — step ${step} abandoned · work kept in tree`;
|
|
151
151
|
return title !== null && title.length > 0 ? `${head} — ${title}` : head;
|
|
152
152
|
}
|
|
153
|
+
/**
|
|
154
|
+
* Flip the parks named by `handles` from `- [ ]` to `- [x]` under `## Park
|
|
155
|
+
* list`, where a park's handle is its place in the list, counting every line
|
|
156
|
+
* ever parked, the same lines `parseParkTotals` counts. A handle already
|
|
157
|
+
* flipped, or past the end of the list, is left as it is.
|
|
158
|
+
*
|
|
159
|
+
* Returns null when the section is absent so the caller can report rather than
|
|
160
|
+
* silently corrupt the doc.
|
|
161
|
+
*/
|
|
162
|
+
export function flipParks(content, handles) {
|
|
163
|
+
const lines = content.split('\n');
|
|
164
|
+
const headingIdx = lines.findIndex((l) => l.trim() === '## Park list');
|
|
165
|
+
if (headingIdx === -1) {
|
|
166
|
+
return null;
|
|
167
|
+
}
|
|
168
|
+
let handle = 0;
|
|
169
|
+
for (let i = headingIdx + 1; i < lines.length && !(lines[i] ?? '').startsWith('## '); i++) {
|
|
170
|
+
const line = lines[i] ?? '';
|
|
171
|
+
if (!/^-\s+\[[ xX]\]\s+\S/.test(line.trim())) {
|
|
172
|
+
continue;
|
|
173
|
+
}
|
|
174
|
+
handle += 1;
|
|
175
|
+
if (handles.includes(handle)) {
|
|
176
|
+
lines[i] = line.replace(/\[ \]/, '[x]');
|
|
177
|
+
}
|
|
178
|
+
}
|
|
179
|
+
return lines.join('\n');
|
|
180
|
+
}
|
|
181
|
+
/**
|
|
182
|
+
* The dated lines a harvest appends under `## Harvest`: one saying where it
|
|
183
|
+
* ran and how many parks it settled, then one per park, its class in bold,
|
|
184
|
+
* then its handle and text, and the reason behind the arrow the card showed it
|
|
185
|
+
* with. Empty when nothing was settled.
|
|
186
|
+
*/
|
|
187
|
+
export function harvestLines(date, where, settled) {
|
|
188
|
+
if (settled.length === 0) {
|
|
189
|
+
return [];
|
|
190
|
+
}
|
|
191
|
+
const count = `${settled.length} parked idea${settled.length === 1 ? '' : 's'}`;
|
|
192
|
+
return [
|
|
193
|
+
`- ${date}, ${where}: ${count}, classified.`,
|
|
194
|
+
...settled.map((p) => `- **${p.kind}**: park ${p.handle} · ${p.text}${p.reason.length === 0 ? '' : ` → ${p.reason}`}`),
|
|
195
|
+
];
|
|
196
|
+
}
|
|
197
|
+
/**
|
|
198
|
+
* Append the harvest's lines to `## Harvest`, dropping the template's
|
|
199
|
+
* `- (none yet)` placeholder on the first one. Returns null when the section
|
|
200
|
+
* is absent.
|
|
201
|
+
*/
|
|
202
|
+
export function appendHarvest(content, lines) {
|
|
203
|
+
const all = content.split('\n');
|
|
204
|
+
const headingIdx = all.findIndex((l) => /^## Harvest\b/.test(l.trim()));
|
|
205
|
+
if (headingIdx === -1) {
|
|
206
|
+
return null;
|
|
207
|
+
}
|
|
208
|
+
const heading = (all[headingIdx] ?? '').trim().slice(3);
|
|
209
|
+
const end = all.findIndex((l, i) => i > headingIdx && l.startsWith('## '));
|
|
210
|
+
const placeholder = all.findIndex((l, i) => i > headingIdx && (end === -1 || i < end) && l.trim() === '- (none yet)');
|
|
211
|
+
if (placeholder !== -1 && lines.length > 0) {
|
|
212
|
+
// The blank above it goes too, since the first line appended brings its own.
|
|
213
|
+
const blank = (all[placeholder - 1] ?? '').trim() === '' ? 1 : 0;
|
|
214
|
+
all.splice(placeholder - blank, 1 + blank);
|
|
215
|
+
}
|
|
216
|
+
let updated = all.join('\n');
|
|
217
|
+
for (const line of lines) {
|
|
218
|
+
updated = updated === null ? null : appendToSection(updated, heading, line);
|
|
219
|
+
}
|
|
220
|
+
return updated;
|
|
221
|
+
}
|
|
222
|
+
/**
|
|
223
|
+
* The Log line `plumbbob harvest` writes for a stop it recorded: dated, the
|
|
224
|
+
* range over the plan's count, and the stop as the plan wrote it. The range's
|
|
225
|
+
* review rides beneath it, as a step's pause rides beneath its checkpoint.
|
|
226
|
+
*/
|
|
227
|
+
export function stopLogLine(date, range, count, stop) {
|
|
228
|
+
return `- ${date} — range ${range} of ${count}'s stop recorded — ${stop}`;
|
|
229
|
+
}
|
package/dist/lib/git.js
CHANGED
|
@@ -136,6 +136,36 @@ function trackedNumstat(root) {
|
|
|
136
136
|
catch {
|
|
137
137
|
return [];
|
|
138
138
|
}
|
|
139
|
+
return numstatLines(out);
|
|
140
|
+
}
|
|
141
|
+
/**
|
|
142
|
+
* One commit's product, one entry per file: what it changed against its
|
|
143
|
+
* parent, or against the empty tree for a root commit.
|
|
144
|
+
*
|
|
145
|
+
* The stop card's range readout reads this for each checkpoint the range
|
|
146
|
+
* landed, where the pause reads `diffNumstat` for the working tree: a landed
|
|
147
|
+
* step's work is its commit, and the tree is clean by then. `--no-renames`
|
|
148
|
+
* keeps a moved file as two plain paths, the shape the seam row wants, as it
|
|
149
|
+
* does at the pause.
|
|
150
|
+
*
|
|
151
|
+
* Information, never a gate: best-effort and never throws ([] for a sha the
|
|
152
|
+
* repo does not know).
|
|
153
|
+
*/
|
|
154
|
+
export function commitNumstat(root, sha) {
|
|
155
|
+
let out;
|
|
156
|
+
try {
|
|
157
|
+
out = runGit(root, ['diff-tree', '--no-commit-id', '--numstat', '--no-renames', '-r', '--root', sha]);
|
|
158
|
+
}
|
|
159
|
+
catch {
|
|
160
|
+
return [];
|
|
161
|
+
}
|
|
162
|
+
return numstatLines(out);
|
|
163
|
+
}
|
|
164
|
+
/**
|
|
165
|
+
* Numstat output as entries: `-` (a binary file) counts 0, and every path is
|
|
166
|
+
* one git has a blob for.
|
|
167
|
+
*/
|
|
168
|
+
function numstatLines(out) {
|
|
139
169
|
if (out.length === 0) {
|
|
140
170
|
return [];
|
|
141
171
|
}
|
|
@@ -144,6 +174,29 @@ function trackedNumstat(root) {
|
|
|
144
174
|
return { added: toCount(added), removed: toCount(removed), path: path.join('\t'), untracked: false };
|
|
145
175
|
});
|
|
146
176
|
}
|
|
177
|
+
/**
|
|
178
|
+
* The commits on the first-parent line from `from` (exclusive) to `to`
|
|
179
|
+
* (inclusive), newest first, leaving out those whose message matches
|
|
180
|
+
* `excluding`.
|
|
181
|
+
*
|
|
182
|
+
* The stop card counts the commits outside the ledger across a range with
|
|
183
|
+
* this, the way `commitsSince` counts them since the last checkpoint: the
|
|
184
|
+
* caller takes the ledger's own shas out of the list, and passes
|
|
185
|
+
* `PLAN_COMMIT_MARKER` so a hand-made plan commit never reads as out-of-band.
|
|
186
|
+
*
|
|
187
|
+
* Best-effort and never throws: [] for an empty span or a sha the repo does
|
|
188
|
+
* not know.
|
|
189
|
+
*/
|
|
190
|
+
export function commitsBetween(root, from, to, excluding) {
|
|
191
|
+
try {
|
|
192
|
+
const skip = excluding === undefined ? [] : ['--grep', excluding, '--invert-grep'];
|
|
193
|
+
const out = runGit(root, ['rev-list', '--first-parent', ...skip, `${from}..${to}`]);
|
|
194
|
+
return out.length === 0 ? [] : out.split('\n');
|
|
195
|
+
}
|
|
196
|
+
catch {
|
|
197
|
+
return [];
|
|
198
|
+
}
|
|
199
|
+
}
|
|
147
200
|
/**
|
|
148
201
|
* The untracked half of `diffNumstat`: every non-ignored new file, counted as
|
|
149
202
|
* all-added lines.
|
package/dist/lib/latch.js
CHANGED
|
@@ -127,6 +127,29 @@ export function checkLatch(root, step) {
|
|
|
127
127
|
settingsAuto: resolveBoolean(root, 'auto', false),
|
|
128
128
|
});
|
|
129
129
|
}
|
|
130
|
+
/**
|
|
131
|
+
* Whether a stop's record may land: a human at the keyboard, a dormant ledger,
|
|
132
|
+
* or a human turn since the stop card printed.
|
|
133
|
+
*
|
|
134
|
+
* No grant opens it. A run's long turn ends on proposals, which is where a
|
|
135
|
+
* model would carry straight on into recording its own, so neither a typed
|
|
136
|
+
* range nor `--auto` stands in for the human reading the card; the reply that
|
|
137
|
+
* settles it is always a turn after the one the card printed in.
|
|
138
|
+
*/
|
|
139
|
+
export function evaluateStopLatch(input) {
|
|
140
|
+
if (input.isTTY)
|
|
141
|
+
return true;
|
|
142
|
+
if (input.turn === null || input.printed === null)
|
|
143
|
+
return true;
|
|
144
|
+
return input.turn > input.printed;
|
|
145
|
+
}
|
|
146
|
+
/**
|
|
147
|
+
* Gather the stop latch's inputs from the worktree and evaluate, for the stop
|
|
148
|
+
* card stamped as printed in turn `printed`.
|
|
149
|
+
*/
|
|
150
|
+
export function checkStopLatch(root, printed) {
|
|
151
|
+
return evaluateStopLatch({ isTTY: process.stdin.isTTY === true, turn: readCount(turnPath(root)), printed });
|
|
152
|
+
}
|
|
130
153
|
/**
|
|
131
154
|
* The current GRANT, or null when the file is absent or unparseable.
|
|
132
155
|
*/
|