New:Socket for Asana Is Now Available.Learn more
Get Started

@touchskyer/opc

Package Overview
Dependencies
Maintainers
1
Versions
26
Alerts
File Explorer

Advanced tools

Socket logo

Install Socket

Detect and block malicious and high-risk dependencies

Install

@touchskyer/opc - npm Package Compare versions

Comparing version
0.10.6
to
0.10.7
+498
bin/lib/flow-authority-depth.test.mjs
import { after, describe, test } from "node:test";
import assert from "node:assert/strict";
import { execFileSync } from "node:child_process";
import { createHash } from "node:crypto";
import { mkdtempSync, mkdirSync, readdirSync, readFileSync, rmSync, writeFileSync } from "node:fs";
import { homedir } from "node:os";
import { dirname, join } from "node:path";
import { fileURLToPath } from "node:url";
const TMPBASE = mkdtempSync(join(homedir(), ".opc", "sessions", "opc-authority-depth-"));
const HARNESS = join(dirname(fileURLToPath(import.meta.url)), "..", "opc-harness.mjs");
const TS0 = "2026-01-01T00:00:00.000Z";
after(() => rmSync(TMPBASE, { recursive: true, force: true }));
function runHarness(command, args) {
try {
const out = execFileSync("node", [HARNESS, command, ...args], {
encoding: "utf8",
stdio: ["pipe", "pipe", "pipe"],
});
return JSON.parse(out.trim().split("\n").pop());
} catch (error) {
const out = String(error.stdout || "").trim();
if (out) return JSON.parse(out.split("\n").pop());
return { error: error.message, stderr: String(error.stderr || "") };
}
}
function writeState(dir, state) {
mkdirSync(dir, { recursive: true });
writeFileSync(join(dir, "flow-state.json"), JSON.stringify({
version: "1.0",
flowTemplate: "build-verify",
currentNode: "gate",
entryNode: "test-execute",
totalSteps: 1,
maxTotalSteps: 25,
maxLoopsPerEdge: 3,
maxNodeReentry: 5,
history: [{ nodeId: "test-execute", runId: "run_1", timestamp: TS0 }],
edgeCounts: {},
repairEdgeCounts: {},
_written_by: "opc-harness",
_write_nonce: "authority-depth-test",
_last_modified: TS0,
...state,
}, null, 2));
}
function writeFlowFile(dir, name, flow) {
const path = join(dir, `${name}.json`);
writeFileSync(path, JSON.stringify({ opc_compat: ">=0.0", ...flow }, null, 2));
return path;
}
function sha256(text) {
return createHash("sha256").update(text).digest("hex");
}
function writeExactAndCanonical(dir, nodeId, runId, exact) {
const runDir = join(dir, "nodes", nodeId, runId);
mkdirSync(runDir, { recursive: true });
writeFileSync(join(runDir, "handshake.json"), JSON.stringify(exact, null, 2));
const canonical = {
...exact,
artifacts: exact.artifacts.map((artifact) => ({
...artifact,
path: artifact.path.startsWith("../") ? artifact.path.slice(3) : `${runId}/${artifact.path}`,
})),
};
writeFileSync(join(dir, "nodes", nodeId, "handshake.json"), JSON.stringify(canonical, null, 2));
}
function writeExactHandshake(dir, nodeId, runId, exact) {
const runDir = join(dir, "nodes", nodeId, runId);
mkdirSync(runDir, { recursive: true });
writeFileSync(join(runDir, "handshake.json"), JSON.stringify(exact, null, 2));
}
function passEval(name) {
return [
`# ${name}`,
"",
`${name} independently reviewed the artifact and found no blocker.`,
`${name} checked authority metadata, artifact references, and verdict routing.`,
"",
"VERDICT: PASS FINDINGS[0]",
"",
].join("\n");
}
function treeSnapshot(dir) {
const out = {};
function walk(rel) {
for (const entry of readdirSync(join(dir, rel), { withFileTypes: true })) {
const child = rel ? `${rel}/${entry.name}` : entry.name;
if (entry.isDirectory()) walk(child);
else if (entry.isFile()) out[child] = readFileSync(join(dir, child), "utf8");
}
}
walk("");
return out;
}
describe("deep authority invariants", () => {
test("validate-chain rejects test-result without deep OPC provenance", () => {
const dir = join(TMPBASE, "chain-provenance");
const runDir = join(dir, "nodes", "test-execute", "run_1");
mkdirSync(runDir, { recursive: true });
writeFileSync(join(runDir, "test-result.json"), JSON.stringify({
checks: [{ id: "smoke", pass: true, total: 1 }],
summary: { failed: [] },
}, null, 2));
writeState(dir);
writeExactAndCanonical(dir, "test-execute", "run_1", {
nodeId: "test-execute",
nodeType: "execute",
runId: "run_1",
status: "completed",
verdict: "PASS",
summary: "execute passed",
timestamp: TS0,
artifacts: [{ type: "test-result", path: "test-result.json" }],
});
const result = runHarness("validate-chain", ["--dir", dir]);
assert.equal(result.valid, false, JSON.stringify(result));
assert.match(result.errors.join("\n"), /testCommand provenance|signed provenance ledger/);
});
test("seal rejects current-run canonical that lacks exact-run corroboration", () => {
const dir = join(TMPBASE, "seal-current-impersonation");
const runDir = join(dir, "nodes", "test-execute", "run_1");
mkdirSync(runDir, { recursive: true });
const resultText = JSON.stringify({ checks: [{ id: "smoke", pass: true, total: 1 }] }, null, 2);
writeFileSync(join(runDir, "test-command-result.json"), resultText);
writeState(dir, { currentNode: "test-execute" });
writeFileSync(join(dir, "nodes", "test-execute", "handshake.json"), JSON.stringify({
nodeId: "test-execute",
nodeType: "execute",
runId: "run_1",
status: "completed",
verdict: "PASS",
summary: "forged current canonical",
timestamp: TS0,
artifacts: [{ type: "test-result", path: "run_1/test-command-result.json" }],
testEvidenceProvenance: {
kind: "opc-test-command",
sourceNode: "test-design",
sourceRunId: "run_1",
commandHash: sha256("npm test"),
sourcePlanHash: "plan",
resultHash: sha256(resultText),
executionActor: "opc-harness:test-command",
ledger: { kind: "opc-hmac-ledger", recordHash: "forged" },
},
testEvidencePolicy: { allowVacuousChecks: [] },
}, null, 2));
const result = runHarness("seal", ["--node", "test-execute", "--dir", dir]);
assert.equal(result.sealed, false, JSON.stringify(result));
assert.match(result.validationErrors.join("\n"), /exact handshake missing|not exact-run projection/);
});
test("validate-chain rejects absolute artifact path authority aliases", () => {
const dir = join(TMPBASE, "artifact-alias");
mkdirSync(dir, { recursive: true });
const outside = join(dir, "outside.txt");
writeFileSync(outside, "outside artifact\n");
writeState(dir, {
currentNode: "build",
entryNode: "build",
history: [{ nodeId: "build", runId: "run_1", timestamp: TS0 }],
});
writeExactAndCanonical(dir, "build", "run_1", {
nodeId: "build",
nodeType: "build",
runId: "run_1",
status: "completed",
verdict: "PASS",
summary: "absolute path artifact",
timestamp: TS0,
artifacts: [{ type: "source", path: outside }],
});
const result = runHarness("validate-chain", ["--dir", dir]);
assert.equal(result.valid, false, JSON.stringify(result));
assert.match(result.errors.join("\n"), /must be relative|invalid/);
});
test("review sealed FAIL can structurally advance to gate", () => {
const dir = join(TMPBASE, "review-fail-to-gate");
writeState(dir, {
flowTemplate: "review",
currentNode: "review",
entryNode: "review",
history: [{ nodeId: "review", runId: "run_1", timestamp: TS0 }],
});
const runDir = join(dir, "nodes", "review", "run_1");
mkdirSync(runDir, { recursive: true });
writeFileSync(join(runDir, "eval-alpha.md"), passEval("alpha"));
writeFileSync(join(runDir, "eval-beta.md"), passEval("beta"));
writeExactAndCanonical(dir, "review", "run_1", {
nodeId: "review",
nodeType: "review",
runId: "run_1",
status: "completed",
verdict: "FAIL",
summary: "review found blockers",
timestamp: TS0,
artifacts: [
{ type: "eval", path: "eval-alpha.md" },
{ type: "eval", path: "eval-beta.md" },
],
});
const result = runHarness("transition", [
"--from", "review", "--to", "gate", "--verdict", "PASS", "--flow", "review", "--dir", dir,
]);
assert.equal(result.allowed, true, JSON.stringify(result));
const state = JSON.parse(readFileSync(join(dir, "flow-state.json"), "utf8"));
assert.equal(state.currentNode, "gate");
});
test("seal rejects malformed stale canonical without mutating fixture tree", () => {
const dir = join(TMPBASE, "stale-malformed-canonical");
const run2 = join(dir, "nodes", "build", "run_2");
mkdirSync(run2, { recursive: true });
writeFileSync(join(run2, "payload.md"), "new payload\n");
writeState(dir, {
currentNode: "build",
entryNode: "build",
history: [
{ nodeId: "build", runId: "run_1", timestamp: TS0 },
{ nodeId: "build", runId: "run_2", timestamp: "2026-01-01T00:01:00.000Z" },
],
});
writeExactHandshake(dir, "build", "run_1", {
nodeId: "build",
nodeType: "build",
runId: "run_1",
status: "completed",
verdict: "PASS",
summary: "prior exact run exists",
timestamp: TS0,
artifacts: [],
});
writeFileSync(join(dir, "nodes", "build", "handshake.json"), JSON.stringify({ runId: "run_1" }, null, 2));
const before = treeSnapshot(dir);
const result = runHarness("seal", ["--node", "build", "--dir", dir]);
assert.equal(result.sealed, false, JSON.stringify(result));
assert.doesNotMatch(result.validationErrors.join("\n"), /exact handshake missing/);
assert.match(result.validationErrors.join("\n"), /canonical/);
assert.deepEqual(treeSnapshot(dir), before);
});
test("seal rejects stale canonical projection conflict without mutating fixture tree", () => {
const dir = join(TMPBASE, "stale-projection-conflict");
const run1 = join(dir, "nodes", "build", "run_1");
const run2 = join(dir, "nodes", "build", "run_2");
mkdirSync(run1, { recursive: true });
mkdirSync(run2, { recursive: true });
writeFileSync(join(run1, "payload.md"), "prior exact payload\n");
writeFileSync(join(run2, "payload.md"), "new payload\n");
writeState(dir, {
currentNode: "build",
entryNode: "build",
history: [
{ nodeId: "build", runId: "run_1", timestamp: TS0 },
{ nodeId: "build", runId: "run_2", timestamp: "2026-01-01T00:01:00.000Z" },
],
});
writeExactHandshake(dir, "build", "run_1", {
nodeId: "build",
nodeType: "build",
runId: "run_1",
status: "completed",
verdict: "PASS",
summary: "prior exact run",
timestamp: TS0,
artifacts: [{ type: "source", path: "payload.md" }],
});
writeFileSync(join(dir, "nodes", "build", "handshake.json"), JSON.stringify({
nodeId: "build",
nodeType: "build",
runId: "run_1",
status: "completed",
verdict: "PASS",
summary: "stale canonical lies about prior exact",
timestamp: TS0,
artifacts: [{ type: "source", path: "run_1/payload.md" }],
}, null, 2));
const before = treeSnapshot(dir);
const result = runHarness("seal", ["--node", "build", "--dir", dir]);
assert.equal(result.sealed, false, JSON.stringify(result));
assert.match(result.validationErrors.join("\n"), /not exact-run projection/);
assert.deepEqual(treeSnapshot(dir), before);
});
test("seal rejects orphan stale canonical even when orphan exact exists", () => {
const dir = join(TMPBASE, "stale-orphan-canonical");
const run1 = join(dir, "nodes", "build", "run_1");
const run2 = join(dir, "nodes", "build", "run_2");
mkdirSync(run1, { recursive: true });
mkdirSync(run2, { recursive: true });
writeFileSync(join(run1, "payload.md"), "orphan payload\n");
writeFileSync(join(run2, "payload.md"), "new payload\n");
writeState(dir, {
currentNode: "build",
entryNode: "build",
history: [{ nodeId: "build", runId: "run_2", timestamp: TS0 }],
});
writeExactAndCanonical(dir, "build", "run_1", {
nodeId: "build",
nodeType: "build",
runId: "run_1",
status: "completed",
verdict: "PASS",
summary: "orphan stale run",
timestamp: TS0,
artifacts: [{ type: "source", path: "payload.md" }],
});
const before = treeSnapshot(dir);
const result = runHarness("seal", ["--node", "build", "--dir", dir]);
assert.equal(result.sealed, false, JSON.stringify(result));
assert.match(result.validationErrors.join("\n"), /not recorded in authoritative history/);
assert.deepEqual(treeSnapshot(dir), before);
});
test("validate-chain rejects dot-slash artifact aliases", () => {
const dir = join(TMPBASE, "dot-slash-alias");
const runDir = join(dir, "nodes", "build", "run_1");
mkdirSync(runDir, { recursive: true });
writeFileSync(join(runDir, "payload.md"), "exact payload\n");
writeFileSync(join(dir, "nodes", "build", "payload.md"), "canonical payload\n");
writeState(dir, {
currentNode: "build",
entryNode: "build",
history: [{ nodeId: "build", runId: "run_1", timestamp: TS0 }],
});
writeExactAndCanonical(dir, "build", "run_1", {
nodeId: "build",
nodeType: "build",
runId: "run_1",
status: "completed",
verdict: "PASS",
summary: "dot alias",
timestamp: TS0,
artifacts: [{ type: "source", path: "./payload.md" }],
});
const before = treeSnapshot(dir);
const result = runHarness("validate-chain", ["--dir", dir]);
assert.equal(result.valid, false, JSON.stringify(result));
assert.match(result.errors.join("\n"), /clean relative path|invalid/);
assert.deepEqual(treeSnapshot(dir), before);
});
test("validate-chain rejects artifact paths pinned to the wrong run_N", () => {
const dir = join(TMPBASE, "wrong-run-alias");
const runDir = join(dir, "nodes", "build", "run_1", "run_2");
mkdirSync(runDir, { recursive: true });
writeFileSync(join(runDir, "payload.md"), "wrong nested payload\n");
writeState(dir, {
currentNode: "build",
entryNode: "build",
history: [{ nodeId: "build", runId: "run_1", timestamp: TS0 }],
});
writeExactAndCanonical(dir, "build", "run_1", {
nodeId: "build",
nodeType: "build",
runId: "run_1",
status: "completed",
verdict: "PASS",
summary: "wrong run path",
timestamp: TS0,
artifacts: [{ type: "source", path: "run_2/payload.md" }],
});
const before = treeSnapshot(dir);
const result = runHarness("validate-chain", ["--dir", dir]);
assert.equal(result.valid, false, JSON.stringify(result));
assert.match(result.errors.join("\n"), /run_N-prefixed|must start with 'run_1\/'/);
assert.deepEqual(treeSnapshot(dir), before);
});
test("validate-chain rejects traversal structured paths without stack trace", () => {
const dir = join(TMPBASE, "structured-traversal");
const runDir = join(dir, "nodes", "test-execute", "run_1");
mkdirSync(runDir, { recursive: true });
writeFileSync(join(runDir, "test-result.json"), JSON.stringify({ checks: [] }, null, 2));
writeState(dir);
writeExactAndCanonical(dir, "test-execute", "run_1", {
nodeId: "test-execute",
nodeType: "execute",
runId: "run_1",
status: "completed",
verdict: "PASS",
summary: "bad structured path",
timestamp: TS0,
artifacts: [{ type: "test-result", path: "../test-result.json" }],
});
const before = treeSnapshot(dir);
const result = runHarness("validate-chain", ["--dir", dir]);
assert.equal(result.valid, false, JSON.stringify(result));
assert.equal(result.stderr || "", "");
assert.match(result.errors.join("\n"), /traversal|escapes run directory|invalid path/);
assert.deepEqual(treeSnapshot(dir), before);
});
test("gate transition rejects absolute structured path without stack trace", () => {
const dir = join(TMPBASE, "gate-absolute-structured");
const runDir = join(dir, "nodes", "test-execute", "run_1");
mkdirSync(runDir, { recursive: true });
const absolute = join(runDir, "test-result.json");
writeFileSync(absolute, JSON.stringify({ checks: [] }, null, 2));
writeState(dir, {
currentNode: "gate",
entryNode: "test-execute",
history: [
{ nodeId: "test-execute", runId: "run_1", timestamp: TS0 },
{ nodeId: "gate", runId: "run_1", timestamp: "2026-01-01T00:01:00.000Z" },
],
});
writeExactAndCanonical(dir, "test-execute", "run_1", {
nodeId: "test-execute",
nodeType: "execute",
runId: "run_1",
status: "completed",
verdict: "PASS",
summary: "absolute structured path",
timestamp: TS0,
artifacts: [{ type: "test-result", path: absolute }],
});
writeExactAndCanonical(dir, "gate", "run_1", {
nodeId: "gate",
nodeType: "gate",
runId: "run_1",
status: "completed",
verdict: "PASS",
summary: "ready",
timestamp: TS0,
artifacts: [],
});
const before = treeSnapshot(dir);
const result = runHarness("transition", [
"--from", "gate", "--to", "null", "--verdict", "PASS", "--flow", "build-verify", "--dir", dir,
]);
assert.notEqual(result.allowed, true, JSON.stringify(result));
assert.notEqual(result.finalized, true, JSON.stringify(result));
assert.equal(result.stderr || "", "");
assert.match(JSON.stringify(result), /must be relative|fail-closed|invalid/);
assert.deepEqual(treeSnapshot(dir), before);
});
test("validate-chain allows whitelisted node-level authority artifact", () => {
const dir = join(TMPBASE, "node-level-control");
const runDir = join(dir, "nodes", "build", "run_1");
mkdirSync(runDir, { recursive: true });
writeFileSync(join(dir, "nodes", "build", "build-brief.md"), "brief\n");
writeState(dir, {
currentNode: "build",
entryNode: "build",
history: [{ nodeId: "build", runId: "run_1", timestamp: TS0 }],
});
writeExactAndCanonical(dir, "build", "run_1", {
nodeId: "build",
nodeType: "build",
runId: "run_1",
status: "completed",
verdict: "PASS",
summary: "node artifact",
timestamp: TS0,
artifacts: [{ type: "brief", path: "../build-brief.md" }],
});
const result = runHarness("validate-chain", ["--dir", dir]);
assert.equal(result.valid, true, JSON.stringify(result));
});
});
import { after, describe, test } from "node:test";
import assert from "node:assert/strict";
import { execFileSync } from "node:child_process";
import { mkdtempSync, mkdirSync, readFileSync, rmSync, writeFileSync } from "node:fs";
import { homedir } from "node:os";
import { dirname, join } from "node:path";
import { fileURLToPath } from "node:url";
const TMPBASE = mkdtempSync(join(homedir(), ".opc", "sessions", "opc-projection-"));
const HARNESS = join(dirname(fileURLToPath(import.meta.url)), "..", "opc-harness.mjs");
const TS0 = "2026-01-01T00:00:00.000Z";
after(() => rmSync(TMPBASE, { recursive: true, force: true }));
function runHarness(command, args) {
try {
const out = execFileSync("node", [HARNESS, command, ...args], {
encoding: "utf8",
stdio: ["pipe", "pipe", "pipe"],
});
return JSON.parse(out.trim().split("\n").pop());
} catch (error) {
const out = String(error.stdout || "").trim();
if (out) return JSON.parse(out.split("\n").pop());
return { error: error.message, stderr: String(error.stderr || "") };
}
}
function writeState(dir, state) {
mkdirSync(dir, { recursive: true });
writeFileSync(join(dir, "flow-state.json"), JSON.stringify({
version: "1.0",
flowTemplate: "build-verify",
currentNode: "build",
entryNode: "build",
totalSteps: 1,
maxTotalSteps: 25,
maxLoopsPerEdge: 3,
maxNodeReentry: 5,
history: [{ nodeId: "build", runId: "run_1", timestamp: TS0 }],
edgeCounts: {},
repairEdgeCounts: {},
_written_by: "opc-harness",
_write_nonce: "projection-test",
_last_modified: TS0,
...state,
}, null, 2));
}
function writeFlowFile(dir, name, flow) {
mkdirSync(dir, { recursive: true });
const path = join(dir, `${name}.json`);
writeFileSync(path, JSON.stringify({ opc_compat: ">=0.0", ...flow }, null, 2));
return path;
}
function passEval(name) {
return [
`# ${name}`,
"",
"Reviewed the selected exact run and found no blocking issues.",
"The evidence is internally consistent for this regression case.",
"",
"VERDICT: PASS FINDINGS[0]",
].join("\n");
}
function writeBuildRun(dir) {
const runDir = join(dir, "nodes", "build", "run_1");
mkdirSync(runDir, { recursive: true });
writeFileSync(join(runDir, "output.md"), "built\n");
const exact = {
nodeId: "build",
nodeType: "build",
runId: "run_1",
status: "completed",
verdict: "PASS",
summary: "built",
timestamp: TS0,
artifacts: [{ type: "source", path: "output.md" }],
};
writeFileSync(join(runDir, "handshake.json"), JSON.stringify(exact, null, 2));
writeFileSync(join(dir, "nodes", "build", "handshake.json"), JSON.stringify({
...exact,
artifacts: [{ type: "source", path: "run_1/output.md" }],
}, null, 2));
}
function writeReviewRun(dir, canonicalVerdict = "PASS") {
const runDir = join(dir, "nodes", "review", "run_1");
mkdirSync(runDir, { recursive: true });
writeFileSync(join(runDir, "eval-a.md"), passEval("Review A"));
writeFileSync(join(runDir, "eval-b.md"), passEval("Review B"));
const exact = {
nodeId: "review",
nodeType: "review",
runId: "run_1",
status: "completed",
verdict: "PASS",
summary: "review passed",
timestamp: TS0,
artifacts: [
{ type: "eval", path: "eval-a.md" },
{ type: "eval", path: "eval-b.md" },
],
};
writeFileSync(join(runDir, "handshake.json"), JSON.stringify(exact, null, 2));
writeFileSync(join(dir, "nodes", "review", "handshake.json"), JSON.stringify({
...exact,
verdict: canonicalVerdict,
artifacts: exact.artifacts.map((a) => ({ ...a, path: `run_1/${a.path}` })),
}, null, 2));
}
describe("canonical projection authority", () => {
test("validate-chain rejects canonical fields that differ from exact run", () => {
const dir = join(TMPBASE, "validate-chain");
writeState(dir);
writeBuildRun(dir);
const path = join(dir, "nodes", "build", "handshake.json");
const canonical = JSON.parse(readFileSync(path, "utf8"));
canonical.verdict = "FAIL";
writeFileSync(path, JSON.stringify(canonical, null, 2));
const result = runHarness("validate-chain", ["--dir", dir]);
assert.equal(result.valid, false, JSON.stringify(result));
assert.match(result.errors.join("\n"), /not exact-run projection/);
assert.match(result.errors.join("\n"), /\$\.verdict/);
});
test("gate transition rejects upstream canonical projection mismatch", () => {
const dir = join(TMPBASE, "gate-transition");
const flowFile = writeFlowFile(dir, "projection-flow", {
nodes: ["review", "gate", "done"],
edges: { review: { PASS: "gate" }, gate: { PASS: "done" }, done: { PASS: null } },
limits: { maxLoopsPerEdge: 3, maxTotalSteps: 10, maxNodeReentry: 5 },
nodeTypes: { review: "review", gate: "gate", done: "build" },
});
writeState(dir, {
flowTemplate: "projection-flow",
_flow_file: flowFile,
currentNode: "gate",
entryNode: "review",
history: [
{ nodeId: "review", runId: "run_1", timestamp: TS0 },
{ nodeId: "gate", runId: "run_1", timestamp: "2026-01-01T00:00:01.000Z" },
],
edgeCounts: { "review->gate": 1 },
});
writeReviewRun(dir, "FAIL");
const result = runHarness("transition", [
"--from", "gate", "--to", "done", "--verdict", "PASS", "--dir", dir,
]);
assert.equal(result.allowed, false, JSON.stringify(result));
assert.match(result.reason, /gate authority check failed/);
assert.match(result.reason, /not exact-run projection/);
});
test("seal selected loopback run replaces stale canonical", () => {
const dir = join(TMPBASE, "seal-loopback");
writeState(dir, {
flowTemplate: "review",
currentNode: "review",
entryNode: "review",
totalSteps: 2,
history: [
{ nodeId: "review", runId: "run_1", timestamp: TS0 },
{ nodeId: "gate", runId: "run_1", timestamp: "2026-01-01T00:00:01.000Z" },
{ nodeId: "review", runId: "run_2", timestamp: "2026-01-01T00:00:02.000Z" },
],
});
writeReviewRun(dir);
const run2 = join(dir, "nodes", "review", "run_2");
mkdirSync(run2, { recursive: true });
writeFileSync(join(run2, "eval-a.md"), passEval("Review A"));
writeFileSync(join(run2, "eval-b.md"), passEval("Review B"));
const result = runHarness("seal", ["--node", "review", "--dir", dir]);
assert.equal(result.sealed, true, JSON.stringify(result));
const canonical = JSON.parse(readFileSync(join(dir, "nodes", "review", "handshake.json"), "utf8"));
const exact = JSON.parse(readFileSync(join(run2, "handshake.json"), "utf8"));
assert.equal(canonical.runId, "run_2");
assert.equal(exact.runId, "run_2");
assert.ok(canonical.artifacts.every((a) => a.path.startsWith("run_2/")));
assert.ok(exact.artifacts.every((a) => !a.path.startsWith("run_2/")));
});
test("strict finalize rejects exact terminal run missing evidence policy", () => {
const dir = join(TMPBASE, "finalize-exact");
const flowFile = writeFlowFile(dir, "terminal-execute-flow", {
nodes: ["test-execute"],
edges: { "test-execute": { PASS: null } },
limits: { maxLoopsPerEdge: 3, maxTotalSteps: 10, maxNodeReentry: 5 },
nodeTypes: { "test-execute": "execute" },
});
writeState(dir, {
flowTemplate: "terminal-execute-flow",
_flow_file: flowFile,
currentNode: "test-execute",
entryNode: "test-execute",
history: [{ nodeId: "test-execute", runId: "run_1", timestamp: TS0 }],
});
const runDir = join(dir, "nodes", "test-execute", "run_1");
mkdirSync(runDir, { recursive: true });
writeFileSync(join(runDir, "test-command-result.json"), JSON.stringify({
summary: { failed: [] },
checks: [{ id: "smoke", pass: true, total: 1 }],
}, null, 2));
const base = {
nodeId: "test-execute",
nodeType: "execute",
runId: "run_1",
status: "completed",
verdict: "PASS",
summary: "terminal execute",
timestamp: TS0,
artifacts: [{ type: "test-result", path: "test-command-result.json" }],
testEvidenceProvenance: {
kind: "opc-test-command",
executionActor: "opc-harness:test-command",
sourceNode: "test-design",
sourceRunId: "run_1",
commandHash: "cmd",
sourcePlanHash: "plan",
resultHash: "result",
ledger: { kind: "opc-hmac-ledger", recordHash: "record" },
},
};
writeFileSync(join(runDir, "handshake.json"), JSON.stringify({ ...base, testEvidencePolicy: null }, null, 2));
writeFileSync(join(dir, "nodes", "test-execute", "handshake.json"), JSON.stringify({
...base,
artifacts: [{ type: "test-result", path: "run_1/test-command-result.json" }],
testEvidencePolicy: { allowVacuousChecks: [] },
}, null, 2));
const result = runHarness("finalize", ["--strict", "--dir", dir]);
assert.equal(result.finalized, false, JSON.stringify(result));
assert.match(result.error, /testEvidencePolicy must be a non-null object|not exact-run projection/);
const state = JSON.parse(readFileSync(join(dir, "flow-state.json"), "utf8"));
assert.notEqual(state.status, "completed");
});
});
import { after, describe, test } from "node:test";
import assert from "node:assert/strict";
import {
existsSync,
mkdirSync,
mkdtempSync,
readFileSync,
rmSync,
writeFileSync,
} from "node:fs";
import { homedir } from "node:os";
import { dirname, join } from "node:path";
import { fileURLToPath } from "node:url";
import { execFileSync } from "node:child_process";
import { resolveNodeExtensionContext } from "./ext-commands.mjs";
import { checkStructuredResults } from "./flow-transition.mjs";
import { buildCumulativeFindingsMarkdown } from "./cumulative-findings.mjs";
const TMPBASE = mkdtempSync(join(homedir(), ".opc", "sessions", "opc-authority-"));
const HARNESS = join(dirname(fileURLToPath(import.meta.url)), "..", "opc-harness.mjs");
after(() => rmSync(TMPBASE, { recursive: true, force: true }));
function runHarness(command, args, options = {}) {
try {
const output = execFileSync("node", [HARNESS, command, ...args], {
encoding: "utf8",
stdio: ["pipe", "pipe", "pipe"],
env: { ...process.env, ...(options.env || {}) },
});
return JSON.parse(output.trim().split("\n").pop());
} catch (error) {
const output = String(error.stdout || "").trim();
if (output) return JSON.parse(output.split("\n").pop());
return { error: error.message, stderr: String(error.stderr || "") };
}
}
function runHarnessRaw(command, args, options = {}) {
try {
return {
status: 0,
stdout: execFileSync("node", [HARNESS, command, ...args], {
encoding: "utf8",
stdio: ["pipe", "pipe", "pipe"],
env: { ...process.env, ...(options.env || {}) },
}),
stderr: "",
};
} catch (error) {
return {
status: error.status || 1,
stdout: String(error.stdout || ""),
stderr: String(error.stderr || ""),
};
}
}
function writeState(dir, state) {
mkdirSync(dir, { recursive: true });
writeFileSync(join(dir, "flow-state.json"), JSON.stringify({
version: "1.0",
flowTemplate: "build-verify",
currentNode: "build",
entryNode: "build",
totalSteps: 1,
maxTotalSteps: 25,
maxLoopsPerEdge: 3,
maxNodeReentry: 5,
history: [{ nodeId: "build", runId: "run_1", timestamp: "2026-01-01T00:00:00.000Z" }],
edgeCounts: {},
repairEdgeCounts: {},
_written_by: "opc-harness",
_write_nonce: "authority-test",
_last_modified: "2026-01-01T00:00:00.000Z",
...state,
}, null, 2));
}
function writeBuildHandshake(dir, runId = "run_1") {
mkdirSync(join(dir, "nodes", "build", runId), { recursive: true });
writeFileSync(join(dir, "nodes", "build", runId, "output.md"), "built");
const handshake = {
nodeId: "build",
nodeType: "build",
runId,
status: "completed",
verdict: null,
summary: "built",
timestamp: "2026-01-01T00:00:00.000Z",
artifacts: [{ type: "source", path: `${runId}/output.md` }],
};
writeFileSync(join(dir, "nodes", "build", runId, "handshake.json"), JSON.stringify({
...handshake,
artifacts: [{ type: "source", path: "output.md" }],
}, null, 2));
writeFileSync(join(dir, "nodes", "build", "handshake.json"), JSON.stringify(handshake, null, 2));
}
function writeReviewRun(dir, nodeId, runId, options = {}) {
const runDir = join(dir, "nodes", nodeId, runId);
mkdirSync(runDir, { recursive: true });
writeFileSync(join(runDir, "eval-a.md"), "# Review A\n\nNo blocking findings.\n");
writeFileSync(join(runDir, "eval-b.md"), "# Review B\n\nIndependent approval.\n");
const handshake = {
nodeId,
nodeType: "review",
runId,
status: "completed",
verdict: "PASS",
summary: "review passed",
timestamp: "2026-01-01T00:00:00.000Z",
artifacts: [
{ type: "eval", path: "eval-a.md" },
{ type: "eval", path: "eval-b.md" },
],
...options,
};
writeFileSync(join(runDir, "handshake.json"), JSON.stringify(handshake, null, 2));
return handshake;
}
function writeTerminalHandshake(dir, nodeId, runId, nodeType = "build") {
mkdirSync(join(dir, "nodes", nodeId, runId), { recursive: true });
const handshake = {
nodeId,
nodeType,
runId,
status: "completed",
verdict: "PASS",
summary: "terminal node passed",
timestamp: "2026-01-01T00:00:00.000Z",
artifacts: [],
};
writeFileSync(join(dir, "nodes", nodeId, runId, "handshake.json"), JSON.stringify(handshake, null, 2));
writeFileSync(join(dir, "nodes", nodeId, "handshake.json"), JSON.stringify(handshake, null, 2));
}
function writeFlowFile(dir, name, flow) {
mkdirSync(dir, { recursive: true });
const path = join(dir, `${name}.json`);
writeFileSync(path, JSON.stringify({ opc_compat: ">=0.0", ...flow }, null, 2));
return path;
}
function completeTestPlan() {
return [
"# Test Plan",
"",
"Covers unit smoke checks with npm test.",
"Covers contract schema validation and edge case behavior.",
"Covers integration workflow and e2e flow behavior.",
"Covers UI visual screenshot responsive a11y and playwright checks.",
"Covers tier baseline polish, dark mode, navigation, and favicon checks.",
"",
"Run:",
"npm test",
"",
].join("\n");
}
describe("flow authority regressions", () => {
test("missing tier is treated as unknown/non-visual", () => {
const dir = join(TMPBASE, "missing-tier");
mkdirSync(dir, { recursive: true });
writeFileSync(join(dir, "acceptance-criteria.md"), "# Backend task\n");
const flowFile = join(dir, "flow.json");
writeFileSync(flowFile, JSON.stringify({
opc_compat: ">=0.0",
nodes: ["build"],
edges: { build: { PASS: null } },
limits: { maxLoopsPerEdge: 3, maxTotalSteps: 10, maxNodeReentry: 5 },
nodeTypes: { build: "build" },
nodeCapabilities: { build: ["design-intelligence@1"] },
}));
writeState(dir, {
flowTemplate: "flow",
_flow_file: flowFile,
tier: undefined,
totalSteps: 0,
history: [],
});
const context = resolveNodeExtensionContext(dir, "build", ["--dir", dir]);
assert.equal(context.tier, null);
assert.equal(context.visualEvaluationRequired, false);
assert.equal(context.nodeCapabilitiesResolved, true);
});
test("seal uses the state-selected run, not filesystem latest", () => {
const dir = join(TMPBASE, "seal-selected-run");
writeState(dir);
writeBuildHandshake(dir, "run_1");
mkdirSync(join(dir, "nodes", "build", "run_2"), { recursive: true });
writeFileSync(join(dir, "nodes", "build", "run_2", "rogue.md"), "rogue");
const sealed = runHarness("seal", ["--node", "build", "--dir", dir]);
assert.equal(sealed.sealed, true, JSON.stringify(sealed));
const handshake = JSON.parse(readFileSync(join(dir, "nodes", "build", "handshake.json"), "utf8"));
assert.equal(handshake.runId, "run_1");
assert.ok(!handshake.artifacts.some((artifact) => artifact.path.includes("run_2")));
const rogue = runHarness("seal", ["--node", "build", "--run", "2", "--dir", dir]);
assert.equal(rogue.sealed, false, JSON.stringify(rogue));
assert.match(rogue.error, /does not match selected run/);
});
test("seal validation failure leaves canonical handshake unchanged", () => {
const dir = join(TMPBASE, "seal-no-mutation");
writeState(dir);
mkdirSync(join(dir, "nodes", "build", "run_1"), { recursive: true });
writeFileSync(join(dir, "nodes", "build", "run_1", "broken.json"), "{broken");
writeBuildHandshake(dir, "run_1");
const handshakePath = join(dir, "nodes", "build", "handshake.json");
const before = readFileSync(handshakePath, "utf8");
const sealed = runHarness("seal", ["--node", "build", "--dir", dir]);
assert.equal(sealed.sealed, false, JSON.stringify(sealed));
assert.match(sealed.validationErrors.join("\n"), /broken\.json/);
assert.equal(readFileSync(handshakePath, "utf8"), before);
});
test("seal refuses to overwrite malformed canonical handshake", () => {
const dir = join(TMPBASE, "seal-malformed-canonical");
writeState(dir);
mkdirSync(join(dir, "nodes", "build", "run_1"), { recursive: true });
writeFileSync(join(dir, "nodes", "build", "run_1", "output.md"), "built");
writeFileSync(join(dir, "nodes", "build", "run_1", "handshake.json"), JSON.stringify({
nodeId: "build",
nodeType: "build",
runId: "run_1",
status: "completed",
verdict: null,
summary: "selected run",
timestamp: "2026-01-01T00:00:00.000Z",
artifacts: [{ type: "source", path: "output.md" }],
}, null, 2));
const handshakePath = join(dir, "nodes", "build", "handshake.json");
writeFileSync(handshakePath, "{broken");
const sealed = runHarness("seal", ["--node", "build", "--dir", dir]);
assert.equal(sealed.sealed, false, JSON.stringify(sealed));
assert.match(sealed.validationErrors.join("\n"), /canonical handshake\.json parse error/);
assert.equal(readFileSync(handshakePath, "utf8"), "{broken");
});
test("stopped flows reject mutating commands", () => {
const dir = join(TMPBASE, "stopped");
writeState(dir, { status: "stopped" });
writeBuildHandshake(dir, "run_1");
assert.match(runHarness("skip", ["--dir", dir]).error, /flow is stopped/);
assert.match(runHarness("goto", ["code-review", "--dir", dir]).error, /flow is stopped/);
assert.match(runHarness("seal", ["--node", "build", "--dir", dir]).error, /flow is stopped/);
assert.match(
runHarness("transition", [
"--from", "build", "--to", "code-review", "--verdict", "PASS", "--dir", dir,
]).reason,
/flow is stopped/,
);
});
test("stopped flows reject extension lifecycle writes and stop is idempotent", () => {
const dir = join(TMPBASE, "stopped-extensions");
writeState(dir, { status: "stopped", stoppedAt: "2026-01-01T00:00:00.000Z" });
writeBuildHandshake(dir, "run_1");
const statePath = join(dir, "flow-state.json");
const before = readFileSync(statePath, "utf8");
assert.match(runHarness("prompt-context", ["--node", "build", "--role", "tester", "--dir", dir]).stderr, /flow is stopped/);
assert.match(runHarness("extension-verdict", ["--node", "build", "--dir", dir]).stderr, /flow is stopped/);
assert.match(runHarness("extension-artifact", ["--node", "build", "--dir", dir]).stderr, /flow is stopped/);
assert.match(runHarness("node-preflight", ["--node", "build", "--dir", dir]).stderr, /flow is stopped/);
const stoppedAgain = runHarness("stop", ["--dir", dir]);
assert.equal(stoppedAgain.alreadyStopped, true, JSON.stringify(stoppedAgain));
assert.equal(readFileSync(statePath, "utf8"), before);
});
test("stopped extension commands do not load extension top-level or startup code", () => {
const dir = join(TMPBASE, "stopped-extension-load");
const extDir = join(dir, "exts");
const markerDir = join(dir, "markers");
const ext = join(extDir, "marker-ext");
writeState(dir, { status: "stopped", stoppedAt: "2026-01-01T00:00:00.000Z" });
writeBuildHandshake(dir, "run_1");
mkdirSync(ext, { recursive: true });
mkdirSync(markerDir, { recursive: true });
writeFileSync(join(ext, "hook.mjs"), `
import { writeFileSync } from "node:fs";
import { join } from "node:path";
if (process.env.OPC_MARKER_DIR) writeFileSync(join(process.env.OPC_MARKER_DIR, "top-level"), "loaded");
export default {
meta: { name: "marker-ext", provides: ["design-system-injection@1"] },
startup: {
check() {
writeFileSync(join(process.env.OPC_MARKER_DIR, "startup"), "checked");
return { ok: true };
}
},
hooks: { "prompt.append": () => "marker" }
};
`);
const result = runHarness("prompt-context", ["--node", "build", "--role", "tester", "--dir", dir], {
env: { OPC_EXTENSIONS_DIR: extDir, OPC_MARKER_DIR: markerDir },
});
assert.match(result.stderr, /flow is stopped/);
assert.equal(existsSync(join(markerDir, "top-level")), false);
assert.equal(existsSync(join(markerDir, "startup")), false);
});
test("seal explicit empty --run fails before rewriting canonical bytes", () => {
const dir = join(TMPBASE, "seal-empty-run");
writeState(dir);
writeBuildHandshake(dir, "run_1");
const handshakePath = join(dir, "nodes", "build", "handshake.json");
const before = readFileSync(handshakePath, "utf8");
const eqEmpty = runHarness("seal", ["--node", "build", "--run=", "--dir", dir]);
const trailing = runHarness("seal", ["--node", "build", "--dir", dir, "--run"]);
assert.equal(eqEmpty.sealed, false, JSON.stringify(eqEmpty));
assert.match(eqEmpty.error, /--run must be a positive numeric ordinal/);
assert.equal(trailing.sealed, false, JSON.stringify(trailing));
assert.match(trailing.error, /--run must be a positive numeric ordinal/);
assert.equal(readFileSync(handshakePath, "utf8"), before);
});
test("seal rejects parsed but invalid canonical handshake roots without rewriting", () => {
for (const [name, content, pattern] of [
["null", "null\n", /root must be a non-null object/],
["array", "[]\n", /root must be a non-null object/],
["scalar", "\"x\"\n", /root must be a non-null object/],
["empty-object", "{}\n", /nodeId.*expected 'build'/],
["partial-object", JSON.stringify({ nodeId: "build", runId: "run_1", status: "completed", artifacts: [] }), /missing or empty required field: nodeType|missing or empty required field: summary|missing or empty required field: timestamp/],
["wrong-run", JSON.stringify({ nodeId: "build", nodeType: "build", runId: "run_2", status: "completed", artifacts: [] }), /runId.*expected 'run_1'/],
]) {
const dir = join(TMPBASE, `seal-invalid-canonical-${name}`);
writeState(dir);
mkdirSync(join(dir, "nodes", "build", "run_1"), { recursive: true });
writeFileSync(join(dir, "nodes", "build", "run_1", "output.md"), "built");
const handshakePath = join(dir, "nodes", "build", "handshake.json");
writeFileSync(handshakePath, content);
const result = runHarness("seal", ["--node", "build", "--dir", dir]);
assert.equal(result.sealed, false, `${name}: ${JSON.stringify(result)}`);
assert.match(result.validationErrors.join("\n"), pattern, name);
assert.equal(readFileSync(handshakePath, "utf8"), content, name);
}
});
test("state-backed commands reject explicit flow identity mismatch", () => {
const dir = join(TMPBASE, "flow-identity");
writeState(dir);
writeBuildHandshake(dir, "run_1");
const skip = runHarness("skip", ["--flow", "quick", "--dir", dir]);
assert.match(skip.error, /persisted flow identity.*build-verify.*quick/);
const chain = runHarness("validate-chain", ["--flow", "quick", "--dir", dir]);
assert.equal(chain.valid, false, JSON.stringify(chain));
assert.match(chain.errors.join("\n"), /persisted flow identity.*build-verify.*quick/);
});
test("viz refuses explicit graph substitution when state has persisted flow identity", () => {
const dir = join(TMPBASE, "viz-flow-identity");
writeState(dir);
const result = runHarnessRaw("viz", ["--flow", "quick", "--dir", dir]);
assert.notEqual(result.status, 0);
assert.match(result.stderr, /persisted flow identity.*build-verify.*quick/);
});
test("validate and validate-chain reject canonical handshake run mismatch", () => {
const dir = join(TMPBASE, "validate-run-mismatch");
writeState(dir);
mkdirSync(join(dir, "nodes", "build", "run_1"), { recursive: true });
writeFileSync(join(dir, "nodes", "build", "run_1", "output.md"), "built");
writeFileSync(join(dir, "nodes", "build", "run_1", "handshake.json"), JSON.stringify({
nodeId: "build",
nodeType: "build",
runId: "run_1",
status: "completed",
verdict: null,
summary: "selected run",
timestamp: "2026-01-01T00:00:00.000Z",
artifacts: [{ type: "source", path: "output.md" }],
}, null, 2));
writeFileSync(join(dir, "nodes", "build", "handshake.json"), JSON.stringify({
nodeId: "build",
nodeType: "build",
runId: "run_2",
status: "completed",
verdict: null,
summary: "wrong run",
timestamp: "2026-01-01T00:00:00.000Z",
artifacts: [{ type: "source", path: "run_1/output.md" }],
}, null, 2));
const positional = runHarness("validate", [join(dir, "nodes", "build", "handshake.json")]);
const defaultValidate = runHarness("validate", ["--dir", dir]);
const chain = runHarness("validate-chain", ["--dir", dir]);
assert.equal(positional.valid, false, JSON.stringify(positional));
assert.match(positional.errors.join("\n"), /runId.*expected 'run_1'/);
assert.equal(defaultValidate.valid, false, JSON.stringify(defaultValidate));
assert.match(defaultValidate.errors.join("\n"), /runId.*expected 'run_1'/);
assert.equal(chain.valid, false, JSON.stringify(chain));
assert.match(chain.errors.join("\n"), /runId.*expected 'run_1'/);
});
test("positional run-scoped validate cannot self-authorize a non-selected run", () => {
const dir = join(TMPBASE, "validate-positional-run-scope");
writeState(dir);
const run2 = join(dir, "nodes", "build", "run_2");
mkdirSync(run2, { recursive: true });
writeFileSync(join(run2, "handshake.json"), JSON.stringify({
nodeId: "build",
nodeType: "build",
runId: "run_2",
status: "completed",
verdict: null,
summary: "valid but not selected",
timestamp: "2026-01-01T00:00:00.000Z",
artifacts: [],
}, null, 2));
const result = runHarness("validate", [join(run2, "handshake.json")]);
assert.equal(result.valid, false, JSON.stringify(result));
assert.match(result.errors.join("\n"), /path run is 'run_2', expected 'run_1'/);
});
test("pass synthesizes authoritative upstream run instead of rogue latest run", () => {
const dir = join(TMPBASE, "pass-exact-upstream");
const flowFile = join(dir, "authority-pass-flow.json");
mkdirSync(dir, { recursive: true });
writeFileSync(flowFile, JSON.stringify({
opc_compat: ">=0.0",
nodes: ["review", "gate", "done"],
edges: { review: { PASS: "gate" }, gate: { PASS: "done" }, done: { PASS: null } },
limits: { maxLoopsPerEdge: 3, maxTotalSteps: 10, maxNodeReentry: 5 },
nodeTypes: { review: "review", gate: "gate", done: "build" },
}));
writeState(dir, {
flowTemplate: "authority-pass-flow",
_flow_file: flowFile,
currentNode: "gate",
entryNode: "review",
history: [
{ nodeId: "review", runId: "run_1", timestamp: "2026-01-01T00:00:00.000Z" },
{ nodeId: "gate", runId: "run_1", timestamp: "2026-01-01T00:00:01.000Z" },
],
edgeCounts: { "review→gate": 1 },
});
const run1 = join(dir, "nodes", "review", "run_1");
const run2 = join(dir, "nodes", "review", "run_2");
mkdirSync(run1, { recursive: true });
mkdirSync(run2, { recursive: true });
const passEval = (title, noun) => [
`# ${title}`,
"",
"## Evidence Scope",
...Array.from({ length: 24 }, (_, i) => `${noun} scope check ${i + 1}: reviewed selected run_1 evidence for the force-pass guard.`),
"",
"## Authority Notes",
...Array.from({ length: 24 }, (_, i) => `${noun} authority note ${i + 1}: rogue run_2 content is intentionally outside the accepted source run.`),
"",
"## Verdict",
...Array.from({ length: 8 }, (_, i) => `${noun} closing note ${i + 1}: no blocking issue found in authoritative run_1.`),
"VERDICT: PASS FINDINGS[0]",
].join("\n");
writeFileSync(join(run1, "eval-skeptic-owner.md"), passEval("Skeptic Owner", "ownership"));
writeFileSync(join(run1, "eval-peer.md"), passEval("Peer", "peer"));
writeFileSync(join(run2, "eval-a.md"), "# A\n\n🔴 Bug — file.js:1 — rogue\n→ fix\nReasoning: rogue\nVERDICT: FAIL FINDINGS[1]\n");
const reviewHandshake = {
nodeId: "review",
nodeType: "review",
runId: "run_1",
status: "completed",
verdict: "PASS",
summary: "authoritative run",
timestamp: "2026-01-01T00:00:00.000Z",
artifacts: [
{ type: "eval", path: "run_1/eval-skeptic-owner.md" },
{ type: "eval", path: "run_1/eval-peer.md" },
],
};
writeFileSync(join(run1, "handshake.json"), JSON.stringify({
...reviewHandshake,
artifacts: [
{ type: "eval", path: "eval-skeptic-owner.md" },
{ type: "eval", path: "eval-peer.md" },
],
}, null, 2));
writeFileSync(join(dir, "nodes", "review", "handshake.json"), JSON.stringify(reviewHandshake, null, 2));
mkdirSync(join(dir, "nodes", "gate"), { recursive: true });
writeFileSync(join(dir, "nodes", "gate", "handshake.json"), JSON.stringify({
nodeId: "gate",
nodeType: "gate",
runId: "run_1",
status: "completed",
verdict: "PASS",
summary: "gate",
timestamp: "2026-01-01T00:00:01.000Z",
artifacts: [],
}, null, 2));
const result = runHarness("pass", ["--dir", dir]);
assert.equal(result.allowed, true, JSON.stringify(result));
const state = JSON.parse(readFileSync(join(dir, "flow-state.json"), "utf8"));
assert.equal(state.currentNode, "done");
});
test("historical evidence fallback is bound to the history runId", () => {
const dir = join(TMPBASE, "history-run-authority");
const rogueRun = join(dir, "nodes", "review", "run_2");
mkdirSync(rogueRun, { recursive: true });
writeFileSync(join(rogueRun, "handshake.json"), JSON.stringify({
nodeId: "review",
nodeType: "review",
runId: "run_1",
status: "completed",
verdict: "PASS",
summary: "forged latest run",
timestamp: "2026-01-01T00:00:00.000Z",
artifacts: [],
}));
const state = {
history: [
{ nodeId: "review", runId: "run_1", timestamp: "2026-01-01T00:00:00.000Z" },
{ nodeId: "gate", runId: "run_1", timestamp: "2026-01-01T00:00:01.000Z" },
],
};
const template = {
nodeTypes: { review: "review", gate: "gate" },
requiredTestCommandEvidence: false,
};
const reasons = checkStructuredResults(dir, state, template, "gate");
assert.ok(reasons.some((reason) => /missing handshake for node 'review' run 'run_1'/.test(reason)), reasons.join("\n"));
});
test("validate-chain binds required extension provenance to the history run", () => {
const dir = join(TMPBASE, "validate-chain-extension-history-run");
const flowFile = writeFlowFile(dir, "authority-extension-flow", {
nodes: ["code-review", "gate"],
edges: { "code-review": { PASS: "gate" }, gate: { PASS: null } },
limits: { maxLoopsPerEdge: 3, maxTotalSteps: 10, maxNodeReentry: 5 },
nodeTypes: { "code-review": "review", gate: "gate" },
nodeCapabilities: { "code-review": ["code-quality-check@1"] },
});
mkdirSync(join(dir, ".opc"), { recursive: true });
writeFileSync(join(dir, ".opc", "config.json"), JSON.stringify({ requiredExtensions: ["req-ext"] }));
writeState(dir, {
flowTemplate: "authority-extension-flow",
_flow_file: flowFile,
currentNode: "gate",
entryNode: "code-review",
history: [
{ nodeId: "code-review", runId: "run_1", timestamp: "2026-01-01T00:00:00.000Z" },
{ nodeId: "gate", runId: "run_1", timestamp: "2026-01-01T00:00:01.000Z" },
],
edgeCounts: { "code-review→gate": 1 },
});
writeReviewRun(dir, "code-review", "run_1", { extensionsApplied: ["req-ext"] });
const rogueRun = join(dir, "nodes", "code-review", "run_2");
mkdirSync(rogueRun, { recursive: true });
writeFileSync(join(rogueRun, "eval-extensions.json"), JSON.stringify({
version: 1,
extensionsApplied: ["req-ext"],
findings: [],
}, null, 2));
const result = runHarness("validate-chain", ["--dir", dir]);
assert.equal(result.valid, false, JSON.stringify(result));
assert.match(result.errors.join("\n"), /eval-extensions\.json not found in run_1/);
});
test("strict finalize validates history exact runs instead of node-level canonical latest", () => {
const dir = join(TMPBASE, "strict-finalize-history-run");
const flowFile = writeFlowFile(dir, "strict-finalize-flow", {
nodes: ["review", "done"],
edges: { review: { PASS: "done" }, done: { PASS: null } },
limits: { maxLoopsPerEdge: 3, maxTotalSteps: 10, maxNodeReentry: 5 },
nodeTypes: { review: "review", done: "build" },
});
writeState(dir, {
flowTemplate: "strict-finalize-flow",
_flow_file: flowFile,
currentNode: "done",
entryNode: "review",
history: [
{ nodeId: "review", runId: "run_1", timestamp: "2026-01-01T00:00:00.000Z" },
{ nodeId: "done", runId: "run_1", timestamp: "2026-01-01T00:00:01.000Z" },
],
edgeCounts: { "review→done": 1 },
});
const canonicalReview = {
nodeId: "review",
nodeType: "review",
runId: "run_2",
status: "completed",
verdict: "PASS",
summary: "wrong canonical run",
timestamp: "2026-01-01T00:00:00.000Z",
artifacts: [],
};
mkdirSync(join(dir, "nodes", "review"), { recursive: true });
writeFileSync(join(dir, "nodes", "review", "handshake.json"), JSON.stringify(canonicalReview, null, 2));
writeTerminalHandshake(dir, "done", "run_1");
const stateBefore = readFileSync(join(dir, "flow-state.json"), "utf8");
const result = runHarness("finalize", ["--strict", "--dir", dir]);
assert.equal(result.finalized, false, JSON.stringify(result));
assert.match(result.error, /missing handshake.*run_1|expected 'run_1'/);
assert.equal(readFileSync(join(dir, "flow-state.json"), "utf8"), stateBefore);
});
test("structured evidence rejects canonical handshake runId mismatch", () => {
const dir = join(TMPBASE, "canonical-run-mismatch");
const nodeDir = join(dir, "nodes", "test-execute");
mkdirSync(join(nodeDir, "run_1"), { recursive: true });
writeFileSync(join(nodeDir, "run_1", "result.json"), "{}");
writeFileSync(join(nodeDir, "run_1", "handshake.json"), JSON.stringify({
nodeId: "test-execute",
nodeType: "execute",
runId: "run_2",
status: "completed",
verdict: "PASS",
summary: "newer run",
timestamp: "2026-01-01T00:00:00.000Z",
artifacts: [{ type: "test-result", path: "result.json" }],
}));
const state = {
history: [
{ nodeId: "test-execute", runId: "run_1", timestamp: "2026-01-01T00:00:00.000Z" },
{ nodeId: "gate", runId: "run_1", timestamp: "2026-01-01T00:00:01.000Z" },
],
};
const template = {
nodeTypes: { "test-execute": "execute", gate: "gate" },
requiredTestCommandEvidence: false,
};
const reasons = checkStructuredResults(dir, state, template, "gate");
assert.ok(reasons.some((reason) => /expected 'run_1'/.test(reason)), reasons.join("\n"));
});
test("test-design plan gate uses the state-selected run, not filesystem latest", () => {
const dir = join(TMPBASE, "test-plan-selected-run");
writeState(dir, {
currentNode: "test-design",
entryNode: "test-design",
history: [{ nodeId: "test-design", runId: "run_1", timestamp: "2026-01-01T00:00:00.000Z" }],
});
const selectedRun = join(dir, "nodes", "test-design", "run_1");
const rogueRun = join(dir, "nodes", "test-design", "run_2");
mkdirSync(selectedRun, { recursive: true });
mkdirSync(rogueRun, { recursive: true });
writeFileSync(join(selectedRun, "eval-a.md"), "# Eval A\n");
writeFileSync(join(selectedRun, "eval-b.md"), "# Eval B\n");
writeFileSync(join(rogueRun, "test-plan.md"), completeTestPlan());
writeFileSync(join(dir, "nodes", "test-design", "handshake.json"), JSON.stringify({
nodeId: "test-design",
nodeType: "review",
runId: "run_1",
status: "completed",
verdict: "PASS",
summary: "selected run has evals but no test plan",
timestamp: "2026-01-01T00:00:00.000Z",
artifacts: [
{ type: "eval", path: "run_1/eval-a.md" },
{ type: "eval", path: "run_1/eval-b.md" },
],
}, null, 2));
writeFileSync(join(selectedRun, "handshake.json"), JSON.stringify({
nodeId: "test-design",
nodeType: "review",
runId: "run_1",
status: "completed",
verdict: "PASS",
summary: "selected run has evals but no test plan",
timestamp: "2026-01-01T00:00:00.000Z",
artifacts: [
{ type: "eval", path: "eval-a.md" },
{ type: "eval", path: "eval-b.md" },
],
}, null, 2));
const result = runHarness("transition", [
"--from", "test-design", "--to", "test-execute", "--verdict", "PASS", "--dir", dir,
]);
assert.equal(result.allowed, false, JSON.stringify(result));
assert.match(result.reason, /test-design test-plan\.md missing/);
assert.equal(existsSync(join(dir, "nodes", "test-execute", "run_1")), false);
});
test("unbound node-level testCommand is rejected before target run or shell marker", () => {
const dir = join(TMPBASE, "unbound-test-command");
const marker = join(dir, "shell-marker");
writeState(dir, {
currentNode: "test-design",
entryNode: "test-design",
history: [{ nodeId: "test-design", runId: "run_1", timestamp: "2026-01-01T00:00:00.000Z" }],
});
const selectedRun = join(dir, "nodes", "test-design", "run_1");
mkdirSync(selectedRun, { recursive: true });
writeFileSync(join(selectedRun, "eval-a.md"), "# Eval A\n");
writeFileSync(join(selectedRun, "eval-b.md"), "# Eval B\n");
writeFileSync(join(selectedRun, "test-plan.md"), completeTestPlan());
writeFileSync(join(dir, "nodes", "test-design", "test-execution.json"), JSON.stringify({
testCommand: `touch ${marker}`,
}));
writeFileSync(join(dir, "nodes", "test-design", "handshake.json"), JSON.stringify({
nodeId: "test-design",
nodeType: "review",
runId: "run_1",
status: "completed",
verdict: "PASS",
summary: "selected run",
timestamp: "2026-01-01T00:00:00.000Z",
artifacts: [
{ type: "eval", path: "run_1/eval-a.md" },
{ type: "eval", path: "run_1/eval-b.md" },
{ type: "test-plan", path: "run_1/test-plan.md" },
],
}, null, 2));
writeFileSync(join(selectedRun, "handshake.json"), JSON.stringify({
nodeId: "test-design",
nodeType: "review",
runId: "run_1",
status: "completed",
verdict: "PASS",
summary: "selected run",
timestamp: "2026-01-01T00:00:00.000Z",
artifacts: [
{ type: "eval", path: "eval-a.md" },
{ type: "eval", path: "eval-b.md" },
{ type: "test-plan", path: "test-plan.md" },
],
}, null, 2));
const beforeState = readFileSync(join(dir, "flow-state.json"), "utf8");
const result = runHarness("transition", [
"--from", "test-design", "--to", "test-execute", "--verdict", "PASS", "--dir", dir,
]);
assert.equal(result.allowed, false, JSON.stringify(result));
assert.match(result.reason, /testCommand source binding failed/);
assert.equal(readFileSync(join(dir, "flow-state.json"), "utf8"), beforeState);
assert.equal(existsSync(join(dir, "nodes", "test-execute", "run_1")), false);
assert.equal(existsSync(marker), false);
});
test("pass rejects malformed upstream history runId instead of synthesizing latest run", () => {
const dir = join(TMPBASE, "pass-malformed-history-run");
const flowFile = writeFlowFile(dir, "pass-malformed-history-flow", {
nodes: ["review", "gate", "done"],
edges: { review: { PASS: "gate" }, gate: { PASS: "done" }, done: { PASS: null } },
limits: { maxLoopsPerEdge: 3, maxTotalSteps: 10, maxNodeReentry: 5 },
nodeTypes: { review: "review", gate: "gate", done: "build" },
});
writeState(dir, {
flowTemplate: "pass-malformed-history-flow",
_flow_file: flowFile,
currentNode: "gate",
entryNode: "review",
history: [
{ nodeId: "review", runId: "bad", timestamp: "2026-01-01T00:00:00.000Z" },
{ nodeId: "gate", runId: "run_1", timestamp: "2026-01-01T00:00:01.000Z" },
],
edgeCounts: { "review→gate": 1 },
});
writeReviewRun(dir, "review", "run_2");
const stateBefore = readFileSync(join(dir, "flow-state.json"), "utf8");
const result = runHarness("pass", ["--dir", dir]);
assert.equal(result.allowed, false, JSON.stringify(result));
assert.match(result.error, /missing or invalid runId/);
assert.equal(readFileSync(join(dir, "flow-state.json"), "utf8"), stateBefore);
assert.equal(existsSync(join(dir, "nodes", "done")), false);
});
test("advance rejects missing upstream history runId instead of synthesizing latest run", () => {
const dir = join(TMPBASE, "advance-missing-history-run");
const flowFile = writeFlowFile(dir, "advance-missing-history-flow", {
nodes: ["review", "gate", "done"],
edges: { review: { PASS: "gate" }, gate: { PASS: "done" }, done: { PASS: null } },
limits: { maxLoopsPerEdge: 3, maxTotalSteps: 10, maxNodeReentry: 5 },
nodeTypes: { review: "review", gate: "gate", done: "build" },
});
writeState(dir, {
flowTemplate: "advance-missing-history-flow",
_flow_file: flowFile,
currentNode: "gate",
entryNode: "review",
history: [
{ nodeId: "review", timestamp: "2026-01-01T00:00:00.000Z" },
{ nodeId: "gate", runId: "run_1", timestamp: "2026-01-01T00:00:01.000Z" },
],
edgeCounts: { "review→gate": 1 },
});
writeReviewRun(dir, "review", "run_2");
const stateBefore = readFileSync(join(dir, "flow-state.json"), "utf8");
const result = runHarness("advance", ["--dir", dir]);
assert.equal(result.advanced, false, JSON.stringify(result));
assert.equal(result.step, "synthesize");
assert.match(result.error, /missing or invalid/);
assert.equal(readFileSync(join(dir, "flow-state.json"), "utf8"), stateBefore);
assert.equal(existsSync(join(dir, "nodes", "done")), false);
});
test("validate fails closed when session state is malformed", () => {
const dir = join(TMPBASE, "validate-corrupt-session-authority");
mkdirSync(join(dir, "nodes", "build", "run_2"), { recursive: true });
writeFileSync(join(dir, "flow-state.json"), "{not-json");
const hsPath = join(dir, "nodes", "build", "run_2", "handshake.json");
writeFileSync(hsPath, JSON.stringify({
nodeId: "build",
nodeType: "build",
runId: "run_2",
status: "completed",
verdict: null,
summary: "standalone-looking rogue evidence",
timestamp: "2026-01-01T00:00:00.000Z",
artifacts: [],
}, null, 2));
const result = runHarness("validate", [hsPath]);
assert.equal(result.valid, false, JSON.stringify(result));
assert.match(result.errors.join("\n"), /cannot parse flow-state\.json/);
});
test("positional validate checks the whole session authority", () => {
const dir = join(TMPBASE, "validate-whole-session-authority");
writeState(dir, {
currentNode: "build",
history: [
{ nodeId: "build", runId: "run_1", timestamp: "2026-01-01T00:00:00.000Z" },
{ nodeId: "review", runId: "bad", timestamp: "2026-01-01T00:00:01.000Z" },
],
});
writeBuildHandshake(dir, "run_1");
const result = runHarness("validate", [join(dir, "nodes", "build", "run_1", "handshake.json")]);
assert.equal(result.valid, false, JSON.stringify(result));
assert.match(result.errors.join("\n"), /history\[1\]\.runId missing or invalid/);
});
test("default validate cannot use canonical when exact selected run is missing", () => {
const dir = join(TMPBASE, "validate-missing-exact-run");
writeState(dir);
mkdirSync(join(dir, "nodes", "build"), { recursive: true });
writeFileSync(join(dir, "nodes", "build", "output.md"), "canonical output");
writeFileSync(join(dir, "nodes", "build", "handshake.json"), JSON.stringify({
nodeId: "build",
nodeType: "build",
runId: "run_1",
status: "completed",
verdict: null,
summary: "canonical impersonation",
timestamp: "2026-01-01T00:00:00.000Z",
artifacts: [{ type: "source", path: "output.md" }],
}, null, 2));
const result = runHarness("validate", ["--dir", dir]);
assert.equal(result.valid, false, JSON.stringify(result));
assert.match(result.errors.join("\n"), /missing exact selected-run handshake/);
});
test("validate-chain rejects repeated-node canonical projection conflict", () => {
const dir = join(TMPBASE, "validate-chain-canonical-projection");
writeState(dir, {
currentNode: "gate",
entryNode: "review",
history: [
{ nodeId: "review", runId: "run_1", timestamp: "2026-01-01T00:00:00.000Z" },
{ nodeId: "gate", runId: "run_1", timestamp: "2026-01-01T00:00:01.000Z" },
{ nodeId: "review", runId: "run_2", timestamp: "2026-01-01T00:00:02.000Z" },
{ nodeId: "gate", runId: "run_2", timestamp: "2026-01-01T00:00:03.000Z" },
],
edgeCounts: { "review→gate": 2, "gate→review": 1 },
});
writeReviewRun(dir, "review", "run_1");
writeReviewRun(dir, "review", "run_2");
for (const runId of ["run_1", "run_2"]) {
mkdirSync(join(dir, "nodes", "gate", runId), { recursive: true });
writeFileSync(join(dir, "nodes", "gate", runId, "handshake.json"), JSON.stringify({
nodeId: "gate",
nodeType: "gate",
runId,
status: "completed",
verdict: "PASS",
summary: "gate",
timestamp: "2026-01-01T00:00:03.000Z",
artifacts: [],
}, null, 2));
}
writeFileSync(join(dir, "nodes", "review", "handshake.json"), JSON.stringify({
nodeId: "review",
nodeType: "review",
runId: "run_3",
status: "completed",
verdict: "PASS",
summary: "rogue canonical",
timestamp: "2026-01-01T00:00:04.000Z",
artifacts: [],
}, null, 2));
const result = runHarness("validate-chain", ["--dir", dir]);
assert.equal(result.valid, false, JSON.stringify(result));
assert.match(result.errors.join("\n"), /canonical .*runId is 'run_3', expected 'run_2'/);
});
test("validate-chain rejects partial canonical projection schema", () => {
const dir = join(TMPBASE, "validate-chain-partial-canonical");
writeState(dir, {
currentNode: "gate",
entryNode: "review",
history: [
{ nodeId: "review", runId: "run_1", timestamp: "2026-01-01T00:00:00.000Z" },
{ nodeId: "gate", runId: "run_1", timestamp: "2026-01-01T00:00:01.000Z" },
],
edgeCounts: { "review→gate": 1 },
});
writeReviewRun(dir, "review", "run_1");
mkdirSync(join(dir, "nodes", "gate", "run_1"), { recursive: true });
writeFileSync(join(dir, "nodes", "gate", "run_1", "handshake.json"), JSON.stringify({
nodeId: "gate",
nodeType: "gate",
runId: "run_1",
status: "completed",
verdict: "PASS",
summary: "gate",
timestamp: "2026-01-01T00:00:01.000Z",
artifacts: [],
}, null, 2));
writeFileSync(join(dir, "nodes", "review", "handshake.json"), JSON.stringify({
nodeId: "review",
runId: "run_1",
status: "completed",
artifacts: [],
}, null, 2));
const result = runHarness("validate-chain", ["--dir", dir]);
assert.equal(result.valid, false, JSON.stringify(result));
assert.match(result.errors.join("\n"), /canonical .*missing or empty required field: nodeType/);
});
test("strict finalize rejects canonical conflict before writing completed state", () => {
const dir = join(TMPBASE, "strict-finalize-canonical-projection");
const flowFile = writeFlowFile(dir, "strict-finalize-canonical-flow", {
nodes: ["review", "done"],
edges: { review: { PASS: "done" }, done: { PASS: null } },
limits: { maxLoopsPerEdge: 3, maxTotalSteps: 10, maxNodeReentry: 5 },
nodeTypes: { review: "review", done: "build" },
});
writeState(dir, {
flowTemplate: "strict-finalize-canonical-flow",
_flow_file: flowFile,
currentNode: "done",
entryNode: "review",
history: [
{ nodeId: "review", runId: "run_1", timestamp: "2026-01-01T00:00:00.000Z" },
{ nodeId: "done", runId: "run_1", timestamp: "2026-01-01T00:00:01.000Z" },
],
edgeCounts: { "review→done": 1 },
});
writeReviewRun(dir, "review", "run_1");
writeFileSync(join(dir, "nodes", "review", "handshake.json"), JSON.stringify({
nodeId: "review",
nodeType: "review",
runId: "run_2",
status: "completed",
verdict: "PASS",
summary: "rogue canonical",
timestamp: "2026-01-01T00:00:02.000Z",
artifacts: [],
}, null, 2));
writeTerminalHandshake(dir, "done", "run_1");
const before = readFileSync(join(dir, "flow-state.json"), "utf8");
const result = runHarness("finalize", ["--strict", "--dir", dir]);
assert.equal(result.finalized, false, JSON.stringify(result));
assert.match(result.error, /expected 'run_1'/);
assert.equal(readFileSync(join(dir, "flow-state.json"), "utf8"), before);
});
test("strict finalize rejects terminal partial test provenance before completion", () => {
const dir = join(TMPBASE, "strict-finalize-terminal-provenance");
const flowFile = writeFlowFile(dir, "strict-terminal-provenance-flow", {
nodes: ["test-execute"],
edges: { "test-execute": { PASS: null } },
limits: { maxLoopsPerEdge: 3, maxTotalSteps: 10, maxNodeReentry: 5 },
nodeTypes: { "test-execute": "execute" },
});
writeState(dir, {
flowTemplate: "strict-terminal-provenance-flow",
_flow_file: flowFile,
currentNode: "test-execute",
entryNode: "test-execute",
history: [{ nodeId: "test-execute", runId: "run_1", timestamp: "2026-01-01T00:00:00.000Z" }],
});
const runDir = join(dir, "nodes", "test-execute", "run_1");
mkdirSync(runDir, { recursive: true });
writeFileSync(join(runDir, "test-command-result.json"), JSON.stringify({
checks: [{ id: "smoke", pass: true, total: 1 }],
provenance: { kind: "opc-test-command", executionActor: "opc-harness:test-command" },
}, null, 2));
const handshake = {
nodeId: "test-execute",
nodeType: "execute",
runId: "run_1",
status: "completed",
verdict: "PASS",
summary: "partial provenance",
timestamp: "2026-01-01T00:00:00.000Z",
artifacts: [{ type: "test-result", path: "test-command-result.json" }],
testEvidenceProvenance: {
kind: "opc-test-command",
sourceNode: "test-design",
commandHash: "abc",
sourcePlanHash: "def",
resultHash: "ghi",
executionActor: "opc-harness:test-command",
},
};
writeFileSync(join(runDir, "handshake.json"), JSON.stringify(handshake, null, 2));
writeFileSync(join(dir, "nodes", "test-execute", "handshake.json"), JSON.stringify({
...handshake,
artifacts: [{ type: "test-result", path: "run_1/test-command-result.json" }],
}, null, 2));
const before = readFileSync(join(dir, "flow-state.json"), "utf8");
const result = runHarness("finalize", ["--strict", "--flow-file", flowFile, "--dir", dir]);
assert.equal(result.finalized, false, JSON.stringify(result));
assert.match(result.error, /testEvidenceProvenance\.sourceRunId/);
assert.equal(readFileSync(join(dir, "flow-state.json"), "utf8"), before);
});
test("hotfix retest rejects missing historical test-design run before shell spawn", () => {
const dir = join(TMPBASE, "hotfix-missing-test-design-run");
const marker = join(dir, "hotfix-marker");
const flowFile = writeFlowFile(dir, "hotfix-retest-flow", {
nodes: ["test-design", "test-execute", "hotfix"],
edges: {
"test-design": { PASS: "test-execute" },
"test-execute": { ITERATE: "hotfix", PASS: null },
hotfix: { PASS: "test-execute" },
},
limits: { maxLoopsPerEdge: 3, maxTotalSteps: 10, maxNodeReentry: 5 },
nodeTypes: { "test-design": "review", "test-execute": "execute", hotfix: "build" },
});
writeState(dir, {
flowTemplate: "hotfix-retest-flow",
_flow_file: flowFile,
currentNode: "hotfix",
entryNode: "test-design",
history: [
{ nodeId: "test-design", timestamp: "2026-01-01T00:00:00.000Z" },
{ nodeId: "test-execute", runId: "run_1", timestamp: "2026-01-01T00:00:01.000Z" },
{ nodeId: "hotfix", runId: "run_1", timestamp: "2026-01-01T00:00:02.000Z" },
],
edgeCounts: { "test-design→test-execute": 1, "test-execute→hotfix": 1 },
});
mkdirSync(join(dir, "nodes", "hotfix", "run_1"), { recursive: true });
writeFileSync(join(dir, "nodes", "hotfix", "run_1", "fix.md"), "fix");
const hotfixHandshake = {
nodeId: "hotfix",
nodeType: "build",
runId: "run_1",
status: "completed",
verdict: "PASS",
summary: "fix",
timestamp: "2026-01-01T00:00:02.000Z",
artifacts: [{ type: "source", path: "run_1/fix.md" }],
};
writeFileSync(join(dir, "nodes", "hotfix", "run_1", "handshake.json"), JSON.stringify({
...hotfixHandshake,
artifacts: [{ type: "source", path: "fix.md" }],
}, null, 2));
writeFileSync(join(dir, "nodes", "hotfix", "handshake.json"), JSON.stringify(hotfixHandshake, null, 2));
mkdirSync(join(dir, "nodes", "test-design", "run_2"), { recursive: true });
writeFileSync(join(dir, "nodes", "test-design", "run_2", "test-execution.json"), JSON.stringify({
runId: "run_2",
testCommand: `touch ${marker}`,
}));
const before = readFileSync(join(dir, "flow-state.json"), "utf8");
const result = runHarness("transition", [
"--from", "hotfix", "--to", "test-execute", "--verdict", "PASS", "--flow-file", flowFile, "--dir", dir,
]);
assert.equal(result.allowed, false, JSON.stringify(result));
assert.match(result.reason, /testCommand source binding failed/);
assert.equal(existsSync(marker), false);
assert.equal(existsSync(join(dir, "nodes", "test-execute", "run_2")), false);
assert.equal(readFileSync(join(dir, "flow-state.json"), "utf8"), before);
});
test("hotfix retest rejects malformed newest test-design history instead of older run", () => {
const dir = join(TMPBASE, "hotfix-malformed-newest-test-design");
const marker = join(dir, "hotfix-newest-marker");
const flowFile = writeFlowFile(dir, "hotfix-newest-flow", {
nodes: ["test-design", "test-execute", "hotfix"],
edges: {
"test-design": { PASS: "test-execute" },
"test-execute": { ITERATE: "hotfix", PASS: null },
hotfix: { PASS: "test-execute" },
},
limits: { maxLoopsPerEdge: 3, maxTotalSteps: 10, maxNodeReentry: 5 },
nodeTypes: { "test-design": "review", "test-execute": "execute", hotfix: "build" },
});
writeState(dir, {
flowTemplate: "hotfix-newest-flow",
_flow_file: flowFile,
currentNode: "hotfix",
entryNode: "test-design",
history: [
{ nodeId: "test-design", runId: "run_1", timestamp: "2026-01-01T00:00:00.000Z" },
{ nodeId: "test-execute", runId: "run_1", timestamp: "2026-01-01T00:00:01.000Z" },
{ nodeId: "test-design", runId: "bad", timestamp: "2026-01-01T00:00:02.000Z" },
{ nodeId: "hotfix", runId: "run_1", timestamp: "2026-01-01T00:00:03.000Z" },
],
edgeCounts: { "test-design→test-execute": 1, "test-execute→test-design": 1, "test-design→hotfix": 1 },
});
const tdRun = join(dir, "nodes", "test-design", "run_1");
mkdirSync(tdRun, { recursive: true });
writeFileSync(join(tdRun, "test-plan.md"), completeTestPlan());
writeFileSync(join(tdRun, "test-execution.json"), JSON.stringify({
runId: "run_1",
testCommand: `touch ${marker}`,
}));
writeReviewRun(dir, "test-design", "run_1", {
artifacts: [
{ type: "eval", path: "eval-a.md" },
{ type: "eval", path: "eval-b.md" },
{ type: "test-plan", path: "test-plan.md" },
],
});
mkdirSync(join(dir, "nodes", "test-execute", "run_1"), { recursive: true });
writeFileSync(join(dir, "nodes", "test-execute", "run_1", "handshake.json"), JSON.stringify({
nodeId: "test-execute",
nodeType: "execute",
runId: "run_1",
status: "completed",
verdict: "ITERATE",
summary: "needs hotfix",
timestamp: "2026-01-01T00:00:01.000Z",
artifacts: [{ type: "cli-output", path: "log.txt" }],
}, null, 2));
mkdirSync(join(dir, "nodes", "hotfix", "run_1"), { recursive: true });
writeFileSync(join(dir, "nodes", "hotfix", "run_1", "handshake.json"), JSON.stringify({
nodeId: "hotfix",
nodeType: "build",
runId: "run_1",
status: "completed",
verdict: "PASS",
summary: "fix",
timestamp: "2026-01-01T00:00:03.000Z",
artifacts: [],
}, null, 2));
const before = readFileSync(join(dir, "flow-state.json"), "utf8");
const result = runHarness("transition", [
"--from", "hotfix", "--to", "test-execute", "--verdict", "PASS", "--flow-file", flowFile, "--dir", dir,
]);
assert.equal(result.allowed, false, JSON.stringify(result));
assert.match(result.reason, /history runId is missing or invalid/);
assert.equal(existsSync(marker), false);
assert.equal(existsSync(join(dir, "nodes", "test-execute", "run_2")), false);
assert.equal(readFileSync(join(dir, "flow-state.json"), "utf8"), before);
});
test("gate backlog rejects unbound canonical warnings before mutation", () => {
const dir = join(TMPBASE, "backlog-exact-upstream");
const flowFile = writeFlowFile(dir, "backlog-exact-flow", {
nodes: ["review", "gate"],
edges: { review: { PASS: "gate" }, gate: { PASS: null, ITERATE: "review" } },
limits: { maxLoopsPerEdge: 3, maxTotalSteps: 10, maxNodeReentry: 5 },
nodeTypes: { review: "review", gate: "gate" },
});
writeState(dir, {
flowTemplate: "backlog-exact-flow",
_flow_file: flowFile,
currentNode: "gate",
entryNode: "review",
history: [
{ nodeId: "review", runId: "run_1", timestamp: "2026-01-01T00:00:00.000Z" },
{ nodeId: "gate", runId: "run_1", timestamp: "2026-01-01T00:00:01.000Z" },
],
edgeCounts: { "review→gate": 1 },
});
writeReviewRun(dir, "review", "run_1", { findings: { critical: 0, warning: 0, suggestion: 0 } });
writeFileSync(join(dir, "nodes", "review", "handshake.json"), JSON.stringify({
nodeId: "review",
nodeType: "review",
runId: "run_1",
status: "completed",
verdict: "PASS",
summary: "rogue warning mirror",
timestamp: "2026-01-01T00:00:02.000Z",
artifacts: [
{ type: "eval", path: "run_1/eval-a.md" },
{ type: "eval", path: "run_1/eval-b.md" },
],
findings: { critical: 0, warning: 1, suggestion: 0 },
}, null, 2));
mkdirSync(join(dir, "nodes", "gate"), { recursive: true });
writeFileSync(join(dir, "nodes", "gate", "handshake.json"), JSON.stringify({
nodeId: "gate",
nodeType: "gate",
runId: "run_1",
status: "completed",
verdict: "ITERATE",
summary: "gate",
timestamp: "2026-01-01T00:00:01.000Z",
artifacts: [],
}, null, 2));
const result = runHarness("transition", [
"--from", "gate", "--to", "review", "--verdict", "ITERATE", "--flow-file", flowFile, "--dir", dir,
]);
assert.equal(result.allowed, false, JSON.stringify(result));
assert.match(result.reason, /not exact-run projection/);
});
test("cumulative findings keeps orphan run findings out of reviewer context", () => {
const dir = join(TMPBASE, "cumulative-orphan-runs");
writeState(dir, {
currentNode: "code-review",
entryNode: "build",
history: [
{ nodeId: "build", runId: "run_1", timestamp: "2026-01-01T00:00:00.000Z" },
{ nodeId: "code-review", runId: "run_1", timestamp: "2026-01-01T00:00:01.000Z" },
],
});
writeBuildHandshake(dir, "run_1");
writeReviewRun(dir, "code-review", "run_1");
const orphan = join(dir, "nodes", "code-review", "run_2");
mkdirSync(orphan, { recursive: true });
writeFileSync(join(orphan, "eval-orphan.md"), [
"# Orphan",
"",
"🔴 Orphan-only critical — rogue.js:1",
"→ Do not inject this into reviewer context.",
"Reasoning: This run is not state-selected.",
].join("\n"));
const markdown = buildCumulativeFindingsMarkdown(dir, JSON.parse(readFileSync(join(dir, "flow-state.json"), "utf8")));
assert.match(markdown, /Forensic Orphan Runs/);
assert.match(markdown, /code-review\/run_2/);
assert.doesNotMatch(markdown, /Orphan-only critical/);
});
test("cumulative findings keeps orphan fixesApplied out of reviewer context", () => {
const dir = join(TMPBASE, "cumulative-orphan-fixes");
writeState(dir, {
currentNode: "code-review",
entryNode: "build",
history: [
{ nodeId: "build", runId: "run_1", timestamp: "2026-01-01T00:00:00.000Z" },
{ nodeId: "code-review", runId: "run_1", timestamp: "2026-01-01T00:00:01.000Z" },
],
});
writeBuildHandshake(dir, "run_1");
writeReviewRun(dir, "code-review", "run_1");
mkdirSync(join(dir, "nodes", "code-review", "run_2"), { recursive: true });
writeFileSync(join(dir, "nodes", "code-review", "run_2", "handshake.json"), JSON.stringify({
nodeId: "code-review",
nodeType: "review",
runId: "run_2",
status: "completed",
verdict: "PASS",
summary: "orphan retry",
timestamp: "2026-01-01T00:00:02.000Z",
artifacts: [],
fixesApplied: ["Orphan-only fix must not enter reviewer prompt"],
}, null, 2));
const markdown = buildCumulativeFindingsMarkdown(dir, JSON.parse(readFileSync(join(dir, "flow-state.json"), "utf8")));
assert.match(markdown, /Forensic Orphan Runs/);
assert.match(markdown, /code-review\/run_2/);
assert.doesNotMatch(markdown, /Orphan-only fix/);
});
test("seal rejects partial testEvidenceProvenance without rewriting canonical bytes", () => {
const dir = join(TMPBASE, "seal-partial-test-provenance");
writeState(dir, {
flowTemplate: "build-verify",
currentNode: "test-execute",
entryNode: "test-execute",
history: [{ nodeId: "test-execute", runId: "run_1", timestamp: "2026-01-01T00:00:00.000Z" }],
});
const nodeDir = join(dir, "nodes", "test-execute");
const runDir = join(nodeDir, "run_1");
mkdirSync(runDir, { recursive: true });
writeFileSync(join(runDir, "test-command-result.json"), JSON.stringify({
checks: [{ id: "smoke", pass: true, total: 1 }],
provenance: { kind: "opc-test-command", executionActor: "opc-harness:test-command" },
}, null, 2));
const handshakePath = join(nodeDir, "handshake.json");
const canonical = {
nodeId: "test-execute",
nodeType: "execute",
runId: "run_1",
status: "completed",
verdict: "PASS",
summary: "partial provenance",
timestamp: "2026-01-01T00:00:00.000Z",
artifacts: [{ type: "test-result", path: "run_1/test-command-result.json" }],
testEvidenceProvenance: {
kind: "opc-test-command",
executionActor: "opc-harness:test-command",
},
};
writeFileSync(handshakePath, JSON.stringify(canonical, null, 2));
const before = readFileSync(handshakePath, "utf8");
const result = runHarness("seal", ["--node", "test-execute", "--dir", dir]);
assert.equal(result.sealed, false, JSON.stringify(result));
assert.match(result.validationErrors.join("\n"), /testEvidenceProvenance\.commandHash missing/);
assert.equal(readFileSync(handshakePath, "utf8"), before);
});
test("seal requires sourceRunId and non-null policy in test evidence provenance", () => {
const dir = join(TMPBASE, "seal-source-run-required");
writeState(dir, {
flowTemplate: "build-verify",
currentNode: "test-execute",
entryNode: "test-execute",
history: [{ nodeId: "test-execute", runId: "run_1", timestamp: "2026-01-01T00:00:00.000Z" }],
});
const nodeDir = join(dir, "nodes", "test-execute");
const runDir = join(nodeDir, "run_1");
mkdirSync(runDir, { recursive: true });
writeFileSync(join(runDir, "test-command-result.json"), JSON.stringify({
checks: [{ id: "smoke", pass: true, total: 1 }],
provenance: {
kind: "opc-test-command",
sourceNode: "test-design",
commandHash: "cmd",
sourcePlanHash: "plan",
executionActor: "opc-harness:test-command",
},
}, null, 2));
const handshakePath = join(nodeDir, "handshake.json");
writeFileSync(handshakePath, JSON.stringify({
nodeId: "test-execute",
nodeType: "execute",
runId: "run_1",
status: "completed",
verdict: "PASS",
summary: "missing source run",
timestamp: "2026-01-01T00:00:00.000Z",
artifacts: [{ type: "test-result", path: "run_1/test-command-result.json" }],
testEvidenceProvenance: {
kind: "opc-test-command",
sourceNode: "test-design",
commandHash: "cmd",
sourcePlanHash: "plan",
resultHash: "result",
executionActor: "opc-harness:test-command",
ledger: { kind: "opc-hmac-ledger", recordHash: "missing" },
},
testEvidencePolicy: null,
}, null, 2));
const before = readFileSync(handshakePath, "utf8");
const result = runHarness("seal", ["--node", "test-execute", "--dir", dir]);
assert.equal(result.sealed, false, JSON.stringify(result));
assert.match(result.validationErrors.join("\n"), /sourceRunId/);
assert.match(result.validationErrors.join("\n"), /testEvidencePolicy/);
assert.equal(readFileSync(handshakePath, "utf8"), before);
});
test("direct gate FAIL rejects invalid upstream authority before mutation", () => {
const dir = join(TMPBASE, "direct-gate-fail-authority");
const flowFile = writeFlowFile(dir, "direct-gate-fail-flow", {
nodes: ["review", "gate"],
edges: { review: { PASS: "gate" }, gate: { FAIL: "review", PASS: null } },
limits: { maxLoopsPerEdge: 3, maxTotalSteps: 10, maxNodeReentry: 5 },
nodeTypes: { review: "review", gate: "gate" },
});
writeState(dir, {
flowTemplate: "direct-gate-fail-flow",
_flow_file: flowFile,
currentNode: "gate",
entryNode: "review",
history: [
{ nodeId: "review", timestamp: "2026-01-01T00:00:00.000Z" },
{ nodeId: "gate", runId: "run_1", timestamp: "2026-01-01T00:00:01.000Z" },
],
edgeCounts: { "review→gate": 1 },
repairEdgeCounts: {},
});
writeReviewRun(dir, "review", "run_2", { verdict: "FAIL", findings: { critical: 1, warning: 0, suggestion: 0 } });
const before = readFileSync(join(dir, "flow-state.json"), "utf8");
const result = runHarness("transition", [
"--from", "gate", "--to", "review", "--verdict", "FAIL", "--flow-file", flowFile, "--dir", dir,
]);
assert.equal(result.allowed, false, JSON.stringify(result));
assert.match(result.reason, /gate authority check failed/);
assert.equal(readFileSync(join(dir, "flow-state.json"), "utf8"), before);
assert.equal(existsSync(join(dir, "nodes", "review", "run_3")), false);
});
test("direct gate FAIL rejects partial canonical projection before mutation", () => {
const dir = join(TMPBASE, "direct-gate-partial-canonical");
const flowFile = writeFlowFile(dir, "direct-gate-partial-flow", {
nodes: ["review", "gate"],
edges: { review: { PASS: "gate" }, gate: { FAIL: "review", PASS: null } },
limits: { maxLoopsPerEdge: 3, maxTotalSteps: 10, maxNodeReentry: 5 },
nodeTypes: { review: "review", gate: "gate" },
});
writeState(dir, {
flowTemplate: "direct-gate-partial-flow",
_flow_file: flowFile,
currentNode: "gate",
entryNode: "review",
history: [
{ nodeId: "review", runId: "run_1", timestamp: "2026-01-01T00:00:00.000Z" },
{ nodeId: "gate", runId: "run_1", timestamp: "2026-01-01T00:00:01.000Z" },
],
edgeCounts: { "review→gate": 1 },
repairEdgeCounts: {},
});
writeReviewRun(dir, "review", "run_1", { verdict: "FAIL", findings: { critical: 1, warning: 0, suggestion: 0 } });
mkdirSync(join(dir, "nodes", "gate", "run_1"), { recursive: true });
writeFileSync(join(dir, "nodes", "gate", "run_1", "handshake.json"), JSON.stringify({
nodeId: "gate",
nodeType: "gate",
runId: "run_1",
status: "completed",
verdict: "FAIL",
summary: "gate",
timestamp: "2026-01-01T00:00:01.000Z",
artifacts: [],
}, null, 2));
writeFileSync(join(dir, "nodes", "review", "handshake.json"), JSON.stringify({
nodeId: "review",
runId: "run_1",
status: "completed",
artifacts: [],
}, null, 2));
const before = readFileSync(join(dir, "flow-state.json"), "utf8");
const result = runHarness("transition", [
"--from", "gate", "--to", "review", "--verdict", "FAIL", "--flow-file", flowFile, "--dir", dir,
]);
assert.equal(result.allowed, false, JSON.stringify(result));
assert.match(result.reason, /gate authority check failed/);
assert.equal(readFileSync(join(dir, "flow-state.json"), "utf8"), before);
assert.equal(existsSync(join(dir, "nodes", "review", "run_2")), false);
});
});
const REPAIR_VERDICTS = new Set(["FAIL", "ITERATE"]);
function resolveLimits(state, template) {
const limits = {};
for (const name of ["maxTotalSteps", "maxLoopsPerEdge", "maxNodeReentry"]) {
limits[name] = Object.hasOwn(state, name)
? state[name]
: template.limits?.[name];
}
for (const [name, value] of Object.entries(limits)) {
if (!Number.isInteger(value) || value <= 0) {
return { error: `${name} must be a positive integer` };
}
}
return limits;
}
function readCount(map, edgeKey, label) {
if (!map || typeof map !== "object" || Array.isArray(map)) {
return { error: `${label} is invalid` };
}
const count = map[edgeKey] ?? 0;
if (!Number.isInteger(count) || count < 0) {
return { error: `${label} count is invalid for edge '${edgeKey}'` };
}
return { count };
}
function validateCountMap(map, label) {
if (!map || typeof map !== "object" || Array.isArray(map)) {
return { error: `${label} is invalid` };
}
for (const edgeKey of Object.keys(map)) {
const result = readCount(map, edgeKey, label);
if (result.error) return result;
}
return {};
}
export function isRepairVerdict(verdict) {
return REPAIR_VERDICTS.has(String(verdict || "").toUpperCase());
}
export function repairEdgeCount(state, edgeKey) {
if (Object.hasOwn(state, "repairEdgeCounts")) {
return readCount(state.repairEdgeCounts, edgeKey, "repairEdgeCounts");
}
return readCount(state.edgeCounts ?? {}, edgeKey, "edgeCounts");
}
export function seedRepairEdgeCounts(state, template) {
if (Object.hasOwn(state, "repairEdgeCounts")) {
const existing = state.repairEdgeCounts;
if (!existing || typeof existing !== "object" || Array.isArray(existing)) {
throw new Error("repairEdgeCounts is invalid");
}
for (const edgeKey of Object.keys(existing)) {
const result = readCount(existing, edgeKey, "repairEdgeCounts");
if (result.error) throw new Error(result.error);
}
return existing;
}
const seeded = {};
for (const [from, edges] of Object.entries(template.edges || {})) {
for (const [verdict, to] of Object.entries(edges || {})) {
if (!isRepairVerdict(verdict) || to === null) continue;
const edgeKey = `${from}→${to}`;
const legacy = readCount(state.edgeCounts ?? {}, edgeKey, "edgeCounts");
if (legacy.error) throw new Error(legacy.error);
seeded[edgeKey] = legacy.count;
}
}
state.repairEdgeCounts = seeded;
return seeded;
}
export function evaluateFlowBudget({ state, template, from, to, verdict }) {
if (!state || typeof state !== "object" || Array.isArray(state)) {
return { allowed: false, reason: "state is invalid" };
}
const limits = resolveLimits(state, template);
if (limits.error) return { allowed: false, reason: limits.error };
if (!Number.isInteger(state.totalSteps) || state.totalSteps < 0) {
return { allowed: false, reason: "totalSteps must be a non-negative integer" };
}
if (!Array.isArray(state.history)) {
return { allowed: false, reason: "history is invalid" };
}
const traversalMap = validateCountMap(state.edgeCounts, "edgeCounts");
if (traversalMap.error) return { allowed: false, reason: traversalMap.error };
if (Object.hasOwn(state, "repairEdgeCounts")) {
const repairMap = validateCountMap(state.repairEdgeCounts, "repairEdgeCounts");
if (repairMap.error) return { allowed: false, reason: repairMap.error };
}
if (to === null) return { allowed: true, terminal: true };
if (state.totalSteps >= limits.maxTotalSteps) {
return { allowed: false, reason: `maxTotalSteps (${limits.maxTotalSteps}) reached` };
}
const edgeKey = `${from}→${to}`;
const edgeCount = state.edgeCounts[edgeKey] ?? 0;
let repairCount = 0;
if (isRepairVerdict(verdict)) {
const repairCounts = seedRepairEdgeCounts(state, template);
repairCount = repairCounts[edgeKey] ?? 0;
if (repairCount >= limits.maxLoopsPerEdge) {
return {
allowed: false,
reason: `maxLoopsPerEdge (${limits.maxLoopsPerEdge}) reached for repair edge '${edgeKey}'`,
edgeKey,
repairCount,
};
}
}
const nodeEntries = state.history.filter((entry) => entry.nodeId === to).length;
if (nodeEntries >= limits.maxNodeReentry) {
return {
allowed: false,
reason: `maxNodeReentry (${limits.maxNodeReentry}) reached for node '${to}'`,
};
}
return { allowed: true, edgeKey, edgeCount, repairCount };
}
export function nodeHasBudgetedExit({ state, template, node }) {
const edges = Object.entries(template.edges?.[node] || {});
const reasons = [];
for (const [verdict, to] of edges) {
if (verdict === "PASS" && to === null) {
return { available: true, terminal: true };
}
if (to === null) continue;
const result = evaluateFlowBudget({ state, template, from: node, to, verdict });
if (result.allowed) return { available: true, verdict, to };
reasons.push(result.reason);
}
return { available: false, reasons };
}
import { existsSync, readFileSync } from "fs";
import { isAbsolute, join } from "path";
import { resolveCurrentRun } from "./runaway-guard.mjs";
export function isRunId(value) {
return typeof value === "string" && /^run_\d+$/.test(value);
}
export function isPlainObject(value) {
return value !== null && typeof value === "object" && !Array.isArray(value);
}
export function parseHandshakeFile(path) {
let data;
try {
data = JSON.parse(readFileSync(path, "utf8"));
} catch (error) {
return { error: `${path}: corrupt/parse error: ${error.message}` };
}
if (!isPlainObject(data)) {
return { error: `${path}: root must be a non-null object` };
}
return { data };
}
export function handshakeIdentityErrors(data, nodeId, runId) {
const errors = [];
if (data.nodeId !== nodeId) {
errors.push(`nodeId is '${data.nodeId}', expected '${nodeId}'`);
}
if (data.runId !== runId) {
errors.push(`runId is '${data.runId}', expected '${runId}'`);
}
if (typeof data.status !== "string" || data.status.length === 0) {
errors.push("status missing or invalid");
}
return errors;
}
export function resolveExactRunHandshake(dir, nodeId, runId) {
if (!isRunId(runId)) {
return { path: null, data: null, error: `invalid runId '${runId}' for node '${nodeId}'` };
}
const nodeDir = join(dir, "nodes", nodeId);
const runPath = join(nodeDir, runId, "handshake.json");
if (!existsSync(runPath)) return { path: runPath, data: null, error: null, missing: true };
const parsed = parseHandshakeFile(runPath);
if (parsed.error) return { path: runPath, data: null, error: parsed.error };
const errors = handshakeIdentityErrors(parsed.data, nodeId, runId);
return errors.length > 0
? { path: runPath, data: parsed.data, error: `${runPath}: ${errors.join("; ")}` }
: { path: runPath, data: parsed.data, error: null };
}
export function sessionAuthorityErrors(state) {
const errors = [];
if (!isPlainObject(state)) return ["flow-state.json must contain an object"];
if (typeof state.currentNode !== "string" || state.currentNode.length === 0) {
errors.push("flow-state.json currentNode missing or invalid");
}
if (!Array.isArray(state.history)) {
errors.push("flow-state.json history must be an array");
return errors;
}
state.history.forEach((entry, i) => {
const nodeId = entry?.nodeId || entry?.node;
const runId = entry?.runId || entry?.run;
if (typeof nodeId !== "string" || nodeId.length === 0) {
errors.push(`flow-state.json history[${i}].nodeId missing or invalid`);
}
if (!isRunId(runId)) {
errors.push(`flow-state.json history[${i}].runId missing or invalid`);
}
});
const current = resolveCurrentRun(state);
if (!current) errors.push(`cannot resolve current run for '${state.currentNode || "unknown"}'`);
return errors;
}
export function readSessionAuthority(dir) {
const statePath = join(dir, "flow-state.json");
if (!existsSync(statePath)) return { exists: false, state: null, error: null };
let state;
try {
state = JSON.parse(readFileSync(statePath, "utf8"));
} catch (error) {
return { exists: true, state: null, error: `cannot parse flow-state.json: ${error.message}` };
}
const errors = sessionAuthorityErrors(state);
if (errors.length > 0) return { exists: true, state: null, error: errors.join("; ") };
return { exists: true, state, error: null };
}
export function authoritativeEntries(state, { includeCurrent = true } = {}) {
const entries = [];
const seen = new Set();
const add = (entry) => {
const nodeId = entry?.nodeId || entry?.node;
const runId = entry?.runId || entry?.run;
if (typeof nodeId !== "string" || nodeId.length === 0 || !isRunId(runId)) return false;
const key = `${nodeId}\0${runId}`;
if (seen.has(key)) return true;
entries.push({ nodeId, runId });
seen.add(key);
return true;
};
if (state?.entryNode) add({ nodeId: state.entryNode, runId: "run_1" });
for (const entry of state?.history || []) add(entry);
if (includeCurrent) {
const current = resolveCurrentRun(state);
if (current) add({ nodeId: state.currentNode, runId: current.runId });
}
return entries;
}
export function latestAuthoritativeRunByNode(state) {
const map = new Map();
for (const entry of authoritativeEntries(state)) map.set(entry.nodeId, entry.runId);
return map;
}
export function expectedRunForNode(state, nodeId) {
return latestAuthoritativeRunByNode(state).get(nodeId) || null;
}
export function canonicalHandshakePath(dir, nodeId) {
return join(dir, "nodes", nodeId, "handshake.json");
}
const NODE_LEVEL_ARTIFACTS = new Set([
"build-brief.md",
"test-plan.md",
"test-execution.json",
]);
function stableJson(value) {
if (Array.isArray(value)) return `[${value.map(stableJson).join(",")}]`;
if (!isPlainObject(value)) return JSON.stringify(value);
const keys = Object.keys(value).sort();
return `{${keys.map((key) => `${JSON.stringify(key)}:${stableJson(value[key])}`).join(",")}}`;
}
function canonicalArtifactPath(path, runId, context = {}) {
if (typeof path !== "string") return path;
let normalized = path;
while (normalized.startsWith("../")) normalized = normalized.slice(3);
if (/^run_\d+\//.test(normalized)) return normalized;
if (NODE_LEVEL_ARTIFACTS.has(normalized)) {
const nodePath = context.nodeDir ? join(context.nodeDir, normalized) : null;
const runPath = context.runDir ? join(context.runDir, normalized) : null;
if (nodePath && existsSync(nodePath) && (!runPath || !existsSync(runPath))) return normalized;
}
if (normalized.startsWith("/") || normalized.startsWith("./")) return normalized;
return `${runId}/${normalized}`;
}
function artifactPathSegments(path) {
return String(path).split(/[\\/]+/);
}
function validateExactArtifactPath(path, runId) {
if (typeof path !== "string" || path.length === 0) return "path missing or invalid";
if (path.includes("\0") || isAbsolute(path) || path.startsWith("./")) return `path '${path}' must be a clean relative path`;
const segments = artifactPathSegments(path);
if (segments.includes("..")) {
if (path.startsWith("../") && NODE_LEVEL_ARTIFACTS.has(path.slice(3)) && segments.length === 2) return null;
return `path '${path}' contains traversal`;
}
if (/^run_\d+\//.test(path)) return `exact artifact path '${path}' must be run-relative, not run_N-prefixed`;
return null;
}
function validateCanonicalArtifactPath(path, runId) {
if (typeof path !== "string" || path.length === 0) return "path missing or invalid";
if (path.includes("\0") || isAbsolute(path) || path.startsWith("./")) return `path '${path}' must be a clean relative path`;
const segments = artifactPathSegments(path);
if (segments.includes("..")) return `path '${path}' contains traversal`;
if (NODE_LEVEL_ARTIFACTS.has(path)) return null;
const prefix = `${runId}/`;
if (!path.startsWith(prefix)) return `canonical artifact path '${path}' must start with '${prefix}'`;
const rest = path.slice(prefix.length);
if (!rest || rest.startsWith("./") || artifactPathSegments(rest).includes("..")) {
return `canonical artifact path '${path}' has invalid run-relative suffix`;
}
if (/^run_\d+\//.test(rest)) return `canonical artifact path '${path}' has duplicate run_N prefix`;
return null;
}
function artifactPathErrors(handshake, runId, kind) {
const errors = [];
if (!Array.isArray(handshake?.artifacts)) return errors;
const validator = kind === "canonical" ? validateCanonicalArtifactPath : validateExactArtifactPath;
for (const [index, artifact] of handshake.artifacts.entries()) {
const error = validator(artifact?.path, runId);
if (error) errors.push(`${kind} artifacts[${index}]: ${error}`);
}
return errors;
}
function normalizeProjection(value, runId, key = "", context = {}) {
if (Array.isArray(value)) {
const items = value.map((item) => normalizeProjection(item, runId, "", context));
return key === "artifacts" ? items.sort((a, b) => stableJson(a).localeCompare(stableJson(b))) : items;
}
if (!isPlainObject(value)) return value;
const out = {};
for (const k of Object.keys(value).sort()) {
out[k] = normalizeProjection(value[k], runId, k, context);
}
if (key === "" && Array.isArray(out.artifacts)) {
out.artifacts = normalizeProjection(out.artifacts, runId, "artifacts", context);
}
if (Object.hasOwn(out, "path")) out.path = canonicalArtifactPath(out.path, runId, context);
return out;
}
function firstProjectionDiff(canonical, exact, path = "$") {
if (stableJson(canonical) === stableJson(exact)) return null;
if (Array.isArray(canonical) || Array.isArray(exact)) {
if (!Array.isArray(canonical) || !Array.isArray(exact)) return `${path}: type mismatch`;
if (canonical.length !== exact.length) return `${path}: canonical length ${canonical.length} != exact length ${exact.length}`;
for (let i = 0; i < canonical.length; i++) {
const diff = firstProjectionDiff(canonical[i], exact[i], `${path}[${i}]`);
if (diff) return diff;
}
return `${path}: array mismatch`;
}
if (!isPlainObject(canonical) || !isPlainObject(exact)) {
return `${path}: canonical ${JSON.stringify(canonical)} != exact ${JSON.stringify(exact)}`;
}
const keys = [...new Set([...Object.keys(canonical), ...Object.keys(exact)])].sort();
for (const key of keys) {
if (!Object.hasOwn(canonical, key)) return `${path}.${key}: missing from canonical`;
if (!Object.hasOwn(exact, key)) return `${path}.${key}: missing from exact`;
const diff = firstProjectionDiff(canonical[key], exact[key], `${path}.${key}`);
if (diff) return diff;
}
return `${path}: object mismatch`;
}
export function canonicalProjectionErrors(dir, state, validateCanonical = null, options = {}) {
const errors = [];
const entries = options.entries
? new Map(options.entries.map((entry) => [entry.nodeId, entry.runId]))
: latestAuthoritativeRunByNode(state);
for (const [nodeId, runId] of entries) {
const path = canonicalHandshakePath(dir, nodeId);
if (!existsSync(path)) continue;
const exact = resolveExactRunHandshake(dir, nodeId, runId);
if (exact.error) {
errors.push(`${nodeId}/${runId}: exact ${exact.error}`);
continue;
}
if (exact.missing || !exact.path || !existsSync(exact.path)) {
errors.push(`${nodeId}/${runId}: exact handshake missing at ${exact.path}`);
continue;
}
const parsed = parseHandshakeFile(path);
if (parsed.error) {
errors.push(`${nodeId}/${runId}: canonical ${parsed.error}`);
continue;
}
const identityErrors = handshakeIdentityErrors(parsed.data, nodeId, runId);
if (identityErrors.length > 0) {
errors.push(`${nodeId}/${runId}: canonical ${path}: ${identityErrors.join("; ")}`);
}
if (validateCanonical) {
const schemaErrors = validateCanonical(parsed.data, path, nodeId, runId);
errors.push(...schemaErrors.map((error) => `${nodeId}/${runId}: canonical ${error}`));
}
errors.push(...artifactPathErrors(parsed.data, runId, "canonical").map((error) => `${nodeId}/${runId}: ${error}`));
errors.push(...artifactPathErrors(exact.data, runId, "exact").map((error) => `${nodeId}/${runId}: ${error}`));
const nodeDir = join(dir, "nodes", nodeId);
const context = { nodeDir, runDir: join(nodeDir, runId) };
const canonicalProjection = normalizeProjection(parsed.data, runId, "", context);
const exactProjection = normalizeProjection(exact.data, runId, "", context);
const diff = firstProjectionDiff(canonicalProjection, exactProjection);
if (diff) {
errors.push(`${nodeId}/${runId}: canonical ${path} is not exact-run projection of ${exact.path}: ${diff}`);
}
}
return errors;
}
export function stoppedFlowError(state, command) {
if (state?.status !== "stopped") return null;
return `flow is stopped - ${command} cannot mutate state`;
}
export function assertFlowMutable(state, command) {
const error = stoppedFlowError(state, command);
if (error) throw new Error(error);
}
import assert from "node:assert/strict";
import { after, describe, test } from "node:test";
import { mkdtempSync, mkdirSync, rmSync, writeFileSync } from "node:fs";
import { homedir, tmpdir } from "node:os";
import { join } from "node:path";
import { cmdSynthesize } from "./eval-commands.mjs";
import { buildCumulativeFindingsMarkdown } from "./cumulative-findings.mjs";
import { loadTestCommandSpec } from "./test-command-execution.mjs";
import { collectTestDesignPlanReasons } from "./test-plan-gate.mjs";
import { compareRunIds, parseRunOrdinal } from "./run-id.mjs";
import { cmdUxVerdict } from "./ux-verdict.mjs";
const TMPBASE = mkdtempSync(join(tmpdir(), "opc-run-id-selection-"));
const UXBASE = mkdtempSync(join(homedir(), ".opc", "sessions", "opc-run-id-selection-"));
after(() => {
rmSync(TMPBASE, { recursive: true, force: true });
rmSync(UXBASE, { recursive: true, force: true });
});
function runDir(session, node, runId) {
const dir = join(session, "nodes", node, runId);
mkdirSync(dir, { recursive: true });
return dir;
}
function captureJson(fn) {
const output = [];
const original = console.log;
console.log = (...args) => output.push(args.join(" "));
try {
fn();
} finally {
console.log = original;
}
return JSON.parse(output.at(-1));
}
function completeTestPlan() {
return [
"# Test Plan",
"## Unit / Smoke",
"node unit.mjs",
"unit assertion one",
"unit assertion two",
"## Contract / Edge Case",
"node contract.mjs",
"boundary assertion one",
"boundary assertion two",
"## Integration / E2E Flow",
"node integration.mjs",
"workflow assertion one",
"workflow assertion two",
"## UI / Visual / A11y",
"node visual.mjs",
"accessibility assertion one",
"accessibility assertion two",
"## Tier Baseline / Polish",
"node tier.mjs",
"baseline assertion one",
"baseline assertion two",
"",
].join("\n");
}
describe("exact run_N selection", () => {
test("run ordinals parse and compare without Number coercion", () => {
assert.equal(parseRunOrdinal("run_9007199254740993"), 9007199254740993n);
assert.equal(parseRunOrdinal("9007199254740992"), 9007199254740992n);
assert.equal(parseRunOrdinal("run_bad"), null);
assert.equal(compareRunIds("run_9007199254740992", "run_9007199254740993"), -1);
assert.ok(compareRunIds("invalid-a", "invalid-b") < 0);
});
test("synthesize selects the exact latest run beyond MAX_SAFE_INTEGER", () => {
const session = join(TMPBASE, "synthesize");
const oldRun = runDir(session, "review", "run_9007199254740992");
const newRun = runDir(session, "review", "run_9007199254740993");
writeFileSync(join(oldRun, "eval-owner.md"), "# Review\n\nNo findings.\n");
writeFileSync(join(newRun, "eval-owner.md"), "# Review\n\n[CRITICAL] src/new.mjs:1 — newest run finding\n");
const result = captureJson(() => cmdSynthesize([session, "--node", "review"]));
assert.equal(result.totals.critical, 1, JSON.stringify(result));
});
test("test command loading selects the exact latest run", () => {
const session = join(TMPBASE, "test-command");
const oldRun = runDir(session, "test-design", "run_9007199254740992");
const newRun = runDir(session, "test-design", "run_9007199254740993");
writeFileSync(join(oldRun, "handshake.json"), JSON.stringify({ testCommand: "node old.mjs" }));
writeFileSync(join(newRun, "handshake.json"), JSON.stringify({ testCommand: "node new.mjs" }));
assert.equal(loadTestCommandSpec(session, "test-design")?.testCommand, "node new.mjs");
});
test("test plan gate selects the exact latest run", () => {
const session = join(TMPBASE, "test-plan");
const oldRun = runDir(session, "test-design", "run_9007199254740992");
const newRun = runDir(session, "test-design", "run_9007199254740993");
writeFileSync(join(oldRun, "test-plan.md"), completeTestPlan());
writeFileSync(join(newRun, "test-plan.md"), "# Test Plan\n\nNo executable cases yet.\n");
const reasons = collectTestDesignPlanReasons(session, "test-design");
assert.ok(reasons.some((reason) => reason.includes("missing layers")), reasons.join("\n"));
});
test("cumulative findings preserve exact numeric run order", () => {
const session = join(TMPBASE, "cumulative");
runDir(session, "review", "run_9007199254740993");
runDir(session, "review", "run_9007199254740992");
const markdown = buildCumulativeFindingsMarkdown(session, {
entryNode: "review",
currentNode: "review",
history: [],
});
assert.ok(
markdown.indexOf("run_9007199254740992") < markdown.indexOf("run_9007199254740993"),
markdown,
);
});
test("UX verdict reads the exact preceding run", () => {
const session = join(UXBASE, "ux-verdict");
const priorRunId = "run_9007199254740992";
const currentRunId = "run_9007199254740993";
const prior = runDir(session, "ux-simulation", priorRunId);
const current = runDir(session, "ux-simulation", currentRunId);
writeFileSync(join(session, "flow-state.json"), JSON.stringify({ tier: "polished" }));
writeFileSync(join(prior, "ux-verdict.json"), JSON.stringify({
verdict: "PASS",
uxResult: { flagDetails: [], redFlags: { critical: 0, warning: 0, suggestion: 0 } },
}));
writeFileSync(join(current, "observer-new-user.md"), [
"# Observer Report",
"",
"```json",
JSON.stringify({
persona: "new-user",
tier: "polished",
red_flags: [],
trust_signals: { present: [], absent: [] },
friction_points: [],
tier_fit: "at-tier",
reasoning: "I found the experience detailed, coherent, and trustworthy throughout the complete workflow.",
}, null, 2),
"```",
"",
].join("\n"));
const result = captureJson(() => cmdUxVerdict([
"--dir", session,
"--run", currentRunId.slice("run_".length),
]));
assert.equal(result.uxResult.delta?.vs_run, priorRunId, JSON.stringify(result));
});
test("rubric convergence reads the two exact preceding runs", () => {
const session = join(TMPBASE, "convergence");
const node = "review";
const current = runDir(session, node, "run_9007199254740993");
const prior = runDir(session, node, "run_9007199254740992");
const prior2 = runDir(session, node, "run_9007199254740991");
const wrongPrior = runDir(session, node, "run_9007199254740990");
for (const dir of [current, prior, prior2, wrongPrior]) {
mkdirSync(join(dir, "ext-design-intelligence"), { recursive: true });
writeFileSync(join(dir, "eval-owner.md"), "# Review\n\nNo findings.\n");
}
writeFileSync(join(current, "ext-design-intelligence", "rubric-verdict.json"), JSON.stringify({ final: 4.1, verdict: "PASS" }));
writeFileSync(join(prior, "ext-design-intelligence", "rubric-verdict.json"), JSON.stringify({ final: 1.0, verdict: "FAIL" }));
writeFileSync(join(prior2, "ext-design-intelligence", "rubric-verdict.json"), JSON.stringify({ final: 4.2, verdict: "PASS" }));
writeFileSync(join(wrongPrior, "ext-design-intelligence", "rubric-verdict.json"), JSON.stringify({ final: 4.3, verdict: "PASS" }));
const result = captureJson(() => cmdSynthesize([
session,
"--node", node,
"--run", "9007199254740993",
]));
assert.equal(result.convergenceWarning, undefined, JSON.stringify(result));
});
});
export function parseRunOrdinal(value) {
const match = /^(?:run_)?(\d+)$/.exec(String(value ?? ""));
return match ? BigInt(match[1]) : null;
}
export function compareRunIds(left, right) {
const leftOrdinal = parseRunOrdinal(left);
const rightOrdinal = parseRunOrdinal(right);
if (leftOrdinal === null || rightOrdinal === null) {
return String(left).localeCompare(String(right));
}
return leftOrdinal === rightOrdinal ? 0 : leftOrdinal < rightOrdinal ? -1 : 1;
}
#!/bin/bash
set -u
ROOT="$(cd "$(dirname "$0")/.." && pwd)"
TMP="$(mktemp -d)"
trap 'rm -rf "$TMP"' EXIT
PASS=0
FAIL=0
ok() {
echo " PASS $1"
PASS=$((PASS + 1))
}
fail() {
echo " FAIL $1"
FAIL=$((FAIL + 1))
}
expect_abort() {
local name="$1"
shift
if "$@" >/dev/null 2>&1; then
fail "$name did not abort"
else
ok "$name aborted"
fi
}
expect_finishes_with_open_stdin() {
local name="$1"
shift
local fifo="$TMP/stdin-fifo-$$"
mkfifo "$fifo"
node -e 'const fs=require("fs"); fs.openSync(process.argv[1],"w"); setTimeout(()=>{},90000)' "$fifo" &
local writer=$!
"$@" < "$fifo" >/dev/null 2>&1 &
local pid=$!
child_done() {
local stat
stat=$(ps -o stat= -p "$pid" 2>/dev/null | tr -d ' ' || true)
[ -z "$stat" ] || [[ "$stat" == Z* ]]
}
local deadline=$((SECONDS + 90))
while [ "$SECONDS" -lt "$deadline" ]; do
if child_done; then
wait "$pid"
kill "$writer" 2>/dev/null || true
wait "$writer" 2>/dev/null || true
rm -f "$fifo"
ok "$name"
return
fi
sleep 1
done
if child_done; then
wait "$pid"
kill "$writer" 2>/dev/null || true
wait "$writer" 2>/dev/null || true
rm -f "$fifo"
ok "$name"
return
fi
kill -TERM "-$pid" 2>/dev/null || kill -TERM "$pid" 2>/dev/null || true
sleep 1
kill -KILL "-$pid" 2>/dev/null || kill -KILL "$pid" 2>/dev/null || true
wait "$pid" 2>/dev/null || true
kill "$writer" 2>/dev/null || true
wait "$writer" 2>/dev/null || true
rm -f "$fifo"
fail "$name hung with open stdin"
}
echo "=== Shell cleanup fault contracts ==="
HARNESS_NAME=".harness-bypass-chain-cleanup-$$"
expect_abort "bypass-chain fault injection" \
env OPC_TEST_HARNESS_NAME="$HARNESS_NAME" OPC_TEST_ABORT_AFTER_HARNESS_INIT=1 \
bash "$ROOT/test/test-bypass-chain.sh"
if [ ! -e "$ROOT/$HARNESS_NAME" ]; then
ok "bypass-chain removes repo-local harness"
else
fail "bypass-chain left repo-local harness"
rm -rf "$ROOT/$HARNESS_NAME"
fi
FLOW_HOME="$TMP/flow-home"
FLOW_HOME_PROBE="$TMP/flow-home-probe"
mkdir -p "$FLOW_HOME/.claude/flows"
printf '%s\n' 'user idea-factory sentinel' > "$FLOW_HOME/.claude/flows/idea-factory.json"
expect_abort "flow-part3 fault injection" \
env OPC_TEST_HOME_OVERRIDE="$FLOW_HOME" OPC_TEST_HOME_PROBE="$FLOW_HOME_PROBE" \
OPC_TEST_ABORT_AFTER_FLOW_FIXTURE=1 bash "$ROOT/test/test-flow-part3.sh"
if [ "$(cat "$FLOW_HOME_PROBE" 2>/dev/null)" = "$FLOW_HOME" ]; then
ok "flow-part3 used the injected fixture HOME"
else
fail "flow-part3 did not use the injected fixture HOME"
fi
if [ "$(cat "$FLOW_HOME/.claude/flows/idea-factory.json" 2>/dev/null)" = "user idea-factory sentinel" ]; then
ok "flow-part3 restores pre-existing flow fixture"
else
fail "flow-part3 overwrote pre-existing flow fixture"
fi
COVERAGE_HOME="$TMP/coverage-home"
COVERAGE_HOME_PROBE="$TMP/coverage-home-probe"
mkdir -p "$COVERAGE_HOME/.claude/flows"
printf '%s\n' 'user coverage idea sentinel' > "$COVERAGE_HOME/.claude/flows/idea-factory.json"
printf '%s\n' 'user context sentinel' > "$COVERAGE_HOME/.claude/flows/test-ctx-flow.json"
expect_abort "coverage-part1 fault injection" \
env OPC_TEST_HOME_OVERRIDE="$COVERAGE_HOME" OPC_TEST_HOME_PROBE="$COVERAGE_HOME_PROBE" \
OPC_TEST_ABORT_AFTER_CONTEXT_FIXTURE=1 bash "$ROOT/test/test-coverage-part1.sh"
if [ "$(cat "$COVERAGE_HOME_PROBE" 2>/dev/null)" = "$COVERAGE_HOME" ]; then
ok "coverage-part1 used the injected fixture HOME"
else
fail "coverage-part1 did not use the injected fixture HOME"
fi
if [ "$(cat "$COVERAGE_HOME/.claude/flows/idea-factory.json" 2>/dev/null)" = "user coverage idea sentinel" ]; then
ok "coverage-part1 restores idea-factory fixture"
else
fail "coverage-part1 overwrote idea-factory fixture"
fi
if [ "$(cat "$COVERAGE_HOME/.claude/flows/test-ctx-flow.json" 2>/dev/null)" = "user context sentinel" ]; then
ok "coverage-part1 restores context fixture"
else
fail "coverage-part1 overwrote context fixture"
fi
expect_finishes_with_open_stdin "gaps-part1 does not hang when stdin stays open" \
bash "$ROOT/test/test-gaps-part1.sh"
if grep -q 'bash "$f" < /dev/null' "$ROOT/test/run-all.sh"; then
ok "run-all closes child stdin"
else
fail "run-all does not close child stdin"
fi
echo ""
echo "Results: $PASS passed, $FAIL failed"
[ "$FAIL" -eq 0 ]
+35
-38

@@ -5,2 +5,4 @@ import { existsSync, readFileSync, readdirSync, statSync, writeFileSync } from "fs";

import { parseStructuredFindings, structuredSeverityName } from "./structured-findings.mjs";
import { compareRunIds } from "./run-id.mjs";
import { authoritativeEntries } from "./flow-evidence.mjs";

@@ -16,5 +18,6 @@ const OUT = "cumulative-findings.md";

try {
return readdirSync(nodeDir)
.filter((name) => /^run_\d+$/.test(name))
.sort((a, b) => Number(a.slice(4)) - Number(b.slice(4)));
return readdirSync(nodeDir, { withFileTypes: true })
.filter((entry) => entry.isDirectory() && /^run_\d+$/.test(entry.name))
.map((entry) => entry.name)
.sort(compareRunIds);
} catch { return []; }

@@ -68,4 +71,4 @@ }

const nodeDir = join(dir, "nodes", nodeId);
const runDir = runId ? join(nodeDir, runId) : join(nodeDir, listRunDirs(nodeDir).at(-1) || "");
const handshake = readJson(join(nodeDir, "handshake.json"));
const runDir = runId ? join(nodeDir, runId) : "";
const handshake = runId ? readJson(join(nodeDir, "handshake.json")) : null;
const runHandshake = readJson(join(runDir, "handshake.json"));

@@ -76,42 +79,29 @@ return { nodeId, runId, nodeDir, runDir, handshake, runHandshake };

function orderedEntries(dir, state) {
const nodes = [];
const seenNodes = new Set();
const addNode = (entry) => {
const nodeId = entry?.nodeId || entry?.node;
if (!nodeId || seenNodes.has(nodeId)) return;
seenNodes.add(nodeId);
nodes.push(nodeId);
};
addNode({ nodeId: state?.entryNode });
for (const entry of state?.history || []) addNode(entry);
addNode({ nodeId: state?.currentNode });
return authoritativeEntries(state);
}
function orphanEntries(dir, state) {
const authoritative = new Set(orderedEntries(dir, state).map((entry) => `${entry.nodeId}\0${entry.runId}`));
const nodesDir = join(dir, "nodes");
if (!existsSync(nodesDir)) return nodes.map((nodeId) => ({ nodeId }));
if (!existsSync(nodesDir)) return [];
const orphans = [];
for (const nodeId of readdirSync(nodesDir).sort()) {
const nodeDir = join(nodesDir, nodeId);
if (statSync(nodeDir).isDirectory()) addNode({ nodeId });
if (!statSync(nodeDir).isDirectory()) continue;
for (const runId of listRunDirs(nodeDir)) {
if (!authoritative.has(`${nodeId}\0${runId}`)) orphans.push({ nodeId, runId });
}
}
return nodes.flatMap((nodeId) => {
const runIds = listRunDirs(join(nodesDir, nodeId));
return runIds.length ? runIds.map((runId) => ({ nodeId, runId })) : [{ nodeId }];
});
return orphans;
}
export function collectExecutionFixes(dir) {
export function collectExecutionFixes(dir, state) {
const fixes = [];
const nodesDir = join(dir, "nodes");
if (!existsSync(nodesDir)) return fixes;
for (const nodeId of readdirSync(nodesDir).sort()) {
const nodeDir = join(nodesDir, nodeId);
if (!statSync(nodeDir).isDirectory()) continue;
for (const raw of fixArrays(readJson(join(nodeDir, "handshake.json")))) {
for (const entry of orderedEntries(dir, state)) {
const { nodeId, runId } = entry;
const hs = readJson(join(dir, "nodes", nodeId, runId, "handshake.json"));
for (const raw of fixArrays(hs)) {
const text = fixText(raw);
if (text) fixes.push({ nodeId, runId: null, text });
if (text) fixes.push({ nodeId, runId, text });
}
for (const runId of listRunDirs(nodeDir)) {
for (const raw of fixArrays(readJson(join(nodeDir, runId, "handshake.json")))) {
const text = fixText(raw);
if (text) fixes.push({ nodeId, runId, text });
}
}
}

@@ -124,3 +114,3 @@ return fixes;

lines.push(`## ${nodeId}${runId ? ` / ${runId}` : ""}`);
const hs = handshake || runHandshake;
const hs = runHandshake || handshake;
if (hs) lines.push(`- Status: ${hs.status || "unknown"}${hs.verdict ? `, verdict: ${hs.verdict}` : ""}`);

@@ -155,3 +145,10 @@ if (existsSync(join(nodeDir, "extension-context.md"))) {

for (const entry of orderedEntries(dir, state)) appendNode(lines, readRunSummary(dir, entry));
const fixes = collectExecutionFixes(dir);
const orphans = orphanEntries(dir, state);
if (orphans.length > 0) {
lines.push("## Forensic Orphan Runs");
lines.push("These runs are present on disk but are not selected by flow-state/history.");
for (const entry of orphans) lines.push(`- ${entry.nodeId}/${entry.runId}`);
lines.push("");
}
const fixes = collectExecutionFixes(dir, state);
if (fixes.length) {

@@ -158,0 +155,0 @@ lines.push("## Fixes Applied During Execution");

@@ -8,5 +8,6 @@ // Evaluation analysis commands: verify, synthesize, tier-baseline

import { parseEvaluation } from "./eval-parser.mjs";
import { getFlag, resolveDir } from "./util.mjs";
import { getFlag, hasFlag, resolveDir } from "./util.mjs";
import { checkBaselineCoverage, generateTierTestCases, VALID_TIERS, TEST_LAYERS, TEST_LAYER_KEYWORDS, TEST_LAYER_LABELS } from "./tier-baselines.mjs";
import { anchorIssues as collectAnchorIssues } from "./test-plan-gate.mjs";
import { compareRunIds, parseRunOrdinal } from "./run-id.mjs";

@@ -216,16 +217,23 @@ /**

const runFlag = args.indexOf("--run");
const runProvided = hasFlag(args, "run");
if (runFlag !== -1 && args[runFlag + 1]) {
targetRunDir = join(dir, "nodes", nodeId, `run_${args[runFlag + 1]}`);
if (runProvided) {
const runValue = getFlag(args, "run", "");
if (!/^[1-9]\d*$/.test(runValue)) {
console.log(JSON.stringify({
roles: [],
totals: { critical: 0, warning: 0, suggestion: 0 },
verdict: "BLOCKED",
reason: "--run must be a positive numeric ordinal",
}));
return;
}
targetRunDir = join(dir, "nodes", nodeId, `run_${runValue}`);
} else {
const nodeDir = join(dir, "nodes", nodeId);
try {
const runs = readdirSync(nodeDir)
.filter((d) => d.startsWith("run_"))
.sort((a, b) => {
const na = parseInt(a.replace("run_", ""), 10);
const nb = parseInt(b.replace("run_", ""), 10);
return nb - na;
});
const runs = readdirSync(nodeDir, { withFileTypes: true })
.filter((entry) => entry.isDirectory() && /^run_\d+$/.test(entry.name))
.map((entry) => entry.name)
.sort((a, b) => compareRunIds(b, a));
if (runs.length === 0) {

@@ -848,9 +856,9 @@ console.log(JSON.stringify({ roles: [], totals: { critical: 0, warning: 0, suggestion: 0 }, verdict: "BLOCKED", reason: `no runs found for node '${nodeId}' in ${nodeDir}` }));

const runFlag = args.indexOf("--run");
const currentRunN = runFlag !== -1 && args[runFlag + 1] ? parseInt(args[runFlag + 1], 10) : null;
const iteration = currentRunN || parseInt((targetRunDir.match(/run_(\d+)$/) || [])[1] || "1", 10);
if (iteration >= 3) {
const explicitRun = runFlag !== -1 ? parseRunOrdinal(args[runFlag + 1]) : null;
const iteration = explicitRun ?? parseRunOrdinal(targetRunDir.match(/run_(\d+)$/)?.[1]) ?? 1n;
if (iteration >= 3n) {
const recentScores = [rubricScore.final];
for (let i = iteration - 1; i >= Math.max(1, iteration - 2); i--) {
for (const previous of [iteration - 1n, iteration - 2n]) {
try {
const prevPath = join(dir, "nodes", nodeId, `run_${i}`, "ext-design-intelligence", "rubric-verdict.json");
const prevPath = join(dir, "nodes", nodeId, `run_${previous}`, "ext-design-intelligence", "rubric-verdict.json");
if (existsSync(prevPath)) {

@@ -857,0 +865,0 @@ const prev = JSON.parse(readFileSync(prevPath, "utf8"));

@@ -5,6 +5,5 @@ // ext-commands.mjs — CLI commands for extension system

import { readFileSync, writeFileSync, existsSync, readdirSync, cpSync, mkdtempSync, rmSync, lstatSync, statSync, realpathSync, mkdirSync, copyFileSync } from "fs";
import { readFile, writeFile } from "fs/promises";
import { tmpdir } from "os";
import { join, resolve } from "path";
import { loadExtensions, firePromptAppend, fireVerdictAppend, fireExecuteRun, fireArtifactEmit, fireNodePreflight, writeFailureReport, saveRegistryCache, normalizeHook, lintCapability, enforceStrictMode, survivingExtensions } from "./extensions.mjs";
import { basename, join, resolve } from "path";
import { loadExtensions, firePromptAppend, fireVerdictAppend, fireExecuteRun, fireArtifactEmit, fireNodePreflight, readFailureReportState, writeFailureReport, saveRegistryCache, normalizeHook, lintCapability, enforceStrictMode, participatingExtensions } from "./extensions.mjs";
import { getFlag, atomicWriteSync, resolveDir, resolveDirReadOnly } from "./util.mjs";

@@ -15,2 +14,5 @@ import { resolveFlowTemplate } from "./flow-templates.mjs";

import { readCumulativeFindingsAppend } from "./cumulative-findings.mjs";
import { compareRunIds } from "./run-id.mjs";
import { resolveCurrentRun } from "./runaway-guard.mjs";
import { assertFlowMutable } from "./flow-state-guard.mjs";

@@ -49,3 +51,3 @@ // ─── Shared helpers ──────────────────────────────────────────────

.map(e => e.name)
.sort((a, b) => parseInt(b.replace("run_", ""), 10) - parseInt(a.replace("run_", ""), 10));
.sort((a, b) => compareRunIds(b, a));
return runDirs.length > 0 ? join(nodeDir, runDirs[0]) : null;

@@ -55,2 +57,143 @@ } catch { return null; }

export function resolveSelectedRunDir(dir, node, command = "extension lifecycle") {
let state;
try {
state = JSON.parse(readFileSync(resolve(dir, "flow-state.json"), "utf8"));
} catch (error) {
throw new Error(`cannot resolve selected run: flow-state.json parse error: ${error.message}`);
}
assertFlowMutable(state, command);
if (state.currentNode !== node) {
throw new Error(`cannot resolve selected run: current node is '${state.currentNode}', not '${node}'`);
}
const selected = resolveCurrentRun(state);
if (!selected) {
throw new Error(`cannot resolve selected run for '${node}'`);
}
const runDir = resolve(dir, "nodes", node, selected.runId);
if (!existsSync(runDir) || !lstatSync(runDir).isDirectory()) {
throw new Error(`selected run directory not found: nodes/${node}/${selected.runId}`);
}
return runDir;
}
function readOptionalHandshake(handshakePath) {
if (!existsSync(handshakePath)) return {};
let handshake;
try {
handshake = JSON.parse(readFileSync(handshakePath, "utf8"));
} catch (error) {
throw new Error(`handshake.json parse error: ${error.message}`);
}
if (!handshake || typeof handshake !== "object" || Array.isArray(handshake)) {
throw new Error("handshake.json schema error: root must be an object");
}
return handshake;
}
function validatePromptProvenanceInputs(runDir, context) {
const runId = basename(runDir);
const nodeId = context?.nodeId;
const sidecarPath = join(runDir, "prompt-extensions.json");
if (existsSync(sidecarPath)) {
let sidecar;
try {
sidecar = JSON.parse(readFileSync(sidecarPath, "utf8"));
} catch (error) {
throw new Error(`prompt-extensions.json parse error: ${error.message}`);
}
if (!sidecar || typeof sidecar !== "object" || Array.isArray(sidecar) || sidecar.version !== 1) {
throw new Error("prompt-extensions.json schema/version error");
}
if (sidecar.nodeId !== nodeId || sidecar.runId !== runId) {
throw new Error("prompt-extensions.json provenance does not match selected run");
}
if (!Array.isArray(sidecar.extensionsApplied) || sidecar.extensionsApplied.some((name) => typeof name !== "string" || !name)) {
throw new Error("prompt-extensions.json extensionsApplied must be an array of names");
}
}
const handshake = readOptionalHandshake(join(runDir, "handshake.json"));
if (handshake.nodeId && handshake.nodeId !== nodeId) {
throw new Error(`run handshake nodeId '${handshake.nodeId}' does not match '${nodeId}'`);
}
if (handshake.runId && handshake.runId !== runId) {
throw new Error(`run handshake runId '${handshake.runId}' does not match '${runId}'`);
}
return handshake;
}
function prevalidateLifecycleEvidence(runDir, context, { prompt = false } = {}) {
readFailureReportState(runDir);
return prompt
? validatePromptProvenanceInputs(runDir, context)
: readOptionalHandshake(join(runDir, "handshake.json"));
}
export function writePromptExtensionProvenance(runDir, context, extensionsApplied) {
const runId = basename(runDir);
const nodeId = context?.nodeId;
if (typeof nodeId !== "string" || !nodeId || !/^run_\d+$/.test(runId)) {
throw new Error("cannot write prompt extension provenance without valid nodeId and runId");
}
if (!Array.isArray(extensionsApplied) || extensionsApplied.some((name) => typeof name !== "string" || !name)) {
throw new Error("cannot write prompt extension provenance: extensionsApplied must be an array of names");
}
const sidecarPath = join(runDir, "prompt-extensions.json");
const handshakePath = join(runDir, "handshake.json");
const handshake = validatePromptProvenanceInputs(runDir, context);
if (extensionsApplied.length === 0 && Object.keys(handshake).length === 0) return null;
const sidecar = {
version: 1,
generatedAt: new Date().toISOString(),
nodeId,
runId,
extensionsApplied: extensionsApplied.slice(),
};
atomicWriteSync(sidecarPath, JSON.stringify(sidecar, null, 2) + "\n");
atomicWriteSync(handshakePath, JSON.stringify({
...handshake,
nodeId,
...(context.nodeType ? { nodeType: context.nodeType } : {}),
runId,
extensionsApplied: extensionsApplied.slice(),
}, null, 2) + "\n");
return sidecarPath;
}
export function collectPromptExtensionProvenanceErrors(runDir, expected) {
const runId = expected?.runId || basename(runDir);
const sidecarPath = join(runDir, "prompt-extensions.json");
if (!existsSync(sidecarPath)) return [`${runId}/prompt-extensions.json not found`];
let sidecar;
try {
sidecar = JSON.parse(readFileSync(sidecarPath, "utf8"));
} catch (error) {
return [`${runId}/prompt-extensions.json parse error: ${error.message}`];
}
const errors = [];
if (!sidecar || typeof sidecar !== "object" || Array.isArray(sidecar) || sidecar.version !== 1) {
errors.push(`${runId}/prompt-extensions.json schema/version invalid`);
return errors;
}
if (sidecar.nodeId !== expected.nodeId) {
errors.push(`${runId}/prompt-extensions.json nodeId '${sidecar.nodeId}' does not match '${expected.nodeId}'`);
}
if (sidecar.runId !== runId) {
errors.push(`${runId}/prompt-extensions.json runId '${sidecar.runId}' does not match '${runId}'`);
}
if (!Array.isArray(sidecar.extensionsApplied) || sidecar.extensionsApplied.some((name) => typeof name !== "string" || !name)) {
errors.push(`${runId}/prompt-extensions.json extensionsApplied missing or invalid`);
} else {
const claimed = expected.extensionsApplied || [];
if (claimed.length !== sidecar.extensionsApplied.length || claimed.some((name) => !sidecar.extensionsApplied.includes(name))) {
errors.push(`${runId}/prompt-extensions.json extensionsApplied does not match handshake claim`);
}
}
return errors;
}
/**

@@ -72,20 +215,64 @@ * Read flow-state.json + resolved flow template, return the current node's

function readNodeCapabilities(dir, node, args) {
try {
const statePath = resolve(dir, "flow-state.json");
let state = null;
if (existsSync(statePath)) {
try { state = JSON.parse(readFileSync(statePath, "utf8")); } catch { /* state corrupt — treat as absent */ }
let state = null;
const statePath = resolve(dir, "flow-state.json");
if (existsSync(statePath)) {
try {
state = JSON.parse(readFileSync(statePath, "utf8"));
} catch (error) {
throw new Error(`flow-state.json parse error: ${error.message}`);
}
const { template } = resolveFlowTemplate(args, state);
const templateResolved = !!template;
const nodeResolved = templateResolved && Array.isArray(template.nodes) && template.nodes.includes(node);
if (!nodeResolved) return { caps: [], templateResolved: false };
if (!template.nodeCapabilities) return { caps: [], templateResolved: true };
const caps = template.nodeCapabilities[node];
return { caps: Array.isArray(caps) ? caps : [], templateResolved: true };
} catch {
return { caps: [], templateResolved: false };
if (!state || typeof state !== "object" || Array.isArray(state)) {
throw new Error("flow-state.json schema error: root must be an object");
}
}
const tier = typeof state?.tier === "string" ? state.tier : null;
const visualEvaluationRequired = tier === "polished" || tier === "delightful";
const resolved = resolveFlowTemplate(args, state);
if (resolved.error) throw new Error(resolved.error);
const { template } = resolved;
const templateResolved = !!template;
const nodeResolved = templateResolved && Array.isArray(template.nodes) && template.nodes.includes(node);
const nodeType = nodeResolved ? template.nodeTypes?.[node] || null : null;
if (!nodeResolved) {
return { caps: [], templateResolved: false, nodeType, tier, visualEvaluationRequired };
}
const caps = template.nodeCapabilities?.[node];
return {
caps: Array.isArray(caps) ? caps : [],
templateResolved: true,
nodeType,
tier,
visualEvaluationRequired,
};
}
export function resolveNodeExtensionContext(dir, node, args = [], overrides = {}) {
const {
caps: nodeCapabilities,
templateResolved,
nodeType,
tier,
visualEvaluationRequired,
} = readNodeCapabilities(dir, node, args);
const task = Object.hasOwn(overrides, "task") ? overrides.task : readTaskFromAC(dir);
const context = {
node,
nodeId: node,
nodeType,
tier,
role: overrides.role ?? null,
task,
taskDescription: task,
visualEvaluationRequired,
flowDir: resolve(dir),
cwd: overrides.cwd ?? process.cwd(),
devServerUrl: overrides.devServerUrl ?? "",
nodeCapabilities,
nodeCapabilitiesResolved: templateResolved,
};
if (overrides.runDir) context.runDir = overrides.runDir;
return context;
}
// ─── prompt-context ──────────────────────────────────────────────

@@ -112,3 +299,20 @@

const task = readTaskFromAC(dir);
const runDir = resolveSelectedRunDir(dir, node, "prompt-context");
const devServerUrl = getFlag(args, "dev-server") || process.env.DEV_SERVER_URL || config.devServerUrl || "";
const context = resolveNodeExtensionContext(dir, node, args, {
role,
task,
runDir,
devServerUrl,
});
const { nodeCapabilities } = context;
try {
prevalidateLifecycleEvidence(runDir, context, { prompt: true });
} catch (error) {
console.error(error.message);
process.exit(1);
return;
}
let registry;

@@ -122,35 +326,2 @@ try {

const devServerUrl = getFlag(args, "dev-server") || process.env.DEV_SERVER_URL || config.devServerUrl || "";
const { caps: nodeCapabilities, templateResolved } = readNodeCapabilities(dir, node, args);
// Resolve nodeType from flow-state.json + template
let nodeType = null;
try {
const stPath = join(resolve(dir), "flow-state.json");
if (existsSync(stPath)) {
const st = JSON.parse(readFileSync(stPath, "utf8"));
const tplName = st.flowTemplate;
if (tplName) {
const { FLOW_TEMPLATES } = await import("./flow-templates.mjs");
const tpl = FLOW_TEMPLATES[tplName];
if (tpl?.nodeTypes) nodeType = tpl.nodeTypes[node] || null;
}
}
} catch { /* best effort */ }
const context = {
node,
nodeId: node,
nodeType,
role,
task,
taskDescription: task,
flowDir: resolve(dir),
runDir: resolve(dir),
cwd: process.cwd(),
devServerUrl,
nodeCapabilities,
nodeCapabilitiesResolved: templateResolved,
};
const extensionAppend = await firePromptAppend(registry, context);

@@ -161,24 +332,11 @@ const append = [

].filter(Boolean).join("\n\n");
const applied = participatingExtensions(registry, context, ["prompt.append"]);
// Stamp extensionsApplied into this node's latest run handshake (if run dir exists)
const nodeDir = resolve(dir, "nodes", node);
const latestRunDir = findLatestRunDir(nodeDir);
if (latestRunDir) {
try {
const handshakePath = join(latestRunDir, 'handshake.json');
let handshake = {};
try { handshake = JSON.parse(readFileSync(handshakePath, 'utf8')); } catch { /* no handshake yet */ }
handshake.extensionsApplied = survivingExtensions(registry);
atomicWriteSync(handshakePath, JSON.stringify(handshake, null, 2));
} catch { /* best effort */ }
// G2 fix: persist prompt-phase failures (e.g. slow-ext timeout) so
// operators see them in extension-failures.md instead of just stderr.
// writeFailureReport now read-merges, so this won't clobber prior phases.
writeFailureReport(registry, latestRunDir);
}
// Failure evidence is canonical and must remain parseable before participant
// provenance can be stamped into the selected run.
writeFailureReport(registry, runDir);
writePromptExtensionProvenance(runDir, context, applied);
saveRegistryCache(resolve(dir), registry);
console.log(JSON.stringify({ append, applied: registry.applied, nodeCapabilities }));
console.log(JSON.stringify({ append, applied, nodeCapabilities }));

@@ -455,3 +613,3 @@ // Strict mode: after isolation work is done, exit non-zero if any failures.

console.error("Usage: opc-harness extension-verdict --node <id> --dir <harness-dir>");
console.error("Loads extensions, fires verdict.append, writes eval-extensions.md to latest run dir.");
console.error("Loads extensions, fires verdict.append, writes eval-extensions.md to the state-selected run dir.");
return;

@@ -471,3 +629,25 @@ }

const task = readTaskFromAC(dir);
const runDir = resolveSelectedRunDir(dir, node, "extension-verdict");
const devServerUrl = getFlag(args, "dev-server") || process.env.DEV_SERVER_URL || config.devServerUrl || "";
const context = resolveNodeExtensionContext(dir, node, args, {
role: "evaluator",
task,
runDir,
devServerUrl,
});
const { nodeCapabilities } = context;
// Existing canonical provenance is a trust boundary. Missing is allowed;
// malformed bytes fail closed and are never replaced with a fresh object.
const handshakePath = join(runDir, "handshake.json");
let handshake;
try {
handshake = prevalidateLifecycleEvidence(runDir, context);
} catch (error) {
console.error(error.message);
process.exit(1);
return;
}
let registry;

@@ -481,34 +661,10 @@ try {

const runDir = findLatestRunDir(resolve(dir, "nodes", node));
if (!runDir) {
console.error(`No run directories found for node '${node}' in ${resolve(dir, "nodes", node)}`);
process.exit(1);
}
const devServerUrl = getFlag(args, "dev-server") || process.env.DEV_SERVER_URL || config.devServerUrl || "";
const { caps: nodeCapabilities, templateResolved } = readNodeCapabilities(dir, node, args);
const context = {
node,
role: "evaluator",
task,
flowDir: resolve(dir),
runDir,
devServerUrl,
nodeCapabilities,
nodeCapabilitiesResolved: templateResolved,
};
await fireVerdictAppend(registry, context);
// Stamp extensionsApplied into the run dir's handshake.json
const handshakePath = join(runDir, 'handshake.json');
let handshake = {};
try {
handshake = JSON.parse(await readFile(handshakePath, 'utf8'));
} catch { /* no handshake yet, start fresh */ }
handshake.extensionsApplied = survivingExtensions(registry);
const appliedExtensions = participatingExtensions(registry, context, ["verdict.append"]);
handshake.extensionsApplied = appliedExtensions;
atomicWriteSync(handshakePath, JSON.stringify(handshake, null, 2));
console.log(JSON.stringify({ ok: true, node, runDir, extensionsApplied: survivingExtensions(registry), nodeCapabilities }));
console.log(JSON.stringify({ ok: true, node, runDir, extensionsApplied: appliedExtensions, nodeCapabilities }));

@@ -548,3 +704,25 @@ // Strict mode: after eval-extensions.md and writeFailureReport have run

const task = readTaskFromAC(dir);
const runDir = resolveSelectedRunDir(dir, node, "extension-artifact");
const devServerUrl = getFlag(args, "dev-server") || process.env.DEV_SERVER_URL || config.devServerUrl || "";
const context = resolveNodeExtensionContext(dir, node, args, {
role: "executor",
task,
runDir,
devServerUrl,
});
const { nodeCapabilities } = context;
// Read before running side-effectful hooks so malformed canonical provenance
// cannot be laundered by a later handshake rewrite.
const handshakePath = join(runDir, "handshake.json");
let handshake;
try {
handshake = prevalidateLifecycleEvidence(runDir, context);
} catch (error) {
console.error(error.message);
process.exit(1);
return;
}
let registry;

@@ -558,22 +736,2 @@ try {

const runDir = findLatestRunDir(resolve(dir, "nodes", node));
if (!runDir) {
console.error(`No run directories found for node '${node}' in ${resolve(dir, "nodes", node)}`);
process.exit(1);
}
const devServerUrl = getFlag(args, "dev-server") || process.env.DEV_SERVER_URL || config.devServerUrl || "";
const { caps: nodeCapabilities, templateResolved } = readNodeCapabilities(dir, node, args);
const context = {
node,
role: "executor",
task,
flowDir: resolve(dir),
runDir,
devServerUrl,
nodeCapabilities,
nodeCapabilitiesResolved: templateResolved,
};
const executeResults = await fireExecuteRun(registry, context);

@@ -588,7 +746,2 @@ const emitted = await fireArtifactEmit(registry, context);

// Merge ext-artifact entries into handshake.artifacts[] (dedup by path)
const handshakePath = join(runDir, 'handshake.json');
let handshake = {};
try {
handshake = JSON.parse(await readFile(handshakePath, 'utf8'));
} catch { /* no handshake yet */ }
if (!Array.isArray(handshake.artifacts)) handshake.artifacts = [];

@@ -599,3 +752,4 @@ const seen = new Set(handshake.artifacts.map(a => (a && a.path) || null).filter(Boolean));

}
handshake.extensionsApplied = survivingExtensions(registry);
const appliedExtensions = participatingExtensions(registry, context, ["execute.run", "artifact.emit"]);
handshake.extensionsApplied = appliedExtensions;
atomicWriteSync(handshakePath, JSON.stringify(handshake, null, 2));

@@ -607,3 +761,3 @@

runDir,
extensionsApplied: survivingExtensions(registry),
extensionsApplied: appliedExtensions,
nodeCapabilities,

@@ -645,3 +799,20 @@ executeRunCount: executeResults.length,

const task = readTaskFromAC(dir);
const { caps: nodeCapabilities, templateResolved } = readNodeCapabilities(dir, node, args);
const devServerUrl = getFlag(args, "dev-server") || process.env.DEV_SERVER_URL || config.devServerUrl || "";
let runDir;
let context;
try {
runDir = resolveSelectedRunDir(dir, node, "node-preflight");
context = resolveNodeExtensionContext(dir, node, args, {
role: "preflight",
task,
runDir,
devServerUrl,
});
prevalidateLifecycleEvidence(runDir, context);
} catch (error) {
console.error(error.message);
process.exit(1);
return;
}
const { nodeCapabilities } = context;

@@ -670,25 +841,5 @@ if (nodeCapabilities.length > 0 && !task.trim()) {

const devServerUrl = getFlag(args, "dev-server") || process.env.DEV_SERVER_URL || config.devServerUrl || "";
const context = {
node,
nodeId: node,
role: "preflight",
task,
taskDescription: task,
flowDir: resolve(dir),
cwd: process.cwd(),
devServerUrl,
nodeCapabilities,
nodeCapabilitiesResolved: templateResolved,
};
const results = await fireNodePreflight(registry, context);
// Write failure report to the node's latest run dir (if exists)
const nodeDir = resolve(dir, "nodes", node);
const latestRunDir = findLatestRunDir(nodeDir);
if (latestRunDir) {
writeFailureReport(registry, latestRunDir);
}
writeFailureReport(registry, runDir);

@@ -705,3 +856,3 @@ saveRegistryCache(resolve(dir), registry);

artifactTypes,
extensionsApplied: survivingExtensions(registry),
extensionsApplied: participatingExtensions(registry, context, ["preflight"]),
nodeCapabilities,

@@ -708,0 +859,0 @@ }));

@@ -96,2 +96,3 @@ // extensions.mjs — OPC Extension System

function recordFailure(registry, ext, hook, kind, message) {
ext._successfulHooks?.delete(hook);
const entry = {

@@ -120,6 +121,14 @@ ext: ext.name,

function recordSuccess(ext) {
function beginHookInvocation(registry, hook) {
for (const ext of registry?.extensions || []) {
ext?._successfulHooks?.delete(hook);
}
}
function recordSuccess(ext, hook) {
// Any successful invocation resets the consecutive-failure streak.
// The breaker only trips on N-in-a-row, not N-total.
if (ext._failStreak) ext._failStreak = 0;
if (!(ext._successfulHooks instanceof Set)) ext._successfulHooks = new Set();
ext._successfulHooks.add(hook);
}

@@ -764,2 +773,3 @@

const parts = [];
beginHookInvocation(registry, "prompt.append");
warnMissingNodeCapsOnce(registry, context);

@@ -782,3 +792,3 @@ const requires = context.nodeCapabilities || [];

if (result === undefined || result === null || result === "") {
recordSuccess(ext);
recordSuccess(ext, "prompt.append");
continue;

@@ -792,3 +802,3 @@ }

parts.push(result);
recordSuccess(ext);
recordSuccess(ext, "prompt.append");
} catch (err) {

@@ -822,4 +832,24 @@ console.error(`WARN: extension ${ext.name} prompt.append failed:`, err.message);

*/
export function readVerdictExtensionState(runDir) {
const jsonPath = join(runDir, "eval-extensions.json");
if (!existsSync(jsonPath)) return null;
let prior;
try {
prior = JSON.parse(readFileSync(jsonPath, "utf8"));
} catch (error) {
throw new Error(`eval-extensions.json parse error: ${error.message}`);
}
if (!prior || typeof prior !== "object" || Array.isArray(prior)) {
throw new Error("eval-extensions.json schema error: root must be an object");
}
return prior;
}
export async function fireVerdictAppend(registry, context) {
const allFindings = [];
const jsonPath = context.runDir
? join(context.runDir, "eval-extensions.json")
: null;
if (context.runDir) readVerdictExtensionState(context.runDir);
beginHookInvocation(registry, "verdict.append");
warnMissingNodeCapsOnce(registry, context);

@@ -842,3 +872,3 @@ const requires = context.nodeCapabilities || [];

if (findings === undefined || findings === null) {
recordSuccess(ext);
recordSuccess(ext, "verdict.append");
continue;

@@ -855,3 +885,3 @@ }

}
recordSuccess(ext);
recordSuccess(ext, "verdict.append");
} catch (err) {

@@ -880,3 +910,2 @@ console.error(`WARN: extension ${ext.name} verdict.append failed:`, err.message);

// consumers (clean schema). Don't rename either without a v2 bump.
const jsonPath = join(context.runDir, "eval-extensions.json");
const jsonDoc = {

@@ -889,2 +918,7 @@ version: 1,

})),
extensionsApplied: participatingExtensions(
registry,
context,
["verdict.append"]
),
findings: allFindings.map((f) => ({

@@ -957,2 +991,3 @@ extension: f._ext,

const results = [];
beginHookInvocation(registry, "execute.run");
warnMissingNodeCapsOnce(registry, context);

@@ -975,3 +1010,3 @@ const requires = context.nodeCapabilities || [];

results.push({ ext: ext.name, result });
recordSuccess(ext);
recordSuccess(ext, "execute.run");
} catch (err) {

@@ -1002,2 +1037,3 @@ console.error(`WARN: extension ${ext.name} execute.run failed:`, err.message);

const emitted = [];
beginHookInvocation(registry, "artifact.emit");
warnMissingNodeCapsOnce(registry, context);

@@ -1023,3 +1059,3 @@ const requires = context.nodeCapabilities || [];

);
if (items === undefined || items === null) { recordSuccess(ext); continue; }
if (items === undefined || items === null) { recordSuccess(ext, "artifact.emit"); continue; }
if (!Array.isArray(items)) {

@@ -1078,3 +1114,3 @@ console.error(`WARN: extension ${ext.name} artifact.emit returned ${typeof items}, expected array — ignoring`);

// write failures (U1.6r semantics F1 fix-forward).
if (!anyItemFailed) recordSuccess(ext);
if (!anyItemFailed) recordSuccess(ext, "artifact.emit");
}

@@ -1100,2 +1136,3 @@ if (registry._flowDir) saveBreakerState(registry._flowDir, registry);

const results = [];
beginHookInvocation(registry, "preflight");
warnMissingNodeCapsOnce(registry, context);

@@ -1118,3 +1155,3 @@ const requires = context.nodeCapabilities || [];

if (result === undefined || result === null) {
recordSuccess(ext);
recordSuccess(ext, "preflight");
continue;

@@ -1128,3 +1165,3 @@ }

results.push({ ...result, _ext: ext.name });
recordSuccess(ext);
recordSuccess(ext, "preflight");
} catch (err) {

@@ -1234,2 +1271,31 @@ console.error(`WARN: extension ${ext.name} preflight failed:`, err.message);

*/
export function readFailureReportState(runDir) {
const sidecarPath = join(runDir, "extension-failures.json");
if (!existsSync(sidecarPath)) return { entries: [], droppedTotal: 0 };
let data;
try {
data = JSON.parse(readFileSync(sidecarPath, "utf8"));
} catch (error) {
throw new Error(`extension-failures.json parse error: ${error.message}`);
}
if (!data || typeof data !== "object" || Array.isArray(data) || !Array.isArray(data.failures)) {
throw new Error("extension-failures.json schema error: failures must be an array");
}
if (!Number.isSafeInteger(data.droppedTotal) || data.droppedTotal < 0) {
throw new Error("extension-failures.json schema error: droppedTotal must be a non-negative safe integer");
}
for (const [index, entry] of data.failures.entries()) {
if (!entry || typeof entry !== "object" || Array.isArray(entry)) {
throw new Error(`extension-failures.json schema error: failures[${index}] must be an object`);
}
for (const field of ["ext", "hook", "kind", "message", "at"]) {
if (typeof entry[field] !== "string" || entry[field].length === 0) {
throw new Error(`extension-failures.json schema error: failures[${index}].${field} must be a non-empty string`);
}
}
}
return { entries: data.failures, droppedTotal: data.droppedTotal };
}
export function writeFailureReport(registry, runDir) {

@@ -1242,22 +1308,6 @@ if (!runDir) return;

// U2.8c: Cross-command merge (G3) via JSON sidecar.
//
// Previous attempt parsed the markdown via regex; that was fragile (missing
// /u flag for emoji, ambiguous ext.hook split on dots) and silently
// degenerated to overwrite. The structurally correct fix is to keep the
// canonical record in a machine-readable JSON sidecar and render the
// markdown view from JSON. Parser/writer skew becomes impossible.
//
// Each CLI invocation reads the sidecar, unions with this run's
// registry.failures (dedup on ext|hook|kind|message), then writes BOTH
// sidecar + markdown atomically.
let priorEntries = [];
let priorDropped = 0;
if (existsSync(sidecarPath)) {
try {
const data = JSON.parse(readFileSync(sidecarPath, "utf8"));
if (Array.isArray(data.failures)) priorEntries = data.failures;
if (typeof data.droppedTotal === "number") priorDropped = data.droppedTotal;
} catch { /* corrupt sidecar = treat as empty, will be overwritten */ }
}
// Parse in a pure helper so lifecycle commands can fail closed before hooks.
const prior = readFailureReportState(runDir);
const priorEntries = prior.entries;
const priorDropped = prior.droppedTotal;

@@ -1318,2 +1368,15 @@ // U2.8e (#2): use JSON.stringify on a tuple instead of `|`-joined string —

export function participatingExtensions(registry, context, hookNames) {
if (!registry || !Array.isArray(registry.extensions)) return [];
const requires = context?.nodeCapabilities || [];
const hooks = Array.isArray(hookNames) ? hookNames : [];
if (hooks.length === 0) return [];
return registry.extensions
.filter((ext) => ext && ext.enabled !== false)
.filter((ext) => extensionMatches(requires, ext.meta.provides, ext.meta.compatibleCapabilities))
.filter((ext) => hooks.some((hookName) => ext._successfulHooks?.has(hookName)))
.map((ext) => ext.name);
}
// ─── Strict mode (CI enforcement) ────────────────────────────────

@@ -1320,0 +1383,0 @@ // OPC_STRICT_EXTENSIONS=1 turns recorded extension hook failures into a

@@ -5,2 +5,5 @@ // flow-core-consistency.test.mjs — consistency, validators, routing

import assert from "node:assert/strict";
import { mkdirSync, mkdtempSync, rmSync, writeFileSync } from "node:fs";
import { homedir } from "node:os";
import { join } from "node:path";
import { validateHandshakeData, RULE_VALIDATORS, cmdRoute } from "./flow-core.mjs";

@@ -204,3 +207,3 @@ import { validHandshake } from "./flow-core.test-helpers.mjs";

describe("cmdRoute", () => {
let logOutput, errOutput, exitCode;
let logOutput, errOutput, exitCode, routeDir;
let origLog, origErr, origExit;

@@ -212,2 +215,5 @@

exitCode = null;
const base = join(homedir(), ".opc", "sessions");
mkdirSync(base, { recursive: true });
routeDir = mkdtempSync(join(base, "opc-route-empty-"));
origLog = console.log;

@@ -225,2 +231,3 @@ origErr = console.error;

process.exit = origExit;
rmSync(routeDir, { recursive: true, force: true });
});

@@ -239,3 +246,3 @@

test("node not in flow → valid=false", () => {
cmdRoute(["--node", "nonexistent", "--verdict", "PASS", "--flow", "review"]);
cmdRoute(["--node", "nonexistent", "--verdict", "PASS", "--flow", "review", "--dir", routeDir]);
const out = JSON.parse(logOutput[0]);

@@ -247,3 +254,3 @@ assert.equal(out.valid, false);

test("valid route → valid=true with next node", () => {
cmdRoute(["--node", "review", "--verdict", "PASS", "--flow", "review"]);
cmdRoute(["--node", "review", "--verdict", "PASS", "--flow", "review", "--dir", routeDir]);
const out = JSON.parse(logOutput[0]);

@@ -254,4 +261,19 @@ assert.equal(out.valid, true);

test("existing falsey non-object flow state fails closed", () => {
const base = join(homedir(), ".opc", "sessions");
mkdirSync(base, { recursive: true });
const dir = mkdtempSync(join(base, "opc-route-state-"));
try {
writeFileSync(join(dir, "flow-state.json"), "0");
cmdRoute(["--node", "review", "--verdict", "PASS", "--flow", "review", "--dir", dir]);
const out = JSON.parse(logOutput[0]);
assert.equal(out.valid, false);
assert.match(out.error, /flow-state\.json.*object/);
} finally {
rmSync(dir, { recursive: true, force: true });
}
});
test("no edge for verdict → valid=false", () => {
cmdRoute(["--node", "review", "--verdict", "BLOCKED", "--flow", "review"]);
cmdRoute(["--node", "review", "--verdict", "BLOCKED", "--flow", "review", "--dir", routeDir]);
const out = JSON.parse(logOutput[0]);

@@ -258,0 +280,0 @@ assert.equal(out.valid, false);

@@ -13,2 +13,3 @@ // Flow core commands: route, init, validate, validateHandshakeData, validate-context

symlinkSync,
lstatSync,
} from "fs";

@@ -22,3 +23,3 @@ import { join, dirname, resolve, basename } from "path";

import {
getFlag, resolveDir, atomicWriteSync, createSessionDir, getProjectRoot, getSessionsBaseDir,
getFlag, hasFlag, resolveDir, atomicWriteSync, createSessionDir, getProjectRoot, getSessionsBaseDir,
VALID_NODE_TYPES, VALID_STATUSES, VALID_VERDICTS, EVIDENCE_TYPES,

@@ -37,4 +38,11 @@ WRITER_SIG,

import { parseBypassArgs } from "./bypass-args.mjs";
import { readTaskFromAC, findLatestRunDir } from "./ext-commands.mjs";
import { collectTestResultReasons } from "./test-result-gate.mjs";
import {
collectPromptExtensionProvenanceErrors,
readTaskFromAC,
resolveNodeExtensionContext,
} from "./ext-commands.mjs";
import {
collectTestEvidenceProvenanceReasons,
collectTestResultReasons,
} from "./test-result-gate.mjs";
import { loadTestCommandSpec, testCommandHash } from "./test-command-execution.mjs";

@@ -45,5 +53,18 @@ import {

registryPath,
resolveCurrentRun,
writeSessionRegistry,
} from "./runaway-guard.mjs";
import { lockFile } from "./file-lock.mjs";
import { evaluateFlowBudget } from "./flow-budget.mjs";
import { parseRunOrdinal } from "./run-id.mjs";
import { stoppedFlowError } from "./flow-state-guard.mjs";
import {
expectedRunForNode,
isRunId,
isPlainObject,
readSessionAuthority,
resolveExactRunHandshake,
authoritativeEntries,
canonicalProjectionErrors,
} from "./flow-evidence.mjs";

@@ -70,6 +91,15 @@ // ─── route ──────────────────────────────────────────────────────

const statePath = join(stateDir, "flow-state.json");
try {
state = JSON.parse(readFileSync(statePath, "utf8"));
if (state._flow_file) loadFlowFromFile(state._flow_file);
} catch { /* no/corrupt state file — resolve from args alone */ }
if (existsSync(statePath)) {
try {
state = JSON.parse(readFileSync(statePath, "utf8"));
if (!state || typeof state !== "object" || Array.isArray(state)) {
console.log(JSON.stringify({ next: null, valid: false, error: "corrupt flow-state.json: expected an object" }));
return;
}
if (state._flow_file) loadFlowFromFile(state._flow_file);
} catch (error) {
console.log(JSON.stringify({ next: null, valid: false, error: `corrupt flow-state.json: ${error.message}` }));
return;
}
}
}

@@ -95,2 +125,11 @@

const next = nodeEdges[verdict];
if (state) {
const budget = evaluateFlowBudget({ state, template, from: node, to: next, verdict });
if (!budget.allowed) {
console.log(JSON.stringify({ next: null, valid: false, error: budget.reason }));
return;
}
}
// Read autoMode from the state loaded above

@@ -102,3 +141,3 @@ let autoReminder;

console.log(JSON.stringify({ next: nodeEdges[verdict], valid: true, ...(autoReminder ? { reminder: autoReminder } : {}) }));
console.log(JSON.stringify({ next, valid: true, ...(autoReminder ? { reminder: autoReminder } : {}) }));
}

@@ -136,2 +175,7 @@

}
const stopped = stoppedFlowError(state, "record-commit");
if (stopped) {
console.log(JSON.stringify({ recorded: false, error: stopped }));
return;
}

@@ -341,2 +385,3 @@ const root = (typeof state.projectRoot === "string" && state.projectRoot) ? state.projectRoot : getProjectRoot();

edgeCounts: {},
repairEdgeCounts: {},
projectRoot,

@@ -348,6 +393,6 @@ // Git floor at flow start + commits the flow produces. changeScope diffs

bypassMode: bypassRecord,
flowStartedAt,
autoMode: autoMode || undefined,
...(autoMode ? {
_claudeSessionId: claudeSessionId,
flowStartedAt,
autoRepairCounts: {},

@@ -458,19 +503,12 @@ } : {}),

preflightNode = firstBriefOrBuild || entryNode;
const preflightCaps = template.nodeCapabilities?.[preflightNode] || [];
const preflightTask = readTaskFromAC(dir);
const preflightCtx = resolveNodeExtensionContext(dir, preflightNode, args, {
role: "preflight",
task: preflightTask,
devServerUrl: process.env.DEV_SERVER_URL || "",
});
const preflightCaps = preflightCtx.nodeCapabilities;
if (preflightCaps.length > 0 && preflightTask.trim()) {
const preflightRegistry = await loadExtensions(bypassCfg);
const preflightCtx = {
node: preflightNode,
nodeId: preflightNode,
nodeType: template.nodeTypes?.[preflightNode] || null,
role: "preflight",
task: preflightTask,
taskDescription: preflightTask,
flowDir: resolve(dir),
cwd: process.cwd(),
devServerUrl: process.env.DEV_SERVER_URL || "",
nodeCapabilities: preflightCaps,
};
preflightResult = await fireNodePreflight(preflightRegistry, preflightCtx);

@@ -621,7 +659,7 @@ if (preflightResult?.length) preflightStatus = { node: preflightNode, status: "ok" };

// cannot pass validation on a loopback without listing what changed.
const runNum = parseInt(String(data.runId).replace(/^run_/, ""), 10);
if (Number.isFinite(runNum) && runNum > 1) {
const runOrdinal = parseRunOrdinal(data.runId);
if (runOrdinal !== null && runOrdinal > 1n) {
const deltaResult = runBriefLint(briefText, { tier: lintTier, hasPriorFindings: true });
if (deltaResult.failures.some(f => f.check === "iteration-delta")) {
errors.push("brief re-entered after gate loopback (run_" + runNum + ") but has no '## Iteration Delta' section — list specific changes from prior findings");
errors.push("brief re-entered after gate loopback (" + data.runId + ") but has no '## Iteration Delta' section — list specific changes from prior findings");
}

@@ -739,9 +777,28 @@ }

function selectedRunHandshakeForNodePath(direct) {
if (basename(direct) !== "handshake.json") return null;
const nodeDir = dirname(direct);
const harnessDir = dirname(dirname(nodeDir));
const statePath = join(harnessDir, "flow-state.json");
if (!existsSync(statePath)) return null;
let state;
try {
state = JSON.parse(readFileSync(statePath, "utf8"));
} catch {
return null;
}
if (state.currentNode !== basename(nodeDir)) return null;
const selected = resolveCurrentRun(state);
if (!selected) return null;
const selectedPath = join(nodeDir, selected.runId, "handshake.json");
return existsSync(selectedPath) ? selectedPath : null;
}
function resolveHandshakeForValidate(file) {
const direct = resolve(file);
if (existsSync(direct)) return direct;
if (basename(direct) !== "handshake.json") return direct;
const latestRun = findLatestRunDir(dirname(direct));
const fallback = latestRun ? join(latestRun, "handshake.json") : null;
return fallback && existsSync(fallback) ? fallback : direct;
if (!existsSync(direct)) {
const selected = selectedRunHandshakeForNodePath(direct);
if (selected) return selected;
}
return direct;
}

@@ -755,2 +812,11 @@

function handshakeBaseDir(file, data = null) {
const dir = dirname(resolve(file));
if (!/^run_\d+$/.test(basename(dir))) return dir;
const artifacts = Array.isArray(data?.artifacts) ? data.artifacts : [];
return artifacts.some((artifact) =>
typeof artifact?.path === "string" && !/^run_\d+\//.test(artifact.path)
) ? dir : dirname(dir);
}
function firstPositionalArg(args) {

@@ -783,2 +849,9 @@ for (let i = 0; i < args.length; i++) {

}
const currentRun = resolveCurrentRun(state);
if (currentRun) {
const exact = resolveExactRunHandshake(dir, state.currentNode, currentRun.runId);
if (exact.error) return { error: exact.error };
if (!exact.missing && exact.path && existsSync(exact.path)) return { file: exact.path };
return { error: `missing exact selected-run handshake for node '${state.currentNode}' run '${currentRun.runId}'` };
}
return {

@@ -789,2 +862,46 @@ file: resolveHandshakeForValidate(join(dir, "nodes", state.currentNode, "handshake.json")),

function sessionExactEvidenceErrors(dir, state) {
const errors = [];
for (const entry of authoritativeEntries(state, { includeCurrent: true })) {
const exact = resolveExactRunHandshake(dir, entry.nodeId, entry.runId);
if (exact.error) errors.push(exact.error);
else if (exact.missing || !existsSync(exact.path)) {
errors.push(`missing exact selected/history handshake for node '${entry.nodeId}' run '${entry.runId}'`);
}
}
return errors;
}
function nodeAndRunFromHandshakePath(file) {
const abs = resolve(file);
const parent = dirname(abs);
const maybeRun = basename(parent);
if (/^run_\d+$/.test(maybeRun)) {
return { nodeId: basename(dirname(parent)), pathRunId: maybeRun };
}
return { nodeId: basename(parent), pathRunId: null };
}
function stateBackedValidateIdentityErrors(file, data) {
if (!isPlainObject(data)) return [];
const harnessDir = harnessDirForHandshake(file);
const authority = readSessionAuthority(harnessDir);
if (!authority.exists) return [];
if (authority.error) return [authority.error];
const state = authority.state;
const { nodeId, pathRunId } = nodeAndRunFromHandshakePath(file);
const expectedRun = expectedRunForNode(state, nodeId);
const errors = sessionExactEvidenceErrors(harnessDir, state);
if (!expectedRun) {
errors.push(`no authoritative selected/history run for node '${nodeId}'`);
return errors;
}
if (pathRunId && pathRunId !== expectedRun) {
errors.push(`handshake path run is '${pathRunId}', expected '${expectedRun}'`);
}
if (data.nodeId !== nodeId) errors.push(`handshake nodeId is '${data.nodeId}', expected '${nodeId}'`);
if (data.runId !== expectedRun) errors.push(`handshake runId is '${data.runId}', expected '${expectedRun}'`);
return errors;
}
export function cmdValidate(args) {

@@ -814,7 +931,14 @@ const inputFile = firstPositionalArg(args);

let tier = null;
let authorityState = null;
let authorityTemplate = null;
try {
const harnessDir = harnessDirForHandshake(file);
const statePath = join(harnessDir, "flow-state.json");
if (existsSync(statePath)) {
const state = JSON.parse(readFileSync(statePath, "utf8"));
const authority = readSessionAuthority(harnessDir);
if (authority.exists && authority.error) {
console.log(JSON.stringify({ valid: false, errors: [authority.error] }));
return;
}
if (authority.exists) {
const state = authority.state;
authorityState = state;
// Auto-restore flow template from _flow_file if needed

@@ -826,2 +950,3 @@ if (state._flow_file) {

const tmpl = FLOW_TEMPLATES[state.flowTemplate];
authorityTemplate = tmpl || null;
if (tmpl && tmpl.softEvidence) soft = true;

@@ -831,3 +956,6 @@ }

}
} catch { /* flow-state.json unreadable — treat as strict */ }
} catch (error) {
console.log(JSON.stringify({ valid: false, errors: [`state-backed validation failed: ${error.message}`] }));
return;
}

@@ -837,5 +965,20 @@ const { errors, warnings } = validateHandshakeData(data, {

softEvidence: soft,
baseDir: dirname(file),
baseDir: handshakeBaseDir(file, data),
tier,
});
if (data?.testEvidenceProvenance != null) {
errors.push(...collectTestEvidenceProvenanceReasons(data));
}
errors.push(...stateBackedValidateIdentityErrors(file, data));
if (authorityState) {
const harnessDir = harnessDirForHandshake(file);
errors.push(...canonicalProjectionErrors(harnessDir, authorityState, (canonical, path, nodeId, runId) =>
canonicalHandshakeErrors(canonical, nodeId, runId, {
nodeType: authorityTemplate?.nodeTypes?.[nodeId],
softEvidence: soft,
baseDir: handshakeBaseDir(path, canonical),
tier,
})
));
}

@@ -855,3 +998,5 @@ for (const w of warnings) {

if (!sourceNode) return {};
const spec = loadTestCommandSpec(dir, sourceNode);
const sourceRunId = handshake?.testEvidenceProvenance?.sourceRunId;
if (!/^run_\d+$/.test(sourceRunId || "")) return {};
const spec = loadTestCommandSpec(dir, sourceNode, sourceRunId);
if (!spec) return {};

@@ -865,3 +1010,3 @@ return {

function collectFilesRecursive(root, prefix = "") {
export function collectFilesRecursive(root, prefix = "") {
const out = [];

@@ -882,5 +1027,6 @@ let entries = [];

function classifyArtifact(relPath, nodeType) {
export function classifyArtifact(relPath, nodeType) {
const name = basename(relPath);
const lower = name.toLowerCase();
if (lower === "handshake.json" || lower === "flow-state.json") return null;
if (lower === "build-brief.md") return "brief";

@@ -895,2 +1041,4 @@ if (lower === "test-plan.md") return "test-plan";

if (/^(.*-)?lint-result\.json$/i.test(name) || /^(.*-)?report\.json$/i.test(name) || /^(.*-)?result\.json$/i.test(name)) return "report";
// Every non-reserved JSON file inside run_N is a machine-readable report artifact.
if (lower.endsWith(".json")) return "report";
if (/\.(ts|tsx|js|jsx|css|html|mjs|cjs)$/i.test(name)) return "source";

@@ -901,2 +1049,14 @@ if (lower.endsWith(".md") || lower.endsWith(".txt")) return "source";

export function scanNodeArtifacts(nodeDir, runDir, nodeType) {
const runId = basename(runDir);
const files = collectFilesRecursive(runDir).map((file) => `${runId}/${file}`);
for (const nodeLevel of ["build-brief.md", "test-plan.md", "test-execution.json"]) {
if (existsSync(join(nodeDir, nodeLevel))) files.push(nodeLevel);
}
return files
.sort()
.map((path) => ({ type: classifyArtifact(path, nodeType), path }))
.filter((artifact) => artifact.type);
}
function normalizeEvalVerdict(raw) {

@@ -933,5 +1093,32 @@ const text = String(raw || "").toUpperCase();

function readJsonFile(path) {
try { return JSON.parse(readFileSync(path, "utf8")); } catch { return null; }
if (!existsSync(path)) return null;
return JSON.parse(readFileSync(path, "utf8"));
}
function canonicalHandshakeErrors(data, nodeId, runId, options = {}) {
if (!isPlainObject(data)) return ["canonical handshake.json root must be a non-null object"];
const errors = [];
const validation = validateHandshakeData(data, {
checkEvidence: true,
softEvidence: !!options.softEvidence,
baseDir: options.baseDir,
tier: options.tier,
});
errors.push(...validation.errors.map((error) => `canonical handshake.json: ${error}`));
if (data.nodeId !== nodeId) errors.push(`canonical handshake.json nodeId is '${data.nodeId}', expected '${nodeId}'`);
if (data.runId !== runId) errors.push(`canonical handshake.json runId is '${data.runId}', expected '${runId}'`);
if (options.nodeType && data.nodeType !== options.nodeType) {
errors.push(`canonical handshake.json nodeType is '${data.nodeType}', expected '${options.nodeType}'`);
}
if (typeof data.status !== "string" || data.status.length === 0) {
errors.push("canonical handshake.json status missing or invalid");
}
if (!Array.isArray(data.artifacts)) errors.push("canonical handshake.json artifacts must be an array");
if (data.testEvidenceProvenance != null) {
errors.push(...collectTestEvidenceProvenanceReasons(data)
.map((error) => `canonical handshake.json: ${error}`));
}
return errors;
}
function preserveHarnessTestEvidence(target, existing) {

@@ -952,5 +1139,54 @@ const prov = existing?.testEvidenceProvenance;

const NODE_LEVEL_SEAL_ARTIFACTS = new Set([
"build-brief.md",
"test-plan.md",
"test-execution.json",
]);
function exactArtifactPathFromCanonical(path, runId) {
if (typeof path !== "string") return path;
if (path.startsWith(`${runId}/`)) return path.slice(runId.length + 1);
if (NODE_LEVEL_SEAL_ARTIFACTS.has(path)) return `../${path}`;
return path;
}
function exactHandshakeFromCanonical(handshake, runId) {
const exact = JSON.parse(JSON.stringify(handshake));
if (Array.isArray(exact.artifacts)) {
exact.artifacts = exact.artifacts.map((artifact) => ({
...artifact,
path: exactArtifactPathFromCanonical(artifact.path, runId),
}));
}
return exact;
}
function isPriorRun(priorRunId, currentRunId) {
const prior = parseRunOrdinal(priorRunId);
const current = parseRunOrdinal(currentRunId);
return prior !== null && current !== null && prior < current;
}
function staleCanonicalAuthorityErrors(dir, state, template, nodeId, runId) {
const authorized = Array.isArray(state.history) &&
state.history.some((entry) => (entry?.nodeId || entry?.node) === nodeId && (entry?.runId || entry?.run) === runId);
if (!authorized) return [`stale canonical ${nodeId}/${runId} is not recorded in authoritative history`];
const exact = resolveExactRunHandshake(dir, nodeId, runId);
if (exact.error) return [`stale canonical ${nodeId}/${runId} exact ${exact.error}`];
if (exact.missing || !existsSync(exact.path || "")) {
return [`stale canonical ${nodeId}/${runId} exact handshake missing at ${exact.path}`];
}
return canonicalProjectionErrors(dir, state, (canonical, path, projectionNodeId, projectionRunId) =>
canonicalHandshakeErrors(canonical, projectionNodeId, projectionRunId, {
nodeType: template.nodeTypes?.[projectionNodeId],
softEvidence: !!template.softEvidence,
baseDir: handshakeBaseDir(path, canonical),
tier: state.tier && VALID_TIERS.has(state.tier) ? state.tier : null,
}), { entries: [{ nodeId, runId }] });
}
export function cmdSeal(args) {
const nodeId = getFlag(args, "node");
const runOverride = getFlag(args, "run");
const runOverrideProvided = hasFlag(args, "run");
const runOverride = getFlag(args, "run", "");
const dir = resolveDir(args);

@@ -977,10 +1213,38 @@

}
const stopped = stoppedFlowError(state, "seal");
if (stopped) {
console.log(JSON.stringify({ sealed: false, error: stopped }));
return;
}
if (state.currentNode !== nodeId) {
console.log(JSON.stringify({
sealed: false,
error: `cannot seal node '${nodeId}': current node is '${state.currentNode}'`,
}));
return;
}
const selectedRun = resolveCurrentRun(state);
if (!selectedRun) {
console.log(JSON.stringify({ sealed: false, error: `cannot resolve selected run for '${nodeId}'` }));
return;
}
if (runOverrideProvided && !/^[1-9]\d*$/.test(runOverride)) {
console.log(JSON.stringify({ sealed: false, error: "--run must be a positive numeric ordinal" }));
return;
}
if (runOverrideProvided && `run_${runOverride}` !== selectedRun.runId) {
console.log(JSON.stringify({
sealed: false,
error: `--run ${runOverride} does not match selected run '${selectedRun.runId}'`,
}));
return;
}
// Resolve template for nodeType lookup
if (state._flow_file) loadFlowFromFile(state._flow_file);
const template = FLOW_TEMPLATES[state.flowTemplate];
if (!template) {
console.log(JSON.stringify({ sealed: false, error: `unknown flow template: ${state.flowTemplate}` }));
const resolved = resolveFlowTemplate(args, state);
if (resolved.error) {
console.log(JSON.stringify({ sealed: false, error: resolved.error }));
return;
}
const { template } = resolved;

@@ -996,22 +1260,5 @@ const nodeType = template.nodeTypes?.[nodeId] || (nodeId.startsWith("gate") ? "gate" : "build");

let runDir;
if (runOverride) {
runDir = join(nodeDir, `run_${runOverride}`);
} else {
// Find latest run_N
const runs = readdirSync(nodeDir, { withFileTypes: true })
.filter(e => e.isDirectory() && /^run_\d+$/.test(e.name))
.sort((a, b) => {
const na = parseInt(a.name.split("_")[1]);
const nb = parseInt(b.name.split("_")[1]);
return nb - na;
});
if (runs.length === 0) {
console.log(JSON.stringify({ sealed: false, error: `no run_N directories found in nodes/${nodeId}` }));
return;
}
runDir = join(nodeDir, runs[0].name);
}
const runDir = join(nodeDir, selectedRun.runId);
if (!existsSync(runDir)) {
if (!existsSync(runDir) || !lstatSync(runDir).isDirectory()) {
console.log(JSON.stringify({ sealed: false, error: `run dir not found: ${runDir}` }));

@@ -1023,18 +1270,78 @@ return;

const handshakePath = join(nodeDir, "handshake.json");
const existingHandshake = readJsonFile(handshakePath);
let existingHandshake;
const canonicalExists = existsSync(handshakePath);
try {
existingHandshake = canonicalExists ? readJsonFile(handshakePath) : null;
} catch (error) {
console.log(JSON.stringify({
sealed: false,
handshakePath,
artifacts: 0,
verdict: null,
validationErrors: [`canonical handshake.json parse error — fail-closed: ${error.message}`],
warnings: [],
}));
return;
}
const staleCanonical = canonicalExists && isPlainObject(existingHandshake) &&
isRunId(existingHandshake.runId) && isPriorRun(existingHandshake.runId, runId);
if (staleCanonical) {
const canonicalErrors = staleCanonicalAuthorityErrors(dir, state, template, nodeId, existingHandshake.runId);
if (canonicalErrors.length > 0) {
console.log(JSON.stringify({
sealed: false,
handshakePath,
artifacts: 0,
verdict: null,
validationErrors: canonicalErrors,
warnings: [],
}));
return;
}
}
if (canonicalExists && !staleCanonical) {
const canonicalErrors = canonicalHandshakeErrors(existingHandshake, nodeId, runId, {
nodeType,
softEvidence: !!template.softEvidence,
baseDir: handshakeBaseDir(handshakePath, existingHandshake),
tier: state.tier && VALID_TIERS.has(state.tier) ? state.tier : null,
});
canonicalErrors.push(...canonicalProjectionErrors(dir, state, (canonical, path, projectionNodeId, projectionRunId) =>
canonicalHandshakeErrors(canonical, projectionNodeId, projectionRunId, {
nodeType: template.nodeTypes?.[projectionNodeId],
softEvidence: !!template.softEvidence,
baseDir: handshakeBaseDir(path, canonical),
tier: state.tier && VALID_TIERS.has(state.tier) ? state.tier : null,
}), { entries: [{ nodeId, runId }] }));
if (canonicalErrors.length > 0) {
console.log(JSON.stringify({
sealed: false,
handshakePath,
artifacts: 0,
verdict: null,
validationErrors: canonicalErrors,
warnings: [],
}));
return;
}
}
const selectedRunHandshakePath = join(runDir, "handshake.json");
let selectedRunHandshake = null;
let selectedRunHandshakeError = null;
if (existsSync(selectedRunHandshakePath)) {
try {
selectedRunHandshake = JSON.parse(readFileSync(selectedRunHandshakePath, "utf8"));
if (!selectedRunHandshake || typeof selectedRunHandshake !== "object" || Array.isArray(selectedRunHandshake)) {
selectedRunHandshakeError = `${runId}/handshake.json schema error: root must be an object`;
selectedRunHandshake = null;
}
} catch (error) {
selectedRunHandshakeError = `${runId}/handshake.json parse error: ${error.message}`;
}
}
// Scan files and classify artifacts
const files = collectFilesRecursive(runDir).map(f => `${runId}/${f}`);
for (const nodeLevel of ["build-brief.md", "test-plan.md", "test-execution.json"]) {
if (existsSync(join(nodeDir, nodeLevel))) files.push(nodeLevel);
}
const artifacts = [];
const artifacts = scanNodeArtifacts(nodeDir, runDir, nodeType);
const warnings = [];
for (const f of files.sort()) {
const type = classifyArtifact(f, nodeType);
if (!type) continue;
artifacts.push({ type, path: f });
}
// Infer verdict from eval files

@@ -1063,4 +1370,14 @@ const evalFiles = artifacts.filter(a => a.type === "eval");

if (nodeType === "execute") preserveHarnessTestEvidence(handshake, existingHandshake);
const selectedParticipants = selectedRunHandshake?.extensionsApplied;
if (
Array.isArray(selectedParticipants) &&
selectedParticipants.every((name) => typeof name === "string" && name.length > 0)
) {
handshake.extensionsApplied = selectedParticipants.slice();
}
if (nodeType === "execute" && existingHandshake?.runId === runId) {
preserveHarnessTestEvidence(handshake, existingHandshake);
}
const { critical, warning, suggestion } = inferred.findings;

@@ -1071,6 +1388,4 @@ if (critical + warning + suggestion > 0) {

// Write handshake
atomicWriteSync(handshakePath, JSON.stringify(handshake, null, 2) + "\n");
// Validate
// Validate before sealing. Invalid machine-readable evidence must not rewrite
// the canonical handshake.
const { errors } = validateHandshakeData(handshake, {

@@ -1080,28 +1395,77 @@ checkEvidence: nodeType === "execute",

});
const exactHandshake = exactHandshakeFromCanonical(handshake, runId);
const exactValidation = validateHandshakeData(exactHandshake, {
checkEvidence: nodeType === "execute",
baseDir: runDir,
});
errors.push(...exactValidation.errors.map((error) => `selected run handshake: ${error}`));
if (selectedRunHandshakeError) errors.push(selectedRunHandshakeError);
if (
(nodeType === "brief" || nodeType === "build") &&
Array.isArray(handshake.extensionsApplied) &&
handshake.extensionsApplied.length > 0
) {
errors.push(...collectPromptExtensionProvenanceErrors(runDir, {
nodeId,
runId,
extensionsApplied: handshake.extensionsApplied,
}));
}
const parsedJsonArtifacts = new Map();
for (const art of artifacts) {
if (!/\.json$/i.test(art.path)) continue;
try {
const text = readFileSync(join(nodeDir, art.path), "utf8");
parsedJsonArtifacts.set(art.path, { text, data: JSON.parse(text) });
} catch (error) {
errors.push(`artifact ${art.path} is not valid JSON — fail-closed: ${error.message}`);
}
}
if (nodeType === "execute") {
const evidenceContext = testEvidenceContext(dir, handshake);
const testResultArtifacts = artifacts.filter((art) =>
art.type === "test-result" && /\.json$/i.test(art.path || ""));
if (handshake.testEvidenceProvenance && testResultArtifacts.length === 0) {
errors.push("testEvidenceProvenance requires a test-result JSON artifact");
}
for (const art of artifacts) {
if (art.type !== "test-result" || !/\.json$/i.test(art.path)) continue;
try {
const text = readFileSync(join(nodeDir, art.path), "utf8");
const data = JSON.parse(text);
errors.push(...collectTestResultReasons(data, {
handshake,
nodeId,
runId: handshake.runId,
artifact: art,
artifactHash: createHash("sha256").update(text).digest("hex"),
sessionDir: dir,
...evidenceContext,
}));
} catch {
errors.push(`artifact ${art.path} unreadable — fail-closed`);
}
const parsed = parsedJsonArtifacts.get(art.path);
if (!parsed) continue;
errors.push(...collectTestEvidenceProvenanceReasons(handshake, {
sessionDir: dir,
artifact: art,
artifactHash: createHash("sha256").update(parsed.text).digest("hex"),
}));
errors.push(...collectTestResultReasons(parsed.data, {
handshake,
nodeId,
runId: handshake.runId,
artifact: art,
artifactHash: createHash("sha256").update(parsed.text).digest("hex"),
sessionDir: dir,
...evidenceContext,
}));
}
}
if (errors.length > 0) {
for (const w of warnings) console.error(`⚠️ ${w}`);
console.log(JSON.stringify({
sealed: false,
handshakePath,
artifacts: artifacts.length,
verdict,
validationErrors: errors,
warnings,
}));
return;
}
atomicWriteSync(selectedRunHandshakePath, JSON.stringify(exactHandshake, null, 2) + "\n");
atomicWriteSync(handshakePath, JSON.stringify(handshake, null, 2) + "\n");
for (const w of warnings) console.error(`⚠️ ${w}`);
console.log(JSON.stringify({
sealed: true,
sealed: errors.length === 0,
handshakePath,

@@ -1108,0 +1472,0 @@ artifacts: artifacts.length,

@@ -9,4 +9,4 @@ // Flow escape hatches + listing: skip, pass, stop, goto, ls

import { fileURLToPath } from "url";
import { FLOW_TEMPLATES, loadFlowFromFile } from "./flow-templates.mjs";
import { cmdTransition } from "./flow-transition.mjs";
import { resolveFlowTemplate, loadFlowFromFile } from "./flow-templates.mjs";
import { allocateNextRunId, cmdTransition, reserveRunDirectory } from "./flow-transition.mjs";
import {

@@ -18,2 +18,6 @@ getFlag, resolveDir, atomicWriteSync, getSessionsBaseDir,

import { resolveCallerIdentity, checkOwnership } from "./driver-owner.mjs";
import { evaluateFlowBudget, nodeHasBudgetedExit } from "./flow-budget.mjs";
import { resolveCurrentRun } from "./runaway-guard.mjs";
import { stoppedFlowError } from "./flow-state-guard.mjs";
import { resolveExactRunHandshake } from "./flow-evidence.mjs";

@@ -43,3 +47,2 @@ // ── Shared state loader ──

const dir = resolveDir(args);
const flow = getFlag(args, "flow");
const statePath = join(dir, "flow-state.json");

@@ -57,5 +60,7 @@

const { state, statePath: sp } = loaded;
const templateName = flow || state.flowTemplate;
const template = Object.hasOwn(FLOW_TEMPLATES, templateName) ? FLOW_TEMPLATES[templateName] : null;
if (!template) { console.log(JSON.stringify({ error: `unknown flow: ${templateName}` })); return; }
const stopped = stoppedFlowError(state, "skip");
if (stopped) { console.log(JSON.stringify({ error: stopped })); return; }
const resolved = resolveFlowTemplate(args, state);
if (resolved.error) { console.log(JSON.stringify({ error: resolved.error })); return; }
const { template } = resolved;

@@ -74,23 +79,15 @@ const current = state.currentNode;

// ── Cycle limit checks (mirror cmdTransition) ──
const limits = {
maxTotalSteps: state.maxTotalSteps ?? template.limits.maxTotalSteps,
maxLoopsPerEdge: state.maxLoopsPerEdge ?? template.limits.maxLoopsPerEdge,
maxNodeReentry: state.maxNodeReentry ?? template.limits.maxNodeReentry,
};
if (state.totalSteps >= limits.maxTotalSteps) {
console.log(JSON.stringify({ error: `maxTotalSteps (${limits.maxTotalSteps}) reached — cannot skip` }));
const sourceRun = resolveCurrentRun(state);
if (!sourceRun) {
console.log(JSON.stringify({ error: `cannot resolve current run for '${current}'` }));
return;
}
// ── Cycle limit checks (mirror cmdTransition) ──
const edgeKey = `${current}\u2192${next}`;
const edgeCount = state.edgeCounts[edgeKey] || 0;
if (edgeCount >= limits.maxLoopsPerEdge) {
console.log(JSON.stringify({ error: `maxLoopsPerEdge (${limits.maxLoopsPerEdge}) reached for '${edgeKey}' — cannot skip` }));
const budget = evaluateFlowBudget({ state, template, from: current, to: next, verdict: "PASS" });
if (!budget.allowed) {
console.log(JSON.stringify({ error: `${budget.reason} — cannot skip` }));
return;
}
const nodeEntries = state.history.filter(h => h.nodeId === next).length;
if (nodeEntries >= limits.maxNodeReentry) {
console.log(JSON.stringify({ error: `maxNodeReentry (${limits.maxNodeReentry}) reached for '${next}' — cannot skip` }));
return;
}
// ── maxSkips: prevent skipping through entire flow ──

@@ -104,2 +101,16 @@ const maxSkips = template.limits.maxSkips ?? 2;

let targetRunEntries = [];
try {
targetRunEntries = readdirSync(join(dir, "nodes", next), { withFileTypes: true });
} catch { /* target node has no run directory yet */ }
const runId = allocateNextRunId(state.history, targetRunEntries, next);
try {
reserveRunDirectory(dir, next, runId);
} catch (error) {
console.log(JSON.stringify({
error: `cannot reserve run directory 'nodes/${next}/${runId}': ${error.message}`,
}));
return;
}
// Write a skip handshake so pre-transition won't block

@@ -111,3 +122,3 @@ const nodeDir = join(dir, "nodes", current);

nodeType: template.nodeTypes?.[current] || "execute",
runId: `run_${(state.history.filter(h => h.nodeId === current).length || 0) + 1}`,
runId: sourceRun.runId,
status: "completed",

@@ -122,7 +133,6 @@ verdict: null,

const runId = `run_${state.history.filter(h => h.nodeId === next).length + 1}`;
state.history.push({ nodeId: next, runId, timestamp: new Date().toISOString(), skipped: true });
state.currentNode = next;
state.totalSteps++;
state.edgeCounts[edgeKey] = (state.edgeCounts[edgeKey] || 0) + 1;
state.edgeCounts[edgeKey] = budget.edgeCount + 1;
state._written_by = WRITER_SIG;

@@ -132,3 +142,2 @@ state._last_modified = new Date().toISOString();

atomicWriteSync(sp, JSON.stringify(state, null, 2) + "\n");
mkdirSync(join(dir, "nodes", next, runId), { recursive: true });

@@ -149,5 +158,7 @@ console.log(JSON.stringify({ skipped: current, next, runId }));

const { state } = loaded;
const templateName = state.flowTemplate;
const template = Object.hasOwn(FLOW_TEMPLATES, templateName) ? FLOW_TEMPLATES[templateName] : null;
if (!template) { console.log(JSON.stringify({ error: `unknown flow: ${templateName}` })); return; }
const stopped = stoppedFlowError(state, "pass");
if (stopped) { console.log(JSON.stringify({ error: stopped })); return; }
const resolved = resolveFlowTemplate(args, state);
if (resolved.error) { console.log(JSON.stringify({ error: resolved.error })); return; }
const { template, name: templateName } = resolved;

@@ -180,9 +191,33 @@ const current = state.currentNode;

if (upstreamId) {
const upstreamHandshakePath = join(dir, "nodes", upstreamId, "handshake.json");
if (existsSync(upstreamHandshakePath)) {
const upstreamEntries = (state.history || []).filter(h => h?.nodeId === upstreamId);
const upstreamEntry = upstreamEntries.length > 0
? upstreamEntries[upstreamEntries.length - 1]
: (upstreamId === state.entryNode ? { nodeId: upstreamId, runId: "run_1", legacyInitial: true } : null);
const runMatch = /^run_(\d+)$/.exec(upstreamEntry?.runId || "");
if (!runMatch) {
console.log(JSON.stringify({
error: `Cannot force-pass: upstream history for '${upstreamId}' has missing or invalid runId.`,
allowed: false,
}));
return;
}
const upstreamExact = resolveExactRunHandshake(dir, upstreamId, upstreamEntry.runId);
if (upstreamExact.error) {
console.log(JSON.stringify({ error: `Cannot force-pass: ${upstreamExact.error}`, allowed: false }));
return;
}
if (upstreamExact.missing || !upstreamExact.path || !existsSync(upstreamExact.path)) {
console.log(JSON.stringify({
error: `Cannot force-pass: upstream evidence missing for '${upstreamId}' ${upstreamEntry.runId}.`,
allowed: false,
}));
return;
}
if (existsSync(upstreamExact.path)) {
try {
const harnessPath = join(dirname(fileURLToPath(import.meta.url)), "..", "opc-harness.mjs");
const synthArgs = [harnessPath, "synthesize", "--node", upstreamId, "--dir", dir, "--no-strict", "--run", runMatch[1]];
const synthOutput = execFileSync(
"node",
[harnessPath, "synthesize", "--node", upstreamId, "--dir", dir, "--no-strict"],
synthArgs,
{ encoding: "utf8", stdio: ["pipe", "pipe", "pipe"] }

@@ -250,2 +285,11 @@ );

}
if (state.status === "stopped") {
console.log(JSON.stringify({
stopped: true,
alreadyStopped: true,
currentNode: state.currentNode,
totalSteps: state.totalSteps,
}));
return;
}

@@ -296,38 +340,58 @@ state.status = "stopped";

const { state, statePath: sp } = loaded;
const template = Object.hasOwn(FLOW_TEMPLATES, state.flowTemplate) ? FLOW_TEMPLATES[state.flowTemplate] : null;
if (!template) { console.log(JSON.stringify({ error: `unknown flow: ${state.flowTemplate}` })); return; }
const stopped = stoppedFlowError(state, "goto");
if (stopped) { console.log(JSON.stringify({ error: stopped })); return; }
const resolved = resolveFlowTemplate(args, state);
if (resolved.error) { console.log(JSON.stringify({ error: resolved.error })); return; }
const { template, name: flowName } = resolved;
if (!template.nodes.includes(targetNode)) {
console.log(JSON.stringify({ error: `'${targetNode}' is not a node in flow '${state.flowTemplate}'` }));
console.log(JSON.stringify({ error: `'${targetNode}' is not a node in flow '${flowName}'` }));
return;
}
// Check node reentry limit
const limits = {
maxNodeReentry: state.maxNodeReentry ?? template.limits.maxNodeReentry,
maxTotalSteps: state.maxTotalSteps ?? template.limits.maxTotalSteps,
maxLoopsPerEdge: state.maxLoopsPerEdge ?? template.limits.maxLoopsPerEdge,
};
if (state.totalSteps >= limits.maxTotalSteps) {
console.log(JSON.stringify({ error: `maxTotalSteps (${limits.maxTotalSteps}) reached — cannot goto` }));
const edgeKey = `${state.currentNode}→${targetNode}`;
const gotoBudget = evaluateFlowBudget({
state,
template,
from: state.currentNode,
to: targetNode,
verdict: "GOTO",
});
if (!gotoBudget.allowed) {
console.log(JSON.stringify({ error: `${gotoBudget.reason} — cannot goto` }));
return;
}
const edgeKey = `${state.currentNode}→${targetNode}`;
const edgeCount = state.edgeCounts?.[edgeKey] || 0;
if (edgeCount >= limits.maxLoopsPerEdge) {
console.log(JSON.stringify({ error: `maxLoopsPerEdge (${limits.maxLoopsPerEdge}) reached for '${edgeKey}' — cannot goto` }));
const projectedState = {
...state,
totalSteps: state.totalSteps + 1,
history: [...state.history, { nodeId: targetNode }],
};
const exit = nodeHasBudgetedExit({ state: projectedState, template, node: targetNode });
if (!exit.available) {
console.log(JSON.stringify({
error: `goto target '${targetNode}' has no budgeted exit`,
reasons: exit.reasons,
}));
return;
}
const nodeEntries = state.history.filter(h => h.nodeId === targetNode).length;
if (nodeEntries >= limits.maxNodeReentry) {
console.log(JSON.stringify({ error: `maxNodeReentry (${limits.maxNodeReentry}) reached for '${targetNode}'` }));
let targetRunEntries = [];
try {
targetRunEntries = readdirSync(join(dir, "nodes", targetNode), { withFileTypes: true });
} catch { /* target node has no run directory yet */ }
const runId = allocateNextRunId(state.history, targetRunEntries, targetNode);
try {
reserveRunDirectory(dir, targetNode, runId);
} catch (error) {
console.log(JSON.stringify({
error: `cannot reserve run directory 'nodes/${targetNode}/${runId}': ${error.message}`,
}));
return;
}
const runId = `run_${nodeEntries + 1}`;
state.history.push({ nodeId: targetNode, runId, timestamp: new Date().toISOString(), goto: true });
state.currentNode = targetNode;
state.totalSteps++;
if (!state.edgeCounts) state.edgeCounts = {};
state.edgeCounts[edgeKey] = (state.edgeCounts[edgeKey] || 0) + 1;
state.edgeCounts[edgeKey] = gotoBudget.edgeCount + 1;
state._written_by = WRITER_SIG;

@@ -337,3 +401,2 @@ state._last_modified = new Date().toISOString();

atomicWriteSync(sp, JSON.stringify(state, null, 2) + "\n");
mkdirSync(join(dir, "nodes", targetNode, runId), { recursive: true });

@@ -340,0 +403,0 @@ console.log(JSON.stringify({ goto: targetNode, runId, totalSteps: state.totalSteps }));

@@ -492,3 +492,44 @@ // Flow graph definitions — nodes, edges, limits per template

// Priority 1: explicit --flow-file
if (state && typeof state === "object") {
const persistedName = typeof state.flowTemplate === "string" && state.flowTemplate.trim()
? state.flowTemplate
: null;
const persistedFile = typeof state._flow_file === "string" && state._flow_file.trim()
? resolve(state._flow_file)
: null;
if (flowName && flowName !== persistedName) {
return {
error: `persisted flow identity '${persistedName ?? "<missing>"}' does not match explicit --flow '${flowName}'`,
};
}
if (flowFile) {
const explicitFile = resolve(flowFile);
if (!persistedFile || explicitFile !== persistedFile) {
return {
error: `persisted flow identity '${persistedFile ?? persistedName ?? "<missing>"}' does not match explicit --flow-file '${explicitFile}'`,
};
}
}
if (persistedFile) {
if (!persistedName) {
return { error: `persisted flow identity is missing flowTemplate for '${persistedFile}'` };
}
const result = loadFlowFromFile(persistedFile);
if (result.error) return { error: `persisted flow identity '${persistedFile}' is unavailable: ${result.error}` };
if (result.name !== persistedName) {
return {
error: `persisted flow identity mismatch: flowTemplate '${persistedName}' does not match _flow_file '${result.name}'`,
};
}
return { template: result.template, name: result.name };
}
if (!persistedName) return { error: "persisted flow identity is missing flowTemplate" };
const template = Object.hasOwn(FLOW_TEMPLATES, persistedName) ? FLOW_TEMPLATES[persistedName] : null;
if (!template) return { error: `unknown flow template: ${persistedName}` };
return { template, name: persistedName };
}
if (flowFile) {

@@ -500,19 +541,6 @@ const result = loadFlowFromFile(flowFile);

// Priority 2: _flow_file from persisted state
if (state && state._flow_file) {
const result = loadFlowFromFile(state._flow_file);
if (result.error) {
// File disappeared — fall through to name lookup
console.error(`⚠️ _flow_file '${state._flow_file}' failed: ${result.error} — falling back to template name`);
} else {
return { template: result.template, name: result.name };
}
}
// Priority 3: lookup by name — explicit --flow, else persisted state.flowTemplate
const resolvedName = flowName || (state && state.flowTemplate);
if (!resolvedName) return { error: "no --flow or --flow-file specified" };
const template = Object.hasOwn(FLOW_TEMPLATES, resolvedName) ? FLOW_TEMPLATES[resolvedName] : null;
if (!template) return { error: `unknown flow template: ${resolvedName}` };
return { template, name: resolvedName };
if (!flowName) return { error: "no --flow or --flow-file specified" };
const template = Object.hasOwn(FLOW_TEMPLATES, flowName) ? FLOW_TEMPLATES[flowName] : null;
if (!template) return { error: `unknown flow template: ${flowName}` };
return { template, name: flowName };
}

@@ -63,3 +63,3 @@ import { test, describe } from "node:test";

if (testPlan !== null) writeFileSync(join(runDir, "test-plan.md"), testPlan);
writeFileSync(join(nodeDir, "handshake.json"), JSON.stringify({
const canonical = {
nodeId: "test-design",

@@ -72,2 +72,3 @@ nodeType: "review",

timestamp: new Date().toISOString(),
testCommand: "printf ok",
artifacts: [

@@ -77,3 +78,12 @@ { type: "eval", path: "run_1/eval-skeptic-owner.md" },

],
}));
};
const runScoped = {
...canonical,
artifacts: canonical.artifacts.map((artifact) => ({
...artifact,
path: artifact.path.replace(/^run_1\//, ""),
})),
};
writeFileSync(join(nodeDir, "handshake.json"), JSON.stringify(canonical));
writeFileSync(join(runDir, "handshake.json"), JSON.stringify(runScoped));
writeFileSync(join(dir, "flow-state.json"), JSON.stringify({

@@ -80,0 +90,0 @@ version: "1.0",

@@ -61,2 +61,12 @@ import {

if (state.autoMode !== true && state.totalSteps === 0 && state.history.length === 0 &&
nonEmptyString(state.entryNode) && state.currentNode === state.entryNode &&
state.flowStartedAt == null) {
return {
runId: "run_1",
startedAt: null,
runKey: `legacy-initial:${state.entryNode}`,
};
}
return null;

@@ -63,0 +73,0 @@ }

@@ -73,2 +73,16 @@ import { describe, test, after } from "node:test";

test("resolves legacy non-auto initial entry without flowStartedAt", () => {
const legacy = {
entryNode: "build",
currentNode: "build",
totalSteps: 0,
history: [],
};
assert.deepEqual(resolveCurrentRun(legacy), {
runId: "run_1",
startedAt: null,
runKey: "legacy-initial:build",
});
});
test("fails closed for unverifiable state", () => {

@@ -88,3 +102,3 @@ const valid = {

{ ...valid, currentNode: "review" },
{ ...valid, flowStartedAt: null },
{ ...valid, flowStartedAt: null, autoMode: true },
{ ...valid, flowStartedAt: "not-a-date" },

@@ -91,0 +105,0 @@ { ...valid, flowStartedAt: "2026-02-30T00:00:00.000Z" },

@@ -207,2 +207,3 @@ import { after, describe, test } from "node:test";

assert.equal(state.autoRepairCounts, undefined);
assert.equal(new Date(state.flowStartedAt).toISOString(), state.flowStartedAt);
assert.equal(readSessionRegistry("unused", fixture.home), null);

@@ -832,2 +833,60 @@ });

if (recovery.name === "pass") {
const runDir = join(dir, "nodes", "acceptance", "run_1");
mkdirSync(runDir, { recursive: true });
const evalLines = [
"# Skeptic Owner Review",
"Role: skeptic-owner",
"",
"## Scope",
...Array.from({ length: 18 }, (_, i) => `Acceptance scope check ${i + 1} verified the recovery fixture without introducing a finding.`),
"",
"## Evidence",
...Array.from({ length: 18 }, (_, i) => `Acceptance evidence ${i + 1}: state, registry, and stopped-run markers remain inspectable after recovery.`),
"",
"## Decision",
...Array.from({ length: 18 }, (_, i) => `Acceptance decision ${i + 1}: LGTM for this recovery contract with no blocking issue.`),
"",
"VERDICT: LGTM",
];
const pmEvalLines = [
"# PM Review",
"Role: pm",
"",
"## Scope",
...Array.from({ length: 18 }, (_, i) => `PM acceptance scope check ${i + 1} confirms recovery remains within the pre-release flow contract.`),
"",
"## Evidence",
...Array.from({ length: 18 }, (_, i) => `PM evidence ${i + 1}: recovery preserves prior stop marker and keeps acceptance evidence available.`),
"",
"## Decision",
...Array.from({ length: 18 }, (_, i) => `PM decision ${i + 1}: LGTM for this recovery contract with no product blocker.`),
"",
"VERDICT: LGTM",
];
writeFileSync(join(runDir, "eval-skeptic-owner.md"), evalLines.join("\n") + "\n");
writeFileSync(join(runDir, "eval-pm.md"), pmEvalLines.join("\n") + "\n");
writeFileSync(join(runDir, "handshake.json"), JSON.stringify({
nodeId: "acceptance",
nodeType: "review",
runId: "run_1",
status: "completed",
verdict: "PASS",
summary: "Acceptance complete",
timestamp: "2024-01-01T00:00:00.000Z",
artifacts: [
{ type: "eval", path: "eval-skeptic-owner.md" },
{ type: "eval", path: "eval-pm.md" },
],
}, null, 2));
const statePath = join(dir, "flow-state.json");
const seededState = JSON.parse(readFileSync(statePath, "utf8"));
seededState.history = [
{ nodeId: "acceptance", runId: "run_1", timestamp: "2024-01-01T00:00:00.000Z" },
{ nodeId: "gate-acceptance", runId: "run_1", timestamp: "2024-01-01T00:01:00.000Z" },
];
seededState.totalSteps = 2;
writeFileSync(statePath, JSON.stringify(seededState, null, 2));
}
const state = JSON.parse(readFileSync(join(dir, "flow-state.json"), "utf8"));

@@ -841,2 +900,5 @@ const stopped = createStopMarker(dir, state, { reason: "tool-call-budget" });

assert.equal(recovered.json?.error, undefined, JSON.stringify(recovered.json));
if (recovery.name === "pass") {
assert.equal(recovered.json?.allowed, true, JSON.stringify(recovered.json));
}
assert.deepEqual(

@@ -843,0 +905,0 @@ evaluatePreToolUse(hookInput(fixture, sessionId, "after-recovery"), { home: fixture.home }),

@@ -6,2 +6,3 @@ import { existsSync, mkdirSync, readFileSync, readdirSync, writeFileSync } from "fs";

import { appendProvenanceEvent } from "./provenance-ledger.mjs";
import { compareRunIds } from "./run-id.mjs";

@@ -22,6 +23,12 @@ const EXECUTION_ACTOR = "opc-harness:test-command";

.filter(entry => entry.isDirectory() && /^run_\d+$/.test(entry.name))
.sort((a, b) => Number(b.name.slice(4)) - Number(a.name.slice(4)));
.sort((a, b) => compareRunIds(b.name, a.name));
return runs[0] ? join(nodeDir, runs[0].name) : null;
}
function runDirFor(nodeDir, runId) {
if (typeof runId !== "string" || !/^run_\d+$/.test(runId)) return null;
const dir = join(nodeDir, runId);
return existsSync(dir) ? dir : null;
}
function commandSpecFrom(data) {

@@ -60,11 +67,23 @@ if (!data || typeof data.testCommand !== "string" || data.testCommand.trim() === "") return null;

function sourcePlanHash(sessionDir, nodeId) {
function readRunBoundJson(path, runId) {
const data = readJson(path);
if (!data) return null;
if (runId && data.runId !== runId) return null;
return data;
}
function sourcePlanHash(sessionDir, nodeId, runId = null) {
const nodeDir = join(sessionDir, "nodes", nodeId);
const runDir = latestRunDir(nodeDir);
const handshake = readJson(join(nodeDir, "handshake.json"));
const candidates = [
planPathFromHandshake(nodeDir, handshake),
runDir ? join(runDir, "test-plan.md") : null,
join(nodeDir, "test-plan.md"),
].filter(Boolean);
const runDir = runId ? runDirFor(nodeDir, runId) : latestRunDir(nodeDir);
const handshake = readRunBoundJson(join(nodeDir, "handshake.json"), runId);
const candidates = runId
? [
runDir ? join(runDir, "test-plan.md") : null,
planPathFromHandshake(nodeDir, handshake),
].filter(Boolean)
: [
runDir ? join(runDir, "test-plan.md") : null,
planPathFromHandshake(nodeDir, handshake),
join(nodeDir, "test-plan.md"),
].filter(Boolean);
for (const path of candidates) {

@@ -77,14 +96,21 @@ const text = readText(path);

export function loadTestCommandSpec(sessionDir, nodeId) {
export function loadTestCommandSpec(sessionDir, nodeId, runId = null) {
const nodeDir = join(sessionDir, "nodes", nodeId);
const runDir = latestRunDir(nodeDir);
const candidates = [
join(nodeDir, "test-execution.json"),
join(nodeDir, "handshake.json"),
runDir ? join(runDir, "test-execution.json") : null,
runDir ? join(runDir, "handshake.json") : null,
].filter(Boolean);
const runDir = runId ? runDirFor(nodeDir, runId) : latestRunDir(nodeDir);
const candidates = runId
? [
runDir ? join(runDir, "test-execution.json") : null,
runDir ? join(runDir, "handshake.json") : null,
join(nodeDir, "test-execution.json"),
join(nodeDir, "handshake.json"),
].filter(Boolean)
: [
join(nodeDir, "test-execution.json"),
runDir ? join(runDir, "test-execution.json") : null,
runDir ? join(runDir, "handshake.json") : null,
join(nodeDir, "handshake.json"),
].filter(Boolean);
for (const path of candidates) {
const spec = commandSpecFrom(readJson(path));
if (spec) return { ...spec, sourcePlanHash: sourcePlanHash(sessionDir, nodeId) };
const spec = commandSpecFrom(readRunBoundJson(path, runId));
if (spec) return { ...spec, sourcePlanHash: sourcePlanHash(sessionDir, nodeId, runId) };
}

@@ -94,2 +120,27 @@ return null;

export function collectTestCommandBindingReasons(sessionDir, nodeId, runId) {
if (!runId) return [];
const nodeDir = join(sessionDir, "nodes", nodeId);
const reasons = [];
const spec = loadTestCommandSpec(sessionDir, nodeId, runId);
if (!spec) {
reasons.push(`${nodeId}/${runId}: testCommand spec not found`);
} else if (typeof spec.sourcePlanHash !== "string" || spec.sourcePlanHash.length === 0) {
reasons.push(`${nodeId}/${runId}: source test-plan hash missing`);
}
for (const name of ["test-execution.json", "handshake.json"]) {
const path = join(nodeDir, name);
if (!existsSync(path)) continue;
const data = readJson(path);
if (!data || !commandSpecFrom(data)) continue;
if (data.nodeId && data.nodeId !== nodeId) {
reasons.push(`${nodeId}/${name} nodeId '${data.nodeId}' does not match '${nodeId}'`);
}
if (data.runId !== runId) {
reasons.push(`${nodeId}/${name} runId '${data.runId}' does not match selected source run '${runId}'`);
}
}
return reasons;
}
function trimOutput(value) {

@@ -147,3 +198,3 @@ const text = String(value || "");

function writeResultFiles(runDir, spec, result, cwdInfo) {
function writeResultFiles(runDir, spec, result, cwdInfo, sourceNode, sourceRunId) {
const stdout = trimOutput(result.stdout);

@@ -161,2 +212,4 @@ const stderrText = result.error?.message ? `${result.stderr || ""}\n${result.error.message}` : result.stderr;

kind: "opc-test-command",
sourceNode,
sourceRunId,
commandHash,

@@ -194,4 +247,5 @@ sourcePlanHash: spec.sourcePlanHash,

export function executeTestCommand(sessionDir, targetNode, runId, sourceNode) {
const spec = loadTestCommandSpec(sessionDir, sourceNode);
export function executeTestCommand(sessionDir, targetNode, runId, sourceNode, sourceRunId = null) {
if (!/^run_\d+$/.test(sourceRunId || "")) return null;
const spec = loadTestCommandSpec(sessionDir, sourceNode, sourceRunId);
if (!spec) return null;

@@ -202,3 +256,3 @@ const runDir = join(sessionDir, "nodes", targetNode, runId);

const result = runTestCommand(spec, cwdInfo.cwd);
const summary = writeResultFiles(runDir, spec, result, cwdInfo);
const summary = writeResultFiles(runDir, spec, result, cwdInfo, sourceNode, sourceRunId);
const verdict = summary.exitCode === 0 ? "PASS" : "FAIL";

@@ -211,2 +265,3 @@ const commandHash = testCommandHash(spec.testCommand);

sourceNode,
sourceRunId,
commandHash,

@@ -221,2 +276,3 @@ sourcePlanHash: spec.sourcePlanHash,

sourceNode,
sourceRunId,
commandHash,

@@ -250,3 +306,10 @@ sourcePlanHash: spec.sourcePlanHash,

writeFileSync(join(sessionDir, "nodes", targetNode, "handshake.json"), JSON.stringify(handshake, null, 2) + "\n");
writeFileSync(join(runDir, "handshake.json"), JSON.stringify({
...handshake,
artifacts: [
{ type: "test-result", path: "test-command-result.json" },
{ type: "cli-output", path: "test-command-output.txt" },
],
}, null, 2) + "\n");
return { executed: true, verdict, exitCode: summary.exitCode, cwd: cwdInfo.cwd, cwdSource: cwdInfo.source, resultPath: join(runDir, "test-command-result.json") };
}
import { existsSync, readFileSync, readdirSync } from "fs";
import { join } from "path";
import { TEST_LAYERS, TEST_LAYER_KEYWORDS } from "./tier-baselines.mjs";
import { compareRunIds } from "./run-id.mjs";

@@ -9,6 +10,7 @@ const CMD_RE = /\b(npm\s+(test|run)|npx\s+\w|pytest|vitest|jest|playwright\s+test|curl\s+|bash\s+|sh\s+|node\s+|python[3]?\s+)/i;

try {
return readdirSync(nodeDir)
.filter(name => /^run_\d+$/.test(name))
.sort((a, b) => parseInt(b.slice(4), 10) - parseInt(a.slice(4), 10))
.map(name => join(nodeDir, name))[0] || null;
return readdirSync(nodeDir, { withFileTypes: true })
.filter((entry) => entry.isDirectory() && /^run_\d+$/.test(entry.name))
.map((entry) => entry.name)
.sort((a, b) => compareRunIds(b, a))
.map((name) => join(nodeDir, name))[0] || null;
} catch {

@@ -19,7 +21,30 @@ return null;

function findPlanPath(dir, nodeId) {
function runDirFor(nodeDir, runId) {
if (typeof runId !== "string" || !/^run_\d+$/.test(runId)) return null;
const dir = join(nodeDir, runId);
return existsSync(dir) ? dir : null;
}
function findPlanPath(dir, nodeId, runId = null) {
const nodeDir = join(dir, "nodes", nodeId);
const runDir = latestRunDir(nodeDir);
const runDir = runId ? runDirFor(nodeDir, runId) : latestRunDir(nodeDir);
const runPlan = runDir ? join(runDir, "test-plan.md") : null;
if (runPlan && existsSync(runPlan)) return runPlan;
if (runId) {
const canonical = join(nodeDir, "handshake.json");
if (!existsSync(canonical)) return null;
let handshake;
try {
handshake = JSON.parse(readFileSync(canonical, "utf8"));
} catch {
return null;
}
if (!handshake || typeof handshake !== "object" || Array.isArray(handshake)) return null;
if (handshake.nodeId !== nodeId || handshake.runId !== runId) return null;
const artifact = Array.isArray(handshake.artifacts)
? handshake.artifacts.find(a => a?.type === "test-plan" && typeof a.path === "string")
: null;
const artifactPath = artifact ? join(nodeDir, artifact.path) : null;
return artifactPath && existsSync(artifactPath) ? artifactPath : null;
}
const nodePlan = join(nodeDir, "test-plan.md");

@@ -120,4 +145,4 @@ return existsSync(nodePlan) ? nodePlan : null;

export function collectTestDesignPlanReasons(dir, nodeId) {
const planPath = findPlanPath(dir, nodeId);
export function collectTestDesignPlanReasons(dir, nodeId, runId = null) {
const planPath = findPlanPath(dir, nodeId, runId);
if (!planPath) return [`${nodeId} test-plan.md missing`];

@@ -124,0 +149,0 @@ const text = readFileSync(planPath, "utf8");

@@ -78,7 +78,12 @@ // Mechanical gate for structured test-result artifacts.

const event = found.event;
const resultPath = `nodes/${context.nodeId}/${context.artifact?.path}`;
if (event.eventType !== "test-command-result") return "ledger event type mismatch";
const expectedRunId = context.runId || context.handshake?.runId;
const artifactPath = context.artifact?.path;
if (typeof artifactPath !== "string" || artifactPath.length === 0) return "ledger result path mismatch";
const resultPath = typeof artifactPath === "string" && artifactPath.startsWith(`${expectedRunId}/`)
? `nodes/${context.nodeId}/${artifactPath}`
: `nodes/${context.nodeId}/${expectedRunId}/${artifactPath}`;
if (event.nodeId !== context.nodeId || event.runId !== expectedRunId) return "ledger node/run mismatch";
if (event.sourceNode !== handshakeProv.sourceNode) return "ledger source node mismatch";
if (event.sourceRunId !== handshakeProv.sourceRunId) return "ledger source run mismatch";
if (event.commandHash !== handshakeProv.commandHash) return "ledger command hash mismatch";

@@ -91,2 +96,41 @@ if (event.sourcePlanHash !== handshakeProv.sourcePlanHash) return "ledger source plan hash mismatch";

export function collectTestEvidenceProvenanceReasons(handshake, context = {}) {
const prov = handshake?.testEvidenceProvenance;
if (prov == null) return [];
const reasons = [];
const needString = (field) => {
if (typeof prov[field] !== "string" || prov[field].length === 0) {
reasons.push(`testEvidenceProvenance.${field} missing or invalid`);
}
};
if (prov.kind !== "opc-test-command") reasons.push("testEvidenceProvenance.kind must be opc-test-command");
if (prov.executionActor !== "opc-harness:test-command") {
reasons.push("testEvidenceProvenance.executionActor mismatch");
}
for (const field of ["sourceNode", "sourceRunId", "commandHash", "sourcePlanHash", "resultHash"]) {
needString(field);
}
if (!/^run_\d+$/.test(prov.sourceRunId || "")) {
reasons.push("testEvidenceProvenance.sourceRunId invalid");
}
const policy = handshake?.testEvidencePolicy;
if (!policy || typeof policy !== "object" || Array.isArray(policy)) {
reasons.push("testEvidencePolicy must be a non-null object");
} else if (!Array.isArray(policy.allowVacuousChecks)) {
reasons.push("testEvidencePolicy.allowVacuousChecks must be an array");
}
if (prov.ledger?.kind !== "opc-hmac-ledger" || typeof prov.ledger?.recordHash !== "string") {
reasons.push("testEvidenceProvenance signed provenance ledger missing or invalid");
} else if (context.sessionDir) {
const reason = ledgerReason(prov, {
...context,
handshake,
nodeId: handshake?.nodeId,
runId: handshake?.runId,
});
if (reason) reasons.push(`testEvidenceProvenance ledger invalid: ${reason}`);
}
return reasons;
}
function hasCommandProvenance(data, handshake, context) {

@@ -105,2 +149,3 @@ const resultProv = data?.provenance || data?.testEvidenceProvenance;

if (resultProv.commandHash !== handshakeProv.commandHash) return "test-result and handshake command hashes differ";
if (resultProv.sourceRunId !== handshakeProv.sourceRunId) return "test-result and handshake source run differ";
if (resultProv.executionActor !== "opc-harness:test-command") return "test-result execution actor mismatch";

@@ -122,3 +167,5 @@ if (handshakeProv.executionActor !== "opc-harness:test-command") return "handshake execution actor mismatch";

}
if (hasCommandProvenance(data, context.handshake, context)) return [];
const schemaReasons = collectTestEvidenceProvenanceReasons(context.handshake, context);
if (schemaReasons.length === 0 && hasCommandProvenance(data, context.handshake, context)) return [];
const reasons = [...schemaReasons];
const resultProv = data?.provenance || data?.testEvidenceProvenance;

@@ -128,6 +175,7 @@ const prov = context.handshake?.testEvidenceProvenance;

if (publicReason) {
return [`test-execute test-result lacks matching OPC testCommand provenance, source test-plan hash, and result hash: ${publicReason}`];
reasons.push(`test-execute test-result lacks matching OPC testCommand provenance, source test-plan hash, and result hash: ${publicReason}`);
}
const reason = ledgerReason(prov, context);
if (reason) return [`test-execute test-result lacks valid OPC signed provenance ledger: ${reason}`];
if (reason) reasons.push(`test-execute test-result lacks valid OPC signed provenance ledger: ${reason}`);
if (reasons.length > 0) return reasons;
return ["test-execute test-result lacks matching OPC testCommand provenance, source test-plan hash, and result hash — self-authored or modified test evidence is weak"];

@@ -134,0 +182,0 @@ }

@@ -20,2 +20,8 @@ // Shared utilities used across all harness modules.

export function hasFlag(args, name) {
const flag = `--${name}`;
const eqPrefix = `${flag}=`;
return args.includes(flag) || args.some(a => typeof a === "string" && a.startsWith(eqPrefix));
}
// ── Safe directory resolution with path traversal guard ─────────

@@ -22,0 +28,0 @@ // When no --dir is given, prefer the latest session dir (if one exists).

@@ -7,2 +7,3 @@ // UX simulation verdict computation — red flag aggregation, delta comparison, gate logic.

import { getFlag, resolveDir, atomicWriteSync } from "./util.mjs";
import { parseRunOrdinal } from "./run-id.mjs";
import {

@@ -240,5 +241,5 @@ VALID_TIERS, RED_FLAG_KEYS, TRUST_SIGNAL_KEYS, TIER_FIT_BUCKETS,

function loadBaseline(dir, currentRun) {
const runNum = parseInt(currentRun, 10);
if (isNaN(runNum) || runNum <= 1) return null;
const prevRun = `run_${runNum - 1}`;
const runOrdinal = parseRunOrdinal(currentRun);
if (runOrdinal === null || runOrdinal <= 1n) return null;
const prevRun = `run_${runOrdinal - 1n}`;
const prevPath = join(dir, "nodes", "ux-simulation", prevRun, "ux-verdict.json");

@@ -245,0 +246,0 @@ try {

@@ -38,3 +38,3 @@ // Visualization and replay commands: getMarker, cmdViz, cmdReplayData

const resolved = resolveFlowTemplate(args, state);
let resolved = resolveFlowTemplate(args, state);
if (resolved.error) {

@@ -41,0 +41,0 @@ console.error(resolved.error);

@@ -127,3 +127,3 @@ #!/usr/bin/env node

const evalsByNode = collectEvals(DIR);
const executionFixes = collectExecutionFixes(DIR);
const executionFixes = collectExecutionFixes(DIR, flowState);

@@ -130,0 +130,0 @@ // Determine R1 vs R2 nodes from loop-state if available

{
"name": "@touchskyer/opc",
"version": "0.10.6",
"version": "0.10.7",
"description": "OPC — One Person Company. Task pipeline with independent multi-role evaluation.",

@@ -5,0 +5,0 @@ "type": "module",

@@ -14,3 +14,3 @@ #!/bin/bash

echo "═══════════════════════════════════════════"
if bash "$f"; then
if bash "$f" < /dev/null; then
TOTAL_PASS=$((TOTAL_PASS + 1))

@@ -17,0 +17,0 @@ else

@@ -339,2 +339,3 @@ #!/bin/bash

HS
sync_run_handshakes .h-forge
OUT=$($HARNESS validate .h-forge/nodes/brief/handshake.json 2>/dev/null)

@@ -363,2 +364,3 @@ VALID=$(echo "$OUT" | python3 -c "import sys,json; print(json.load(sys.stdin).get('valid'))" 2>/dev/null)

HS
sync_run_handshakes .h-real
OUT=$($HARNESS validate .h-real/nodes/brief/handshake.json 2>/dev/null)

@@ -394,10 +396,26 @@ VALID=$(echo "$OUT" | python3 -c "import sys,json; print(json.load(sys.stdin).get('valid'))" 2>/dev/null)

echo '{"pass":true}' > .h-loop/nodes/brief/run_2/brief-lint-result.json
cat > .h-loop/nodes/brief/handshake.json << 'HS'
cat > .h-loop/nodes/brief/run_2/handshake.json << 'HS'
{
"nodeId": "brief", "nodeType": "brief", "runId": "run_2",
"status": "completed", "summary": "loopback no delta", "timestamp": "2024-01-01T00:00:00Z",
"artifacts": [{"type":"brief","path":"build-brief.md"},{"type":"report","path":"run_2/brief-lint-result.json"}]
"artifacts": [{"type":"brief","path":"../build-brief.md"},{"type":"report","path":"brief-lint-result.json"}]
}
HS
OUT=$($HARNESS validate .h-loop/nodes/brief/handshake.json 2>/dev/null)
mkdir -p .h-loop/nodes/brief/run_1
python3 - <<'PY'
import json, pathlib
p = pathlib.Path(".h-loop/nodes/brief/run_2/handshake.json")
d = json.loads(p.read_text())
d["runId"] = "run_1"
pathlib.Path(".h-loop/nodes/brief/run_1/handshake.json").write_text(json.dumps(d, indent=2) + "\n")
PY
python3 -c "
import json
p='.h-loop/flow-state.json'
s=json.load(open(p))
s['history']=[{'nodeId':'brief','runId':'run_2','timestamp':'2024-01-01T00:00:00.000Z'}]
s['totalSteps']=1
json.dump(s, open(p,'w'), indent=2)
"
OUT=$($HARNESS validate .h-loop/nodes/brief/run_2/handshake.json 2>/dev/null)
VALID=$(echo "$OUT" | python3 -c "import sys,json; print(json.load(sys.stdin).get('valid'))" 2>/dev/null)

@@ -419,10 +437,26 @@ ERRORS=$(echo "$OUT" | python3 -c "import sys,json; print(' '.join(json.load(sys.stdin).get('errors',[])))" 2>/dev/null)

echo '{"pass":true}' > .h-loop2/nodes/brief/run_2/brief-lint-result.json
cat > .h-loop2/nodes/brief/handshake.json << 'HS'
cat > .h-loop2/nodes/brief/run_2/handshake.json << 'HS'
{
"nodeId": "brief", "nodeType": "brief", "runId": "run_2",
"status": "completed", "summary": "loopback with delta", "timestamp": "2024-01-01T00:00:00Z",
"artifacts": [{"type":"brief","path":"build-brief.md"},{"type":"report","path":"run_2/brief-lint-result.json"}]
"artifacts": [{"type":"brief","path":"../build-brief.md"},{"type":"report","path":"brief-lint-result.json"}]
}
HS
OUT=$($HARNESS validate .h-loop2/nodes/brief/handshake.json 2>/dev/null)
mkdir -p .h-loop2/nodes/brief/run_1
python3 - <<'PY'
import json, pathlib
p = pathlib.Path(".h-loop2/nodes/brief/run_2/handshake.json")
d = json.loads(p.read_text())
d["runId"] = "run_1"
pathlib.Path(".h-loop2/nodes/brief/run_1/handshake.json").write_text(json.dumps(d, indent=2) + "\n")
PY
python3 -c "
import json
p='.h-loop2/flow-state.json'
s=json.load(open(p))
s['history']=[{'nodeId':'brief','runId':'run_2','timestamp':'2024-01-01T00:00:00.000Z'}]
s['totalSteps']=1
json.dump(s, open(p,'w'), indent=2)
"
OUT=$($HARNESS validate .h-loop2/nodes/brief/run_2/handshake.json 2>/dev/null)
VALID=$(echo "$OUT" | python3 -c "import sys,json; print(json.load(sys.stdin).get('valid'))" 2>/dev/null)

@@ -429,0 +463,0 @@ if [ "$VALID" = "True" ]; then

#!/bin/bash
# test-bypass-chain.sh — validate-chain honors bypass
# Ensures: a flow initialized under OPC_DISABLE_EXTENSIONS=1 does NOT fail
# validate-chain even when ~/.opc/config.json declares requiredExtensions.
# This is the benchmark-reproducibility contract from U1.1.
# test-bypass-chain.sh — validate-chain records bypass without waiving provenance.

@@ -26,4 +23,16 @@ set -u

TMP=$(mktemp -d)
trap "rm -rf '$TMP'" EXIT
HARNESS="${OPC_TEST_HARNESS_NAME:-.harness-bypass-chain-$$}"
cleanup() {
rm -rf "$TMP" "$HARNESS"
rm -f "/tmp/nb-stderr.$$"
}
trap cleanup EXIT
trap 'exit 129' HUP
trap 'exit 130' INT
trap 'exit 143' TERM
sync_hs() {
SYNC_DIR="$HARNESS" bash -c 'source test/test-helpers.sh; sync_run_handshakes "$SYNC_DIR"'
}
# Seed a fake ~/.opc/config.json inside TMP (we'll override HOME for the test)

@@ -36,6 +45,5 @@ mkdir -p "$TMP/fake-home/.opc"

# Work inside a harness dir under cwd so resolveDir doesn't refuse it
HARNESS=".harness-bypass-chain-$$"
rm -rf "$HARNESS"
echo "=== TEST: validate-chain honors bypass ==="
echo "=== TEST: validate-chain records bypass without provenance waiver ==="

@@ -45,3 +53,6 @@ # 1) init under OPC_DISABLE_EXTENSIONS=1

HOME="$TMP/fake-home" OPC_DISABLE_EXTENSIONS=1 node bin/opc-harness.mjs init \
--flow review --entry review --dir "$HARNESS" >/dev/null 2>&1
--flow build-verify --entry code-review --dir "$HARNESS" >/dev/null 2>&1
if [ "${OPC_TEST_ABORT_AFTER_HARNESS_INIT:-0}" = "1" ]; then
exit 97
fi
if [ -f "$HARNESS/flow-state.json" ]; then

@@ -78,44 +89,71 @@ MODE=$(jq -r '.bypassMode.mode // "null"' "$HARNESS/flow-state.json")

# 3) validate-chain under bypass passes despite requiredExtensions config
echo "--- 1.3: validate-chain under bypass waives requiredExtensions"
# 3) A capability-bearing current node claims no extension provenance.
mkdir -p "$HARNESS/nodes/code-review/run_1"
cat > "$HARNESS/nodes/code-review/handshake.json" <<'EOF'
{
"nodeId": "code-review",
"nodeType": "review",
"runId": "run_1",
"status": "completed",
"summary": "review complete",
"timestamp": "2024-01-01T00:00:00Z",
"artifacts": []
}
EOF
sync_hs
echo "--- 1.3: validate-chain under bypass still enforces requiredExtensions"
OUT=$(HOME="$TMP/fake-home" OPC_DISABLE_EXTENSIONS=1 node bin/opc-harness.mjs validate-chain \
--dir "$HARNESS" 2>/dev/null)
VALID=$(echo "$OUT" | jq -r '.valid // false')
if [ "$VALID" = "true" ]; then
echo " ✅ validate-chain valid=true under bypass"
ERRORS=$(echo "$OUT" | jq -r '.errors | join(" ")')
if [ "$VALID" = "false" ] && [[ "$ERRORS" == *"extensionsApplied missing"* ]]; then
echo " ✅ bypass cannot turn missing required provenance green"
PASS=$((PASS + 1))
else
echo " ❌ validate-chain failed under bypass: $OUT"
echo " ❌ validate-chain waived missing provenance under bypass: $OUT"
FAIL=$((FAIL + 1))
fi
# 3b) validate-chain JSON exposes bypassActive/bypassSource/waivedRequiredExtensions
echo "--- 1.3b: validate-chain JSON exposes bypass state (machine-readable)"
# 3b) Bypass remains machine-readable audit metadata, but waives nothing.
echo "--- 1.3b: validate-chain exposes bypass state without waiver"
BACTIVE=$(echo "$OUT" | jq -r '.bypassActive')
BSOURCE=$(echo "$OUT" | jq -r '.bypassSource')
WAIVED=$(echo "$OUT" | jq -r '.waivedRequiredExtensions | join(",")')
if [ "$BACTIVE" = "true" ] && [[ "$BSOURCE" == flow-state* ]] && [ "$WAIVED" = "non-existent-ext" ]; then
echo " ✅ bypassActive=true, bypassSource=$BSOURCE, waived=[$WAIVED]"
WAIVED_COUNT=$(echo "$OUT" | jq -r '.waivedRequiredExtensions | length')
if [ "$BACTIVE" = "true" ] && [[ "$BSOURCE" == flow-state* ]] && [ "$WAIVED_COUNT" = "0" ]; then
echo " ✅ bypassActive=true, bypassSource=$BSOURCE, waived=[]"
PASS=$((PASS + 1))
else
echo " ❌ JSON fields wrong: bypassActive=$BACTIVE bypassSource=$BSOURCE waived=$WAIVED"
echo " ❌ JSON fields wrong: bypassActive=$BACTIVE bypassSource=$BSOURCE waived.length=$WAIVED_COUNT"
FAIL=$((FAIL + 1))
fi
# 4) Negative case: without bypass, validate-chain would still enforce requiredExtensions
# (We can't easily test this in a passing way because a pristine init has no handshakes
# yet, so no nodes fail. But we can confirm the waiver message only fires when bypass
# is active: it should NOT appear without bypass.)
echo "--- 1.4: without bypass, no waiver message emitted"
# 4) Without bypass, the same missing provenance fails for the same reason.
echo "--- 1.4: without bypass, required provenance is enforced without waiver"
rm -rf "$HARNESS"
HOME="$TMP/fake-home" node bin/opc-harness.mjs init \
--flow review --entry review --dir "$HARNESS" >/dev/null 2>&1
--flow build-verify --entry code-review --dir "$HARNESS" >/dev/null 2>&1
mkdir -p "$HARNESS/nodes/code-review/run_1"
cat > "$HARNESS/nodes/code-review/handshake.json" <<'EOF'
{
"nodeId": "code-review",
"nodeType": "review",
"runId": "run_1",
"status": "completed",
"summary": "review complete",
"timestamp": "2024-01-01T00:00:00Z",
"artifacts": []
}
EOF
sync_hs
OUT_NB=$(HOME="$TMP/fake-home" node bin/opc-harness.mjs validate-chain --dir "$HARNESS" 2>/tmp/nb-stderr.$$)
MSG=$(grep -c "waiving requiredExtensions" /tmp/nb-stderr.$$ || true)
NB_VALID=$(echo "$OUT_NB" | jq -r '.valid')
NB_ERRORS=$(echo "$OUT_NB" | jq -r '.errors | join(" ")')
rm -f /tmp/nb-stderr.$$
if [ "$MSG" = "0" ]; then
echo " ✅ no 'waiving' message without bypass"
if [ "$MSG" = "0" ] && [ "$NB_VALID" = "false" ] && [[ "$NB_ERRORS" == *"extensionsApplied missing"* ]]; then
echo " ✅ missing required provenance fails without waiver"
PASS=$((PASS + 1))
else
echo " ❌ unexpected waiver message without bypass"
echo " ❌ required provenance enforcement mismatch: $OUT_NB"
FAIL=$((FAIL + 1))

@@ -136,5 +174,131 @@ fi

# 5) Cleanup
# 5) Required extension provenance must parse and corroborate the sidecar
# Use a capability-bearing node so validate-chain actually enters the provenance gate.
rm -rf "$HARNESS"
HOME="$TMP/fake-home" node bin/opc-harness.mjs init \
--flow build-verify --entry code-review --dir "$HARNESS" >/dev/null 2>&1
mkdir -p "$HARNESS/nodes/code-review/run_1"
cat > "$HARNESS/nodes/code-review/run_1/eval-alpha.md" <<'EOF'
# Alpha Review
Role: alpha
LGTM from alpha after checking extension provenance.
VERDICT: LGTM
EOF
cat > "$HARNESS/nodes/code-review/run_1/eval-beta.md" <<'EOF'
# Beta Review
Role: beta
LGTM from beta after checking required extension sidecars.
VERDICT: LGTM
EOF
cat > "$HARNESS/nodes/code-review/handshake.json" <<'EOF'
{
"nodeId": "code-review",
"nodeType": "review",
"runId": "run_1",
"status": "completed",
"summary": "review complete",
"timestamp": "2024-01-01T00:00:00Z",
"artifacts": [
{"type":"eval","path":"run_1/eval-alpha.md"},
{"type":"eval","path":"run_1/eval-beta.md"}
],
"extensionsApplied": ["non-existent-ext"]
}
EOF
sync_hs
printf '%s\n' '{broken' > "$HARNESS/nodes/code-review/run_1/eval-extensions.json"
OUT_BAD=$(HOME="$TMP/fake-home" node bin/opc-harness.mjs validate-chain --dir "$HARNESS" 2>/dev/null)
BAD_VALID=$(echo "$OUT_BAD" | jq -r '.valid')
BAD_ERROR=$(echo "$OUT_BAD" | jq -r '.errors | join(" ")')
if [ "$BAD_VALID" = "false" ] && [[ "$BAD_ERROR" == *"eval-extensions.json"*parse* ]]; then
echo " ✅ malformed extension sidecar fails closed"
PASS=$((PASS + 1))
else
echo " ❌ malformed extension sidecar accepted: $OUT_BAD"
FAIL=$((FAIL + 1))
fi
cat > "$HARNESS/nodes/code-review/run_1/eval-extensions.json" <<'EOF'
{
"version": 1,
"extensionsLoaded": [
{ "name": "non-existent-ext", "enabled": true }
],
"extensionsApplied": ["non-existent-ext"],
"findings": []
}
EOF
OUT_GOOD=$(HOME="$TMP/fake-home" node bin/opc-harness.mjs validate-chain --dir "$HARNESS" 2>/dev/null)
GOOD_VALID=$(echo "$OUT_GOOD" | jq -r '.valid')
if [ "$GOOD_VALID" = "true" ]; then
echo " ✅ canonical sidecar corroborates required participant"
PASS=$((PASS + 1))
else
echo " ❌ canonical sidecar rejected: $OUT_GOOD"
FAIL=$((FAIL + 1))
fi
# 5b) Canonical handshake runId is authoritative; a newer rogue run cannot corroborate it.
mkdir -p "$HARNESS/nodes/code-review/run_2"
printf '%s\n' '{broken' > "$HARNESS/nodes/code-review/run_1/eval-extensions.json"
cp "$HARNESS/nodes/code-review/run_1/eval-extensions.json" "$HARNESS/nodes/code-review/run_2/ignored-broken.json"
cat > "$HARNESS/nodes/code-review/run_2/eval-extensions.json" <<'EOF'
{
"version": 1,
"extensionsApplied": ["non-existent-ext"],
"findings": []
}
EOF
OUT_BOUND=$(HOME="$TMP/fake-home" node bin/opc-harness.mjs validate-chain --dir "$HARNESS" 2>/dev/null)
BOUND_VALID=$(echo "$OUT_BOUND" | jq -r '.valid')
BOUND_ERROR=$(echo "$OUT_BOUND" | jq -r '.errors | join(" ")')
if [ "$BOUND_VALID" = "false" ] && [[ "$BOUND_ERROR" == *"run_1/eval-extensions.json"*parse* ]]; then
echo " ✅ sidecar corroboration is bound to canonical runId"
PASS=$((PASS + 1))
else
echo " ❌ rogue run_2 corroborated run_1: $OUT_BOUND"
FAIL=$((FAIL + 1))
fi
# 5c) Handshake self-reported nodeType cannot authorize prompt-phase exemption.
python3 - "$HARNESS/nodes/code-review/handshake.json" <<'PY'
import json, sys
path = sys.argv[1]
data = json.load(open(path))
data["nodeType"] = "build"
json.dump(data, open(path, "w"))
PY
OUT_TYPE=$(HOME="$TMP/fake-home" node bin/opc-harness.mjs validate-chain --dir "$HARNESS" 2>/dev/null)
TYPE_VALID=$(echo "$OUT_TYPE" | jq -r '.valid')
if [ "$TYPE_VALID" = "false" ]; then
echo " ✅ forged nodeType cannot waive review provenance"
PASS=$((PASS + 1))
else
echo " ❌ forged nodeType bypassed provenance: $OUT_TYPE"
FAIL=$((FAIL + 1))
fi
# 5d) Handshake self-reported nodeId cannot authorize gate exemption.
python3 - "$HARNESS/nodes/code-review/handshake.json" <<'PY'
import json, sys
path = sys.argv[1]
data = json.load(open(path))
data["nodeType"] = "review"
data["nodeId"] = "gate"
json.dump(data, open(path, "w"))
PY
OUT_ID=$(HOME="$TMP/fake-home" node bin/opc-harness.mjs validate-chain --dir "$HARNESS" 2>/dev/null)
ID_VALID=$(echo "$OUT_ID" | jq -r '.valid')
if [ "$ID_VALID" = "false" ]; then
echo " ✅ forged nodeId cannot waive review provenance"
PASS=$((PASS + 1))
else
echo " ❌ forged nodeId bypassed provenance: $OUT_ID"
FAIL=$((FAIL + 1))
fi
# 6) Cleanup
rm -rf "$HARNESS"
echo ""

@@ -141,0 +305,0 @@ echo "==========================================="

@@ -45,2 +45,3 @@ #!/bin/bash

"$NODE" "$(date -u +%Y-%m-%dT%H:%M:%SZ)" "$VERDICT" > "$DIR/nodes/$NODE/handshake.json"
sync_run_handshakes "$DIR"
}

@@ -54,2 +55,3 @@

"$NODE" "$(date -u +%Y-%m-%dT%H:%M:%SZ)" > "$DIR/nodes/$NODE/handshake.json"
sync_run_handshakes "$DIR"
}

@@ -63,2 +65,3 @@

"$NODE" "$(date -u +%Y-%m-%dT%H:%M:%SZ)" > "$DIR/nodes/$NODE/handshake.json"
sync_run_handshakes "$DIR"
}

@@ -65,0 +68,0 @@

#!/bin/bash
set -e
OPC_BIN="$(dirname "$(dirname "$(realpath "$0")")")/bin/opc-harness.mjs"
SCRIPT_PATH="$(realpath "$0")"
OPC_BIN="$(dirname "$(dirname "$SCRIPT_PATH")")/bin/opc-harness.mjs"
source "$(dirname "$0")/test-helpers.sh"
source "$(dirname "$SCRIPT_PATH")/test-helpers.sh"
setup_tmpdir
opc() { node "$OPC_BIN" "$@"; }
TESTBASE="/tmp/opc-comprehensive-test2-$$"
FLOW_FIXTURE="$HOME/.claude/flows/_opc_test_schema.json"
cleanup() {
rm -rf "$TESTBASE" "$TMPDIR"
rm -f "$FLOW_FIXTURE"
}
trap cleanup EXIT
if [ "${OPC_TEST_CLEANUP_PROBE:-0}" = "1" ]; then
mkdir -p "$(dirname "$FLOW_FIXTURE")"
printf '%s\n' '{"probe":true}' > "$FLOW_FIXTURE"
exit 23
fi
cleanup_probe() {
if OPC_TEST_CLEANUP_PROBE=1 "$SCRIPT_PATH" >/dev/null 2>&1; then
return 1
else
[ "$?" -eq 23 ] && [ ! -e "$FLOW_FIXTURE" ]
fi
}
mkdir -p "$TESTBASE"

@@ -62,6 +84,6 @@ TOTAL=0

write_complete_test_plan "$DIR/nodes/$NODE/run_1/test-plan.md"
cat > "$DIR/nodes/$NODE/test-execution.json" <<'JSON'
{"testCommand":"node -e \"process.exit(0)\"","prerequisites":["fixture command"]}
cat > "$DIR/nodes/$NODE/run_1/test-execution.json" <<'JSON'
{"nodeId":"test-design","runId":"run_1","testCommand":"node -e \"process.exit(0)\"","prerequisites":["fixture command"]}
JSON
artifacts='[{"type":"eval","path":"run_1/eval-skeptic-owner.md"},{"type":"eval","path":"run_1/eval-comprehensive-peer.md"},{"type":"test-plan","path":"run_1/test-plan.md"},{"type":"test-plan","path":"test-execution.json"}]'
artifacts='[{"type":"eval","path":"run_1/eval-skeptic-owner.md"},{"type":"eval","path":"run_1/eval-comprehensive-peer.md"},{"type":"test-plan","path":"run_1/test-plan.md"},{"type":"test-plan","path":"run_1/test-execution.json"}]'
extra=',"testCommand":"node -e \"process.exit(0)\"","prerequisites":["fixture command"]'

@@ -71,2 +93,3 @@ fi

"$NODE" "$(date -u +%Y-%m-%dT%H:%M:%SZ)" "$artifacts" "$VERDICT" "$extra" > "$DIR/nodes/$NODE/handshake.json"
sync_run_handshakes "$DIR"
}

@@ -80,2 +103,3 @@

"$NODE" "$(date -u +%Y-%m-%dT%H:%M:%SZ)" > "$DIR/nodes/$NODE/handshake.json"
sync_run_handshakes "$DIR"
}

@@ -89,2 +113,3 @@

"$NODE" "$(date -u +%Y-%m-%dT%H:%M:%SZ)" > "$DIR/nodes/$NODE/handshake.json"
sync_run_handshakes "$DIR"
}

@@ -107,7 +132,7 @@

# goto maxLoopsPerEdge (self-loop build→build hits edge limit=3 before nodeReentry=5)
# After prior goto test-execute, first goto build = test-execute→build, then build→build starts
for i in 1 2 3 4; do opc goto build --dir .harness > /dev/null 2>&1; done
# Manual goto traffic is not semantic repair; repeated reentry is bounded by maxNodeReentry.
# After prior goto test-execute, first goto build = test-execute→build, then build→build starts.
for i in 1 2 3 4 5; do opc goto build --dir .harness > /dev/null 2>&1; done
R=$(opc goto build --dir .harness)
check_json "maxLoopsPerEdge enforced" "'maxLoopsPerEdge' in d.get('error','')" "$R"
check_json "maxNodeReentry enforced" "'maxNodeReentry' in d.get('error','')" "$R"

@@ -127,2 +152,14 @@ # stop

opc goto gate-test --dir .harness > /dev/null 2>&1
write_exec_hs .harness test-execute
python3 -c "
import json
p='.harness/flow-state.json'
s=json.load(open(p))
gate=s['history'][-1]
s['history']=[
{'nodeId':'test-execute','runId':'run_1','timestamp':'2024-01-01T00:00:00.000Z'},
gate,
]
json.dump(s, open(p,'w'), indent=2)
"
R=$(opc pass --dir .harness 2>/dev/null)

@@ -182,4 +219,5 @@ check_json "pass advances gate" "d.get('next')=='acceptance'" "$R"

mkdir -p ~/.claude/flows
cat > ~/.claude/flows/_opc_test_schema.json << 'EOF'
check "global flow fixture cleaned after abnormal exit" cleanup_probe
mkdir -p "$(dirname "$FLOW_FIXTURE")"
cat > "$FLOW_FIXTURE" << 'EOF'
{"nodes":["build","gate"],"edges":{"build":{"PASS":"gate"},"gate":{"PASS":null}},"limits":{"maxLoopsPerEdge":3,"maxTotalSteps":10,"maxNodeReentry":5},"nodeTypes":{"build":"build","gate":"gate"},"contextSchema":{"build":{"required":["task"],"rules":{"task":"non-empty-string"}}}}

@@ -202,3 +240,3 @@ EOF

rm -f ~/.claude/flows/_opc_test_schema.json
rm -f "$FLOW_FIXTURE"

@@ -211,3 +249,6 @@ # ─────────────────────────────────────────────────────────────────

mkdir -p "$T9" && cd "$T9"
git init -q && git commit --allow-empty -m "init" -q
git init -q
git config user.email "test@test.com"
git config user.name "Test"
git commit --allow-empty -m "init" -q

@@ -257,2 +298,3 @@ cat > plan.md << 'EOF'

sleep 1; opc transition --from test-design --to test-execute --verdict PASS --flow build-verify --dir .harness 2>/dev/null > /dev/null
write_exec_hs ".harness" "test-execute"
sleep 1; opc transition --from test-execute --to gate --verdict PASS --flow build-verify --dir .harness 2>/dev/null > /dev/null

@@ -262,9 +304,10 @@ R=$(opc finalize --dir .harness)

# legacy-linear routing
R=$(opc route --node evaluate --verdict FAIL --flow legacy-linear)
# legacy-linear routing uses an empty harness so build-verify state cannot substitute its graph.
LEGACY_HARNESS="$T10/legacy-harness"
mkdir -p "$LEGACY_HARNESS"
R=$(opc route --node evaluate --verdict FAIL --flow legacy-linear --dir "$LEGACY_HARNESS")
check_json "legacy-linear FAIL → build" "d['next']=='build'" "$R"
R=$(opc route --node deliver --verdict PASS --flow legacy-linear)
R=$(opc route --node deliver --verdict PASS --flow legacy-linear --dir "$LEGACY_HARNESS")
check_json "legacy-linear terminal" "d['next']==None" "$R"
rm -rf "$TESTBASE"
print_results

@@ -9,5 +9,35 @@ #!/bin/bash

FLOW_DIR="$HOME/.claude/flows"
IDEA_FLOW="$FLOW_DIR/idea-factory.json"
CONTEXT_FLOW="$FLOW_DIR/test-ctx-flow.json"
IDEA_BACKUP="$TMPDIR/idea-factory.json.backup"
CONTEXT_BACKUP="$TMPDIR/test-ctx-flow.json.backup"
IDEA_EXISTED=0
CONTEXT_EXISTED=0
mkdir -p "$FLOW_DIR"
if [ -e "$IDEA_FLOW" ] || [ -L "$IDEA_FLOW" ]; then
mv "$IDEA_FLOW" "$IDEA_BACKUP"
IDEA_EXISTED=1
fi
if [ -e "$CONTEXT_FLOW" ] || [ -L "$CONTEXT_FLOW" ]; then
mv "$CONTEXT_FLOW" "$CONTEXT_BACKUP"
CONTEXT_EXISTED=1
fi
cleanup() {
rm -f "$IDEA_FLOW" "$CONTEXT_FLOW"
if [ "$IDEA_EXISTED" -eq 1 ]; then
mv "$IDEA_BACKUP" "$IDEA_FLOW"
fi
if [ "$CONTEXT_EXISTED" -eq 1 ]; then
mv "$CONTEXT_BACKUP" "$CONTEXT_FLOW"
fi
rm -rf "$TMPDIR"
}
trap cleanup EXIT
trap 'exit 129' HUP
trap 'exit 130' INT
trap 'exit 143' TERM
# Create idea-factory fixture for testing (not a built-in template)
mkdir -p "$HOME/.claude/flows"
cat > "$HOME/.claude/flows/idea-factory.json" << 'FIXTURE'
cat > "$IDEA_FLOW" << 'FIXTURE'
{

@@ -103,2 +133,3 @@ "nodes": ["discover", "validate", "build", "gate", "synthesize", "pitch"],

d['edgeCounts']['gate→brief'] = d['maxLoopsPerEdge']
d['repairEdgeCounts']['gate→brief'] = d['maxLoopsPerEdge']
json.dump(d, open('.h-edge/flow-state.json', 'w'), indent=2)

@@ -134,24 +165,17 @@ "

echo "--- CG-3.1: Duplicate transition within 5s window blocked ---"
rm -rf .h-idemp && $HARNESS init --flow build-verify --dir .h-idemp >/dev/null 2>/dev/null
mkdir -p .h-idemp/nodes/build
cat > .h-idemp/nodes/build/handshake.json << 'HS'
rm -rf .h-idemp && $HARNESS init --flow build-verify --entry build --dir .h-idemp >/dev/null 2>/dev/null
mkdir -p .h-idemp/nodes/build/run_1
cat > .h-idemp/nodes/build/run_1/handshake.json << 'HS'
{"nodeId":"build","nodeType":"build","runId":"run_1","status":"completed","summary":"done","timestamp":"2024-01-01T00:00:00Z","artifacts":[]}
HS
$HARNESS transition --from build --to code-review --verdict PASS --flow build-verify --dir .h-idemp >/dev/null 2>/dev/null
mkdir -p .h-idemp/nodes/code-review
cat > .h-idemp/nodes/code-review/handshake.json << 'HS'
{"nodeId":"code-review","nodeType":"review","runId":"run_1","status":"completed","summary":"done","timestamp":"2024-01-01T00:00:00Z","artifacts":[]}
HS
OUT=$($HARNESS transition --from code-review --to test-execute --verdict PASS --flow build-verify --dir .h-idemp 2>/dev/null)
rm -rf .h-idemp2 && $HARNESS init --flow build-verify --entry gate --dir .h-idemp2 >/dev/null 2>/dev/null
$HARNESS transition --from gate --to brief --verdict FAIL --flow build-verify --dir .h-idemp2 >/dev/null 2>/dev/null
python3 -c "
import json
d = json.load(open('.h-idemp2/flow-state.json'))
d['currentNode'] = 'gate'
json.dump(d, open('.h-idemp2/flow-state.json', 'w'), indent=2)
d = json.load(open('.h-idemp/flow-state.json'))
d['currentNode'] = 'build'
json.dump(d, open('.h-idemp/flow-state.json', 'w'), indent=2)
"
OUT=$($HARNESS transition --from gate --to brief --verdict FAIL --flow build-verify --dir .h-idemp2 2>/dev/null)
OUT=$($HARNESS transition --from build --to code-review --verdict PASS --flow build-verify --dir .h-idemp 2>/dev/null)
assert_field_eq "idempotency blocked" "$OUT" "allowed" "false"
assert_contains "idempotency guard" "$OUT" "idempotency"
assert_contains "manual rewind rejected before duplicate transition" "$OUT" "cannot resolve current run"

@@ -165,8 +189,19 @@ # ═══════════════════════════════════════════════════════════════

rm -rf .h-backlog && $HARNESS init --flow build-verify --entry gate --dir .h-backlog >/dev/null 2>/dev/null
mkdir -p .h-backlog/nodes/test-execute
cat > .h-backlog/nodes/test-execute/handshake.json << 'HS'
mkdir -p .h-backlog/nodes/test-execute/run_1
cat > .h-backlog/nodes/test-execute/run_1/handshake.json << 'HS'
{"nodeId":"test-execute","nodeType":"execute","runId":"run_1","status":"completed","summary":"done",
"timestamp":"2024-01-01T00:00:00Z","artifacts":["evidence.txt"],"findings":{"warning":2,"critical":0}}
"timestamp":"2024-01-01T00:00:00Z","artifacts":[{"type":"cli-output","path":"evidence.txt"}],"findings":{"warning":2,"critical":0}}
HS
echo "test evidence" > .h-backlog/nodes/test-execute/evidence.txt
echo "test evidence" > .h-backlog/nodes/test-execute/run_1/evidence.txt
python3 -c "
import json
p='.h-backlog/flow-state.json'
s=json.load(open(p))
s['history']=[
{'nodeId':'test-execute','runId':'run_1','timestamp':'2024-01-01T00:00:00.000Z'},
{'nodeId':'gate','runId':'run_1','timestamp':'2024-01-01T00:00:01.000Z'},
]
s['totalSteps']=2
json.dump(s, open(p,'w'), indent=2)
"
OUT=$($HARNESS transition --from gate --to brief --verdict ITERATE --flow build-verify --dir .h-backlog 2>/dev/null)

@@ -179,8 +214,19 @@ assert_field_eq "backlog required" "$OUT" "allowed" "false"

rm -rf .h-backlog2 && $HARNESS init --flow build-verify --entry gate --dir .h-backlog2 >/dev/null 2>/dev/null
mkdir -p .h-backlog2/nodes/test-execute
cat > .h-backlog2/nodes/test-execute/handshake.json << 'HS'
mkdir -p .h-backlog2/nodes/test-execute/run_1
cat > .h-backlog2/nodes/test-execute/run_1/handshake.json << 'HS'
{"nodeId":"test-execute","nodeType":"execute","runId":"run_1","status":"completed","summary":"done",
"timestamp":"2024-01-01T00:00:00Z","artifacts":["evidence.txt"],"findings":{"warning":2,"critical":0}}
"timestamp":"2024-01-01T00:00:00Z","artifacts":[{"type":"cli-output","path":"evidence.txt"}],"findings":{"warning":2,"critical":0}}
HS
echo "test evidence" > .h-backlog2/nodes/test-execute/evidence.txt
echo "test evidence" > .h-backlog2/nodes/test-execute/run_1/evidence.txt
python3 -c "
import json
p='.h-backlog2/flow-state.json'
s=json.load(open(p))
s['history']=[
{'nodeId':'test-execute','runId':'run_1','timestamp':'2024-01-01T00:00:00.000Z'},
{'nodeId':'gate','runId':'run_1','timestamp':'2024-01-01T00:00:01.000Z'},
]
s['totalSteps']=2
json.dump(s, open(p,'w'), indent=2)
"
cat > .h-backlog2/backlog.md << 'BL'

@@ -197,8 +243,19 @@ # Backlog

rm -rf .h-backlog3 && $HARNESS init --flow build-verify --entry gate --dir .h-backlog3 >/dev/null 2>/dev/null
mkdir -p .h-backlog3/nodes/test-execute
cat > .h-backlog3/nodes/test-execute/handshake.json << 'HS'
mkdir -p .h-backlog3/nodes/test-execute/run_1
cat > .h-backlog3/nodes/test-execute/run_1/handshake.json << 'HS'
{"nodeId":"test-execute","nodeType":"execute","runId":"run_1","status":"completed","summary":"done",
"timestamp":"2024-01-01T00:00:00Z","artifacts":["evidence.txt"],"findings":{"warning":3,"critical":0}}
"timestamp":"2024-01-01T00:00:00Z","artifacts":[{"type":"cli-output","path":"evidence.txt"}],"findings":{"warning":3,"critical":0}}
HS
echo "test evidence" > .h-backlog3/nodes/test-execute/evidence.txt
echo "test evidence" > .h-backlog3/nodes/test-execute/run_1/evidence.txt
python3 -c "
import json
p='.h-backlog3/flow-state.json'
s=json.load(open(p))
s['history']=[
{'nodeId':'test-execute','runId':'run_1','timestamp':'2024-01-01T00:00:00.000Z'},
{'nodeId':'gate','runId':'run_1','timestamp':'2024-01-01T00:00:01.000Z'},
]
s['totalSteps']=2
json.dump(s, open(p,'w'), indent=2)
"
cat > .h-backlog3/backlog.md << 'BL'

@@ -241,2 +298,5 @@ - [ ] 🟡 Only one entry [test-execute]

CTX
if [ "${OPC_TEST_ABORT_AFTER_CONTEXT_FIXTURE:-0}" = "1" ]; then
exit 97
fi
$HARNESS init --flow test-ctx-flow --dir .h-ctx2 >/dev/null 2>/dev/null

@@ -243,0 +303,0 @@ OUT=$($HARNESS validate-context --flow test-ctx-flow --node step1 --dir .h-ctx2 2>/dev/null)

@@ -53,9 +53,12 @@ #!/bin/bash

local artifacts="[]"
local run_dir="$dir/nodes/$node/run_1"
local run_id="run_1"
local run_dir="$dir/nodes/$node/$run_id"
local latest
latest=$(ls -d "$dir/nodes/$node"/run_* 2>/dev/null | sort -V | tail -1 || true)
if [ -n "$latest" ]; then
run_id=$(basename "$latest")
run_dir="$latest"
fi
if [ "$node_type" = "review" ] && [ -d "$run_dir" ]; then
artifacts=$(ls "$run_dir"/eval-*.md 2>/dev/null | python3 -c "
import sys, json
files = [l.strip() for l in sys.stdin if l.strip()]
print(json.dumps([{'path': f, 'type': 'eval'} for f in files]))
" 2>/dev/null || echo "[]")
artifacts=$(find "$run_dir" -maxdepth 1 -name 'eval-*.md' -print | python3 -c 'import json, os, sys; run_id=sys.argv[1]; files=[line.strip() for line in sys.stdin if line.strip()]; print(json.dumps([{"path": f"{run_id}/{os.path.basename(path)}", "type": "eval"} for path in files]))' "$run_id" 2>/dev/null || echo "[]")
fi

@@ -66,3 +69,3 @@ cat > "$path" << HSEOF

"nodeType": "$node_type",
"runId": "run_1",
"runId": "$run_id",
"status": "completed",

@@ -75,2 +78,3 @@ "summary": "$summary",

HSEOF
sync_run_handshakes "$dir"
}

@@ -188,5 +192,9 @@

write_good_eval .harness review tester
write_good_eval .harness review skeptic-owner
write_handshake .harness review "Review round 1" "PASS"
$HARNESS transition --from review --to gate --verdict PASS --flow review --dir .harness 2>/dev/null
write_warning_eval .harness review senior
write_good_eval .harness review tester
write_handshake .harness review "Review round 1" "ITERATE"
$HARNESS transition --from review --to gate --verdict PASS --flow review --dir .harness 2>/dev/null
SYNTH=$($HARNESS synthesize .harness --node review)
SYNTH=$($HARNESS synthesize .harness --node review --run 1)
assert_field_eq "2.1: round 1 ITERATE" "$SYNTH" "verdict" '"ITERATE"'

@@ -200,8 +208,23 @@ write_handshake .harness gate "Gate iterates" "ITERATE" gate

mkdir -p .harness/nodes/review/run_2
write_good_eval .harness review senior
mv .harness/nodes/review/run_1/eval-senior.md .harness/nodes/review/run_2/eval-senior.md
write_good_eval .harness review tester
mv .harness/nodes/review/run_1/eval-tester.md .harness/nodes/review/run_2/eval-tester.md
write_good_eval .harness review skeptic-owner
mv .harness/nodes/review/run_1/eval-skeptic-owner.md .harness/nodes/review/run_2/eval-skeptic-owner.md
cat > .harness/nodes/review/run_2/eval-senior.md <<'EVAL'
# senior Review
🔵 src/handler.ts:15 — Input validation is acceptable
Reasoning: The second review verified the boundary checks.
→ Keep the existing validation.
VERDICT: PASS FINDINGS[0]
EVAL
cat > .harness/nodes/review/run_2/eval-tester.md <<'EVAL'
# tester Review
🔵 src/service.ts:55 — Test coverage is acceptable
Reasoning: The second review verified edge cases.
→ Keep the existing tests.
VERDICT: PASS FINDINGS[0]
EVAL
cat > .harness/nodes/review/run_2/eval-skeptic-owner.md <<'EVAL'
# skeptic-owner Review
🔵 src/flow.ts:30 — Mechanism holds
Reasoning: The second review verified the requested authority boundary.
→ No action required.
VERDICT: PASS FINDINGS[0]
EVAL
write_handshake .harness review "Review round 2" "PASS"

@@ -208,0 +231,0 @@ $HARNESS transition --from review --to gate --verdict PASS --flow review --dir .harness 2>/dev/null

@@ -48,2 +48,16 @@ #!/bin/bash

assert_transition_allowed() {
local desc="$1"
shift
local out
out=$("$HARNESS" transition "$@" 2>/dev/null)
if echo "$out" | grep -q '"allowed":true'; then
echo " ✅ $desc"
PASS=$((PASS + 1))
else
echo " ❌ $desc — $out"
FAIL=$((FAIL + 1))
fi
}
write_handshake() {

@@ -54,10 +68,38 @@ local dir="$1" node="$2" summary="$3" verdict="$4" node_type="${5:-review}"

local artifacts="[]"
local extra=""
local run_dir="$dir/nodes/$node/run_1"
if [ "$node_type" = "review" ] && [ -d "$run_dir" ]; then
artifacts=$(ls "$run_dir"/eval-*.md 2>/dev/null | python3 -c "
import sys, json
import pathlib, sys, json
files = [l.strip() for l in sys.stdin if l.strip()]
print(json.dumps([{'path': f, 'type': 'eval'} for f in files]))
print(json.dumps([{'path': 'run_1/' + pathlib.Path(f).name, 'type': 'eval'} for f in files]))
" 2>/dev/null || echo "[]")
fi
if [ "$node_type" = "brief" ]; then
mkdir -p "$run_dir"
write_golden_brief "$dir/nodes/$node/build-brief.md"
echo '{"pass":true}' > "$run_dir/brief-lint-result.json"
artifacts='[{"type":"brief","path":"build-brief.md"},{"type":"report","path":"run_1/brief-lint-result.json"}]'
fi
if [ "$node_type" = "execute" ]; then
mkdir -p "$run_dir"
echo "tests passed" > "$run_dir/command-output.txt"
artifacts='[{"type":"cli-output","path":"run_1/command-output.txt"}]'
fi
if [ "$node" = "test-design" ]; then
mkdir -p "$run_dir"
write_complete_test_plan "$run_dir/test-plan.md"
printf '%s\n' '{"nodeId":"test-design","runId":"run_1","testCommand":"node -e \"process.exit(0)\"","prerequisites":["fixture"]}' > "$run_dir/test-execution.json"
artifacts=$(python3 - "$artifacts" <<'PY'
import json, sys
items = json.loads(sys.argv[1])
items.extend([
{"type": "test-plan", "path": "run_1/test-plan.md"},
{"type": "test-command", "path": "run_1/test-execution.json"},
])
print(json.dumps(items))
PY
)
extra=', "testCommand": "node -e \"process.exit(0)\"", "prerequisites": ["fixture"]'
fi
cat > "$path" << HSEOF

@@ -71,6 +113,7 @@ {

"verdict": "$verdict",
"artifacts": $artifacts,
"artifacts": $artifacts$extra,
"timestamp": "$(date -u +%Y-%m-%dT%H:%M:%SZ)"
}
HSEOF
sync_run_handshakes "$dir"
}

@@ -157,4 +200,5 @@

write_good_eval .harness review tester
write_handshake .harness review "Review found critical" "FAIL"
$HARNESS transition --from review --to gate --verdict PASS --flow review --dir .harness 2>/dev/null
write_good_eval .harness review skeptic-owner
write_handshake .harness review "Review found critical" "PASS"
assert_transition_allowed "3.0: review → gate" --from review --to gate --verdict PASS --flow review --dir .harness
SYNTH=$($HARNESS synthesize .harness --node review)

@@ -181,3 +225,3 @@ assert_field_eq "3.1: critical → FAIL" "$SYNTH" "verdict" '"FAIL"'

assert_contains "4.1b: brief → build" "$NEXT" "build"
$HARNESS transition --from brief --to build --verdict PASS --flow build-verify --dir .harness 2>/dev/null
assert_transition_allowed "4.1c: transition brief → build" --from brief --to build --verdict PASS --flow build-verify --dir .harness

@@ -188,3 +232,3 @@ write_handshake .harness build "Implementation complete" "PASS" build

assert_contains "4.2: build → code-review" "$NEXT" "code-review"
$HARNESS transition --from build --to code-review --verdict PASS --flow build-verify --dir .harness 2>/dev/null
assert_transition_allowed "4.2b: transition build → code-review" --from build --to code-review --verdict PASS --flow build-verify --dir .harness

@@ -198,4 +242,6 @@ write_good_eval .harness code-review frontend

assert_contains "4.3: code-review → test-design" "$NEXT" "test-design"
$HARNESS transition --from code-review --to test-design --verdict PASS --flow build-verify --dir .harness 2>/dev/null
assert_transition_allowed "4.3b: transition code-review → test-design" --from code-review --to test-design --verdict PASS --flow build-verify --dir .harness
write_good_eval .harness test-design tester
write_good_eval .harness test-design skeptic-owner
write_handshake .harness test-design "Test cases designed" "PASS"

@@ -205,3 +251,3 @@ ROUTE=$($HARNESS route --node test-design --verdict PASS --flow build-verify)

assert_contains "4.4: test-design → test-execute" "$NEXT" "test-execute"
$HARNESS transition --from test-design --to test-execute --verdict PASS --flow build-verify --dir .harness 2>/dev/null
assert_transition_allowed "4.4b: transition test-design → test-execute" --from test-design --to test-execute --verdict PASS --flow build-verify --dir .harness

@@ -212,3 +258,3 @@ write_handshake .harness test-execute "Tests pass" "PASS" execute

assert_contains "4.5: test-execute → gate" "$NEXT" "gate"
$HARNESS transition --from test-execute --to gate --verdict PASS --flow build-verify --dir .harness 2>/dev/null
assert_transition_allowed "4.5b: transition test-execute → gate" --from test-execute --to gate --verdict PASS --flow build-verify --dir .harness

@@ -230,11 +276,14 @@ SYNTH=$($HARNESS synthesize .harness --node code-review)

write_handshake .harness build "Built" "PASS" build
$HARNESS transition --from build --to code-review --verdict PASS --flow build-verify --dir .harness 2>/dev/null
assert_transition_allowed "5.0a: build → code-review" --from build --to code-review --verdict PASS --flow build-verify --dir .harness
write_critical_eval .harness code-review security
write_good_eval .harness code-review frontend
write_handshake .harness code-review "Found critical" "FAIL"
$HARNESS transition --from code-review --to test-design --verdict PASS --flow build-verify --dir .harness 2>/dev/null
write_good_eval .harness code-review skeptic-owner
write_handshake .harness code-review "Found critical" "PASS"
assert_transition_allowed "5.0b: code-review → test-design" --from code-review --to test-design --verdict PASS --flow build-verify --dir .harness
write_good_eval .harness test-design tester
write_good_eval .harness test-design skeptic-owner
write_handshake .harness test-design "Test design" "PASS"
$HARNESS transition --from test-design --to test-execute --verdict PASS --flow build-verify --dir .harness 2>/dev/null
assert_transition_allowed "5.0c: test-design → test-execute" --from test-design --to test-execute --verdict PASS --flow build-verify --dir .harness
write_handshake .harness test-execute "Tests" "PASS" execute
$HARNESS transition --from test-execute --to gate --verdict PASS --flow build-verify --dir .harness 2>/dev/null
assert_transition_allowed "5.0d: test-execute → gate" --from test-execute --to gate --verdict PASS --flow build-verify --dir .harness
SYNTH=$($HARNESS synthesize .harness --node code-review)

@@ -241,0 +290,0 @@ assert_field_eq "5.1: gate FAIL on critical" "$SYNTH" "verdict" '"FAIL"'

@@ -48,14 +48,30 @@ #!/bin/bash

current_run_id() {
local dir="$1" node="$2"
python3 - "$dir/flow-state.json" "$node" <<'PY'
import json, sys
state = json.load(open(sys.argv[1]))
history = state.get("history", [])
tail = history[-1] if history else None
if tail and tail.get("nodeId") == sys.argv[2] and tail.get("runId"):
print(tail["runId"])
elif (state.get("totalSteps") == 0 and not history and
state.get("currentNode") == state.get("entryNode") == sys.argv[2] and
state.get("flowStartedAt")):
print("run_1")
else:
raise SystemExit(1)
PY
}
write_handshake() {
local dir="$1" node="$2" summary="$3" verdict="$4" node_type="${5:-review}"
local path="$dir/nodes/$node/handshake.json"
local run_id
run_id=$(current_run_id "$dir" "$node") || return 1
mkdir -p "$(dirname "$path")"
local artifacts="[]"
local run_dir="$dir/nodes/$node/run_1"
local run_dir="$dir/nodes/$node/$run_id"
if [ "$node_type" = "review" ] && [ -d "$run_dir" ]; then
artifacts=$(ls "$run_dir"/eval-*.md 2>/dev/null | python3 -c "
import sys, json
files = [l.strip() for l in sys.stdin if l.strip()]
print(json.dumps([{'path': f, 'type': 'eval'} for f in files]))
" 2>/dev/null || echo "[]")
artifacts=$(find "$run_dir" -maxdepth 1 -name 'eval-*.md' -print | python3 -c 'import json, os, sys; run_id=sys.argv[1]; files=[line.strip() for line in sys.stdin if line.strip()]; print(json.dumps([{"path": f"{run_id}/{os.path.basename(path)}", "type": "eval"} for path in files]))' "$run_id" 2>/dev/null || echo "[]")
fi

@@ -66,3 +82,3 @@ cat > "$path" << HSEOF

"nodeType": "$node_type",
"runId": "run_1",
"runId": "$run_id",
"status": "completed",

@@ -75,2 +91,3 @@ "summary": "$summary",

HSEOF
sync_run_handshakes "$dir"
}

@@ -80,3 +97,5 @@

local dir="$1" node="$2" role="$3"
local run_dir="$dir/nodes/$node/run_1"
local run_id
run_id=$(current_run_id "$dir" "$node") || return 1
local run_dir="$dir/nodes/$node/$run_id"
mkdir -p "$run_dir"

@@ -118,3 +137,5 @@ cat > "$run_dir/eval-${role}.md" << EVALEOF

local dir="$1" node="$2" role="$3"
local run_dir="$dir/nodes/$node/run_1"
local run_id
run_id=$(current_run_id "$dir" "$node") || return 1
local run_dir="$dir/nodes/$node/$run_id"
mkdir -p "$run_dir"

@@ -138,3 +159,5 @@ cat > "$run_dir/eval-${role}.md" << EVALEOF

local dir="$1" node="$2" role="$3"
local run_dir="$dir/nodes/$node/run_1"
local run_id
run_id=$(current_run_id "$dir" "$node") || return 1
local run_dir="$dir/nodes/$node/$run_id"
mkdir -p "$run_dir"

@@ -191,2 +214,3 @@ cat > "$run_dir/eval-${role}.md" << EVALEOF

write_good_eval .harness review "backup${round}"
write_good_eval .harness review "skeptic-owner"
write_handshake .harness review "Round $round" "PASS"

@@ -202,2 +226,3 @@ $HARNESS transition --from review --to gate --verdict PASS --flow review --dir .harness 2>/dev/null

write_good_eval .harness review "backup4"
write_good_eval .harness review "skeptic-owner"
write_handshake .harness review "Round 4" "PASS"

@@ -218,3 +243,3 @@ TRANS_4=$($HARNESS transition --from review --to gate --verdict PASS --flow review --dir .harness 2>/dev/null || echo '{"allowed":false}')

write_good_eval .harness review analyst
write_good_eval .harness review architect
write_good_eval .harness review skeptic-owner
write_handshake .harness review "Clean review" "PASS"

@@ -221,0 +246,0 @@ $HARNESS transition --from review --to gate --verdict PASS --flow review --dir .harness 2>/dev/null

@@ -45,5 +45,5 @@ #!/bin/bash

artifacts=$(ls "$run_dir"/eval-*.md 2>/dev/null | python3 -c "
import sys, json
import pathlib, sys, json
files = [l.strip() for l in sys.stdin if l.strip()]
print(json.dumps([{'path': f, 'type': 'eval'} for f in files]))
print(json.dumps([{'path': 'run_1/' + pathlib.Path(f).name, 'type': 'eval'} for f in files]))
" 2>/dev/null || echo "[]")

@@ -70,2 +70,3 @@ fi

HSEOF
sync_run_handshakes "$dir"
}

@@ -72,0 +73,0 @@

@@ -14,8 +14,7 @@ #!/bin/bash

# - build-verify:code-review (node HAS caps) → NO WARN (control)
# - prompt-context with no flow-state + no --flow (template unresolvable) → WARN STILL fires
# - prompt-context with no flow-state + no --flow (template unresolvable) → FAIL CLOSED
#
# The last case is the preservation guard: we suppress the false positive without blanket-
# silencing the WARN. Template-unresolvable-yet-extensions-loaded is a real, actionable
# misconfig and must stay loud. The F2 raw-library WARN-once contract (extensions.test.mjs)
# covers the programmatic "forgot to pass caps" path and must remain green independently.
# CLI callers must name the state-selected node. Unresolvable templates and unknown nodes
# are rejected before extension routing; the raw-library WARN-once contract remains covered
# independently by extensions.test.mjs.

@@ -46,10 +45,21 @@ source "$(dirname "$0")/test-helpers.sh"

# init <flow> into a fresh subdir, fire prompt-context on <node>, capture stderr only.
# Init <flow> at <node>, then capture prompt-context stderr for that selected node.
warn_stderr() {
local flow="$1" node="$2" sub="sess-${flow}-${node}"
local flow="$1" node="$2" sub
sub="sess-${flow}-${node}"
rm -rf "$sub"; mkdir -p "$sub"
( cd "$sub" && $HARNESS init --flow "$flow" --dir . >/dev/null 2>&1 )
( cd "$sub" && $HARNESS init --flow "$flow" --entry "$node" --dir . >/dev/null 2>&1 )
mkdir -p "$sub/nodes/$node/run_1"
OPC_EXTENSIONS_DIR="$TMPDIR/exts" $HARNESS prompt-context --node "$node" --role tester --dir "$sub" 2>&1 1>/dev/null
}
assert_rejected() {
local desc="$1" rc="$2" output="$3"
if [ "$rc" -ne 0 ] && [ -n "$output" ]; then
echo " ✅ $desc"; PASS=$((PASS + 1))
else
echo " ❌ $desc — expected non-zero exit with diagnostic, got rc=$rc output='$output'"; FAIL=$((FAIL + 1))
fi
}
echo "=== F10: nodeCapabilities WARN fires only on unresolved-template path ==="

@@ -82,20 +92,24 @@

# ───────────────────────────────────────────────────────────────
# Preservation guard: no flow-state.json and no --flow → template cannot resolve →
# caps genuinely unknown while extensions are loaded → the WARN MUST still fire.
# No flow-state.json and no --flow is not a routable lifecycle invocation.
echo ""
echo "--- N5 (guard): unresolved template + extensions loaded → WARN STILL fires ---"
echo "--- N5 (guard): unresolved template + extensions loaded → FAIL CLOSED ---"
rm -rf bare; mkdir -p bare # no init: no flow-state.json
OUT=$(OPC_EXTENSIONS_DIR="$TMPDIR/exts" $HARNESS prompt-context --node review --role tester --dir bare 2>&1 1>/dev/null)
assert_warn_count "unresolved template → warns" 1 "$OUT"
set +e
OUT=$(OPC_EXTENSIONS_DIR="$TMPDIR/exts" $HARNESS prompt-context --node review --role tester --dir bare 2>&1)
RC=$?
set -e
assert_rejected "unresolved template is rejected" "$RC" "$OUT"
# ───────────────────────────────────────────────────────────────
# Node-typo guard: template resolves, but the requested node is NOT in template.nodes.
# An empty caps list here is NOT a legitimate "capless node" — it's a misconfig (typo /
# wrong --node). Suppressing the WARN would hide a loaded-but-no-match extension with zero
# diagnostics. So an unknown node must be treated as unresolved → WARN STILL fires.
# A node typo must be rejected, not degraded into a capless extension context.
echo ""
echo "--- N6 (guard): resolved template + unknown node → WARN STILL fires ---"
OUT=$(warn_stderr build-verify typo-node)
assert_warn_count "unknown node → warns" 1 "$OUT"
echo "--- N6 (guard): resolved template + unknown node → FAIL CLOSED ---"
rm -rf sess-build-verify-typo-node
$HARNESS init --flow build-verify --entry brief --dir sess-build-verify-typo-node >/dev/null 2>&1
set +e
OUT=$(OPC_EXTENSIONS_DIR="$TMPDIR/exts" $HARNESS prompt-context --node typo-node --role tester --dir sess-build-verify-typo-node 2>&1)
RC=$?
set -e
assert_rejected "unknown node is rejected" "$RC" "$OUT"
print_results

@@ -15,2 +15,3 @@ #!/bin/bash

rm -rf "$DIR"
trap 'rm -rf "$DIR"' EXIT
mkdir -p "$EXT_DIR/versioned-ext"

@@ -32,3 +33,6 @@

OPC_EXTENSIONS_DIR="$EXT_DIR" "$HARNESS" init --flow build-verify --entry brief --dir "$DIR/.harness" >/dev/null
(
cd "$REPO_DIR"
OPC_EXTENSIONS_DIR="$EXT_DIR" "$HARNESS" init --flow build-verify --entry brief --dir "$DIR/.harness" >/dev/null
)

@@ -49,6 +53,4 @@ VERSION=$(python3 - "$DIR/.harness/flow-state.json" <<'PY'

rm -rf "$DIR"
echo ""
echo "Extension version state tests: $PASS passed, $FAIL failed"
[ "$FAIL" -eq 0 ] || exit 1

@@ -74,2 +74,3 @@ #!/usr/bin/env bash

JSON
$HARNESS seal --node review --dir "$SESSION" >/dev/null 2>/dev/null
OUT=$($HARNESS validate 2>/dev/null)

@@ -100,2 +101,3 @@ check_json "validate without path uses latest current handshake" "d['valid']==True" "$OUT"

JSON
sync_run_handshakes ".harness"
OUT=$($HARNESS transition --from test-execute --to hotfix --verdict ITERATE --flow build-verify --dir .harness 2>/dev/null)

@@ -105,6 +107,37 @@ check_json "test-execute ITERATE routes to hotfix" "d['allowed']==True and d['next']=='hotfix'" "$OUT"

mkdir -p .harness/nodes/hotfix/run_1
mkdir -p .harness/nodes/test-design
cat > .harness/nodes/test-design/test-execution.json <<'JSON'
{ "testCommand": "printf retest > hotfix-retest.txt", "timeoutMs": 10000 }
mkdir -p .harness/nodes/test-design/run_1
cat > .harness/nodes/test-design/run_1/test-plan.md <<'PLAN'
# Test Plan
Run the hotfix retest command.
PLAN
cat > .harness/nodes/test-design/run_1/test-execution.json <<'JSON'
{ "runId": "run_1", "testCommand": "printf retest > hotfix-retest.txt", "timeoutMs": 10000 }
JSON
cat > .harness/nodes/test-design/run_1/handshake.json <<'JSON'
{
"nodeId": "test-design",
"nodeType": "review",
"runId": "run_1",
"status": "completed",
"verdict": "PASS",
"summary": "Retest command selected.",
"timestamp": "2026-01-01T00:00:00.000Z",
"artifacts": [{ "type": "test-plan", "path": "run_1/test-plan.md" }]
}
JSON
sync_run_handshakes ".harness"
python3 - <<'PY'
import json
p = ".harness/flow-state.json"
s = json.load(open(p))
s["history"] = [
{"nodeId": "test-design", "runId": "run_1", "timestamp": "2026-01-01T00:00:00.000Z"},
{"nodeId": "test-execute", "runId": "run_1", "timestamp": "2026-01-01T00:00:01.000Z"},
{"nodeId": "hotfix", "runId": "run_1", "timestamp": "2026-01-01T00:00:02.000Z"},
]
s["currentNode"] = "hotfix"
s["totalSteps"] = 3
json.dump(s, open(p, "w"), indent=2)
PY
printf 'Added aria-label only.\n' > .harness/nodes/hotfix/run_1/hotfix-report.md

@@ -129,2 +162,3 @@ cat > .harness/nodes/hotfix/handshake.json <<'JSON'

JSON
sync_run_handshakes ".harness"
OUT=$($HARNESS transition --from hotfix --to test-execute --verdict PASS --flow build-verify --dir .harness 2>/dev/null)

@@ -151,2 +185,3 @@ check_json "hotfix PASS routes back to test-execute evidence node" "d['allowed']==True and d['next']=='test-execute' and d['testCommandExecution']['executed']==True" "$OUT"

JSON
sync_run_handshakes ".harness"
OUT=$($HARNESS validate .harness/nodes/hotfix/handshake.json 2>/dev/null)

@@ -153,0 +188,0 @@ check_json "structural hotfix handshake is rejected" "d['valid']==False and any('hotfix.scope' in e for e in d['errors'])" "$OUT"

@@ -102,2 +102,3 @@ #!/bin/bash

HS
sync_run_handshakes .h-trans
sleep 1

@@ -139,3 +140,3 @@ OUT=$($HARNESS transition --from brief --to build --verdict PASS --flow build-verify --dir .h-trans 2>/dev/null)

assert_field_eq "hs missing" "$OUT" "allowed" "false"
assert_contains "handshake missing" "$OUT" "handshake.json missing"
assert_contains "handshake missing" "$OUT" "missing handshake for node 'brief' run 'run_1'"

@@ -153,2 +154,3 @@ echo ""

HS
sync_run_handshakes .h-trans3
OUT=$($HARNESS transition --from brief --to build --verdict PASS --flow build-verify --dir .h-trans3 2>/dev/null)

@@ -177,2 +179,3 @@ assert_field_eq "status not completed" "$OUT" "allowed" "false"

HS
sync_run_handshakes .h-trans4
OUT=$($HARNESS transition --from brief --to build --verdict PASS --flow build-verify --dir .h-trans4 2>/dev/null)

@@ -195,2 +198,3 @@ assert_field_eq "tamper detected" "$OUT" "allowed" "false"

HS
sync_run_handshakes .h-limit
OUT=$($HARNESS transition --from review --to gate --verdict PASS --flow review --dir .h-limit 2>/dev/null)

@@ -252,2 +256,3 @@ assert_field_eq "steps limit" "$OUT" "allowed" "false"

HS
sync_run_handshakes .h-fin
sleep 1

@@ -259,2 +264,3 @@ $HARNESS transition --from review --to gate --verdict PASS --flow review --dir .h-fin >/dev/null 2>/dev/null

HS
sync_run_handshakes .h-fin
OUT=$($HARNESS finalize --dir .h-fin)

@@ -283,3 +289,6 @@ assert_field_eq "finalized" "$OUT" "finalized" "true"

d = json.load(open('.h-strict/flow-state.json'))
d['history'].append({'nodeId': 'review', 'runId': 'run_1', 'timestamp': '2024-01-01T00:00:00Z'})
d['history'] = [
{'nodeId': 'review', 'runId': 'run_1', 'timestamp': '2024-01-01T00:00:00.000Z'},
{'nodeId': 'gate', 'runId': 'run_1', 'timestamp': '2024-01-01T00:01:00.000Z'},
]
json.dump(d, open('.h-strict/flow-state.json', 'w'), indent=2)

@@ -291,5 +300,6 @@ "

HS
sync_run_handshakes .h-strict
OUT=$($HARNESS finalize --dir .h-strict --strict)
assert_field_eq "strict fails" "$OUT" "finalized" "false"
assert_contains "missing upstream handshake" "$OUT" "handshake for review is missing"
assert_contains "missing upstream handshake" "$OUT" "missing handshake for node 'review' run 'run_1'"

@@ -296,0 +306,0 @@ echo ""

@@ -9,4 +9,24 @@ #!/bin/bash

mkdir -p "$HOME/.claude/flows"
cat > "$HOME/.claude/flows/idea-factory.json" << 'FIXTURE'
FLOW_DIR="$HOME/.claude/flows"
IDEA_FLOW="$FLOW_DIR/idea-factory.json"
IDEA_BACKUP="$TMPDIR/idea-factory.json.backup"
IDEA_EXISTED=0
mkdir -p "$FLOW_DIR"
if [ -e "$IDEA_FLOW" ] || [ -L "$IDEA_FLOW" ]; then
mv "$IDEA_FLOW" "$IDEA_BACKUP"
IDEA_EXISTED=1
fi
cleanup() {
rm -f "$IDEA_FLOW"
if [ "$IDEA_EXISTED" -eq 1 ]; then
mv "$IDEA_BACKUP" "$IDEA_FLOW"
fi
rm -rf "$TMPDIR"
}
trap cleanup EXIT
trap 'exit 129' HUP
trap 'exit 130' INT
trap 'exit 143' TERM
cat > "$IDEA_FLOW" << 'FIXTURE'
{

@@ -34,2 +54,5 @@ "nodes": ["discover", "validate", "build", "gate", "synthesize", "pitch"],

FIXTURE
if [ "${OPC_TEST_ABORT_AFTER_FLOW_FIXTURE:-0}" = "1" ]; then
exit 97
fi

@@ -173,7 +196,7 @@ jq_field() {

rm -rf .h-reentry && $HARNESS init --flow build-verify --dir .h-reentry >/dev/null 2>/dev/null
for i in 1 2 3; do
for i in 1 2 3 4 5; do
$HARNESS goto brief --dir .h-reentry >/dev/null
done
OUT=$($HARNESS goto brief --dir .h-reentry)
assert_contains "edge limit" "$OUT" "maxLoopsPerEdge"
assert_contains "node reentry limit" "$OUT" "maxNodeReentry"

@@ -226,2 +249,3 @@ echo ""

HS
sync_run_handshakes ".h-trans"
sleep 1

@@ -228,0 +252,0 @@ $HARNESS transition --from brief --to build --verdict PASS --flow build-verify --dir .h-trans 2>/dev/null

@@ -69,4 +69,2 @@ #!/bin/bash

echo "--- 1.1: No-args shows help ---"
OUT=$(node "$(cd "$(dirname "$0")/.." 2>/dev/null || echo "$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)")" 2>&1 || true)
# Use the HARNESS variable properly
OUT=$($HARNESS 2>&1 || true)

@@ -207,6 +205,7 @@ assert_contains "help output" "$OUT" "opc-harness"

rm -rf .h-fin5 && $HARNESS init --flow review --entry gate --dir .h-fin5 >/dev/null 2>/dev/null
mkdir -p .h-fin5/nodes/gate
mkdir -p .h-fin5/nodes/gate/run_1
cat > .h-fin5/nodes/gate/handshake.json << 'HS'
{"nodeId":"gate","nodeType":"gate","runId":"run_1","status":"failed","summary":"x","timestamp":"2024-01-01T00:00:00Z","artifacts":[]}
HS
cp .h-fin5/nodes/gate/handshake.json .h-fin5/nodes/gate/run_1/handshake.json
OUT=$($HARNESS finalize --dir .h-fin5 2>/dev/null)

@@ -220,9 +219,10 @@ assert_field_eq "finalize bad status" "$OUT" "finalized" "false"

rm -rf .h-fin6 && $HARNESS init --flow review --entry gate --dir .h-fin6 >/dev/null 2>/dev/null
mkdir -p .h-fin6/nodes/gate
mkdir -p .h-fin6/nodes/gate/run_1
echo "not json" > .h-fin6/nodes/gate/handshake.json
echo "not json" > .h-fin6/nodes/gate/run_1/handshake.json
OUT=$($HARNESS finalize --dir .h-fin6 2>/dev/null)
assert_field_eq "finalize corrupt hs" "$OUT" "finalized" "false"
assert_contains "corrupt hs msg" "$OUT" "cannot parse"
assert_contains "corrupt hs msg" "$OUT" "parse error"
print_results

@@ -90,3 +90,3 @@ #!/bin/bash

{"nodeId":"test-execute","nodeType":"execute","runId":"run_1","status":"completed","summary":"done",
"timestamp":"2024-01-01T00:00:00Z","artifacts":["ev.txt"],"findings":{"warning":1,"critical":0}}
"timestamp":"2024-01-01T00:00:00Z","artifacts":[{"type":"cli-output","path":"ev.txt"}],"findings":{"warning":1,"critical":0}}
HS

@@ -97,8 +97,19 @@ echo "evidence" > .h-bp/nodes/test-execute/ev.txt

rm -rf .h-bp2 && $HARNESS init --flow full-stack --entry gate-test --dir .h-bp2 >/dev/null 2>/dev/null
mkdir -p .h-bp2/nodes/test-execute
cat > .h-bp2/nodes/test-execute/handshake.json << 'HS'
mkdir -p .h-bp2/nodes/test-execute/run_1
cat > .h-bp2/nodes/test-execute/run_1/handshake.json << 'HS'
{"nodeId":"test-execute","nodeType":"execute","runId":"run_1","status":"completed","summary":"done",
"timestamp":"2024-01-01T00:00:00Z","artifacts":["ev.txt"],"findings":{"warning":1,"critical":0}}
"timestamp":"2024-01-01T00:00:00Z","artifacts":[{"type":"cli-output","path":"ev.txt"}],"findings":{"warning":1,"critical":0}}
HS
echo "evidence" > .h-bp2/nodes/test-execute/ev.txt
echo "evidence" > .h-bp2/nodes/test-execute/run_1/ev.txt
python3 -c "
import json
p='.h-bp2/flow-state.json'
s=json.load(open(p))
s['history']=[
{'nodeId':'test-execute','runId':'run_1','timestamp':'2024-01-01T00:00:00Z'},
{'nodeId':'gate-test','runId':'run_1','timestamp':'2024-01-01T00:00:01Z'},
]
s['totalSteps']=2
json.dump(s, open(p,'w'), indent=2)
"
OUT=$($HARNESS transition --from gate-test --to acceptance --verdict PASS --flow full-stack --dir .h-bp2 2>/dev/null)

@@ -111,8 +122,19 @@ assert_field_eq "PASS backlog check" "$OUT" "allowed" "false"

rm -rf .h-bp3 && $HARNESS init --flow full-stack --entry gate-test --dir .h-bp3 >/dev/null 2>/dev/null
mkdir -p .h-bp3/nodes/test-execute
cat > .h-bp3/nodes/test-execute/handshake.json << 'HS'
mkdir -p .h-bp3/nodes/test-execute/run_1
cat > .h-bp3/nodes/test-execute/run_1/handshake.json << 'HS'
{"nodeId":"test-execute","nodeType":"execute","runId":"run_1","status":"completed","summary":"done",
"timestamp":"2024-01-01T00:00:00Z","artifacts":["ev.txt"],"findings":{"warning":1,"critical":0}}
"timestamp":"2024-01-01T00:00:00Z","artifacts":[{"type":"cli-output","path":"ev.txt"}],"findings":{"warning":1,"critical":0}}
HS
echo "evidence" > .h-bp3/nodes/test-execute/ev.txt
echo "evidence" > .h-bp3/nodes/test-execute/run_1/ev.txt
python3 -c "
import json
p='.h-bp3/flow-state.json'
s=json.load(open(p))
s['history']=[
{'nodeId':'test-execute','runId':'run_1','timestamp':'2024-01-01T00:00:00Z'},
{'nodeId':'gate-test','runId':'run_1','timestamp':'2024-01-01T00:00:01Z'},
]
s['totalSteps']=2
json.dump(s, open(p,'w'), indent=2)
"
# Backlog exists but no entries from test-execute

@@ -171,8 +193,19 @@ cat > .h-bp3/backlog.md << 'BL'

# gate-test upstream = test-execute. Create handshake with no warnings to skip backlog check.
mkdir -p .h-esc4/nodes/test-execute
cat > .h-esc4/nodes/test-execute/handshake.json << 'HS'
mkdir -p .h-esc4/nodes/test-execute/run_1
cat > .h-esc4/nodes/test-execute/run_1/handshake.json << 'HS'
{"nodeId":"test-execute","nodeType":"execute","runId":"run_1","status":"completed","summary":"done",
"timestamp":"2024-01-01T00:00:00Z","artifacts":["ev.txt"],"findings":{"warning":0,"critical":0}}
"timestamp":"2024-01-01T00:00:00Z","artifacts":[{"type":"cli-output","path":"ev.txt"}],"findings":{"warning":0,"critical":0}}
HS
echo "evidence" > .h-esc4/nodes/test-execute/ev.txt
echo "evidence" > .h-esc4/nodes/test-execute/run_1/ev.txt
python3 -c "
import json
p='.h-esc4/flow-state.json'
s=json.load(open(p))
s['history']=[
{'nodeId':'test-execute','runId':'run_1','timestamp':'2024-01-01T00:00:00.000Z'},
{'nodeId':'gate-test','runId':'run_1','timestamp':'2024-01-01T00:01:00.000Z'},
]
s['totalSteps']=1
json.dump(s, open(p,'w'), indent=2)
"
OUT=$($HARNESS pass --dir .h-esc4 2>/dev/null)

@@ -179,0 +212,0 @@ assert_field_eq "pass gate→acceptance" "$OUT" "allowed" "true"

@@ -86,4 +86,5 @@ #!/bin/bash

rm -rf .h-vc2 && $HARNESS init --flow build-verify --dir .h-vc2 >/dev/null 2>/dev/null
mkdir -p .h-vc2/nodes/build
mkdir -p .h-vc2/nodes/build/run_1
echo "not json" > .h-vc2/nodes/build/handshake.json
echo "not json" > .h-vc2/nodes/build/run_1/handshake.json
# Add history so validator checks build's handshake

@@ -90,0 +91,0 @@ python3 -c "

@@ -143,2 +143,3 @@ #!/bin/bash

EOF
sync_run_handshakes .
# Validate should produce warning (softEvidence) not error

@@ -174,5 +175,5 @@ OUT=$($HARNESS validate nodes/exec-node/handshake.json 2>&1)

cd "$D5"
# Should fall back to strict (soft=false) → produce error not warning
# State-backed validation fails closed when session authority is corrupt.
OUT=$($HARNESS validate nodes/exec-node/handshake.json 2>/dev/null)
assert_contains "$OUT" "executor node missing evidence" "corrupt state → strict mode → error"
assert_contains "$OUT" "cannot parse flow-state.json" "corrupt state → authority error"
rm -rf "$D5"

@@ -212,28 +213,16 @@ cd /tmp

# ─────────────────────────────────────────────────────────────────
# GAP2-7: transition without prior flow-state.json → fresh state
# GAP2-7: transition without prior flow-state.json fails closed
# ─────────────────────────────────────────────────────────────────
echo ""
echo "── GAP2-7: transition creates fresh state when no flow-state.json"
echo "── GAP2-7: transition without init fails closed"
D7=$(mktemp -d)
mkdir -p "$D7/nodes/build"
# Write handshake for 'build' so pre-transition check passes
cat > "$D7/nodes/build/handshake.json" << 'EOF'
{
"nodeId": "build",
"nodeType": "build",
"runId": "run_1",
"status": "completed",
"summary": "built",
"timestamp": "2024-01-01T00:00:00Z",
"artifacts": [],
"verdict": null
}
EOF
cd "$D7"
# Transition without prior init — should create state
OUT=$($HARNESS transition --from build --to code-review --verdict PASS --flow build-verify --dir . 2>/dev/null)
assert_field_eq "$OUT" "['allowed']" "True" "transition without init creates fresh state"
# Verify state was created
test -f flow-state.json
assert_contains "$(cat flow-state.json)" "code-review" "fresh state has correct currentNode"
assert_field_eq "$OUT" "['allowed']" "False" "transition without init is rejected"
assert_contains "$OUT" "flow-state.json" "missing state diagnostic emitted"
if [ ! -e flow-state.json ] && [ ! -e nodes ]; then
echo "✅ rejected transition creates no state or node artifacts"; PASS=$((PASS+1))
else
echo "❌ rejected transition left state or node artifacts"; FAIL=$((FAIL+1))
fi
rm -rf "$D7"

@@ -240,0 +229,0 @@ cd /tmp

@@ -82,2 +82,3 @@ #!/bin/bash

EOF
sync_run_handshakes .
OUT=$($HARNESS transition --from build --to gate-check --verdict PASS --flow test-no-types --dir . 2>/dev/null)

@@ -99,2 +100,13 @@ assert_field_eq "$OUT" "['allowed']" "True" "transition from build to gate-check"

D9=$(mktemp -d)
mkdir -p "$HOME/.claude/flows"
cat > "$HOME/.claude/flows/test-soft-ev.json" << 'EOF'
{
"nodes": ["exec-node", "gate"],
"edges": {"exec-node": {"PASS": "gate"}, "gate": {"PASS": null}},
"limits": {"maxLoopsPerEdge": 3, "maxTotalSteps": 10, "maxNodeReentry": 5},
"nodeTypes": {"exec-node": "execute", "gate": "gate"},
"softEvidence": true,
"opc_compat": ">=0.5"
}
EOF
cd "$D9"

@@ -117,2 +129,3 @@ $HARNESS init --flow test-soft-ev --dir . > /dev/null 2>&1

EOF
sync_run_handshakes .
# Transition should succeed (softEvidence → warning not error)

@@ -139,7 +152,29 @@ OUT=$($HARNESS transition --from exec-node --to gate --verdict PASS --flow test-soft-ev --dir . 2>&1)

# Advance to gate node with proper handshakes
mkdir -p nodes/build nodes/code-review nodes/test-execute
for n in build code-review test-execute; do
mkdir -p nodes/build/run_1 nodes/code-review/run_1 nodes/test-design/run_1 nodes/test-execute/run_1
for n in build code-review test-design test-execute; do
nt="build"
[ "$n" = "code-review" ] && nt="review"
[ "$n" = "test-design" ] && nt="review"
[ "$n" = "test-execute" ] && nt="execute"
artifacts="[]"
if [ "$nt" = "review" ]; then
echo "# A" > "nodes/$n/run_1/eval-a.md"
echo "# B" > "nodes/$n/run_1/eval-b.md"
artifacts='[{"type":"eval","path":"run_1/eval-a.md"},{"type":"eval","path":"run_1/eval-b.md"}]'
fi
if [ "$nt" = "execute" ]; then
echo "ok" > "nodes/$n/run_1/cli-output.txt"
artifacts='[{"type":"cli-output","path":"run_1/cli-output.txt"}]'
fi
cat > "nodes/$n/handshake.json" << EOF
{"nodeId":"$n","nodeType":"build","runId":"run_1","status":"completed","summary":"ok","timestamp":"2024-01-01T00:00:00Z","artifacts":[],"verdict":null}
{"nodeId":"$n","nodeType":"$nt","runId":"run_1","status":"completed","summary":"ok","timestamp":"2024-01-01T00:00:00Z","artifacts":$artifacts,"verdict":null}
EOF
python3 - "nodes/$n/handshake.json" "nodes/$n/run_1/handshake.json" <<'PY'
import json, sys
d=json.load(open(sys.argv[1]))
for a in d["artifacts"]:
if a["path"].startswith("run_1/"):
a["path"]=a["path"][len("run_1/"):]
json.dump(d, open(sys.argv[2], "w"))
PY
done

@@ -158,2 +193,3 @@ # Manually advance state to gate

echo "NOT JSON AT ALL" > nodes/test-execute/handshake.json
echo "NOT JSON AT ALL" > nodes/test-execute/run_1/handshake.json
# Try gate ITERATE transition — should detect corrupt upstream during backlog check

@@ -245,3 +281,3 @@ OUT=$($HARNESS transition --from gate --to brief --verdict ITERATE --flow build-verify --dir . 2>/dev/null)

# ─────────────────────────────────────────────────────────────────
# GAP2-15: cmdVerify — non-ENOENT read error
# GAP2-15: cmdVerify — deterministic non-ENOENT read error
# ─────────────────────────────────────────────────────────────────

@@ -251,32 +287,30 @@ echo ""

D15=$(mktemp -d)
mkdir "$D15/unreadable"
chmod 000 "$D15/unreadable" 2>/dev/null || true
# Try to read a file inside an unreadable directory
if ! $HARNESS verify "$D15/unreadable/eval.md" > /dev/null 2>&1; then
echo "✅ verify exits non-zero on permission error"; PASS=$((PASS+1))
mkdir "$D15/not-a-file"
set +e
OUT=$($HARNESS verify "$D15/not-a-file" 2>&1)
RC=$?
set -e
if [ "$RC" -ne 0 ] && echo "$OUT" | grep -q "Cannot read"; then
echo "✅ verify rejects a directory with a non-ENOENT diagnostic"; PASS=$((PASS+1))
else
# chmod may not work on this platform (root, container, macOS quirk)
echo "⏭️ verify handles unreadable (chmod not enforced on this OS — skip)"; PASS=$((PASS+1)) # platform-dependent skip
echo "❌ verify expected non-zero with non-ENOENT diagnostic, got rc=$RC: $OUT"; FAIL=$((FAIL+1))
fi
chmod 755 "$D15/unreadable" 2>/dev/null || true
rm -rf "$D15"
# ─────────────────────────────────────────────────────────────────
# GAP2-16: cmdSynthesize — unreadable node dir (catch)
# GAP2-16: cmdSynthesize — deterministic unreadable node path (catch)
# ─────────────────────────────────────────────────────────────────
echo ""
echo "── GAP2-16: synthesize unreadable node dir"
echo "── GAP2-16: synthesize unreadable node path"
D16=$(mktemp -d)
mkdir -p "$D16/nodes/broken-node"
# Make node dir unreadable
chmod 000 "$D16/nodes/broken-node" 2>/dev/null || true
if ! $HARNESS synthesize "$D16" --node broken-node 2>/dev/null; then
echo "✅ synthesize exits non-zero for unreadable node dir"; PASS=$((PASS+1))
mkdir -p "$D16/nodes"
printf 'not a directory\n' > "$D16/nodes/broken-node"
OUT=$($HARNESS synthesize "$D16" --node broken-node 2>/dev/null)
if echo "$OUT" | grep -q '"verdict":"BLOCKED"' && echo "$OUT" | grep -q "cannot read node dir"; then
echo "✅ synthesize reports BLOCKED for an unreadable node path"; PASS=$((PASS+1))
else
# chmod may not work on this platform (root, container, macOS quirk)
echo "⏭️ synthesize handles unreadable node dir (chmod not enforced — skip)"; PASS=$((PASS+1)) # platform-dependent skip
echo "❌ synthesize expected BLOCKED read diagnostic, got: $OUT"; FAIL=$((FAIL+1))
fi
chmod 755 "$D16/nodes/broken-node" 2>/dev/null || true
rm -rf "$D16"
print_results

@@ -92,2 +92,3 @@ #!/bin/bash

EOF
sync_run_handshakes .
$HARNESS transition --from code-review --to test-design --verdict PASS --flow build-verify --dir . > /dev/null 2>&1

@@ -94,0 +95,0 @@ # Now viz should show entryNode code-review as ✅ (not ▶)

@@ -148,3 +148,3 @@ #!/usr/bin/env bash

# Manually build state at gate with proper history
mkdir -p nodes/build nodes/code-review nodes/test-execute
mkdir -p nodes/build/run_1 nodes/code-review/run_1 nodes/test-execute/run_1
# build handshake with warnings (triggers backlog check)

@@ -154,9 +154,19 @@ cat > nodes/build/handshake.json << 'EOF'

EOF
cp nodes/build/handshake.json nodes/build/run_1/handshake.json
echo "# A" > nodes/code-review/run_1/eval-a.md
echo "# B" > nodes/code-review/run_1/eval-b.md
cat > nodes/code-review/handshake.json << 'EOF'
{"nodeId":"code-review","nodeType":"review","runId":"run_1","status":"completed","summary":"ok","timestamp":"2024-01-01T00:00:00Z","artifacts":[],"verdict":null}
{"nodeId":"code-review","nodeType":"review","runId":"run_1","status":"completed","summary":"ok","timestamp":"2024-01-01T00:00:00Z","artifacts":[{"type":"eval","path":"run_1/eval-a.md"},{"type":"eval","path":"run_1/eval-b.md"}],"verdict":null}
EOF
cat > nodes/code-review/run_1/handshake.json << 'EOF'
{"nodeId":"code-review","nodeType":"review","runId":"run_1","status":"completed","summary":"ok","timestamp":"2024-01-01T00:00:00Z","artifacts":[{"type":"eval","path":"eval-a.md"},{"type":"eval","path":"eval-b.md"}],"verdict":null}
EOF
# test-execute handshake is the upstream of gate — make it have warnings then corrupt it
echo "ok" > nodes/test-execute/run_1/cli-output.txt
cat > nodes/test-execute/handshake.json << 'EOF'
{"nodeId":"test-execute","nodeType":"execute","runId":"run_1","status":"completed","summary":"ok","timestamp":"2024-01-01T00:00:00Z","artifacts":[],"verdict":null,"findings":{"warning":2}}
{"nodeId":"test-execute","nodeType":"execute","runId":"run_1","status":"completed","summary":"ok","timestamp":"2024-01-01T00:00:00Z","artifacts":[{"type":"cli-output","path":"run_1/cli-output.txt"}],"verdict":null,"findings":{"warning":2}}
EOF
cat > nodes/test-execute/run_1/handshake.json << 'EOF'
{"nodeId":"test-execute","nodeType":"execute","runId":"run_1","status":"completed","summary":"ok","timestamp":"2024-01-01T00:00:00Z","artifacts":[{"type":"cli-output","path":"cli-output.txt"}],"verdict":null,"findings":{"warning":2}}
EOF
# Advance state to gate

@@ -179,2 +189,3 @@ python3 -c "

echo "CORRUPT JSON {{{{" > nodes/test-execute/handshake.json
echo "CORRUPT JSON {{{{" > nodes/test-execute/run_1/handshake.json
# ITERATE from gate triggers backlog check on upstream test-execute

@@ -221,3 +232,3 @@ OUT=$($HARNESS transition --from gate --to brief --verdict ITERATE --flow build-verify --dir . 2>/dev/null)

assert_field_eq "$OUT" "['allowed']" "False" "missing upstream handshake blocks gate transition"
assert_contains "$OUT" "handshake for test-execute is missing" "missing upstream handshake reported"
assert_contains "$OUT" "missing handshake for node 'test-execute' run 'run_1'" "missing upstream handshake reported"
rm -rf "$D"

@@ -224,0 +235,0 @@ cd /tmp

@@ -192,2 +192,3 @@ #!/usr/bin/env bash

HS
sync_run_handshakes .
$HARNESS transition --from review --to gate --verdict PASS --flow review --dir . > /dev/null 2>&1

@@ -194,0 +195,0 @@ OUT=$($HARNESS viz --flow review --dir . 2>/dev/null)

@@ -100,4 +100,6 @@ #!/usr/bin/env bash

echo "── 8.1: viz with FAIL+ITERATE edges shows FAIL in ASCII"
OUT=$($HARNESS viz --flow build-verify 2>/dev/null)
VIZ_DIR=$(mktemp -d)
OUT=$($HARNESS viz --flow build-verify --dir "$VIZ_DIR" 2>/dev/null)
assert_contains "$OUT" "FAIL" "8.1a: viz ASCII shows FAIL edge for gate"
rm -rf "$VIZ_DIR"

@@ -124,2 +126,3 @@ echo ""

EOF
sync_run_handshakes .
sleep 2

@@ -140,13 +143,18 @@ STDERR_FILE=$(mktemp)

echo ""
echo "── 9.1: validate-chain skips missing handshake for currentNode"
echo "── 9.1: validate-chain rejects missing currentNode handshake"
D=$(mktemp -d)
cd "$D"
$HARNESS init --flow build-verify --entry build --dir . > /dev/null 2>&1
mkdir -p nodes/build
cat > nodes/build/handshake.json << 'EOF'
{"nodeId":"build","nodeType":"build","status":"completed","summary":"done","timestamp":"2024-01-01T00:00:00Z"}
mkdir -p nodes/build/run_1
cat > nodes/build/run_1/handshake.json << 'EOF'
{"nodeId":"build","nodeType":"build","runId":"run_1","status":"completed","summary":"done","timestamp":"2024-01-01T00:00:00Z","artifacts":[]}
EOF
cp nodes/build/run_1/handshake.json nodes/build/handshake.json
cat > nodes/build/run_1/build.log << 'EOF'
build complete
EOF
$HARNESS transition --from build --to code-review --verdict PASS --flow build-verify --dir . > /dev/null 2>&1
OUT=$($HARNESS validate-chain --dir . 2>/dev/null)
assert_field_eq "$OUT" "['valid']" "True" "9.1a: currentNode without handshake is not an error"
assert_field_eq "$OUT" "['valid']" "False" "9.1a: currentNode without handshake fails closed"
assert_contains "$OUT" "code-review/run_1: .*status missing or invalid" "9.1b: error names exact selected run"
cd "$ORIG_DIR"

@@ -153,0 +161,0 @@ rm -rf "$D"

@@ -131,5 +131,7 @@ #!/usr/bin/env bash

echo "── 17.1: viz --json includes FAIL and ITERATE loopbacks"
OUT=$($HARNESS viz --flow build-verify --json 2>/dev/null)
VIZ_DIR=$(mktemp -d)
OUT=$($HARNESS viz --flow build-verify --dir "$VIZ_DIR" --json 2>/dev/null)
assert_contains "$OUT" '"FAIL"' "17.1a: viz --json has FAIL loopback"
assert_contains "$OUT" '"ITERATE"' "17.1b: viz --json has ITERATE loopback"
rm -rf "$VIZ_DIR"

@@ -136,0 +138,0 @@ # ═══════════════════════════════════════════════════════════════════

@@ -41,36 +41,30 @@ #!/usr/bin/env bash

# ═══════════════════════════════════════════════════════════════════
echo "=== PART 1: 🔴 HIGH — transition without init (fresh state creation) ==="
# flow-transition.mjs:73-86 — else branch when flow-state.json doesn't exist
echo "=== PART 1: 🔴 HIGH — transition without init fails closed ==="
# A transition may only advance initialized, validated state.
# ═══════════════════════════════════════════════════════════════════
echo ""
echo "── 1.1: transition from gate without init creates fresh state"
# Gates skip pre-transition handshake check, so this path is reachable
echo "── 1.1: gate transition without init is rejected without mutation"
D=$(mktemp -d)
cd "$D"
mkdir -p nodes
# No init! Direct transition from gate node
OUT=$($HARNESS transition --from gate --to brief --verdict FAIL --flow build-verify --dir . 2>/dev/null)
assert_field_eq "$OUT" "['allowed']" "True" "1.1a: transition without init succeeds (fresh state created)"
assert_field_eq "$OUT" "['next']" "brief" "1.1b: next node is brief"
# Verify state was created with correct structure
assert_contains "$(cat flow-state.json)" '"version": "1.0"' "1.1c: fresh state has version"
assert_contains "$(cat flow-state.json)" '"flowTemplate": "build-verify"' "1.1d: fresh state has correct flow"
assert_contains "$(cat flow-state.json)" '"entryNode": "brief"' "1.1e: fresh state entryNode = first template node"
assert_contains "$(cat flow-state.json)" '"maxTotalSteps": 25' "1.1f: fresh state has limits from template"
assert_field_eq "$OUT" "['allowed']" "False" "1.1a: gate transition without init is blocked"
assert_contains "$OUT" "flow-state.json" "1.1b: error identifies missing state"
if [ ! -e "flow-state.json" ] && [ ! -e "nodes" ]; then
echo " ✅ 1.1c: rejection leaves no state or node artifacts"; PASS=$((PASS+1))
else
echo " ❌ 1.1c: rejection mutated the harness directory"; FAIL=$((FAIL+1))
fi
echo ""
echo "── 1.2: transition without init — non-gate node blocked by handshake check"
echo "── 1.2: non-gate transition without init is rejected without mutation"
D2=$(mktemp -d)
cd "$D2"
mkdir -p nodes
OUT=$($HARNESS transition --from build --to code-review --verdict PASS --flow build-verify --dir . 2>/dev/null)
assert_field_eq "$OUT" "['allowed']" "False" "1.2a: non-gate transition without init blocked"
assert_contains "$OUT" "handshake.json missing" "1.2b: blocked by pre-transition handshake check"
# Fresh state path (L73-86) IS exercised: mkdirSync creates nodes/ dir even though
# the function returns before writing flow-state.json to disk.
if [ -d "nodes" ]; then
echo " ✅ 1.2c: fresh state path exercised (nodes/ dir created at L74)"; PASS=$((PASS+1))
assert_field_eq "$OUT" "['allowed']" "False" "1.2a: non-gate transition without init is blocked"
assert_contains "$OUT" "flow-state.json" "1.2b: error identifies missing state"
if [ ! -e "flow-state.json" ] && [ ! -e "nodes" ]; then
echo " ✅ 1.2c: rejection leaves no state or node artifacts"; PASS=$((PASS+1))
else
echo " ❌ 1.2c: nodes/ dir not created — fresh state path not exercised"; FAIL=$((FAIL+1))
echo " ❌ 1.2c: rejection mutated the harness directory"; FAIL=$((FAIL+1))
fi

@@ -140,4 +134,16 @@ cd "$ORIG_DIR"

# Write corrupt handshake for test-execute (upstream of gate)
mkdir -p nodes/test-execute
mkdir -p nodes/test-execute/run_1
echo "NOT VALID JSON {{{" > nodes/test-execute/handshake.json
echo "NOT VALID JSON {{{" > nodes/test-execute/run_1/handshake.json
python3 - <<'PY'
import json
p = "flow-state.json"
s = json.load(open(p))
s["history"] = [
{"nodeId": "test-execute", "runId": "run_1", "timestamp": "2024-01-01T00:00:00.000Z"},
{"nodeId": "gate", "runId": "run_1", "timestamp": "2024-01-01T00:00:01.000Z"},
]
s["totalSteps"] = 2
json.dump(s, open(p, "w"), indent=2)
PY
# Try to transition gate → brief (ITERATE)

@@ -144,0 +150,0 @@ # Wait for idempotency window

@@ -161,6 +161,7 @@ #!/usr/bin/env bash

$HARNESS init --flow review --entry gate --dir . > /dev/null 2>&1
mkdir -p nodes/gate
mkdir -p nodes/gate/run_1
cat > nodes/gate/handshake.json << 'EOF'
{"nodeId":"gate","nodeType":"gate","runId":"run_1","status":"in_progress","summary":"not done yet","timestamp":"2024-01-01T00:00:00Z","artifacts":[]}
EOF
cp nodes/gate/handshake.json nodes/gate/run_1/handshake.json
OUT=$($HARNESS finalize --dir . 2>/dev/null)

@@ -167,0 +168,0 @@ assert_contains "$OUT" "in_progress" "13.1a: finalize rejects non-completed status"

@@ -18,3 +18,6 @@ #!/bin/bash

data = json.load(open(path))
data["history"] = [{"nodeId": "test-execute", "runId": "run_1", "timestamp": "2026-01-01T00:00:00.000Z"}]
data["history"] = [
{"nodeId": "test-execute", "runId": "run_1", "timestamp": "2026-01-01T00:00:00.000Z"},
{"nodeId": "gate", "runId": "run_1", "timestamp": "2026-01-01T00:01:00.000Z"},
]
data["currentNode"] = "gate"

@@ -34,2 +37,3 @@ json.dump(data, open(path, "w"), indent=2)

{"nodeId": "test-execute", "runId": "run_2", "timestamp": "2026-01-01T00:01:00.000Z"},
{"nodeId": "gate", "runId": "run_1", "timestamp": "2026-01-01T00:02:00.000Z"},
]

@@ -43,5 +47,6 @@ data["currentNode"] = "gate"

local dir="$1" run_id="$2"
python3 - "$dir/nodes/test-execute/handshake.json" "$run_id" <<'PY'
echo "ok" > "$dir/nodes/test-execute/$run_id/cli-output.txt"
python3 - "$dir/nodes/test-execute/handshake.json" "$dir/nodes/test-execute/$run_id/handshake.json" "$run_id" <<'PY'
import json, sys
path, run_id = sys.argv[1], sys.argv[2]
canonical_path, run_path, run_id = sys.argv[1], sys.argv[2], sys.argv[3]
data = {

@@ -55,5 +60,14 @@ "nodeId": "test-execute",

"timestamp": "2026-01-01T00:00:00.000Z",
"artifacts": [{"type": "report", "path": f"{run_id}/report.json"}]
"artifacts": [
{"type": "report", "path": f"{run_id}/report.json"},
{"type": "cli-output", "path": f"{run_id}/cli-output.txt"}
]
}
open(path, "w").write(json.dumps(data))
open(canonical_path, "w").write(json.dumps(data))
run_data = dict(data)
run_data["artifacts"] = [
{"type": "report", "path": "report.json"},
{"type": "cli-output", "path": "cli-output.txt"}
]
open(run_path, "w").write(json.dumps(run_data))
PY

@@ -60,0 +74,0 @@ }

#!/usr/bin/env bash
set -euo pipefail
# Test: goto maxLoopsPerEdge enforcement
# Test: goto audit counts and maxNodeReentry enforcement

@@ -45,3 +45,3 @@ SCRIPT_DIR="$(cd "$(dirname "$0")/.." && pwd)"

echo "=== TEST GROUP 1: goto respects maxLoopsPerEdge ==="
echo "=== TEST GROUP 1: goto ignores repair-edge limits but respects node reentry ==="

@@ -54,16 +54,12 @@ setup_flow "run1"

R2=$(H goto build --dir run1)
check "goto back to build succeeds" 'echo "$R2" | grep -q "\"goto\":\"build\""'
R2=$(H goto code-review --dir run1)
check "first same-edge goto succeeds" 'echo "$R2" | grep -q "\"goto\":\"code-review\""'
R3=$(H goto code-review --dir run1)
check "second goto code-review succeeds" 'echo "$R3" | grep -q "\"goto\":\"code-review\""'
H goto build --dir run1 > /dev/null
R4=$(H goto code-review --dir run1)
R5=$(H goto code-review --dir run1)
check "third goto code-review succeeds" 'echo "$R5" | grep -q "\"goto\":\"code-review\""'
check "4th same-edge goto still succeeds" 'echo "$R5" | grep -q "\"goto\":\"code-review\""'
# 4th goto code-review from build — should fail (maxLoopsPerEdge=3)
H goto build --dir run1 > /dev/null
R7=$(H goto code-review --dir run1)
check "4th goto code-review blocked" 'echo "$R7" | grep -q "maxLoopsPerEdge"'
R6=$(H goto code-review --dir run1)
check "6th code-review entry is blocked" 'echo "$R6" | grep -q "maxNodeReentry"'
check "manual edge traversals remain audited" 'grep -q "code-review→code-review" "$TMPD/run1/flow-state.json"'

@@ -70,0 +66,0 @@ echo ""

@@ -64,2 +64,6 @@ #!/bin/bash

local artifacts="[]"
local extra=""
if [ "$node" = "test-design" ]; then
extra=', "testCommand": "printf ok"'
fi
if [ -n "$run_dir" ]; then

@@ -82,5 +86,6 @@ artifacts=$(ls "$run_dir"/eval-*.md 2>/dev/null | python3 -c "

"artifacts": $artifacts,
"findings": null
"findings": null$extra
}
EOF
sync_run_handshakes "$dir"
}

@@ -120,2 +125,3 @@

EOF
sync_run_handshakes .harness
$HARNESS transition --from test-execute --to gate-test --verdict PASS --flow full-stack --dir .harness 2>/dev/null

@@ -253,3 +259,3 @@ # Now at gate-test. Write upstream review evals to test-design for synthesize

write_good_eval .harness test-design engineer
write_complete_test_plan .harness/nodes/test-design/test-plan.md
write_complete_test_plan .harness/nodes/test-design/run_1/test-plan.md
write_handshake .harness test-design "Test design done" "PASS"

@@ -321,2 +327,3 @@ TRANS=$($HARNESS transition --from test-design --to test-execute --verdict PASS --flow build-verify --dir .harness 2>/dev/null)

EOF
sync_run_handshakes .harness
TRANS=$($HARNESS transition --from review --to gate --verdict PASS --flow review --dir .harness 2>/dev/null)

@@ -323,0 +330,0 @@ if echo "$TRANS" | grep -q "no eval-type artifacts\|eval artifacts"; then

@@ -7,3 +7,7 @@ #!/bin/bash

# ── Repo-relative harness path (portable, no hardcoded install path) ──
HARNESS="node $(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)/bin/opc-harness.mjs"
OPC_HARNESS_BIN="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)/bin/opc-harness.mjs"
opc_harness() {
node "$OPC_HARNESS_BIN" "$@"
}
HARNESS=opc_harness

@@ -17,3 +21,13 @@ # ── Counters ──

TMPDIR=$(mktemp -d)
trap "rm -rf $TMPDIR" EXIT
if [ -n "${OPC_TEST_HOME_OVERRIDE:-}" ]; then
export HOME="$OPC_TEST_HOME_OVERRIDE"
else
export HOME="$TMPDIR/home"
fi
mkdir -p "$HOME"
if [ -n "${OPC_TEST_HOME_PROBE:-}" ]; then
printf '%s\n' "$HOME" > "$OPC_TEST_HOME_PROBE"
fi
trap 'rm -rf "$TMPDIR"' EXIT
trap 'exit 130' HUP INT TERM
cd "$TMPDIR"

@@ -99,1 +113,45 @@ }

}
sync_run_handshakes() {
local dir="$1"
python3 - "$dir" <<'PY'
import json
import pathlib
import sys
root = pathlib.Path(sys.argv[1])
nodes = root / "nodes"
if not nodes.exists():
raise SystemExit(0)
for canonical in nodes.glob("*/handshake.json"):
try:
data = json.loads(canonical.read_text())
except Exception:
continue
run_id = data.get("runId")
if not isinstance(run_id, str) or not run_id.startswith("run_"):
continue
run_data = dict(data)
run_dir = canonical.parent / run_id
artifacts = []
for artifact in data.get("artifacts") or []:
if not isinstance(artifact, dict):
artifacts.append(artifact)
continue
copied = dict(artifact)
path = copied.get("path")
prefix = f"{run_id}/"
if isinstance(path, str) and path.startswith(prefix):
copied["path"] = path[len(prefix):]
elif isinstance(path, str) and not pathlib.Path(path).is_absolute():
node_relative = canonical.parent / path
run_relative = run_dir / path
if node_relative.exists() and not run_relative.exists():
copied["path"] = f"../{path}"
artifacts.append(copied)
run_data["artifacts"] = artifacts
run_dir.mkdir(parents=True, exist_ok=True)
(run_dir / "handshake.json").write_text(json.dumps(run_data, indent=2) + "\n")
PY
}

@@ -52,2 +52,17 @@ #!/bin/bash

MD
mkdir -p "$HARNESS/nodes/build/run_1"
cat > "$HARNESS/flow-state.json" <<EOF
{
"version": "1.0",
"flowTemplate": "flow",
"currentNode": "build",
"entryNode": "build",
"tier": "functional",
"flowStartedAt": "2026-01-01T00:00:00.000Z",
"totalSteps": 0,
"history": [],
"edgeCounts": {},
"_flow_file": "$FLOW_FILE"
}
EOF

@@ -54,0 +69,0 @@ HARNESS_BIN="node $REPO_ROOT/bin/opc-harness.mjs"

@@ -85,6 +85,11 @@ #!/bin/bash

{
"version": "1.0",
"currentNode": "build",
"flow": "preflight-test",
"entryNode": "build",
"flowTemplate": "preflight-test",
"tier": "functional",
"flowStartedAt": "2026-01-01T00:00:00.000Z",
"_flow_file": "$FLOW_FILE",
"totalSteps": 0,
"history": [],
"edgeCounts": {},

@@ -244,6 +249,11 @@ "reentryCount": {}

{
"currentNode": "build",
"flow": "noop-flow",
"version": "1.0",
"currentNode": "code-review",
"entryNode": "code-review",
"flowTemplate": "noop-flow",
"tier": "functional",
"flowStartedAt": "2026-01-01T00:00:00.000Z",
"_flow_file": "$NOOP_FLOW",
"totalSteps": 0,
"history": [],
"edgeCounts": {},

@@ -253,2 +263,3 @@ "reentryCount": {}

EOF
mkdir -p "$NOOP_HARNESS/nodes/code-review/run_1"
NOOP_OUT=$(OPC_BREAKER_STATE=disabled $HARNESS_BIN node-preflight --node code-review --dir "$NOOP_HARNESS" --flow-file "$NOOP_FLOW" 2>/dev/null)

@@ -255,0 +266,0 @@ if echo "$NOOP_OUT" | grep -q '"preflightResults":0'; then

@@ -29,7 +29,8 @@ #!/bin/bash

write_quick_build() {
mkdir -p .harness/nodes/build
mkdir -p .harness/nodes/build/run_1
cat > .harness/nodes/build/handshake.json <<'EOF'
{"nodeId":"build","nodeType":"build","runId":"run_1","status":"completed","verdict":"PASS","summary":"built","timestamp":"2026-01-01T00:01:00.000Z","artifacts":[{"type":"code","path":"x"}]}
{"nodeId":"build","nodeType":"build","runId":"run_1","status":"completed","verdict":"PASS","summary":"built","timestamp":"2026-01-01T00:01:00.000Z","artifacts":[{"type":"code","path":"run_1/x"}]}
EOF
touch .harness/nodes/build/x
touch .harness/nodes/build/run_1/x
sync_run_handshakes .harness
}

@@ -44,2 +45,3 @@

EOF
sync_run_handshakes .harness
}

@@ -55,2 +57,3 @@

EOF
sync_run_handshakes .harness
}

@@ -57,0 +60,0 @@

@@ -24,9 +24,10 @@ #!/usr/bin/env bash

mkdir -p "$dir/nodes/review/run_1"
printf '# Reviewer A\n\n[WARNING] src/app.js:12 — Report hides warning finding\n→ Render it in the final report\nReasoning: The warning must stay visible after recovery.\nVERDICT: ITERATE FINDINGS[1]\n' > "$dir/nodes/review/run_1/eval-a.md"
printf '# Reviewer A\n\n[SUGGESTION] src/app.js:12 — Report hides warning finding\n→ Render it in the final report\nReasoning: The warning must stay visible after recovery.\nVERDICT: PASS FINDINGS[1]\n' > "$dir/nodes/review/run_1/eval-a.md"
printf '# Reviewer B\n\n[SUGGESTION] src/app.js:18 — Add recovery context\n→ Include cumulative findings in prompt context\nReasoning: Compaction needs prior findings.\nVERDICT: PASS FINDINGS[1]\n' > "$dir/nodes/review/run_1/eval-b.md"
printf '# Legacy Review\n\n## Finding 1 — Real structured issue title\n**Severity**: 🔴\n**Location**: src/legacy.js:7\n**R2 Status**: ⚠️\n\nVERDICT: FAIL FINDINGS[1]\n' > "$dir/nodes/review/run_1/eval-legacy.md"
printf '# Skeptic Owner\n\n[SUGGESTION] src/app.js:22 — Keep recovery observable\n→ Preserve cumulative findings after transition\nReasoning: Mandatory reviewer presence should not hide recovery evidence.\nVERDICT: PASS FINDINGS[1]\n' > "$dir/nodes/review/run_1/eval-skeptic-owner.md"
printf '# Legacy Review\n\n## Finding 1 — Real structured issue title\n**Severity**: 🔵\n**Location**: src/legacy.js:7\n**R2 Status**: ✅\n\nVERDICT: PASS FINDINGS[1]\n' > "$dir/nodes/review/run_1/eval-legacy.md"
mkdir -p "$dir/nodes/review/run_2"
printf '# Reviewer C\n\n[WARNING] src/retry.js:33 — Retry run finding stays visible\n→ Preserve loopback findings per run\nReasoning: Retry runs must not be hidden by node-level de-duplication.\nVERDICT: ITERATE FINDINGS[1]\n' > "$dir/nodes/review/run_2/eval-c.md"
printf '{"nodeId":"review","nodeType":"review","runId":"run_1","status":"completed","summary":"done","timestamp":"2026-06-20T00:00:00Z","artifacts":[{"type":"eval","path":"run_1/eval-a.md"},{"type":"eval","path":"run_1/eval-b.md"}],"verdict":"PASS"}\n' > "$dir/nodes/review/handshake.json"
printf '{"nodeId":"review","runId":"run_1","status":"completed","fixes_applied":["Bound report parser to canonical eval severity parsing"]}\n' > "$dir/nodes/review/run_1/handshake.json"
printf '{"nodeId":"review","nodeType":"review","runId":"run_1","status":"completed","summary":"done","timestamp":"2026-06-20T00:00:00Z","artifacts":[{"type":"eval","path":"run_1/eval-a.md"},{"type":"eval","path":"run_1/eval-b.md"},{"type":"eval","path":"run_1/eval-skeptic-owner.md"}],"verdict":"PASS","fixes_applied":["Bound report parser to canonical eval severity parsing"]}\n' > "$dir/nodes/review/handshake.json"
printf '{"nodeId":"review","nodeType":"review","runId":"run_1","status":"completed","summary":"done","timestamp":"2026-06-20T00:00:00Z","artifacts":[{"type":"eval","path":"eval-a.md"},{"type":"eval","path":"eval-b.md"},{"type":"eval","path":"eval-skeptic-owner.md"}],"verdict":"PASS","fixes_applied":["Bound report parser to canonical eval severity parsing"]}\n' > "$dir/nodes/review/run_1/handshake.json"
}

@@ -46,3 +47,4 @@

check "cumulative findings include legacy structured title" 'grep -q "Real structured issue title" .harness/cumulative-findings.md'
check "cumulative findings include retry run" 'grep -q "Retry run finding stays visible" .harness/cumulative-findings.md'
check "cumulative findings lists retry run as forensic orphan" 'grep -q "review/run_2" .harness/cumulative-findings.md'
check "cumulative findings excludes orphan retry text" '! grep -q "Retry run finding stays visible" .harness/cumulative-findings.md'
check "cumulative findings include execution fix" 'grep -q "Bound report parser" .harness/cumulative-findings.md'

@@ -55,2 +57,3 @@

check "prompt-context includes legacy structured title" 'printf "%s" "$PROMPT_APPEND" | grep -q "Real structured issue title"'
check "prompt-context excludes orphan retry text" '! printf "%s" "$PROMPT_APPEND" | grep -q "Retry run finding stays visible"'

@@ -61,6 +64,6 @@ $HARNESS transition --from gate --to null --verdict PASS --flow review --dir .harness > /dev/null 2>&1

check "viz shows completed terminal state" 'printf "%s" "$VIZ" | grep -q "FLOW COMPLETED at gate"'
check "viz no longer marks terminal as current" '! printf "%s" "$VIZ" | grep -q "▶ gate"'
check "viz json exposes completion" 'printf "%s" "$VIZ_JSON" | grep -q "\"completed\": true"'
check "viz json exposes terminal node" 'printf "%s" "$VIZ_JSON" | grep -q "\"terminalNode\": \"gate\""'
check "viz shows completed terminal state" 'grep -q "FLOW COMPLETED at gate" <<< "$VIZ"'
check "viz no longer marks terminal as current" '! grep -q "▶ gate" <<< "$VIZ"'
check "viz json exposes completion" 'grep -q "\"completed\": true" <<< "$VIZ_JSON"'
check "viz json exposes terminal node" 'grep -q "\"terminalNode\": \"gate\"" <<< "$VIZ_JSON"'

@@ -67,0 +70,0 @@ node "$ROOT/bin/opc-report.mjs" --dir .harness --output report.html --title "Recovery Report" > /dev/null

@@ -20,2 +20,3 @@ #!/bin/bash

$HARNESS init --flow build-verify --entry brief --dir .harness 2>/dev/null
mkdir -p .harness/nodes/brief/run_1
STATE=$(cat .harness/flow-state.json)

@@ -58,3 +59,5 @@ TPL=$(echo "$STATE" | python3 -c "import sys,json; print(json.load(sys.stdin).get('flowTemplate',''))" 2>/dev/null)

echo "--- 4: per-node capabilities (build ≠ brief) ---"
OUT=$($HARNESS prompt-context --node build --role implementer --dir .harness 2>/dev/null)
$HARNESS init --flow build-verify --entry build --dir .harness-build >/dev/null 2>/dev/null
mkdir -p .harness-build/nodes/build/run_1
OUT=$($HARNESS prompt-context --node build --role implementer --dir .harness-build 2>/dev/null)
BUILD_CAPS=$(echo "$OUT" | python3 -c "import sys,json; print(','.join(json.load(sys.stdin).get('nodeCapabilities',[])))" 2>/dev/null)

@@ -115,11 +118,13 @@ # build has design-system-injection@1 but NOT design-spec-conformance@1 (that's brief-only)

# ── 5: unknown node → empty caps (no crash) ──
echo "--- 5: unknown node → empty caps ---"
OUT=$($HARNESS prompt-context --node nonexistent --role implementer --dir .harness 2>/dev/null)
CAPS=$(echo "$OUT" | python3 -c "import sys,json; print(','.join(json.load(sys.stdin).get('nodeCapabilities',[])))" 2>/dev/null)
if [ -z "$CAPS" ]; then
echo " ✅ unknown node → empty caps (no crash)"
# ── 5: unknown node is rejected before capability routing ──
echo "--- 5: unknown node → fail closed ---"
set +e
OUT=$($HARNESS prompt-context --node nonexistent --role implementer --dir .harness 2>&1)
RC=$?
set -e
if [ "$RC" -ne 0 ] && echo "$OUT" | grep -q "nonexistent"; then
echo " ✅ unknown node rejected with diagnostic"
PASS=$((PASS + 1))
else
echo " ❌ unexpected caps: '$CAPS'"
echo " ❌ expected unknown-node rejection, got rc=$RC: $OUT"
FAIL=$((FAIL + 1))

@@ -126,0 +131,0 @@ fi

@@ -29,8 +29,9 @@ #!/bin/bash

local dir="$1" verdict="$2"
mkdir -p "$dir/nodes/code-review"
mkdir -p "$dir/nodes/code-review/run_1"
cat > "$dir/nodes/code-review/handshake.json" <<EOF
{"nodeId":"code-review","nodeType":"review","runId":"run_1","status":"completed","verdict":"$verdict","summary":"review found work for producer","timestamp":"2026-01-01T00:00:00.000Z","artifacts":[{"type":"eval","path":"eval-a.md"},{"type":"eval","path":"eval-b.md"}]}
{"nodeId":"code-review","nodeType":"review","runId":"run_1","status":"completed","verdict":"$verdict","summary":"review found work for producer","timestamp":"2026-01-01T00:00:00.000Z","artifacts":[{"type":"eval","path":"run_1/eval-a.md"},{"type":"eval","path":"run_1/eval-b.md"}]}
EOF
echo "# Eval A" > "$dir/nodes/code-review/eval-a.md"
echo "# Eval B" > "$dir/nodes/code-review/eval-b.md"
sync_run_handshakes "$dir"
echo "# Eval A" > "$dir/nodes/code-review/run_1/eval-a.md"
echo "# Eval B" > "$dir/nodes/code-review/run_1/eval-b.md"
}

@@ -37,0 +38,0 @@

@@ -32,2 +32,25 @@ #!/bin/bash

write_build_state() {
local dir="$1"
mkdir -p "$dir"
cat > "$dir/flow-state.json" <<'JSON'
{
"version": "1.0",
"flowTemplate": "build-verify",
"currentNode": "build",
"entryNode": "build",
"totalSteps": 1,
"maxTotalSteps": 25,
"maxLoopsPerEdge": 3,
"maxNodeReentry": 5,
"history": [{ "nodeId": "build", "runId": "run_1", "timestamp": "2026-01-01T00:00:00.000Z" }],
"edgeCounts": {},
"repairEdgeCounts": {},
"_written_by": "opc-harness",
"_write_nonce": "test-run-handshake-fallback",
"_last_modified": "2026-01-01T00:00:00.000Z"
}
JSON
}
write_run_handshake_with_failing_report() {

@@ -59,5 +82,6 @@ local dir="$1"

write_run_handshake "$DIR/.harness"
write_build_state "$DIR/.harness"
OUT=$($HARNESS validate "$DIR/.harness/nodes/build/handshake.json" 2>/dev/null)
if echo "$OUT" | grep -q '"valid":true'; then
ok "validate falls back to latest run_N/handshake.json"
ok "validate falls back to state-selected run_N/handshake.json"
else

@@ -74,3 +98,3 @@ bad "validate did not use run fallback: $OUT"

if echo "$OUT" | grep -q '"allowed":true'; then
ok "transition falls back to latest run_N/handshake.json"
ok "transition falls back to state-selected run_N/handshake.json"
else

@@ -82,3 +106,3 @@ bad "transition did not use run fallback: $OUT"

if echo "$OUT" | grep -q '"valid":true'; then
ok "validate-chain accepts latest run_N/handshake.json"
ok "validate-chain accepts state-selected run_N/handshake.json"
else

@@ -85,0 +109,0 @@ bad "validate-chain did not use run fallback: $OUT"

@@ -14,50 +14,90 @@ #!/bin/bash

# ── Helper: advance brief→build→code-review→test-design→test-execute→gate ──
advance_to_gate() {
mkdir -p .harness/nodes/brief/run_1
cat > .harness/nodes/brief/handshake.json <<'EOF'
{"nodeId":"brief","nodeType":"brief","runId":"run_1","status":"completed","verdict":"PASS","summary":"brief done","timestamp":"2026-01-01T00:00:30.000Z","artifacts":[{"type":"brief","path":"build-brief.md"},{"type":"report","path":"run_1/brief-lint-result.json"}]}
EOF
write_golden_brief .harness/nodes/brief/build-brief.md
echo '{"pass":true}' > .harness/nodes/brief/run_1/brief-lint-result.json
$HARNESS transition --from brief --to build --verdict PASS --flow build-verify --dir .harness 2>/dev/null >/dev/null
current_run_id() {
local node="$1"
python3 - .harness/flow-state.json "$node" <<'PY'
import json, sys
state = json.load(open(sys.argv[1]))
history = state.get("history", [])
tail = history[-1] if history else None
if tail and tail.get("nodeId") == sys.argv[2] and tail.get("runId"):
print(tail["runId"])
elif (state.get("totalSteps") == 0 and not history and
state.get("currentNode") == state.get("entryNode") == sys.argv[2] and
state.get("flowStartedAt")):
print("run_1")
else:
raise SystemExit(1)
PY
}
mkdir -p .harness/nodes/build
cat > .harness/nodes/build/handshake.json <<'EOF'
{"nodeId":"build","nodeType":"build","runId":"run_1","status":"completed","verdict":"PASS","summary":"ok","timestamp":"2026-01-01T00:01:00.000Z","artifacts":[{"type":"code","path":"x"}]}
advance_build_to_gate() {
local run_id
run_id=$(current_run_id build) || return 1
mkdir -p ".harness/nodes/build/$run_id"
cat > .harness/nodes/build/handshake.json <<EOF
{"nodeId":"build","nodeType":"build","runId":"$run_id","status":"completed","verdict":"PASS","summary":"ok","timestamp":"2026-01-01T00:01:00.000Z","artifacts":[{"type":"code","path":"$run_id/x"}]}
EOF
touch .harness/nodes/build/x
touch ".harness/nodes/build/$run_id/x"
sync_run_handshakes ".harness"
$HARNESS transition --from build --to code-review --verdict PASS --flow build-verify --dir .harness 2>/dev/null >/dev/null
mkdir -p .harness/nodes/code-review
cat > .harness/nodes/code-review/handshake.json <<'EOF'
{"nodeId":"code-review","nodeType":"review","runId":"run_1","status":"completed","verdict":"PASS","summary":"ok","timestamp":"2026-01-01T00:02:00.000Z","artifacts":[{"type":"eval","path":"eval-a.md"},{"type":"eval","path":"eval-b.md"}]}
run_id=$(current_run_id code-review) || return 1
mkdir -p ".harness/nodes/code-review/$run_id"
cat > .harness/nodes/code-review/handshake.json <<EOF
{"nodeId":"code-review","nodeType":"review","runId":"$run_id","status":"completed","verdict":"PASS","summary":"ok","timestamp":"2026-01-01T00:02:00.000Z","artifacts":[{"type":"eval","path":"$run_id/eval-a.md"},{"type":"eval","path":"$run_id/eval-b.md"}]}
EOF
echo "# Eval A - review findings" > .harness/nodes/code-review/eval-a.md
echo "# Eval B - secondary review" > .harness/nodes/code-review/eval-b.md
echo "# Eval A - review findings" > ".harness/nodes/code-review/$run_id/eval-a.md"
echo "# Eval B - secondary review" > ".harness/nodes/code-review/$run_id/eval-b.md"
sync_run_handshakes ".harness"
$HARNESS transition --from code-review --to test-design --verdict PASS --flow build-verify --dir .harness 2>/dev/null >/dev/null
mkdir -p .harness/nodes/test-design
cat > .harness/nodes/test-design/handshake.json <<'EOF'
{"nodeId":"test-design","nodeType":"review","runId":"run_1","status":"completed","verdict":"PASS","summary":"ok","timestamp":"2026-01-01T00:03:00.000Z","artifacts":[{"type":"eval","path":"eval-a.md"},{"type":"eval","path":"eval-b.md"}]}
run_id=$(current_run_id test-design) || return 1
mkdir -p ".harness/nodes/test-design/$run_id"
cat > .harness/nodes/test-design/handshake.json <<EOF
{"nodeId":"test-design","nodeType":"review","runId":"$run_id","status":"completed","verdict":"PASS","summary":"ok","timestamp":"2026-01-01T00:03:00.000Z","artifacts":[{"type":"eval","path":"$run_id/eval-a.md"},{"type":"eval","path":"$run_id/eval-b.md"},{"type":"test-plan","path":"$run_id/test-plan.md"}]}
EOF
echo "# Eval A - test design findings" > .harness/nodes/test-design/eval-a.md
echo "# Eval B - test design secondary" > .harness/nodes/test-design/eval-b.md
write_complete_test_plan .harness/nodes/test-design/test-plan.md
echo "# Eval A - test design findings" > ".harness/nodes/test-design/$run_id/eval-a.md"
echo "# Eval B - test design secondary" > ".harness/nodes/test-design/$run_id/eval-b.md"
write_complete_test_plan ".harness/nodes/test-design/$run_id/test-plan.md"
printf '%s\n' "{\"nodeId\":\"test-design\",\"runId\":\"$run_id\",\"testCommand\":\"node -e \\\"process.exit(0)\\\"\",\"prerequisites\":[\"fixture\"]}" > ".harness/nodes/test-design/$run_id/test-execution.json"
sync_run_handshakes ".harness"
$HARNESS transition --from test-design --to test-execute --verdict PASS --flow build-verify --dir .harness 2>/dev/null >/dev/null
mkdir -p .harness/nodes/test-execute
cat > .harness/nodes/test-execute/handshake.json <<'EOF'
{"nodeId":"test-execute","nodeType":"execute","runId":"run_1","status":"completed","verdict":"PASS","summary":"ok","timestamp":"2026-01-01T00:04:00.000Z","artifacts":[{"type":"test-result","path":"o"}]}
run_id=$(current_run_id test-execute) || return 1
mkdir -p ".harness/nodes/test-execute/$run_id"
cat > .harness/nodes/test-execute/handshake.json <<EOF
{"nodeId":"test-execute","nodeType":"execute","runId":"$run_id","status":"completed","verdict":"PASS","summary":"ok","timestamp":"2026-01-01T00:04:00.000Z","artifacts":[{"type":"test-result","path":"$run_id/o"}]}
EOF
touch .harness/nodes/test-execute/o
touch ".harness/nodes/test-execute/$run_id/o"
sync_run_handshakes ".harness"
$HARNESS transition --from test-execute --to gate --verdict PASS --flow build-verify --dir .harness 2>/dev/null >/dev/null
}
# ── Helper: advance brief→build→code-review→test-design→test-execute→gate ──
advance_to_gate() {
local run_id
run_id=$(current_run_id brief) || return 1
mkdir -p ".harness/nodes/brief/$run_id"
cat > .harness/nodes/brief/handshake.json <<EOF
{"nodeId":"brief","nodeType":"brief","runId":"$run_id","status":"completed","verdict":"PASS","summary":"brief done","timestamp":"2026-01-01T00:00:30.000Z","artifacts":[{"type":"brief","path":"build-brief.md"},{"type":"report","path":"$run_id/brief-lint-result.json"}]}
EOF
write_golden_brief .harness/nodes/brief/build-brief.md
if [ "$run_id" != "run_1" ]; then
printf '\n## Iteration Delta\n- Applied the prior gate findings for %s.\n' "$run_id" >> .harness/nodes/brief/build-brief.md
fi
echo '{"pass":true}' > ".harness/nodes/brief/$run_id/brief-lint-result.json"
sync_run_handshakes ".harness"
$HARNESS transition --from brief --to build --verdict PASS --flow build-verify --dir .harness 2>/dev/null >/dev/null
advance_build_to_gate
}
loopback_gate_to_brief() {
mkdir -p .harness/nodes/gate
cat > .harness/nodes/gate/handshake.json <<'EOF'
{"nodeId":"gate","nodeType":"gate","runId":"run_1","status":"completed","verdict":"FAIL","summary":"fail","timestamp":"2026-01-01T00:05:00.000Z","artifacts":[]}
local run_id
run_id=$(current_run_id gate) || return 1
mkdir -p ".harness/nodes/gate/$run_id"
cat > .harness/nodes/gate/handshake.json <<EOF
{"nodeId":"gate","nodeType":"gate","runId":"$run_id","status":"completed","verdict":"FAIL","summary":"fail","timestamp":"2026-01-01T00:05:00.000Z","artifacts":[]}
EOF
echo "- fix" > .harness/backlog.md
sync_run_handshakes ".harness"
$HARNESS transition --from gate --to brief --verdict FAIL --flow build-verify --dir .harness 2>/dev/null >/dev/null

@@ -78,28 +118,43 @@ }

# ── Test 1: after 3 loopbacks, edges are blocked at limit ──
echo "1. After 3 loops, brief→build edge (count=3) is blocked on 4th attempt"
mkdir -p .harness/nodes/brief/run_1
cat > .harness/nodes/brief/handshake.json <<'EOF'
{"nodeId":"brief","nodeType":"brief","runId":"run_1","status":"completed","verdict":"PASS","summary":"brief done","timestamp":"2026-01-01T00:00:30.000Z","artifacts":[{"type":"brief","path":"build-brief.md"},{"type":"report","path":"run_1/brief-lint-result.json"}]}
# ── Test 1: forward PASS remains available after 3 repair loops ──
echo "1. After 3 repairs, the 4th brief→build forward PASS remains available"
BRIEF_RUN=$(current_run_id brief)
mkdir -p ".harness/nodes/brief/$BRIEF_RUN"
cat > .harness/nodes/brief/handshake.json <<EOF
{"nodeId":"brief","nodeType":"brief","runId":"$BRIEF_RUN","status":"completed","verdict":"PASS","summary":"brief done","timestamp":"2026-01-01T00:00:30.000Z","artifacts":[{"type":"brief","path":"build-brief.md"},{"type":"report","path":"$BRIEF_RUN/brief-lint-result.json"}]}
EOF
write_golden_brief .harness/nodes/brief/build-brief.md
echo '{"pass":true}' > .harness/nodes/brief/run_1/brief-lint-result.json
printf '\n## Iteration Delta\n- Applied the prior gate findings for %s.\n' "$BRIEF_RUN" >> .harness/nodes/brief/build-brief.md
echo '{"pass":true}' > ".harness/nodes/brief/$BRIEF_RUN/brief-lint-result.json"
sync_run_handshakes ".harness"
TRANS=$($HARNESS transition --from brief --to build --verdict PASS --flow build-verify --dir .harness 2>/dev/null || true)
ALLOWED=$(echo "$TRANS" | python3 -c "import sys,json; print(json.load(sys.stdin).get('allowed', True))" 2>/dev/null)
if [ "$ALLOWED" = "False" ]; then
echo " ✅ 4th traversal of brief→build blocked (maxLoopsPerEdge=3)"
ALLOWED=$(echo "$TRANS" | python3 -c "import sys,json; print(json.load(sys.stdin).get('allowed', False))" 2>/dev/null)
if [ "$ALLOWED" = "True" ]; then
echo " ✅ 4th forward traversal allowed"
PASS=$((PASS + 1))
else
echo " ❌ was allowed: $TRANS"
echo " ❌ was blocked: $TRANS"
FAIL=$((FAIL + 1))
fi
# ── Test 2: check reason mentions limit ──
echo "2. Blocked reason mentions edge limit"
REASON=$(echo "$TRANS" | python3 -c "import sys,json; print(json.load(sys.stdin).get('reason',''))" 2>/dev/null)
if echo "$REASON" | grep -qi "edge\|loop\|limit\|max"; then
# The explicit transition above already entered build; continue from that state.
advance_build_to_gate
GATE_RUN=$(current_run_id gate)
mkdir -p ".harness/nodes/gate/$GATE_RUN"
cat > .harness/nodes/gate/handshake.json <<EOF
{"nodeId":"gate","nodeType":"gate","runId":"$GATE_RUN","status":"completed","verdict":"FAIL","summary":"fail","timestamp":"2026-01-01T00:05:00.000Z","artifacts":[]}
EOF
echo "- fix" > .harness/backlog.md
sync_run_handshakes ".harness"
REPAIR=$($HARNESS transition --from gate --to brief --verdict FAIL --flow build-verify --dir .harness 2>/dev/null || true)
# ── Test 2: fourth semantic repair is blocked by maxLoopsPerEdge ──
echo "2. Fourth gate→brief repair is blocked by maxLoopsPerEdge"
REPAIR_ALLOWED=$(echo "$REPAIR" | python3 -c "import sys,json; print(json.load(sys.stdin).get('allowed', True))" 2>/dev/null)
REASON=$(echo "$REPAIR" | python3 -c "import sys,json; print(json.load(sys.stdin).get('reason',''))" 2>/dev/null)
if [ "$REPAIR_ALLOWED" = "False" ] && echo "$REASON" | grep -q "maxLoopsPerEdge"; then
echo " ✅ reason: $REASON"
PASS=$((PASS + 1))
else
echo " ❌ reason: $REASON"
echo " ❌ repair was not blocked: $REPAIR"
FAIL=$((FAIL + 1))

@@ -106,0 +161,0 @@ fi

@@ -72,4 +72,8 @@ #!/bin/bash

# Skip review to get to gate
$HARNESS skip --dir .harness --flow review 2>/dev/null >/dev/null
# Complete review to get to gate with valid upstream authority
mkdir -p .harness/nodes/review/run_1
echo "# Eval A" > .harness/nodes/review/run_1/eval-a.md
echo "# Eval B" > .harness/nodes/review/run_1/eval-b.md
$HARNESS seal --node review --dir .harness 2>/dev/null >/dev/null
$HARNESS transition --from review --to gate --verdict PASS --flow review --dir .harness 2>/dev/null >/dev/null
NODE3=$(python3 -c "import json; print(json.load(open('.harness/flow-state.json'))['currentNode'])")

@@ -76,0 +80,0 @@ if [ "$NODE3" = "gate" ]; then

@@ -49,3 +49,3 @@ #!/bin/bash

# Need handshake for build node
mkdir -p .harness/nodes/build
mkdir -p .harness/nodes/build/run_1/src
cat > .harness/nodes/build/handshake.json <<'EOF'

@@ -60,6 +60,7 @@ {

"timestamp": "2026-01-01T00:00:00.000Z",
"artifacts": [{"type":"code","path":"src/app.tsx"}]
"artifacts": [{"type":"code","path":"run_1/src/app.tsx"}]
}
EOF
mkdir -p .harness/nodes/build/src && touch .harness/nodes/build/src/app.tsx
touch .harness/nodes/build/run_1/src/app.tsx
sync_run_handshakes ".harness"

@@ -91,26 +92,30 @@ TRANS=$($HARNESS transition --from build --to code-review --verdict PASS --flow build-verify --dir .harness 2>/dev/null)

# Transition code-review → test-design
mkdir -p .harness/nodes/code-review
mkdir -p .harness/nodes/code-review/run_1
cat > .harness/nodes/code-review/handshake.json <<'EOF'
{"nodeId":"code-review","nodeType":"review","runId":"run_1","status":"completed","verdict":"PASS","summary":"Review passed","timestamp":"2026-01-01T00:00:00.000Z","artifacts":[{"type":"eval","path":"eval-frontend.md"},{"type":"eval","path":"eval-backend.md"}]}
{"nodeId":"code-review","nodeType":"review","runId":"run_1","status":"completed","verdict":"PASS","summary":"Review passed","timestamp":"2026-01-01T00:00:00.000Z","artifacts":[{"type":"eval","path":"run_1/eval-frontend.md"},{"type":"eval","path":"run_1/eval-backend.md"}]}
EOF
touch .harness/nodes/code-review/eval-frontend.md
touch .harness/nodes/code-review/eval-backend.md
touch .harness/nodes/code-review/run_1/eval-frontend.md
touch .harness/nodes/code-review/run_1/eval-backend.md
sync_run_handshakes ".harness"
$HARNESS transition --from code-review --to test-design --verdict PASS --flow build-verify --dir .harness 2>/dev/null >/dev/null
# Transition test-design → test-execute
mkdir -p .harness/nodes/test-design
mkdir -p .harness/nodes/test-design/run_1
cat > .harness/nodes/test-design/handshake.json <<'EOF'
{"nodeId":"test-design","nodeType":"review","runId":"run_1","status":"completed","verdict":"PASS","summary":"Tests designed","timestamp":"2026-01-01T00:00:00.000Z","artifacts":[{"type":"eval","path":"eval-a.md"},{"type":"eval","path":"eval-b.md"}]}
{"nodeId":"test-design","nodeType":"review","runId":"run_1","status":"completed","verdict":"PASS","summary":"Tests designed","timestamp":"2026-01-01T00:00:00.000Z","artifacts":[{"type":"eval","path":"run_1/eval-a.md"},{"type":"eval","path":"run_1/eval-b.md"},{"type":"test-plan","path":"run_1/test-plan.md"},{"type":"test-command","path":"run_1/test-execution.json"}]}
EOF
touch .harness/nodes/test-design/eval-a.md
touch .harness/nodes/test-design/eval-b.md
write_complete_test_plan .harness/nodes/test-design/test-plan.md
touch .harness/nodes/test-design/run_1/eval-a.md
touch .harness/nodes/test-design/run_1/eval-b.md
write_complete_test_plan .harness/nodes/test-design/run_1/test-plan.md
printf '%s\n' '{"nodeId":"test-design","runId":"run_1","testCommand":"node -e \"process.exit(0)\"","prerequisites":["fixture"]}' > .harness/nodes/test-design/run_1/test-execution.json
sync_run_handshakes ".harness"
$HARNESS transition --from test-design --to test-execute --verdict PASS --flow build-verify --dir .harness 2>/dev/null >/dev/null
# Transition test-execute → gate
mkdir -p .harness/nodes/test-execute
mkdir -p .harness/nodes/test-execute/run_1
cat > .harness/nodes/test-execute/handshake.json <<'EOF'
{"nodeId":"test-execute","nodeType":"execute","runId":"run_1","status":"completed","verdict":"PASS","summary":"Tests passed","timestamp":"2026-01-01T00:00:00.000Z","artifacts":[{"type":"test-result","path":"output.txt"}]}
{"nodeId":"test-execute","nodeType":"execute","runId":"run_1","status":"completed","verdict":"PASS","summary":"Tests passed","timestamp":"2026-01-01T00:00:00.000Z","artifacts":[{"type":"test-result","path":"run_1/output.txt"}]}
EOF
touch .harness/nodes/test-execute/output.txt
touch .harness/nodes/test-execute/run_1/output.txt
sync_run_handshakes ".harness"
$HARNESS transition --from test-execute --to gate --verdict PASS --flow build-verify --dir .harness 2>/dev/null >/dev/null

@@ -117,0 +122,0 @@

@@ -15,31 +15,36 @@ #!/bin/bash

# Advance to gate
mkdir -p .harness/nodes/build
mkdir -p .harness/nodes/build/run_1
cat > .harness/nodes/build/handshake.json <<'EOF'
{"nodeId":"build","nodeType":"build","runId":"run_1","status":"completed","verdict":"PASS","summary":"ok","timestamp":"2026-01-01T00:01:00.000Z","artifacts":[{"type":"code","path":"x"}]}
{"nodeId":"build","nodeType":"build","runId":"run_1","status":"completed","verdict":"PASS","summary":"ok","timestamp":"2026-01-01T00:01:00.000Z","artifacts":[{"type":"code","path":"run_1/x"}]}
EOF
touch .harness/nodes/build/x
touch .harness/nodes/build/run_1/x
sync_run_handshakes ".harness"
$HARNESS transition --from build --to code-review --verdict PASS --flow build-verify --dir .harness 2>/dev/null >/dev/null
mkdir -p .harness/nodes/code-review
mkdir -p .harness/nodes/code-review/run_1
cat > .harness/nodes/code-review/handshake.json <<'EOF'
{"nodeId":"code-review","nodeType":"review","runId":"run_1","status":"completed","verdict":"PASS","summary":"ok","timestamp":"2026-01-01T00:02:00.000Z","artifacts":[{"type":"eval","path":"eval-a.md"},{"type":"eval","path":"eval-b.md"}]}
{"nodeId":"code-review","nodeType":"review","runId":"run_1","status":"completed","verdict":"PASS","summary":"ok","timestamp":"2026-01-01T00:02:00.000Z","artifacts":[{"type":"eval","path":"run_1/eval-a.md"},{"type":"eval","path":"run_1/eval-b.md"}]}
EOF
echo "# Eval A" > .harness/nodes/code-review/eval-a.md
echo "# Eval B" > .harness/nodes/code-review/eval-b.md
echo "# Eval A" > .harness/nodes/code-review/run_1/eval-a.md
echo "# Eval B" > .harness/nodes/code-review/run_1/eval-b.md
sync_run_handshakes ".harness"
$HARNESS transition --from code-review --to test-design --verdict PASS --flow build-verify --dir .harness 2>/dev/null >/dev/null
mkdir -p .harness/nodes/test-design
mkdir -p .harness/nodes/test-design/run_1
cat > .harness/nodes/test-design/handshake.json <<'EOF'
{"nodeId":"test-design","nodeType":"review","runId":"run_1","status":"completed","verdict":"PASS","summary":"ok","timestamp":"2026-01-01T00:03:00.000Z","artifacts":[{"type":"eval","path":"eval-a.md"},{"type":"eval","path":"eval-b.md"}]}
{"nodeId":"test-design","nodeType":"review","runId":"run_1","status":"completed","verdict":"PASS","summary":"ok","timestamp":"2026-01-01T00:03:00.000Z","artifacts":[{"type":"eval","path":"run_1/eval-a.md"},{"type":"eval","path":"run_1/eval-b.md"},{"type":"test-plan","path":"run_1/test-plan.md"},{"type":"test-command","path":"run_1/test-execution.json"}]}
EOF
echo "# Eval A" > .harness/nodes/test-design/eval-a.md
echo "# Eval B" > .harness/nodes/test-design/eval-b.md
write_complete_test_plan .harness/nodes/test-design/test-plan.md
echo "# Eval A" > .harness/nodes/test-design/run_1/eval-a.md
echo "# Eval B" > .harness/nodes/test-design/run_1/eval-b.md
write_complete_test_plan .harness/nodes/test-design/run_1/test-plan.md
printf '%s\n' '{"nodeId":"test-design","runId":"run_1","testCommand":"node -e \"process.exit(0)\"","prerequisites":["fixture"]}' > .harness/nodes/test-design/run_1/test-execution.json
sync_run_handshakes ".harness"
$HARNESS transition --from test-design --to test-execute --verdict PASS --flow build-verify --dir .harness 2>/dev/null >/dev/null
mkdir -p .harness/nodes/test-execute
mkdir -p .harness/nodes/test-execute/run_1
cat > .harness/nodes/test-execute/handshake.json <<'EOF'
{"nodeId":"test-execute","nodeType":"execute","runId":"run_1","status":"completed","verdict":"PASS","summary":"ok","timestamp":"2026-01-01T00:04:00.000Z","artifacts":[{"type":"test-result","path":"o"}]}
{"nodeId":"test-execute","nodeType":"execute","runId":"run_1","status":"completed","verdict":"PASS","summary":"ok","timestamp":"2026-01-01T00:04:00.000Z","artifacts":[{"type":"test-result","path":"run_1/o"}]}
EOF
touch .harness/nodes/test-execute/o
touch .harness/nodes/test-execute/run_1/o
sync_run_handshakes ".harness"
$HARNESS transition --from test-execute --to gate --verdict PASS --flow build-verify --dir .harness 2>/dev/null >/dev/null

@@ -79,2 +84,3 @@

echo "- Fix null reference" > .harness/backlog.md
sync_run_handshakes ".harness"
TRANS=$($HARNESS transition --from gate --to brief --verdict FAIL --flow build-verify --dir .harness 2>/dev/null)

@@ -81,0 +87,0 @@ ALLOWED=$(echo "$TRANS" | python3 -c "import sys,json; print(json.load(sys.stdin)['allowed'])")

@@ -13,4 +13,5 @@ #!/bin/bash

echo " GOT: $(echo "$haystack" | head -5)"
fi
}
fi
sync_run_handshakes "$dir"
}

@@ -120,2 +121,3 @@ assert_not_contains() {

fi
sync_run_handshakes "$dir"
}

@@ -137,4 +139,4 @@

write_handshake "." "gate" "gate" "completed"
# Now delete review handshake to simulate missing
rm -f nodes/review/handshake.json
# Now delete review handshake to simulate missing selected evidence.
rm -f nodes/review/handshake.json nodes/review/run_1/handshake.json
OUT=$($HARNESS finalize --dir . --strict 2>/dev/null || true)

@@ -170,2 +172,3 @@ assert_field_eq "$OUT" "['finalized']" "False" "6a: --strict rejects with missing handshake"

EOF
cp nodes/review/handshake.json nodes/review/run_1/handshake.json
# Write completed handshake for gate (terminal)

@@ -208,4 +211,4 @@ write_handshake "." "gate" "gate" "completed"

$HARNESS transition --from review --to gate --verdict PASS --flow review --dir . > /dev/null 2>&1
# Delete review handshake — should still finalize without --strict
rm -f nodes/review/handshake.json
# Delete review handshake — non-strict finalization keeps the compatibility path.
rm -f nodes/review/handshake.json nodes/review/run_1/handshake.json
write_handshake "." "gate" "gate" "completed"

@@ -230,6 +233,7 @@ OUT=$($HARNESS finalize --dir . 2>/dev/null || true)

echo "NOT VALID JSON{{{{" > nodes/review/handshake.json
echo "NOT VALID JSON{{{{" > nodes/review/run_1/handshake.json
write_handshake "." "gate" "gate" "completed"
OUT=$($HARNESS finalize --dir . --strict 2>/dev/null || true)
assert_field_eq "$OUT" "['finalized']" "False" "10a: --strict rejects corrupt handshake"
assert_contains "$OUT" "cannot parse" "10b: error mentions parse failure"
assert_contains "$OUT" "parse error" "10b: error mentions parse failure"
rm -rf "$D"

@@ -236,0 +240,0 @@ cd /tmp

@@ -7,3 +7,3 @@ #!/usr/bin/env bash

SCRIPT_DIR="$(cd "$(dirname "$0")/.." && pwd)"
HARNESS="node $SCRIPT_DIR/bin/opc-harness.mjs"
HARNESS=(node "$SCRIPT_DIR/bin/opc-harness.mjs")
PASS=0; FAIL=0

@@ -48,3 +48,3 @@

SEAL_OUT=$(cd "$D1" && $HARNESS seal --node review --dir "$D1" 2>/dev/null)
SEAL_OUT=$(cd "$D1" && "${HARNESS[@]}" seal --node review --dir "$D1" 2>/dev/null)
check "seal produces JSON" 'echo "$SEAL_OUT" | python3 -c "import json,sys; json.load(sys.stdin)" 2>/dev/null'

@@ -68,3 +68,3 @@ check "seal reports sealed=true" 'echo "$SEAL_OUT" | python3 -c "import json,sys; d=json.load(sys.stdin); assert d[\"sealed\"]==True"'

SEAL_ERR=$(cd "$D2" && $HARNESS seal --node review --dir "$D2" 2>&1 1>/dev/null || true)
SEAL_ERR=$(cd "$D2" && "${HARNESS[@]}" seal --node review --dir "$D2" 2>&1 1>/dev/null || true)
check "warns about < 2 evals for review" 'echo "$SEAL_ERR" | grep -q "expected.*2"'

@@ -79,3 +79,3 @@

SEAL_FAIL=$(cd "$D3" && $HARNESS seal --node build --dir "$D3" 2>/dev/null)
SEAL_FAIL=$(cd "$D3" && "${HARNESS[@]}" seal --node build --dir "$D3" 2>/dev/null)
check "seal fails when no run dirs" 'echo "$SEAL_FAIL" | python3 -c "import json,sys; d=json.load(sys.stdin); assert d[\"sealed\"]==False"'

@@ -90,3 +90,3 @@

ADV_OUT=$(cd "$D4" && $HARNESS advance --dir "$D4" 2>/dev/null)
ADV_OUT=$(cd "$D4" && "${HARNESS[@]}" advance --dir "$D4" 2>/dev/null)
check "advance fails on non-gate node" 'echo "$ADV_OUT" | python3 -c "import json,sys; d=json.load(sys.stdin); assert d[\"advanced\"]==False"'

@@ -125,3 +125,3 @@ check "advance error mentions gate" 'echo "$ADV_OUT" | grep -q "gate"'

echo '{"pass":true}' > "$D5/nodes/brief/run_1/brief-lint-result.json"
SEAL_BRIEF=$(cd "$D5" && $HARNESS seal --node brief --dir "$D5" 2>/dev/null)
SEAL_BRIEF=$(cd "$D5" && "${HARNESS[@]}" seal --node brief --dir "$D5" 2>/dev/null)
check "brief seal has no validation errors" 'echo "$SEAL_BRIEF" | python3 -c "import json,sys; d=json.load(sys.stdin); assert d[\"validationErrors\"]==[], d[\"validationErrors\"]"'

@@ -147,3 +147,3 @@ check "brief handshake includes brief artifact" 'python3 - "$D5/nodes/brief/handshake.json" <<PY

echo '.copy { color: black; }' > "$D6/nodes/build/run_1/src/app.css"
SEAL_BUILD=$(cd "$D6" && $HARNESS seal --node build --dir "$D6" 2>/dev/null)
SEAL_BUILD=$(cd "$D6" && "${HARNESS[@]}" seal --node build --dir "$D6" 2>/dev/null)
check "build seal finds recursive source artifacts" 'python3 - "$D6/nodes/build/handshake.json" <<PY

@@ -171,3 +171,12 @@ import json,sys

EOF
SEAL_REVIEW=$(cd "$D6" && $HARNESS seal --node review --dir "$D6" 2>/dev/null)
python3 - "$D6/flow-state.json" <<'PY'
import json, sys
path = sys.argv[1]
state = json.load(open(path))
state["currentNode"] = "review"
state["history"] = [{"nodeId": "review", "runId": "run_1", "timestamp": "2026-01-01T00:00:00.000Z"}]
state["totalSteps"] = 1
json.dump(state, open(path, "w"))
PY
SEAL_REVIEW=$(cd "$D6" && "${HARNESS[@]}" seal --node review --dir "$D6" 2>/dev/null)
check "referential severity prose does not inflate warnings" 'python3 - "$D6/nodes/review/handshake.json" <<PY

@@ -195,3 +204,3 @@ import json,sys

echo "ok" > "$D7/nodes/test-execute/run_1/cli-output.txt"
SEAL_EXEC=$(cd "$D7" && $HARNESS seal --node test-execute --dir "$D7" 2>/dev/null)
SEAL_EXEC=$(cd "$D7" && "${HARNESS[@]}" seal --node test-execute --dir "$D7" 2>/dev/null)
check "test-execution.json is classified as plan/spec" 'python3 - "$D7/nodes/test-execute/handshake.json" <<PY

@@ -205,3 +214,85 @@ import json,sys

printf '{broken' > "$D7/nodes/test-execute/test-execution.json"
SEAL_EXEC_MALFORMED=$(cd "$D7" && "${HARNESS[@]}" seal --node test-execute --dir "$D7" 2>/dev/null)
check "malformed test-execution JSON rejects sealing" 'echo "$SEAL_EXEC_MALFORMED" | python3 -c "import json,sys; d=json.load(sys.stdin); assert d[\"sealed\"] is False, d"'
check "malformed test-execution JSON produces validation error" 'echo "$SEAL_EXEC_MALFORMED" | python3 -c "import json,sys; d=json.load(sys.stdin); assert any(\"test-execution.json\" in e for e in d[\"validationErrors\"]), d[\"validationErrors\"]"'
check "malformed test-execution leaves prior canonical handshake unchanged" 'python3 - "$D7/nodes/test-execute/handshake.json" <<PY
import json, sys
handshake = json.load(open(sys.argv[1]))
assert handshake["status"] == "completed", handshake
assert {"type": "test-plan", "path": "test-execution.json"} in handshake["artifacts"], handshake["artifacts"]
PY'
echo ""
echo "=== TEST GROUP 8: seal — run-level machine-readable acceptance JSON ==="
D8="$TMPD/s8"
RUN8="$D8/nodes/build/run_1"
mkdir -p "$RUN8"
echo '{"version":"1.0","flowTemplate":"build-verify","currentNode":"build","entryNode":"build","totalSteps":0,"_written_by":"opc-harness","_write_nonce":"abc","_last_modified":"2025-01-01","history":[],"edgeCounts":{}}' > "$D8/flow-state.json"
for name in \
functional-lifecycle-assertions.json \
functional-lifecycle-eval-extensions.json \
functional-lifecycle-execute-handshake.json \
immutability-fresh-before.json \
immutability-fresh-after.json \
immutability-fresh-comparison.json \
runtime-parity.json; do
printf '{"checks":[],"artifact":"%s"}\n' "$name" > "$RUN8/$name"
done
printf '{"checks":[]}\n' > "$RUN8/acceptance-report.json"
printf '{"checks":[]}\n' > "$RUN8/test-command-result.json"
printf '{"status":"internal-envelope"}\n' > "$RUN8/handshake.json"
printf '{"status":"internal-state"}\n' > "$RUN8/flow-state.json"
printf '{"status":"outside-run"}\n' > "$D8/nodes/build/outside.json"
SEAL_ACCEPTANCE=$(cd "$D8" && "${HARNESS[@]}" seal --node build --dir "$D8" 2>/dev/null)
check "build acceptance seal has no validation errors" 'echo "$SEAL_ACCEPTANCE" | python3 -c "import json,sys; d=json.load(sys.stdin); assert d[\"validationErrors\"]==[], d[\"validationErrors\"]"'
check "all run-level acceptance JSON files enter handshake as reports" 'python3 - "$D8/nodes/build/handshake.json" <<PY
import json, sys
handshake = json.load(open(sys.argv[1]))
artifacts = {a["path"]: a["type"] for a in handshake["artifacts"]}
expected = {
"run_1/functional-lifecycle-assertions.json",
"run_1/functional-lifecycle-eval-extensions.json",
"run_1/functional-lifecycle-execute-handshake.json",
"run_1/immutability-fresh-before.json",
"run_1/immutability-fresh-after.json",
"run_1/immutability-fresh-comparison.json",
"run_1/runtime-parity.json",
}
assert expected <= artifacts.keys(), (expected - artifacts.keys(), artifacts)
assert all(artifacts[path] == "report" for path in expected), artifacts
PY'
check "named report and test-result special classifications remain specific" 'python3 - "$D8/nodes/build/handshake.json" <<PY
import json, sys
handshake = json.load(open(sys.argv[1]))
artifacts = {a["path"]: a["type"] for a in handshake["artifacts"]}
assert artifacts["run_1/acceptance-report.json"] == "report", artifacts
assert artifacts["run_1/test-command-result.json"] == "test-result", artifacts
PY'
check "seal excludes reserved state envelopes and node-level non-run JSON" 'python3 - "$D8/nodes/build/handshake.json" <<PY
import json, sys
handshake = json.load(open(sys.argv[1]))
paths = {a["path"] for a in handshake["artifacts"]}
assert "run_1/handshake.json" not in paths, paths
assert "run_1/flow-state.json" not in paths, paths
assert "outside.json" not in paths, paths
PY'
echo ""
echo "=== TEST GROUP 9: seal — malformed machine-readable JSON fails closed ==="
D9="$TMPD/s9"
RUN9="$D9/nodes/build/run_1"
mkdir -p "$RUN9"
echo '{"version":"1.0","flowTemplate":"build-verify","currentNode":"build","entryNode":"build","totalSteps":0,"_written_by":"opc-harness","_write_nonce":"abc","_last_modified":"2025-01-01","history":[],"edgeCounts":{}}' > "$D9/flow-state.json"
printf '{broken' > "$RUN9/acceptance-evidence.json"
SEAL_MALFORMED=$(cd "$D9" && "${HARNESS[@]}" seal --node build --dir "$D9" 2>/dev/null)
check "malformed acceptance JSON rejects sealing" 'echo "$SEAL_MALFORMED" | python3 -c "import json,sys; d=json.load(sys.stdin); assert d[\"sealed\"] is False, d"'
check "malformed acceptance JSON produces validation error" 'echo "$SEAL_MALFORMED" | python3 -c "import json,sys; d=json.load(sys.stdin); assert any(\"acceptance-evidence.json\" in e for e in d[\"validationErrors\"]), d[\"validationErrors\"]"'
check "malformed JSON does not create canonical handshake" '[ ! -f "$D9/nodes/build/handshake.json" ]'
echo ""
echo "==========================================="

@@ -208,0 +299,0 @@ echo " Results: $PASS passed, $FAIL failed"

@@ -14,9 +14,9 @@ #!/bin/bash

local dir="$1" command="$2"
mkdir -p "$dir/nodes/test-design"
echo "# Eval A" > "$dir/nodes/test-design/eval-a.md"
echo "# Eval B" > "$dir/nodes/test-design/eval-b.md"
write_complete_test_plan "$dir/nodes/test-design/test-plan.md"
python3 - "$dir/nodes/test-design/handshake.json" "$command" <<'PY'
mkdir -p "$dir/nodes/test-design/run_1"
echo "# Eval A" > "$dir/nodes/test-design/run_1/eval-a.md"
echo "# Eval B" > "$dir/nodes/test-design/run_1/eval-b.md"
write_complete_test_plan "$dir/nodes/test-design/run_1/test-plan.md"
python3 - "$dir/nodes/test-design/handshake.json" "$dir/nodes/test-design/run_1/handshake.json" "$command" <<'PY'
import json, sys
path, command = sys.argv[1], sys.argv[2]
path, run_path, command = sys.argv[1], sys.argv[2], sys.argv[3]
data = {

@@ -31,4 +31,5 @@ "nodeId": "test-design",

"artifacts": [
{"type": "eval", "path": "eval-a.md"},
{"type": "eval", "path": "eval-b.md"}
{"type": "eval", "path": "run_1/eval-a.md"},
{"type": "eval", "path": "run_1/eval-b.md"},
{"type": "test-plan", "path": "run_1/test-plan.md"}
],

@@ -39,2 +40,6 @@ "testCommand": command,

open(path, "w").write(json.dumps(data))
run_data = dict(data)
for artifact in run_data["artifacts"]:
artifact["path"] = artifact["path"].replace("run_1/", "")
open(run_path, "w").write(json.dumps(run_data))
PY

@@ -45,7 +50,9 @@ }

local dir="$1" command="$2" cwd="$3"
mkdir -p "$dir/nodes/test-design"
python3 - "$dir/nodes/test-design/test-execution.json" "$command" "$cwd" <<'PY'
mkdir -p "$dir/nodes/test-design/run_1"
python3 - "$dir/nodes/test-design/run_1/test-execution.json" "$command" "$cwd" <<'PY'
import json, sys
path, command, cwd = sys.argv[1], sys.argv[2], sys.argv[3]
data = {
"nodeId": "test-design",
"runId": "run_1",
"testCommand": command,

@@ -88,2 +95,3 @@ "cwd": cwd,

grep -q '"sourcePlanHash":' .harness/nodes/test-execute/handshake.json &&
grep -q '"sourceRunId": "run_1"' .harness/nodes/test-execute/handshake.json &&
grep -q '"resultHash":' .harness/nodes/test-execute/handshake.json &&

@@ -95,2 +103,3 @@ grep -q '"ledger":' .harness/nodes/test-execute/handshake.json &&

grep -q '"sourcePlanHash":' .harness/nodes/test-execute/run_1/test-command-result.json &&
grep -q '"sourceRunId": "run_1"' .harness/nodes/test-execute/run_1/test-command-result.json &&
grep -q '"executionActor": "opc-harness:test-command"' .harness/nodes/test-execute/run_1/test-command-result.json; then

@@ -179,2 +188,9 @@ echo " ✅ testCommand evidence records OPC provenance"

JSON
python3 - <<'PY'
import json
data = json.load(open('.harness-forged/nodes/test-execute/handshake.json'))
run_data = dict(data)
run_data["artifacts"] = [{"type": "test-result", "path": "test-execution.json"}]
open('.harness-forged/nodes/test-execute/run_1/handshake.json', 'w').write(json.dumps(run_data))
PY
OUT=$($HARNESS transition --from test-execute --to gate --verdict PASS --flow build-verify --dir .harness-forged 2>/dev/null)

@@ -204,2 +220,4 @@ ALLOWED=$(json_field "$OUT" "allowed")

"kind": "opc-test-command",
"sourceNode": "test-design",
"sourceRunId": "run_1",
"commandHash": command_hash,

@@ -227,13 +245,21 @@ "sourcePlanHash": plan_hash,

"artifacts": [{"type": "test-result", "path": "run_1/test-command-result.json"}],
"testEvidenceProvenance": {
"kind": "opc-test-command",
"sourceNode": "test-design",
"commandHash": command_hash,
"testEvidenceProvenance": {
"kind": "opc-test-command",
"sourceNode": "test-design",
"sourceRunId": "run_1",
"commandHash": command_hash,
"sourcePlanHash": plan_hash,
"resultHash": result_hash,
"executionActor": "opc-harness:test-command"
}
}
},
"testEvidencePolicy": {"allowVacuousChecks": []}
}
design_run_hs = dict(design_hs)
design_run_hs["artifacts"] = [{"type": "test-plan", "path": "test-plan.md"}]
exec_run_hs = dict(exec_hs)
exec_run_hs["artifacts"] = [{"type": "test-result", "path": "test-command-result.json"}]
open('.harness-consistent-forge/nodes/test-design/handshake.json', 'w').write(json.dumps(design_hs))
open('.harness-consistent-forge/nodes/test-design/run_1/handshake.json', 'w').write(json.dumps(design_run_hs))
open('.harness-consistent-forge/nodes/test-execute/handshake.json', 'w').write(json.dumps(exec_hs))
open('.harness-consistent-forge/nodes/test-execute/run_1/handshake.json', 'w').write(json.dumps(exec_run_hs))
PY

@@ -240,0 +266,0 @@ OUT=$($HARNESS transition --from test-execute --to gate --verdict PASS --flow build-verify --dir .harness-consistent-forge 2>/dev/null)

@@ -145,4 +145,4 @@ #!/bin/bash

$HARNESS init --flow full-stack --tier polished --entry test-execute --dir "$dir" 2>/dev/null >/dev/null
mkdir -p "$dir/nodes/test-execute"
touch "$dir/nodes/test-execute/screen.png"
mkdir -p "$dir/nodes/test-execute/run_1"
touch "$dir/nodes/test-execute/run_1/screen.png"
}

@@ -157,5 +157,6 @@

"timestamp": "2024-01-01T00:00:00Z",
"artifacts": [{"type": "screenshot", "path": "screen.png"}]
"artifacts": [{"type": "screenshot", "path": "run_1/screen.png"}]
}
HS
sync_run_handshakes .h-t1
OUT=$($HARNESS validate .h-t1/nodes/test-execute/handshake.json 2>/dev/null)

@@ -168,3 +169,3 @@ assert_field_eq "missing tierCoverage rejected" "$OUT" "valid" "false"

setup_tier_flow .h-t2
echo "npm test: 42 passed, 0 failed" > .h-t2/nodes/test-execute/test-output.txt
echo "npm test: 42 passed, 0 failed" > .h-t2/nodes/test-execute/run_1/test-output.txt
cat > .h-t2/nodes/test-execute/handshake.json << 'HS'

@@ -175,3 +176,3 @@ {

"timestamp": "2024-01-01T00:00:00Z",
"artifacts": [{"type": "screenshot", "path": "screen.png"}, {"type": "cli-output", "path": "test-output.txt"}],
"artifacts": [{"type": "screenshot", "path": "run_1/screen.png"}, {"type": "cli-output", "path": "run_1/test-output.txt"}],
"tierCoverage": {

@@ -183,2 +184,3 @@ "covered": ["typography","color-scheme","navigation","responsive","code-blocks","tables","loading-states","error-states","favicon-meta","focus-styles","testing-md"],

HS
sync_run_handshakes .h-t2
OUT=$($HARNESS validate .h-t2/nodes/test-execute/handshake.json 2>/dev/null)

@@ -195,3 +197,3 @@ assert_field_eq "full coverage accepted" "$OUT" "valid" "true"

"timestamp": "2024-01-01T00:00:00Z",
"artifacts": [{"type": "screenshot", "path": "screen.png"}],
"artifacts": [{"type": "screenshot", "path": "run_1/screen.png"}],
"tierCoverage": {

@@ -203,2 +205,3 @@ "covered": ["typography","color-scheme","navigation","responsive","code-blocks","tables","loading-states","error-states","favicon-meta"],

HS
sync_run_handshakes .h-t3
OUT=$($HARNESS validate .h-t3/nodes/test-execute/handshake.json 2>/dev/null)

@@ -216,3 +219,3 @@ assert_field_eq "short reason rejected" "$OUT" "valid" "false"

"timestamp": "2024-01-01T00:00:00Z",
"artifacts": [{"type": "screenshot", "path": "screen.png"}],
"artifacts": [{"type": "screenshot", "path": "run_1/screen.png"}],
"tierCoverage": {

@@ -224,2 +227,3 @@ "covered": ["typography","banana","color-scheme","navigation","responsive","code-blocks","tables","loading-states","error-states","favicon-meta","focus-styles"],

HS
sync_run_handshakes .h-t4
OUT=$($HARNESS validate .h-t4/nodes/test-execute/handshake.json 2>/dev/null)

@@ -239,3 +243,3 @@ assert_field_eq "unknown key rejected" "$OUT" "valid" "false"

"timestamp": "2024-01-01T00:00:00Z",
"artifacts": [{"type": "screenshot", "path": "screen.png"}],
"artifacts": [{"type": "screenshot", "path": "run_1/screen.png"}],
"tierCoverage": {

@@ -247,2 +251,3 @@ "covered": ["typography","color-scheme","navigation","responsive"],

HS
sync_run_handshakes .h-t5
OUT=$($HARNESS validate .h-t5/nodes/test-execute/handshake.json 2>/dev/null)

@@ -255,3 +260,3 @@ assert_field_eq "incomplete coverage rejected" "$OUT" "valid" "false"

setup_tier_flow .h-t6
echo "npm test: all passed" > .h-t6/nodes/test-execute/test-output.txt
echo "npm test: all passed" > .h-t6/nodes/test-execute/run_1/test-output.txt
cat > .h-t6/nodes/test-execute/handshake.json << 'HS'

@@ -262,3 +267,3 @@ {

"timestamp": "2024-01-01T00:00:00Z",
"artifacts": [{"type": "screenshot", "path": "screen.png"}, {"type": "cli-output", "path": "test-output.txt"}],
"artifacts": [{"type": "screenshot", "path": "run_1/screen.png"}, {"type": "cli-output", "path": "run_1/test-output.txt"}],
"tierCoverage": {

@@ -270,2 +275,3 @@ "covered": ["typography","color-scheme","navigation","responsive","tables","loading-states","error-states","favicon-meta","focus-styles","testing-md"],

HS
sync_run_handshakes .h-t6
OUT=$($HARNESS validate .h-t6/nodes/test-execute/handshake.json 2>/dev/null)

@@ -286,2 +292,13 @@ assert_field_eq "valid skip accepted" "$OUT" "valid" "true"

HS
sync_run_handshakes .h-t7
python3 -c "
import json
p='.h-t7/flow-state.json'
s=json.load(open(p))
s['entryNode']='build'
s['currentNode']='build'
s['totalSteps']=1
s['history']=[{'nodeId':'build','runId':'run_1','timestamp':'2024-01-01T00:00:00.000Z'}]
json.dump(s, open(p,'w'), indent=2)
"
OUT=$($HARNESS validate .h-t7/nodes/build/handshake.json 2>/dev/null)

@@ -294,4 +311,4 @@ assert_field_eq "build node unaffected" "$OUT" "valid" "true"

$HARNESS init --flow full-stack --tier functional --entry test-execute --dir .h-t8 2>/dev/null >/dev/null
mkdir -p .h-t8/nodes/test-execute
touch .h-t8/nodes/test-execute/screen.png
mkdir -p .h-t8/nodes/test-execute/run_1
touch .h-t8/nodes/test-execute/run_1/screen.png
cat > .h-t8/nodes/test-execute/handshake.json << 'HS'

@@ -302,5 +319,6 @@ {

"timestamp": "2024-01-01T00:00:00Z",
"artifacts": [{"type": "screenshot", "path": "screen.png"}]
"artifacts": [{"type": "screenshot", "path": "run_1/screen.png"}]
}
HS
sync_run_handshakes .h-t8
OUT=$($HARNESS validate .h-t8/nodes/test-execute/handshake.json 2>/dev/null)

@@ -307,0 +325,0 @@ assert_field_eq "functional tier no coverage needed" "$OUT" "valid" "true"

@@ -7,3 +7,3 @@ #!/usr/bin/env bash

SCRIPT_DIR="$(cd "$(dirname "$0")/.." && pwd)"
HARNESS="node $SCRIPT_DIR/bin/opc-harness.mjs"
HARNESS=(node "$SCRIPT_DIR/bin/opc-harness.mjs")
PASS=0; FAIL=0

@@ -35,2 +35,4 @@

"$node" "$(date -u +%Y-%m-%dT%H:%M:%SZ)" "$verdict" > "$dir/nodes/$node/handshake.json"
printf '{"nodeId":"%s","nodeType":"review","runId":"run_1","status":"completed","summary":"Done","timestamp":"%s","artifacts":[{"type":"eval","path":"eval-a.md"},{"type":"eval","path":"eval-b.md"}],"verdict":"%s"}\n' \
"$node" "$(date -u +%Y-%m-%dT%H:%M:%SZ)" "$verdict" > "$dir/nodes/$node/run_1/handshake.json"
}

@@ -50,2 +52,6 @@

PY
mkdir -p .harness/nodes/gate/run_1
printf '{"nodeId":"gate","nodeType":"gate","runId":"run_1","status":"completed","summary":"gate ready","timestamp":"%s","artifacts":[],"verdict":"PASS"}\n' \
"$(date -u +%Y-%m-%dT%H:%M:%SZ)" > .harness/nodes/gate/run_1/handshake.json
cp .harness/nodes/gate/run_1/handshake.json .harness/nodes/gate/handshake.json
}

@@ -57,7 +63,7 @@

mkdir -p "$D1" && cd "$D1"
$HARNESS init --flow review --entry review --dir .harness > /dev/null 2>&1
"${HARNESS[@]}" init --flow review --entry review --dir .harness > /dev/null 2>&1
write_review_hs ".harness" "review"
$HARNESS transition --from review --to gate --verdict PASS --flow review --dir .harness > /dev/null 2>&1
"${HARNESS[@]}" transition --from review --to gate --verdict PASS --flow review --dir .harness > /dev/null 2>&1
RESULT=$(cd "$D1" && $HARNESS transition --from gate --to null --verdict PASS --flow review --dir .harness 2>&1)
RESULT=$(cd "$D1" && "${HARNESS[@]}" transition --from gate --to null --verdict PASS --flow review --dir .harness 2>&1)
check "terminal transition returns finalized" 'echo "$RESULT" | grep -q "finalized"'

@@ -70,5 +76,5 @@

mkdir -p "$D2" && cd "$D2"
$HARNESS init --flow review --entry review --dir .harness > /dev/null 2>&1
"${HARNESS[@]}" init --flow review --entry review --dir .harness > /dev/null 2>&1
RESULT2=$(cd "$D2" && $HARNESS transition --from review --to null --verdict PASS --flow review --dir .harness 2>&1)
RESULT2=$(cd "$D2" && "${HARNESS[@]}" transition --from review --to null --verdict PASS --flow review --dir .harness 2>&1)
check "non-terminal node rejects --to null" 'echo "$RESULT2" | grep -q "no terminal edge"'

@@ -79,15 +85,15 @@

ROUTE_RESULT=$($HARNESS route --node gate --verdict PASS --flow review 2>&1)
ROUTE_RESULT=$("${HARNESS[@]}" route --node gate --verdict PASS --flow review 2>&1)
check "route returns null for terminal" 'echo "$ROUTE_RESULT" | grep -q "\"next\":null"'
echo ""
echo "=== TEST GROUP 4: sealed ITERATE cannot be overridden by CLI PASS ==="
echo "=== TEST GROUP 4: sealed ITERATE can structurally enter gate ==="
D3="$TMPD/t3"
mkdir -p "$D3" && cd "$D3"
$HARNESS init --flow review --entry review --dir .harness > /dev/null 2>&1
"${HARNESS[@]}" init --flow review --entry review --dir .harness > /dev/null 2>&1
write_review_hs ".harness" "review" "ITERATE" "FINDINGS[1]"
RESULT3=$(cd "$D3" && $HARNESS transition --from review --to gate --verdict PASS --flow review --dir .harness 2>&1)
check "review PASS edge rejects sealed ITERATE" 'echo "$RESULT3" | grep -q "sealed verdict is.*ITERATE"'
RESULT3=$(cd "$D3" && "${HARNESS[@]}" transition --from review --to gate --verdict PASS --flow review --dir .harness 2>&1)
check "review PASS edge reaches gate for gate adjudication" 'echo "$RESULT3" | grep -q "\"allowed\":true"'

@@ -99,7 +105,7 @@ echo ""

mkdir -p "$D4" && cd "$D4"
$HARNESS init --flow review --entry review --dir .harness > /dev/null 2>&1
"${HARNESS[@]}" init --flow review --entry review --dir .harness > /dev/null 2>&1
write_review_hs ".harness" "review" "ITERATE" "FINDINGS[1]"
set_gate_state
RESULT4=$(cd "$D4" && $HARNESS finalize --dir .harness 2>&1)
RESULT4=$(cd "$D4" && "${HARNESS[@]}" finalize --dir .harness 2>&1)
check "finalize rejects gate with upstream ITERATE" 'echo "$RESULT4" | grep -q "sealed verdict for review is ITERATE"'

@@ -112,9 +118,10 @@

mkdir -p "$D5" && cd "$D5"
$HARNESS init --flow review --entry review --dir .harness > /dev/null 2>&1
"${HARNESS[@]}" init --flow review --entry review --dir .harness > /dev/null 2>&1
write_review_hs ".harness" "review"
set_gate_state
printf '{broken json\n' > .harness/nodes/review/handshake.json
printf '{broken json\n' > .harness/nodes/review/run_1/handshake.json
RESULT5=$(cd "$D5" && $HARNESS finalize --dir .harness 2>&1)
check "finalize rejects corrupt upstream handshake" 'echo "$RESULT5" | grep -q "handshake for review is corrupt"'
RESULT5=$(cd "$D5" && "${HARNESS[@]}" finalize --dir .harness 2>&1)
check "finalize rejects corrupt upstream handshake" 'echo "$RESULT5" | grep -q "parse error"'

@@ -126,10 +133,10 @@ echo ""

mkdir -p "$D6" && cd "$D6"
$HARNESS init --flow review --entry review --dir .harness > /dev/null 2>&1
"${HARNESS[@]}" init --flow review --entry review --dir .harness > /dev/null 2>&1
write_review_hs ".harness" "review"
set_gate_state
rm -f .harness/nodes/review/handshake.json
rm -rf .harness/nodes/review/run_1
rm -f .harness/nodes/review/run_1/handshake.json
RESULT6=$(cd "$D6" && $HARNESS finalize --dir .harness 2>&1)
check "finalize rejects missing upstream handshake" 'echo "$RESULT6" | grep -q "handshake for review is missing"'
RESULT6=$(cd "$D6" && "${HARNESS[@]}" finalize --dir .harness 2>&1)
check "finalize rejects missing upstream handshake" 'echo "$RESULT6" | grep -q "missing handshake for node '\''review'\'' run '\''run_1'\''"'

@@ -141,8 +148,8 @@ echo ""

mkdir -p "$D7" && cd "$D7"
$HARNESS init --flow review --entry review --dir .harness > /dev/null 2>&1
"${HARNESS[@]}" init --flow review --entry review --dir .harness > /dev/null 2>&1
write_review_hs ".harness" "review"
set_gate_state
rm -rf .harness/nodes/review/run_1
rm -f .harness/nodes/review/run_1/eval-a.md
RESULT7=$(cd "$D7" && $HARNESS finalize --dir .harness 2>&1)
RESULT7=$(cd "$D7" && "${HARNESS[@]}" finalize --dir .harness 2>&1)
check "finalize rejects missing review eval artifact" 'echo "$RESULT7" | grep -q "review eval artifact for review unreadable"'

@@ -149,0 +156,0 @@

Sorry, the diff of this file is too big to display

Sorry, the diff of this file is too big to display

Sorry, the diff of this file is too big to display