@haiyangbg/buildbeat 3.3.1 → 4.0.0
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/CHANGELOG.md +19 -0
- package/README.en.md +20 -124
- package/README.md +20 -124
- package/SKILL.md +73 -84
- package/docs/CAPABILITY-MATRIX.md +30 -39
- package/docs/MIGRATION.md +93 -0
- package/docs/README.md +8 -37
- package/docs/RELEASING.md +4 -3
- package/docs/v2/RFC-0001-product-definition.md +3 -0
- package/docs/v2/RFC-0002-domain-model.md +3 -0
- package/docs/v2/RFC-0003-workflow-policy.md +3 -0
- package/docs/v2/SPEC-0001-events-v1.md +3 -0
- package/docs/v2/guide/00-how-to-talk.en.md +12 -58
- package/docs/v2/guide/00-how-to-talk.md +12 -58
- package/docs/v2/guide/01-quickstart.en.md +41 -112
- package/docs/v2/guide/01-quickstart.md +41 -112
- package/docs/v2/guide/02-workflow-guide.en.md +13 -123
- package/docs/v2/guide/02-workflow-guide.md +14 -122
- package/docs/v2/guide/03-policy-guide.en.md +6 -50
- package/docs/v2/guide/03-policy-guide.md +6 -50
- package/docs/v2/guide/04-adapter-guide.en.md +7 -60
- package/docs/v2/guide/04-adapter-guide.md +7 -60
- package/docs/v2/guide/05-worker-contract.en.md +6 -53
- package/docs/v2/guide/05-worker-contract.md +7 -52
- package/docs/v2/guide/06-evidence-guide.en.md +8 -44
- package/docs/v2/guide/06-evidence-guide.md +9 -43
- package/docs/v2/guide/07-approval-guide.en.md +8 -110
- package/docs/v2/guide/07-approval-guide.md +8 -121
- package/docs/v2/guide/09-security-boundaries.en.md +13 -37
- package/docs/v2/guide/09-security-boundaries.md +13 -37
- package/docs/v2/guide/10-recovery.en.md +12 -96
- package/docs/v2/guide/10-recovery.md +12 -96
- package/docs/v2/guide/11-session-handoff.en.md +7 -78
- package/docs/v2/guide/11-session-handoff.md +7 -78
- package/docs/v2/guide/12-without-runtime.en.md +46 -0
- package/docs/v2/guide/12-without-runtime.md +46 -0
- package/docs/v2/guide/README.en.md +16 -36
- package/docs/v2/guide/README.md +16 -36
- package/example/AGENTS.md +15 -69
- package/example/README.md +8 -23
- package/example/delivery/envelope/prompts/builder.md +3 -1
- package/example/delivery/envelope/prompts/fixer.md +3 -1
- package/example/delivery/envelope/prompts/reviewer.md +3 -1
- package/example//346/214/207/346/214/245/345/217/260.md +8 -40
- package/package.json +2 -1
- package/src/v2/adapters/notification-payload.js +40 -0
- package/src/v2/adapters/shell.js +3 -1
- package/src/v2/cli/run-config-check.js +129 -45
- package/src/v2/cli/run.js +821 -252
- package/src/v2/engine/reducer.js +2 -0
- package/src/v2/engine/risk-preset.js +14 -47
- package/src/v2/engine/workflow.js +177 -185
- package/src/v2/evidence/collector.js +12 -1
- package/src/v2/evidence/image.js +346 -0
- package/src/v2/policy/policy.js +219 -286
- package/src/v2/presets/software-delivery.yaml +1 -16
- package/src/v2/runtime/cache.js +22 -1
- package/src/v2/runtime/decisions.js +185 -31
- package/src/v2/runtime/findings.js +4 -1
- package/src/v2/runtime/notify.js +42 -36
- package/src/v2/runtime/orchestrator.js +811 -297
- package/src/v2/runtime/overview.js +163 -34
- package/src/v2/runtime/release.js +233 -0
- package/src/v2/runtime/run-record.js +1 -0
- package/src/v2/workspace/workspace-manager.js +8 -0
- package/templates/v2/AGENTS.md +15 -69
- package/templates/v2/BUILDBEAT.md +2 -13
- package/templates/v2/envelope/prompts/builder.md +3 -1
- package/templates/v2/envelope/prompts/fixer.md +3 -1
- package/templates/v2/envelope/prompts/reviewer.md +3 -1
- package/templates/v2/run-config.example.yaml +7 -5
- package/templates/v2/work.example.md +16 -0
- package/templates/v2//346/214/207/346/214/245/345/217/260.md +8 -40
- package/docs/v2/skill/01-principles.md +0 -26
- package/docs/v2/skill/02-project-layout.md +0 -31
- package/docs/v2/skill/03-collaboration-rules.md +0 -45
- package/docs/v2/skill/04-rhythm-and-rituals.md +0 -104
- package/docs/v2/skill/05-red-lines.md +0 -13
- package/docs/v2/skill/06-bootstrap-and-takeover.md +0 -84
- package/docs/v2/skill/07-templates-and-lessons.md +0 -24
- package/example/.buildbeat/observe.yaml +0 -31
- package/src/v2/observe/observe-config.js +0 -194
- package/src/v2/observe/observe-reducer.js +0 -117
- package/src/v2/observe/observe.js +0 -427
- package/src/v2/presets/observe.yaml +0 -45
- package/src/v2/presets/policies/ui-render-gate.yaml +0 -13
- package/src/v2/presets/release-readback.yaml +0 -36
- package/src/v2/presets/risk/controlled.yaml +0 -39
- package/src/v2/presets/risk/fast.yaml +0 -19
- package/src/v2/presets/risk/release.yaml +0 -21
- package/src/v2/presets/risk/standard.yaml +0 -28
- package/templates/ARCHITECTURE.md +0 -50
- package/templates/contracts/PROTOCOL.md +0 -28
- package/templates/pm/adr/ADR-0000-template.md +0 -25
- package/templates/pm/adr/README.md +0 -15
- package/templates/pm/decisions.md +0 -11
- package/templates/standards/CODE.md +0 -23
- package/templates/standards/DESIGN.md +0 -36
- package/templates/standards/REVIEW.md +0 -20
- package/templates/standards/STACK.md +0 -31
package/src/v2/cli/run.js
CHANGED
|
@@ -17,11 +17,22 @@ import { fileURLToPath } from "node:url";
|
|
|
17
17
|
|
|
18
18
|
import { createShellAdapter } from "../adapters/shell.js";
|
|
19
19
|
import { loadRiskPreset } from "../engine/risk-preset.js";
|
|
20
|
-
import {
|
|
20
|
+
import {
|
|
21
|
+
DELIVERY_TEXT,
|
|
22
|
+
loadWorkflow,
|
|
23
|
+
deliveryWorkflow,
|
|
24
|
+
nextStep,
|
|
25
|
+
} from "../engine/workflow.js";
|
|
21
26
|
import { parseYamlSubset } from "../engine/yaml-subset.js";
|
|
22
|
-
import {
|
|
23
|
-
import {
|
|
24
|
-
|
|
27
|
+
import { deliveryChecks } from "../policy/policy.js";
|
|
28
|
+
import {
|
|
29
|
+
acceptArtifacts,
|
|
30
|
+
adoptCandidate,
|
|
31
|
+
boundArtifacts,
|
|
32
|
+
approveRun,
|
|
33
|
+
listInbox,
|
|
34
|
+
rejectRun,
|
|
35
|
+
} from "../runtime/decisions.js";
|
|
25
36
|
import { checkRequires } from "../runtime/env-contract.js";
|
|
26
37
|
import {
|
|
27
38
|
adjudicateFinding,
|
|
@@ -31,7 +42,14 @@ import {
|
|
|
31
42
|
} from "../runtime/findings.js";
|
|
32
43
|
import { loadEnvelope, nextAttemptId } from "../runtime/envelope.js";
|
|
33
44
|
import { applyGc, planGc } from "../runtime/gc.js";
|
|
34
|
-
import {
|
|
45
|
+
import {
|
|
46
|
+
artifactStatus,
|
|
47
|
+
computeOverview,
|
|
48
|
+
readJsonl,
|
|
49
|
+
renderOverview,
|
|
50
|
+
runsFor,
|
|
51
|
+
} from "../runtime/overview.js";
|
|
52
|
+
import { closeWork, runReadback } from "../runtime/release.js";
|
|
35
53
|
import {
|
|
36
54
|
DEFAULT_STALL_AFTER_MS,
|
|
37
55
|
describeLiveness,
|
|
@@ -42,17 +60,31 @@ import { computeMetrics, renderMetrics } from "../runtime/metrics.js";
|
|
|
42
60
|
import {
|
|
43
61
|
NOTIFY_CONFIG,
|
|
44
62
|
buildNotification,
|
|
63
|
+
candidateScreenshots,
|
|
45
64
|
dispatchNotification,
|
|
46
65
|
loadNotifyConfig,
|
|
47
66
|
nextReply,
|
|
48
67
|
subscribes,
|
|
49
68
|
} from "../runtime/notify.js";
|
|
50
69
|
import { writeRunRecord } from "../runtime/run-record.js";
|
|
51
|
-
import {
|
|
70
|
+
import {
|
|
71
|
+
DEFAULT_REVIEW_ROUNDS_PER_WORK,
|
|
72
|
+
assertRunConfiguration,
|
|
73
|
+
resumeRun,
|
|
74
|
+
startRun,
|
|
75
|
+
} from "../runtime/orchestrator.js";
|
|
52
76
|
import { toRepoRef } from "../runtime/repo-ref.js";
|
|
53
77
|
import { EventLedger } from "../storage/event-ledger.js";
|
|
54
|
-
import {
|
|
55
|
-
|
|
78
|
+
import {
|
|
79
|
+
acquireLock,
|
|
80
|
+
listHeldRunLocks,
|
|
81
|
+
releaseLock,
|
|
82
|
+
} from "../workspace/workspace-manager.js";
|
|
83
|
+
import {
|
|
84
|
+
checkRunConfigAgainstWorkflow,
|
|
85
|
+
checkRunConfigShape,
|
|
86
|
+
RunConfigError,
|
|
87
|
+
} from "./run-config-check.js";
|
|
56
88
|
|
|
57
89
|
const KERNEL = { kind: "kernel", id: "cli" };
|
|
58
90
|
|
|
@@ -60,32 +92,25 @@ const USAGE = `BuildBeat runtime
|
|
|
60
92
|
|
|
61
93
|
Usage:
|
|
62
94
|
buildbeat --version
|
|
63
|
-
buildbeat
|
|
64
|
-
buildbeat
|
|
65
|
-
buildbeat status --repo <path> --
|
|
66
|
-
buildbeat
|
|
67
|
-
buildbeat
|
|
68
|
-
buildbeat
|
|
69
|
-
buildbeat
|
|
70
|
-
buildbeat
|
|
71
|
-
buildbeat doctor --config <run-config.yaml>
|
|
72
|
-
buildbeat events --repo <path> --run <RUN-ID>
|
|
73
|
-
buildbeat replay --repo <path> --run <RUN-ID>
|
|
74
|
-
buildbeat metrics --repo <path> [--json]
|
|
95
|
+
buildbeat accept --repo <path> --work <WORK-ID> [--by <name>]
|
|
96
|
+
buildbeat run --config <run-config.yaml> [--run <RUN-ID>] [--new] [--adopt <sha> --by <name>]
|
|
97
|
+
buildbeat status --repo <path> [--work <WORK-ID> | --run <RUN-ID>] [--json]
|
|
98
|
+
buildbeat decide --repo <path> --run <RUN-ID> --action approve|reject --transition <t> [--by <name>] [--reason <text>]
|
|
99
|
+
buildbeat decide --repo <path> --work <WORK-ID> --action accept|dismiss --fingerprint <fp> [--by <name>] [--note <text>]
|
|
100
|
+
buildbeat release --config <run-config.yaml> [--ref <ref>] [--note <text>]
|
|
101
|
+
buildbeat decide --repo <path> --work <WORK-ID> --action close --result <text> [--by <name>]
|
|
102
|
+
buildbeat check --config <run-config.yaml> [--step <id>]
|
|
75
103
|
buildbeat stop --repo <path> --run <RUN-ID> --reason <text>
|
|
76
104
|
buildbeat gc --repo <path> [--apply] [--force]
|
|
77
|
-
buildbeat
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
buildbeat preflight --config <run-config.yaml> --step <id>
|
|
82
|
-
buildbeat findings list --repo <path> --work <WORK-ID>
|
|
83
|
-
buildbeat findings adjudicate --repo <path> --work <WORK-ID> --fingerprint <fp> --action <accept|dismiss> [--by <name>] [--note <text>]
|
|
105
|
+
buildbeat history --repo <path> --run <RUN-ID> [--verify]
|
|
106
|
+
|
|
107
|
+
Core legacy command spellings remain aliases; see docs/MIGRATION.md.
|
|
108
|
+
check --step executes that worker in the main checkout, outside the evidence ledger.
|
|
84
109
|
`;
|
|
85
110
|
|
|
86
111
|
// Switches may stand alone (`--json`) or take an explicit true/false
|
|
87
112
|
// (`--json true`, the older spelling); every other flag needs a value.
|
|
88
|
-
const SWITCHES = new Set(["json", "apply", "force", "once"]);
|
|
113
|
+
const SWITCHES = new Set(["json", "apply", "force", "once", "new", "verify"]);
|
|
89
114
|
|
|
90
115
|
function parseFlags(argv) {
|
|
91
116
|
const flags = {};
|
|
@@ -115,7 +140,14 @@ function parseFlags(argv) {
|
|
|
115
140
|
}
|
|
116
141
|
|
|
117
142
|
function ledgerPathFor(repo, runId) {
|
|
118
|
-
return join(
|
|
143
|
+
return join(
|
|
144
|
+
resolve(repo),
|
|
145
|
+
".buildbeat",
|
|
146
|
+
"runtime",
|
|
147
|
+
"runs",
|
|
148
|
+
runId,
|
|
149
|
+
"events.jsonl",
|
|
150
|
+
);
|
|
119
151
|
}
|
|
120
152
|
|
|
121
153
|
function stallAfterFromFlags(flags, fallbackMs = DEFAULT_STALL_AFTER_MS) {
|
|
@@ -177,7 +209,9 @@ function printState(state, ledger, view = {}) {
|
|
|
177
209
|
: { steps: {}, inFlight: null };
|
|
178
210
|
console.log(`run: ${state.run.id} (work ${state.run.work})`);
|
|
179
211
|
console.log(`status: ${state.run.status}`);
|
|
180
|
-
console.log(
|
|
212
|
+
console.log(
|
|
213
|
+
`workflow: ${state.run.workflowRef} @ ${state.run.workflowDigest}`,
|
|
214
|
+
);
|
|
181
215
|
for (const [id, workspace] of Object.entries(state.workspaces)) {
|
|
182
216
|
console.log(
|
|
183
217
|
`workspace ${id}: base ${workspace.base.slice(0, 7)} candidate ${workspace.candidate ? workspace.candidate.slice(0, 7) : "(none)"}`,
|
|
@@ -201,11 +235,16 @@ function printState(state, ledger, view = {}) {
|
|
|
201
235
|
suffix = ` [${parts.join(", ")}]`;
|
|
202
236
|
}
|
|
203
237
|
}
|
|
204
|
-
console.log(
|
|
238
|
+
console.log(
|
|
239
|
+
`step ${step}: ${info.status} (attempts ${info.attempts})${suffix}`,
|
|
240
|
+
);
|
|
205
241
|
}
|
|
206
242
|
const live = liveness.inFlight;
|
|
207
243
|
if (live) {
|
|
208
|
-
const typical =
|
|
244
|
+
const typical =
|
|
245
|
+
live.typicalMs !== null
|
|
246
|
+
? `, typical ${formatMs(live.typicalMs)} n=${live.samples}`
|
|
247
|
+
: "";
|
|
209
248
|
console.log(
|
|
210
249
|
`in flight: ${live.step} attempt ${live.attempt} since ${live.startedAt} (elapsed ${formatMs(live.elapsedMs)}${typical})`,
|
|
211
250
|
);
|
|
@@ -213,9 +252,13 @@ function printState(state, ledger, view = {}) {
|
|
|
213
252
|
console.log(` worker: ${live.command}`);
|
|
214
253
|
}
|
|
215
254
|
if (live.lastOutputAt) {
|
|
216
|
-
console.log(
|
|
255
|
+
console.log(
|
|
256
|
+
` last output: ${live.lastOutputAt} (${formatMs(live.sinceOutputMs)} ago, ${live.bytes} bytes so far)`,
|
|
257
|
+
);
|
|
217
258
|
} else {
|
|
218
|
-
console.log(
|
|
259
|
+
console.log(
|
|
260
|
+
` last output: (none yet, ${formatMs(live.sinceOutputMs)} since start)`,
|
|
261
|
+
);
|
|
219
262
|
}
|
|
220
263
|
if (live.stalled) {
|
|
221
264
|
console.log(
|
|
@@ -228,9 +271,15 @@ function printState(state, ledger, view = {}) {
|
|
|
228
271
|
}
|
|
229
272
|
}
|
|
230
273
|
for (const item of state.evidence) {
|
|
231
|
-
const ref = isAbsolute(item.ref)
|
|
274
|
+
const ref = isAbsolute(item.ref)
|
|
275
|
+
? "<legacy-absolute-evidence-ref>"
|
|
276
|
+
: item.ref;
|
|
232
277
|
const reused = item.reused ? ` (reused from ${item.reused.run})` : "";
|
|
233
|
-
|
|
278
|
+
// A screenshot is judged by the file its digest names.
|
|
279
|
+
const digest = item.kind === "screenshot" ? ` ${item.digest}` : "";
|
|
280
|
+
console.log(
|
|
281
|
+
`evidence [${item.status}/${item.grade}] ${item.kind} ${ref}${digest}${reused}`,
|
|
282
|
+
);
|
|
234
283
|
}
|
|
235
284
|
if (state.pendingHuman) {
|
|
236
285
|
console.log(`waiting on human: ${state.pendingHuman.transition}`);
|
|
@@ -242,7 +291,9 @@ function printState(state, ledger, view = {}) {
|
|
|
242
291
|
}
|
|
243
292
|
}
|
|
244
293
|
if (state.terminal) {
|
|
245
|
-
console.log(
|
|
294
|
+
console.log(
|
|
295
|
+
`terminal: ${state.terminal.status} (${state.terminal.reason})`,
|
|
296
|
+
);
|
|
246
297
|
}
|
|
247
298
|
if (state.compacted) {
|
|
248
299
|
console.log(`compacted: ${state.compacted.runRecordRef}`);
|
|
@@ -254,7 +305,9 @@ function workerEnvOverrides(worker, env) {
|
|
|
254
305
|
return {};
|
|
255
306
|
}
|
|
256
307
|
if (typeof env !== "object" || Array.isArray(env)) {
|
|
257
|
-
throw new Error(
|
|
308
|
+
throw new Error(
|
|
309
|
+
`workers.${worker}.env must be a mapping of variable names to scalar values`,
|
|
310
|
+
);
|
|
258
311
|
}
|
|
259
312
|
const out = {};
|
|
260
313
|
for (const [key, value] of Object.entries(env)) {
|
|
@@ -288,9 +341,15 @@ function loadRunConfig(flags, command) {
|
|
|
288
341
|
workflowText = readFileSync(workflowPath, "utf8");
|
|
289
342
|
workflow = loadWorkflow(workflowPath);
|
|
290
343
|
} catch (error) {
|
|
291
|
-
problems.push(
|
|
344
|
+
problems.push(
|
|
345
|
+
`workflow: cannot load ${config.workflow}: ${error.message}`,
|
|
346
|
+
);
|
|
292
347
|
}
|
|
293
348
|
}
|
|
349
|
+
if (config?.workflow === undefined) {
|
|
350
|
+
workflow = deliveryWorkflow();
|
|
351
|
+
workflowPath = null;
|
|
352
|
+
}
|
|
294
353
|
if (workflow) {
|
|
295
354
|
problems.push(...checkRunConfigAgainstWorkflow(config, workflow));
|
|
296
355
|
}
|
|
@@ -298,6 +357,7 @@ function loadRunConfig(flags, command) {
|
|
|
298
357
|
throw new RunConfigError(flags.config, problems);
|
|
299
358
|
}
|
|
300
359
|
const repoRoot = resolve(configDir, config.repo);
|
|
360
|
+
workflowText ??= DELIVERY_TEXT;
|
|
301
361
|
const workflowDigest = `sha256:${createHash("sha256").update(workflowText, "utf8").digest("hex")}`;
|
|
302
362
|
|
|
303
363
|
const adapters = {};
|
|
@@ -323,45 +383,89 @@ function loadRunConfig(flags, command) {
|
|
|
323
383
|
return `sha256:${createHash("sha256").update(readFileSync(filePath, "utf8"), "utf8").digest("hex")}`;
|
|
324
384
|
};
|
|
325
385
|
|
|
326
|
-
const
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
riskPreset
|
|
334
|
-
}
|
|
335
|
-
|
|
336
|
-
|
|
386
|
+
const workDir = join(repoRoot, "delivery", "work", config.work);
|
|
387
|
+
const artifact =
|
|
388
|
+
existsSync(join(workDir, "work.md")) || !config.workflow ? "work" : "plan";
|
|
389
|
+
// 3.3.1 ran a workflow config without riskPreset with no artifact gate;
|
|
390
|
+
// keep that (the merge evidence floor is now always on). New configs
|
|
391
|
+
// (no workflow) default to standard.
|
|
392
|
+
const riskPreset =
|
|
393
|
+
config.riskPreset ?? (config.workflow === undefined ? "standard" : "fast");
|
|
394
|
+
const preset = loadRiskPreset(riskPreset, { artifact });
|
|
395
|
+
const safeguards = { ...preset.checks };
|
|
396
|
+
if (config.maxReviewSeverity) {
|
|
397
|
+
if (safeguards.maxSeverity === "P3" && config.maxReviewSeverity === "P2") {
|
|
398
|
+
throw new Error(
|
|
399
|
+
"maxReviewSeverity cannot weaken the legacy controlled safeguards",
|
|
400
|
+
);
|
|
401
|
+
}
|
|
402
|
+
safeguards.maxSeverity = config.maxReviewSeverity;
|
|
403
|
+
}
|
|
404
|
+
// Frozen with the run like every other safeguard; absent unless enabled,
|
|
405
|
+
// so runs created before the switch existed compare equal.
|
|
406
|
+
if (config.requireScreenshot === true) {
|
|
407
|
+
safeguards.requireScreenshot = true;
|
|
408
|
+
}
|
|
409
|
+
const policies = deliveryChecks(safeguards);
|
|
410
|
+
const presetStopAt = [];
|
|
411
|
+
const entry = config.entry ?? workflow.entry;
|
|
412
|
+
// Legacy planner steps would write the artifact inside the run, but an
|
|
413
|
+
// accepted artifact is bound (and checked in base) before the run starts.
|
|
414
|
+
if (
|
|
415
|
+
safeguards.requireAcceptance &&
|
|
416
|
+
["intent", "spec", "plan"].includes(entry)
|
|
417
|
+
) {
|
|
418
|
+
throw new Error(
|
|
419
|
+
`entry ${entry} starts at the retired planner steps, but risk preset ${riskPreset} binds an accepted ${artifact}.md before the run starts; write, accept and commit ${artifact}.md, then set entry: build (docs/MIGRATION.md)`,
|
|
420
|
+
);
|
|
337
421
|
}
|
|
338
422
|
|
|
339
|
-
if (
|
|
340
|
-
|
|
423
|
+
if (
|
|
424
|
+
config.reviewTriage !== undefined &&
|
|
425
|
+
!["required", "off"].includes(config.reviewTriage)
|
|
426
|
+
) {
|
|
427
|
+
throw new Error(
|
|
428
|
+
`reviewTriage must be "required" or "off", got: ${config.reviewTriage}`,
|
|
429
|
+
);
|
|
341
430
|
}
|
|
342
|
-
if (
|
|
343
|
-
|
|
431
|
+
if (
|
|
432
|
+
config.supersede !== undefined &&
|
|
433
|
+
!["waiting", "off"].includes(config.supersede)
|
|
434
|
+
) {
|
|
435
|
+
throw new Error(
|
|
436
|
+
`supersede must be "waiting" or "off", got: ${config.supersede}`,
|
|
437
|
+
);
|
|
344
438
|
}
|
|
345
439
|
if (config.stallAfterMs !== undefined && !(Number(config.stallAfterMs) > 0)) {
|
|
346
|
-
throw new Error(
|
|
440
|
+
throw new Error(
|
|
441
|
+
`stallAfterMs must be a positive number, got: ${config.stallAfterMs}`,
|
|
442
|
+
);
|
|
347
443
|
}
|
|
348
444
|
// budgets: run config beats the preset (a preset cap that could not be
|
|
349
445
|
// raised per run once ended a pilot's shipped candidates as CANCELLED
|
|
350
446
|
// runs).
|
|
351
447
|
const budgets = {};
|
|
352
448
|
if (config.budgets !== undefined) {
|
|
353
|
-
if (
|
|
449
|
+
if (
|
|
450
|
+
!config.budgets ||
|
|
451
|
+
typeof config.budgets !== "object" ||
|
|
452
|
+
Array.isArray(config.budgets)
|
|
453
|
+
) {
|
|
354
454
|
throw new Error("budgets must be a map");
|
|
355
455
|
}
|
|
356
456
|
for (const key of Object.keys(config.budgets)) {
|
|
357
457
|
if (!["maxAttempts", "reviewRoundsPerWork"].includes(key)) {
|
|
358
|
-
throw new Error(
|
|
458
|
+
throw new Error(
|
|
459
|
+
`unknown budgets key: ${key} (known: maxAttempts, reviewRoundsPerWork)`,
|
|
460
|
+
);
|
|
359
461
|
}
|
|
360
462
|
}
|
|
361
463
|
if (config.budgets.maxAttempts !== undefined) {
|
|
362
464
|
const map = config.budgets.maxAttempts;
|
|
363
465
|
if (!map || typeof map !== "object" || Array.isArray(map)) {
|
|
364
|
-
throw new Error(
|
|
466
|
+
throw new Error(
|
|
467
|
+
"budgets.maxAttempts must be a map of step -> positive integer",
|
|
468
|
+
);
|
|
365
469
|
}
|
|
366
470
|
budgets.maxAttempts = {};
|
|
367
471
|
for (const [step, value] of Object.entries(map)) {
|
|
@@ -369,7 +473,9 @@ function loadRunConfig(flags, command) {
|
|
|
369
473
|
throw new Error(`budgets.maxAttempts.${step}: step not in workflow`);
|
|
370
474
|
}
|
|
371
475
|
if (!Number.isInteger(Number(value)) || Number(value) < 1) {
|
|
372
|
-
throw new Error(
|
|
476
|
+
throw new Error(
|
|
477
|
+
`budgets.maxAttempts.${step} must be a positive integer, got: ${value}`,
|
|
478
|
+
);
|
|
373
479
|
}
|
|
374
480
|
budgets.maxAttempts[step] = Number(value);
|
|
375
481
|
}
|
|
@@ -377,7 +483,9 @@ function loadRunConfig(flags, command) {
|
|
|
377
483
|
if (config.budgets.reviewRoundsPerWork !== undefined) {
|
|
378
484
|
const value = Number(config.budgets.reviewRoundsPerWork);
|
|
379
485
|
if (!Number.isInteger(value) || value < 1) {
|
|
380
|
-
throw new Error(
|
|
486
|
+
throw new Error(
|
|
487
|
+
`budgets.reviewRoundsPerWork must be a positive integer, got: ${config.budgets.reviewRoundsPerWork}`,
|
|
488
|
+
);
|
|
381
489
|
}
|
|
382
490
|
budgets.reviewRoundsPerWork = value;
|
|
383
491
|
}
|
|
@@ -396,10 +504,16 @@ function loadRunConfig(flags, command) {
|
|
|
396
504
|
try {
|
|
397
505
|
return new RegExp(String(pattern), "g");
|
|
398
506
|
} catch {
|
|
399
|
-
throw new Error(
|
|
507
|
+
throw new Error(
|
|
508
|
+
`redact pattern is not a valid regular expression: ${pattern}`,
|
|
509
|
+
);
|
|
400
510
|
}
|
|
401
511
|
});
|
|
402
|
-
const envelope = loadEnvelope(
|
|
512
|
+
const envelope = loadEnvelope(
|
|
513
|
+
config,
|
|
514
|
+
configDir,
|
|
515
|
+
Object.keys(config.workers ?? {}),
|
|
516
|
+
);
|
|
403
517
|
|
|
404
518
|
return {
|
|
405
519
|
envelope,
|
|
@@ -411,11 +525,19 @@ function loadRunConfig(flags, command) {
|
|
|
411
525
|
workId: config.work,
|
|
412
526
|
runId: config.run,
|
|
413
527
|
base: config.base ?? "HEAD",
|
|
414
|
-
entry
|
|
528
|
+
entry,
|
|
415
529
|
stopAt: config.stopAt ?? presetStopAt ?? [],
|
|
416
530
|
adapters,
|
|
417
531
|
adapterConfigs: config.workers ?? {},
|
|
532
|
+
release: config.release
|
|
533
|
+
? {
|
|
534
|
+
...config.release,
|
|
535
|
+
env: workerEnvOverrides("release", config.release.env),
|
|
536
|
+
}
|
|
537
|
+
: null,
|
|
418
538
|
policies,
|
|
539
|
+
deliveryChecks: safeguards,
|
|
540
|
+
workArtifact: artifact,
|
|
419
541
|
riskPreset,
|
|
420
542
|
maxAttemptsPerStep: config.maxAttemptsPerStep ?? 4,
|
|
421
543
|
budgets,
|
|
@@ -425,8 +547,11 @@ function loadRunConfig(flags, command) {
|
|
|
425
547
|
reviewTriage: config.reviewTriage === "required" ? "required" : null,
|
|
426
548
|
supersede: config.supersede ?? "waiting",
|
|
427
549
|
parallel: config.parallel === true,
|
|
428
|
-
stallAfterMs:
|
|
429
|
-
|
|
550
|
+
stallAfterMs:
|
|
551
|
+
config.stallAfterMs !== undefined
|
|
552
|
+
? Number(config.stallAfterMs)
|
|
553
|
+
: DEFAULT_STALL_AFTER_MS,
|
|
554
|
+
planDigest: digestOfWorkFile(`${artifact}.md`),
|
|
430
555
|
intentDigest: digestOfWorkFile("intent.md"),
|
|
431
556
|
};
|
|
432
557
|
}
|
|
@@ -461,9 +586,17 @@ async function notifyForState(repoRoot, repoLabel, state) {
|
|
|
461
586
|
if (!subscribes(config, kind)) {
|
|
462
587
|
continue;
|
|
463
588
|
}
|
|
464
|
-
const results = await dispatchNotification(
|
|
589
|
+
const results = await dispatchNotification(
|
|
590
|
+
config,
|
|
591
|
+
buildNotification(kind, { repoLabel, state }),
|
|
592
|
+
{ repoRoot },
|
|
593
|
+
);
|
|
465
594
|
for (const row of results) {
|
|
466
|
-
const outcome = row.ok
|
|
595
|
+
const outcome = row.ok
|
|
596
|
+
? "sent"
|
|
597
|
+
: row.skipped
|
|
598
|
+
? `skipped (${row.error})`
|
|
599
|
+
: `FAILED (${row.error})`;
|
|
467
600
|
console.log(`notify ${kind} -> ${row.channel}: ${outcome}`);
|
|
468
601
|
}
|
|
469
602
|
}
|
|
@@ -498,29 +631,108 @@ function spawnStallWatcher(repoRoot, runId, stallAfterMs) {
|
|
|
498
631
|
return true;
|
|
499
632
|
}
|
|
500
633
|
|
|
501
|
-
|
|
502
|
-
|
|
634
|
+
const textDigest = (text) =>
|
|
635
|
+
`sha256:${createHash("sha256").update(text, "utf8").digest("hex")}`;
|
|
636
|
+
|
|
637
|
+
async function commandStart(flags, options = loadRunConfig(flags, "start")) {
|
|
638
|
+
// Pin the base first: the artifacts are checked in, and the workspace is
|
|
639
|
+
// created from, the same commit even if the ref moves meanwhile.
|
|
640
|
+
try {
|
|
641
|
+
options.base = execFileSync(
|
|
642
|
+
"git",
|
|
643
|
+
[
|
|
644
|
+
"-C",
|
|
645
|
+
options.repoRoot,
|
|
646
|
+
"rev-parse",
|
|
647
|
+
"--verify",
|
|
648
|
+
`${options.base}^{commit}`,
|
|
649
|
+
],
|
|
650
|
+
{ encoding: "utf8", stdio: ["ignore", "pipe", "pipe"] },
|
|
651
|
+
).trim();
|
|
652
|
+
} catch {
|
|
653
|
+
throw new Error(`base ${options.base} does not name a commit`);
|
|
654
|
+
}
|
|
655
|
+
const creation = {
|
|
656
|
+
deliveryChecks: options.deliveryChecks,
|
|
657
|
+
intentDigest: options.intentDigest ?? "UNVERIFIED",
|
|
658
|
+
};
|
|
659
|
+
const committed = boundArtifacts(
|
|
660
|
+
creation,
|
|
661
|
+
options.planDigest ?? "UNVERIFIED",
|
|
662
|
+
).filter((entry) => entry.committed);
|
|
663
|
+
const mismatch = (ref, text, digest) => {
|
|
664
|
+
if (text === null) {
|
|
665
|
+
throw new Error(
|
|
666
|
+
`commit ${ref} into the selected base before starting; workers read that isolated checkout`,
|
|
667
|
+
);
|
|
668
|
+
}
|
|
669
|
+
if (textDigest(text) !== digest) {
|
|
670
|
+
throw new Error(
|
|
671
|
+
`the selected base contains a different ${ref}; commit the accepted work artifact before starting`,
|
|
672
|
+
);
|
|
673
|
+
}
|
|
674
|
+
};
|
|
675
|
+
// Presence is checked before anything is created. Content is compared on
|
|
676
|
+
// the isolated checkout itself: Git applies that checkout's attributes and
|
|
677
|
+
// filters there, which no pre-read reproduces on every Git version.
|
|
678
|
+
for (const { artifact } of committed) {
|
|
679
|
+
const ref = `delivery/work/${options.workId}/${artifact}.md`;
|
|
680
|
+
try {
|
|
681
|
+
execFileSync(
|
|
682
|
+
"git",
|
|
683
|
+
["-C", options.repoRoot, "cat-file", "-e", `${options.base}:${ref}`],
|
|
684
|
+
{ stdio: "ignore" },
|
|
685
|
+
);
|
|
686
|
+
} catch {
|
|
687
|
+
mismatch(ref, null);
|
|
688
|
+
}
|
|
689
|
+
}
|
|
690
|
+
options.verifyCheckout = (worktreePath) => {
|
|
691
|
+
for (const { artifact, digest } of committed) {
|
|
692
|
+
const ref = `delivery/work/${options.workId}/${artifact}.md`;
|
|
693
|
+
const file = join(worktreePath, ref);
|
|
694
|
+
const text = existsSync(file) ? readFileSync(file, "utf8") : null;
|
|
695
|
+
mismatch(ref, text, digest);
|
|
696
|
+
}
|
|
697
|
+
};
|
|
698
|
+
|
|
503
699
|
if (flags.attempt !== undefined) {
|
|
504
700
|
if (flags.attempt !== "new") {
|
|
505
|
-
throw new Error(
|
|
701
|
+
throw new Error(
|
|
702
|
+
`--attempt must be "new" (auto-number the next run of this family), got: ${flags.attempt}`,
|
|
703
|
+
);
|
|
506
704
|
}
|
|
507
705
|
// The run config names the family; the kernel numbers the attempt. One
|
|
508
706
|
// config per work, not one per retry (the campaign hand-numbered -01..-30).
|
|
509
|
-
options.runId = nextAttemptId(
|
|
707
|
+
options.runId = nextAttemptId(
|
|
708
|
+
options.repoRoot,
|
|
709
|
+
options.workId,
|
|
710
|
+
options.runId,
|
|
711
|
+
);
|
|
510
712
|
console.log(`attempt: ${options.runId}`);
|
|
511
713
|
}
|
|
512
714
|
if (options.envelope) {
|
|
513
|
-
console.log(
|
|
715
|
+
console.log(
|
|
716
|
+
`envelope: ${options.envelope.source} (${Object.keys(options.envelope.prompts).join(", ")}) ${options.envelope.digest}`,
|
|
717
|
+
);
|
|
514
718
|
}
|
|
515
719
|
if (process.stdout.isTTY) {
|
|
516
720
|
// Run launch discipline (real incident: a host-tool timeout killed a
|
|
517
721
|
// verify worker mid-run): anything longer than minutes belongs in a
|
|
518
722
|
// detached process, not an interactive foreground shell.
|
|
519
|
-
console.log(
|
|
723
|
+
console.log(
|
|
724
|
+
"tip: long runs should be started detached (nohup/setsid); interactive shells die with their host",
|
|
725
|
+
);
|
|
520
726
|
}
|
|
521
|
-
const watching = spawnStallWatcher(
|
|
727
|
+
const watching = spawnStallWatcher(
|
|
728
|
+
options.repoRoot,
|
|
729
|
+
options.runId,
|
|
730
|
+
options.stallAfterMs,
|
|
731
|
+
);
|
|
522
732
|
if (watching) {
|
|
523
|
-
console.log(
|
|
733
|
+
console.log(
|
|
734
|
+
`stall watcher armed (no output for ${formatMs(options.stallAfterMs)} notifies STALLED)`,
|
|
735
|
+
);
|
|
524
736
|
}
|
|
525
737
|
let result;
|
|
526
738
|
try {
|
|
@@ -535,28 +747,45 @@ async function commandStart(flags) {
|
|
|
535
747
|
if (holders.length === 0) {
|
|
536
748
|
// A dead owner would already have been reclaimed; the error below
|
|
537
749
|
// names who holds the lock and what to do.
|
|
538
|
-
console.error(
|
|
750
|
+
console.error(
|
|
751
|
+
"blocked by: the active-run lock alone (no run lock beside it); its owner is named below",
|
|
752
|
+
);
|
|
539
753
|
}
|
|
540
754
|
for (const holder of holders) {
|
|
541
|
-
const ledgerPath = join(
|
|
755
|
+
const ledgerPath = join(
|
|
756
|
+
options.repoRoot,
|
|
757
|
+
".buildbeat",
|
|
758
|
+
"runtime",
|
|
759
|
+
"runs",
|
|
760
|
+
holder,
|
|
761
|
+
"events.jsonl",
|
|
762
|
+
);
|
|
542
763
|
let summary = "(no ledger found)";
|
|
543
764
|
if (existsSync(ledgerPath)) {
|
|
544
765
|
const ledger = EventLedger.open(ledgerPath);
|
|
545
766
|
const state = ledger.state;
|
|
546
|
-
const step = state.currentStep
|
|
767
|
+
const step = state.currentStep
|
|
768
|
+
? `step ${state.currentStep} attempt ${state.steps[state.currentStep]?.attempts ?? "?"}`
|
|
769
|
+
: "between steps";
|
|
547
770
|
const since = ledger.events[ledger.events.length - 1]?.ts;
|
|
548
771
|
summary = `${state.run?.work ?? "?"} ${state.run?.status ?? "?"} ${step}${since ? `, last event ${formatMs(Date.now() - Date.parse(since))} ago` : ""}`;
|
|
549
772
|
}
|
|
550
773
|
console.error(`blocked by ${holder}: ${summary}`);
|
|
551
|
-
console.error(
|
|
774
|
+
console.error(
|
|
775
|
+
` watch it: buildbeat status --repo ${label} --run ${holder}`,
|
|
776
|
+
);
|
|
552
777
|
}
|
|
553
|
-
console.error(
|
|
778
|
+
console.error(
|
|
779
|
+
"queue position: next after the holder(s) above stop or wait on a human (by default one run drives a repository at a time; works whose run configs both set parallel: true can drive together)",
|
|
780
|
+
);
|
|
554
781
|
}
|
|
555
782
|
throw error;
|
|
556
783
|
}
|
|
557
784
|
const repoLabel = repoLabelFor(options.repoRoot);
|
|
558
785
|
for (const run of result.superseded ?? []) {
|
|
559
|
-
console.log(
|
|
786
|
+
console.log(
|
|
787
|
+
`superseded ${run} (was waiting on a human for the same work; now SUPERSEDED)`,
|
|
788
|
+
);
|
|
560
789
|
}
|
|
561
790
|
for (const row of result.supersedeSkipped ?? []) {
|
|
562
791
|
console.log(`could not supersede ${row.run}: ${row.reason}`);
|
|
@@ -571,27 +800,52 @@ async function commandStart(flags) {
|
|
|
571
800
|
await notifyForState(options.repoRoot, repoLabel, ledger.state);
|
|
572
801
|
}
|
|
573
802
|
|
|
803
|
+
// A run family is the configured run id plus its numbered attempts
|
|
804
|
+
// (<run>-01, <run>-02, ...); a longer id that merely shares the prefix
|
|
805
|
+
// belongs to another family.
|
|
806
|
+
function inRunFamily(family, id) {
|
|
807
|
+
const escaped = family.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
|
|
808
|
+
return id === family || new RegExp(`^${escaped}-\\d{2,}$`).test(id);
|
|
809
|
+
}
|
|
810
|
+
|
|
574
811
|
// Resolve only from runtime ledgers. Reading candidates does not acquire
|
|
575
|
-
// their locks; resumeRun still owns the lock and freshness checks.
|
|
576
|
-
|
|
577
|
-
|
|
812
|
+
// their locks; resumeRun still owns the lock and freshness checks. The
|
|
813
|
+
// legacy resume spelling keeps 3.3.1's exact-id precedence; `run` picks the
|
|
814
|
+
// unique unfinished run across the whole family (exact id included), and
|
|
815
|
+
// with none unfinished reports the latest one so the user sees --new.
|
|
816
|
+
function resolveResumeRun(
|
|
817
|
+
repoRoot,
|
|
818
|
+
family,
|
|
819
|
+
explicitRun,
|
|
820
|
+
{ familyWide = false } = {},
|
|
821
|
+
) {
|
|
578
822
|
if (explicitRun !== undefined) {
|
|
579
|
-
if (
|
|
580
|
-
throw new Error(
|
|
823
|
+
if (!inRunFamily(family, explicitRun)) {
|
|
824
|
+
throw new Error(
|
|
825
|
+
`--run ${explicitRun} is not in run family ${family} of this config`,
|
|
826
|
+
);
|
|
581
827
|
}
|
|
582
828
|
if (!existsSync(ledgerPathFor(repoRoot, explicitRun))) {
|
|
583
829
|
throw new Error(`no ledger for run ${explicitRun}`);
|
|
584
830
|
}
|
|
585
831
|
return explicitRun;
|
|
586
832
|
}
|
|
587
|
-
if (existsSync(ledgerPathFor(repoRoot, family))) {
|
|
833
|
+
if (!familyWide && existsSync(ledgerPathFor(repoRoot, family))) {
|
|
588
834
|
return family;
|
|
589
835
|
}
|
|
590
836
|
const runsDir = join(repoRoot, ".buildbeat", "runtime", "runs");
|
|
591
837
|
const runs = (existsSync(runsDir) ? readdirSync(runsDir) : [])
|
|
592
|
-
.filter(
|
|
838
|
+
.filter(
|
|
839
|
+
(id) =>
|
|
840
|
+
(familyWide || id !== family) &&
|
|
841
|
+
inRunFamily(family, id) &&
|
|
842
|
+
existsSync(ledgerPathFor(repoRoot, id)),
|
|
843
|
+
)
|
|
593
844
|
.sort((a, b) => a.localeCompare(b, "en", { numeric: true }))
|
|
594
|
-
.map((id) => ({
|
|
845
|
+
.map((id) => ({
|
|
846
|
+
id,
|
|
847
|
+
state: EventLedger.open(ledgerPathFor(repoRoot, id)).state,
|
|
848
|
+
}));
|
|
595
849
|
const open = runs.filter(({ state }) => !state.terminal);
|
|
596
850
|
if (open.length === 1) {
|
|
597
851
|
console.log(`resuming ${open[0].id} (the open run of family ${family})`);
|
|
@@ -599,14 +853,60 @@ function resolveResumeRun(repoRoot, family, explicitRun) {
|
|
|
599
853
|
}
|
|
600
854
|
if (open.length === 0) {
|
|
601
855
|
const latest = runs.at(-1);
|
|
602
|
-
|
|
856
|
+
if (familyWide && latest) {
|
|
857
|
+
return latest.id;
|
|
858
|
+
}
|
|
859
|
+
throw new Error(
|
|
860
|
+
`no open run in family ${family}; ${latest ? `latest run ${latest.id}: ${latest.state.terminal.status}` : "no ledgers found"}; use --run <RUN-ID> to select an existing run explicitly, or run --new to start another attempt`,
|
|
861
|
+
);
|
|
603
862
|
}
|
|
604
|
-
throw new Error(
|
|
863
|
+
throw new Error(
|
|
864
|
+
`multiple open runs in family ${family}: ${open.map(({ id }) => id).join(", ")}; use --run <RUN-ID> to select one`,
|
|
865
|
+
);
|
|
605
866
|
}
|
|
606
867
|
|
|
607
|
-
async function commandResume(
|
|
608
|
-
|
|
609
|
-
options
|
|
868
|
+
async function commandResume(
|
|
869
|
+
flags,
|
|
870
|
+
options = loadRunConfig(flags, "resume"),
|
|
871
|
+
{ familyWide = false } = {},
|
|
872
|
+
) {
|
|
873
|
+
options.runId = resolveResumeRun(options.repoRoot, options.runId, flags.run, {
|
|
874
|
+
familyWide,
|
|
875
|
+
});
|
|
876
|
+
const prior = EventLedger.open(
|
|
877
|
+
ledgerPathFor(options.repoRoot, options.runId),
|
|
878
|
+
);
|
|
879
|
+
if (
|
|
880
|
+
prior.state.run &&
|
|
881
|
+
!prior.state.terminal &&
|
|
882
|
+
!prior.state.run.deliveryChecks
|
|
883
|
+
) {
|
|
884
|
+
throw new Error(
|
|
885
|
+
"legacy active run: finish or cancel with its 3.3.1 runtime; new attempts can use this version (docs/MIGRATION.md)",
|
|
886
|
+
);
|
|
887
|
+
}
|
|
888
|
+
// A finished run only reports its outcome: a config migrated since it ran
|
|
889
|
+
// must not turn "already terminal" into a configuration error.
|
|
890
|
+
if (!prior.state.terminal) {
|
|
891
|
+
assertRunConfiguration(prior.state, options);
|
|
892
|
+
}
|
|
893
|
+
// A frozen run continues only under the artifacts it was created with;
|
|
894
|
+
// its final approval would refuse a changed scope anyway.
|
|
895
|
+
if (!prior.state.terminal && prior.state.run?.deliveryChecks) {
|
|
896
|
+
const now = {
|
|
897
|
+
[prior.state.run.deliveryChecks.artifact]:
|
|
898
|
+
options.planDigest ?? "UNVERIFIED",
|
|
899
|
+
intent: options.intentDigest ?? "UNVERIFIED",
|
|
900
|
+
};
|
|
901
|
+
const changed = boundArtifacts(prior.state.run)
|
|
902
|
+
.filter(({ artifact, digest }) => now[artifact] !== digest)
|
|
903
|
+
.map(({ artifact }) => `${artifact}.md`);
|
|
904
|
+
if (changed.length > 0) {
|
|
905
|
+
throw new Error(
|
|
906
|
+
`work artifact changed since the run was created (${changed.join(", ")}); cannot ${flags.adopt !== undefined ? "adopt a candidate" : "resume"} under a different scope; accept it and start a new attempt with run --new`,
|
|
907
|
+
);
|
|
908
|
+
}
|
|
909
|
+
}
|
|
610
910
|
if (flags.adopt !== undefined) {
|
|
611
911
|
const resumeAt = nextStep(options.workflow, "fix", "succeeded") ?? "verify";
|
|
612
912
|
const adopted = adoptCandidate(options.repoRoot, options.runId, {
|
|
@@ -614,16 +914,29 @@ async function commandResume(flags) {
|
|
|
614
914
|
by: flags.by ?? "human",
|
|
615
915
|
resumeAt,
|
|
616
916
|
});
|
|
617
|
-
console.log(
|
|
917
|
+
console.log(
|
|
918
|
+
`adopted ${adopted.adopted} as candidate (${adopted.decisionRef}, answers ${adopted.transition}); resuming at ${adopted.resumeAt}`,
|
|
919
|
+
);
|
|
618
920
|
}
|
|
619
|
-
const watching = spawnStallWatcher(
|
|
921
|
+
const watching = spawnStallWatcher(
|
|
922
|
+
options.repoRoot,
|
|
923
|
+
options.runId,
|
|
924
|
+
options.stallAfterMs,
|
|
925
|
+
);
|
|
620
926
|
if (watching) {
|
|
621
|
-
console.log(
|
|
927
|
+
console.log(
|
|
928
|
+
`stall watcher armed (no output for ${formatMs(options.stallAfterMs)} notifies STALLED)`,
|
|
929
|
+
);
|
|
622
930
|
}
|
|
623
931
|
const result = resumeRun(options);
|
|
624
932
|
const repoLabel = repoLabelFor(options.repoRoot);
|
|
625
933
|
if (!result.resumed) {
|
|
626
934
|
console.log(`nothing to resume: ${result.reason}`);
|
|
935
|
+
if (result.state?.terminal) {
|
|
936
|
+
console.log(
|
|
937
|
+
"start another attempt with: buildbeat run --config <run-config.yaml> --new",
|
|
938
|
+
);
|
|
939
|
+
}
|
|
627
940
|
}
|
|
628
941
|
console.log(`ledger: ${toRepoRef(options.repoRoot, result.ledgerPath)}`);
|
|
629
942
|
const ledger = EventLedger.open(result.ledgerPath);
|
|
@@ -637,21 +950,31 @@ async function commandResume(flags) {
|
|
|
637
950
|
}
|
|
638
951
|
}
|
|
639
952
|
|
|
640
|
-
function
|
|
953
|
+
function pendingFor(repoRoot, work) {
|
|
954
|
+
return listInbox(repoRoot).filter(
|
|
955
|
+
(row) => !work || row.work === work || row.corrupted,
|
|
956
|
+
);
|
|
957
|
+
}
|
|
958
|
+
|
|
959
|
+
function commandInbox(flags, rows = null) {
|
|
641
960
|
if (!flags.repo) {
|
|
642
961
|
throw new Error("inbox requires --repo");
|
|
643
962
|
}
|
|
644
963
|
const repoRoot = resolve(flags.repo);
|
|
645
|
-
|
|
964
|
+
rows ??= pendingFor(repoRoot, flags.work);
|
|
646
965
|
if (rows.length === 0) {
|
|
647
966
|
console.log("inbox empty: no runs waiting on a human");
|
|
648
967
|
return;
|
|
649
968
|
}
|
|
650
|
-
const sorted = [...rows].sort((a, b) =>
|
|
969
|
+
const sorted = [...rows].sort((a, b) =>
|
|
970
|
+
`${a.work ?? ""}${a.run}`.localeCompare(`${b.work ?? ""}${b.run}`),
|
|
971
|
+
);
|
|
651
972
|
let lastWork = null;
|
|
652
973
|
for (const row of sorted) {
|
|
653
974
|
if (row.corrupted) {
|
|
654
|
-
console.log(
|
|
975
|
+
console.log(
|
|
976
|
+
`${row.run}: LEDGER CORRUPTED after seq=${row.corrupted.afterSeq} (${row.corrupted.reason})`,
|
|
977
|
+
);
|
|
655
978
|
continue;
|
|
656
979
|
}
|
|
657
980
|
if (row.work !== lastWork) {
|
|
@@ -659,16 +982,31 @@ function commandInbox(flags) {
|
|
|
659
982
|
lastWork = row.work;
|
|
660
983
|
}
|
|
661
984
|
const ledger = EventLedger.open(ledgerPathFor(repoRoot, row.run));
|
|
662
|
-
const requested = [...ledger.events]
|
|
663
|
-
|
|
664
|
-
|
|
985
|
+
const requested = [...ledger.events]
|
|
986
|
+
.reverse()
|
|
987
|
+
.find((event) => event.type === "HUMAN_REQUESTED");
|
|
988
|
+
const age = requested
|
|
989
|
+
? formatMs(Date.now() - Date.parse(requested.ts))
|
|
990
|
+
: "?";
|
|
991
|
+
console.log(
|
|
992
|
+
` ${row.run} [${row.kind}] ${row.transition} — waiting ${age}${requested ? ` (since ${requested.ts})` : ""}`,
|
|
993
|
+
);
|
|
665
994
|
console.log(` candidate: ${row.subject.candidate}`);
|
|
666
995
|
console.log(` planDigest: ${row.subject.planDigest}`);
|
|
667
996
|
console.log(` evidenceDigest: ${row.subject.evidenceDigest}`);
|
|
997
|
+
for (const shot of candidateScreenshots(
|
|
998
|
+
ledger.state,
|
|
999
|
+
row.subject.candidate,
|
|
1000
|
+
)) {
|
|
1001
|
+
console.log(` screenshot: ${shot.ref} ${shot.digest}`);
|
|
1002
|
+
}
|
|
668
1003
|
for (const reason of row.reasons) {
|
|
669
1004
|
console.log(` reason: ${reason}`);
|
|
670
1005
|
}
|
|
671
|
-
for (const line of nextReply({
|
|
1006
|
+
for (const line of nextReply({
|
|
1007
|
+
repoLabel: repoLabelFor(repoRoot, flags.repo),
|
|
1008
|
+
state: ledger.state,
|
|
1009
|
+
})) {
|
|
672
1010
|
console.log(` next: ${line}`);
|
|
673
1011
|
}
|
|
674
1012
|
}
|
|
@@ -678,6 +1016,16 @@ function commandApprove(flags) {
|
|
|
678
1016
|
if (!flags.repo || !flags.run || !flags.transition) {
|
|
679
1017
|
throw new Error("approve requires --repo, --run and --transition");
|
|
680
1018
|
}
|
|
1019
|
+
const prior = EventLedger.open(ledgerPathFor(resolve(flags.repo), flags.run));
|
|
1020
|
+
if (
|
|
1021
|
+
prior.state.run &&
|
|
1022
|
+
!prior.state.terminal &&
|
|
1023
|
+
!prior.state.run.deliveryChecks
|
|
1024
|
+
) {
|
|
1025
|
+
throw new Error(
|
|
1026
|
+
"legacy active run: approve with its original 3.3.1 runtime (docs/MIGRATION.md)",
|
|
1027
|
+
);
|
|
1028
|
+
}
|
|
681
1029
|
const policies = flags.config ? loadRunConfig(flags, "approve").policies : [];
|
|
682
1030
|
const result = approveRun(resolve(flags.repo), flags.run, {
|
|
683
1031
|
by: flags.by ?? "human",
|
|
@@ -685,7 +1033,9 @@ function commandApprove(flags) {
|
|
|
685
1033
|
policies,
|
|
686
1034
|
});
|
|
687
1035
|
if (!result.approved) {
|
|
688
|
-
console.log(
|
|
1036
|
+
console.log(
|
|
1037
|
+
"NOT approved: the subject changed since the request; a refreshed request was filed",
|
|
1038
|
+
);
|
|
689
1039
|
console.log(` new candidate: ${result.subject.candidate}`);
|
|
690
1040
|
return;
|
|
691
1041
|
}
|
|
@@ -696,9 +1046,13 @@ function commandApprove(flags) {
|
|
|
696
1046
|
console.log(` advisory: ${warning}`);
|
|
697
1047
|
}
|
|
698
1048
|
if (result.terminal) {
|
|
699
|
-
console.log(
|
|
1049
|
+
console.log(
|
|
1050
|
+
"run is terminal: SUCCEEDED (merge itself stays a manual external action)",
|
|
1051
|
+
);
|
|
700
1052
|
} else {
|
|
701
|
-
console.log(
|
|
1053
|
+
console.log(
|
|
1054
|
+
`decision recorded; continue with: buildbeat run --config <run-config.yaml> --run ${flags.run}`,
|
|
1055
|
+
);
|
|
702
1056
|
}
|
|
703
1057
|
}
|
|
704
1058
|
|
|
@@ -715,10 +1069,17 @@ function commandReject(flags) {
|
|
|
715
1069
|
}
|
|
716
1070
|
|
|
717
1071
|
function commandAccept(flags) {
|
|
718
|
-
if (!flags.repo || !flags.work
|
|
719
|
-
throw new Error("accept requires --repo
|
|
720
|
-
}
|
|
721
|
-
const
|
|
1072
|
+
if (!flags.repo || !flags.work) {
|
|
1073
|
+
throw new Error("accept requires --repo and --work");
|
|
1074
|
+
}
|
|
1075
|
+
const workDir = join(resolve(flags.repo), "delivery", "work", flags.work);
|
|
1076
|
+
const selection =
|
|
1077
|
+
flags.artifact ??
|
|
1078
|
+
(existsSync(join(workDir, "work.md")) ? "work" : "intent,plan");
|
|
1079
|
+
const artifacts = String(selection)
|
|
1080
|
+
.split(",")
|
|
1081
|
+
.map((name) => name.trim())
|
|
1082
|
+
.filter(Boolean);
|
|
722
1083
|
const results = acceptArtifacts(resolve(flags.repo), flags.work, artifacts, {
|
|
723
1084
|
by: flags.by ?? "human",
|
|
724
1085
|
});
|
|
@@ -726,33 +1087,17 @@ function commandAccept(flags) {
|
|
|
726
1087
|
console.log(`accepted ${result.artifact} as ${result.decisionRef}`);
|
|
727
1088
|
console.log(` digest: ${result.digest}`);
|
|
728
1089
|
}
|
|
729
|
-
console.log(
|
|
730
|
-
|
|
731
|
-
|
|
732
|
-
// Artifacts a policy rule requires to be accepted (artifact.accepted leaves
|
|
733
|
-
// anywhere under all/any/not).
|
|
734
|
-
function artifactsRequiredBy(rule, found = []) {
|
|
735
|
-
if (!rule || typeof rule !== "object") {
|
|
736
|
-
return found;
|
|
737
|
-
}
|
|
738
|
-
for (const [key, value] of Object.entries(rule)) {
|
|
739
|
-
if (key === "artifact.accepted" && value && typeof value.artifact === "string") {
|
|
740
|
-
found.push(value.artifact);
|
|
741
|
-
} else if (Array.isArray(value)) {
|
|
742
|
-
for (const item of value) {
|
|
743
|
-
artifactsRequiredBy(item, found);
|
|
744
|
-
}
|
|
745
|
-
} else if (value && typeof value === "object") {
|
|
746
|
-
artifactsRequiredBy(value, found);
|
|
747
|
-
}
|
|
748
|
-
}
|
|
749
|
-
return found;
|
|
1090
|
+
console.log(
|
|
1091
|
+
" note: editing an artifact after acceptance makes its acceptance stale",
|
|
1092
|
+
);
|
|
750
1093
|
}
|
|
751
1094
|
|
|
752
1095
|
function commandDoctor(flags) {
|
|
753
1096
|
const options = loadRunConfig(flags, "doctor");
|
|
754
1097
|
console.log(`risk preset: ${options.riskPreset}`);
|
|
755
|
-
console.log(
|
|
1098
|
+
console.log(
|
|
1099
|
+
`stopAt boundaries: ${options.stopAt.length > 0 ? options.stopAt.join(", ") : "(none)"}`,
|
|
1100
|
+
);
|
|
756
1101
|
console.log("policies (declared vs actually achieved enforcement):");
|
|
757
1102
|
if (options.policies.length === 0) {
|
|
758
1103
|
console.log(" (none configured)");
|
|
@@ -762,24 +1107,36 @@ function commandDoctor(flags) {
|
|
|
762
1107
|
if (policy.enforcement === "ADVISORY") {
|
|
763
1108
|
actual = "ADVISORY (prompt-level only; nothing stops a worker)";
|
|
764
1109
|
} else if (policy.enforcement === "SERVER_ENFORCED") {
|
|
765
|
-
actual =
|
|
1110
|
+
actual =
|
|
1111
|
+
"UNVERIFIED (branch protection/CI cannot be verified from here; not claiming it)";
|
|
766
1112
|
} else if (policy.type === "transition") {
|
|
767
1113
|
actual = "LOCAL_ENFORCED (approve gate refuses the stamp)";
|
|
768
1114
|
} else {
|
|
769
1115
|
actual = "LOCAL_ENFORCED (runner gate before/after the step)";
|
|
770
1116
|
}
|
|
771
|
-
console.log(
|
|
1117
|
+
console.log(
|
|
1118
|
+
` ${policy.name}: type=${policy.type} appliesTo=${policy.appliesTo} declared=${policy.enforcement} actual=${actual}`,
|
|
1119
|
+
);
|
|
772
1120
|
}
|
|
1121
|
+
console.log(
|
|
1122
|
+
`screenshot evidence: ${options.deliveryChecks.requireScreenshot ? "required (verify writes images to BUILDBEAT_SCREENSHOT_DIR)" : "not required"}`,
|
|
1123
|
+
);
|
|
1124
|
+
console.log(
|
|
1125
|
+
`release readback: ${options.release ? `${options.release.command} ${(options.release.args ?? []).join(" ")}`.trim() : "none configured (release: section)"}`,
|
|
1126
|
+
);
|
|
773
1127
|
console.log("worker isolation:");
|
|
774
1128
|
for (const [worker, spec] of Object.entries(options.adapterConfigs)) {
|
|
775
|
-
const mode =
|
|
776
|
-
|
|
777
|
-
|
|
1129
|
+
const mode =
|
|
1130
|
+
spec.inheritEnv === true
|
|
1131
|
+
? "WARNING inherit (worker sees the full host env; credential isolation is ADVISORY only)"
|
|
1132
|
+
: "env allowlist (host credential env vars withheld from the worker)";
|
|
778
1133
|
console.log(` ${worker}: ${mode}`);
|
|
779
1134
|
}
|
|
780
1135
|
let remotes = [];
|
|
781
1136
|
try {
|
|
782
|
-
remotes = execFileSync("git", ["-C", options.repoRoot, "remote"], {
|
|
1137
|
+
remotes = execFileSync("git", ["-C", options.repoRoot, "remote"], {
|
|
1138
|
+
encoding: "utf8",
|
|
1139
|
+
})
|
|
783
1140
|
.split("\n")
|
|
784
1141
|
.filter(Boolean);
|
|
785
1142
|
} catch {
|
|
@@ -790,11 +1147,16 @@ function commandDoctor(flags) {
|
|
|
790
1147
|
`push protection: worktree pushurl override is applied at workspace creation for: ${remotes.join(", ")}`,
|
|
791
1148
|
);
|
|
792
1149
|
} else {
|
|
793
|
-
console.log(
|
|
1150
|
+
console.log(
|
|
1151
|
+
"push protection: repository has no remotes (nothing to protect)",
|
|
1152
|
+
);
|
|
794
1153
|
}
|
|
795
|
-
console.log(
|
|
1154
|
+
console.log(
|
|
1155
|
+
"kernel capabilities: merge/deploy/publish have no call path in the runner (invariant 20)",
|
|
1156
|
+
);
|
|
796
1157
|
const budgetLines = [];
|
|
797
|
-
const reviewRounds =
|
|
1158
|
+
const reviewRounds =
|
|
1159
|
+
options.budgets.reviewRoundsPerWork ?? DEFAULT_REVIEW_ROUNDS_PER_WORK;
|
|
798
1160
|
for (const step of options.workflow.steps) {
|
|
799
1161
|
if (!step.worker) {
|
|
800
1162
|
continue;
|
|
@@ -802,20 +1164,38 @@ function commandDoctor(flags) {
|
|
|
802
1164
|
const fromRun = options.budgets.maxAttempts?.[step.id];
|
|
803
1165
|
const fromPreset = options.workflow.budgets?.maxAttempts?.[step.id];
|
|
804
1166
|
const review = step.id === "review" || step.worker === "reviewer";
|
|
805
|
-
const effective =
|
|
806
|
-
|
|
1167
|
+
const effective =
|
|
1168
|
+
fromRun ??
|
|
1169
|
+
fromPreset ??
|
|
1170
|
+
(review ? reviewRounds : options.maxAttemptsPerStep);
|
|
1171
|
+
const source =
|
|
1172
|
+
fromRun !== undefined
|
|
1173
|
+
? "run config"
|
|
1174
|
+
: fromPreset !== undefined
|
|
1175
|
+
? "workflow preset"
|
|
1176
|
+
: review
|
|
1177
|
+
? "reviewRoundsPerWork"
|
|
1178
|
+
: "default";
|
|
807
1179
|
budgetLines.push(`${step.id}=${effective} (${source})`);
|
|
808
1180
|
}
|
|
809
|
-
console.log(
|
|
810
|
-
|
|
1181
|
+
console.log(
|
|
1182
|
+
`budgets (maxAttempts per step; approving resume-<step> after exhaustion grants +1): ${budgetLines.join(", ")}`,
|
|
1183
|
+
);
|
|
1184
|
+
console.log(
|
|
1185
|
+
`budgets.reviewRoundsPerWork: ${reviewRounds}${options.budgets.reviewRoundsPerWork === undefined ? " (default)" : ""} (counted across every run of the work, superseded ones included)`,
|
|
1186
|
+
);
|
|
811
1187
|
// Same preconditions start's first gate will read (real incident, twice:
|
|
812
1188
|
// doctor passed, start stopped at build because plan.md was not mirrored
|
|
813
1189
|
// into the repository the run was started in).
|
|
814
1190
|
const workDir = join(options.repoRoot, "delivery", "work", options.workId);
|
|
815
1191
|
const decisions = readJsonl(join(workDir, "decisions.jsonl"));
|
|
816
|
-
console.log(
|
|
1192
|
+
console.log(
|
|
1193
|
+
`work artifacts in this repository (delivery/work/${options.workId}):`,
|
|
1194
|
+
);
|
|
817
1195
|
const artifactState = {};
|
|
818
|
-
for (const artifact of
|
|
1196
|
+
for (const artifact of options.workArtifact === "work"
|
|
1197
|
+
? ["work"]
|
|
1198
|
+
: ["intent", "plan"]) {
|
|
819
1199
|
const status = artifactStatus(workDir, decisions, artifact);
|
|
820
1200
|
artifactState[artifact] = status;
|
|
821
1201
|
const label = !status.exists
|
|
@@ -828,12 +1208,17 @@ function commandDoctor(flags) {
|
|
|
828
1208
|
console.log(` ${artifact}.md: ${label}`);
|
|
829
1209
|
}
|
|
830
1210
|
for (const policy of options.policies) {
|
|
831
|
-
for (const artifact of
|
|
832
|
-
|
|
1211
|
+
for (const artifact of policy.kind === "artifact"
|
|
1212
|
+
? [policy.artifact]
|
|
1213
|
+
: []) {
|
|
1214
|
+
const status =
|
|
1215
|
+
artifactState[artifact] ?? artifactStatus(workDir, decisions, artifact);
|
|
833
1216
|
if (!status.accepted) {
|
|
834
1217
|
console.log(
|
|
835
1218
|
` WARNING policy ${policy.name} (${policy.type} ${policy.appliesTo}) needs an accepted ${artifact}.md; start will stop at ${policy.appliesTo}` +
|
|
836
|
-
(!status.exists
|
|
1219
|
+
(!status.exists
|
|
1220
|
+
? " (file missing here: mirror it into this repository, then accept)"
|
|
1221
|
+
: " (accept it first)"),
|
|
837
1222
|
);
|
|
838
1223
|
}
|
|
839
1224
|
}
|
|
@@ -842,31 +1227,47 @@ function commandDoctor(flags) {
|
|
|
842
1227
|
console.log("environment contract (requires):");
|
|
843
1228
|
const check = checkRequires(options.requires);
|
|
844
1229
|
for (const row of check.checked) {
|
|
845
|
-
console.log(
|
|
1230
|
+
console.log(
|
|
1231
|
+
` ${row.command}: OK${row.version ? ` (${row.version})` : ""}`,
|
|
1232
|
+
);
|
|
846
1233
|
}
|
|
847
1234
|
for (const problem of check.problems) {
|
|
848
1235
|
console.log(` PROBLEM ${problem}`);
|
|
849
1236
|
}
|
|
850
1237
|
} else {
|
|
851
|
-
console.log(
|
|
1238
|
+
console.log(
|
|
1239
|
+
"environment contract: none declared (implicit PATH facts stay unchecked)",
|
|
1240
|
+
);
|
|
852
1241
|
}
|
|
853
|
-
console.log(
|
|
1242
|
+
console.log(
|
|
1243
|
+
`supersede: ${options.supersede} (new run for the same work ${options.supersede === "off" ? "leaves" : "supersedes"} older WAITING_HUMAN runs)`,
|
|
1244
|
+
);
|
|
854
1245
|
console.log(
|
|
855
1246
|
options.parallel
|
|
856
1247
|
? "concurrency: parallel (runs of other works with parallel: true may drive at the same time; runs of this work stay exclusive; verifiers must not share fixed ports or databases)"
|
|
857
1248
|
: "concurrency: exclusive (default: one driving run per repository; set parallel: true to drive alongside other works)",
|
|
858
1249
|
);
|
|
859
|
-
console.log(
|
|
860
|
-
|
|
1250
|
+
console.log(
|
|
1251
|
+
`stall threshold: ${formatMs(options.stallAfterMs)} without worker output`,
|
|
1252
|
+
);
|
|
1253
|
+
const { config: notify, error: notifyError } = notifyConfigFor(
|
|
1254
|
+
options.repoRoot,
|
|
1255
|
+
);
|
|
861
1256
|
if (notifyError) {
|
|
862
1257
|
console.log(`notify: PROBLEM ${notifyError}`);
|
|
863
1258
|
} else if (!notify) {
|
|
864
|
-
console.log(
|
|
1259
|
+
console.log(
|
|
1260
|
+
`notify: none (${NOTIFY_CONFIG} absent; a waiting run reaches nobody until someone runs inbox)`,
|
|
1261
|
+
);
|
|
865
1262
|
} else {
|
|
866
1263
|
console.log("notify channels:");
|
|
867
1264
|
for (const channel of notify.channels) {
|
|
868
|
-
const urlState = process.env[channel.urlEnv]
|
|
869
|
-
|
|
1265
|
+
const urlState = process.env[channel.urlEnv]
|
|
1266
|
+
? "url env set"
|
|
1267
|
+
: `WARNING env ${channel.urlEnv} not set in this shell`;
|
|
1268
|
+
console.log(
|
|
1269
|
+
` ${channel.id}: type=${channel.type} events=${channel.events.join(",")} (${urlState})`,
|
|
1270
|
+
);
|
|
870
1271
|
}
|
|
871
1272
|
}
|
|
872
1273
|
}
|
|
@@ -881,13 +1282,17 @@ function commandPreflight(flags) {
|
|
|
881
1282
|
if (!flags.step) {
|
|
882
1283
|
throw new Error("preflight requires --step");
|
|
883
1284
|
}
|
|
884
|
-
const stepDef = options.workflow.steps.find(
|
|
1285
|
+
const stepDef = options.workflow.steps.find(
|
|
1286
|
+
(candidate) => candidate.id === flags.step,
|
|
1287
|
+
);
|
|
885
1288
|
if (!stepDef) {
|
|
886
1289
|
throw new Error(`step not in workflow: ${flags.step}`);
|
|
887
1290
|
}
|
|
888
1291
|
const spec = stepDef.worker ? options.adapterConfigs[stepDef.worker] : null;
|
|
889
1292
|
if (!spec) {
|
|
890
|
-
throw new Error(
|
|
1293
|
+
throw new Error(
|
|
1294
|
+
`step ${flags.step} has no configured worker command to preflight`,
|
|
1295
|
+
);
|
|
891
1296
|
}
|
|
892
1297
|
if (options.requires.length > 0) {
|
|
893
1298
|
const check = checkRequires(options.requires);
|
|
@@ -906,7 +1311,16 @@ function commandPreflight(flags) {
|
|
|
906
1311
|
env = { ...process.env };
|
|
907
1312
|
} else {
|
|
908
1313
|
env = {};
|
|
909
|
-
for (const key of [
|
|
1314
|
+
for (const key of [
|
|
1315
|
+
"PATH",
|
|
1316
|
+
"HOME",
|
|
1317
|
+
"LANG",
|
|
1318
|
+
"LC_ALL",
|
|
1319
|
+
"TMPDIR",
|
|
1320
|
+
"TERM",
|
|
1321
|
+
"USER",
|
|
1322
|
+
"SHELL",
|
|
1323
|
+
]) {
|
|
910
1324
|
if (process.env[key] !== undefined) {
|
|
911
1325
|
env[key] = process.env[key];
|
|
912
1326
|
}
|
|
@@ -914,8 +1328,12 @@ function commandPreflight(flags) {
|
|
|
914
1328
|
}
|
|
915
1329
|
Object.assign(env, spec.env ?? {});
|
|
916
1330
|
env.BUILDBEAT_PREFLIGHT = "1";
|
|
917
|
-
console.log(
|
|
918
|
-
|
|
1331
|
+
console.log(
|
|
1332
|
+
`PREFLIGHT (dry signal, never evidence): step ${flags.step} -> ${spec.command} ${args.join(" ")}`,
|
|
1333
|
+
);
|
|
1334
|
+
console.log(
|
|
1335
|
+
`cwd: main checkout (no worktree, no ledger, no evidence written)`,
|
|
1336
|
+
);
|
|
919
1337
|
const result = spawnSync(spec.command, args, {
|
|
920
1338
|
cwd: options.repoRoot,
|
|
921
1339
|
stdio: "inherit",
|
|
@@ -923,10 +1341,14 @@ function commandPreflight(flags) {
|
|
|
923
1341
|
timeout: spec.timeoutMs,
|
|
924
1342
|
});
|
|
925
1343
|
if (result.error) {
|
|
926
|
-
throw new Error(
|
|
1344
|
+
throw new Error(
|
|
1345
|
+
`preflight could not run the command: ${result.error.message}`,
|
|
1346
|
+
);
|
|
927
1347
|
}
|
|
928
1348
|
const exitCode = result.status ?? 1;
|
|
929
|
-
console.log(
|
|
1349
|
+
console.log(
|
|
1350
|
+
`preflight exit=${exitCode} — a Run must reproduce this before it counts`,
|
|
1351
|
+
);
|
|
930
1352
|
process.exitCode = exitCode;
|
|
931
1353
|
}
|
|
932
1354
|
|
|
@@ -948,11 +1370,15 @@ function commandFindings(rest) {
|
|
|
948
1370
|
const verdict = adjudicated.get(row.fingerprint);
|
|
949
1371
|
const status = verdict ? `${verdict.action} by ${verdict.by}` : "open";
|
|
950
1372
|
const reRaised = row.reRaised ? " RE-RAISED" : "";
|
|
951
|
-
console.log(
|
|
1373
|
+
console.log(
|
|
1374
|
+
`[${row.severity} ${row.fingerprint}] (${status})${reRaised} ${row.summary}`,
|
|
1375
|
+
);
|
|
952
1376
|
}
|
|
953
1377
|
} else if (sub === "adjudicate") {
|
|
954
1378
|
if (!flags.repo || !flags.work || !flags.fingerprint || !flags.action) {
|
|
955
|
-
throw new Error(
|
|
1379
|
+
throw new Error(
|
|
1380
|
+
"findings adjudicate requires --repo, --work, --fingerprint and --action",
|
|
1381
|
+
);
|
|
956
1382
|
}
|
|
957
1383
|
const row = adjudicateFinding(resolve(flags.repo), flags.work, {
|
|
958
1384
|
fingerprint: flags.fingerprint,
|
|
@@ -960,12 +1386,18 @@ function commandFindings(rest) {
|
|
|
960
1386
|
by: flags.by,
|
|
961
1387
|
note: flags.note,
|
|
962
1388
|
});
|
|
963
|
-
console.log(
|
|
1389
|
+
console.log(
|
|
1390
|
+
`adjudicated ${row.fingerprint} -> ${row.action} ([${row.severity}] ${row.summary})`,
|
|
1391
|
+
);
|
|
964
1392
|
if (row.action === "dismiss") {
|
|
965
|
-
console.log(
|
|
1393
|
+
console.log(
|
|
1394
|
+
"dismissed: this fingerprint no longer blocks; an escalated severity reopens on its own",
|
|
1395
|
+
);
|
|
966
1396
|
}
|
|
967
1397
|
} else {
|
|
968
|
-
throw new Error(
|
|
1398
|
+
throw new Error(
|
|
1399
|
+
`findings subcommand must be list|adjudicate, got: ${sub ?? "(none)"}`,
|
|
1400
|
+
);
|
|
969
1401
|
}
|
|
970
1402
|
}
|
|
971
1403
|
|
|
@@ -1001,7 +1433,9 @@ function commandReplay(flags) {
|
|
|
1001
1433
|
"state below reflects the valid prefix only",
|
|
1002
1434
|
);
|
|
1003
1435
|
} else {
|
|
1004
|
-
console.log(
|
|
1436
|
+
console.log(
|
|
1437
|
+
`chain OK: ${ledger.events.length} events verified (digest/prev/seq)`,
|
|
1438
|
+
);
|
|
1005
1439
|
}
|
|
1006
1440
|
printState(ledger.state, { corruption: null }, {});
|
|
1007
1441
|
}
|
|
@@ -1019,16 +1453,166 @@ function commandMetrics(flags) {
|
|
|
1019
1453
|
}
|
|
1020
1454
|
|
|
1021
1455
|
function commandStatus(flags) {
|
|
1022
|
-
if (!flags.repo
|
|
1023
|
-
throw new Error("status requires --repo and --run");
|
|
1024
|
-
}
|
|
1456
|
+
if (!flags.repo) throw new Error("status requires --repo");
|
|
1025
1457
|
const repoRoot = resolve(flags.repo);
|
|
1026
|
-
|
|
1027
|
-
|
|
1028
|
-
|
|
1029
|
-
|
|
1030
|
-
|
|
1458
|
+
if (flags.run && flags.work)
|
|
1459
|
+
throw new Error("status: choose --run or --work");
|
|
1460
|
+
if (!flags.run) {
|
|
1461
|
+
const rows = computeOverview(repoRoot, {
|
|
1462
|
+
work: flags.work ?? null,
|
|
1463
|
+
repoLabel: repoLabelFor(repoRoot, flags.repo),
|
|
1464
|
+
});
|
|
1465
|
+
const pending = pendingFor(repoRoot, flags.work);
|
|
1466
|
+
if (flags.json === "true")
|
|
1467
|
+
console.log(
|
|
1468
|
+
JSON.stringify(
|
|
1469
|
+
{
|
|
1470
|
+
works: rows,
|
|
1471
|
+
pending,
|
|
1472
|
+
metrics: flags.work ? null : computeMetrics(repoRoot),
|
|
1473
|
+
},
|
|
1474
|
+
null,
|
|
1475
|
+
2,
|
|
1476
|
+
),
|
|
1477
|
+
);
|
|
1478
|
+
else {
|
|
1479
|
+
console.log(renderOverview(rows));
|
|
1480
|
+
if (pending.length) commandInbox(flags, pending);
|
|
1481
|
+
if (!flags.work) console.log(renderMetrics(computeMetrics(repoRoot)));
|
|
1482
|
+
}
|
|
1483
|
+
return;
|
|
1484
|
+
}
|
|
1485
|
+
const path = ledgerPathFor(repoRoot, flags.run);
|
|
1486
|
+
if (!existsSync(path))
|
|
1487
|
+
throw new Error(
|
|
1488
|
+
`no runtime ledger for ${flags.run}; use status --work to read archived records`,
|
|
1489
|
+
);
|
|
1490
|
+
const ledger = EventLedger.open(path);
|
|
1491
|
+
if (flags.json === "true")
|
|
1492
|
+
console.log(
|
|
1493
|
+
JSON.stringify(
|
|
1494
|
+
{ state: ledger.state, corruption: ledger.corruption },
|
|
1495
|
+
null,
|
|
1496
|
+
2,
|
|
1497
|
+
),
|
|
1498
|
+
);
|
|
1499
|
+
else
|
|
1500
|
+
printState(ledger.state, ledger, {
|
|
1501
|
+
repoRoot,
|
|
1502
|
+
repoLabel: repoLabelFor(repoRoot, flags.repo),
|
|
1503
|
+
stallAfterMs: stallAfterFromFlags(flags),
|
|
1504
|
+
});
|
|
1505
|
+
}
|
|
1506
|
+
|
|
1507
|
+
async function commandRun(flags) {
|
|
1508
|
+
const options = loadRunConfig(flags, "run");
|
|
1509
|
+
if (flags.new === "true" && (flags.run || flags.adopt))
|
|
1510
|
+
throw new Error("--new cannot be combined with --run or --adopt");
|
|
1511
|
+
if (flags.new === "true")
|
|
1512
|
+
return commandStart({ ...flags, attempt: "new" }, options);
|
|
1513
|
+
if (flags.run || flags.adopt)
|
|
1514
|
+
return commandResume(flags, options, { familyWide: true });
|
|
1515
|
+
// Resume an existing family, including its exact ID. Never interpret an
|
|
1516
|
+
// ambiguous, corrupt, or terminal run as permission to start another one.
|
|
1517
|
+
const dir = join(options.repoRoot, ".buildbeat", "runtime", "runs");
|
|
1518
|
+
const found =
|
|
1519
|
+
existsSync(dir) &&
|
|
1520
|
+
readdirSync(dir).some(
|
|
1521
|
+
(id) =>
|
|
1522
|
+
inRunFamily(options.runId, id) &&
|
|
1523
|
+
existsSync(ledgerPathFor(options.repoRoot, id)),
|
|
1524
|
+
);
|
|
1525
|
+
const records = join(
|
|
1526
|
+
options.repoRoot,
|
|
1527
|
+
"delivery",
|
|
1528
|
+
"work",
|
|
1529
|
+
options.workId,
|
|
1530
|
+
"runs",
|
|
1531
|
+
);
|
|
1532
|
+
const archived =
|
|
1533
|
+
existsSync(records) &&
|
|
1534
|
+
readdirSync(records).some((id) => inRunFamily(options.runId, id));
|
|
1535
|
+
if (found) return commandResume(flags, options, { familyWide: true });
|
|
1536
|
+
if (archived)
|
|
1537
|
+
throw new Error(
|
|
1538
|
+
"this work has archived runs; inspect status, then use --new explicitly for a new attempt",
|
|
1539
|
+
);
|
|
1540
|
+
return commandStart({ ...flags, attempt: "new" }, options);
|
|
1541
|
+
}
|
|
1542
|
+
|
|
1543
|
+
// Runs the project's release readback once and records it in the Work.
|
|
1544
|
+
function commandRelease(flags) {
|
|
1545
|
+
const options = loadRunConfig(flags, "release");
|
|
1546
|
+
if (!options.release) {
|
|
1547
|
+
throw new Error(
|
|
1548
|
+
"release needs a release: section in the run config (command, args, timeoutMs, env, like a worker)",
|
|
1549
|
+
);
|
|
1550
|
+
}
|
|
1551
|
+
const row = runReadback({
|
|
1552
|
+
repoRoot: options.repoRoot,
|
|
1553
|
+
workId: options.workId,
|
|
1554
|
+
spec: options.release,
|
|
1555
|
+
redact: options.redact,
|
|
1556
|
+
runs: runsFor(options.repoRoot, options.workId),
|
|
1557
|
+
ref: flags.ref ?? "HEAD",
|
|
1558
|
+
note: flags.note ?? null,
|
|
1031
1559
|
});
|
|
1560
|
+
const repoLabel = repoLabelFor(options.repoRoot);
|
|
1561
|
+
console.log(
|
|
1562
|
+
`readback ${row.status} (exit ${row.exitCode}) for ${row.ref} at ${row.commit.slice(0, 7)}, candidate ${row.candidate.slice(0, 7)} (${row.run})${row.note ? ` — ${row.note}` : ""}`,
|
|
1563
|
+
);
|
|
1564
|
+
if (row.checkout !== row.commit) {
|
|
1565
|
+
console.log(
|
|
1566
|
+
` note: the command ran in the main checkout at ${row.checkout.slice(0, 7)}, not at ${row.ref}`,
|
|
1567
|
+
);
|
|
1568
|
+
}
|
|
1569
|
+
console.log(` log: ${row.log}`);
|
|
1570
|
+
console.log(` recorded: delivery/work/${options.workId}/releases.jsonl`);
|
|
1571
|
+
for (const line of row.tail.slice(-5)) {
|
|
1572
|
+
console.log(` | ${line}`);
|
|
1573
|
+
}
|
|
1574
|
+
console.log(
|
|
1575
|
+
row.status === "passed"
|
|
1576
|
+
? `next: buildbeat decide --repo ${repoLabel} --work ${options.workId} --action close --result <what was released> --by <you>`
|
|
1577
|
+
: "next: fix the release, then run buildbeat release again; the window cannot close on a failed readback",
|
|
1578
|
+
);
|
|
1579
|
+
if (row.status !== "passed") {
|
|
1580
|
+
process.exitCode = 1;
|
|
1581
|
+
}
|
|
1582
|
+
}
|
|
1583
|
+
|
|
1584
|
+
function commandDecide(flags) {
|
|
1585
|
+
if (flags.action === "close") {
|
|
1586
|
+
if (!flags.repo || !flags.work)
|
|
1587
|
+
throw new Error("closing a work requires --repo and --work");
|
|
1588
|
+
const row = closeWork({
|
|
1589
|
+
repoRoot: resolve(flags.repo),
|
|
1590
|
+
workId: flags.work,
|
|
1591
|
+
result: flags.result,
|
|
1592
|
+
by: flags.by ?? "human",
|
|
1593
|
+
});
|
|
1594
|
+
console.log(
|
|
1595
|
+
`closed ${flags.work} as ${row.decisionRef} (readback ${row.subject.readback.slice(0, 19)} at ${row.subject.commit.slice(0, 7)})`,
|
|
1596
|
+
);
|
|
1597
|
+
return;
|
|
1598
|
+
}
|
|
1599
|
+
if (flags.action === "approve") return commandApprove(flags);
|
|
1600
|
+
if (flags.action === "reject") return commandReject(flags);
|
|
1601
|
+
if (["accept", "dismiss"].includes(flags.action) && flags.fingerprint) {
|
|
1602
|
+
if (!flags.repo || !flags.work)
|
|
1603
|
+
throw new Error("finding decisions require --repo and --work");
|
|
1604
|
+
const result = adjudicateFinding(resolve(flags.repo), flags.work, {
|
|
1605
|
+
fingerprint: flags.fingerprint,
|
|
1606
|
+
action: flags.action,
|
|
1607
|
+
by: flags.by ?? "human",
|
|
1608
|
+
note: flags.note,
|
|
1609
|
+
});
|
|
1610
|
+
console.log(JSON.stringify(result));
|
|
1611
|
+
return;
|
|
1612
|
+
}
|
|
1613
|
+
throw new Error(
|
|
1614
|
+
"decide requires --action approve|reject, accept|dismiss with --work and --fingerprint, or close with --work and --result",
|
|
1615
|
+
);
|
|
1032
1616
|
}
|
|
1033
1617
|
|
|
1034
1618
|
function commandStop(flags) {
|
|
@@ -1068,7 +1652,10 @@ function commandOverview(flags) {
|
|
|
1068
1652
|
throw new Error("overview requires --repo");
|
|
1069
1653
|
}
|
|
1070
1654
|
const repoRoot = resolve(flags.repo);
|
|
1071
|
-
const rows = computeOverview(repoRoot, {
|
|
1655
|
+
const rows = computeOverview(repoRoot, {
|
|
1656
|
+
work: flags.work ?? null,
|
|
1657
|
+
repoLabel: repoLabelFor(repoRoot, flags.repo),
|
|
1658
|
+
});
|
|
1072
1659
|
if (flags.json === "true") {
|
|
1073
1660
|
console.log(JSON.stringify(rows, null, 2));
|
|
1074
1661
|
return;
|
|
@@ -1095,12 +1682,16 @@ function commandGc(flags) {
|
|
|
1095
1682
|
if (action.kind === "delete-branch") {
|
|
1096
1683
|
return `delete branch ${action.branch} (${action.reason})`;
|
|
1097
1684
|
}
|
|
1098
|
-
return action.owner
|
|
1685
|
+
return action.owner
|
|
1686
|
+
? "remove active-run lock (owner process is gone)"
|
|
1687
|
+
: "remove stale lock";
|
|
1099
1688
|
});
|
|
1100
1689
|
actionable += row.actions.length;
|
|
1101
1690
|
const keep = row.keep.map((reason) => `keep: ${reason}`);
|
|
1102
1691
|
const parts = [...summary, ...keep];
|
|
1103
|
-
console.log(
|
|
1692
|
+
console.log(
|
|
1693
|
+
`${row.run} [${row.status}] ${parts.length > 0 ? parts.join("; ") : "nothing to do"}`,
|
|
1694
|
+
);
|
|
1104
1695
|
}
|
|
1105
1696
|
if (flags.apply !== "true") {
|
|
1106
1697
|
console.log(
|
|
@@ -1113,7 +1704,8 @@ function commandGc(flags) {
|
|
|
1113
1704
|
const results = applyGc(repoRoot, rows, { force: flags.force === "true" });
|
|
1114
1705
|
let done = 0;
|
|
1115
1706
|
for (const result of results) {
|
|
1116
|
-
const target =
|
|
1707
|
+
const target =
|
|
1708
|
+
result.kind === "delete-branch" ? result.branch : result.path;
|
|
1117
1709
|
if (result.done) {
|
|
1118
1710
|
done += 1;
|
|
1119
1711
|
console.log(` ${result.kind} ${target}: done`);
|
|
@@ -1133,7 +1725,8 @@ function processAlive(pid) {
|
|
|
1133
1725
|
}
|
|
1134
1726
|
}
|
|
1135
1727
|
|
|
1136
|
-
const sleep = (ms) =>
|
|
1728
|
+
const sleep = (ms) =>
|
|
1729
|
+
new Promise((resolveSleep) => setTimeout(resolveSleep, ms));
|
|
1137
1730
|
|
|
1138
1731
|
// Watches one RUNNING run and reports a stall once per step attempt. Exits
|
|
1139
1732
|
// when the run leaves RUNNING, the parent process (if given) dies, or after a
|
|
@@ -1159,7 +1752,12 @@ async function commandWatch(flags) {
|
|
|
1159
1752
|
console.log(`watch: run is ${ledger.state.run.status}; done`);
|
|
1160
1753
|
return;
|
|
1161
1754
|
} else {
|
|
1162
|
-
const liveness = describeLiveness({
|
|
1755
|
+
const liveness = describeLiveness({
|
|
1756
|
+
repoRoot,
|
|
1757
|
+
runId: flags.run,
|
|
1758
|
+
ledger,
|
|
1759
|
+
stallAfterMs,
|
|
1760
|
+
});
|
|
1163
1761
|
const live = liveness.inFlight;
|
|
1164
1762
|
if (live?.stalled) {
|
|
1165
1763
|
const key = `${live.step}#${live.attempt}`;
|
|
@@ -1183,14 +1781,20 @@ async function commandWatch(flags) {
|
|
|
1183
1781
|
command: live.command,
|
|
1184
1782
|
},
|
|
1185
1783
|
});
|
|
1186
|
-
const results = await dispatchNotification(config, notification, {
|
|
1784
|
+
const results = await dispatchNotification(config, notification, {
|
|
1785
|
+
repoRoot,
|
|
1786
|
+
});
|
|
1187
1787
|
for (const row of results) {
|
|
1188
|
-
console.log(
|
|
1788
|
+
console.log(
|
|
1789
|
+
`watch: notify -> ${row.channel}: ${row.ok ? "sent" : row.error}`,
|
|
1790
|
+
);
|
|
1189
1791
|
}
|
|
1190
1792
|
}
|
|
1191
1793
|
}
|
|
1192
1794
|
} else if (live) {
|
|
1193
|
-
console.log(
|
|
1795
|
+
console.log(
|
|
1796
|
+
`watch: ${live.step} attempt ${live.attempt} elapsed ${formatMs(live.elapsedMs)}, last output ${formatMs(live.sinceOutputMs)} ago`,
|
|
1797
|
+
);
|
|
1194
1798
|
}
|
|
1195
1799
|
}
|
|
1196
1800
|
if (flags.once === "true") {
|
|
@@ -1204,66 +1808,14 @@ async function commandWatch(flags) {
|
|
|
1204
1808
|
}
|
|
1205
1809
|
}
|
|
1206
1810
|
|
|
1207
|
-
function commandObserve(rest) {
|
|
1208
|
-
const [sub, ...args] = rest;
|
|
1209
|
-
const flags = parseFlags(args);
|
|
1210
|
-
if (sub === "run") {
|
|
1211
|
-
if (!flags.config) {
|
|
1212
|
-
throw new Error("observe run requires --config <observe.yaml>");
|
|
1213
|
-
}
|
|
1214
|
-
const result = runObserveCycle({ configPath: flags.config });
|
|
1215
|
-
console.log(`observe cycle ${result.cycle} finished (ledger: ${result.ledgerRef})`);
|
|
1216
|
-
for (const row of result.results) {
|
|
1217
|
-
const bands = row.bands.length > 0 ? ` bands=${row.bands.join(",")}` : "";
|
|
1218
|
-
const intent = row.intent ? ` intent=${row.intent.outcome}:${row.intent.intentRef}` : "";
|
|
1219
|
-
console.log(
|
|
1220
|
-
` ${row.provider}: ${row.status}${row.severity ? ` severity=${row.severity}` : ""}${bands}${intent}`,
|
|
1221
|
-
);
|
|
1222
|
-
}
|
|
1223
|
-
} else if (sub === "status") {
|
|
1224
|
-
if (!flags.repo) {
|
|
1225
|
-
throw new Error("observe status requires --repo");
|
|
1226
|
-
}
|
|
1227
|
-
const status = observeStatus({ repoRoot: flags.repo });
|
|
1228
|
-
if (status.corruption) {
|
|
1229
|
-
console.log(
|
|
1230
|
-
`WARNING: observe ledger corrupted after seq=${status.corruption.afterSeq}: ${status.corruption.reason}`,
|
|
1231
|
-
);
|
|
1232
|
-
}
|
|
1233
|
-
console.log(`cycles: ${status.cycles}`);
|
|
1234
|
-
for (const [id, info] of Object.entries(status.providers)) {
|
|
1235
|
-
console.log(`provider ${id}: ${info.lastStatus} (${info.lastKind} on ${info.lastSubject}) at ${info.lastTs}`);
|
|
1236
|
-
}
|
|
1237
|
-
if (status.intents.length === 0) {
|
|
1238
|
-
console.log("intents: none");
|
|
1239
|
-
}
|
|
1240
|
-
for (const intent of status.intents) {
|
|
1241
|
-
console.log(
|
|
1242
|
-
`intent [${intent.status}] ${intent.file} (${intent.provider}, severity ${intent.severity})`,
|
|
1243
|
-
);
|
|
1244
|
-
}
|
|
1245
|
-
} else if (sub === "triage") {
|
|
1246
|
-
if (!flags.repo || !flags.intent || !flags.action) {
|
|
1247
|
-
throw new Error("observe triage requires --repo, --intent and --action");
|
|
1248
|
-
}
|
|
1249
|
-
const result = triageIntent({
|
|
1250
|
-
repoRoot: flags.repo,
|
|
1251
|
-
intentRef: flags.intent,
|
|
1252
|
-
action: flags.action,
|
|
1253
|
-
by: flags.by,
|
|
1254
|
-
note: flags.note,
|
|
1255
|
-
});
|
|
1256
|
-
console.log(`intent ${result.intentRef} -> ${result.status}`);
|
|
1257
|
-
if (result.suggestion) {
|
|
1258
|
-
console.log(result.suggestion);
|
|
1259
|
-
}
|
|
1260
|
-
} else {
|
|
1261
|
-
throw new Error(`observe subcommand must be run|status|triage, got: ${sub ?? "(none)"}`);
|
|
1262
|
-
}
|
|
1263
|
-
}
|
|
1264
|
-
|
|
1265
1811
|
function packageVersion() {
|
|
1266
|
-
const packageJson = join(
|
|
1812
|
+
const packageJson = join(
|
|
1813
|
+
dirname(fileURLToPath(import.meta.url)),
|
|
1814
|
+
"..",
|
|
1815
|
+
"..",
|
|
1816
|
+
"..",
|
|
1817
|
+
"package.json",
|
|
1818
|
+
);
|
|
1267
1819
|
return JSON.parse(readFileSync(packageJson, "utf8")).version;
|
|
1268
1820
|
}
|
|
1269
1821
|
|
|
@@ -1273,9 +1825,16 @@ async function main() {
|
|
|
1273
1825
|
process.stdout.write(`${packageVersion()}\n`);
|
|
1274
1826
|
return;
|
|
1275
1827
|
}
|
|
1276
|
-
if (command === "observe"
|
|
1828
|
+
if (command === "observe") {
|
|
1829
|
+
console.error(
|
|
1830
|
+
"error: observe execution is retired; existing records are preserved. See docs/MIGRATION.md",
|
|
1831
|
+
);
|
|
1832
|
+
process.exitCode = 1;
|
|
1833
|
+
return;
|
|
1834
|
+
}
|
|
1835
|
+
if (command === "findings") {
|
|
1277
1836
|
try {
|
|
1278
|
-
|
|
1837
|
+
commandFindings(rest);
|
|
1279
1838
|
} catch (error) {
|
|
1280
1839
|
console.error(`error: ${error.message}`);
|
|
1281
1840
|
process.exitCode = 1;
|
|
@@ -1284,7 +1843,17 @@ async function main() {
|
|
|
1284
1843
|
}
|
|
1285
1844
|
try {
|
|
1286
1845
|
const flags = parseFlags(rest);
|
|
1287
|
-
if (command === "
|
|
1846
|
+
if (command === "run") {
|
|
1847
|
+
await commandRun(flags);
|
|
1848
|
+
} else if (command === "decide") {
|
|
1849
|
+
commandDecide(flags);
|
|
1850
|
+
} else if (command === "release") {
|
|
1851
|
+
commandRelease(flags);
|
|
1852
|
+
} else if (command === "check") {
|
|
1853
|
+
flags.step ? commandPreflight(flags) : commandDoctor(flags);
|
|
1854
|
+
} else if (command === "history") {
|
|
1855
|
+
flags.verify === "true" ? commandReplay(flags) : commandEvents(flags);
|
|
1856
|
+
} else if (command === "start") {
|
|
1288
1857
|
await commandStart(flags);
|
|
1289
1858
|
} else if (command === "resume") {
|
|
1290
1859
|
await commandResume(flags);
|