intentdna 1.5.13 → 1.5.15

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.
@@ -14,11 +14,11 @@
14
14
  * Step checkpoints are handled by the CLI, not here.
15
15
  */
16
16
  import type { ConstraintIR, RoleDef, CompletedArtifactEntry } from "../schema/types.js";
17
- import type { PreToolUseInput, PostToolUseInput, UserPromptSubmitInput, SubagentStopInput, NotificationInput, HookOutput } from "./protocol.js";
17
+ import type { PreToolUseInput, PostToolUseInput, UserPromptSubmitInput, SubagentStopInput, NotificationInput, EnforceResult } from "./protocol.js";
18
18
  /** SessionStart input fields */
19
19
  export interface SessionStartInput {
20
20
  cwd?: string;
21
- session_id?: string;
21
+ sessionId?: string;
22
22
  trigger?: string;
23
23
  }
24
24
  export interface EnforceState {
@@ -41,22 +41,22 @@ export interface EnforceState {
41
41
  *
42
42
  * Optional `roles` parameter provides output_schema enforcement.
43
43
  */
44
- export declare function enforcePreToolUse(ir: ConstraintIR, input: PreToolUseInput, state?: EnforceState, roles?: Record<string, RoleDef>): HookOutput;
44
+ export declare function enforcePreToolUse(ir: ConstraintIR, input: PreToolUseInput, state?: EnforceState, roles?: Record<string, RoleDef>): EnforceResult | null;
45
45
  /**
46
46
  * Enforce PostToolUse validators.
47
47
  * 1. Runs post-execution validators (audit)
48
48
  * 2. Verifies Bash write paths were within scope (defense-in-depth)
49
49
  */
50
- export declare function enforcePostToolUse(ir: ConstraintIR, input: PostToolUseInput): HookOutput;
50
+ export declare function enforcePostToolUse(ir: ConstraintIR, input: PostToolUseInput): EnforceResult | null;
51
51
  /**
52
52
  * Inject DNA policy reminders on each user prompt.
53
53
  * Only injects when there are high-priority directives.
54
54
  */
55
- export declare function enforceUserPromptSubmit(ir: ConstraintIR, _input: UserPromptSubmitInput): HookOutput;
55
+ export declare function enforceUserPromptSubmit(ir: ConstraintIR, _input: UserPromptSubmitInput): EnforceResult | null;
56
56
  /**
57
57
  * Audit role scope compliance when a subagent finishes.
58
58
  */
59
- export declare function enforceSubagentStop(ir: ConstraintIR, input: SubagentStopInput): HookOutput;
59
+ export declare function enforceSubagentStop(ir: ConstraintIR, input: SubagentStopInput): EnforceResult | null;
60
60
  /**
61
61
  * Preserve DNA directive summary before context compaction.
62
62
  * Includes: high-priority directives, role scope map, active workflow state.
@@ -65,21 +65,21 @@ export declare function enforcePreCompact(ir: ConstraintIR, workflowContext?: {
65
65
  workflow: string;
66
66
  current_step: string;
67
67
  current_role: string;
68
- } | null): HookOutput;
68
+ } | null): EnforceResult | null;
69
69
  /**
70
70
  * Check if a notification is a DNA violation and return audit info.
71
71
  * The CLI handles actual audit file writes.
72
72
  */
73
- export declare function enforceNotification(ir: ConstraintIR, input: NotificationInput): HookOutput;
73
+ export declare function enforceNotification(ir: ConstraintIR, input: NotificationInput): EnforceResult | null;
74
74
  /**
75
75
  * Initialize DNA state and inject policy summary on session start.
76
76
  * Returns silent if no DNA policies are active.
77
77
  */
78
- export declare function enforceSessionStart(ir: ConstraintIR, input: SessionStartInput): HookOutput;
78
+ export declare function enforceSessionStart(ir: ConstraintIR, input: SessionStartInput): EnforceResult | null;
79
79
  /** Stop enforcement input — includes workflow awareness */
80
80
  export interface StopEnforceInput {
81
81
  cwd?: string;
82
- session_id?: string;
82
+ sessionId?: string;
83
83
  stop_reason?: string;
84
84
  }
85
85
  /** Workflow state context for stop enforcement */
@@ -103,7 +103,7 @@ export interface StopWorkflowContext {
103
103
  * Block when:
104
104
  * - Workflow active + unmet checkpoints for current step
105
105
  */
106
- export declare function enforceStop(ir: ConstraintIR, input: StopEnforceInput, workflowState: StopWorkflowContext | null): HookOutput;
106
+ export declare function enforceStop(ir: ConstraintIR, input: StopEnforceInput, workflowState: StopWorkflowContext | null): EnforceResult | null;
107
107
  /**
108
108
  * Enforce handoff produces — verify that the current step has produced
109
109
  * its declared artifacts. Called at stop/checkpoint time.
@@ -113,7 +113,7 @@ export declare function enforceHandoffProduces(ir: ConstraintIR, wfState: {
113
113
  current_step: string;
114
114
  workflow: string;
115
115
  completed_artifacts?: CompletedArtifactEntry[];
116
- }): HookOutput | null;
116
+ }): EnforceResult | null;
117
117
  /** Check if a file path is allowed by a list of write globs (prefix matching). */
118
118
  export declare function checkWriteAllowed(filePath: string, allowedGlobs: string[]): boolean;
119
119
  /**
@@ -157,3 +157,4 @@ export interface ContextReadinessResult {
157
157
  * Pure function — no I/O.
158
158
  */
159
159
  export declare function checkContextReadiness(sessionReads: string[], required: string[]): ContextReadinessResult;
160
+ export declare function checkWorkflowBoundary(workflowCompleted: boolean, targetSkill: string): EnforceResult;
@@ -13,7 +13,7 @@
13
13
  *
14
14
  * Step checkpoints are handled by the CLI, not here.
15
15
  */
16
- import { allowOutput, blockOutput, escalateOutput, silentOutput } from "./protocol.js";
16
+ import { allowOutput, blockOutput, escalateOutput } from "./protocol.js";
17
17
  import { relative, normalize } from "node:path";
18
18
  // ── PreToolUse Enforcement ─────────────────────────────────
19
19
  /**
@@ -27,43 +27,42 @@ export function enforcePreToolUse(ir, input, state, roles) {
27
27
  if (state?.workflowState && ir.workflows_ir) {
28
28
  const stepRuleResult = enforceStepRules(ir, input, state);
29
29
  if (stepRuleResult !== null)
30
- return stepRuleResult;
30
+ return { output: stepRuleResult, trace: { matched_rule: "step_rule", step_id: state.workflowState.current_step } };
31
31
  }
32
32
  // Layer 2: Role scope
33
33
  if (input.agent_type && ir.roles_scope_map && ir.roles_scope_map.length > 0) {
34
- // G4: Check if scope is relaxed by iteration-based rule
35
34
  const relaxed = isStepScopeRelaxed(ir, state);
36
35
  if (!relaxed) {
37
36
  const result = enforceRoleScope(ir.roles_scope_map, input, getStepAdditionalPaths(ir, state));
38
37
  if (result)
39
- return result;
38
+ return { output: result, trace: { matched_rule: "scope", role: input.agent_type } };
40
39
  }
41
40
  }
42
41
  // Layer 3: Tool filters
43
42
  if (ir.tool_filters.length > 0) {
44
43
  const result = enforceToolFilters(ir.tool_filters, input);
45
44
  if (result)
46
- return result;
45
+ return { output: result, trace: { matched_rule: "filter" } };
47
46
  }
48
47
  // Layer 4: Pre-execution gates
49
48
  if (ir.pre_execution_gates.length > 0) {
50
49
  const result = enforceGates(ir.pre_execution_gates, input);
51
50
  if (result)
52
- return result;
51
+ return { output: result, trace: { matched_rule: "gate" } };
53
52
  }
54
53
  // Layer 5: Output schema
55
54
  if (roles && input.tool_name === "Write") {
56
55
  const result = enforceOutputSchema(roles, input);
57
56
  if (result)
58
- return result;
57
+ return { output: result, trace: { matched_rule: "output_schema" } };
59
58
  }
60
59
  // Layer 6: Handoff — check consumed artifacts are available
61
60
  if (state?.workflowState && ir.workflows_ir) {
62
61
  const result = enforceHandoffConsumes(ir, state.workflowState, state.existingArtifactPaths);
63
62
  if (result)
64
- return result;
63
+ return { output: result, trace: { matched_rule: "handoff", step_id: state.workflowState.current_step } };
65
64
  }
66
- return silentOutput();
65
+ return null;
67
66
  }
68
67
  // ── PostToolUse Enforcement ────────────────────────────────
69
68
  /**
@@ -92,8 +91,13 @@ export function enforcePostToolUse(ir, input) {
92
91
  }
93
92
  }
94
93
  if (lines.length === 0)
95
- return silentOutput();
96
- return allowOutput(lines.join("\n"));
94
+ return null;
95
+ // Audit-only path: validator entries emit audit lines, plus bash scope WARN lines.
96
+ // If any bash scope violation was found use "scope" (real path match); otherwise
97
+ // this is validator-only output — use "validator" for accurate trace semantics.
98
+ const hasScopeViolation = lines.some(l => l.startsWith("WARN"));
99
+ const matchedRule = hasScopeViolation ? "scope" : "validator";
100
+ return { output: allowOutput(lines.join("\n")), trace: { matched_rule: matchedRule } };
97
101
  }
98
102
  // ── UserPromptSubmit Enforcement ───────────────────────────
99
103
  /**
@@ -103,7 +107,7 @@ export function enforcePostToolUse(ir, input) {
103
107
  export function enforceUserPromptSubmit(ir, _input) {
104
108
  const highPriority = ir.prompt_directives.filter(d => d.priority === "high");
105
109
  if (highPriority.length === 0)
106
- return silentOutput();
110
+ return null;
107
111
  const lines = [
108
112
  "<system-reminder>[Intent DNA] Active high-priority policies:",
109
113
  ];
@@ -111,7 +115,7 @@ export function enforceUserPromptSubmit(ir, _input) {
111
115
  lines.push(` - [${d.source_gene}] ${d.text}`);
112
116
  }
113
117
  lines.push("</system-reminder>");
114
- return allowOutput(lines.join("\n"));
118
+ return { output: allowOutput(lines.join("\n")) };
115
119
  }
116
120
  // ── SubagentStop Enforcement ───────────────────────────────
117
121
  /**
@@ -120,9 +124,9 @@ export function enforceUserPromptSubmit(ir, _input) {
120
124
  export function enforceSubagentStop(ir, input) {
121
125
  const rolesScopeMap = ir.roles_scope_map;
122
126
  if (!rolesScopeMap || rolesScopeMap.length === 0)
123
- return silentOutput();
127
+ return null;
124
128
  if (!input.agent_type)
125
- return silentOutput();
129
+ return null;
126
130
  for (const entry of rolesScopeMap) {
127
131
  const agentTypeName = `dna-${toKebabCase(entry.role_name)}`;
128
132
  if (input.agent_type === agentTypeName) {
@@ -130,10 +134,15 @@ export function enforceSubagentStop(ir, input) {
130
134
  const scopeDesc = writeGlobs.length > 0
131
135
  ? `write:[${writeGlobs.join(",")}]`
132
136
  : "write:[]";
133
- return allowOutput(`[Intent DNA] SubagentStop audit: role=${entry.role_name} agent=${input.agent_name ?? "unknown"} scope=${scopeDesc}`);
137
+ return {
138
+ output: allowOutput(`[Intent DNA] SubagentStop audit: role=${entry.role_name} agent=${input.agent_name ?? "unknown"} scope=${scopeDesc}`),
139
+ trace: { matched_rule: "scope", role: entry.role_name },
140
+ };
134
141
  }
135
142
  }
136
- return allowOutput(`[Intent DNA] SubagentStop: unknown role for agent_type=${input.agent_type}`);
143
+ return {
144
+ output: allowOutput(`[Intent DNA] SubagentStop: unknown role for agent_type=${input.agent_type}`),
145
+ };
137
146
  }
138
147
  // ── PreCompact Enforcement ─────────────────────────────────
139
148
  /**
@@ -145,7 +154,7 @@ export function enforcePreCompact(ir, workflowContext) {
145
154
  const hasRoles = ir.roles_scope_map && ir.roles_scope_map.length > 0;
146
155
  const hasWorkflow = !!workflowContext;
147
156
  if (!hasDirectives && !hasRoles && !hasWorkflow)
148
- return silentOutput();
157
+ return null;
149
158
  const critical = ir.prompt_directives.filter(d => d.priority === "high");
150
159
  const standard = ir.prompt_directives.filter(d => d.priority === "medium");
151
160
  const lines = [
@@ -176,7 +185,7 @@ export function enforcePreCompact(ir, workflowContext) {
176
185
  if (hasWorkflow) {
177
186
  lines.push(` Active workflow: ${workflowContext.workflow} (step: ${workflowContext.current_step}, role: ${workflowContext.current_role})`);
178
187
  }
179
- return allowOutput(lines.join("\n"));
188
+ return { output: allowOutput(lines.join("\n")) };
180
189
  }
181
190
  // ── Notification Enforcement ───────────────────────────────
182
191
  /**
@@ -187,12 +196,14 @@ export function enforceNotification(ir, input) {
187
196
  const blockGates = ir.pre_execution_gates.filter(g => g.action === "block" || g.action === "escalate");
188
197
  const blockFilters = ir.tool_filters.filter(f => f.action === "remove");
189
198
  if (blockGates.length === 0 && blockFilters.length === 0)
190
- return silentOutput();
199
+ return null;
191
200
  const message = input.message ?? "";
192
201
  if (message.includes("Intent DNA")) {
193
- return allowOutput(`[Intent DNA] Notification: violation detected — ${input.title ?? "untitled"}: ${message}`);
202
+ return {
203
+ output: allowOutput(`[Intent DNA] Notification: violation detected — ${input.title ?? "untitled"}: ${message}`),
204
+ };
194
205
  }
195
- return silentOutput();
206
+ return null;
196
207
  }
197
208
  // ── SessionStart Enforcement ───────────────────────────────
198
209
  /**
@@ -200,16 +211,14 @@ export function enforceNotification(ir, input) {
200
211
  * Returns silent if no DNA policies are active.
201
212
  */
202
213
  export function enforceSessionStart(ir, input) {
203
- // Check if there's anything meaningful to report
204
214
  const directiveCount = ir.prompt_directives.length;
205
215
  const gateCount = ir.pre_execution_gates.length;
206
216
  const filterCount = ir.tool_filters.length;
207
217
  const roleCount = ir.roles_scope_map?.length ?? 0;
208
218
  const workflowCount = ir.workflows_ir?.length ?? 0;
209
219
  if (directiveCount === 0 && gateCount === 0 && filterCount === 0 && roleCount === 0) {
210
- return silentOutput();
220
+ return null;
211
221
  }
212
- // Build policy summary
213
222
  const lines = [
214
223
  "[Intent DNA] Session initialized — active governance:",
215
224
  ];
@@ -230,7 +239,7 @@ export function enforceSessionStart(ir, input) {
230
239
  if (stats.length > 0) {
231
240
  lines.push(` Enforcement: ${stats.join(", ")}`);
232
241
  }
233
- return allowOutput(lines.join("\n"), "SessionStart");
242
+ return { output: allowOutput(lines.join("\n"), "SessionStart") };
234
243
  }
235
244
  /**
236
245
  * Enforce Stop hook — verify workflow checkpoint completion.
@@ -245,31 +254,25 @@ export function enforceSessionStart(ir, input) {
245
254
  * - Workflow active + unmet checkpoints for current step
246
255
  */
247
256
  export function enforceStop(ir, input, workflowState) {
248
- // Safety valve 1: no workflow state → silent
249
257
  if (!workflowState || !workflowState.active) {
250
- return silentOutput();
258
+ return null;
251
259
  }
252
- // Safety valve 2: context_limit stops → NEVER block
253
260
  const reason = input.stop_reason ?? "";
254
261
  if (reason.includes("context_limit") || reason.includes("context_window")) {
255
- return silentOutput();
262
+ return null;
256
263
  }
257
- // Safety valve 3: user abort → don't block
258
264
  if (reason === "user_abort" || reason === "sigint") {
259
- return silentOutput();
265
+ return null;
260
266
  }
261
- // Safety valve 4: stale state (>2h)
262
267
  const STALE_MS = 2 * 60 * 60 * 1000;
263
268
  if (workflowState.started_at) {
264
269
  const age = Date.now() - new Date(workflowState.started_at).getTime();
265
270
  if (age > STALE_MS) {
266
- return silentOutput();
271
+ return null;
267
272
  }
268
273
  }
269
- // Check for unmet checkpoints in current step
270
274
  const checkpoints = ir.step_checkpoints ?? [];
271
275
  const currentStepCheckpoints = checkpoints.filter(cp => cp.step_id === workflowState.current_step);
272
- // Also check workflows_ir for the active workflow's checkpoints
273
276
  let workflowCheckpoints = currentStepCheckpoints;
274
277
  if (ir.workflows_ir && ir.workflows_ir.length > 0) {
275
278
  const activeWf = ir.workflows_ir.find(w => w.workflow_name === workflowState.workflow);
@@ -281,27 +284,27 @@ export function enforceStop(ir, input, workflowState) {
281
284
  }
282
285
  }
283
286
  if (workflowCheckpoints.length === 0) {
284
- return silentOutput(); // No checkpoints for current step
287
+ return null;
285
288
  }
286
- // Build list of blocking checkpoints
287
289
  const blocking = workflowCheckpoints.flatMap(cp => cp.checkpoints.filter(c => (c.action ?? "block") === "block"));
288
290
  if (blocking.length === 0) {
289
- // No checkpoint blocking — check handoff produces
290
291
  if (workflowState.workflow) {
291
292
  const handoffResult = enforceHandoffProduces(ir, {
292
293
  current_step: workflowState.current_step,
293
294
  workflow: workflowState.workflow,
294
295
  completed_artifacts: workflowState.completed_artifacts,
295
296
  });
296
- if (handoffResult && !handoffResult.continue)
297
+ if (handoffResult)
297
298
  return handoffResult;
298
299
  }
299
- return silentOutput();
300
+ return null;
300
301
  }
301
- // Block: workflow active with unmet checkpoints
302
302
  const messages = blocking.map(c => c.message);
303
- return blockOutput(`[Intent DNA] Workflow '${workflowState.workflow}' has unmet checkpoints at step '${workflowState.current_step}':\n` +
304
- messages.map(m => ` - ${m}`).join("\n"));
303
+ return {
304
+ output: blockOutput(`[Intent DNA] Workflow '${workflowState.workflow}' has unmet checkpoints at step '${workflowState.current_step}':\n` +
305
+ messages.map(m => ` - ${m}`).join("\n")),
306
+ trace: { matched_rule: "handoff", step_id: workflowState.current_step },
307
+ };
305
308
  }
306
309
  // ── G4: State-driven Step Enforcement ─────────────────────
307
310
  /** Write tools that step read_only rule should block */
@@ -393,10 +396,12 @@ export function enforceHandoffProduces(ir, wfState) {
393
396
  if (!currentEntry?.produces || currentEntry.produces.length === 0)
394
397
  return null;
395
398
  const currentArtifacts = (wfState.completed_artifacts ?? []).find(a => a.step_id === wfState.current_step);
396
- // Check that each produced artifact with a path has been recorded
397
399
  for (const produced of currentEntry.produces) {
398
400
  if (produced.path && (!currentArtifacts || !currentArtifacts.artifacts.some(a => a.path === produced.path))) {
399
- return blockOutput(`[Intent DNA] Step '${wfState.current_step}' must produce artifact '${produced.description}' (path: ${produced.path}) before proceeding.`);
401
+ return {
402
+ output: blockOutput(`[Intent DNA] Step '${wfState.current_step}' must produce artifact '${produced.description}' (path: ${produced.path}) before proceeding.`),
403
+ trace: { matched_rule: "handoff", step_id: wfState.current_step },
404
+ };
400
405
  }
401
406
  }
402
407
  return null;
@@ -712,3 +717,13 @@ export function checkContextReadiness(sessionReads, required) {
712
717
  const missing = required.filter(f => !readSet.has(f));
713
718
  return { ready: missing.length === 0, missing };
714
719
  }
720
+ // ── Workflow Boundary Gate ────────────────────────────────────
721
+ export function checkWorkflowBoundary(workflowCompleted, targetSkill) {
722
+ if (workflowCompleted) {
723
+ return {
724
+ output: blockOutput(`[Intent DNA] Workflow completed. Cannot start skill '${targetSkill}'.`),
725
+ trace: { matched_rule: "workflow_boundary" },
726
+ };
727
+ }
728
+ return { output: allowOutput() };
729
+ }
@@ -54,6 +54,17 @@ export interface HookOutput {
54
54
  permissionDecision?: "ask" | "allow" | "deny";
55
55
  };
56
56
  }
57
+ export type MatchedRule = "scope" | "filter" | "gate" | "schema" | "validator" | "handoff" | "step_rule" | "output_schema" | "context_gate" | "reflection_gate" | "workflow_boundary";
58
+ export interface TraceMetadata {
59
+ matched_rule: MatchedRule;
60
+ gene_id?: string;
61
+ step_id?: string;
62
+ role?: string;
63
+ }
64
+ export interface EnforceResult {
65
+ output: HookOutput;
66
+ trace?: TraceMetadata;
67
+ }
57
68
  /** Create an allow output, optionally injecting text into the conversation. */
58
69
  export declare function allowOutput(additionalContext?: string, hookEventName?: string): HookOutput;
59
70
  /** Create a block output with reason. */
@@ -0,0 +1,22 @@
1
+ /**
2
+ * Hook input validation — CC protocol boundary.
3
+ * Zero external dependencies. Hand-written validators.
4
+ * Fail-open: invalid input → log warning + return { valid: false, ... }
5
+ */
6
+ export interface ValidationResult {
7
+ valid: boolean;
8
+ errors: string[];
9
+ normalized?: Record<string, unknown>;
10
+ }
11
+ /**
12
+ * Validate hook input against expected schema for each event type.
13
+ * Returns normalized input (camelCase) on success.
14
+ */
15
+ export declare function validateHookInput(event: string, raw: unknown): ValidationResult;
16
+ /**
17
+ * Normalize session_id field from various formats:
18
+ * - session_id (snake_case from CC)
19
+ * - sessionId (camelCase internal)
20
+ * - CLAUDE_SESSION_ID (env var fallback)
21
+ */
22
+ export declare function normalizeSessionId(input: Record<string, unknown>): string | undefined;
@@ -0,0 +1,86 @@
1
+ /**
2
+ * Hook input validation — CC protocol boundary.
3
+ * Zero external dependencies. Hand-written validators.
4
+ * Fail-open: invalid input → log warning + return { valid: false, ... }
5
+ */
6
+ /**
7
+ * Validate hook input against expected schema for each event type.
8
+ * Returns normalized input (camelCase) on success.
9
+ */
10
+ export function validateHookInput(event, raw) {
11
+ if (typeof raw !== "object" || raw === null) {
12
+ return { valid: false, errors: ["Input must be an object"] };
13
+ }
14
+ const input = raw;
15
+ const errors = [];
16
+ const normalized = { ...input };
17
+ // Normalize session_id to sessionId
18
+ const sessionId = normalizeSessionId(input);
19
+ if (sessionId !== undefined) {
20
+ normalized.sessionId = sessionId;
21
+ delete normalized.session_id;
22
+ }
23
+ switch (event) {
24
+ case "PreToolUse":
25
+ if (typeof input.tool_name !== "string") {
26
+ errors.push("PreToolUse: tool_name must be a string");
27
+ }
28
+ if (typeof input.tool_input !== "object" || input.tool_input === null) {
29
+ errors.push("PreToolUse: tool_input must be an object");
30
+ }
31
+ break;
32
+ case "PostToolUse":
33
+ if (typeof input.tool_name !== "string") {
34
+ errors.push("PostToolUse: tool_name must be a string");
35
+ }
36
+ if (typeof input.tool_input !== "object" || input.tool_input === null) {
37
+ errors.push("PostToolUse: tool_input must be an object");
38
+ }
39
+ // tool_output is optional
40
+ break;
41
+ case "SessionStart":
42
+ // session_id is optional but should be normalized if present
43
+ break;
44
+ case "SubagentStop":
45
+ // agent_type or agent_name should be present but not strictly required
46
+ break;
47
+ case "Notification":
48
+ if (typeof input.title !== "string" && input.title !== undefined) {
49
+ errors.push("Notification: title must be a string if present");
50
+ }
51
+ if (typeof input.message !== "string" && input.message !== undefined) {
52
+ errors.push("Notification: message must be a string if present");
53
+ }
54
+ break;
55
+ case "UserPromptSubmit":
56
+ case "PreCompact":
57
+ case "Stop":
58
+ // Minimal validation — these events have no required fields beyond base
59
+ break;
60
+ default:
61
+ errors.push(`Unknown event type: ${event}`);
62
+ }
63
+ return {
64
+ valid: errors.length === 0,
65
+ errors,
66
+ normalized: errors.length === 0 ? normalized : undefined,
67
+ };
68
+ }
69
+ /**
70
+ * Normalize session_id field from various formats:
71
+ * - session_id (snake_case from CC)
72
+ * - sessionId (camelCase internal)
73
+ * - CLAUDE_SESSION_ID (env var fallback)
74
+ */
75
+ export function normalizeSessionId(input) {
76
+ if (typeof input.sessionId === "string") {
77
+ return input.sessionId;
78
+ }
79
+ if (typeof input.session_id === "string") {
80
+ return input.session_id;
81
+ }
82
+ if (typeof input.CLAUDE_SESSION_ID === "string") {
83
+ return input.CLAUDE_SESSION_ID;
84
+ }
85
+ return undefined;
86
+ }
@@ -0,0 +1,48 @@
1
+ /**
2
+ * Intent DNA — DNAStateManager
3
+ *
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.
6
+ * Adds experience chain persistence (markdown + structured JSON) and
7
+ * lifecycle operations (init / cleanup / isStale) that state.ts lacks.
8
+ *
9
+ * Existing state.ts functions remain the canonical I/O primitives — this
10
+ * class delegates to them. Prefer this class in new code; state.ts exports
11
+ * are @deprecated for direct use.
12
+ */
13
+ import type { DNAWorkflowState, AuditEntry, SurgeonAttemptState, SessionReadsState } from "./state.js";
14
+ /** Single entry in the experience chain — one per failed surgeon round. */
15
+ export interface ExperienceEntry {
16
+ round: number;
17
+ analysis: string;
18
+ attempt: string;
19
+ result: string;
20
+ lesson: string;
21
+ timestamp: string;
22
+ }
23
+ export declare class DNAStateManager {
24
+ private readonly projectDir;
25
+ private readonly sessionId?;
26
+ constructor(projectDir: string, sessionId?: string | undefined);
27
+ readWorkflowState(stalenessMs?: number): Promise<DNAWorkflowState | null>;
28
+ writeWorkflowState(state: DNAWorkflowState): Promise<void>;
29
+ clearWorkflowState(): Promise<void>;
30
+ appendAudit(entry: AuditEntry): Promise<void>;
31
+ readSurgeonAttempts(): Promise<SurgeonAttemptState>;
32
+ writeSurgeonAttempts(state: SurgeonAttemptState): Promise<void>;
33
+ readSessionReads(): Promise<SessionReadsState>;
34
+ appendSessionRead(filePath: string): Promise<void>;
35
+ readExperienceChain(): Promise<ExperienceEntry[]>;
36
+ writeExperienceChain(chain: ExperienceEntry[]): Promise<void>;
37
+ appendExperience(entry: ExperienceEntry): Promise<void>;
38
+ /** Ensure session directory structure exists. */
39
+ init(): Promise<void>;
40
+ /**
41
+ * Remove this manager's state files.
42
+ * Callers that need trace rotation should invoke rotateTraces() from state.ts separately.
43
+ */
44
+ cleanup(): Promise<void>;
45
+ /** True when workflow.json started_at exceeds the staleness threshold. */
46
+ isStale(stalenessMs?: number): Promise<boolean>;
47
+ private path;
48
+ }