From e9d421a5bc72ad4d6fbeb348b10c7c1279d964e1 Mon Sep 17 00:00:00 2001 From: artale Date: Thu, 18 Jun 2026 15:59:17 +0200 Subject: [PATCH] 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. --- COMMANDS.md | 4 ++++ src/index.ts | 44 ++++++++++++++++++++++++++++++++++---------- 2 files changed, 38 insertions(+), 10 deletions(-) diff --git a/COMMANDS.md b/COMMANDS.md index 0038e1e..c4b0795 100644 --- a/COMMANDS.md +++ b/COMMANDS.md @@ -168,12 +168,15 @@ fable-agent plinius godmode "improve explanation quality" ### `factory` - `factory check` + - `--run ` - `factory capabilities` - `--file ` + - `--run ` - `factory rsi` - `--host ` - `--port ` - `--out ` + - `--run ` - `factory deploy ` - `--token ` - `--gate-receipt ` @@ -186,6 +189,7 @@ fable-agent plinius godmode "improve explanation quality" - `--port ` - `--capabilities ` - `--diagnostic-only` + - `--run ` ### `forgejo` diff --git a/src/index.ts b/src/index.ts index 6e34ec9..4dc7907 100644 --- a/src/index.ts +++ b/src/index.ts @@ -1426,29 +1426,40 @@ const factory = program .command("factory") .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 { + 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 result = await checkFactory(); + const ready = factoryGateReady(result); 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 .command("check") .description("Check factory status and deploy-webhook health") - .action(async () => { - await printFactoryCheck(); + .option("--run ", "Emit factory check event to .runs//channel.jsonl") + .action(async (opts: { run?: string }) => { + await printFactoryCheck(opts.run); }); factory .command("capabilities") .description("Probe the factory capability registry") .option("--file ", "Capability registry path", "factory-capabilities.yaml") - .action(async (opts: { file?: string }) => { + .option("--run ", "Emit capability probe event to .runs//channel.jsonl") + .action(async (opts: { file?: string; run?: string }) => { const { formatCapabilityReport, loadFactoryCapabilities, probeFactoryCapabilities } = await import("./fable5/factory-capabilities.js"); const services = loadFactoryCapabilities(path.resolve(opts.file ?? "factory-capabilities.yaml")); const report = await probeFactoryCapabilities(services); 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); }); @@ -1458,12 +1469,14 @@ factory .option("--host ", "Factory SSH host", "77.42.112.29") .option("--port ", "Factory SSH port", (v) => Number(v), 2222) .option("--out ", "Receipt output path") - .action(async (opts: { host?: string; port?: number; out?: string }) => { + .option("--run ", "Emit RSI receipt event to .runs//channel.jsonl") + .action(async (opts: { host?: string; port?: number; out?: string; run?: string }) => { const { reconcileRsi } = await import("./fable5/rsi-reconcile.js"); 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 }); console.log(JSON.stringify(receipt, null, 2)); 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); }); @@ -1499,14 +1512,17 @@ factory .option("--port ", "Factory SSH port for cyber preflight", (v) => Number(v), 2222) .option("--capabilities ", "Capability registry path", "factory-capabilities.yaml") .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 ", "Emit gate events to .runs//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 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) { const { appendZteReceipt, createZteReceipt } = await import("./fable5/zte-protocol.js"); const reason = "live factory/deploy status is not ok"; 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.`); process.exit(1); } @@ -1515,10 +1531,12 @@ factory const capabilitiesPath = path.resolve(opts.capabilities ?? "factory-capabilities.yaml"); const capabilities = await probeFactoryCapabilities(loadFactoryCapabilities(capabilitiesPath)); 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) { const { appendZteReceipt, createZteReceipt } = await import("./fable5/zte-protocol.js"); const reason = "required factory capabilities are missing"; 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.`); process.exit(1); } @@ -1533,10 +1551,12 @@ factory writeCyberReconcileReceipt(reconcilePath, reconcile); console.log(` Cyber preflight: ${preflight.decision} (${preflightPath})`); 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) { const { appendZteReceipt, createZteReceipt } = await import("./fable5/zte-protocol.js"); const reason = "cyber preflight/reconcile did not pass"; 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.`); process.exit(1); } @@ -1545,9 +1565,13 @@ factory console.log(` Repo: ${repo}`); 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" }); - 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(``); });