plumbbob 0.12.1 → 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.
@@ -10,7 +10,7 @@ import { readTemplate, stampTemplate } from "../lib/templates.js";
10
10
  import { findRepoRoot, hasCommit, headSha, isDirty, isIgnored } from "../lib/git.js";
11
11
  import { sidecarDir, buildDir, listBuilds, slugify, checkpointsPath, intentPath, buildLogPath, beginSession, hasSession, excludeControl, excludeSidecar, setGrant, stampTick, } from "../lib/sidecar.js";
12
12
  import { settingsPath } from "../lib/settings.js";
13
- import { advisory, ending, notice, transition } from "../lib/notice.js";
13
+ import { advisory, ending, notice, printEnding, transition } from "../lib/notice.js";
14
14
  /**
15
15
  * Human-readable echo of the default check gate, stamped into the templates'
16
16
  * {{CHECK}} line.
@@ -177,15 +177,19 @@ export async function start(cwd, args) {
177
177
  // No pointer: the plan skill owns the turn that follows, and there is no step
178
178
  // yet to aim at, so the ending closes on the remedy line that says where to
179
179
  // frame the build.
180
- process.stdout.write(ending({
181
- lead: transition({
182
- label: 'Session',
183
- fact: `started "${title}"`,
184
- detail: [`baseline ${sha.slice(0, 9)}`],
185
- remedy: `frame and decide in ${intentLocation}, then build a step`,
180
+ printEnding(root, {
181
+ tier: 'driver',
182
+ render: 'start',
183
+ text: ending({
184
+ lead: transition({
185
+ label: 'Session',
186
+ fact: `started "${title}"`,
187
+ detail: [`baseline ${sha.slice(0, 9)}`],
188
+ remedy: `frame and decide in ${intentLocation}, then build a step`,
189
+ }),
190
+ advisories,
186
191
  }),
187
- advisories,
188
- }));
192
+ });
189
193
  return 0;
190
194
  }
191
195
  /**
@@ -6,7 +6,7 @@
6
6
  // behavior, so the `NO ACTIVE SESSION` sentinel is kept exact.
7
7
  import { readFileSync } from 'node:fs';
8
8
  import { join } from 'node:path';
9
- import { commitsSince, findRepoRoot } from "../lib/git.js";
9
+ import { PLAN_COMMIT_MARKER, commitsSince, findRepoRoot } from "../lib/git.js";
10
10
  import { SLOTS, readHarnessFile, resolveAgent, } from "../lib/agents.js";
11
11
  import { buildFolder, buildLogPath, checkpointsPath, hasSession, inSpike, intentPath, listBuilds, resolveBuild, stepPath, } from "../lib/sidecar.js";
12
12
  import { formatOrientation, lastLedgerSha, orient, parseRequestedStep } from "../lib/orient.js";
@@ -80,7 +80,7 @@ export function status(cwd, args = []) {
80
80
  inFlight: /^\d+$/.test(inFlightRaw) ? Number(inFlightRaw) : null,
81
81
  spiking: inSpike(root, slug),
82
82
  requested: parseRequestedStep(invoked),
83
- outOfBand: anchor === null ? 0 : commitsSince(root, anchor),
83
+ outOfBand: anchor === null ? 0 : commitsSince(root, anchor, PLAN_COMMIT_MARKER),
84
84
  });
85
85
  const lines = [formatOrientation(orientation), ...harnessSection(root, slug)];
86
86
  process.stdout.write(`${lines.join('\n')}\n`);
@@ -1,32 +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. Guidance only: it blocks nothing. It
14
- // ALWAYS exits 0 and any failure degrades to no output, so neither the tick nor
15
- // 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.
16
20
  import { readFileSync, writeFileSync } from 'node:fs';
17
- 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";
18
23
  /**
19
- * 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.
20
26
  *
21
27
  * Always returns 0: a broken turn must never wedge a prompt, so any failure is
22
28
  * swallowed and degrades to no output.
23
29
  */
