plumbbob 0.13.3 → 0.13.5

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.
@@ -18,7 +18,6 @@
18
18
  // human-owned (check/auto): the tool only ever reads it, never rewrites it. The
19
19
  // phase the dashboard shows (DESIGN/BUILD/SPIKE) is derived, not stored: BUILD ⇔
20
20
  // a STEP is in flight, SPIKE ⇔ the SPIKE marker is present, otherwise DESIGN.
21
- import { createHash } from 'node:crypto';
22
21
  import { existsSync, readdirSync, readFileSync, writeFileSync, appendFileSync, rmSync, mkdirSync } from 'node:fs';
23
22
  import { dirname, join } from 'node:path';
24
23
  import { gitPath } from "./git.js";
@@ -215,56 +214,85 @@ export function clearStep(root, slug) {
215
214
  rmSync(stepPath(root, slug), { force: true });
216
215
  }
217
216
  /**
218
- * PAUSE: the stamp of the step pause `handoff` last rendered whole, the step
219
- * number and a hash of what the card showed. While a later render matches it,
220
- * handoff prints the one-line reminder instead of the card.
217
+ * PAUSE: which card the human has already read, so a later `handoff` prints
218
+ * the one line that points back at it instead of the card. It holds a step
219
+ * number once that step's pause card has shown (`2`), or `landed 2` once the
220
+ * boundary step 2 landed on has shown.
221
+ *
222
+ * It records that a card showed, never what the card said: a later turn that
223
+ * changes the work is the model's reply, and the card comes back only on
224
+ * request (D110 (reply-after-the-card)).
221
225
  */
222
226
  export function pausePath(root, slug) {
223
227
  return join(artifactDir(root, slug), 'PAUSE');
224
228
  }
225
229
  /**
226
- * Remove the PAUSE stamp: a checkpoint, a revert, or an abandon ends the pause
227
- * it recorded, so the next pause renders whole. Absent is a no-op.
230
+ * The card PAUSE says has shown, or null when the file is absent or does not
231
+ * parse. A stamp written before D110 (reply-after-the-card) holds a step
232
+ * number and a hash, and reads as that step's pause.
233
+ */
234
+ export function readPause(root, slug) {
235
+ try {
236
+ const m = /^(landed )?(\d+)(?:\s|$)/.exec(readFileSync(pausePath(root, slug), 'utf8').trim());
237
+ return m === null ? null : { kind: m[1] === undefined ? 'pause' : 'landed', step: Number(m[2]) };
238
+ }
239
+ catch {
240
+ return null;
241
+ }
242
+ }
243
+ /**
244
+ * Record that a card has shown. Best-effort: a failed write costs one more
245
+ * full card on a later turn, never this turn's output or its exit code.
246
+ */
247
+ export function writePause(root, slug, stamp) {
248
+ try {
249
+ writeFileSync(pausePath(root, slug), `${stamp.kind === 'landed' ? 'landed ' : ''}${stamp.step}\n`);
250
+ }
251
+ catch {
252
+ // A read-only tree or a vanished build folder: the card still renders.
253
+ }
254
+ }
255
+ /**
256
+ * Remove the PAUSE stamp: a revert or an abandon ends the pause it recorded,
257
+ * so the next pause renders whole. Absent is a no-op.
228
258
  */
229
259
  export function clearPause(root, slug) {
230
260
  rmSync(pausePath(root, slug), { force: true });
231
261
  }
232
262
  /**
233
263
  * STOP: the stamp of the stop card `handoff --stop` last rendered whole, the
234
- * range it reviews, the TURN it printed in, a hash of what the card showed,
235
- * and a digest of the detail file it printed from. While a later render
236
- * matches the range and the hash, handoff prints the one-line reminder;
237
- * `plumbbob harvest` records the stop only from the detail the card showed,
238
- * and only once a human turn has read it.
264
+ * range it reviews and the TURN it printed in. While a later render names the
265
+ * same range, handoff prints the one-line reminder, whatever changed since;
266
+ * `plumbbob harvest` records the stop only once a card for the range has shown
267
+ * and a human turn has read it.
239
268
  */
