plumbbob 0.13.4 → 0.13.6

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,8 +10,10 @@
10
10
  // A stop's record is latched (D103 (stop-latched)). It refuses in the turn
11
11
  // the stop card printed, since a run's long turn ends on proposals and that is
12
12
  // where a model would carry straight on into recording its own; it refuses a
13
- // range's detail no stop card showed, read off the digest the card's STOP
14
- // stamp keeps; and it refuses a range that no longer reads as landed, telling
13
+ // range no stop card showed, read off the range the card's STOP stamp names,
14
+ // though not a detail file rewritten since, which is a lean the human changed
15
+ // in a reply (D110 (reply-after-the-card)); and it refuses a range that no
16
+ // longer reads as landed, telling
15
17
  // a step landed and reopened from one that joined the range after it landed,
16
18
  // which moves to a range still to come. A harvest outside a stop (`# Detail · Harvest`) needs no stamp. A proposal
17
19
  // naming a handle the list lacks (`P9` with eight parked), or a class outside
@@ -24,9 +26,9 @@ import { ending, notice, printEnding, transition } from "../lib/notice.js";
24
26
  import { appendHarvest, appendToSection, flipParks, harvestLines, logEntry, stopLogLine } from "../lib/buildlog.js";
25
27
  import { checkStopLatch } from "../lib/latch.js";
26
28
  import { parseBuildRanges, parseDetailStep, parseLastCheckpoint, parseOrderedSteps, stepShas } from "../lib/orient.js";
27
- import { activeBuild, buildLogPath, checkpointsPath, clearDetail, clearStop, detailDigest, detailPath, hasSession, intentPath, readStop, } from "../lib/sidecar.js";
29
+ import { activeBuild, buildLogPath, checkpointsPath, clearDetail, clearStop, detailPath, hasSession, intentPath, readStop, } from "../lib/sidecar.js";
28
30
  import { hasStep } from "../lib/steplist.js";
29
- import { boundaryEnding, harvestProposals, parkList, parseDetailRange, stopRecord } from "./handoff.js";
31
+ import { boundaryEnding, harvestProposals, isHarvestDetail, parkList, parseDetailRange, stopRecord, } from "./handoff.js";
30
32
  // The three classes a park settles as, in the order the ending counts them.
31
33
  const CLASSES = ['blocker', 'tangent', 'pivot'];
32
34
  // Where a refusal about the proposals sends the model: the file it wrote them in.
