intentdna 1.0.0

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 (184) hide show
  1. package/LICENSE +69 -0
  2. package/README.md +251 -0
  3. package/dist/audit/index.d.ts +109 -0
  4. package/dist/audit/index.d.ts.map +1 -0
  5. package/dist/audit/index.js +144 -0
  6. package/dist/audit/index.js.map +1 -0
  7. package/dist/cli/commands/compile.d.ts +14 -0
  8. package/dist/cli/commands/compile.d.ts.map +1 -0
  9. package/dist/cli/commands/compile.js +89 -0
  10. package/dist/cli/commands/compile.js.map +1 -0
  11. package/dist/cli/commands/epigenetic.d.ts +20 -0
  12. package/dist/cli/commands/epigenetic.d.ts.map +1 -0
  13. package/dist/cli/commands/epigenetic.js +112 -0
  14. package/dist/cli/commands/epigenetic.js.map +1 -0
  15. package/dist/cli/commands/evolve.d.ts +12 -0
  16. package/dist/cli/commands/evolve.d.ts.map +1 -0
  17. package/dist/cli/commands/evolve.js +52 -0
  18. package/dist/cli/commands/evolve.js.map +1 -0
  19. package/dist/cli/commands/guard.d.ts +22 -0
  20. package/dist/cli/commands/guard.d.ts.map +1 -0
  21. package/dist/cli/commands/guard.js +257 -0
  22. package/dist/cli/commands/guard.js.map +1 -0
  23. package/dist/cli/commands/import.d.ts +71 -0
  24. package/dist/cli/commands/import.d.ts.map +1 -0
  25. package/dist/cli/commands/import.js +428 -0
  26. package/dist/cli/commands/import.js.map +1 -0
  27. package/dist/cli/commands/init.d.ts +13 -0
  28. package/dist/cli/commands/init.d.ts.map +1 -0
  29. package/dist/cli/commands/init.js +171 -0
  30. package/dist/cli/commands/init.js.map +1 -0
  31. package/dist/cli/commands/run.d.ts +59 -0
  32. package/dist/cli/commands/run.d.ts.map +1 -0
  33. package/dist/cli/commands/run.js +350 -0
  34. package/dist/cli/commands/run.js.map +1 -0
  35. package/dist/cli/commands/show.d.ts +12 -0
  36. package/dist/cli/commands/show.d.ts.map +1 -0
  37. package/dist/cli/commands/show.js +78 -0
  38. package/dist/cli/commands/show.js.map +1 -0
  39. package/dist/cli/commands/sync.d.ts +31 -0
  40. package/dist/cli/commands/sync.d.ts.map +1 -0
  41. package/dist/cli/commands/sync.js +307 -0
  42. package/dist/cli/commands/sync.js.map +1 -0
  43. package/dist/cli/commands/validate.d.ts +11 -0
  44. package/dist/cli/commands/validate.d.ts.map +1 -0
  45. package/dist/cli/commands/validate.js +30 -0
  46. package/dist/cli/commands/validate.js.map +1 -0
  47. package/dist/cli/commands/verify.d.ts +33 -0
  48. package/dist/cli/commands/verify.d.ts.map +1 -0
  49. package/dist/cli/commands/verify.js +102 -0
  50. package/dist/cli/commands/verify.js.map +1 -0
  51. package/dist/cli/index.d.ts +15 -0
  52. package/dist/cli/index.d.ts.map +1 -0
  53. package/dist/cli/index.js +358 -0
  54. package/dist/cli/index.js.map +1 -0
  55. package/dist/cli/util/format.d.ts +24 -0
  56. package/dist/cli/util/format.d.ts.map +1 -0
  57. package/dist/cli/util/format.js +67 -0
  58. package/dist/cli/util/format.js.map +1 -0
  59. package/dist/cli/util/prompt.d.ts +11 -0
  60. package/dist/cli/util/prompt.d.ts.map +1 -0
  61. package/dist/cli/util/prompt.js +51 -0
  62. package/dist/cli/util/prompt.js.map +1 -0
  63. package/dist/compiler/activate.d.ts +29 -0
  64. package/dist/compiler/activate.d.ts.map +1 -0
  65. package/dist/compiler/activate.js +141 -0
  66. package/dist/compiler/activate.js.map +1 -0
  67. package/dist/compiler/cascade.d.ts +27 -0
  68. package/dist/compiler/cascade.d.ts.map +1 -0
  69. package/dist/compiler/cascade.js +124 -0
  70. package/dist/compiler/cascade.js.map +1 -0
  71. package/dist/compiler/compile.d.ts +17 -0
  72. package/dist/compiler/compile.d.ts.map +1 -0
  73. package/dist/compiler/compile.js +219 -0
  74. package/dist/compiler/compile.js.map +1 -0
  75. package/dist/compiler/index.d.ts +28 -0
  76. package/dist/compiler/index.d.ts.map +1 -0
  77. package/dist/compiler/index.js +51 -0
  78. package/dist/compiler/index.js.map +1 -0
  79. package/dist/compiler/workflow.d.ts +37 -0
  80. package/dist/compiler/workflow.d.ts.map +1 -0
  81. package/dist/compiler/workflow.js +274 -0
  82. package/dist/compiler/workflow.js.map +1 -0
  83. package/dist/evolution/collector.d.ts +52 -0
  84. package/dist/evolution/collector.d.ts.map +1 -0
  85. package/dist/evolution/collector.js +72 -0
  86. package/dist/evolution/collector.js.map +1 -0
  87. package/dist/evolution/fitness.d.ts +33 -0
  88. package/dist/evolution/fitness.d.ts.map +1 -0
  89. package/dist/evolution/fitness.js +96 -0
  90. package/dist/evolution/fitness.js.map +1 -0
  91. package/dist/evolution/index.d.ts +53 -0
  92. package/dist/evolution/index.d.ts.map +1 -0
  93. package/dist/evolution/index.js +69 -0
  94. package/dist/evolution/index.js.map +1 -0
  95. package/dist/evolution/marker-gen.d.ts +31 -0
  96. package/dist/evolution/marker-gen.d.ts.map +1 -0
  97. package/dist/evolution/marker-gen.js +141 -0
  98. package/dist/evolution/marker-gen.js.map +1 -0
  99. package/dist/evolution/store.d.ts +24 -0
  100. package/dist/evolution/store.d.ts.map +1 -0
  101. package/dist/evolution/store.js +78 -0
  102. package/dist/evolution/store.js.map +1 -0
  103. package/dist/evolution/types.d.ts +73 -0
  104. package/dist/evolution/types.d.ts.map +1 -0
  105. package/dist/evolution/types.js +14 -0
  106. package/dist/evolution/types.js.map +1 -0
  107. package/dist/index.d.ts +4 -0
  108. package/dist/index.d.ts.map +1 -0
  109. package/dist/index.js +4 -0
  110. package/dist/index.js.map +1 -0
  111. package/dist/log/logger.d.ts +15 -0
  112. package/dist/log/logger.d.ts.map +1 -0
  113. package/dist/log/logger.js +36 -0
  114. package/dist/log/logger.js.map +1 -0
  115. package/dist/runtime/agent-md.d.ts +56 -0
  116. package/dist/runtime/agent-md.d.ts.map +1 -0
  117. package/dist/runtime/agent-md.js +257 -0
  118. package/dist/runtime/agent-md.js.map +1 -0
  119. package/dist/runtime/claude-sdk.d.ts +51 -0
  120. package/dist/runtime/claude-sdk.d.ts.map +1 -0
  121. package/dist/runtime/claude-sdk.js +78 -0
  122. package/dist/runtime/claude-sdk.js.map +1 -0
  123. package/dist/runtime/hooks.d.ts +67 -0
  124. package/dist/runtime/hooks.d.ts.map +1 -0
  125. package/dist/runtime/hooks.js +645 -0
  126. package/dist/runtime/hooks.js.map +1 -0
  127. package/dist/runtime/index.d.ts +9 -0
  128. package/dist/runtime/index.d.ts.map +1 -0
  129. package/dist/runtime/index.js +5 -0
  130. package/dist/runtime/index.js.map +1 -0
  131. package/dist/runtime/markdown.d.ts +36 -0
  132. package/dist/runtime/markdown.d.ts.map +1 -0
  133. package/dist/runtime/markdown.js +214 -0
  134. package/dist/runtime/markdown.js.map +1 -0
  135. package/dist/runtime/settings-adapter.d.ts +33 -0
  136. package/dist/runtime/settings-adapter.d.ts.map +1 -0
  137. package/dist/runtime/settings-adapter.js +78 -0
  138. package/dist/runtime/settings-adapter.js.map +1 -0
  139. package/dist/runtime/skill-adapter.d.ts +22 -0
  140. package/dist/runtime/skill-adapter.d.ts.map +1 -0
  141. package/dist/runtime/skill-adapter.js +170 -0
  142. package/dist/runtime/skill-adapter.js.map +1 -0
  143. package/dist/runtime/workflow-runner.d.ts +53 -0
  144. package/dist/runtime/workflow-runner.d.ts.map +1 -0
  145. package/dist/runtime/workflow-runner.js +420 -0
  146. package/dist/runtime/workflow-runner.js.map +1 -0
  147. package/dist/schema/index.d.ts +2 -0
  148. package/dist/schema/index.d.ts.map +1 -0
  149. package/dist/schema/index.js +2 -0
  150. package/dist/schema/index.js.map +1 -0
  151. package/dist/schema/types.d.ts +275 -0
  152. package/dist/schema/types.d.ts.map +1 -0
  153. package/dist/schema/types.js +11 -0
  154. package/dist/schema/types.js.map +1 -0
  155. package/dist/schema/validate.d.ts +20 -0
  156. package/dist/schema/validate.d.ts.map +1 -0
  157. package/dist/schema/validate.js +379 -0
  158. package/dist/schema/validate.js.map +1 -0
  159. package/dist/schema/yaml-parser.d.ts +12 -0
  160. package/dist/schema/yaml-parser.d.ts.map +1 -0
  161. package/dist/schema/yaml-parser.js +298 -0
  162. package/dist/schema/yaml-parser.js.map +1 -0
  163. package/dist/species/default.dna.json +62 -0
  164. package/dist/templates/api-backend.dna.yaml +77 -0
  165. package/dist/templates/brainstorming-first.dna.yaml +71 -0
  166. package/dist/templates/branch-finishing.dna.yaml +54 -0
  167. package/dist/templates/code-review-pipeline.dna.yaml +91 -0
  168. package/dist/templates/devops-cicd.dna.yaml +67 -0
  169. package/dist/templates/documentation-writer.dna.yaml +71 -0
  170. package/dist/templates/enterprise-baseline.dna.yaml +66 -0
  171. package/dist/templates/frontend-quality.dna.yaml +88 -0
  172. package/dist/templates/full-pipeline.dna.yaml +129 -0
  173. package/dist/templates/mobile-dev.dna.yaml +68 -0
  174. package/dist/templates/monorepo-governance.dna.yaml +60 -0
  175. package/dist/templates/multi-perspective-review.dna.yaml +109 -0
  176. package/dist/templates/pr-submitter.dna.yaml +55 -0
  177. package/dist/templates/safe-refactoring.dna.yaml +65 -0
  178. package/dist/templates/secure-dev.dna.yaml +90 -0
  179. package/dist/templates/subagent-parallel.dna.yaml +98 -0
  180. package/dist/templates/systematic-debugging.dna.yaml +55 -0
  181. package/dist/templates/tdd-strict.dna.yaml +69 -0
  182. package/dist/templates/verification-loop.dna.yaml +53 -0
  183. package/dist/templates/yolo-with-guardrails.dna.yaml +56 -0
  184. package/package.json +43 -0
