plumbbob 0.8.18 → 0.9.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.
Files changed (47) hide show
  1. package/.claude-plugin/plugin.json +1 -1
  2. package/README.md +51 -44
  3. package/dist/cli-core.js +35 -15
  4. package/dist/lib/agents.js +230 -112
  5. package/dist/lib/buildlog.js +50 -30
  6. package/dist/lib/buildlogsync.js +21 -12
  7. package/dist/lib/check.js +69 -35
  8. package/dist/lib/commitmsg.js +53 -32
  9. package/dist/lib/git.js +86 -31
  10. package/dist/lib/intent.js +91 -31
  11. package/dist/lib/latch.js +70 -39
  12. package/dist/lib/orient.js +78 -39
  13. package/dist/lib/plugins.js +11 -8
  14. package/dist/lib/settings.js +65 -32
  15. package/dist/lib/sidecar.js +356 -150
  16. package/dist/lib/templates.js +17 -6
  17. package/dist/verbs/agent.js +116 -55
  18. package/dist/verbs/build.js +27 -12
  19. package/dist/verbs/check.js +33 -7
  20. package/dist/verbs/checkpoint.js +158 -79
  21. package/dist/verbs/doctor.js +192 -95
  22. package/dist/verbs/finish.js +88 -46
  23. package/dist/verbs/handoff.js +56 -20
  24. package/dist/verbs/init.js +37 -12
  25. package/dist/verbs/park.js +14 -3
  26. package/dist/verbs/revert.js +64 -24
  27. package/dist/verbs/spike.js +89 -30
  28. package/dist/verbs/start.js +72 -38
  29. package/dist/verbs/status.js +44 -17
  30. package/dist/verbs/turn.js +88 -45
  31. package/dist/verbs/use.js +20 -9
  32. package/package.json +1 -1
  33. package/skills/{pb-build → build}/SKILL.md +25 -25
  34. package/skills/{pb-doctor → doctor}/SKILL.md +1 -1
  35. package/skills/{pb-finish → finish}/SKILL.md +3 -3
  36. package/skills/{pb-harvest → harvest}/SKILL.md +4 -4
  37. package/skills/{pb-park → park}/SKILL.md +2 -2
  38. package/skills/{pb-plan → plan}/SKILL.md +11 -11
  39. package/skills/{pb-refine → refine}/SKILL.md +7 -7
  40. package/skills/{pb-revert → revert}/SKILL.md +2 -2
  41. package/skills/{pb-spike → spike}/SKILL.md +4 -4
  42. package/skills/{pb-status → status}/SKILL.md +3 -3
  43. package/skills/{pb-step → step}/SKILL.md +9 -9
  44. package/skills/{pb-verify → verify}/SKILL.md +8 -8
  45. package/templates/build-log.md +7 -7
  46. package/templates/intent.md +8 -8
  47. package/templates/spike-report.md +1 -1
@@ -1,15 +1,17 @@
1
- // `plumbbob checkpoint [<n>] [-m <msg>]` — the executor-agnostic commit tick (D3).
2
- // It does NOT require a STEP file: the step is whatever you pass,
3
- // else the in-flight STEP, else the next undone step in intent. It gates on a green
4
- // check, then commits any pending work (or records the existing HEAD when the tree
5
- // is already clean — the human's commit skill may have committed first), records the
6
- // SHA, flips the intent checkbox to `[x]`, and clears any STEP/SEAM (which returns
7
- // the dashboard to the DESIGN boundary). The diff's author is irrelevant:
8
- // `/pb-build`, your hands, a vibe session, or another harness all checkpoint
9
- // the same way.
1
+ // `plumbbob checkpoint [<n>] [-m <msg>]` — the commit tick that lands a step.
2
+ // Executor-agnostic and author-blind: it reads the diff, not who wrote it, so a
3
+ // `/plumbbob:build` run, your own hands, a vibe session, or another harness all
4
+ // checkpoint identically. It does NOT require a STEP marker (the flat control
5
+ // file recording the step in flight): the step is whatever you pass, else the
6
+ // in-flight STEP, else the next undone step in intent.md. It evaluates the
7
+ // approval latch, gates on a green check, commits any pending work (or records
8
+ // the existing HEAD when the tree is already clean — the human's commit skill
9
+ // may have committed first), records the SHA, flips the intent checkbox to
10
+ // `[x]`, and clears the STEP/SEAM markers — returning the dashboard to the
11
+ // DESIGN boundary.
10
12
  import { appendFileSync, readFileSync, rmSync, writeFileSync } from 'node:fs';
