feat: emit factory gate channel events

Add optional --run support to factory check, capabilities, rsi, and gate so factory verification steps can write channel JSONL events alongside existing receipts. Wire factory gate through fable5 verify with the same run id and document the new options in COMMANDS.md.
This commit is contained in:
artale 2026-06-18 15:59:17 +02:00
parent c3b1fda182
commit e9d421a5bc
2 changed files with 38 additions and 10 deletions

View File

@ -168,12 +168,15 @@ fable-agent plinius godmode "improve explanation quality"
### `factory` ### `factory`
- `factory check` - `factory check`
- `--run <id>`
- `factory capabilities` - `factory capabilities`
- `--file <path>` - `--file <path>`
- `--run <id>`
- `factory rsi` - `factory rsi`
- `--host <host>` - `--host <host>`
- `--port <n>` - `--port <n>`
- `--out <path>` - `--out <path>`
- `--run <id>`
- `factory deploy <command>` - `factory deploy <command>`
- `--token <token>` - `--token <token>`
- `--gate-receipt <path>` - `--gate-receipt <path>`
@ -186,6 +189,7 @@ fable-agent plinius godmode "improve explanation quality"
- `--port <n>` - `--port <n>`
- `--capabilities <path>` - `--capabilities <path>`
- `--diagnostic-only` - `--diagnostic-only`
- `--run <id>`
### `forgejo` ### `forgejo`

View File

