@haiyangbg/buildbeat 1.20.0 → 2.0.0-beta.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +29 -7
- package/README.en.md +6 -4
- package/README.md +6 -4
- package/SKILL.md +33 -2
- package/bin/buildbeat-v2.js +6 -0
- package/docs/BuildBeat v2/357/274/232AI /345/216/237/347/224/237/350/275/257/344/273/266/344/272/244/344/273/230/346/216/247/345/210/266/345/271/263/351/235/242.md" +2053 -0
- package/docs/CAPABILITY-MATRIX.md +4 -4
- package/docs/CLI.md +6 -6
- package/docs/EXECUTION-PLAN.md +9 -9
- package/docs/PHASE4-STABILITY-AUDIT-2026-08-25.md +10 -8
- package/docs/PHASE4-V1.20-PILOT-2026-08-25.md +4 -0
- package/docs/RELEASING.md +6 -6
- package/docs/ROADMAP.md +16 -14
- package/docs/V1.21-RELEASE-EVIDENCE-2026-08-25.md +55 -0
- package/docs/V2-D2-DECISION-CARD.md +37 -0
- package/docs/V2-DECISIONS.md +11 -0
- package/docs/V2-ITERATION-01.md +60 -0
- package/docs/V2-ITERATION-02.md +32 -0
- package/docs/V2-ITERATION-03.md +30 -0
- package/docs/V2-ITERATION-04.md +29 -0
- package/docs/V2-ITERATION-05.md +20 -0
- package/docs/V2-ITERATION-06.md +18 -0
- package/docs/V2-ITERATION-07.md +36 -0
- package/docs/V2-PLAN.md +333 -0
- package/docs/V2-PROPOSAL.md +319 -0
- package/docs/WP4.3-RELEASE-EVIDENCE-2026-08-25.md +73 -0
- package/docs/v2/M1-ACCEPTANCE-2026-08-28.md +38 -0
- package/docs/v2/M2-DOD-2026-08-28.md +34 -0
- package/docs/v2/M4-CHICKAI-PILOT-2026-08-28.md +44 -0
- package/docs/v2/M4-EXTERNAL-PILOT-2026-08-28.md +46 -0
- package/docs/v2/M4-SELFHOST-2026-08-28.md +53 -0
- package/docs/v2/RFC-0001-product-definition.md +92 -0
- package/docs/v2/RFC-0002-domain-model.md +149 -0
- package/docs/v2/RFC-0003-workflow-policy.md +204 -0
- package/docs/v2/SPEC-0001-events-v1.md +98 -0
- package/docs/v2/guide/01-quickstart.md +92 -0
- package/docs/v2/guide/02-workflow-guide.md +42 -0
- package/docs/v2/guide/03-policy-guide.md +53 -0
- package/docs/v2/guide/04-adapter-guide.md +45 -0
- package/docs/v2/guide/05-worker-contract.md +37 -0
- package/docs/v2/guide/06-evidence-guide.md +38 -0
- package/docs/v2/guide/07-approval-guide.md +34 -0
- package/docs/v2/guide/08-migration-v1.md +68 -0
- package/docs/v2/guide/09-security-boundaries.md +28 -0
- package/docs/v2/guide/10-recovery.md +55 -0
- package/docs/v2/guide/README.md +18 -0
- package/example/.buildbeat/manifest.json +3 -3
- package/example/BUILDBEAT.md +1 -1
- package/example/README.md +22 -0
- package/lessons.md +8 -0
- package/package.json +4 -2
- package/src/constants.js +4 -1
- package/src/project.js +6 -1
- package/src/v2/adapters/mock.js +67 -0
- package/src/v2/adapters/shell.js +78 -0
- package/src/v2/cli/run.js +494 -0
- package/src/v2/domain/event-registry.js +100 -0
- package/src/v2/domain/model.js +61 -0
- package/src/v2/engine/reducer.js +253 -0
- package/src/v2/engine/risk-preset.js +48 -0
- package/src/v2/engine/workflow.js +201 -0
- package/src/v2/engine/yaml-subset.js +194 -0
- package/src/v2/evidence/collector.js +63 -0
- package/src/v2/observe/observe-config.js +194 -0
- package/src/v2/observe/observe-reducer.js +117 -0
- package/src/v2/observe/observe.js +420 -0
- package/src/v2/policy/policy.js +302 -0
- package/src/v2/presets/observe.yaml +45 -0
- package/src/v2/presets/policies/ui-render-gate.yaml +13 -0
- package/src/v2/presets/risk/controlled.yaml +39 -0
- package/src/v2/presets/risk/fast.yaml +19 -0
- package/src/v2/presets/risk/legacy-four-gates.yaml +44 -0
- package/src/v2/presets/risk/standard.yaml +28 -0
- package/src/v2/presets/software-delivery.yaml +39 -0
- package/src/v2/runtime/decisions.js +288 -0
- package/src/v2/runtime/metrics.js +140 -0
- package/src/v2/runtime/orchestrator.js +754 -0
- package/src/v2/runtime/run-record.js +55 -0
- package/src/v2/storage/event-ledger.js +154 -0
- package/src/v2/workspace/workspace-manager.js +137 -0
- package/templates/AGENTS.md +21 -0
- package/templates//346/214/207/346/214/245/345/217/260.md +23 -0
package/src/project.js
CHANGED
|
@@ -171,7 +171,12 @@ export function validateManifest(data, target) {
|
|
|
171
171
|
if (typeof data.scaffoldVersion !== "string" || !/^v\d+\.\d+(?:\.\d+)?$/.test(data.scaffoldVersion)) {
|
|
172
172
|
addIssue("manifest.invalid_scaffold_version", "Manifest scaffoldVersion is missing or invalid.");
|
|
173
173
|
}
|
|
174
|
-
|
|
174
|
+
// Three-part SemVer with an optional pre-release tag: v2 ships beta package
|
|
175
|
+
// versions (e.g. 2.0.0-beta.1) while the v1 scaffold surface stays frozen.
|
|
176
|
+
if (
|
|
177
|
+
typeof data.cliVersion !== "string" ||
|
|
178
|
+
!/^\d+\.\d+\.\d+(?:-[0-9A-Za-z]+(?:\.[0-9A-Za-z]+)*)?$/.test(data.cliVersion)
|
|
179
|
+
) {
|
|
175
180
|
addIssue("manifest.invalid_cli_version", "Manifest cliVersion is missing or invalid.");
|
|
176
181
|
}
|
|
177
182
|
if (!["default", "compact"].includes(data.layout)) {
|
|
@@ -0,0 +1,67 @@
|
|
|
1
|
+
// Mock adapter for deterministic engine tests: scripted behaviors per step,
|
|
2
|
+
// consumed one per attempt. Covers the failure shapes the kernel must survive
|
|
3
|
+
// (fail, timeout, crash, invalid output) without any real process. An entry
|
|
4
|
+
// may be a plain behavior string or { behavior, envelope } to also return a
|
|
5
|
+
// worker output envelope (e.g. reviewer findings).
|
|
6
|
+
|
|
7
|
+
export class MockScriptError extends Error {
|
|
8
|
+
constructor(message) {
|
|
9
|
+
super(message);
|
|
10
|
+
this.name = "MockScriptError";
|
|
11
|
+
}
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
const BEHAVIORS = new Set(["succeed", "fail", "timeout", "crash", "invalid-output"]);
|
|
15
|
+
|
|
16
|
+
export function createMockAdapter(script) {
|
|
17
|
+
const remaining = {};
|
|
18
|
+
for (const [step, entries] of Object.entries(script ?? {})) {
|
|
19
|
+
for (const entry of entries) {
|
|
20
|
+
const behavior = typeof entry === "string" ? entry : entry.behavior;
|
|
21
|
+
if (!BEHAVIORS.has(behavior)) {
|
|
22
|
+
throw new MockScriptError(`unknown mock behavior: ${behavior}`);
|
|
23
|
+
}
|
|
24
|
+
}
|
|
25
|
+
remaining[step] = [...entries];
|
|
26
|
+
}
|
|
27
|
+
return {
|
|
28
|
+
name: "mock",
|
|
29
|
+
execute({ step }) {
|
|
30
|
+
const queue = remaining[step];
|
|
31
|
+
if (!queue || queue.length === 0) {
|
|
32
|
+
throw new MockScriptError(`mock adapter has no scripted behavior left for step: ${step}`);
|
|
33
|
+
}
|
|
34
|
+
const entry = queue.shift();
|
|
35
|
+
const behavior = typeof entry === "string" ? entry : entry.behavior;
|
|
36
|
+
const envelope = typeof entry === "string" ? undefined : entry.envelope;
|
|
37
|
+
const startedAt = new Date().toISOString();
|
|
38
|
+
const finishedAt = startedAt;
|
|
39
|
+
const base = {
|
|
40
|
+
adapter: "mock",
|
|
41
|
+
command: `mock:${step}:${behavior}`,
|
|
42
|
+
signal: null,
|
|
43
|
+
stdout: "",
|
|
44
|
+
stderr: "",
|
|
45
|
+
timedOut: false,
|
|
46
|
+
spawnError: null,
|
|
47
|
+
startedAt,
|
|
48
|
+
finishedAt,
|
|
49
|
+
envelope,
|
|
50
|
+
};
|
|
51
|
+
switch (behavior) {
|
|
52
|
+
case "succeed":
|
|
53
|
+
return { ...base, exitCode: 0 };
|
|
54
|
+
case "fail":
|
|
55
|
+
return { ...base, exitCode: 1, stderr: "mock failure" };
|
|
56
|
+
case "timeout":
|
|
57
|
+
return { ...base, exitCode: null, timedOut: true };
|
|
58
|
+
case "crash":
|
|
59
|
+
return { ...base, exitCode: null, signal: "SIGKILL" };
|
|
60
|
+
case "invalid-output":
|
|
61
|
+
return { ...base, exitCode: 0, stdout: "not-a-valid-worker-envelope" };
|
|
62
|
+
default:
|
|
63
|
+
throw new MockScriptError(`unreachable behavior: ${behavior}`);
|
|
64
|
+
}
|
|
65
|
+
},
|
|
66
|
+
};
|
|
67
|
+
}
|
|
@@ -0,0 +1,78 @@
|
|
|
1
|
+
// Shell adapter: runs one configured command per step invocation inside the
|
|
2
|
+
// step's workspace. This is the vendor-neutral path to any CLI agent
|
|
3
|
+
// (claude -p, codex exec, plain scripts). Adapters never touch kernel state;
|
|
4
|
+
// they only return what actually happened — the orchestrator writes events.
|
|
5
|
+
|
|
6
|
+
import { spawnSync } from "node:child_process";
|
|
7
|
+
|
|
8
|
+
export class AdapterConfigError extends Error {
|
|
9
|
+
constructor(message) {
|
|
10
|
+
super(message);
|
|
11
|
+
this.name = "AdapterConfigError";
|
|
12
|
+
}
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
function fillTemplate(text, context) {
|
|
16
|
+
return text
|
|
17
|
+
.replaceAll("{workspace}", context.workspacePath)
|
|
18
|
+
.replaceAll("{step}", context.step)
|
|
19
|
+
.replaceAll("{worker}", context.worker);
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
// Workers get a minimal environment by default: host credentials living in
|
|
23
|
+
// env vars never reach a worker process unless the adapter explicitly opts
|
|
24
|
+
// in with inheritEnv (which doctor reports as ADVISORY-only isolation).
|
|
25
|
+
const DEFAULT_ENV_KEYS = ["PATH", "HOME", "LANG", "LC_ALL", "TMPDIR", "TERM", "USER", "SHELL"];
|
|
26
|
+
|
|
27
|
+
export function createShellAdapter(config) {
|
|
28
|
+
if (!config || typeof config.command !== "string" || config.command.length === 0) {
|
|
29
|
+
throw new AdapterConfigError("shell adapter requires a command");
|
|
30
|
+
}
|
|
31
|
+
const name = config.name ?? "shell";
|
|
32
|
+
const envMode = config.inheritEnv === true ? "inherit" : "allowlist";
|
|
33
|
+
return {
|
|
34
|
+
name,
|
|
35
|
+
envMode,
|
|
36
|
+
execute({ step, worker, workspacePath, input, timeoutMs, outputPath }) {
|
|
37
|
+
const context = { step, worker, workspacePath };
|
|
38
|
+
const args = (config.args ?? []).map((arg) => fillTemplate(String(arg), context));
|
|
39
|
+
const startedAt = new Date().toISOString();
|
|
40
|
+
let env;
|
|
41
|
+
if (config.inheritEnv === true) {
|
|
42
|
+
env = { ...process.env };
|
|
43
|
+
} else {
|
|
44
|
+
env = {};
|
|
45
|
+
for (const key of DEFAULT_ENV_KEYS) {
|
|
46
|
+
if (process.env[key] !== undefined) {
|
|
47
|
+
env[key] = process.env[key];
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
Object.assign(env, config.env ?? {});
|
|
52
|
+
env.BUILDBEAT_INPUT = JSON.stringify(input ?? {});
|
|
53
|
+
if (outputPath) {
|
|
54
|
+
env.BUILDBEAT_OUTPUT = outputPath;
|
|
55
|
+
}
|
|
56
|
+
const result = spawnSync(config.command, args, {
|
|
57
|
+
cwd: workspacePath,
|
|
58
|
+
encoding: "utf8",
|
|
59
|
+
timeout: timeoutMs ?? config.timeoutMs,
|
|
60
|
+
env,
|
|
61
|
+
maxBuffer: 16 * 1024 * 1024,
|
|
62
|
+
});
|
|
63
|
+
const finishedAt = new Date().toISOString();
|
|
64
|
+
return {
|
|
65
|
+
adapter: name,
|
|
66
|
+
command: [config.command, ...args].join(" "),
|
|
67
|
+
exitCode: result.status,
|
|
68
|
+
signal: result.signal ?? null,
|
|
69
|
+
stdout: result.stdout ?? "",
|
|
70
|
+
stderr: result.stderr ?? "",
|
|
71
|
+
timedOut: result.error?.code === "ETIMEDOUT",
|
|
72
|
+
spawnError: result.error && result.error.code !== "ETIMEDOUT" ? result.error.message : null,
|
|
73
|
+
startedAt,
|
|
74
|
+
finishedAt,
|
|
75
|
+
};
|
|
76
|
+
},
|
|
77
|
+
};
|
|
78
|
+
}
|
|
@@ -0,0 +1,494 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
// M1 runtime CLI: run start / status / stop for a single foreground run.
|
|
3
|
+
// Deliberately thin — all facts live in the event ledger; this file only
|
|
4
|
+
// parses input, wires adapters, and renders derived state.
|
|
5
|
+
|
|
6
|
+
import { execFileSync } from "node:child_process";
|
|
7
|
+
import { createHash } from "node:crypto";
|
|
8
|
+
import { existsSync, readFileSync } from "node:fs";
|
|
9
|
+
import { dirname, join, resolve } from "node:path";
|
|
10
|
+
|
|
11
|
+
import { createShellAdapter } from "../adapters/shell.js";
|
|
12
|
+
import { loadRiskPreset } from "../engine/risk-preset.js";
|
|
13
|
+
import { loadWorkflow } from "../engine/workflow.js";
|
|
14
|
+
import { parseYamlSubset } from "../engine/yaml-subset.js";
|
|
15
|
+
import { parsePolicyDoc } from "../policy/policy.js";
|
|
16
|
+
import { observeStatus, runObserveCycle, triageIntent } from "../observe/observe.js";
|
|
17
|
+
import { acceptArtifact, approveRun, listInbox, rejectRun } from "../runtime/decisions.js";
|
|
18
|
+
import { computeMetrics, renderMetrics } from "../runtime/metrics.js";
|
|
19
|
+
import { writeRunRecord } from "../runtime/run-record.js";
|
|
20
|
+
import { resumeRun, startRun } from "../runtime/orchestrator.js";
|
|
21
|
+
import { EventLedger } from "../storage/event-ledger.js";
|
|
22
|
+
import { acquireLock, releaseLock } from "../workspace/workspace-manager.js";
|
|
23
|
+
|
|
24
|
+
const KERNEL = { kind: "kernel", id: "cli" };
|
|
25
|
+
|
|
26
|
+
const USAGE = `BuildBeat v2 runtime
|
|
27
|
+
|
|
28
|
+
Usage:
|
|
29
|
+
run.js start --config <run-config.yaml>
|
|
30
|
+
run.js resume --config <run-config.yaml>
|
|
31
|
+
run.js status --repo <path> --run <RUN-ID>
|
|
32
|
+
run.js inbox --repo <path>
|
|
33
|
+
run.js approve --repo <path> --run <RUN-ID> --transition <t> [--by <name>] [--config <run-config.yaml>]
|
|
34
|
+
run.js reject --repo <path> --run <RUN-ID> [--transition <t>] [--reason <text>] [--by <name>]
|
|
35
|
+
run.js accept --repo <path> --work <WORK-ID> --artifact <plan|intent|spec> [--by <name>]
|
|
36
|
+
run.js doctor --config <run-config.yaml>
|
|
37
|
+
run.js events --repo <path> --run <RUN-ID>
|
|
38
|
+
run.js replay --repo <path> --run <RUN-ID>
|
|
39
|
+
run.js metrics --repo <path> [--json true]
|
|
40
|
+
run.js stop --repo <path> --run <RUN-ID> --reason <text>
|
|
41
|
+
run.js observe run --config <observe.yaml>
|
|
42
|
+
run.js observe status --repo <path>
|
|
43
|
+
run.js observe triage --repo <path> --intent <ref> --action <fix_now|schedule|dismiss> [--by <name>] [--note <text>]
|
|
44
|
+
`;
|
|
45
|
+
|
|
46
|
+
function parseFlags(argv) {
|
|
47
|
+
const flags = {};
|
|
48
|
+
for (let index = 0; index < argv.length; index += 2) {
|
|
49
|
+
const key = argv[index];
|
|
50
|
+
const value = argv[index + 1];
|
|
51
|
+
if (!key?.startsWith("--") || value === undefined) {
|
|
52
|
+
throw new Error(`bad arguments near: ${key ?? "(end)"}`);
|
|
53
|
+
}
|
|
54
|
+
flags[key.slice(2)] = value;
|
|
55
|
+
}
|
|
56
|
+
return flags;
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
function ledgerPathFor(repo, runId) {
|
|
60
|
+
return join(resolve(repo), ".buildbeat", "runtime", "runs", runId, "events.jsonl");
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
function printState(state, ledger) {
|
|
64
|
+
if (ledger.corruption) {
|
|
65
|
+
console.log(
|
|
66
|
+
`WARNING: ledger corrupted after seq=${ledger.corruption.afterSeq} at line ${ledger.corruption.atLine}: ${ledger.corruption.reason}`,
|
|
67
|
+
);
|
|
68
|
+
}
|
|
69
|
+
if (!state.run) {
|
|
70
|
+
console.log("no run recorded");
|
|
71
|
+
return;
|
|
72
|
+
}
|
|
73
|
+
console.log(`run: ${state.run.id} (work ${state.run.work})`);
|
|
74
|
+
console.log(`status: ${state.run.status}`);
|
|
75
|
+
console.log(`workflow: ${state.run.workflowRef} @ ${state.run.workflowDigest}`);
|
|
76
|
+
for (const [id, workspace] of Object.entries(state.workspaces)) {
|
|
77
|
+
console.log(
|
|
78
|
+
`workspace ${id}: base ${workspace.base.slice(0, 7)} candidate ${workspace.candidate ? workspace.candidate.slice(0, 7) : "(none)"}`,
|
|
79
|
+
);
|
|
80
|
+
}
|
|
81
|
+
for (const [step, info] of Object.entries(state.steps)) {
|
|
82
|
+
console.log(`step ${step}: ${info.status} (attempts ${info.attempts})`);
|
|
83
|
+
}
|
|
84
|
+
for (const item of state.evidence) {
|
|
85
|
+
console.log(`evidence [${item.status}/${item.grade}] ${item.kind} ${item.ref}`);
|
|
86
|
+
}
|
|
87
|
+
if (state.pendingHuman) {
|
|
88
|
+
console.log(`waiting on human: ${state.pendingHuman.transition}`);
|
|
89
|
+
for (const reason of state.pendingHuman.reasons) {
|
|
90
|
+
console.log(` reason: ${reason}`);
|
|
91
|
+
}
|
|
92
|
+
}
|
|
93
|
+
if (state.terminal) {
|
|
94
|
+
console.log(`terminal: ${state.terminal.status} (${state.terminal.reason})`);
|
|
95
|
+
}
|
|
96
|
+
if (state.compacted) {
|
|
97
|
+
console.log(`compacted: ${state.compacted.runRecordRef}`);
|
|
98
|
+
}
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
function loadRunConfig(flags, command) {
|
|
102
|
+
if (!flags.config) {
|
|
103
|
+
throw new Error(`${command} requires --config`);
|
|
104
|
+
}
|
|
105
|
+
const configPath = resolve(flags.config);
|
|
106
|
+
const config = parseYamlSubset(readFileSync(configPath, "utf8"));
|
|
107
|
+
const configDir = dirname(configPath);
|
|
108
|
+
const repoRoot = resolve(configDir, config.repo);
|
|
109
|
+
const workflowPath = resolve(configDir, config.workflow);
|
|
110
|
+
const workflowText = readFileSync(workflowPath, "utf8");
|
|
111
|
+
const workflow = loadWorkflow(workflowPath);
|
|
112
|
+
const workflowDigest = `sha256:${createHash("sha256").update(workflowText, "utf8").digest("hex")}`;
|
|
113
|
+
|
|
114
|
+
const adapters = {};
|
|
115
|
+
for (const [worker, spec] of Object.entries(config.workers ?? {})) {
|
|
116
|
+
adapters[worker] = createShellAdapter({
|
|
117
|
+
name: `shell:${worker}`,
|
|
118
|
+
command: spec.command,
|
|
119
|
+
args: spec.args ?? [],
|
|
120
|
+
timeoutMs: spec.timeoutMs,
|
|
121
|
+
});
|
|
122
|
+
}
|
|
123
|
+
const digestOfWorkFile = (name) => {
|
|
124
|
+
const filePath = join(repoRoot, "delivery", "work", config.work, name);
|
|
125
|
+
if (!existsSync(filePath)) {
|
|
126
|
+
return undefined;
|
|
127
|
+
}
|
|
128
|
+
return `sha256:${createHash("sha256").update(readFileSync(filePath, "utf8"), "utf8").digest("hex")}`;
|
|
129
|
+
};
|
|
130
|
+
|
|
131
|
+
const policies = [];
|
|
132
|
+
let presetStopAt = null;
|
|
133
|
+
let riskPreset = "standard";
|
|
134
|
+
if (config.riskPreset) {
|
|
135
|
+
const preset = loadRiskPreset(config.riskPreset);
|
|
136
|
+
policies.push(...preset.policies);
|
|
137
|
+
presetStopAt = preset.stopAt;
|
|
138
|
+
riskPreset = preset.name;
|
|
139
|
+
}
|
|
140
|
+
for (const policyPath of config.policies ?? []) {
|
|
141
|
+
policies.push(parsePolicyDoc(parseYamlSubset(readFileSync(resolve(configDir, policyPath), "utf8"))));
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
return {
|
|
145
|
+
repoRoot,
|
|
146
|
+
workflow,
|
|
147
|
+
workflowDigest,
|
|
148
|
+
workId: config.work,
|
|
149
|
+
runId: config.run,
|
|
150
|
+
base: config.base ?? "HEAD",
|
|
151
|
+
entry: config.entry ?? workflow.entry,
|
|
152
|
+
stopAt: config.stopAt ?? presetStopAt ?? [],
|
|
153
|
+
adapters,
|
|
154
|
+
adapterConfigs: config.workers ?? {},
|
|
155
|
+
policies,
|
|
156
|
+
riskPreset,
|
|
157
|
+
maxAttemptsPerStep: config.maxAttemptsPerStep ?? 4,
|
|
158
|
+
stepTimeoutMs: config.stepTimeoutMs,
|
|
159
|
+
allowedPaths: config.allowedPaths,
|
|
160
|
+
planDigest: digestOfWorkFile("plan.md"),
|
|
161
|
+
intentDigest: digestOfWorkFile("intent.md"),
|
|
162
|
+
};
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
function commandStart(flags) {
|
|
166
|
+
const options = loadRunConfig(flags, "start");
|
|
167
|
+
const result = startRun(options);
|
|
168
|
+
console.log(`ledger: ${result.ledgerPath}`);
|
|
169
|
+
printState(result.state, { corruption: null });
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
function commandResume(flags) {
|
|
173
|
+
const options = loadRunConfig(flags, "resume");
|
|
174
|
+
const result = resumeRun(options);
|
|
175
|
+
if (!result.resumed) {
|
|
176
|
+
console.log(`nothing to resume: ${result.reason}`);
|
|
177
|
+
}
|
|
178
|
+
console.log(`ledger: ${result.ledgerPath}`);
|
|
179
|
+
printState(result.state, { corruption: null });
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
function commandInbox(flags) {
|
|
183
|
+
if (!flags.repo) {
|
|
184
|
+
throw new Error("inbox requires --repo");
|
|
185
|
+
}
|
|
186
|
+
const rows = listInbox(resolve(flags.repo));
|
|
187
|
+
if (rows.length === 0) {
|
|
188
|
+
console.log("inbox empty: no runs waiting on a human");
|
|
189
|
+
return;
|
|
190
|
+
}
|
|
191
|
+
for (const row of rows) {
|
|
192
|
+
if (row.corrupted) {
|
|
193
|
+
console.log(`${row.run}: LEDGER CORRUPTED after seq=${row.corrupted.afterSeq} (${row.corrupted.reason})`);
|
|
194
|
+
continue;
|
|
195
|
+
}
|
|
196
|
+
console.log(`${row.run} (work ${row.work}) [${row.kind}] ${row.transition}`);
|
|
197
|
+
console.log(` candidate: ${row.subject.candidate}`);
|
|
198
|
+
console.log(` planDigest: ${row.subject.planDigest}`);
|
|
199
|
+
console.log(` evidenceDigest: ${row.subject.evidenceDigest}`);
|
|
200
|
+
for (const reason of row.reasons) {
|
|
201
|
+
console.log(` reason: ${reason}`);
|
|
202
|
+
}
|
|
203
|
+
}
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
function commandApprove(flags) {
|
|
207
|
+
if (!flags.repo || !flags.run || !flags.transition) {
|
|
208
|
+
throw new Error("approve requires --repo, --run and --transition");
|
|
209
|
+
}
|
|
210
|
+
const policies = flags.config ? loadRunConfig(flags, "approve").policies : [];
|
|
211
|
+
const result = approveRun(resolve(flags.repo), flags.run, {
|
|
212
|
+
by: flags.by ?? "human",
|
|
213
|
+
transition: flags.transition,
|
|
214
|
+
policies,
|
|
215
|
+
});
|
|
216
|
+
if (!result.approved) {
|
|
217
|
+
console.log("NOT approved: the subject changed since the request; a refreshed request was filed");
|
|
218
|
+
console.log(` new candidate: ${result.subject.candidate}`);
|
|
219
|
+
return;
|
|
220
|
+
}
|
|
221
|
+
console.log(`approved ${result.transition} as ${result.decisionRef}`);
|
|
222
|
+
console.log(` candidate: ${result.subject.candidate}`);
|
|
223
|
+
console.log(` planDigest: ${result.subject.planDigest}`);
|
|
224
|
+
for (const warning of result.warnings ?? []) {
|
|
225
|
+
console.log(` advisory: ${warning}`);
|
|
226
|
+
}
|
|
227
|
+
if (result.terminal) {
|
|
228
|
+
console.log("run is terminal: SUCCEEDED (merge itself stays a manual external action)");
|
|
229
|
+
} else {
|
|
230
|
+
console.log("decision recorded; continue with: run.js resume --config <run-config.yaml>");
|
|
231
|
+
}
|
|
232
|
+
}
|
|
233
|
+
|
|
234
|
+
function commandReject(flags) {
|
|
235
|
+
if (!flags.repo || !flags.run) {
|
|
236
|
+
throw new Error("reject requires --repo and --run");
|
|
237
|
+
}
|
|
238
|
+
const result = rejectRun(resolve(flags.repo), flags.run, {
|
|
239
|
+
by: flags.by ?? "human",
|
|
240
|
+
transition: flags.transition,
|
|
241
|
+
reason: flags.reason,
|
|
242
|
+
});
|
|
243
|
+
console.log(`rejected as ${result.decisionRef}; run cancelled and compacted`);
|
|
244
|
+
}
|
|
245
|
+
|
|
246
|
+
function commandAccept(flags) {
|
|
247
|
+
if (!flags.repo || !flags.work || !flags.artifact) {
|
|
248
|
+
throw new Error("accept requires --repo, --work and --artifact");
|
|
249
|
+
}
|
|
250
|
+
const result = acceptArtifact(resolve(flags.repo), flags.work, flags.artifact, {
|
|
251
|
+
by: flags.by ?? "human",
|
|
252
|
+
});
|
|
253
|
+
console.log(`accepted ${flags.artifact} as ${result.decisionRef}`);
|
|
254
|
+
console.log(` digest: ${result.digest}`);
|
|
255
|
+
console.log(" note: editing the artifact after acceptance makes this acceptance stale");
|
|
256
|
+
}
|
|
257
|
+
|
|
258
|
+
function commandDoctor(flags) {
|
|
259
|
+
const options = loadRunConfig(flags, "doctor");
|
|
260
|
+
console.log(`risk preset: ${options.riskPreset}`);
|
|
261
|
+
console.log(`stopAt boundaries: ${options.stopAt.length > 0 ? options.stopAt.join(", ") : "(none)"}`);
|
|
262
|
+
console.log("policies (declared vs actually achieved enforcement):");
|
|
263
|
+
if (options.policies.length === 0) {
|
|
264
|
+
console.log(" (none configured)");
|
|
265
|
+
}
|
|
266
|
+
for (const policy of options.policies) {
|
|
267
|
+
let actual;
|
|
268
|
+
if (policy.enforcement === "ADVISORY") {
|
|
269
|
+
actual = "ADVISORY (prompt-level only; nothing stops a worker)";
|
|
270
|
+
} else if (policy.enforcement === "SERVER_ENFORCED") {
|
|
271
|
+
actual = "UNVERIFIED (branch protection/CI cannot be verified from here; not claiming it)";
|
|
272
|
+
} else if (policy.type === "transition") {
|
|
273
|
+
actual = "LOCAL_ENFORCED (approve gate refuses the stamp)";
|
|
274
|
+
} else {
|
|
275
|
+
actual = "LOCAL_ENFORCED (runner gate before/after the step)";
|
|
276
|
+
}
|
|
277
|
+
console.log(` ${policy.name}: type=${policy.type} appliesTo=${policy.appliesTo} declared=${policy.enforcement} actual=${actual}`);
|
|
278
|
+
}
|
|
279
|
+
console.log("worker isolation:");
|
|
280
|
+
for (const [worker, spec] of Object.entries(options.adapterConfigs)) {
|
|
281
|
+
const mode = spec.inheritEnv === true
|
|
282
|
+
? "WARNING inherit (worker sees the full host env; credential isolation is ADVISORY only)"
|
|
283
|
+
: "env allowlist (host credential env vars withheld from the worker)";
|
|
284
|
+
console.log(` ${worker}: ${mode}`);
|
|
285
|
+
}
|
|
286
|
+
let remotes = [];
|
|
287
|
+
try {
|
|
288
|
+
remotes = execFileSync("git", ["-C", options.repoRoot, "remote"], { encoding: "utf8" })
|
|
289
|
+
.split("\n")
|
|
290
|
+
.filter(Boolean);
|
|
291
|
+
} catch {
|
|
292
|
+
remotes = [];
|
|
293
|
+
}
|
|
294
|
+
if (remotes.length > 0) {
|
|
295
|
+
console.log(
|
|
296
|
+
`push protection: worktree pushurl override is applied at workspace creation for: ${remotes.join(", ")}`,
|
|
297
|
+
);
|
|
298
|
+
} else {
|
|
299
|
+
console.log("push protection: repository has no remotes (nothing to protect)");
|
|
300
|
+
}
|
|
301
|
+
console.log("kernel capabilities: merge/deploy/publish have no call path in the runner (invariant 20)");
|
|
302
|
+
}
|
|
303
|
+
|
|
304
|
+
function commandEvents(flags) {
|
|
305
|
+
if (!flags.repo || !flags.run) {
|
|
306
|
+
throw new Error("events requires --repo and --run");
|
|
307
|
+
}
|
|
308
|
+
const ledger = EventLedger.open(ledgerPathFor(flags.repo, flags.run));
|
|
309
|
+
for (const event of ledger.events) {
|
|
310
|
+
const detail =
|
|
311
|
+
event.data.step ??
|
|
312
|
+
event.data.transition ??
|
|
313
|
+
event.data.policy ??
|
|
314
|
+
event.data.status ??
|
|
315
|
+
"";
|
|
316
|
+
console.log(`${event.seq}\t${event.ts}\t${event.type}\t${detail}`);
|
|
317
|
+
}
|
|
318
|
+
if (ledger.corruption) {
|
|
319
|
+
console.log(
|
|
320
|
+
`WARNING: ledger corrupted after seq=${ledger.corruption.afterSeq} at line ${ledger.corruption.atLine}: ${ledger.corruption.reason}`,
|
|
321
|
+
);
|
|
322
|
+
}
|
|
323
|
+
}
|
|
324
|
+
|
|
325
|
+
function commandReplay(flags) {
|
|
326
|
+
if (!flags.repo || !flags.run) {
|
|
327
|
+
throw new Error("replay requires --repo and --run");
|
|
328
|
+
}
|
|
329
|
+
const ledger = EventLedger.open(ledgerPathFor(flags.repo, flags.run));
|
|
330
|
+
if (ledger.corruption) {
|
|
331
|
+
console.log(
|
|
332
|
+
`chain BROKEN after seq=${ledger.corruption.afterSeq}: ${ledger.corruption.reason}; ` +
|
|
333
|
+
"state below reflects the valid prefix only",
|
|
334
|
+
);
|
|
335
|
+
} else {
|
|
336
|
+
console.log(`chain OK: ${ledger.events.length} events verified (digest/prev/seq)`);
|
|
337
|
+
}
|
|
338
|
+
printState(ledger.state, { corruption: null });
|
|
339
|
+
}
|
|
340
|
+
|
|
341
|
+
function commandMetrics(flags) {
|
|
342
|
+
if (!flags.repo) {
|
|
343
|
+
throw new Error("metrics requires --repo");
|
|
344
|
+
}
|
|
345
|
+
const summary = computeMetrics(resolve(flags.repo));
|
|
346
|
+
if (flags.json === "true") {
|
|
347
|
+
console.log(JSON.stringify(summary, null, 2));
|
|
348
|
+
return;
|
|
349
|
+
}
|
|
350
|
+
console.log(renderMetrics(summary));
|
|
351
|
+
}
|
|
352
|
+
|
|
353
|
+
function commandStatus(flags) {
|
|
354
|
+
if (!flags.repo || !flags.run) {
|
|
355
|
+
throw new Error("status requires --repo and --run");
|
|
356
|
+
}
|
|
357
|
+
const ledger = EventLedger.open(ledgerPathFor(flags.repo, flags.run));
|
|
358
|
+
printState(ledger.state, ledger);
|
|
359
|
+
}
|
|
360
|
+
|
|
361
|
+
function commandStop(flags) {
|
|
362
|
+
if (!flags.repo || !flags.run) {
|
|
363
|
+
throw new Error("stop requires --repo and --run");
|
|
364
|
+
}
|
|
365
|
+
const repoRoot = resolve(flags.repo);
|
|
366
|
+
const ledger = EventLedger.open(ledgerPathFor(repoRoot, flags.run));
|
|
367
|
+
if (!ledger.state.run) {
|
|
368
|
+
throw new Error(`no ledger for run ${flags.run}`);
|
|
369
|
+
}
|
|
370
|
+
if (ledger.state.terminal) {
|
|
371
|
+
console.log(`run already terminal: ${ledger.state.terminal.status}`);
|
|
372
|
+
return;
|
|
373
|
+
}
|
|
374
|
+
acquireLock(repoRoot, flags.run);
|
|
375
|
+
try {
|
|
376
|
+
ledger.append({
|
|
377
|
+
type: "RUN_TERMINAL",
|
|
378
|
+
actor: KERNEL,
|
|
379
|
+
data: { status: "CANCELLED", reason: flags.reason ?? "stopped via CLI" },
|
|
380
|
+
});
|
|
381
|
+
writeRunRecord({ repoRoot, ledger });
|
|
382
|
+
console.log("run cancelled and compacted");
|
|
383
|
+
} finally {
|
|
384
|
+
releaseLock(repoRoot, flags.run);
|
|
385
|
+
}
|
|
386
|
+
}
|
|
387
|
+
|
|
388
|
+
function commandObserve(rest) {
|
|
389
|
+
const [sub, ...args] = rest;
|
|
390
|
+
const flags = parseFlags(args);
|
|
391
|
+
if (sub === "run") {
|
|
392
|
+
if (!flags.config) {
|
|
393
|
+
throw new Error("observe run requires --config <observe.yaml>");
|
|
394
|
+
}
|
|
395
|
+
const result = runObserveCycle({ configPath: flags.config });
|
|
396
|
+
console.log(`observe cycle ${result.cycle} finished (ledger: ${result.ledgerPath})`);
|
|
397
|
+
for (const row of result.results) {
|
|
398
|
+
const bands = row.bands.length > 0 ? ` bands=${row.bands.join(",")}` : "";
|
|
399
|
+
const intent = row.intent ? ` intent=${row.intent.outcome}:${row.intent.intentRef}` : "";
|
|
400
|
+
console.log(
|
|
401
|
+
` ${row.provider}: ${row.status}${row.severity ? ` severity=${row.severity}` : ""}${bands}${intent}`,
|
|
402
|
+
);
|
|
403
|
+
}
|
|
404
|
+
} else if (sub === "status") {
|
|
405
|
+
if (!flags.repo) {
|
|
406
|
+
throw new Error("observe status requires --repo");
|
|
407
|
+
}
|
|
408
|
+
const status = observeStatus({ repoRoot: flags.repo });
|
|
409
|
+
if (status.corruption) {
|
|
410
|
+
console.log(
|
|
411
|
+
`WARNING: observe ledger corrupted after seq=${status.corruption.afterSeq}: ${status.corruption.reason}`,
|
|
412
|
+
);
|
|
413
|
+
}
|
|
414
|
+
console.log(`cycles: ${status.cycles}`);
|
|
415
|
+
for (const [id, info] of Object.entries(status.providers)) {
|
|
416
|
+
console.log(`provider ${id}: ${info.lastStatus} (${info.lastKind} on ${info.lastSubject}) at ${info.lastTs}`);
|
|
417
|
+
}
|
|
418
|
+
if (status.intents.length === 0) {
|
|
419
|
+
console.log("intents: none");
|
|
420
|
+
}
|
|
421
|
+
for (const intent of status.intents) {
|
|
422
|
+
console.log(
|
|
423
|
+
`intent [${intent.status}] ${intent.file} (${intent.provider}, severity ${intent.severity})`,
|
|
424
|
+
);
|
|
425
|
+
}
|
|
426
|
+
} else if (sub === "triage") {
|
|
427
|
+
if (!flags.repo || !flags.intent || !flags.action) {
|
|
428
|
+
throw new Error("observe triage requires --repo, --intent and --action");
|
|
429
|
+
}
|
|
430
|
+
const result = triageIntent({
|
|
431
|
+
repoRoot: flags.repo,
|
|
432
|
+
intentRef: flags.intent,
|
|
433
|
+
action: flags.action,
|
|
434
|
+
by: flags.by,
|
|
435
|
+
note: flags.note,
|
|
436
|
+
});
|
|
437
|
+
console.log(`intent ${result.intentRef} -> ${result.status}`);
|
|
438
|
+
if (result.suggestion) {
|
|
439
|
+
console.log(result.suggestion);
|
|
440
|
+
}
|
|
441
|
+
} else {
|
|
442
|
+
throw new Error(`observe subcommand must be run|status|triage, got: ${sub ?? "(none)"}`);
|
|
443
|
+
}
|
|
444
|
+
}
|
|
445
|
+
|
|
446
|
+
function main() {
|
|
447
|
+
const [command, ...rest] = process.argv.slice(2);
|
|
448
|
+
if (command === "observe") {
|
|
449
|
+
try {
|
|
450
|
+
commandObserve(rest);
|
|
451
|
+
} catch (error) {
|
|
452
|
+
console.error(`error: ${error.message}`);
|
|
453
|
+
process.exitCode = 1;
|
|
454
|
+
}
|
|
455
|
+
return;
|
|
456
|
+
}
|
|
457
|
+
try {
|
|
458
|
+
const flags = parseFlags(rest);
|
|
459
|
+
if (command === "start") {
|
|
460
|
+
commandStart(flags);
|
|
461
|
+
} else if (command === "resume") {
|
|
462
|
+
commandResume(flags);
|
|
463
|
+
} else if (command === "inbox") {
|
|
464
|
+
commandInbox(flags);
|
|
465
|
+
} else if (command === "approve") {
|
|
466
|
+
commandApprove(flags);
|
|
467
|
+
} else if (command === "reject") {
|
|
468
|
+
commandReject(flags);
|
|
469
|
+
} else if (command === "accept") {
|
|
470
|
+
commandAccept(flags);
|
|
471
|
+
} else if (command === "doctor") {
|
|
472
|
+
commandDoctor(flags);
|
|
473
|
+
} else if (command === "events") {
|
|
474
|
+
commandEvents(flags);
|
|
475
|
+
} else if (command === "replay") {
|
|
476
|
+
commandReplay(flags);
|
|
477
|
+
} else if (command === "metrics") {
|
|
478
|
+
commandMetrics(flags);
|
|
479
|
+
} else if (command === "status") {
|
|
480
|
+
commandStatus(flags);
|
|
481
|
+
} else if (command === "stop") {
|
|
482
|
+
commandStop(flags);
|
|
483
|
+
} else {
|
|
484
|
+
process.stdout.write(USAGE);
|
|
485
|
+
process.exitCode = command ? 2 : 0;
|
|
486
|
+
return;
|
|
487
|
+
}
|
|
488
|
+
} catch (error) {
|
|
489
|
+
console.error(`error: ${error.message}`);
|
|
490
|
+
process.exitCode = 1;
|
|
491
|
+
}
|
|
492
|
+
}
|
|
493
|
+
|
|
494
|
+
main();
|