intentdna 1.8.3 → 1.8.5

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.3",
12
+ "version": "1.8.5",
13
13
  "author": {
14
14
  "name": "Samuel"
15
15
  },
@@ -25,5 +25,5 @@
25
25
  ]
26
26
  }
27
27
  ],
28
- "version": "1.8.3"
28
+ "version": "1.8.5"
29
29
  }
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "intentdna",
3
- "version": "1.8.3",
3
+ "version": "1.8.5",
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.5
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.5` keeps Claude plugin Hook delivery in `hooks/hooks.json`: plugin sync does not register project `settings.json` Hooks, retracts exact stale bin-mode `dna-hook` entries once, and leaves user settings untouched in steady state. 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
  }
@@ -612,6 +612,13 @@ export async function runSync(opts) {
612
612
  opts.skillsDir = resolve(projectDir, ".agents", "skills");
613
613
  process.stderr.write("Codex workflow projection: .agents/skills\n");
614
614
  }
615
+ const requestedMode = opts.mode ?? (opts.plugin === true ? "plugin" : opts.plugin === false ? "bin" : "auto");
616
+ let resolvedMode = requestedMode === "auto" ? "bin" : requestedMode;
617
+ if (!opts.remove && requestedMode === "auto") {
618
+ resolvedMode = await detectMode();
619
+ process.stderr.write(`Auto-detect: ${resolvedMode} mode${resolvedMode === "plugin" ? " (intentdna registered as Claude Code plugin)" : ""}\n`);
620
+ }
621
+ opts.plugin = resolvedMode === "plugin";
615
622
  if (!opts.remove && !opts.inject && !opts.hooksDir && !opts.agentsDir && !opts.workflowDir && !opts.skillsDir && !opts.settingsPath && !opts.codex) {
616
623
  // Auto-detect Claude Code environment → fill all outputs
617
624
  const hasClaudeDir = await fileExists(resolve(projectDir, ".claude"));
@@ -621,16 +628,11 @@ export async function runSync(opts) {
621
628
  opts.agentsDir = resolve(projectDir, ".claude", "agents");
622
629
  opts.skillsDir = resolve(projectDir, ".claude", "skills");
623
630
  opts.settingsPath = resolve(projectDir, ".claude", "settings.json");
624
- process.stderr.write(`Auto-detect: Claude Code → CLAUDE.md + agents + skills + settings\n`);
631
+ process.stderr.write(resolvedMode === "plugin"
632
+ ? "Auto-detect: Claude Code → CLAUDE.md + agents + skills; plugin hooks (no settings injection)\n"
633
+ : "Auto-detect: Claude Code → CLAUDE.md + agents + skills + settings\n");
625
634
  }
626
635
  }
627
- const requestedMode = opts.mode ?? (opts.plugin === true ? "plugin" : opts.plugin === false ? "bin" : "auto");
628
- let resolvedMode = requestedMode === "auto" ? "bin" : requestedMode;
629
- if (!opts.remove && requestedMode === "auto") {
630
- resolvedMode = await detectMode();
631
- process.stderr.write(`Auto-detect: ${resolvedMode} mode${resolvedMode === "plugin" ? " (intentdna registered as Claude Code plugin)" : ""}\n`);
632
- }
633
- opts.plugin = resolvedMode === "plugin";
634
636
  // Handle --remove
635
637
  if (opts.remove) {
636
638
  if (!opts.inject && !opts.hooksDir && !opts.agentsDir && !opts.workflowDir && !opts.skillsDir && !opts.codex) {
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,6 +1171,37 @@ 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
1207
  results.push(...await runVerifiersForTest(projectDir, ir, workflowState, "pre_handoff", sessionId, options));
@@ -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[];