plumbbob 0.13.6 → 0.13.7

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.
@@ -29,6 +29,14 @@
29
29
  // stop card is a miss however whole the boundary was, since the card is the
30
30
  // run's ending, and its reason names `plumbbob handoff --stop` as the retry.
31
31
  //
32
+ // A reminder can close the turn and still leave the human without the reply.
33
+ // When the reply was written before a tool call and the final message is the
34
+ // reminder alone, a client that folds text written ahead of a tool call shows
35
+ // the line and nothing else (GitHub issue #12). That shape is held once too,
36
+ // by a reason that asks for the reply again and ends on the line to type. It
37
+ // names no command, since a retry that ran one would bury the reply the same
38
+ // way (D121 (reply-after-the-tools)).
39
+ //
32
40
  // The final message comes from the payload's `last_assistant_message`, never
33
41
  // from the transcript, which does not yet hold that message when Stop fires;
34
42
  // the transcript supplies only the turn's earlier messages, which an
@@ -79,13 +87,53 @@ export function relayCheck(cwd, raw) {
79
87
  // relayed: the card is the run's ending, and `handoff --stop` overwrites
80
88
  // this record once it prints.
81
89
  const carried = record.owes !== undefined ? false : carriedEnding(record, payload.last_assistant_message, payload);
90
+ if (carried === false)
91
+ return hold(root, record, reason(record));
92
+ // The reminder closed the turn, but the reply stood above a tool call.
93
+ if (carried === true && buriedReply(record, payload.last_assistant_message, payload))
94
+ return hold(root, record, buriedReason(record));
82
95
  // Whole, or not this check's to judge: let the turn end.
83
- if (carried !== false)
84
- return null;
96
+ return null;
97
+ }
98
+ /**
99
+ * Stamp the once in `RELAY` and emit the hold with its reason, or nothing when
100
+ * the stamp cannot land.
101
+ */
102
+ function hold(root, record, text) {
85
103
  // A hold that cannot be counted could repeat, so it is no hold at all.
86
104
  if (!holdRelay(root, record))
87
105
  return null;
88
- return `${JSON.stringify({ hookSpecificOutput: { hookEventName: 'Stop', additionalContext: reason(record) } })}\n`;
106
+ return `${JSON.stringify({ hookSpecificOutput: { hookEventName: 'Stop', additionalContext: text } })}\n`;
107
+ }
108
+ // The renders a reply at a card already shown closes on: the one line that
109
+ // points back at the pause or the stop.
110
+ const REMINDERS = new Set(['reminder', 'stop-reminder']);
111
+ /**
112
+ * Whether a reminder that closed the turn left the reply above a tool call:
113
+ * the final message is the reminder alone, with no words above its label,
114
+ * while the turn's earlier messages carried words. The printed line itself is
115
+ * not counted as words, since the transcript can already hold the final
116
+ * message when Stop fires. A transcript the check cannot read or scope is no
117
+ * evidence, and neither is a turn whose earlier messages said nothing.
118
+ */
119
+ function buriedReply(record, last, payload) {
120
+ if (!REMINDERS.has(record.render) || leadOf(last).trim().length > 0)
121
+ return false;
122
+ const earlier = turnText(payload);
123
+ if (earlier === null)
124
+ return false;
125
+ return earlier.split(record.text.trim()).join('').trim().length > 0;
126
+ }
127
+ /**
128
+ * The one-line reason for a reply buried above a tool call: what happened, and
129
+ * the line to end on, with no command to run, since a retry that ran one would
130
+ * leave the reply above a tool call again.
131
+ */
132
+ function buriedReason(record) {
133
+ return (`plumbbob: the turn ended on the ${DECISION_NAMES[record.render] ?? 'pause reminder'} it printed, ` +
134
+ 'but the reply stood above a tool call, where a client can fold it out of sight. ' +
135
+ 'Write the reply again, in your own words, with no tool call after it, ' +
136
+ `and end the turn on the same line, typed as it printed: ${record.text.trim()}`);
89
137
  }
90
138
  /**
91
139
  * Whether the turn carried the recorded ending: true when it did, false when
@@ -197,7 +245,27 @@ function blockCount(part) {
197
245
  * runs to the next. Text before the first label is not part of any.
198
246
  */
