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.
@@ -1,33 +1,43 @@
1
1
  // `plumbbob turn`: the turn-ledger hook. Wired to the harness's
2
- // UserPromptSubmit event, it runs once per human prompt and maintains two of the
3
- // sidecar's git-excluded control files (flat files under `.plumbbob/`): it ticks
4
- // `TURN`, the monotonic count of human turns the checkpoint latch compares
5
- // against a step's entry stamp to know a human turn intervened since the step
6
- // began, and it rewrites `GRANT` from the literal prompt, minting a one-turn
7
- // self-approval only when the human typed `/build --auto` or an `N-M` step
8
- // range, clearing it otherwise (those strings can only come from a human
9
- // keystroke, because `build` is disable-model-invocation: a grant the model
10
- // could forge would be no grant). When a step is in flight it also emits ONE
11
- // `UserPromptSubmit` additionalContext line so a fresh session (post-compaction,
12
- // or a scripted `-p` turn where no skill prose is in context) still knows the
13
- // beat: a tangent is parked, not chased, and a message that also asks something
14
- // is answered before the park's block. Guidance only: it blocks nothing. It
15
- // ALWAYS exits 0 and any failure degrades to no output, so neither the tick nor
16
- // the emit can ever wedge prompting.
2
+ // UserPromptSubmit event, it runs once per prompt and maintains the sidecar's
3
+ // git-excluded turn ledger (flat files under `.plumbbob/`): it ticks `TURN`,
4
+ // the monotonic count of human turns the checkpoint latch compares against a
5
+ // step's entry stamp to know a human turn intervened since the step began, and
6
+ // it rewrites `GRANT` from the literal prompt, minting a one-turn
7
+ // self-approval only when the human typed `/build --auto`, a step range (`N-M`,
8
+ // or a list such as `20,5-9`), or `/plumbbob:run`, clearing it otherwise (those
9
+ // strings can only come from a human keystroke, because `build` and `run` are
10
+ // disable-model-invocation: a grant the model could forge would be no grant).
11
+ // A run's grant is the steps left of the range the plan names, read from the
12
+ // active build's `intent.md` at the keystroke, and `RUN` beside it marks the
13
+ // range the run is landing. The harness also sends a background job's
14
+ // completion notice through this event, as a prompt the human never typed, so
15
+ // a notice ticks nothing, keeps the grant and the mark, and clears only the
16
+ // ending `RELAY` holds from an earlier turn. When a step is in flight a typed
17
+ // prompt also emits ONE `UserPromptSubmit` additionalContext line so a fresh
18
+ // session (post-compaction, or a scripted `-p` turn where no skill prose is in
19
+ // context) still knows the beat: a tangent is parked, not chased, and a
20
+ // message that also asks something is answered before the park's block.
21
+ // Guidance only: it blocks nothing. It ALWAYS exits 0 and any failure degrades
22
+ // to no output, so neither the tick nor the emit can ever wedge prompting.
17
23
  import { readFileSync, writeFileSync } from 'node:fs';
18
- import { findSessionRoot, setGrant, stepPath, turnPath } from "../lib/sidecar.js";
24
+ import { parseBuildRanges, readRunArgs, resolveRun } from "../lib/orient.js";
25
+ import { clearRelay, findSessionRoot, inSpike, intentPath, notePrompt, readPrompt, readRun, setGrant, setRun, stepPath, turnPath, } from "../lib/sidecar.js";
26
+ import { hasStep, normalizeStepList, readBuildArgs } from "../lib/steplist.js";
19
27
  /**
20
- * The hook entry point: apply the tick, then emit the in-flight-step nudge.
28
+ * The hook entry point: apply the prompt, then emit the in-flight-step nudge
29
+ * when it counted as a human turn.
21
30
  *
22
31
  * Always returns 0: a broken turn must never wedge a prompt, so any failure is
23
32
  * swallowed and degrades to no output.
24
33
  */
25
34
  export function turn(cwd, _args) {
26
35
  try {
27
- applyTurn(cwd, readStdin());
28
- const context = stepInFlightContext(cwd);
29
- if (context !== null)
30
- process.stdout.write(context);
36
+ if (applyTurn(cwd, readStdin())) {
37
+ const context = stepInFlightContext(cwd);
38
+ if (context !== null)
39
+ process.stdout.write(context);
40
+ }
31
41
  }
32
42
  catch {
33
43
  // A broken turn must never wedge a prompt: swallow and exit 0.
@@ -78,7 +88,17 @@ function inFlightStep(root) {
78
88
  }
79
89
  }
80
90
  /**
81
- * The testable core: apply one tick given the raw hook stdin.
91
+ * The testable core: apply one prompt given the raw hook stdin, and say
92
+ * whether it counted as a human turn.
93
+ *
94
+ * A typed prompt ticks TURN and rewrites GRANT and the RUN mark. A notice
95
+ * touches none of them, because the latch reads TURN passing a step's entry
96
+ * stamp as its one sign that a human answered, and a grant the human typed has
97
+ * to outlive a job that reports back partway through the range. A notice that starts a turn of
98
+ * its own clears RELAY, whose ending was judged when its own turn ended; one
99
+ * the harness folds into the running turn arrives under that turn's prompt id
100
+ * and leaves RELAY alone, since the ending the turn just printed has yet to be
101
+ * judged.
82
102
  *
83
103
  * Split from the fd-0 read so the unit tests can drive it directly: an
84
104
  * in-process test cannot feed fd 0 (the same constraint checkpoint's `--body`
@@ -87,20 +107,56 @@ function inFlightStep(root) {
87
107
  export function applyTurn(cwd, raw) {
88
108
  const root = findSessionRoot(cwd);
89
109
  if (root === null)
90
- return 0; // no active session above cwd: silent no-op.
91
- writeFileSync(turnPath(root), `${readTurn(root) + 1}\n`);
92
- setGrant(root, grantFromPrompt(extractPrompt(raw)));
93
- return 0;
110
+ return false; // no active session above cwd: silent no-op.
111
+ const { prompt, promptId } = readPayload(raw);
112
+ const notice = isNotice(prompt);
113
+ if (notice) {
114
+ if (promptId === null || promptId !== readPrompt(root))
115
+ clearRelay(root);
116
+ }
117
+ else {
118
+ writeFileSync(turnPath(root), `${readTurn(root) + 1}\n`);
119
+ const minted = mintSafely(root, prompt);
120
+ setGrant(root, minted.grant);
121
+ setRun(root, minted.run);
122
+ }
123
+ // Last, so a record that cannot be written never costs the tick or the grant.
124
+ notePrompt(root, promptId);
125
+ return !notice;
94
126
  }
127
+ /**
128
+ * Whether a prompt is the harness reporting on background work rather than
129
+ * the human typing.
130
+ *
131
+ * Nothing in the payload marks one, but every notice opens on a frame of the
132
+ * harness's own at its very first character: `<task-notification>` for a
133
+ * finished shell job, a Monitor event, or a subagent's task, and
134
+ * `<agent-message ` for a subagent's or another session's report. A misread
135
+ * errs safe: a typed prompt taken for a notice costs one tick and one grant,
136
+ * so the latch only grows stricter, while a notice taken for a typed prompt
137
+ * is a turn no human took.
138
+ */
139
+ export function isNotice(prompt) {
140
+ return prompt.startsWith('<task-notification>') || prompt.startsWith('<agent-message ');
141
+ }
142
+ // A build invocation, bare or namespaced, starting a token.
143
+ const BUILD_INVOCATION = /(?<![\w/.~-])\/(?:plumbbob:)?build\b/;
144
+ // A run invocation: only the namespaced verb, since a bare `/run` reaches
145
+ // Claude Code's own skill, starting a token and ending at a space, a line's
146
+ // end, or sentence punctuation.
147
+ const RUN_INVOCATION = /(?<![\w/.~-])\/plumbbob:run(?=[\s.;:!?,]|$)/;
95
148
  /**
96
149
  * Derive the GRANT content from the human's prompt, or null to clear it.
97
150
  *
98
151
  * A grant is minted only from a literal `build` invocation the human typed:
99
152
  * `build` is disable-model-invocation, so this string can only come from a
100
153
  * human keystroke, never model-authored pressure: a grant the model can forge
101
- * is no grant. A bounded range beats `--auto` (bounded wins); a bare
102
- * `/build`, or any non-invocation prompt, mints nothing. The namespaced
103
- * `/plumbbob:build` form is honored alongside the bare one.
154
+ * is no grant. A range mints `steps <list>`, exactly the steps it names in
155
+ * their normal form (`20, 5-9` mints `steps 5-9,20`), and beats `--auto`
156
+ * (bounded wins), so a range that does not parse mints nothing rather than
157
+ * falling back to `auto`. A bare `/build`, a bare step number, or any
158
+ * non-invocation prompt mints nothing. The namespaced `/plumbbob:build` form is
159
+ * honored alongside the bare one.
104
160
  *
105
161
  * The invocation must *start* a token: a slash preceded by a word character,
106
162
  * `/`, `.`, `-`, or `~` is a path segment (`src/build`, `./build`), not a
@@ -116,47 +172,103 @@ export function applyTurn(cwd, raw) {
116
172
  * argument the human typed.
117
173
  */
118
174
  export function grantFromPrompt(prompt) {
119
- const invocation = /(?<![\w/.~-])\/(?:plumbbob:)?build\b/.exec(prompt);
175
+ const invocation = BUILD_INVOCATION.exec(prompt);
120
176
  if (invocation === null)
121
177
  return null;
122
- const line = prompt.slice(invocation.index + invocation[0].length).split('\n', 1)[0] ?? '';
123
- let auto = false;
124
- let ceiling;
125
- for (const token of line.split(/\s+/)) {
126
- const word = token.replace(/[.,;:!?]+$/, '');
127
- if (word === '')
128
- continue;
129
- if (word === '--auto') {
130
- auto = true;
131
- continue;
132
- }
133
- const range = /^\d+-(\d+)$/.exec(word);
134
- if (range !== null) {
135
- ceiling = range[1];
136
- continue;
137
- }
138
- if (/^\d+$/.test(word))
139
- continue; // a bare step number: keep scanning for a range/--auto
140
- break; // an unrecognized flag or free text: the arguments have ended
178
+ const args = readBuildArgs(argumentLine(prompt, invocation));
179
+ if (args.range)
180
+ return args.steps === null ? null : `steps ${normalizeStepList(args.steps)}`;
181
+ return args.auto ? 'auto' : null;
182
+ }
183
+ /**
184
+ * The text an invocation's arguments are read from: the rest of its line.
185
+ */
186
+ function argumentLine(prompt, invocation) {
187
+ return prompt.slice(invocation.index + invocation[0].length).split('\n', 1)[0] ?? '';
188
+ }
189
+ /**
190
+ * The GRANT one typed prompt mints and the RUN mark it leaves, given the
191
+ * active build as the hook read it (null when it could not) and the range the
192
+ * RUN mark names now.
193
+ *
194
+ * Whichever invocation comes first in the prompt is the one the human fired.
195
+ * A run mints `steps <what is left>` of the range the plan names at the
196
+ * keystroke, and marks that range, so a later edit to the ranges or the build
197
+ * order leaves both as they were. A bare range's run mints nothing and marks
198
+ * nothing, since it builds its step to the pause, and so does a run that names
199
+ * nothing it can walk, a span or a list of ranges, and a plan the hook could
200
+ * not read. Any other prompt mints what `grantFromPrompt` reads, and keeps the
201
+ * mark only while a step of the marked range waits at its pause, so a halt
202
+ * inside the run approved with `looks good` still lands under it.
203
+ */
204
+ export function mintFromPrompt(prompt, build, marked) {
205
+ const run = RUN_INVOCATION.exec(prompt);
206
+ const first = BUILD_INVOCATION.exec(prompt);
207
+ if (run === null || (first !== null && first.index < run.index)) {
208
+ return { grant: grantFromPrompt(prompt), run: heldMark(build, marked) };
209
+ }
210
+ const args = readRunArgs(argumentLine(prompt, run));
211
+ const resolved = args === null || build === null ? null : resolveRun({ ...build, item: args.item });
212
+ if (resolved?.ok !== true || resolved.grant === null)
213
+ return { grant: null, run: null };
214
+ return { grant: resolved.grant, run: resolved.range.item };
215
+ }
216
+ /**
217
+ * The RUN mark a prompt that is not a run leaves: the marked range while a
218
+ * step it names is in flight, so waiting at its pause, else none.
219
+ */
220
+ function heldMark(build, marked) {
221
+ if (build === null || marked === null || build.inFlight === null)
222
+ return null;
223
+ const step = build.inFlight;
224
+ const range = parseBuildRanges(build.intent).ranges.find((r) => r.item === marked);
225
+ return range !== undefined && hasStep(range.steps, step) ? marked : null;
226
+ }
227
+ /**
228
+ * What a typed prompt mints, read against the active build, degrading to no
229
+ * grant and no mark on any failure, so a broken read can never leave the last
230
+ * turn's grant standing.
231
+ */
232
+ function mintSafely(root, prompt) {
233
+ try {
234
+ return mintFromPrompt(prompt, readBuild(root), readRun(root));
235
+ }
236
+ catch {
237
+ return { grant: null, run: null };
141
238
  }
142
- if (ceiling !== undefined)
143
- return `range ${ceiling}`;
144
- return auto ? 'auto' : null;
145
239
  }
146
240
  /**
147
- * Extract the `prompt` field from the UserPromptSubmit JSON, or '' otherwise.
241
+ * The active build as a run reads it: the plan's text, the step in flight,
242
+ * and whether a spike is open; null when the plan cannot be read, which mints
243
+ * no run.
244
+ */
245
+ function readBuild(root) {
246
+ try {
247
+ return { intent: readFileSync(intentPath(root), 'utf8'), inFlight: inFlightStep(root), spiking: inSpike(root) };
248
+ }
249
+ catch {
250
+ return null;
251
+ }
252
+ }
253
+ /**
254
+ * The `prompt` and `prompt_id` fields of the UserPromptSubmit JSON, as '' and
255
+ * null when either is absent.
148
256
  *
149
257
  * Absent, unparseable, or non-string input contributes nothing rather than
150
- * wedging the tool: the tick still lands (a human turn did occur) and the empty
151
- * prompt clears any GRANT; the verb never throws over bad stdin.
258
+ * wedging the tool: the tick still lands (a human turn did occur), the empty
259
+ * prompt clears any GRANT, and a missing id records none; the verb never
260
+ * throws over bad stdin.
152
261
  */
153
- function extractPrompt(raw) {
262
+ function readPayload(raw) {
154
263
  try {
155
264
  const parsed = JSON.parse(raw);
156
- return typeof parsed.prompt === 'string' ? parsed.prompt : '';
265
+ return {
266
+ prompt: typeof parsed.prompt === 'string' ? parsed.prompt : '',
267
+ promptId: typeof parsed.prompt_id === 'string' && parsed.prompt_id !== '' ? parsed.prompt_id : null,
268
+ };
157
269
  }
158
270
  catch {
159
- return '';
271
+ return { prompt: '', promptId: null };
160
272
  }
161
273
  }
162
274
  /**
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "plumbbob",
3
- "version": "0.13.0",
3
+ "version": "0.13.2",
4
4
  "description": "Attention-first build process: 15 skills + a CLI that keep you the decider — guidance on the work, a latch on the record.",
5
5
  "keywords": [
6
6
  "claude-code",
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  name: build
3
- description: "The default engine: read the next planned step from intent, implement it (its done-when, seam, Decisions, Constraints), then verify it through to the approval pause. Swappable: build by hand/vibed/another harness and run /plumbbob:verify instead. `--auto` self-approves and chains to done; a step range like `1-3` self-approves through step 3, then stops."
4
- argument-hint: "[step-number | step-range] [--auto]"
3
+ description: "The default engine: read the next planned step from intent, implement it (its done-when, seam, Decisions, Constraints), then verify it through to the approval pause. Swappable: build by hand/vibed/another harness and run /plumbbob:verify instead. `--auto` self-approves and chains to done; a step range like `1-3`, or a list like `20,5-9`, self-approves exactly the steps it names, in build order, then stops."
4
+ argument-hint: "[step-number | step-range | step-list] [--auto]"
5
5
  disable-model-invocation: true
6
6
  allowed-tools: Read, Edit, Write, Bash(plumbbob status:*), Bash(plumbbob build:*), Bash(plumbbob handoff:*), Bash(plumbbob check:*), Bash(plumbbob checkpoint:*), Bash(plumbbob park:*), Bash(plumbbob agent:*), Bash(plumbbob spike:*), Bash(git diff:*)
7
7
  ---
@@ -35,15 +35,18 @@ switch with `/model` and rerun to honor it, or wave you on. Advisory, never a ga
35
35
  ## What this skill does, in order
36
36
 
37
37
  1. **Pick the step.** Use the number you were invoked with (for example `/plumbbob:build 4`), or,
38
- if you were given a range like `/plumbbob:build 1-3`, start at the first number and treat
39
- the second as the last step you approve and land yourself (see the range note under
40
- `--auto`). With no argument, don't resolve the next step yourself; the CLI does: bare `plumbbob build`
38
+ if you were given a range like `/plumbbob:build 1-3` or a list like `/plumbbob:build 20,5-9`,
39
+ start at its first undone step in build order, the one the dashboard above marks
40
+ `← requested`, and land only the steps it names (see the range note under `--auto`).
41
+ With no argument, don't resolve the next step yourself; the CLI does: bare `plumbbob build`
41
42
  (step 2) enters the next undone step in build order (the `## Build order` line when the
42
43
  plan has one, else the numbering) and refuses with a `/plumbbob:step` nudge when every
43
44
  step is checkpointed.
44
45
  2. **Enter the step.** Run `plumbbob build <n>`, or bare `plumbbob build` to enter the
45
46
  next undone step (records the in-flight STEP + SEAM so `/plumbbob:status` shows the step in
46
- flight; the seam is awareness, not a lock).
47
+ flight; the seam is awareness, not a lock). When it refuses with `no step entered` because
48
+ a stop card is waiting for its reply, relay the refusal whole and end the turn: the card's
49
+ reply is the human's, and no step is built until it records.
47
50
  - **Spike-as-step.** If the step's title opens with `spike:` / `Spike:`, the increment
48
51
  *is* the experiment: resolving a fork the plan couldn't settle on paper. After
49
52
  entering it, run `plumbbob spike report "<slug>"` (the CLI scaffolds a
@@ -181,6 +184,12 @@ switch with `/model` and rerun to honor it, or wave you on. Advisory, never a ga
181
184
  line in the build-log's `## Log` and then truncates it, so the detail lands in the
182
185
  tracked ledger rather than piling up on disk.
183
186
 
187
+ **A bare range's pause is its stop.** When the injected dashboard's ranges block lists
188
+ this step as a bare range (`/plumbbob:build 5`, its row marked `▸`), the plan reserved a
189
+ call for this pause, and the `stop:` line beneath the row names it. The recommendation
190
+ speaks to that call: the move you would take on the plan's question, and why, rather
191
+ than a verdict on the diff alone.
192
+
184
193
  *Paste the turn.* Run `plumbbob handoff` and paste its output whole, verbatim, at top
185
194
  level, trailing blank line included, then end the turn. Never nest it inside a fence of
186
195
  your own; it carries fences of its own and they cannot nest. It prints the Now line
@@ -223,8 +232,11 @@ switch with `/model` and rerun to honor it, or wave you on. Advisory, never a ga
223
232
 
224
233
  **Then hand off with the next model.** `plumbbob checkpoint` prints the whole boundary
225
234
  ending itself: `**Checkpoint**: Step N complete (<sha>)`, the Verdict the step earned,
226
- any advisory (a staged path outside the seam is the common one), and Next Up. Relay
227
- that block whole, run no second command, and write nothing around it. It is the
235
+ any advisory (a staged path outside the seam is the common one), the Stop part when
236
+ this landing completes a range (`**Stop**: Range 3 of 5 landed - <its stop>`, the reason
237
+ the build stops here), and Next Up. Relay that block whole, the Stop part with the rest
238
+ (the relay check holds a boundary that dropped it), run no second command, and write
239
+ nothing around it. It is the
228
240
  orientation tier (no Your Call block, no recommendation), and it points at the next
229
241
  undone step carrying that step's `- model:` recommendation (the plan's
230
242
  smallest-model-that-fits call) so the human knows which `/model` to select before
@@ -287,7 +299,18 @@ and approves in the human's place**, and it **chains**:
287
299
  advance the loop), or a new decision is needed. The step is still in flight and the
288
300
  human has a call to make about it, so end on the same turn the default pause does:
289
301
  write the detail file, run `plumbbob handoff`, and paste its block.
290
- - **A clean halt lands its step first.** The top of a requested range is reached, or no
302
+ - **A call the plan reserves halts at its pause.** A step the plan's `## Build ranges`
303
+ names as a bare range (`/plumbbob:build 10`) is a decision the plan left for the human,
304
+ and `plumbbob build` says so as it enters the step (`step 10 ends at its pause, where
305
+ the plan reserves the call for the human`). Build it, verify it, and end on the same
306
+ turn a halt on trouble does: write the detail file, run `plumbbob handoff`, and paste
307
+ its block. The latch would let it land, since `--auto` grants every step, so keeping
308
+ the step open is yours to do
309
+ ([D94 (auto-and-stops)](https://github.com/robmclarty/plumbbob/blob/main/docs/decisions.md#d94)).
310
+ A range that lands its steps runs through, since its stop is a review that can wait
311
+ for the end of the run, and a typed range stays exactly as typed.
312
+ - **A clean halt lands its step first.** The top of a requested range is reached (the
313
+ next step in build order is one the range does not name, or none is left), or no
291
314
  planned steps remain, and `plumbbob build` names that step as it enters it (for
292
315
  example `step 3 is the last undone step in the plan`). That step checkpoints like
293
316
  every step before it, and its
@@ -302,36 +325,64 @@ only because the human asked for it by name. A range hands the clock back once i
302
325
  step has landed: the step after it waits for the human's next run. The default (no flag,
303
326
  no range) always ends at the pause.
304
327
 
305
- ### A step range (`N-M`) is a bounded `--auto`
328
+ ### A step range (`N-M`, or a list like `20,5-9`) is a bounded `--auto`
306
329
 
307
330
  `/plumbbob:build 1-3` self-approves steps 1 through 3 exactly as `--auto` does, then **stops
308
- once step 3 has landed** instead of chaining to done. The range *is* the opt-in; you do not
309
- also pass `--auto`. Run it as the `--auto` loop with one extra halt: **stop before building
310
- any next step whose plan number is past the top of the range.** It adds no machinery: just
311
- the one more entry already in the halt list above, and it is a clean halt, so the top step
312
- is never held back for approval. The human approved it by typing the range, and
313
- `plumbbob build` says so as it enters that step:
314
- `step 3 is the top of the range you granted`, or, when the plan ends first,
331
+ once step 3 has landed** instead of chaining to done. A range may also list its steps, numbers
332
+ and spans joined by commas (`/plumbbob:build 20,5-9`, typed with or without a space after a
333
+ comma), and it grants exactly the steps it names: the checkpoint refuses any other
334
+ ([D91 (step-list-grants)](https://github.com/robmclarty/plumbbob/blob/main/docs/decisions.md#d91)).
335
+ The range *is* the opt-in; you do not also pass `--auto`.
336
+
337
+ **A run hands you a range the plan names, walked the same way.** `/plumbbob:run` (the run
338
+ skill) reads this section for its walk: its grant is the `steps` list the turn hook minted
339
+ from the plan's `## Build ranges` when the human typed the run, in the normal form a typed
340
+ list mints, so the latch, `plumbbob build`'s notice at the top step, and every halt below
341
+ read it exactly as they read a typed range
342
+ ([D96 (plan-names-the-grant)](https://github.com/robmclarty/plumbbob/blob/main/docs/decisions.md#d96)).
343
+
344
+ Run it as the `--auto` loop, walked in build order. **Start at the range's first undone step
345
+ in build order, however it is written** (`5-9,20` under an order that puts 20 first starts at
346
+ 20), and after each landing take the next undone step in build order. Build it if the range
347
+ names it; **stop before building any step the range does not name**, or once none is left. It
348
+ adds no machinery: just the one more entry already in the halt list above, and it is a clean
349
+ halt, so the top step is never held back for approval. The human approved it by typing the
350
+ range, and `plumbbob build` says so as it enters that step:
351
+ `step 3 is the top of the range you granted`, or, when the walk ends early (the plan runs
352
+ out, or the next step in build order is one the range does not name),
315
353
  `the last undone step in the range you granted`.
316
354
 
317
- - **`N-M` with N ≤ M**: build N…M, self-approving and checkpointing each, M included, then
318
- relay M's checkpoint block and end the turn. `N-N` is a range like any other: it builds
319
- and lands the single step `N`. Only a bare `/plumbbob:build N` ends at the pause.
320
- - **N > M** (for example `3-1`): that is not a range you can walk; report it and stop rather
321
- than guess the intent.
322
- - **M past the last planned step** (for example `1-9` with three steps): build through the
323
- last planned step and stop; that is the existing "no planned steps remain" halt, not
324
- an error.
325
- - **A step inside the range won't build** (missing, or its seam won't parse): `plumbbob
326
- build <n>` fails exactly as it always does; surface it and stop, the same as a red
327
- check. Do not skip past the gap.
328
- - **N is above the next undone step**: you are jumping over earlier planned work; note
329
- that you are skipping the steps before N, then proceed (the same latitude as a
355
+ - **A range that parses**: build each step it names in build order, self-approving and
356
+ checkpointing each, the last included, then relay the last one's checkpoint block and end
357
+ the turn. `N-N` is a range like any other: it builds and lands the single step `N`. Only a
358
+ bare `/plumbbob:build N` ends at the pause.
359
+ - **A range that does not parse** (for example `3-1`, or `1--3`): that is not a range you can
360
+ walk, and it grants nothing; report it and stop rather than guess the intent.
361
+ - **A number past the last planned step** (for example `1-9` with three steps): build through
362
+ the last planned step the range names and stop; that is the existing "no planned steps
363
+ remain" halt, not an error. A number the plan lacks is never reached.
364
+ - **A step inside the range won't build** (its seam won't parse): `plumbbob build <n>` fails
365
+ exactly as it always does; surface it and stop, the same as a red check. Do not skip past
366
+ the gap.
367
+ - **The range starts above the next undone step**: you are jumping over earlier planned work;
368
+ note that you are skipping the steps before it, then proceed (the same latitude as a
330
369
  single-number jump like `/plumbbob:build 4`).
331
- - **The build order puts a number past the top ahead of the rest** (the order reads
332
- `1, 4, 2, 3` under `/plumbbob:build 1-3`): a range names plan numbers, so the halt above
333
- fires before step 4 and `plumbbob build` names step 1 as the last undone step in the
334
- range. Build a re-sequenced stretch step by step, or grant `--auto`.
370
+ - **The build order puts a step the range does not name inside it** (the order reads
371
+ `1, 4, 2, 3` under `/plumbbob:build 1-3`): the halt above fires before step 4, and
372
+ `plumbbob build` names step 1 as the last undone step in the range. The range that walks
373
+ the whole stretch lists step 4 too (`/plumbbob:build 1-4`, or `/plumbbob:build 1,4,2,3`).
374
+ - **At a range's stop, harvest before the next range.** No harvest runs between the steps
375
+ inside a range, so the parks pile up, and a blocker among them would otherwise ride into
376
+ the next one. A run's range ends on the stop card, which lists every open park with a
377
+ proposed class, and the reply that settles the card records them (the run skill's
378
+ section on the reply at a stop). A range landed here, by a typed range, ends on the plain
379
+ boundary instead; when the human asks what comes next there with parks open (the note's
380
+ `parked 2 of 7 open`), the move is `/plumbbob:harvest`, then the run the card names.
381
+ - **After an early stop, settle the step, then rerun what is left.** A halt on trouble
382
+ inside a range ends at that step's pause, and the grant ends with the turn. Once the
383
+ human approves and the step lands, the note beneath Next Up names the run that lands
384
+ what is left of the range (`· range 3 of 5: /plumbbob:run (steps 8-9)`), and that is what
385
+ the human types to carry on; `/plumbbob:build 8-9` lands the same steps by hand.
335
386
 
336
387
  ## The hard contracts
337
388
 
@@ -2,7 +2,7 @@
2
2
  name: finish
3
3
  description: "Finish the build: write the report (what shipped, decisions, parked/harvested items, deferred tangents), then make the final commit that closes the session. The build folder rides the branch into the PR: no separate archive. Report by default, no gate."
4
4
  disable-model-invocation: true
5
- allowed-tools: Read, Write, Bash(plumbbob status:*), Bash(plumbbob finish:*)
5
+ allowed-tools: Read, Edit, Write, Bash(plumbbob status:*), Bash(plumbbob harvest:*), Bash(plumbbob order:*), Bash(plumbbob finish:*)
6
6
  ---
7
7
 
8
8
  # PlumbBob: finish the build (the close-out)
@@ -17,7 +17,21 @@ record.
17
17
 
18
18
  ## What this skill does, in order
19
19
 
20
- 1. **Write the report** to the active build's `report.md`
20
+ 1. **Settle a stop card still waiting.** Read `.plumbbob/detail.md`. When it opens on a
21
+ `# Detail · Range <N>` header, the last range's stop card is waiting for its reply, and
22
+ `/plumbbob:finish` typed at it is that reply, the move its Your Call block names
23
+ ([D102 (settle-in-one-reply)](https://github.com/robmclarty/plumbbob/blob/main/docs/decisions.md#d102)).
24
+ Record it the way `looks good` would before anything else: fold each blocker its
25
+ `## Harvest` section proposes into `intent.md` by the harvest skill's rules (read
26
+ `../harvest/SKILL.md` from this skill's base directory), then run `plumbbob harvest`,
27
+ which writes the range's review into the Log and flips the parks the card settled. A
28
+ finish that skipped it would leave the last range's review out of the Log and its parks
29
+ open in the report. When `plumbbob harvest` refuses with `step 13 joined it after it
30
+ landed`, a blocker went into the range the card reviews: move that step to a range
31
+ still to come, or a bare range of its own, leave the detail file as it is, fold nothing
32
+ again, and run `plumbbob harvest` once more. On any other refusal, report what it names
33
+ and stop before the report. Any other detail file, or none, has nothing waiting.
34
+ 2. **Write the report** to the active build's `report.md`
21
35
  (`.plumbbob/builds/<slug>/report.md`), from `intent.md` + `build-log.md`.
22
36
  The build-log's `## Log` is already the chronological history; `plumbbob checkpoint`
23
37
  wrote a dated line for every step as it landed. **Read it as the spine; do not
@@ -34,7 +48,7 @@ record.
34
48
  `## Stats` (the per-step receipts) itself, so do **not** hand-write either; a report
35
49
  that spells them out collides with the CLI's own tail. This is the "yeah, I did that"
36
50
  artifact. Write it by default; the human may edit it.
37
- 2. **Finish.** Run `plumbbob finish`, which appends the checkpoint SHAs to the
51
+ 3. **Finish.** Run `plumbbob finish`, which appends the checkpoint SHAs to the
38
52
  report and makes the final commit (subject `chore(<scope>): finish`, its body
39
53
  led by a `plumbbob finish` marker plus an optional proportional `--body`), then
40
54
  clears the control state (the in-flight markers, then `STATE` last; one delete
@@ -58,7 +72,7 @@ record.
58
72
  cannot analyze the extra redirect, so it blocks the command or stops to ask instead of
59
73
  letting this skill's allowance cover it. Omit `--body` entirely and the commit carries
60
74
  the `plumbbob finish` marker subject only.
61
- 3. **Point at the next goal.** Relay `plumbbob finish`'s output verbatim, blank lines
75
+ 4. **Point at the next goal.** Relay `plumbbob finish`'s output verbatim, blank lines
62
76
  included: its lead line, which names the branch the tracked folder now rides into the
63
77
  PR, any advisory beneath it, and `**Next Up**: Nothing planned - /plumbbob:plan`. That
64
78
  is the orientation tier's close (the
@@ -1,8 +1,8 @@
1
1
  ---
2
2
  name: harvest
3
- description: "Triage the park list at a step boundary: propose one class (blocker/tangent/pivot) per parked item, write only after the human confirms each, record under ## Harvest, and fold a confirmed blocker into intent."
3
+ description: "Triage the park list at a step boundary: propose one class (blocker/tangent/pivot) for every open parked item at once, and once the human settles them in one reply, fold a confirmed blocker into intent and record the rest with plumbbob harvest. A run's stop card carries its own harvest."
4
4
  disable-model-invocation: true
5
- allowed-tools: Read, Edit, Bash(plumbbob status:*), Bash(plumbbob order:*)
5
+ allowed-tools: Read, Edit, Write, Bash(plumbbob status:*), Bash(plumbbob order:*), Bash(plumbbob harvest:*)
6
6
  ---
7
7
 
8
8
  # PlumbBob: harvest the park list
@@ -23,10 +23,21 @@ DESIGN boundary, not mid-step. Read the dashboard injected above:
23
23
  Chasing parked items mid-step is the disease parking prevents.
24
24
  - At the boundary (the dashboard reads `[DESIGN]`): go ahead.
25
25
 
26
+ A range's stop is the boundary that matters most. No harvest runs between the steps
27
+ inside a range, so the parks it piled up wait for the stop where it lands, and a
28
+ blocker among them is caught there before the next range builds on it. A run's stop card
29
+ carries its harvest
30
+ ([D100 (stop-card)](https://github.com/robmclarty/plumbbob/blob/main/docs/decisions.md#d100)):
31
+ every open park is on the card with a proposed lean, and the reply that settles the card
32
+ records them, so there is nothing left for this skill to do there. If
33
+ `.plumbbob/detail.md` opens on a `# Detail · Range <N>` header, that card is still waiting:
34
+ **refuse**, since writing this skill's detail file would overwrite the range's review, and
35
+ point the human at the card's own reply (`looks good`, or a change to a lean).
36
+
26
37
  ## What this skill does
27
38
 
28
- Walk the **Park list** in `build-log.md` item by item and, for each, **propose exactly
29
- one class**:
39
+ Walk the **Park list** in `build-log.md` and, for every open item, **propose exactly one
40
+ class**, all of them at once:
30
41
 
31
42
  - **blocker**: the plan was wrong or incomplete; can't proceed. Folds into `intent.md`
32
43
  and is handled now.
@@ -35,18 +46,50 @@ one class**:
35
46
 
36
47
  ## The one hard contract
37
48
 
38
- You **propose**; the **human calls it**. For each item, state your proposed class and
39
- one line of reasoning, then **wait for the human to confirm or override**. Write
40
- **only after** per-item confirmation:
49
+ You **propose**; the **human calls it**. Propose a class for every open item at once,
50
+ with one line of reasoning each, and the human settles them all in one reply
51
+ ([D102 (settle-in-one-reply)](https://github.com/robmclarty/plumbbob/blob/main/docs/decisions.md#d102)):
52
+ one reply per item is what made a harvest a chore, and seeing every proposal at once keeps
53
+ the call on each one theirs. The detail file you write first is the proposal; record
54
+ **only after** that reply confirms them:
41
55
 
42
- - Record each confirmed class in `build-log.md`'s `## Harvest` section.
43
- - **Flip the harvested item** from `- [ ]` to `- [x]` in the Park list, so `/plumbbob:status`
44
- stops counting it as open.
56
+ - Write the proposals into `.plumbbob/detail.md` first, under a `# Detail · Harvest`
57
+ header, one line per open item in a `## Harvest` section:
58
+ `- P6 → tangent: <the reason, one clause>`, the class written `blocker`, `tangent`,
59
+ or `pivot` (for a pivot signal). A park's handle is `P` and its place in the
60
+ Park list, counting the harvested lines too (the count the note sizes as
61
+ `parked 3 of 8 open`), and it is the one name the detail file, the turn, and the
62
+ human's reply all use
63
+ ([D105 (park-handles)](https://github.com/robmclarty/plumbbob/blob/main/docs/decisions.md#d105)).
64
+ Then list the same proposals in the turn, each with its handle and the park's text, and
65
+ end the turn there.
66
+ - A reply that changes a proposal (`make P7 a tangent`) is the human's call: rewrite
67
+ that line, list the proposals again, and end the turn. A reply that confirms them
68
+ (`looks good`) is what lets you write.
69
+ - On that confirmation, run `plumbbob harvest`
70
+ ([D104 (harvest-records)](https://github.com/robmclarty/plumbbob/blob/main/docs/decisions.md#d104)).
71
+ It flips each settled item from `- [ ]` to `- [x]` in the Park list, so
72
+ `/plumbbob:status` stops counting it as open, and records each confirmed class under
73
+ `build-log.md`'s `## Harvest` section. Relay its ending whole, the `**Recorded**:` line
74
+ and the Next Up beneath it. It refuses a proposal naming a handle the list lacks (`P9`
75
+ with eight parked) or a class other than blocker, tangent, or pivot; fix the line it
76
+ names and run it again.
45
77
  - A confirmed **blocker** also folds its decision into `intent.md`, slugged at birth
46
78
  like any Decision, anchored the same way
47
79
  (`- <a id="d5"></a>**D5 (retry-cap)**: <the call>, *because* <why>`), never a bare `D5`.
48
80
  A blocker that becomes a new step takes the next free number under `## Steps` and,
49
81
  when it has to land before an existing undone step, its place in the sequence through
50
- `plumbbob order 7 8 5` (the `## Build order` line), never by renumbering.
82
+ `plumbbob order 7 8 5` (the `## Build order` line), never by renumbering. When the plan
83
+ carries `## Build ranges`, the new step goes into a range too: a range still to come
84
+ (the one it builds with), or a bare range of its own when it brings a call the human
85
+ has to make before it lands, and never a range that has landed, since that range's call
86
+ was made and the step would reopen it. A new range takes the next free item number, and
87
+ no item is renumbered, because the stop card's stamp, the detail file's header, and
88
+ `plumbbob harvest` find a landed range by its number. A range the step breaks is re-cut
89
+ in the same confirmed edit
90
+ ([D92 (build-ranges)](https://github.com/robmclarty/plumbbob/blob/main/docs/decisions.md#d92));
91
+ `plumbbob status` afterward carries a `⚠` row for any range still broken.
92
+ - Fold a confirmed blocker into `intent.md` before you run `plumbbob harvest`, so the
93
+ record and the plan agree when it lands.
51
94
  - Never reclassify or resolve an item the human hasn't confirmed, and default every
52
95
  uncertain item to **tangent**, never to blocker.