plumbbob 0.13.0 → 0.13.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.claude-plugin/plugin.json +1 -1
- package/README.md +75 -54
- package/dist/cli-core.js +29 -9
- package/dist/lib/buildlog.js +77 -0
- package/dist/lib/git.js +53 -0
- package/dist/lib/latch.js +46 -5
- package/dist/lib/notice.js +3 -2
- package/dist/lib/orient.js +878 -61
- package/dist/lib/sidecar.js +164 -3
- package/dist/lib/steplist.js +128 -0
- package/dist/verbs/build.js +108 -14
- package/dist/verbs/checkpoint.js +41 -13
- package/dist/verbs/handoff.js +508 -42
- package/dist/verbs/harvest.js +224 -0
- package/dist/verbs/order.js +15 -11
- package/dist/verbs/recover.js +31 -1
- package/dist/verbs/relay.js +15 -2
- package/dist/verbs/status.js +64 -17
- package/dist/verbs/turn.js +170 -58
- package/package.json +1 -1
- package/skills/build/SKILL.md +85 -34
- package/skills/finish/SKILL.md +18 -4
- package/skills/harvest/SKILL.md +54 -11
- package/skills/order/SKILL.md +20 -10
- package/skills/plan/SKILL.md +77 -17
- package/skills/refine/SKILL.md +24 -3
- package/skills/run/SKILL.md +256 -0
- package/skills/status/SKILL.md +10 -3
- package/skills/step/SKILL.md +25 -3
- package/skills/verify/SKILL.md +6 -2
- package/templates/intent.md +30 -8
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "plumbbob",
|
|
3
3
|
"displayName": "PlumbBob",
|
|
4
|
-
"version": "0.13.
|
|
4
|
+
"version": "0.13.2",
|
|
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.
|
|
@@ -53,20 +55,24 @@ PlumbBob itself, running its own loop.
|
|
|
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.**
|
|
70
76
|
|
|
71
77
|
## What a prompt can't replicate
|
|
72
78
|
|
|
@@ -131,20 +137,23 @@ In Claude Code, inside any git repo with a clean tree:
|
|
|
131
137
|
|
|
132
138
|
1. **Plan.** Fire `/plumbbob:plan` and give it whatever you have: nothing (it interviews
|
|
133
139
|
you), a rough line (`/plumbbob:plan rate-limit POST /login, 5/min/IP, return 429`), or a
|
|
134
|
-
path to a spec file. Together you fill the build's `intent.md
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
140
|
+
path to a spec file. Together you fill the build's `intent.md`: the steps, and the
|
|
141
|
+
build ranges that say where the build stops for your call. For a three-step rate
|
|
142
|
+
limiter that might be step 1 alone, to settle how the bucket refills, then steps 2
|
|
143
|
+
and 3 together. No code is written yet.
|
|
144
|
+
2. **Run.** Fire `/plumbbob:run`. It runs the range holding the next step, here step 1
|
|
145
|
+
alone, so it implements that step, runs the heavy check gate, reviews its own diff
|
|
146
|
+
against the plan, and stops at the step's pause, where the plan reserved your call.
|
|
147
|
+
What it stops on is one block with a fixed shape (the
|
|
148
|
+
[turn anatomy](docs/presentation.md)): the CLI measures the check, the seam, and the
|
|
149
|
+
diff, the model supplies only the judgment, and nothing is written around it.
|
|
141
150
|
|
|
142
151
|
````markdown
|
|
143
152
|
**Now**: Step 1 of 3 - feat(limiter): add a token-bucket limiter
|
|
144
153
|
|
|
145
|
-
**Summary**: A per-IP token bucket now refuses the 6th attempt inside a minute. (details:
|
|
154
|
+
**Summary**: A per-IP token bucket now refuses the 6th attempt inside a minute. (details: [detail.md](.plumbbob/detail.md))
|
|
146
155
|
|
|
147
|
-
1. The bucket lives in memory, keyed by IP, refilling five tokens a minute.
|
|
156
|
+
1. The bucket lives in memory, keyed by IP, refilling on read: five tokens a minute.
|
|
148
157
|
2. The clock is injected, so the test advances time instead of sleeping on it.
|
|
149
158
|
|
|
150
159
|
**Readout**:
|
|
@@ -161,7 +170,8 @@ In Claude Code, inside any git repo with a clean tree:
|
|
|
161
170
|
|
|
162
171
|
**Verdict**: ● Plumb
|
|
163
172
|
|
|
164
|
-
**Next Up**: Step 2 of 3 - feat(login): wire the limiter into POST /login (model: **Sonnet**, details:
|
|
173
|
+
**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))
|
|
174
|
+
· range 1 of 2 lands with step 1
|
|
165
175
|
|
|
166
176
|
**Your Call**:
|
|
167
177
|
|
|
@@ -170,7 +180,7 @@ In Claude Code, inside any git repo with a clean tree:
|
|
|
170
180
|
- anything that reads as direction → I take it as what to change; nothing lands until you approve
|
|
171
181
|
- `revert` → I wind the work back to the last checkpoint
|
|
172
182
|
|
|
173
|
-
**Recommendation**: Approve it. The gate is green
|
|
183
|
+
**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
184
|
````
|
|
175
185
|
|
|
176
186
|

