intentdna 1.8.0 → 1.8.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -9,7 +9,7 @@
9
9
  {
10
10
  "name": "intentdna",
11
11
  "description": "Declarative policy layer for AI agent behavior with plugin-managed hook runtime for Claude Code.",
12
- "version": "1.8.0",
12
+ "version": "1.8.2",
13
13
  "author": {
14
14
  "name": "Samuel"
15
15
  },
@@ -25,5 +25,5 @@
25
25
  ]
26
26
  }
27
27
  ],
28
- "version": "1.8.0"
28
+ "version": "1.8.2"
29
29
  }
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "intentdna",
3
- "version": "1.8.0",
3
+ "version": "1.8.2",
4
4
  "description": "Declarative policy layer for AI agent behavior",
5
5
  "author": {
6
6
  "name": "Samuel"
@@ -6,6 +6,7 @@
6
6
  * via `claude -p --agent` child processes.
7
7
  */
8
8
  import type { WorkflowStep, ParallelGroup, TransitionRule } from "../../schema/types.js";
9
+ import type { ConstraintIR } from "../../schema/types.js";
9
10
  export interface RunOptions {
10
11
  dnaFiles: string[];
11
12
  workflowName: string;
@@ -35,6 +36,7 @@ interface StepExecOptions {
35
36
  workflowName: string;
36
37
  workflowAsset?: string;
37
38
  inputs: Record<string, string>;
39
+ ir: Pick<ConstraintIR, "verifier_policy">;
38
40
  }
39
41
  /**
40
42
  * Replace {{var}} placeholders in a template string.
@@ -6,7 +6,7 @@
6
6
  * via `claude -p --agent` child processes.
7
7
  */
8
8
  import { spawn } from "node:child_process";
9
- import { mkdir, writeFile, readFile } from "node:fs/promises";
9
+ import { mkdir, writeFile } from "node:fs/promises";
10
10
  import { join, resolve } from "node:path";
11
11
  import { randomUUID } from "node:crypto";
12
12
  import { loadDNA, compileFromFiles, expandDNAInputFiles } from "../../compiler/index.js";
@@ -17,6 +17,7 @@ import { createCompiledIR, writeCompiledIR } from "../../runtime/plugin-adapter.
17
17
  import { trustedEvidenceCaptureAttribution } from "../../governance/index.js";
18
18
  import { DNAStateManager } from "../../hooks/state-manager.js";
19
19
  import { appendEvidenceCaptureEvent, EVIDENCE_CAPTURE_SCHEMA_VERSION, } from "../../hooks/state.js";
20
+ import { runCompletionVerifier, runCheckpointVerifier, } from "../../runtime/verifier.js";
20
21
  // ── Template & Condition Helpers ───────────────────────────
21
22
  /**
22
23
  * Replace {{var}} placeholders in a template string.
@@ -56,53 +57,44 @@ export function evaluateRunIf(condition, round) {
56
57
  * Run checkpoint assertions for a completed step.
57
58
  * Returns true if all checkpoints pass, false if any fail.
58
59
  */
59
- async function runCheckpoints(step, projectDir) {
60
+ async function runCompletions(step, vars, opts) {
61
+ if (!step.completion)
62
+ return true;
63
+ for (const completion of step.completion) {
64
+ const result = await runCompletionVerifier(completion, opts.ir, {
65
+ projectDir: opts.projectDir,
66
+ variables: vars,
67
+ });
68
+ if (!result.passed) {
69
+ logVerifierFailure("Completion failed", step.id, result);
70
+ return false;
71
+ }
72
+ }
73
+ return true;
74
+ }
75
+ async function runCheckpoints(step, vars, opts) {
60
76
  if (!step.checkpoints)
61
77
  return true;
62
- const assertCommands = {
63
- clean_working_tree: 'git status --porcelain',
64
- lint_passing: 'npm run lint --silent 2>/dev/null',
65
- build_passing: 'npm run build --silent 2>/dev/null',
66
- };
67
78
  for (const cp of step.checkpoints) {
68
79
  const action = cp.action ?? "block";
69
- let passed = false;
70
- try {
71
- if (cp.command) {
72
- // Custom command — must exit 0
73
- await spawnAsync("bash", ["-c", cp.command], "", projectDir);
74
- passed = true;
75
- }
76
- else if (cp.assert === "clean_working_tree") {
77
- const output = await spawnAsync("git", ["status", "--porcelain"], "", projectDir);
78
- passed = output.trim() === "";
79
- }
80
- else if (cp.assert === "no_test_regression") {
81
- // Read baselines from .dna/
82
- const baseline = parseInt(await readFile(join(projectDir, ".dna", "test-baseline"), "utf-8").catch(() => "0"), 10);
83
- const current = parseInt(await readFile(join(projectDir, ".dna", "test-current"), "utf-8").catch(() => "0"), 10);
84
- passed = current <= baseline;
85
- }
86
- else if (assertCommands[cp.assert]) {
87
- await spawnAsync("bash", ["-c", assertCommands[cp.assert]], "", projectDir);
88
- passed = true;
89
- }
90
- else {
91
- log(`Unknown checkpoint assert: ${cp.assert} (step: ${step.id})`);
92
- passed = false;
93
- }
94
- }
95
- catch {
96
- passed = false;
97
- }
98
- if (!passed) {
99
- log(`Checkpoint failed: ${cp.assert} — ${cp.message} (step: ${step.id})`);
80
+ const result = await runCheckpointVerifier(cp, opts.ir, {
81
+ projectDir: opts.projectDir,
82
+ variables: vars,
83
+ });
84
+ if (!result.passed) {
85
+ logVerifierFailure(`Checkpoint failed: ${cp.assert}`, step.id, result);
100
86
  if (action === "block")
101
87
  return false;
102
88
  }
103
89
  }
104
90
  return true;
105
91
  }
92
+ function logVerifierFailure(prefix, stepId, result) {
93
+ const exitCode = result.exit_code === undefined ? "" : ` exit_code=${result.exit_code}`;
94
+ const target = result.target ? ` target=${result.target}` : "";
95
+ const message = result.message ? ` — ${result.message}` : "";
96
+ log(`${prefix}${target}${exitCode}${message} (step: ${stepId})`);
97
+ }
106
98
  // ── Step Execution ─────────────────────────────────────────
107
99
  async function recordStepResultEvidence(step, opts, sessionId, status, durationMs) {
108
100
  const result = status === "pass" ? "success" : "failure";
@@ -180,9 +172,17 @@ export async function executeStep(step, vars, opts, iteration = 1) {
180
172
  // Non-JSON output → treat as success
181
173
  }
182
174
  const status = isError ? "fail" : "pass";
183
- // Run checkpoints if step passed and has checkpoints defined
175
+ if (status === "pass" && step.completion && step.completion.length > 0) {
176
+ const completionsPassed = await runCompletions(step, vars, opts);
177
+ if (!completionsPassed) {
178
+ const durationMs = Date.now() - start;
179
+ await recordStepResultEvidence(step, opts, sessionId, "fail", durationMs);
180
+ log(`Step ${step.id} COMPLETION_FAIL (${durationMs}ms)`);
181
+ return { stepId: step.id, status: "fail", durationMs, resultFile };
182
+ }
183
+ }
184
184
  if (status === "pass" && step.checkpoints && step.checkpoints.length > 0) {
185
- const checkpointsPassed = await runCheckpoints(step, opts.projectDir);
185
+ const checkpointsPassed = await runCheckpoints(step, vars, opts);
186
186
  if (!checkpointsPassed) {
187
187
  const durationMs = Date.now() - start;
188
188
  await recordStepResultEvidence(step, opts, sessionId, "fail", durationMs);
@@ -343,6 +343,7 @@ export async function runRun(opts) {
343
343
  const vars = {
344
344
  ...(opts.vars ?? {}),
345
345
  task_id: opts.taskId,
346
+ ARGUMENTS: opts.taskId,
346
347
  workflow: opts.workflowName,
347
348
  };
348
349
  // ── Dry-run mode ─────────────────────────────────────
@@ -375,8 +376,10 @@ export async function runRun(opts) {
375
376
  inputs: {
376
377
  ...(opts.vars ?? {}),
377
378
  task_id: opts.taskId,
379
+ ARGUMENTS: opts.taskId,
378
380
  workflow: opts.workflowName,
379
381
  },
382
+ ir,
380
383
  };
381
384
  // ── Step 6: Execute workflow ──────────────────────────
382
385
  const maxRetries = plan.retry.max_retries;
@@ -99,6 +99,8 @@ export declare function runVerifiersForTest(projectDir: string, ir: ConstraintIR
99
99
  workflow: string;
100
100
  current_step: string;
101
101
  current_role: string;
102
+ inputs?: Record<string, string>;
103
+ resolved_variables?: Record<string, string>;
102
104
  }, when: VerifierSpec["when"], sessionId?: string, options?: {
103
105
  commandTimeoutMs?: number;
104
106
  }): Promise<VerifierResultEntry[]>;
@@ -106,6 +108,8 @@ export declare function runStopVerifiersForTest(projectDir: string, ir: Constrai
106
108
  workflow: string;
107
109
  current_step: string;
108
110
  current_role: string;
111
+ inputs?: Record<string, string>;
112
+ resolved_variables?: Record<string, string>;
109
113
  }, sessionId?: string, options?: {
110
114
  commandTimeoutMs?: number;
111
115
  }): Promise<VerifierResultEntry[]>;
package/dist/hooks/cli.js CHANGED
@@ -28,6 +28,7 @@ import { hookEventsForSurface } from "./event-registry.js";
28
28
  import { writeAuditEvent } from "../audit/index.js";
29
29
  import { RUNTIME_DECISION_EVENT_SCHEMA_VERSION, trustedEvidenceCaptureAttribution } from "../governance/index.js";
30
30
  import { runCodexHook } from "../runtime/codex-adapter.js";
31
+ import { runCheckpointVerifier as runSharedCheckpointVerifier, runCompletionVerifier, toProjectRelativePath, } from "../runtime/verifier.js";
31
32
  // ── Constants ──────────────────────────────────────────────
32
33
  const DEFAULT_IR_PATH = ".dna/compiled/ir.json";
33
34
  const VALID_EVENTS = new Set(hookEventsForSurface("cli"));
@@ -423,6 +424,8 @@ export async function runHookEvent(options) {
423
424
  current_step: wfState.current_step,
424
425
  current_role: wfState.current_role,
425
426
  started_at: wfState.started_at,
427
+ inputs: wfState.inputs,
428
+ resolved_variables: wfState.resolved_variables,
426
429
  completed_artifacts: wfState.completed_artifacts,
427
430
  artifact_facts: stopArtifactFacts,
428
431
  } : null;
@@ -507,6 +510,8 @@ export async function runHookEvent(options) {
507
510
  workflow: wfStateRaw.workflow,
508
511
  current_step: wfStateRaw.current_step,
509
512
  current_role: wfStateRaw.current_role,
513
+ inputs: wfStateRaw.inputs,
514
+ resolved_variables: wfStateRaw.resolved_variables,
510
515
  }, "post_tool_use", sessionId);
511
516
  const failingResults = verifierResults.filter((result) => result.status === "fail");
512
517
  if (failingResults.length > 0) {
@@ -951,8 +956,6 @@ function clearBlockingVerifierCheckpoints(ir, workflowState) {
951
956
  }),
952
957
  };
953
958
  }
954
- const DEFAULT_VERIFIER_COMMAND_TIMEOUT_MS = 30_000;
955
- const MAX_VERIFIER_EVIDENCE_BYTES = 4096;
956
959
  function appendOutputText(output, text, hookEventName) {
957
960
  if (!text)
958
961
  return output;
@@ -999,316 +1002,6 @@ export function appendStopVerifierWarnings(output, verifierResults, currentStep)
999
1002
  "\nFix the warning above before relying on this step, or rerun the verifier after repair.";
1000
1003
  return appendOutputText(output, warningText, "Stop");
1001
1004
  }
1002
- function trimEvidence(raw) {
1003
- if (!raw)
1004
- return undefined;
1005
- const trimmed = raw.trim();
1006
- if (!trimmed)
1007
- return undefined;
1008
- return trimmed.length > MAX_VERIFIER_EVIDENCE_BYTES
1009
- ? trimmed.slice(0, MAX_VERIFIER_EVIDENCE_BYTES)
1010
- : trimmed;
1011
- }
1012
- function hasUnsafeShellControl(command) {
1013
- let inSingle = false;
1014
- let inDouble = false;
1015
- let escaped = false;
1016
- for (let i = 0; i < command.length; i++) {
1017
- const ch = command[i];
1018
- const next = command[i + 1] ?? "";
1019
- if (escaped) {
1020
- escaped = false;
1021
- continue;
1022
- }
1023
- if (ch === "\\" && !inSingle) {
1024
- escaped = true;
1025
- continue;
1026
- }
1027
- if (ch === "'" && !inDouble) {
1028
- inSingle = !inSingle;
1029
- continue;
1030
- }
1031
- if (ch === '"' && !inSingle) {
1032
- inDouble = !inDouble;
1033
- continue;
1034
- }
1035
- if (!inSingle && !inDouble) {
1036
- if (ch === ";" || ch === "`" || ch === "\n" || ch === "\r")
1037
- return true;
1038
- if ((ch === "&" || ch === "|" || ch === "<" || ch === ">") && next === ch)
1039
- return true;
1040
- if (ch === "$" && next === "(")
1041
- return true;
1042
- if ((ch === "<" || ch === ">") && next === "(")
1043
- return true;
1044
- if (ch === "|" || ch === "&" || ch === "<" || ch === ">")
1045
- return true;
1046
- }
1047
- if (inDouble) {
1048
- if (ch === "`")
1049
- return true;
1050
- if (ch === "$" && next === "(")
1051
- return true;
1052
- }
1053
- }
1054
- return false;
1055
- }
1056
- function isVerifierCommandAllowed(ir, command) {
1057
- const normalized = command.trim();
1058
- const policy = ir.verifier_policy;
1059
- if (!policy)
1060
- return false;
1061
- if (policy.allow_commands?.includes(normalized))
1062
- return true;
1063
- if (hasUnsafeShellControl(normalized))
1064
- return false;
1065
- return policy.allow_command_prefixes?.some((prefix) => normalized.startsWith(prefix)) ?? false;
1066
- }
1067
- function verifierCommandPolicyMessage(command) {
1068
- return `Verifier command not allowed by verifier_policy: ${command}`;
1069
- }
1070
- function isBuiltinAssertAllowed(ir, assertName) {
1071
- const policy = ir.verifier_policy;
1072
- return policy?.allow_builtin_asserts?.includes(assertName) ?? false;
1073
- }
1074
- function verifierAssertPolicyMessage(assertName) {
1075
- return `Verifier assert not allowed by verifier_policy: ${assertName}`;
1076
- }
1077
- function summarizeCommandEvidence(stdout, stderr) {
1078
- const stdoutBytes = Buffer.byteLength(stdout, "utf8");
1079
- const stderrBytes = Buffer.byteLength(stderr, "utf8");
1080
- if (stdoutBytes === 0 && stderrBytes === 0)
1081
- return undefined;
1082
- return trimEvidence(`stdout_bytes=${stdoutBytes} stderr_bytes=${stderrBytes}`);
1083
- }
1084
- async function execVerifierCommand(projectDir, command, timeoutMs = DEFAULT_VERIFIER_COMMAND_TIMEOUT_MS) {
1085
- return new Promise((resolvePromise) => {
1086
- const child = spawn("bash", ["-lc", command], { cwd: projectDir, stdio: ["ignore", "pipe", "pipe"] });
1087
- let stdout = "";
1088
- let stderr = "";
1089
- let settled = false;
1090
- const settle = (result) => {
1091
- if (settled)
1092
- return;
1093
- settled = true;
1094
- clearTimeout(timeoutId);
1095
- resolvePromise(result);
1096
- };
1097
- const timeoutId = setTimeout(() => {
1098
- child.kill("SIGTERM");
1099
- setTimeout(() => child.kill("SIGKILL"), 1000).unref();
1100
- settle({ passed: false, timedOut: true, exitCode: 124, evidence: summarizeCommandEvidence(stdout, stderr) });
1101
- }, timeoutMs);
1102
- child.stdout.on("data", (chunk) => {
1103
- stdout += String(chunk);
1104
- if (stdout.length > MAX_VERIFIER_EVIDENCE_BYTES) {
1105
- stdout = stdout.slice(0, MAX_VERIFIER_EVIDENCE_BYTES);
1106
- }
1107
- });
1108
- child.stderr.on("data", (chunk) => {
1109
- stderr += String(chunk);
1110
- if (stderr.length > MAX_VERIFIER_EVIDENCE_BYTES) {
1111
- stderr = stderr.slice(0, MAX_VERIFIER_EVIDENCE_BYTES);
1112
- }
1113
- });
1114
- child.on("error", (_error) => settle({
1115
- passed: false,
1116
- timedOut: false,
1117
- exitCode: 1,
1118
- evidence: summarizeCommandEvidence(stdout, stderr),
1119
- }));
1120
- child.on("close", (code) => settle({
1121
- passed: code === 0,
1122
- timedOut: false,
1123
- exitCode: code ?? 1,
1124
- evidence: summarizeCommandEvidence(stdout, stderr),
1125
- }));
1126
- });
1127
- }
1128
- async function runCheckpointVerifier(projectDir, ir, checkpoint, commandTimeoutMs = DEFAULT_VERIFIER_COMMAND_TIMEOUT_MS) {
1129
- const assertCommands = {
1130
- clean_working_tree: "git status --porcelain",
1131
- lint_passing: "npm run lint --silent 2>/dev/null",
1132
- build_passing: "npm run build --silent 2>/dev/null",
1133
- };
1134
- if (checkpoint.command) {
1135
- if (!isVerifierCommandAllowed(ir, checkpoint.command)) {
1136
- return {
1137
- passed: false,
1138
- target: checkpoint.command,
1139
- evidence: "policy_denied",
1140
- exit_code: 126,
1141
- message: verifierCommandPolicyMessage(checkpoint.command),
1142
- };
1143
- }
1144
- const commandResult = await execVerifierCommand(projectDir, checkpoint.command, commandTimeoutMs);
1145
- return {
1146
- passed: commandResult.passed,
1147
- target: checkpoint.command,
1148
- evidence: commandResult.evidence,
1149
- exit_code: commandResult.exitCode,
1150
- message: commandResult.passed
1151
- ? checkpoint.message
1152
- : commandResult.timedOut
1153
- ? `Verifier command timed out after ${commandTimeoutMs}ms: ${checkpoint.command}`
1154
- : `Verifier command failed: ${checkpoint.command}`,
1155
- };
1156
- }
1157
- if (checkpoint.assert === "clean_working_tree") {
1158
- if (!isBuiltinAssertAllowed(ir, checkpoint.assert)) {
1159
- return {
1160
- passed: false,
1161
- target: checkpoint.assert,
1162
- evidence: "policy_denied",
1163
- exit_code: 126,
1164
- message: verifierAssertPolicyMessage(checkpoint.assert),
1165
- };
1166
- }
1167
- try {
1168
- const raw = await new Promise((resolvePromise, rejectPromise) => {
1169
- const child = spawn("git", ["status", "--porcelain"], { cwd: projectDir, stdio: ["ignore", "pipe", "ignore"] });
1170
- let stdout = "";
1171
- child.stdout.on("data", (chunk) => { stdout += String(chunk); });
1172
- child.on("error", rejectPromise);
1173
- child.on("close", (code) => code === 0 ? resolvePromise(stdout) : rejectPromise(new Error("git status failed")));
1174
- });
1175
- const changedEntries = raw.split("\n").filter(Boolean).length;
1176
- return {
1177
- passed: raw.trim() === "",
1178
- target: checkpoint.assert,
1179
- evidence: `changed_entries=${changedEntries}`,
1180
- exit_code: 0,
1181
- message: checkpoint.message,
1182
- };
1183
- }
1184
- catch {
1185
- return { passed: false, target: checkpoint.assert, exit_code: 1, message: checkpoint.message };
1186
- }
1187
- }
1188
- if (checkpoint.assert === "no_test_regression") {
1189
- if (!isBuiltinAssertAllowed(ir, checkpoint.assert)) {
1190
- return {
1191
- passed: false,
1192
- target: checkpoint.assert,
1193
- evidence: "policy_denied",
1194
- exit_code: 126,
1195
- message: verifierAssertPolicyMessage(checkpoint.assert),
1196
- };
1197
- }
1198
- try {
1199
- const baseline = parseInt(await readFile(resolve(projectDir, ".dna/test-baseline"), "utf-8").catch(() => "0"), 10);
1200
- const current = parseInt(await readFile(resolve(projectDir, ".dna/test-current"), "utf-8").catch(() => "0"), 10);
1201
- return {
1202
- passed: current <= baseline,
1203
- target: checkpoint.assert,
1204
- evidence: `baseline=${baseline} current=${current}`,
1205
- exit_code: 0,
1206
- message: checkpoint.message,
1207
- };
1208
- }
1209
- catch {
1210
- return { passed: false, target: checkpoint.assert, exit_code: 1, message: checkpoint.message };
1211
- }
1212
- }
1213
- if (assertCommands[checkpoint.assert]) {
1214
- if (!isBuiltinAssertAllowed(ir, checkpoint.assert)) {
1215
- return {
1216
- passed: false,
1217
- target: checkpoint.assert,
1218
- evidence: "policy_denied",
1219
- exit_code: 126,
1220
- message: verifierAssertPolicyMessage(checkpoint.assert),
1221
- };
1222
- }
1223
- const commandResult = await execVerifierCommand(projectDir, assertCommands[checkpoint.assert], commandTimeoutMs);
1224
- return {
1225
- passed: commandResult.passed,
1226
- target: checkpoint.assert,
1227
- evidence: commandResult.evidence,
1228
- exit_code: commandResult.exitCode,
1229
- message: commandResult.passed
1230
- ? checkpoint.message
1231
- : commandResult.timedOut
1232
- ? `Verifier command timed out after ${commandTimeoutMs}ms: ${assertCommands[checkpoint.assert]}`
1233
- : checkpoint.message,
1234
- };
1235
- }
1236
- return { passed: false, target: checkpoint.assert, exit_code: 1, message: checkpoint.message };
1237
- }
1238
- function isWithinProjectRoot(projectRoot, targetPath) {
1239
- const relPath = relative(projectRoot, targetPath);
1240
- return relPath === "" || (!relPath.startsWith("..") && !isAbsolute(relPath));
1241
- }
1242
- async function resolveVerifierTarget(projectDir, targetPath) {
1243
- const projectRoot = await realpath(projectDir).catch(() => resolve(projectDir));
1244
- const resolvedPath = resolve(projectDir, targetPath);
1245
- if (!isWithinProjectRoot(projectRoot, resolvedPath)) {
1246
- return null;
1247
- }
1248
- const canonicalParent = await realpath(dirname(resolvedPath)).catch(() => null);
1249
- if (canonicalParent && !isWithinProjectRoot(projectRoot, canonicalParent)) {
1250
- return null;
1251
- }
1252
- const canonicalTarget = await realpath(resolvedPath).catch(() => null);
1253
- if (canonicalTarget && !isWithinProjectRoot(projectRoot, canonicalTarget)) {
1254
- return null;
1255
- }
1256
- return canonicalTarget ?? resolvedPath;
1257
- }
1258
- async function toProjectRelativePath(projectDir, filePath) {
1259
- const projectRoot = await realpath(projectDir).catch(() => resolve(projectDir));
1260
- const resolvedPath = resolve(projectDir, filePath);
1261
- const canonicalPath = await realpath(resolvedPath).catch(() => resolvedPath);
1262
- const targetPath = isWithinProjectRoot(projectRoot, canonicalPath) ? canonicalPath : resolvedPath;
1263
- const relPath = relative(projectRoot, targetPath);
1264
- return relPath === "" ? "." : relPath;
1265
- }
1266
- function getCompletionCheckFields(completion) {
1267
- if (typeof completion !== "object" || completion === null || Array.isArray(completion))
1268
- return [];
1269
- const fields = [];
1270
- if (completion.file_exists !== undefined)
1271
- fields.push("file_exists");
1272
- if (completion.file_not_empty !== undefined)
1273
- fields.push("file_not_empty");
1274
- if (completion.file_contains !== undefined)
1275
- fields.push("file_contains");
1276
- if (completion.command_success !== undefined)
1277
- fields.push("command_success");
1278
- return fields;
1279
- }
1280
- function validateCompletionVerifier(completion) {
1281
- const fields = getCompletionCheckFields(completion);
1282
- if (fields.length !== 1) {
1283
- return {
1284
- message: "Completion verifier must define exactly one check",
1285
- evidence: `fields:${fields.join(",") || "none"}`,
1286
- };
1287
- }
1288
- const field = fields[0];
1289
- if (field === "file_exists" && (typeof completion.file_exists !== "string" || !completion.file_exists)) {
1290
- return { message: "Completion verifier file_exists must be a non-empty string", evidence: "invalid:file_exists" };
1291
- }
1292
- if (field === "file_not_empty" && (typeof completion.file_not_empty !== "string" || !completion.file_not_empty)) {
1293
- return { message: "Completion verifier file_not_empty must be a non-empty string", evidence: "invalid:file_not_empty" };
1294
- }
1295
- if (field === "command_success" && (typeof completion.command_success !== "string" || !completion.command_success)) {
1296
- return { message: "Completion verifier command_success must be a non-empty string", evidence: "invalid:command_success" };
1297
- }
1298
- if (field === "file_contains") {
1299
- const fileContains = completion.file_contains;
1300
- if (typeof fileContains !== "object" || fileContains === null) {
1301
- return { message: "Completion verifier file_contains must define path and pattern", evidence: "invalid:file_contains" };
1302
- }
1303
- if (typeof fileContains.path !== "string" || !fileContains.path) {
1304
- return { message: "Completion verifier file_contains.path must be a non-empty string", evidence: "invalid:file_contains.path" };
1305
- }
1306
- if (typeof fileContains.pattern !== "string" || !fileContains.pattern) {
1307
- return { message: "Completion verifier file_contains.pattern must be a non-empty string", evidence: "invalid:file_contains.pattern" };
1308
- }
1309
- }
1310
- return null;
1311
- }
1312
1005
  function getAuditCheckType(spec) {
1313
1006
  if (spec.completion?.file_exists !== undefined)
1314
1007
  return "file_exists";
@@ -1353,7 +1046,11 @@ async function runVerifierSpec(projectDir, ir, spec, workflowState, sessionId, o
1353
1046
  let artifact;
1354
1047
  let message = spec.checkpoint?.message;
1355
1048
  if (spec.kind === "checkpoint" && spec.checkpoint) {
1356
- const checkpointResult = await runCheckpointVerifier(projectDir, ir, spec.checkpoint, options?.commandTimeoutMs);
1049
+ const checkpointResult = await runSharedCheckpointVerifier(spec.checkpoint, ir, {
1050
+ projectDir,
1051
+ variables: { ...(workflowState.resolved_variables ?? {}), ...(workflowState.inputs ?? {}) },
1052
+ commandTimeoutMs: options?.commandTimeoutMs,
1053
+ });
1357
1054
  passed = checkpointResult.passed;
1358
1055
  target = checkpointResult.target;
1359
1056
  evidence = checkpointResult.evidence;
@@ -1369,99 +1066,17 @@ async function runVerifierSpec(projectDir, ir, spec, workflowState, sessionId, o
1369
1066
  message = "Completion verifier must define exactly one check";
1370
1067
  }
1371
1068
  else {
1372
- const invalidCompletion = validateCompletionVerifier(completion);
1373
- if (invalidCompletion) {
1374
- passed = false;
1375
- evidence = invalidCompletion.evidence;
1376
- message = invalidCompletion.message;
1377
- }
1378
- else if (completion.file_exists) {
1379
- target = completion.file_exists;
1380
- const resolvedTarget = await resolveVerifierTarget(projectDir, completion.file_exists);
1381
- artifact = resolvedTarget ?? undefined;
1382
- evidence = resolvedTarget ? `exists:${resolvedTarget}` : "path_escape";
1383
- if (!resolvedTarget) {
1384
- passed = false;
1385
- message = `Verifier target escapes project root: ${completion.file_exists}`;
1386
- }
1387
- else {
1388
- try {
1389
- await stat(resolvedTarget);
1390
- }
1391
- catch {
1392
- passed = false;
1393
- message = `Required file missing: ${completion.file_exists}`;
1394
- }
1395
- }
1396
- }
1397
- else if (completion.file_not_empty) {
1398
- target = completion.file_not_empty;
1399
- const resolvedTarget = await resolveVerifierTarget(projectDir, completion.file_not_empty);
1400
- artifact = resolvedTarget ?? undefined;
1401
- evidence = resolvedTarget ? `not_empty:${resolvedTarget}` : "path_escape";
1402
- if (!resolvedTarget) {
1403
- passed = false;
1404
- message = `Verifier target escapes project root: ${completion.file_not_empty}`;
1405
- }
1406
- else {
1407
- try {
1408
- const raw = await readFile(resolvedTarget, "utf-8");
1409
- evidence = `bytes=${Buffer.byteLength(raw, "utf8")}`;
1410
- if (raw.trim().length === 0) {
1411
- passed = false;
1412
- message = `Required file is empty: ${completion.file_not_empty}`;
1413
- }
1414
- }
1415
- catch {
1416
- passed = false;
1417
- message = `Required file missing: ${completion.file_not_empty}`;
1418
- }
1419
- }
1420
- }
1421
- else if (completion.file_contains) {
1422
- target = completion.file_contains.path;
1423
- const resolvedTarget = await resolveVerifierTarget(projectDir, completion.file_contains.path);
1424
- artifact = resolvedTarget ?? undefined;
1425
- evidence = `pattern:${completion.file_contains.pattern}`;
1426
- if (!resolvedTarget) {
1427
- passed = false;
1428
- message = `Verifier target escapes project root: ${completion.file_contains.path}`;
1429
- }
1430
- else {
1431
- try {
1432
- const raw = await readFile(resolvedTarget, "utf-8");
1433
- const re = new RegExp(completion.file_contains.pattern);
1434
- if (!re.test(raw)) {
1435
- passed = false;
1436
- message = `Required pattern missing in ${completion.file_contains.path}`;
1437
- }
1438
- }
1439
- catch {
1440
- passed = false;
1441
- message = `Required file missing: ${completion.file_contains.path}`;
1442
- }
1443
- }
1444
- }
1445
- else if (completion.command_success) {
1446
- target = completion.command_success;
1447
- if (!isVerifierCommandAllowed(ir, completion.command_success)) {
1448
- passed = false;
1449
- evidence = "policy_denied";
1450
- exit_code = 126;
1451
- message = verifierCommandPolicyMessage(completion.command_success);
1452
- }
1453
- else {
1454
- const commandResult = await execVerifierCommand(projectDir, completion.command_success, options?.commandTimeoutMs);
1455
- passed = commandResult.passed;
1456
- evidence = commandResult.evidence;
1457
- exit_code = commandResult.exitCode;
1458
- if (!passed) {
1459
- message = commandResult.timedOut
1460
- ? `Verifier command timed out after ${options?.commandTimeoutMs ?? DEFAULT_VERIFIER_COMMAND_TIMEOUT_MS}ms: ${completion.command_success}`
1461
- : `Verifier command failed: ${completion.command_success}`;
1462
- }
1463
- }
1464
- }
1069
+ const completionResult = await runCompletionVerifier(completion, ir, {
1070
+ projectDir,
1071
+ variables: { ...(workflowState.resolved_variables ?? {}), ...(workflowState.inputs ?? {}) },
1072
+ commandTimeoutMs: options?.commandTimeoutMs,
1073
+ });
1074
+ passed = completionResult.passed;
1075
+ target = completionResult.target;
1076
+ evidence = completionResult.evidence;
1077
+ exit_code = completionResult.exit_code;
1078
+ artifact = completionResult.artifact;
1079
+ message = completionResult.message;
1465
1080
  }
1466
1081
  }
1467
1082
  const entry = {