@@ -0,0 +1,645 @@
1
+ /**
2
+ * Intent DNA — Hook Adapter
3
+ *
4
+ * Compiles Constraint IR into shell scripts for Claude Code hooks.
5
+ * These scripts enforce DNA constraints at the harness level (deterministic),
6
+ * not the prompt level (probabilistic).
7
+ *
8
+ * Output:
9
+ * - PreToolUse hook: blocks/escalates tool calls based on pre_execution_gates + tool_filters
10
+ * - PostToolUse hook: validates tool outputs based on post_execution_validators
11
+ * - UserPromptSubmit hook: injects DNA policy reminders on each user prompt
12
+ * - SubagentStop hook: audits role scope compliance when subagents finish
13
+ * - PreCompact hook: preserves critical DNA directives before context compaction
14
+ * - Notification hook: fires on DNA policy violations
15
+ *
16
+ * See docs/references/cc-source-analysis.md for the full Claude Code hook event catalog (27 events).
17
+ *
18
+ * Hook scripts read JSON from stdin (tool_name, tool_input, agent_type)
19
+ * and exit with code 2 to block, or 0 to allow.
20
+ */
21
+ import { mkdir, writeFile, readFile, rm } from "node:fs/promises";
22
+ import { join } from "node:path";
23
+ import { toKebabCase } from "./agent-md.js";
24
+ const SENTINEL_COMMENT = "# intentdna:managed — do not edit manually";
25
+ /**
26
+ * Compile Constraint IR into hook shell scripts.
27
+ * Returns null for a hook type if there are no relevant constraints.
28
+ */
29
+ export function compileToHooks(ir, options) {
30
+ const agentPrefix = options?.agentPrefix ?? "dna-";
31
+ const preToolUse = compilePreToolUseHook(ir, agentPrefix, options?.roles);
32
+ const postToolUse = compilePostToolUseHook(ir, options?.roles);
33
+ const userPromptSubmit = compileUserPromptSubmitHook(ir);
34
+ const subagentStop = compileSubagentStopHook(ir);
35
+ const preCompact = compilePreCompactHook(ir);
36
+ const notification = compileNotificationHook(ir);
37
+ return { preToolUse, postToolUse, userPromptSubmit, subagentStop, preCompact, notification };
38
+ }
39
+ /**
40
+ * Write hook scripts to the .claude/hooks/ directory.
41
+ * Creates the directory if it doesn't exist.
42
+ * Returns list of files written.
43
+ */
44
+ export async function writeHookScripts(hooksDir, scripts) {
45
+ await mkdir(hooksDir, { recursive: true });
46
+ const written = [];
47
+ const entries = [
48
+ ["preToolUse", "dna-pre-tool-use.sh"],
49
+ ["postToolUse", "dna-post-tool-use.sh"],
50
+ ["userPromptSubmit", "dna-user-prompt-submit.sh"],
51
+ ["subagentStop", "dna-subagent-stop.sh"],
52
+ ["preCompact", "dna-pre-compact.sh"],
53
+ ["notification", "dna-notification.sh"],
54
+ ];
55
+ for (const [key, filename] of entries) {
56
+ if (scripts[key]) {
57
+ const path = join(hooksDir, filename);
58
+ await writeFile(path, scripts[key], { mode: 0o755 });
59
+ written.push(path);
60
+ }
61
+ }
62
+ return written;
63
+ }
64
+ /**
65
+ * Remove hook scripts previously written by Intent DNA.
66
+ * Only removes files that contain the sentinel comment.
67
+ * Returns number of files removed.
68
+ */
69
+ export async function removeHookScripts(hooksDir) {
70
+ const files = [
71
+ "dna-pre-tool-use.sh",
72
+ "dna-post-tool-use.sh",
73
+ "dna-user-prompt-submit.sh",
74
+ "dna-subagent-stop.sh",
75
+ "dna-pre-compact.sh",
76
+ "dna-notification.sh",
77
+ ];
78
+ let removed = 0;
79
+ for (const file of files) {
80
+ const path = join(hooksDir, file);
81
+ try {
82
+ const content = await readFile(path, "utf-8");
83
+ if (content.includes(SENTINEL_COMMENT)) {
84
+ await rm(path);
85
+ removed++;
86
+ }
87
+ }
88
+ catch {
89
+ // File doesn't exist, skip
90
+ }
91
+ }
92
+ return removed;
93
+ }
94
+ /**
95
+ * Generate settings.json hook configuration snippet.
96
+ * This can be merged into the project's .claude/settings.json.
97
+ */
98
+ export function generateSettingsSnippet(hooksDir) {
99
+ return {
100
+ hooks: {
101
+ PreToolUse: [
102
+ {
103
+ command: `bash ${join(hooksDir, "dna-pre-tool-use.sh")}`,
104
+ },
105
+ ],
106
+ PostToolUse: [
107
+ {
108
+ command: `bash ${join(hooksDir, "dna-post-tool-use.sh")}`,
109
+ },
110
+ ],
111
+ UserPromptSubmit: [
112
+ {
113
+ command: `bash ${join(hooksDir, "dna-user-prompt-submit.sh")}`,
114
+ },
115
+ ],
116
+ SubagentStop: [
117
+ {
118
+ command: `bash ${join(hooksDir, "dna-subagent-stop.sh")}`,
119
+ },
120
+ ],
121
+ PreCompact: [
122
+ {
123
+ command: `bash ${join(hooksDir, "dna-pre-compact.sh")}`,
124
+ },
125
+ ],
126
+ Notification: [
127
+ {
128
+ command: `bash ${join(hooksDir, "dna-notification.sh")}`,
129
+ },
130
+ ],
131
+ },
132
+ };
133
+ }
134
+ // ── Internal: Script generators ──────────────────────────────
135
+ function compilePreToolUseHook(ir, agentPrefix, roles) {
136
+ const gates = ir.pre_execution_gates;
137
+ const filters = ir.tool_filters;
138
+ const rolesScopeMap = ir.roles_scope_map;
139
+ const hasRoleScope = rolesScopeMap && rolesScopeMap.length > 0;
140
+ // Check if any role has structured output_schema
141
+ const outputSchemaRoles = roles
142
+ ? Object.entries(roles).filter(([, r]) => r.output_schema && typeof r.output_schema !== "string")
143
+ : [];
144
+ const hasOutputSchema = outputSchemaRoles.length > 0;
145
+ if (gates.length === 0 && filters.length === 0 && !hasRoleScope && !hasOutputSchema)
146
+ return null;
147
+ const lines = [];
148
+ lines.push("#!/bin/bash");
149
+ lines.push(SENTINEL_COMMENT);
150
+ lines.push(`# Compiled: ${ir.compiled_at}`);
151
+ lines.push(`# Sources: ${ir.source_dna_ids.join(", ")}`);
152
+ lines.push("");
153
+ lines.push("set -euo pipefail");
154
+ lines.push("");
155
+ lines.push("# Read hook input from stdin");
156
+ lines.push('INPUT=$(cat)');
157
+ lines.push('TOOL_NAME=$(echo "$INPUT" | jq -r \'.tool_name // empty\')');
158
+ lines.push('AGENT_TYPE=$(echo "$INPUT" | jq -r \'.agent_type // empty\')');
159
+ lines.push('TOOL_INPUT_JSON=$(echo "$INPUT" | jq -c \'.tool_input // {}\')');
160
+ lines.push("");
161
+ // Role scope enforcement — inserted before tool filters (hard constraint, checked first)
162
+ if (hasRoleScope) {
163
+ lines.push(...generateCheckWriteAllowedFn());
164
+ lines.push("");
165
+ lines.push(...generateRoleScopeBlocks(rolesScopeMap, agentPrefix));
166
+ lines.push("");
167
+ }
168
+ // Tool filters — block/warn tools based on conditions
169
+ if (filters.length > 0) {
170
+ lines.push("# ── Tool Filters ──");
171
+ for (const filter of filters) {
172
+ const toolMatch = extractToolMatch(filter);
173
+ if (toolMatch) {
174
+ lines.push(...generateToolFilterBlock(filter, toolMatch));
175
+ lines.push("");
176
+ }
177
+ }
178
+ }
179
+ // Pre-execution gates — block/escalate based on conditions
180
+ if (gates.length > 0) {
181
+ lines.push("# ── Pre-Execution Gates ──");
182
+ for (const gate of gates) {
183
+ lines.push(...generateGateBlock(gate));
184
+ lines.push("");
185
+ }
186
+ }
187
+ // Output schema enforcement — validate required sections on Write
188
+ if (hasOutputSchema) {
189
+ lines.push("# ── Output Schema Enforcement ──");
190
+ lines.push('if [ "$TOOL_NAME" = "Write" ]; then');
191
+ lines.push(' CONTENT=$(echo "$TOOL_INPUT_JSON" | jq -r \'.content // empty\')');
192
+ lines.push(' if [ -n "$CONTENT" ] && [ -n "$AGENT_TYPE" ]; then');
193
+ lines.push(' case "$AGENT_TYPE" in');
194
+ for (const [roleName, role] of outputSchemaRoles) {
195
+ const schema = role.output_schema;
196
+ if (!schema.required_sections || schema.required_sections.length === 0)
197
+ continue;
198
+ const agentName = `${agentPrefix}${roleName.replace(/_/g, "-")}`;
199
+ lines.push(` ${agentName})`);
200
+ for (const section of schema.required_sections) {
201
+ const escaped = section.replace(/'/g, "'\\''");
202
+ lines.push(` if ! echo "$CONTENT" | grep -q "## ${escaped}"; then`);
203
+ lines.push(` echo "BLOCK [Intent DNA]: Output missing required section '${escaped}' (role: ${roleName})" >&2`);
204
+ lines.push(` exit 2`);
205
+ lines.push(` fi`);
206
+ }
207
+ lines.push(` ;;`);
208
+ }
209
+ lines.push(' esac');
210
+ lines.push(' fi');
211
+ lines.push('fi');
212
+ lines.push("");
213
+ }
214
+ lines.push("# All checks passed");
215
+ lines.push("exit 0");
216
+ return lines.join("\n") + "\n";
217
+ }
218
+ function compilePostToolUseHook(ir, roles) {
219
+ const validators = ir.post_execution_validators;
220
+ // Check for converter roles
221
+ const converterRoles = roles
222
+ ? Object.entries(roles).filter(([, r]) => {
223
+ const os = r.output_schema;
224
+ return os && typeof os !== "string" && os.converter && os.format && os.format !== "markdown";
225
+ })
226
+ : [];
227
+ if (validators.length === 0 && converterRoles.length === 0)
228
+ return null;
229
+ const lines = [];
230
+ lines.push("#!/bin/bash");
231
+ lines.push(SENTINEL_COMMENT);
232
+ lines.push(`# Compiled: ${ir.compiled_at}`);
233
+ lines.push(`# Sources: ${ir.source_dna_ids.join(", ")}`);
234
+ lines.push("");
235
+ lines.push("set -euo pipefail");
236
+ lines.push("");
237
+ lines.push("# Read hook input from stdin");
238
+ lines.push('INPUT=$(cat)');
239
+ lines.push('TOOL_NAME=$(echo "$INPUT" | jq -r \'.tool_name // empty\')');
240
+ lines.push('AGENT_TYPE=$(echo "$INPUT" | jq -r \'.agent_type // empty\')');
241
+ lines.push('TOOL_OUTPUT=$(echo "$INPUT" | jq -r \'.tool_output // empty\')');
242
+ lines.push("");
243
+ lines.push("# ── Post-Execution Validators ──");
244
+ for (const v of validators) {
245
+ lines.push(`# [${v.source_gene}] ${v.check}`);
246
+ lines.push(`# Validator: "${v.check}" (threshold: ${v.threshold ?? "none"})`);
247
+ lines.push(`# Note: Complex validation requires application-level logic.`);
248
+ lines.push(`# This hook logs the check for audit purposes.`);
249
+ lines.push(`echo "[Intent DNA] PostToolUse validator: ${escapeShell(v.check)} (gene: ${v.source_gene})" >&2`);
250
+ lines.push("");
251
+ }
252
+ // Output format conversion
253
+ if (converterRoles.length > 0) {
254
+ lines.push("# ── Output Format Conversion ──");
255
+ lines.push('if [ "$TOOL_NAME" = "Write" ]; then');
256
+ lines.push(' FILE_PATH=$(echo "$INPUT" | jq -r \'.tool_input.file_path // empty\')');
257
+ lines.push(' if [ -n "$FILE_PATH" ] && [ -n "$AGENT_TYPE" ]; then');
258
+ lines.push(' case "$AGENT_TYPE" in');
259
+ for (const [roleName, role] of converterRoles) {
260
+ const schema = role.output_schema;
261
+ const agentName = `dna-${roleName.replace(/_/g, "-")}`;
262
+ const converter = schema.converter
263
+ .replace(/\{\{output_path\}\}/g, '"${FILE_PATH%.*}.' + (schema.format ?? "pdf") + '"');
264
+ lines.push(` ${agentName})`);
265
+ lines.push(` OUTPUT_PATH="\${FILE_PATH%.*}.${schema.format}"`);
266
+ lines.push(` ${converter} "$FILE_PATH" 2>/dev/null && \\`);
267
+ lines.push(` echo "[Intent DNA] Converted: $FILE_PATH → $OUTPUT_PATH" >&2 || \\`);
268
+ lines.push(` echo "[Intent DNA] Conversion failed: $FILE_PATH (${schema.converter})" >&2`);
269
+ lines.push(` ;;`);
270
+ }
271
+ lines.push(' esac');
272
+ lines.push(' fi');
273
+ lines.push('fi');
274
+ lines.push("");
275
+ }
276
+ lines.push("exit 0");
277
+ return lines.join("\n") + "\n";
278
+ }
279
+ /**
280
+ * Extract tool name patterns from a ToolFilter.
281
+ * The filter's target field may contain tool names or categories.
282
+ */
283
+ function extractToolMatch(filter) {
284
+ if (!filter.target)
285
+ return null;
286
+ // Map known DNA targets to Claude Code tool names
287
+ const toolMappings = {
288
+ file_modification: ["Edit", "Write", "NotebookEdit"],
289
+ file_write: ["Write"],
290
+ file_edit: ["Edit"],
291
+ code_execution: ["Bash"],
292
+ shell: ["Bash"],
293
+ harmful_actions: ["Bash"],
294
+ unsafe_operations: ["Bash"],
295
+ data_sharing_external: ["WebFetch", "Bash"],
296
+ };
297
+ const tools = toolMappings[filter.target];
298
+ if (tools)
299
+ return tools.join("|");
300
+ // If target looks like a tool name already (PascalCase), use it directly
301
+ if (/^[A-Z][a-zA-Z]+$/.test(filter.target))
302
+ return filter.target;
303
+ return null;
304
+ }
305
+ /**
306
+ * Generate bash code block for a tool filter.
307
+ */
308
+ function generateToolFilterBlock(filter, toolPattern) {
309
+ const lines = [];
310
+ const action = filter.action;
311
+ const reason = escapeShell(filter.reason);
312
+ lines.push(`# [${filter.source_gene}] ${filter.action}: ${filter.target}`);
313
+ if (action === "remove") {
314
+ // Hard block — exit 2
315
+ lines.push(`if echo "$TOOL_NAME" | grep -qE '^(${toolPattern})$'; then`);
316
+ lines.push(` echo "BLOCK [Intent DNA]: ${reason} (gene: ${filter.source_gene})" >&2`);
317
+ lines.push(" exit 2");
318
+ lines.push("fi");
319
+ }
320
+ else if (action === "warn") {
321
+ // Warn but allow — write to stderr, exit 0
322
+ lines.push(`if echo "$TOOL_NAME" | grep -qE '^(${toolPattern})$'; then`);
323
+ lines.push(` echo "WARN [Intent DNA]: ${reason} (gene: ${filter.source_gene})" >&2`);
324
+ lines.push("fi");
325
+ }
326
+ else {
327
+ // deprioritize — just log, can't actually deprioritize at hook level
328
+ lines.push(`if echo "$TOOL_NAME" | grep -qE '^(${toolPattern})$'; then`);
329
+ lines.push(` echo "DEPRIORITIZE [Intent DNA]: ${reason} (gene: ${filter.source_gene})" >&2`);
330
+ lines.push("fi");
331
+ }
332
+ return lines;
333
+ }
334
+ /**
335
+ * Generate bash code block for a pre-execution gate.
336
+ */
337
+ function generateGateBlock(gate) {
338
+ const lines = [];
339
+ const message = escapeShell(gate.message);
340
+ const condition = gate.condition;
341
+ lines.push(`# [${gate.source_gene}] ${gate.action}: ${condition}`);
342
+ // Parse condition to generate bash logic
343
+ const toolCheck = parseConditionToToolCheck(condition);
344
+ if (toolCheck) {
345
+ if (gate.action === "block") {
346
+ lines.push(`if ${toolCheck}; then`);
347
+ lines.push(` echo "BLOCK [Intent DNA]: ${message}" >&2`);
348
+ lines.push(" exit 2");
349
+ lines.push("fi");
350
+ }
351
+ else if (gate.action === "escalate") {
352
+ // Escalate = ask user (Claude Code supports this via permissionDecision)
353
+ lines.push(`if ${toolCheck}; then`);
354
+ lines.push(` echo '{"hookSpecificOutput":{"permissionDecision":"ask","permissionDecisionReason":"[Intent DNA] ${message}"}}' >&2`);
355
+ lines.push(" exit 2");
356
+ lines.push("fi");
357
+ }
358
+ else {
359
+ // warn
360
+ lines.push(`if ${toolCheck}; then`);
361
+ lines.push(` echo "WARN [Intent DNA]: ${message}" >&2`);
362
+ lines.push("fi");
363
+ }
364
+ }
365
+ else {
366
+ // Condition is too complex for shell — log it as a comment
367
+ lines.push(`# Complex condition cannot be enforced at hook level: ${condition}`);
368
+ lines.push(`# Action would be: ${gate.action} — "${gate.message}"`);
369
+ lines.push(`# Enforce via SDK adapter or application logic.`);
370
+ }
371
+ return lines;
372
+ }
373
+ /**
374
+ * Try to parse a DNA condition string into a bash conditional expression.
375
+ * Returns null if the condition is too complex for shell logic.
376
+ */
377
+ function parseConditionToToolCheck(condition) {
378
+ // Pattern: "X == false" → check for tool usage (threshold violation)
379
+ // These are boolean thresholds — they translate to tool-level blocks
380
+ const boolFalseMatch = condition.match(/^(\w+)\s*==\s*false$/);
381
+ if (boolFalseMatch) {
382
+ const metric = boolFalseMatch[1];
383
+ // Map metrics to tool checks
384
+ const metricToolMap = {
385
+ personal_data_exposure: 'echo "$TOOL_INPUT_JSON" | jq -e \'.. | strings | test("password|secret|token|api_key|ssn|credit_card"; "i")\' >/dev/null 2>&1',
386
+ };
387
+ if (metricToolMap[metric]) {
388
+ return metricToolMap[metric];
389
+ }
390
+ // Generic boolean thresholds can't be checked at hook level
391
+ return null;
392
+ }
393
+ // Pattern: "metric <= N" or "metric >= N"
394
+ // These are numeric thresholds — generally can't be checked per-call
395
+ const numericMatch = condition.match(/^(\w+)\s*(<=|>=|<|>|==)\s*(\d+)$/);
396
+ if (numericMatch) {
397
+ // Numeric conditions need aggregation — can't enforce per-call at hook level
398
+ return null;
399
+ }
400
+ // Pattern: "tool_name in ['X', 'Y']" or similar
401
+ const toolInMatch = condition.match(/^tool_name\s+in\s+\[([^\]]+)\]/);
402
+ if (toolInMatch) {
403
+ const tools = toolInMatch[1]
404
+ .split(",")
405
+ .map(t => t.trim().replace(/['"]/g, ""))
406
+ .join("|");
407
+ return `echo "$TOOL_NAME" | grep -qE '^(${tools})$'`;
408
+ }
409
+ return null;
410
+ }
411
+ /**
412
+ * Extract directory prefix from a glob pattern.
413
+ * "src/**\/*.ts" → "src/"
414
+ * "test/**" → "test/"
415
+ * "*.md" → "" (root)
416
+ * "package.json" → "package.json" (exact path)
417
+ */
418
+ export function globToPrefix(glob) {
419
+ const starIdx = glob.indexOf("*");
420
+ if (starIdx === -1)
421
+ return glob; // exact path
422
+ return glob.substring(0, starIdx);
423
+ }
424
+ /**
425
+ * Generate the check_write_allowed shell helper function.
426
+ */
427
+ function generateCheckWriteAllowedFn() {
428
+ return [
429
+ "# ── Path Allowlist Helper ──",
430
+ "check_write_allowed() {",
431
+ ' local file_path="$1"',
432
+ " shift",
433
+ ' for prefix in "$@"; do',
434
+ ' case "$file_path" in',
435
+ " ${prefix}*) return 0 ;;",
436
+ " esac",
437
+ " done",
438
+ " return 1",
439
+ "}",
440
+ ];
441
+ }
442
+ /**
443
+ * Generate role scope enforcement blocks for PreToolUse hook.
444
+ * Creates a case/esac structure branching on AGENT_TYPE.
445
+ * Uses agentPrefix + toKebabCase(role_name) for case matching,
446
+ * so that "planner" with prefix "dna-" matches agent_type "dna-planner".
447
+ */
448
+ function generateRoleScopeBlocks(rolesScopeMap, agentPrefix) {
449
+ if (rolesScopeMap.length === 0)
450
+ return [];
451
+ const lines = [];
452
+ lines.push("# ── Role Scope Enforcement ──");
453
+ lines.push('if [ -n "$AGENT_TYPE" ]; then');
454
+ lines.push(" FILE_PATH=$(echo \"$TOOL_INPUT_JSON\" | jq -r '.file_path // empty')");
455
+ lines.push("");
456
+ lines.push(" # Check write operations against role scope");
457
+ lines.push(" if echo \"$TOOL_NAME\" | grep -qE '^(Edit|Write|NotebookEdit)$'; then");
458
+ lines.push(' if [ -n "$FILE_PATH" ]; then');
459
+ lines.push(' case "$AGENT_TYPE" in');
460
+ for (const entry of rolesScopeMap) {
461
+ const writeGlobs = entry.scope.write ?? [];
462
+ const agentTypeName = `${agentPrefix}${toKebabCase(entry.role_name)}`;
463
+ lines.push(` ${agentTypeName})`);
464
+ if (writeGlobs.length === 0) {
465
+ // No write permission at all
466
+ lines.push(` echo "BLOCK [Intent DNA]: Role '${entry.role_name}' has no write permission (path: $FILE_PATH)" >&2`);
467
+ lines.push(" exit 2");
468
+ }
469
+ else {
470
+ // Check against prefixes
471
+ const prefixes = writeGlobs.map(globToPrefix).filter(p => p.length > 0);
472
+ const prefixArgs = prefixes.map(p => `"${p}"`).join(" ");
473
+ lines.push(` # Write scope: ${writeGlobs.join(", ")}`);
474
+ lines.push(` if ! check_write_allowed "$FILE_PATH" ${prefixArgs}; then`);
475
+ lines.push(` echo "BLOCK [Intent DNA]: Role '${entry.role_name}' cannot write to '$FILE_PATH' (allowed: ${writeGlobs.join(", ")})" >&2`);
476
+ lines.push(" exit 2");
477
+ lines.push(" fi");
478
+ }
479
+ lines.push(" ;;");
480
+ }
481
+ lines.push(" esac");
482
+ lines.push(" fi");
483
+ lines.push(" fi");
484
+ lines.push("fi");
485
+ return lines;
486
+ }
487
+ /**
488
+ * Escape a string for safe use in shell single-quoted context.
489
+ */
490
+ function escapeShell(s) {
491
+ return s.replace(/'/g, "'\\''").replace(/"/g, '\\"');
492
+ }
493
+ // ── New hook events (Phase 5.9) ─────────────────────────────
494
+ // Reference: docs/references/cc-source-analysis.md §2
495
+ /**
496
+ * Compile UserPromptSubmit hook.
497
+ * Injects DNA policy reminders each time the user submits a prompt.
498
+ * Only generated when there are high-priority prompt directives.
499
+ */
500
+ function compileUserPromptSubmitHook(ir) {
501
+ const highPriority = ir.prompt_directives.filter(d => d.priority === "high");
502
+ if (highPriority.length === 0)
503
+ return null;
504
+ const lines = [];
505
+ lines.push("#!/bin/bash");
506
+ lines.push(SENTINEL_COMMENT);
507
+ lines.push(`# Compiled: ${ir.compiled_at}`);
508
+ lines.push(`# Sources: ${ir.source_dna_ids.join(", ")}`);
509
+ lines.push("");
510
+ lines.push("# UserPromptSubmit: Inject DNA policy reminders on each user prompt");
511
+ lines.push("# This hook fires before the model processes the user's message.");
512
+ lines.push("");
513
+ lines.push("REMINDERS=()");
514
+ for (const d of highPriority) {
515
+ const text = escapeShell(d.text);
516
+ lines.push(`REMINDERS+=("[${d.source_gene}] ${text}")`);
517
+ }
518
+ lines.push("");
519
+ lines.push("if [ ${#REMINDERS[@]} -gt 0 ]; then");
520
+ lines.push(' echo "<system-reminder>[Intent DNA] Active high-priority policies:" >&2');
521
+ lines.push(' for r in "${REMINDERS[@]}"; do');
522
+ lines.push(' echo " - $r" >&2');
523
+ lines.push(" done");
524
+ lines.push(' echo "</system-reminder>" >&2');
525
+ lines.push("fi");
526
+ lines.push("");
527
+ lines.push("exit 0");
528
+ return lines.join("\n") + "\n";
529
+ }
530
+ /**
531
+ * Compile SubagentStop hook.
532
+ * Audits whether the subagent stayed within its role scope.
533
+ * Only generated when roles_scope_map is defined.
534
+ */
535
+ function compileSubagentStopHook(ir) {
536
+ const rolesScopeMap = ir.roles_scope_map;
537
+ if (!rolesScopeMap || rolesScopeMap.length === 0)
538
+ return null;
539
+ const lines = [];
540
+ lines.push("#!/bin/bash");
541
+ lines.push(SENTINEL_COMMENT);
542
+ lines.push(`# Compiled: ${ir.compiled_at}`);
543
+ lines.push(`# Sources: ${ir.source_dna_ids.join(", ")}`);
544
+ lines.push("");
545
+ lines.push("# SubagentStop: Audit role scope compliance when a subagent finishes");
546
+ lines.push("");
547
+ lines.push('INPUT=$(cat)');
548
+ lines.push('AGENT_NAME=$(echo "$INPUT" | jq -r \'.agent_name // empty\')');
549
+ lines.push('AGENT_TYPE=$(echo "$INPUT" | jq -r \'.agent_type // empty\')');
550
+ lines.push("");
551
+ lines.push("# Log scope audit for each known role");
552
+ lines.push('if [ -n "$AGENT_TYPE" ]; then');
553
+ lines.push(' case "$AGENT_TYPE" in');
554
+ for (const entry of rolesScopeMap) {
555
+ const agentTypeName = `dna-${toKebabCase(entry.role_name)}`;
556
+ const writeGlobs = entry.scope.write ?? [];
557
+ const readGlobs = entry.scope.read ?? [];
558
+ const scopeDesc = writeGlobs.length > 0
559
+ ? `write:[${writeGlobs.join(",")}]`
560
+ : "write:[]";
561
+ lines.push(` ${agentTypeName})`);
562
+ lines.push(` echo "[Intent DNA] SubagentStop audit: role=${entry.role_name} agent=$AGENT_NAME scope=${scopeDesc}" >&2`);
563
+ lines.push(` ;;`);
564
+ }
565
+ lines.push(' *)');
566
+ lines.push(' echo "[Intent DNA] SubagentStop: unknown role for agent_type=$AGENT_TYPE" >&2');
567
+ lines.push(' ;;');
568
+ lines.push(" esac");
569
+ lines.push("fi");
570
+ lines.push("");
571
+ lines.push("exit 0");
572
+ return lines.join("\n") + "\n";
573
+ }
574
+ /**
575
+ * Compile PreCompact hook.
576
+ * Emits DNA directive summary before context compaction, so the compact
577
+ * process can incorporate critical constraints into the summary.
578
+ * Only generated when there are prompt directives.
579
+ */
580
+ function compilePreCompactHook(ir) {
581
+ if (ir.prompt_directives.length === 0)
582
+ return null;
583
+ const lines = [];
584
+ lines.push("#!/bin/bash");
585
+ lines.push(SENTINEL_COMMENT);
586
+ lines.push(`# Compiled: ${ir.compiled_at}`);
587
+ lines.push(`# Sources: ${ir.source_dna_ids.join(", ")}`);
588
+ lines.push("");
589
+ lines.push("# PreCompact: Preserve critical DNA directives before context compaction");
590
+ lines.push("# The compact process should incorporate these into the conversation summary.");
591
+ lines.push("");
592
+ const critical = ir.prompt_directives.filter(d => d.priority === "high");
593
+ const standard = ir.prompt_directives.filter(d => d.priority === "medium");
594
+ lines.push('echo "[Intent DNA] PreCompact: Preserving directive summary for compact" >&2');
595
+ if (critical.length > 0) {
596
+ lines.push('echo " Critical directives:" >&2');
597
+ for (const d of critical) {
598
+ lines.push(`echo " - [${d.source_gene}] ${escapeShell(d.text)}" >&2`);
599
+ }
600
+ }
601
+ if (standard.length > 0) {
602
+ lines.push('echo " Standard directives (${' + standard.length + '}):" >&2');
603
+ for (const d of standard) {
604
+ lines.push(`echo " - [${d.source_gene}] ${escapeShell(d.text)}" >&2`);
605
+ }
606
+ }
607
+ lines.push("");
608
+ lines.push("exit 0");
609
+ return lines.join("\n") + "\n";
610
+ }
611
+ /**
612
+ * Compile Notification hook.
613
+ * Logs DNA policy violation events for audit trail.
614
+ * Only generated when there are pre_execution_gates with block/escalate actions.
615
+ */
616
+ function compileNotificationHook(ir) {
617
+ const blockGates = ir.pre_execution_gates.filter(g => g.action === "block" || g.action === "escalate");
618
+ const blockFilters = ir.tool_filters.filter(f => f.action === "remove");
619
+ if (blockGates.length === 0 && blockFilters.length === 0)
620
+ return null;
621
+ const lines = [];
622
+ lines.push("#!/bin/bash");
623
+ lines.push(SENTINEL_COMMENT);
624
+ lines.push(`# Compiled: ${ir.compiled_at}`);
625
+ lines.push(`# Sources: ${ir.source_dna_ids.join(", ")}`);
626
+ lines.push("");
627
+ lines.push("# Notification: Log DNA policy violation events for audit");
628
+ lines.push("");
629
+ lines.push('INPUT=$(cat)');
630
+ lines.push('MESSAGE=$(echo "$INPUT" | jq -r \'.message // empty\')');
631
+ lines.push('TITLE=$(echo "$INPUT" | jq -r \'.title // empty\')');
632
+ lines.push("");
633
+ // Write to audit log if it looks like a DNA violation
634
+ lines.push('if echo "$MESSAGE" | grep -q "Intent DNA"; then');
635
+ lines.push(' AUDIT_DIR="${HOME}/.intentdna/audit"');
636
+ lines.push(' mkdir -p "$AUDIT_DIR"');
637
+ lines.push(' AUDIT_FILE="$AUDIT_DIR/violations-$(date +%Y-%m-%d).log"');
638
+ lines.push(' echo "[$(date -Iseconds)] title=$TITLE message=$MESSAGE" >> "$AUDIT_FILE"');
639
+ lines.push(' echo "[Intent DNA] Notification: violation logged to $AUDIT_FILE" >&2');
640
+ lines.push("fi");
641
+ lines.push("");
642
+ lines.push("exit 0");
643
+ return lines.join("\n") + "\n";
644
+ }
645
+ //# sourceMappingURL=hooks.js.map