intentdna 1.8.6 → 1.8.7

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 (60) hide show
  1. package/.claude-plugin/marketplace.json +2 -2
  2. package/.claude-plugin/plugin.json +1 -1
  3. package/README.md +1 -0
  4. package/dist/cli/commands/run-lifecycle.d.ts +73 -0
  5. package/dist/cli/commands/run-lifecycle.js +240 -0
  6. package/dist/cli/commands/run.d.ts +22 -40
  7. package/dist/cli/commands/run.js +674 -392
  8. package/dist/cli/index.js +87 -2
  9. package/dist/compiler/workflow.js +3 -2
  10. package/dist/hooks/cli.d.ts +1 -2
  11. package/dist/hooks/cli.js +119 -80
  12. package/dist/hooks/enforce.d.ts +2 -0
  13. package/dist/hooks/enforce.js +56 -27
  14. package/dist/hooks/enforcement-boundary.d.ts +13 -0
  15. package/dist/hooks/enforcement-boundary.js +33 -0
  16. package/dist/hooks/index.d.ts +3 -2
  17. package/dist/hooks/index.js +3 -2
  18. package/dist/hooks/protocol.d.ts +12 -4
  19. package/dist/hooks/protocol.js +20 -14
  20. package/dist/hooks/schema.d.ts +2 -1
  21. package/dist/hooks/schema.js +6 -2
  22. package/dist/hooks/state-manager.d.ts +5 -5
  23. package/dist/hooks/state-manager.js +26 -24
  24. package/dist/hooks/state.d.ts +19 -3
  25. package/dist/hooks/state.js +327 -80
  26. package/dist/mcp/index.js +0 -0
  27. package/dist/runtime/diagnosis-contract-verifier.d.ts +11 -0
  28. package/dist/runtime/diagnosis-contract-verifier.js +417 -0
  29. package/dist/runtime/execution-provider.d.ts +40 -0
  30. package/dist/runtime/execution-provider.js +138 -0
  31. package/dist/runtime/handoff-resolver.d.ts +61 -0
  32. package/dist/runtime/handoff-resolver.js +167 -0
  33. package/dist/runtime/index.d.ts +24 -0
  34. package/dist/runtime/index.js +13 -0
  35. package/dist/runtime/process-tree.d.ts +47 -0
  36. package/dist/runtime/process-tree.js +402 -0
  37. package/dist/runtime/providers/claude.d.ts +9 -0
  38. package/dist/runtime/providers/claude.js +64 -0
  39. package/dist/runtime/providers/codex.d.ts +8 -0
  40. package/dist/runtime/providers/codex.js +72 -0
  41. package/dist/runtime/result-store.d.ts +32 -0
  42. package/dist/runtime/result-store.js +130 -0
  43. package/dist/runtime/run-contracts.d.ts +290 -0
  44. package/dist/runtime/run-contracts.js +58 -0
  45. package/dist/runtime/run-controller.d.ts +149 -0
  46. package/dist/runtime/run-controller.js +1108 -0
  47. package/dist/runtime/run-store.d.ts +96 -0
  48. package/dist/runtime/run-store.js +725 -0
  49. package/dist/runtime/worker-executor.d.ts +19 -0
  50. package/dist/runtime/worker-executor.js +194 -0
  51. package/dist/runtime/workflow-plan-adapter.d.ts +26 -0
  52. package/dist/runtime/workflow-plan-adapter.js +416 -0
  53. package/dist/runtime/workflow-runner.d.ts +15 -3
  54. package/dist/runtime/workflow-runner.js +13 -1
  55. package/dist/runtime/workspace-isolation.d.ts +103 -0
  56. package/dist/runtime/workspace-isolation.js +373 -0
  57. package/dist/schema/types.d.ts +1 -0
  58. package/dist/schema/validate.js +64 -6
  59. package/dist/schema/yaml-parser.js +7 -2
  60. package/package.json +1 -1
package/dist/cli/index.js CHANGED
@@ -25,7 +25,7 @@ Commands:
25
25
  guard Zero-config guardrails — detect environment, apply safety rules
26
26
  sync Compile + inject DNA into target file (CLAUDE.md, SOUL.md, etc.)
27
27
  verify Verify synced files match .dna/lock checksums (drift detection)
28
- run Execute a workflow directly (TypeScript runtime)
28
+ run Execute and inspect a durable Controller workflow run
29
29
  init Create a new DNA file interactively
30
30
  generate Generate DNA config from natural language description
31
31
  compile Compile DNA files to framework configuration
@@ -138,6 +138,10 @@ Examples:
138
138
  dna import . Import existing harness configs into DNA format
139
139
  dna run --dna my.dna.json --workflow dev-pipeline --task P5.8
