intentdna 1.9.0 → 1.9.3

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (131) hide show
  1. package/.claude-plugin/marketplace.json +2 -2
  2. package/.claude-plugin/plugin.json +1 -1
  3. package/README.md +227 -87
  4. package/dist/cli/commands/run-lifecycle.d.ts +61 -11
  5. package/dist/cli/commands/run-lifecycle.js +184 -50
  6. package/dist/cli/commands/run-observer.d.ts +15 -0
  7. package/dist/cli/commands/run-observer.js +210 -0
  8. package/dist/cli/commands/run.d.ts +16 -20
  9. package/dist/cli/commands/run.js +461 -659
  10. package/dist/cli/commands/sync.js +16 -12
  11. package/dist/cli/index.js +14 -5
  12. package/dist/compiler/compile.d.ts +1 -0
  13. package/dist/compiler/compile.js +1 -1
  14. package/dist/compiler/controller.d.ts +16 -0
  15. package/dist/compiler/controller.js +693 -0
  16. package/dist/compiler/input-resolver.d.ts +2 -0
  17. package/dist/compiler/input-resolver.js +22 -6
  18. package/dist/hooks/cli.d.ts +45 -0
  19. package/dist/hooks/cli.js +263 -31
  20. package/dist/hooks/host-event.d.ts +57 -0
  21. package/dist/hooks/host-event.js +187 -0
  22. package/dist/hooks/index.d.ts +1 -0
  23. package/dist/hooks/index.js +1 -0
  24. package/dist/hooks/protocol.d.ts +7 -0
  25. package/dist/hooks/schema.d.ts +1 -0
  26. package/dist/hooks/schema.js +89 -1
  27. package/dist/hooks/state.d.ts +23 -1
  28. package/dist/hooks/state.js +9 -0
  29. package/dist/index.d.ts +1 -0
  30. package/dist/index.js +1 -0
  31. package/dist/mcp/index.js +2 -0
  32. package/dist/mcp/tools-run.d.ts +4 -0
  33. package/dist/mcp/tools-run.js +419 -0
  34. package/dist/mcp/tools-state.d.ts +1 -1
  35. package/dist/mcp/tools-state.js +13 -241
  36. package/dist/runtime/artifact-store.d.ts +146 -0
  37. package/dist/runtime/artifact-store.js +1436 -0
  38. package/dist/runtime/canonical-attempt-outcome.d.ts +12 -0
  39. package/dist/runtime/canonical-attempt-outcome.js +195 -0
  40. package/dist/runtime/canonical-json.d.ts +10 -0
  41. package/dist/runtime/canonical-json.js +105 -0
  42. package/dist/runtime/canonical-run-application.d.ts +93 -0
  43. package/dist/runtime/canonical-run-application.js +229 -0
  44. package/dist/runtime/canonical-run-service.d.ts +454 -0
  45. package/dist/runtime/canonical-run-service.js +4760 -0
  46. package/dist/runtime/canonical-runtime-composition.d.ts +22 -0
  47. package/dist/runtime/canonical-runtime-composition.js +34 -0
  48. package/dist/runtime/canonical-runtime-projection.d.ts +3 -0
  49. package/dist/runtime/canonical-runtime-projection.js +4 -0
  50. package/dist/runtime/canonical-target-compiler.d.ts +46 -0
  51. package/dist/runtime/canonical-target-compiler.js +514 -0
  52. package/dist/runtime/claude-agent-identity.d.ts +2 -0
  53. package/dist/runtime/claude-agent-identity.js +21 -0
  54. package/dist/runtime/executable-run-plan.d.ts +328 -0
  55. package/dist/runtime/executable-run-plan.js +1485 -0
  56. package/dist/runtime/execution-authority.d.ts +83 -0
  57. package/dist/runtime/execution-authority.js +98 -0
  58. package/dist/runtime/handoff-resolver.js +4 -1
  59. package/dist/runtime/harness-pull-adapter.d.ts +146 -0
  60. package/dist/runtime/harness-pull-adapter.js +315 -0
  61. package/dist/runtime/index.d.ts +39 -14
  62. package/dist/runtime/index.js +29 -7
  63. package/dist/runtime/local-execution-authority.d.ts +194 -0
  64. package/dist/runtime/local-execution-authority.js +2897 -0
  65. package/dist/runtime/local-execution-reconciliation.d.ts +20 -0
  66. package/dist/runtime/local-execution-reconciliation.js +107 -0
  67. package/dist/runtime/local-execution-supervisor-script.d.ts +7 -0
  68. package/dist/runtime/local-execution-supervisor-script.js +793 -0
  69. package/dist/runtime/local-provider-sandbox.d.ts +42 -0
  70. package/dist/runtime/local-provider-sandbox.js +154 -0
  71. package/dist/runtime/local-verifier-execution.d.ts +24 -0
  72. package/dist/runtime/local-verifier-execution.js +290 -0
  73. package/dist/runtime/local-windows-execution-supervisor-script.d.ts +6 -0
  74. package/dist/runtime/local-windows-execution-supervisor-script.js +788 -0
  75. package/dist/runtime/plan-store.d.ts +30 -0
  76. package/dist/runtime/plan-store.js +231 -0
  77. package/dist/runtime/process-tree.d.ts +9 -0
  78. package/dist/runtime/process-tree.js +97 -24
  79. package/dist/runtime/providers/codex.js +2 -2
  80. package/dist/runtime/push-driver.d.ts +184 -0
  81. package/dist/runtime/push-driver.js +1002 -0
  82. package/dist/runtime/result-store.d.ts +34 -3
  83. package/dist/runtime/result-store.js +1073 -2
  84. package/dist/runtime/run-binding.d.ts +68 -0
  85. package/dist/runtime/run-binding.js +278 -0
  86. package/dist/runtime/run-contracts.d.ts +575 -0
  87. package/dist/runtime/run-contracts.js +58 -7
  88. package/dist/runtime/run-controller.d.ts +1 -6
  89. package/dist/runtime/run-controller.js +0 -41
  90. package/dist/runtime/run-store.d.ts +85 -7
  91. package/dist/runtime/run-store.js +2528 -186
  92. package/dist/runtime/skill-adapter.d.ts +18 -7
  93. package/dist/runtime/skill-adapter.js +100 -742
  94. package/dist/runtime/structured-output-validator.d.ts +44 -0
  95. package/dist/runtime/structured-output-validator.js +171 -0
  96. package/dist/runtime/verifier-command-binding.d.ts +22 -0
  97. package/dist/runtime/verifier-command-binding.js +162 -0
  98. package/dist/runtime/verifier.d.ts +46 -6
  99. package/dist/runtime/verifier.js +355 -53
  100. package/dist/runtime/windows-job-keeper.d.ts +47 -0
  101. package/dist/runtime/windows-job-keeper.js +231 -0
  102. package/dist/runtime/worker-executor.d.ts +17 -1
  103. package/dist/runtime/worker-executor.js +26 -5
  104. package/dist/runtime/workflow-plan-adapter.d.ts +19 -0
  105. package/dist/runtime/workflow-plan-adapter.js +502 -6
  106. package/dist/runtime/workflow-runtime-manifest.d.ts +1 -0
  107. package/dist/runtime/workflow-runtime-manifest.js +5 -0
  108. package/dist/runtime/workspace-isolation.d.ts +29 -2
  109. package/dist/runtime/workspace-isolation.js +488 -11
  110. package/dist/runtime/workspace-observation.d.ts +44 -0
  111. package/dist/runtime/workspace-observation.js +216 -0
  112. package/dist/schema/controller-registry.d.ts +0 -7
  113. package/dist/schema/controller-registry.js +0 -8
  114. package/dist/schema/types.d.ts +84 -10
  115. package/dist/schema/types.js +2 -0
  116. package/dist/schema/validate.js +151 -1
  117. package/dist/schema/validators/controllers.d.ts +1 -1
  118. package/dist/schema/validators/controllers.js +126 -41
  119. package/dist/schema/workflow-authoring-contract.js +2 -0
  120. package/dist/schema/yaml-parser.js +1 -1
  121. package/dist/templates/flutter-rewrite-new.dna.yaml +1 -0
  122. package/dist/templates/flutter-rewrite.dna.yaml +87 -60
  123. package/dist/templates/safe-refactoring.dna.yaml +1 -0
  124. package/native/windows-job-keeper/README.md +80 -0
  125. package/native/windows-job-keeper/bin/aarch64/intentdna-windows-job-keeper.exe +0 -0
  126. package/native/windows-job-keeper/bin/x86_64/intentdna-windows-job-keeper.exe +0 -0
  127. package/native/windows-job-keeper/keeper.c +979 -0
  128. package/native/windows-job-keeper/manifest.json +33 -0
  129. package/package.json +6 -2
  130. package/scripts/build-windows-job-keeper.mjs +172 -0
  131. package/scripts/verify-windows-job-keeper.mjs +184 -0