240
269
  export function stopPath(root, slug) {
241
270
  return join(artifactDir(root, slug), 'STOP');
242
271
  }
243
272
  /**
244
273
  * The stamp of the stop card last rendered, or null when STOP is absent or
245
- * does not parse.
274
+ * does not parse. A stamp written before D110 (reply-after-the-card) carries
275
+ * a hash and a digest after the turn, and both are ignored.
246
276
  */
247
277
  export function readStop(root, slug) {
248
278
  try {
249
- const m = /^(\d+) (\d+|-) (\S+)(?: (\S+))?$/.exec(readFileSync(stopPath(root, slug), 'utf8').trim());
279
+ const m = /^(\d+) (\d+|-)(?:\s|$)/.exec(readFileSync(stopPath(root, slug), 'utf8').trim());
250
280
  if (m === null)
251
281
  return null;
252
- return { range: Number(m[1]), turn: m[2] === '-' ? null : Number(m[2]), hash: m[3] ?? '', detail: m[4] ?? null };
282
+ return { range: Number(m[1]), turn: m[2] === '-' ? null : Number(m[2]) };
253
283
  }
254
284
  catch {
255
285
  return null;
256
286
  }
257
287
  }
258
288
  /**
259
- * Record the stamp of the stop card just printed, with the digest of the
260
- * detail file as it stands, which is the file the card was rendered from.
261
- * Best-effort: a failed write costs one more full card on a later turn, never
262
- * this turn's output or its exit code.
289
+ * Record the stamp of the stop card just printed. Best-effort: a failed write
290
+ * costs one more full card on a later turn, never this turn's output or its
291
+ * exit code.
263
292
  */
