feat: add eval trace receipts
This commit is contained in:
parent
3a235a8c9c
commit
df845b63cc
|
|
@ -246,6 +246,8 @@ fable-agent plinius godmode "improve explanation quality"
|
||||||
- `--out <path>`
|
- `--out <path>`
|
||||||
- `fable5 verify <task>`
|
- `fable5 verify <task>`
|
||||||
- `--repo <path>`
|
- `--repo <path>`
|
||||||
|
- `--run <id>`
|
||||||
|
- `--trace-out <path>`
|
||||||
- `fable5 goal <text>`
|
- `fable5 goal <text>`
|
||||||
- `-i, --iterations <n>`
|
- `-i, --iterations <n>`
|
||||||
- `-s, --min-score <n>`
|
- `-s, --min-score <n>`
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,33 @@
|
||||||
|
import * as fs from "node:fs";
|
||||||
|
import * as os from "node:os";
|
||||||
|
import * as path from "node:path";
|
||||||
|
import { afterEach, describe, expect, it } from "vitest";
|
||||||
|
import { createEvalTraceReceipt, writeEvalTraceReceipt } from "./eval-trace.js";
|
||||||
|
|
||||||
|
const roots: string[] = [];
|
||||||
|
|
||||||
|
afterEach(() => {
|
||||||
|
for (const root of roots.splice(0)) fs.rmSync(root, { recursive: true, force: true });
|
||||||
|
});
|
||||||
|
|
||||||
|
function tmpRoot(): string {
|
||||||
|
const root = fs.mkdtempSync(path.join(os.tmpdir(), "fable-eval-trace-"));
|
||||||
|
roots.push(root);
|
||||||
|
return root;
|
||||||
|
}
|
||||||
|
|
||||||
|
describe("eval trace receipt", () => {
|
||||||
|
it("passes only when every command exits zero", () => {
|
||||||
|
expect(createEvalTraceReceipt("task", ".", [{ command: "test", status: 0 }]).status).toBe("passed");
|
||||||
|
expect(createEvalTraceReceipt("task", ".", [{ command: "test", status: 1 }]).status).toBe("failed");
|
||||||
|
});
|
||||||
|
|
||||||
|
it("writes a JSON receipt", () => {
|
||||||
|
const out = path.join(tmpRoot(), "trace.json");
|
||||||
|
const receipt = createEvalTraceReceipt("task", ".", [{ command: "test", status: 0 }], "abc123", new Date("2026-06-18T00:00:00.000Z"));
|
||||||
|
|
||||||
|
writeEvalTraceReceipt(out, receipt);
|
||||||
|
|
||||||
|
expect(JSON.parse(fs.readFileSync(out, "utf-8"))).toMatchObject({ task: "task", status: "passed", gitHead: "abc123" });
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
@ -0,0 +1,33 @@
|
||||||
|
import * as fs from "node:fs";
|
||||||
|
import * as path from "node:path";
|
||||||
|
|
||||||
|
export interface EvalTraceCommand {
|
||||||
|
command: string;
|
||||||
|
status: number | null;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface EvalTraceReceipt {
|
||||||
|
task: string;
|
||||||
|
repo: string;
|
||||||
|
status: "passed" | "failed";
|
||||||
|
commands: EvalTraceCommand[];
|
||||||
|
gitHead?: string;
|
||||||
|
createdAt: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function createEvalTraceReceipt(task: string, repo: string, commands: EvalTraceCommand[], gitHead?: string, now = new Date()): EvalTraceReceipt {
|
||||||
|
return {
|
||||||
|
task,
|
||||||
|
repo,
|
||||||
|
status: commands.every((cmd) => cmd.status === 0) ? "passed" : "failed",
|
||||||
|
commands,
|
||||||
|
gitHead,
|
||||||
|
createdAt: now.toISOString(),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
export function writeEvalTraceReceipt(file: string, receipt: EvalTraceReceipt): string {
|
||||||
|
fs.mkdirSync(path.dirname(file), { recursive: true });
|
||||||
|
fs.writeFileSync(file, `${JSON.stringify(receipt, null, 2)}\n`);
|
||||||
|
return file;
|
||||||
|
}
|
||||||
|
|
@ -62,6 +62,9 @@ export type { ChannelEvent, ChannelEventSource, ChannelEventType, NewChannelEven
|
||||||
export { sendMissionHeartbeat, writeMissionHeartbeatReceipt } from "./mission-heartbeat.js";
|
export { sendMissionHeartbeat, writeMissionHeartbeatReceipt } from "./mission-heartbeat.js";
|
||||||
export type { MissionHeartbeatOptions, MissionHeartbeatReceipt, MissionHeartbeatStatus } from "./mission-heartbeat.js";
|
export type { MissionHeartbeatOptions, MissionHeartbeatReceipt, MissionHeartbeatStatus } from "./mission-heartbeat.js";
|
||||||
|
|
||||||
|
export { createEvalTraceReceipt, writeEvalTraceReceipt } from "./eval-trace.js";
|
||||||
|
export type { EvalTraceCommand, EvalTraceReceipt } from "./eval-trace.js";
|
||||||
|
|
||||||
export { runCyberPreflight, writeCyberPreflightReceipt } from "./cyber-preflight.js";
|
export { runCyberPreflight, writeCyberPreflightReceipt } from "./cyber-preflight.js";
|
||||||
export type { CyberPreflightCheck, CyberPreflightOptions, CyberPreflightReceipt } from "./cyber-preflight.js";
|
export type { CyberPreflightCheck, CyberPreflightOptions, CyberPreflightReceipt } from "./cyber-preflight.js";
|
||||||
|
|
||||||
|
|
|
||||||
42
src/index.ts
42
src/index.ts
|
|
@ -1712,7 +1712,14 @@ fable
|
||||||
.command("verify <task>")
|
.command("verify <task>")
|
||||||
.description("Run independent verifier against a task")
|
.description("Run independent verifier against a task")
|
||||||
.option("--repo <path>", "Also run the repo verification gate at this path")
|
.option("--repo <path>", "Also run the repo verification gate at this path")
|
||||||
.action(async (task: string, opts: { repo?: string }) => {
|
.option("--run <id>", "Emit verify events to .runs/<id>/channel.jsonl")
|
||||||
|
.option("--trace-out <path>", "Eval trace receipt path")
|
||||||
|
.action(async (task: string, opts: { repo?: string; run?: string; traceOut?: string }) => {
|
||||||
|
if (opts.run) {
|
||||||
|
const { emitChannelEvent } = await import("./fable5/channel.js");
|
||||||
|
emitChannelEvent(opts.run, { source: "gate", type: "started", data: { task, repo: opts.repo } });
|
||||||
|
}
|
||||||
|
|
||||||
if (opts.repo) {
|
if (opts.repo) {
|
||||||
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);
|
||||||
|
|
@ -1723,28 +1730,43 @@ fable
|
||||||
["npx", ["tsc", "--noEmit"]],
|
["npx", ["tsc", "--noEmit"]],
|
||||||
[process.execPath, [process.argv[1], "security", "scan", repo]],
|
[process.execPath, [process.argv[1], "security", "scan", repo]],
|
||||||
];
|
];
|
||||||
|
const commandResults: Array<{ command: string; status: number | null }> = [];
|
||||||
|
|
||||||
console.log(`
|
console.log(`
|
||||||
Repo Verification Gate: ${repo}`);
|
Repo Verification Gate: ${repo}`);
|
||||||
for (const [cmd, args] of checks) {
|
for (const [cmd, args] of checks) {
|
||||||
|
const command = [cmd, ...args].join(" ");
|
||||||
console.log(`
|
console.log(`
|
||||||
$ ${[cmd, ...args].join(" ")}`);
|
$ ${command}`);
|
||||||
// ponytail: Windows npm/npx shims need cmd.exe; commands are fixed, no user shell input.
|
// ponytail: Windows npm/npx shims need cmd.exe; commands are fixed, no user shell input.
|
||||||
const check = process.platform === "win32" && (cmd === "npm" || cmd === "npx")
|
const check = process.platform === "win32" && (cmd === "npm" || cmd === "npx")
|
||||||
? spawnSync("cmd.exe", ["/d", "/s", "/c", [cmd, ...args].join(" ")], { cwd: repo, stdio: "inherit" })
|
? spawnSync("cmd.exe", ["/d", "/s", "/c", command], { cwd: repo, stdio: "inherit" })
|
||||||
: spawnSync(cmd, args, { cwd: repo, stdio: "inherit" });
|
: spawnSync(cmd, args, { cwd: repo, stdio: "inherit" });
|
||||||
if (check.status !== 0) {
|
commandResults.push({ command, status: check.status });
|
||||||
console.error(`
|
if (check.status !== 0) break;
|
||||||
✗ Repo verification failed`);
|
|
||||||
process.exit(check.status ?? 1);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
const { appendZteReceipt, createZteReceipt } = await import("./fable5/zte-protocol.js");
|
const { appendZteReceipt, createZteReceipt } = await import("./fable5/zte-protocol.js");
|
||||||
const receipt = createZteReceipt(task, repo, "passed", checks.map(([cmd, args]) => [cmd, ...args].join(" ")));
|
const { createEvalTraceReceipt, writeEvalTraceReceipt } = await import("./fable5/eval-trace.js");
|
||||||
|
const gitHead = spawnSync("git", ["rev-parse", "HEAD"], { cwd: repo, encoding: "utf-8" }).stdout.trim() || undefined;
|
||||||
|
const trace = createEvalTraceReceipt(task, repo, commandResults, gitHead);
|
||||||
|
const tracePath = opts.traceOut ?? path.join(repo, ".fable", "eval-traces", `${new Date().toISOString().replace(/[:.]/g, "-")}.json`);
|
||||||
|
writeEvalTraceReceipt(tracePath, trace);
|
||||||
|
if (opts.run) {
|
||||||
|
const { emitChannelEvent } = await import("./fable5/channel.js");
|
||||||
|
emitChannelEvent(opts.run, { source: "gate", type: trace.status === "passed" ? "receipt" : "error", data: trace });
|
||||||
|
}
|
||||||
|
const receipt = createZteReceipt(task, repo, trace.status, commandResults.map((r) => r.command), trace.status === "failed" ? "repo verification failed" : undefined);
|
||||||
const receiptPath = appendZteReceipt(repo, receipt);
|
const receiptPath = appendZteReceipt(repo, receipt);
|
||||||
|
if (trace.status === "failed") {
|
||||||
|
console.error(`
|
||||||
|
✗ Repo verification failed`);
|
||||||
|
console.error(` Eval trace: ${tracePath}`);
|
||||||
|
process.exit(commandResults.find((r) => r.status !== 0)?.status ?? 1);
|
||||||
|
}
|
||||||
console.log(`
|
console.log(`
|
||||||
✓ Repo verification passed`);
|
✓ Repo verification passed`);
|
||||||
console.log(` ✓ ZTE receipt written: ${receiptPath}
|
console.log(` ✓ ZTE receipt written: ${receiptPath}`);
|
||||||
|
console.log(` ✓ Eval trace written: ${tracePath}
|
||||||
`);
|
`);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue