intentdna 1.7.5 → 1.8.1

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.
Files changed (154) hide show
  1. package/.claude-plugin/marketplace.json +2 -2
  2. package/.claude-plugin/plugin.json +1 -1
  3. package/LICENSE +201 -0
  4. package/README.md +143 -194
  5. package/dist/assets/candidates.d.ts +124 -0
  6. package/dist/assets/candidates.js +1249 -0
  7. package/dist/assets/catalog.d.ts +101 -0
  8. package/dist/assets/catalog.js +311 -0
  9. package/dist/assets/evidence.d.ts +93 -0
  10. package/dist/assets/evidence.js +391 -0
  11. package/dist/assets/index.d.ts +4 -0
  12. package/dist/assets/index.js +4 -0
  13. package/dist/assets/metadata.d.ts +19 -0
  14. package/dist/assets/metadata.js +227 -0
  15. package/dist/cli/commands/assets.d.ts +65 -0
  16. package/dist/cli/commands/assets.js +2871 -0
  17. package/dist/cli/commands/auth.d.ts +322 -0
  18. package/dist/cli/commands/auth.js +1592 -0
  19. package/dist/cli/commands/cloud-auth-guidance.d.ts +2 -0
  20. package/dist/cli/commands/cloud-auth-guidance.js +4 -0
  21. package/dist/cli/commands/cloud.d.ts +1359 -0
  22. package/dist/cli/commands/cloud.js +8364 -0
  23. package/dist/cli/commands/evolve.js +18 -2
  24. package/dist/cli/commands/generate.js +25 -2
  25. package/dist/cli/commands/guard.d.ts +1 -1
  26. package/dist/cli/commands/guard.js +1 -1
  27. package/dist/cli/commands/guide.d.ts +15 -0
  28. package/dist/cli/commands/guide.js +1747 -0
  29. package/dist/cli/commands/init.d.ts +12 -0
  30. package/dist/cli/commands/init.js +95 -60
  31. package/dist/cli/commands/org.d.ts +16 -0
  32. package/dist/cli/commands/org.js +2393 -0
  33. package/dist/cli/commands/release.d.ts +59 -0
  34. package/dist/cli/commands/release.js +716 -0
  35. package/dist/cli/commands/report.d.ts +82 -0
  36. package/dist/cli/commands/report.js +1516 -0
  37. package/dist/cli/commands/run.d.ts +8 -1
  38. package/dist/cli/commands/run.js +129 -57
  39. package/dist/cli/commands/sync.d.ts +8 -1
  40. package/dist/cli/commands/sync.js +436 -141
  41. package/dist/cli/commands/templates.d.ts +64 -0
  42. package/dist/cli/commands/templates.js +2151 -1
  43. package/dist/cli/commands/workflow.d.ts +26 -0
  44. package/dist/cli/commands/workflow.js +3053 -0
  45. package/dist/cli/index.js +703 -36
  46. package/dist/compiler/cascade.js +4 -9
  47. package/dist/compiler/compile.js +1 -0
  48. package/dist/governance/evidence-capture-attribution.d.ts +12 -0
  49. package/dist/governance/evidence-capture-attribution.js +46 -0
  50. package/dist/governance/index.d.ts +2 -0
  51. package/dist/governance/index.js +1 -0
  52. package/dist/hooks/cli.d.ts +9 -0
  53. package/dist/hooks/cli.js +170 -411
  54. package/dist/hooks/enforce.d.ts +1 -0
  55. package/dist/hooks/protocol.d.ts +1 -1
  56. package/dist/hooks/protocol.js +1 -1
  57. package/dist/hooks/state.d.ts +41 -0
  58. package/dist/hooks/state.js +126 -2
  59. package/dist/index.d.ts +3 -0
  60. package/dist/index.js +3 -0
  61. package/dist/mcp/tools-state.js +2 -0
  62. package/dist/organization/index.d.ts +121 -0
  63. package/dist/organization/index.js +413 -0
  64. package/dist/runtime/claude-sync-target.d.ts +49 -0
  65. package/dist/runtime/claude-sync-target.js +280 -0
  66. package/dist/runtime/codex-adapter.d.ts +254 -0
  67. package/dist/runtime/codex-adapter.js +1204 -0
  68. package/dist/runtime/codex-sync-target.d.ts +33 -0
  69. package/dist/runtime/codex-sync-target.js +241 -0
  70. package/dist/runtime/index.d.ts +8 -0
  71. package/dist/runtime/index.js +4 -0
  72. package/dist/runtime/output-artifact-registry.d.ts +1 -1
  73. package/dist/runtime/output-artifact-registry.js +2 -0
  74. package/dist/runtime/settings-adapter.d.ts +1 -1
  75. package/dist/runtime/settings-adapter.js +1 -1
  76. package/dist/runtime/skill-adapter.d.ts +5 -1
  77. package/dist/runtime/skill-adapter.js +164 -39
  78. package/dist/runtime/sync-artifact-registry.d.ts +99 -0
  79. package/dist/runtime/sync-artifact-registry.js +194 -0
  80. package/dist/runtime/sync-target-plan.d.ts +45 -0
  81. package/dist/runtime/sync-target-plan.js +19 -0
  82. package/dist/runtime/verifier.d.ts +51 -0
  83. package/dist/runtime/verifier.js +462 -0
  84. package/dist/schema/controller-registry.d.ts +8 -1
  85. package/dist/schema/controller-registry.js +8 -0
  86. package/dist/schema/types.d.ts +35 -23
  87. package/dist/schema/types.js +10 -1
  88. package/dist/schema/validate.js +22 -22
  89. package/dist/schema/validators/controllers.js +38 -20
  90. package/dist/schema/workflow-authoring-contract.d.ts +29 -0
  91. package/dist/schema/workflow-authoring-contract.js +109 -0
  92. package/dist/schema/yaml-parser.js +18 -0
  93. package/dist/templates/api-backend.dna.yaml +19 -0
  94. package/dist/templates/brainstorming-first.dna.yaml +19 -0
  95. package/dist/templates/code-cleanup.dna.yaml +19 -0
  96. package/dist/templates/code-review-pipeline.dna.yaml +18 -0
  97. package/dist/templates/documentation-writer.dna.yaml +19 -0
  98. package/dist/templates/flutter-behavior-lock.dna.yaml +19 -0
  99. package/dist/templates/flutter-refactoring-rescue.dna.yaml +19 -0
  100. package/dist/templates/flutter-rewrite-diagnosis-review.dna.yaml +210 -0
  101. package/dist/templates/flutter-rewrite-fix-review-loop.dna.yaml +260 -0
  102. package/dist/templates/flutter-rewrite-new.dna.yaml +506 -0
  103. package/dist/templates/flutter-rewrite.dna.yaml +100 -4
  104. package/dist/templates/frontend-quality.dna.yaml +19 -0
  105. package/dist/templates/full-pipeline.dna.yaml +20 -0
  106. package/dist/templates/marketplace.d.ts +92 -0
  107. package/dist/templates/marketplace.js +502 -0
  108. package/dist/templates/metadata.d.ts +25 -0
  109. package/dist/templates/metadata.js +136 -3
  110. package/dist/templates/mobile-dev.dna.yaml +19 -0
  111. package/dist/templates/multi-agent-handoff-coordination.dna.yaml +156 -0
  112. package/dist/templates/multi-perspective-review.dna.yaml +19 -0
  113. package/dist/templates/persistent-executor.dna.yaml +19 -0
  114. package/dist/templates/qa-loop.dna.yaml +18 -0
  115. package/dist/templates/release-evidence-gate.dna.yaml +131 -0
  116. package/dist/templates/requirements-gate.dna.yaml +19 -0
  117. package/dist/templates/research-orchestration.dna.yaml +19 -0
  118. package/dist/templates/root-cause-analysis.dna.yaml +19 -0
  119. package/dist/templates/safe-refactoring.dna.yaml +321 -0
  120. package/dist/templates/secure-dev.dna.yaml +19 -0
  121. package/dist/templates/subagent-parallel.dna.yaml +19 -0
  122. package/dist/templates/systematic-debugging.dna.yaml +19 -0
  123. package/dist/templates/tdd-strict.dna.yaml +19 -0
  124. package/dist/templates/ui-visual-regression-qa.dna.yaml +133 -0
  125. package/dist/templates/workflow-author.dna.yaml +302 -0
  126. package/dist/templates/workflow-blocked-scope-triage.dna.yaml +242 -0
  127. package/dist/templates/workflow-completion-audit.dna.yaml +254 -0
  128. package/dist/templates/workflow-cross-boundary-resource-approval.dna.yaml +249 -0
  129. package/dist/templates/workflow-evidence-review.dna.yaml +198 -0
  130. package/dist/templates/workflow-module-inventory.dna.yaml +257 -0
  131. package/dist/templates/workflow-runtime-smoke-review.dna.yaml +252 -0
  132. package/dist/templates/workflow-verification-command-routing.dna.yaml +278 -0
  133. package/dist/workflow/authoring-packet.d.ts +67 -0
  134. package/dist/workflow/authoring-packet.js +218 -0
  135. package/dist/workflow/draft.d.ts +159 -0
  136. package/dist/workflow/draft.js +800 -0
  137. package/dist/workflow/index.d.ts +3 -0
  138. package/dist/workflow/index.js +3 -0
  139. package/dist/workflow/suggestion-review.d.ts +65 -0
  140. package/dist/workflow/suggestion-review.js +245 -0
  141. package/package.json +1 -1
  142. package/spec/README.md +15 -48
  143. package/spec/codex-adapter-contract.md +176 -0
  144. package/spec/sync-target-plan.md +158 -0
  145. package/spec/workflow-capability-ir.md +153 -0
  146. package/spec/agent-dispatch-fix.md +0 -116
  147. package/spec/control-plane-convergence-handoff-2026-04-24.md +0 -432
  148. package/spec/flutter-rewrite-template-optimization.md +0 -301
  149. package/spec/foundation-hardening.md +0 -178
  150. package/spec/hooks-infra-harness-hardening.md +0 -741
  151. package/spec/multi-config.md +0 -172
  152. package/spec/parallel-isolation.md +0 -268
  153. package/spec/template-version-namespace-fix.md +0 -653
  154. package/spec/workflow-pipeline.md +0 -101