@@ -18,6 +18,8 @@ export interface ExpandDNAInputOptions {
18
18
  }
19
19
  export interface ExpandedDNAInput {
20
20
  files: string[];
21
+ dnas: IntentDNA[];
22
+ snapshot_bytes: Uint8Array[];
21
23
  provenance: DNASourceProvenance[];
22
24
  diagnostics: Diagnostic[];
23
25
  }
@@ -3,6 +3,7 @@ import { homedir } from "node:os";
3
3
  import { readdir, readFile, stat } from "node:fs/promises";
4
4
  import { dirname, resolve } from "node:path";
5
5
  import { fileURLToPath } from "node:url";
6
+ import { TextDecoder } from "node:util";
6
7
  import { validateDNA } from "../schema/validate.js";
7
8
  import { parseYAML } from "../schema/yaml-parser.js";
8
9
  import { DNADiagnosticsError, hasDiagnosticErrors } from "./diagnostics.js";
@@ -59,8 +60,8 @@ export async function detectDNAConfigs(projectDir = process.cwd()) {
59
60
  }
60
61
  return [];
61
62
  }
62
- function fingerprint(raw) {
63
- return `sha256:${createHash("sha256").update(raw, "utf-8").digest("hex")}`;
63
+ function fingerprint(snapshotBytes) {
64
+ return `sha256:${createHash("sha256").update(snapshotBytes).digest("hex")}`;
64
65
  }
65
66
  function enterprisePolicyCandidates(name, cwd, options) {
66
67
  const configuredDirs = [
@@ -103,8 +104,13 @@ function sourceTrustForInput(file, ref, cwd) {
103
104
  return "input";
104
105
  }
105
106
  async function readDNAWithRaw(path) {
106
- const raw = await readFile(path, "utf-8");
107
- return { raw, dna: parseDNAContent(path, raw) };
107
+ const snapshotBytes = await readFile(path);
108
+ const raw = new TextDecoder("utf-8", { fatal: true }).decode(snapshotBytes);
109
+ return {
110
+ raw,
111
+ dna: parseDNAContent(path, raw),
112
+ snapshot_bytes: Uint8Array.from(snapshotBytes),
113
+ };
108
114
  }
109
115
  export async function expandDNAInputs(files, options) {
110
116
  const cwd = options?.cwd ?? process.cwd();
@@ -112,6 +118,8 @@ export async function expandDNAInputs(files, options) {
112
118
  const seen = new Set();
113
119
  const diagnostics = [];
114
120
  const provenance = new Map();
121
+ const snapshots = new Map();
122
+ const snapshotBytes = new Map();
115
123
  const stack = [];
116
124
  const visit = async (file, ref, inheritedBy, trust) => {
117
125
  const abs = resolve(cwd, file);
@@ -169,11 +177,13 @@ export async function expandDNAInputs(files, options) {
169
177
  stack.pop();
170
178
  seen.add(abs);
171
179
  ordered.push(abs);
180
+ snapshots.set(abs, loaded.dna);
181
+ snapshotBytes.set(abs, loaded.snapshot_bytes);
172
182
  provenance.set(abs, {
173
183
  source_id: loaded.dna.id,
174
184
  source_ref: ref,
175
185
  resolved_path: abs,
176
- fingerprint: fingerprint(loaded.raw),
186
+ fingerprint: fingerprint(loaded.snapshot_bytes),
177
187
  type: loaded.dna.type,
178
188
  cascade_priority: loaded.dna.cascade.priority,
179
189
  version: loaded.dna.version,
@@ -188,7 +198,13 @@ export async function expandDNAInputs(files, options) {
188
198
  if (hasDiagnosticErrors(diagnostics)) {
189
199
  throw new DNADiagnosticsError("DNA inheritance resolution failed", diagnostics);
190
200
  }
191
- return { files: ordered, provenance: ordered.map((file) => provenance.get(file)).filter(Boolean), diagnostics };
201
+ return {
202
+ files: ordered,
203
+ dnas: ordered.map((file) => snapshots.get(file)).filter(Boolean),
204
+ snapshot_bytes: ordered.map((file) => snapshotBytes.get(file)).filter(Boolean),
205
+ provenance: ordered.map((file) => provenance.get(file)).filter(Boolean),
206
+ diagnostics,
207
+ };
192
208
  }
193
209
  export async function expandDNAInputFiles(files, options) {
194
210
  return (await expandDNAInputs(files, options)).files;
@@ -18,11 +18,18 @@ import type { ArtifactFact, ConstraintIR, RoleDef, VerifierSpec } from "../schem
18
18
  import type { HookEvent, HookOutput } from "./protocol.js";
19
19
  import { blockOutput } from "./protocol.js";
20
20
  import { readWorkflowState } from "./state.js";
21
+ import type { DNAWorkflowState } from "./state.js";
21
22
  import type { VerifierResultEntry } from "./state.js";
23
+ import type { HeartbeatRequest } from "../runtime/canonical-run-service.js";
22
24
  interface HookRuntimePaths {
23
25
  projectDir: string;
24
26
  irPath: string;
25
27
  }
28
+ interface HookRuntimeIdentity {
29
+ readonly host_session_id: string | undefined;
30
+ readonly worker_session_id: string | undefined;
31
+ }
32
+ declare function resolveHookRuntimeIdentity(event: HookEvent, input: Record<string, unknown>, rawInput: Record<string, unknown>): HookRuntimeIdentity;
26
33
  declare function resolveHookRuntimePaths(cwd: string, irPath?: string, explicitIRPath?: boolean): Promise<HookRuntimePaths>;
27
34
  export interface SessionSummary {
28
35
  total: number;
@@ -57,6 +64,40 @@ export declare function runHookEvent(options: RunHookEventOptions): Promise<Hook
57
64
  export declare function codexHookEventForInput(rawInput: Record<string, unknown>, args: string[]): unknown;
58
65
  declare function inferWorkflowAssetFromIr(ir: ConstraintIR, workflowId?: string): string | undefined;
59
66
  declare function runCodexHookWithLoadedIr(rawInput: Record<string, unknown>, projectDir: string, args: string[], ir: ConstraintIR): Promise<import("../runtime/codex-adapter.js").CodexHookRunnerResult>;
67
+ interface HookIRLoadOptions {
68
+ readonly projectDir: string;
69
+ readonly projectIRPath: string;
70
+ readonly sessionId?: string;
71
+ readonly trustedWorkerIdentity?: string;
72
+ }
73
+ interface HookIRLoadResult {
74
+ readonly ir: ConstraintIR | null;
75
+ readonly error?: string;
76
+ readonly source: "session" | "project" | "none";
77
+ }
78
+ interface CanonicalHeartbeatPort {
79
+ heartbeat(request: HeartbeatRequest): Promise<unknown>;
80
+ }
81
+ type CanonicalHeartbeatOutcome = {
82
+ readonly kind: "renewed";
83
+ } | {
84
+ readonly kind: "not_canonical";
85
+ } | {
86
+ readonly kind: "no_projection";
87
+ } | {
88
+ readonly kind: "projection_unreadable";
89
+ readonly message: string;
90
+ } | {
91
+ readonly kind: "mismatched_projection";
92
+ } | {
93
+ readonly kind: "legacy_projection";
94
+ } | {
95
+ readonly kind: "service_rejected";
96
+ readonly message: string;
97
+ };
98
+ declare function heartbeatCanonicalAttempt(projectDir: string, state: DNAWorkflowState | null, service?: CanonicalHeartbeatPort): Promise<CanonicalHeartbeatOutcome>;
99
+ declare function recordCanonicalHeartbeatOutcome(projectDir: string, outcome: CanonicalHeartbeatOutcome, event: string, state: DNAWorkflowState | null, sessionId?: string): Promise<void>;
100
+ declare function loadHookIR(options: HookIRLoadOptions): Promise<HookIRLoadResult>;
60
101
  declare function recordProducedArtifacts(projectDir: string, ir: ConstraintIR, wfState: {
61
102
  workflow: string;
62
103
  current_step: string;
@@ -97,6 +138,10 @@ export declare const resolveWorkflowArtifactFactsForTest: typeof resolveWorkflow
97
138
  export declare const readWorkflowStateForHookTest: typeof readWorkflowStateForHook;
98
139
  export declare const runCodexHookWithLoadedIrForTest: typeof runCodexHookWithLoadedIr;
99
140
  export declare const resolveHookRuntimePathsForTest: typeof resolveHookRuntimePaths;
141
+ export declare const resolveHookRuntimeIdentityForTest: typeof resolveHookRuntimeIdentity;
142
+ export declare const loadHookIRForTest: typeof loadHookIR;
143
+ export declare const heartbeatCanonicalAttemptForTest: typeof heartbeatCanonicalAttempt;
144
+ export declare const recordCanonicalHeartbeatOutcomeForTest: typeof recordCanonicalHeartbeatOutcome;
100
145
  export declare const inferWorkflowAssetFromIrForTest: typeof inferWorkflowAssetFromIr;
101
146
  export declare const resolveWorkflowArtifactFactsForHookTest: typeof resolveWorkflowArtifactFactsForHook;
102
147
  export declare const finalizeAndResolveArtifactsForHookTest: typeof finalizeAndResolveArtifactsForHook;
package/dist/hooks/cli.js CHANGED
@@ -21,18 +21,52 @@ import { dirname, isAbsolute, join, relative, resolve } from "node:path";
21
21
  import { createHash, randomUUID } from "node:crypto";
22
22
  import { fileURLToPath } from "node:url";
23
23
  import { readStdinResult, writeOutput, silentOutput, allowOutput, blockOutput, stopOutput } from "./protocol.js";
24
- import { validateHookInput } from "./schema.js";
24
+ import { isCurrentClaudeSubagentStopInput, validateHookInput } from "./schema.js";
25
25
  import { hookFailureOutput } from "./enforcement-boundary.js";
26
26
  import { enforcePreToolUse, enforcePostToolUse, enforceUserPromptSubmit, enforceSubagentStop, enforcePreCompact, enforceNotification, enforceSessionStart, enforceStop, extractBashWritePaths, checkReflectionLimit, checkContextReadiness, checkWorkflowBoundary, } from "./enforce.js";
27
- import { appendAudit, readWorkflowState, appendTrace, appendRuntimeDecisionEvent, appendEvidenceCaptureEvent, rotateTraces, readTraces, cleanStaleState, readSurgeonAttempts, updateSurgeonAttempts, appendSessionRead, readSessionReads, appendVerifierResult, readVerifierResults, appendCompletedArtifact, artifactIdentity, buildArtifactKey, readArtifactManifest, resolveArtifactTemplate, safePathComponent, workflowTemplateVariables, writeArtifactManifest, EVIDENCE_CAPTURE_SCHEMA_VERSION } from "./state.js";
27
+ import { appendAudit, readWorkflowState, appendTrace, appendRuntimeDecisionEvent, appendEvidenceCaptureEvent, rotateTraces, readTraces, cleanStaleState, readSurgeonAttempts, updateSurgeonAttempts, appendSessionRead, readSessionReads, appendVerifierResult, readVerifierResults, appendCompletedArtifact, artifactIdentity, buildArtifactKey, readArtifactManifest, resolveArtifactTemplate, resolveSessionCompiledIRPath, safePathComponent, workflowTemplateVariables, writeArtifactManifest, EVIDENCE_CAPTURE_SCHEMA_VERSION } from "./state.js";
28
28
  import { hookEventsForSurface } from "./event-registry.js";
29
29
  import { writeAuditEvent } from "../audit/index.js";
30
30
  import { RUNTIME_DECISION_EVENT_SCHEMA_VERSION, trustedEvidenceCaptureAttribution } from "../governance/index.js";
31
31
  import { runCodexHook } from "../runtime/codex-adapter.js";
32
+ import { createCanonicalRuntimeComposition } from "../runtime/canonical-runtime-composition.js";
33
+ import { buildClaudeSubagentStopHostEvent, encodeClaudeHostEventForStderr, } from "./host-event.js";
32
34
  import { runCheckpointVerifier as runSharedCheckpointVerifier, runCompletionVerifier, toProjectRelativePath, } from "../runtime/verifier.js";
33
35
  // ── Constants ──────────────────────────────────────────────
34
36
  const DEFAULT_IR_PATH = ".dna/compiled/ir.json";
35
37
  const VALID_EVENTS = new Set(hookEventsForSurface("cli"));
38
+ function resolveHookRuntimeIdentity(event, input, rawInput) {
39
+ const hostSessionId = typeof rawInput.session_id === "string"
40
+ ? rawInput.session_id
41
+ : typeof rawInput.sessionId === "string"
42
+ ? rawInput.sessionId
43
+ : typeof input.sessionId === "string"
44
+ ? input.sessionId
45
+ : undefined;
46
+ const agentId = typeof input.agent_id === "string" && input.agent_id.trim().length > 0
47
+ ? input.agent_id
48
+ : undefined;
49
+ const suppliedWorkerIdentity = typeof rawInput.worker_session_id === "string"
50
+ && rawInput.worker_session_id.trim().length > 0
51
+ ? rawInput.worker_session_id
52
+ : undefined;
53
+ const useHostAgentIdentity = event === "SubagentStop"
54
+ && agentId !== undefined
55
+ && isCurrentClaudeSubagentStopInput(rawInput);
56
+ return {
57
+ host_session_id: hostSessionId,
58
+ worker_session_id: useHostAgentIdentity
59
+ ? agentId
60
+ : suppliedWorkerIdentity ?? (typeof input.sessionId === "string" ? input.sessionId : hostSessionId),
61
+ };
62
+ }
63
+ function isCanonicalWorkflowState(state) {
64
+ return Boolean(state?.active
65
+ && state.worker_session_id
66
+ && state.run_id
67
+ && state.step_id
68
+ && state.attempt_id);
69
+ }
36
70
  function workflowRuntimeRevisionMismatch(ir, state) {
37
71
  if (!state?.active || state.resolved_variables_source !== "compiled_manifest")
38
72
  return null;
@@ -391,15 +425,34 @@ async function main() {
391
425
  const cwd = typeof rawInput.cwd === "string" ? rawInput.cwd : process.cwd();
392
426
  const runtimePaths = await resolveHookRuntimePaths(cwd, irPath, explicitIRPath);
393
427
  const { projectDir } = runtimePaths;
394
- const sessionId = typeof rawInput.sessionId === "string" ? rawInput.sessionId : undefined;
395
- // Load compiled IR
428
+ const identity = resolveHookRuntimeIdentity(event, rawInput, rawStdin);
429
+ // Canonical attempts are pinned to an attempt-local projection. Legacy hooks
430
+ // keep the project-level compiled IR compatibility path.
396
431
  const resolvedIRPath = runtimePaths.irPath;
397
- const ir = await loadIR(resolvedIRPath);
398
- if (!ir) {
399
- writeOutput(hookFailureOutput(event, "missing_ir", `Compiled IR is missing, unreadable, invalid, or version-incompatible at ${resolvedIRPath}.`));
432
+ const hookStateIdentity = identity.worker_session_id;
433
+ const loaded = await loadHookIR({
434
+ projectDir,
435
+ projectIRPath: resolvedIRPath,
436
+ sessionId: hookStateIdentity,
437
+ trustedWorkerIdentity: hookStateIdentity,
438
+ });
439
+ if (!loaded.ir) {
440
+ writeOutput(hookFailureOutput(event, "missing_ir", loaded.error ?? `Compiled IR is missing, unreadable, invalid, or version-incompatible at ${resolvedIRPath}.`));
400
441
  return;
401
442
  }
402
- const output = await runHookEvent({ event, ir, rawInput, projectDir, sessionId });
443
+ const output = await runHookEvent({
444
+ event,
445
+ ir: loaded.ir,
446
+ rawInput,
447
+ projectDir,
448
+ sessionId: hookStateIdentity,
449
+ });
450
+ if (event === "SubagentStop" && output.continue !== false && hookStateIdentity) {
451
+ const state = await readWorkflowState(projectDir, hookStateIdentity);
452
+ const hostEvent = buildClaudeSubagentStopHostEvent(rawStdin, state);
453
+ if (hostEvent)
454
+ process.stderr.write(encodeClaudeHostEventForStderr(hostEvent));
455
+ }
403
456
  writeOutput(output);
404
457
  }
405
458
  export function normalizeHookOutputForEvent(output, event) {
@@ -485,6 +538,10 @@ export async function runHookEvent(options) {
485
538
  && wfStateRaw.attempt_id),
486
539
  };
487
540
  }
541
+ if (event === "PreToolUse" || event === "PostToolUse") {
542
+ const heartbeatOutcome = await heartbeatCanonicalAttempt(projectDir, wfStateRaw);
543
+ await recordCanonicalHeartbeatOutcome(projectDir, heartbeatOutcome, event, wfStateRaw, sessionId);
544
+ }
488
545
  }
489
546
  if (event === "PreToolUse" && wfStateRaw?.active) {
490
547
  try {
@@ -824,10 +881,11 @@ function inferWorkflowAssetFromIr(ir, workflowId) {
824
881
  function agentTypeForWorkflowRole(role) {
825
882
  if (!role)
826
883
  return undefined;
827
- return `dna-${role.replace(/([a-z])([A-Z])/g, "$1-$2").replace(/[_\s]+/g, "-").toLowerCase()}`;
884
+ return `dna-${toKebabCase(role)}`;
828
885
  }
829
886
  function subagentMatchesWorkflowRole(rawInput, wfState) {
830
- return typeof rawInput.agent_type === "string" && rawInput.agent_type === agentTypeForWorkflowRole(wfState.current_role);
887
+ return typeof rawInput.agent_type === "string"
888
+ && rawInput.agent_type === agentTypeForWorkflowRole(wfState.current_role);
831
889
  }
832
890
  async function runCodexHookWithLoadedIr(rawInput, projectDir, args, ir) {
833
891
  const sessionId = typeof rawInput.worker_session_id === "string"
@@ -838,8 +896,13 @@ async function runCodexHookWithLoadedIr(rawInput, projectDir, args, ir) {
838
896
  ? rawInput.sessionId
839
897
  : undefined;
840
898
  const workflowState = await readWorkerWorkflowState(projectDir, sessionId, typeof rawInput.worker_session_id !== "string");
899
+ const hookEvent = codexHookEventForInput(rawInput, args);
900
+ if (hookEvent === "PreToolUse" || hookEvent === "PostToolUse") {
901
+ const heartbeatOutcome = await heartbeatCanonicalAttempt(projectDir, workflowState);
902
+ await recordCanonicalHeartbeatOutcome(projectDir, heartbeatOutcome, typeof hookEvent === "string" ? hookEvent : "CodexHook", workflowState, sessionId);
903
+ }
841
904
  const workflowAsset = inferWorkflowAssetFromIr(ir, workflowState?.workflow);
842
- return runCodexHook({ ...rawInput, event: codexHookEventForInput(rawInput, args) }, {
905
+ return runCodexHook({ ...rawInput, event: hookEvent }, {
843
906
  ir,
844
907
  projectDir,
845
908
  hookInstallMode: codexInstallMode(argValue(args, "--codex-install-mode")),
@@ -868,12 +931,23 @@ async function runCodexHookCli(args) {
868
931
  const explicitIRPath = argValue(args, "--ir");
869
932
  const runtimePaths = await resolveHookRuntimePaths(cwd, explicitIRPath ?? DEFAULT_IR_PATH, explicitIRPath !== undefined);
870
933
  const { projectDir, irPath } = runtimePaths;
871
- const ir = await loadIR(irPath);
872
- if (!ir) {
873
- writeOutput(hookFailureOutput(policyEvent, "missing_ir", `Compiled IR is missing, unreadable, invalid, or version-incompatible at ${irPath}.`));
934
+ const workerSessionId = typeof rawInput.worker_session_id === "string"
935
+ ? rawInput.worker_session_id
936
+ : undefined;
937
+ const claimedSessionId = workerSessionId
938
+ ?? (typeof rawInput.session_id === "string" ? rawInput.session_id : undefined)
939
+ ?? (typeof rawInput.sessionId === "string" ? rawInput.sessionId : undefined);
940
+ const loaded = await loadHookIR({
941
+ projectDir,
942
+ projectIRPath: irPath,
943
+ sessionId: claimedSessionId,
944
+ trustedWorkerIdentity: workerSessionId,
945
+ });
946
+ if (!loaded.ir) {
947
+ writeOutput(hookFailureOutput(policyEvent, "missing_ir", loaded.error ?? `Compiled IR is missing, unreadable, invalid, or version-incompatible at ${irPath}.`));
874
948
  return;
875
949
  }
876
- const result = await runCodexHookWithLoadedIr(rawInput, projectDir, args, ir);
950
+ const result = await runCodexHookWithLoadedIr(rawInput, projectDir, args, loaded.ir);
877
951
  writeOutput(result.output);
878
952
  }
879
953
  catch (err) {
@@ -885,26 +959,180 @@ async function runCodexHookCli(args) {
885
959
  // ── IR Loading ─────────────────────────────────────────────
886
960
  /** Expected IR schema version — must match plugin-adapter.ts */
887
961
  const EXPECTED_IR_VERSION = 1;
888
- async function loadIR(irPath) {
962
+ function canonicalProjectionMismatch(projection, state) {
963
+ if (projection.schema_version !== "intentdna.canonical_hook_projection.v1"
964
+ && projection.schema_version !== "intentdna.canonical_hook_projection.v2") {
965
+ return "Canonical session IR has an unsupported schema version.";
966
+ }
967
+ for (const [label, actual, expected] of [
968
+ ["run", projection.run_id, state.run_id],
969
+ ["step", projection.step_id, state.step_id],
970
+ ["attempt", projection.attempt_id, state.attempt_id],
971
+ ["worker session", projection.worker_session_id, state.worker_session_id],
972
+ ]) {
973
+ if (actual !== expected)
974
+ return `Canonical session IR ${label} identity does not match workflow state.`;
975
+ }
976
+ const compiled = projection.compiled_ir;
977
+ if (!projection.compiled_ir_hash
978
+ || compiled?.compiled_ir_hash !== projection.compiled_ir_hash
979
+ || compiled?.ir?.compiled_ir_hash !== projection.compiled_ir_hash
980
+ || compiled?.workflow_runtime?.compiled_ir_hash !== projection.compiled_ir_hash) {
981
+ return "Canonical session IR hash does not match its compiled projection.";
982
+ }
983
+ if (state.workflow_runtime_compiled_ir_hash !== projection.compiled_ir_hash) {
984
+ return "Canonical session IR hash does not match workflow state.";
985
+ }
986
+ return null;
987
+ }
988
+ function canonicalHeartbeatService(projectDir) {
989
+ return createCanonicalRuntimeComposition({
990
+ project_directory: projectDir,
991
+ }).service;
992
+ }
993
+ async function heartbeatCanonicalAttempt(projectDir, state, service) {
994
+ if (!isCanonicalWorkflowState(state))
995
+ return { kind: "not_canonical" };
996
+ let projection;
889
997
  try {
890
- const raw = await readFile(irPath, "utf-8");
891
- const data = JSON.parse(raw);
892
- // Support both raw IR and wrapped CompiledIRFile format
893
- if (data.ir_version && data.ir) {
894
- if (data.ir_version !== EXPECTED_IR_VERSION) {
895
- process.stderr.write(`\n Intent DNA: IR version mismatch (v${data.ir_version} on disk, v${EXPECTED_IR_VERSION} expected)\n` +
896
- ` Fix: run \`dna sync\` to recompile\n\n`);
897
- return null;
898
- }
899
- return data.ir;
998
+ const projectionPath = resolveSessionCompiledIRPath(projectDir, state.worker_session_id);
999
+ projection = JSON.parse(await readFile(projectionPath, "utf-8"));
1000
+ }
1001
+ catch (error) {
1002
+ const code = error instanceof Error && "code" in error
1003
+ ? error.code
1004
+ : undefined;
1005
+ if (code === "ENOENT")
1006
+ return { kind: "no_projection" };
1007
+ return {
1008
+ kind: "projection_unreadable",
1009
+ message: error instanceof Error ? error.message : String(error),
1010
+ };
1011
+ }
1012
+ if (canonicalProjectionMismatch(projection, state))
1013
+ return { kind: "mismatched_projection" };
1014
+ if (projection.schema_version !== "intentdna.canonical_hook_projection.v2") {
1015
+ return { kind: "legacy_projection" };
1016
+ }
1017
+ try {
1018
+ await (service ?? canonicalHeartbeatService(projectDir)).heartbeat({
1019
+ lease_token: projection.lease_token,
1020
+ owner_id: projection.owner_id,
1021
+ });
1022
+ return { kind: "renewed" };
1023
+ }
1024
+ catch (error) {
1025
+ return {
1026
+ kind: "service_rejected",
1027
+ message: error instanceof Error ? error.message : String(error),
1028
+ };
1029
+ }
1030
+ }
1031
+ async function recordCanonicalHeartbeatOutcome(projectDir, outcome, event, state, sessionId) {
1032
+ if (outcome.kind === "renewed" || outcome.kind === "not_canonical")
1033
+ return;
1034
+ const detail = outcome.kind === "service_rejected" || outcome.kind === "projection_unreadable"
1035
+ ? `: ${outcome.message}`
1036
+ : "";
1037
+ await appendTrace(projectDir, {
1038
+ trace_id: randomUUID(),
1039
+ event,
1040
+ workflow: state?.workflow,
1041
+ step: state?.current_step,
1042
+ decision: "warn",
1043
+ reason: `canonical heartbeat ${outcome.kind}${detail}`,
1044
+ duration_ms: 0,
1045
+ timestamp: new Date().toISOString(),
1046
+ }, sessionId);
1047
+ }
1048
+ async function loadHookIR(options) {
1049
+ let projectionRaw = null;
1050
+ let projectionPath = null;
1051
+ if (options.sessionId) {
1052
+ try {
1053
+ projectionPath = resolveSessionCompiledIRPath(options.projectDir, options.sessionId);
1054
+ projectionRaw = await readFile(projectionPath, "utf-8");
900
1055
  }
901
- // Direct ConstraintIR
902
- if (data.compiled_at && data.source_dna_ids) {
903
- return data;
1056
+ catch (error) {
1057
+ const code = error instanceof Error && "code" in error
1058
+ ? error.code
1059
+ : undefined;
1060
+ if (code !== "ENOENT") {
1061
+ return {
1062
+ ir: null,
1063
+ source: "none",
1064
+ error: `Canonical session IR is unreadable at ${projectionPath ?? "unknown"}: ${error instanceof Error ? error.message : String(error)}`,
1065
+ };
1066
+ }
904
1067
  }
905
- process.stderr.write(`\n Intent DNA: unrecognized IR format at ${irPath}\n` +
906
- ` Fix: run \`dna sync\` to regenerate\n\n`);
1068
+ }
1069
+ let state = null;
1070
+ try {
1071
+ state = options.sessionId
1072
+ ? await readWorkflowState(options.projectDir, options.sessionId)
1073
+ : null;
1074
+ }
1075
+ catch (error) {
1076
+ return { ir: null, source: "none", error: `Hook session identity is invalid: ${error instanceof Error ? error.message : String(error)}` };
1077
+ }
1078
+ if (!isCanonicalWorkflowState(state) && projectionRaw === null) {
1079
+ return { ir: await loadIR(options.projectIRPath), source: "project" };
1080
+ }
1081
+ if (!isCanonicalWorkflowState(state)) {
1082
+ return {
1083
+ ir: null,
1084
+ source: "none",
1085
+ error: "Canonical Hook workflow state is missing, unreadable, or incomplete; project/root IR fallback is forbidden.",
1086
+ };
1087
+ }
1088
+ if (!options.trustedWorkerIdentity || options.trustedWorkerIdentity !== state.worker_session_id) {
1089
+ return {
1090
+ ir: null,
1091
+ source: "none",
1092
+ error: "Canonical Hook execution requires a trusted worker_session_id; project/root IR fallback is forbidden.",
1093
+ };
1094
+ }
1095
+ projectionPath ??= resolveSessionCompiledIRPath(options.projectDir, state.worker_session_id);
1096
+ try {
1097
+ const projection = JSON.parse(projectionRaw ?? await readFile(projectionPath, "utf-8"));
1098
+ const mismatch = canonicalProjectionMismatch(projection, state);
1099
+ if (mismatch)
1100
+ return { ir: null, source: "none", error: mismatch };
1101
+ const ir = await parseIR(projection.compiled_ir, projectionPath);
1102
+ return ir
1103
+ ? { ir, source: "session" }
1104
+ : { ir: null, source: "none", error: `Canonical session IR is invalid at ${projectionPath}.` };
1105
+ }
1106
+ catch (error) {
1107
+ return {
1108
+ ir: null,
1109
+ source: "none",
1110
+ error: `Canonical session IR is missing, unreadable, or malformed at ${projectionPath}: ${error instanceof Error ? error.message : String(error)}`,
1111
+ };
1112
+ }
1113
+ }
1114
+ async function parseIR(data, irPath) {
1115
+ if (!data || typeof data !== "object")
907
1116
  return null;
1117
+ const record = data;
1118
+ if (record.ir_version && record.ir) {
1119
+ if (record.ir_version !== EXPECTED_IR_VERSION) {
1120
+ process.stderr.write(`\n Intent DNA: IR version mismatch (v${record.ir_version} on disk, v${EXPECTED_IR_VERSION} expected)\n` +
1121
+ ` Fix: run \`dna sync\` to recompile\n\n`);
1122
+ return null;
1123
+ }
1124
+ return record.ir;
1125
+ }
1126
+ if (record.compiled_at && record.source_dna_ids)
1127
+ return record;
1128
+ process.stderr.write(`\n Intent DNA: unrecognized IR format at ${irPath}\n` +
1129
+ ` Fix: run \`dna sync\` to regenerate\n\n`);
1130
+ return null;
1131
+ }
1132
+ async function loadIR(irPath) {
1133
+ try {
1134
+ const raw = await readFile(irPath, "utf-8");
1135
+ return parseIR(JSON.parse(raw), irPath);
908
1136
  }
909
1137
  catch (err) {
910
1138
  // U3: Human-readable error instead of silent failure
@@ -1122,6 +1350,10 @@ export const resolveWorkflowArtifactFactsForTest = resolveWorkflowArtifactFacts;
1122
1350
  export const readWorkflowStateForHookTest = readWorkflowStateForHook;
1123
1351
  export const runCodexHookWithLoadedIrForTest = runCodexHookWithLoadedIr;
1124
1352
  export const resolveHookRuntimePathsForTest = resolveHookRuntimePaths;
1353
+ export const resolveHookRuntimeIdentityForTest = resolveHookRuntimeIdentity;
1354
+ export const loadHookIRForTest = loadHookIR;
1355
+ export const heartbeatCanonicalAttemptForTest = heartbeatCanonicalAttempt;
1356
+ export const recordCanonicalHeartbeatOutcomeForTest = recordCanonicalHeartbeatOutcome;
1125
1357
  export const inferWorkflowAssetFromIrForTest = inferWorkflowAssetFromIr;
1126
1358
  export const resolveWorkflowArtifactFactsForHookTest = resolveWorkflowArtifactFactsForHook;
1127
1359
  export const finalizeAndResolveArtifactsForHookTest = finalizeAndResolveArtifactsForHook;
@@ -0,0 +1,57 @@
1
+ import type { DNAWorkflowState } from "./state.js";
2
+ export declare const CLAUDE_HOST_EVENT_SCHEMA_VERSION: "intentdna.claude_host_event.v1";
3
+ export declare const CLAUDE_HOST_EVENT_STDERR_PREFIX = "INTENTDNA_CLAUDE_HOST_EVENT_V1 ";
4
+ export interface ClaudeSubagentStopHostEvent {
5
+ readonly schema_version: typeof CLAUDE_HOST_EVENT_SCHEMA_VERSION;
6
+ readonly hook_event_name: "SubagentStop";
7
+ readonly session_id: string;
8
+ readonly transcript_path: string;
9
+ readonly agent_id: string;
10
+ readonly agent_type: string;
11
+ readonly agent_transcript_path: string;
12
+ readonly last_assistant_message_digest: `sha256:${string}`;
13
+ readonly run_id: string;
14
+ readonly step_id: string;
15
+ readonly attempt_id: string;
16
+ readonly worker_session_id: string;
17
+ }
18
+ export interface ClaudeHookResponseFrame {
19
+ readonly type: "system";
20
+ readonly subtype: "hook_response";
21
+ readonly hook_id: string;
22
+ readonly hook_event: "SubagentStop";
23
+ readonly session_id: string;
24
+ readonly uuid: string;
25
+ readonly outcome: "success";
26
+ readonly exit_code: 0;
27
+ readonly stderr: string;
28
+ }
29
+ export interface VerifiedClaudeSubagentStopHostEvent {
30
+ readonly hook_id: string;
31
+ readonly frame_uuid: string;
32
+ readonly event: ClaudeSubagentStopHostEvent;
33
+ }
34
+ export interface ClaudeSubagentStopHostEventExpectation {
35
+ readonly hook_id: string;
36
+ readonly session_id: string;
37
+ readonly run_id: string;
38
+ readonly step_id: string;
39
+ readonly attempt_id: string;
40
+ readonly worker_session_id: string;
41
+ }
42
+ export interface ClaudeHostEventConsumptionKey {
43
+ readonly session_id: string;
44
+ readonly frame_uuid: string;
45
+ readonly hook_id: string;
46
+ }
47
+ export interface ClaudeHostEventConsumptionStore {
48
+ consume(key: ClaudeHostEventConsumptionKey): Promise<boolean>;
49
+ }
50
+ export type ClaudeHostEventErrorCode = "invalid_frame" | "missing_envelope" | "duplicate_envelope" | "malformed_envelope" | "identity_mismatch" | "context_mismatch" | "replayed_frame";
51
+ export declare class ClaudeHostEventError extends Error {
52
+ readonly code: ClaudeHostEventErrorCode;
53
+ constructor(code: ClaudeHostEventErrorCode, message: string);
54
+ }
55
+ export declare function buildClaudeSubagentStopHostEvent(input: Record<string, unknown>, state: DNAWorkflowState | null): ClaudeSubagentStopHostEvent | null;
56
+ export declare function encodeClaudeHostEventForStderr(event: ClaudeSubagentStopHostEvent): string;
57
+ export declare function parseClaudeSubagentStopHookResponse(frame: unknown, expected: ClaudeSubagentStopHostEventExpectation, consumptionStore: ClaudeHostEventConsumptionStore): Promise<VerifiedClaudeSubagentStopHostEvent>;