24
30
  export function turn(cwd, _args) {
25
31
  try {
26
- applyTurn(cwd, readStdin());
27
- const context = stepInFlightContext(cwd);
28
- if (context !== null)
29
- process.stdout.write(context);
32
+ if (applyTurn(cwd, readStdin())) {
33
+ const context = stepInFlightContext(cwd);
34
+ if (context !== null)
35
+ process.stdout.write(context);
36
+ }
30
37
  }
31
38
  catch {
32
39
  // A broken turn must never wedge a prompt: swallow and exit 0.
@@ -39,8 +46,10 @@ export function turn(cwd, _args) {
39
46
  * A step is in flight when a STEP marker (the flat control file recording the
40
47
  * step between `build` and `checkpoint`) is present. Guidance only: it reminds a
41
48
  * fresh session that a tangent is a park (not an edit), that the park's own
42
- * block is the whole turn, and how the step lands, since neither the build nor
43
- * the park prose may be in context after compaction or on a scripted turn.
49
+ * block is the whole turn when the message is only a tangent, that a message
50
+ * which also asks something is answered first with the block after the answer,
51
+ * and how the step lands, since neither the build nor the park prose may be in
52
+ * context after compaction or on a scripted turn.
44
53
  * Exported because an in-process test cannot read the hook's real stdout.
45
54
  */
46
55
  export function stepInFlightContext(cwd) {
@@ -53,6 +62,9 @@ export function stepInFlightContext(cwd) {
53
62
  const text = `plumbbob: step ${step} is in flight — a new idea or tangent is a park, not an edit: ` +
54
63
  `capture it with \`plumbbob park "<one line>"\` and relay the block it prints whole, ` +
55
64
  `with nothing written around it, since that block is the whole turn. ` +
65
+ `The one exception is a message that also asks something: answer the question first, ` +
66
+ `then relay the park's block after the answer and write nothing after it. ` +
67
+ `The block's pointer back at the step closes the turn either way, so run no \`plumbbob handoff\` after it. ` +
56
68
  `The step stays open and lands when you run \`plumbbob checkpoint ${step}\` after approval.`;
57
69
  return `${JSON.stringify({ hookSpecificOutput: { hookEventName: 'UserPromptSubmit', additionalContext: text } })}\n`;
58
70
  }
@@ -72,7 +84,17 @@ function inFlightStep(root) {
72
84
  }
73
85
  }
74
86
  /**
75
- * 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.
76
98
  *
77
99
  * Split from the fd-0 read so the unit tests can drive it directly: an
78
100
  * in-process test cannot feed fd 0 (the same constraint checkpoint's `--body`
@@ -81,10 +103,35 @@ function inFlightStep(root) {
81
103
  export function applyTurn(cwd, raw) {
82
104
  const root = findSessionRoot(cwd);
83
105
  if (root === null)
84
- return 0; // no active session above cwd: silent no-op.
85
- writeFileSync(turnPath(root), `${readTurn(root) + 1}\n`);
86
- setGrant(root, grantFromPrompt(extractPrompt(raw)));
87
- 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 ');
88
135
  }
89
136
  /**
90
137
  * Derive the GRANT content from the human's prompt, or null to clear it.
@@ -92,9 +139,12 @@ export function applyTurn(cwd, raw) {
92
139
  * A grant is minted only from a literal `build` invocation the human typed:
93
140
  * `build` is disable-model-invocation, so this string can only come from a
94
141
  * human keystroke, never model-authored pressure: a grant the model can forge
95
- * is no grant. A bounded range beats `--auto` (bounded wins); a bare
96
- * `/build`, or any non-invocation prompt, mints nothing. The namespaced
97
- * `/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.
98
148
  *
99
149
  * The invocation must *start* a token: a slash preceded by a word character,
100
150
  * `/`, `.`, `-`, or `~` is a path segment (`src/build`, `./build`), not a
@@ -114,43 +164,30 @@ export function grantFromPrompt(prompt) {
114
164
  if (invocation === null)
115
165
  return null;
116
166
  const line = prompt.slice(invocation.index + invocation[0].length).split('\n', 1)[0] ?? '';
117
- let auto = false;
118
- let ceiling;
119
- for (const token of line.split(/\s+/)) {
120
- const word = token.replace(/[.,;:!?]+$/, '');
121
- if (word === '')
122
- continue;
123
- if (word === '--auto') {
124
- auto = true;
125
- continue;
126
- }
127
- const range = /^\d+-(\d+)$/.exec(word);
128
- if (range !== null) {
129
- ceiling = range[1];
130
- continue;
131
- }
132
- if (/^\d+$/.test(word))
133
- continue; // a bare step number: keep scanning for a range/--auto
134
- break; // an unrecognized flag or free text: the arguments have ended
135
- }
136
- if (ceiling !== undefined)
137
- return `range ${ceiling}`;
138
- 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;
139
171
  }
140
172
  /**
141
- * 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.
142
175
  *
143
176
  * Absent, unparseable, or non-string input contributes nothing rather than
144
- * wedging the tool: the tick still lands (a human turn did occur) and the empty
145
- * 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.
146
180
  */
147
- function extractPrompt(raw) {
181
+ function readPayload(raw) {
148
182
  try {
149
183
  const parsed = JSON.parse(raw);
150
- 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
+ };
151
188
  }
152
189
  catch {
153
- return '';
190
+ return { prompt: '', promptId: null };
154
191
  }
155
192
  }
156
193
  /**
package/dist/verbs/use.js CHANGED
@@ -11,7 +11,7 @@
11
11
  import { existsSync } from 'node:fs';
12
12
  import { findRepoRoot } from "../lib/git.js";
13
13
  import { activeBuild, hasSession, intentPath, listBuilds, setActiveBuild, stepPath } from "../lib/sidecar.js";
14
- import { advisory, ending, notice, transition } from "../lib/notice.js";
14
+ import { advisory, ending, notice, printEnding, transition } from "../lib/notice.js";
15
15
  import { driverPointer } from "./handoff.js";
16
16
  /**
17
17
  * Switch the active-build cursor to the named build.
@@ -43,22 +43,26 @@ export function use(cwd, args) {
43
43
  // The lead line, then the advisory it qualifies, then the pointer into the
44
44
  // build just switched to: one fixed order for every ending, so the relay never
45
45
  // has to guess which line leads.
46
- process.stdout.write(ending({
47
- lead: transition({
48
- label: 'Active build',
49
- fact: target,
50
- detail: existsSync(stepPath(root, target)) ? ['a step is in flight'] : [],
46
+ printEnding(root, {
47
+ tier: 'driver',
48
+ render: 'use',
49
+ text: ending({
50
+ lead: transition({
51
+ label: 'Active build',
52
+ fact: target,
53
+ detail: existsSync(stepPath(root, target)) ? ['a step is in flight'] : [],
54
+ }),
55
+ advisories: left === null
56
+ ? []
57
+ : [
58
+ advisory({
59
+ fact: `build "${left}" has a step in flight`,
60
+ detail: ['its in-flight state is preserved'],
61
+ remedy: `plumbbob use ${left} to pick it back up`,
62
+ }),
63
+ ],
64
+ pointer: driverPointer(root, target),
51
65
  }),
52
- advisories: left === null
53
- ? []
54
- : [
55
- advisory({
56
- fact: `build "${left}" has a step in flight`,
57
- detail: ['its in-flight state is preserved'],
58
- remedy: `plumbbob use ${left} to pick it back up`,
59
- }),
60
- ],
61
- pointer: driverPointer(root, target),
62
- }));
66
+ });
63
67
  return 0;
64
68
  }
package/hooks/hooks.json CHANGED
@@ -22,6 +22,13 @@
22
22
  { "type": "command", "command": "sh \"${CLAUDE_PLUGIN_ROOT}/bin/plumbbob\" turn" }
23
23
  ]
24
24
  }
25
+ ],
26
+ "Stop": [
27
+ {
28
+ "hooks": [
29
+ { "type": "command", "command": "sh \"${CLAUDE_PLUGIN_ROOT}/bin/plumbbob\" relay" }
30
+ ]
31
+ }
25
32
  ]
26
33
  }
27
34
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "plumbbob",
3
- "version": "0.12.1",
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.
@@ -144,6 +145,7 @@ switch with `/model` and rerun to honor it, or wave you on. Advisory, never a ga
144
145
  paragraph when the step needs explaining>
145
146
 
146
147
  ## 1 <the first highlight: one sentence, one move>
148
+
147
149
  <the full story: what moved, why, what was tried and dropped>
148
150
 
149
151
  ## 2 ...
@@ -180,32 +182,59 @@ switch with `/model` and rerun to honor it, or wave you on. Advisory, never a ga
180
182
  line in the build-log's `## Log` and then truncates it, so the detail lands in the
181
183
  tracked ledger rather than piling up on disk.
182
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
+
183
191
  *Paste the turn.* Run `plumbbob handoff` and paste its output whole, verbatim, at top
184
192
  level, trailing blank line included, then end the turn. Never nest it inside a fence of
185
- your own; it carries fences of its own and they cannot nest. It prints the Summary and
186
- its highlights, the Readout fence (its measured `check`, `seam`, `diff`, and `spent`
187
- rows folded with your three), a `diff` fence when the change is 20 lines or fewer, the
188
- Verdict folded worst-of from the same rows, Next Up, Your Call, and your recommendation
189
- last. The gate verdict's one home is that `check` row: no standalone verdict line
190
- exists, and you never restate the verdict in prose. A narrowed run names the slots it
191
- skipped there (`· without test`), and that named narrowing is the whole disclosure. The
192
- notice checkride's Stop hook appends after your turn is not yours to relay or repeat;
193
- the trailing blank line you kept is what lands it on its own line. Relayed CLI strings
194
- keep their em-dashes; your own lines never use one, the write-versus-relay line of
193
+ your own; it carries fences of its own and they cannot nest. It prints the Now line
194
+ naming the step in flight, the Summary and its highlights, the Readout fence (its
195
+ measured `check`, `seam`, `diff`, and `spent` rows folded with your three), a `diff`
196
+ fence when the change is 20 lines or fewer, the Verdict folded worst-of from the same
197
+ rows, Next Up, Your Call, and your recommendation last. Run it once: a second run at an
198
+ unchanged pause prints the one-line reminder instead of the card, so if that line comes
199
+ back where the card belongs (you ran handoff earlier in this turn), run
200
+ `plumbbob handoff --full` and paste that. The gate verdict's one home is that `check`
201
+ row: no standalone verdict line exists, and you never restate the verdict in prose. A
202
+ narrowed run names the slots it skipped there (`· without test`), and that named
203
+ narrowing is the whole disclosure. The notice checkride's Stop hook appends after your
204
+ turn is not yours to relay or repeat; the trailing blank line you kept is what lands it
205
+ on its own line. Relayed CLI strings keep their em-dashes; your own lines never use
206
+ one, the write-versus-relay line of
195
207
  [D78 (em-dash-ban)](https://github.com/robmclarty/plumbbob/blob/main/docs/decisions.md#d78).
196
208
 
197
209
  *Then read the reply as an ask or a direction.* A message that **asks** ("expand 2",
198
- "what does that mean?", "why did the seam row flag that?") is an expand: answer it from the detail file, from
199
- `git diff`, or from the build-log's `## Log` for an older step, never from recall, then run `plumbbob handoff` again and
200
- paste it. The step is still in flight, so it renders the same pause, and the Your Call
201
- block stays the CLI's to render rather than yours to retype. A message that **directs**
202
- is needs-work: take it as what to change, and nothing lands until the human says
203
- `looks good`.
210
+ "what does that mean?", "why did the seam row flag that?") is an expand: answer it from
211
+ the detail file, from `git diff`, or from the build-log's `## Log` for an older step,
212
+ never from recall, then run `plumbbob handoff` and paste its output beneath your answer,
213
+ whole and verbatim. The turn is your answer, then that output, and nothing after it. The
214
+ pause renders its card once
215
+ ([D87 (pause-once)](https://github.com/robmclarty/plumbbob/blob/main/docs/decisions.md#d87)):
216
+ while nothing about it has changed, handoff prints one line,
217
+ `**Next Up**: Back to Step <N> of <M> - <title>`, in place of the card the human already
218
+ read, so your answer stays on screen. The CLI picks the form, never you: paste whichever
219
+ one it prints, and never retype the card or reach for `--full` to put it beneath an
220
+ answer.
221
+
222
+ Two moments bring the whole card back, and in both you run `plumbbob handoff --full` and
223
+ paste its output whole, with nothing written around it: the human asks to see the pause
224
+ (`show the pause`, or any wording of it), or the card is not in this conversation (a
225
+ fresh session, or one after `/clear`, that finds the step still at its pause). A
226
+ message that **directs** is needs-work: take it as what to change, and nothing lands
227
+ until the human says `looks good`. Once the change is made, rewrite the detail file and
228
+ end on `plumbbob handoff` as the first render did; the pause has changed, so it prints
229
+ the whole card.
204
230
 
205
231
  **Then hand off with the next model.** `plumbbob checkpoint` prints the whole boundary
206
232
  ending itself: `**Checkpoint**: Step N complete (<sha>)`, the Verdict the step earned,
207
- any advisory (a staged path outside the seam is the common one), and Next Up. Relay
208
- 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
209
238
  orientation tier (no Your Call block, no recommendation), and it points at the next
210
239
  undone step carrying that step's `- model:` recommendation (the plan's
211
240
  smallest-model-that-fits call) so the human knows which `/model` to select before
@@ -268,7 +297,18 @@ and approves in the human's place**, and it **chains**:
268
297
  advance the loop), or a new decision is needed. The step is still in flight and the
269
298
  human has a call to make about it, so end on the same turn the default pause does:
270
299
  write the detail file, run `plumbbob handoff`, and paste its block.
271
- - **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
272
312
  planned steps remain, and `plumbbob build` names that step as it enters it (for
273
313
  example `step 3 is the last undone step in the plan`). That step checkpoints like
274
314
  every step before it, and its
@@ -283,36 +323,54 @@ only because the human asked for it by name. A range hands the clock back once i
283
323
  step has landed: the step after it waits for the human's next run. The default (no flag,
284
324
  no range) always ends at the pause.
285
325
 
286
- ### 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`
287
327
 
288
328
  `/plumbbob:build 1-3` self-approves steps 1 through 3 exactly as `--auto` does, then **stops
289
- once step 3 has landed** instead of chaining to done. The range *is* the opt-in; you do not
290
- also pass `--auto`. Run it as the `--auto` loop with one extra halt: **stop before building
291
- any next step whose plan number is past the top of the range.** It adds no machinery: just
292
- the one more entry already in the halt list above, and it is a clean halt, so the top step
293
- is never held back for approval. The human approved it by typing the range, and
294
- `plumbbob build` says so as it enters that step:
295
- `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),
296
344
  `the last undone step in the range you granted`.
297
345
 
298
- - **`N-M` with N ≤ M**: build N…M, self-approving and checkpointing each, M included, then
299
- relay M's checkpoint block and end the turn. `N-N` is a range like any other: it builds
300
- and lands the single step `N`. Only a bare `/plumbbob:build N` ends at the pause.
301
- - **N > M** (for example `3-1`): that is not a range you can walk; report it and stop rather
302
- than guess the intent.
303
- - **M past the last planned step** (for example `1-9` with three steps): build through the
304
- last planned step and stop; that is the existing "no planned steps remain" halt, not
305
- an error.
306
- - **A step inside the range won't build** (missing, or its seam won't parse): `plumbbob
307
- build <n>` fails exactly as it always does; surface it and stop, the same as a red
308
- check. Do not skip past the gap.
309
- - **N is above the next undone step**: you are jumping over earlier planned work; note
310
- 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
311
360
  single-number jump like `/plumbbob:build 4`).
312
- - **The build order puts a number past the top ahead of the rest** (the order reads
313
- `1, 4, 2, 3` under `/plumbbob:build 1-3`): a range names plan numbers, so the halt above
314
- fires before step 4 and `plumbbob build` names step 1 as the last undone step in the
315
- 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.
316
374
 
317
375
  ## The hard contracts
318
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.