plumbbob 0.13.0 → 0.13.2

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.
@@ -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, readStats, refreshExcludes, seamPath, stampStepStat, stepPath, } from "../lib/sidecar.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";
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,7 +22,8 @@ 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, planRecord, stepRecord } from "./handoff.js";
25
+ import { boundaryEnding, detailsLink, parseDetailRange, planRecord, stepRecord } from "./handoff.js";
26
+ import { isHarvestDetail } from "./harvest.js";
26
27
  /**
27
28
  * Land a step: latch, check gate, commit, record, return to the boundary.
28
29
  *
@@ -102,7 +103,12 @@ export async function checkpoint(cwd, args) {
102
103
  else {
103
104
  sha = headSha(root);
104
105
  }
105
- clearDetail(root); // the detail is recorded in the Log below; clear it so no stale detail rides into the next step.
106
+ // The detail is recorded in the Log below; clear it so no stale detail rides
107
+ // into the next step. A stop's or a harvest's detail is the record's to
108
+ // clear, so it stays where it is.
109
+ if (!heldDetail(root)) {
110
+ clearDetail(root);
111
+ }
106
112
  appendFileSync(checkpointsPath(root), `step ${step} ${sha}\n`);
107
113
  const flipNotice = flipIntent(root, step);
108
114
  stampStepStat(root, undefined, step, 'landedAt', new Date().toISOString());
@@ -122,13 +128,18 @@ export async function checkpoint(cwd, args) {
122
128
  // pointer. Read after the flip and the ledger append, so the fold and the
123
129
  // pointer both see the boundary the human is standing at.
124
130
  const landed = boundaryEnding(root, activeBuild(root), step);
131
+ // A landing that completes the range a typed run marked owes the stop card:
132
+ // the record says so, and the relay check holds a turn that ends here.
133
+ const owes = landed.range !== null && readRun(root) === landed.range.item ? { range: landed.range.item, count: landed.range.count, step } : null;
125
134
  printEnding(root, {
126
135
  tier: 'orientation',
127
136
  render: 'checkpoint',
137
+ ...(owes === null ? {} : { owes }),
128
138
  text: ending({
129
139
  lead: transition({ label: 'Checkpoint', fact: `Step ${step} complete`, detail: leadDetail(sha, pointer) }),
130
140
  verdict: landed.verdict,
131
141
  advisories: [drift, flipNotice],
142
+ stop: landed.stop,
132
143
  pointer: landed.pointer,
133
144
  }),
134
145
  });
@@ -301,10 +312,11 @@ function flipIntent(root, step) {
301
312
  }
302
313
  /**
303
314
  * The lead line's trailing parenthetical: the short SHA, and where the Log
304
- * entry landed when one did, as the same `details:` clause Next Up carries.
315
+ * entry landed when one did, as the same short linked `details:` clause Next
316
+ * Up carries.
305
317
  */
306
318
  function leadDetail(sha, pointer) {
307
- return pointer === null ? [sha.slice(0, 9)] : [sha.slice(0, 9), `details: \`${pointer}\``];
319
+ return pointer === null ? [sha.slice(0, 9)] : [sha.slice(0, 9), detailsLink(pointer.path, pointer.line)];
308
320
  }
309
321
  /**
310
322
  * Append the step's entry to the build-log's `## Log` section: the dated line,
@@ -313,8 +325,8 @@ function leadDetail(sha, pointer) {
313
325
  * The build's history accrues at each checkpoint instead of being reconstructed
314
326
  * at finish; the step's title is lifted from intent.md when still present.
315
327
  * Best-effort: a missing or odd build-log never blocks a checkpoint; the
316
- * `checkpoints` SHA is the source of truth. Returns where the line landed, as
317
- * `path:line` from the repo root, or null when nothing was written.
328
+ * `checkpoints` SHA is the source of truth. Returns where the line landed, or
329
+ * null when nothing was written.
318
330
  */
319
331
  function logCheckpoint(root, step, sha, record) {
320
332
  const date = new Date().toISOString().slice(0, 10);
@@ -328,8 +340,8 @@ function logPlan(root, sha, record) {
328
340
  return appendLog(root, planLogLine(new Date().toISOString().slice(0, 10), sha), record);
329
341
  }
330
342
  /**
331
- * Write one Log entry and say where its dated line landed (`path:line` from
332
- * the repo root), or null when the section is missing or the write failed.
343
+ * Write one Log entry and say where its dated line landed, or null when the
344
+ * section is missing or the write failed.
333
345
  */
334
346
  function appendLog(root, line, record) {
335
347
  try {
@@ -340,7 +352,7 @@ function appendLog(root, line, record) {
340
352
  }
341
353
  writeFileSync(path, updated);
342
354
  const at = updated.split('\n').lastIndexOf(line);
343
- return at === -1 ? null : `${relative(root, path)}:${at + 1}`;
355
+ return at === -1 ? null : { path: relative(root, path), line: at + 1 };
344
356
  }
345
357
  catch {
346
358
  // best-effort ledger; never fail a checkpoint over the build-log.
@@ -459,13 +471,15 @@ function fallbackBody(root, step) {
459
471
  * the Log's record, and the caller clears it afterward. A missing or blank file
460
472
  * records nothing, and so does one whose header names a different step: that
461
473
  * file is a leftover, written after its own step had already landed, and
462
- * recording it would file one step's story under another. The plan commit
463
- * passes no step and takes the file as it is.
474
+ * recording it would file one step's story under another. A range's detail
475
+ * and a boundary harvest's record nothing here either: they are
476
+ * `plumbbob harvest`'s to record. The plan commit passes no step and takes the
477
+ * file as it is.
464
478
  */
465
479
  function readDetail(root, step) {
466
480
  try {
467
481
  const raw = readFileSync(detailPath(root), 'utf8').trim();
468
- if (raw.length === 0) {
482
+ if (raw.length === 0 || (step !== null && heldDetail(root))) {
469
483
  return null;
470
484
  }
471
485
  const written = step === null ? null : parseDetailStep(raw);
@@ -475,6 +489,20 @@ function readDetail(root, step) {
475
489
  return null;
476
490
  }
477
491
  }
492
+ /**
493
+ * Whether the detail file is one a step's checkpoint leaves alone: a range's,
494
+ * the stop card's wire, or a boundary harvest's, each read off its header and
495
+ * each recorded and cleared by `plumbbob harvest`.
496
+ */
497
+ function heldDetail(root) {
498
+ try {
499
+ const raw = readFileSync(detailPath(root), 'utf8');
500
+ return parseDetailRange(raw) !== null || isHarvestDetail(raw);
501
+ }
502
+ catch {
503
+ return false;
504
+ }
505
+ }
478
506
  /**
479
507
  * The step's done-when line from intent.md, or null when absent or unreadable.
480
508
  */