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.
- package/.claude-plugin/marketplace.json +2 -2
- package/.claude-plugin/plugin.json +1 -1
- package/dist/cli/commands/run.d.ts +2 -0
- package/dist/cli/commands/run.js +43 -40
- package/dist/hooks/cli.d.ts +4 -0
- package/dist/hooks/cli.js +21 -406
- package/dist/runtime/skill-adapter.js +44 -5
- package/dist/runtime/verifier.d.ts +51 -0
- package/dist/runtime/verifier.js +462 -0
- package/dist/templates/flutter-rewrite.dna.yaml +80 -4
- package/package.json +1 -1
|
@@ -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.
|
|
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.
|
|
28
|
+
"version": "1.8.2"
|
|
29
29
|
}
|
|
@@ -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.
|
package/dist/cli/commands/run.js
CHANGED
|
@@ -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
|
|
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
|
|
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
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
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
|
-
|
|
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
|
|
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;
|
package/dist/hooks/cli.d.ts
CHANGED
|
@@ -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
|
|
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
|
|
1373
|
-
|
|
1374
|
-
|
|
1375
|
-
|
|
1376
|
-
|
|
1377
|
-
|
|
1378
|
-
|
|
1379
|
-
|
|
1380
|
-
|
|
1381
|
-
|
|
1382
|
-
|
|
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 = {
|