- **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>
246 lines
7.1 KiB
TypeScript
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();
|
|
}
|