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.
@@ -107,11 +107,12 @@ export function compileControllerToSkill(controllerKey, controller, variables, w
107
107
  lines.push("2. Create a stable controller_run_id once for this invocation before using any artifact path.");
108
108
  lines.push(" - Reuse the same controller_run_id in every child Agent prompt and in every progress validation.");
109
109
  lines.push(" - Do not regenerate controller_run_id inside fix rounds.");
110
- lines.push("3. Reuse diagnosis only when all diagnosis reuse criteria pass:");
110
+ lines.push("3. Reuse the diagnosis artifact only when all diagnosis reuse criteria pass:");
111
111
  for (const criterion of policy.diagnosis_reuse_criteria) {
112
112
  lines.push(` - ${criterion}`);
113
113
  }
114
- lines.push(`4. If diagnosis is missing, invalid, stale, or scope-changed, run diagnosis once with fresh context and sequential gates:`);
114
+ lines.push(" Reuse skips fresh analyzer and analysis_reviewer agents; it never skips the declared diagnosis contract completion gates or the persisted diagnosis review JSON approval gate before any surgeon step.");
115
+ lines.push(`4. If diagnosis is missing, invalid, stale, or scope-changed, run the analyzer once with fresh context:`);
115
116
  lines.push(" a. Spawn fresh analyzer agent with controller_run_id, then wait for completion.");
116
117
  lines.push(" ```");
117
118
  lines.push(" Agent(");
@@ -119,6 +120,7 @@ export function compileControllerToSkill(controllerKey, controller, variables, w
119
120
  lines.push(` prompt="controller_run_id=<controller_run_id>. ${escapePrompt(stepPrompt(diagnosisAnalyzeStep))}"`);
120
121
  lines.push(" )");
121
122
  lines.push(" ```");
123
+ pushWorkflowStepVerifierLines(lines, diagnosisAnalyzeStep, " ");
122
124
  lines.push(` b. Diagnosis artifact gate: read ${controller.diagnosis_artifact_path}; validate it exists, is non-empty, names the safe module argument, and is current for this module before continuing.`);
123
125
  lines.push(" c. Spawn fresh analysis_reviewer agent with controller_run_id, then wait for completion.");
124
126
  lines.push(" ```");
@@ -127,9 +129,12 @@ export function compileControllerToSkill(controllerKey, controller, variables, w
127
129
  lines.push(` prompt="controller_run_id=<controller_run_id>; expected_review_artifact=${controller.diagnosis_review_artifact_path}. ${escapePrompt(stepPrompt(diagnosisReviewStep))}"`);
128
130
  lines.push(" )");
129
131
  lines.push(" ```");
130
- lines.push(` d. Diagnosis review JSON gate: read ${controller.diagnosis_review_artifact_path}; require existing valid JSON with verdict APPROVE before fix rounds.`);
132
+ pushWorkflowStepVerifierLines(lines, diagnosisReviewStep, " ");
133
+ lines.push("5. If the diagnosis artifact was reused, do not spawn analyzer or analysis_reviewer; rerun the declared diagnosis contract completion gates against the existing artifacts:");
134
+ pushWorkflowStepVerifierLines(lines, diagnosisReviewStep, " ");
135
+ lines.push(`6. Diagnosis review JSON gate: read ${controller.diagnosis_review_artifact_path}; require existing valid JSON with verdict APPROVE before fix rounds.`);
131
136
  lines.push(" If the review JSON is missing, invalid, or verdict is not APPROVE, stop immediately with the configured stop report; do NOT enter fix rounds.");
132
- lines.push(`5. For round 1..${controller.max_rounds}, set current_round to the loop number and run one fresh context fix round with sequential gates:`);
137
+ lines.push(`7. For round 1..${controller.max_rounds}, set current_round to the loop number and run one fresh context fix round with sequential gates:`);
133
138
  lines.push(" a. Spawn fresh surgeon agent with controller_run_id and current_round, then wait for completion.");
134
139
  lines.push(" ```");
135
140
  lines.push(" Agent(");
@@ -137,6 +142,7 @@ export function compileControllerToSkill(controllerKey, controller, variables, w
137
142
  lines.push(` prompt="controller_run_id=<controller_run_id>; current_round=<current_round>. Fresh context round for $ARGUMENTS. ${escapePrompt(stepPrompt(fixBugsStep))}"`);
138
143
  lines.push(" )");
139
144
  lines.push(" ```");
145
+ pushWorkflowStepVerifierLines(lines, fixBugsStep, " ");
140
146
  lines.push(" b. Fix handoff gate: validate the current round handoff and fix commit, or an explicit no-commit handoff for current_round, before review.");
141
147
  lines.push(" c. Spawn fresh fix_reviewer agent with controller_run_id, current_round, and expected review artifact path, then wait for completion.");
142
148
  lines.push(" ```");
@@ -145,6 +151,7 @@ export function compileControllerToSkill(controllerKey, controller, variables, w
145
151
  lines.push(` prompt="controller_run_id=<controller_run_id>; current_round=<current_round>; expected_review_artifact=${controller.review_artifact_path}. Fresh context review for $ARGUMENTS. ${escapePrompt(stepPrompt(reviewChangesStep))}"`);
146
152
  lines.push(" )");
147
153
  lines.push(" ```");
154
+ pushWorkflowStepVerifierLines(lines, reviewChangesStep, " ");
148
155
  lines.push(` d. Review JSON gate: read ${controller.review_artifact_path}; validate it exists, is valid JSON, module equals the safe module argument, round equals the provided current_round, run_id equals the provided controller_run_id, updated_at is present, and verdict is APPROVE or REQUEST_CHANGES before verification.`);
149
156
  lines.push(" e. Spawn fresh test_runner agent only after the review gate passes; include controller_run_id and current_round, then wait for completion.");
150
157
  lines.push(" ```");
@@ -153,6 +160,7 @@ export function compileControllerToSkill(controllerKey, controller, variables, w
153
160
  lines.push(` prompt="controller_run_id=<controller_run_id>; current_round=<current_round>. Fresh context verification for $ARGUMENTS. The progress JSON run_id must equal the provided controller_run_id and round must equal the provided current_round; do not use manual-run during controller execution. ${escapePrompt(stepPrompt(verifyReportStep))}"`);
154
161
  lines.push(" )");
155
162
  lines.push(" ```");
163
+ pushWorkflowStepVerifierLines(lines, verifyReportStep, " ");
156
164
  lines.push(` f. Progress JSON read/provenance validation: read ${controller.progress_artifact_path}. The controller must read this JSON sidecar, not Markdown prose.`);
157
165
  lines.push(" g. Verify progress JSON provenance before branching:");
158
166
  for (const check of policy.progress_provenance_checks) {
@@ -163,7 +171,7 @@ export function compileControllerToSkill(controllerKey, controller, variables, w
163
171
  for (const branch of policy.verdict_branches) {
164
172
  lines.push(` - ${branch}`);
165
173
  }
166
- lines.push(`6. If max ${controller.max_rounds} rounds is reached, stop with the configured stop report.`);
174
+ lines.push(`8. If max ${controller.max_rounds} rounds is reached, stop with the configured stop report.`);
167
175
  lines.push("</Steps>");
168
176
  lines.push("");
169
177
  lines.push("<Verdict_Mapping>");
@@ -223,6 +231,37 @@ function requireStepByRole(workflow, role, workflowName) {
223
231
  }
224
232
  return step;
225
233
  }
234
+ function pushWorkflowStepVerifierLines(lines, step, indent) {
235
+ if (step.completion?.length) {
236
+ lines.push(`${indent}Completion gates for workflow step '${step.id}' must pass in declaration order before treating the step as complete:`);
237
+ for (let i = 0; i < step.completion.length; i++) {
238
+ lines.push(`${indent} ${i + 1}. ${describeCompletionCheck(step.completion[i])}`);
239
+ }
240
+ }
241
+ if (step.checkpoints?.length) {
242
+ lines.push(`${indent}Checkpoint gates for workflow step '${step.id}' must pass in declaration order before transitioning:`);
243
+ for (let i = 0; i < step.checkpoints.length; i++) {
244
+ const checkpoint = step.checkpoints[i];
245
+ const action = checkpoint.action ?? "block";
246
+ const command = checkpoint.command ? `; command: ${checkpoint.command}` : "";
247
+ lines.push(`${indent} ${i + 1}. ${checkpoint.assert}: ${checkpoint.message} (${action}${command})`);
248
+ }
249
+ }
250
+ if (step.completion?.some((check) => check.command_success) || step.checkpoints?.some((check) => check.command)) {
251
+ lines.push(`${indent}Command-backed completion/checkpoint gates must use runtime verifier_policy semantics: first authorize the declared raw command against exact allow_commands or allow_command_prefixes, then resolve templates safely, execute the exact resolved command, require exit code 0 for pass, treat policy denial as exit_code=126, and treat any non-zero exit code as blocking failure unless the checkpoint action is warn.`);
252
+ }
253
+ }
254
+ function describeCompletionCheck(check) {
255
+ if (check.file_exists)
256
+ return `file_exists: ${check.file_exists}`;
257
+ if (check.file_not_empty)
258
+ return `file_not_empty: ${check.file_not_empty}`;
259
+ if (check.file_contains)
260
+ return `file_contains: ${check.file_contains.path} matches ${check.file_contains.pattern}`;
261
+ if (check.command_success)
262
+ return `command_success: execute exact command "${check.command_success}", require exit code 0, and block on any non-zero exit code`;
263
+ return "invalid completion check: exactly one completion check field is required";
264
+ }
226
265
  function createSkillLauncher(params) {
227
266
  const lines = [];
228
267
  lines.push("---");
@@ -0,0 +1,51 @@
1
+ import type { CompletionCheck, ConstraintIR, StepCheckpoint, VerifierCommandPolicy } from "../schema/types.js";
2
+ export declare const DEFAULT_VERIFIER_COMMAND_TIMEOUT_MS = 30000;
3
+ export declare const MAX_VERIFIER_EVIDENCE_BYTES = 4096;
4
+ export interface VerifierRuntimeContext {
5
+ projectDir: string;
6
+ variables?: Record<string, string>;
7
+ commandTimeoutMs?: number;
8
+ }
9
+ export interface VerifierCheckResult {
10
+ passed: boolean;
11
+ target?: string;
12
+ evidence?: string;
13
+ exit_code?: number;
14
+ artifact?: string;
15
+ message?: string;
16
+ timedOut?: boolean;
17
+ }
18
+ export declare function resolveVerifierTemplate(template: string, variables?: Record<string, string>): string;
19
+ export declare function verifierTemplateVariableNames(template: string): string[];
20
+ export declare function validateVerifierTemplateVariables(template: string, variables?: Record<string, string>, mode?: "command" | "path"): {
21
+ valid: true;
22
+ } | {
23
+ valid: false;
24
+ variable: string;
25
+ evidence: string;
26
+ message: string;
27
+ };
28
+ export declare function hasUnresolvedVerifierTemplate(value: string): boolean;
29
+ export declare function unresolvedVerifierTemplateMessage(value: string): string;
30
+ export declare function hasUnsafeShellControl(command: string): boolean;
31
+ export declare function isVerifierCommandAllowed(policy: VerifierCommandPolicy | undefined, command: string): boolean;
32
+ export declare function verifierCommandPolicyMessage(command: string): string;
33
+ export declare function isBuiltinAssertAllowed(policy: VerifierCommandPolicy | undefined, assertName: string): boolean;
34
+ export declare function verifierAssertPolicyMessage(assertName: string): string;
35
+ export declare function trimVerifierEvidence(raw: string | undefined): string | undefined;
36
+ export declare function summarizeCommandEvidence(stdout: string, stderr: string): string | undefined;
37
+ export declare function execVerifierCommand(projectDir: string, command: string, timeoutMs?: number, env?: NodeJS.ProcessEnv): Promise<{
38
+ passed: boolean;
39
+ timedOut: boolean;
40
+ exitCode?: number;
41
+ evidence?: string;
42
+ }>;
43
+ export declare function resolveVerifierTarget(projectDir: string, targetPath: string): Promise<string | null>;
44
+ export declare function toProjectRelativePath(projectDir: string, filePath: string): Promise<string>;
45
+ export declare function getCompletionCheckFields(completion: CompletionCheck): string[];
46
+ export declare function validateCompletionVerifier(completion: CompletionCheck): {
47
+ message: string;
48
+ evidence: string;
49
+ } | null;
50
+ export declare function runCompletionVerifier(completion: CompletionCheck, ir: Pick<ConstraintIR, "verifier_policy">, context: VerifierRuntimeContext): Promise<VerifierCheckResult>;
51
+ export declare function runCheckpointVerifier(checkpoint: StepCheckpoint, ir: Pick<ConstraintIR, "verifier_policy">, context: VerifierRuntimeContext): Promise<VerifierCheckResult>;
@@ -0,0 +1,462 @@
1
+ import { spawn } from "node:child_process";
2
+ import { readFile, realpath, stat } from "node:fs/promises";
3
+ import { dirname, isAbsolute, relative, resolve } from "node:path";
4
+ export const DEFAULT_VERIFIER_COMMAND_TIMEOUT_MS = 30_000;
5
+ export const MAX_VERIFIER_EVIDENCE_BYTES = 4096;
6
+ const SAFE_COMMAND_VARIABLE_RE = /^[A-Za-z0-9_-]+$/;
7
+ const SAFE_PATH_VARIABLE_RE = /^[-A-Za-z0-9_./]+$/;
8
+ export function resolveVerifierTemplate(template, variables) {
9
+ let resolved = template;
10
+ if (variables?.ARGUMENTS !== undefined) {
11
+ resolved = resolved.replace(/(^|[^\\])\$ARGUMENTS/g, (_match, prefix) => `${prefix}${variables.ARGUMENTS}`);
12
+ }
13
+ resolved = resolved.replace(/\{\{(\w+)\}\}/g, (match, key) => variables?.[key] ?? match);
14
+ return resolved;
15
+ }
16
+ export function verifierTemplateVariableNames(template) {
17
+ const names = new Set();
18
+ if (/(^|[^\\])\$ARGUMENTS/.test(template))
19
+ names.add("ARGUMENTS");
20
+ for (const match of template.matchAll(/\{\{(\w+)\}\}/g))
21
+ names.add(match[1]);
22
+ return [...names];
23
+ }
24
+ function isWholeVerifierCommandTemplate(command) {
25
+ return /^\{\{\w+\}\}$/.test(command.trim()) || command.trim() === "$ARGUMENTS";
26
+ }
27
+ export function validateVerifierTemplateVariables(template, variables, mode = "command") {
28
+ for (const name of verifierTemplateVariableNames(template)) {
29
+ const value = variables?.[name];
30
+ if (value === undefined)
31
+ continue;
32
+ const matchesSafeCharacters = mode === "path"
33
+ ? SAFE_PATH_VARIABLE_RE.test(value) && !value.startsWith("/") && !value.includes("//") && !value.split(/[\\/]+/).includes("..")
34
+ : SAFE_COMMAND_VARIABLE_RE.test(value);
35
+ if (!matchesSafeCharacters) {
36
+ return {
37
+ valid: false,
38
+ variable: name,
39
+ evidence: `unsafe_variable:${name}`,
40
+ message: `Verifier template variable '${name}' contains unsafe ${mode} characters`,
41
+ };
42
+ }
43
+ }
44
+ return { valid: true };
45
+ }
46
+ function resolveVerifierCommand(rawCommand, policy, variables) {
47
+ const declaredCommand = rawCommand.trim();
48
+ if (isWholeVerifierCommandTemplate(declaredCommand)) {
49
+ const command = resolveVerifierTemplate(declaredCommand, variables).trim();
50
+ if (hasUnresolvedVerifierTemplate(command)) {
51
+ return { target: command, evidence: "unresolved_template", exit_code: 126, message: unresolvedVerifierTemplateMessage(command) };
52
+ }
53
+ if (!isVerifierCommandAllowed(policy, command)) {
54
+ return { target: command, evidence: "policy_denied", exit_code: 126, message: verifierCommandPolicyMessage(command) };
55
+ }
56
+ return { command };
57
+ }
58
+ if (!isVerifierCommandAllowed(policy, declaredCommand)) {
59
+ return { target: declaredCommand, evidence: "policy_denied", exit_code: 126, message: verifierCommandPolicyMessage(declaredCommand) };
60
+ }
61
+ const safeVariables = validateVerifierTemplateVariables(declaredCommand, variables, "command");
62
+ if (!safeVariables.valid) {
63
+ return { target: declaredCommand, evidence: safeVariables.evidence, exit_code: 126, message: safeVariables.message };
64
+ }
65
+ const command = resolveVerifierTemplate(declaredCommand, variables).trim();
66
+ if (hasUnresolvedVerifierTemplate(command)) {
67
+ return { target: command, evidence: "unresolved_template", exit_code: 126, message: unresolvedVerifierTemplateMessage(command) };
68
+ }
69
+ return { command };
70
+ }
71
+ function verifierCommandEnv(variables) {
72
+ const env = {};
73
+ for (const [name, value] of Object.entries(variables ?? {})) {
74
+ if (/^\w+$/.test(name) && SAFE_COMMAND_VARIABLE_RE.test(value)) {
75
+ env[name] = value;
76
+ }
77
+ }
78
+ return env;
79
+ }
80
+ export function hasUnresolvedVerifierTemplate(value) {
81
+ return /\{\{\w+\}\}/.test(value) || /(^|[^\\])\$ARGUMENTS/.test(value);
82
+ }
83
+ export function unresolvedVerifierTemplateMessage(value) {
84
+ return `Verifier template could not be fully resolved: ${value}`;
85
+ }
86
+ export function hasUnsafeShellControl(command) {
87
+ let inSingle = false;
88
+ let inDouble = false;
89
+ let escaped = false;
90
+ for (let i = 0; i < command.length; i++) {
91
+ const ch = command[i];
92
+ const next = command[i + 1] ?? "";
93
+ if (escaped) {
94
+ escaped = false;
95
+ continue;
96
+ }
97
+ if (ch === "\\" && !inSingle) {
98
+ escaped = true;
99
+ continue;
100
+ }
101
+ if (ch === "'" && !inDouble) {
102
+ inSingle = !inSingle;
103
+ continue;
104
+ }
105
+ if (ch === '"' && !inSingle) {
106
+ inDouble = !inDouble;
107
+ continue;
108
+ }
109
+ if (!inSingle && !inDouble) {
110
+ if (ch === ";" || ch === "`" || ch === "\n" || ch === "\r")
111
+ return true;
112
+ if ((ch === "&" || ch === "|" || ch === "<" || ch === ">") && next === ch)
113
+ return true;
114
+ if (ch === "$" && next === "(")
115
+ return true;
116
+ if ((ch === "<" || ch === ">") && next === "(")
117
+ return true;
118
+ if (ch === "|" || ch === "&" || ch === "<" || ch === ">")
119
+ return true;
120
+ }
121
+ if (inDouble) {
122
+ if (ch === "`")
123
+ return true;
124
+ if (ch === "$" && next === "(")
125
+ return true;
126
+ }
127
+ }
128
+ return false;
129
+ }
130
+ export function isVerifierCommandAllowed(policy, command) {
131
+ const normalized = command.trim();
132
+ if (!policy)
133
+ return false;
134
+ if (policy.allow_commands?.includes(normalized))
135
+ return true;
136
+ if (hasUnsafeShellControl(normalized))
137
+ return false;
138
+ return policy.allow_command_prefixes?.some((prefix) => normalized.startsWith(prefix)) ?? false;
139
+ }
140
+ export function verifierCommandPolicyMessage(command) {
141
+ return `Verifier command not allowed by verifier_policy: ${command}`;
142
+ }
143
+ export function isBuiltinAssertAllowed(policy, assertName) {
144
+ return policy?.allow_builtin_asserts?.includes(assertName) ?? false;
145
+ }
146
+ export function verifierAssertPolicyMessage(assertName) {
147
+ return `Verifier assert not allowed by verifier_policy: ${assertName}`;
148
+ }
149
+ export function trimVerifierEvidence(raw) {
150
+ if (!raw)
151
+ return undefined;
152
+ const trimmed = raw.trim();
153
+ if (!trimmed)
154
+ return undefined;
155
+ return trimmed.length > MAX_VERIFIER_EVIDENCE_BYTES
156
+ ? trimmed.slice(0, MAX_VERIFIER_EVIDENCE_BYTES)
157
+ : trimmed;
158
+ }
159
+ export function summarizeCommandEvidence(stdout, stderr) {
160
+ const stdoutBytes = Buffer.byteLength(stdout, "utf8");
161
+ const stderrBytes = Buffer.byteLength(stderr, "utf8");
162
+ if (stdoutBytes === 0 && stderrBytes === 0)
163
+ return undefined;
164
+ return trimVerifierEvidence(`stdout_bytes=${stdoutBytes} stderr_bytes=${stderrBytes}`);
165
+ }
166
+ export async function execVerifierCommand(projectDir, command, timeoutMs = DEFAULT_VERIFIER_COMMAND_TIMEOUT_MS, env) {
167
+ return new Promise((resolvePromise) => {
168
+ const child = spawn("bash", ["-lc", command], {
169
+ cwd: projectDir,
170
+ stdio: ["ignore", "pipe", "pipe"],
171
+ env: { ...process.env, ...(env ?? {}) },
172
+ });
173
+ let stdout = "";
174
+ let stderr = "";
175
+ let settled = false;
176
+ const settle = (result) => {
177
+ if (settled)
178
+ return;
179
+ settled = true;
180
+ clearTimeout(timeoutId);
181
+ resolvePromise(result);
182
+ };
183
+ const timeoutId = setTimeout(() => {
184
+ child.kill("SIGTERM");
185
+ setTimeout(() => child.kill("SIGKILL"), 1000).unref();
186
+ settle({ passed: false, timedOut: true, exitCode: 124, evidence: summarizeCommandEvidence(stdout, stderr) });
187
+ }, timeoutMs);
188
+ child.stdout.on("data", (chunk) => {
189
+ stdout += String(chunk);
190
+ if (stdout.length > MAX_VERIFIER_EVIDENCE_BYTES)
191
+ stdout = stdout.slice(0, MAX_VERIFIER_EVIDENCE_BYTES);
192
+ });
193
+ child.stderr.on("data", (chunk) => {
194
+ stderr += String(chunk);
195
+ if (stderr.length > MAX_VERIFIER_EVIDENCE_BYTES)
196
+ stderr = stderr.slice(0, MAX_VERIFIER_EVIDENCE_BYTES);
197
+ });
198
+ child.on("error", () => settle({
199
+ passed: false,
200
+ timedOut: false,
201
+ exitCode: 1,
202
+ evidence: summarizeCommandEvidence(stdout, stderr),
203
+ }));
204
+ child.on("close", (code) => settle({
205
+ passed: code === 0,
206
+ timedOut: false,
207
+ exitCode: code ?? 1,
208
+ evidence: summarizeCommandEvidence(stdout, stderr),
209
+ }));
210
+ });
211
+ }
212
+ async function execVerifierStdout(projectDir, command, args) {
213
+ return new Promise((resolvePromise, rejectPromise) => {
214
+ const child = spawn(command, args, { cwd: projectDir, stdio: ["ignore", "pipe", "ignore"] });
215
+ let stdout = "";
216
+ child.stdout.on("data", (chunk) => { stdout += String(chunk); });
217
+ child.on("error", rejectPromise);
218
+ child.on("close", (code) => resolvePromise({ stdout, exitCode: code ?? 1 }));
219
+ });
220
+ }
221
+ function isWithinProjectRoot(projectRoot, targetPath) {
222
+ const relPath = relative(projectRoot, targetPath);
223
+ return relPath === "" || (!relPath.startsWith("..") && !isAbsolute(relPath));
224
+ }
225
+ export async function resolveVerifierTarget(projectDir, targetPath) {
226
+ const projectRoot = await realpath(projectDir).catch(() => resolve(projectDir));
227
+ const resolvedPath = resolve(projectRoot, targetPath);
228
+ if (!isWithinProjectRoot(projectRoot, resolvedPath))
229
+ return null;
230
+ const canonicalParent = await realpath(dirname(resolvedPath)).catch(() => null);
231
+ if (canonicalParent && !isWithinProjectRoot(projectRoot, canonicalParent))
232
+ return null;
233
+ const canonicalTarget = await realpath(resolvedPath).catch(() => null);
234
+ if (canonicalTarget && !isWithinProjectRoot(projectRoot, canonicalTarget))
235
+ return null;
236
+ return canonicalTarget ?? resolvedPath;
237
+ }
238
+ export async function toProjectRelativePath(projectDir, filePath) {
239
+ const projectRoot = await realpath(projectDir).catch(() => resolve(projectDir));
240
+ const resolvedPath = resolve(projectRoot, filePath);
241
+ const canonicalPath = await realpath(resolvedPath).catch(() => resolvedPath);
242
+ const targetPath = isWithinProjectRoot(projectRoot, canonicalPath) ? canonicalPath : resolvedPath;
243
+ const relPath = relative(projectRoot, targetPath);
244
+ return relPath === "" ? "." : relPath;
245
+ }
246
+ export function getCompletionCheckFields(completion) {
247
+ if (typeof completion !== "object" || completion === null || Array.isArray(completion))
248
+ return [];
249
+ const fields = [];
250
+ if (completion.file_exists !== undefined)
251
+ fields.push("file_exists");
252
+ if (completion.file_not_empty !== undefined)
253
+ fields.push("file_not_empty");
254
+ if (completion.file_contains !== undefined)
255
+ fields.push("file_contains");
256
+ if (completion.command_success !== undefined)
257
+ fields.push("command_success");
258
+ return fields;
259
+ }
260
+ export function validateCompletionVerifier(completion) {
261
+ const fields = getCompletionCheckFields(completion);
262
+ if (fields.length !== 1) {
263
+ return {
264
+ message: "Completion verifier must define exactly one check",
265
+ evidence: `fields:${fields.join(",") || "none"}`,
266
+ };
267
+ }
268
+ const field = fields[0];
269
+ if (field === "file_exists" && (typeof completion.file_exists !== "string" || !completion.file_exists)) {
270
+ return { message: "Completion verifier file_exists must be a non-empty string", evidence: "invalid:file_exists" };
271
+ }
272
+ if (field === "file_not_empty" && (typeof completion.file_not_empty !== "string" || !completion.file_not_empty)) {
273
+ return { message: "Completion verifier file_not_empty must be a non-empty string", evidence: "invalid:file_not_empty" };
274
+ }
275
+ if (field === "command_success" && (typeof completion.command_success !== "string" || !completion.command_success)) {
276
+ return { message: "Completion verifier command_success must be a non-empty string", evidence: "invalid:command_success" };
277
+ }
278
+ if (field === "file_contains") {
279
+ const fileContains = completion.file_contains;
280
+ if (typeof fileContains !== "object" || fileContains === null) {
281
+ return { message: "Completion verifier file_contains must define path and pattern", evidence: "invalid:file_contains" };
282
+ }
283
+ if (typeof fileContains.path !== "string" || !fileContains.path) {
284
+ return { message: "Completion verifier file_contains.path must be a non-empty string", evidence: "invalid:file_contains.path" };
285
+ }
286
+ if (typeof fileContains.pattern !== "string" || !fileContains.pattern) {
287
+ return { message: "Completion verifier file_contains.pattern must be a non-empty string", evidence: "invalid:file_contains.pattern" };
288
+ }
289
+ }
290
+ return null;
291
+ }
292
+ export async function runCompletionVerifier(completion, ir, context) {
293
+ const invalidCompletion = validateCompletionVerifier(completion);
294
+ if (invalidCompletion) {
295
+ return { passed: false, evidence: invalidCompletion.evidence, message: invalidCompletion.message };
296
+ }
297
+ if (completion.file_exists) {
298
+ const safeVariables = validateVerifierTemplateVariables(completion.file_exists, context.variables, "path");
299
+ if (!safeVariables.valid)
300
+ return { passed: false, target: completion.file_exists, evidence: safeVariables.evidence, message: safeVariables.message };
301
+ const target = resolveVerifierTemplate(completion.file_exists, context.variables);
302
+ if (hasUnresolvedVerifierTemplate(target))
303
+ return { passed: false, target, evidence: "unresolved_template", message: unresolvedVerifierTemplateMessage(target) };
304
+ const resolvedTarget = await resolveVerifierTarget(context.projectDir, target);
305
+ const artifact = resolvedTarget ?? undefined;
306
+ const evidence = resolvedTarget ? `exists:${resolvedTarget}` : "path_escape";
307
+ if (!resolvedTarget)
308
+ return { passed: false, target, evidence, artifact, message: `Verifier target escapes project root: ${target}` };
309
+ try {
310
+ await stat(resolvedTarget);
311
+ return { passed: true, target, evidence, artifact };
312
+ }
313
+ catch {
314
+ return { passed: false, target, evidence, artifact, message: `Required file missing: ${target}` };
315
+ }
316
+ }
317
+ if (completion.file_not_empty) {
318
+ const safeVariables = validateVerifierTemplateVariables(completion.file_not_empty, context.variables, "path");
319
+ if (!safeVariables.valid)
320
+ return { passed: false, target: completion.file_not_empty, evidence: safeVariables.evidence, message: safeVariables.message };
321
+ const target = resolveVerifierTemplate(completion.file_not_empty, context.variables);
322
+ if (hasUnresolvedVerifierTemplate(target))
323
+ return { passed: false, target, evidence: "unresolved_template", message: unresolvedVerifierTemplateMessage(target) };
324
+ const resolvedTarget = await resolveVerifierTarget(context.projectDir, target);
325
+ const artifact = resolvedTarget ?? undefined;
326
+ if (!resolvedTarget)
327
+ return { passed: false, target, evidence: "path_escape", artifact, message: `Verifier target escapes project root: ${target}` };
328
+ try {
329
+ const raw = await readFile(resolvedTarget, "utf-8");
330
+ const evidence = `bytes=${Buffer.byteLength(raw, "utf8")}`;
331
+ return raw.trim().length === 0
332
+ ? { passed: false, target, evidence, artifact, message: `Required file is empty: ${target}` }
333
+ : { passed: true, target, evidence, artifact };
334
+ }
335
+ catch {
336
+ return { passed: false, target, evidence: `not_empty:${resolvedTarget}`, artifact, message: `Required file missing: ${target}` };
337
+ }
338
+ }
339
+ if (completion.file_contains) {
340
+ const safeTargetVariables = validateVerifierTemplateVariables(completion.file_contains.path, context.variables, "path");
341
+ if (!safeTargetVariables.valid)
342
+ return { passed: false, target: completion.file_contains.path, evidence: safeTargetVariables.evidence, message: safeTargetVariables.message };
343
+ const safePatternVariables = validateVerifierTemplateVariables(completion.file_contains.pattern, context.variables);
344
+ if (!safePatternVariables.valid)
345
+ return { passed: false, target: completion.file_contains.pattern, evidence: safePatternVariables.evidence, message: safePatternVariables.message };
346
+ const target = resolveVerifierTemplate(completion.file_contains.path, context.variables);
347
+ const pattern = resolveVerifierTemplate(completion.file_contains.pattern, context.variables);
348
+ if (hasUnresolvedVerifierTemplate(target) || hasUnresolvedVerifierTemplate(pattern)) {
349
+ return { passed: false, target, evidence: "unresolved_template", message: unresolvedVerifierTemplateMessage(hasUnresolvedVerifierTemplate(target) ? target : pattern) };
350
+ }
351
+ const resolvedTarget = await resolveVerifierTarget(context.projectDir, target);
352
+ const artifact = resolvedTarget ?? undefined;
353
+ const evidence = `pattern:${pattern}`;
354
+ if (!resolvedTarget)
355
+ return { passed: false, target, evidence: "path_escape", artifact, message: `Verifier target escapes project root: ${target}` };
356
+ try {
357
+ const raw = await readFile(resolvedTarget, "utf-8");
358
+ const re = new RegExp(pattern);
359
+ return re.test(raw)
360
+ ? { passed: true, target, evidence, artifact }
361
+ : { passed: false, target, evidence, artifact, message: `Required pattern missing in ${target}` };
362
+ }
363
+ catch {
364
+ return { passed: false, target, evidence, artifact, message: `Required file missing: ${target}` };
365
+ }
366
+ }
367
+ const rawCommand = completion.command_success;
368
+ const commandResolution = resolveVerifierCommand(rawCommand, ir.verifier_policy, context.variables);
369
+ if (!commandResolution.command)
370
+ return { passed: false, target: commandResolution.target, evidence: commandResolution.evidence, exit_code: commandResolution.exit_code, message: commandResolution.message };
371
+ const command = commandResolution.command;
372
+ const commandResult = await execVerifierCommand(context.projectDir, command, context.commandTimeoutMs, verifierCommandEnv(context.variables));
373
+ return {
374
+ passed: commandResult.passed,
375
+ target: command,
376
+ evidence: commandResult.evidence,
377
+ exit_code: commandResult.exitCode,
378
+ timedOut: commandResult.timedOut,
379
+ message: commandResult.passed
380
+ ? undefined
381
+ : commandResult.timedOut
382
+ ? `Verifier command timed out after ${context.commandTimeoutMs ?? DEFAULT_VERIFIER_COMMAND_TIMEOUT_MS}ms: ${command}`
383
+ : `Verifier command failed: ${command}`,
384
+ };
385
+ }
386
+ export async function runCheckpointVerifier(checkpoint, ir, context) {
387
+ const assertCommands = {
388
+ clean_working_tree: "git status --porcelain",
389
+ lint_passing: "npm run lint --silent 2>/dev/null",
390
+ build_passing: "npm run build --silent 2>/dev/null",
391
+ };
392
+ if (checkpoint.command) {
393
+ const rawCommand = checkpoint.command;
394
+ const commandResolution = resolveVerifierCommand(rawCommand, ir.verifier_policy, context.variables);
395
+ if (!commandResolution.command)
396
+ return { passed: false, target: commandResolution.target, evidence: commandResolution.evidence, exit_code: commandResolution.exit_code, message: commandResolution.message };
397
+ const command = commandResolution.command;
398
+ const commandResult = await execVerifierCommand(context.projectDir, command, context.commandTimeoutMs, verifierCommandEnv(context.variables));
399
+ return {
400
+ passed: commandResult.passed,
401
+ target: command,
402
+ evidence: commandResult.evidence,
403
+ exit_code: commandResult.exitCode,
404
+ timedOut: commandResult.timedOut,
405
+ message: commandResult.passed
406
+ ? checkpoint.message
407
+ : commandResult.timedOut
408
+ ? `Verifier command timed out after ${context.commandTimeoutMs ?? DEFAULT_VERIFIER_COMMAND_TIMEOUT_MS}ms: ${command}`
409
+ : `Verifier command failed: ${command}`,
410
+ };
411
+ }
412
+ if (checkpoint.assert === "clean_working_tree") {
413
+ if (!isBuiltinAssertAllowed(ir.verifier_policy, checkpoint.assert)) {
414
+ return { passed: false, target: checkpoint.assert, evidence: "policy_denied", exit_code: 126, message: verifierAssertPolicyMessage(checkpoint.assert) };
415
+ }
416
+ try {
417
+ const { stdout, exitCode } = await execVerifierStdout(context.projectDir, "git", ["status", "--porcelain"]);
418
+ const changedEntries = stdout.split("\n").filter(Boolean).length;
419
+ return {
420
+ passed: exitCode === 0 && stdout.trim() === "",
421
+ target: checkpoint.assert,
422
+ evidence: `changed_entries=${changedEntries}`,
423
+ exit_code: exitCode,
424
+ message: checkpoint.message,
425
+ };
426
+ }
427
+ catch {
428
+ return { passed: false, target: checkpoint.assert, exit_code: 1, message: checkpoint.message };
429
+ }
430
+ }
431
+ if (checkpoint.assert === "no_test_regression") {
432
+ if (!isBuiltinAssertAllowed(ir.verifier_policy, checkpoint.assert)) {
433
+ return { passed: false, target: checkpoint.assert, evidence: "policy_denied", exit_code: 126, message: verifierAssertPolicyMessage(checkpoint.assert) };
434
+ }
435
+ try {
436
+ const baseline = parseInt(await readFile(resolve(context.projectDir, ".dna/test-baseline"), "utf-8").catch(() => "0"), 10);
437
+ const current = parseInt(await readFile(resolve(context.projectDir, ".dna/test-current"), "utf-8").catch(() => "0"), 10);
438
+ return { passed: current <= baseline, target: checkpoint.assert, evidence: `baseline=${baseline} current=${current}`, exit_code: 0, message: checkpoint.message };
439
+ }
440
+ catch {
441
+ return { passed: false, target: checkpoint.assert, exit_code: 1, message: checkpoint.message };
442
+ }
443
+ }
444
+ if (assertCommands[checkpoint.assert]) {
445
+ if (!isBuiltinAssertAllowed(ir.verifier_policy, checkpoint.assert)) {
446
+ return { passed: false, target: checkpoint.assert, evidence: "policy_denied", exit_code: 126, message: verifierAssertPolicyMessage(checkpoint.assert) };
447
+ }
448
+ const commandResult = await execVerifierCommand(context.projectDir, assertCommands[checkpoint.assert], context.commandTimeoutMs);
449
+ return {
450
+ passed: commandResult.passed,
451
+ target: checkpoint.assert,
452
+ evidence: commandResult.evidence,
453
+ exit_code: commandResult.exitCode,
454
+ message: commandResult.passed
455
+ ? checkpoint.message
456
+ : commandResult.timedOut
457
+ ? `Verifier command timed out after ${context.commandTimeoutMs ?? DEFAULT_VERIFIER_COMMAND_TIMEOUT_MS}ms: ${assertCommands[checkpoint.assert]}`
458
+ : checkpoint.message,
459
+ };
460
+ }
461
+ return { passed: false, target: checkpoint.assert, exit_code: 1, message: checkpoint.message };
462
+ }