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.
- package/.claude-plugin/plugin.json +1 -1
- package/README.md +72 -53
- 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.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
|
|
|
@@ -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.
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
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,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`.
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
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
|
|
212
|
-
|
|
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
|
|
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**:
|
|
225
|
-
|
|
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:
|
|
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
|
|
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
|
|
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 **
|
|
301
|
-
|
|
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 (
|
|
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
|
|
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:
|
|
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
|
*/
|