intentdna 1.2.3 → 1.3.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.
@@ -0,0 +1,12 @@
1
+ {
2
+ "hooks": {
3
+ "PreToolUse": [{ "matcher": "*", "hooks": [{ "type": "command", "command": "dna-hook PreToolUse", "timeout": 5000 }] }],
4
+ "PostToolUse": [{ "matcher": "*", "hooks": [{ "type": "command", "command": "dna-hook PostToolUse", "timeout": 3000 }] }],
5
+ "UserPromptSubmit": [{ "matcher": "*", "hooks": [{ "type": "command", "command": "dna-hook UserPromptSubmit", "timeout": 5000 }] }],
6
+ "SubagentStop": [{ "matcher": "*", "hooks": [{ "type": "command", "command": "dna-hook SubagentStop", "timeout": 3000 }] }],
7
+ "PreCompact": [{ "matcher": "*", "hooks": [{ "type": "command", "command": "dna-hook PreCompact", "timeout": 3000 }] }],
8
+ "Notification": [{ "matcher": "*", "hooks": [{ "type": "command", "command": "dna-hook Notification", "timeout": 3000 }] }],
9
+ "Stop": [{ "matcher": "*", "hooks": [{ "type": "command", "command": "dna-hook Stop", "timeout": 3000 }] }],
10
+ "SessionStart": [{ "matcher": "*", "hooks": [{ "type": "command", "command": "dna-hook SessionStart", "timeout": 5000 }] }]
11
+ }
12
+ }
@@ -0,0 +1,5 @@
1
+ {
2
+ "name": "intentdna",
3
+ "version": "0.6.0",
4
+ "description": "Declarative policy layer for AI agent governance"
5
+ }
@@ -0,0 +1,9 @@
1
+ /**
2
+ * dna setup
3
+ *
4
+ * Registers intentdna as a Claude Code plugin.
5
+ * Auto-detects whether claude CLI is available and prints guidance accordingly.
6
+ */
7
+ export interface SetupOptions {
8
+ }
9
+ export declare function runSetup(_opts: SetupOptions): Promise<number>;
@@ -0,0 +1,42 @@
1
+ /**
2
+ * dna setup
3
+ *
4
+ * Registers intentdna as a Claude Code plugin.
5
+ * Auto-detects whether claude CLI is available and prints guidance accordingly.
6
+ */
7
+ import { execSync } from "node:child_process";
8
+ export async function runSetup(_opts) {
9
+ let claudeAvailable = false;
10
+ try {
11
+ execSync("claude --version", { stdio: "ignore" });
12
+ claudeAvailable = true;
13
+ }
14
+ catch {
15
+ // claude CLI not found — fall through to bin mode
16
+ }
17
+ if (claudeAvailable) {
18
+ process.stderr.write("Claude Code CLI detected.\n");
19
+ process.stderr.write("\n");
20
+ process.stderr.write("Intent DNA plugin mode:\n");
21
+ process.stderr.write(" The .claude-plugin/ directory contains plugin.json and hook scripts.\n");
22
+ process.stderr.write(" To register as a plugin, run:\n");
23
+ process.stderr.write("\n");
24
+ let npmRoot = "";
25
+ try {
26
+ npmRoot = execSync("npm root -g", { stdio: ["ignore", "pipe", "ignore"] }).toString().trim();
27
+ }
28
+ catch {
29
+ // ignore
30
+ }
31
+ const pluginPath = npmRoot ? `${npmRoot}/intentdna` : "<npm-root>/intentdna";
32
+ process.stderr.write(` claude plugin add ${pluginPath}\n`);
33
+ process.stderr.write("\n");
34
+ process.stderr.write(" Or run 'dna sync --plugin' to activate hooks via settings.json.\n");
35
+ process.stderr.write("Intent DNA plugin registered. Run 'dna sync' to activate.\n");
36
+ }
37
+ else {
38
+ process.stderr.write("Claude Code CLI not detected. Using bin mode.\n");
39
+ process.stderr.write("Run 'dna sync' to register dna-hook in settings.json\n");
40
+ }
41
+ return 0;
42
+ }
@@ -28,4 +28,16 @@ export interface SyncOptions {
28
28
  remove: boolean;
29
29
  plugin?: boolean;
30
30
  }