@ -1426,29 +1426,40 @@ const factory = program
.command("factory") .command("factory")
.description("Factory/VPS status and deployment guards"); .description("Factory/VPS status and deployment guards");
async function printFactoryCheck(): Promise<{ ready: boolean }> { async function emitRunEvent(runId: string | undefined, event: { source: "agent" | "tool" | "workflow" | "gate" | "intake" | "mission"; type: "started" | "output" | "receipt" | "error" | "done"; data: unknown }): Promise<void> {
if (!runId) return;
const { emitChannelEvent } = await import("./fable5/channel.js");
emitChannelEvent(runId, event);
}
async function printFactoryCheck(runId?: string): Promise<{ ready: boolean }> {
const { checkFactory, factoryGateReady, formatFactoryCheck } = await import("./fable5/factory-status.js"); const { checkFactory, factoryGateReady, formatFactoryCheck } = await import("./fable5/factory-status.js");
const result = await checkFactory(); const result = await checkFactory();
const ready = factoryGateReady(result);
console.log(formatFactoryCheck(result.factory, result.deploy, result.errors)); console.log(formatFactoryCheck(result.factory, result.deploy, result.errors));
return { ready: factoryGateReady(result) }; await emitRunEvent(runId, { source: "gate", type: ready ? "receipt" : "error", data: { kind: "factory-check", ready, ...result } });
return { ready };
} }
factory factory
.command("check") .command("check")
.description("Check factory status and deploy-webhook health") .description("Check factory status and deploy-webhook health")
.action(async () => { .option("--run <id>", "Emit factory check event to .runs/<id>/channel.jsonl")
await printFactoryCheck(); .action(async (opts: { run?: string }) => {
await printFactoryCheck(opts.run);
}); });
factory factory
.command("capabilities") .command("capabilities")
.description("Probe the factory capability registry") .description("Probe the factory capability registry")
.option("--file <path>", "Capability registry path", "factory-capabilities.yaml") .option("--file <path>", "Capability registry path", "factory-capabilities.yaml")
.action(async (opts: { file?: string }) => { .option("--run <id>", "Emit capability probe event to .runs/<id>/channel.jsonl")
.action(async (opts: { file?: string; run?: string }) => {
const { formatCapabilityReport, loadFactoryCapabilities, probeFactoryCapabilities } = await import("./fable5/factory-capabilities.js"); const { formatCapabilityReport, loadFactoryCapabilities, probeFactoryCapabilities } = await import("./fable5/factory-capabilities.js");
const services = loadFactoryCapabilities(path.resolve(opts.file ?? "factory-capabilities.yaml")); const services = loadFactoryCapabilities(path.resolve(opts.file ?? "factory-capabilities.yaml"));
const report = await probeFactoryCapabilities(services); const report = await probeFactoryCapabilities(services);
console.log(formatCapabilityReport(report)); console.log(formatCapabilityReport(report));
await emitRunEvent(opts.run, { source: "gate", type: report.requiredOk ? "receipt" : "error", data: { kind: "factory-capabilities", report } });
if (!report.requiredOk) process.exit(1); if (!report.requiredOk) process.exit(1);
}); });
@ -1458,12 +1469,14 @@ factory
.option("--host <host>", "Factory SSH host", "77.42.112.29") .option("--host <host>", "Factory SSH host", "77.42.112.29")
.option("--port <n>", "Factory SSH port", (v) => Number(v), 2222) .option("--port <n>", "Factory SSH port", (v) => Number(v), 2222)
.option("--out <path>", "Receipt output path") .option("--out <path>", "Receipt output path")
.action(async (opts: { host?: string; port?: number; out?: string }) => { .option("--run <id>", "Emit RSI receipt event to .runs/<id>/channel.jsonl")
.action(async (opts: { host?: string; port?: number; out?: string; run?: string }) => {
const { reconcileRsi } = await import("./fable5/rsi-reconcile.js"); const { reconcileRsi } = await import("./fable5/rsi-reconcile.js");
const out = opts.out ?? path.join(".fable", "rsi", `${new Date().toISOString().replace(/[:.]/g, "-")}.json`); const out = opts.out ?? path.join(".fable", "rsi", `${new Date().toISOString().replace(/[:.]/g, "-")}.json`);
const receipt = reconcileRsi({ host: opts.host ?? "77.42.112.29", port: opts.port, out }); const receipt = reconcileRsi({ host: opts.host ?? "77.42.112.29", port: opts.port, out });
console.log(JSON.stringify(receipt, null, 2)); console.log(JSON.stringify(receipt, null, 2));
console.log(`\n Receipt: ${out}\n`); console.log(`\n Receipt: ${out}\n`);
await emitRunEvent(opts.run, { source: "gate", type: receipt.decision === "degraded" ? "error" : "receipt", data: { kind: "factory-rsi", receipt, path: out } });
if (receipt.decision === "degraded") process.exit(1); if (receipt.decision === "degraded") process.exit(1);
}); });
@ -1499,14 +1512,17 @@ factory
.option("--port <n>", "Factory SSH port for cyber preflight", (v) => Number(v), 2222) .option("--port <n>", "Factory SSH port for cyber preflight", (v) => Number(v), 2222)
.option("--capabilities <path>", "Capability registry path", "factory-capabilities.yaml") .option("--capabilities <path>", "Capability registry path", "factory-capabilities.yaml")
.option("--diagnostic-only", "Bypass live factory/deploy readiness failures for non-deploy diagnostics") .option("--diagnostic-only", "Bypass live factory/deploy readiness failures for non-deploy diagnostics")
.action(async (task: string, opts: { repo?: string; host?: string; port?: number; capabilities?: string; diagnosticOnly?: boolean }) => { .option("--run <id>", "Emit gate events to .runs/<id>/channel.jsonl")
.action(async (task: string, opts: { repo?: string; host?: string; port?: number; capabilities?: string; diagnosticOnly?: boolean; run?: string }) => {
const { spawnSync } = await import("node:child_process"); const { spawnSync } = await import("node:child_process");
const repo = path.resolve(opts.repo ?? "."); const repo = path.resolve(opts.repo ?? ".");
const status = await printFactoryCheck(); await emitRunEvent(opts.run, { source: "gate", type: "started", data: { kind: "factory-gate", task, repo } });
const status = await printFactoryCheck(opts.run);
if (!status.ready && !opts.diagnosticOnly) { if (!status.ready && !opts.diagnosticOnly) {
const { appendZteReceipt, createZteReceipt } = await import("./fable5/zte-protocol.js"); const { appendZteReceipt, createZteReceipt } = await import("./fable5/zte-protocol.js");
const reason = "live factory/deploy status is not ok"; const reason = "live factory/deploy status is not ok";
appendZteReceipt(repo, createZteReceipt(task, repo, "blocked", ["factory check"], reason)); appendZteReceipt(repo, createZteReceipt(task, repo, "blocked", ["factory check"], reason));
await emitRunEvent(opts.run, { source: "gate", type: "error", data: { kind: "factory-gate", reason } });
console.error(` ✗ Factory gate failed: ${reason}; use --diagnostic-only for non-deploy diagnostics.`); console.error(` ✗ Factory gate failed: ${reason}; use --diagnostic-only for non-deploy diagnostics.`);
process.exit(1); process.exit(1);
} }
@ -1515,10 +1531,12 @@ factory
const capabilitiesPath = path.resolve(opts.capabilities ?? "factory-capabilities.yaml"); const capabilitiesPath = path.resolve(opts.capabilities ?? "factory-capabilities.yaml");
const capabilities = await probeFactoryCapabilities(loadFactoryCapabilities(capabilitiesPath)); const capabilities = await probeFactoryCapabilities(loadFactoryCapabilities(capabilitiesPath));
console.log(formatCapabilityReport(capabilities)); console.log(formatCapabilityReport(capabilities));
await emitRunEvent(opts.run, { source: "gate", type: capabilities.requiredOk ? "receipt" : "error", data: { kind: "factory-capabilities", report: capabilities } });
if (!capabilities.requiredOk && !opts.diagnosticOnly) { if (!capabilities.requiredOk && !opts.diagnosticOnly) {
const { appendZteReceipt, createZteReceipt } = await import("./fable5/zte-protocol.js"); const { appendZteReceipt, createZteReceipt } = await import("./fable5/zte-protocol.js");
const reason = "required factory capabilities are missing"; const reason = "required factory capabilities are missing";
appendZteReceipt(repo, createZteReceipt(task, repo, "blocked", ["factory capabilities"], reason)); appendZteReceipt(repo, createZteReceipt(task, repo, "blocked", ["factory capabilities"], reason));
await emitRunEvent(opts.run, { source: "gate", type: "error", data: { kind: "factory-gate", reason } });
console.error(` ✗ Factory gate failed: ${reason}; use --diagnostic-only for non-deploy diagnostics.`); console.error(` ✗ Factory gate failed: ${reason}; use --diagnostic-only for non-deploy diagnostics.`);
process.exit(1); process.exit(1);
} }
@ -1533,10 +1551,12 @@ factory
writeCyberReconcileReceipt(reconcilePath, reconcile); writeCyberReconcileReceipt(reconcilePath, reconcile);
console.log(` Cyber preflight: ${preflight.decision} (${preflightPath})`); console.log(` Cyber preflight: ${preflight.decision} (${preflightPath})`);
console.log(` Cyber reconcile: ${reconcile.decision} (${reconcilePath})`); console.log(` Cyber reconcile: ${reconcile.decision} (${reconcilePath})`);
await emitRunEvent(opts.run, { source: "gate", type: preflight.decision === "allow_read_only" && reconcile.decision === "allow_read_only" ? "receipt" : "error", data: { kind: "cyber-preflight", preflightPath, reconcilePath, preflight: preflight.decision, reconcile: reconcile.decision } });
if ((preflight.decision !== "allow_read_only" || reconcile.decision !== "allow_read_only") && !opts.diagnosticOnly) { if ((preflight.decision !== "allow_read_only" || reconcile.decision !== "allow_read_only") && !opts.diagnosticOnly) {
const { appendZteReceipt, createZteReceipt } = await import("./fable5/zte-protocol.js"); const { appendZteReceipt, createZteReceipt } = await import("./fable5/zte-protocol.js");
const reason = "cyber preflight/reconcile did not pass"; const reason = "cyber preflight/reconcile did not pass";
appendZteReceipt(repo, createZteReceipt(task, repo, "blocked", ["cyber preflight", "cyber reconcile"], reason)); appendZteReceipt(repo, createZteReceipt(task, repo, "blocked", ["cyber preflight", "cyber reconcile"], reason));
await emitRunEvent(opts.run, { source: "gate", type: "error", data: { kind: "factory-gate", reason } });
console.error(` ✗ Factory gate failed: ${reason}; use --diagnostic-only for non-deploy diagnostics.`); console.error(` ✗ Factory gate failed: ${reason}; use --diagnostic-only for non-deploy diagnostics.`);
process.exit(1); process.exit(1);
} }
@ -1545,9 +1565,13 @@ factory
console.log(` Repo: ${repo}`); console.log(` Repo: ${repo}`);
console.log(``); console.log(``);
const args = [process.argv[1], "fable5", "verify", task, "--repo", repo]; const args = [process.argv[1], "fable5", "verify", task, "--repo", repo, ...(opts.run ? ["--run", opts.run] : [])];
const check = spawnSync(process.execPath, args, { cwd: repo, stdio: "inherit" }); const check = spawnSync(process.execPath, args, { cwd: repo, stdio: "inherit" });
if (check.status !== 0) process.exit(check.status ?? 1); if (check.status !== 0) {
await emitRunEvent(opts.run, { source: "gate", type: "error", data: { kind: "factory-gate", reason: "repo verification failed", status: check.status } });
process.exit(check.status ?? 1);
}
await emitRunEvent(opts.run, { source: "gate", type: "done", data: { kind: "factory-gate", task, repo } });
console.log(` ✓ Factory gate passed; deploy still requires explicit token/contract.`); console.log(` ✓ Factory gate passed; deploy still requires explicit token/contract.`);
console.log(``); console.log(``);
}); });