140
140
  dna run --dna my.dna.json --workflow dev-pipeline --task P5.8 --dry-run
141
+ dna run status <run-id> --json
142
+ dna run inspect <run-id>
143
+ dna run resume <run-id>
144
+ dna run cancel <run-id> --reason "operator request"
141
145
  dna compile my.dna.json --context work
142
146
  dna show my.dna.json --context work
143
147
  dna evolve my_dna_id Preview marker changes without persisting
@@ -229,7 +233,35 @@ Boundaries:
229
233
  guide: `Usage: dna guide [asset] [--asset <name>] [--name <org>] [--project <project>] [--goal <workflow>] [--sync-target <target>] [--brief] [--json]\n\nShows the current workflow governance route: organization setup, external-agent authoring/validated asset editing, asset governance, runtime sync, evidence review, and manager review.\n\nOptions:\n --asset <name> Focus the guide on a workflow asset (default prefers ${WORKFLOW_ASSET_IDS.default})\n --name <org> Organization/workspace name to use in suggested setup commands\n --project <project> Project name to use in suggested commands\n --goal <workflow> Natural-language workflow goal for intake/authoring-packet commands\n --sync-target <target> Runtime surface such as codex or claude for suggested commands\n --project-dir <path> Read organization state and project-local assets from another project directory\n --brief Print the current stage, current command, checkpoints, and near next steps only\n --json Print JSON to stdout\n\nBoundaries:\n - dna CLI/core/runtime owns product behavior and governance decisions\n - Claude Code and Codex are runtime surfaces/adapters\n - Dashboard is read-only projection\n - Obsidian/wiki is internal project memory, not product runtime\n - Web/public surface changes require Vercel CLI deploy + deployed URL verification\n`,
230
234
  quickstart: `Usage: dna quickstart [asset] [--asset <name>] [--sync-target <target>] [--project-dir <path>] [--json]\n\nShows the shortest local route for getting value without Online or Supabase: choose a workflow asset, initialize local dna, preview sync, sync the harness, run in the harness, package/sign/verify local evidence, then inspect sync status.\n\nOptions:\n --asset <name> Workflow asset to start from (default prefers ${WORKFLOW_ASSET_IDS.default})\n --sync-target <target> Runtime surface such as codex or claude for suggested commands\n --project-dir <path> Read organization state and project-local assets from another project directory\n --json Print a concise intentdna.local_quickstart.v1 packet\n\nBoundaries:\n - quickstart is read-only guidance; it does not initialize, sync, run workflows, sign reports, upload reports, or deploy anything\n - local dna/dna-hook remain runtime truth\n - Online, marketplace, and Supabase do not execute workflows or make allow/warn/block/validate decisions\n - MCP remains hidden transport\n`,
231
235
  context: `Usage: dna context <index|search|doc> [query|id] [files...]\n\nSubcommands:\n index List explicitly declared planning-only advisory context sources\n search <query> Search source metadata\n doc <id> Read a local file/doc source body\n`,
232
- run: `Usage: dna run --dna <file> --workflow <name> --task <id> [options]\n\nOptions:\n --dry-run Print execution plan only\n --permission-mode <mode> Claude permission mode (default: default)\n --project-dir <path> Run and write project-scoped agents, IR, state, and logs in another project directory\n`,
236
+ run: `Usage:
237
+ dna run [start] --dna <file> --workflow <name> --task <id> [options]
238
+ dna run status <run-id> [--json] [--project-dir <path>]
239
+ dna run inspect <run-id> [--json] [--project-dir <path>]
240
+ dna run resume <run-id> [--json] [--project-dir <path>]
241
+ dna run cancel <run-id> [--reason <text>] [--json] [--project-dir <path>]
242
+
243
+ Actions:
244
+ start Create a durable run and execute until terminal (default)
245
+ status Read the durable run summary
246
+ inspect Read attempts, worker sessions, handoffs, events, and results
247
+ resume Resume from durable task/attempt/result state
248
+ cancel Persist durable cancellation intent
249
+
250
+ Options:
251
+ --run-id <id> Explicit run id; lifecycle actions also accept a positional id
252
+ --dry-run Print the compiled Controller plan without starting a run
253
+ --provider <name> claude|codex (default: claude)
254
+ --provider-executable <path> Override the selected provider executable
255
+ --max-concurrency <n> Maximum simultaneously active Steps (default: 1)
256
+ --timeout-ms <n> Per-attempt timeout in milliseconds
257
+ --cancellation-grace-ms <n> Process-tree cancellation grace period (default: 5000)
258
+ --max-budget <usd> Claude budget per Step attempt (default: 5)
259
+ --permission-mode <mode> Claude permission mode (default: default)
260
+ --agents <dir> Project agent output directory (default: .claude/agents)
261
+ --reason <text> Cancellation reason
262
+ --json Print lifecycle output as JSON
263
+ --project-dir <path> Read/write the project-scoped runtime in another directory
264
+ `,
233
265
  compile: `Usage: dna compile <files...> [--target claude-sdk] [--context <name>] [--output <path>] [--ir]\n`,
