auth-yes/.forum/src/agents/evaluator.ts
Tyler Gillispie 5ea79ce63d
feat: setup agent pipeline runner and scope-based BMC rules (#77)
- **Gatekeeper/Evaluator**: Fixed schema mismatch so Gatekeeper outputs `"Approved"` and Evaluator accepts `"Approved"` or `"Completed"`.
- **Transitions / The Wall**: Introduced path-scoped BMC routing (`scopes`) to `transitions.json` using glob patterns to selectively enforce rules based on matched paths. Mixed scopes correctly union rules for maximum strictness.
- **Pipeline Runner**: Built `.forum/src/run.ts` to orchestrate agents sequentially based on diff analysis, halting rigorously on blocks and displaying a final summary table.

Co-authored-by: google-labs-jules[bot] <161369871+google-labs-jules[bot]@users.noreply.github.com>
Co-authored-by: mrteye <1945243+mrteye@users.noreply.github.com>
2026-08-29 17:28:16 -07:00

246 lines
7.1 KiB
TypeScript

/**
* Agent Forum v4 - Evaluator Agent
*
* Role: Govern pipeline integrity.
* Inputs: transitions.json (BMC rules), YAML DAGs
* Outputs: Pipeline progression (Allows or Blocks workflow transitions)
*/
import { parseArgs } from "https://deno.land/std@0.224.0/cli/parse_args.ts";
import { parse as parseYaml } from "npm:yaml";
import { globToRegExp } from "https://deno.land/std@0.224.0/path/mod.ts";
async function runCommand(
cmd: string,
args: string[],
): Promise<{ code: number; stdout: string; stderr: string }> {
const command = new Deno.Command(cmd, {
args,
stdout: "piped",
stderr: "piped",
});
const { code, stdout, stderr } = await command.output();
const decoder = new TextDecoder();
return {
code,
stdout: decoder.decode(stdout).trim(),
stderr: decoder.decode(stderr).trim(),
};
}
/**
* Fetches content from the meta-state branch without checking it out.
*/
async function fetchFromMetaState(path: string): Promise<string | null> {
const check = await runCommand("git", ["show", `meta-state:${path}`]);
if (check.code !== 0) {
// Fallback for evaluator to use local file if meta-state missing during test
if (path === "transitions.json") {
const local = await runCommand("cat", [".forum/src/core/transitions.json"]);
if (local.code === 0) return local.stdout;
}
return null;
}
return check.stdout;
}
/**
* Fetches list of files in a directory on the meta-state branch.
*/
async function listMetaStateDir(path: string): Promise<string[]> {
const check = await runCommand("git", ["ls-tree", "-r", "--name-only", "meta-state", path]);
if (check.code !== 0) {
return [];
}
return check.stdout.split("\n").filter(Boolean);
}
/**
* Checks if a specific Git Note exists for a namespace on HEAD.
*/
async function getGitNotePayload(namespace: string): Promise<any | null> {
const cmd = await runCommand("git", ["notes", "--ref", `refs/notes/${namespace}`, "show", "HEAD"]);
if (cmd.code !== 0 || !cmd.stdout) return null;
try {
return JSON.parse(cmd.stdout);
} catch (_e) {
return null;
}
}
async function writeGitNote(
namespace: string,
payload: Record<string, unknown>,
commitRef = "HEAD",
) {
const jsonString = JSON.stringify(payload);
const ref = `refs/notes/${namespace}`;
const cmd = await runCommand("git", [
"notes",
"--ref",
ref,
"add",
"-f",
"-m",
jsonString,
commitRef,
]);
if (cmd.code !== 0) {
console.error(`Failed to write Git Note to ${ref}:`, cmd.stderr);
throw new Error("Git Note write failed.");
}
return true;
}
async function evaluatePipeline() {
console.log("-> Evaluator Agent: Governing pipeline progression...");
// 1. Fetch transitions.json
const transitionsStr = await fetchFromMetaState("transitions.json");
if (!transitionsStr) {
console.warn(
"⚠️ transitions.json not found on meta-state branch. Defaulting to strict fail-safe.",
);
Deno.exit(1);
}
let transitions;
try {
transitions = JSON.parse(transitionsStr);
} catch (_e) {
console.error("❌ Failed to parse transitions.json.");
Deno.exit(1);
}
console.log(
" Loaded Bounded Model Checking rules:",
Object.keys(transitions).join(", "),
);
// 2. Fetch YAML DAGs to check task states
console.log(" Checking Project DAG constraints...");
const taskFiles = await listMetaStateDir("tasks/");
const tasks = [];
for (const file of taskFiles) {
const content = await fetchFromMetaState(file);
if (content) {
try {
const task = parseYaml(content);
tasks.push(task);
} catch (_e) {
console.warn(` ⚠️ Failed to parse YAML for ${file}`);
}
}
}
console.log(` Found ${tasks.length} tasks in meta-state:tasks/`);
// 3. Evaluate Bounded Model Checking rules
console.log(" Evaluating role transitions and notes based on changed paths...");
let pipelineValid = true;
let blockReason = "";
// Get changed files
let diff = await runCommand("git", ["diff", "--cached", "--name-only"]);
if (diff.stdout.trim() === "") {
diff = await runCommand("git", ["diff", "HEAD", "--name-only"]);
}
const changedFiles = diff.stdout.split("\n").map(f => f.trim()).filter(f => f.length > 0);
const scopes = transitions.scopes || {};
const requiredRules = new Set<string>();
if (changedFiles.length > 0) {
console.log(` Found ${changedFiles.length} changed file(s).`);
for (const [scopeName, scopeDef] of Object.entries(scopes)) {
const patterns: string[] = (scopeDef as any).patterns || [];
const rules: string[] = (scopeDef as any).requires || [];
const matched = changedFiles.some(file => {
return patterns.some(pattern => {
const regex = globToRegExp(pattern);
return regex.test(file);
});
});
if (matched) {
console.log(` -> Matched scope: ${scopeName}`);
for (const r of rules) {
requiredRules.add(r);
}
}
}
} else {
console.log(" No files changed. Falling back to default Coder requires for validation.");
if (transitions["Coder"] && transitions["Coder"].requires) {
for (const req of transitions["Coder"].requires) {
requiredRules.add(req);
}
}
}
console.log(` -> Enforcing Rules: ${Array.from(requiredRules).join(", ")}`);
// Verification based on union of rules
for (const req of requiredRules) {
if (req === "Gatekeeper_Approval") {
const gatekeeperNote = await getGitNotePayload("gatekeeper_checklist");
if (!gatekeeperNote || (gatekeeperNote.status !== "Completed" && gatekeeperNote.status !== "Approved")) {
pipelineValid = false;
blockReason = "Gatekeeper checklist is missing or not marked as 'Approved' or 'Completed'.";
console.warn(` ❌ BMC Rule Failed: ${blockReason}`);
} else {
console.log(` ✅ BMC Rule Passed: Gatekeeper_Approval satisfied.`);
}
}
// Add other rule checks here if they exist
// e.g. Adversary_Pass, Translator_Check
}
const evaluationResult = {
agent: "Evaluator",
timestamp: Date.now(),
pipelineState: pipelineValid ? "Valid" : "Blocked",
message: pipelineValid
? "All Bounded Model Checking rules satisfied. Transition permitted."
: `Transition Blocked. Reason: ${blockReason}`,
};
console.log(
`-> Evaluator Agent: Pipeline is ${evaluationResult.pipelineState}.`,
);
try {
await writeGitNote("evaluator_state", evaluationResult);
console.log(
"-> Evaluation state written to Git Notes (refs/notes/evaluator_state).",
);
} catch (e) {
console.warn("⚠️ Failed to write evaluator state to Git Notes:", e);
}
if (!pipelineValid) {
Deno.exit(1);
}
}
async function main() {
const args = parseArgs(Deno.args, {
boolean: ["run"],
});
if (args["run"]) {
await evaluatePipeline();
} else {
console.log("Evaluator Agent installed. Run with --run flag.");
}
}
if (import.meta.main) {
await main();
}