plumbbob 0.13.0 → 0.13.1

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,39 @@
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
2
+ // UserPromptSubmit event, it runs once per prompt and maintains two of the
3
3
  // sidecar's git-excluded control files (flat files under `.plumbbob/`): it ticks
4
4
  // `TURN`, the monotonic count of human turns the checkpoint latch compares
5
5
  // against a step's entry stamp to know a human turn intervened since the step
6
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
7
+ // self-approval only when the human typed `/build --auto` or a step range
8
+ // (`N-M`, or a list such as `20,5-9`), clearing it otherwise (those strings can only come from a human
9
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.
10
+ // could forge would be no grant). The harness also sends a background job's
11
+ // completion notice through this event, as a prompt the human never typed, so
12
+ // a notice ticks nothing, keeps the grant, and clears only the ending `RELAY`
13
+ // holds from an earlier turn. When a step is in flight a typed prompt also
14
+ // emits ONE `UserPromptSubmit` additionalContext line so a fresh session
15
+ // (post-compaction, or a scripted `-p` turn where no skill prose is in context)
16
+ // still knows the beat: a tangent is parked, not chased, and a message that
17
+ // also asks something is answered before the park's block. Guidance only: it
18
+ // blocks nothing. It ALWAYS exits 0 and any failure degrades to no output, so
19
+ // neither the tick nor the emit can ever wedge prompting.
17
20
  import { readFileSync, writeFileSync } from 'node:fs';
18
- import { findSessionRoot, setGrant, stepPath, turnPath } from "../lib/sidecar.js";
21
+ import { clearRelay, findSessionRoot, notePrompt, readPrompt, setGrant, stepPath, turnPath, } from "../lib/sidecar.js";
22
+ import { normalizeStepList, readBuildArgs } from "../lib/steplist.js";
19
23
  /**
20
- * The hook entry point: apply the tick, then emit the in-flight-step nudge.
24
+ * The hook entry point: apply the prompt, then emit the in-flight-step nudge
25
+ * when it counted as a human turn.
21
26
  *
22
27
  * Always returns 0: a broken turn must never wedge a prompt, so any failure is
23
28
  * swallowed and degrades to no output.
24
29
  */
25
30
  export function turn(cwd, _args) {
26
31
  try {
27
- applyTurn(cwd, readStdin());
28
- const context = stepInFlightContext(cwd);
29
- if (context !== null)
30
- process.stdout.write(context);
32
+ if (applyTurn(cwd, readStdin())) {
33
+ const context = stepInFlightContext(cwd);
34
+ if (context !== null)
35
+ process.stdout.write(context);
36
+ }
31
37
  }
32
38
  catch {
33
39
  // A broken turn must never wedge a prompt: swallow and exit 0.
@@ -78,7 +84,17 @@ function inFlightStep(root) {
78
84
  }
79
85
  }
80
86
  /**
81
- * The testable core: apply one tick given the raw hook stdin.
87
+ * The testable core: apply one prompt given the raw hook stdin, and say
88
+ * whether it counted as a human turn.
89
+ *
90
+ * A typed prompt ticks TURN and rewrites GRANT. A notice touches neither,
91
+ * because the latch reads TURN passing a step's entry stamp as its one sign
92
+ * that a human answered, and a grant the human typed has to outlive a job
93
+ * that reports back partway through the range. A notice that starts a turn of
94
+ * its own clears RELAY, whose ending was judged when its own turn ended; one
95
+ * the harness folds into the running turn arrives under that turn's prompt id
96
+ * and leaves RELAY alone, since the ending the turn just printed has yet to be
97
+ * judged.
82
98
  *
83
99
  * Split from the fd-0 read so the unit tests can drive it directly: an
84
100
  * in-process test cannot feed fd 0 (the same constraint checkpoint's `--body`
@@ -87,10 +103,35 @@ function inFlightStep(root) {
87
103
  export function applyTurn(cwd, raw) {
88
104
  const root = findSessionRoot(cwd);
89
105
  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;
106
+ return false; // no active session above cwd: silent no-op.
107
+ const { prompt, promptId } = readPayload(raw);
108
+ const notice = isNotice(prompt);
109
+ if (notice) {
110
+ if (promptId === null || promptId !== readPrompt(root))
111
+ clearRelay(root);
112
+ }
113
+ else {
114
+ writeFileSync(turnPath(root), `${readTurn(root) + 1}\n`);
115
+ setGrant(root, grantFromPrompt(prompt));
116
+ }
117
+ // Last, so a record that cannot be written never costs the tick or the grant.
118
+ notePrompt(root, promptId);
119
+ return !notice;
120
+ }
121
+ /**
122
+ * Whether a prompt is the harness reporting on background work rather than
123
+ * the human typing.
124
+ *
125
+ * Nothing in the payload marks one, but every notice opens on a frame of the
126
+ * harness's own at its very first character: `<task-notification>` for a
127
+ * finished shell job, a Monitor event, or a subagent's task, and
128
+ * `<agent-message ` for a subagent's or another session's report. A misread
129
+ * errs safe: a typed prompt taken for a notice costs one tick and one grant,
130
+ * so the latch only grows stricter, while a notice taken for a typed prompt
131
+ * is a turn no human took.
132
+ */
133
+ export function isNotice(prompt) {
134
+ return prompt.startsWith('<task-notification>') || prompt.startsWith('<agent-message ');
94
135
  }
95
136
  /**
96
137
  * Derive the GRANT content from the human's prompt, or null to clear it.
@@ -98,9 +139,12 @@ export function applyTurn(cwd, raw) {
98
139
  * A grant is minted only from a literal `build` invocation the human typed:
99
140
  * `build` is disable-model-invocation, so this string can only come from a
100
141
  * 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.
142
+ * is no grant. A range mints `steps <list>`, exactly the steps it names in
143
+ * their normal form (`20, 5-9` mints `steps 5-9,20`), and beats `--auto`
144
+ * (bounded wins), so a range that does not parse mints nothing rather than
145
+ * falling back to `auto`. A bare `/build`, a bare step number, or any
146
+ * non-invocation prompt mints nothing. The namespaced `/plumbbob:build` form is
147
+ * honored alongside the bare one.
104
148
  *
105
149
  * The invocation must *start* a token: a slash preceded by a word character,
106
150
  * `/`, `.`, `-`, or `~` is a path segment (`src/build`, `./build`), not a
@@ -120,43 +164,30 @@ export function grantFromPrompt(prompt) {
120
164
  if (invocation === null)
121
165
  return null;
122
166
  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
141
- }
142
- if (ceiling !== undefined)
143
- return `range ${ceiling}`;
144
- return auto ? 'auto' : null;
167
+ const args = readBuildArgs(line);
168
+ if (args.range)
169
+ return args.steps === null ? null : `steps ${normalizeStepList(args.steps)}`;
170
+ return args.auto ? 'auto' : null;
145
171
  }
146
172
  /**
147
- * Extract the `prompt` field from the UserPromptSubmit JSON, or '' otherwise.
173
+ * The `prompt` and `prompt_id` fields of the UserPromptSubmit JSON, as '' and
174
+ * null when either is absent.
148
175
  *
149
176
  * 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.
177
+ * wedging the tool: the tick still lands (a human turn did occur), the empty
178
+ * prompt clears any GRANT, and a missing id records none; the verb never
179
+ * throws over bad stdin.
152
180
  */
153
- function extractPrompt(raw) {
181
+ function readPayload(raw) {
154
182
  try {
155
183
  const parsed = JSON.parse(raw);
156
- return typeof parsed.prompt === 'string' ? parsed.prompt : '';
184
+ return {
185
+ prompt: typeof parsed.prompt === 'string' ? parsed.prompt : '',
186
+ promptId: typeof parsed.prompt_id === 'string' && parsed.prompt_id !== '' ? parsed.prompt_id : null,
187
+ };
157
188
  }
158
189
  catch {
159
- return '';
190
+ return { prompt: '', promptId: null };
160
191
  }
161
192
  }
162
193
  /**
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "plumbbob",
3
- "version": "0.13.0",
3
+ "version": "0.13.1",
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,9 +35,10 @@ 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.
@@ -181,6 +182,12 @@ switch with `/model` and rerun to honor it, or wave you on. Advisory, never a ga
181
182
  line in the build-log's `## Log` and then truncates it, so the detail lands in the
182
183
  tracked ledger rather than piling up on disk.
183
184
 
185
+ **A bare range's pause is its stop.** When the injected dashboard's ranges block lists
186
+ this step as a bare range (`/plumbbob:build 5`, its row marked `▸`), the plan reserved a
187
+ call for this pause, and the `stop:` line beneath the row names it. The recommendation
188
+ speaks to that call: the move you would take on the plan's question, and why, rather
189
+ than a verdict on the diff alone.
190
+
184
191
  *Paste the turn.* Run `plumbbob handoff` and paste its output whole, verbatim, at top
185
192
  level, trailing blank line included, then end the turn. Never nest it inside a fence of
186
193
  your own; it carries fences of its own and they cannot nest. It prints the Now line
@@ -223,8 +230,11 @@ switch with `/model` and rerun to honor it, or wave you on. Advisory, never a ga
223
230
 
224
231
  **Then hand off with the next model.** `plumbbob checkpoint` prints the whole boundary
225
232
  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
233
+ any advisory (a staged path outside the seam is the common one), the Stop part when
234
+ this landing completes a range (`**Stop**: Range 3 of 5 landed - <its stop>`, the reason
235
+ the build stops here), and Next Up. Relay that block whole, the Stop part with the rest
236
+ (the relay check holds a boundary that dropped it), run no second command, and write
237
+ nothing around it. It is the
228
238
  orientation tier (no Your Call block, no recommendation), and it points at the next
229
239
  undone step carrying that step's `- model:` recommendation (the plan's
230
240
  smallest-model-that-fits call) so the human knows which `/model` to select before
@@ -287,7 +297,18 @@ and approves in the human's place**, and it **chains**:
287
297
  advance the loop), or a new decision is needed. The step is still in flight and the
288
298
  human has a call to make about it, so end on the same turn the default pause does:
289
299
  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
300
+ - **A call the plan reserves halts at its pause.** A step the plan's `## Build ranges`
301
+ names as a bare range (`/plumbbob:build 10`) is a decision the plan left for the human,
302
+ and `plumbbob build` says so as it enters the step (`step 10 ends at its pause, where
303
+ the plan reserves the call for the human`). Build it, verify it, and end on the same
304
+ turn a halt on trouble does: write the detail file, run `plumbbob handoff`, and paste
305
+ its block. The latch would let it land, since `--auto` grants every step, so keeping
306
+ the step open is yours to do
307
+ ([D94 (auto-and-stops)](https://github.com/robmclarty/plumbbob/blob/main/docs/decisions.md#d94)).
308
+ A range that lands its steps runs through, since its stop is a review that can wait
309
+ for the end of the run, and a typed range stays exactly as typed.
310
+ - **A clean halt lands its step first.** The top of a requested range is reached (the
311
+ next step in build order is one the range does not name, or none is left), or no
291
312
  planned steps remain, and `plumbbob build` names that step as it enters it (for
292
313
  example `step 3 is the last undone step in the plan`). That step checkpoints like
293
314
  every step before it, and its
@@ -302,36 +323,54 @@ only because the human asked for it by name. A range hands the clock back once i
302
323
  step has landed: the step after it waits for the human's next run. The default (no flag,
303
324
  no range) always ends at the pause.
304
325
 
305
- ### A step range (`N-M`) is a bounded `--auto`
326
+ ### A step range (`N-M`, or a list like `20,5-9`) is a bounded `--auto`
306
327
 
307
328
  `/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,
329
+ once step 3 has landed** instead of chaining to done. A range may also list its steps, numbers
330
+ and spans joined by commas (`/plumbbob:build 20,5-9`, typed with or without a space after a
331
+ comma), and it grants exactly the steps it names: the checkpoint refuses any other
332
+ ([D91 (step-list-grants)](https://github.com/robmclarty/plumbbob/blob/main/docs/decisions.md#d91)).
333
+ The range *is* the opt-in; you do not also pass `--auto`.
334
+
335
+ Run it as the `--auto` loop, walked in build order. **Start at the range's first undone step
336
+ in build order, however it is written** (`5-9,20` under an order that puts 20 first starts at
337
+ 20), and after each landing take the next undone step in build order. Build it if the range
338
+ names it; **stop before building any step the range does not name**, or once none is left. It
339
+ adds no machinery: just the one more entry already in the halt list above, and it is a clean
340
+ halt, so the top step is never held back for approval. The human approved it by typing the
341
+ range, and `plumbbob build` says so as it enters that step:
342
+ `step 3 is the top of the range you granted`, or, when the walk ends early (the plan runs
343
+ out, or the next step in build order is one the range does not name),
315
344
  `the last undone step in the range you granted`.
316
345
 
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
346
+ - **A range that parses**: build each step it names in build order, self-approving and
347
+ checkpointing each, the last included, then relay the last one's checkpoint block and end
348
+ the turn. `N-N` is a range like any other: it builds and lands the single step `N`. Only a
349
+ bare `/plumbbob:build N` ends at the pause.
350
+ - **A range that does not parse** (for example `3-1`, or `1--3`): that is not a range you can
351
+ walk, and it grants nothing; report it and stop rather than guess the intent.
352
+ - **A number past the last planned step** (for example `1-9` with three steps): build through
353
+ the last planned step the range names and stop; that is the existing "no planned steps
354
+ remain" halt, not an error. A number the plan lacks is never reached.
355
+ - **A step inside the range won't build** (its seam won't parse): `plumbbob build <n>` fails
356
+ exactly as it always does; surface it and stop, the same as a red check. Do not skip past
357
+ the gap.
358
+ - **The range starts above the next undone step**: you are jumping over earlier planned work;
359
+ note that you are skipping the steps before it, then proceed (the same latitude as a
330
360
  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`.
361
+ - **The build order puts a step the range does not name inside it** (the order reads
362
+ `1, 4, 2, 3` under `/plumbbob:build 1-3`): the halt above fires before step 4, and
363
+ `plumbbob build` names step 1 as the last undone step in the range. The range that walks
364
+ the whole stretch lists step 4 too (`/plumbbob:build 1-4`, or `/plumbbob:build 1,4,2,3`).
365
+ - **At a range's stop, harvest before the next range.** No harvest runs between the steps
366
+ inside a range, so the parks pile up, and a blocker among them would otherwise ride into
367
+ the next one. When the human asks what comes next at a stop with parks open (the note's
368
+ `parked 2 of 7 open`), the move is `/plumbbob:harvest`, then the range the card names.
369
+ - **After an early stop, settle the step, then rerun what is left.** A halt on trouble
370
+ inside a range ends at that step's pause, and the grant ends with the turn. Once the
371
+ human approves and the step lands, the note beneath Next Up names what is left of the
372
+ range (`· range 3 of 5: /plumbbob:build 8-9`), and that is the command the human types
373
+ to carry on.
335
374
 
336
375
  ## The hard contracts
337
376
 
@@ -23,6 +23,10 @@ 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.
29
+
26
30
  ## What this skill does
27
31
 
28
32
  Walk the **Park list** in `build-log.md` item by item and, for each, **propose exactly
@@ -47,6 +51,11 @@ one line of reasoning, then **wait for the human to confirm or override**. Write
47
51
  (`- <a id="d5"></a>**D5 (retry-cap)**: <the call>, *because* <why>`), never a bare `D5`.
48
52
  A blocker that becomes a new step takes the next free number under `## Steps` and,
49
53
  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.
54
+ `plumbbob order 7 8 5` (the `## Build order` line), never by renumbering. When the plan
55
+ carries `## Build ranges`, the new step goes into a range too (the one it builds with,
56
+ or a bare range of its own when it brings a call the human has to make before it
57
+ lands), and a range it breaks is re-cut in the same confirmed edit
58
+ ([D92 (build-ranges)](https://github.com/robmclarty/plumbbob/blob/main/docs/decisions.md#d92));
59
+ `plumbbob status` afterward carries a `⚠` row for any range still broken.
51
60
  - Never reclassify or resolve an item the human hasn't confirmed, and default every
52
61
  uncertain item to **tangent**, never to blocker.
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  name: order
3
3
  description: "Set the build order: turn the human's sequence (or a relative one, such as 7 and 8 before 5) into the full list of undone steps and write it by shelling `plumbbob order`, never by editing intent.md. A step keeps its number; the sequence moves."
4
- argument-hint: "[steps in the sequence to build them]"
4
+ argument-hint: "[steps in the sequence to build them, such as 1, 6, 2-4, 7, 5]"
5
5
  disable-model-invocation: true
6
6
  model: haiku
7
7
  allowed-tools: Bash(plumbbob status:*), Bash(plumbbob order:*)
@@ -27,20 +27,30 @@ after the step in flight is the usual reason to run it.
27
27
  ## What this skill does
28
28
 
29
29
  1. **Compose the full sequence.** Read the step list and the `build order` row on the
30
- dashboard. Take the human's instruction, whether a full list (`7 8 5 10 6`) or a relative
31
- one ("7 and 8 before 5", "10 last"), and turn it into **every undone step, in the sequence
32
- to build them**. Never renumber a step and never move one inside `## Steps`; the sequence
33
- lives in this one line.
30
+ dashboard. Take the human's instruction, whether a full list (`7 8 5 10 6`, or
31
+ `1, 6, 2-4, 7, 5` with spans and commas) or a relative one ("7 and 8 before 5", "10
32
+ last"), and turn it into **every undone step, in the sequence to build them**. Never
33
+ renumber a step and never move one inside `## Steps`; the sequence lives in this one
34
+ line.
34
35
  2. **Show the sequence and get the human's OK in-turn.** When the human's own message states
35
36
  the full sequence, that message **is** the approval: run the verb directly.
36
- 3. **Write it by shelling `plumbbob order 7 8 5 10 6`** via Bash. `plumbbob order --reset`
37
- drops the line, and the plan reads in document order again.
37
+ 3. **Write it by shelling `plumbbob order 7 8 5 10 6`** via Bash. The verb reads spans and
38
+ commas, with or without a space after one, so a list typed as `1, 6, 2-4, 7, 5` goes to it
39
+ as typed, and the line it writes spells the span out. `plumbbob order --reset` drops the
40
+ line, and the plan reads in document order again.
38
41
  4. **Relay the ending verbatim, then stop.** The verb prints its whole ending: the
39
42
  `**Build order**: 7, 8, 5, 10, 6` line, a blank line, and the `**Next Up**` pointer (back
40
43
  at the step in flight, or forward to the first step in the new sequence). Run no second
41
- command. A refusal (a number the plan lacks, a repeat, a step already checkpointed) is the
42
- CLI's call: report it verbatim, never retry or work around it, and ask the human which
43
- number they meant.
44
+ command but the one in step 5. A refusal (a number the plan lacks, a repeat, a step
45
+ already checkpointed) is the CLI's call: report it verbatim, never retry or work around
46
+ it, and ask the human which number they meant.
47
+ 5. **Read the ranges when the plan has them.** If the dashboard injected above shows a
48
+ `ranges` block, run `plumbbob status` after the verb and read that block again. A `⚠`
49
+ row that was not there before means the new sequence broke a range (most often
50
+ `the build order puts step 11 inside it`). This skill edits nothing, so it cannot re-cut
51
+ one: open the turn with one line naming the range, its drift words, and
52
+ `/plumbbob:refine` as the move that re-cuts it, then the verb's ending, whole, as the
53
+ turn's last text. With no new `⚠` row, or no ranges block, relay the ending alone.
44
54
 
45
55
  ## The one hard contract
46
56
 
@@ -1,6 +1,6 @@
1
1
  ---
2
2
  name: plan
3
- description: "Frame a fresh goal and author the whole plan (Frame, Decisions, Constraints, and all Steps) before any code. Three input modes: no arg interviews you; a file path (or @-mention) absorbs a spec; any other text expands your inline intent."
3
+ description: "Frame a fresh goal and author the whole plan (Frame, Decisions, Constraints, all Steps, and the build ranges) before any code. Three input modes: no arg interviews you; a file path (or @-mention) absorbs a spec; any other text expands your inline intent."
4
4
  argument-hint: "[spec-path | intent]"
5
5
  disable-model-invocation: true
6
6
  allowed-tools: Read, Edit, Write, Bash(plumbbob status:*), Bash(plumbbob handoff:*), Bash(plumbbob start:*), Bash(plumbbob checkpoint:*), Bash(plumbbob agent list:*)
@@ -117,7 +117,48 @@ an agent can follow with `/plumbbob:build`. The argument only seeds how you get
117
117
  It is advisory metadata for the human (`/plumbbob:status` surfaces it before each build),
118
118
  never a gate, and nothing switches models automatically. Write it plain (no
119
119
  backticks) and omit it when any model would do.
120
- 5. **Offer harness bindings** *(optional)*. If the build will lean on
120
+ 5. **Cut the build ranges.** With the steps written, say where the build stops for a
121
+ human call, under `## Build ranges`
122
+ ([D92 (build-ranges)](https://github.com/robmclarty/plumbbob/blob/main/docs/decisions.md#d92)):
123
+ one sentence on how to drive the build, then one numbered item per range, in the
124
+ format the parser reads:
125
+
126
+ ```markdown
127
+ 1. `/plumbbob:build 1`, **stop:** at step 1's pause, approve the schema before anything writes to it
128
+ - model: opus
129
+ 2. `/plumbbob:build 2-5`, **stop:** review the parser before the renderers build on it
130
+ - model: sonnet
131
+ - note: <optional; what the range should watch for>
132
+ ```
133
+
134
+ **A range ends where a human call is due, and nowhere else.** Three things make a
135
+ call: a decision the plan leaves open (an Open question, or a fork only a spike can
136
+ settle), a review before later steps build on what landed, and a thing only the human
137
+ can do, such as a push, a live key, or spending money. A range that runs through one
138
+ has the model make the human's call; a stop that marks none of them is a pause nobody
139
+ needed. The command's form says what kind of stop it is
140
+ ([D85 (range-top-lands)](https://github.com/robmclarty/plumbbob/blob/main/docs/decisions.md#d85)).
141
+ A span such as `2-5` lands every step it names and stops at the boundary after the
142
+ last, so its stop is a review of what landed, or a decision the next steps need before
143
+ they build. A bare `N` builds step N to its pause, so write one for a call that has to
144
+ be made before its step lands, on the step's own work: approving the wording it
145
+ drafted, or the rule it found. Write each stop as what the human decides or reviews
146
+ there, in words they can act on cold.
147
+
148
+ Name every step in exactly one range, by its plan number. A fresh plan has no build
149
+ order, so the ranges follow the numbering. Give each range a `- model:` line naming the
150
+ strongest model any step inside it recommends, written plain the way a step's is,
151
+ because that is the `/model` the human picks before typing the command; any other
152
+ sub-line (a `- note:` on what to watch for) is the reader's, and the CLI leaves it
153
+ alone. Replace the template's placeholder item rather than writing beside it: an item
154
+ still holding its `<...>` fill-ins reads as no range at all.
155
+
156
+ **Size the ranges to the work.** A plan of two or three steps may carry one range, or
157
+ none. When every step would be its own bare range, delete the section: the default
158
+ loop already stops at each step's pause. A range is advice, never a grant
159
+ ([D67 (auto-not-a-grant)](https://github.com/robmclarty/plumbbob/blob/main/docs/decisions.md#d67)):
160
+ the card names the range to type beneath Next Up, and the human still types it.
161
+ 6. **Offer harness bindings** *(optional)*. If the build will lean on
121
162
  user-authored agents, author `harness.json` in the build folder (beside `intent.md`)
122
163
  and review it at the **same plan pause**, alongside the steps; bindings are
123
164
  plan-adjacent configuration, so they converge with the plan. It binds agents to a
@@ -138,7 +179,7 @@ an agent can follow with `/plumbbob:build`. The argument only seeds how you get
138
179
  `note` and decides when to run one mid-build. Skip the file entirely when no step
139
180
  uses an agent; the loop runs identically without it. The plan commit picks it
140
181
  up automatically (it lives in the build folder).
141
- 6. **Commit the plan.** Once the human approves the frame and steps, run
182
+ 7. **Commit the plan.** Once the human approves the frame and steps, run
142
183
  `plumbbob checkpoint --plan` to commit the scaffold on its own: subject
143
184
  `chore(<scope>): plan` (with a `plumbbob plan` body marker), only `.plumbbob/builds/<slug>/`,
144
185
  a `plan <sha>` line in
@@ -148,8 +189,9 @@ an agent can follow with `/plumbbob:build`. The argument only seeds how you get
148
189
  rationale is worth carrying; skip it for a small plan. Do this
149
190
  only on the human's approval; the plan is their convergence.
150
191
  - **The plan pause is a decision turn.** Present the framed plan for the human's call,
151
- then give it the cold read (**§ The cold read**), write the result into
152
- `.plumbbob/detail.md`, and relay `plumbbob handoff --plan`'s block whole: the CLI
192
+ its build ranges with it (each range's command and its stop, since a stop is where
193
+ the human will be asked, and moving one is cheapest now), then give it the cold
194
+ read (**§ The cold read**), write the result into `.plumbbob/detail.md`, and relay `plumbbob handoff --plan`'s block whole: the CLI
153
195
  renders the your-call block with the moves that apply before anything is recorded and
154
196
  prints your recommendation last (the decision tier of the
155
197
  [turn anatomy](https://github.com/robmclarty/plumbbob/blob/main/docs/presentation.md)),
@@ -182,7 +224,7 @@ an agent can follow with `/plumbbob:build`. The argument only seeds how you get
182
224
  (the `--body` earns its keep here), the files staying untracked. Say that
183
225
  instead: the rationale is published in `git log`, not the folder, so pass a
184
226
  fuller `--body` than you otherwise would, since the message is the whole record.
185
- 7. **Offer to stress-test it.** Suggest `/plumbbob:refine` to attack the frame for holes (or
227
+ 8. **Offer to stress-test it.** Suggest `/plumbbob:refine` to attack the frame for holes (or
186
228
  to repair the plan as it drifts). Optional, the human's call.
187
229
 
188
230
  ## The cold read
@@ -198,7 +240,9 @@ Open questions, and keep it to one screen of reading. The lens:
198
240
  - a collision with the existing code (read the files the seams name);
199
241
  - a done-when no test or check can measure, or a seam that names a file that does not
200
242
  exist or misses one that must change;
201
- - a step too large to review in one pass, or a Decision with no *because*.
243
+ - a step too large to review in one pass, or a Decision with no *because*;
244
+ - a range that runs through a call the plan leaves open, so the model would make it;
245
+ - a range that stops where no call is due, a pause the human did not need.
202
246
 
203
247
  Write what you found into `.plumbbob/detail.md`, overwriting whatever a past session
204
248
  left, the findings first and the recommendation last:
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  name: refine
3
- description: "Keep intent.md true: attack the plan for holes (append as Open questions) and refine or repair the Frame, Decisions, Constraints, and Steps to match reality. Usable at any point; you propose, the human approves."
4
- argument-hint: "[focus]"
3
+ description: "Keep intent.md true: attack the plan for holes (append as Open questions) and refine or repair the Frame, Decisions, Constraints, Steps, and build ranges to match reality (`ranges` re-cuts the whole section). Usable at any point; you propose, the human approves."
4
+ argument-hint: "[focus | ranges]"
5
5
  disable-model-invocation: true
6
6
  allowed-tools: Read, Edit, Bash(plumbbob status:*), Bash(plumbbob order:*)
7
7
  ---
@@ -22,7 +22,7 @@ it is `NO ACTIVE SESSION`, **refuse** in one line and tell the human to run
22
22
  `plumbbob start "<title>"` (or `/plumbbob:plan`) first, and edit nothing. Every active state
23
23
  is fine; refining is always available.
24
24
 
25
- ## Two modes
25
+ ## Two modes, and a re-cut
26
26
 
27
27
  - **Attack (diverge in the problem space).** Hand the **Frame**, **Decisions**, and
28
28
  **Architecture sketch** a cold, adversarial read and surface holes: ambiguities,
@@ -57,6 +57,24 @@ is fine; refining is always available.
57
57
  sequence to build them, and the `## Build order` line it writes is what every next-step
58
58
  pick follows. Never renumber, and never edit that line by hand.
59
59
 
60
+ **A repair keeps the build ranges true.** When the plan carries `## Build ranges`, a
61
+ step the repair adds goes into a range (the one it builds with, or a bare range of its
62
+ own when it brings a call the human has to make before it lands), and a range the
63
+ repair breaks is re-cut in the same before/after
64
+ ([D92 (build-ranges)](https://github.com/robmclarty/plumbbob/blob/main/docs/decisions.md#d92)).
65
+ Read `plumbbob status` once the edits are written: its ranges block carries a `⚠` row
66
+ for a range naming a step the plan lacks, a step two ranges name, a step the build order
67
+ puts inside a range that does not name it, and an undone step no range names, and none
68
+ once the section is sound.
69
+ - **Re-cut the ranges (`/plumbbob:refine ranges`).** On request, re-cut the whole
70
+ `## Build ranges` section against the plan as it stands, by the rule `/plumbbob:plan`
71
+ cuts it with: a range ends where a human call is due (a decision the plan leaves open,
72
+ a review before later steps build on what landed, or a thing only the human can do)
73
+ and nowhere else, a bare build for a call made on a step's own work before it lands,
74
+ every undone step in exactly one range, walked in build order, and a `- model:` line
75
+ per range. A landed range stays as written, since its call was made. Show the section
76
+ before and after, and write only what the human approves.
77
+
60
78
  ## The hard contracts
61
79
 
62
80
  - **You propose; the human converges.** Surface holes and draft repairs, but the human
@@ -11,8 +11,8 @@ allowed-tools: Bash(plumbbob status:*)
11
11
  Current session state (injected when this skill runs): !`plumbbob status 2>/dev/null || echo "plumbbob CLI not on PATH in this session. Marketplace install: confirm the plugin is enabled in /plugin, then /reload-plugins. Skills-dir/global install: npm i -g plumbbob && plumbbob init."`
12
12
 
13
13
  This is a **thin driver** for `plumbbob status`. The dashboard above is your
14
- orientation: the intent, the step list, the parked/open-question counts, and the
15
- inferred next move. Report it verbatim and point the human at that next move. This
14
+ orientation: the intent, the step list, the build ranges when the plan has them, the
15
+ parked/open-question counts, and the inferred next move. Report it verbatim and point the human at that next move. This
16
16
  skill carries **no Edit and no Write tool**: the CLI is the source of truth, so
17
17
  **never retry**, and never edit a file to change what the orientation says.
18
18
 
@@ -31,4 +31,9 @@ annotate it.
31
31
  can switch (for example `/model sonnet`) before firing `/plumbbob:build`, or ignore it.
32
32
  Guidance, never a gate. You relay that line; you do not re-state it on top of the
33
33
  verbatim dashboard.
34
- 3. If it reads `NO ACTIVE SESSION`, tell the human to `/plumbbob:plan` to frame a goal.
34
+ 3. When the plan carries `## Build ranges`, the dashboard lists them beneath the steps:
35
+ `ranges 2 of 5 done`, `✓` on a landed range and `▸` on the one holding the next step,
36
+ each undone range's stop on the line beneath it, and a `⚠` row for any range the plan
37
+ has outgrown. Its `next →` then names the range's command to type. You relay the block
38
+ with the rest; a `⚠` row is `/plumbbob:refine`'s to fix.
39
+ 4. If it reads `NO ACTIVE SESSION`, tell the human to `/plumbbob:plan` to frame a goal.