package/dist/hooks/cli.js CHANGED
@@ -23,10 +23,12 @@ import { fileURLToPath } from "node:url";
23
23
  import { readStdin, writeOutput, silentOutput, allowOutput, blockOutput, stopOutput } from "./protocol.js";
24
24
  import { validateHookInput } from "./schema.js";
25
25
  import { enforcePreToolUse, enforcePostToolUse, enforceUserPromptSubmit, enforceSubagentStop, enforcePreCompact, enforceNotification, enforceSessionStart, enforceStop, extractBashWritePaths, checkReflectionLimit, checkContextReadiness, checkWorkflowBoundary, } from "./enforce.js";
26
- import { appendAudit, readWorkflowState, appendTrace, appendRuntimeDecisionEvent, rotateTraces, readTraces, cleanStaleState, readSurgeonAttempts, writeSurgeonAttempts, appendSessionRead, readSessionReads, appendVerifierResult, appendCompletedArtifact, artifactIdentity, buildArtifactKey, readArtifactManifest, resolveArtifactTemplate, safePathComponent, writeArtifactManifest } from "./state.js";
26
+ import { appendAudit, readWorkflowState, appendTrace, appendRuntimeDecisionEvent, appendEvidenceCaptureEvent, rotateTraces, readTraces, cleanStaleState, readSurgeonAttempts, writeSurgeonAttempts, appendSessionRead, readSessionReads, appendVerifierResult, appendCompletedArtifact, artifactIdentity, buildArtifactKey, readArtifactManifest, resolveArtifactTemplate, safePathComponent, writeArtifactManifest, EVIDENCE_CAPTURE_SCHEMA_VERSION } from "./state.js";
27
27
  import { hookEventsForSurface } from "./event-registry.js";
