plumbbob 0.13.1 → 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.
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "plumbbob",
3
3
  "displayName": "PlumbBob",
4
- "version": "0.13.1",
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 (fifteen skills and a small CLI) that runs
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; then the model builds one small step at a time and **stops after
16
- each one**, waiting for you to read the diff and approve it before anything is
17
- committed. It's built for the small-to-medium work that fills most days (a feature, a
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 you approve one at a time, each small enough to
36
- read as an editor.
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*), and the **Steps**: a numbered list where every step carries a
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 — frame, decisions, all steps (once)
62
- per step:
63
- /plumbbob:build implement the next step → run checks → self-review → PAUSE
64
- (looks good) → the step is committed as a checkpoint; fire /plumbbob:build again
65
- /plumbbob:finish report what shipped and why, final commit, clear (once)
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
- Nothing is locked and nothing refuses you: the loop does a step's labor, pulls up to
69
- the line, and waits for you to advance it. **You are the clock.**
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
 
@@ -132,19 +138,22 @@ In Claude Code, inside any git repo with a clean tree:
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
140
  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. No code is written yet.
136
- 2. **Build.** Fire `/plumbbob:build`. It implements the next undone step, runs the
137
- heavy check gate, reviews its own diff against the plan, and stops. What it stops on
138
- is one block with a fixed shape (the [turn anatomy](docs/presentation.md)): the CLI
139
- measures the check, the seam, and the diff, the model supplies only the judgment, and
140
- nothing is written around it.
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: `.plumbbob/detail.md`)
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: `.plumbbob/builds/2026-07-03-rate-limit-the-login-endpoint/intent.md:20`)
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, the seam held, and every call the step made is one the plan already decided.
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
  ![The build pause as Claude Code renders it](docs/media/pause.svg)
@@ -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 with the same three lines every landed step gets:
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: `.plumbbob/builds/2026-07-03-rate-limit-the-login-endpoint/build-log.md:44`)
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: `.plumbbob/builds/2026-07-03-rate-limit-the-login-endpoint/intent.md:20`)
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
  ![The boundary after looks good](docs/media/checkpoint-boundary.svg)
@@ -197,20 +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`. Then fire `/plumbbob:build` again for the next step: re-firing it
201
- *is* the clock tick. When the plan cut build ranges, the line beneath Next Up names
202
- one to type instead (`· range 2 of 2: /plumbbob:build 2-3`): it lands each step it
203
- names and stops where the plan reserved a call for you. Whenever you lose the thread, `/plumbbob:status` shows where you
204
- are and names the next move.
205
- 4. **Finish.** When the last step is done, `/plumbbob:finish` writes a report of what shipped
206
- and why into the build's tracked folder, makes the final commit, and clears the slate
207
- for the next goal. The folder rides the branch into the PR: the build's record merges
208
- into `main` instead of dying with the worktree.
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.
209
226
 
210
227
  A mid-build "ooh, what if…" never derails the step: `/plumbbob:park` captures it to a list
211
- in one line, and `/plumbbob:harvest` triages the list between steps. One goal walked end to
212
- end, with the real output at every stage, is in
213
- [`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).
214
231
 
215
232
  ## The skills
216
233
 
@@ -218,16 +235,17 @@ You drive the whole loop from your IDE with `/plumbbob:*` skills: no step number
218
235
  remember, no raw CLI to type. Every skill is `disable-model-invocation`, so *you* fire
219
236
  every move, and `/plumbbob:status` always names your next one. (Claude Code namespaces
220
237
  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 four of the fourteen share one with
238
+ too, but only where nothing else owns that name, and five of the sixteen share one with
222
239
  a Claude Code built-in, so these docs write the full form.)
223
240
 
224
- **The happy path**: the three moves every session makes; many sessions need
225
- nothing else:
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:
226
243
 
227
244
  | Skill <img alt="" width="110" height="1"> | Does |
228
245
  | ------------------------------------ | ------ |
