intentdna 1.5.2 → 1.5.4

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.
@@ -23,11 +23,21 @@ import { relative, normalize } from "node:path";
23
23
  * Optional `roles` parameter provides output_schema enforcement.
24
24
  */
25
25
  export function enforcePreToolUse(ir, input, state, roles) {
26
+ // G4 Layer 1: Step enforce rules (state-driven)
27
+ if (state?.workflowState && ir.workflows_ir) {
28
+ const stepRuleResult = enforceStepRules(ir, input, state);
29
+ if (stepRuleResult !== null)
30
+ return stepRuleResult;
31
+ }
26
32
  // Layer 2: Role scope
27
33
  if (input.agent_type && ir.roles_scope_map && ir.roles_scope_map.length > 0) {
28
- const result = enforceRoleScope(ir.roles_scope_map, input);
29
- if (result)
30
- return result;
34
+ // G4: Check if scope is relaxed by iteration-based rule
35
+ const relaxed = isStepScopeRelaxed(ir, state);
36
+ if (!relaxed) {
37
+ const result = enforceRoleScope(ir.roles_scope_map, input, getStepAdditionalPaths(ir, state));
38
+ if (result)
39
+ return result;
40
+ }
31
41
  }
32
42
  // Layer 3: Tool filters
33
43
  if (ir.tool_filters.length > 0) {
@@ -293,6 +303,53 @@ export function enforceStop(ir, input, workflowState) {
293
303
  return blockOutput(`[Intent DNA] Workflow '${workflowState.workflow}' has unmet checkpoints at step '${workflowState.current_step}':\n` +
294
304
  messages.map(m => ` - ${m}`).join("\n"));
295
305
  }
306
+ // ── G4: State-driven Step Enforcement ─────────────────────
307
+ /** Write tools that step read_only rule should block */
308
+ const ALL_WRITE_TOOLS = new Set(["Edit", "Write", "NotebookEdit", "Bash"]);
309
+ /**
310
+ * Find the StepEnforceRule for the current step in the active workflow.
311
+ */
312
+ function findStepRule(ir, state) {
313
+ if (!state?.workflowState || !ir.workflows_ir)
314
+ return null;
315
+ const wf = ir.workflows_ir.find(w => w.workflow_name === state.workflowState.workflow);
316
+ if (!wf?.step_enforce_rules)
317
+ return null;
318
+ return wf.step_enforce_rules.find(r => r.step_id === state.workflowState.current_step) ?? null;
319
+ }
320
+ /**
321
+ * G4: Enforce step-level rules.
322
+ * Returns HookOutput if a rule triggers (block for read_only), null to continue.
323
+ */
324
+ function enforceStepRules(ir, input, state) {
325
+ const rule = findStepRule(ir, state);
326
+ if (!rule)
327
+ return null;
328
+ // read_only: block all write tools
329
+ if (rule.read_only && ALL_WRITE_TOOLS.has(input.tool_name)) {
330
+ return blockOutput(`[Intent DNA] Step '${rule.step_id}' is read-only. Write tool '${input.tool_name}' blocked.`);
331
+ }
332
+ return null;
333
+ }
334
+ /**
335
+ * G4: Check if scope enforcement is relaxed for this step based on iteration.
336
+ * When iteration >= relax_after_iteration, scope checks are skipped (rescue mode).
337
+ */
338
+ function isStepScopeRelaxed(ir, state) {
339
+ const rule = findStepRule(ir, state);
340
+ if (!rule?.relax_after_iteration)
341
+ return false;
342
+ const iteration = state?.workflowState?.iteration ?? 1;
343
+ return iteration >= rule.relax_after_iteration;
344
+ }
345
+ /**
346
+ * G4: Get additional write paths granted to the current step.
347
+ * These extend the role's scope for this specific step only.
348
+ */
349
+ function getStepAdditionalPaths(ir, state) {
350
+ const rule = findStepRule(ir, state);
351
+ return rule?.additional_write_paths ?? [];
352
+ }
296
353
  // ── Internal: Layer Enforcement ────────────────────────────
297
354
  /**
298
355
  * Enforce handoff consumes — verify that the current step's consumed
@@ -344,7 +401,7 @@ export function enforceHandoffProduces(ir, wfState) {
344
401
  }
345
402
  return null;
346
403
  }
347
- function enforceRoleScope(rolesScopeMap, input) {
404
+ function enforceRoleScope(rolesScopeMap, input, additionalWritePaths = []) {
348
405
  if (!input.agent_type)
349
406
  return null;
350
407
  // Standard write tools: check single file path
@@ -352,7 +409,7 @@ function enforceRoleScope(rolesScopeMap, input) {
352
409
  const filePath = extractFilePath(input);
353
410
  if (!filePath)
354
411
  return null;
355
- return checkPathAgainstScope(rolesScopeMap, input, filePath);
412
+ return checkPathAgainstScope(rolesScopeMap, input, filePath, additionalWritePaths);
356
413
  }
357
414
  // Bash: extract potential write targets from command
358
415
  if (input.tool_name === "Bash") {
@@ -363,7 +420,7 @@ function enforceRoleScope(rolesScopeMap, input) {
363
420
  if (writePaths.length === 0)
364
421
  return null;
365
422
  for (const p of writePaths) {
366
- const result = checkPathAgainstScope(rolesScopeMap, input, p);
423
+ const result = checkPathAgainstScope(rolesScopeMap, input, p, additionalWritePaths);
367
424
  if (result)
368
425
  return result;
369
426
  }
@@ -372,7 +429,7 @@ function enforceRoleScope(rolesScopeMap, input) {
372
429
  return null;
373
430
  }
374
431
  /** Check a single file path against role scope. Shared by Write tools and Bash. */
375
- function checkPathAgainstScope(rolesScopeMap, input, filePath) {
432
+ function checkPathAgainstScope(rolesScopeMap, input, filePath, additionalWritePaths = []) {
376
433
  const cwd = input.cwd;
377
434
  let relativePath = cwd && filePath.startsWith("/") ? relative(cwd, filePath) : filePath;
378
435
  // Normalize to resolve traversal (e.g., "src/../../test/x.ts" → "../test/x.ts")
@@ -382,11 +439,15 @@ function checkPathAgainstScope(rolesScopeMap, input, filePath) {
382
439
  if (input.agent_type !== agentTypeName)
383
440
  continue;
384
441
  const writeGlobs = entry.scope.write ?? [];
385
- if (writeGlobs.length === 0) {
442
+ // G4: merge step-specific additional_write_paths
443
+ const allWriteGlobs = additionalWritePaths.length > 0
444
+ ? [...writeGlobs, ...additionalWritePaths]
445
+ : writeGlobs;
446
+ if (allWriteGlobs.length === 0) {
386
447
  return allowOutput(`WARN [Intent DNA]: Role '${entry.role_name}' has no write permission (path: ${filePath}). Merge-time scope gate will filter.`);
387
448
  }
388
- if (!checkWriteAllowed(relativePath, writeGlobs)) {
389
- return allowOutput(`WARN [Intent DNA]: Role '${entry.role_name}' cannot write to '${filePath}' (allowed: ${writeGlobs.join(", ")}). Merge-time scope gate will filter.`);
449
+ if (!checkWriteAllowed(relativePath, allWriteGlobs)) {
450
+ return allowOutput(`WARN [Intent DNA]: Role '${entry.role_name}' cannot write to '${filePath}' (allowed: ${allWriteGlobs.join(", ")}). Merge-time scope gate will filter.`);
390
451
  }
391
452
  return null; // Role matched, write allowed
392
453
  }
@@ -79,19 +79,31 @@ export interface TraceEntry {
79
79
  timestamp: string;
80
80
  }
81
81
  /**
82
- * Append a trace entry to the daily trace file.
83
- * With sessionId: `.dna/state/trace/trace-YYYY-MM-DD-{sessionId}.jsonl`
84
- * Without: `.dna/state/trace/trace-YYYY-MM-DD.jsonl` (backward compat)
82
+ * Append a trace entry with dual-write strategy:
83
+ * 1. Always → global trace: `.dna/state/trace/trace-YYYY-MM-DD.jsonl` (merged view)
84
+ * 2. If sessionId → session-local: `.dna/state/sessions/{id}/trace.jsonl`
85
+ *
86
+ * G1: Session isolation — session traces live in the session directory,
87
+ * global trace provides a unified view across all sessions.
85
88
  * Fail-open: never throws.
86
89
  */
87
90
  export declare function appendTrace(projectDir: string, entry: TraceEntry, sessionId?: string): Promise<void>;
88
91
  /**
89
- * Read trace entries from the last N days.
90
- * With sessionId: reads only that session's trace files.
91
- * Without: reads all trace files (merged view).
92
+ * Read trace entries.
93
+ *
94
+ * G1 session isolation:
95
+ * - With sessionId: reads from `.dna/state/sessions/{id}/trace.jsonl`
96
+ * (falls back to legacy global files with sessionId in name for backward compat)
97
+ * - Without sessionId: reads from `.dna/state/trace/trace-{date}.jsonl` (global merged view)
98
+ *
92
99
  * Returns parsed entries sorted by timestamp.
93
100
  */
94
101
  export declare function readTraces(projectDir: string, days?: number, sessionId?: string): Promise<TraceEntry[]>;
102
+ /**
103
+ * List active session directories under `.dna/state/sessions/`.
104
+ * Returns session IDs (directory names).
105
+ */
106
+ export declare function listSessions(projectDir: string): Promise<string[]>;
95
107
  /**
96
108
  * Clean up trace files older than retention period.
97
109
  * Removes `.dna/state/trace/trace-*.jsonl` files older than TRACE_RETENTION_DAYS.
@@ -140,48 +140,104 @@ const TRACE_DIR = "trace";
140
140
  const MAX_TRACE_SIZE = 10 * 1024 * 1024; // 10MB
141
141
  const TRACE_RETENTION_DAYS = 7;
142
142
  /**
143
- * Build trace file name.
144
- * With session isolation: `trace-{date}-{sessionId}.jsonl`
145
- * Without: `trace-{date}.jsonl` (backward compat / merged view)
143
+ * Build global trace file name (merged view, no sessionId).
144
+ * Legacy files with sessionId in name are still readable for backward compat.
146
145
  */
147
- function traceFileName(date, sessionId) {
148
- if (sessionId) {
149
- return `trace-${date}-${sessionId}.jsonl`;
150
- }
146
+ function globalTraceFileName(date) {
151
147
  return `trace-${date}.jsonl`;
152
148
  }
153
149
  /**
154
- * Append a trace entry to the daily trace file.
155
- * With sessionId: `.dna/state/trace/trace-YYYY-MM-DD-{sessionId}.jsonl`
156
- * Without: `.dna/state/trace/trace-YYYY-MM-DD.jsonl` (backward compat)
150
+ * Append a trace entry with dual-write strategy:
151
+ * 1. Always → global trace: `.dna/state/trace/trace-YYYY-MM-DD.jsonl` (merged view)
152
+ * 2. If sessionId → session-local: `.dna/state/sessions/{id}/trace.jsonl`
153
+ *
154
+ * G1: Session isolation — session traces live in the session directory,
155
+ * global trace provides a unified view across all sessions.
157
156
  * Fail-open: never throws.
158
157
  */
159
158
  export async function appendTrace(projectDir, entry, sessionId) {
160
159
  try {
161
- const traceDir = join(projectDir, ".dna", "state", TRACE_DIR);
162
- await mkdir(traceDir, { recursive: true });
160
+ const line = JSON.stringify(entry) + "\n";
163
161
  const date = entry.timestamp.slice(0, 10);
164
- const tracePath = join(traceDir, traceFileName(date, sessionId));
165
- // Check file size rotate if over limit
162
+ // 1. Global trace (merged view)
163
+ const globalTraceDir = join(projectDir, ".dna", "state", TRACE_DIR);
164
+ await mkdir(globalTraceDir, { recursive: true });
165
+ const globalPath = join(globalTraceDir, globalTraceFileName(date));
166
166
  try {
167
- const stats = await stat(tracePath);
167
+ const stats = await stat(globalPath);
168
168
  if (stats.size >= MAX_TRACE_SIZE)
169
- return; // silently skip if file too large
169
+ return;
170
170
  }
171
171
  catch { /* file doesn't exist yet */ }
172
- await appendFile(tracePath, JSON.stringify(entry) + "\n", "utf-8");
172
+ await appendFile(globalPath, line, "utf-8");
173
+ // 2. Session-local trace (if session isolated)
174
+ if (sessionId) {
175
+ const sessionDir = resolveStateDir(projectDir, sessionId);
176
+ await mkdir(sessionDir, { recursive: true });
177
+ const sessionTracePath = join(sessionDir, "trace.jsonl");
178
+ try {
179
+ const stats = await stat(sessionTracePath);
180
+ if (stats.size >= MAX_TRACE_SIZE)
181
+ return;
182
+ }
183
+ catch { /* file doesn't exist yet */ }
184
+ await appendFile(sessionTracePath, line, "utf-8");
185
+ }
173
186
  }
174
187
  catch {
175
188
  // Fail-open: trace write failure never affects hook execution
176
189
  }
177
190
  }
178
191
  /**
179
- * Read trace entries from the last N days.
180
- * With sessionId: reads only that session's trace files.
181
- * Without: reads all trace files (merged view).
192
+ * Read trace entries.
193
+ *
194
+ * G1 session isolation:
195
+ * - With sessionId: reads from `.dna/state/sessions/{id}/trace.jsonl`
196
+ * (falls back to legacy global files with sessionId in name for backward compat)
197
+ * - Without sessionId: reads from `.dna/state/trace/trace-{date}.jsonl` (global merged view)
198
+ *
182
199
  * Returns parsed entries sorted by timestamp.
183
200
  */
184
201
  export async function readTraces(projectDir, days = 1, sessionId) {
202
+ const entries = [];
203
+ if (sessionId) {
204
+ // Session-specific: read from session directory first
205
+ const sessionDir = resolveStateDir(projectDir, sessionId);
206
+ const sessionTracePath = join(sessionDir, "trace.jsonl");
207
+ const sessionEntries = await readTraceFile(sessionTracePath);
208
+ entries.push(...sessionEntries);
209
+ // Backward compat: also check legacy files in global trace dir
210
+ if (entries.length === 0) {
211
+ const legacyEntries = await readGlobalTraces(projectDir, days, sessionId);
212
+ entries.push(...legacyEntries);
213
+ }
214
+ }
215
+ else {
216
+ // Global merged view
217
+ const globalEntries = await readGlobalTraces(projectDir, days);
218
+ entries.push(...globalEntries);
219
+ }
220
+ return entries.sort((a, b) => a.timestamp.localeCompare(b.timestamp));
221
+ }
222
+ /** Read all entries from a single trace file. */
223
+ async function readTraceFile(filePath) {
224
+ const entries = [];
225
+ try {
226
+ const content = await readFile(filePath, "utf-8");
227
+ for (const line of content.trim().split("\n")) {
228
+ if (!line)
229
+ continue;
230
+ try {
231
+ entries.push(JSON.parse(line));
232
+ }
233
+ catch { /* skip malformed */ }
234
+ }
235
+ }
236
+ catch { /* file doesn't exist */ }
237
+ return entries;
238
+ }
239
+ /** Read global trace files from `.dna/state/trace/`. */
240
+ async function readGlobalTraces(projectDir, days, sessionIdFilter) {
185
241
  const traceDir = join(projectDir, ".dna", "state", TRACE_DIR);
186
242
  const entries = [];
187
243
  const cutoff = new Date();
@@ -192,28 +248,20 @@ export async function readTraces(projectDir, days = 1, sessionId) {
192
248
  const traceFiles = files
193
249
  .filter(f => f.startsWith("trace-") && f.endsWith(".jsonl"))
194
250
  .filter(f => {
195
- // Extract date from filename: trace-YYYY-MM-DD.jsonl or trace-YYYY-MM-DD-{sessionId}.jsonl
196
251
  const fileDate = f.slice(6, 16); // "trace-YYYY-MM-DD..."
197
252
  if (fileDate < cutoffDate)
198
253
  return false;
199
- // Session filter: only include files for this session (or shared files)
200
- if (sessionId) {
201
- const suffix = f.slice(16); // "-{sessionId}.jsonl" or ".jsonl"
202
- return suffix === `-${sessionId}.jsonl` || suffix === ".jsonl";
254
+ // Legacy session filter: files with sessionId in name
255
+ if (sessionIdFilter) {
256
+ const suffix = f.slice(16);
257
+ return suffix === `-${sessionIdFilter}.jsonl` || suffix === ".jsonl";
203
258
  }
204
259
  return true;
205
260
  })
206
261
  .sort();
207
262
  for (const file of traceFiles) {
208
- const content = await readFile(join(traceDir, file), "utf-8");
209
- for (const line of content.trim().split("\n")) {
210
- if (!line)
211
- continue;
212
- try {
213
- entries.push(JSON.parse(line));
214
- }
215
- catch { /* skip malformed */ }
216
- }
263
+ const fileEntries = await readTraceFile(join(traceDir, file));
264
+ entries.push(...fileEntries);
217
265
  }
218
266
  }
219
267
  catch {
@@ -221,6 +269,29 @@ export async function readTraces(projectDir, days = 1, sessionId) {
221
269
  }
222
270
  return entries;
223
271
  }
272
+ /**
273
+ * List active session directories under `.dna/state/sessions/`.
274
+ * Returns session IDs (directory names).
275
+ */
276
+ export async function listSessions(projectDir) {
277
+ const sessionsDir = join(projectDir, ".dna", "state", "sessions");
278
+ try {
279
+ const entries = await readdir(sessionsDir);
280
+ const sessions = [];
281
+ for (const entry of entries) {
282
+ try {
283
+ const entryStat = await stat(join(sessionsDir, entry));
284
+ if (entryStat.isDirectory())
285
+ sessions.push(entry);
286
+ }
287
+ catch { /* skip */ }
288
+ }
289
+ return sessions;
290
+ }
291
+ catch {
292
+ return [];
293
+ }
294
+ }
224
295
  /**
225
296
  * Clean up trace files older than retention period.
226
297
  * Removes `.dna/state/trace/trace-*.jsonl` files older than TRACE_RETENTION_DAYS.
@@ -0,0 +1,13 @@
1
+ #!/usr/bin/env node
2
+ /**
3
+ * Intent DNA — MCP Server (`dna-mcp`)
4
+ *
5
+ * Claude Code MCP server providing DNA governance tools:
6
+ * G2: State management (workflow, trace, status)
7
+ * G3: Compile toolchain (compile, validate, sync)
8
+ *
9
+ * Usage: dna-mcp [--project-dir <path>]
10
+ *
11
+ * Speaks JSON-RPC 2.0 over stdio (MCP protocol).
12
+ */
13
+ export {};
@@ -0,0 +1,40 @@
1
+ #!/usr/bin/env node
2
+ /**
3
+ * Intent DNA — MCP Server (`dna-mcp`)
4
+ *
5
+ * Claude Code MCP server providing DNA governance tools:
6
+ * G2: State management (workflow, trace, status)
7
+ * G3: Compile toolchain (compile, validate, sync)
8
+ *
9
+ * Usage: dna-mcp [--project-dir <path>]
10
+ *
11
+ * Speaks JSON-RPC 2.0 over stdio (MCP protocol).
12
+ */
13
+ import { createMCPServer } from "./server.js";
14
+ import { createStateTools } from "./tools-state.js";
15
+ import { createCompileTools } from "./tools-compile.js";
16
+ import { createEnforceTools } from "./tools-enforce.js";
17
+ // Parse args
18
+ const args = process.argv.slice(2);
19
+ let projectDir = process.cwd();
20
+ const dirIdx = args.indexOf("--project-dir");
21
+ if (dirIdx !== -1 && args[dirIdx + 1]) {
22
+ projectDir = args[dirIdx + 1];
23
+ }
24
+ // Resolve version from package.json (best-effort)
25
+ let version = "1.5.3";
26
+ try {
27
+ const { readFileSync } = await import("node:fs");
28
+ const { resolve } = await import("node:path");
29
+ const { fileURLToPath } = await import("node:url");
30
+ const pkgPath = resolve(fileURLToPath(import.meta.url), "..", "..", "..", "package.json");
31
+ const pkg = JSON.parse(readFileSync(pkgPath, "utf-8"));
32
+ version = pkg.version ?? version;
33
+ }
34
+ catch { /* use default */ }
35
+ const tools = [
36
+ ...createStateTools(projectDir),
37
+ ...createCompileTools(projectDir),
38
+ ...createEnforceTools(projectDir),
39
+ ];
40
+ createMCPServer({ name: "intentdna", version }, tools);
@@ -0,0 +1,39 @@
1
+ /**
2
+ * Intent DNA — MCP Server Framework
3
+ *
4
+ * Minimal MCP server supporting:
5
+ * - initialize / initialized handshake
6
+ * - tools/list — list available tools
7
+ * - tools/call — execute a tool
8
+ * - ping — health check
9
+ *
10
+ * Zero external dependencies. Speaks MCP protocol over stdio.
11
+ */
12
+ export interface ToolDef {
13
+ name: string;
14
+ description: string;
15
+ inputSchema: {
16
+ type: "object";
17
+ properties: Record<string, unknown>;
18
+ required?: string[];
19
+ };
20
+ handler: (args: Record<string, unknown>) => Promise<ToolResult>;
21
+ }
22
+ export interface ToolResult {
23
+ content: Array<{
24
+ type: "text";
25
+ text: string;
26
+ }>;
27
+ isError?: boolean;
28
+ }
29
+ export declare function textResult(text: string): ToolResult;
30
+ export declare function errorResult(text: string): ToolResult;
31
+ export interface ServerInfo {
32
+ name: string;
33
+ version: string;
34
+ }
35
+ /**
36
+ * Create and start an MCP server with the given tools.
37
+ * Blocks on stdin — call this as the last thing in your entry point.
38
+ */
39
+ export declare function createMCPServer(info: ServerInfo, tools: ToolDef[]): void;
@@ -0,0 +1,84 @@
1
+ /**
2
+ * Intent DNA — MCP Server Framework
3
+ *
4
+ * Minimal MCP server supporting:
5
+ * - initialize / initialized handshake
6
+ * - tools/list — list available tools
7
+ * - tools/call — execute a tool
8
+ * - ping — health check
9
+ *
10
+ * Zero external dependencies. Speaks MCP protocol over stdio.
11
+ */
12
+ import { startTransport, sendResult, sendError } from "./transport.js";
13
+ // ── Result Helpers ───────────────────────────────────────
14
+ export function textResult(text) {
15
+ return { content: [{ type: "text", text }] };
16
+ }
17
+ export function errorResult(text) {
18
+ return { content: [{ type: "text", text }], isError: true };
19
+ }
20
+ /**
21
+ * Create and start an MCP server with the given tools.
22
+ * Blocks on stdin — call this as the last thing in your entry point.
23
+ */
24
+ export function createMCPServer(info, tools) {
25
+ const toolMap = new Map();
26
+ for (const tool of tools) {
27
+ toolMap.set(tool.name, tool);
28
+ }
29
+ startTransport(async (msg) => {
30
+ switch (msg.method) {
31
+ case "initialize":
32
+ sendResult(msg.id ?? null, {
33
+ protocolVersion: "2024-11-05",
34
+ capabilities: { tools: {} },
35
+ serverInfo: { name: info.name, version: info.version },
36
+ });
37
+ break;
38
+ case "notifications/initialized":
39
+ // Client acknowledgement — no response needed
40
+ break;
41
+ case "ping":
42
+ sendResult(msg.id ?? null, {});
43
+ break;
44
+ case "tools/list":
45
+ sendResult(msg.id ?? null, {
46
+ tools: tools.map(t => ({
47
+ name: t.name,
48
+ description: t.description,
49
+ inputSchema: t.inputSchema,
50
+ })),
51
+ });
52
+ break;
53
+ case "tools/call": {
54
+ const params = msg.params ?? {};
55
+ const toolName = params.name;
56
+ const args = (params.arguments ?? {});
57
+ const tool = toolMap.get(toolName);
58
+ if (!tool) {
59
+ sendResult(msg.id ?? null, {
60
+ content: [{ type: "text", text: `Unknown tool: ${toolName}` }],
61
+ isError: true,
62
+ });
63
+ break;
64
+ }
65
+ try {
66
+ const result = await tool.handler(args);
67
+ sendResult(msg.id ?? null, result);
68
+ }
69
+ catch (err) {
70
+ sendResult(msg.id ?? null, {
71
+ content: [{ type: "text", text: `Error: ${err instanceof Error ? err.message : String(err)}` }],
72
+ isError: true,
73
+ });
74
+ }
75
+ break;
76
+ }
77
+ default:
78
+ if (msg.id !== undefined) {
79
+ sendError(msg.id, -32601, `Method not found: ${msg.method}`);
80
+ }
81
+ break;
82
+ }
83
+ });
84
+ }
@@ -0,0 +1,10 @@
1
+ /**
2
+ * Intent DNA — MCP Compile Tools (G3)
3
+ *
4
+ * Tools for compiling and managing DNA via MCP:
5
+ * - dna_compile: Compile DNA config to Constraint IR
6
+ * - dna_validate: Validate a DNA config file
7
+ * - dna_sync: Run full sync pipeline (compile + inject + hooks)
8
+ */
9
+ import type { ToolDef } from "./server.js";
10
+ export declare function createCompileTools(projectDir: string): ToolDef[];