199
247
  function partsOf(text) {
248
+ return readEnding(text).parts;
249
+ }
250
+ /** The text above an ending's first labeled part: the words a reply wrote above the line it closes on. */
251
+ function leadOf(text) {
252
+ return readEnding(text).lead.join('\n');
253
+ }
254
+ /**
255
+ * One walk over a stretch of text: the lines before its first labeled part,
256
+ * and the parts themselves, each run to the next, with a fence read as one
257
+ * stretch so a label inside it opens nothing.
258
+ */
259
+ function readEnding(text) {
260
+ const lead = [];
200
261
  const parts = [];
262
+ const keep = (line) => {
263
+ const last = parts.at(-1);
264
+ if (last === undefined)
265
+ lead.push(line);
266
+ else
267
+ last.lines.push(line);
268
+ };
201
269
  let rail = null;
202
270
  for (const line of text.split('\n')) {
203
271
  const fence = /^\s*(`{3,})/.exec(line)?.[1];
@@ -206,7 +274,7 @@ function partsOf(text) {
206
274
  }
207
275
  else if (rail !== null && fence !== undefined && fence.length >= rail.length && line.trim() === fence) {
208
276
  rail = null;
209
- parts.at(-1)?.lines.push(line);
277
+ keep(line);
210
278
  continue;
211
279
  }
212
280
  const label = rail === null ? labelOf(line) : undefined;
@@ -214,10 +282,10 @@ function partsOf(text) {
214
282
  parts.push({ label, lines: [line] });
215
283
  }
216
284
  else {
217
- parts.at(-1)?.lines.push(line);
285
+ keep(line);
218
286
  }
219
287
  }
220
- return parts;
288
+ return { lead, parts };
221
289
  }
222
290
  /**
223
291
  * The label a line opens a part on, or undefined when it opens none: a bold
@@ -7,8 +7,8 @@ import { existsSync, writeFileSync, mkdirSync } from 'node:fs';
7
7
  import { join } from 'node:path';
8
8
  import { detectGate } from "../lib/check.js";
9
9
  import { readTemplate, stampTemplate } from "../lib/templates.js";
10
- import { findRepoRoot, hasCommit, headSha, isDirty, isIgnored } from "../lib/git.js";
11
- import { sidecarDir, buildDir, listBuilds, slugify, checkpointsPath, intentPath, buildLogPath, beginSession, hasSession, excludeControl, excludeSidecar, setGrant, stampTick, } from "../lib/sidecar.js";
10
+ import { findRepoRoot, hasCommit, headSha, isDirty } from "../lib/git.js";
11
+ import { sidecarDir, buildDir, listBuilds, slugify, checkpointsPath, intentPath, buildLogPath, beginSession, hasSession, excludeControl, excludeSidecar, buildFolderIgnored, planCommitted, setGrant, stampTick, } from "../lib/sidecar.js";
12
12
  import { settingsPath } from "../lib/settings.js";
13
13
  import { advisory, ending, notice, printEnding, transition } from "../lib/notice.js";
14
14
  /**
@@ -41,6 +41,12 @@ export async function start(cwd, args) {
41
41
  // Every advisory the session accrues on its way open, collected rather than
42
42
  // printed: they belong inside the one ending below, in the order found.
43
43
  const advisories = [];
44
+ // `--local` on a session already open is the plan card's way out: keep this
45
+ // build untracked before its plan commit lands. It needs no title.
46
+ const open = local ? findRepoRoot(cwd) : null;
47
+ if (open !== null && hasSession(open)) {
48
+ return keepLocal(open);
49
+ }
44
50
  if (title.length === 0) {
45
51
  process.stderr.write(notice({ fact: 'start needs a title', remedy: 'plumbbob start "what you are building"' }));
46
52
  return 1;
@@ -154,7 +160,7 @@ export async function start(cwd, args) {
154
160
  // excludes the whole sidecar by design and says so, so skip the probe there.
155
161
  // Guidance only, and the session is already open: a probe hiccup stays silent
156
162
  // rather than half-crashing a scaffolded session.
157
- if (!local && sidecarIsIgnored(root, slug)) {
163
+ if (!local && buildFolderIgnored(root, slug)) {
158
164
  advisories.push(advisory({
159
165
  fact: 'this repo gitignores .plumbbob/',
160
166
  detail: ['the build folder cannot ride the branch', 'plan and finish commits will be record-only'],
@@ -210,20 +216,40 @@ function datedSlug(title) {
210
216
  return `${now.getFullYear()}-${pad(now.getMonth() + 1)}-${pad(now.getDate())}-${base}`;
211
217
  }
212
218
  /**
213
- * Whether the tracked build folder is gitignored: the record-only advisory's
214
- * predicate, probing exactly the path `checkpoint --plan` will try to stage.
219
+ * Keep an open session untracked: `plumbbob start --local` typed while a build
220
+ * is open and its plan not yet committed, the way out the plan card offers
221
+ * (D117 (publish-on-the-card)).
215
222
  *
216
- * Best-effort: a probe that throws (a fatal `git check-ignore`) reads as
217
- * not-ignored so the advisory stays silent rather than crashing a session that
218
- * is already open.
223
+ * It writes the blanket `.plumbbob/` line to this clone's info/exclude, the
224
+ * line a `--local` start writes, so the plan and finish commits land
225
+ * record-only and nothing under `.plumbbob/` rides the branch. The folder
226
+ * keeps its place and its slug; only git's view of it changes. Refuses once the
227
+ * plan has landed, since the folder is in a commit by then and an exclude
228
+ * cannot take it back.
219
229
  */
220
- function sidecarIsIgnored(root, slug) {
221
- try {
222
- return isIgnored(root, buildDir(root, slug));
223
- }
224
- catch {
225
- return false;
230
+ function keepLocal(root) {
231
+ if (planCommitted(root)) {
232
+ process.stderr.write(notice({
233
+ fact: 'this session can no longer be kept local',
234
+ detail: ['its plan is already committed with the build folder'],
235
+ remedy: 'plumbbob finish to close it before starting another',
236
+ }));
237
+ return 1;
226
238
  }
239
+ excludeSidecar(root);
240
+ printEnding(root, {
241
+ tier: 'driver',
242
+ render: 'start',
243
+ text: ending({
244
+ lead: transition({
245
+ label: 'Session',
246
+ fact: 'kept local',
247
+ detail: ['.plumbbob/ excluded in this clone', 'the plan and finish commits land record-only'],
248
+ remedy: 'plumbbob handoff --plan to show the plan card again',
249
+ }),
250
+ }),
251
+ });
252
+ return 0;
227
253
  }
228
254
  /**
229
255
  * Read the value that follows a `--flag` in argv (for example `--slug my-build`), or
@@ -8,14 +8,15 @@
8
8
  // `--run`, it prints the run a `/plumbbob:run` invocation names above the
9
9
  // dashboard, and names the waiting card again beneath the run's grant.
10
10
  import { readFileSync } from 'node:fs';
11
- import { join } from 'node:path';
11
+ import { join, relative } from 'node:path';
12
12
  import { PLAN_COMMIT_MARKER, commitsSince, findRepoRoot } from "../lib/git.js";
13
13
  import { SLOTS, readHarnessFile, resolveAgent, } from "../lib/agents.js";
14
14
  import { buildFolder, buildLogPath, checkpointsPath, hasSession, inSpike, intentPath, listBuilds, resolveBuild, stepPath, } from "../lib/sidecar.js";
15
15
  import { readGrant } from "../lib/latch.js";
16
- import { formatOrientation, formatRunDashboard, lastLedgerSha, orient, parseRequestedStep, resolveRunArgs, } from "../lib/orient.js";
16
+ import { formatOrientation, formatRunDashboard, lastLedgerSha, orient, parseOpenQuestionRows, parseRequestedStep, resolveRunArgs, } from "../lib/orient.js";
17
17
  import { normalizeStepList } from "../lib/steplist.js";
18
- import { waitingCard } from "./handoff.js";
18
+ import { blocks, notice } from "../lib/notice.js";
19
+ import { questionsPart, waitingCard } from "./handoff.js";
19
20
  /**
20
21
  * Read a file as utf8, or '' when it is missing or unreadable.
21
22
  */
@@ -67,7 +68,10 @@ function grantText(grant) {
67
68
  * With `--run "<raw args>"` (how the `/plumbbob:run` skill passes its own),
68
69
  * the dashboard prints beneath a header naming the run the text resolves to
69
70
  * and the grant the turn hook minted, or carries the refusal in its pointer
70
- * when the run names nothing it can walk.
71
+ * when the run names nothing it can walk. With `--questions` it prints the
72
+ * plan's open questions as the plan card tables them, and nothing else: the
73
+ * ones that wait on the human in the table, and the ones deferred to the work
74
+ * on a line beneath it.
71
75
  */
72
76
  export function status(cwd, args = []) {
73
77
  const root = findRepoRoot(cwd);
@@ -88,6 +92,14 @@ export function status(cwd, args = []) {
88
92
  return 0;
89
93
  }
90
94
  }
95
+ if (rest.includes('--questions')) {
96
+ // The open questions alone, as the plan card tables them: the view a skill
97
+ // relays when the human asks what is still open (D118 (questions-table)),
98
+ // with the deferred ones on their own line (D120 (deferred-questions)).
99
+ const part = questionsPart(parseOpenQuestionRows(readOr(intentPath(root, slug))), relative(root, intentPath(root, slug)));
100
+ process.stdout.write(part === null ? notice({ fact: 'no open questions in this plan' }) : blocks([part]));
101
+ return 0;
102
+ }
91
103
  const inFlightRaw = readOr(stepPath(root, slug)).trim();
92
104
  // Out-of-band commits (ones that landed outside the checkpoints ledger) are
93
105
  // surfaced here, never blocked. Counting them needs git (orient is pure), so
@@ -19,7 +19,10 @@
19
19
  // is in context) still knows it: mid-step, a tangent is parked, not chased;
20
20
  // at a pause or a stop whose card has shown, the message is the human's reply,
21
21
  // answered in the model's own words and closed on the one line `handoff`
22
- // prints, never on the card again (D110 (reply-after-the-card)). Guidance
22
+ // prints, never on the card again (D110 (reply-after-the-card)). Every tool
23
+ // call comes before that answer and none after it, since a client that folds
24
+ // text written ahead of a tool call would show the line and hide the answer
25
+ // (D121 (reply-after-the-tools)). Guidance
23
26
  // only: it blocks nothing. It ALWAYS exits 0 and any failure degrades to no
24
27
  // output, so neither the tick nor the emit can ever wedge prompting.
25
28
  import { readFileSync, writeFileSync } from 'node:fs';
@@ -60,8 +63,9 @@ export function turn(cwd, _args) {
60
63
  * Three beats, read off the control files. A step whose pause card has shown
61
64
  * (STEP, and PAUSE naming that step's pause) is waiting for the human's call,
62
65
  * so the message is the reply to it: answered or acted on in the model's own
63
- * words, and closed on the one line `handoff` prints rather than the card
64
- * again (D110 (reply-after-the-card)). A step in flight with no card yet is
66
+ * words after every tool call, and closed on the one line `handoff` prints
67
+ * rather than the card again (D110 (reply-after-the-card),
68
+ * D121 (reply-after-the-tools)). A step in flight with no card yet is
65
69
  * mid-build, where a tangent is a park and not an edit. A stop card waiting
66
70
  * for its reply is the pause's beat at a stop. At a pause or a stop, a
67
71
  * `prompt` asking to see the card (`show the pause`, the phrase the card's
@@ -127,18 +131,23 @@ function parkNudge(step) {
127
131
  return (`plumbbob: step ${step} is in flight — a new idea or tangent is a park, not an edit: ` +
128
132
  `capture it with \`plumbbob park "<one line>"\` and relay the block it prints whole, ` +
129
133
  `with nothing written around it, since that block is the whole turn. ` +
130
- `The one exception is a message that also asks something: answer the question first, ` +
131
- `then relay the park's block after the answer and write nothing after it. ` +
134
+ `The one exception is a message that also asks something: run \`plumbbob park\` first, then answer the question, ` +
135
+ `then end the turn on the park's block, typed as it printed, with no tool call after the answer. ` +
132
136
  `The block's pointer back at the step closes the turn either way, so run no \`plumbbob handoff\` after it. ` +
133
137
  `The step stays open and lands when you run \`plumbbob checkpoint ${step}\` after approval.`);
134
138
  }
135
- /** The nudge at a pause whose card has shown: the message is the reply, and the card is not printed again. */
139
+ /**
140
+ * The nudge at a pause whose card has shown: the message is the reply, every
141
+ * tool call comes before it, and the card is not printed again.
142
+ */
136
143
  function pauseNudge(step) {
137
144
  return (`plumbbob: step ${step}'s card has shown, and this message is the human's reply to it. ` +
138
145
  `An approval lands the step: run \`plumbbob checkpoint ${step}\` and relay its ending. ` +
139
- `Anything else is answered in your own words: answer the question, or make the change it asks for, ` +
140
- `and when a change moves what the card said, write \`.plumbbob/detail.md\` again so the record matches what lands. ` +
141
- `Print no card for it: run \`plumbbob handoff\` and end the turn on the one line it prints, with nothing after it. ` +
146
+ `Anything else is answered in your own words, after every tool call and with none after it: ` +
147
+ `first read what you need and make the change it asks for, ` +
148
+ `write \`.plumbbob/detail.md\` again when a change moves what the card said, so the record matches what lands, ` +
149
+ `and run \`plumbbob handoff\` last among them, keeping the one line it prints. ` +
150
+ REPLY_LAST +
142
151
  `Only when the human asks to see the card again, run \`plumbbob handoff --full\` and paste its output exactly as it printed, as the whole turn. ` +
143
152
  `A tangent the human hands you is still a park: \`plumbbob park "<one line>"\`.`);
144
153
  }
@@ -146,11 +155,19 @@ function pauseNudge(step) {
146
155
  function stopNudge(range, count) {
147
156
  return (`plumbbob: range ${range} of ${count}'s stop card has shown, and this message is the human's reply to it. ` +
148
157
  `An approval records the stop: fold each confirmed blocker into the plan, then run \`plumbbob harvest\` and relay its ending. ` +
149
- `Anything else is answered in your own words: answer the question, or make the change it asks for ` +
150
- `(a lean is its line under the detail file's \`## Harvest\`). ` +
151
- `Print no card for it: run \`plumbbob handoff --stop\` and end the turn on the one line it prints, with nothing after it. ` +
158
+ `Anything else is answered in your own words, after every tool call and with none after it: ` +
159
+ `first read what you need and make the change it asks for (a lean is its line under the detail file's \`## Harvest\`), ` +
160
+ `and run \`plumbbob handoff --stop\` last among them, keeping the one line it prints. ` +
161
+ REPLY_LAST +
152
162
  `Only when the human asks to see the card again, run \`plumbbob handoff --stop --full\` and paste its output exactly as it printed, as the whole turn.`);
153
163
  }
164
+ // How a reply at a card already shown ends, the same at a pause and a stop: the
165
+ // answer after the last tool call, then the line, because a client that folds
166
+ // text written before a tool call shows the line and hides the answer
167
+ // (D121 (reply-after-the-tools)).
168
+ const REPLY_LAST = `Then write your answer and end the turn on that line, typed as it printed, with no tool call after your answer: ` +
169
+ `a client that folds text written before a tool call would hide the answer and show the line alone. ` +
170
+ `Print no card for it. `;
154
171
  /**
155
172
  * The in-flight step number, or null when no STEP marker is present.
156
173
  *
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "plumbbob",
3
- "version": "0.13.6",
3
+ "version": "0.13.7",
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",
@@ -186,7 +186,9 @@ switch with `/model` and rerun to honor it, or wave you on. Advisory, never a ga
186
186
  of its own, one command to a line, which handoff keeps line for line. A slash command
187
187
  or a path stays in backticks in the sentence. `checkpoint` records this file beneath the step's dated
188
188
  line in the build-log's `## Log` and then truncates it, so the detail lands in the
189
- tracked ledger rather than piling up on disk.
189
+ tracked ledger rather than piling up on disk. Write a relative link from `.plumbbob/`, where
190
+ the file lives (`builds/<slug>/intent.md#d5`); the record re-aims it at the build folder.
191
+ A decision cited by its slug in plain text (`D5 (no-link-no-reconnect)`) needs no link at all.
190
192
 
191
193
  **A bare range's pause is its stop.** When the injected dashboard's ranges block lists
192
194
  this step as a bare range (`/plumbbob:build 5`, its row marked `▸`), the plan reserved a
@@ -213,7 +215,7 @@ switch with `/model` and rerun to honor it, or wave you on. Advisory, never a ga
213
215
  one, the write-versus-relay line of
214
216
  [D78 (em-dash-ban)](https://github.com/robmclarty/plumbbob/blob/main/docs/decisions.md#d78).
215
217
 
216
- *Then answer the reply in your own words.* The card shows once
218
+ *Then answer the reply in your own words, after every tool call.* The card shows once
217
219
  ([D110 (reply-after-the-card)](https://github.com/robmclarty/plumbbob/blob/main/docs/decisions.md#d110)):
218
220
  every later turn at this pause is your reply, closed by one line. A message that
219
221
  **asks** ("expand 2", "what does that mean?", "why did the seam row flag that?") is an
@@ -222,11 +224,16 @@ switch with `/model` and rerun to honor it, or wave you on. Advisory, never a ga
222
224
  make the change, run the check, and say what moved and what the check found; when the
223
225
  change moves what the card said (a judgment row, the Summary, the recommendation), write
224
226
  the detail file again so the record matches what lands. Nothing lands until the human
225
- says `looks good`. Either way, then run `plumbbob handoff` and paste its output beneath
226
- your reply, whole and verbatim: at a pause whose card has shown it prints one line,
227
- `**Next Up**: Back to Step <N> of <M> - <title>`, whatever changed since, so your reply
228
- stays on screen. The turn is your reply, then that line, and nothing after it. The CLI
229
- picks the form, never you: paste whichever one it prints, and never retype the card.
227
+ says `looks good`. Either way, every tool call comes before your reply and none after
228
+ it ([D121 (reply-after-the-tools)](https://github.com/robmclarty/plumbbob/blob/main/docs/decisions.md#d121)):
229
+ read, edit, run the check, write the detail file, and run `plumbbob handoff` last among
230
+ them. At a pause whose card has shown it prints one line,
231
+ `**Next Up**: Back to **Step N** on <Model> - <title>`, whatever changed since. Keep
232
+ that line, then write your reply, and end the turn on the line, whole and verbatim. The
233
+ turn is your tool calls, then your reply, then that line, and nothing after it: a
234
+ client that folds text written before a tool call (Zed, through the agent bridge) shows
235
+ the line and hides a reply written above the call. The CLI picks the form, never you:
236
+ paste whichever one it prints, and never retype the card.
230
237
 
231
238
  The card comes back one way: the human asks to see it (`show the pause`, or any wording
232
239
  of it). Then run `plumbbob handoff --full` and paste its output whole, with nothing
@@ -238,12 +245,13 @@ switch with `/model` and rerun to honor it, or wave you on. Advisory, never a ga
238
245
  **Then hand off with the next model.** `plumbbob checkpoint` prints the whole boundary
239
246
  ending itself: `**Checkpoint**: Step N complete (<sha>)`, any advisory (a staged path
240
247
  outside the seam is the common one), and the state table: the Verdict the step earned,
241
- the Stop row when this landing completes a range (`| Stop | Range 3 of 5 landed - <its
248
+ the Stop row when this landing completes a range (`| Stop | **Range 3** landed - <its
242
249
  stop> |`, the reason the build stops here), Next Up, and the rows beneath it. Relay that
243
250
  block whole, the table with the rest (the relay check holds a boundary that dropped a
244
251
  row), run no second command, and write nothing around it. A later turn at this boundary
245
- is your reply; a `plumbbob handoff` there prints Next Up's one line, since the boundary
246
- has shown. It is the
252
+ is your reply, in the same order as at a pause: a `plumbbob handoff` there prints Next
253
+ Up's one line, since the boundary has shown, so run it last among your tool calls, then
254
+ write your reply and end on that line, with no tool call after it. It is the
247
255
  orientation tier (no Your Call block, no recommendation), and its Next Up points at the
248
256
  next undone step carrying that step's `- model:` recommendation (the plan's
249
257
  smallest-model-that-fits call) so the human knows which `/model` to select before
@@ -371,9 +379,13 @@ out, or the next step in build order is one the range does not name),
371
379
  - **A step inside the range won't build** (its seam won't parse): `plumbbob build <n>` fails
372
380
  exactly as it always does; surface it and stop, the same as a red check. Do not skip past
373
381
  the gap.
374
- - **The range starts above the next undone step**: you are jumping over earlier planned work;
375
- note that you are skipping the steps before it, then proceed (the same latitude as a
376
- single-number jump like `/plumbbob:build 4`).
382
+ - **The range starts above the next undone step** (the order reads `6, 4, 5` under
383
+ `/plumbbob:build 4-5`): the first step enters as a single-number jump like `/plumbbob:build 4`
384
+ would, with the skipped step noted. Once it lands, the next undone step in build order is
385
+ the one you jumped, which the range does not name, so the halt above fires there, and
386
+ `plumbbob build` names step 4 as the last undone step in the range, its detail naming step 6
387
+ as the step the walk halts before. The range that walks the whole stretch names the jumped
388
+ step too (`/plumbbob:build 6,4-5`).
377
389
  - **The build order puts a step the range does not name inside it** (the order reads
378
390
  `1, 4, 2, 3` under `/plumbbob:build 1-3`): the halt above fires before step 4, and
379
391
  `plumbbob build` names step 1 as the last undone step in the range. The range that walks
@@ -388,7 +400,7 @@ out, or the next step in build order is one the range does not name),
388
400
  - **After an early stop, settle the step, then rerun what is left.** A halt on trouble
389
401
  inside a range ends at that step's pause, and the grant ends with the turn. Once the
390
402
  human approves and the step lands, the state table's Range row names the run that lands
391
- what is left of the range (`| Range | 3 of 5: /plumbbob:run (steps 8-9) |`), and that is
403
+ what is left of the range (`| Range | **3** on Opus: /plumbbob:run (steps 8-9) |`), and that is
392
404
  what the human types to carry on; `/plumbbob:build 8-9` lands the same steps by hand.
393
405
 
394
406
  ## The hard contracts
@@ -47,12 +47,15 @@ Then:
47
47
  the capture plus that one pointer are the whole turn, and both are the CLI's to render,
48
48
  never yours to compose from the dashboard. The capture is a one-beat interruption; the
49
49
  step keeps its focus.
50
- 4. **A question beside the tangent is answered first.** When the human's message also
51
- asks something, answer the question, then relay the park's block after the answer,
52
- and write nothing after it. A message that is only a tangent ends on the block alone.
53
- Either way the block's pointer back at the step closes the turn, so run no
54
- `plumbbob handoff` after it. The answer is what the human asked for; dropping it to
55
- keep the block alone, or stacking a second ending under the block, are the two ways
50
+ 4. **A question beside the tangent is answered after the capture.** When the human's
51
+ message also asks something, run `plumbbob park` first, then answer the question, then
52
+ end the turn on the park's block, typed as it printed, with no tool call after the
53
+ answer ([D121 (reply-after-the-tools)](https://github.com/robmclarty/plumbbob/blob/main/docs/decisions.md#d121)):
54
+ a client that folds text written before a tool call would show the block and hide the
55
+ answer. A message that is only a tangent ends on the block alone. Either way the
56
+ block's pointer back at the step closes the turn, so run no `plumbbob handoff` after
57
+ it. The answer is what the human asked for; dropping it to keep the block alone,
58
+ writing it above the capture, or stacking a second ending under the block are the ways
56
59
  this goes wrong.
57
60
 
58
61
  ## The one hard contract
@@ -75,7 +75,24 @@ The argument only seeds how you get there.
75
75
  stake, in plain words, enough to judge it cold), then a `*lean:*` sub-line (your
76
76
  proposed resolution: one answer to react to, not a menu), so the human settles it in
77
77
  one pass without a round-trip. (A tiny, obvious question may stay one bare line; the
78
- sub-lines earn their keep only when a human decision genuinely waits on it.)
78
+ sub-lines earn their keep only when a human decision genuinely waits on it.) The plan
79
+ card tables the open questions, one row each, and a cell shows only the first sentence
80
+ of its line, so write each to stand on that sentence: the opener asks one question in
81
+ one sentence, `*plain:*` opens on why it matters, and `*lean:*` opens on the answer
82
+ ([D118 (questions-table)](https://github.com/robmclarty/plumbbob/blob/main/docs/decisions.md#d118)).
83
+ The rest of each line is for the cold reader of `intent.md`. The card's
84
+ `yes to all leans` move (or `yes to all leans except Q3: <their answer>`) settles them
85
+ in one reply: on each opener line swap `*resolve by:*` for `*resolved:* <date>, <the
86
+ answer>`, add the Decision the answer settles, and run `plumbbob handoff --plan` again.
87
+ A question the human would rather let the work answer is deferred, not resolved: on
88
+ `defer Q3 until <what settles it>`, keep the question and swap its `*resolve by:*` for
89
+ `*deferred:* <date>, until <what settles it>` on the opener line, then run
90
+ `plumbbob handoff --plan` again. It stays open, the card lists it on a `Deferred` line
91
+ beneath the table, out of reach of `yes to all leans`, and the Questions row counts it
92
+ apart (`**1** to answer · 2 deferred`); once the work answers it, swap the marker for
93
+ `*resolved:*` as above
94
+ ([D120 (deferred-questions)](https://github.com/robmclarty/plumbbob/blob/main/docs/decisions.md#d120)).
95
+ Deferring is the human's call, never yours: a lean you are unsure of is still a lean.
79
96
  4. **Author the Steps.** Write the **full build plan** under `## Steps` (each step a
80
97
  small, verifiable increment) in the exact format the parser reads:
81
98
 
@@ -215,7 +232,8 @@ The argument only seeds how you get there.
215
232
  a message that asks (`expand 2`, or any question) is answered from the detail file or
216
233
  `intent.md` and changes nothing; a message that directs is what to sharpen, cheap
217
234
  now, before any code. The revert move vanishes there on its own: nothing has landed
218
- to wind back.
235
+ to wind back. The turn is your framed plan, then that block, and nothing after it:
236
+ anything you would add goes above the `---` rule the block opens on.
219
237
  - **The plan commit is latched too.** Once the turn ledger exists, `checkpoint
220
238
  --plan` refuses to land in the same turn `start` stamped it: present the plan,
221
239
  **end the turn**, and the human's approving message is the tick that lets it
@@ -230,17 +248,21 @@ The argument only seeds how you get there.
230
248
  run that lands that step's range when the plan declares ranges. Relay that block whole and run no second command; the
231
249
  plan-pause card above is the one place `handoff` is still called, because no verb has
232
250
  run yet there.
233
- - **Say what the commit publishes.** The close-out line names the tracked folder:
234
- `.plumbbob/builds/<slug>/` now rides this branch into the PR, so teammates will
235
- see the plan and build record in review. In the same breath, offer the way out:
236
- a repo that won't track a tool folder can restart with `plumbbob start --local`
237
- (a fully untracked sidecar) before any steps are built.
238
- - **Record-only variant.** When the repo's own `.gitignore` excludes `.plumbbob/`,
239
- the folder can't ride the branch; `start` flags this up front, and the plan
240
- commit lands *record-only*: an empty commit whose message carries the plan
241
- (the `--body` earns its keep here), the files staying untracked. Say that
242
- instead: the rationale is published in `git log`, not the folder, so pass a
243
- fuller `--body` than you otherwise would, since the message is the whole record.
251
+ - **The card says what the commit publishes.** Until the plan commit lands, the plan
252
+ card's `looks good` row names the tracked folder, which rides this branch into the PR
253
+ (teammates will see the plan and build record in review), and a `keep it local` row
254
+ offers the way out. You compose neither note, before the
255
+ card or after it. When the human picks `keep it local`, run `plumbbob start --local`
256
+ on the open session: it excludes `.plumbbob/` in this clone, so the plan and finish
257
+ commits land record-only and the folder never rides the branch. Then run
258
+ `plumbbob handoff --plan` and end on the card, which now says record-only. Once the
259
+ plan has landed, the folder is in a commit and `start --local` refuses.
260
+ - **Record-only variant.** When the repo's own `.gitignore` excludes `.plumbbob/`, or
261
+ the session was kept local, the folder can't ride the branch; `start` flags a
262
+ gitignored repo up front, the card's `looks good` row says record-only, and the plan
263
+ commit lands as an empty commit whose message carries the plan, the files staying
264
+ untracked. The rationale is published in `git log`, not the folder, so pass a fuller
265
+ `--body` than you otherwise would, since the message is the whole record.
244
266
  8. **Offer to stress-test it.** Suggest `/plumbbob:refine` to attack the frame for holes (or
245
267
  to repair the plan as it drifts). Optional, the human's call.
246
268
 
@@ -38,10 +38,19 @@ is fine; refining is always available.
38
38
  may stay one bare line; the sub-lines earn their keep only when a human decision
39
39
  genuinely waits on it.)
40
40
 
41
- Then walk the human through it in the chat: list every hole you appended, each as
42
- its explanation first and its lean second, and invite per-question answers; the
43
- human can settle any of them right there in one message, no doc-flipping, no extra
44
- round-trip per hole.
41
+ Then show the human what is open: run `plumbbob status --questions` and paste its
42
+ table whole, one row per open question with why it matters and your lean, each cell
43
+ the first sentence of its line, so write each line to stand on that sentence. Then
44
+ invite per-question answers; the human can settle any of them right there in one
45
+ message (`yes to all leans`, or `yes to all leans except Q3: <their answer>`), no
46
+ doc-flipping, no extra round-trip per hole. A resolved question gets
47
+ `*resolved:* <date>, <the answer>` in place of its `*resolve by:*` on the opener line. The human
48
+ can also leave one to the work (`defer Q3 until <what settles it>`): keep the question
49
+ and swap its `*resolve by:*` for `*deferred:* <date>, until <what settles it>` on the
50
+ opener line, and the table moves it to a `Deferred` line beneath, still open
51
+ ([D120 (deferred-questions)](https://github.com/robmclarty/plumbbob/blob/main/docs/decisions.md#d120)).
52
+ Never retype the table as a list of your own
53
+ ([D118 (questions-table)](https://github.com/robmclarty/plumbbob/blob/main/docs/decisions.md#d118)).
45
54
  - **Repair (re-sync to reality).** When the plan has drifted (a Decision was overtaken
46
55
  by what you built, a Constraint changed, a Step no longer matches the seam, or a
47
56
  **step's title no longer describes what the diff actually does**), propose the edits
@@ -122,7 +122,7 @@ walk before it, since the checkpoint would refuse it. The halt list is the build
122
122
  red check, a self-review mismatch, a bound agent's `blocked` or `drift`, or a new decision
123
123
  ends at that step's pause card. `plumbbob build` names the top step as the top of the range
124
124
  you granted when it enters it. Once that step lands, its checkpoint prints the boundary
125
- with the Stop row (`| Stop | Range 3 of 5 landed - <its stop> |`); relay none of it, and end
125
+ with the Stop row (`| Stop | **Range 3** landed - <its stop> |`); relay none of it, and end
126
126
  the run on the stop card, below, in the same turn.
127
127
 
128
128
  **A bare range.** `grant: none, as a bare range builds to its pause`: build the range's one
@@ -207,6 +207,15 @@ it, and record nothing in this turn: `plumbbob harvest` refuses in the turn the
207
207
  because the human has not read it yet. If the turn is held with a reason saying the stop card
208
208
  is still owed, do exactly what the reason says.
209
209
 
210
+ *After a halt the human approved, the card comes short.* When the step that completed the
211
+ range halted at its pause and `looks good` landed it, the human read that step's Summary,
212
+ Readout, and recommendation a moment ago, so `plumbbob handoff --stop` prints only the
213
+ Harvested table, the moves, the state table with its Stop row, and the link
214
+ ([D119 (short-stop-after-a-halt)](https://github.com/robmclarty/plumbbob/blob/main/docs/decisions.md#d119)).
215
+ Write the range's detail file all the same, in full: the stop's record reads it, and
216
+ `show the stop` prints the card whole from it. Paste the short card exactly as it printed;
217
+ never add back the parts it left out.
218
+
210
219
  ## 6. The reply at a stop
211
220
 
212
221
  The card's proposals are settled in one reply
@@ -224,14 +233,18 @@ and the Your Call block names the replies. Read the human's message as one of th
224
233
  - **A change** (`make P7 a tangent`, or a change to the review) is direction, and the turn
225
234
  that makes it is your reply
226
235
  ([D110 (reply-after-the-card)](https://github.com/robmclarty/plumbbob/blob/main/docs/decisions.md#d110)).
227
- Rewrite the detail file's line or section, say in your own words what moved, then run
228
- `plumbbob handoff --stop` and paste the one-line reminder it prints beneath your reply.
236
+ Rewrite the detail file's line or section, run `plumbbob handoff --stop` last among your
237
+ tool calls and keep the one-line reminder it prints, then say in your own words what
238
+ moved and end the turn on that line, with no tool call after your reply
239
+ ([D121 (reply-after-the-tools)](https://github.com/robmclarty/plumbbob/blob/main/docs/decisions.md#d121)).
229
240
  The card does not come back for a change; nothing is recorded until `looks good`, which
230
241
  records every lean as the detail file then reads.
231
242
  - **A question**, or `expand 2`, is answered from the detail file, the range's commits, or
232
- the Log, never from recall; then run `plumbbob handoff --stop` and paste the one-line
233
- reminder it prints beneath your answer. Only `show the stop`, or any wording of it, takes
234
- `plumbbob handoff --stop --full`, and never to read the card for yourself.
243
+ the Log, never from recall: read what you need and run `plumbbob handoff --stop` first,
244
+ then write the answer, and end the turn on the one-line reminder it printed, with no tool
245
+ call after the answer. Only `show the stop`, or any wording of it, takes
246
+ `plumbbob handoff --stop --full`, which prints the card whole after a short one too, and
247
+ never to read the card for yourself.
235
248
  - **`/plumbbob:run`** typed at the card confirms everything the way `looks good` does, and
236
249
  runs the next range. Record first: fold the blockers and run `plumbbob harvest`, then read
237
250
  `plumbbob status --run "<the same argument>"` again. When its grant line says
@@ -39,3 +39,10 @@ annotate it.
39
39
  the `next →` names the run to type, `/plumbbob:run` with the steps it will land beside
40
40
  it. You relay the block with the rest; a `⚠` row is `/plumbbob:refine`'s to fix.
41
41
  4. If it reads `NO ACTIVE SESSION`, tell the human to `/plumbbob:plan` to frame a goal.
42
+ 5. If the human asks what is still open, run `plumbbob status --questions` and relay its
43
+ output whole: a table row per question that waits on the human, linked to its entry in
44
+ `intent.md`, with why it matters and the lean, and a `Deferred` line beneath it naming
45
+ the questions left to the work and what settles each
46
+ ([D120 (deferred-questions)](https://github.com/robmclarty/plumbbob/blob/main/docs/decisions.md#d120)).
47
+ Never list the questions in your own words
48
+ ([D118 (questions-table)](https://github.com/robmclarty/plumbbob/blob/main/docs/decisions.md#d118)).