234
266
  evolve: `Usage: dna evolve <dna-id> [--store-dir <path>] [--preview] [--apply]\n\nPreview self-evolution marker changes from recorded outcomes. This command is read-only; it never writes markers or runtime artifacts.\n\nOptions:\n --store-dir <path> Evolution store directory (default .dna/evolution)\n --preview Explicit preview flag; preview is the default behavior\n --apply Rejected in this kernel; use reviewed dna sync --evolve for mutation\n\nRoute:\n 1. Record outcomes: dna epigenetic record <dna-id> --action <action> --genes <gene[,gene]> --feedback '[{\"gene\":\"quality\",\"effect\":\"helped\"}]'\n 2. Inspect outcomes: dna epigenetic summary <dna-id>\n 3. Preview marker changes: dna evolve <dna-id>\n 4. Mutate only after review: dna sync --evolve\n\nBoundaries:\n - No outcomes means no evidence to evolve\n - dna evolve is preview-only and does not create missing outcome or marker directories\n - Marker mutation is allowed only through explicit reviewed sync/evolve gates\n`,
235
267
  epigenetic: `Usage: dna epigenetic <record|summary|markers> <dna-id> [options]\n\nSubcommands:\n record Record an execution outcome for self-evolution\n summary Show outcome summary for a DNA id\n markers Show persisted epigenetic markers\n\nOptions:\n --store-dir <path> Evolution store directory (default .dna/evolution)\n --action <action> Action name for record\n --success Mark record outcome as success (default true)\n --genes <a,b> Active genes for record\n --feedback <json> Constraint feedback JSON array\n --json Print JSON for summary or markers\n\nExamples:\n dna epigenetic record template_flutter_rewrite --action fix-loop --genes quality --feedback '[{\"gene\":\"quality\",\"effect\":\"helped\"}]'\n dna epigenetic summary template_flutter_rewrite\n dna epigenetic markers template_flutter_rewrite\n`,
@@ -400,10 +432,48 @@ async function main() {
400
432
  agents: { type: "string" },
401
433
  hooks: { type: "string" },
402
434
  "project-dir": { type: "string" },
435
+ "run-id": { type: "string" },
436
+ provider: { type: "string" },
437
+ "provider-executable": { type: "string" },
438
+ "max-concurrency": { type: "string" },
439
+ "timeout-ms": { type: "string" },
440
+ "cancellation-grace-ms": { type: "string" },
441
+ reason: { type: "string" },
442
+ json: { type: "boolean", default: false },
403
443
  },
404
444
  allowPositionals: true,
405
445
  strict: false,
406
446
  });
447
+ const runActions = new Set([
448
+ "start",
449
+ "status",
450
+ "inspect",
451
+ "resume",
452
+ "cancel",
453
+ ]);
454
+ const actionCandidate = runPositionals[0] ?? "";
455
+ const runAction = runActions.has(actionCandidate)
456
+ ? runPositionals.shift()
457
+ : "start";
458
+ const positionalRunId = runAction === "start"
459
+ ? undefined
460
+ : runPositionals.shift();
461
+ const optionRunId = runValues["run-id"];
462
+ if (positionalRunId
463
+ && optionRunId
464
+ && positionalRunId !== optionRunId) {
465
+ process.stderr.write("Error: positional run id and --run-id must match when both are provided\n");
466
+ return exitWith(2);
467
+ }
468
+ if (runAction !== "start" && runPositionals.length > 0) {
469
+ process.stderr.write(`Error: dna run ${runAction} accepts exactly one run id\n`);
470
+ return exitWith(2);
471
+ }
472
+ const provider = runValues.provider;
473
+ if (provider && provider !== "claude" && provider !== "codex") {
474
+ process.stderr.write("Error: --provider must be one of: claude, codex\n");
475
+ return exitWith(2);
476
+ }
407
477
  // Parse --var key=value pairs
408
478
  const varEntries = runValues.var ?? [];
409
479
  const vars = {};
@@ -415,6 +485,8 @@ async function main() {
415
485
  }
416
486
  const { runRun } = await import("./commands/run.js");