28
28
  import { writeAuditEvent } from "../audit/index.js";
29
- import { RUNTIME_DECISION_EVENT_SCHEMA_VERSION } from "../governance/index.js";
29
+ import { RUNTIME_DECISION_EVENT_SCHEMA_VERSION, trustedEvidenceCaptureAttribution } from "../governance/index.js";
30
+ import { runCodexHook } from "../runtime/codex-adapter.js";
31
+ import { runCheckpointVerifier as runSharedCheckpointVerifier, runCompletionVerifier, toProjectRelativePath, } from "../runtime/verifier.js";
30
32
  // ── Constants ──────────────────────────────────────────────
31
33
  const DEFAULT_IR_PATH = ".dna/compiled/ir.json";
32
34
  const VALID_EVENTS = new Set(hookEventsForSurface("cli"));
@@ -150,6 +152,74 @@ function buildRuntimeDecisionEvent(params) {
150
152
  evidence_refs: [{ type: "trace", ref: params.traceId }],
151
153
  };
152
154
  }
155
+ function captureResultForHook(rawInput, decision) {
156
+ if (decision === "block")
157
+ return "blocked";
158
+ if (typeof rawInput.result === "string" && ["success", "failure", "blocked", "skipped", "unknown", "unsupported"].includes(rawInput.result)) {
159
+ return rawInput.result;
160
+ }
161
+ if (typeof rawInput.exit_code === "number")
162
+ return rawInput.exit_code === 0 ? "success" : "failure";
163
+ return "unknown";
164
+ }
165
+ function captureFactForHook(rawInput, decision, event) {
166
+ if (decision === "block")
167
+ return "blocker";
168
+ if (event === "PostToolUse" || event === "Stop" || rawInput.result || rawInput.exit_code !== undefined)
169
+ return "result";
170
+ const toolInput = typeof rawInput.tool_input === "object" && rawInput.tool_input !== null
171
+ ? rawInput.tool_input
172
+ : {};
173
+ if (typeof rawInput.command === "string" || typeof toolInput.command === "string")
174
+ return "command";
175
+ return "result";
176
+ }
177
+ function commandForHook(rawInput) {
178
+ const toolInput = typeof rawInput.tool_input === "object" && rawInput.tool_input !== null
179
+ ? rawInput.tool_input
180
+ : {};
181
+ const command = rawInput.command ?? toolInput.command;
182
+ return typeof command === "string" && command.trim().length > 0 ? command : undefined;
183
+ }
184
+ function buildClaudeEvidenceCaptureEvent(params) {
185
+ const fact = captureFactForHook(params.rawInput, params.decision, params.event);
186
+ const result = captureResultForHook(params.rawInput, params.decision);
187
+ const command = commandForHook(params.rawInput);
188
+ const toolName = typeof params.rawInput.tool_name === "string" ? params.rawInput.tool_name : "unknown tool";
189
+ const subject = command ?? `${toolName} ${params.event}`;
190
+ const attribution = trustedEvidenceCaptureAttribution(params.ir, {
191
+ workflowAsset: params.wfState?.workflow_asset,
192
+ workflowId: params.wfState?.workflow,
193
+ stepId: params.wfState?.current_step ?? params.runtimeEvent.step_id,
194
+ });
195
+ return {
196
+ schema_version: EVIDENCE_CAPTURE_SCHEMA_VERSION,
197
+ event_id: `c5:${params.runtimeEvent.event_id}`,
198
+ captured_at: params.runtimeEvent.timestamp,
199
+ project_root: params.projectDir,
200
+ source: "claude_code",
201
+ workflow_asset: attribution.workflowAsset,
202
+ workflow_id: attribution.workflowId,
203
+ run_id: params.runtimeEvent.run_id || params.runtimeEvent.session_id || "unknown",
204
+ step_id: attribution.stepId === "unknown" && !params.wfState
205
+ ? `${params.event}:direct`
206
+ : attribution.stepId,
207
+ fact,
208
+ summary: `Claude hook observed ${fact} for ${subject}: ${result}`,
209
+ command,
210
+ result,
211
+ exit_code: typeof params.rawInput.exit_code === "number" ? params.rawInput.exit_code : undefined,
212
+ duration_ms: typeof params.rawInput.duration_ms === "number" ? params.rawInput.duration_ms : undefined,
213
+ manual_intervention: fact === "manual_intervention"
214
+ ? { required: true, reason: params.reason, actor: "human" }
215
+ : undefined,
216
+ evidence_refs: [
217
+ { type: "runtime_decision_event", ref: params.runtimeEvent.event_id },
218
+ ...params.runtimeEvent.evidence_refs.map((ref) => ({ type: ref.type, ref: ref.ref })),
219
+ ],
220
+ redaction: "intentdna.c5.claude_hook_secret_redaction.v1",
221
+ };
222
+ }
153
223
  export function computeSummary(traces) {
154
224
  let blocks = 0, warns = 0;
155
225
  const toolCounts = new Map();
@@ -193,7 +263,12 @@ export function formatSummary(s) {
193
263
  async function main() {
194
264
  // Parse args
195
265
  const args = process.argv.slice(2);
196
- const event = args[0];
266
+ const eventArg = args[0];
267
+ if (eventArg === "CodexHook") {
268
+ await runCodexHookCli(args);
269
+ return;
270
+ }
271
+ const event = eventArg;
197
272
  if (!event || !VALID_EVENTS.has(event)) {
198
273
  writeOutput(silentOutput());
199
274
  return;
@@ -245,7 +320,7 @@ export async function runHookEvent(options) {
245
320
  const writeRuntimeDecision = async (output, result, wfState, traceId = randomUUID()) => {
246
321
  const decision = normalizeDecision(output);
247
322
  const reason = decision !== "allow" ? (output.reason ?? output.hookSpecificOutput?.additionalContext ?? "unknown") : "allowed by active policy";
248
- await appendRuntimeDecisionEvent(projectDir, buildRuntimeDecisionEvent({
323
+ const runtimeEvent = buildRuntimeDecisionEvent({
249
324
  ir,
250
325
  rawInput,
251
326
  output,
@@ -257,7 +332,18 @@ export async function runHookEvent(options) {
257
332
  decision,
258
333
  reason,
259
334
  wfState,
260
- }), sessionId);
335
+ });
336
+ await appendRuntimeDecisionEvent(projectDir, runtimeEvent, sessionId);
337
+ await appendEvidenceCaptureEvent(projectDir, buildClaudeEvidenceCaptureEvent({
338
+ ir,
339
+ rawInput,
340
+ event,
341
+ projectDir,
342
+ decision,
343
+ reason,
344
+ runtimeEvent,
345
+ wfState,
346
+ }));
261
347
  };
262
348
  let wfStateRaw = null;
263
349
  // Load workflow state for events that need it (handoff context + PreCompact preservation)
@@ -272,6 +358,7 @@ export async function runHookEvent(options) {
272
358
  state.workflowState = {
273
359
  current_step: wfStateRaw.current_step,
274
360
  workflow: wfStateRaw.workflow,
361
+ workflow_asset: wfStateRaw.workflow_asset,
275
362
  current_role: wfStateRaw.current_role,
276
363
  completed_artifacts: wfStateRaw.completed_artifacts,
277
364
  iteration: wfStateRaw.iteration, // G4: pass iteration for state-driven rules
@@ -337,6 +424,8 @@ export async function runHookEvent(options) {
337
424
  current_step: wfState.current_step,
338
425
  current_role: wfState.current_role,
339
426
  started_at: wfState.started_at,
427
+ inputs: wfState.inputs,
428
+ resolved_variables: wfState.resolved_variables,
340
429
  completed_artifacts: wfState.completed_artifacts,
341
430
  artifact_facts: stopArtifactFacts,
342
431
  } : null;
@@ -421,6 +510,8 @@ export async function runHookEvent(options) {
421
510
  workflow: wfStateRaw.workflow,
422
511
  current_step: wfStateRaw.current_step,
423
512
  current_role: wfStateRaw.current_role,
513
+ inputs: wfStateRaw.inputs,
514
+ resolved_variables: wfStateRaw.resolved_variables,
424
515
  }, "post_tool_use", sessionId);
425
516
  const failingResults = verifierResults.filter((result) => result.status === "fail");
426
517
  if (failingResults.length > 0) {
@@ -554,6 +645,62 @@ function dispatch(event, ir, input, state, roles) {
554
645
  return null;
555
646
  }
556
647
  }
648
+ // ── Codex Hook Entry ───────────────────────────────────────
649
+ function argValue(args, flag) {
650
+ const index = args.indexOf(flag);
651
+ return index >= 0 ? args[index + 1] : undefined;
652
+ }
653
+ function codexInstallMode(value) {
654
+ return value === "plugin_scoped" || value === "managed" || value === "none" ? value : "project_fallback";
655
+ }
656
+ export function codexHookEventForInput(rawInput, args) {
657
+ return argValue(args, "--codex-event") ?? rawInput.event;
658
+ }
659
+ function inferWorkflowAssetFromIr(ir, workflowId) {
660
+ const attribution = trustedEvidenceCaptureAttribution(ir, { workflowId });
661
+ return attribution.workflowAsset === "unknown" ? undefined : attribution.workflowAsset;
662
+ }
663
+ async function runCodexHookWithLoadedIr(rawInput, projectDir, args, ir) {
664
+ const sessionId = typeof rawInput.session_id === "string"
665
+ ? rawInput.session_id
666
+ : typeof rawInput.sessionId === "string"
667
+ ? rawInput.sessionId
668
+ : undefined;
669
+ const sessionWorkflowState = sessionId
670
+ ? await readWorkflowState(projectDir, sessionId)
671
+ : null;
672
+ const workflowState = sessionWorkflowState ?? await readWorkflowState(projectDir);
673
+ const workflowAsset = inferWorkflowAssetFromIr(ir, workflowState?.workflow);
674
+ return runCodexHook({ ...rawInput, event: codexHookEventForInput(rawInput, args) }, {
675
+ ir,
676
+ projectDir,
677
+ hookInstallMode: codexInstallMode(argValue(args, "--codex-install-mode")),
678
+ hookTrusted: true,
679
+ codexVersion: process.env.CODEX_VERSION,
680
+ workflowAsset,
681
+ workflowId: workflowState?.workflow,
682
+ stepId: workflowState?.current_step,
683
+ appendEvent: true,
684
+ });
685
+ }
686
+ async function runCodexHookCli(args) {
687
+ try {
688
+ const rawInput = await readStdin(5000);
689
+ const projectDir = typeof rawInput.cwd === "string" ? rawInput.cwd : process.cwd();
690
+ const irPath = argValue(args, "--ir") ?? DEFAULT_IR_PATH;
691
+ const ir = await loadIR(resolve(projectDir, irPath));
692
+ if (!ir) {
693
+ writeOutput(silentOutput());
694
+ return;
695
+ }
696
+ const result = await runCodexHookWithLoadedIr(rawInput, projectDir, args, ir);
697
+ writeOutput(result.output);
698
+ }
699
+ catch (err) {
700
+ process.stderr.write(`\n Intent DNA: CodexHook failed open — ${err instanceof Error ? err.message : String(err)}\n`);
701
+ writeOutput(silentOutput());
702
+ }
703
+ }
557
704
  // ── IR Loading ─────────────────────────────────────────────
558
705
  /** Expected IR schema version — must match plugin-adapter.ts */
559
706
  const EXPECTED_IR_VERSION = 1;
@@ -777,6 +924,8 @@ function appendArtifactResolverTrace(projectDir, event, wfState, output, session
777
924
  export const recordProducedArtifactsForTest = recordProducedArtifacts;
778
925
  export const resolveWorkflowArtifactFactsForTest = resolveWorkflowArtifactFacts;
779
926
  export const readWorkflowStateForHookTest = readWorkflowStateForHook;
927
+ export const runCodexHookWithLoadedIrForTest = runCodexHookWithLoadedIr;
928
+ export const inferWorkflowAssetFromIrForTest = inferWorkflowAssetFromIr;
780
929
  export const resolveWorkflowArtifactFactsForHookTest = resolveWorkflowArtifactFactsForHook;
781
930
  export const finalizeAndResolveArtifactsForHookTest = finalizeAndResolveArtifactsForHook;
782
931
  function clearBlockingVerifierCheckpoints(ir, workflowState) {
@@ -807,8 +956,6 @@ function clearBlockingVerifierCheckpoints(ir, workflowState) {
807
956
  }),
808
957
  };
809
958
  }
810
- const DEFAULT_VERIFIER_COMMAND_TIMEOUT_MS = 30_000;
811
- const MAX_VERIFIER_EVIDENCE_BYTES = 4096;
812
959
  function appendOutputText(output, text, hookEventName) {
813
960
  if (!text)
814
961
  return output;
@@ -855,316 +1002,6 @@ export function appendStopVerifierWarnings(output, verifierResults, currentStep)
855
1002
  "\nFix the warning above before relying on this step, or rerun the verifier after repair.";
856
1003
  return appendOutputText(output, warningText, "Stop");
857
1004
  }
858
- function trimEvidence(raw) {
859
- if (!raw)
860
- return undefined;
861
- const trimmed = raw.trim();
862
- if (!trimmed)
863
- return undefined;
864
- return trimmed.length > MAX_VERIFIER_EVIDENCE_BYTES
865
- ? trimmed.slice(0, MAX_VERIFIER_EVIDENCE_BYTES)
866
- : trimmed;
867
- }
868
- function hasUnsafeShellControl(command) {
869
- let inSingle = false;
870
- let inDouble = false;
871
- let escaped = false;
872
- for (let i = 0; i < command.length; i++) {
873
- const ch = command[i];
874
- const next = command[i + 1] ?? "";
875
- if (escaped) {
876
- escaped = false;
877
- continue;
878
- }
879
- if (ch === "\\" && !inSingle) {
880
- escaped = true;
881
- continue;
882
- }
883
- if (ch === "'" && !inDouble) {
884
- inSingle = !inSingle;
885
- continue;
886
- }
887
- if (ch === '"' && !inSingle) {
888
- inDouble = !inDouble;
889
- continue;
890
- }
891
- if (!inSingle && !inDouble) {
892
- if (ch === ";" || ch === "`" || ch === "\n" || ch === "\r")
893
- return true;
894
- if ((ch === "&" || ch === "|" || ch === "<" || ch === ">") && next === ch)
895
- return true;
896
- if (ch === "$" && next === "(")
897
- return true;
898
- if ((ch === "<" || ch === ">") && next === "(")
899
- return true;
900
- if (ch === "|" || ch === "&" || ch === "<" || ch === ">")
901
- return true;
902
- }
903
- if (inDouble) {
904
- if (ch === "`")
905
- return true;
906
- if (ch === "$" && next === "(")
907
- return true;
908
- }
909
- }
910
- return false;
911
- }
912
- function isVerifierCommandAllowed(ir, command) {
913
- const normalized = command.trim();
914
- const policy = ir.verifier_policy;
915
- if (!policy)
916
- return false;
917
- if (policy.allow_commands?.includes(normalized))
918
- return true;
919
- if (hasUnsafeShellControl(normalized))
920
- return false;
921
- return policy.allow_command_prefixes?.some((prefix) => normalized.startsWith(prefix)) ?? false;
922
- }
923
- function verifierCommandPolicyMessage(command) {
924
- return `Verifier command not allowed by verifier_policy: ${command}`;
925
- }
926
- function isBuiltinAssertAllowed(ir, assertName) {
927
- const policy = ir.verifier_policy;
928
- return policy?.allow_builtin_asserts?.includes(assertName) ?? false;
929
- }
930
- function verifierAssertPolicyMessage(assertName) {
931
- return `Verifier assert not allowed by verifier_policy: ${assertName}`;
932
- }
933
- function summarizeCommandEvidence(stdout, stderr) {
934
- const stdoutBytes = Buffer.byteLength(stdout, "utf8");
935
- const stderrBytes = Buffer.byteLength(stderr, "utf8");
936
- if (stdoutBytes === 0 && stderrBytes === 0)
937
- return undefined;
938
- return trimEvidence(`stdout_bytes=${stdoutBytes} stderr_bytes=${stderrBytes}`);
939
- }
940
- async function execVerifierCommand(projectDir, command, timeoutMs = DEFAULT_VERIFIER_COMMAND_TIMEOUT_MS) {
941
- return new Promise((resolvePromise) => {
942
- const child = spawn("bash", ["-lc", command], { cwd: projectDir, stdio: ["ignore", "pipe", "pipe"] });
943
- let stdout = "";
944
- let stderr = "";
945
- let settled = false;
946
- const settle = (result) => {
947
- if (settled)
948
- return;
949
- settled = true;
950
- clearTimeout(timeoutId);
951
- resolvePromise(result);
952
- };
953
- const timeoutId = setTimeout(() => {
954
- child.kill("SIGTERM");
955
- setTimeout(() => child.kill("SIGKILL"), 1000).unref();
956
- settle({ passed: false, timedOut: true, exitCode: 124, evidence: summarizeCommandEvidence(stdout, stderr) });
957
- }, timeoutMs);
958
- child.stdout.on("data", (chunk) => {
959
- stdout += String(chunk);
960
- if (stdout.length > MAX_VERIFIER_EVIDENCE_BYTES) {
961
- stdout = stdout.slice(0, MAX_VERIFIER_EVIDENCE_BYTES);
962
- }
963
- });
964
- child.stderr.on("data", (chunk) => {
965
- stderr += String(chunk);
966
- if (stderr.length > MAX_VERIFIER_EVIDENCE_BYTES) {
967
- stderr = stderr.slice(0, MAX_VERIFIER_EVIDENCE_BYTES);
968
- }
969
- });
970
- child.on("error", (_error) => settle({
971
- passed: false,
972
- timedOut: false,
973
- exitCode: 1,
974
- evidence: summarizeCommandEvidence(stdout, stderr),
975
- }));
976
- child.on("close", (code) => settle({
977
- passed: code === 0,
978
- timedOut: false,
979
- exitCode: code ?? 1,
980
- evidence: summarizeCommandEvidence(stdout, stderr),
981
- }));
982
- });
983
- }
984
- async function runCheckpointVerifier(projectDir, ir, checkpoint, commandTimeoutMs = DEFAULT_VERIFIER_COMMAND_TIMEOUT_MS) {
985
- const assertCommands = {
986
- clean_working_tree: "git status --porcelain",
987
- lint_passing: "npm run lint --silent 2>/dev/null",
988
- build_passing: "npm run build --silent 2>/dev/null",
989
- };
990
- if (checkpoint.command) {
991
- if (!isVerifierCommandAllowed(ir, checkpoint.command)) {
992
- return {
993
- passed: false,
994
- target: checkpoint.command,
995
- evidence: "policy_denied",
996
- exit_code: 126,
997
- message: verifierCommandPolicyMessage(checkpoint.command),
998
- };
999
- }
1000
- const commandResult = await execVerifierCommand(projectDir, checkpoint.command, commandTimeoutMs);
1001
- return {
1002
- passed: commandResult.passed,
1003
- target: checkpoint.command,
1004
- evidence: commandResult.evidence,
1005
- exit_code: commandResult.exitCode,
1006
- message: commandResult.passed
1007
- ? checkpoint.message
1008
- : commandResult.timedOut
1009
- ? `Verifier command timed out after ${commandTimeoutMs}ms: ${checkpoint.command}`
1010
- : `Verifier command failed: ${checkpoint.command}`,
1011
- };
1012
- }
1013
- if (checkpoint.assert === "clean_working_tree") {
1014
- if (!isBuiltinAssertAllowed(ir, checkpoint.assert)) {
1015
- return {
1016
- passed: false,
1017
- target: checkpoint.assert,
1018
- evidence: "policy_denied",
1019
- exit_code: 126,
1020
- message: verifierAssertPolicyMessage(checkpoint.assert),
1021
- };
1022
- }
1023
- try {
1024
- const raw = await new Promise((resolvePromise, rejectPromise) => {
1025
- const child = spawn("git", ["status", "--porcelain"], { cwd: projectDir, stdio: ["ignore", "pipe", "ignore"] });
1026
- let stdout = "";
1027
- child.stdout.on("data", (chunk) => { stdout += String(chunk); });
1028
- child.on("error", rejectPromise);
1029
- child.on("close", (code) => code === 0 ? resolvePromise(stdout) : rejectPromise(new Error("git status failed")));
1030
- });
1031
- const changedEntries = raw.split("\n").filter(Boolean).length;
1032
- return {
1033
- passed: raw.trim() === "",
1034
- target: checkpoint.assert,
1035
- evidence: `changed_entries=${changedEntries}`,
1036
- exit_code: 0,
1037
- message: checkpoint.message,
1038
- };
1039
- }
1040
- catch {
1041
- return { passed: false, target: checkpoint.assert, exit_code: 1, message: checkpoint.message };
1042
- }
1043
- }
1044
- if (checkpoint.assert === "no_test_regression") {
1045
- if (!isBuiltinAssertAllowed(ir, checkpoint.assert)) {
1046
- return {
1047
- passed: false,
1048
- target: checkpoint.assert,
1049
- evidence: "policy_denied",
1050
- exit_code: 126,
1051
- message: verifierAssertPolicyMessage(checkpoint.assert),
1052
- };
1053
- }
1054
- try {
1055
- const baseline = parseInt(await readFile(resolve(projectDir, ".dna/test-baseline"), "utf-8").catch(() => "0"), 10);
1056
- const current = parseInt(await readFile(resolve(projectDir, ".dna/test-current"), "utf-8").catch(() => "0"), 10);
1057
- return {
1058
- passed: current <= baseline,
1059
- target: checkpoint.assert,
1060
- evidence: `baseline=${baseline} current=${current}`,
1061
- exit_code: 0,
1062
- message: checkpoint.message,
1063
- };
1064
- }
1065
- catch {
1066
- return { passed: false, target: checkpoint.assert, exit_code: 1, message: checkpoint.message };
1067
- }
1068
- }
1069
- if (assertCommands[checkpoint.assert]) {
1070
- if (!isBuiltinAssertAllowed(ir, checkpoint.assert)) {
1071
- return {
1072
- passed: false,
1073
- target: checkpoint.assert,
1074
- evidence: "policy_denied",
1075
- exit_code: 126,
1076
- message: verifierAssertPolicyMessage(checkpoint.assert),
1077
- };
1078
- }
1079
- const commandResult = await execVerifierCommand(projectDir, assertCommands[checkpoint.assert], commandTimeoutMs);
1080
- return {
1081
- passed: commandResult.passed,
1082
- target: checkpoint.assert,
1083
- evidence: commandResult.evidence,
1084
- exit_code: commandResult.exitCode,
1085
- message: commandResult.passed
1086
- ? checkpoint.message
1087
- : commandResult.timedOut
1088
- ? `Verifier command timed out after ${commandTimeoutMs}ms: ${assertCommands[checkpoint.assert]}`
1089
- : checkpoint.message,
1090
- };
1091
- }
1092
- return { passed: false, target: checkpoint.assert, exit_code: 1, message: checkpoint.message };
1093
- }
1094
- function isWithinProjectRoot(projectRoot, targetPath) {
1095
- const relPath = relative(projectRoot, targetPath);
1096
- return relPath === "" || (!relPath.startsWith("..") && !isAbsolute(relPath));
1097
- }
1098
- async function resolveVerifierTarget(projectDir, targetPath) {
1099
- const projectRoot = await realpath(projectDir).catch(() => resolve(projectDir));
1100
- const resolvedPath = resolve(projectDir, targetPath);
1101
- if (!isWithinProjectRoot(projectRoot, resolvedPath)) {
1102
- return null;
1103
- }
1104
- const canonicalParent = await realpath(dirname(resolvedPath)).catch(() => null);
1105
- if (canonicalParent && !isWithinProjectRoot(projectRoot, canonicalParent)) {
1106
- return null;
1107
- }
1108
- const canonicalTarget = await realpath(resolvedPath).catch(() => null);
1109
- if (canonicalTarget && !isWithinProjectRoot(projectRoot, canonicalTarget)) {
1110
- return null;
1111
- }
1112
- return canonicalTarget ?? resolvedPath;
1113
- }
1114
- async function toProjectRelativePath(projectDir, filePath) {
1115
- const projectRoot = await realpath(projectDir).catch(() => resolve(projectDir));
1116
- const resolvedPath = resolve(projectDir, filePath);
1117
- const canonicalPath = await realpath(resolvedPath).catch(() => resolvedPath);
1118
- const targetPath = isWithinProjectRoot(projectRoot, canonicalPath) ? canonicalPath : resolvedPath;
1119
- const relPath = relative(projectRoot, targetPath);
1120
- return relPath === "" ? "." : relPath;
1121
- }
1122
- function getCompletionCheckFields(completion) {
1123
- if (typeof completion !== "object" || completion === null || Array.isArray(completion))
1124
- return [];
1125
- const fields = [];
1126
- if (completion.file_exists !== undefined)
1127
- fields.push("file_exists");
1128
- if (completion.file_not_empty !== undefined)
1129
- fields.push("file_not_empty");
1130
- if (completion.file_contains !== undefined)
1131
- fields.push("file_contains");
1132
- if (completion.command_success !== undefined)
1133
- fields.push("command_success");
1134
- return fields;
1135
- }
1136
- function validateCompletionVerifier(completion) {
1137
- const fields = getCompletionCheckFields(completion);
1138
- if (fields.length !== 1) {
1139
- return {
1140
- message: "Completion verifier must define exactly one check",
1141
- evidence: `fields:${fields.join(",") || "none"}`,
1142
- };
1143
- }
1144
- const field = fields[0];
1145
- if (field === "file_exists" && (typeof completion.file_exists !== "string" || !completion.file_exists)) {
1146
- return { message: "Completion verifier file_exists must be a non-empty string", evidence: "invalid:file_exists" };
1147
- }
1148
- if (field === "file_not_empty" && (typeof completion.file_not_empty !== "string" || !completion.file_not_empty)) {
1149
- return { message: "Completion verifier file_not_empty must be a non-empty string", evidence: "invalid:file_not_empty" };
1150
- }
1151
- if (field === "command_success" && (typeof completion.command_success !== "string" || !completion.command_success)) {
1152
- return { message: "Completion verifier command_success must be a non-empty string", evidence: "invalid:command_success" };
1153
- }
1154
- if (field === "file_contains") {
1155
- const fileContains = completion.file_contains;
1156
- if (typeof fileContains !== "object" || fileContains === null) {
1157
- return { message: "Completion verifier file_contains must define path and pattern", evidence: "invalid:file_contains" };
1158
- }
1159
- if (typeof fileContains.path !== "string" || !fileContains.path) {
1160
- return { message: "Completion verifier file_contains.path must be a non-empty string", evidence: "invalid:file_contains.path" };
1161
- }
1162
- if (typeof fileContains.pattern !== "string" || !fileContains.pattern) {
1163
- return { message: "Completion verifier file_contains.pattern must be a non-empty string", evidence: "invalid:file_contains.pattern" };
1164
- }
1165
- }
1166
- return null;
1167
- }
1168
1005
  function getAuditCheckType(spec) {
1169
1006
  if (spec.completion?.file_exists !== undefined)
1170
1007
  return "file_exists";
@@ -1209,7 +1046,11 @@ async function runVerifierSpec(projectDir, ir, spec, workflowState, sessionId, o
1209
1046
  let artifact;
1210
1047
  let message = spec.checkpoint?.message;
1211
1048
  if (spec.kind === "checkpoint" && spec.checkpoint) {
1212
- 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
+ });
1213
1054
  passed = checkpointResult.passed;
1214
1055
  target = checkpointResult.target;
1215
1056
  evidence = checkpointResult.evidence;
@@ -1225,99 +1066,17 @@ async function runVerifierSpec(projectDir, ir, spec, workflowState, sessionId, o
1225
1066
  message = "Completion verifier must define exactly one check";
1226
1067
  }
1227
1068
  else {
1228
- const invalidCompletion = validateCompletionVerifier(completion);
1229
- if (invalidCompletion) {
1230
- passed = false;
1231
- evidence = invalidCompletion.evidence;
1232
- message = invalidCompletion.message;
1233
- }
1234
- else if (completion.file_exists) {
1235
- target = completion.file_exists;
1236
- const resolvedTarget = await resolveVerifierTarget(projectDir, completion.file_exists);
1237
- artifact = resolvedTarget ?? undefined;
1238
- evidence = resolvedTarget ? `exists:${resolvedTarget}` : "path_escape";
1239
- if (!resolvedTarget) {
1240
- passed = false;
1241
- message = `Verifier target escapes project root: ${completion.file_exists}`;
1242
- }
1243
- else {
1244
- try {
1245
- await stat(resolvedTarget);
1246
- }
1247
- catch {
1248
- passed = false;
1249
- message = `Required file missing: ${completion.file_exists}`;
1250
- }
1251
- }
1252
- }
1253
- else if (completion.file_not_empty) {
1254
- target = completion.file_not_empty;
1255
- const resolvedTarget = await resolveVerifierTarget(projectDir, completion.file_not_empty);
1256
- artifact = resolvedTarget ?? undefined;
1257
- evidence = resolvedTarget ? `not_empty:${resolvedTarget}` : "path_escape";
1258
- if (!resolvedTarget) {
1259
- passed = false;
1260
- message = `Verifier target escapes project root: ${completion.file_not_empty}`;
1261
- }
1262
- else {
1263
- try {
1264
- const raw = await readFile(resolvedTarget, "utf-8");
1265
- evidence = `bytes=${Buffer.byteLength(raw, "utf8")}`;
1266
- if (raw.trim().length === 0) {
1267
- passed = false;
1268
- message = `Required file is empty: ${completion.file_not_empty}`;
1269
- }
1270
- }
1271
- catch {
1272
- passed = false;
1273
- message = `Required file missing: ${completion.file_not_empty}`;
1274
- }
1275
- }
1276
- }
1277
- else if (completion.file_contains) {
1278
- target = completion.file_contains.path;
1279
- const resolvedTarget = await resolveVerifierTarget(projectDir, completion.file_contains.path);
1280
- artifact = resolvedTarget ?? undefined;
1281
- evidence = `pattern:${completion.file_contains.pattern}`;
1282
- if (!resolvedTarget) {
1283
- passed = false;
1284
- message = `Verifier target escapes project root: ${completion.file_contains.path}`;
1285
- }
1286
- else {
1287
- try {
1288
- const raw = await readFile(resolvedTarget, "utf-8");
1289
- const re = new RegExp(completion.file_contains.pattern);
1290
- if (!re.test(raw)) {
1291
- passed = false;
1292
- message = `Required pattern missing in ${completion.file_contains.path}`;
1293
- }
1294
- }
1295
- catch {
1296
- passed = false;
1297
- message = `Required file missing: ${completion.file_contains.path}`;
1298
- }
1299
- }
1300
- }
1301
- else if (completion.command_success) {
1302
- target = completion.command_success;
1303
- if (!isVerifierCommandAllowed(ir, completion.command_success)) {
1304
- passed = false;
1305
- evidence = "policy_denied";
1306
- exit_code = 126;
1307
- message = verifierCommandPolicyMessage(completion.command_success);
1308
- }
1309
- else {
1310
- const commandResult = await execVerifierCommand(projectDir, completion.command_success, options?.commandTimeoutMs);
1311
- passed = commandResult.passed;
1312
- evidence = commandResult.evidence;
1313
- exit_code = commandResult.exitCode;
1314
- if (!passed) {
1315
- message = commandResult.timedOut
1316
- ? `Verifier command timed out after ${options?.commandTimeoutMs ?? DEFAULT_VERIFIER_COMMAND_TIMEOUT_MS}ms: ${completion.command_success}`
1317
- : `Verifier command failed: ${completion.command_success}`;
1318
- }
1319
- }
1320
- }
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;
1321
1080
  }
1322
1081
  }
1323
1082
  const entry = {