31
+ /**
32
+ * Detect whether intentdna is running as a Claude Code plugin or standalone binary.
33
+ *
34
+ * Plugin mode: claude plugin system manages hook dispatch
35
+ * → generate ir.json + agents + skills + .mcp.json
36
+ * → do NOT write settings.json (hooks provided by plugin framework)
37
+ *
38
+ * Bin mode: fallback when plugin not installed
39
+ * → same as above
40
+ * → ALSO write settings.json to register dna-hook events
41
+ */
42
+ export declare function detectMode(): Promise<"plugin" | "bin">;
31
43
  export declare function runSync(opts: SyncOptions): Promise<number>;
@@ -25,6 +25,33 @@ import { createCompiledIR, writeCompiledIR } from "../../runtime/plugin-adapter.
25
25
  import { cascadeDNA } from "../../compiler/cascade.js";
26
26
  import { EvolutionEngine } from "../../evolution/index.js";
27
27
  const VALID_TARGETS = ["claude-md", "soul-md", "cursorrules", "system-prompt"];
28
+ /**
29
+ * Detect whether intentdna is running as a Claude Code plugin or standalone binary.
30
+ *
31
+ * Plugin mode: claude plugin system manages hook dispatch
32
+ * → generate ir.json + agents + skills + .mcp.json
33
+ * → do NOT write settings.json (hooks provided by plugin framework)
34
+ *
35
+ * Bin mode: fallback when plugin not installed
36
+ * → same as above
37
+ * → ALSO write settings.json to register dna-hook events
38
+ */
39
+ export async function detectMode() {
40
+ // Check: is intentdna registered as a Claude Code plugin?
41
+ try {
42
+ const { execSync } = await import("node:child_process");
43
+ const result = execSync("claude plugin list 2>/dev/null", {
44
+ encoding: "utf-8",
45
+ timeout: 5000,
46
+ });
47
+ if (result.includes("intentdna"))
48
+ return "plugin";
49
+ }
50
+ catch {
51
+ // claude CLI not available or plugin not registered
52
+ }
53
+ return "bin";
54
+ }
28
55
  async function fileExists(path) {
29
56
  try {
30
57
  await stat(path);
@@ -245,6 +272,14 @@ export async function runSync(opts) {
245
272
  process.stderr.write(`Auto-detect: Claude Code → CLAUDE.md + agents + skills + settings\n`);
246
273
  }
247
274
  }
275
+ // Auto-detect plugin vs bin mode if --plugin not explicitly set
276
+ if (!opts.remove && opts.plugin === undefined) {
277
+ const mode = await detectMode();
278
+ if (mode === "plugin") {
279
+ opts.plugin = true;
280
+ process.stderr.write("Auto-detect: plugin mode (intentdna registered as Claude Code plugin)\n");
281
+ }
282
+ }
248
283
  // Handle --remove
249
284
  if (opts.remove) {
250
285
  if (!opts.inject && !opts.hooksDir && !opts.agentsDir && !opts.workflowDir) {
@@ -340,34 +375,37 @@ export async function runSync(opts) {
340
375
  }
341
376
  trackedOutputs.push(resolve(opts.inject));
342
377
  }
343
- // Step 3.5: Plugin mode write IR + register dna-hook
344
- if (opts.plugin) {
378
+ // Step 3.5: Write compiled IR (needed for both plugin and bin mode)
379
+ {
345
380
  const cwd = process.cwd();
346
- // Write compiled IR
347
381
  const dnas = await Promise.all(expandedFiles.map(loadDNA));
348
382
  const cascadedForIR = cascadeDNA(dnas);
349
383
  const roles = cascadedForIR.roles;
350
384
  const compiled = createCompiledIR(ir, roles);
351
385
  const irPath = await writeCompiledIR(cwd, compiled);
352
- process.stderr.write(`Plugin: wrote ${irPath}\n`);
386
+ process.stderr.write(`Wrote compiled IR: ${irPath}\n`);
353
387
  trackedOutputs.push(resolve(irPath));
354
- // Register dna-hook events in settings.json
355
- if (opts.settingsPath) {
356
- const enabledEvents = detectEnabledEvents(ir);
357
- if (enabledEvents.length > 0) {
358
- const pluginSettings = compilePluginSettings(enabledEvents);
359
- await mergeSettingsFile(pluginSettings, opts.settingsPath);
360
- process.stderr.write(`Plugin: registered ${enabledEvents.length} dna-hook event(s) in ${opts.settingsPath}\n`);
361
- trackedOutputs.push(resolve(opts.settingsPath));
362
- }
363
- }
364
388
  // Clean up old bash hooks if they exist
365
389
  const oldHooksDir = resolve(cwd, ".claude", "hooks");
366
390
  const removed = await removeLegacyBashHooks(oldHooksDir);
367
391
  if (removed > 0) {
368
- process.stderr.write(`Plugin: cleaned ${removed} legacy bash hook(s) from ${oldHooksDir}\n`);
392
+ process.stderr.write(`Cleaned ${removed} legacy bash hook(s) from ${oldHooksDir}\n`);
393
+ }
394
+ }
395
+ // Step 3.6: Register hooks in settings.json (bin mode only)
396
+ // Plugin mode: hooks managed by plugin framework — skip settings.json
397
+ if (!opts.plugin && opts.settingsPath) {
398
+ const enabledEvents = detectEnabledEvents(ir);
399
+ if (enabledEvents.length > 0) {
400
+ const pluginSettings = compilePluginSettings(enabledEvents);
401
+ await mergeSettingsFile(pluginSettings, opts.settingsPath);
402
+ process.stderr.write(`Bin mode: registered ${enabledEvents.length} hook event(s) in ${opts.settingsPath}\n`);
403
+ trackedOutputs.push(resolve(opts.settingsPath));
369
404
  }
370
405
  }
406
+ else if (opts.plugin && opts.settingsPath) {
407
+ process.stderr.write(`Plugin mode: hooks managed by plugin framework (skipping settings.json)\n`);
408
+ }
371
409
  // Step 5: Generate agent MD files
372
410
  if (opts.agentsDir) {
373
411
  // Clean stale DNA-managed agents before regenerating
@@ -448,6 +486,20 @@ export async function runSync(opts) {
448
486
  }
449
487
  const configPath = await autoDetectDNA();
450
488
  const variables = await resolveVariables(rawVars, configPath);
489
+ // Step 8: Generate .claude/.mcp.json from MCP dependencies (inside skillsDir block, shares variables)
490
+ {
491
+ const mcpDeps = {};
492
+ for (const d of dnas) {
493
+ if (d.mcp)
494
+ Object.assign(mcpDeps, d.mcp);
495
+ }
496
+ if (Object.keys(mcpDeps).length > 0) {
497
+ const cwd = process.cwd();
498
+ const mcpJsonPath = resolve(cwd, ".claude", ".mcp.json");
499
+ await writeMCPConfig(mcpDeps, mcpJsonPath, variables);
500
+ process.stderr.write(`MCP: wrote ${Object.keys(mcpDeps).length} server(s) to ${mcpJsonPath}\n`);
501
+ }
502
+ }
451
503
  if (Object.keys(workflows).length === 0) {
452
504
  process.stderr.write("No workflow defined — no skills to generate\n");
453
505
  }
@@ -488,3 +540,51 @@ export async function runSync(opts) {
488
540
  return 1;
489
541
  }
490
542
  }
543
+ /**
544
+ * Write MCP server configuration to .claude/.mcp.json.
545
+ * Merges with existing config — DNA-declared servers are added/updated,
546
+ * but existing user-configured servers are NOT overwritten.
547
+ */
548
+ async function writeMCPConfig(mcpDeps, mcpJsonPath, variables) {
549
+ // Read existing .mcp.json
550
+ let existing = {};
551
+ try {
552
+ const raw = await readFile(mcpJsonPath, "utf-8");
553
+ existing = JSON.parse(raw);
554
+ }
555
+ catch { /* doesn't exist yet */ }
556
+ const mcpServers = (existing.mcpServers ?? {});
557
+ for (const [name, server] of Object.entries(mcpDeps)) {
558
+ // Don't overwrite existing user-configured servers
559
+ if (mcpServers[name])
560
+ continue;
561
+ const entry = {};
562
+ if (server.command) {
563
+ entry.command = server.command;
564
+ if (server.args)
565
+ entry.args = server.args;
566
+ }
567
+ if (server.url) {
568
+ entry.url = server.url;
569
+ }
570
+ if (server.env) {
571
+ // Substitute {{variable}} placeholders
572
+ const resolvedEnv = {};
573
+ for (const [k, v] of Object.entries(server.env)) {
574
+ resolvedEnv[k] = substituteVariables(v, variables);
575
+ }
576
+ entry.env = resolvedEnv;
577
+ }
578
+ if (server.timeout) {
579
+ entry.timeout = server.timeout;
580
+ }
581
+ mcpServers[name] = entry;
582
+ }
583
+ existing.mcpServers = mcpServers;
584
+ await mkdir(dirname(mcpJsonPath), { recursive: true });
585
+ await writeFileAsync(mcpJsonPath, JSON.stringify(existing, null, 2) + "\n", "utf-8");
586
+ }
587
+ /** Replace {{var}} placeholders with resolved variable values */
588
+ function substituteVariables(template, vars) {
589
+ return template.replace(/\{\{(\w+)\}\}/g, (_, key) => vars[key] ?? `{{${key}}}`);
590
+ }
package/dist/cli/index.js CHANGED
@@ -17,6 +17,7 @@ const HELP = `Intent DNA CLI v0.3.0
17
17
  Usage: dna <command> [options]
18
18
 
19
19
  Commands:
20
+ setup Register intentdna as a Claude Code plugin (auto-detect mode)
20
21
  guard Zero-config guardrails — detect environment, apply safety rules
21
22
  sync Compile + inject DNA into target file (CLAUDE.md, SOUL.md, etc.)
22
23
  verify Verify synced files match .dna/lock checksums (drift detection)
@@ -355,6 +356,12 @@ async function main() {
355
356
  process.exit(code);
356
357
  break;
357
358
  }
359
+ case "setup": {
360
+ const { runSetup } = await import("./commands/setup.js");
361
+ const code = await runSetup({});
362
+ process.exit(code);
363
+ break;
364
+ }
358
365
  default:
359
366
  process.stderr.write(`Unknown command: ${command}\n\n`);
360
367
  process.stderr.write(HELP);
@@ -212,6 +212,35 @@ export function compileDNA(activated, cascaded) {
212
212
  }
213
213
  }
214
214
  }
215
+ // Build workflows_ir: per-workflow partitioned view with namespace context
216
+ const workflowsIR = [];
217
+ if (cascaded?.workflows) {
218
+ for (const [wfKey, wf] of Object.entries(cascaded.workflows)) {
219
+ const wfStepCheckpoints = [];
220
+ const activeRoles = [];
221
+ for (const step of wf.steps ?? []) {
222
+ if (!activeRoles.includes(step.role)) {
223
+ activeRoles.push(step.role);
224
+ }
225
+ if (step.checkpoints && step.checkpoints.length > 0) {
226
+ wfStepCheckpoints.push({
227
+ step_role: step.role,
228
+ step_id: step.id,
229
+ checkpoints: [...step.checkpoints],
230
+ });
231
+ }
232
+ }
233
+ // Derive namespace from workflow key: "ns_wfname" → "ns", "wfname" → ""
234
+ const underscoreIdx = wfKey.indexOf("_");
235
+ const namespace = underscoreIdx > 0 ? wfKey.slice(0, underscoreIdx) : "";
236
+ workflowsIR.push({
237
+ workflow_name: wfKey,
238
+ namespace,
239
+ step_checkpoints: wfStepCheckpoints,
240
+ active_roles: activeRoles,
241
+ });
242
+ }
243
+ }
215
244
  // Sort directives by priority
216
245
  const priorityOrder = { high: 0, medium: 1, low: 2 };
217
246
  directives.sort((a, b) => priorityOrder[a.priority] - priorityOrder[b.priority]);
@@ -229,5 +258,6 @@ export function compileDNA(activated, cascaded) {
229
258
  role_scope: roleScope,
230
259
  roles_scope_map: rolesScopeMap.length > 0 ? rolesScopeMap : undefined,
231
260
  step_checkpoints: stepCheckpoints.length > 0 ? stepCheckpoints : undefined,
261
+ workflows_ir: workflowsIR.length > 0 ? workflowsIR : undefined,
232
262
  };
233
263
  }
package/dist/hooks/cli.js CHANGED
@@ -17,8 +17,8 @@
17
17
  import { readFile } from "node:fs/promises";
18
18
  import { resolve } from "node:path";
19
19
  import { readStdin, writeOutput, silentOutput } from "./protocol.js";
20
- import { enforcePreToolUse, enforcePostToolUse, enforceUserPromptSubmit, enforceSubagentStop, enforcePreCompact, enforceNotification, } from "./enforce.js";
21
- import { appendAudit } from "./state.js";
20
+ import { enforcePreToolUse, enforcePostToolUse, enforceUserPromptSubmit, enforceSubagentStop, enforcePreCompact, enforceNotification, enforceSessionStart, enforceStop, } from "./enforce.js";
21
+ import { appendAudit, readWorkflowState } from "./state.js";
22
22
  // ── Constants ──────────────────────────────────────────────
23
23
  const DEFAULT_IR_PATH = ".dna/compiled/ir.json";
24
24
  const VALID_EVENTS = new Set([
@@ -52,6 +52,24 @@ async function main() {
52
52
  return;
53
53
  }
54
54
  const state = {};
55
+ // Special handling for Stop — needs async workflow state read
56
+ if (event === "Stop") {
57
+ const wfState = await readWorkflowState(projectDir, sessionId);
58
+ const stopContext = wfState ? {
59
+ active: wfState.active,
60
+ workflow: wfState.workflow,
61
+ current_step: wfState.current_step,
62
+ current_role: wfState.current_role,
63
+ started_at: wfState.started_at,
64
+ } : null;
65
+ const stopOutput = enforceStop(ir, {
66
+ cwd: typeof rawInput.cwd === "string" ? rawInput.cwd : undefined,
67
+ session_id: sessionId,
68
+ stop_reason: typeof rawInput.stop_reason === "string" ? rawInput.stop_reason : undefined,
69
+ }, stopContext);
70
+ writeOutput(stopOutput);
71
+ return;
72
+ }
55
73
  // Dispatch to enforcement engine
56
74
  const output = dispatch(event, ir, rawInput, state);
57
75
  writeOutput(output);
@@ -103,7 +121,11 @@ function dispatch(event, ir, input, state) {
103
121
  case "Stop":
104
122
  return silentOutput(); // DNA doesn't block stops
105
123
  case "SessionStart":
106
- return silentOutput(); // Placeholder — will initialize DNA state in future
124
+ return enforceSessionStart(ir, {
125
+ cwd: typeof input.cwd === "string" ? input.cwd : undefined,
126
+ session_id: typeof input.sessionId === "string" ? input.sessionId : undefined,
127
+ trigger: typeof input.trigger === "string" ? input.trigger : undefined,
128
+ });
107
129
  default:
108
130
  return silentOutput();
109
131
  }
@@ -15,6 +15,12 @@
15
15
  */
16
16
  import type { ConstraintIR, RoleDef } from "../schema/types.js";
17
17
  import type { PreToolUseInput, PostToolUseInput, UserPromptSubmitInput, SubagentStopInput, NotificationInput, HookOutput } from "./protocol.js";
18
+ /** SessionStart input fields */
19
+ export interface SessionStartInput {
20
+ cwd?: string;
21
+ session_id?: string;
22
+ trigger?: string;
23
+ }
18
24
  export interface EnforceState {
19
25
  }
20
26
  /**
@@ -47,6 +53,38 @@ export declare function enforcePreCompact(ir: ConstraintIR): HookOutput;
47
53
  * The CLI handles actual audit file writes.
48
54
  */
49
55
  export declare function enforceNotification(ir: ConstraintIR, input: NotificationInput): HookOutput;
56
+ /**
57
+ * Initialize DNA state and inject policy summary on session start.
58
+ * Returns silent if no DNA policies are active.
59
+ */
60
+ export declare function enforceSessionStart(ir: ConstraintIR, input: SessionStartInput): HookOutput;
61
+ /** Stop enforcement input — includes workflow awareness */
62
+ export interface StopEnforceInput {
63
+ cwd?: string;
64
+ session_id?: string;
65
+ stop_reason?: string;
66
+ }
67
+ /** Workflow state context for stop enforcement */
68
+ export interface StopWorkflowContext {
69
+ active: boolean;
70
+ workflow: string;
71
+ current_step: string;
72
+ current_role: string;
73
+ started_at: string;
74
+ }
75
+ /**
76
+ * Enforce Stop hook — verify workflow checkpoint completion.
77
+ *
78
+ * Safety valves (NEVER block):
79
+ * - context_limit / context_window stops
80
+ * - Stale workflow state (>2h)
81
+ * - No active workflow
82
+ * - No checkpoints defined
83
+ *
84
+ * Block when:
85
+ * - Workflow active + unmet checkpoints for current step
86
+ */
87
+ export declare function enforceStop(ir: ConstraintIR, input: StopEnforceInput, workflowState: StopWorkflowContext | null): HookOutput;
50
88
  /** Check if a file path is allowed by a list of write globs (prefix matching). */
51
89
  export declare function checkWriteAllowed(filePath: string, allowedGlobs: string[]): boolean;
52
90
  /**
@@ -145,6 +145,105 @@ export function enforceNotification(ir, input) {
145
145
  }
146
146
  return silentOutput();
147
147
  }
148
+ // ── SessionStart Enforcement ───────────────────────────────
149
+ /**
150
+ * Initialize DNA state and inject policy summary on session start.
151
+ * Returns silent if no DNA policies are active.
152
+ */
153
+ export function enforceSessionStart(ir, input) {
154
+ // Check if there's anything meaningful to report
155
+ const directiveCount = ir.prompt_directives.length;
156
+ const gateCount = ir.pre_execution_gates.length;
157
+ const filterCount = ir.tool_filters.length;
158
+ const roleCount = ir.roles_scope_map?.length ?? 0;
159
+ const workflowCount = ir.workflows_ir?.length ?? 0;
160
+ if (directiveCount === 0 && gateCount === 0 && filterCount === 0 && roleCount === 0) {
161
+ return silentOutput();
162
+ }
163
+ // Build policy summary
164
+ const lines = [
165
+ "[Intent DNA] Session initialized — active governance:",
166
+ ];
167
+ if (ir.source_dna_ids.length > 0) {
168
+ lines.push(` Templates: ${ir.source_dna_ids.join(", ")}`);
169
+ }
170
+ const stats = [];
171
+ if (directiveCount > 0)
172
+ stats.push(`${directiveCount} directives`);
173
+ if (gateCount > 0)
174
+ stats.push(`${gateCount} gates`);
175
+ if (filterCount > 0)
176
+ stats.push(`${filterCount} filters`);
177
+ if (roleCount > 0)
178
+ stats.push(`${roleCount} roles`);
179
+ if (workflowCount > 0)
180
+ stats.push(`${workflowCount} workflows`);
181
+ if (stats.length > 0) {
182
+ lines.push(` Enforcement: ${stats.join(", ")}`);
183
+ }
184
+ return allowOutput(lines.join("\n"));
185
+ }
186
+ /**
187
+ * Enforce Stop hook — verify workflow checkpoint completion.
188
+ *
189
+ * Safety valves (NEVER block):
190
+ * - context_limit / context_window stops
191
+ * - Stale workflow state (>2h)
192
+ * - No active workflow
193
+ * - No checkpoints defined
194
+ *
195
+ * Block when:
196
+ * - Workflow active + unmet checkpoints for current step
197
+ */
198
+ export function enforceStop(ir, input, workflowState) {
199
+ // Safety valve 1: no workflow state → silent
200
+ if (!workflowState || !workflowState.active) {
201
+ return silentOutput();
202
+ }
203
+ // Safety valve 2: context_limit stops → NEVER block
204
+ const reason = input.stop_reason ?? "";
205
+ if (reason.includes("context_limit") || reason.includes("context_window")) {
206
+ return silentOutput();
207
+ }
208
+ // Safety valve 3: user abort → don't block
209
+ if (reason === "user_abort" || reason === "sigint") {
210
+ return silentOutput();
211
+ }
212
+ // Safety valve 4: stale state (>2h)
213
+ const STALE_MS = 2 * 60 * 60 * 1000;
214
+ if (workflowState.started_at) {
215
+ const age = Date.now() - new Date(workflowState.started_at).getTime();
216
+ if (age > STALE_MS) {
217
+ return silentOutput();
218
+ }
219
+ }
220
+ // Check for unmet checkpoints in current step
221
+ const checkpoints = ir.step_checkpoints ?? [];
222
+ const currentStepCheckpoints = checkpoints.filter(cp => cp.step_id === workflowState.current_step);
223
+ // Also check workflows_ir for the active workflow's checkpoints
224
+ let workflowCheckpoints = currentStepCheckpoints;
225
+ if (ir.workflows_ir && ir.workflows_ir.length > 0) {
226
+ const activeWf = ir.workflows_ir.find(w => w.workflow_name === workflowState.workflow);
227
+ if (activeWf) {
228
+ const wfStepCps = activeWf.step_checkpoints.filter(cp => cp.step_id === workflowState.current_step);
229
+ if (wfStepCps.length > 0) {
230
+ workflowCheckpoints = wfStepCps;
231
+ }
232
+ }
233
+ }
234
+ if (workflowCheckpoints.length === 0) {
235
+ return silentOutput(); // No checkpoints for current step
236
+ }
237
+ // Build list of blocking checkpoints
238
+ const blocking = workflowCheckpoints.flatMap(cp => cp.checkpoints.filter(c => (c.action ?? "block") === "block"));
239
+ if (blocking.length === 0) {
240
+ return silentOutput();
241
+ }
242
+ // Block: workflow active with unmet checkpoints
243
+ const messages = blocking.map(c => c.message);
244
+ return blockOutput(`[Intent DNA] Workflow '${workflowState.workflow}' has unmet checkpoints at step '${workflowState.current_step}':\n` +
245
+ messages.map(m => ` - ${m}`).join("\n"));
246
+ }
148
247
  // ── Internal: Layer Enforcement ────────────────────────────
149
248
  function enforceRoleScope(rolesScopeMap, input) {
150
249
  if (!input.agent_type)
@@ -4,6 +4,6 @@
4
4
  * Public exports for the hook enforcement engine.
5
5
  * Used by the `dna-hook` CLI and importable for programmatic use.
6
6
  */
7
- export { type HookEvent, type HookInput, type HookOutput, type HookInputBase, type PreToolUseInput, type PostToolUseInput, type UserPromptSubmitInput, type SubagentStopInput, type PreCompactInput, type NotificationInput, type StopInput, readStdin, writeOutput, allowOutput, blockOutput, escalateOutput, silentOutput, } from "./protocol.js";
8
- export { type EnforceState, enforcePreToolUse, enforcePostToolUse, enforceUserPromptSubmit, enforceSubagentStop, enforcePreCompact, enforceNotification, checkWriteAllowed, globToPrefix, evaluateGateCondition, resolveToolTarget, } from "./enforce.js";
7
+ export { type HookEvent, type HookInput, type HookOutput, type HookInputBase, type PreToolUseInput, type PostToolUseInput, type UserPromptSubmitInput, type SubagentStopInput, type PreCompactInput, type NotificationInput, type StopInput, type SessionStartInput as SessionStartHookInput, readStdin, writeOutput, allowOutput, blockOutput, escalateOutput, silentOutput, } from "./protocol.js";
8
+ export { type EnforceState, type SessionStartInput, type StopEnforceInput, type StopWorkflowContext, enforcePreToolUse, enforcePostToolUse, enforceUserPromptSubmit, enforceSubagentStop, enforcePreCompact, enforceNotification, enforceSessionStart, enforceStop, checkWriteAllowed, globToPrefix, evaluateGateCondition, resolveToolTarget, } from "./enforce.js";
9
9
  export { type DNAWorkflowState, type AuditEntry, resolveStateDir, readWorkflowState, writeWorkflowState, clearWorkflowState, appendAudit, } from "./state.js";
@@ -7,6 +7,6 @@
7
7
  // Protocol types and I/O
8
8
  export { readStdin, writeOutput, allowOutput, blockOutput, escalateOutput, silentOutput, } from "./protocol.js";
9
9
  // Enforcement engine
10
- export { enforcePreToolUse, enforcePostToolUse, enforceUserPromptSubmit, enforceSubagentStop, enforcePreCompact, enforceNotification, checkWriteAllowed, globToPrefix, evaluateGateCondition, resolveToolTarget, } from "./enforce.js";
10
+ export { enforcePreToolUse, enforcePostToolUse, enforceUserPromptSubmit, enforceSubagentStop, enforcePreCompact, enforceNotification, enforceSessionStart, enforceStop, checkWriteAllowed, globToPrefix, evaluateGateCondition, resolveToolTarget, } from "./enforce.js";
11
11
  // State management
12
12
  export { resolveStateDir, readWorkflowState, writeWorkflowState, clearWorkflowState, appendAudit, } from "./state.js";
@@ -39,7 +39,10 @@ export interface NotificationInput extends HookInputBase {
39
39
  export interface StopInput extends HookInputBase {
40
40
  stop_reason?: string;
41
41
  }
42
- export type HookInput = PreToolUseInput | PostToolUseInput | UserPromptSubmitInput | SubagentStopInput | PreCompactInput | NotificationInput | StopInput;
42
+ export interface SessionStartInput extends HookInputBase {
43
+ trigger?: string;
44
+ }
45
+ export type HookInput = PreToolUseInput | PostToolUseInput | UserPromptSubmitInput | SubagentStopInput | PreCompactInput | NotificationInput | StopInput | SessionStartInput;
43
46
  export interface HookOutput {
44
47
  continue: boolean;
45
48
  decision?: "block" | "allow";
@@ -166,6 +166,16 @@ export interface DNAMetadata {
166
166
  gene_count: number;
167
167
  epigenetic_marker_count: number;
168
168
  }
169
+ /** MCP server dependency declaration */
170
+ export interface MCPServerDef {
171
+ description: string;
172
+ command?: string;
173
+ args?: string[];
174
+ env?: Record<string, string>;
175
+ url?: string;
176
+ timeout?: number;
177
+ optional?: boolean;
178
+ }
169
179
  export interface IntentDNA {
170
180
  $schema?: string;
171
181
  version: string;
@@ -179,6 +189,7 @@ export interface IntentDNA {
179
189
  workflow?: WorkflowDef;
180
190
  workflows?: Record<string, WorkflowDef>;
181
191
  variables?: Record<string, string | VariableDef>;
192
+ mcp?: Record<string, MCPServerDef>;
182
193
  epigenetic: {
183
194
  markers: EpigeneticMarker[];
184
195
  };
@@ -225,6 +236,13 @@ export interface StepCheckpointIR {
225
236
  step_id: string;
226
237
  checkpoints: StepCheckpoint[];
227
238
  }
239
+ /** Workflow-partitioned IR — isolates constraints per workflow */
240
+ export interface WorkflowIR {
241
+ workflow_name: string;
242
+ namespace: string;
243
+ step_checkpoints: StepCheckpointIR[];
244
+ active_roles: string[];
245
+ }
228
246
  /** Runtime workflow state written to .dna/state/workflow.json */
229
247
  export interface WorkflowState {
230
248
  active: boolean;
@@ -249,6 +267,7 @@ export interface ConstraintIR {
249
267
  role_scope?: ScopeDef;
250
268
  roles_scope_map?: RoleScopeEntry[];
251
269
  step_checkpoints?: StepCheckpointIR[];
270
+ workflows_ir?: WorkflowIR[];
252
271
  }
253
272
  /** A compiled workflow step with resolved metadata */
254
273
  export interface WorkflowStep {
@@ -393,6 +393,35 @@ export function validateDNA(dna) {
393
393
  errors.push(...validateWorkflow(wfDef, roleNamesSet, `workflows.${wfName}`));
394
394
  }
395
395
  }
396
+ // Validate mcp
397
+ if (dna.mcp) {
398
+ for (const [name, server] of Object.entries(dna.mcp)) {
399
+ const mcpPath = `mcp.${name}`;
400
+ if (!server.description) {
401
+ errors.push({ path: `${mcpPath}.description`, message: "MCP server requires 'description'" });
402
+ }
403
+ // Must have either command or url
404
+ if (!server.command && !server.url) {
405
+ errors.push({ path: mcpPath, message: "MCP server requires either 'command' or 'url'" });
406
+ }
407
+ // Can't have both command and url
408
+ if (server.command && server.url) {
409
+ errors.push({ path: mcpPath, message: "MCP server cannot have both 'command' and 'url'" });
410
+ }
411
+ // timeout must be positive
412
+ if (server.timeout !== undefined && server.timeout <= 0) {
413
+ errors.push({ path: `${mcpPath}.timeout`, message: "timeout must be positive" });
414
+ }
415
+ // env values must be strings
416
+ if (server.env) {
417
+ for (const [envKey, envVal] of Object.entries(server.env)) {
418
+ if (typeof envVal !== "string") {
419
+ errors.push({ path: `${mcpPath}.env.${envKey}`, message: "env value must be a string" });
420
+ }
421
+ }
422
+ }
423
+ }
424
+ }
396
425
  // Validate epigenetic markers
397
426
  for (let i = 0; i < (dna.epigenetic?.markers?.length ?? 0); i++) {
398
427
  const marker = dna.epigenetic.markers[i];
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "intentdna",
3
- "version": "1.2.3",
3
+ "version": "1.3.0",
4
4
  "description": "Intent DNA — Declarative policy layer for AI agent behavior",
5
5
  "type": "module",
6
6
  "main": "dist/index.js",
@@ -33,6 +33,7 @@
33
33
  ],
34
34
  "files": [
35
35
  "dist",
36
+ ".claude-plugin",
36
37
  "README.md",
37
38
  "LICENSE"
38
39
  ],