417
487
  const code = await runRun({
488
+ action: runAction,
489
+ runId: optionRunId ?? positionalRunId,
418
490
  dnaFiles: runValues.dna ?? runPositionals,
419
491
  workflowName: runValues.workflow,
420
492
  taskId: runValues.task,
@@ -429,6 +501,19 @@ async function main() {
429
501
  agentsDir: runValues.agents,
430
502
  hooksDir: runValues.hooks,
431
503
  projectDir: runValues["project-dir"],
504
+ provider: provider,
505
+ providerExecutable: runValues["provider-executable"],
506
+ maxConcurrency: runValues["max-concurrency"] === undefined
507
+ ? undefined
508
+ : Number(runValues["max-concurrency"]),
509
+ timeoutMs: runValues["timeout-ms"] === undefined
510
+ ? undefined
511
+ : Number(runValues["timeout-ms"]),
512
+ cancellationGraceMs: runValues["cancellation-grace-ms"] === undefined
513
+ ? undefined
514
+ : Number(runValues["cancellation-grace-ms"]),
515
+ reason: runValues.reason,
516
+ json: runValues.json,
432
517
  });
433
518
  return exitWith(code);
434
519
  break;
@@ -40,7 +40,7 @@ export function expandParallelBlocks(steps) {
40
40
  // Add depends_on from last step before this block
41
41
  depends_on: lastStepId
42
42
  ? [...(inner.depends_on ?? []), lastStepId]
43
- : inner.depends_on,
43
+ : (inner.depends_on ?? []),
44
44
  // Inherit isolation from parallel block if step doesn't override
45
45
  isolation: inner.isolation ?? block.isolation,
46
46
  };
@@ -72,7 +72,7 @@ export function expandParallelBlocks(steps) {
72
72
  * Returns a new array of steps with injected deps (never mutates input).
73
73
  */
74
74
  function inferImplicitDeps(steps) {
75
- const anyExplicit = steps.some((s) => s.depends_on !== undefined && s.depends_on.length > 0);
75
+ const anyExplicit = steps.some((s) => s.depends_on !== undefined);
76
76
  if (anyExplicit) {
77
77
  return steps;
78
78
  }
@@ -237,6 +237,7 @@ function toWorkflowStep(def) {
237
237
  completion: def.completion && def.completion.length > 0 ? [...def.completion] : null,
238
238
  checkpoints: def.checkpoints && def.checkpoints.length > 0 ? [...def.checkpoints] : null,
239
239
  handoff: def.handoff ?? null,
240
+ enforce: def.enforce ?? null,
240
241
  };
241
242
  if (def.max_attempts !== undefined)
242
243
  step.max_attempts = def.max_attempts;
@@ -12,7 +12,7 @@
12
12
  * 4. Call enforcement engine
13
13
  * 5. Write JSON result to stdout
14
14
  *
15
- * Fail-open: all errors { continue: true, suppressOutput: true }
15
+ * Enforcement failures use the explicit advisory/blocking boundary.
16
16
  */
17
17
  import type { ArtifactFact, ConstraintIR, RoleDef, VerifierSpec } from "../schema/types.js";
18
18
  import type { HookEvent, HookOutput } from "./protocol.js";
@@ -122,7 +122,6 @@ export declare function runStopVerifiersForTest(projectDir: string, ir: Constrai
122
122
  * 2. Context Gate — block Edit/Write/Bash when required context files unread
123
123
  *
124
124
  * Returns the first triggered block, or null if all gates pass.
125
- * Fail-open on any exception.
126
125
  */
127
126
  export declare function handlePreToolGates(ir: ConstraintIR, rawInput: Record<string, unknown>, wfState: Awaited<ReturnType<typeof readWorkflowState>>, projectDir: string, sessionId?: string): Promise<{
128
127
  output: ReturnType<typeof blockOutput>;
package/dist/hooks/cli.js CHANGED
@@ -12,7 +12,7 @@
12
12
  * 4. Call enforcement engine
13
13
  * 5. Write JSON result to stdout
14
14
  *
15
- * Fail-open: all errors { continue: true, suppressOutput: true }
15
+ * Enforcement failures use the explicit advisory/blocking boundary.
16
16
  */
17
17
  import { mkdir, readFile, realpath, stat, unlink, writeFile } from "node:fs/promises";
18
18
  import { realpathSync } from "node:fs";
@@ -20,10 +20,11 @@ import { spawn } from "node:child_process";
20
20
  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
- import { readStdin, writeOutput, silentOutput, allowOutput, blockOutput, stopOutput } from "./protocol.js";
23
+ import { readStdinResult, writeOutput, silentOutput, allowOutput, blockOutput, stopOutput } from "./protocol.js";
24
24
  import { validateHookInput } from "./schema.js";
25
+ import { hookFailureOutput } from "./enforcement-boundary.js";
25
26
  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, readVerifierResults, appendCompletedArtifact, artifactIdentity, buildArtifactKey, readArtifactManifest, resolveArtifactTemplate, safePathComponent, 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, safePathComponent, writeArtifactManifest, EVIDENCE_CAPTURE_SCHEMA_VERSION } from "./state.js";
27
28
  import { hookEventsForSurface } from "./event-registry.js";
28
29
  import { writeAuditEvent } from "../audit/index.js";
29
30
  import { RUNTIME_DECISION_EVENT_SCHEMA_VERSION, trustedEvidenceCaptureAttribution } from "../governance/index.js";
@@ -201,7 +202,9 @@ function buildClaudeEvidenceCaptureEvent(params) {
201
202
  workflow_asset: attribution.workflowAsset,
202
203
  workflow_id: attribution.workflowId,
203
204
  run_id: params.runtimeEvent.run_id || params.runtimeEvent.session_id || "unknown",
204
- step_id: attribution.stepId === "unknown" && !params.wfState
205
+ step_id: (attribution.stepId === "unknown"
206
+ && attribution.workflowId === "unknown"
207
+ && !params.wfState)
205
208
  ? `${params.event}:direct`
206
209
  : attribution.stepId,
207
210
  fact,
@@ -279,13 +282,17 @@ async function main() {
279
282
  irPath = args[irFlagIdx + 1];
280
283
  }
281
284
  // Read stdin
282
- const rawStdin = await readStdin(5000);
285
+ const stdin = await readStdinResult(5000);
286
+ if (!stdin.ok) {
287
+ writeOutput(hookFailureOutput(event, "malformed_input", `Hook input could not be read (${stdin.error ?? "unknown"}).`));
288
+ return;
289
+ }
290
+ const rawStdin = stdin.data;
283
291
  // Validate + normalize input against CC hook protocol schema.
284
- // Fail-open: invalid input → write stderr warning + silent exit.
285
292
  const validation = validateHookInput(event, rawStdin);
286
293
  if (!validation.valid) {
287
294
  process.stderr.write(`\n Intent DNA: invalid ${event} input — ${validation.errors.join("; ")}\n`);
288
- writeOutput(silentOutput());
295
+ writeOutput(hookFailureOutput(event, "malformed_input", validation.errors.join("; ")));
289
296
  return;
290
297
  }
291
298
  const rawInput = validation.normalized;
@@ -296,7 +303,7 @@ async function main() {
296
303
  const resolvedIRPath = resolve(projectDir, irPath);
297
304
  const ir = await loadIR(resolvedIRPath);
298
305
  if (!ir) {
299
- writeOutput(silentOutput());
306
+ writeOutput(hookFailureOutput(event, "missing_ir", `Compiled IR is missing, unreadable, invalid, or version-incompatible at ${resolvedIRPath}.`));
300
307
  return;
301
308
  }
302
309
  const output = await runHookEvent({ event, ir, rawInput, projectDir, sessionId });
@@ -354,7 +361,16 @@ export async function runHookEvent(options) {
354
361
  return finish(workflowState.output);
355
362
  }
356
363
  wfStateRaw = workflowState.state;
357
- if (event === "SubagentStop" && wfStateRaw && !subagentMatchesWorkflowRole(rawInput, wfStateRaw)) {
364
+ if (wfStateRaw?.active
365
+ && typeof rawInput.agent_type === "string"
366
+ && !subagentMatchesWorkflowRole(rawInput, wfStateRaw)) {
367
+ if (wfStateRaw.worker_session_id
368
+ && wfStateRaw.run_id
369
+ && wfStateRaw.attempt_id) {
370
+ const mismatch = hookFailureOutput(event, "role_mismatch", "Worker role does not match the Controller-owned Step role.");
371
+ await writeRuntimeDecision(mismatch, { output: mismatch, trace: { matched_rule: "scope", role: wfStateRaw.current_role } }, wfStateRaw);
372
+ return finish(mismatch);
373
+ }
358
374
  wfStateRaw = null;
359
375
  }
360
376
  if (wfStateRaw && wfStateRaw.active) {
@@ -365,16 +381,10 @@ export async function runHookEvent(options) {
365
381
  current_role: wfStateRaw.current_role,
366
382
  completed_artifacts: wfStateRaw.completed_artifacts,
367
383
  iteration: wfStateRaw.iteration, // G4: pass iteration for state-driven rules
384
+ strict_boundary: Boolean(wfStateRaw.worker_session_id
385
+ && wfStateRaw.run_id
386
+ && wfStateRaw.attempt_id),
368
387
  };
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;
377
- }
378
388
  }
379
389
  }
380
390
  if (event === "PreToolUse" && wfStateRaw?.active) {
@@ -715,12 +725,14 @@ function subagentMatchesWorkflowRole(rawInput, wfState) {
715
725
  return typeof rawInput.agent_type === "string" && rawInput.agent_type === agentTypeForWorkflowRole(wfState.current_role);
716
726
  }
717
727
  async function runCodexHookWithLoadedIr(rawInput, projectDir, args, ir) {
718
- const sessionId = typeof rawInput.session_id === "string"
719
- ? rawInput.session_id
720
- : typeof rawInput.sessionId === "string"
721
- ? rawInput.sessionId
722
- : undefined;
723
- const workflowState = await readWorkflowStateWithRootFallback(projectDir, sessionId);
728
+ const sessionId = typeof rawInput.worker_session_id === "string"
729
+ ? rawInput.worker_session_id
730
+ : typeof rawInput.session_id === "string"
731
+ ? rawInput.session_id
732
+ : typeof rawInput.sessionId === "string"
733
+ ? rawInput.sessionId
734
+ : undefined;
735
+ const workflowState = await readWorkerWorkflowState(projectDir, sessionId, typeof rawInput.worker_session_id !== "string");
724
736
  const workflowAsset = inferWorkflowAssetFromIr(ir, workflowState?.workflow);
725
737
  return runCodexHook({ ...rawInput, event: codexHookEventForInput(rawInput, args) }, {
726
738
  ir,
@@ -735,21 +747,32 @@ async function runCodexHookWithLoadedIr(rawInput, projectDir, args, ir) {
735
747
  });
736
748
  }
737
749
  async function runCodexHookCli(args) {
750
+ let policyEvent = "PostToolUse";
738
751
  try {
739
- const rawInput = await readStdin(5000);
752
+ const stdin = await readStdinResult(5000);
753
+ const candidateEvent = codexHookEventForInput(stdin.data, args);
754
+ if (typeof candidateEvent === "string" && VALID_EVENTS.has(candidateEvent)) {
755
+ policyEvent = candidateEvent;
756
+ }
757
+ if (!stdin.ok) {
758
+ writeOutput(hookFailureOutput(policyEvent, "malformed_input", `Codex Hook input could not be read (${stdin.error ?? "unknown"}).`));
759
+ return;
760
+ }
761
+ const rawInput = stdin.data;
740
762
  const projectDir = typeof rawInput.cwd === "string" ? rawInput.cwd : process.cwd();
741
763
  const irPath = argValue(args, "--ir") ?? DEFAULT_IR_PATH;
742
764
  const ir = await loadIR(resolve(projectDir, irPath));
743
765
  if (!ir) {
744
- writeOutput(silentOutput());
766
+ writeOutput(hookFailureOutput(policyEvent, "missing_ir", `Compiled IR is missing, unreadable, invalid, or version-incompatible at ${resolve(projectDir, irPath)}.`));
745
767
  return;
746
768
  }
747
769
  const result = await runCodexHookWithLoadedIr(rawInput, projectDir, args, ir);
748
770
  writeOutput(result.output);
749
771
  }
750
772
  catch (err) {
751
- process.stderr.write(`\n Intent DNA: CodexHook failed open — ${err instanceof Error ? err.message : String(err)}\n`);
752
- writeOutput(silentOutput());
773
+ const detail = err instanceof Error ? err.message : String(err);
774
+ process.stderr.write(`\n Intent DNA: CodexHook enforcement failed — ${detail}\n`);
775
+ writeOutput(hookFailureOutput(policyEvent, "gate_evaluation", detail));
753
776
  }
754
777
  }
755
778
  // ── IR Loading ─────────────────────────────────────────────
@@ -764,6 +787,7 @@ async function loadIR(irPath) {
764
787
  if (data.ir_version !== EXPECTED_IR_VERSION) {
765
788
  process.stderr.write(`\n Intent DNA: IR version mismatch (v${data.ir_version} on disk, v${EXPECTED_IR_VERSION} expected)\n` +
766
789
  ` Fix: run \`dna sync\` to recompile\n\n`);
790
+ return null;
767
791
  }
768
792
  return data.ir;
769
793
  }
@@ -938,20 +962,25 @@ function artifactResolverErrorOutput(event, error) {
938
962
  }
939
963
  async function readWorkflowStateForHook(projectDir, event, sessionId) {
940
964
  try {
941
- const state = event === "SubagentStop"
942
- ? await readWorkflowStateWithRootFallback(projectDir, sessionId)
943
- : await readWorkflowState(projectDir, sessionId);
965
+ let state = sessionId
966
+ ? await readWorkflowState(projectDir, sessionId)
967
+ : null;
968
+ if (!state && event === "SubagentStop") {
969
+ state = await readWorkflowState(projectDir);
970
+ }
944
971
  return { state };
945
972
  }
946
973
  catch (error) {
947
974
  return { output: artifactResolverErrorOutput(event, error) };
948
975
  }
949
976
  }
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);
977
+ async function readWorkerWorkflowState(projectDir, sessionId, allowRootFallback = false) {
978
+ const workerState = sessionId
979
+ ? await readWorkflowState(projectDir, sessionId)
980
+ : null;
981
+ if (workerState || !allowRootFallback)
982
+ return workerState;
983
+ return readWorkflowState(projectDir);
955
984
  }
956
985
  async function resolveWorkflowArtifactFactsForHook(projectDir, ir, wfState, event, sessionId) {
957
986
  try {
@@ -1302,7 +1331,7 @@ const TEST_PASSED_RE = /(\d+)\s+(?:tests?\s+)?passed/i;
1302
1331
  */
1303
1332
  async function handleSurgeonReflection(ir, input, projectDir, sessionId) {
1304
1333
  const agentType = typeof input.agent_type === "string" ? input.agent_type : undefined;
1305
- if (!agentType)
1334
+ if (!agentType || !sessionId)
1306
1335
  return null;
1307
1336
  const toolName = String(input.tool_name ?? "");
1308
1337
  const toolOutput = typeof input.tool_output === "string" ? input.tool_output : undefined;
@@ -1341,12 +1370,14 @@ async function handleSurgeonReflection(ir, input, projectDir, sessionId) {
1341
1370
  blockedItemsPath = reflectionConfig.blocked_items_path;
1342
1371
  if (!maxAttempts)
1343
1372
  return null;
1344
- // Read current surgeon state
1345
- const surgeonState = await readSurgeonAttempts(projectDir, sessionId);
1346
1373
  // Track edits
1347
1374
  if (toolName === "Edit") {
1348
- surgeonState.edit_count++;
1349
- await writeSurgeonAttempts(projectDir, surgeonState, sessionId);
1375
+ if (!sessionId)
1376
+ return null;
1377
+ await updateSurgeonAttempts(projectDir, sessionId, (state) => ({
1378
+ ...state,
1379
+ edit_count: state.edit_count + 1,
1380
+ }));
1350
1381
  return null;
1351
1382
  }
1352
1383
  // Track test results from Bash
@@ -1354,43 +1385,44 @@ async function handleSurgeonReflection(ir, input, projectDir, sessionId) {
1354
1385
  const match = toolOutput.match(TEST_PASSED_RE);
1355
1386
  if (!match)
1356
1387
  return null; // Can't parse — fail-open
1357
- const greenCount = parseInt(match[1], 10);
1358
- surgeonState.bash_test_count++;
1359
- surgeonState.current_green_count = greenCount;
1360
- if (!surgeonState.baseline_established) {
1361
- surgeonState.baseline_established = true;
1362
- surgeonState.last_green_count = greenCount;
1363
- await writeSurgeonAttempts(projectDir, surgeonState, sessionId);
1388
+ if (!sessionId)
1364
1389
  return null;
1365
- }
1366
- // Check progress: green count should increase after edits
1367
- if (surgeonState.edit_count >= 2 && greenCount <= surgeonState.last_green_count) {
1368
- surgeonState.fail_count++;
1369
- const result = checkReflectionLimit(surgeonState.fail_count, maxAttempts, surgeonState.handoff_count, maxHandoffs);
1370
- // Update last_green_count for next comparison
1371
- surgeonState.last_green_count = greenCount;
1372
- surgeonState.edit_count = 0;
1373
- await writeSurgeonAttempts(projectDir, surgeonState, sessionId);
1374
- if (result.action === "handoff" || result.action === "warn") {
1375
- const target = handoffTo ? ` Hand off to '${handoffTo}' for re-analysis.` : "";
1376
- return allowOutput(`WARN [Intent DNA] Reflection Gate: ${result.reason}${target}\n` +
1377
- `Review experience_chain in .dna/state/workflow/surgeon-attempts.json before next attempt.`);
1390
+ const greenCount = parseInt(match[1], 10);
1391
+ let reflectionOutput = null;
1392
+ await updateSurgeonAttempts(projectDir, sessionId, (surgeonState) => {
1393
+ surgeonState.bash_test_count++;
1394
+ surgeonState.current_green_count = greenCount;
1395
+ if (!surgeonState.baseline_established) {
1396
+ surgeonState.baseline_established = true;
1397
+ surgeonState.last_green_count = greenCount;
1398
+ return surgeonState;
1378
1399
  }
1379
- if (result.action === "skip") {
1380
- const path = blockedItemsPath ? ` Record blocked item to ${blockedItemsPath}.` : "";
1381
- return allowOutput(`WARN [Intent DNA] Reflection Gate: ${result.reason}${path}\n` +
1382
- `SKIP this item and move to the next.`);
1400
+ if (surgeonState.edit_count >= 2 && greenCount <= surgeonState.last_green_count) {
1401
+ surgeonState.fail_count++;
1402
+ const result = checkReflectionLimit(surgeonState.fail_count, maxAttempts, surgeonState.handoff_count, maxHandoffs);
1403
+ surgeonState.last_green_count = greenCount;
1404
+ surgeonState.edit_count = 0;
1405
+ if (result.action === "handoff" || result.action === "warn") {
1406
+ const target = handoffTo ? ` Hand off to '${handoffTo}' for re-analysis.` : "";
1407
+ reflectionOutput = allowOutput(`WARN [Intent DNA] Reflection Gate: ${result.reason}${target}\n` +
1408
+ `Review experience_chain in the worker Hook state before next attempt.`);
1409
+ }
1410
+ else if (result.action === "skip") {
1411
+ const path = blockedItemsPath ? ` Record blocked item to ${blockedItemsPath}.` : "";
1412
+ reflectionOutput = allowOutput(`WARN [Intent DNA] Reflection Gate: ${result.reason}${path}\n` +
1413
+ `SKIP this item and move to the next.`);
1414
+ }
1383
1415
  }
1384
- }
1385
- else {
1386
- // Progress made — reset counters
1387
- surgeonState.last_green_count = greenCount;
1388
- surgeonState.edit_count = 0;
1389
- if (greenCount > surgeonState.last_green_count) {
1390
- surgeonState.fail_count = 0;
1416
+ else {
1417
+ const madeProgress = greenCount > surgeonState.last_green_count;
1418
+ surgeonState.last_green_count = greenCount;
1419
+ surgeonState.edit_count = 0;
1420
+ if (madeProgress)
1421
+ surgeonState.fail_count = 0;
1391
1422
  }
1392
- }
1393
- await writeSurgeonAttempts(projectDir, surgeonState, sessionId);
1423
+ return surgeonState;
1424
+ });
1425
+ return reflectionOutput;
1394
1426
  }
1395
1427
  return null;
1396
1428
  }
@@ -1418,7 +1450,6 @@ async function loadReflectionConfig(projectDir, _workflowName, _stepId) {
1418
1450
  * 2. Context Gate — block Edit/Write/Bash when required context files unread
1419
1451
  *
1420
1452
  * Returns the first triggered block, or null if all gates pass.
1421
- * Fail-open on any exception.
1422
1453
  */
1423
1454
  export async function handlePreToolGates(ir, rawInput, wfState, projectDir, sessionId) {
1424
1455
  try {
@@ -1477,8 +1508,12 @@ export async function handlePreToolGates(ir, rawInput, wfState, projectDir, sess
1477
1508
  }
1478
1509
  }
1479
1510
  }
1480
- catch {
1481
- // Fail-open: gate failures never block execution
1511
+ catch (error) {
1512
+ const detail = error instanceof Error ? error.message : String(error);
1513
+ return {
1514
+ output: blockOutput(`[Intent DNA] gate_evaluation: PreToolUse gates could not be evaluated safely (${detail}).`),
1515
+ matched_rule: "context_gate",
1516
+ };
1482
1517
  }
1483
1518
  return null;
1484
1519
  }
@@ -1548,8 +1583,12 @@ function isDirectEntryPoint() {
1548
1583
  }
1549
1584
  }
1550
1585
  if (isDirectEntryPoint()) {
1551
- main().catch(() => {
1552
- // Fail-open: never block Claude Code on unexpected errors
1553
- writeOutput(silentOutput());
1586
+ main().catch((error) => {
1587
+ const eventArg = process.argv[2];
1588
+ const event = VALID_EVENTS.has(eventArg)
1589
+ ? eventArg
1590
+ : "PostToolUse";
1591
+ const detail = error instanceof Error ? error.message : String(error);
1592
+ writeOutput(hookFailureOutput(event, "gate_evaluation", detail));
1554
1593
  });
1555
1594
  }
@@ -31,6 +31,8 @@ export interface EnforceState {
31
31
  completed_artifacts?: CompletedArtifactEntry[];
32
32
  /** G4: current workflow iteration (for relax_after_iteration rules) */
33
33
  iteration?: number;
34
+ /** True only for Controller-owned disposable worker Hook state. */
35
+ strict_boundary?: boolean;
34
36
  };
35
37
  /** Resolved artifact facts loaded by CLI/state before pure enforcement. */
36
38
  artifactFacts?: ArtifactFact[];