11
13
  import { commit, findRepoRoot, headSha, isDirty, stageAll, stagePath, stagedPaths, stagedStat } from "../lib/git.js";
12
- import { activeBuild, buildFolder, buildLogPath, buildScope, bumpStepStat, checkpointsPath, clearHandoff, clearTick, hasSession, intentPath, readStats, seamPath, stampStepStat, stepPath, } from "../lib/sidecar.js";
14
+ import { activeBuild, buildFolder, buildLogPath, buildScope, bumpStepStat, checkpointsPath, clearHandoff, clearTick, hasSession, intentPath, readStats, refreshExcludes, seamPath, stampStepStat, stepPath, } from "../lib/sidecar.js";
13
15
  import { runCheck } from "../lib/check.js";
14
16
  import { checkLatch } from "../lib/latch.js";
15
17
  import { markStepDone, parseSteps } from "../lib/orient.js";
@@ -17,12 +19,22 @@ import { parseBuildScope, parseStepSeam, scopeDrift } from "../lib/intent.js";
17
19
  import { conventionalSubject, subjectFromTitle, withMarker } from "../lib/commitmsg.js";
18
20
  import { appendToSection, checkpointLogLine } from "../lib/buildlog.js";
19
21
  import { AT_BOUNDARY, syncBuildLogState } from "../lib/buildlogsync.js";