264
293
  export function writeStop(root, slug, stamp) {
265
294
  try {
266
- const detail = detailDigest(readFileSync(detailPath(root), 'utf8'));
267
- writeFileSync(stopPath(root, slug), `${stamp.range} ${stamp.turn ?? '-'} ${stamp.hash} ${detail}\n`);
295
+ writeFileSync(stopPath(root, slug), `${stamp.range} ${stamp.turn ?? '-'}\n`);
268
296
  }
269
297
  catch {
270
298
  // A read-only tree or a vanished build folder: the card still renders.
@@ -277,13 +305,6 @@ export function writeStop(root, slug, stamp) {
277
305
  export function clearStop(root, slug) {
278
306
  rmSync(stopPath(root, slug), { force: true });
279
307
  }
280
- /**
281
- * The digest a STOP stamp keeps of a detail file, so the record can tell the
282
- * detail a card showed from one written after it.
283
- */
284
- export function detailDigest(detail) {
285
- return createHash('sha256').update(detail).digest('hex');
286
- }
287
308
  /**
288
309
  * The build's `checkpoints` ledger: the recorded SHAs of landed plan and step
289
310
  * commits, one line each.
@@ -689,7 +710,7 @@ export function readTurn(root) {
689
710
  // ending: a decision ending closes the turn, an orientation ending appears in
690
711
  // it, and a driver ending is recorded only so that the last ending wins.
691
712
  const RELAY_TIERS = ['decision', 'orientation', 'driver'];
692
- // Which ending printed: handoff's eight forms first, then one per transition.
713
+ // Which ending printed: handoff's nine forms first, then one per transition.
693
714
  // It is what the check's reason names when the ending went missing.
694
715
  const RELAY_RENDERS = [
695
716
  'card',
@@ -700,6 +721,7 @@ const RELAY_RENDERS = [
700
721
  'boundary',
701
722
  'pointer',
702
723
  'driver',
724
+ 'harvest-card',
703
725
  'checkpoint',
704
726
  'plan-commit',
705
727
  'park',
@@ -911,10 +933,10 @@ export function excludeControl(root) {
911
933
  // The entry stamp is in-flight control like STEP/SEAM: checkpoint's
912
934
  // stageAll must never sweep it into a step commit.
913
935
  `${DIRNAME}/builds/*/TICK`,
914
- // The pause stamp is in-flight control too: what handoff last showed, so
915
- // a repeat prints the reminder. It must never ride a step commit.
936
+ // The pause stamp is in-flight control too: which card has shown, so a
937
+ // later handoff prints its one line. It must never ride a step commit.
916
938
  `${DIRNAME}/builds/*/PAUSE`,
917
- // The stop stamp likewise: what the stop card last showed, and the turn it
939
+ // The stop stamp likewise: the range whose card has shown, and the turn it
918
940
  // printed in, for the reminder and the record's latch.
919
941
  `${DIRNAME}/builds/*/STOP`,
920
942
  // The agent-run handoff ledger is step-scoped in-flight state, not a
@@ -12,7 +12,7 @@
12
12
  import { appendFileSync, readFileSync, rmSync, writeFileSync } from 'node:fs';
13
13
  import { relative } from 'node:path';
14
14
  import { commit, findRepoRoot, headSha, isDirty, stageAll, stagePath, stagedPaths, stagedStat } from "../lib/git.js";
15
- import { activeBuild, buildFolder, buildLogPath, buildScope, bumpStepStat, checkpointsPath, clearDetail, clearHandoff, clearPause, clearTick, detailPath, hasSession, intentPath, readRun, readStats, refreshExcludes, seamPath, stampStepStat, stepPath, } from "../lib/sidecar.js";
15
+ import { activeBuild, buildFolder, buildLogPath, buildScope, bumpStepStat, checkpointsPath, clearDetail, clearHandoff, clearTick, detailPath, hasSession, intentPath, readRun, readStats, refreshExcludes, seamPath, stampStepStat, stepPath, writePause, } from "../lib/sidecar.js";
16
16
  import { runCheck } from "../lib/check.js";
17
17
  import { readCommitBody } from "../lib/commitbody.js";
18
18
  import { checkLatch } from "../lib/latch.js";
@@ -22,8 +22,7 @@ import { conventionalSubject, subjectFromTitle, withMarker } from "../lib/commit
22
22
  import { appendToSection, checkpointLogLine, logEntry, planLogLine } from "../lib/buildlog.js";
23
23
  import { AT_BOUNDARY, syncBuildLogState } from "../lib/buildlogsync.js";
24
24
  import { advisory, ending, notice, printEnding, transition } from "../lib/notice.js";
25
- import { boundaryEnding, detailsLink, parseDetailRange, planRecord, stepRecord } from "./handoff.js";
26
- import { isHarvestDetail } from "./harvest.js";
25
+ import { boundaryEnding, detailsLink, isHarvestDetail, parseDetailRange, planRecord, stepRecord } from "./handoff.js";
27
26
  /**
28
27
  * Land a step: latch, check gate, commit, record, return to the boundary.
29
28
  *
@@ -122,11 +121,14 @@ export async function checkpoint(cwd, args) {
122
121
  rmSync(stepPath(root), { force: true });
123
122
  clearHandoff(root); // the agent-run handoff ledger is step-scoped; clear it with the markers.
124
123
  clearTick(root); // the step's entry stamp is spent; the next `build <n>` re-stamps.
125
- clearPause(root); // the pause it stamped is over; the next step's pause renders whole.
126
- // The whole ending, one block: the lead line, the Verdict folded over what
127
- // just landed, the advisories this checkpoint alone can state, and the
128
- // pointer. Read after the flip and the ledger append, so the fold and the
129
- // pointer both see the boundary the human is standing at.
124
+ // The pause it stamped is over, and the boundary this ending prints is the
125
+ // card the stamp now names, so a later handoff here ends on its pointer
126
+ // (D110 (reply-after-the-card)); the next step's pause renders whole.
127
+ writePause(root, undefined, { kind: 'landed', step });
128
+ // The whole ending, one block: the lead line, the advisories this checkpoint
129
+ // alone can state, and the state table headed by the Verdict folded over
130
+ // what just landed. Read after the flip and the ledger append, so the fold
131
+ // and the pointer both see the boundary the human is standing at.
130
132
  const landed = boundaryEnding(root, activeBuild(root), step);
131
133
  // A landing that completes the range a typed run marked owes the stop card:
132
134
  // the record says so, and the relay check holds a turn that ends here.
@@ -137,10 +139,8 @@ export async function checkpoint(cwd, args) {
137
139
  ...(owes === null ? {} : { owes }),
138
140
  text: ending({
139
141
  lead: transition({ label: 'Checkpoint', fact: `Step ${step} complete`, detail: leadDetail(sha, pointer) }),
140
- verdict: landed.verdict,
141
142
  advisories: [drift, flipNotice],
142
- stop: landed.stop,
143
- pointer: landed.pointer,
143
+ state: landed.state,
144
144
  }),
145
145
  });
146
146
  return 0;
@@ -202,7 +202,7 @@ function checkpointPlan(root, args) {
202
202
  remedy: 'unignore .plumbbob/builds/ to keep the record in the tree',
203
203
  }),
204
204
  ],
205
- pointer: boundaryEnding(root, activeBuild(root), null).pointer,
205
+ state: boundaryEnding(root, activeBuild(root), null).state,
206
206
  }),
207
207
  });
208
208
  return 0;
@@ -18,10 +18,10 @@ import { buildScope, checkpointsPath, clearTick, hasSession, intentPath, readSta
18
18
  import { conventionalSubject, withMarker } from "../lib/commitmsg.js";
19
19
  import { parseBuildScope } from "../lib/intent.js";
20
20
  import { advisory, ending, notice, printEnding, transition } from "../lib/notice.js";
21
- // The pointer finish leaves the turn on. Every other one is `handoff`'s, but a
22
- // finished session has no state left to render from, and past a closed build
23
- // the only move is framing the next goal.
24
- const NEXT_UP = '**Next Up**: Nothing planned - /plumbbob:plan';
21
+ // The pointer finish leaves the turn on, as its state table's one row. Every
22
+ // other one is `handoff`'s, but a finished session has no state left to render
23
+ // from, and past a closed build the only move is framing the next goal.
24
+ const NEXT_UP = 'Nothing planned - /plumbbob:plan';
25
25
  /**
26
26
  * Close out the active build: report tail, final commit, control-state cleanup.
27
27
  *
@@ -80,8 +80,8 @@ export function finish(cwd, args = []) {
80
80
  rmSync(join(sidecarDir(root), 'STATE'), { force: true });
81
81
  // The whole ending in the fixed order: the lead line, the advisory that
82
82
  // qualifies it (finishing without a report is guidance, never a gate; the
83
- // session is already closed by the time it prints), and the pointer, which is
84
- // the turn's last text. finish renders that pointer itself: it just cleared
83
+ // session is already closed by the time it prints), and the state table's
84
+ // pointer, which is the turn's last text. finish renders that pointer itself: it just cleared
85
85
  // the session `handoff` would read one from.
86
86
  const where = slug === null ? '.plumbbob/' : `.plumbbob/builds/${slug}/`;
87
87
  printEnding(root, {
@@ -102,7 +102,7 @@ export function finish(cwd, args = []) {
102
102
  remedy: '/plumbbob:finish normally writes the report first',
103
103
  }),
104
104
  ],
105
- pointer: NEXT_UP,
105
+ state: [{ label: 'Next Up', value: NEXT_UP }],
106
106
  }),
107
107
  });
108
108
  return 0;