229
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 |
230
- | `/plumbbob:build` | implement the next planned step, then verify it to the pause: `--auto` self-approves and chains to done; a range like `1-3`, or a list like `20,5-9`, self-approves exactly the steps it names, then stops |
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 |
231
249
  | `/plumbbob:finish` | finish up: write the report, make the final commit, clear for a fresh goal |
232
250
 
233
251
  **Plan-shaping moves**: optional, for when the plan needs work mid-flight:
@@ -236,6 +254,7 @@ nothing else:
236
254
  | ------------------------------------ | ------ |
237
255
  | `/plumbbob:step` | revise/sharpen the next step (empty input auto-syncs it to reality) |
238
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 |
239
258
  | `/plumbbob:spike` | throwaway worktree experiment for a fork the plan can't settle |
240
259
 
241
260
  **Helpers**: orient, verify, recover, diagnose:
@@ -254,16 +273,16 @@ nothing else:
254
273
  | Skill <img alt="" width="110" height="1"> | Does |
255
274
  | ------------------------------------ | ------ |
256
275
  | `/plumbbob:park` | capture a mid-build idea without chasing it |
257
- | `/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 |
258
277
 
259
- All fourteen, with inputs and effects, are in
278
+ All sixteen, with inputs and effects, are in
260
279
  [`docs/skills-reference.md`](docs/skills-reference.md).
261
280
 
262
281
  Under the skills ships a lean `plumbbob` CLI (the mechanical verbs the
263
282
  skills shell out to), four hooks (a non-blocking post-edit lint, a `git commit`
264
283
  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 relayed the ending plumbbob
266
- 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
267
286
  `builds/<slug>/` folder per build (`intent.md`, `build-log.md`, `checkpoints`, `report.md`)
268
287
  that rides the branch into the PR, plus an untracked control plane (`settings.local.json`,
269
288
  the session sentinel, the in-flight markers).
@@ -297,9 +316,9 @@ The questions a skeptic should ask, answered straight.
297
316
  scoped to one session. The plan lives in the chat, so it fades with the context
298
317
  window, and the approval gates the *start* of the work, not each increment of it.
299
318
  PlumbBob puts the plan in a file in your repo that survives compaction, restarts,
300
- and next week; moves the approval to **every step** (each checked, reviewed
301
- against the plan, and committed as its own checkpoint), so a wrong turn costs one
302
- 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
303
322
  archive can still answer "what did we decide, and why" long after the session is
304
323
  gone. The two compose fine: plan mode (or anything else) can produce a step's
305
324
  diff, and `/plumbbob:verify` reads it like any other.
@@ -316,7 +335,7 @@ modes, spec kits, native checkpoints), and that convergence is evidence *for* th
316
335
  premise. PlumbBob is deliberately thin against that future: the artifacts are
317
336
  plain markdown and ordinary git commits in your own repo, the CLI is lean
318
337
  (node builtins plus one deliberate dependency), and nothing locks you in. The
319
- investment is small (fourteen
338
+ investment is small (sixteen
320
339
  skills over a plain-markdown sidecar, learnable in an afternoon), and what you're
321
340
  actually learning is the method: decisions before code, one verified step at a
322
341
  time, capture instead of chase. That transfers to whatever tool wins. If
@@ -353,7 +372,7 @@ Each doc answers one question (in rough reading order for a new user):
353
372
  - *Show me the artifacts it leaves behind.* → [`examples/`](examples/): that same session's finished build folder, file by file.
354
373
  - *Should I / can I / what about…?* → [`docs/faq.md`](docs/faq.md): the adoption questions, answered straight.
355
374
  - *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 fifteen skills: inputs, effects, when to reach for each.
375
+ - *What does each skill do?* → [`docs/skills-reference.md`](docs/skills-reference.md): all sixteen skills: inputs, effects, when to reach for each.
357
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.
358
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).
359
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: 'Refuses (exit 1) with no session, a non-numeric or < 1 step, or a seam it cannot parse.',
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 driver), or a reminder for a pause already shown',
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 that pause was already shown' },
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: 'Writes only the detail file (a blank line around each heading) and the PAUSE stamp; refuses (exit 1) with no session, or with --plan and --driver together.',
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':
@@ -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
  */