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/lib/templates.js
CHANGED
|
@@ -1,15 +1,26 @@
|
|
|
1
1
|
// Scaffold templates live at `<pkg-root>/templates/*.md` and carry `{{PLACEHOLDER}}`
|
|
2
|
-
// tokens the verbs fill in
|
|
3
|
-
// spike-report.md. Shared here so
|
|
4
|
-
// to the compiled module URL) and
|
|
5
|
-
// node
|
|
2
|
+
// tokens the verbs fill in: `start` stamps intent.md and build-log.md, `spike` stamps
|
|
3
|
+
// spike-report.md. Shared here so every verb resolves the template dir the same way
|
|
4
|
+
// (relative to the compiled module URL) and substitutes with one mechanism. Pure and
|
|
5
|
+
// procedural, node builtins only.
|
|
6
6
|
import { fileURLToPath } from 'node:url';
|
|
7
7
|
import { readFileSync } from 'node:fs';
|
|
8
|
+
/**
|
|
9
|
+
* Read a template by filename from the package's `templates/` directory.
|
|
10
|
+
*
|
|
11
|
+
* Resolved relative to the compiled module URL, so the same relative hop works
|
|
12
|
+
* from `src/lib/` and `dist/lib/` alike — the templates ship with the package,
|
|
13
|
+
* never with the consumer's repo.
|
|
14
|
+
*/
|
|
8
15
|
export function readTemplate(name) {
|
|
9
16
|
return readFileSync(fileURLToPath(new URL(`../../templates/${name}`, import.meta.url)), 'utf8');
|
|
10
17
|
}
|
|
11
|
-
|
|
12
|
-
|
|
18
|
+
/**
|
|
19
|
+
* Replace every `{{KEY}}` in the template with its value from `subs`.
|
|
20
|
+
*
|
|
21
|
+
* Generic over the placeholder set so a new template can add tokens
|
|
22
|
+
* (spike-report's `{{VIA}}`/`{{DATE}}`) without touching this.
|
|
23
|
+
*/
|
|
13
24
|
export function stampTemplate(template, subs) {
|
|
14
25
|
let out = template;
|
|
15
26
|
for (const [key, value] of Object.entries(subs)) {
|
package/dist/verbs/agent.js
CHANGED
|
@@ -1,12 +1,17 @@
|
|
|
1
|
-
// `plumbbob agent <subcommand>` — the doorway to user-authored agents
|
|
2
|
-
//
|
|
3
|
-
//
|
|
1
|
+
// `plumbbob agent <subcommand>` — the doorway to user-authored agents: anything
|
|
2
|
+
// executable that speaks the JSON envelope contract (JSON in on stdin, JSON out
|
|
3
|
+
// on stdout, prose streamed on stderr). `agent list` walks the two agent tiers —
|
|
4
|
+
// the repo's tracked `.plumbbob/agents/<name>/`, then the personal
|
|
5
|
+
// `~/.plumbbob/agents/<name>/` — and prints each resolvable agent. `agent run
|
|
6
|
+
// <name> [--step N] [--mode before|build|after]` composes the StepContext,
|
|
4
7
|
// spawns the manifest command, streams its stderr live, captures and validates
|
|
5
8
|
// the child's envelope, re-emits it on this verb's own stdout (machine) with the
|
|
6
|
-
// human summary on stderr
|
|
7
|
-
//
|
|
8
|
-
//
|
|
9
|
-
//
|
|
9
|
+
// human summary on stderr, lands `parked[]` through the build-log, and appends
|
|
10
|
+
// the envelope to the handoff ledger (`builds/<slug>/handoff.json` — untracked,
|
|
11
|
+
// step-scoped, cleared at checkpoint) so later runs can thread it back in.
|
|
12
|
+
// There is deliberately no code path here to checkpoint, flip a step, or chain
|
|
13
|
+
// agents — the subprocess boundary keeps the human as the clock by
|
|
14
|
+
// construction, not by policy. A thin read-write shell: resolution,
|
|
10
15
|
// composition, and spawn mechanics live in lib/agents.ts.
|
|
11
16
|
import { readFileSync, writeFileSync } from 'node:fs';
|
|
12
17
|
import { join } from 'node:path';
|
|
@@ -15,6 +20,9 @@ import { SLOTS, composeStepContext, formatAgentList, isSlot, listAgents, parseSl
|
|
|
15
20
|
import { appendToSection } from "../lib/buildlog.js";
|
|
16
21
|
import { resolveBoolean, resolveNumber, resolveRecord } from "../lib/settings.js";
|
|
17
22
|
import { appendHandoff, buildFolder, buildLogPath, hasSession, intentPath, resolveBuild, stepPath } from "../lib/sidecar.js";
|
|
23
|
+
/**
|
|
24
|
+
* Dispatch `plumbbob agent list|run`, refusing an unknown subcommand with a hint.
|
|
25
|
+
*/
|
|
18
26
|
export async function agent(cwd, args = []) {
|
|
19
27
|
const [sub, ...rest] = args;
|
|
20
28
|
if (sub === 'list')
|
|
@@ -26,6 +34,9 @@ export async function agent(cwd, args = []) {
|
|
|
26
34
|
process.stderr.write(`${message}\n`);
|
|
27
35
|
return 1;
|
|
28
36
|
}
|
|
37
|
+
/**
|
|
38
|
+
* Print every resolvable agent across the project and personal tiers.
|
|
39
|
+
*/
|
|
29
40
|
function list(cwd, _args) {
|
|
30
41
|
const root = findRepoRoot(cwd);
|
|
31
42
|
if (root === null) {
|
|
@@ -35,10 +46,15 @@ function list(cwd, _args) {
|
|
|
35
46
|
process.stdout.write(`${formatAgentList(listAgents(root))}\n`);
|
|
36
47
|
return 0;
|
|
37
48
|
}
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
49
|
+
/**
|
|
50
|
+
* Run one named agent, or a slot's harness-bound agents, against the step in flight.
|
|
51
|
+
*
|
|
52
|
+
* With a name (or `--agent` flag) it runs exactly that agent, failing loud on a
|
|
53
|
+
* miss — the user who typed the name asked for it specifically. With no name it
|
|
54
|
+
* runs whatever the build's harness.json binds to the requested slot. Either
|
|
55
|
+
* way it composes the StepContext, spawns, and applies side effects — never
|
|
56
|
+
* advancing the loop.
|
|
57
|
+
*/
|
|
42
58
|
async function run(cwd, args) {
|
|
43
59
|
const root = findRepoRoot(cwd);
|
|
44
60
|
if (root === null || !hasSession(root)) {
|
|
@@ -62,8 +78,8 @@ async function run(cwd, args) {
|
|
|
62
78
|
process.stderr.write('plumbbob: --agent needs an agent name too, for the run label. Try: plumbbob agent run reviewer --agent ./path --step 3.\n');
|
|
63
79
|
return 1;
|
|
64
80
|
}
|
|
65
|
-
//
|
|
66
|
-
//
|
|
81
|
+
// An explicit name outranks every binding: run exactly it, fail loud on a
|
|
82
|
+
// miss. No name: resolve the harness bindings for the slot and run them.
|
|
67
83
|
if (parsed.name !== undefined) {
|
|
68
84
|
return runOne(root, slug, step, {
|
|
69
85
|
name: parsed.name,
|
|
@@ -74,6 +90,16 @@ async function run(cwd, args) {
|
|
|
74
90
|
}
|
|
75
91
|
return runBound(root, slug, step, parsed.mode);
|
|
76
92
|
}
|
|
93
|
+
/**
|
|
94
|
+
* One agent's full run: resolve it, pick its slot, compose the StepContext, spawn, report.
|
|
95
|
+
*
|
|
96
|
+
* `ambient` marks a harness-bound run whose resolution or slot mismatch
|
|
97
|
+
* degrades to a warning so a batch keeps going — a binding is ambient
|
|
98
|
+
* configuration the loop must survive without; an explicit ask
|
|
99
|
+
* (`ambient: false`) fails loud on the same miss. A run that actually starts
|
|
100
|
+
* and fails (non-zero exit, timeout, …) is a hard failure either way — the
|
|
101
|
+
* softening covers a *missing* agent, never a broken one.
|
|
102
|
+
*/
|
|
77
103
|
async function runOne(root, slug, step, spec) {
|
|
78
104
|
const resolution = resolveAgent(root, spec.name, spec.flagPath !== undefined ? { flagPath: spec.flagPath } : {});
|
|
79
105
|
if (!resolution.ok) {
|
|
@@ -99,15 +125,18 @@ async function runOne(root, slug, step, spec) {
|
|
|
99
125
|
step,
|
|
100
126
|
mode,
|
|
101
127
|
settings: {
|
|
102
|
-
// The
|
|
103
|
-
//
|
|
104
|
-
//
|
|
128
|
+
// The envelope reports the `auto` setting to agents as information only.
|
|
129
|
+
// The checkpoint latch never reads it — a model can write a settings
|
|
130
|
+
// file, so a standing `auto` cannot be a self-approval grant; approval
|
|
131
|
+
// comes only from what the human literally typed. A `true` here informs
|
|
132
|
+
// an agent, it does not self-approve.
|
|
105
133
|
auto: resolveBoolean(root, 'auto', false),
|
|
106
134
|
agentTimeout: resolveNumber(root, 'agentTimeout', 0),
|
|
107
|
-
// Hand this agent its own config block over the
|
|
108
|
-
// field
|
|
109
|
-
//
|
|
110
|
-
//
|
|
135
|
+
// Hand this agent its own config block over the envelope's existing
|
|
136
|
+
// `settings` field — no new envelope field, no new verb; the config just
|
|
137
|
+
// rides here. Resolution: settings.json's agentConfig[name], with the
|
|
138
|
+
// untracked personal overlay (settings.local.json) replacing the project
|
|
139
|
+
// entry whole — no deep merge — and {} when neither defines it.
|
|
111
140
|
agent: resolveRecord(root, 'agentConfig')[spec.name] ?? {},
|
|
112
141
|
},
|
|
113
142
|
});
|
|
@@ -127,11 +156,16 @@ async function runOne(root, slug, step, spec) {
|
|
|
127
156
|
});
|
|
128
157
|
return report(root, slug, spec.name, mode, step, result);
|
|
129
158
|
}
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
159
|
+
/**
|
|
160
|
+
* No name given: run the agents the build's harness.json binds to the requested slot.
|
|
161
|
+
*
|
|
162
|
+
* Bindings merge as a ladder — the per-step harness entry beats the harness
|
|
163
|
+
* `defaults`, which beat the settings-level `agents` key; the first level that
|
|
164
|
+
* names the slot wins, replace not append. A missing bound agent degrades to a
|
|
165
|
+
* warning; an absent harness, or one that binds nothing to this slot, is a
|
|
166
|
+
* clean no-op. Each bound agent runs in turn; the batch exits 1 if any agent
|
|
167
|
+
* that actually ran failed.
|
|
168
|
+
*/
|
|
135
169
|
async function runBound(root, slug, step, modeFlag) {
|
|
136
170
|
if (modeFlag === undefined) {
|
|
137
171
|
process.stderr.write('plumbbob: agent run needs an agent name, or --mode <slot> to run the step\'s bound agents.\n');
|
|
@@ -165,18 +199,25 @@ async function runBound(root, slug, step, modeFlag) {
|
|
|
165
199
|
}
|
|
166
200
|
return worst;
|
|
167
201
|
}
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
202
|
+
/**
|
|
203
|
+
* Emit the loud error or the soft warning by whether this run was an explicit
|
|
204
|
+
* ask (name/flag) or an ambient harness binding, and return the matching code.
|
|
205
|
+
*
|
|
206
|
+
* A hard miss stops with 1; a degraded one warns and returns 0 so a batch of
|
|
207
|
+
* bound agents carries on.
|
|
208
|
+
*/
|
|
172
209
|
function degrade(ambient, hard, soft) {
|
|
173
210
|
process.stderr.write(`${ambient ? soft : hard}\n`);
|
|
174
211
|
return ambient ? 0 : 1;
|
|
175
212
|
}
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
213
|
+
/**
|
|
214
|
+
* Split `run`'s argv (with `--build` already stripped by resolveBuild) into the
|
|
215
|
+
* agent name and the value flags.
|
|
216
|
+
*
|
|
217
|
+
* A value flag missing its value, or `--step` given a non-number, is a loud
|
|
218
|
+
* error (returned as a string) rather than a silent default. Unknown `--flags`
|
|
219
|
+
* are ignored — the point is the named agent.
|
|
220
|
+
*/
|
|
180
221
|
function parseRunArgs(args) {
|
|
181
222
|
const positionals = [];
|
|
182
223
|
let step;
|
|
@@ -209,11 +250,15 @@ function parseRunArgs(args) {
|
|
|
209
250
|
}
|
|
210
251
|
return { name: positionals[0], step, mode, flagPath };
|
|
211
252
|
}
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
253
|
+
/**
|
|
254
|
+
* Resolve the slot the agent runs in.
|
|
255
|
+
*
|
|
256
|
+
* An explicit `--mode` must name a real slot AND one the manifest declares —
|
|
257
|
+
* an undeclared slot is refused loud, because the user asked for that exact
|
|
258
|
+
* run. With no `--mode`, a single-slot agent uses its only slot; a multi-slot
|
|
259
|
+
* agent must be told which one (harness bindings pick the slot from the
|
|
260
|
+
* lifecycle point; a bare `run` cannot guess).
|
|
261
|
+
*/
|
|
217
262
|
function resolveMode(flag, manifest) {
|
|
218
263
|
if (flag !== undefined) {
|
|
219
264
|
if (!isSlot(flag))
|
|
@@ -233,13 +278,16 @@ function resolveMode(flag, manifest) {
|
|
|
233
278
|
error: `plumbbob: agent "${manifest.name}" declares multiple slots (${manifest.slots.join(', ')}) — pass --mode <slot>.`,
|
|
234
279
|
};
|
|
235
280
|
}
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
281
|
+
/**
|
|
282
|
+
* Map the run outcome to reporting and side effects.
|
|
283
|
+
*
|
|
284
|
+
* A failed run — non-zero exit, out-of-contract stdout, timeout, interrupt, or a
|
|
285
|
+
* shell that never started — reports on stderr and stops with NO side effects:
|
|
286
|
+
* the envelope of a failed child is not authoritative. A clean run lands
|
|
287
|
+
* `parked[]`, records the envelope in the handoff ledger, prints the human
|
|
288
|
+
* summary on stderr, and re-emits the machine envelope on stdout — nothing else,
|
|
289
|
+
* ever (the stream discipline: stdout carries the envelope alone).
|
|
290
|
+
*/
|
|
243
291
|
function report(root, slug, name, mode, step, result) {
|
|
244
292
|
if (!result.ok) {
|
|
245
293
|
process.stderr.write(`${failureLine(name, result)}\n`);
|
|
@@ -252,6 +300,9 @@ function report(root, slug, name, mode, step, result) {
|
|
|
252
300
|
process.stdout.write(`${JSON.stringify(envelope)}\n`);
|
|
253
301
|
return 0;
|
|
254
302
|
}
|
|
303
|
+
/**
|
|
304
|
+
* One stderr line naming exactly how a failed run failed.
|
|
305
|
+
*/
|
|
255
306
|
function failureLine(name, result) {
|
|
256
307
|
switch (result.reason) {
|
|
257
308
|
case 'exit':
|
|
@@ -266,9 +317,13 @@ function failureLine(name, result) {
|
|
|
266
317
|
return `plumbbob: could not spawn agent "${name}" — ${result.error}`;
|
|
267
318
|
}
|
|
268
319
|
}
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
320
|
+
/**
|
|
321
|
+
* Land each parked concern through the build-log's Park list.
|
|
322
|
+
*
|
|
323
|
+
* The agent never writes .plumbbob/ itself — parked lines only reach the
|
|
324
|
+
* build-log through this verb. A build-log with no "## Park list" section, or no
|
|
325
|
+
* build-log at all, warns once rather than losing the lines silently.
|
|
326
|
+
*/
|
|
272
327
|
function applyParked(root, slug, parked) {
|
|
273
328
|
if (parked.length === 0)
|
|
274
329
|
return;
|
|
@@ -292,10 +347,13 @@ function applyParked(root, slug, parked) {
|
|
|
292
347
|
}
|
|
293
348
|
writeFileSync(path, content);
|
|
294
349
|
}
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
350
|
+
/**
|
|
351
|
+
* The human-facing summary printed on stderr.
|
|
352
|
+
*
|
|
353
|
+
* A headline plus, for a halt, the route out — `blocked` unblocks and re-runs,
|
|
354
|
+
* `drift` sends the plan to /plumbbob:refine. The machine envelope on stdout carries
|
|
355
|
+
* the same status for the calling skill; this stderr copy is the terminal read.
|
|
356
|
+
*/
|
|
299
357
|
function humanSummary(name, mode, envelope) {
|
|
300
358
|
const head = `plumbbob: agent "${name}" (${mode}) — ${envelope.status}: ${envelope.summary}\n`;
|
|
301
359
|
const notes = envelope.notes.length > 0 ? ` notes: ${envelope.notes}\n` : '';
|
|
@@ -303,12 +361,15 @@ function humanSummary(name, mode, envelope) {
|
|
|
303
361
|
return `${head} blocked — the agent couldn't finish; unblock and re-run.\n${notes}`;
|
|
304
362
|
}
|
|
305
363
|
if (envelope.status === 'drift') {
|
|
306
|
-
return `${head} drift — the plan no longer matches reality; /
|
|
364
|
+
return `${head} drift — the plan no longer matches reality; /plumbbob:refine before continuing.\n${notes}`;
|
|
307
365
|
}
|
|
308
366
|
return head;
|
|
309
367
|
}
|
|
310
|
-
|
|
311
|
-
|
|
368
|
+
/**
|
|
369
|
+
* The in-flight step for this build (the STEP marker `build` wrote), or null.
|
|
370
|
+
*
|
|
371
|
+
* With no marker set, the caller requires an explicit `--step`.
|
|
372
|
+
*/
|
|
312
373
|
function readStep(root, slug) {
|
|
313
374
|
try {
|
|
314
375
|
const raw = readFileSync(stepPath(root, slug), 'utf8').trim();
|
package/dist/verbs/build.js
CHANGED
|
@@ -1,12 +1,23 @@
|
|
|
1
|
-
// `plumbbob build <n>` — read step n's seam from intent.md
|
|
2
|
-
// SEAM + STEP
|
|
3
|
-
//
|
|
1
|
+
// `plumbbob build <n>` — enter a step: read step n's seam from intent.md and
|
|
2
|
+
// write the normalized SEAM + STEP control files (flat, untracked, per-build).
|
|
3
|
+
// The seam is the step's edit grant — the exact paths and `dir/` prefixes the
|
|
4
|
+
// step expects to touch, for orientation, never a lock. The STEP file is the
|
|
5
|
+
// in-flight signal (the dashboard derives the BUILD phase from it); this verb
|
|
6
|
+
// never checkpoints — only `checkpoint` commits.
|
|
4
7
|
import { readFileSync, writeFileSync } from 'node:fs';
|
|
5
8
|
import { findRepoRoot } from "../lib/git.js";
|
|
6
9
|
import { hasSession, intentPath, resolveBuild, seamPath, stampStepStat, stampTick, stepPath } from "../lib/sidecar.js";
|
|
7
10
|
import { parseStepSeam } from "../lib/intent.js";
|
|
8
11
|
import { parseSteps } from "../lib/orient.js";
|
|
9
12
|
import { stepLabel, syncBuildLogState } from "../lib/buildlogsync.js";
|
|
13
|
+
/**
|
|
14
|
+
* Enter step n — or the next undone step — and write its SEAM/STEP markers.
|
|
15
|
+
*
|
|
16
|
+
* Refuses a missing session, a malformed step argument, an `N-M` range (a
|
|
17
|
+
* `/plumbbob:build` skill feature, not a CLI one), and a seam that fails to parse.
|
|
18
|
+
* On entry it stamps the turn ledger and the step's start time, and re-renders
|
|
19
|
+
* the build-log's Current step line.
|
|
20
|
+
*/
|
|
10
21
|
export function build(cwd, args) {
|
|
11
22
|
const root = findRepoRoot(cwd);
|
|
12
23
|
if (root === null || !hasSession(root)) {
|
|
@@ -15,11 +26,11 @@ export function build(cwd, args) {
|
|
|
15
26
|
}
|
|
16
27
|
const { build: slug, rest } = resolveBuild(root, args);
|
|
17
28
|
const raw = rest.find((a) => !a.startsWith('--'));
|
|
18
|
-
// A step range like `1-3` is a `/
|
|
29
|
+
// A step range like `1-3` is a `/plumbbob:build` skill affordance (auto-approve
|
|
19
30
|
// through the range, then pause), not a CLI capability — the CLI records one
|
|
20
31
|
// in-flight step at a time. Name it rather than bounce off the generic usage.
|
|
21
32
|
if (raw !== undefined && /^\d+-\d*$/.test(raw)) {
|
|
22
|
-
process.stderr.write(`plumbbob: build takes one step number; \`${raw}\` step ranges are a \`/
|
|
33
|
+
process.stderr.write(`plumbbob: build takes one step number; \`${raw}\` step ranges are a \`/plumbbob:build\` feature (auto-approve through the range, then pause). Try \`plumbbob build ${raw.split('-')[0]}\`.\n`);
|
|
23
34
|
return 1;
|
|
24
35
|
}
|
|
25
36
|
// An explicit arg that isn't a positive integer is a usage error — caught before
|
|
@@ -31,13 +42,13 @@ export function build(cwd, args) {
|
|
|
31
42
|
const intent = readFileSync(intentPath(root, slug), 'utf8');
|
|
32
43
|
// No argument ⇒ enter the next undone step in intent.md (the same idiom
|
|
33
44
|
// `checkpoint` uses), so a bare `plumbbob build` advances the loop without the
|
|
34
|
-
// skill re-deriving the step in prose. Every step checkpointed ⇒ a `/
|
|
45
|
+
// skill re-deriving the step in prose. Every step checkpointed ⇒ a `/plumbbob:step`
|
|
35
46
|
// nudge, not a silent no-op.
|
|
36
47
|
let step;
|
|
37
48
|
if (raw === undefined) {
|
|
38
49
|
const nextUndone = parseSteps(intent).find((s) => !s.done);
|
|
39
50
|
if (nextUndone === undefined) {
|
|
40
|
-
process.stderr.write('plumbbob: no undone step to build — every planned step is checkpointed. `/
|
|
51
|
+
process.stderr.write('plumbbob: no undone step to build — every planned step is checkpointed. `/plumbbob:step` to add an increment, or `/plumbbob:finish`.\n');
|
|
41
52
|
return 1;
|
|
42
53
|
}
|
|
43
54
|
step = nextUndone.n;
|
|
@@ -52,12 +63,16 @@ export function build(cwd, args) {
|
|
|
52
63
|
}
|
|
53
64
|
writeFileSync(seamPath(root, slug), `${parsed.seam.join('\n')}\n`);
|
|
54
65
|
writeFileSync(stepPath(root, slug), `${step}\n`);
|
|
55
|
-
|
|
56
|
-
//
|
|
57
|
-
//
|
|
66
|
+
// Stamp the turn ledger on entry (TICK = TURN): the checkpoint latch demands
|
|
67
|
+
// a human turn after this point before the step may land. Skipped when the
|
|
68
|
+
// ledger is dormant — a host with no hooks grows no TURN/TICK files.
|
|
69
|
+
stampTick(root, slug);
|
|
70
|
+
// The wall-clock receipt starts here: checkpoint stamps landedAt, and the
|
|
71
|
+
// pair becomes the step's duration in the finish report.
|
|
58
72
|
stampStepStat(root, slug, step, 'startedAt', new Date().toISOString());
|
|
59
|
-
// The build-log's top half
|
|
60
|
-
// the ☐/☑ mirror, re-rendered from intent.md.
|
|
73
|
+
// The build-log's top half is CLI-owned so it never lies: show this step in
|
|
74
|
+
// flight — Current step plus the ☐/☑ mirror, re-rendered from intent.md.
|
|
75
|
+
// Best-effort — a missing or hand-edited build-log never blocks the build.
|
|
61
76
|
const title = parseSteps(intent).find((s) => s.n === step)?.title ?? null;
|
|
62
77
|
syncBuildLogState(root, slug, stepLabel(step, title));
|
|
63
78
|
const picked = raw === undefined ? ' (next undone)' : '';
|
package/dist/verbs/check.js
CHANGED
|
@@ -1,14 +1,24 @@
|
|
|
1
|
-
// `plumbbob check` — run the heavy gate
|
|
2
|
-
//
|
|
3
|
-
// pause so the human approves on a known-green check.
|
|
4
|
-
//
|
|
1
|
+
// `plumbbob check` — run the heavy gate and report, with NO state change. This
|
|
2
|
+
// is the read-only half of the verify tick: `/plumbbob:verify` runs it before the
|
|
3
|
+
// pause so the human approves on a known-green check. The gate itself lives in
|
|
4
|
+
// lib/check.ts: checkride (our sibling check-runner package) by default, or a
|
|
5
|
+
// spawn command named by the `check` setting — resolved through the settings
|
|
6
|
+
// ladder (flag → local overlay → tracked settings.json → default) — for repos
|
|
7
|
+
// that gate through something else. Exits with the check's own code (0 = green,
|
|
8
|
+
// 1 = red, 2 = the gate itself broke).
|
|
5
9
|
//
|
|
6
10
|
// Narrowing flags for the iteration loop (`check --bail --only types,lint`) map
|
|
7
|
-
// straight onto checkride's
|
|
11
|
+
// straight onto checkride's run flags. Only this verb takes them — the
|
|
8
12
|
// checkpoint gate is always full-fat.
|
|
9
13
|
import { findRepoRoot } from "../lib/git.js";
|
|
10
14
|
import { hasSession } from "../lib/sidecar.js";
|
|
11
15
|
import { runCheck } from "../lib/check.js";
|
|
16
|
+
/**
|
|
17
|
+
* Run the heavy check gate for the active session and print the verdict line.
|
|
18
|
+
*
|
|
19
|
+
* Refuses without a session; otherwise returns the gate's own exit code so a
|
|
20
|
+
* caller can branch on green (0), red (1), or a broken gate (2) directly.
|
|
21
|
+
*/
|
|
12
22
|
export async function check(cwd, args = []) {
|
|
13
23
|
const root = findRepoRoot(cwd);
|
|
14
24
|
if (root === null || !hasSession(root)) {
|
|
@@ -19,8 +29,12 @@ export async function check(cwd, args = []) {
|
|
|
19
29
|
process.stdout.write(verdictLine(code));
|
|
20
30
|
return code;
|
|
21
31
|
}
|
|
22
|
-
|
|
23
|
-
|
|
32
|
+
/**
|
|
33
|
+
* Map argv onto checkride's flags: bare booleans plus comma-separated slot lists.
|
|
34
|
+
*
|
|
35
|
+
* Unknown args are ignored rather than refused — running the gate is the point,
|
|
36
|
+
* not flag hygiene.
|
|
37
|
+
*/
|
|
24
38
|
function parseFlags(args) {
|
|
25
39
|
return {
|
|
26
40
|
...(args.includes('--bail') ? { bail: true } : {}),
|
|
@@ -31,6 +45,12 @@ function parseFlags(args) {
|
|
|
31
45
|
...slotList(args, '--include'),
|
|
32
46
|
};
|
|
33
47
|
}
|
|
48
|
+
/**
|
|
49
|
+
* Read one `--only a,b`-style flag into its slot-list entry.
|
|
50
|
+
*
|
|
51
|
+
* Returns {} when the flag is absent, valueless, or followed by another flag,
|
|
52
|
+
* so the spread in parseFlags simply contributes nothing.
|
|
53
|
+
*/
|
|
34
54
|
function slotList(args, flag) {
|
|
35
55
|
const i = args.indexOf(flag);
|
|
36
56
|
const value = i >= 0 ? args[i + 1] : undefined;
|
|
@@ -42,6 +62,12 @@ function slotList(args, flag) {
|
|
|
42
62
|
.filter((s) => s.length > 0);
|
|
43
63
|
return names.length > 0 ? { [flag.slice(2)]: names } : {};
|
|
44
64
|
}
|
|
65
|
+
/**
|
|
66
|
+
* The one-line human verdict for a check exit code.
|
|
67
|
+
*
|
|
68
|
+
* A broken gate (exit 2) reports distinctly from red — a misconfigured harness
|
|
69
|
+
* must never read as broken code, though both block a checkpoint.
|
|
70
|
+
*/
|
|
45
71
|
function verdictLine(code) {
|
|
46
72
|
if (code === 0)
|
|
47
73
|
return '\nplumbbob: check green.\n';
|