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.
- package/.claude-plugin/plugin.json +1 -1
- package/README.md +51 -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 +356 -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 +158 -79
- package/dist/verbs/doctor.js +192 -95
- package/dist/verbs/finish.js +88 -46
- 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 +1 -1
- 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 +3 -3
- 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,15 +1,17 @@
|
|
|
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
|
-
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
|
|
35
|
-
// without a human turn since
|
|
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
|
-
//
|
|
46
|
-
//
|
|
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
|
|
69
|
-
// the boundary
|
|
70
|
-
// artifact-plane whitelist keeps this build-log write from ever reading as
|
|
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
|
|
75
|
-
clearTick(root); // the entry stamp
|
|
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
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
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
|
|
88
|
-
// stamped: the plan pause is a pause. No step number — a range
|
|
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
|
-
//
|
|
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
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
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
|
-
|
|
112
|
-
|
|
113
|
-
|
|
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
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
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
|
|
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 \`/
|
|
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
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
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
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
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
|
-
|
|
193
|
-
|
|
194
|
-
|
|
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
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
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
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
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
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
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
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
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();
|