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.
- package/LICENSE +69 -0
- package/README.md +251 -0
- package/dist/audit/index.d.ts +109 -0
- package/dist/audit/index.d.ts.map +1 -0
- package/dist/audit/index.js +144 -0
- package/dist/audit/index.js.map +1 -0
- package/dist/cli/commands/compile.d.ts +14 -0
- package/dist/cli/commands/compile.d.ts.map +1 -0
- package/dist/cli/commands/compile.js +89 -0
- package/dist/cli/commands/compile.js.map +1 -0
- package/dist/cli/commands/epigenetic.d.ts +20 -0
- package/dist/cli/commands/epigenetic.d.ts.map +1 -0
- package/dist/cli/commands/epigenetic.js +112 -0
- package/dist/cli/commands/epigenetic.js.map +1 -0
- package/dist/cli/commands/evolve.d.ts +12 -0
- package/dist/cli/commands/evolve.d.ts.map +1 -0
- package/dist/cli/commands/evolve.js +52 -0
- package/dist/cli/commands/evolve.js.map +1 -0
- package/dist/cli/commands/guard.d.ts +22 -0
- package/dist/cli/commands/guard.d.ts.map +1 -0
- package/dist/cli/commands/guard.js +257 -0
- package/dist/cli/commands/guard.js.map +1 -0
- package/dist/cli/commands/import.d.ts +71 -0
- package/dist/cli/commands/import.d.ts.map +1 -0
- package/dist/cli/commands/import.js +428 -0
- package/dist/cli/commands/import.js.map +1 -0
- package/dist/cli/commands/init.d.ts +13 -0
- package/dist/cli/commands/init.d.ts.map +1 -0
- package/dist/cli/commands/init.js +171 -0
- package/dist/cli/commands/init.js.map +1 -0
- package/dist/cli/commands/run.d.ts +59 -0
- package/dist/cli/commands/run.d.ts.map +1 -0
- package/dist/cli/commands/run.js +350 -0
- package/dist/cli/commands/run.js.map +1 -0
- package/dist/cli/commands/show.d.ts +12 -0
- package/dist/cli/commands/show.d.ts.map +1 -0
- package/dist/cli/commands/show.js +78 -0
- package/dist/cli/commands/show.js.map +1 -0
- package/dist/cli/commands/sync.d.ts +31 -0
- package/dist/cli/commands/sync.d.ts.map +1 -0
- package/dist/cli/commands/sync.js +307 -0
- package/dist/cli/commands/sync.js.map +1 -0
- package/dist/cli/commands/validate.d.ts +11 -0
- package/dist/cli/commands/validate.d.ts.map +1 -0
- package/dist/cli/commands/validate.js +30 -0
- package/dist/cli/commands/validate.js.map +1 -0
- package/dist/cli/commands/verify.d.ts +33 -0
- package/dist/cli/commands/verify.d.ts.map +1 -0
- package/dist/cli/commands/verify.js +102 -0
- package/dist/cli/commands/verify.js.map +1 -0
- package/dist/cli/index.d.ts +15 -0
- package/dist/cli/index.d.ts.map +1 -0
- package/dist/cli/index.js +358 -0
- package/dist/cli/index.js.map +1 -0
- package/dist/cli/util/format.d.ts +24 -0
- package/dist/cli/util/format.d.ts.map +1 -0
- package/dist/cli/util/format.js +67 -0
- package/dist/cli/util/format.js.map +1 -0
- package/dist/cli/util/prompt.d.ts +11 -0
- package/dist/cli/util/prompt.d.ts.map +1 -0
- package/dist/cli/util/prompt.js +51 -0
- package/dist/cli/util/prompt.js.map +1 -0
- package/dist/compiler/activate.d.ts +29 -0
- package/dist/compiler/activate.d.ts.map +1 -0
- package/dist/compiler/activate.js +141 -0
- package/dist/compiler/activate.js.map +1 -0
- package/dist/compiler/cascade.d.ts +27 -0
- package/dist/compiler/cascade.d.ts.map +1 -0
- package/dist/compiler/cascade.js +124 -0
- package/dist/compiler/cascade.js.map +1 -0
- package/dist/compiler/compile.d.ts +17 -0
- package/dist/compiler/compile.d.ts.map +1 -0
- package/dist/compiler/compile.js +219 -0
- package/dist/compiler/compile.js.map +1 -0
- package/dist/compiler/index.d.ts +28 -0
- package/dist/compiler/index.d.ts.map +1 -0
- package/dist/compiler/index.js +51 -0
- package/dist/compiler/index.js.map +1 -0
- package/dist/compiler/workflow.d.ts +37 -0
- package/dist/compiler/workflow.d.ts.map +1 -0
- package/dist/compiler/workflow.js +274 -0
- package/dist/compiler/workflow.js.map +1 -0
- package/dist/evolution/collector.d.ts +52 -0
- package/dist/evolution/collector.d.ts.map +1 -0
- package/dist/evolution/collector.js +72 -0
- package/dist/evolution/collector.js.map +1 -0
- package/dist/evolution/fitness.d.ts +33 -0
- package/dist/evolution/fitness.d.ts.map +1 -0
- package/dist/evolution/fitness.js +96 -0
- package/dist/evolution/fitness.js.map +1 -0
- package/dist/evolution/index.d.ts +53 -0
- package/dist/evolution/index.d.ts.map +1 -0
- package/dist/evolution/index.js +69 -0
- package/dist/evolution/index.js.map +1 -0
- package/dist/evolution/marker-gen.d.ts +31 -0
- package/dist/evolution/marker-gen.d.ts.map +1 -0
- package/dist/evolution/marker-gen.js +141 -0
- package/dist/evolution/marker-gen.js.map +1 -0
- package/dist/evolution/store.d.ts +24 -0
- package/dist/evolution/store.d.ts.map +1 -0
- package/dist/evolution/store.js +78 -0
- package/dist/evolution/store.js.map +1 -0
- package/dist/evolution/types.d.ts +73 -0
- package/dist/evolution/types.d.ts.map +1 -0
- package/dist/evolution/types.js +14 -0
- package/dist/evolution/types.js.map +1 -0
- package/dist/index.d.ts +4 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +4 -0
- package/dist/index.js.map +1 -0
- package/dist/log/logger.d.ts +15 -0
- package/dist/log/logger.d.ts.map +1 -0
- package/dist/log/logger.js +36 -0
- package/dist/log/logger.js.map +1 -0
- package/dist/runtime/agent-md.d.ts +56 -0
- package/dist/runtime/agent-md.d.ts.map +1 -0
- package/dist/runtime/agent-md.js +257 -0
- package/dist/runtime/agent-md.js.map +1 -0
- package/dist/runtime/claude-sdk.d.ts +51 -0
- package/dist/runtime/claude-sdk.d.ts.map +1 -0
- package/dist/runtime/claude-sdk.js +78 -0
- package/dist/runtime/claude-sdk.js.map +1 -0
- package/dist/runtime/hooks.d.ts +67 -0
- package/dist/runtime/hooks.d.ts.map +1 -0
- package/dist/runtime/hooks.js +645 -0
- package/dist/runtime/hooks.js.map +1 -0
- package/dist/runtime/index.d.ts +9 -0
- package/dist/runtime/index.d.ts.map +1 -0
- package/dist/runtime/index.js +5 -0
- package/dist/runtime/index.js.map +1 -0
- package/dist/runtime/markdown.d.ts +36 -0
- package/dist/runtime/markdown.d.ts.map +1 -0
- package/dist/runtime/markdown.js +214 -0
- package/dist/runtime/markdown.js.map +1 -0
- package/dist/runtime/settings-adapter.d.ts +33 -0
- package/dist/runtime/settings-adapter.d.ts.map +1 -0
- package/dist/runtime/settings-adapter.js +78 -0
- package/dist/runtime/settings-adapter.js.map +1 -0
- package/dist/runtime/skill-adapter.d.ts +22 -0
- package/dist/runtime/skill-adapter.d.ts.map +1 -0
- package/dist/runtime/skill-adapter.js +170 -0
- package/dist/runtime/skill-adapter.js.map +1 -0
- package/dist/runtime/workflow-runner.d.ts +53 -0
- package/dist/runtime/workflow-runner.d.ts.map +1 -0
- package/dist/runtime/workflow-runner.js +420 -0
- package/dist/runtime/workflow-runner.js.map +1 -0
- package/dist/schema/index.d.ts +2 -0
- package/dist/schema/index.d.ts.map +1 -0
- package/dist/schema/index.js +2 -0
- package/dist/schema/index.js.map +1 -0
- package/dist/schema/types.d.ts +275 -0
- package/dist/schema/types.d.ts.map +1 -0
- package/dist/schema/types.js +11 -0
- package/dist/schema/types.js.map +1 -0
- package/dist/schema/validate.d.ts +20 -0
- package/dist/schema/validate.d.ts.map +1 -0
- package/dist/schema/validate.js +379 -0
- package/dist/schema/validate.js.map +1 -0
- package/dist/schema/yaml-parser.d.ts +12 -0
- package/dist/schema/yaml-parser.d.ts.map +1 -0
- package/dist/schema/yaml-parser.js +298 -0
- package/dist/schema/yaml-parser.js.map +1 -0
- package/dist/species/default.dna.json +62 -0
- package/dist/templates/api-backend.dna.yaml +77 -0
- package/dist/templates/brainstorming-first.dna.yaml +71 -0
- package/dist/templates/branch-finishing.dna.yaml +54 -0
- package/dist/templates/code-review-pipeline.dna.yaml +91 -0
- package/dist/templates/devops-cicd.dna.yaml +67 -0
- package/dist/templates/documentation-writer.dna.yaml +71 -0
- package/dist/templates/enterprise-baseline.dna.yaml +66 -0
- package/dist/templates/frontend-quality.dna.yaml +88 -0
- package/dist/templates/full-pipeline.dna.yaml +129 -0
- package/dist/templates/mobile-dev.dna.yaml +68 -0
- package/dist/templates/monorepo-governance.dna.yaml +60 -0
- package/dist/templates/multi-perspective-review.dna.yaml +109 -0
- package/dist/templates/pr-submitter.dna.yaml +55 -0
- package/dist/templates/safe-refactoring.dna.yaml +65 -0
- package/dist/templates/secure-dev.dna.yaml +90 -0
- package/dist/templates/subagent-parallel.dna.yaml +98 -0
- package/dist/templates/systematic-debugging.dna.yaml +55 -0
- package/dist/templates/tdd-strict.dna.yaml +69 -0
- package/dist/templates/verification-loop.dna.yaml +53 -0
- package/dist/templates/yolo-with-guardrails.dna.yaml +56 -0
- 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
|