@@ -57,7 +59,7 @@ export function harvest(cwd, _args) {
57
59
  const parks = parkList(buildLog);
58
60
  let stop = null;
59
61
  if (item !== null) {
60
- const found = stopRange(root, slug, intent, detail, item);
62
+ const found = stopRange(root, slug, intent, item);
61
63
  if (typeof found === 'string') {
62
64
  process.stderr.write(found);
63
65
  return 1;
@@ -101,26 +103,31 @@ export function harvest(cwd, _args) {
101
103
  printEnding(root, {
102
104
  tier: 'orientation',
103
105
  render: 'harvest',
104
- text: ending({ lead, pointer: boundaryEnding(root, slug, null).pointer }),
106
+ text: ending({ lead, state: boundaryEnding(root, slug, null).state }),
105
107
  });
106
108
  return 0;
107
109
  }
108
110
  /**
109
111
  * The range a range's detail reviews, once the record may land: the plan
110
- * holds the range, a stop card showed this detail, a human turn has read it,
112
+ * holds the range, a stop card for it has shown, a human turn has read it,
111
113
  * and every step of the range has landed. Otherwise the refusal, with the move
112
114
  * that unblocks it.
113
115
  *
116
+ * The card need not have shown this detail file byte for byte: a reply that
117
+ * changed a lean rewrote the file without printing the card again, and the
118
+ * change is the human's own (D110 (reply-after-the-card)).
119
+ *
114
120
  * An undone step in the range reads two ways, told apart by the ledger. One
115
121
  * with a `step N <sha>` line landed and was reopened, so it lands again and
116
- * the card re-renders. One with none joined the range after the card, a
122
+ * the card prints again on `--stop --full`, since the range it reviews has
123
+ * changed under it. One with none joined the range after the card, a
117
124
  * blocker folded into the range that just landed, and `handoff --stop` would
118
125
  * refuse too, since the last landing no longer completes the range; so the
119
126
  * remedy moves the step to a range still to come and runs the record again,
120
127
  * which holds in the same reply while the detail file stays as the card
121
128
  * showed it.
122
129
  */
123
- function stopRange(root, slug, intent, detail, item) {
130
+ function stopRange(root, slug, intent, item) {
124
131
  const plan = parseBuildRanges(intent);
125
132
  const count = plan.ranges.length + plan.malformed.length;
126
133
  const range = plan.ranges.find((r) => r.item === item);
@@ -129,22 +136,22 @@ function stopRange(root, slug, intent, detail, item) {
129
136
  return refuse(`the plan has no range ${item}`, `write the range's number in the header of .plumbbob/detail.md`);
130
137
  }
131
138
  const stamp = readStop(root, slug);
132
- if (stamp === null || stamp.range !== item || stamp.detail !== detailDigest(detail)) {
133
- return refuse('no stop card showed its detail', 'plumbbob handoff --stop, then end the turn on the card');
139
+ if (stamp === null || stamp.range !== item) {
140
+ return refuse('no stop card showed it', 'plumbbob handoff --stop, then end the turn on the card');
134
141
  }
135
142
  if (!checkStopLatch(root, stamp.turn)) {
136
143
  return refuse("its card printed this turn, and no human turn has read it", "end the turn on the card; the human's reply is what records it");
137
144
  }
138
145
  const held = parseOrderedSteps(intent).filter((s) => hasStep(range.steps, s.n));
139
146
  if (held.length === 0) {
140
- return refuse('the plan holds none of its steps', 'land it, then plumbbob handoff --stop');
147
+ return refuse('the plan holds none of its steps', 'land it, then plumbbob handoff --stop --full');
141
148
  }
142
149
  const open = held.find((s) => !s.done);
143
150
  if (open !== undefined) {
144
151
  // A step the ledger landed was reopened since, and lands again; one it never
145
152
  // landed was folded into the range after the card, and moves out instead.
146
153
  return stepShas(readOr(checkpointsPath(root, slug))).has(open.n)
147
- ? refuse(`step ${open.n} is open again`, 'land it, then plumbbob handoff --stop')
154
+ ? refuse(`step ${open.n} is open again`, 'land it, then plumbbob handoff --stop --full')
148
155
  : refuse(`step ${open.n} joined it after it landed`, `move step ${open.n} to a range still to come, then plumbbob harvest`);
149
156
  }
150
157
  return { item, count, stop: range.stop };
@@ -183,14 +190,6 @@ function boundaryWords(root, slug) {
183
190
  const last = parseLastCheckpoint(readOr(checkpointsPath(root, slug)));
184
191
  return last === null ? 'before the first step landed' : `at the step ${last.n} boundary`;
185
192
  }
186
- /**
187
- * Whether a detail file is a boundary harvest's wire, read off its
188
- * `# Detail · Harvest` header.
189
- */
190
- export function isHarvestDetail(detail) {
191
- const first = detail.trimStart().split('\n', 1)[0] ?? '';
192
- return /^#\s+Detail\s*·\s*Harvest\s*$/.test(first);
193
- }
194
193
  /**
195
194
  * The refusal for a detail file that is no range's and no harvest's: a step's
196
195
  * is checkpoint's to record, and an empty one, or one with no header, records nothing.
@@ -66,7 +66,9 @@ export function relayCheck(cwd, raw) {
66
66
  // A host too old to send the final message has nothing to check against.
67
67
  if (payload === null || typeof payload.last_assistant_message !== 'string')
68
68
  return null;
69
- const root = findSessionRoot(typeof payload.cwd === 'string' ? payload.cwd : cwd);
69
+ // The session the payload names, else the one the process stands in: the
70
+ // rule the turn hook ticks by, so the two always read the same session.
71
+ const root = (typeof payload.cwd === 'string' ? findSessionRoot(payload.cwd) : null) ?? findSessionRoot(cwd);
70
72
  if (root === null)
71
73
  return null;
72
74
  const record = readRelay(root);
@@ -135,13 +137,24 @@ function sameParts(relayed, printed, counted) {
135
137
  }
136
138
  /**
137
139
  * The facts the CLI measured, as one comparable string: the Readout table,
138
- * whitespace aside, and the Verdict line. Every other line of a block may be
140
+ * whitespace aside, and the Verdict's fact. Every other line of a block may be
139
141
  * reworded or reordered without the relay counting as missed.
140
142
  */
141
143
  function factsOf(parts) {
142
144
  const readout = parts.find((p) => p.label === 'Readout');
143
145
  const verdict = parts.find((p) => p.label === 'Verdict');
144
- return `${flat(readout === undefined ? '' : tableOf(readout))}\n${flat(verdict?.lines[0] ?? '')}`;
146
+ return `${flat(readout === undefined ? '' : tableOf(readout))}\n${flat(verdict === undefined ? '' : verdictOf(verdict))}`;
147
+ }
148
+ /**
149
+ * The Verdict's fact: the second cell of the state table's header row, where
150
+ * a card carries it (D111 (state-table)), or the text after the label on the
151
+ * line an older ending printed. Read as the fact, not the line, so a relay
152
+ * that changed the spacing around the table's pipes still carries the Verdict
153
+ * it printed.
154
+ */
155
+ function verdictOf(part) {
156
+ const first = part.lines[0] ?? '';
157
+ return stateRowOf(first)?.value ?? first.replace(/^\*\*Verdict\*\*:\s*/, '');
145
158
  }
146
159
  /** The `text` fence inside the Readout part, rails included, or '' when it has none. */
147
160
  function tableOf(part) {
@@ -180,8 +193,8 @@ function blockCount(part) {
180
193
  }
181
194
  /**
182
195
  * An ending's labeled parts, in order: each opens on a line that starts with a
183
- * bold label and a colon, outside a fence, and runs to the next. Text before
184
- * the first label is not part of any.
196
+ * bold label and a colon, or on a row of the state table, outside a fence, and
197
+ * runs to the next. Text before the first label is not part of any.
185
198
  */
186
199
  function partsOf(text) {
187
200
  const parts = [];
@@ -196,7 +209,7 @@ function partsOf(text) {
196
209
  parts.at(-1)?.lines.push(line);
197
210
  continue;
198
211
  }
199
- const label = rail === null ? /^\*\*([^*\n]+)\*\*:(?:\s|$)/.exec(line)?.[1] : undefined;
212
+ const label = rail === null ? labelOf(line) : undefined;
200
213
  if (label !== undefined) {
201
214
  parts.push({ label, lines: [line] });
202
215
  }
@@ -206,6 +219,35 @@ function partsOf(text) {
206
219
  }
207
220
  return parts;
208
221
  }
222
+ /**
223
+ * The label a line opens a part on, or undefined when it opens none: a bold
224
+ * label and a colon at the line's start, or a state-table row, whose first
225
+ * cell is its label. Each row of the table is a part of its own, so the
226
+ * table's rows are read in order as the labeled lines were.
227
+ */
228
+ function labelOf(line) {
229
+ return /^\*\*([^*\n]+)\*\*:(?:\s|$)/.exec(line)?.[1] ?? stateRowOf(line)?.label;
230
+ }
231
+ /**
232
+ * A state-table row's label and fact, or null for any other line: a table row
233
+ * of two cells, its label with the bold stripped. The delimiter row and the
234
+ * lean table's four-cell rows are no state row, so they stay inside the part
235
+ * above them.
236
+ */
237
+ function stateRowOf(line) {
238
+ const cells = tableCells(line);
239
+ const [label, value] = cells ?? [];
240
+ if (cells === null || cells.length !== 2 || label === undefined || value === undefined || /^:?-{3,}:?$/.test(label))
241
+ return null;
242
+ return { label: label.replace(/^\*\*(.*)\*\*$/, '$1').trim(), value };
243
+ }
244
+ /** The cells of a markdown table row, split on unescaped pipes, or null when the line is not one. */
245
+ function tableCells(line) {
246
+ const text = line.trim();
247
+ if (text.length < 2 || !text.startsWith('|') || !text.endsWith('|'))
248
+ return null;
249
+ return text.slice(1, -1).split(/(?<!\\)\|/).map((c) => c.trim());
250
+ }
209
251
  /**
210
252
  * The one-line reason the model acts on and the human reads in full: what the
211
253
  * turn missed, and the command or block that puts it back.
@@ -13,28 +13,37 @@
13
13
  // range the run is landing. The harness also sends a background job's
14
14
  // completion notice through this event, as a prompt the human never typed, so
15
15
  // a notice ticks nothing, keeps the grant and the mark, and clears only the
16
- // ending `RELAY` holds from an earlier turn. When a step is in flight a typed
17
- // prompt also emits ONE `UserPromptSubmit` additionalContext line so a fresh
18
- // session (post-compaction, or a scripted `-p` turn where no skill prose is in
19
- // context) still knows the beat: a tangent is parked, not chased, and a
20
- // message that also asks something is answered before the park's block.
21
- // Guidance only: it blocks nothing. It ALWAYS exits 0 and any failure degrades
22
- // to no output, so neither the tick nor the emit can ever wedge prompting.
16
+ // ending `RELAY` holds from an earlier turn. A typed prompt also emits ONE
17
+ // `UserPromptSubmit` additionalContext line when there is a beat to keep, so a
18
+ // fresh session (post-compaction, or a scripted `-p` turn where no skill prose
19
+ // is in context) still knows it: mid-step, a tangent is parked, not chased;
20
+ // at a pause or a stop whose card has shown, the message is the human's reply,
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
23
+ // only: it blocks nothing. It ALWAYS exits 0 and any failure degrades to no
24
+ // output, so neither the tick nor the emit can ever wedge prompting.
23
25
  import { readFileSync, writeFileSync } from 'node:fs';
24
26
  import { parseBuildRanges, readRunArgs, resolveRun } from "../lib/orient.js";
25
- import { clearRelay, findSessionRoot, inSpike, intentPath, notePrompt, readPrompt, readRun, setGrant, setRun, stepPath, turnPath, } from "../lib/sidecar.js";
27
+ import { activeBuild, clearRelay, findSessionRoot, inSpike, intentPath, notePrompt, readPause, readPrompt, readRun, setGrant, setRun, stepPath, turnPath, } from "../lib/sidecar.js";
26
28
  import { hasStep, normalizeStepList, readBuildArgs } from "../lib/steplist.js";
29
+ import { waitingCard } from "./handoff.js";
27
30
  /**
28
- * The hook entry point: apply the prompt, then emit the in-flight-step nudge
29
- * when it counted as a human turn.
31
+ * The hook entry point: apply the prompt, then emit the nudge when it counted
32
+ * as a human turn.
30
33
  *
34
+ * The session is the one the payload's `cwd` names when it names one, else
35
+ * the one the process stands in, the rule the `Stop` check follows too, so the
36
+ * turn that ticks and the turn that is judged are always the same session's.
31
37
  * Always returns 0: a broken turn must never wedge a prompt, so any failure is
32
38
  * swallowed and degrades to no output.
33
39
  */
34
40
  export function turn(cwd, _args) {
35
41
  try {
36
- if (applyTurn(cwd, readStdin())) {
37
- const context = stepInFlightContext(cwd);
42
+ const raw = readStdin();
43
+ const { cwd: named, prompt } = readPayload(raw);
44
+ const where = named !== null && findSessionRoot(named) !== null ? named : cwd;
45
+ if (applyTurn(where, raw)) {
46
+ const context = turnContext(where, prompt);
38
47
  if (context !== null)
39
48
  process.stdout.write(context);
40
49
  }
@@ -45,32 +54,102 @@ export function turn(cwd, _args) {
45
54
  return 0;
46
55
  }
47
56
  /**
48
- * The `UserPromptSubmit` additionalContext payload when a step is in flight, or null.
57
+ * The `UserPromptSubmit` additionalContext payload for the beat the session is
58
+ * on, or null when there is none to keep.
49
59
  *
50
- * A step is in flight when a STEP marker (the flat control file recording the
51
- * step between `build` and `checkpoint`) is present. Guidance only: it reminds a
52
- * fresh session that a tangent is a park (not an edit), that the park's own
53
- * block is the whole turn when the message is only a tangent, that a message
54
- * which also asks something is answered first with the block after the answer,
55
- * and how the step lands, since neither the build nor the park prose may be in
56
- * context after compaction or on a scripted turn.
57
- * Exported because an in-process test cannot read the hook's real stdout.
60
+ * Three beats, read off the control files. A step whose pause card has shown
61
+ * (STEP, and PAUSE naming that step's pause) is waiting for the human's call,
62
+ * 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
65
+ * mid-build, where a tangent is a park and not an edit. A stop card waiting
66
+ * for its reply is the pause's beat at a stop. At a pause or a stop, a
67
+ * `prompt` asking to see the card (`show the pause`, the phrase the card's
68
+ * `expand` move teaches) gets the one instruction that matters for it: paste
69
+ * the `--full` output exactly as it printed, as the whole turn. Guidance only,
70
+ * since none of the build, run, or park prose may be in context after
71
+ * compaction or on a scripted turn. Exported because an in-process test cannot
72
+ * read the hook's real stdout.
58
73
  */
59
- export function stepInFlightContext(cwd) {
74
+ export function turnContext(cwd, prompt = '') {
60
75
  const root = findSessionRoot(cwd);
61
76
  if (root === null)
62
77
  return null;
78
+ const text = nudgeText(root, SHOW_REQUEST.test(prompt));
79
+ return text === null ? null : `${JSON.stringify({ hookSpecificOutput: { hookEventName: 'UserPromptSubmit', additionalContext: text } })}\n`;
80
+ }
81
+ // A message asking to see the card again: `show the pause` and `show the stop`,
82
+ // the phrases the card's `expand` move teaches, and `show me the card` beside
83
+ // them. In a fresh session the model ran `--full` on that request and then
84
+ // retyped the card behind a line of its own, three runs in five of the
85
+ // 2026-10-27 eval, because nothing in its context said how to relay it.
86
+ const SHOW_REQUEST = /\bshow\s+(?:me\s+)?the\s+(?:pause|stop|card)\b/i;
87
+ /**
88
+ * The nudge's words for the beat the session is on, or null when it is on
89
+ * none: no step in flight and no stop card waiting. `showing` is a message
90
+ * that asks to see the card, which at a pause or a stop takes the show nudge.
91
+ */
92
+ function nudgeText(root, showing) {
63
93
  const step = inFlightStep(root);
64
- if (step === null)
94
+ if (step !== null) {
95
+ const shown = readPause(root);
96
+ if (shown?.kind !== 'pause' || shown.step !== step)
97
+ return parkNudge(step);
98
+ return showing ? showNudge(`step ${step}'s card`, 'plumbbob handoff --full') : pauseNudge(step);
99
+ }
100
+ const slug = activeBuild(root);
101
+ let intent = '';
102
+ try {
103
+ intent = readFileSync(intentPath(root, slug), 'utf8');
104
+ }
105
+ catch {
65
106
  return null;
66
- const text = `plumbbob: step ${step} is in flight — a new idea or tangent is a park, not an edit: ` +
107
+ }
108
+ const card = waitingCard(root, slug, intent);
109
+ if (card === null)
110
+ return null;
111
+ return showing
112
+ ? showNudge(`range ${card.range} of ${card.count}'s stop card`, 'plumbbob handoff --stop --full')
113
+ : stopNudge(card.range, card.count);
114
+ }
115
+ /**
116
+ * The nudge when the human asks to see a card again: the one command and how
117
+ * to relay what it prints, since the card is the whole turn and a retyped one
118
+ * is the miss the relay check holds.
119
+ */
120
+ function showNudge(card, command) {
121
+ return (`plumbbob: the human asked to see ${card} again. ` +
122
+ `Run \`${command}\` and paste its output exactly as it printed, as the whole turn: ` +
123
+ `no line before it, nothing after it, and nothing in it retyped, reworded, or cut.`);
124
+ }
125
+ /** The mid-build nudge: a tangent is a park, not an edit, and the step lands on approval. */
126
+ function parkNudge(step) {
127
+ return (`plumbbob: step ${step} is in flight — a new idea or tangent is a park, not an edit: ` +
67
128
  `capture it with \`plumbbob park "<one line>"\` and relay the block it prints whole, ` +
68
129
  `with nothing written around it, since that block is the whole turn. ` +
69
130
  `The one exception is a message that also asks something: answer the question first, ` +
70
131
  `then relay the park's block after the answer and write nothing after it. ` +
71
132
  `The block's pointer back at the step closes the turn either way, so run no \`plumbbob handoff\` after it. ` +
72
- `The step stays open and lands when you run \`plumbbob checkpoint ${step}\` after approval.`;
73
- return `${JSON.stringify({ hookSpecificOutput: { hookEventName: 'UserPromptSubmit', additionalContext: text } })}\n`;
133
+ `The step stays open and lands when you run \`plumbbob checkpoint ${step}\` after approval.`);
134
+ }
135
+ /** The nudge at a pause whose card has shown: the message is the reply, and the card is not printed again. */
136
+ function pauseNudge(step) {
137
+ return (`plumbbob: step ${step}'s card has shown, and this message is the human's reply to it. ` +
138
+ `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. ` +
142
+ `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
+ `A tangent the human hands you is still a park: \`plumbbob park "<one line>"\`.`);
144
+ }
145
+ /** The nudge at a stop card waiting for its reply: the stop's version of the pause's. */
146
+ function stopNudge(range, count) {
147
+ return (`plumbbob: range ${range} of ${count}'s stop card has shown, and this message is the human's reply to it. ` +
148
+ `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. ` +
152
+ `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.`);
74
153
  }
75
154
  /**
76
155
  * The in-flight step number, or null when no STEP marker is present.
@@ -251,13 +330,13 @@ function readBuild(root) {
251
330
  }
252
331
  }
253
332
  /**
254
- * The `prompt` and `prompt_id` fields of the UserPromptSubmit JSON, as '' and
255
- * null when either is absent.
333
+ * The `prompt`, `prompt_id`, and `cwd` fields of the UserPromptSubmit JSON, as
334
+ * '' and null when any is absent.
256
335
  *
257
336
  * Absent, unparseable, or non-string input contributes nothing rather than
258
337
  * wedging the tool: the tick still lands (a human turn did occur), the empty
259
- * prompt clears any GRANT, and a missing id records none; the verb never
260
- * throws over bad stdin.
338
+ * prompt clears any GRANT, a missing id records none, and a missing `cwd`
339
+ * leaves the process's; the verb never throws over bad stdin.
261
340
  */
262
341
  function readPayload(raw) {
263
342
  try {
@@ -265,10 +344,11 @@ function readPayload(raw) {
265
344
  return {
266
345
  prompt: typeof parsed.prompt === 'string' ? parsed.prompt : '',
267
346
  promptId: typeof parsed.prompt_id === 'string' && parsed.prompt_id !== '' ? parsed.prompt_id : null,
347
+ cwd: typeof parsed.cwd === 'string' && parsed.cwd !== '' ? parsed.cwd : null,
268
348
  };
269
349
  }
270
350
  catch {
271
- return { prompt: '', promptId: null };
351
+ return { prompt: '', promptId: null, cwd: null };
272
352
  }
273
353
  }
274
354
  /**
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "plumbbob",
3
- "version": "0.13.4",
3
+ "version": "0.13.6",
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",
@@ -168,19 +168,23 @@ switch with `/model` and rerun to honor it, or wave you on. Advisory, never a ga
168
168
  fence's 80 columns (13 of label, 67 of value); two or more items break onto indented
169
169
  continuation lines opening with `-`, and a red row's evidence onto an indented `→` line.
170
170
 
171
- The `## Summary` lead and the `## <n>` section titles are the turn's opening block:
172
- handoff prints the lead behind the `**Summary**:` label, appends the `(details: …)`
173
- bracket, and renders the titles as the numbered highlights, so you type neither the path
174
- nor the list. Five highlights at most, each one sentence and one move, plain English
175
- first, a path in backticks only when the name itself is the news. The numbers are
176
- handles: "expand 2" opens `## 2`, so every highlight has a section behind it with the
177
- full story. A judgment or a flag (a stray the seam row will name, a decision you had to
178
- bend, a doubt about the done-when) is one of those highlights, never a paragraph of its
179
- own. The recommendation is the last section: the move you would take as its own
171
+ The `## Summary` lead opens the card: handoff prints it behind the `**Summary**:` label.
172
+ The `## <n>` sections stay in the detail file, where `expand 2` opens `## 2` and the
173
+ build log's record keeps them; the card prints none of them and closes instead on a
174
+ `[more details](…)` link to the file, which handoff writes, so you type no path. Five
175
+ sections at most, each titled with one sentence and one move, plain English first, a
176
+ path in backticks only when the name itself is the news. A judgment or a flag (a stray
177
+ the seam row will name, a decision you had to bend, a doubt about the done-when) is one
178
+ of those sections, and the card carries it where it bears on the call: a bent row's `→`
179
+ line points at it, and your recommendation names it when it changes the move. It is
180
+ never a paragraph of its own. The recommendation is the last section: the move you would take as its own
180
181
  sentence, closed by a period, then the reason as a capitalized sentence or two. The lead
181
182
  and the recommendation are flowing prose and handoff unwraps both to the renderer's
182
183
  width, so never hard-wrap them; handoff also prepends the bold `**Recommendation**:`
183
- label, which you never type. `checkpoint` records this file beneath the step's dated
184
+ label, which you never type. A shell command you want the human to run never rides
185
+ inside a sentence: end the sentence on a colon and give the command a fenced `sh` block
186
+ of its own, one command to a line, which handoff keeps line for line. A slash command
187
+ or a path stays in backticks in the sentence. `checkpoint` records this file beneath the step's dated
184
188
  line in the build-log's `## Log` and then truncates it, so the detail lands in the
185
189
  tracked ledger rather than piling up on disk.
186
190
 
@@ -192,14 +196,15 @@ switch with `/model` and rerun to honor it, or wave you on. Advisory, never a ga
192
196
 
193
197
  *Paste the turn.* Run `plumbbob handoff` and paste its output whole, verbatim, at top
194
198
  level, trailing blank line included, then end the turn. Never nest it inside a fence of
195
- your own; it carries fences of its own and they cannot nest. It prints the Now line
196
- naming the step in flight, the Summary and its highlights, the Readout fence (its
197
- measured `check`, `seam`, `diff`, and `spent` rows folded with your three), a `diff`
198
- fence when the change is 20 lines or fewer, the Verdict folded worst-of from the same
199
- rows, Next Up, Your Call, and your recommendation last. Run it once: a second run at an
200
- unchanged pause prints the one-line reminder instead of the card, so if that line comes
201
- back where the card belongs (you ran handoff earlier in this turn), run
202
- `plumbbob handoff --full` and paste that. The gate verdict's one home is that `check`
199
+ your own; it carries fences of its own and they cannot nest. It prints the Summary, the
200
+ Readout fence (its measured `check`, `seam`, `diff`, and `spent` rows folded with your
201
+ three, the changed files listed beneath `diff`), a `diff` fence when the change is 20
202
+ lines or fewer, Your Call, the state table (the Verdict folded worst-of from the same
203
+ rows, the Now row naming the step in flight, Next Up, and the Range, Order, and Parked
204
+ rows when they apply), your recommendation, and the `more details` link last. Run it
205
+ once: a second run at the same pause prints the one-line reminder instead of the card,
206
+ so if that line comes back where the card belongs (you ran handoff earlier in this
207
+ turn), run `plumbbob handoff --full` and paste that. The gate verdict's one home is that `check`
203
208
  row: no standalone verdict line exists, and you never restate the verdict in prose. A
204
209
  narrowed run names the slots it skipped there (`· without test`), and that named
205
210
  narrowing is the whole disclosure. The notice checkride's Stop hook appends after your
@@ -208,37 +213,39 @@ switch with `/model` and rerun to honor it, or wave you on. Advisory, never a ga
208
213
  one, the write-versus-relay line of
209
214
  [D78 (em-dash-ban)](https://github.com/robmclarty/plumbbob/blob/main/docs/decisions.md#d78).
210
215
 
211
- *Then read the reply as an ask or a direction.* A message that **asks** ("expand 2",
212
- "what does that mean?", "why did the seam row flag that?") is an expand: answer it from
213
- the detail file, from `git diff`, or from the build-log's `## Log` for an older step,
214
- never from recall, then run `plumbbob handoff` and paste its output beneath your answer,
215
- whole and verbatim. The turn is your answer, then that output, and nothing after it. The
216
- pause renders its card once
217
- ([D87 (pause-once)](https://github.com/robmclarty/plumbbob/blob/main/docs/decisions.md#d87)):
218
- while nothing about it has changed, handoff prints one line,
219
- `**Next Up**: Back to Step <N> of <M> - <title>`, in place of the card the human already
220
- read, so your answer stays on screen. The CLI picks the form, never you: paste whichever
221
- one it prints, and never retype the card or reach for `--full` to put it beneath an
222
- answer.
223
-
224
- Two moments bring the whole card back, and in both you run `plumbbob handoff --full` and
225
- paste its output whole, with nothing written around it: the human asks to see the pause
226
- (`show the pause`, or any wording of it), or the card is not in this conversation (a
227
- fresh session, or one after `/clear`, that finds the step still at its pause). A
228
- message that **directs** is needs-work: take it as what to change, and nothing lands
229
- until the human says `looks good`. Once the change is made, rewrite the detail file and
230
- end on `plumbbob handoff` as the first render did; the pause has changed, so it prints
231
- the whole card.
216
+ *Then answer the reply in your own words.* The card shows once
217
+ ([D110 (reply-after-the-card)](https://github.com/robmclarty/plumbbob/blob/main/docs/decisions.md#d110)):
218
+ every later turn at this pause is your reply, closed by one line. A message that
219
+ **asks** ("expand 2", "what does that mean?", "why did the seam row flag that?") is an
220
+ expand: answer it from the detail file, from `git diff`, or from the build-log's
221
+ `## Log` for an older step, never from recall. A message that **directs** is needs-work:
222
+ make the change, run the check, and say what moved and what the check found; when the
223
+ change moves what the card said (a judgment row, the Summary, the recommendation), write
224
+ 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.
230
+
231
+ The card comes back one way: the human asks to see it (`show the pause`, or any wording
232
+ of it). Then run `plumbbob handoff --full` and paste its output whole, with nothing
233
+ written around it. Never run `--full` to read the card for yourself, after a compaction
234
+ or in a fresh session; what it prints is the turn's ending, and the relay check holds a
235
+ turn that does not close on it. Read the detail file and `plumbbob status` instead. A
236
+ tangent the human hands you at the pause is still a park.
232
237
 
233
238
  **Then hand off with the next model.** `plumbbob checkpoint` prints the whole boundary
234
- ending itself: `**Checkpoint**: Step N complete (<sha>)`, the Verdict the step earned,
235
- any advisory (a staged path outside the seam is the common one), the Stop part when
236
- this landing completes a range (`**Stop**: Range 3 of 5 landed - <its stop>`, the reason
237
- the build stops here), and Next Up. Relay that block whole, the Stop part with the rest
238
- (the relay check holds a boundary that dropped it), run no second command, and write
239
- nothing around it. It is the
240
- orientation tier (no Your Call block, no recommendation), and it points at the next
241
- undone step carrying that step's `- model:` recommendation (the plan's
239
+ ending itself: `**Checkpoint**: Step N complete (<sha>)`, any advisory (a staged path
240
+ 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
242
+ stop> |`, the reason the build stops here), Next Up, and the rows beneath it. Relay that
243
+ block whole, the table with the rest (the relay check holds a boundary that dropped a
244
+ 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
247
+ orientation tier (no Your Call block, no recommendation), and its Next Up points at the
248
+ next undone step carrying that step's `- model:` recommendation (the plan's
242
249
  smallest-model-that-fits call) so the human knows which `/model` to select before
243
250
  running `/plumbbob:build` again. It is what carries the plan's suggestion across a fresh
244
251
  context window, which inherits the session model, not the plan's. No `- model:` line
@@ -376,13 +383,13 @@ out, or the next step in build order is one the range does not name),
376
383
  the next one. A run's range ends on the stop card, which lists every open park with a
377
384
  proposed class, and the reply that settles the card records them (the run skill's
378
385
  section on the reply at a stop). A range landed here, by a typed range, ends on the plain
379
- boundary instead; when the human asks what comes next there with parks open (the note's
380
- `parked 2 of 7 open`), the move is `/plumbbob:harvest`, then the run the card names.
386
+ boundary instead; when the human asks what comes next there with parks open (the table's
387
+ `Parked` row, `2 of 7 open`), the move is `/plumbbob:harvest`, then the run the card names.
381
388
  - **After an early stop, settle the step, then rerun what is left.** A halt on trouble
382
389
  inside a range ends at that step's pause, and the grant ends with the turn. Once the
383
- human approves and the step lands, the note beneath Next Up names the run that lands
384
- what is left of the range (`· range 3 of 5: /plumbbob:run (steps 8-9)`), and that is what
385
- the human types to carry on; `/plumbbob:build 8-9` lands the same steps by hand.
390
+ 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
392
+ what the human types to carry on; `/plumbbob:build 8-9` lands the same steps by hand.
386
393
 
387
394
  ## The hard contracts
388
395
 
@@ -41,7 +41,13 @@ record.
41
41
  - `## Decisions and why`: the settled calls that shaped the build (the *why* behind
42
42
  the log). Copy each one's `D# (slug)` from `intent.md` (never a bare `D#`) so
43
43
  the report reads without flipping back to the intent.
44
- - `## Parked & harvested`: what was captured and how each was classified.
44
+ - `## Parked & harvested`: what was captured and how each was classified, as the
45
+ table the stop card's leans use, with the settled class where the card had the lean
46
+ ([D112 (lean-table)](https://github.com/robmclarty/plumbbob/blob/main/docs/decisions.md#d112)):
47
+ `| Park | What it is | Why it matters | Settled |`, one row per park, its handle and a
48
+ short title, the park's text as the Park list holds it, why it matters (the why the
49
+ stop's record kept, or your own clause for a park harvested before the table), and
50
+ the class it settled as with its reason.
45
51
  - `## Final status`: done or partial, and what is left.
46
52
  - `## Deferred tangents`: the harvested items that become future work.
47
53
  Stop there. `plumbbob finish` appends `## Checkpoints` (the baseline and step SHAs) and
@@ -74,9 +80,9 @@ record.
74
80
  the `plumbbob finish` marker subject only.
75
81
  4. **Point at the next goal.** Relay `plumbbob finish`'s output verbatim, blank lines
76
82
  included: its lead line, which names the branch the tracked folder now rides into the
77
- PR, any advisory beneath it, and `**Next Up**: Nothing planned - /plumbbob:plan`. That
78
- is the orientation tier's close (the
79
- [turn anatomy](https://github.com/robmclarty/plumbbob/blob/main/docs/presentation.md)):
83
+ PR, any advisory beneath it, and its state table, whose one row is
84
+ `| **Next Up** | Nothing planned - /plumbbob:plan |`. That is the orientation tier's close
85
+ (the [turn anatomy](https://github.com/robmclarty/plumbbob/blob/main/docs/presentation.md)):
80
86
  the lead line, then the forward pointer. `finish` renders that pointer itself rather
81
87
  than leaving it to `plumbbob handoff`, because finish has just cleared the session
82
88
  handoff would read one from. Relay the block and write nothing after it.