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