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
@@ -33,7 +33,7 @@ export function enforcePreToolUse(ir, input, state, roles) {
33
33
  if (input.agent_type && ir.roles_scope_map && ir.roles_scope_map.length > 0) {
34
34
  const relaxed = isStepScopeRelaxed(ir, state);
35
35
  if (!relaxed) {
36
- const result = enforceRoleScope(ir.roles_scope_map, input, getStepAdditionalPaths(ir, state));
36
+ const result = enforceRoleScope(ir.roles_scope_map, input, getStepAdditionalPaths(ir, state), state?.workflowState?.strict_boundary === true);
37
37
  if (result)
38
38
  return { output: result, trace: { matched_rule: "scope", role: input.agent_type } };
39
39
  }
@@ -46,7 +46,7 @@ export function enforcePreToolUse(ir, input, state, roles) {
46
46
  }
47
47
  // Layer 4: Pre-execution gates
48
48
  if (ir.pre_execution_gates.length > 0) {
49
- const result = enforceGates(ir.pre_execution_gates, input);
49
+ const result = enforceGates(ir.pre_execution_gates, input, state?.workflowState?.strict_boundary === true);
50
50
  if (result)
51
51
  return { output: result, trace: { matched_rule: "gate" } };
52
52
  }
@@ -56,11 +56,19 @@ export function enforcePreToolUse(ir, input, state, roles) {
56
56
  if (result)
57
57
  return { output: result, trace: { matched_rule: "output_schema" } };
58
58
  }
59
- // Layer 6: Handoff — check consumed artifacts are available
60
- if (state?.workflowState && ir.workflows_ir) {
59
+ if (state?.workflowState
60
+ && !state.workflowState.strict_boundary
61
+ && ir.workflows_ir) {
61
62
  const result = enforceHandoffConsumes(ir, state.workflowState, state.artifactFacts ?? []);
62
- if (result)
63
- return { output: result, trace: { matched_rule: "handoff", step_id: state.workflowState.current_step } };
63
+ if (result) {
64
+ return {
65
+ output: result,
66
+ trace: {
67
+ matched_rule: "handoff",
68
+ step_id: state.workflowState.current_step,
69
+ },
70
+ };
71
+ }
64
72
  }
65
73
  return null;
66
74
  }
@@ -352,27 +360,26 @@ function getStepAdditionalPaths(ir, state) {
352
360
  return rule?.additional_write_paths ?? [];
353
361
  }
354
362
  // ── Internal: Layer Enforcement ────────────────────────────
355
- /**
356
- * Enforce handoff consumes — verify that the current step's consumed
357
- * artifacts have been produced by a preceding step.
358
- * Returns block output if a required artifact is missing, null if all satisfied.
359
- */
360
363
  function enforceHandoffConsumes(ir, wfState, artifactFacts) {
361
- const activeWf = ir.workflows_ir?.find(w => w.workflow_name === wfState.workflow);
362
- if (!activeWf || (activeWf.handoff_chain?.length ?? 0) === 0)
364
+ const activeWorkflow = ir.workflows_ir?.find((workflow) => workflow.workflow_name === wfState.workflow);
365
+ if (!activeWorkflow || (activeWorkflow.handoff_chain?.length ?? 0) === 0) {
363
366
  return null;
364
- const currentEntry = activeWf.handoff_chain.find(h => h.step_id === wfState.current_step);
367
+ }
368
+ const currentEntry = activeWorkflow.handoff_chain.find((entry) => entry.step_id === wfState.current_step);
365
369
  if (!currentEntry?.consumes || currentEntry.consumes.length === 0)
366
370
  return null;
367
371
  for (const consumed of currentEntry.consumes) {
368
372
  if (consumed.required === false)
369
373
  continue;
370
- const producerStep = consumed.from ?? findProducerStep(activeWf.handoff_chain, consumed) ?? "external";
371
- const fact = artifactFacts.find(a => artifactMatches(a, consumed, producerStep, wfState.workflow));
374
+ const producerStep = consumed.from
375
+ ?? findProducerStep(activeWorkflow.handoff_chain, consumed)
376
+ ?? "external";
377
+ const fact = artifactFacts.find((entry) => artifactMatches(entry, consumed, producerStep, wfState.workflow));
372
378
  if (!fact) {
373
379
  return blockOutput(`[Intent DNA] Step '${wfState.current_step}' requires artifact '${consumed.description}' from '${producerStep}', but no verified manifest was resolved.`);
374
380
  }
375
- if (consumed.type === "git_commit" && typeof fact.metadata?.commit !== "string") {
381
+ if (consumed.type === "git_commit"
382
+ && typeof fact.metadata?.commit !== "string") {
376
383
  return blockOutput(`[Intent DNA] Step '${wfState.current_step}' requires git_commit artifact from '${producerStep}', but no commit identity was recorded.`);
377
384
  }
378
385
  }
@@ -411,9 +418,14 @@ export function enforceHandoffProduces(ir, wfState) {
411
418
  return null;
412
419
  }
413
420
  function findProducerStep(handoffChain, consumed) {
414
- const producer = handoffChain.find(h => h.produces?.some(p => p.type === consumed.type && ((p.path && consumed.path && p.path === consumed.path) ||
415
- (p.name && consumed.name && p.name === consumed.name) ||
416
- (p.artifact_id && consumed.artifact_id && p.artifact_id === consumed.artifact_id))));
421
+ const producer = handoffChain.find((entry) => entry.produces?.some((produced) => produced.type === consumed.type
422
+ && ((produced.path && consumed.path && produced.path === consumed.path)
423
+ || (produced.name
424
+ && consumed.name
425
+ && produced.name === consumed.name)
426
+ || (produced.artifact_id
427
+ && consumed.artifact_id
428
+ && produced.artifact_id === consumed.artifact_id))));
417
429
  return producer?.step_id ?? null;
418
430
  }
419
431
  function artifactMatches(fact, artifact, producerStep, workflow) {
@@ -431,7 +443,7 @@ function artifactMatches(fact, artifact, producerStep, workflow) {
431
443
  return false;
432
444
  return true;
433
445
  }
434
- function enforceRoleScope(rolesScopeMap, input, additionalWritePaths = []) {
446
+ function enforceRoleScope(rolesScopeMap, input, additionalWritePaths = [], strictBoundary = false) {
435
447
  if (!input.agent_type)
436
448
  return null;
437
449
  // Standard write tools: check single file path
@@ -450,7 +462,7 @@ function enforceRoleScope(rolesScopeMap, input, additionalWritePaths = []) {
450
462
  if (writePaths.length === 0)
451
463
  return null;
452
464
  for (const p of writePaths) {
453
- const result = checkPathAgainstScope(rolesScopeMap, input, p, additionalWritePaths, false);
465
+ const result = checkPathAgainstScope(rolesScopeMap, input, p, additionalWritePaths, strictBoundary, strictBoundary);
454
466
  if (result)
455
467
  return result;
456
468
  }
@@ -459,7 +471,7 @@ function enforceRoleScope(rolesScopeMap, input, additionalWritePaths = []) {
459
471
  return null;
460
472
  }
461
473
  /** Check a single file path against role scope. Shared by Write tools and Bash. */
462
- function checkPathAgainstScope(rolesScopeMap, input, filePath, additionalWritePaths = [], blockViolation = true) {
474
+ function checkPathAgainstScope(rolesScopeMap, input, filePath, additionalWritePaths = [], blockViolation = true, blockUnknownRole = false) {
463
475
  const cwd = input.cwd;
464
476
  let relativePath = cwd && filePath.startsWith("/") ? relative(cwd, filePath) : filePath;
465
477
  // Normalize to resolve traversal (e.g., "src/../../test/x.ts" → "../test/x.ts")
@@ -487,7 +499,9 @@ function checkPathAgainstScope(rolesScopeMap, input, filePath, additionalWritePa
487
499
  }
488
500
  return null; // Role matched, write allowed
489
501
  }
490
- return null; // No matching role — allow
502
+ return blockUnknownRole
503
+ ? blockOutput("[Intent DNA] role_mismatch: worker role has no configured role scope.")
504
+ : null;
491
505
  }
492
506
  /**
493
507
  * Verify a list of paths against role scope. Returns violation messages.
@@ -495,10 +509,12 @@ function checkPathAgainstScope(rolesScopeMap, input, filePath, additionalWritePa
495
509
  */
496
510
  function verifyPathsAgainstScope(rolesScopeMap, agentType, paths, cwd) {
497
511
  const violations = [];
512
+ let matchedRole = false;
498
513
  for (const entry of rolesScopeMap) {
499
514
  const agentTypeName = `dna-${toKebabCase(entry.role_name)}`;
500
515
  if (agentType !== agentTypeName)
501
516
  continue;
517
+ matchedRole = true;
502
518
  const writeGlobs = entry.scope.write ?? [];
503
519
  for (const p of paths) {
504
520
  const relativePath = cwd && p.startsWith("/") ? relative(cwd, p) : p;
@@ -508,6 +524,9 @@ function verifyPathsAgainstScope(rolesScopeMap, agentType, paths, cwd) {
508
524
  }
509
525
  break; // Found the role, done
510
526
  }
527
+ if (!matchedRole) {
528
+ violations.push(`agent_type '${agentType}' has no configured role scope`);
529
+ }
511
530
  return violations;
512
531
  }
513
532
  function enforceToolFilters(filters, input) {
@@ -529,11 +548,21 @@ function enforceToolFilters(filters, input) {
529
548
  }
530
549
  return null;
531
550
  }
532
- function enforceGates(gates, input) {
551
+ function enforceGates(gates, input, strictBoundary = false) {
533
552
  for (const gate of gates) {
534
553
  const matches = evaluateGateCondition(gate.condition, input);
535
- if (matches === null)
536
- continue; // Condition too complex — skip
554
+ if (matches === null) {
555
+ if (!strictBoundary)
556
+ continue;
557
+ const detail = `Gate condition '${gate.condition}' could not be evaluated.`;
558
+ if (gate.action === "block") {
559
+ return blockOutput(`[Intent DNA] gate_evaluation: ${detail}`);
560
+ }
561
+ if (gate.action === "escalate") {
562
+ return escalateOutput(`[Intent DNA] gate_evaluation: ${detail}`);
563
+ }
564
+ return allowOutput(`WARN [Intent DNA] gate_evaluation: ${detail}`);
565
+ }
537
566
  if (!matches)
538
567
  continue; // Condition not triggered
539
568
  if (gate.action === "block") {
@@ -0,0 +1,13 @@
1
+ import type { HookEvent } from "./event-registry.js";
2
+ import { type HookOutput } from "./protocol.js";
3
+ export type HookFailureKind = "malformed_input" | "missing_ir" | "role_mismatch" | "gate_evaluation" | "bash_write_scope";
4
+ export type HookFailureDisposition = "advisory" | "blocking";
5
+ /**
6
+ * Foundation Hook failure policy.
7
+ *
8
+ * Pre-execution and terminal-boundary events block when enforcement cannot be
9
+ * evaluated safely. Observation-only and post-execution events remain advisory.
10
+ * Bash scope is blocking before execution and advisory after the write occurred.
11
+ */
12
+ export declare function hookFailureDisposition(event: HookEvent, kind: HookFailureKind): HookFailureDisposition;
13
+ export declare function hookFailureOutput(event: HookEvent, kind: HookFailureKind, detail: string): HookOutput;
@@ -0,0 +1,33 @@
1
+ import { allowOutput, blockOutput } from "./protocol.js";
2
+ const BLOCKING_EVENTS = new Set([
3
+ "PreToolUse",
4
+ "SubagentStop",
5
+ "Stop",
6
+ ]);
7
+ /**
8
+ * Foundation Hook failure policy.
9
+ *
10
+ * Pre-execution and terminal-boundary events block when enforcement cannot be
11
+ * evaluated safely. Observation-only and post-execution events remain advisory.
12
+ * Bash scope is blocking before execution and advisory after the write occurred.
13
+ */
14
+ export function hookFailureDisposition(event, kind) {
15
+ if (kind === "gate_evaluation") {
16
+ return BLOCKING_EVENTS.has(event) ? "blocking" : "advisory";
17
+ }
18
+ if (kind === "bash_write_scope") {
19
+ return event === "PreToolUse" ? "blocking" : "advisory";
20
+ }
21
+ if (kind === "role_mismatch") {
22
+ return event === "PreToolUse" || event === "SubagentStop"
23
+ ? "blocking"
24
+ : "advisory";
25
+ }
26
+ return BLOCKING_EVENTS.has(event) ? "blocking" : "advisory";
27
+ }
28
+ export function hookFailureOutput(event, kind, detail) {
29
+ const message = `[Intent DNA] ${kind}: ${detail}`;
30
+ return hookFailureDisposition(event, kind) === "blocking"
31
+ ? blockOutput(message)
32
+ : allowOutput(`WARN ${message}`, event);
33
+ }
@@ -4,7 +4,8 @@
4
4
  * Public exports for the hook enforcement engine.
5
5
  * Used by the `dna-hook` CLI and importable for programmatic use.
6
6
  */
7
- export { type HookEvent, type HookInput, type HookOutput, type HookInputBase, type PreToolUseInput, type PostToolUseInput, type UserPromptSubmitInput, type SubagentStopInput, type PreCompactInput, type NotificationInput, type StopInput, type SessionStartInput as SessionStartHookInput, readStdin, writeOutput, allowOutput, blockOutput, escalateOutput, silentOutput, } from "./protocol.js";
7
+ export { type HookEvent, type HookInput, type HookOutput, type HookInputBase, type PreToolUseInput, type PostToolUseInput, type UserPromptSubmitInput, type SubagentStopInput, type PreCompactInput, type NotificationInput, type StopInput, type SessionStartInput as SessionStartHookInput, type StdinReadResult, readStdin, readStdinResult, writeOutput, allowOutput, blockOutput, escalateOutput, silentOutput, } from "./protocol.js";
8
+ export { type HookFailureDisposition, type HookFailureKind, hookFailureDisposition, hookFailureOutput, } from "./enforcement-boundary.js";
8
9
  export { type EnforceState, type SessionStartInput, type StopEnforceInput, type StopWorkflowContext, enforcePreToolUse, enforcePostToolUse, enforceUserPromptSubmit, enforceSubagentStop, enforcePreCompact, enforceNotification, enforceSessionStart, enforceStop, checkWriteAllowed, globToPrefix, evaluateGateCondition, resolveToolTarget, } from "./enforce.js";
9
- export { type DNAWorkflowState, type AuditEntry, resolveStateDir, readWorkflowState, writeWorkflowState, clearWorkflowState, appendAudit, } from "./state.js";
10
+ export { type DNAWorkflowState, type AuditEntry, resolveStateDir, readWorkflowState, writeWorkflowState, clearWorkflowState, cleanupWorkerHookState, appendAudit, } from "./state.js";
10
11
  export { type SessionSummary, computeSummary, formatSummary, } from "./cli.js";
@@ -5,10 +5,11 @@
5
5
  * Used by the `dna-hook` CLI and importable for programmatic use.
6
6
  */
7
7
  // Protocol types and I/O
8
- export { readStdin, writeOutput, allowOutput, blockOutput, escalateOutput, silentOutput, } from "./protocol.js";
8
+ export { readStdin, readStdinResult, writeOutput, allowOutput, blockOutput, escalateOutput, silentOutput, } from "./protocol.js";
9
+ export { hookFailureDisposition, hookFailureOutput, } from "./enforcement-boundary.js";
9
10
  // Enforcement engine
10
11
  export { enforcePreToolUse, enforcePostToolUse, enforceUserPromptSubmit, enforceSubagentStop, enforcePreCompact, enforceNotification, enforceSessionStart, enforceStop, checkWriteAllowed, globToPrefix, evaluateGateCondition, resolveToolTarget, } from "./enforce.js";
11
12
  // State management
12
- export { resolveStateDir, readWorkflowState, writeWorkflowState, clearWorkflowState, appendAudit, } from "./state.js";
13
+ export { resolveStateDir, readWorkflowState, writeWorkflowState, clearWorkflowState, cleanupWorkerHookState, appendAudit, } from "./state.js";
13
14
  // Session summary
14
15
  export { computeSummary, formatSummary, } from "./cli.js";
@@ -11,6 +11,10 @@ export type { HookEvent } from "./event-registry.js";
11
11
  export interface HookInputBase {
12
12
  cwd?: string;
13
13
  sessionId?: string;
14
+ worker_session_id?: string;
15
+ run_id?: string;
16
+ step_id?: string;
17
+ attempt_id?: string;
14
18
  }
15
19
  export interface PreToolUseInput extends HookInputBase {
16
20
  tool_name: string;
@@ -76,10 +80,14 @@ export declare function escalateOutput(reason: string): HookOutput;
76
80
  export declare function stopOutput(systemMessage?: string): HookOutput;
77
81
  /** Create a silent allow output (no visible output to user). */
78
82
  export declare function silentOutput(): HookOutput;
79
- /**
80
- * Read JSON from stdin with timeout protection.
81
- * Returns parsed object on success, empty object on failure (fail-open).
82
- */
83
+ export interface StdinReadResult {
84
+ ok: boolean;
85
+ data: Record<string, unknown>;
86
+ error?: "timeout" | "empty" | "malformed" | "read_error";
87
+ }
88
+ /** Read JSON from stdin without erasing malformed-input evidence. */
89
+ export declare function readStdinResult(timeoutMs?: number): Promise<StdinReadResult>;
90
+ /** Compatibility reader for non-enforcement call sites. */
83
91
  export declare function readStdin(timeoutMs?: number): Promise<Record<string, unknown>>;
84
92
  /**
85
93
  * Write hook output to stdout as JSON.
@@ -46,16 +46,12 @@ export function stopOutput(systemMessage) {
46
46
  export function silentOutput() {
47
47
  return { continue: true, suppressOutput: true };
48
48
  }
49
- // ── Stdin/Stdout I/O ───────────────────────────────────────
50
- /**
51
- * Read JSON from stdin with timeout protection.
52
- * Returns parsed object on success, empty object on failure (fail-open).
53
- */
54
- export function readStdin(timeoutMs = 5000) {
49
+ /** Read JSON from stdin without erasing malformed-input evidence. */
50
+ export function readStdinResult(timeoutMs = 5000) {
55
51
  return new Promise((resolve) => {
56
52
  const chunks = [];
57
53
  let settled = false;
58
- const finish = (data) => {
54
+ const finish = (result) => {
59
55
  if (settled)
60
56
  return;
61
57
  settled = true;
@@ -63,9 +59,9 @@ export function readStdin(timeoutMs = 5000) {
63
59
  process.stdin.removeAllListeners("data");
64
60
  process.stdin.removeAllListeners("end");
65
61
  process.stdin.removeAllListeners("error");
66
- resolve(data);
62
+ resolve(result);
67
63
  };
68
- const timer = setTimeout(() => finish({}), timeoutMs);
64
+ const timer = setTimeout(() => finish({ ok: false, data: {}, error: "timeout" }), timeoutMs);
69
65
  process.stdin.on("data", (chunk) => {
70
66
  chunks.push(chunk);
71
67
  });
@@ -73,23 +69,33 @@ export function readStdin(timeoutMs = 5000) {
73
69
  try {
74
70
  const raw = Buffer.concat(chunks).toString("utf-8").trim();
75
71
  if (!raw) {
76
- finish({});
72
+ finish({ ok: false, data: {}, error: "empty" });
77
73
  return;
78
74
  }
79
75
  const parsed = JSON.parse(raw);
80
- finish(typeof parsed === "object" && parsed !== null ? parsed : {});
76
+ if (typeof parsed !== "object" || parsed === null || Array.isArray(parsed)) {
77
+ finish({ ok: false, data: {}, error: "malformed" });
78
+ return;
79
+ }
80
+ finish({ ok: true, data: parsed });
81
81
  }
82
82
  catch {
83
- finish({});
83
+ finish({ ok: false, data: {}, error: "malformed" });
84
84
  }
85
85
  });
86
- process.stdin.on("error", () => finish({}));
86
+ process.stdin.on("error", () => {
87
+ finish({ ok: false, data: {}, error: "read_error" });
88
+ });
87
89
  // If stdin is already ended (piped and closed)
88
90
  if (process.stdin.readableEnded) {
89
- finish({});
91
+ finish({ ok: false, data: {}, error: "empty" });
90
92
  }
91
93
  });
92
94
  }
95
+ /** Compatibility reader for non-enforcement call sites. */
96
+ export async function readStdin(timeoutMs = 5000) {
97
+ return (await readStdinResult(timeoutMs)).data;
98
+ }
93
99
  /**
94
100
  * Write hook output to stdout as JSON.
95
101
  */
@@ -1,7 +1,7 @@
1
1
  /**
2
2
  * Hook input validation — CC protocol boundary.
3
3
  * Zero external dependencies. Hand-written validators.
4
- * Fail-open: invalid input log warning + return { valid: false, ... }
4
+ * Classification of invalid input belongs to the event boundary.
5
5
  */
6
6
  export interface ValidationResult {
7
7
  valid: boolean;
@@ -15,6 +15,7 @@ export interface ValidationResult {
15
15
  export declare function validateHookInput(event: string, raw: unknown): ValidationResult;
16
16
  /**
17
17
  * Normalize session_id field from various formats:
18
+ * - worker_session_id (disposable Controller worker identity)
18
19
  * - session_id (snake_case from CC)
19
20
  * - sessionId (camelCase internal)
20
21
  * - CLAUDE_SESSION_ID (env var fallback)
@@ -1,7 +1,7 @@
1
1
  /**
2
2
  * Hook input validation — CC protocol boundary.
3
3
  * Zero external dependencies. Hand-written validators.
4
- * Fail-open: invalid input log warning + return { valid: false, ... }
4
+ * Classification of invalid input belongs to the event boundary.
5
5
  */
6
6
  import { isHookEventSupportedBySurface } from "./event-registry.js";
7
7
  /**
@@ -15,7 +15,7 @@ export function validateHookInput(event, raw) {
15
15
  const input = raw;
16
16
  const errors = [];
17
17
  const normalized = { ...input };
18
- // Normalize session_id to sessionId
18
+ // The Controller's disposable worker session is the Hook context key.
19
19
  const sessionId = normalizeSessionId(input);
20
20
  if (sessionId !== undefined) {
21
21
  normalized.sessionId = sessionId;
@@ -72,11 +72,15 @@ export function validateHookInput(event, raw) {
72
72
  }
73
73
  /**
74
74
  * Normalize session_id field from various formats:
75
+ * - worker_session_id (disposable Controller worker identity)
75
76
  * - session_id (snake_case from CC)
76
77
  * - sessionId (camelCase internal)
77
78
  * - CLAUDE_SESSION_ID (env var fallback)
78
79
  */
79
80
  export function normalizeSessionId(input) {
81
+ if (typeof input.worker_session_id === "string") {
82
+ return input.worker_session_id;
83
+ }
80
84
  if (typeof input.sessionId === "string") {
81
85
  return input.sessionId;
82
86
  }
@@ -2,7 +2,7 @@
2
2
  * Intent DNA — DNAStateManager
3
3
  *
4
4
  * Class-based facade over state.ts module functions. Bundles projectDir +
5
- * sessionId into a single object so call sites don't repeat those args.
5
+ * disposable workerSessionId into a single object so call sites don't repeat it.
6
6
  * Adds experience chain persistence (markdown + structured JSON) and
7
7
  * lifecycle operations (init / cleanup / isStale) that state.ts lacks.
8
8
  *
@@ -22,8 +22,8 @@ export interface ExperienceEntry {
22
22
  }
23
23
  export declare class DNAStateManager {
24
24
  private readonly projectDir;
25
- private readonly sessionId?;
26
- constructor(projectDir: string, sessionId?: string | undefined);
25
+ private readonly workerSessionId?;
26
+ constructor(projectDir: string, workerSessionId?: string | undefined);
27
27
  readWorkflowState(stalenessMs?: number): Promise<DNAWorkflowState | null>;
28
28
  writeWorkflowState(state: DNAWorkflowState): Promise<void>;
29
29
  clearWorkflowState(): Promise<void>;
@@ -38,8 +38,8 @@ export declare class DNAStateManager {
38
38
  /** Ensure session directory structure exists. */
39
39
  init(): Promise<void>;
40
40
  /**
41
- * Remove this manager's state files.
42
- * Callers that need trace rotation should invoke rotateTraces() from state.ts separately.
41
+ * Remove only this disposable worker's Hook directory.
42
+ * Durable Controller run/task/result/handoff records are outside this boundary.
43
43
  */
44
44
  cleanup(): Promise<void>;
45
45
  /** True when workflow.json started_at exceeds the staleness threshold. */
@@ -2,7 +2,7 @@
2
2
  * Intent DNA — DNAStateManager
3
3
  *
4
4
  * Class-based facade over state.ts module functions. Bundles projectDir +
5
- * sessionId into a single object so call sites don't repeat those args.
5
+ * disposable workerSessionId into a single object so call sites don't repeat it.
6
6
  * Adds experience chain persistence (markdown + structured JSON) and
7
7
  * lifecycle operations (init / cleanup / isStale) that state.ts lacks.
8
8
  *
@@ -10,29 +10,31 @@
10
10
  * class delegates to them. Prefer this class in new code; state.ts exports
11
11
  * are @deprecated for direct use.
12
12
  */
13
- import { readFile, mkdir, unlink, stat } from "node:fs/promises";
13
+ import { readFile, mkdir, stat } from "node:fs/promises";
14
14
  import { join } from "node:path";
15
- import { resolveStateDir, readWorkflowState as rwState, writeWorkflowState as wwState, clearWorkflowState as cwState, appendAudit as aAudit, readSurgeonAttempts as rSurg, writeSurgeonAttempts as wSurg, readSessionReads as rReads, appendSessionRead as aRead, atomicWrite, } from "./state.js";
15
+ import { resolveStateDir, readWorkflowState as rwState, writeWorkflowState as wwState, clearWorkflowState as cwState, appendAudit as aAudit, readSurgeonAttempts as rSurg, writeSurgeonAttempts as wSurg, readSessionReads as rReads, appendSessionRead as aRead, atomicWrite, atomicUpdateJson, cleanupWorkerHookState, } from "./state.js";
16
16
  const EXPERIENCE_JSON = "workflow/experience.json";
17
17
  const EXPERIENCE_MD = "workflow/experience.md";
18
18
  const DEFAULT_STALENESS_MS = 2 * 60 * 60 * 1000; // 2h — matches state.ts
19
19
  // ── Manager ────────────────────────────────────────────────
20
20
  export class DNAStateManager {
21
21
  projectDir;
22
- sessionId;
23
- constructor(projectDir, sessionId) {
22
+ workerSessionId;
23
+ constructor(projectDir, workerSessionId) {
24
24
  this.projectDir = projectDir;
25
- this.sessionId = sessionId;
25
+ this.workerSessionId = workerSessionId;
26
26
  }
27
27
  // ── Workflow state ──
28
28
  readWorkflowState(stalenessMs = DEFAULT_STALENESS_MS) {
29
- return rwState(this.projectDir, this.sessionId, stalenessMs);
29
+ return rwState(this.projectDir, this.workerSessionId, stalenessMs);
30
30
  }
31
31
  writeWorkflowState(state) {
32
- return wwState(this.projectDir, state, this.sessionId);
32
+ return wwState(this.projectDir, state, this.workerSessionId);
33
33
  }
34
34
  clearWorkflowState() {
35
- return cwState(this.projectDir, this.sessionId);
35
+ return this.workerSessionId
36
+ ? cleanupWorkerHookState(this.projectDir, this.workerSessionId)
37
+ : cwState(this.projectDir);
36
38
  }
37
39
  // ── Audit log ──
38
40
  appendAudit(entry) {
@@ -40,17 +42,17 @@ export class DNAStateManager {
40
42
  }
41
43
  // ── Surgeon attempts ──
42
44
  readSurgeonAttempts() {
43
- return rSurg(this.projectDir, this.sessionId);
45
+ return rSurg(this.projectDir, this.workerSessionId);
44
46
  }
45
47
  writeSurgeonAttempts(state) {
46
- return wSurg(this.projectDir, state, this.sessionId);
48
+ return wSurg(this.projectDir, state, this.workerSessionId);
47
49
  }
48
50
  // ── Session reads ──
49
51
  readSessionReads() {
50
- return rReads(this.projectDir, this.sessionId);
52
+ return rReads(this.projectDir, this.workerSessionId);
51
53
  }
52
54
  appendSessionRead(filePath) {
53
- return aRead(this.projectDir, filePath, this.sessionId);
55
+ return aRead(this.projectDir, filePath, this.workerSessionId);
54
56
  }
55
57
  // ── Experience chain (new) ──
56
58
  async readExperienceChain() {
@@ -69,28 +71,28 @@ export class DNAStateManager {
69
71
  await atomicWrite(this.path(EXPERIENCE_MD), renderExperienceMarkdown(chain));
70
72
  }
71
73
  async appendExperience(entry) {
72
- const chain = await this.readExperienceChain();
73
- chain.push(entry);
74
- await this.writeExperienceChain(chain);
74
+ const chain = await atomicUpdateJson(this.path(EXPERIENCE_JSON), [], (current) => [...current, entry]);
75
+ await atomicWrite(this.path(EXPERIENCE_MD), renderExperienceMarkdown(chain));
75
76
  }
76
77
  // ── Lifecycle ──
77
78
  /** Ensure session directory structure exists. */
78
79
  async init() {
79
- await mkdir(resolveStateDir(this.projectDir, this.sessionId), { recursive: true });
80
+ await mkdir(resolveStateDir(this.projectDir, this.workerSessionId), { recursive: true });
80
81
  }
81
82
  /**
82
- * Remove this manager's state files.
83
- * Callers that need trace rotation should invoke rotateTraces() from state.ts separately.
83
+ * Remove only this disposable worker's Hook directory.
84
+ * Durable Controller run/task/result/handoff records are outside this boundary.
84
85
  */
85
86
  async cleanup() {
86
- const dir = resolveStateDir(this.projectDir, this.sessionId);
87
- for (const rel of ["workflow.json", EXPERIENCE_JSON, EXPERIENCE_MD, "workflow/surgeon-attempts.json", "workflow/session-reads.json"]) {
88
- await unlink(join(dir, rel)).catch(() => { });
87
+ if (this.workerSessionId) {
88
+ await cleanupWorkerHookState(this.projectDir, this.workerSessionId);
89
+ return;
89
90
  }
91
+ await cwState(this.projectDir);
90
92
  }
91
93
  /** True when workflow.json started_at exceeds the staleness threshold. */
92
94
  async isStale(stalenessMs = DEFAULT_STALENESS_MS) {
93
- const wfPath = join(resolveStateDir(this.projectDir, this.sessionId), "workflow.json");
95
+ const wfPath = join(resolveStateDir(this.projectDir, this.workerSessionId), "workflow.json");
94
96
  try {
95
97
  const raw = await readFile(wfPath, "utf-8");
96
98
  const state = JSON.parse(raw);
@@ -110,7 +112,7 @@ export class DNAStateManager {
110
112
  }
111
113
  }
112
114
  path(rel) {
113
- return join(resolveStateDir(this.projectDir, this.sessionId), rel);
115
+ return join(resolveStateDir(this.projectDir, this.workerSessionId), rel);
114
116
  }
115
117
  }
116
118
  // ── Markdown rendering ────────────────────────────────────
@@ -21,6 +21,12 @@ export interface DNAWorkflowState {
21
21
  current_role: string;
22
22
  iteration: number;
23
23
  session_id: string;
24
+ /** Disposable Controller worker identity used as the Hook state key. */
25
+ worker_session_id?: string;
26
+ /** Durable correlation only; Hook state is never authoritative for these IDs. */
27
+ run_id?: string;
28
+ step_id?: string;
29
+ attempt_id?: string;
24
30
  started_at: string;
25
31
  inputs?: Record<string, string>;
26
32
  resolved_variables?: Record<string, string>;
@@ -145,8 +151,10 @@ export declare function appendCompletedArtifact(projectDir: string, stepId: stri
145
151
  artifact_id?: string;
146
152
  metadata?: Record<string, unknown>;
147
153
  }, sessionId?: string): Promise<void>;
148
- /** Atomic write: write to temp file then rename. */
154
+ /** Atomic owner-checked temp/fsync/rename write adapted from the approved store primitive. */
149
155
  export declare function atomicWrite(filePath: string, data: string): Promise<void>;
156
+ export declare function atomicAppend(filePath: string, data: string): Promise<void>;
157
+ export declare function atomicUpdateJson<T>(filePath: string, fallback: T, update: (current: T) => T | undefined): Promise<T>;
150
158
  /**
151
159
  * Read surgeon attempt state. Returns default state if not found.
152
160
  * @deprecated Prefer `DNAStateManager.readSurgeonAttempts()`.
@@ -157,6 +165,7 @@ export declare function readSurgeonAttempts(projectDir: string, sessionId?: stri
157
165
  * @deprecated Prefer `DNAStateManager.writeSurgeonAttempts()`.
158
166
  */
159
167
  export declare function writeSurgeonAttempts(projectDir: string, state: SurgeonAttemptState, sessionId?: string): Promise<void>;
168
+ export declare function updateSurgeonAttempts(projectDir: string, sessionId: string, update: (current: SurgeonAttemptState) => SurgeonAttemptState): Promise<SurgeonAttemptState>;
160
169
  /** Tracks which files a session has Read — for context gate enforcement */
161
170
  export interface SessionReadsState {
162
171
  session_id: string;
@@ -238,9 +247,16 @@ export declare function listSessions(projectDir: string): Promise<string[]>;
238
247
  */
239
248
  export declare function rotateTraces(projectDir: string): Promise<number>;
240
249
  /**
241
- * Clean up stale state from `.dna/state/sessions/` and root state.
242
- * Removes workflow.json files older than DEFAULT_STALENESS_MS (2h).
250
+ * Clean up stale worker state from `.dna/state/sessions/`.
251
+ * Removes worker directories older than DEFAULT_STALENESS_MS (2h).
243
252
  * Called on SessionStart to prevent state accumulation.
244
253
  * Fail-open: never throws.
245
254
  */
246
255
  export declare function cleanStaleState(projectDir: string): Promise<number>;
256
+ /**
257
+ * Delete exactly one disposable worker's Hook context.
258
+ *
259
+ * Controller ledgers live outside `.dna/state/sessions` and are never inspected
260
+ * or removed by this operation.
261
+ */
262
+ export declare function cleanupWorkerHookState(projectDir: string, workerSessionId: string): Promise<void>;