intentdna 1.8.2 → 1.8.4

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -9,7 +9,7 @@
9
9
  {
10
10
  "name": "intentdna",
11
11
  "description": "Declarative policy layer for AI agent behavior with plugin-managed hook runtime for Claude Code.",
12
- "version": "1.8.2",
12
+ "version": "1.8.4",
13
13
  "author": {
14
14
  "name": "Samuel"
15
15
  },
@@ -25,5 +25,5 @@
25
25
  ]
26
26
  }
27
27
  ],
28
- "version": "1.8.2"
28
+ "version": "1.8.4"
29
29
  }
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "intentdna",
3
- "version": "1.8.2",
3
+ "version": "1.8.4",
4
4
  "description": "Declarative policy layer for AI agent behavior",
5
5
  "author": {
6
6
  "name": "Samuel"
package/README.md CHANGED
@@ -17,11 +17,11 @@ IntentDNA is not a model company, generic agent framework, prompt library, canva
17
17
  ## Install or update
18
18
 
19
19
  ```bash
20
- npm install --global intentdna@1.8.0
20
+ npm install --global intentdna@1.8.4
21
21
  dna --version
22
22
  ```
23
23
 
24
- Version `1.8.0` adds the passive evidence-to-review improvement path and the Online `/create` C3 authoring guide. Online generates copyable local commands only; you still run and review them in your own project.
24
+ Version `1.8.4` makes delegated workflow completion gates enforceable at the real Claude handoff boundary, returns bounded validator diagnostics to retries, and links C5-backed suggestions to concrete capture event IDs. Online generates copyable local commands only; you still run and review them in your own project.
25
25
 
26
26
  ## Product Shape
27
27
 
@@ -20,6 +20,7 @@ export interface AssetImprovementSuggestion {
20
20
  type: string;
21
21
  ref: string;
22
22
  }>;
23
+ targetWorkflow?: string;
23
24
  rationale: string;
24
25
  sourceModules: string[];
25
26
  nextAction: string;
@@ -644,7 +644,7 @@ function suggestionAction(params) {
644
644
  asset: params.asset,
645
645
  suggestionId: params.suggestion.id,
646
646
  saveTemplate: params.saveTemplate,
647
- workflow: params.workflow,
647
+ workflow: params.workflow ?? params.suggestion.targetWorkflow,
648
648
  }),
649
649
  };
650
650
  }
@@ -667,7 +667,7 @@ function buildSuggestionImprovementRoute(params) {
667
667
  ? suggestionReviewCommand({
668
668
  asset: params.asset.name,
669
669
  suggestionId: params.draftSuggestion.id,
670
- workflow: "fix",
670
+ workflow: params.draftSuggestion.targetWorkflow ?? "fix",
671
671
  decision: "accept",
672
672
  })
673
673
  : undefined;
@@ -722,8 +722,8 @@ function buildSuggestionImprovementRoute(params) {
722
722
  ];
723
723
  }
724
724
  function firstDraftableSuggestion(suggestions) {
725
- return suggestions.find((suggestion) => suggestion.type === "template_improvement" ||
726
- suggestion.id === WORKFLOW_SUGGESTION_IDS.c5CapturedRunFacts);
725
+ return suggestions.find((suggestion) => suggestion.id === WORKFLOW_SUGGESTION_IDS.c5CapturedRunFacts && suggestion.targetWorkflow) ??
726
+ suggestions.find((suggestion) => suggestion.type === "template_improvement");
727
727
  }
728
728
  function evidenceSuggestionDraftCommand(asset, evidence, saveTemplate) {
729
729
  if (!evidence || asset.status.evidencePosture !== "dogfood_linked")
@@ -753,7 +753,10 @@ function c5GapCaptures(index) {
753
753
  }
754
754
  function c5EvidenceRefs(captures) {
755
755
  const refs = captures.flatMap((capture) => [
756
+ { type: "capture_event_id", ref: capture.eventId },
756
757
  { type: "capture_event", ref: capture.path },
758
+ ...(capture.workflowAsset !== "unknown" ? [{ type: "workflow_asset", ref: capture.workflowAsset }] : []),
759
+ ...(capture.workflowId !== "unknown" ? [{ type: "workflow", ref: capture.workflowId }] : []),
757
760
  ...capture.evidenceRefs,
758
761
  ]);
759
762
  const seen = new Set();
@@ -765,6 +768,31 @@ function c5EvidenceRefs(captures) {
765
768
  return true;
766
769
  });
767
770
  }
