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:
parent
69a4e1c613
commit
b654b72905
|
|
@ -45,6 +45,8 @@ export type { ToolPolicy, EffectiveToolPolicy } from "./orchestrator-policy.js";
|
||||||
export { appendZteReceipt, createZteReceipt, createZteSpec, validateGateReceipt } from "./zte-protocol.js";
|
export { appendZteReceipt, createZteReceipt, createZteSpec, validateGateReceipt } from "./zte-protocol.js";
|
||||||
export type { GateReceiptOptions, GateReceiptValidation, ZteReceipt, ZteSpec, ZteSpecOptions } 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 { formatCapabilityReport, loadFactoryCapabilities, parseFactoryCapabilities, probeFactoryCapabilities } from "./factory-capabilities.js";
|
||||||
export type { CapabilityProbeResult, CapabilityReport, FactoryCapabilityService } from "./factory-capabilities.js";
|
export type { CapabilityProbeResult, CapabilityReport, FactoryCapabilityService } from "./factory-capabilities.js";
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -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);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
@ -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 };
|
||||||
|
}
|
||||||
Loading…
Reference in New Issue