',
+ },
+ {
+ line: 9,
+ content: '

',
+ },
+ {
+ line: 10,
+ content: '

',
+ },
{
line: 11,
- content: '

',
+ content: "
",
},
{
line: 12,
diff --git a/.flue/lib/run-review-validation.test.ts b/.flue/lib/run-review-validation.test.ts
new file mode 100644
index 00000000000..aefe616c814
--- /dev/null
+++ b/.flue/lib/run-review-validation.test.ts
@@ -0,0 +1,85 @@
+import { describe, expect, it } from "vitest";
+import { applyValidationDecisions } from "./run-review-validation";
+import type { ReconcileFinding } from "../agents/reconcile-reviewer";
+
+function finding(id: string): ReconcileFinding {
+ return {
+ id,
+ severity: "warning",
+ path: "src/example.ts",
+ line: 10,
+ rule: "Test rule",
+ evidence: "Test evidence",
+ suggestion: "Test suggestion",
+ };
+}
+
+describe("applyValidationDecisions", () => {
+ it("keeps all findings when decisions are empty", () => {
+ const findings = [finding("CR-1"), finding("CR-2")];
+ const result = applyValidationDecisions(findings, []);
+ expect(result).toHaveLength(2);
+ });
+
+ it("removes findings marked invalid", () => {
+ const findings = [finding("CR-1"), finding("CR-2"), finding("CR-3")];
+ const result = applyValidationDecisions(findings, [
+ { id: "CR-1", verdict: "valid", reason: "ok" },
+ { id: "CR-2", verdict: "invalid", reason: "false positive" },
+ { id: "CR-3", verdict: "valid", reason: "ok" },
+ ]);
+ expect(result).toHaveLength(2);
+ expect(result.map((f) => f.id)).toEqual(["CR-1", "CR-3"]);
+ });
+
+ it("keeps findings with no decision (fail-open)", () => {
+ const findings = [finding("CR-1"), finding("CR-2")];
+ const result = applyValidationDecisions(findings, [
+ { id: "CR-1", verdict: "valid", reason: "ok" },
+ ]);
+ expect(result).toHaveLength(2);
+ });
+
+ it("keeps findings marked valid", () => {
+ const findings = [finding("CR-1"), finding("CR-2")];
+ const result = applyValidationDecisions(findings, [
+ { id: "CR-1", verdict: "valid", reason: "ok" },
+ { id: "CR-2", verdict: "valid", reason: "ok" },
+ ]);
+ expect(result).toHaveLength(2);
+ });
+
+ it("prefers valid over invalid for duplicate decisions", () => {
+ const findings = [finding("CR-1")];
+ const result = applyValidationDecisions(findings, [
+ { id: "CR-1", verdict: "invalid", reason: "false positive" },
+ { id: "CR-1", verdict: "valid", reason: "actually correct" },
+ ]);
+ expect(result).toHaveLength(1);
+ });
+
+ it("ignores decisions for unknown finding ids", () => {
+ const findings = [finding("CR-1")];
+ const result = applyValidationDecisions(findings, [
+ { id: "CR-999", verdict: "invalid", reason: "unknown" },
+ ]);
+ expect(result).toHaveLength(1);
+ });
+
+ it("removes all findings when all are invalid", () => {
+ const findings = [finding("CR-1"), finding("CR-2")];
+ const result = applyValidationDecisions(findings, [
+ { id: "CR-1", verdict: "invalid", reason: "fp" },
+ { id: "CR-2", verdict: "invalid", reason: "fp" },
+ ]);
+ expect(result).toHaveLength(0);
+ });
+
+ it("handles empty findings array", () => {
+ const result = applyValidationDecisions(
+ [],
+ [{ id: "CR-1", verdict: "invalid", reason: "fp" }],
+ );
+ expect(result).toHaveLength(0);
+ });
+});
diff --git a/.flue/lib/run-review-validation.ts b/.flue/lib/run-review-validation.ts
new file mode 100644
index 00000000000..623c6a916cc
--- /dev/null
+++ b/.flue/lib/run-review-validation.ts
@@ -0,0 +1,178 @@
+/**
+ * Trusted-code driver for the review validator agent.
+ *
+ * This is the control-flow half of finding validation — the part that runs
+ * in ordinary TypeScript, not in the model. It dispatches the validator
+ * agent with the active findings from one review stream, reads the
+ * structured decisions back, and suppresses findings marked invalid.
+ *
+ * Fail-open policy: on any error (timeout, schema failure, missing result),
+ * all findings are kept as-is. The validator can only suppress findings,
+ * never add new ones.
+ */
+import { init } from "@flue/runtime";
+import * as v from "valibot";
+import ReviewValidator, {
+ REVIEW_VALIDATION_DATA,
+ ReviewValidationSchema,
+ type ReviewValidatorInput,
+ type ReviewValidationData,
+} from "../agents/review-validator";
+import type { ReconcileFinding } from "../agents/reconcile-reviewer";
+
+const DISPATCH_MESSAGE =
+ "Validate the review findings by reading the actual file content, then submit your decisions.";
+
+/** Per-validation hard timeout — a wedged read must not hang the orchestrator step. */
+export const VALIDATION_TIMEOUT_MS = 5 * 60_000;
+
+// ── Pure helpers (unit-testable) ─────────────────────────────────────────────
+
+/**
+ * Apply validation decisions to a set of findings, returning only the
+ * findings that should remain active.
+ *
+ * - Findings explicitly marked `invalid` are removed.
+ * - Findings explicitly marked `valid` are kept.
+ * - Findings with no decision are kept (fail-open).
+ * - If a finding has both `valid` and `invalid` decisions (duplicate), it
+ * is kept — prefer keeping a potentially real finding over suppressing it.
+ * - Unknown decision IDs are ignored (they don't match any finding).
+ */
+export function applyValidationDecisions(
+ findings: ReconcileFinding[],
+ decisions: ReviewValidationData["decisions"],
+): ReconcileFinding[] {
+ const validIds = new Set