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