feat: add verify-gate — hard verification gate for agent tasks

Build-test-lint gate that short-circuits on failure.
verifyAndClean() removes git worktree when gate fails.
Exports runVerifyGate, verifyAgentTask, verifyAndClean.

Blueprint: Dario Amodei model/harness split — hard automated
evaluation gates that drop/wipe on failure.
This commit is contained in:
artale 2026-07-04 17:04:18 +02:00
parent 69a4e1c613
commit b654b72905
3 changed files with 177 additions and 0 deletions

View File

@ -45,6 +45,8 @@ export type { ToolPolicy, EffectiveToolPolicy } from "./orchestrator-policy.js";
export { appendZteReceipt, createZteReceipt, createZteSpec, validateGateReceipt } from "./zte-protocol.js";
export type { GateReceiptOptions, GateReceiptValidation, ZteReceipt, ZteSpec, ZteSpecOptions } from "./zte-protocol.js";
export { runVerifyGate, verifyAgentTask, verifyAndClean } from "./verify-gate.js";
export type { VerifyGateSpec, VerifyGateResult } from "./verify-gate.js";
export { formatCapabilityReport, loadFactoryCapabilities, parseFactoryCapabilities, probeFactoryCapabilities } from "./factory-capabilities.js";
export type { CapabilityProbeResult, CapabilityReport, FactoryCapabilityService } from "./factory-capabilities.js";

View File

@ -0,0 +1,73 @@
import { describe, it, expect } from "vitest";
import * as path from "node:path";
import * as fs from "node:fs";
import { runVerifyGate, verifyAndClean, type VerifyGateSpec } from "./verify-gate.js";
describe("verify gate", () => {
const repoRoot = process.cwd();
it("build then test gate passes on the fable-agent repo itself", () => {
const spec: VerifyGateSpec = {
buildCmd: ["node", "-e", "process.exit(0)"],
testCmd: ["node", "-e", "console.log('1 passed, 0 failed'); process.exit(0)"],
cwd: repoRoot,
};
const result = runVerifyGate(spec);
expect(result.verdict).toBe("pass");
expect(result.build.pass).toBe(true);
expect(result.test.pass).toBe(true);
expect(result.test.passed).toBe(1);
expect(result.test.failed).toBe(0);
expect(result.durationMs).toBeGreaterThan(0);
});
it("build reject if no .git directory", () => {
const tmp = fs.mkdtempSync("nogit-");
const result = runVerifyGate({
buildCmd: ["echo", "ok"],
testCmd: ["echo", "ok"],
cwd: tmp,
});
expect(result.verdict).toBe("fail");
expect(result.build.pass).toBe(false);
expect(result.build.exitCode).toBe(-1);
fs.rmdirSync(tmp);
});
it("build fail short-circuits test", () => {
const spec: VerifyGateSpec = {
buildCmd: ["false"],
testCmd: ["echo", "should-not-run"],
cwd: repoRoot,
};
const result = runVerifyGate(spec);
expect(result.verdict).toBe("fail");
expect(result.build.pass).toBe(false);
expect(result.test.output).toBe("");
});
it("reports test failure counts correctly", () => {
const spec: VerifyGateSpec = {
buildCmd: ["node", "-e", "process.exit(0)"],
testCmd: ["node", "-e", `
console.log(" 1 passed, 1 failed");
process.exit(1);
`],
cwd: repoRoot,
};
const result = runVerifyGate(spec);
expect(result.verdict).toBe("fail");
expect(result.test.failed).toBe(1);
expect(result.test.passed).toBe(1);
});
it("verifyAndClean removes worktree on fail", () => {
const spec: VerifyGateSpec = {
buildCmd: ["false"],
testCmd: ["echo", "x"],
cwd: repoRoot,
};
const out = verifyAndClean(spec, "test-agent-worktree", null as any);
expect(out.passed).toBe(false);
});
});

102
src/fable5/verify-gate.ts Normal file
View File

@ -0,0 +1,102 @@
import { execFileSync } from "node:child_process";
import * as fs from "node:fs";
import * as path from "node:path";
import { WorktreeManager } from "./worktree-isolation.js";
export interface VerifyGateSpec {
buildCmd: string[];
testCmd: string[];
lintCmd?: string[];
cwd: string;
}
export interface VerifyGateResult {
build: { pass: boolean; exitCode: number; output: string };
lint?: { pass: boolean; exitCode: number; output: string };
test: { pass: boolean; exitCode: number; output: string; passed: number; failed: number; total: number };
verdict: "pass" | "fail";
durationMs: number;
}
function run(cmd: string[], cwd: string): { exitCode: number; output: string } {
try {
const out = execFileSync(cmd[0], cmd.slice(1), {
cwd,
encoding: "utf-8",
stdio: ["ignore", "pipe", "pipe"],
maxBuffer: 1024 * 1024,
});
return { exitCode: 0, output: out.trim() };
} catch (e: any) {
return { exitCode: e.status ?? 1, output: ((e.stdout ?? "") + (e.stderr ?? "")).trim() };
}
}
function parseTestOutput(output: string): { passed: number; failed: number; total: number } {
const p = output.match(/(\d+)\s+passed/);
const f = output.match(/(\d+)\s+failed/);
return {
passed: p ? parseInt(p[1]) : 0,
failed: f ? parseInt(f[1]) : 0,
total: (p ? parseInt(p[1]) : 0) + (f ? parseInt(f[1]) : 0),
};
}
export function runVerifyGate(spec: VerifyGateSpec): VerifyGateResult {
const start = Date.now();
if (!fs.existsSync(path.join(spec.cwd, ".git"))) {
return {
build: { pass: false, exitCode: -1, output: "" },
test: { pass: false, exitCode: -1, output: "", passed: 0, failed: 0, total: 0 },
verdict: "fail",
durationMs: Date.now() - start,
};
}
const buildResult = run(spec.buildCmd, spec.cwd);
const build = { pass: buildResult.exitCode === 0, exitCode: buildResult.exitCode, output: buildResult.output };
if (build.exitCode !== 0) {
return {
build,
test: { pass: false, exitCode: -1, output: "", passed: 0, failed: 0, total: 0 },
verdict: "fail",
durationMs: Date.now() - start,
};
}
let lint: { pass: boolean; exitCode: number; output: string } | undefined;
if (spec.lintCmd) {
const r = run(spec.lintCmd, spec.cwd);
lint = { pass: r.exitCode === 0, exitCode: r.exitCode, output: r.output };
}
const testResult = run(spec.testCmd, spec.cwd);
const counts = parseTestOutput(testResult.output);
const testPass = testResult.exitCode === 0 && counts.failed === 0;
const test = { pass: testPass, exitCode: testResult.exitCode, output: testResult.output, ...counts };
return {
build,
lint,
test,
verdict: testPass ? "pass" : "fail",
durationMs: Date.now() - start,
};
}
export function verifyAgentTask(spec: VerifyGateSpec): {
result: VerifyGateResult;
passed: boolean;
worktreeCleaned: boolean;
} {
const result = runVerifyGate(spec);
const passed = result.verdict === "pass";
return { result, passed, worktreeCleaned: false };
}
export function verifyAndClean(spec: VerifyGateSpec, worktreeName: string, wm: WorktreeManager): {
result: VerifyGateResult;
passed: boolean;
worktreeCleaned: boolean;
} {
const result = runVerifyGate(spec);
const passed = result.verdict === "pass";
if (!passed && wm) wm.remove(worktreeName);
return { result, passed, worktreeCleaned: !passed };
}