|
|
@@ -181,14 +191,16 @@ In Claude Code, inside any git repo with a clean tree:
|
|
|
181
191
|
`.plumbbob/settings.json`, for example `"check": "npm test"`; in a repo where
|
|
182
192
|
checkride finds no tools at all, `start` says so up front and names that key.)
|
|
183
193
|
3. **Reply.** Say `looks good` and it commits the step as a checkpoint, marks it done,
|
|
184
|
-
and returns to the boundary
|
|
194
|
+
and returns to the boundary, where the line beneath Next Up names the run to type
|
|
195
|
+
next and the steps it will land:
|
|
185
196
|
|
|
186
197
|
```text
|
|
187
|
-
**Checkpoint**: Step 1 complete (a1b2c3d4e, details:
|
|
198
|
+
**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
199
|
|
|
189
200
|
**Verdict**: ● Plumb
|
|
190
201
|
|
|
191
|
-
**Next Up**: Step 2 of 3 - feat(login): wire the limiter into POST /login (model: **Sonnet**, details:
|
|
202
|
+
**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))
|
|
203
|
+
· range 2 of 2: /plumbbob:run (steps 2-3)
|
|
192
204
|
```
|
|
193
205
|
|
|
194
206
|

|
|
@@ -197,18 +209,25 @@ In Claude Code, inside any git repo with a clean tree:
|
|
|
197
209
|
changes nothing. The answer closes on one line pointing back at the step, not on the
|
|
198
210
|
whole block again, since you have already read it; say `show the pause` to see the
|
|
199
211
|
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
|
-
|
|
212
|
+
`looks good`.
|
|
213
|
+
4. **Run again.** Fire `/plumbbob:run` again; re-firing it *is* the clock tick. Range 2
|
|
214
|
+
lands steps 2 and 3, each on a green check with no pause between them, and ends on the
|
|
215
|
+
**stop card**: the range's review (a summary of both steps, a readout measured across
|
|
216
|
+
them, the Verdict), with every idea parked along the way listed under `Harvested` and
|
|
217
|
+
a lean proposed for each. One reply settles it: `looks good` records the review and
|
|
218
|
+
every lean, and `make P1 a blocker` changes one first. `/plumbbob:build` stays the
|
|
219
|
+
granular move whenever you want it, one step to its pause. Whenever you lose the
|
|
220
|
+
thread, `/plumbbob:status` shows where you are and names the next move.
|
|
221
|
+
5. **Finish.** When the last range lands, answer its stop card with `/plumbbob:finish`: it
|
|
222
|
+
records the card, writes a report of what shipped and why into the build's tracked
|
|
223
|
+
folder, makes the final commit, and clears the slate for the next goal. The folder
|
|
224
|
+
rides the branch into the PR: the build's record merges into `main` instead of dying
|
|
225
|
+
with the worktree.
|
|
207
226
|
|
|
208
227
|
A mid-build "ooh, what if…" never derails the step: `/plumbbob:park` captures it to a list
|
|
209
|
-
in one line, and
|
|
210
|
-
|
|
211
|
-
[`docs/happy-path.md`](docs/happy-path.md).
|
|
228
|
+
in one line, and the next stop card proposes a class for it, settled with the rest of the
|
|
229
|
+
review (`/plumbbob:harvest` triages the list anywhere else). One goal walked end to end,
|
|
230
|
+
with the real output at every stage, is in [`docs/happy-path.md`](docs/happy-path.md).
|
|
212
231
|
|
|
213
232
|
## The skills
|
|
214
233
|
|
|
@@ -216,16 +235,17 @@ You drive the whole loop from your IDE with `/plumbbob:*` skills: no step number
|
|
|
216
235
|
remember, no raw CLI to type. Every skill is `disable-model-invocation`, so *you* fire
|
|
217
236
|
every move, and `/plumbbob:status` always names your next one. (Claude Code namespaces
|
|
218
237
|
them under the plugin, so the command is `/plumbbob:plan`. The bare `/plan` reaches it
|
|
219
|
-
too, but only where nothing else owns that name, and
|
|
238
|
+
too, but only where nothing else owns that name, and five of the sixteen share one with
|
|
220
239
|
a Claude Code built-in, so these docs write the full form.)
|
|
221
240
|
|
|
222
|
-
**The happy path**:
|
|
223
|
-
|
|
241
|
+
**The happy path**: plan, run each range, finish; many sessions need nothing else, and
|
|
242
|
+
build is the same loop a step at a time:
|
|
224
243
|
|
|
225
244
|
| Skill <img alt="" width="110" height="1"> | Does |
|
|
226
245
|
| ------------------------------------ | ------ |
|
|
227
|
-
| `/plumbbob:plan` | plan the whole goal: open the session and author intent's Frame, Decisions, Constraints, **and all Steps
|
|
228
|
-
| `/plumbbob:
|
|
246
|
+
| `/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 |
|
|
247
|
+
| `/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 |
|
|
248
|
+
| `/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 |
|
|
229
249
|
| `/plumbbob:finish` | finish up: write the report, make the final commit, clear for a fresh goal |
|
|
230
250
|
|
|
231
251
|
**Plan-shaping moves**: optional, for when the plan needs work mid-flight:
|
|
@@ -233,7 +253,8 @@ nothing else:
|
|
|
233
253
|
| Skill <img alt="" width="110" height="1"> | Does |
|
|
234
254
|
| ------------------------------------ | ------ |
|
|
235
255
|
| `/plumbbob:step` | revise/sharpen the next step (empty input auto-syncs it to reality) |
|
|
236
|
-
| `/plumbbob:refine` | attack the frame for holes, or repair a drifted plan |
|
|
256
|
+
| `/plumbbob:refine` | attack the frame for holes, or repair a drifted plan (`ranges` re-cuts the build ranges) |
|
|
257
|
+
| `/plumbbob:order` | set the build order: the undone steps in the sequence to build them |
|
|
237
258
|
| `/plumbbob:spike` | throwaway worktree experiment for a fork the plan can't settle |
|
|
238
259
|
|
|
239
260
|
**Helpers**: orient, verify, recover, diagnose:
|
|
@@ -252,16 +273,16 @@ nothing else:
|
|
|
252
273
|
| Skill <img alt="" width="110" height="1"> | Does |
|
|
253
274
|
| ------------------------------------ | ------ |
|
|
254
275
|
| `/plumbbob:park` | capture a mid-build idea without chasing it |
|
|
255
|
-
| `/plumbbob:harvest` | triage parked ideas between steps (blocker / tangent / pivot) |
|
|
276
|
+
| `/plumbbob:harvest` | triage parked ideas between steps (blocker / tangent / pivot); a run's stop card carries its own |
|
|
256
277
|
|
|
257
|
-
All
|
|
278
|
+
All sixteen, with inputs and effects, are in
|
|
258
279
|
[`docs/skills-reference.md`](docs/skills-reference.md).
|
|
259
280
|
|
|
260
281
|
Under the skills ships a lean `plumbbob` CLI (the mechanical verbs the
|
|
261
282
|
skills shell out to), four hooks (a non-blocking post-edit lint, a `git commit`
|
|
262
283
|
ask-hook while a step is in flight, the per-prompt turn ledger the approval latch
|
|
263
|
-
reads, and a check at each turn's end that the turn
|
|
264
|
-
printed), and a `.plumbbob/` sidecar you can open and edit by hand at any time: a tracked
|
|
284
|
+
reads, which also mints a run's grant, and a check at each turn's end that the turn
|
|
285
|
+
relayed the ending plumbbob printed), and a `.plumbbob/` sidecar you can open and edit by hand at any time: a tracked
|
|
265
286
|
`builds/<slug>/` folder per build (`intent.md`, `build-log.md`, `checkpoints`, `report.md`)
|
|
266
287
|
that rides the branch into the PR, plus an untracked control plane (`settings.local.json`,
|
|
267
288
|
the session sentinel, the in-flight markers).
|
|
@@ -295,9 +316,9 @@ The questions a skeptic should ask, answered straight.
|
|
|
295
316
|
scoped to one session. The plan lives in the chat, so it fades with the context
|
|
296
317
|
window, and the approval gates the *start* of the work, not each increment of it.
|
|
297
318
|
PlumbBob puts the plan in a file in your repo that survives compaction, restarts,
|
|
298
|
-
and next week; moves the approval to **
|
|
299
|
-
|
|
300
|
-
step, not the afternoon; and records the *whys* alongside the decisions, so the
|
|
319
|
+
and next week; moves the approval to **the stops the plan names**, down to every step
|
|
320
|
+
when you want it, with each step checked, reviewed against the plan, and committed as
|
|
321
|
+
its own checkpoint, so a wrong turn costs one step, not the afternoon; and records the *whys* alongside the decisions, so the
|
|
301
322
|
archive can still answer "what did we decide, and why" long after the session is
|
|
302
323
|
gone. The two compose fine: plan mode (or anything else) can produce a step's
|
|
303
324
|
diff, and `/plumbbob:verify` reads it like any other.
|
|
@@ -314,7 +335,7 @@ modes, spec kits, native checkpoints), and that convergence is evidence *for* th
|
|
|
314
335
|
premise. PlumbBob is deliberately thin against that future: the artifacts are
|
|
315
336
|
plain markdown and ordinary git commits in your own repo, the CLI is lean
|
|
316
337
|
(node builtins plus one deliberate dependency), and nothing locks you in. The
|
|
317
|
-
investment is small (
|
|
338
|
+
investment is small (sixteen
|
|
318
339
|
skills over a plain-markdown sidecar, learnable in an afternoon), and what you're
|
|
319
340
|
actually learning is the method: decisions before code, one verified step at a
|
|
320
341
|
time, capture instead of chase. That transfers to whatever tool wins. If
|
|
@@ -351,7 +372,7 @@ Each doc answers one question (in rough reading order for a new user):
|
|
|
351
372
|
- *Show me the artifacts it leaves behind.* → [`examples/`](examples/): that same session's finished build folder, file by file.
|
|
352
373
|
- *Should I / can I / what about…?* → [`docs/faq.md`](docs/faq.md): the adoption questions, answered straight.
|
|
353
374
|
- *What is each method for?* → [`docs/techniques.md`](docs/techniques.md): steps, seams, the pause, park/harvest, spikes.
|
|
354
|
-
- *What does each skill do?* → [`docs/skills-reference.md`](docs/skills-reference.md): all
|
|
375
|
+
- *What does each skill do?* → [`docs/skills-reference.md`](docs/skills-reference.md): all sixteen skills: inputs, effects, when to reach for each.
|
|
355
376
|
- *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.
|
|
356
377
|
- *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).
|
|
357
378
|
- *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',
|
|
@@ -146,10 +153,21 @@ const VERBS = [
|
|
|
146
153
|
{
|
|
147
154
|
name: 'order',
|
|
148
155
|
description: 'set the build order: the undone steps in the sequence to build them',
|
|
149
|
-
synopsis: ['order <
|
|
150
|
-
args: [
|
|
156
|
+
synopsis: ['order <steps>', 'order --reset'],
|
|
157
|
+
args: [
|
|
158
|
+
{
|
|
159
|
+
name: '<steps>',
|
|
160
|
+
gloss: 'undone step numbers and spans (2-4) in the sequence to build them, space- or comma-separated (1, 6, 2-4, 7, 5); the rest follow in document order',
|
|
161
|
+
},
|
|
162
|
+
],
|
|
151
163
|
flags: [{ name: '--reset', gloss: 'drop the line, so the plan reads in document order again' }],
|
|
152
|
-
notes: 'Writes the one numeric line under `## Build order` in intent.md, which status, build, checkpoint, and handoff all read. Refuses (exit 1) with no session, no numbers, a number the plan lacks, a repeat, or a step already checkpointed.',
|
|
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.',
|
|
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.",
|
|
153
171
|
},
|
|
154
172
|
{
|
|
155
173
|
name: 'spike',
|
|
@@ -360,6 +378,8 @@ async function dispatch(verb, cwd, rest) {
|
|
|
360
378
|
return park(cwd, rest);
|
|
361
379
|
case 'order':
|
|
362
380
|
return order(cwd, rest);
|
|
381
|
+
case 'harvest':
|
|
382
|
+
return harvest(cwd, rest);
|
|
363
383
|
case 'build':
|
|
364
384
|
return build(cwd, rest);
|
|
365
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
|
@@ -17,6 +17,7 @@
|
|
|
17
17
|
import { readFileSync } from 'node:fs';
|
|
18
18
|
import { grantPath, tickPath, turnPath } from "./sidecar.js";
|
|
19
19
|
import { resolveBoolean } from "./settings.js";
|
|
20
|
+
import { hasStep, normalizeStepList, parseStepList } from "./steplist.js";
|
|
20
21
|
const ALLOW = { allow: true };
|
|
21
22
|
/**
|
|
22
23
|
* The refusal message is the affordance; it tells the model what the pause is,
|
|
@@ -51,12 +52,20 @@ const SETTINGS_AUTO_NOTE = `note: \`auto\` is set in settings but is no longer a
|
|
|
51
52
|
function ceilingMessage(ceiling) {
|
|
52
53
|
return `plumbbob: checkpoint refused — the range you granted ends at step ${ceiling} — pause here; run it again to continue.\n`;
|
|
53
54
|
}
|
|
55
|
+
/**
|
|
56
|
+
* The refusal for a step a list grant does not name, naming the steps it does.
|
|
57
|
+
*/
|
|
58
|
+
function outsideMessage(step, steps) {
|
|
59
|
+
return `plumbbob: checkpoint refused — step ${step} is not in the range you granted (${normalizeStepList(steps)}) — pause here; run it again to continue.\n`;
|
|
60
|
+
}
|
|
54
61
|
/**
|
|
55
62
|
* The five-row matrix, in order, first hit wins.
|
|
56
63
|
*
|
|
57
|
-
* A
|
|
64
|
+
* A range grant speaks only to numbered steps: on a `--plan` checkpoint (step
|
|
58
65
|
* null) it neither allows nor refuses, and the decision falls through to the
|
|
59
|
-
* turn comparison.
|
|
66
|
+
* turn comparison. A `steps` grant lands exactly the steps the human typed and
|
|
67
|
+
* refuses any other, so no later edit to the plan or its build order can widen
|
|
68
|
+
* it; both kinds refuse with the `ceiling` reason, a step outside the range.
|
|
60
69
|
*/
|
|
61
70
|
export function evaluateLatch(input) {
|
|
62
71
|
if (input.isTTY)
|
|
@@ -65,6 +74,11 @@ export function evaluateLatch(input) {
|
|
|
65
74
|
return ALLOW; // 2: dormant ledger / hand-built diff
|
|
66
75
|
if (input.grant?.kind === 'auto')
|
|
67
76
|
return ALLOW; // 3: one-turn grant the human typed
|
|
77
|
+
if (input.grant?.kind === 'steps' && input.step !== null) {
|
|
78
|
+
if (hasStep(input.grant.steps, input.step))
|
|
79
|
+
return ALLOW;
|
|
80
|
+
return { allow: false, reason: 'ceiling', message: outsideMessage(input.step, input.grant.steps) };
|
|
81
|
+
}
|
|
68
82
|
if (input.grant?.kind === 'range' && input.step !== null) {
|
|
69
83
|
if (input.step <= input.grant.ceiling)
|
|
70
84
|
return ALLOW;
|
|
@@ -78,15 +92,19 @@ export function evaluateLatch(input) {
|
|
|
78
92
|
return { allow: false, reason: 'no-turn', message: input.settingsAuto ? base + SETTINGS_AUTO_NOTE : base };
|
|
79
93
|
}
|
|
80
94
|
/**
|
|
81
|
-
* Parse a GRANT file's content (`auto` | `range <M>`).
|
|
95
|
+
* Parse a GRANT file's content (`auto` | `steps <list>` | `range <M>`).
|
|
82
96
|
*
|
|
83
|
-
* Anything else (including a corrupted or hand-mangled file
|
|
84
|
-
* nothing: no grant, never an error.
|
|
97
|
+
* Anything else (including a corrupted or hand-mangled file, or a list that
|
|
98
|
+
* does not parse) contributes nothing: no grant, never an error.
|
|
85
99
|
*/
|
|
86
100
|
export function parseGrant(raw) {
|
|
87
101
|
const trimmed = raw.trim();
|
|
88
102
|
if (trimmed === 'auto')
|
|
89
103
|
return { kind: 'auto' };
|
|
104
|
+
const listed = /^steps (\S+)$/.exec(trimmed);
|
|
105
|
+
const steps = listed === null ? null : parseStepList(listed[1] ?? '');
|
|
106
|
+
if (steps !== null)
|
|
107
|
+
return { kind: 'steps', steps };
|
|
90
108
|
const range = /^range (\d+)$/.exec(trimmed);
|
|
91
109
|
if (range !== null)
|
|
92
110
|
return { kind: 'range', ceiling: Number(range[1]) };
|
|
@@ -109,6 +127,29 @@ export function checkLatch(root, step) {
|
|
|
109
127
|
settingsAuto: resolveBoolean(root, 'auto', false),
|
|
110
128
|
});
|
|
111
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
|
+
}
|
|
112
153
|
/**
|
|
113
154
|
* The current GRANT, or null when the file is absent or unparseable.
|
|
114
155
|
*/
|
package/dist/lib/notice.js
CHANGED
|
@@ -67,13 +67,14 @@ export function advisory(a) {
|
|
|
67
67
|
}
|
|
68
68
|
/**
|
|
69
69
|
* Assemble a transition's whole ending: the labeled lead line, the Verdict
|
|
70
|
-
* where one was measured, the advisories,
|
|
70
|
+
* where one was measured, the advisories, the Stop part where a range landed,
|
|
71
|
+
* and the pointer.
|
|
71
72
|
*
|
|
72
73
|
* The order is the type's field order, so a verb composes parts and never a
|
|
73
74
|
* sequence. A part with nothing in it vanishes rather than leaving a gap.
|
|
74
75
|
*/
|
|
75
76
|
export function ending(e) {
|
|
76
|
-
return blocks([e.lead, e.verdict ?? null, ...(e.advisories ?? []), e.pointer ?? null]);
|
|
77
|
+
return blocks([e.lead, e.verdict ?? null, ...(e.advisories ?? []), e.stop ?? null, e.pointer ?? null]);
|
|
77
78
|
}
|
|
78
79
|
/**
|
|
79
80
|
* Stack a turn's parts: one blank line between each, one beneath the last.
|