22
+ /**
23
+ * Land a step: latch, check gate, commit, record, return to the boundary.
24
+ *
25
+ * Exit 1 on any refusal — no session, no resolvable step, a latched tick, or a
26
+ * red/broken check; each refusal says what unblocks it.
27
+ */
20
28
  export async function checkpoint(cwd, args) {
21
29
  const root = findRepoRoot(cwd);
22
30
  if (root === null || !hasSession(root)) {
23
31
  process.stderr.write('plumbbob: no active session. Run `plumbbob start "<title>"` first.\n');
24
32
  return 1;
25
33
  }
34
+ // Before either staging path — `-A` below, and the plan commit's folder-scoped
35
+ // add, which also leans on the excludes to skip the in-flight markers inside
36
+ // the build folder.
37
+ refreshExcludes(root);
26
38
  if (args.includes('--plan')) {
27
39
  return checkpointPlan(root, args);
28
40
  }
@@ -31,8 +43,9 @@ export async function checkpoint(cwd, args) {
31
43
  process.stderr.write('plumbbob: no step to checkpoint — pass a number, or plan a step in intent.md first.\n');
32
44
  return 1;
33
45
  }
34
- // The approval latch (D64) precedes the check gate (C4): the tick may not land
35
- // without a human turn since entry, a grant, or a dormant ledger. The refusal
46
+ // The approval latch runs before the check gate — cheap first. The tick may
47
+ // not land without a human turn since the step was entered, a one-turn grant
48
+ // the human typed, or a dormant ledger. A refusal is not an error: the
36
49
  // message is the pause affordance — exit 1 and hand the turn back.
37
50
  const latch = checkLatch(root, step);
38
51
  if (!latch.allow) {
@@ -42,8 +55,9 @@ export async function checkpoint(cwd, args) {
42
55
  const gate = await runCheck(root);
43
56
  if (gate !== 0) {
44
57
  if (gate === 1) {
45
- // The dogfood receipt (research/07 Build 2b): red attempts before green
46
- // are a per-step count. Harness breakage (2) is not a red check.
58
+ // Red attempts before green accrue as a per-step stat, surfaced in the
59
+ // build-log receipt and the finish report. Harness breakage (exit 2) is
60
+ // not a red check.
47
61
  bumpStepStat(root, undefined, step, 'redChecks');
48
62
  }
49
63
  process.stderr.write(gate === 2
@@ -65,28 +79,33 @@ export async function checkpoint(cwd, args) {
65
79
  flipIntent(root, step);
66
80
  stampStepStat(root, undefined, step, 'landedAt', new Date().toISOString());
67
81
  logCheckpoint(root, step, sha);
68
- // The step landed: the build-log's mirror flips to ☑ and Current step returns to
69
- // the boundary (D69), re-read from the intent.md `flipIntent` just wrote. The
70
- // artifact-plane whitelist keeps this build-log write from ever reading as drift.
82
+ // The step landed: the build-log's ☐/☑ mirror flips and its Current-step line
83
+ // returns to the boundary, re-read from the intent.md `flipIntent` just wrote.
84
+ // The artifact-plane whitelist keeps this build-log write from ever reading as
85
+ // scope drift.
71
86
  syncBuildLogState(root, activeBuild(root), AT_BOUNDARY);
72
87
  rmSync(seamPath(root), { force: true });
73
88
  rmSync(stepPath(root), { force: true });
74
- clearHandoff(root); // the agent-run ledger is step-scoped (D47) — clear it with the markers.
75
- clearTick(root); // the entry stamp (D64) is spent — the next `build <n>` re-stamps.
89
+ clearHandoff(root); // the agent-run handoff ledger is step-scoped — clear it with the markers.
90
+ clearTick(root); // the step's entry stamp is spent — the next `build <n>` re-stamps.
76
91
  process.stdout.write(`plumbbob: step ${step} checkpointed — ${sha.slice(0, 9)}. Back at the boundary.\n`);
77
92
  return 0;
78
93
  }
79
- // The plan-approval commit (D36): stage only the active build's artifact folder and
80
- // commit it as `chore(<scope>): plan` (D68), then record a `plan <sha>` line. Giving
81
- // the plan its own commit keeps the first step's diff from absorbing the scaffold, so
82
- // `git log` reads baseline → plan → steps. No check gate (there is no code work to
83
- // verify yet), no intent flip, no step markers — the plan lives entirely in DESIGN.
84
- // An optional `--body` (stdin heredoc, D68) rides after the `plumbbob plan` marker;
85
- // the folder is whitelisted artifact plane, so there is no scope-drift to warn about.
94
+ /**
95
+ * The plan-approval commit: stage only the build's artifact folder, commit it as
96
+ * `chore(<scope>): plan`, and record a `plan <sha>` line.
97
+ *
98
+ * Giving the plan its own commit keeps the first step's diff from absorbing the
99
+ * scaffold, so `git log` reads baseline → plan → steps. No check gate (there is
100
+ * no code work to verify yet), no intent flip, no step markers — the plan lives
101
+ * entirely in DESIGN. An optional `--body` (stdin heredoc) rides after the
102
+ * `plumbbob plan` marker line; the folder is whitelisted artifact plane, so
103
+ * there is no scope drift to warn about.
104
+ */
86
105
  function checkpointPlan(root, args) {
87
- // The latch (D64) covers the plan commit too, keyed on the TICK that `start`
88
- // stamped: the plan pause is a pause. No step number — a range grant does not
89
- // speak to a plan.
106
+ // The latch covers the plan commit too, keyed on the TICK that `start`
107
+ // stamped: the plan pause is a real pause. No step number — a step-range
108
+ // grant does not speak to a plan.
90
109
  const latch = checkLatch(root, null);
91
110
  if (!latch.allow) {
92
111
  process.stderr.write(latch.message);
@@ -95,22 +114,29 @@ function checkpointPlan(root, args) {
95
114
  stagePath(root, buildFolder(root));
96
115
  const sha = commit(root, planSubject(root), withMarker('plumbbob plan', bodyArg(args) ?? undefined));
97
116
  appendFileSync(checkpointsPath(root), `plan ${sha}\n`);
98
- // The plan landing consumes `start`'s entry stamp (D64): a later hand-built diff
117
+ // Landing the plan consumes `start`'s entry stamp: a later hand-built diff
99
118
  // (no `build <n>`) must find no stale TICK and stay guidance-governed.
100
119
  clearTick(root);
101
120
  process.stdout.write(`plumbbob: plan committed — ${sha.slice(0, 9)}. Baseline → plan → steps.\n`);
102
121
  return 0;
103
122
  }
104
- // The plan commit's CLI-owned subject (D68): `chore(<scope>): plan`, the scope
105
- // resolved through the build-default fallback chain (a bare `chore: plan` when
106
- // none resolves). The `plumbbob plan` identifier rides the body marker, not the
107
- // subject.
123
+ /**
124
+ * Compose the plan commit's CLI-owned subject: `chore(<scope>): plan`.
125
+ *
126
+ * The scope resolves through the build-default fallback chain — a bare
127
+ * `chore: plan` when none resolves. The `plumbbob plan` identifier rides the
128
+ * body marker line, never the subject.
129
+ */
108
130
  function planSubject(root) {
109
131
  return conventionalSubject({ type: 'chore', scope: buildDefaultScope(root), description: 'plan' });
110
132
  }
111
- // Step resolution (D3): explicit arg > in-flight STEP file > first undone step in
112
- // intent.md. Returns null when none can be determined. A `-m` value is a message,
113
- // never a step — `checkpoint -m "2"` must not read as step 2.
133
+ /**
134
+ * Resolve the step being checkpointed, or null when none can be determined.
135
+ *
136
+ * Explicit arg > in-flight STEP file > first undone step in intent.md. A `-m`
137
+ * value is a message, never a step — `checkpoint -m "2"` must not read as
138
+ * step 2.
139
+ */
114
140
  function resolveStep(root, args) {
115
141
  const explicit = args.filter((_, i) => args[i - 1] !== '-m').find((a) => /^\d+$/.test(a));
116
142
  if (explicit !== undefined) {
@@ -127,24 +153,33 @@ function resolveStep(root, args) {
127
153
  return null;
128
154
  }
129
155
  }
130
- // Guidance, not a gate (the enforce→guide pivot): warn when the staged tree reaches
131
- // beyond the step's seam, then commit anyway. The seam comes from the in-flight SEAM
132
- // file when a build is live, else the step's declared seam in intent.md. Plumbbob's
133
- // own artifact plane is whitelisted (`scopeDrift`), so the `[x]` flip and build-log
134
- // line this very checkpoint stages never read as drift. No seam ⇒ no warning.
156
+ /**
157
+ * Warn — never refuse — when the staged tree reaches beyond the step's seam.
158
+ *
159
+ * Guidance, not a gate: the checkpoint captures the drift and says so. The seam
160
+ * (the step's edit grant — exact paths or `dir/` prefixes) comes from the
161
+ * in-flight SEAM file when a build is live, else the step's declared seam in
162
+ * intent.md. Plumbbob's own artifact plane is whitelisted inside `scopeDrift`,
163
+ * so the `[x]` flip and build-log line this very checkpoint stages never read
164
+ * as drift. No seam means no warning.
165
+ */
135
166
  function warnScopeDrift(root, step) {
136
167
  const seam = seamTokens(root, step);
137
168
  const outside = scopeDrift(stagedPaths(root), seam);
138
169
  if (outside.length > 0) {
139
- bumpStepStat(root, undefined, step, 'driftWarnings'); // the dogfood receipt (research/07 2b)
170
+ bumpStepStat(root, undefined, step, 'driftWarnings'); // accrues into the build-log's stats receipt
140
171
  process.stderr.write(`plumbbob: heads-up — staged paths outside step ${step}'s seam: ${outside.join(', ')}. ` +
141
- `The checkpoint captures them; if that's real scope drift, the plan may need a \`/pb-step\` revision.\n`);
172
+ `The checkpoint captures them; if that's real scope drift, the plan may need a \`/plumbbob:step\` revision.\n`);
142
173
  }
143
174
  }
144
- // The seam tokens for the in-flight step: the normalized SEAM file `build` wrote
145
- // (authoritative while a build is live), falling back to the step's declared seam
146
- // parsed from intent.md. Empty when neither resolves — the caller then skips the
147
- // warning rather than flagging the whole tree.
175
+ /**
176
+ * The seam tokens governing the in-flight step.
177
+ *
178
+ * The normalized SEAM file `build` wrote is authoritative while a build is
179
+ * live; fall back to the step's declared seam parsed from intent.md. Empty when
180
+ * neither resolves — the caller then skips the drift warning rather than
181
+ * flagging the whole tree.
182
+ */
148
183
  function seamTokens(root, step) {
149
184
  try {
150
185
  const fromFile = readFileSync(seamPath(root), 'utf8')
@@ -160,21 +195,30 @@ function seamTokens(root, step) {
160
195
  }
161
196
  return seamForStep(root, step);
162
197
  }
198
+ /**
199
+ * Flip the step's checkbox in intent.md to `[x]`.
200
+ *
201
+ * Best-effort bookkeeping — the checkpoint SHA is the source of truth — but the
202
+ * dashboard reads intent.md, so a swallowed failure here would make orientation
203
+ * lie; the catch warns and asks for a hand flip instead.
204
+ */
163
205
  function flipIntent(root, step) {
164
206
  try {
165
207
  writeFileSync(intentPath(root), markStepDone(readFileSync(intentPath(root), 'utf8'), step));
166
208
  }
167
209
  catch {
168
- // Best-effort bookkeeping — the checkpoint SHA is the source of truth — but the
169
- // dashboard reads intent.md, so a swallowed failure here makes orientation lie.
170
210
  process.stderr.write(`plumbbob: heads-up — could not flip step ${step} to [x] in intent.md; ` +
171
211
  `the checkpoint is recorded, but the dashboard will still show step ${step} as next. Flip it by hand.\n`);
172
212
  }
173
213
  }
174
- // Append a dated line to the build-log's `## Log` so the build's history accrues at
175
- // each checkpoint instead of being reconstructed at finish. The step's title is lifted
176
- // from intent.md when still present. Best-effort: a missing/odd build-log never blocks
177
- // a checkpoint — the `checkpoints` SHA is the source of truth.
214
+ /**
215
+ * Append a dated line to the build-log's `## Log` section.
216
+ *
217
+ * The build's history accrues at each checkpoint instead of being reconstructed
218
+ * at finish; the step's title is lifted from intent.md when still present.
219
+ * Best-effort: a missing or odd build-log never blocks a checkpoint — the
220
+ * `checkpoints` SHA is the source of truth.
221
+ */
178
222
  function logCheckpoint(root, step, sha) {
179
223
  try {
180
224
  const path = buildLogPath(root);
@@ -189,9 +233,13 @@ function logCheckpoint(root, step, sha) {
189
233
  // best-effort ledger; never fail a checkpoint over the build-log.
190
234
  }
191
235
  }
192
- // The compact receipt riding the Log line (research/07 Build 2b): only what
193
- // accrued — a clean first-try step gets no suffix at all. Wall-clock needs both
194
- // stamps (a hand-built step never ran `build <n>`, so it has no startedAt).
236
+ /**
237
+ * The compact stats receipt riding the Log line, or null when nothing accrued.
238
+ *
239
+ * Only what happened is shown — a clean first-try step gets no suffix at all.
240
+ * Wall-clock needs both stamps: a hand-built step never ran `build <n>`, so it
241
+ * has no startedAt.
242
+ */
195
243
  function statsSuffix(root, step) {
196
244
  const stats = readStats(root)[String(step)];
197
245
  if (stats === undefined)
@@ -210,14 +258,17 @@ function statsSuffix(root, step) {
210
258
  }
211
259
  return parts.length > 0 ? parts.join(', ') : null;
212
260
  }
213
- // The CLI-owned, deterministic commit subject (D68): a Conventional `type(scope):
214
- // description`. The type and wording come from the step's title — an author-written
215
- // prefix (`fix(parser): …`) is honoured verbatim, a bare title defaults to `feat`.
216
- // The scope falls back through the chain (D3): the title's own `(scope)` wins
217
- // (handled inside `subjectFromTitle`), else `buildDefaultScope`. A titleless step
218
- // falls back to `chore(<scope>): checkpoint`. The `plumbbob`/`step N` identifiers
219
- // ride the body marker, not the subject; a `-m` override or `--body` prose is a
220
- // separate concern.
261
+ /**
262
+ * Compose the step's CLI-owned, deterministic Conventional Commit subject.
263
+ *
264
+ * The type and wording come from the step's title: an author-written prefix
265
+ * (`fix(parser): …`) is honoured verbatim, a bare prose title defaults to
266
+ * `feat`. The title's own `(scope)` wins (handled inside `subjectFromTitle`),
267
+ * else the build-default scope. A titleless step falls back to
268
+ * `chore(<scope>): checkpoint`. The `plumbbob`/`step N` identifiers ride the
269
+ * body marker line, not the subject; a `-m` override or `--body` prose is a
270
+ * separate concern.
271
+ */
221
272
  function subjectForStep(root, step) {
222
273
  const scope = buildDefaultScope(root);
223
274
  const title = titleForStep(root, step);
@@ -225,10 +276,13 @@ function subjectForStep(root, step) {
225
276
  ? subjectFromTitle(title, 'feat', scope)
226
277
  : conventionalSubject({ type: 'chore', scope, description: 'checkpoint' });
227
278
  }
228
- // The build-default Conventional scope (D3/D4): the `**Scope:**` header field in
229
- // intent.md when authored and filled, else the build slug (D68's original rung,
230
- // now the penultimate one), else null — a bare subject (C2 back-compat: a build
231
- // with neither field behaves exactly as before D4).
279
+ /**
280
+ * The build-default Conventional scope, or null for a bare subject.
281
+ *
282
+ * The `**Scope:**` header field in intent.md wins when authored and filled;
283
+ * else the build slug with its date prefix stripped; else null — a build with
284
+ * neither field keeps producing bare subjects.
285
+ */
232
286
  function buildDefaultScope(root) {
233
287
  try {
234
288
  const fromHeader = parseBuildScope(readFileSync(intentPath(root), 'utf8'));
@@ -241,6 +295,9 @@ function buildDefaultScope(root) {
241
295
  }
242
296
  return buildScope(activeBuild(root));
243
297
  }
298
+ /**
299
+ * The step's title from intent.md, or null when absent or unreadable.
300
+ */
244
301
  function titleForStep(root, step) {
245
302
  try {
246
303
  return parseSteps(readFileSync(intentPath(root), 'utf8')).find((s) => s.n === step)?.title ?? null;
@@ -249,16 +306,22 @@ function titleForStep(root, step) {
249
306
  return null;
250
307
  }
251
308
  }
309
+ /**
310
+ * The `-m <msg>` subject override, or null when the flag is absent.
311
+ */
252
312
  function messageArg(args) {
253
313
  const i = args.indexOf('-m');
254
314
  return i !== -1 && i + 1 < args.length ? (args[i + 1] ?? null) : null;
255
315
  }
256
- // `--body` reads the commit body from stdin (the single-quoted heredoc of D34),
257
- // so the skill can compose proportional prose the CLI never could. Returns null
258
- // when the flag is absent or stdin is empty — either way the deterministic
259
- // fallback body takes over. Reading fd 0 blocks until EOF, which the heredoc
260
- // supplies; a read error (no stdin attached) degrades to the fallback, and an
261
- // interactive TTY — which would never send EOF — skips the read instead of hanging.
316
+ /**
317
+ * Read the `--body` commit body from stdin, or null when absent or empty.
318
+ *
319
+ * The stdin heredoc lets a skill compose proportional prose the CLI never
320
+ * could; null either way hands over to the deterministic fallback body. Reading
321
+ * fd 0 blocks until EOF, which the heredoc supplies; a read error (no stdin
322
+ * attached) degrades to the fallback, and an interactive TTY — which would
323
+ * never send EOF — skips the read instead of hanging.
324
+ */
262
325
  function bodyArg(args) {
263
326
  if (!args.includes('--body') || process.stdin.isTTY === true) {
264
327
  return null;
@@ -271,10 +334,14 @@ function bodyArg(args) {
271
334
  return null;
272
335
  }
273
336
  }
274
- // The deterministic checkpoint body (D35): the step's done-when, its seam, and the
275
- // staged diffstat — so a hand-built or vibed checkpoint still gets informative
276
- // history without a model turn. Each part is best-effort; a missing piece is
277
- // simply omitted, and an empty result leaves the commit body blank.
337
+ /**
338
+ * The deterministic fallback commit body: the step's done-when, its seam, and
339
+ * the staged diffstat.
340
+ *
341
+ * A hand-built or vibed checkpoint still gets informative history without a
342
+ * model turn. Each part is best-effort; a missing piece is simply omitted, and
343
+ * an empty result leaves the commit body blank.
344
+ */
278
345
  function fallbackBody(root, step) {
279
346
  const parts = [];
280
347
  const doneWhen = doneWhenForStep(root, step);
@@ -291,6 +358,9 @@ function fallbackBody(root, step) {
291
358
  }
292
359
  return parts.length > 0 ? parts.join('\n\n') : undefined;
293
360
  }
361
+ /**
362
+ * The step's done-when line from intent.md, or null when absent or unreadable.
363
+ */
294
364
  function doneWhenForStep(root, step) {
295
365
  try {
296
366
  return parseSteps(readFileSync(intentPath(root), 'utf8')).find((s) => s.n === step)?.doneWhen ?? null;
@@ -299,6 +369,9 @@ function doneWhenForStep(root, step) {
299
369
  return null;
300
370
  }
301
371
  }
372
+ /**
373
+ * The step's declared seam from intent.md, or empty when missing or malformed.
374
+ */
302
375
  function seamForStep(root, step) {
303
376
  try {
304
377
  const parsed = parseStepSeam(readFileSync(intentPath(root), 'utf8'), step);
@@ -308,6 +381,9 @@ function seamForStep(root, step) {
308
381
  return [];
309
382
  }
310
383
  }
384
+ /**
385
+ * The staged diffstat, or '' when git refuses.
386
+ */
311
387
  function safeStagedStat(root) {
312
388
  try {
313
389
  return stagedStat(root);
@@ -316,6 +392,9 @@ function safeStagedStat(root) {
316
392
  return '';
317
393
  }
318
394
  }
395
+ /**
396
+ * The in-flight step number from the STEP file, or null when absent or garbled.
397
+ */
319
398
  function readStep(root) {
320
399
  try {
321
400
  const raw = readFileSync(stepPath(root), 'utf8').trim();