771
+ function assetWorkflowTarget(asset, runtimeWorkflowId) {
772
+ if (!runtimeWorkflowId || runtimeWorkflowId === "unknown")
773
+ return undefined;
774
+ const workflowIds = new Set(asset.workflows.map((workflow) => workflow.id));
775
+ if (workflowIds.has(runtimeWorkflowId))
776
+ return runtimeWorkflowId;
777
+ if (asset.namespace && runtimeWorkflowId.startsWith(`${asset.namespace}_`)) {
778
+ const unprefixed = runtimeWorkflowId.slice(asset.namespace.length + 1);
779
+ if (workflowIds.has(unprefixed))
780
+ return unprefixed;
781
+ }
782
+ return undefined;
783
+ }
784
+ function c5TargetWorkflow(asset, captures) {
785
+ const targets = new Set();
786
+ for (const capture of captures) {
787
+ const target = assetWorkflowTarget(asset, capture.workflowId);
788
+ if (!target)
789
+ return undefined;
790
+ targets.add(target);
791
+ if (targets.size > 1)
792
+ return undefined;
793
+ }
794
+ return targets.size === 1 ? [...targets][0] : undefined;
795
+ }
768
796
  function c5CaptureBootstrapCommands(assetName) {
769
797
  return [
770
798
  `dna init --template ${assetName}`,
@@ -836,7 +864,7 @@ function defaultSuggestionCommands(params) {
836
864
  const reviewCommand = suggestionReviewCommand({
837
865
  asset: params.asset.name,
838
866
  suggestionId: params.suggestion.id,
839
- workflow: "fix",
867
+ workflow: params.suggestion.targetWorkflow ?? "fix",
840
868
  decision: "accept",
841
869
  });
842
870
  return [
@@ -846,7 +874,7 @@ function defaultSuggestionCommands(params) {
846
874
  asset: params.asset.name,
847
875
  suggestionId: params.suggestion.id,
848
876
  saveTemplate: `${params.asset.name}-v2`,
849
- workflow: "fix",
877
+ workflow: params.suggestion.targetWorkflow ?? "fix",
850
878
  }),
851
879
  `dna assets diff ${params.asset.name} ${params.asset.name}-v2`,
852
880
  `dna assets adoption ${params.asset.name}-v2`,
@@ -926,20 +954,21 @@ export function buildAssetSuggestions(asset, index, assets = [asset]) {
926
954
  });
927
955
  }
928
956
  if (c5Gaps.length > 0) {
957
+ const targetWorkflow = c5TargetWorkflow(asset, c5Gaps);
929
958
  const evidenceCommand = `dna assets evidence ${asset.name} --json`;
930
959
  const suggestionsCommand = `dna workflow suggestions ${asset.name} --json`;
931
- const reviewCommand = suggestionReviewCommand({
960
+ const reviewCommand = targetWorkflow ? suggestionReviewCommand({
932
961
  asset: asset.name,
933
962
  suggestionId: WORKFLOW_SUGGESTION_IDS.c5CapturedRunFacts,
934
- workflow: "fix",
963
+ workflow: targetWorkflow,
935
964
  decision: "accept",
936
- });
937
- const draftCommand = suggestionDraftCommand({
965
+ }) : undefined;
966
+ const draftCommand = targetWorkflow ? suggestionDraftCommand({
938
967
  asset: asset.name,
939
968
  suggestionId: WORKFLOW_SUGGESTION_IDS.c5CapturedRunFacts,
940
969
  saveTemplate: `${asset.name}-v2`,
941
- workflow: "fix",
942
- });
970
+ workflow: targetWorkflow,
971
+ }) : undefined;
943
972
  suggestions.push({
944
973
  id: WORKFLOW_SUGGESTION_IDS.c5CapturedRunFacts,
945
974
  type: "evidence_improvement",
@@ -947,14 +976,17 @@ export function buildAssetSuggestions(asset, index, assets = [asset]) {
947
976
  title: "Review captured local run facts before changing the workflow asset",
948
977
  evidence: c5Gaps.slice(0, 5).map(compactEvidenceCapture).join("; "),
949
978
  evidenceRefs: c5EvidenceRefs(c5Gaps),
979
+ targetWorkflow,
950
980
  rationale: "C5 hook evidence shows blocked, failed, retry, unsupported, or human-intervention facts. Evidence can suggest a reviewed change, but it must not mutate policy or templates automatically.",
951
981
  sourceModules: [],
952
- nextAction: `Review and accept the C5-backed suggestion before drafting: ${reviewCommand}`,
982
+ nextAction: reviewCommand
983
+ ? `Review and accept the C5-backed suggestion before drafting: ${reviewCommand}`
984
+ : `Inspect the C5 evidence and choose one matching workflow before review; mixed or unknown workflow attribution cannot be routed automatically.`,
953
985
  nextCommands: [
954
986
  evidenceCommand,
955
987
  suggestionsCommand,
956
- reviewCommand,
957
- draftCommand,
988
+ ...(reviewCommand ? [reviewCommand] : []),
989
+ ...(draftCommand ? [draftCommand] : []),
958
990
  `record explicit manager rationale for any C5-derived change to ${asset.name}`,
959
991
  ],
960
992
  mutationBoundary: "C5 captured facts are local review evidence only; they do not edit templates, mutate policy, adopt assets, bind organizations, sync runtime artifacts, upload cloud reports, or create runtime authority.",
@@ -1055,8 +1087,10 @@ export function buildAssetSuggestions(asset, index, assets = [asset]) {
1055
1087
  index,
1056
1088
  suggestion,
1057
1089
  }));
1058
- const activeFollowup = active.length > 0 ? activeEvidenceFollowupReviewRoute(asset.name, assets, index) : undefined;
1059
1090
  const draftSuggestion = firstDraftableSuggestion(completedSuggestions);
1091
+ const activeFollowup = active.length > 0 && draftSuggestion?.id !== WORKFLOW_SUGGESTION_IDS.c5CapturedRunFacts
1092
+ ? activeEvidenceFollowupReviewRoute(asset.name, assets, index)
1093
+ : undefined;
1060
1094
  const nextCommands = [
1061
1095
  `dna assets evidence ${asset.name}`,
1062
1096
  ];
@@ -1075,7 +1109,7 @@ export function buildAssetSuggestions(asset, index, assets = [asset]) {
1075
1109
  nextCommands.push(suggestionReviewCommand({
1076
1110
  asset: asset.name,
1077
1111
  suggestionId: draftSuggestion.id,
1078
- workflow: "fix",
1112
+ workflow: draftSuggestion.targetWorkflow ?? "fix",
1079
1113
  decision: "accept",
1080
1114
  }));
1081
1115
  }
package/dist/cli/index.js CHANGED
File without changes
@@ -99,10 +99,13 @@ export declare function runVerifiersForTest(projectDir: string, ir: ConstraintIR
99
99
  workflow: string;
100
100
  current_step: string;
101
101
  current_role: string;
102
+ iteration?: number;
103
+ started_at?: string;
102
104
  inputs?: Record<string, string>;
103
105
  resolved_variables?: Record<string, string>;
104
106
  }, when: VerifierSpec["when"], sessionId?: string, options?: {
105
107
  commandTimeoutMs?: number;
108
+ triggerEvent?: HookEvent;
106
109
  }): Promise<VerifierResultEntry[]>;
107
110
  export declare function runStopVerifiersForTest(projectDir: string, ir: ConstraintIR, workflowState: {
108
111
  workflow: string;
package/dist/hooks/cli.js CHANGED
@@ -23,7 +23,7 @@ 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, appendEvidenceCaptureEvent, rotateTraces, readTraces, cleanStaleState, readSurgeonAttempts, writeSurgeonAttempts, appendSessionRead, readSessionReads, appendVerifierResult, appendCompletedArtifact, artifactIdentity, buildArtifactKey, readArtifactManifest, resolveArtifactTemplate, safePathComponent, writeArtifactManifest, EVIDENCE_CAPTURE_SCHEMA_VERSION } from "./state.js";
26
+ import { appendAudit, readWorkflowState, appendTrace, appendRuntimeDecisionEvent, appendEvidenceCaptureEvent, rotateTraces, readTraces, cleanStaleState, readSurgeonAttempts, writeSurgeonAttempts, appendSessionRead, readSessionReads, appendVerifierResult, readVerifierResults, 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
29
  import { RUNTIME_DECISION_EVENT_SCHEMA_VERSION, trustedEvidenceCaptureAttribution } from "../governance/index.js";
@@ -347,13 +347,16 @@ export async function runHookEvent(options) {
347
347
  };
348
348
  let wfStateRaw = null;
349
349
  // Load workflow state for events that need it (handoff context + PreCompact preservation)
350
- if (event === "PreToolUse" || event === "PostToolUse" || event === "PreCompact") {
350
+ if (event === "PreToolUse" || event === "PostToolUse" || event === "PreCompact" || event === "SubagentStop") {
351
351
  const workflowState = await readWorkflowStateForHook(projectDir, event, sessionId);
352
352
  if ("output" in workflowState) {
353
353
  await writeRuntimeDecision(workflowState.output, { output: workflowState.output, trace: { matched_rule: "workflow_boundary" } }, null);
354
354
  return finish(workflowState.output);
355
355
  }
356
356
  wfStateRaw = workflowState.state;
357
+ if (event === "SubagentStop" && wfStateRaw && !subagentMatchesWorkflowRole(rawInput, wfStateRaw)) {
358
+ wfStateRaw = null;
359
+ }
357
360
  if (wfStateRaw && wfStateRaw.active) {
358
361
  state.workflowState = {
359
362
  current_step: wfStateRaw.current_step,
@@ -363,13 +366,15 @@ export async function runHookEvent(options) {
363
366
  completed_artifacts: wfStateRaw.completed_artifacts,
364
367
  iteration: wfStateRaw.iteration, // G4: pass iteration for state-driven rules
365
368
  };
366
- const artifactFacts = await resolveWorkflowArtifactFactsForHook(projectDir, ir, wfStateRaw, event, sessionId);
367
- if ("output" in artifactFacts) {
368
- appendArtifactResolverTrace(projectDir, event, wfStateRaw, artifactFacts.output, sessionId);
369
- await writeRuntimeDecision(artifactFacts.output, { output: artifactFacts.output, trace: { matched_rule: "handoff", step_id: wfStateRaw.current_step } }, wfStateRaw);
370
- return finish(artifactFacts.output);
369
+ if (event !== "SubagentStop") {
370
+ const artifactFacts = await resolveWorkflowArtifactFactsForHook(projectDir, ir, wfStateRaw, event, sessionId);
371
+ if ("output" in artifactFacts) {
372
+ appendArtifactResolverTrace(projectDir, event, wfStateRaw, artifactFacts.output, sessionId);
373
+ await writeRuntimeDecision(artifactFacts.output, { output: artifactFacts.output, trace: { matched_rule: "handoff", step_id: wfStateRaw.current_step } }, wfStateRaw);
374
+ return finish(artifactFacts.output);
375
+ }
376
+ state.artifactFacts = artifactFacts.facts;
371
377
  }
372
- state.artifactFacts = artifactFacts.facts;
373
378
  }
374
379
  }
375
380
  if (event === "PreToolUse" && wfStateRaw?.active) {
@@ -400,6 +405,46 @@ export async function runHookEvent(options) {
400
405
  return finish(gateResult.output);
401
406
  }
402
407
  }
408
+ if (event === "SubagentStop" && wfStateRaw?.active && subagentMatchesWorkflowRole(rawInput, wfStateRaw)) {
409
+ const artifactFacts = await finalizeAndResolveArtifactsForHook(projectDir, ir, wfStateRaw, event, sessionId);
410
+ if ("output" in artifactFacts) {
411
+ appendArtifactResolverTrace(projectDir, event, wfStateRaw, artifactFacts.output, sessionId);
412
+ await writeRuntimeDecision(artifactFacts.output, { output: artifactFacts.output, trace: { matched_rule: "handoff", step_id: wfStateRaw.current_step } }, wfStateRaw);
413
+ return finish(artifactFacts.output);
414
+ }
415
+ const verifierContext = {
416
+ workflow: wfStateRaw.workflow,
417
+ current_step: wfStateRaw.current_step,
418
+ current_role: wfStateRaw.current_role,
419
+ iteration: wfStateRaw.iteration,
420
+ started_at: wfStateRaw.started_at,
421
+ inputs: wfStateRaw.inputs,
422
+ resolved_variables: wfStateRaw.resolved_variables,
423
+ artifact_facts: artifactFacts.facts,
424
+ };
425
+ const verifierResults = [
426
+ ...await runVerifiersForTest(projectDir, ir, verifierContext, "pre_handoff", sessionId, { triggerEvent: "SubagentStop" }),
427
+ ...await runVerifiersForTest(projectDir, ir, verifierContext, "post_step", sessionId, { triggerEvent: "SubagentStop" }),
428
+ ];
429
+ const blockingVerifierFailures = verifierResults.filter((result) => result.status === "fail" && result.severity === "block");
430
+ if (blockingVerifierFailures.length > 0) {
431
+ const output = blockOutput(`[Intent DNA] SubagentStop verifier failures at step '${wfStateRaw.current_step}':\n` +
432
+ blockingVerifierFailures.map((result) => ` - ${result.message ?? result.verifier_id}`).join("\n"));
433
+ const traceId = randomUUID();
434
+ appendTrace(projectDir, {
435
+ trace_id: traceId,
436
+ event,
437
+ workflow: wfStateRaw.workflow,
438
+ step: wfStateRaw.current_step,
439
+ decision: "block",
440
+ reason: output.reason,
441
+ duration_ms: 0,
442
+ timestamp: new Date().toISOString(),
443
+ }, sessionId).catch(() => { });
444
+ await writeRuntimeDecision(output, { output, trace: { matched_rule: "validator", step_id: wfStateRaw.current_step } }, wfStateRaw, traceId);
445
+ return finish(output);
446
+ }
447
+ }
403
448
  // Special handling for Stop — needs async workflow state read + session summary
404
449
  if (event === "Stop") {
405
450
  const workflowState = await readWorkflowStateForHook(projectDir, event, sessionId);
@@ -423,6 +468,7 @@ export async function runHookEvent(options) {
423
468
  workflow: wfState.workflow,
424
469
  current_step: wfState.current_step,
425
470
  current_role: wfState.current_role,
471
+ iteration: wfState.iteration,
426
472
  started_at: wfState.started_at,
427
473
  inputs: wfState.inputs,
428
474
  resolved_variables: wfState.resolved_variables,
@@ -430,7 +476,7 @@ export async function runHookEvent(options) {
430
476
  artifact_facts: stopArtifactFacts,
431
477
  } : null;
432
478
  const verifierResults = stopContext?.active
433
- ? await runStopVerifiersForTest(projectDir, ir, stopContext, sessionId)
479
+ ? await runStopVerifiersForHook(projectDir, ir, stopContext, sessionId)
434
480
  : [];
435
481
  const blockingVerifierFailures = verifierResults.filter((result) => result.status === "fail" && result.severity === "block");
436
482
  let stopOutput = blockingVerifierFailures.length > 0
@@ -660,16 +706,21 @@ function inferWorkflowAssetFromIr(ir, workflowId) {
660
706
  const attribution = trustedEvidenceCaptureAttribution(ir, { workflowId });
661
707
  return attribution.workflowAsset === "unknown" ? undefined : attribution.workflowAsset;
662
708
  }
709
+ function agentTypeForWorkflowRole(role) {
710
+ if (!role)
711
+ return undefined;
712
+ return `dna-${role.replace(/([a-z])([A-Z])/g, "$1-$2").replace(/[_\s]+/g, "-").toLowerCase()}`;
713
+ }
714
+ function subagentMatchesWorkflowRole(rawInput, wfState) {
715
+ return typeof rawInput.agent_type === "string" && rawInput.agent_type === agentTypeForWorkflowRole(wfState.current_role);
716
+ }
663
717
  async function runCodexHookWithLoadedIr(rawInput, projectDir, args, ir) {
664
718
  const sessionId = typeof rawInput.session_id === "string"
665
719
  ? rawInput.session_id
666
720
  : typeof rawInput.sessionId === "string"
667
721
  ? rawInput.sessionId
668
722
  : undefined;
669
- const sessionWorkflowState = sessionId
670
- ? await readWorkflowState(projectDir, sessionId)
671
- : null;
672
- const workflowState = sessionWorkflowState ?? await readWorkflowState(projectDir);
723
+ const workflowState = await readWorkflowStateWithRootFallback(projectDir, sessionId);
673
724
  const workflowAsset = inferWorkflowAssetFromIr(ir, workflowState?.workflow);
674
725
  return runCodexHook({ ...rawInput, event: codexHookEventForInput(rawInput, args) }, {
675
726
  ir,
@@ -887,12 +938,21 @@ function artifactResolverErrorOutput(event, error) {
887
938
  }
888
939
  async function readWorkflowStateForHook(projectDir, event, sessionId) {
889
940
  try {
890
- return { state: await readWorkflowState(projectDir, sessionId) };
941
+ const state = event === "SubagentStop"
942
+ ? await readWorkflowStateWithRootFallback(projectDir, sessionId)
943
+ : await readWorkflowState(projectDir, sessionId);
944
+ return { state };
891
945
  }
892
946
  catch (error) {
893
947
  return { output: artifactResolverErrorOutput(event, error) };
894
948
  }
895
949
  }
950
+ async function readWorkflowStateWithRootFallback(projectDir, sessionId) {
951
+ if (!sessionId)
952
+ return readWorkflowState(projectDir);
953
+ const sessionState = await readWorkflowState(projectDir, sessionId);
954
+ return sessionState ?? readWorkflowState(projectDir);
955
+ }
896
956
  async function resolveWorkflowArtifactFactsForHook(projectDir, ir, wfState, event, sessionId) {
897
957
  try {
898
958
  return { facts: await resolveWorkflowArtifactFacts(projectDir, ir, wfState, sessionId) };
@@ -1082,6 +1142,8 @@ async function runVerifierSpec(projectDir, ir, spec, workflowState, sessionId, o
1082
1142
  const entry = {
1083
1143
  verifier_id: spec.id,
1084
1144
  when: spec.when,
1145
+ iteration: workflowState.iteration,
1146
+ trigger_event: options?.triggerEvent,
1085
1147
  severity: spec.severity,
1086
1148
  kind: spec.kind,
1087
1149
  workflow: workflowState.workflow,
@@ -1109,10 +1171,41 @@ export async function runVerifiersForTest(projectDir, ir, workflowState, when, s
1109
1171
  }
1110
1172
  return results;
1111
1173
  }
1174
+ async function reusableSubagentStopResults(projectDir, ir, workflowState, when, sessionId) {
1175
+ if (!sessionId)
1176
+ return null;
1177
+ const specs = (ir.verifier_specs ?? []).filter((spec) => spec.when === when &&
1178
+ spec.workflow_name === workflowState.workflow &&
1179
+ spec.step_id === workflowState.current_step);
1180
+ if (specs.length === 0)
1181
+ return [];
1182
+ const startedAt = workflowState.started_at ? Date.parse(workflowState.started_at) : Number.NaN;
1183
+ const prior = (await readVerifierResults(projectDir, sessionId)).filter((result) => result.trigger_event === "SubagentStop" &&
1184
+ result.workflow === workflowState.workflow &&
1185
+ result.step_id === workflowState.current_step &&
1186
+ result.when === when &&
1187
+ (workflowState.iteration === undefined || result.iteration === workflowState.iteration) &&
1188
+ (Number.isNaN(startedAt) || Date.parse(result.timestamp) >= startedAt));
1189
+ const latestByVerifier = new Map();
1190
+ for (const result of prior)
1191
+ latestByVerifier.set(result.verifier_id, result);
1192
+ if (!specs.every((spec) => latestByVerifier.has(spec.id)))
1193
+ return null;
1194
+ return specs.map((spec) => latestByVerifier.get(spec.id));
1195
+ }
1196
+ async function runStopVerifiersForHook(projectDir, ir, workflowState, sessionId) {
1197
+ const results = [];
1198
+ for (const when of ["pre_handoff", "post_step"]) {
1199
+ const reusable = await reusableSubagentStopResults(projectDir, ir, workflowState, when, sessionId);
1200
+ results.push(...(reusable ?? await runVerifiersForTest(projectDir, ir, workflowState, when, sessionId, { triggerEvent: "Stop" })));
1201
+ }
1202
+ results.push(...await runVerifiersForTest(projectDir, ir, workflowState, "stop", sessionId, { triggerEvent: "Stop" }));
1203
+ return results;
1204
+ }
1112
1205
  export async function runStopVerifiersForTest(projectDir, ir, workflowState, sessionId, options) {
1113
1206
  const results = [];
1114
- results.push(...await runVerifiersForTest(projectDir, ir, workflowState, "post_step", sessionId, options));
1115
1207
  results.push(...await runVerifiersForTest(projectDir, ir, workflowState, "pre_handoff", sessionId, options));
1208
+ results.push(...await runVerifiersForTest(projectDir, ir, workflowState, "post_step", sessionId, options));
1116
1209
  results.push(...await runVerifiersForTest(projectDir, ir, workflowState, "stop", sessionId, options));
1117
1210
  return results;
1118
1211
  }
@@ -1229,7 +1322,7 @@ async function handleSurgeonReflection(ir, input, projectDir, sessionId) {
1229
1322
  if (!wfState || !wfState.active)
1230
1323
  return null;
1231
1324
  // Match agent_type to current role in workflow
1232
- const expectedAgentType = `dna-${wfState.current_role.replace(/([a-z])([A-Z])/g, "$1-$2").replace(/[_\s]+/g, "-").toLowerCase()}`;
1325
+ const expectedAgentType = agentTypeForWorkflowRole(wfState.current_role);
1233
1326
  if (agentType !== expectedAgentType)
1234
1327
  return null;
1235
1328
  // Look up step config from the compiled IR's workflow
@@ -39,7 +39,7 @@ export const HOOK_EVENT_REGISTRY = [
39
39
  {
40
40
  event: "SubagentStop",
41
41
  settingsKey: "subagentStop",
42
- manifestTimeout: 3,
42
+ manifestTimeout: 45,
43
43
  surfaces: { protocol: true, cli: true, settings: true, manifest: true, mcp: true },
44
44
  },
45
45
  {
@@ -180,6 +180,8 @@ export interface VerifierResultEntry {
180
180
  result_id?: string;
181
181
  verifier_id: string;
182
182
  when: VerifierWhen;
183
+ iteration?: number;
184
+ trigger_event?: string;
183
185
  severity: VerifierSeverity;
184
186
  kind: VerifierKind;
185
187
  workflow?: string;
@@ -338,6 +338,8 @@ export function verifierResultId(result) {
338
338
  .update(JSON.stringify({
339
339
  verifier_id: result.verifier_id,
340
340
  when: result.when,
341
+ iteration: result.iteration ?? 0,
342
+ trigger_event: result.trigger_event ?? "",
341
343
  severity: result.severity,
342
344
  kind: result.kind,
343
345
  workflow: result.workflow ?? "",
@@ -370,6 +372,8 @@ function verifierResultKey(result) {
370
372
  return [
371
373
  result.verifier_id,
372
374
  result.when,
375
+ result.iteration ?? 0,
376
+ result.trigger_event ?? "",
373
377
  result.severity,
374
378
  result.kind,
375
379
  result.workflow ?? "",
package/dist/mcp/index.js CHANGED
@@ -16,6 +16,7 @@ import { createCompileTools } from "./tools-compile.js";
16
16
  import { createEnforceTools } from "./tools-enforce.js";
17
17
  import { createObservabilityTools } from "./tools-observability.js";
18
18
  import { createContextTools } from "./tools-context.js";
19
+ import { createArtifactTools } from "./tools-artifacts.js";
19
20
  // Parse args
20
21
  const args = process.argv.slice(2);
21
22
  let projectDir = process.cwd();
@@ -38,6 +39,7 @@ const tools = [
38
39
  ...createStateTools(projectDir),
39
40
  ...createCompileTools(projectDir),
40
41
  ...createContextTools(projectDir),
42
+ ...createArtifactTools(projectDir),
41
43
  ...createEnforceTools(projectDir),
42
44
  ...createObservabilityTools(projectDir),
43
45
  ];
@@ -0,0 +1,2 @@
1
+ import type { ToolDef } from "./server.js";
2
+ export declare function createArtifactTools(projectDir: string): ToolDef[];
@@ -0,0 +1,137 @@
1
+ import { createHash } from "node:crypto";
2
+ import { readFile, realpath, stat } from "node:fs/promises";
3
+ import { isAbsolute, relative, resolve, sep } from "node:path";
4
+ import { errorResult, textResult } from "./server.js";
5
+ function sha256(value) {
6
+ return createHash("sha256").update(value).digest("hex");
7
+ }
8
+ function isContained(root, target) {
9
+ return target === root || target.startsWith(`${root}${sep}`);
10
+ }
11
+ function canonicalJson(value) {
12
+ if (Array.isArray(value))
13
+ return `[${value.map(canonicalJson).join(",")}]`;
14
+ if (value && typeof value === "object") {
15
+ const record = value;
16
+ return `{${Object.keys(record).sort().map((key) => `${JSON.stringify(key)}:${canonicalJson(record[key])}`).join(",")}}`;
17
+ }
18
+ return JSON.stringify(value) ?? "null";
19
+ }
20
+ function decodeJsonPointer(pointer) {
21
+ if (!pointer.startsWith("/") || /~(?:[^01]|$)/.test(pointer)) {
22
+ throw new Error("exclude_json_pointers must contain valid non-root JSON pointers");
23
+ }
24
+ return pointer.slice(1).split("/").map((segment) => segment.replace(/~1/g, "/").replace(/~0/g, "~"));
25
+ }
26
+ function omitJsonPointer(value, pointer) {
27
+ const segments = decodeJsonPointer(pointer);
28
+ let cursor = value;
29
+ for (const segment of segments.slice(0, -1)) {
30
+ if (!cursor || typeof cursor !== "object" || Array.isArray(cursor) || !Object.hasOwn(cursor, segment)) {
31
+ throw new Error(`JSON pointer does not identify an object member: ${pointer}`);
32
+ }
33
+ cursor = cursor[segment];
34
+ }
35
+ const leaf = segments.at(-1);
36
+ if (!cursor || typeof cursor !== "object" || Array.isArray(cursor) || !Object.hasOwn(cursor, leaf)) {
37
+ throw new Error(`JSON pointer does not identify an object member: ${pointer}`);
38
+ }
39
+ delete cursor[leaf];
40
+ }
41
+ async function resolveArtifact(projectDir, requestedPath) {
42
+ if (!requestedPath || requestedPath.includes("\0") || isAbsolute(requestedPath)) {
43
+ throw new Error("path must be a non-empty project-relative path");
44
+ }
45
+ const root = await realpath(projectDir);
46
+ const lexicalTarget = resolve(root, requestedPath);
47
+ if (!isContained(root, lexicalTarget))
48
+ throw new Error("path escapes project directory");
49
+ let absolutePath;
50
+ try {
51
+ absolutePath = await realpath(lexicalTarget);
52
+ }
53
+ catch {
54
+ throw new Error("artifact is not a readable regular file");
55
+ }
56
+ if (!isContained(root, absolutePath))
57
+ throw new Error("path resolves outside project directory");
58
+ const artifactStat = await stat(absolutePath);
59
+ if (!artifactStat.isFile())
60
+ throw new Error("artifact is not a readable regular file");
61
+ return {
62
+ absolutePath,
63
+ projectRelativePath: relative(root, lexicalTarget).split(sep).join("/"),
64
+ };
65
+ }
66
+ export function createArtifactTools(projectDir) {
67
+ return [
68
+ {
69
+ name: "dna_artifact_digest",
70
+ description: "Read-only SHA-256 digest for one project-relative regular file. Supports raw bytes, whitespace-normalized text, and canonical JSON with explicit object-member exclusions; refuses project escapes and writes nothing.",
71
+ inputSchema: {
72
+ type: "object",
73
+ properties: {
74
+ path: { type: "string", description: "Project-relative path to a regular file" },
75
+ mode: {
76
+ type: "string",
77
+ enum: ["bytes", "whitespace_normalized_text", "canonical_json"],
78
+ description: "Digest representation (default: bytes)",
79
+ },
80
+ exclude_json_pointers: {
81
+ type: "array",
82
+ items: { type: "string" },
83
+ description: "For canonical_json only, object members to omit using RFC 6901 JSON pointers",
84
+ },
85
+ },
86
+ required: ["path"],
87
+ },
88
+ handler: async (args) => {
89
+ try {
90
+ const requestedPath = typeof args.path === "string" ? args.path : "";
91
+ const mode = (args.mode ?? "bytes");
92
+ if (!["bytes", "whitespace_normalized_text", "canonical_json"].includes(mode)) {
93
+ return errorResult("mode must be bytes, whitespace_normalized_text, or canonical_json");
94
+ }
95
+ const excluded = args.exclude_json_pointers === undefined
96
+ ? []
97
+ : Array.isArray(args.exclude_json_pointers) && args.exclude_json_pointers.every((item) => typeof item === "string")
98
+ ? args.exclude_json_pointers
99
+ : undefined;
100
+ if (!excluded)
101
+ return errorResult("exclude_json_pointers must be an array of strings");
102
+ if (mode !== "canonical_json" && excluded.length > 0) {
103
+ return errorResult("exclude_json_pointers is only valid with canonical_json mode");
104
+ }
105
+ const artifact = await resolveArtifact(projectDir, requestedPath);
106
+ const content = await readFile(artifact.absolutePath);
107
+ const artifactStat = await stat(artifact.absolutePath);
108
+ const contentSha256 = sha256(content);
109
+ let digest = contentSha256;
110
+ if (mode === "whitespace_normalized_text") {
111
+ digest = sha256(content.toString("utf-8").replace(/\s+/g, " ").trim());
112
+ }
113
+ else if (mode === "canonical_json") {
114
+ const parsed = JSON.parse(content.toString("utf-8"));
115
+ for (const pointer of excluded)
116
+ omitJsonPointer(parsed, pointer);
117
+ digest = sha256(canonicalJson(parsed));
118
+ }
119
+ return textResult(JSON.stringify({
120
+ schema_version: "intentdna.artifact_digest.v1",
121
+ path: artifact.projectRelativePath,
122
+ mode,
123
+ sha256: digest,
124
+ content_sha256: contentSha256,
125
+ size_bytes: content.byteLength,
126
+ modified_at: artifactStat.mtime.toISOString(),
127
+ excluded_json_pointers: excluded,
128
+ read_only: true,
129
+ }, null, 2));
130
+ }
131
+ catch (error) {
132
+ return errorResult(error instanceof Error ? error.message : "artifact digest failed");
133
+ }
134
+ },
135
+ },
136
+ ];
137
+ }
@@ -46,6 +46,7 @@ export function recommendHookTimeouts(ir, baseTimeout = 10) {
46
46
  spec.checkpoint?.assert === "no_test_regression")) ?? false;
47
47
  if (hasStopCommandVerifiers) {
48
48
  timeouts.stop = Math.max(baseTimeout, 45);
49
+ timeouts.subagentStop = Math.max(baseTimeout, 45);
49
50
  }
50
51
  return timeouts;
51
52
  }
@@ -70,7 +71,7 @@ export function detectEnabledEvents(ir) {
70
71
  if (ir.prompt_directives.some(d => d.priority === "high")) {
71
72
  events.push("userPromptSubmit");
72
73
  }
73
- if (hasRoleScope) {
74
+ if (hasRoleScope || hasStopVerifiers) {
74
75
  events.push("subagentStop");
75
76
  }
76
77
  if (ir.prompt_directives.length > 0) {
@@ -448,10 +448,19 @@ function workflowSkillMapEntry(skillName, dirName, plan) {
448
448
  produces,
449
449
  };
450
450
  }
451
+ function inferredWorkflowAsset(ir) {
452
+ const sourceIds = [...new Set(ir?.source_dna_ids ?? [])]
453
+ .filter((sourceId) => sourceId.length > 0)
454
+ .filter((sourceId) => !sourceId.startsWith("species:"))
455
+ .filter((sourceId) => !sourceId.startsWith("enterprise:"));
456
+ return sourceIds.length === 1 ? sourceIds[0] : undefined;
457
+ }
451
458
  export function compileWorkflowToSkill(plan, roles, ir, variables, options) {
452
459
  const lines = [];
453
460
  const skillName = `dna-${toKebabCase(plan.name)}`;
454
461
  const surface = options?.surface ?? "claude_code";
462
+ const workflowId = plan.workflow_key ?? plan.source_workflow;
463
+ const workflowAsset = inferredWorkflowAsset(ir);
455
464
  assertSafeGeneratedName(skillName, "skill name");
456
465
  // Frontmatter
457
466
  lines.push("---");
@@ -478,9 +487,20 @@ export function compileWorkflowToSkill(plan, roles, ir, variables, options) {
478
487
  lines.push("");
479
488
  }
480
489
  lines.push("<Workflow_State>");
481
- lines.push("At workflow start, persist runtime inputs in DNA workflow state before executing steps.");
490
+ lines.push("At workflow start, call the MCP tool `dna_workflow_write` before any step or hook-triggering action so hook verifier evidence is attributed to this workflow instead of `*:direct`.");
491
+ lines.push(`Use workflow id \`${workflowId}\`.`);
492
+ if (workflowAsset) {
493
+ lines.push(`Use workflow_asset \`${workflowAsset}\`; it is the single non-species/non-enterprise IR source id.`);
494
+ }
495
+ else {
496
+ lines.push("Do not set workflow_asset: IR does not provide exactly one non-species/non-enterprise source id, so the asset cannot be inferred safely.");
497
+ }
498
+ const firstStep = plan.steps[0];
499
+ lines.push(`Initial state write: workflow=${workflowId}, current_step=${firstStep.id}, current_role=${firstStep.role}, iteration=1, active=true.`);
482
500
  lines.push("Record the invocation argument as inputs.ARGUMENTS so handoff artifact paths like $ARGUMENTS can be resolved deterministically.");
483
501
  lines.push("If template variables are shown below, persist their resolved values as resolved_variables.");
502
+ lines.push("Before each workflow step, call `dna_workflow_write` again with workflow, workflow_asset only when safely inferred above, current_step, current_role, iteration, active=true, inputs.ARGUMENTS, and resolved_variables.");
503
+ lines.push("At workflow completion or terminal failure, call `dna_workflow_write` with active=false using the same workflow id and safe workflow_asset value.");
484
504
  lines.push("</Workflow_State>");
485
505
  lines.push("");
486
506
  // Required Context — context files that must be read before any work
@@ -607,6 +627,7 @@ export function compileWorkflowToSkill(plan, roles, ir, variables, options) {
607
627
  const agentPrompt = escapePrompt(promptParts.join(" "));
608
628
  lines.push(`${stepNumber}. **${step.id}**${optional}`);
609
629
  lines.push(` ${step.description}${runIf}`);
630
+ lines.push(` Before this step, call \`dna_workflow_write\` with workflow=${workflowId}, current_step=${step.id}, current_role=${step.role}, iteration=<current iteration>, active=true, inputs.ARGUMENTS, resolved_variables, and ${workflowAsset ? `workflow_asset=${workflowAsset}` : "no workflow_asset unless one has been explicitly supplied by trusted workflow state"}.`);
610
631
  lines.push("");
611
632
  if (humanOwned) {
612
633
  lines.push(" **Human-owned gate (`model: human`)**");
@@ -640,6 +661,11 @@ export function compileWorkflowToSkill(plan, roles, ir, variables, options) {
640
661
  const artifacts = step.handoff.produces.map(p => p.description).join(", ");
641
662
  lines.push(` Verify produced artifacts: ${artifacts}`);
642
663
  }
664
+ pushWorkflowStepVerifierLines(lines, step, " ");
665
+ if (step.completion?.length || step.checkpoints?.length) {
666
+ lines.push(" If any completion/checkpoint gate fails, preserve the exact verifier diagnostics verbatim: id, check/assert or command, message, evidence, target/artifact, and exit_code when available.");
667
+ lines.push(` Feed those exact diagnostics to ${step.on_fail === "retry_with_feedback" ? "this step's on_fail retry" : "the retry_from/on_fail step"} before any retry, reanalysis, or downstream transition.`);
668
+ }
643
669
  lines.push("");
644
670
  // Reflection gate: inject Reflection_Gate after steps with max_attempts
645
671
  if (step.max_attempts) {
@@ -648,8 +674,9 @@ export function compileWorkflowToSkill(plan, roles, ir, variables, options) {
648
674
  const blockedPath = step.blocked_items_path ?? "blocked_items.md";
649
675
  lines.push(`<Reflection_Gate>`);
650
676
  lines.push(`max_attempts=${step.max_attempts}, handoff_to=${handoffStep}, max_handoffs=${maxH}`);
651
- lines.push(`No test progress after ${step.max_attempts} attempts → handoff to ${handoffStep} for re-analysis.`);
652
- lines.push(`After ${maxH} handoffs with no progress SKIP and record to ${blockedPath}.`);
677
+ lines.push(`No declared completion progress after ${step.max_attempts} attempts → handoff to ${handoffStep} for re-analysis.`);
678
+ lines.push(`When the declared gates are test or no_test_regression gates, preserve the existing test-progress meaning as part of declared completion progress.`);
679
+ lines.push(`After ${maxH} handoffs with no declared completion progress → SKIP and record to ${blockedPath}.`);
653
680
  lines.push(`</Reflection_Gate>`);
654
681
  lines.push("");
655
682
  }
@@ -662,7 +689,9 @@ export function compileWorkflowToSkill(plan, roles, ir, variables, options) {
662
689
  lines.push(`- max_retries: ${plan.retry.max_retries}`);
663
690
  lines.push(`- retry_from: ${plan.retry.retry_from ?? "first_failed_step"}`);
664
691
  lines.push(`- backoff: ${plan.retry.backoff}`);
665
- lines.push("- On a blocking checkpoint or validator failure, preserve the exact diagnostics and return them to the retry_from step.");
692
+ lines.push("- On a blocking completion, checkpoint, or validator failure, preserve the exact diagnostics and return them verbatim to the retry_from/on_fail step.");
693
+ lines.push("- Exact diagnostics means every available id/result_id, check/assert or command, message, evidence, target/artifact, and exit_code. Do not paraphrase away failed paths, command output, or policy-denied exit codes.");
694
+ lines.push("- Retry prompts must include the prior failed step id, the declared gates that failed, and the exact verifier diagnostics before asking for another attempt.");
666
695
  lines.push("- Repeat only the retry slice and its dependent steps; do not continue to save, adoption, or sync while validation is red.");
667
696
  lines.push("- Stop and report the remaining diagnostics when the retry budget is exhausted.");
668
697
  lines.push("</Retry_Policy>");
@@ -797,6 +826,7 @@ export function compileWorkflowToSkill(plan, roles, ir, variables, options) {
797
826
  }
798
827
  // Workflow Boundary — prevent cross-workflow execution
799
828
  lines.push("<Workflow_Boundary>");
829
+ lines.push(`Before reporting final success, terminal failure, or exhaustion, call \`dna_workflow_write\` with workflow=${workflowId}, current_step=workflow_complete, current_role=workflow, active=false, and ${workflowAsset ? `workflow_asset=${workflowAsset}` : "no workflow_asset unless one has been explicitly supplied by trusted workflow state"}.`);
800
830
  lines.push("This workflow is COMPLETE. Do NOT proceed to any other workflow.");
801
831
  lines.push("Report your results and STOP. The user will decide the next step.");
802
832
  lines.push("</Workflow_Boundary>");
@@ -1,6 +1,8 @@
1
1
  import type { CompletionCheck, ConstraintIR, StepCheckpoint, VerifierCommandPolicy } from "../schema/types.js";
2
2
  export declare const DEFAULT_VERIFIER_COMMAND_TIMEOUT_MS = 30000;
3
3
  export declare const MAX_VERIFIER_EVIDENCE_BYTES = 4096;
4
+ export declare const VERIFIER_DIAGNOSTIC_PREFIX = "INTENTDNA_DIAGNOSTIC:";
5
+ export declare const MAX_VERIFIER_DIAGNOSTIC_BYTES = 2048;
4
6
  export interface VerifierRuntimeContext {
5
7
  projectDir: string;
6
8
  variables?: Record<string, string>;
@@ -29,7 +31,7 @@ export declare function hasUnresolvedVerifierTemplate(value: string): boolean;
29
31
  export declare function unresolvedVerifierTemplateMessage(value: string): string;
30
32
  export declare function hasUnsafeShellControl(command: string): boolean;
31
33
  export declare function isVerifierCommandAllowed(policy: VerifierCommandPolicy | undefined, command: string): boolean;
32
- export declare function verifierCommandPolicyMessage(command: string): string;
34
+ export declare function verifierCommandPolicyMessage(_command: string): string;
33
35
  export declare function isBuiltinAssertAllowed(policy: VerifierCommandPolicy | undefined, assertName: string): boolean;
34
36
  export declare function verifierAssertPolicyMessage(assertName: string): string;
35
37
  export declare function trimVerifierEvidence(raw: string | undefined): string | undefined;
@@ -3,6 +3,9 @@ import { readFile, realpath, stat } from "node:fs/promises";
3
3
  import { dirname, isAbsolute, relative, resolve } from "node:path";
4
4
  export const DEFAULT_VERIFIER_COMMAND_TIMEOUT_MS = 30_000;
5
5
  export const MAX_VERIFIER_EVIDENCE_BYTES = 4096;
6
+ export const VERIFIER_DIAGNOSTIC_PREFIX = "INTENTDNA_DIAGNOSTIC:";
7
+ export const MAX_VERIFIER_DIAGNOSTIC_BYTES = 2048;
8
+ const VERIFIER_DIAGNOSTIC_CODE = /^INTENTDNA_DIAGNOSTIC:[A-Za-z0-9][A-Za-z0-9_.:-]{0,255}$/;
6
9
  const SAFE_COMMAND_VARIABLE_RE = /^[A-Za-z0-9_-]+$/;
7
10
  const SAFE_PATH_VARIABLE_RE = /^[-A-Za-z0-9_./]+$/;
8
11
  export function resolveVerifierTemplate(template, variables) {
@@ -59,7 +62,7 @@ function resolveVerifierCommand(rawCommand, policy, variables) {
59
62
  return { target: declaredCommand, evidence: "policy_denied", exit_code: 126, message: verifierCommandPolicyMessage(declaredCommand) };
60
63
  }
61
64
  const safeVariables = validateVerifierTemplateVariables(declaredCommand, variables, "command");
62
- if (!safeVariables.valid) {
65
+ if (safeVariables.valid === false) {
63
66
  return { target: declaredCommand, evidence: safeVariables.evidence, exit_code: 126, message: safeVariables.message };
64
67
  }
65
68
  const command = resolveVerifierTemplate(declaredCommand, variables).trim();
@@ -137,8 +140,8 @@ export function isVerifierCommandAllowed(policy, command) {
137
140
  return false;
138
141
  return policy.allow_command_prefixes?.some((prefix) => normalized.startsWith(prefix)) ?? false;
139
142
  }
140
- export function verifierCommandPolicyMessage(command) {
141
- return `Verifier command not allowed by verifier_policy: ${command}`;
143
+ export function verifierCommandPolicyMessage(_command) {
144
+ return "Verifier command not allowed by verifier_policy";
142
145
  }
143
146
  export function isBuiltinAssertAllowed(policy, assertName) {
144
147
  return policy?.allow_builtin_asserts?.includes(assertName) ?? false;
@@ -156,12 +159,52 @@ export function trimVerifierEvidence(raw) {
156
159
  ? trimmed.slice(0, MAX_VERIFIER_EVIDENCE_BYTES)
157
160
  : trimmed;
158
161
  }
162
+ function sanitizeVerifierDiagnosticLine(raw) {
163
+ return raw
164
+ .replace(/[\u0000-\u0008\u000B\u000C\u000D\u000E-\u001F\u007F]/g, "")
165
+ .trim();
166
+ }
167
+ function trimDiagnosticBytes(raw) {
168
+ const marker = "...[truncated]";
169
+ if (Buffer.byteLength(raw, "utf8") <= MAX_VERIFIER_DIAGNOSTIC_BYTES)
170
+ return raw;
171
+ let trimmed = raw;
172
+ while (trimmed.length > 0 && Buffer.byteLength(`${trimmed}${marker}`, "utf8") > MAX_VERIFIER_DIAGNOSTIC_BYTES) {
173
+ trimmed = trimmed.slice(0, -1);
174
+ }
175
+ return `${trimmed}${marker}`;
176
+ }
177
+ function extractVerifierDiagnostics(raw) {
178
+ return raw
179
+ .split(/\r?\n/)
180
+ .map((line) => sanitizeVerifierDiagnosticLine(line))
181
+ .filter((line) => VERIFIER_DIAGNOSTIC_CODE.test(line));
182
+ }
183
+ function collectVerifierDiagnostics(pendingLine, chunk, diagnostics) {
184
+ const lines = `${pendingLine}${chunk}`.split("\n");
185
+ const nextPendingLine = lines.pop() ?? "";
186
+ diagnostics.push(...extractVerifierDiagnostics(lines.join("\n")));
187
+ return nextPendingLine.length > MAX_VERIFIER_EVIDENCE_BYTES
188
+ ? nextPendingLine.slice(0, MAX_VERIFIER_EVIDENCE_BYTES)
189
+ : nextPendingLine;
190
+ }
191
+ function summarizeCommandEvidenceFromStats(stdoutBytes, stderrBytes, diagnostics = []) {
192
+ const summary = stdoutBytes === 0 && stderrBytes === 0
193
+ ? undefined
194
+ : `stdout_bytes=${stdoutBytes} stderr_bytes=${stderrBytes}`;
195
+ const diagnostic = diagnostics.length > 0
196
+ ? trimDiagnosticBytes(diagnostics.join("\n"))
197
+ : undefined;
198
+ return trimVerifierEvidence([summary, diagnostic].filter(Boolean).join("\n"));
199
+ }
159
200
  export function summarizeCommandEvidence(stdout, stderr) {
160
201
  const stdoutBytes = Buffer.byteLength(stdout, "utf8");
161
202
  const stderrBytes = Buffer.byteLength(stderr, "utf8");
162
- if (stdoutBytes === 0 && stderrBytes === 0)
163
- return undefined;
164
- return trimVerifierEvidence(`stdout_bytes=${stdoutBytes} stderr_bytes=${stderrBytes}`);
203
+ return summarizeCommandEvidenceFromStats(stdoutBytes, stderrBytes, extractVerifierDiagnostics(`${stdout}\n${stderr}`));
204
+ }
205
+ function appendFailureDiagnostic(message, evidence) {
206
+ const diagnostics = extractVerifierDiagnostics(evidence ?? "").join("\n");
207
+ return diagnostics ? `${message}\n${diagnostics}` : message;
165
208
  }
166
209
  export async function execVerifierCommand(projectDir, command, timeoutMs = DEFAULT_VERIFIER_COMMAND_TIMEOUT_MS, env) {
167
210
  return new Promise((resolvePromise) => {
@@ -170,9 +213,12 @@ export async function execVerifierCommand(projectDir, command, timeoutMs = DEFAU
170
213
  stdio: ["ignore", "pipe", "pipe"],
171
214
  env: { ...process.env, ...(env ?? {}) },
172
215
  });
173
- let stdout = "";
174
- let stderr = "";
216
+ let stdoutPendingLine = "";
217
+ let stderrPendingLine = "";
218
+ let stdoutBytes = 0;
219
+ let stderrBytes = 0;
175
220
  let settled = false;
221
+ const diagnostics = [];
176
222
  const settle = (result) => {
177
223
  if (settled)
178
224
  return;
@@ -183,29 +229,36 @@ export async function execVerifierCommand(projectDir, command, timeoutMs = DEFAU
183
229
  const timeoutId = setTimeout(() => {
184
230
  child.kill("SIGTERM");
185
231
  setTimeout(() => child.kill("SIGKILL"), 1000).unref();
186
- settle({ passed: false, timedOut: true, exitCode: 124, evidence: summarizeCommandEvidence(stdout, stderr) });
232
+ diagnostics.push(...extractVerifierDiagnostics(`${stdoutPendingLine}\n${stderrPendingLine}`));
233
+ settle({ passed: false, timedOut: true, exitCode: 124, evidence: summarizeCommandEvidenceFromStats(stdoutBytes, stderrBytes, diagnostics) });
187
234
  }, timeoutMs);
188
235
  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);
236
+ const text = String(chunk);
237
+ stdoutBytes += Buffer.byteLength(text, "utf8");
238
+ stdoutPendingLine = collectVerifierDiagnostics(stdoutPendingLine, text, diagnostics);
192
239
  });
193
240
  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);
241
+ const text = String(chunk);
242
+ stderrBytes += Buffer.byteLength(text, "utf8");
243
+ stderrPendingLine = collectVerifierDiagnostics(stderrPendingLine, text, diagnostics);
197
244
  });
198
245
  child.on("error", () => settle({
199
246
  passed: false,
200
247
  timedOut: false,
201
248
  exitCode: 1,
202
- evidence: summarizeCommandEvidence(stdout, stderr),
249
+ evidence: summarizeCommandEvidenceFromStats(stdoutBytes, stderrBytes, [
250
+ ...diagnostics,
251
+ ...extractVerifierDiagnostics(`${stdoutPendingLine}\n${stderrPendingLine}`),
252
+ ]),
203
253
  }));
204
254
  child.on("close", (code) => settle({
205
255
  passed: code === 0,
206
256
  timedOut: false,
207
257
  exitCode: code ?? 1,
208
- evidence: summarizeCommandEvidence(stdout, stderr),
258
+ evidence: summarizeCommandEvidenceFromStats(stdoutBytes, stderrBytes, [
259
+ ...diagnostics,
260
+ ...extractVerifierDiagnostics(`${stdoutPendingLine}\n${stderrPendingLine}`),
261
+ ]),
209
262
  }));
210
263
  });
211
264
  }
@@ -296,7 +349,7 @@ export async function runCompletionVerifier(completion, ir, context) {
296
349
  }
297
350
  if (completion.file_exists) {
298
351
  const safeVariables = validateVerifierTemplateVariables(completion.file_exists, context.variables, "path");
299
- if (!safeVariables.valid)
352
+ if (safeVariables.valid === false)
300
353
  return { passed: false, target: completion.file_exists, evidence: safeVariables.evidence, message: safeVariables.message };
301
354
  const target = resolveVerifierTemplate(completion.file_exists, context.variables);
302
355
  if (hasUnresolvedVerifierTemplate(target))
@@ -316,7 +369,7 @@ export async function runCompletionVerifier(completion, ir, context) {
316
369
  }
317
370
  if (completion.file_not_empty) {
318
371
  const safeVariables = validateVerifierTemplateVariables(completion.file_not_empty, context.variables, "path");
319
- if (!safeVariables.valid)
372
+ if (safeVariables.valid === false)
320
373
  return { passed: false, target: completion.file_not_empty, evidence: safeVariables.evidence, message: safeVariables.message };
321
374
  const target = resolveVerifierTemplate(completion.file_not_empty, context.variables);
322
375
  if (hasUnresolvedVerifierTemplate(target))
@@ -338,10 +391,10 @@ export async function runCompletionVerifier(completion, ir, context) {
338
391
  }
339
392
  if (completion.file_contains) {
340
393
  const safeTargetVariables = validateVerifierTemplateVariables(completion.file_contains.path, context.variables, "path");
341
- if (!safeTargetVariables.valid)
394
+ if (safeTargetVariables.valid === false)
342
395
  return { passed: false, target: completion.file_contains.path, evidence: safeTargetVariables.evidence, message: safeTargetVariables.message };
343
396
  const safePatternVariables = validateVerifierTemplateVariables(completion.file_contains.pattern, context.variables);
344
- if (!safePatternVariables.valid)
397
+ if (safePatternVariables.valid === false)
345
398
  return { passed: false, target: completion.file_contains.pattern, evidence: safePatternVariables.evidence, message: safePatternVariables.message };
346
399
  const target = resolveVerifierTemplate(completion.file_contains.path, context.variables);
347
400
  const pattern = resolveVerifierTemplate(completion.file_contains.pattern, context.variables);
@@ -379,8 +432,8 @@ export async function runCompletionVerifier(completion, ir, context) {
379
432
  message: commandResult.passed
380
433
  ? undefined
381
434
  : commandResult.timedOut
382
- ? `Verifier command timed out after ${context.commandTimeoutMs ?? DEFAULT_VERIFIER_COMMAND_TIMEOUT_MS}ms: ${command}`
383
- : `Verifier command failed: ${command}`,
435
+ ? `Verifier command timed out after ${context.commandTimeoutMs ?? DEFAULT_VERIFIER_COMMAND_TIMEOUT_MS}ms`
436
+ : appendFailureDiagnostic("Verifier command failed", commandResult.evidence),
384
437
  };
385
438
  }
386
439
  export async function runCheckpointVerifier(checkpoint, ir, context) {
@@ -405,8 +458,8 @@ export async function runCheckpointVerifier(checkpoint, ir, context) {
405
458
  message: commandResult.passed
406
459
  ? checkpoint.message
407
460
  : commandResult.timedOut
408
- ? `Verifier command timed out after ${context.commandTimeoutMs ?? DEFAULT_VERIFIER_COMMAND_TIMEOUT_MS}ms: ${command}`
409
- : `Verifier command failed: ${command}`,
461
+ ? `Verifier command timed out after ${context.commandTimeoutMs ?? DEFAULT_VERIFIER_COMMAND_TIMEOUT_MS}ms`
462
+ : appendFailureDiagnostic("Verifier command failed", commandResult.evidence),
410
463
  };
411
464
  }
412
465
  if (checkpoint.assert === "clean_working_tree") {
@@ -454,8 +507,10 @@ export async function runCheckpointVerifier(checkpoint, ir, context) {
454
507
  message: commandResult.passed
455
508
  ? checkpoint.message
456
509
  : commandResult.timedOut
457
- ? `Verifier command timed out after ${context.commandTimeoutMs ?? DEFAULT_VERIFIER_COMMAND_TIMEOUT_MS}ms: ${assertCommands[checkpoint.assert]}`
458
- : checkpoint.message,
510
+ ? `Verifier command timed out after ${context.commandTimeoutMs ?? DEFAULT_VERIFIER_COMMAND_TIMEOUT_MS}ms`
511
+ : checkpoint.message
512
+ ? appendFailureDiagnostic(checkpoint.message, commandResult.evidence)
513
+ : appendFailureDiagnostic("Verifier command failed", commandResult.evidence),
459
514
  };
460
515
  }
461
516
  return { passed: false, target: checkpoint.assert, exit_code: 1, message: checkpoint.message };
@@ -166,7 +166,8 @@ context_files:
166
166
 
167
167
  verifier_policy:
168
168
  allow_commands:
169
- - "node -e \"const fs=require('node:fs');const crypto=require('node:crypto');const path=require('node:path');const mod=process.env.ARGUMENTS||'$ARGUMENTS';const allowed=['BUG','UNIMPLEMENTED','INFRA','REMOVED','TEST_BUG'];const action=['BUG','UNIMPLEMENTED','INFRA','TEST_BUG'];const fail=m=>{throw new Error(m)};const same=(a,b)=>Array.isArray(a)&&a.length===b.length&&a.every((v,i)=>v===b[i]);const snapSame=(a,b)=>a&&b&&a.baseline_source_sha256===b.baseline_source_sha256&&a.diagnosis_artifact_sha256===b.diagnosis_artifact_sha256&&a.observed_failure_count===b.observed_failure_count&&a.work_item_count===b.work_item_count&&same(a.allowed_kinds,b.allowed_kinds);const root=process.cwd();const inside=p=>{if(typeof p!=='string'||!p||path.isAbsolute(p))fail('path_invalid');const r=path.resolve(root,p);if(r!==root&&!r.startsWith(root+path.sep))fail('path_escape');return r};const readJson=p=>JSON.parse(fs.readFileSync(inside(p),'utf8'));if(!/^[A-Za-z0-9_-]+$/.test(mod))fail('module');const contractPath='.dna/specs/diagnosis-'+mod+'.contract.json';const reviewPath='.dna/specs/diagnosis-'+mod+'.review.json';const diagnosisPath='.dna/specs/diagnosis-'+mod+'.md';const data=readJson(contractPath);if(data.contract_version!=='diagnosis-contract/v1')fail('contract_version');if(data.module!==mod)fail('module_mismatch');if(data.diagnosis_artifact!==diagnosisPath)fail('diagnosis_artifact_path');if(!same(data.allowed_kinds,allowed))fail('allowed_kinds');const observed=Array.isArray(data.observed_failures)?data.observed_failures:fail('observed_failures');const work=Array.isArray(data.work_items)?data.work_items:fail('work_items');if(!observed.length)fail('empty_observed_failures');if(!work.length)fail('empty_work_items');const ids=new Set(observed.map(x=>x&&x.id).filter(Boolean));if(ids.size!==observed.length)fail('observed_failure_ids');const failureById=new Map();for(const f of observed){if(typeof f.test_name!=='string'||!f.test_name.trim())fail('observed_failure_test_name');if(typeof f.symptom!=='string'||!f.symptom.trim())fail('observed_failure_symptom');if(!Array.isArray(f.evidence_paths)||!f.evidence_paths.length)fail('observed_failure_evidence');for(const p of f.evidence_paths)inside(p);if(f.kind!==undefined)fail('observed_failure_kind_forbidden');failureById.set(f.id,f)}const mapped=new Set();for(const item of work){if(!allowed.includes(item.kind))fail('invalid_kind');if(item.kind==='PLACEHOLDER_CONTRACT')fail('placeholder_kind');if(!Array.isArray(item.observed_failure_ids)||!item.observed_failure_ids.length)fail('work_item_observed_failure_ids');if(!Array.isArray(item.evidence_paths)||!item.evidence_paths.length)fail('work_item_evidence');for(const p of item.evidence_paths)inside(p);if(action.includes(item.kind)){if(!Array.isArray(item.v1_evidence_paths)||!item.v1_evidence_paths.length)fail('work_item_v1_evidence');if(!Array.isArray(item.v2_evidence_paths)||!item.v2_evidence_paths.length)fail('work_item_v2_evidence');for(const p of item.v1_evidence_paths)inside(p);for(const p of item.v2_evidence_paths)inside(p)}for(const id of item.observed_failure_ids){const f=failureById.get(id);if(!f)fail('unknown_observed_failure_id');if(action.includes(item.kind)&&(!f.test_name||!String(f.test_name).trim()))fail('action_test_evidence');mapped.add(id)}}if(mapped.size!==ids.size)fail('unmapped_observed_failures');for(const f of observed.filter(x=>String(x.symptom).includes('PLACEHOLDER_CONTRACT'))){const kinds=new Set(work.filter(i=>i.observed_failure_ids.includes(f.id)).map(i=>i.kind));if(![...kinds].every(k=>k==='TEST_BUG'||k==='UNIMPLEMENTED')||kinds.size===0)fail('placeholder_mapping')}const counts=data.baseline&&data.baseline.counts;if(!data.baseline||!data.baseline.source_path||!counts)fail('baseline');inside(data.baseline.source_path);for(const k of ['failed','skipped','hung']){if(!Number.isInteger(counts[k])||counts[k]<0)fail('baseline_count_'+k)}const total=counts.failed+counts.skipped+counts.hung;if(total!==observed.length)fail('baseline_count_mismatch');const summary=data.summary_counts||{};for(const kind of allowed){if(!Number.isInteger(summary[kind])||summary[kind]!==work.filter(x=>x.kind===kind).length)fail('summary_count_'+kind)}const sha=p=>crypto.createHash('sha256').update(fs.readFileSync(inside(p))).digest('hex');const snap=data.contract_snapshot||{};if(snap.observed_failure_count!==observed.length||snap.work_item_count!==work.length||!same(snap.allowed_kinds,allowed))fail('contract_snapshot');if(snap.baseline_source_sha256!==sha(data.baseline.source_path))fail('baseline_sha256');if(snap.diagnosis_artifact_sha256!==sha(diagnosisPath))fail('diagnosis_sha256');if(!data.staleness||data.staleness.is_stale!==false)fail('stale_contract');const review=readJson(reviewPath);if(review.contract_version!=='diagnosis-contract-review/v1')fail('review_contract_version');if(review.contract_valid!==true)fail('review_contract_valid');if(review.verdict!=='APPROVE')fail('review_verdict');if(review.contract_artifact_reviewed!==contractPath)fail('review_contract_artifact_path');if(review.artifact_reviewed!==diagnosisPath)fail('review_artifact_path');if(!snapSame(review.contract_snapshot,snap))fail('review_contract_snapshot');if(!review.updated_at||Number.isNaN(Date.parse(review.updated_at)))fail('review_updated_at');if(!Array.isArray(review.evidence_paths)||!review.evidence_paths.length)fail('review_evidence_paths');for(const p of review.evidence_paths)inside(p);const cm=fs.statSync(inside(contractPath)).mtimeMs;const dm=fs.statSync(inside(diagnosisPath)).mtimeMs;const rm=fs.statSync(inside(reviewPath)).mtimeMs;if(rm<cm||rm<dm)fail('review_stale');\""
169
+ - "node -e \"const fs=require('node:fs');const crypto=require('node:crypto');const path=require('node:path');const mod=process.env.ARGUMENTS||'$ARGUMENTS';const allowed=['BUG','UNIMPLEMENTED','INFRA','REMOVED','TEST_BUG'];const action=['BUG','UNIMPLEMENTED','INFRA','TEST_BUG'];const fail=m=>{console.error('INTENTDNA_DIAGNOSTIC:'+m);process.exit(1)};const same=(a,b)=>Array.isArray(a)&&a.length===b.length&&a.every((v,i)=>v===b[i]);const legacySame=(a,b)=>a&&b&&a.baseline_source_sha256===b.baseline_source_sha256&&a.diagnosis_artifact_sha256===b.diagnosis_artifact_sha256&&a.observed_failure_count===b.observed_failure_count&&a.work_item_count===b.work_item_count&&same(a.allowed_kinds,b.allowed_kinds);const modern=s=>s&&typeof s.sidecar_semantic_sha256==='string'&&typeof s.diagnosis_semantic_sha256==='string';const root=fs.realpathSync(process.cwd());const inside=p=>{if(typeof p!=='string'||!p||path.isAbsolute(p))fail('path_invalid');const r=path.resolve(root,p);if(r!==root&&!r.startsWith(root+path.sep))fail('path_escape');if(fs.existsSync(r)){const real=fs.realpathSync(r);if(real!==root&&!real.startsWith(root+path.sep))fail('path_escape')}return r};const readJson=p=>JSON.parse(fs.readFileSync(inside(p),'utf8'));const hash=v=>crypto.createHash('sha256').update(v).digest('hex');const fileHash=p=>hash(fs.readFileSync(inside(p)));const normalizedDiagnosisHash=p=>hash(fs.readFileSync(inside(p),'utf8').replace(/\\s+/g,' ').trim());const canon=v=>Array.isArray(v)?'['+v.map(canon).join(',')+']':v&&typeof v==='object'?'{'+Object.keys(v).sort().map(k=>JSON.stringify(k)+':'+canon(v[k])).join(',')+'}':JSON.stringify(v);if(!/^[A-Za-z0-9_-]+$/.test(mod))fail('module');const contractPath='.dna/specs/diagnosis-'+mod+'.contract.json';const reviewPath='.dna/specs/diagnosis-'+mod+'.review.json';const diagnosisPath='.dna/specs/diagnosis-'+mod+'.md';const behaviorPath='docs/behavior/'+mod+'.md';const data=readJson(contractPath);if(data.contract_version!=='diagnosis-contract/v1')fail('contract_version');if(data.module!==mod)fail('module_mismatch');if(data.diagnosis_artifact!==diagnosisPath)fail('diagnosis_artifact_path');if(!same(data.allowed_kinds,allowed))fail('allowed_kinds');const observed=Array.isArray(data.observed_failures)?data.observed_failures:fail('observed_failures');const work=Array.isArray(data.work_items)?data.work_items:fail('work_items');if(!observed.length)fail('empty_observed_failures');if(!work.length)fail('empty_work_items');const idOk=id=>typeof id==='string'&&/^[A-Za-z0-9][A-Za-z0-9_.:-]*$/.test(id);const ids=new Set();for(const f of observed){if(!idOk(f&&f.id)||ids.has(f.id))fail('observed_failure_ids');ids.add(f.id)}const workIds=new Set();for(const item of work){if(!idOk(item&&item.id)||workIds.has(item.id))fail('work_item_ids');workIds.add(item.id)}const failureById=new Map();for(const f of observed){if(typeof f.test_name!=='string'||!f.test_name.trim())fail('observed_failure_test_name');if(typeof f.symptom!=='string'||!f.symptom.trim())fail('observed_failure_symptom');if(!Array.isArray(f.evidence_paths)||!f.evidence_paths.length)fail('observed_failure_evidence');for(const p of f.evidence_paths)inside(p);if(f.kind!==undefined)fail('observed_failure_kind_forbidden');failureById.set(f.id,f)}const mapped=new Set();for(const item of work){if(!allowed.includes(item.kind))fail('invalid_kind');if(item.kind==='PLACEHOLDER_CONTRACT')fail('placeholder_kind');if(!Array.isArray(item.observed_failure_ids)||!item.observed_failure_ids.length)fail('work_item_observed_failure_ids');if(!Array.isArray(item.evidence_paths)||!item.evidence_paths.length)fail('work_item_evidence');for(const p of item.evidence_paths)inside(p);if(action.includes(item.kind)){if(!Array.isArray(item.v1_evidence_paths)||!item.v1_evidence_paths.length)fail('work_item_v1_evidence');if(!Array.isArray(item.v2_evidence_paths)||!item.v2_evidence_paths.length)fail('work_item_v2_evidence');for(const p of item.v1_evidence_paths)inside(p);for(const p of item.v2_evidence_paths)inside(p)}for(const id of item.observed_failure_ids){const f=failureById.get(id);if(!f)fail('unknown_observed_failure_id');if(action.includes(item.kind)&&(!f.test_name||!String(f.test_name).trim()))fail('action_test_evidence');mapped.add(id)}}if(mapped.size!==ids.size)fail('unmapped_observed_failures');for(const f of observed.filter(x=>String(x.symptom).includes('PLACEHOLDER_CONTRACT'))){const kinds=new Set(work.filter(i=>i.observed_failure_ids.includes(f.id)).map(i=>i.kind));if(![...kinds].every(k=>k==='TEST_BUG'||k==='UNIMPLEMENTED')||kinds.size===0)fail('placeholder_mapping')}const counts=data.baseline&&data.baseline.counts;if(!data.baseline||!data.baseline.source_path||!counts)fail('baseline');for(const k of ['failed','skipped','hung']){if(!Number.isInteger(counts[k])||counts[k]<0)fail('baseline_count_'+k)}if(counts.failed+counts.skipped+counts.hung!==observed.length)fail('baseline_count_mismatch');const summary=data.summary_counts||{};for(const kind of allowed){if(!Number.isInteger(summary[kind])||summary[kind]!==work.filter(x=>x.kind===kind).length)fail('summary_count_'+kind)}const snap=data.contract_snapshot||{};const baselineHash=fileHash(data.baseline.source_path);const diagnosisHash=fileHash(diagnosisPath);if(snap.observed_failure_count!==observed.length||snap.work_item_count!==work.length||!same(snap.allowed_kinds,allowed))fail('contract_snapshot');if(snap.baseline_source_sha256!==baselineHash)fail('baseline_sha256');if(snap.diagnosis_artifact_sha256!==diagnosisHash)fail('diagnosis_sha256');const stale=data.staleness;if(!stale||stale.is_stale!==false)fail('stale_contract');const source=(entry,p,n,expected)=>{if(!entry||entry.path!==p)fail('staleness_'+n+'_path');if(entry.sha256!==expected)fail('staleness_'+n+'_sha256')};source(stale.behavior_doc,behaviorPath,'behavior_doc',fileHash(behaviorPath));source(stale.baseline,data.baseline.source_path,'baseline',baselineHash);source(stale.diagnosis_artifact,diagnosisPath,'diagnosis_artifact',diagnosisHash);const semantic=JSON.parse(JSON.stringify(data));delete semantic.contract_snapshot.sidecar_semantic_sha256;delete semantic.contract_snapshot.diagnosis_artifact_sha256;delete semantic.staleness.diagnosis_artifact;const sidecarSemanticHash=hash(canon(semantic));const diagnosisSemanticHash=normalizedDiagnosisHash(diagnosisPath);if(snap.sidecar_semantic_sha256!==sidecarSemanticHash||snap.diagnosis_semantic_sha256!==diagnosisSemanticHash)fail('contract_snapshot_semantic');if(fs.existsSync(inside(reviewPath))){const prev=readJson(reviewPath);if(prev.verdict==='REQUEST_REANALYSIS'){const ps=prev.contract_snapshot||{};const unchanged=modern(ps)?ps.sidecar_semantic_sha256===sidecarSemanticHash&&ps.diagnosis_semantic_sha256===diagnosisSemanticHash:legacySame(ps,snap);if(unchanged)fail('reanalysis_unchanged');const cm=fs.statSync(inside(contractPath)).mtimeMs;const dm=fs.statSync(inside(diagnosisPath)).mtimeMs;const rm=fs.statSync(inside(reviewPath)).mtimeMs;if(cm<=rm||dm<=rm)fail('reanalysis_not_rewritten')}}\""
170
+ - "node -e \"const fs=require('node:fs');const crypto=require('node:crypto');const path=require('node:path');const mod=process.env.ARGUMENTS||'$ARGUMENTS';const allowed=['BUG','UNIMPLEMENTED','INFRA','REMOVED','TEST_BUG'];const action=['BUG','UNIMPLEMENTED','INFRA','TEST_BUG'];const fail=m=>{console.error('INTENTDNA_DIAGNOSTIC:'+m);process.exit(1)};const same=(a,b)=>Array.isArray(a)&&a.length===b.length&&a.every((v,i)=>v===b[i]);const snapSame=(a,b)=>a&&b&&a.baseline_source_sha256===b.baseline_source_sha256&&a.diagnosis_artifact_sha256===b.diagnosis_artifact_sha256&&a.sidecar_semantic_sha256===b.sidecar_semantic_sha256&&a.diagnosis_semantic_sha256===b.diagnosis_semantic_sha256&&a.observed_failure_count===b.observed_failure_count&&a.work_item_count===b.work_item_count&&same(a.allowed_kinds,b.allowed_kinds);const root=fs.realpathSync(process.cwd());const inside=p=>{if(typeof p!=='string'||!p||path.isAbsolute(p))fail('path_invalid');const r=path.resolve(root,p);if(r!==root&&!r.startsWith(root+path.sep))fail('path_escape');if(fs.existsSync(r)){const real=fs.realpathSync(r);if(real!==root&&!real.startsWith(root+path.sep))fail('path_escape')}return r};const readJson=p=>JSON.parse(fs.readFileSync(inside(p),'utf8'));const hash=v=>crypto.createHash('sha256').update(v).digest('hex');const fileHash=p=>hash(fs.readFileSync(inside(p)));const normalizedDiagnosisHash=p=>hash(fs.readFileSync(inside(p),'utf8').replace(/\\s+/g,' ').trim());const canon=v=>Array.isArray(v)?'['+v.map(canon).join(',')+']':v&&typeof v==='object'?'{'+Object.keys(v).sort().map(k=>JSON.stringify(k)+':'+canon(v[k])).join(',')+'}':JSON.stringify(v);if(!/^[A-Za-z0-9_-]+$/.test(mod))fail('module');const contractPath='.dna/specs/diagnosis-'+mod+'.contract.json';const reviewPath='.dna/specs/diagnosis-'+mod+'.review.json';const diagnosisPath='.dna/specs/diagnosis-'+mod+'.md';const behaviorPath='docs/behavior/'+mod+'.md';const data=readJson(contractPath);if(data.contract_version!=='diagnosis-contract/v1')fail('contract_version');if(data.module!==mod)fail('module_mismatch');if(data.diagnosis_artifact!==diagnosisPath)fail('diagnosis_artifact_path');if(!same(data.allowed_kinds,allowed))fail('allowed_kinds');const observed=Array.isArray(data.observed_failures)?data.observed_failures:fail('observed_failures');const work=Array.isArray(data.work_items)?data.work_items:fail('work_items');if(!observed.length)fail('empty_observed_failures');if(!work.length)fail('empty_work_items');const idOk=id=>typeof id==='string'&&/^[A-Za-z0-9][A-Za-z0-9_.:-]*$/.test(id);const ids=new Set();for(const f of observed){if(!idOk(f&&f.id)||ids.has(f.id))fail('observed_failure_ids');ids.add(f.id)}const workIds=new Set();for(const item of work){if(!idOk(item&&item.id)||workIds.has(item.id))fail('work_item_ids');workIds.add(item.id)}const failureById=new Map();for(const f of observed){if(typeof f.test_name!=='string'||!f.test_name.trim())fail('observed_failure_test_name');if(typeof f.symptom!=='string'||!f.symptom.trim())fail('observed_failure_symptom');if(!Array.isArray(f.evidence_paths)||!f.evidence_paths.length)fail('observed_failure_evidence');for(const p of f.evidence_paths)inside(p);if(f.kind!==undefined)fail('observed_failure_kind_forbidden');failureById.set(f.id,f)}const mapped=new Set();for(const item of work){if(!allowed.includes(item.kind))fail('invalid_kind');if(item.kind==='PLACEHOLDER_CONTRACT')fail('placeholder_kind');if(!Array.isArray(item.observed_failure_ids)||!item.observed_failure_ids.length)fail('work_item_observed_failure_ids');if(!Array.isArray(item.evidence_paths)||!item.evidence_paths.length)fail('work_item_evidence');for(const p of item.evidence_paths)inside(p);if(action.includes(item.kind)){if(!Array.isArray(item.v1_evidence_paths)||!item.v1_evidence_paths.length)fail('work_item_v1_evidence');if(!Array.isArray(item.v2_evidence_paths)||!item.v2_evidence_paths.length)fail('work_item_v2_evidence');for(const p of item.v1_evidence_paths)inside(p);for(const p of item.v2_evidence_paths)inside(p)}for(const id of item.observed_failure_ids){const f=failureById.get(id);if(!f)fail('unknown_observed_failure_id');if(action.includes(item.kind)&&(!f.test_name||!String(f.test_name).trim()))fail('action_test_evidence');mapped.add(id)}}if(mapped.size!==ids.size)fail('unmapped_observed_failures');for(const f of observed.filter(x=>String(x.symptom).includes('PLACEHOLDER_CONTRACT'))){const kinds=new Set(work.filter(i=>i.observed_failure_ids.includes(f.id)).map(i=>i.kind));if(![...kinds].every(k=>k==='TEST_BUG'||k==='UNIMPLEMENTED')||kinds.size===0)fail('placeholder_mapping')}const counts=data.baseline&&data.baseline.counts;if(!data.baseline||!data.baseline.source_path||!counts)fail('baseline');for(const k of ['failed','skipped','hung']){if(!Number.isInteger(counts[k])||counts[k]<0)fail('baseline_count_'+k)}if(counts.failed+counts.skipped+counts.hung!==observed.length)fail('baseline_count_mismatch');const summary=data.summary_counts||{};for(const kind of allowed){if(!Number.isInteger(summary[kind])||summary[kind]!==work.filter(x=>x.kind===kind).length)fail('summary_count_'+kind)}const snap=data.contract_snapshot||{};const baselineHash=fileHash(data.baseline.source_path);const diagnosisHash=fileHash(diagnosisPath);if(snap.observed_failure_count!==observed.length||snap.work_item_count!==work.length||!same(snap.allowed_kinds,allowed))fail('contract_snapshot');if(snap.baseline_source_sha256!==baselineHash)fail('baseline_sha256');if(snap.diagnosis_artifact_sha256!==diagnosisHash)fail('diagnosis_sha256');const stale=data.staleness;if(!stale||stale.is_stale!==false)fail('stale_contract');const source=(entry,p,n,expected)=>{if(!entry||entry.path!==p)fail('staleness_'+n+'_path');if(entry.sha256!==expected)fail('staleness_'+n+'_sha256')};source(stale.behavior_doc,behaviorPath,'behavior_doc',fileHash(behaviorPath));source(stale.baseline,data.baseline.source_path,'baseline',baselineHash);source(stale.diagnosis_artifact,diagnosisPath,'diagnosis_artifact',diagnosisHash);const semantic=JSON.parse(JSON.stringify(data));delete semantic.contract_snapshot.sidecar_semantic_sha256;delete semantic.contract_snapshot.diagnosis_artifact_sha256;delete semantic.staleness.diagnosis_artifact;const sidecarSemanticHash=hash(canon(semantic));const diagnosisSemanticHash=normalizedDiagnosisHash(diagnosisPath);if(snap.sidecar_semantic_sha256!==sidecarSemanticHash||snap.diagnosis_semantic_sha256!==diagnosisSemanticHash)fail('contract_snapshot_semantic');const review=readJson(reviewPath);if(review.contract_version!=='diagnosis-contract-review/v1')fail('review_contract_version');if(review.contract_valid!==true)fail('review_contract_valid');if(review.verdict!=='APPROVE')fail('review_verdict');if(review.contract_artifact_reviewed!==contractPath)fail('review_contract_artifact_path');if(review.artifact_reviewed!==diagnosisPath)fail('review_artifact_path');if(!snapSame(review.contract_snapshot,snap))fail('review_contract_snapshot');if(!review.updated_at||Number.isNaN(Date.parse(review.updated_at)))fail('review_updated_at');if(!Array.isArray(review.evidence_paths)||!review.evidence_paths.length)fail('review_evidence_paths');for(const p of review.evidence_paths)inside(p);const cm=fs.statSync(inside(contractPath)).mtimeMs;const dm=fs.statSync(inside(diagnosisPath)).mtimeMs;const rm=fs.statSync(inside(reviewPath)).mtimeMs;if(rm<cm||rm<dm)fail('review_stale');\""
170
171
  allow_builtin_asserts:
171
172
  - clean_working_tree
172
173
 
@@ -248,7 +249,7 @@ roles:
248
249
  analyzer:
249
250
  description: "Reads code and classifies failing tests. Writes diagnosis spec only."
250
251
  tool_permissions:
251
- allow: [Read, Grep, Glob, Write]
252
+ allow: [Read, Grep, Glob, Write, mcp__intentdna__dna_artifact_digest]
252
253
  deny: [Bash, Edit, NotebookEdit]
253
254
  scope:
254
255
  read: ["**/*"]
@@ -263,13 +264,14 @@ roles:
263
264
  - "The sidecar allowed_kinds array must be exactly BUG, UNIMPLEMENTED, INFRA, REMOVED, TEST_BUG in that order; PLACEHOLDER_CONTRACT is never a kind"
264
265
  - "A placeholder-contract symptom may appear only in observed_failures.symptom and must map to concrete TEST_BUG and/or UNIMPLEMENTED work_items with evidence"
265
266
  - "If reanalyzing after REQUEST_REANALYSIS, update every reviewer sections_to_fix item and report sections_updated"
267
+ - "Use mcp__intentdna__dna_artifact_digest for every exact contract SHA-256; never guess a digest or use Bash to compute one"
266
268
  - "DO NOT include fix prescriptions, implementation order, call-site instructions, or Notes for Surgeon"
267
269
  - "DO NOT run tests. DO NOT edit app code. Analysis only."
268
270
 
269
271
  analysis_reviewer:
270
272
  description: "Reviews analyzer output quality and writes durable diagnosis review verdict."
271
273
  tool_permissions:
272
- allow: [Read, Grep, Glob, Write]
274
+ allow: [Read, Grep, Glob, Write, mcp__intentdna__dna_artifact_digest]
273
275
  deny: [Bash, Edit, NotebookEdit]
274
276
  scope:
275
277
  read: ["**/*"]
@@ -278,6 +280,7 @@ roles:
278
280
  - "REQUIRED FIRST: Read all context files listed in SKILL.md"
279
281
  - "Verify analyzer spec: v1 references exist? Classifications sound?"
280
282
  - "Independently validate .dna/specs/diagnosis-$ARGUMENTS.contract.json against the template sidecar contract; do not use Bash"
283
+ - "Use mcp__intentdna__dna_artifact_digest to independently verify exact raw, normalized-text, and canonical-JSON SHA-256 values"
281
284
  - "Check missing: any failing test not covered?"
282
285
  - "Verify diagnosis remains evidence-only: no fix prescriptions, implementation order, call-site instructions, or Notes for Surgeon"
283
286
  - "Output a structured verdict block with contract_version, contract_valid, failure_reason, contract_artifact_reviewed, contract_snapshot, verdict, artifact_reviewed, sections_to_fix, evidence_paths, confidence, summary, and updated_at"
@@ -486,16 +489,20 @@ workflows:
486
489
  - observed_failures: one entry per failing/skipped/hung test with id, test_name, symptom, evidence_paths, and no kind field
487
490
  - work_items: one or more entries mapping observed_failure_ids to kind, evidence_paths, v1_evidence_paths, and v2_evidence_paths
488
491
  - summary_counts: exact count of work_items by BUG / UNIMPLEMENTED / INFRA / REMOVED / TEST_BUG
489
- - contract_snapshot: baseline_source_sha256, diagnosis_artifact_sha256, observed_failure_count, work_item_count, allowed_kinds
490
- - staleness: source mtimes or hashes for behavior_doc, baseline, diagnosis_artifact, and is_stale false
492
+ - contract_snapshot: baseline_source_sha256, diagnosis_artifact_sha256, sidecar_semantic_sha256, diagnosis_semantic_sha256, observed_failure_count, work_item_count, allowed_kinds
493
+ - staleness: behavior_doc, baseline, and diagnosis_artifact entries each contain the exact project-relative path and current sha256; is_stale is false
494
+ - Obtain hashes with mcp__intentdna__dna_artifact_digest, which is read-only and project-relative: bytes mode for behavior/baseline/diagnosis, whitespace_normalized_text for diagnosis_semantic_sha256, and canonical_json for the sidecar with exclude_json_pointers=["/contract_snapshot/sidecar_semantic_sha256", "/contract_snapshot/diagnosis_artifact_sha256", "/staleness/diagnosis_artifact"] for sidecar_semantic_sha256. Write the returned values back to the sidecar and re-run the canonical digest to confirm it is stable. Never guess hashes.
491
495
  7. Contract rules:
492
496
  - observed_failures must not be empty; work_items must not be empty.
493
497
  - Every observed_failure id must be mapped by at least one work_item.
494
498
  - Every work_item kind must be one of the five allowed kinds exactly.
495
499
  - PLACEHOLDER_CONTRACT is forbidden as a kind. If a placeholder-contract symptom exists, keep it in observed_failures.symptom and map it to concrete TEST_BUG and/or UNIMPLEMENTED work_items with evidence.
496
500
  - REMOVED may be diagnosed but must be marked manual/user-approved before any future fix can execute it.
497
- - Mixed unknown kinds, missing evidence, count mismatches, missing/changed contract_snapshot, and stale baseline/diagnosis snapshots make the contract invalid.
501
+ - Mixed unknown kinds, missing evidence, count mismatches, missing/changed contract_snapshot, and stale behavior/baseline/diagnosis snapshots make the contract invalid.
498
502
  8. If this is a reanalysis after REQUEST_REANALYSIS or DIAGNOSIS_CONTRACT_INVALID, read the reviewer verdict first, update every section listed in sections_to_fix plus the summary table and contract sidecar, and report sections_updated in your final response. Reanalysis is not successful unless both the spec artifact and contract sidecar are rewritten.
503
+ 9. Before handing off to review, the producer-side diagnosis contract completion gate must pass. It rejects grouped observations that hide per-test failures, duplicate/bad ids, unmapped observed_failures, missing evidence, count mismatches, bad hashes, stale state, and no-material-change reanalysis after REQUEST_REANALYSIS. Reanalysis compares the canonical sidecar with its self-digest and raw diagnosis snapshot fields omitted, plus a whitespace-normalized diagnosis hash, so a real sidecar-only correction is allowed while formatting-only Markdown changes are not.
504
+ completion:
505
+ - command_success: "node -e \"const fs=require('node:fs');const crypto=require('node:crypto');const path=require('node:path');const mod=process.env.ARGUMENTS||'$ARGUMENTS';const allowed=['BUG','UNIMPLEMENTED','INFRA','REMOVED','TEST_BUG'];const action=['BUG','UNIMPLEMENTED','INFRA','TEST_BUG'];const fail=m=>{console.error('INTENTDNA_DIAGNOSTIC:'+m);process.exit(1)};const same=(a,b)=>Array.isArray(a)&&a.length===b.length&&a.every((v,i)=>v===b[i]);const legacySame=(a,b)=>a&&b&&a.baseline_source_sha256===b.baseline_source_sha256&&a.diagnosis_artifact_sha256===b.diagnosis_artifact_sha256&&a.observed_failure_count===b.observed_failure_count&&a.work_item_count===b.work_item_count&&same(a.allowed_kinds,b.allowed_kinds);const modern=s=>s&&typeof s.sidecar_semantic_sha256==='string'&&typeof s.diagnosis_semantic_sha256==='string';const root=fs.realpathSync(process.cwd());const inside=p=>{if(typeof p!=='string'||!p||path.isAbsolute(p))fail('path_invalid');const r=path.resolve(root,p);if(r!==root&&!r.startsWith(root+path.sep))fail('path_escape');if(fs.existsSync(r)){const real=fs.realpathSync(r);if(real!==root&&!real.startsWith(root+path.sep))fail('path_escape')}return r};const readJson=p=>JSON.parse(fs.readFileSync(inside(p),'utf8'));const hash=v=>crypto.createHash('sha256').update(v).digest('hex');const fileHash=p=>hash(fs.readFileSync(inside(p)));const normalizedDiagnosisHash=p=>hash(fs.readFileSync(inside(p),'utf8').replace(/\\s+/g,' ').trim());const canon=v=>Array.isArray(v)?'['+v.map(canon).join(',')+']':v&&typeof v==='object'?'{'+Object.keys(v).sort().map(k=>JSON.stringify(k)+':'+canon(v[k])).join(',')+'}':JSON.stringify(v);if(!/^[A-Za-z0-9_-]+$/.test(mod))fail('module');const contractPath='.dna/specs/diagnosis-'+mod+'.contract.json';const reviewPath='.dna/specs/diagnosis-'+mod+'.review.json';const diagnosisPath='.dna/specs/diagnosis-'+mod+'.md';const behaviorPath='docs/behavior/'+mod+'.md';const data=readJson(contractPath);if(data.contract_version!=='diagnosis-contract/v1')fail('contract_version');if(data.module!==mod)fail('module_mismatch');if(data.diagnosis_artifact!==diagnosisPath)fail('diagnosis_artifact_path');if(!same(data.allowed_kinds,allowed))fail('allowed_kinds');const observed=Array.isArray(data.observed_failures)?data.observed_failures:fail('observed_failures');const work=Array.isArray(data.work_items)?data.work_items:fail('work_items');if(!observed.length)fail('empty_observed_failures');if(!work.length)fail('empty_work_items');const idOk=id=>typeof id==='string'&&/^[A-Za-z0-9][A-Za-z0-9_.:-]*$/.test(id);const ids=new Set();for(const f of observed){if(!idOk(f&&f.id)||ids.has(f.id))fail('observed_failure_ids');ids.add(f.id)}const workIds=new Set();for(const item of work){if(!idOk(item&&item.id)||workIds.has(item.id))fail('work_item_ids');workIds.add(item.id)}const failureById=new Map();for(const f of observed){if(typeof f.test_name!=='string'||!f.test_name.trim())fail('observed_failure_test_name');if(typeof f.symptom!=='string'||!f.symptom.trim())fail('observed_failure_symptom');if(!Array.isArray(f.evidence_paths)||!f.evidence_paths.length)fail('observed_failure_evidence');for(const p of f.evidence_paths)inside(p);if(f.kind!==undefined)fail('observed_failure_kind_forbidden');failureById.set(f.id,f)}const mapped=new Set();for(const item of work){if(!allowed.includes(item.kind))fail('invalid_kind');if(item.kind==='PLACEHOLDER_CONTRACT')fail('placeholder_kind');if(!Array.isArray(item.observed_failure_ids)||!item.observed_failure_ids.length)fail('work_item_observed_failure_ids');if(!Array.isArray(item.evidence_paths)||!item.evidence_paths.length)fail('work_item_evidence');for(const p of item.evidence_paths)inside(p);if(action.includes(item.kind)){if(!Array.isArray(item.v1_evidence_paths)||!item.v1_evidence_paths.length)fail('work_item_v1_evidence');if(!Array.isArray(item.v2_evidence_paths)||!item.v2_evidence_paths.length)fail('work_item_v2_evidence');for(const p of item.v1_evidence_paths)inside(p);for(const p of item.v2_evidence_paths)inside(p)}for(const id of item.observed_failure_ids){const f=failureById.get(id);if(!f)fail('unknown_observed_failure_id');if(action.includes(item.kind)&&(!f.test_name||!String(f.test_name).trim()))fail('action_test_evidence');mapped.add(id)}}if(mapped.size!==ids.size)fail('unmapped_observed_failures');for(const f of observed.filter(x=>String(x.symptom).includes('PLACEHOLDER_CONTRACT'))){const kinds=new Set(work.filter(i=>i.observed_failure_ids.includes(f.id)).map(i=>i.kind));if(![...kinds].every(k=>k==='TEST_BUG'||k==='UNIMPLEMENTED')||kinds.size===0)fail('placeholder_mapping')}const counts=data.baseline&&data.baseline.counts;if(!data.baseline||!data.baseline.source_path||!counts)fail('baseline');for(const k of ['failed','skipped','hung']){if(!Number.isInteger(counts[k])||counts[k]<0)fail('baseline_count_'+k)}if(counts.failed+counts.skipped+counts.hung!==observed.length)fail('baseline_count_mismatch');const summary=data.summary_counts||{};for(const kind of allowed){if(!Number.isInteger(summary[kind])||summary[kind]!==work.filter(x=>x.kind===kind).length)fail('summary_count_'+kind)}const snap=data.contract_snapshot||{};const baselineHash=fileHash(data.baseline.source_path);const diagnosisHash=fileHash(diagnosisPath);if(snap.observed_failure_count!==observed.length||snap.work_item_count!==work.length||!same(snap.allowed_kinds,allowed))fail('contract_snapshot');if(snap.baseline_source_sha256!==baselineHash)fail('baseline_sha256');if(snap.diagnosis_artifact_sha256!==diagnosisHash)fail('diagnosis_sha256');const stale=data.staleness;if(!stale||stale.is_stale!==false)fail('stale_contract');const source=(entry,p,n,expected)=>{if(!entry||entry.path!==p)fail('staleness_'+n+'_path');if(entry.sha256!==expected)fail('staleness_'+n+'_sha256')};source(stale.behavior_doc,behaviorPath,'behavior_doc',fileHash(behaviorPath));source(stale.baseline,data.baseline.source_path,'baseline',baselineHash);source(stale.diagnosis_artifact,diagnosisPath,'diagnosis_artifact',diagnosisHash);const semantic=JSON.parse(JSON.stringify(data));delete semantic.contract_snapshot.sidecar_semantic_sha256;delete semantic.contract_snapshot.diagnosis_artifact_sha256;delete semantic.staleness.diagnosis_artifact;const sidecarSemanticHash=hash(canon(semantic));const diagnosisSemanticHash=normalizedDiagnosisHash(diagnosisPath);if(snap.sidecar_semantic_sha256!==sidecarSemanticHash||snap.diagnosis_semantic_sha256!==diagnosisSemanticHash)fail('contract_snapshot_semantic');if(fs.existsSync(inside(reviewPath))){const prev=readJson(reviewPath);if(prev.verdict==='REQUEST_REANALYSIS'){const ps=prev.contract_snapshot||{};const unchanged=modern(ps)?ps.sidecar_semantic_sha256===sidecarSemanticHash&&ps.diagnosis_semantic_sha256===diagnosisSemanticHash:legacySame(ps,snap);if(unchanged)fail('reanalysis_unchanged');const cm=fs.statSync(inside(contractPath)).mtimeMs;const dm=fs.statSync(inside(diagnosisPath)).mtimeMs;const rm=fs.statSync(inside(reviewPath)).mtimeMs;if(cm<=rm||dm<=rm)fail('reanalysis_not_rewritten')}}\""
499
506
  handoff:
500
507
  produces:
501
508
  - type: file
@@ -527,8 +534,9 @@ workflows:
527
534
  - summary_counts match work_items by kind
528
535
  - every work_item has evidence_paths and only allowed kinds
529
536
  - PLACEHOLDER_CONTRACT is rejected as a kind but placeholder-contract symptoms are allowed when mapped to TEST_BUG and/or UNIMPLEMENTED
530
- - contract_snapshot includes diagnosis_artifact_sha256, baseline_source_sha256, observed_failure_count, work_item_count, and allowed_kinds
531
- - staleness.is_stale is false; stale baseline/diagnosis/contract snapshots are invalid
537
+ - contract_snapshot includes diagnosis_artifact_sha256, baseline_source_sha256, sidecar_semantic_sha256, diagnosis_semantic_sha256, observed_failure_count, work_item_count, and allowed_kinds
538
+ - staleness.is_stale is false and behavior_doc, baseline, and diagnosis_artifact path+sha256 entries match current project files; stale source snapshots are invalid
539
+ - independently recompute hashes with mcp__intentdna__dna_artifact_digest using the same bytes, whitespace_normalized_text, and canonical_json modes; canonical_json must omit /contract_snapshot/sidecar_semantic_sha256, /contract_snapshot/diagnosis_artifact_sha256, and /staleness/diagnosis_artifact
532
540
  - REMOVED work_items are reviewable diagnosis facts only and must not authorize automatic fix execution
533
541
 
534
542
  Output exactly one structured verdict block:
@@ -541,6 +549,8 @@ workflows:
541
549
  "contract_snapshot": {
542
550
  "baseline_source_sha256": "sha256 from contract sidecar",
543
551
  "diagnosis_artifact_sha256": "sha256 from contract sidecar",
552
+ "sidecar_semantic_sha256": "canonical routing sidecar sha256 from contract sidecar",
553
+ "diagnosis_semantic_sha256": "whitespace-normalized diagnosis sha256 from contract sidecar",
544
554
  "observed_failure_count": 0,
545
555
  "work_item_count": 0,
546
556
  "allowed_kinds": ["BUG", "UNIMPLEMENTED", "INFRA", "REMOVED", "TEST_BUG"]
@@ -560,7 +570,7 @@ workflows:
560
570
  APPROVE → diagnosis complete.
561
571
  REQUEST_REANALYSIS → sections_to_fix must be passed back to analyze. If contract_valid is false, set failure_reason to DIAGNOSIS_CONTRACT_INVALID and include the exact invalid rule in sections_to_fix. docs/behavior/blocked_items.md is historical context only and must not be treated as the current run verdict.
562
572
  completion:
563
- - command_success: "node -e \"const fs=require('node:fs');const crypto=require('node:crypto');const path=require('node:path');const mod=process.env.ARGUMENTS||'$ARGUMENTS';const allowed=['BUG','UNIMPLEMENTED','INFRA','REMOVED','TEST_BUG'];const action=['BUG','UNIMPLEMENTED','INFRA','TEST_BUG'];const fail=m=>{throw new Error(m)};const same=(a,b)=>Array.isArray(a)&&a.length===b.length&&a.every((v,i)=>v===b[i]);const snapSame=(a,b)=>a&&b&&a.baseline_source_sha256===b.baseline_source_sha256&&a.diagnosis_artifact_sha256===b.diagnosis_artifact_sha256&&a.observed_failure_count===b.observed_failure_count&&a.work_item_count===b.work_item_count&&same(a.allowed_kinds,b.allowed_kinds);const root=process.cwd();const inside=p=>{if(typeof p!=='string'||!p||path.isAbsolute(p))fail('path_invalid');const r=path.resolve(root,p);if(r!==root&&!r.startsWith(root+path.sep))fail('path_escape');return r};const readJson=p=>JSON.parse(fs.readFileSync(inside(p),'utf8'));if(!/^[A-Za-z0-9_-]+$/.test(mod))fail('module');const contractPath='.dna/specs/diagnosis-'+mod+'.contract.json';const reviewPath='.dna/specs/diagnosis-'+mod+'.review.json';const diagnosisPath='.dna/specs/diagnosis-'+mod+'.md';const data=readJson(contractPath);if(data.contract_version!=='diagnosis-contract/v1')fail('contract_version');if(data.module!==mod)fail('module_mismatch');if(data.diagnosis_artifact!==diagnosisPath)fail('diagnosis_artifact_path');if(!same(data.allowed_kinds,allowed))fail('allowed_kinds');const observed=Array.isArray(data.observed_failures)?data.observed_failures:fail('observed_failures');const work=Array.isArray(data.work_items)?data.work_items:fail('work_items');if(!observed.length)fail('empty_observed_failures');if(!work.length)fail('empty_work_items');const ids=new Set(observed.map(x=>x&&x.id).filter(Boolean));if(ids.size!==observed.length)fail('observed_failure_ids');const failureById=new Map();for(const f of observed){if(typeof f.test_name!=='string'||!f.test_name.trim())fail('observed_failure_test_name');if(typeof f.symptom!=='string'||!f.symptom.trim())fail('observed_failure_symptom');if(!Array.isArray(f.evidence_paths)||!f.evidence_paths.length)fail('observed_failure_evidence');for(const p of f.evidence_paths)inside(p);if(f.kind!==undefined)fail('observed_failure_kind_forbidden');failureById.set(f.id,f)}const mapped=new Set();for(const item of work){if(!allowed.includes(item.kind))fail('invalid_kind');if(item.kind==='PLACEHOLDER_CONTRACT')fail('placeholder_kind');if(!Array.isArray(item.observed_failure_ids)||!item.observed_failure_ids.length)fail('work_item_observed_failure_ids');if(!Array.isArray(item.evidence_paths)||!item.evidence_paths.length)fail('work_item_evidence');for(const p of item.evidence_paths)inside(p);if(action.includes(item.kind)){if(!Array.isArray(item.v1_evidence_paths)||!item.v1_evidence_paths.length)fail('work_item_v1_evidence');if(!Array.isArray(item.v2_evidence_paths)||!item.v2_evidence_paths.length)fail('work_item_v2_evidence');for(const p of item.v1_evidence_paths)inside(p);for(const p of item.v2_evidence_paths)inside(p)}for(const id of item.observed_failure_ids){const f=failureById.get(id);if(!f)fail('unknown_observed_failure_id');if(action.includes(item.kind)&&(!f.test_name||!String(f.test_name).trim()))fail('action_test_evidence');mapped.add(id)}}if(mapped.size!==ids.size)fail('unmapped_observed_failures');for(const f of observed.filter(x=>String(x.symptom).includes('PLACEHOLDER_CONTRACT'))){const kinds=new Set(work.filter(i=>i.observed_failure_ids.includes(f.id)).map(i=>i.kind));if(![...kinds].every(k=>k==='TEST_BUG'||k==='UNIMPLEMENTED')||kinds.size===0)fail('placeholder_mapping')}const counts=data.baseline&&data.baseline.counts;if(!data.baseline||!data.baseline.source_path||!counts)fail('baseline');inside(data.baseline.source_path);for(const k of ['failed','skipped','hung']){if(!Number.isInteger(counts[k])||counts[k]<0)fail('baseline_count_'+k)}const total=counts.failed+counts.skipped+counts.hung;if(total!==observed.length)fail('baseline_count_mismatch');const summary=data.summary_counts||{};for(const kind of allowed){if(!Number.isInteger(summary[kind])||summary[kind]!==work.filter(x=>x.kind===kind).length)fail('summary_count_'+kind)}const sha=p=>crypto.createHash('sha256').update(fs.readFileSync(inside(p))).digest('hex');const snap=data.contract_snapshot||{};if(snap.observed_failure_count!==observed.length||snap.work_item_count!==work.length||!same(snap.allowed_kinds,allowed))fail('contract_snapshot');if(snap.baseline_source_sha256!==sha(data.baseline.source_path))fail('baseline_sha256');if(snap.diagnosis_artifact_sha256!==sha(diagnosisPath))fail('diagnosis_sha256');if(!data.staleness||data.staleness.is_stale!==false)fail('stale_contract');const review=readJson(reviewPath);if(review.contract_version!=='diagnosis-contract-review/v1')fail('review_contract_version');if(review.contract_valid!==true)fail('review_contract_valid');if(review.verdict!=='APPROVE')fail('review_verdict');if(review.contract_artifact_reviewed!==contractPath)fail('review_contract_artifact_path');if(review.artifact_reviewed!==diagnosisPath)fail('review_artifact_path');if(!snapSame(review.contract_snapshot,snap))fail('review_contract_snapshot');if(!review.updated_at||Number.isNaN(Date.parse(review.updated_at)))fail('review_updated_at');if(!Array.isArray(review.evidence_paths)||!review.evidence_paths.length)fail('review_evidence_paths');for(const p of review.evidence_paths)inside(p);const cm=fs.statSync(inside(contractPath)).mtimeMs;const dm=fs.statSync(inside(diagnosisPath)).mtimeMs;const rm=fs.statSync(inside(reviewPath)).mtimeMs;if(rm<cm||rm<dm)fail('review_stale');\""
573
+ - command_success: "node -e \"const fs=require('node:fs');const crypto=require('node:crypto');const path=require('node:path');const mod=process.env.ARGUMENTS||'$ARGUMENTS';const allowed=['BUG','UNIMPLEMENTED','INFRA','REMOVED','TEST_BUG'];const action=['BUG','UNIMPLEMENTED','INFRA','TEST_BUG'];const fail=m=>{console.error('INTENTDNA_DIAGNOSTIC:'+m);process.exit(1)};const same=(a,b)=>Array.isArray(a)&&a.length===b.length&&a.every((v,i)=>v===b[i]);const snapSame=(a,b)=>a&&b&&a.baseline_source_sha256===b.baseline_source_sha256&&a.diagnosis_artifact_sha256===b.diagnosis_artifact_sha256&&a.sidecar_semantic_sha256===b.sidecar_semantic_sha256&&a.diagnosis_semantic_sha256===b.diagnosis_semantic_sha256&&a.observed_failure_count===b.observed_failure_count&&a.work_item_count===b.work_item_count&&same(a.allowed_kinds,b.allowed_kinds);const root=fs.realpathSync(process.cwd());const inside=p=>{if(typeof p!=='string'||!p||path.isAbsolute(p))fail('path_invalid');const r=path.resolve(root,p);if(r!==root&&!r.startsWith(root+path.sep))fail('path_escape');if(fs.existsSync(r)){const real=fs.realpathSync(r);if(real!==root&&!real.startsWith(root+path.sep))fail('path_escape')}return r};const readJson=p=>JSON.parse(fs.readFileSync(inside(p),'utf8'));const hash=v=>crypto.createHash('sha256').update(v).digest('hex');const fileHash=p=>hash(fs.readFileSync(inside(p)));const normalizedDiagnosisHash=p=>hash(fs.readFileSync(inside(p),'utf8').replace(/\\s+/g,' ').trim());const canon=v=>Array.isArray(v)?'['+v.map(canon).join(',')+']':v&&typeof v==='object'?'{'+Object.keys(v).sort().map(k=>JSON.stringify(k)+':'+canon(v[k])).join(',')+'}':JSON.stringify(v);if(!/^[A-Za-z0-9_-]+$/.test(mod))fail('module');const contractPath='.dna/specs/diagnosis-'+mod+'.contract.json';const reviewPath='.dna/specs/diagnosis-'+mod+'.review.json';const diagnosisPath='.dna/specs/diagnosis-'+mod+'.md';const behaviorPath='docs/behavior/'+mod+'.md';const data=readJson(contractPath);if(data.contract_version!=='diagnosis-contract/v1')fail('contract_version');if(data.module!==mod)fail('module_mismatch');if(data.diagnosis_artifact!==diagnosisPath)fail('diagnosis_artifact_path');if(!same(data.allowed_kinds,allowed))fail('allowed_kinds');const observed=Array.isArray(data.observed_failures)?data.observed_failures:fail('observed_failures');const work=Array.isArray(data.work_items)?data.work_items:fail('work_items');if(!observed.length)fail('empty_observed_failures');if(!work.length)fail('empty_work_items');const idOk=id=>typeof id==='string'&&/^[A-Za-z0-9][A-Za-z0-9_.:-]*$/.test(id);const ids=new Set();for(const f of observed){if(!idOk(f&&f.id)||ids.has(f.id))fail('observed_failure_ids');ids.add(f.id)}const workIds=new Set();for(const item of work){if(!idOk(item&&item.id)||workIds.has(item.id))fail('work_item_ids');workIds.add(item.id)}const failureById=new Map();for(const f of observed){if(typeof f.test_name!=='string'||!f.test_name.trim())fail('observed_failure_test_name');if(typeof f.symptom!=='string'||!f.symptom.trim())fail('observed_failure_symptom');if(!Array.isArray(f.evidence_paths)||!f.evidence_paths.length)fail('observed_failure_evidence');for(const p of f.evidence_paths)inside(p);if(f.kind!==undefined)fail('observed_failure_kind_forbidden');failureById.set(f.id,f)}const mapped=new Set();for(const item of work){if(!allowed.includes(item.kind))fail('invalid_kind');if(item.kind==='PLACEHOLDER_CONTRACT')fail('placeholder_kind');if(!Array.isArray(item.observed_failure_ids)||!item.observed_failure_ids.length)fail('work_item_observed_failure_ids');if(!Array.isArray(item.evidence_paths)||!item.evidence_paths.length)fail('work_item_evidence');for(const p of item.evidence_paths)inside(p);if(action.includes(item.kind)){if(!Array.isArray(item.v1_evidence_paths)||!item.v1_evidence_paths.length)fail('work_item_v1_evidence');if(!Array.isArray(item.v2_evidence_paths)||!item.v2_evidence_paths.length)fail('work_item_v2_evidence');for(const p of item.v1_evidence_paths)inside(p);for(const p of item.v2_evidence_paths)inside(p)}for(const id of item.observed_failure_ids){const f=failureById.get(id);if(!f)fail('unknown_observed_failure_id');if(action.includes(item.kind)&&(!f.test_name||!String(f.test_name).trim()))fail('action_test_evidence');mapped.add(id)}}if(mapped.size!==ids.size)fail('unmapped_observed_failures');for(const f of observed.filter(x=>String(x.symptom).includes('PLACEHOLDER_CONTRACT'))){const kinds=new Set(work.filter(i=>i.observed_failure_ids.includes(f.id)).map(i=>i.kind));if(![...kinds].every(k=>k==='TEST_BUG'||k==='UNIMPLEMENTED')||kinds.size===0)fail('placeholder_mapping')}const counts=data.baseline&&data.baseline.counts;if(!data.baseline||!data.baseline.source_path||!counts)fail('baseline');for(const k of ['failed','skipped','hung']){if(!Number.isInteger(counts[k])||counts[k]<0)fail('baseline_count_'+k)}if(counts.failed+counts.skipped+counts.hung!==observed.length)fail('baseline_count_mismatch');const summary=data.summary_counts||{};for(const kind of allowed){if(!Number.isInteger(summary[kind])||summary[kind]!==work.filter(x=>x.kind===kind).length)fail('summary_count_'+kind)}const snap=data.contract_snapshot||{};const baselineHash=fileHash(data.baseline.source_path);const diagnosisHash=fileHash(diagnosisPath);if(snap.observed_failure_count!==observed.length||snap.work_item_count!==work.length||!same(snap.allowed_kinds,allowed))fail('contract_snapshot');if(snap.baseline_source_sha256!==baselineHash)fail('baseline_sha256');if(snap.diagnosis_artifact_sha256!==diagnosisHash)fail('diagnosis_sha256');const stale=data.staleness;if(!stale||stale.is_stale!==false)fail('stale_contract');const source=(entry,p,n,expected)=>{if(!entry||entry.path!==p)fail('staleness_'+n+'_path');if(entry.sha256!==expected)fail('staleness_'+n+'_sha256')};source(stale.behavior_doc,behaviorPath,'behavior_doc',fileHash(behaviorPath));source(stale.baseline,data.baseline.source_path,'baseline',baselineHash);source(stale.diagnosis_artifact,diagnosisPath,'diagnosis_artifact',diagnosisHash);const semantic=JSON.parse(JSON.stringify(data));delete semantic.contract_snapshot.sidecar_semantic_sha256;delete semantic.contract_snapshot.diagnosis_artifact_sha256;delete semantic.staleness.diagnosis_artifact;const sidecarSemanticHash=hash(canon(semantic));const diagnosisSemanticHash=normalizedDiagnosisHash(diagnosisPath);if(snap.sidecar_semantic_sha256!==sidecarSemanticHash||snap.diagnosis_semantic_sha256!==diagnosisSemanticHash)fail('contract_snapshot_semantic');const review=readJson(reviewPath);if(review.contract_version!=='diagnosis-contract-review/v1')fail('review_contract_version');if(review.contract_valid!==true)fail('review_contract_valid');if(review.verdict!=='APPROVE')fail('review_verdict');if(review.contract_artifact_reviewed!==contractPath)fail('review_contract_artifact_path');if(review.artifact_reviewed!==diagnosisPath)fail('review_artifact_path');if(!snapSame(review.contract_snapshot,snap))fail('review_contract_snapshot');if(!review.updated_at||Number.isNaN(Date.parse(review.updated_at)))fail('review_updated_at');if(!Array.isArray(review.evidence_paths)||!review.evidence_paths.length)fail('review_evidence_paths');for(const p of review.evidence_paths)inside(p);const cm=fs.statSync(inside(contractPath)).mtimeMs;const dm=fs.statSync(inside(diagnosisPath)).mtimeMs;const rm=fs.statSync(inside(reviewPath)).mtimeMs;if(rm<cm||rm<dm)fail('review_stale');\""
564
574
  handoff:
565
575
  consumes:
566
576
  - type: file
package/hooks/hooks.json CHANGED
@@ -3,7 +3,7 @@
3
3
  "PreToolUse": [{ "matcher": "*", "hooks": [{ "type": "command", "command": "dna-hook PreToolUse", "timeout": 5 }] }],
4
4
  "PostToolUse": [{ "matcher": "*", "hooks": [{ "type": "command", "command": "dna-hook PostToolUse", "timeout": 3 }] }],
5
5
  "UserPromptSubmit": [{ "matcher": "*", "hooks": [{ "type": "command", "command": "dna-hook UserPromptSubmit", "timeout": 5 }] }],
6
- "SubagentStop": [{ "matcher": "*", "hooks": [{ "type": "command", "command": "dna-hook SubagentStop", "timeout": 3 }] }],
6
+ "SubagentStop": [{ "matcher": "*", "hooks": [{ "type": "command", "command": "dna-hook SubagentStop", "timeout": 45 }] }],
7
7
  "PreCompact": [{ "matcher": "*", "hooks": [{ "type": "command", "command": "dna-hook PreCompact", "timeout": 3 }] }],
8
8
  "Notification": [{ "matcher": "*", "hooks": [{ "type": "command", "command": "dna-hook Notification", "timeout": 3 }] }],
9
9
  "Stop": [{ "matcher": "*", "hooks": [{ "type": "command", "command": "dna-hook Stop", "timeout": 45 }] }],
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "intentdna",
3
- "version": "1.8.2",
3
+ "version": "1.8.4",
4
4
  "description": "Intent DNA — Declarative policy layer for AI agent behavior",
5
5
  "type": "module",
6
6
  "main": "dist/index.js",