@haiyangbg/buildbeat 2.0.0-beta.2 → 2.0.0-beta.4
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 +42 -6
- package/SKILL.md +76 -2
- package/bin/buildbeat-v2.js +13 -1
- package/docs/CLI-PILOT-2026-08-23.md +1 -1
- package/docs/CLI.md +1 -1
- package/docs/EXECUTION-PLAN.md +2 -2
- package/docs/PHASE2-PILOT-PREFLIGHT-2026-08-25.md +2 -2
- package/docs/PHASE4-V1.20-PILOT-2026-08-25.md +2 -2
- package/docs/RELEASING.md +1 -1
- package/docs/V2-D2-DECISION-CARD.md +2 -2
- package/docs/V2-DECISIONS.md +2 -2
- package/docs/V2-ITERATION-01.md +13 -13
- package/docs/V2-ITERATION-06.md +2 -2
- package/docs/V2-ITERATION-08.md +62 -0
- package/docs/V2-PLAN.md +6 -6
- package/docs/V2-PROPOSAL.md +2 -2
- package/docs/V2.0.0-BETA.1-RELEASE-EVIDENCE-2026-08-28.md +1 -1
- package/docs/V2.0.0-BETA.2-RELEASE-EVIDENCE-2026-08-28.md +8 -0
- package/docs/V2.0.0-BETA.3-RELEASE-EVIDENCE-2026-09-01.md +8 -0
- package/docs/v2/M4-EXTERNAL-PILOT-2026-08-28.md +11 -11
- package/docs/v2/{M4-CHICKAI-PILOT-2026-08-28.md → M4-PILOT-APP-2026-08-28.md} +4 -4
- package/docs/v2/M4-SELFHOST-2026-08-28.md +1 -1
- package/docs/v2/RFC-0001-product-definition.md +2 -2
- package/docs/v2/SPEC-0001-events-v1.md +2 -2
- package/docs/v2/guide/00-how-to-talk.md +57 -0
- package/docs/v2/guide/01-quickstart.md +6 -0
- package/docs/v2/guide/02-workflow-guide.md +31 -0
- package/docs/v2/guide/04-adapter-guide.md +4 -0
- package/docs/v2/guide/05-worker-contract.md +10 -0
- package/docs/v2/guide/06-evidence-guide.md +10 -0
- package/docs/v2/guide/07-approval-guide.md +50 -0
- package/docs/v2/guide/10-recovery.md +25 -2
- package/docs/v2/guide/README.md +3 -0
- package/example/.buildbeat/manifest.json +1 -1
- package/lessons.md +12 -0
- package/package.json +1 -1
- package/src/v2/adapters/shell.js +87 -14
- package/src/v2/cli/run.js +602 -30
- package/src/v2/domain/event-registry.js +24 -0
- package/src/v2/engine/reducer.js +5 -0
- package/src/v2/engine/workflow.js +8 -1
- package/src/v2/evidence/collector.js +14 -3
- package/src/v2/observe/observe.js +9 -2
- package/src/v2/presets/release-readback.yaml +36 -0
- package/src/v2/presets/risk/release.yaml +21 -0
- package/src/v2/presets/software-delivery.yaml +5 -0
- package/src/v2/runtime/cache.js +124 -0
- package/src/v2/runtime/decisions.js +6 -4
- package/src/v2/runtime/env-contract.js +135 -0
- package/src/v2/runtime/envelope.js +183 -0
- package/src/v2/runtime/findings.js +158 -0
- package/src/v2/runtime/gc.js +182 -0
- package/src/v2/runtime/liveness.js +193 -0
- package/src/v2/runtime/metrics.js +8 -0
- package/src/v2/runtime/notify.js +223 -0
- package/src/v2/runtime/orchestrator.js +252 -24
- package/src/v2/runtime/overview.js +264 -0
- package/src/v2/runtime/repo-ref.js +38 -0
- package/src/v2/runtime/run-record.js +18 -3
- package/src/v2/workspace/workspace-manager.js +4 -1
- package/templates/v2/AGENTS.md +72 -0
- package/templates/v2//346/214/207/346/214/245/345/217/260.md +36 -0
package/src/v2/cli/run.js
CHANGED
|
@@ -2,11 +2,18 @@
|
|
|
2
2
|
// M1 runtime CLI: run start / status / stop for a single foreground run.
|
|
3
3
|
// Deliberately thin — all facts live in the event ledger; this file only
|
|
4
4
|
// parses input, wires adapters, and renders derived state.
|
|
5
|
+
//
|
|
6
|
+
// Iteration 08 adds the human-facing layer the deploy campaign showed was
|
|
7
|
+
// missing: elapsed/typical time and live output in `status`, next-reply
|
|
8
|
+
// commands wherever a run waits, supersession of stale waits, `gc` for the
|
|
9
|
+
// runtime plane, `watch` + outbound notifications so a stopped run reaches
|
|
10
|
+
// its human.
|
|
5
11
|
|
|
6
|
-
import { execFileSync } from "node:child_process";
|
|
12
|
+
import { execFileSync, spawn, spawnSync } from "node:child_process";
|
|
7
13
|
import { createHash } from "node:crypto";
|
|
8
14
|
import { existsSync, readFileSync } from "node:fs";
|
|
9
|
-
import { dirname, join, resolve } from "node:path";
|
|
15
|
+
import { dirname, isAbsolute, join, relative, resolve } from "node:path";
|
|
16
|
+
import { fileURLToPath } from "node:url";
|
|
10
17
|
|
|
11
18
|
import { createShellAdapter } from "../adapters/shell.js";
|
|
12
19
|
import { loadRiskPreset } from "../engine/risk-preset.js";
|
|
@@ -15,9 +22,34 @@ import { parseYamlSubset } from "../engine/yaml-subset.js";
|
|
|
15
22
|
import { parsePolicyDoc } from "../policy/policy.js";
|
|
16
23
|
import { observeStatus, runObserveCycle, triageIntent } from "../observe/observe.js";
|
|
17
24
|
import { acceptArtifact, approveRun, listInbox, rejectRun } from "../runtime/decisions.js";
|
|
25
|
+
import { checkRequires } from "../runtime/env-contract.js";
|
|
26
|
+
import {
|
|
27
|
+
adjudicateFinding,
|
|
28
|
+
findingsAccountRef,
|
|
29
|
+
latestAdjudications,
|
|
30
|
+
readFindingsAccount,
|
|
31
|
+
} from "../runtime/findings.js";
|
|
32
|
+
import { loadEnvelope, nextAttemptId } from "../runtime/envelope.js";
|
|
33
|
+
import { applyGc, planGc } from "../runtime/gc.js";
|
|
34
|
+
import { computeOverview, renderOverview } from "../runtime/overview.js";
|
|
35
|
+
import {
|
|
36
|
+
DEFAULT_STALL_AFTER_MS,
|
|
37
|
+
describeLiveness,
|
|
38
|
+
formatMs,
|
|
39
|
+
tailLive,
|
|
40
|
+
} from "../runtime/liveness.js";
|
|
18
41
|
import { computeMetrics, renderMetrics } from "../runtime/metrics.js";
|
|
42
|
+
import {
|
|
43
|
+
NOTIFY_CONFIG,
|
|
44
|
+
buildNotification,
|
|
45
|
+
dispatchNotification,
|
|
46
|
+
loadNotifyConfig,
|
|
47
|
+
nextReply,
|
|
48
|
+
subscribes,
|
|
49
|
+
} from "../runtime/notify.js";
|
|
19
50
|
import { writeRunRecord } from "../runtime/run-record.js";
|
|
20
51
|
import { resumeRun, startRun } from "../runtime/orchestrator.js";
|
|
52
|
+
import { toRepoRef } from "../runtime/repo-ref.js";
|
|
21
53
|
import { EventLedger } from "../storage/event-ledger.js";
|
|
22
54
|
import { acquireLock, releaseLock } from "../workspace/workspace-manager.js";
|
|
23
55
|
|
|
@@ -26,10 +58,11 @@ const KERNEL = { kind: "kernel", id: "cli" };
|
|
|
26
58
|
const USAGE = `BuildBeat v2 runtime
|
|
27
59
|
|
|
28
60
|
Usage:
|
|
29
|
-
run.js start --config <run-config.yaml>
|
|
61
|
+
run.js start --config <run-config.yaml> [--attempt new]
|
|
30
62
|
run.js resume --config <run-config.yaml>
|
|
31
|
-
run.js status --repo <path> --run <RUN-ID>
|
|
63
|
+
run.js status --repo <path> --run <RUN-ID> [--stall-after <minutes>]
|
|
32
64
|
run.js inbox --repo <path>
|
|
65
|
+
run.js overview --repo <path> [--work <WORK-ID>] [--json true]
|
|
33
66
|
run.js approve --repo <path> --run <RUN-ID> --transition <t> [--by <name>] [--config <run-config.yaml>]
|
|
34
67
|
run.js reject --repo <path> --run <RUN-ID> [--transition <t>] [--reason <text>] [--by <name>]
|
|
35
68
|
run.js accept --repo <path> --work <WORK-ID> --artifact <plan|intent|spec> [--by <name>]
|
|
@@ -38,9 +71,14 @@ Usage:
|
|
|
38
71
|
run.js replay --repo <path> --run <RUN-ID>
|
|
39
72
|
run.js metrics --repo <path> [--json true]
|
|
40
73
|
run.js stop --repo <path> --run <RUN-ID> --reason <text>
|
|
74
|
+
run.js gc --repo <path> [--apply true] [--force true]
|
|
75
|
+
run.js watch --repo <path> --run <RUN-ID> [--stall-after <minutes>] [--interval <seconds>] [--once true]
|
|
41
76
|
run.js observe run --config <observe.yaml>
|
|
42
77
|
run.js observe status --repo <path>
|
|
43
78
|
run.js observe triage --repo <path> --intent <ref> --action <fix_now|schedule|dismiss> [--by <name>] [--note <text>]
|
|
79
|
+
run.js preflight --config <run-config.yaml> --step <id>
|
|
80
|
+
run.js findings list --repo <path> --work <WORK-ID>
|
|
81
|
+
run.js findings adjudicate --repo <path> --work <WORK-ID> --fingerprint <fp> --action <accept|dismiss> [--by <name>] [--note <text>]
|
|
44
82
|
`;
|
|
45
83
|
|
|
46
84
|
function parseFlags(argv) {
|
|
@@ -60,7 +98,45 @@ function ledgerPathFor(repo, runId) {
|
|
|
60
98
|
return join(resolve(repo), ".buildbeat", "runtime", "runs", runId, "events.jsonl");
|
|
61
99
|
}
|
|
62
100
|
|
|
63
|
-
function
|
|
101
|
+
function stallAfterFromFlags(flags, fallbackMs = DEFAULT_STALL_AFTER_MS) {
|
|
102
|
+
if (flags["stall-after-ms"] !== undefined) {
|
|
103
|
+
return Number(flags["stall-after-ms"]);
|
|
104
|
+
}
|
|
105
|
+
if (flags["stall-after"] !== undefined) {
|
|
106
|
+
return Number(flags["stall-after"]) * 60 * 1000;
|
|
107
|
+
}
|
|
108
|
+
return fallbackMs;
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
// The --repo value to print in copyable commands. Inside the project it is
|
|
112
|
+
// the relative path the user would type; outside it a placeholder — printing
|
|
113
|
+
// a machine-local absolute path is exactly what the output must never do.
|
|
114
|
+
function repoLabelFor(repoRoot, typed) {
|
|
115
|
+
if (typed && !isAbsolute(typed)) {
|
|
116
|
+
return typed;
|
|
117
|
+
}
|
|
118
|
+
const rel = relative(process.cwd(), repoRoot);
|
|
119
|
+
if (rel === "") {
|
|
120
|
+
return ".";
|
|
121
|
+
}
|
|
122
|
+
if (rel.startsWith("..") || isAbsolute(rel)) {
|
|
123
|
+
return "<repo-path>";
|
|
124
|
+
}
|
|
125
|
+
return rel;
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
function printNextReply(repoLabel, state) {
|
|
129
|
+
const lines = nextReply({ repoLabel, state });
|
|
130
|
+
if (lines.length === 0) {
|
|
131
|
+
return;
|
|
132
|
+
}
|
|
133
|
+
console.log("next (copy one):");
|
|
134
|
+
for (const line of lines) {
|
|
135
|
+
console.log(` ${line}`);
|
|
136
|
+
}
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
function printState(state, ledger, view = {}) {
|
|
64
140
|
if (ledger.corruption) {
|
|
65
141
|
console.log(
|
|
66
142
|
`WARNING: ledger corrupted after seq=${ledger.corruption.afterSeq} at line ${ledger.corruption.atLine}: ${ledger.corruption.reason}`,
|
|
@@ -70,6 +146,15 @@ function printState(state, ledger) {
|
|
|
70
146
|
console.log("no run recorded");
|
|
71
147
|
return;
|
|
72
148
|
}
|
|
149
|
+
const liveness =
|
|
150
|
+
view.repoRoot && ledger.events
|
|
151
|
+
? describeLiveness({
|
|
152
|
+
repoRoot: view.repoRoot,
|
|
153
|
+
runId: state.run.id,
|
|
154
|
+
ledger,
|
|
155
|
+
stallAfterMs: view.stallAfterMs ?? DEFAULT_STALL_AFTER_MS,
|
|
156
|
+
})
|
|
157
|
+
: { steps: {}, inFlight: null };
|
|
73
158
|
console.log(`run: ${state.run.id} (work ${state.run.work})`);
|
|
74
159
|
console.log(`status: ${state.run.status}`);
|
|
75
160
|
console.log(`workflow: ${state.run.workflowRef} @ ${state.run.workflowDigest}`);
|
|
@@ -79,16 +164,62 @@ function printState(state, ledger) {
|
|
|
79
164
|
);
|
|
80
165
|
}
|
|
81
166
|
for (const [step, info] of Object.entries(state.steps)) {
|
|
82
|
-
|
|
167
|
+
const timing = liveness.steps[step];
|
|
168
|
+
let suffix = "";
|
|
169
|
+
if (timing && info.status !== "RUNNING") {
|
|
170
|
+
const parts = [];
|
|
171
|
+
if (timing.lastMs !== null) {
|
|
172
|
+
parts.push(`last ${formatMs(timing.lastMs)}`);
|
|
173
|
+
}
|
|
174
|
+
if (timing.attempts > 1) {
|
|
175
|
+
parts.push(`total ${formatMs(timing.totalMs)}`);
|
|
176
|
+
}
|
|
177
|
+
if (timing.typicalMs !== null) {
|
|
178
|
+
parts.push(`typical ${formatMs(timing.typicalMs)} n=${timing.samples}`);
|
|
179
|
+
}
|
|
180
|
+
if (parts.length > 0) {
|
|
181
|
+
suffix = ` [${parts.join(", ")}]`;
|
|
182
|
+
}
|
|
183
|
+
}
|
|
184
|
+
console.log(`step ${step}: ${info.status} (attempts ${info.attempts})${suffix}`);
|
|
185
|
+
}
|
|
186
|
+
const live = liveness.inFlight;
|
|
187
|
+
if (live) {
|
|
188
|
+
const typical = live.typicalMs !== null ? `, typical ${formatMs(live.typicalMs)} n=${live.samples}` : "";
|
|
189
|
+
console.log(
|
|
190
|
+
`in flight: ${live.step} attempt ${live.attempt} since ${live.startedAt} (elapsed ${formatMs(live.elapsedMs)}${typical})`,
|
|
191
|
+
);
|
|
192
|
+
if (live.command) {
|
|
193
|
+
console.log(` worker: ${live.command}`);
|
|
194
|
+
}
|
|
195
|
+
if (live.lastOutputAt) {
|
|
196
|
+
console.log(` last output: ${live.lastOutputAt} (${formatMs(live.sinceOutputMs)} ago, ${live.bytes} bytes so far)`);
|
|
197
|
+
} else {
|
|
198
|
+
console.log(` last output: (none yet, ${formatMs(live.sinceOutputMs)} since start)`);
|
|
199
|
+
}
|
|
200
|
+
if (live.stalled) {
|
|
201
|
+
console.log(
|
|
202
|
+
` STALLED: no output for ${formatMs(live.sinceOutputMs)} (threshold ${formatMs(live.stallAfterMs)}); process not killed — inspect, then stop or wait`,
|
|
203
|
+
);
|
|
204
|
+
}
|
|
205
|
+
const tail = tailLive(view.repoRoot, state.run.id, 3);
|
|
206
|
+
for (const line of tail) {
|
|
207
|
+
console.log(` | ${line.slice(0, 160)}`);
|
|
208
|
+
}
|
|
83
209
|
}
|
|
84
210
|
for (const item of state.evidence) {
|
|
85
|
-
|
|
211
|
+
const ref = isAbsolute(item.ref) ? "<legacy-absolute-evidence-ref>" : item.ref;
|
|
212
|
+
const reused = item.reused ? ` (reused from ${item.reused.run})` : "";
|
|
213
|
+
console.log(`evidence [${item.status}/${item.grade}] ${item.kind} ${ref}${reused}`);
|
|
86
214
|
}
|
|
87
215
|
if (state.pendingHuman) {
|
|
88
216
|
console.log(`waiting on human: ${state.pendingHuman.transition}`);
|
|
89
217
|
for (const reason of state.pendingHuman.reasons) {
|
|
90
218
|
console.log(` reason: ${reason}`);
|
|
91
219
|
}
|
|
220
|
+
if (view.repoLabel) {
|
|
221
|
+
printNextReply(view.repoLabel, state);
|
|
222
|
+
}
|
|
92
223
|
}
|
|
93
224
|
if (state.terminal) {
|
|
94
225
|
console.log(`terminal: ${state.terminal.status} (${state.terminal.reason})`);
|
|
@@ -141,7 +272,38 @@ function loadRunConfig(flags, command) {
|
|
|
141
272
|
policies.push(parsePolicyDoc(parseYamlSubset(readFileSync(resolve(configDir, policyPath), "utf8"))));
|
|
142
273
|
}
|
|
143
274
|
|
|
275
|
+
if (config.reviewTriage !== undefined && !["required", "off"].includes(config.reviewTriage)) {
|
|
276
|
+
throw new Error(`reviewTriage must be "required" or "off", got: ${config.reviewTriage}`);
|
|
277
|
+
}
|
|
278
|
+
if (config.supersede !== undefined && !["waiting", "off"].includes(config.supersede)) {
|
|
279
|
+
throw new Error(`supersede must be "waiting" or "off", got: ${config.supersede}`);
|
|
280
|
+
}
|
|
281
|
+
if (config.stallAfterMs !== undefined && !(Number(config.stallAfterMs) > 0)) {
|
|
282
|
+
throw new Error(`stallAfterMs must be a positive number, got: ${config.stallAfterMs}`);
|
|
283
|
+
}
|
|
284
|
+
const cache = {};
|
|
285
|
+
for (const [step, mode] of Object.entries(config.cache ?? {})) {
|
|
286
|
+
if (mode !== "tree") {
|
|
287
|
+
throw new Error(`cache.${step} must be "tree", got: ${mode}`);
|
|
288
|
+
}
|
|
289
|
+
if (!workflow.stepIds.has(step)) {
|
|
290
|
+
throw new Error(`cache.${step}: step not in workflow`);
|
|
291
|
+
}
|
|
292
|
+
cache[step] = mode;
|
|
293
|
+
}
|
|
294
|
+
const redact = (config.redact ?? []).map((pattern) => {
|
|
295
|
+
try {
|
|
296
|
+
return new RegExp(String(pattern), "g");
|
|
297
|
+
} catch {
|
|
298
|
+
throw new Error(`redact pattern is not a valid regular expression: ${pattern}`);
|
|
299
|
+
}
|
|
300
|
+
});
|
|
301
|
+
const envelope = loadEnvelope(config, configDir, Object.keys(config.workers ?? {}));
|
|
302
|
+
|
|
144
303
|
return {
|
|
304
|
+
envelope,
|
|
305
|
+
cache,
|
|
306
|
+
redact,
|
|
145
307
|
repoRoot,
|
|
146
308
|
workflow,
|
|
147
309
|
workflowDigest,
|
|
@@ -157,48 +319,180 @@ function loadRunConfig(flags, command) {
|
|
|
157
319
|
maxAttemptsPerStep: config.maxAttemptsPerStep ?? 4,
|
|
158
320
|
stepTimeoutMs: config.stepTimeoutMs,
|
|
159
321
|
allowedPaths: config.allowedPaths,
|
|
322
|
+
requires: config.requires ?? [],
|
|
323
|
+
reviewTriage: config.reviewTriage === "required" ? "required" : null,
|
|
324
|
+
supersede: config.supersede ?? "waiting",
|
|
325
|
+
stallAfterMs: config.stallAfterMs !== undefined ? Number(config.stallAfterMs) : DEFAULT_STALL_AFTER_MS,
|
|
160
326
|
planDigest: digestOfWorkFile("plan.md"),
|
|
161
327
|
intentDigest: digestOfWorkFile("intent.md"),
|
|
162
328
|
};
|
|
163
329
|
}
|
|
164
330
|
|
|
165
|
-
|
|
331
|
+
// Notification config is optional and never fatal for a run: a broken file
|
|
332
|
+
// is reported once and the run proceeds without outbound messages.
|
|
333
|
+
function notifyConfigFor(repoRoot) {
|
|
334
|
+
try {
|
|
335
|
+
return { config: loadNotifyConfig(repoRoot), error: null };
|
|
336
|
+
} catch (error) {
|
|
337
|
+
return { config: null, error: error.message };
|
|
338
|
+
}
|
|
339
|
+
}
|
|
340
|
+
|
|
341
|
+
async function notifyForState(repoRoot, repoLabel, state) {
|
|
342
|
+
const { config, error } = notifyConfigFor(repoRoot);
|
|
343
|
+
if (error) {
|
|
344
|
+
console.log(`notify: config ignored (${error})`);
|
|
345
|
+
return;
|
|
346
|
+
}
|
|
347
|
+
if (!config || !state.run) {
|
|
348
|
+
return;
|
|
349
|
+
}
|
|
350
|
+
const kinds = [];
|
|
351
|
+
if (state.pendingHuman && state.run.status === "WAITING_HUMAN") {
|
|
352
|
+
kinds.push("HUMAN_REQUESTED");
|
|
353
|
+
}
|
|
354
|
+
if (state.terminal) {
|
|
355
|
+
kinds.push("RUN_TERMINAL");
|
|
356
|
+
}
|
|
357
|
+
for (const kind of kinds) {
|
|
358
|
+
if (!subscribes(config, kind)) {
|
|
359
|
+
continue;
|
|
360
|
+
}
|
|
361
|
+
const results = await dispatchNotification(config, buildNotification(kind, { repoLabel, state }), { repoRoot });
|
|
362
|
+
for (const row of results) {
|
|
363
|
+
const outcome = row.ok ? "sent" : row.skipped ? `skipped (${row.error})` : `FAILED (${row.error})`;
|
|
364
|
+
console.log(`notify ${kind} -> ${row.channel}: ${outcome}`);
|
|
365
|
+
}
|
|
366
|
+
}
|
|
367
|
+
}
|
|
368
|
+
|
|
369
|
+
// A stall watcher is a separate detached process: the orchestrator blocks in
|
|
370
|
+
// spawnSync while a worker runs, so it cannot look at the clock itself. The
|
|
371
|
+
// watcher exits on its own once the run is no longer RUNNING or the parent
|
|
372
|
+
// process is gone.
|
|
373
|
+
function spawnStallWatcher(repoRoot, runId, stallAfterMs) {
|
|
374
|
+
const { config } = notifyConfigFor(repoRoot);
|
|
375
|
+
if (!subscribes(config, "STALLED")) {
|
|
376
|
+
return false;
|
|
377
|
+
}
|
|
378
|
+
const child = spawn(
|
|
379
|
+
process.execPath,
|
|
380
|
+
[
|
|
381
|
+
fileURLToPath(import.meta.url),
|
|
382
|
+
"watch",
|
|
383
|
+
"--repo",
|
|
384
|
+
repoRoot,
|
|
385
|
+
"--run",
|
|
386
|
+
runId,
|
|
387
|
+
"--stall-after-ms",
|
|
388
|
+
String(stallAfterMs),
|
|
389
|
+
"--parent",
|
|
390
|
+
String(process.pid),
|
|
391
|
+
],
|
|
392
|
+
{ detached: true, stdio: "ignore" },
|
|
393
|
+
);
|
|
394
|
+
child.unref();
|
|
395
|
+
return true;
|
|
396
|
+
}
|
|
397
|
+
|
|
398
|
+
async function commandStart(flags) {
|
|
166
399
|
const options = loadRunConfig(flags, "start");
|
|
400
|
+
if (flags.attempt !== undefined) {
|
|
401
|
+
if (flags.attempt !== "new") {
|
|
402
|
+
throw new Error(`--attempt must be "new" (auto-number the next run of this family), got: ${flags.attempt}`);
|
|
403
|
+
}
|
|
404
|
+
// The run config names the family; the kernel numbers the attempt. One
|
|
405
|
+
// config per work, not one per retry (the campaign hand-numbered -01..-30).
|
|
406
|
+
options.runId = nextAttemptId(options.repoRoot, options.workId, options.runId);
|
|
407
|
+
console.log(`attempt: ${options.runId}`);
|
|
408
|
+
}
|
|
409
|
+
if (options.envelope) {
|
|
410
|
+
console.log(`envelope: ${options.envelope.source} (${Object.keys(options.envelope.prompts).join(", ")}) ${options.envelope.digest}`);
|
|
411
|
+
}
|
|
412
|
+
if (process.stdout.isTTY) {
|
|
413
|
+
// Run launch discipline (real incident: a host-tool timeout killed a
|
|
414
|
+
// verify worker mid-run): anything longer than minutes belongs in a
|
|
415
|
+
// detached process, not an interactive foreground shell.
|
|
416
|
+
console.log("tip: long runs should be started detached (nohup/setsid); interactive shells die with their host");
|
|
417
|
+
}
|
|
418
|
+
const watching = spawnStallWatcher(options.repoRoot, options.runId, options.stallAfterMs);
|
|
419
|
+
if (watching) {
|
|
420
|
+
console.log(`stall watcher armed (no output for ${formatMs(options.stallAfterMs)} notifies STALLED)`);
|
|
421
|
+
}
|
|
167
422
|
const result = startRun(options);
|
|
168
|
-
|
|
169
|
-
|
|
423
|
+
const repoLabel = repoLabelFor(options.repoRoot);
|
|
424
|
+
for (const run of result.superseded ?? []) {
|
|
425
|
+
console.log(`superseded ${run} (was waiting on a human for the same work; now SUPERSEDED)`);
|
|
426
|
+
}
|
|
427
|
+
for (const row of result.supersedeSkipped ?? []) {
|
|
428
|
+
console.log(`could not supersede ${row.run}: ${row.reason}`);
|
|
429
|
+
}
|
|
430
|
+
console.log(`ledger: ${toRepoRef(options.repoRoot, result.ledgerPath)}`);
|
|
431
|
+
const ledger = EventLedger.open(result.ledgerPath);
|
|
432
|
+
printState(ledger.state, ledger, {
|
|
433
|
+
repoRoot: options.repoRoot,
|
|
434
|
+
repoLabel,
|
|
435
|
+
stallAfterMs: options.stallAfterMs,
|
|
436
|
+
});
|
|
437
|
+
await notifyForState(options.repoRoot, repoLabel, ledger.state);
|
|
170
438
|
}
|
|
171
439
|
|
|
172
|
-
function commandResume(flags) {
|
|
440
|
+
async function commandResume(flags) {
|
|
173
441
|
const options = loadRunConfig(flags, "resume");
|
|
442
|
+
const watching = spawnStallWatcher(options.repoRoot, options.runId, options.stallAfterMs);
|
|
443
|
+
if (watching) {
|
|
444
|
+
console.log(`stall watcher armed (no output for ${formatMs(options.stallAfterMs)} notifies STALLED)`);
|
|
445
|
+
}
|
|
174
446
|
const result = resumeRun(options);
|
|
447
|
+
const repoLabel = repoLabelFor(options.repoRoot);
|
|
175
448
|
if (!result.resumed) {
|
|
176
449
|
console.log(`nothing to resume: ${result.reason}`);
|
|
177
450
|
}
|
|
178
|
-
console.log(`ledger: ${result.ledgerPath}`);
|
|
179
|
-
|
|
451
|
+
console.log(`ledger: ${toRepoRef(options.repoRoot, result.ledgerPath)}`);
|
|
452
|
+
const ledger = EventLedger.open(result.ledgerPath);
|
|
453
|
+
printState(ledger.state, ledger, {
|
|
454
|
+
repoRoot: options.repoRoot,
|
|
455
|
+
repoLabel,
|
|
456
|
+
stallAfterMs: options.stallAfterMs,
|
|
457
|
+
});
|
|
458
|
+
if (result.resumed) {
|
|
459
|
+
await notifyForState(options.repoRoot, repoLabel, ledger.state);
|
|
460
|
+
}
|
|
180
461
|
}
|
|
181
462
|
|
|
182
463
|
function commandInbox(flags) {
|
|
183
464
|
if (!flags.repo) {
|
|
184
465
|
throw new Error("inbox requires --repo");
|
|
185
466
|
}
|
|
186
|
-
const
|
|
467
|
+
const repoRoot = resolve(flags.repo);
|
|
468
|
+
const rows = listInbox(repoRoot);
|
|
187
469
|
if (rows.length === 0) {
|
|
188
470
|
console.log("inbox empty: no runs waiting on a human");
|
|
189
471
|
return;
|
|
190
472
|
}
|
|
191
|
-
|
|
473
|
+
const sorted = [...rows].sort((a, b) => `${a.work ?? ""}${a.run}`.localeCompare(`${b.work ?? ""}${b.run}`));
|
|
474
|
+
let lastWork = null;
|
|
475
|
+
for (const row of sorted) {
|
|
192
476
|
if (row.corrupted) {
|
|
193
477
|
console.log(`${row.run}: LEDGER CORRUPTED after seq=${row.corrupted.afterSeq} (${row.corrupted.reason})`);
|
|
194
478
|
continue;
|
|
195
479
|
}
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
480
|
+
if (row.work !== lastWork) {
|
|
481
|
+
console.log(`work ${row.work}:`);
|
|
482
|
+
lastWork = row.work;
|
|
483
|
+
}
|
|
484
|
+
const ledger = EventLedger.open(ledgerPathFor(repoRoot, row.run));
|
|
485
|
+
const requested = [...ledger.events].reverse().find((event) => event.type === "HUMAN_REQUESTED");
|
|
486
|
+
const age = requested ? formatMs(Date.now() - Date.parse(requested.ts)) : "?";
|
|
487
|
+
console.log(` ${row.run} [${row.kind}] ${row.transition} — waiting ${age}${requested ? ` (since ${requested.ts})` : ""}`);
|
|
488
|
+
console.log(` candidate: ${row.subject.candidate}`);
|
|
489
|
+
console.log(` planDigest: ${row.subject.planDigest}`);
|
|
490
|
+
console.log(` evidenceDigest: ${row.subject.evidenceDigest}`);
|
|
200
491
|
for (const reason of row.reasons) {
|
|
201
|
-
console.log(`
|
|
492
|
+
console.log(` reason: ${reason}`);
|
|
493
|
+
}
|
|
494
|
+
for (const line of nextReply({ repoLabel: repoLabelFor(repoRoot, flags.repo), state: ledger.state })) {
|
|
495
|
+
console.log(` next: ${line}`);
|
|
202
496
|
}
|
|
203
497
|
}
|
|
204
498
|
}
|
|
@@ -299,6 +593,130 @@ function commandDoctor(flags) {
|
|
|
299
593
|
console.log("push protection: repository has no remotes (nothing to protect)");
|
|
300
594
|
}
|
|
301
595
|
console.log("kernel capabilities: merge/deploy/publish have no call path in the runner (invariant 20)");
|
|
596
|
+
if (options.requires.length > 0) {
|
|
597
|
+
console.log("environment contract (requires):");
|
|
598
|
+
const check = checkRequires(options.requires);
|
|
599
|
+
for (const row of check.checked) {
|
|
600
|
+
console.log(` ${row.command}: OK${row.version ? ` (${row.version})` : ""}`);
|
|
601
|
+
}
|
|
602
|
+
for (const problem of check.problems) {
|
|
603
|
+
console.log(` PROBLEM ${problem}`);
|
|
604
|
+
}
|
|
605
|
+
} else {
|
|
606
|
+
console.log("environment contract: none declared (implicit PATH facts stay unchecked)");
|
|
607
|
+
}
|
|
608
|
+
console.log(`supersede: ${options.supersede} (new run for the same work ${options.supersede === "off" ? "leaves" : "supersedes"} older WAITING_HUMAN runs)`);
|
|
609
|
+
console.log(`stall threshold: ${formatMs(options.stallAfterMs)} without worker output`);
|
|
610
|
+
const { config: notify, error: notifyError } = notifyConfigFor(options.repoRoot);
|
|
611
|
+
if (notifyError) {
|
|
612
|
+
console.log(`notify: PROBLEM ${notifyError}`);
|
|
613
|
+
} else if (!notify) {
|
|
614
|
+
console.log(`notify: none (${NOTIFY_CONFIG} absent; a waiting run reaches nobody until someone runs inbox)`);
|
|
615
|
+
} else {
|
|
616
|
+
console.log("notify channels:");
|
|
617
|
+
for (const channel of notify.channels) {
|
|
618
|
+
const urlState = process.env[channel.urlEnv] ? "url env set" : `WARNING env ${channel.urlEnv} not set in this shell`;
|
|
619
|
+
console.log(` ${channel.id}: type=${channel.type} events=${channel.events.join(",")} (${urlState})`);
|
|
620
|
+
}
|
|
621
|
+
}
|
|
622
|
+
}
|
|
623
|
+
|
|
624
|
+
// Preflight: run one step's configured worker command directly in the main
|
|
625
|
+
// checkout — no worktree, no ledger, no evidence. Minute-level dry loops
|
|
626
|
+
// against the first failure boundary before a full run is what turned the
|
|
627
|
+
// deploy campaign's idle phase around; the output is a dry signal only and a
|
|
628
|
+
// Run must reproduce anything it finds.
|
|
629
|
+
function commandPreflight(flags) {
|
|
630
|
+
const options = loadRunConfig(flags, "preflight");
|
|
631
|
+
if (!flags.step) {
|
|
632
|
+
throw new Error("preflight requires --step");
|
|
633
|
+
}
|
|
634
|
+
const stepDef = options.workflow.steps.find((candidate) => candidate.id === flags.step);
|
|
635
|
+
if (!stepDef) {
|
|
636
|
+
throw new Error(`step not in workflow: ${flags.step}`);
|
|
637
|
+
}
|
|
638
|
+
const spec = stepDef.worker ? options.adapterConfigs[stepDef.worker] : null;
|
|
639
|
+
if (!spec) {
|
|
640
|
+
throw new Error(`step ${flags.step} has no configured worker command to preflight`);
|
|
641
|
+
}
|
|
642
|
+
if (options.requires.length > 0) {
|
|
643
|
+
const check = checkRequires(options.requires);
|
|
644
|
+
for (const problem of check.problems) {
|
|
645
|
+
console.log(`requires PROBLEM: ${problem}`);
|
|
646
|
+
}
|
|
647
|
+
}
|
|
648
|
+
const fill = (text) =>
|
|
649
|
+
String(text)
|
|
650
|
+
.replaceAll("{workspace}", options.repoRoot)
|
|
651
|
+
.replaceAll("{step}", flags.step)
|
|
652
|
+
.replaceAll("{worker}", stepDef.worker);
|
|
653
|
+
const args = (spec.args ?? []).map(fill);
|
|
654
|
+
let env;
|
|
655
|
+
if (spec.inheritEnv === true) {
|
|
656
|
+
env = { ...process.env };
|
|
657
|
+
} else {
|
|
658
|
+
env = {};
|
|
659
|
+
for (const key of ["PATH", "HOME", "LANG", "LC_ALL", "TMPDIR", "TERM", "USER", "SHELL"]) {
|
|
660
|
+
if (process.env[key] !== undefined) {
|
|
661
|
+
env[key] = process.env[key];
|
|
662
|
+
}
|
|
663
|
+
}
|
|
664
|
+
}
|
|
665
|
+
Object.assign(env, spec.env ?? {});
|
|
666
|
+
env.BUILDBEAT_PREFLIGHT = "1";
|
|
667
|
+
console.log(`PREFLIGHT (dry signal, never evidence): step ${flags.step} -> ${spec.command} ${args.join(" ")}`);
|
|
668
|
+
console.log(`cwd: main checkout (no worktree, no ledger, no evidence written)`);
|
|
669
|
+
const result = spawnSync(spec.command, args, {
|
|
670
|
+
cwd: options.repoRoot,
|
|
671
|
+
stdio: "inherit",
|
|
672
|
+
env,
|
|
673
|
+
timeout: spec.timeoutMs,
|
|
674
|
+
});
|
|
675
|
+
if (result.error) {
|
|
676
|
+
throw new Error(`preflight could not run the command: ${result.error.message}`);
|
|
677
|
+
}
|
|
678
|
+
const exitCode = result.status ?? 1;
|
|
679
|
+
console.log(`preflight exit=${exitCode} — a Run must reproduce this before it counts`);
|
|
680
|
+
process.exitCode = exitCode;
|
|
681
|
+
}
|
|
682
|
+
|
|
683
|
+
function commandFindings(rest) {
|
|
684
|
+
const [sub, ...args] = rest;
|
|
685
|
+
const flags = parseFlags(args);
|
|
686
|
+
if (sub === "list") {
|
|
687
|
+
if (!flags.repo || !flags.work) {
|
|
688
|
+
throw new Error("findings list requires --repo and --work");
|
|
689
|
+
}
|
|
690
|
+
const rows = readFindingsAccount(resolve(flags.repo), flags.work);
|
|
691
|
+
const findings = rows.filter((row) => row.kind === "finding");
|
|
692
|
+
if (findings.length === 0) {
|
|
693
|
+
console.log(`no recorded findings (${findingsAccountRef(flags.work)})`);
|
|
694
|
+
return;
|
|
695
|
+
}
|
|
696
|
+
const adjudicated = latestAdjudications(rows);
|
|
697
|
+
for (const row of findings) {
|
|
698
|
+
const verdict = adjudicated.get(row.fingerprint);
|
|
699
|
+
const status = verdict ? `${verdict.action} by ${verdict.by}` : "open";
|
|
700
|
+
const reRaised = row.reRaised ? " RE-RAISED" : "";
|
|
701
|
+
console.log(`[${row.severity} ${row.fingerprint}] (${status})${reRaised} ${row.summary}`);
|
|
702
|
+
}
|
|
703
|
+
} else if (sub === "adjudicate") {
|
|
704
|
+
if (!flags.repo || !flags.work || !flags.fingerprint || !flags.action) {
|
|
705
|
+
throw new Error("findings adjudicate requires --repo, --work, --fingerprint and --action");
|
|
706
|
+
}
|
|
707
|
+
const row = adjudicateFinding(resolve(flags.repo), flags.work, {
|
|
708
|
+
fingerprint: flags.fingerprint,
|
|
709
|
+
action: flags.action,
|
|
710
|
+
by: flags.by,
|
|
711
|
+
note: flags.note,
|
|
712
|
+
});
|
|
713
|
+
console.log(`adjudicated ${row.fingerprint} -> ${row.action} ([${row.severity}] ${row.summary})`);
|
|
714
|
+
if (row.action === "dismiss") {
|
|
715
|
+
console.log("dismissed: this fingerprint no longer blocks; an escalated severity reopens on its own");
|
|
716
|
+
}
|
|
717
|
+
} else {
|
|
718
|
+
throw new Error(`findings subcommand must be list|adjudicate, got: ${sub ?? "(none)"}`);
|
|
719
|
+
}
|
|
302
720
|
}
|
|
303
721
|
|
|
304
722
|
function commandEvents(flags) {
|
|
@@ -335,7 +753,7 @@ function commandReplay(flags) {
|
|
|
335
753
|
} else {
|
|
336
754
|
console.log(`chain OK: ${ledger.events.length} events verified (digest/prev/seq)`);
|
|
337
755
|
}
|
|
338
|
-
printState(ledger.state, { corruption: null });
|
|
756
|
+
printState(ledger.state, { corruption: null }, {});
|
|
339
757
|
}
|
|
340
758
|
|
|
341
759
|
function commandMetrics(flags) {
|
|
@@ -354,8 +772,13 @@ function commandStatus(flags) {
|
|
|
354
772
|
if (!flags.repo || !flags.run) {
|
|
355
773
|
throw new Error("status requires --repo and --run");
|
|
356
774
|
}
|
|
357
|
-
const
|
|
358
|
-
|
|
775
|
+
const repoRoot = resolve(flags.repo);
|
|
776
|
+
const ledger = EventLedger.open(ledgerPathFor(repoRoot, flags.run));
|
|
777
|
+
printState(ledger.state, ledger, {
|
|
778
|
+
repoRoot,
|
|
779
|
+
repoLabel: repoLabelFor(repoRoot, flags.repo),
|
|
780
|
+
stallAfterMs: stallAfterFromFlags(flags),
|
|
781
|
+
});
|
|
359
782
|
}
|
|
360
783
|
|
|
361
784
|
function commandStop(flags) {
|
|
@@ -385,6 +808,147 @@ function commandStop(flags) {
|
|
|
385
808
|
}
|
|
386
809
|
}
|
|
387
810
|
|
|
811
|
+
function commandOverview(flags) {
|
|
812
|
+
if (!flags.repo) {
|
|
813
|
+
throw new Error("overview requires --repo");
|
|
814
|
+
}
|
|
815
|
+
const repoRoot = resolve(flags.repo);
|
|
816
|
+
const rows = computeOverview(repoRoot, { work: flags.work ?? null, repoLabel: repoLabelFor(repoRoot, flags.repo) });
|
|
817
|
+
if (flags.json === "true") {
|
|
818
|
+
console.log(JSON.stringify(rows, null, 2));
|
|
819
|
+
return;
|
|
820
|
+
}
|
|
821
|
+
console.log(renderOverview(rows));
|
|
822
|
+
}
|
|
823
|
+
|
|
824
|
+
function commandGc(flags) {
|
|
825
|
+
if (!flags.repo) {
|
|
826
|
+
throw new Error("gc requires --repo");
|
|
827
|
+
}
|
|
828
|
+
const repoRoot = resolve(flags.repo);
|
|
829
|
+
const rows = planGc(repoRoot);
|
|
830
|
+
if (rows.length === 0) {
|
|
831
|
+
console.log("gc: no run ledgers found");
|
|
832
|
+
return;
|
|
833
|
+
}
|
|
834
|
+
let actionable = 0;
|
|
835
|
+
for (const row of rows) {
|
|
836
|
+
const summary = row.actions.map((action) => {
|
|
837
|
+
if (action.kind === "remove-worktree") {
|
|
838
|
+
return `remove worktree${action.dirty ? " (DIRTY, needs --force)" : ""}`;
|
|
839
|
+
}
|
|
840
|
+
if (action.kind === "delete-branch") {
|
|
841
|
+
return `delete branch ${action.branch} (${action.reason})`;
|
|
842
|
+
}
|
|
843
|
+
return "remove stale lock";
|
|
844
|
+
});
|
|
845
|
+
actionable += row.actions.length;
|
|
846
|
+
const keep = row.keep.map((reason) => `keep: ${reason}`);
|
|
847
|
+
const parts = [...summary, ...keep];
|
|
848
|
+
console.log(`${row.run} [${row.status}] ${parts.length > 0 ? parts.join("; ") : "nothing to do"}`);
|
|
849
|
+
}
|
|
850
|
+
if (flags.apply !== "true") {
|
|
851
|
+
console.log(
|
|
852
|
+
actionable > 0
|
|
853
|
+
? `plan only: ${actionable} action(s); rerun with --apply true to execute (branches whose candidate lives only there are always kept)`
|
|
854
|
+
: "nothing to collect",
|
|
855
|
+
);
|
|
856
|
+
return;
|
|
857
|
+
}
|
|
858
|
+
const results = applyGc(repoRoot, rows, { force: flags.force === "true" });
|
|
859
|
+
let done = 0;
|
|
860
|
+
for (const result of results) {
|
|
861
|
+
const target = result.kind === "delete-branch" ? result.branch : result.path;
|
|
862
|
+
if (result.done) {
|
|
863
|
+
done += 1;
|
|
864
|
+
console.log(` ${result.kind} ${target}: done`);
|
|
865
|
+
} else {
|
|
866
|
+
console.log(` ${result.kind} ${target}: NOT done (${result.error})`);
|
|
867
|
+
}
|
|
868
|
+
}
|
|
869
|
+
console.log(`gc applied: ${done}/${results.length} action(s)`);
|
|
870
|
+
}
|
|
871
|
+
|
|
872
|
+
function processAlive(pid) {
|
|
873
|
+
try {
|
|
874
|
+
process.kill(pid, 0);
|
|
875
|
+
return true;
|
|
876
|
+
} catch {
|
|
877
|
+
return false;
|
|
878
|
+
}
|
|
879
|
+
}
|
|
880
|
+
|
|
881
|
+
const sleep = (ms) => new Promise((resolveSleep) => setTimeout(resolveSleep, ms));
|
|
882
|
+
|
|
883
|
+
// Watches one RUNNING run and reports a stall once per step attempt. Exits
|
|
884
|
+
// when the run leaves RUNNING, the parent process (if given) dies, or after a
|
|
885
|
+
// single pass with --once true.
|
|
886
|
+
async function commandWatch(flags) {
|
|
887
|
+
if (!flags.repo || !flags.run) {
|
|
888
|
+
throw new Error("watch requires --repo and --run");
|
|
889
|
+
}
|
|
890
|
+
const repoRoot = resolve(flags.repo);
|
|
891
|
+
const stallAfterMs = stallAfterFromFlags(flags);
|
|
892
|
+
const intervalMs = Number(flags.interval ?? 30) * 1000;
|
|
893
|
+
const parent = flags.parent ? Number(flags.parent) : null;
|
|
894
|
+
const repoLabel = repoLabelFor(repoRoot, flags.repo);
|
|
895
|
+
const { config } = notifyConfigFor(repoRoot);
|
|
896
|
+
const notified = new Set();
|
|
897
|
+
let notifiedThisPass = false;
|
|
898
|
+
for (;;) {
|
|
899
|
+
notifiedThisPass = false;
|
|
900
|
+
const ledger = EventLedger.open(ledgerPathFor(repoRoot, flags.run));
|
|
901
|
+
if (!ledger.state.run) {
|
|
902
|
+
console.log(`watch: no ledger for ${flags.run} yet`);
|
|
903
|
+
} else if (ledger.state.run.status !== "RUNNING") {
|
|
904
|
+
console.log(`watch: run is ${ledger.state.run.status}; done`);
|
|
905
|
+
return;
|
|
906
|
+
} else {
|
|
907
|
+
const liveness = describeLiveness({ repoRoot, runId: flags.run, ledger, stallAfterMs });
|
|
908
|
+
const live = liveness.inFlight;
|
|
909
|
+
if (live?.stalled) {
|
|
910
|
+
const key = `${live.step}#${live.attempt}`;
|
|
911
|
+
if (!notified.has(key)) {
|
|
912
|
+
notified.add(key);
|
|
913
|
+
notifiedThisPass = true;
|
|
914
|
+
console.log(
|
|
915
|
+
`watch: STALLED ${live.step} attempt ${live.attempt} — no output for ${formatMs(live.sinceOutputMs)} (threshold ${formatMs(stallAfterMs)})`,
|
|
916
|
+
);
|
|
917
|
+
if (subscribes(config, "STALLED")) {
|
|
918
|
+
const notification = buildNotification("STALLED", {
|
|
919
|
+
repoLabel,
|
|
920
|
+
state: ledger.state,
|
|
921
|
+
detail: {
|
|
922
|
+
step: live.step,
|
|
923
|
+
attempt: live.attempt,
|
|
924
|
+
startedAt: live.startedAt,
|
|
925
|
+
elapsed: formatMs(live.elapsedMs),
|
|
926
|
+
sinceOutput: formatMs(live.sinceOutputMs),
|
|
927
|
+
threshold: formatMs(stallAfterMs),
|
|
928
|
+
command: live.command,
|
|
929
|
+
},
|
|
930
|
+
});
|
|
931
|
+
const results = await dispatchNotification(config, notification, { repoRoot });
|
|
932
|
+
for (const row of results) {
|
|
933
|
+
console.log(`watch: notify -> ${row.channel}: ${row.ok ? "sent" : row.error}`);
|
|
934
|
+
}
|
|
935
|
+
}
|
|
936
|
+
}
|
|
937
|
+
} else if (live) {
|
|
938
|
+
console.log(`watch: ${live.step} attempt ${live.attempt} elapsed ${formatMs(live.elapsedMs)}, last output ${formatMs(live.sinceOutputMs)} ago`);
|
|
939
|
+
}
|
|
940
|
+
}
|
|
941
|
+
if (flags.once === "true") {
|
|
942
|
+
return notifiedThisPass;
|
|
943
|
+
}
|
|
944
|
+
if (parent !== null && !processAlive(parent)) {
|
|
945
|
+
console.log("watch: parent process gone; done");
|
|
946
|
+
return;
|
|
947
|
+
}
|
|
948
|
+
await sleep(intervalMs);
|
|
949
|
+
}
|
|
950
|
+
}
|
|
951
|
+
|
|
388
952
|
function commandObserve(rest) {
|
|
389
953
|
const [sub, ...args] = rest;
|
|
390
954
|
const flags = parseFlags(args);
|
|
@@ -393,7 +957,7 @@ function commandObserve(rest) {
|
|
|
393
957
|
throw new Error("observe run requires --config <observe.yaml>");
|
|
394
958
|
}
|
|
395
959
|
const result = runObserveCycle({ configPath: flags.config });
|
|
396
|
-
console.log(`observe cycle ${result.cycle} finished (ledger: ${result.
|
|
960
|
+
console.log(`observe cycle ${result.cycle} finished (ledger: ${result.ledgerRef})`);
|
|
397
961
|
for (const row of result.results) {
|
|
398
962
|
const bands = row.bands.length > 0 ? ` bands=${row.bands.join(",")}` : "";
|
|
399
963
|
const intent = row.intent ? ` intent=${row.intent.outcome}:${row.intent.intentRef}` : "";
|
|
@@ -443,11 +1007,11 @@ function commandObserve(rest) {
|
|
|
443
1007
|
}
|
|
444
1008
|
}
|
|
445
1009
|
|
|
446
|
-
function main() {
|
|
1010
|
+
async function main() {
|
|
447
1011
|
const [command, ...rest] = process.argv.slice(2);
|
|
448
|
-
if (command === "observe") {
|
|
1012
|
+
if (command === "observe" || command === "findings") {
|
|
449
1013
|
try {
|
|
450
|
-
commandObserve(rest);
|
|
1014
|
+
(command === "observe" ? commandObserve : commandFindings)(rest);
|
|
451
1015
|
} catch (error) {
|
|
452
1016
|
console.error(`error: ${error.message}`);
|
|
453
1017
|
process.exitCode = 1;
|
|
@@ -457,9 +1021,9 @@ function main() {
|
|
|
457
1021
|
try {
|
|
458
1022
|
const flags = parseFlags(rest);
|
|
459
1023
|
if (command === "start") {
|
|
460
|
-
commandStart(flags);
|
|
1024
|
+
await commandStart(flags);
|
|
461
1025
|
} else if (command === "resume") {
|
|
462
|
-
commandResume(flags);
|
|
1026
|
+
await commandResume(flags);
|
|
463
1027
|
} else if (command === "inbox") {
|
|
464
1028
|
commandInbox(flags);
|
|
465
1029
|
} else if (command === "approve") {
|
|
@@ -480,6 +1044,14 @@ function main() {
|
|
|
480
1044
|
commandStatus(flags);
|
|
481
1045
|
} else if (command === "stop") {
|
|
482
1046
|
commandStop(flags);
|
|
1047
|
+
} else if (command === "gc") {
|
|
1048
|
+
commandGc(flags);
|
|
1049
|
+
} else if (command === "overview") {
|
|
1050
|
+
commandOverview(flags);
|
|
1051
|
+
} else if (command === "watch") {
|
|
1052
|
+
await commandWatch(flags);
|
|
1053
|
+
} else if (command === "preflight") {
|
|
1054
|
+
commandPreflight(flags);
|
|
483
1055
|
} else {
|
|
484
1056
|
process.stdout.write(USAGE);
|
|
485
1057
|
process.exitCode = command ? 2 : 0;
|
|
@@ -491,4 +1063,4 @@ function main() {
|
|
|
491
1063
|
}
|
|
492
1064
|
}
|
|
493
1065
|
|
|
494
|
-
main();
|
|
1066
|
+
await main();
|