intentdna 1.2.3 → 1.4.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,12 @@
1
+ {
2
+ "name": "intentdna",
3
+ "description": "Declarative policy layer for AI agent governance — compile DNA templates to hooks, agents, and constraints",
4
+ "plugins": [
5
+ {
6
+ "name": "intentdna",
7
+ "description": "DNA template compilation + runtime enforcement",
8
+ "version": "1.2.3",
9
+ "source": "./"
10
+ }
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,21 @@
1
+ /**
2
+ * dna setup [--scope user|project] [--yes]
3
+ *
4
+ * Registers intentdna as a Claude Code plugin:
5
+ * 1. Detect claude CLI availability
6
+ * 2. Resolve package path (npm global root)
7
+ * 3. Confirm with user (unless --yes)
8
+ * 4. Run: claude plugin marketplace add <path>
9
+ * 5. Run: claude plugin install intentdna@intentdna --scope <scope>
10
+ * 6. Verify registration
11
+ *
12
+ * Falls back to bin mode guidance when claude CLI is unavailable.
13
+ *
14
+ * Reference: OMC plugin registration flow (marketplace add → plugin install)
15
+ */
16
+ export type SetupScope = "user" | "project";
17
+ export interface SetupOptions {
18
+ scope: SetupScope;
19
+ yes: boolean;
20
+ }
21
+ export declare function runSetup(opts: SetupOptions): Promise<number>;
@@ -0,0 +1,192 @@
1
+ /**
2
+ * dna setup [--scope user|project] [--yes]
3
+ *
4
+ * Registers intentdna as a Claude Code plugin:
5
+ * 1. Detect claude CLI availability
6
+ * 2. Resolve package path (npm global root)
7
+ * 3. Confirm with user (unless --yes)
8
+ * 4. Run: claude plugin marketplace add <path>
9
+ * 5. Run: claude plugin install intentdna@intentdna --scope <scope>
10
+ * 6. Verify registration
11
+ *
12
+ * Falls back to bin mode guidance when claude CLI is unavailable.
13
+ *
14
+ * Reference: OMC plugin registration flow (marketplace add → plugin install)
15
+ */
16
+ import { execSync } from "node:child_process";
17
+ import { createInterface } from "node:readline";
18
+ import { resolve, dirname } from "node:path";
19
+ import { fileURLToPath } from "node:url";
20
+ import { stat } from "node:fs/promises";
21
+ // ── Helpers ───────────────────────────────────────────────
22
+ function log(msg) {
23
+ process.stderr.write(msg + "\n");
24
+ }
25
+ async function fileExists(path) {
26
+ try {
27
+ await stat(path);
28
+ return true;
29
+ }
30
+ catch {
31
+ return false;
32
+ }
33
+ }
34
+ function confirm(question) {
35
+ return new Promise((res) => {
36
+ const rl = createInterface({ input: process.stdin, output: process.stderr });
37
+ rl.question(question + " [Y/n] ", (answer) => {
38
+ rl.close();
39
+ const a = answer.trim().toLowerCase();
40
+ res(a === "" || a === "y" || a === "yes");
41
+ });
42
+ });
43
+ }
44
+ /**
45
+ * Check if claude CLI is available and return its version string.
46
+ */
47
+ function detectClaudeCLI() {
48
+ try {
49
+ return execSync("claude --version 2>/dev/null", { encoding: "utf-8", timeout: 5000 }).trim();
50
+ }
51
+ catch {
52
+ return null;
53
+ }
54
+ }
55
+ /**
56
+ * Resolve the intentdna package root directory.
57
+ * Priority: npm global install → local package directory (development).
58
+ */
59
+ async function resolvePackagePath() {
60
+ // 1. Try npm global root
61
+ try {
62
+ const npmRoot = execSync("npm root -g", {
63
+ encoding: "utf-8",
64
+ timeout: 5000,
65
+ stdio: ["ignore", "pipe", "ignore"],
66
+ }).trim();
67
+ const globalPath = resolve(npmRoot, "intentdna");
68
+ if (await fileExists(resolve(globalPath, ".claude-plugin", "plugin.json"))) {
69
+ return globalPath;
70
+ }
71
+ }
72
+ catch { /* not installed globally */ }
73
+ // 2. Try local package (development mode — this file is inside the package)
74
+ const thisDir = dirname(fileURLToPath(import.meta.url));
75
+ // In dist: dist/cli/commands/setup.js → ../../.. = package root
76
+ const localRoot = resolve(thisDir, "..", "..", "..");
77
+ if (await fileExists(resolve(localRoot, ".claude-plugin", "plugin.json"))) {
78
+ return localRoot;
79
+ }
80
+ return null;
81
+ }
82
+ /**
83
+ * Check if intentdna is already registered as a plugin.
84
+ */
85
+ function isPluginRegistered() {
86
+ try {
87
+ const result = execSync("claude plugin list 2>/dev/null", {
88
+ encoding: "utf-8",
89
+ timeout: 5000,
90
+ });
91
+ return result.includes("intentdna");
92
+ }
93
+ catch {
94
+ return false;
95
+ }
96
+ }
97
+ // ── Main ──────────────────────────────────────────────────
98
+ export async function runSetup(opts) {
99
+ const scope = opts.scope;
100
+ // Step 1: Detect claude CLI
101
+ const claudeVersion = detectClaudeCLI();
102
+ if (!claudeVersion) {
103
+ log("Claude Code CLI not detected.");
104
+ log("");
105
+ log("Intent DNA can run in bin mode (standalone dna-hook binary):");
106
+ log(" dna sync Register dna-hook events in settings.json");
107
+ log(" dna sync --plugin Force plugin-style IR generation");
108
+ log("");
109
+ log("To use plugin mode, install Claude Code first, then re-run 'dna setup'.");
110
+ return 0;
111
+ }
112
+ log(`Claude Code detected: ${claudeVersion}`);
113
+ // Step 2: Check if already registered
114
+ if (isPluginRegistered()) {
115
+ log("Intent DNA plugin is already registered.");
116
+ log("Run 'dna sync' to compile and activate your DNA templates.");
117
+ return 0;
118
+ }
119
+ // Step 3: Resolve package path
120
+ const pkgPath = await resolvePackagePath();
121
+ if (!pkgPath) {
122
+ log("Error: Could not find intentdna package with .claude-plugin/ directory.");
123
+ log("Ensure intentdna is installed: npm install -g intentdna");
124
+ return 1;
125
+ }
126
+ log(`Package found: ${pkgPath}`);
127
+ log(`Scope: ${scope}`);
128
+ // Step 4: Confirmation prompt
129
+ if (!opts.yes) {
130
+ log("");
131
+ log("This will:");
132
+ log(` 1. Register '${pkgPath}' as a plugin marketplace`);
133
+ log(` 2. Install intentdna plugin (scope: ${scope})`);
134
+ log("");
135
+ if (process.stdin.isTTY) {
136
+ const ok = await confirm("Proceed?");
137
+ if (!ok) {
138
+ log("Aborted.");
139
+ return 0;
140
+ }
141
+ }
142
+ }
143
+ // Step 5: Register marketplace
144
+ try {
145
+ log("");
146
+ log(`Running: claude plugin marketplace add ${pkgPath}`);
147
+ execSync(`claude plugin marketplace add "${pkgPath}"`, {
148
+ encoding: "utf-8",
149
+ timeout: 30000,
150
+ stdio: ["inherit", "pipe", "pipe"],
151
+ });
152
+ log("Marketplace registered.");
153
+ }
154
+ catch (err) {
155
+ const msg = err instanceof Error ? err.message : String(err);
156
+ log(`Warning: marketplace add failed: ${msg}`);
157
+ log("You may need to run manually:");
158
+ log(` claude plugin marketplace add "${pkgPath}"`);
159
+ // Continue — install may still work if marketplace was already added
160
+ }
161
+ // Step 6: Install plugin
162
+ try {
163
+ const installCmd = `claude plugin install intentdna@intentdna --scope ${scope}`;
164
+ log(`Running: ${installCmd}`);
165
+ execSync(installCmd, {
166
+ encoding: "utf-8",
167
+ timeout: 30000,
168
+ stdio: ["inherit", "pipe", "pipe"],
169
+ });
170
+ log("Plugin installed.");
171
+ }
172
+ catch (err) {
173
+ const msg = err instanceof Error ? err.message : String(err);
174
+ log(`Warning: plugin install failed: ${msg}`);
175
+ log("You may need to run manually:");
176
+ log(` claude plugin install intentdna@intentdna --scope ${scope}`);
177
+ return 1;
178
+ }
179
+ // Step 7: Verify
180
+ if (isPluginRegistered()) {
181
+ log("");
182
+ log("Intent DNA plugin registered successfully.");
183
+ log("Run 'dna sync' to compile and activate your DNA templates.");
184
+ return 0;
185
+ }
186
+ else {
187
+ log("");
188
+ log("Plugin install completed but verification failed.");
189
+ log("Try: claude plugin list");
190
+ return 1;
191
+ }
192
+ }
@@ -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
+ }
@@ -11,6 +11,7 @@
11
11
  */
12
12
  export interface VerifyOptions {
13
13
  lockFile: string;
14
+ stats?: boolean;
14
15
  }
15
16
  export interface LockFileEntry {
16
17
  sha256: string;
@@ -11,6 +11,7 @@
11
11
  */
12
12
  import { readFile } from "node:fs/promises";
13
13
  import { createHash } from "node:crypto";
14
+ import { readTraces } from "../../hooks/state.js";
14
15
  // ── Helpers ────────────────────────────────────────────────
15
16
  export function sha256(content) {
16
17
  return createHash("sha256").update(content, "utf-8").digest("hex");
@@ -53,6 +54,10 @@ export async function verifyLock(lock) {
53
54
  }
54
55
  // ── CLI entry ──────────────────────────────────────────────
55
56
  export async function runVerify(opts) {
57
+ // Handle --stats mode
58
+ if (opts.stats) {
59
+ return runStats();
60
+ }
56
61
  // Read lock file
57
62
  let raw;
58
63
  try {
@@ -99,3 +104,67 @@ export async function runVerify(opts) {
99
104
  return 0;
100
105
  }
101
106
  }
107
+ // ── Stats Mode ────────────────────────────────────────────
108
+ async function runStats() {
109
+ const cwd = process.cwd();
110
+ const entries = await readTraces(cwd, 1); // last 24h
111
+ if (entries.length === 0) {
112
+ process.stderr.write("No trace data found (last 24h).\n");
113
+ process.stderr.write("Traces are written to .dna/state/trace/ during hook execution.\n");
114
+ return 0;
115
+ }
116
+ // Group by event
117
+ const byEvent = new Map();
118
+ for (const e of entries) {
119
+ const list = byEvent.get(e.event) ?? [];
120
+ list.push(e);
121
+ byEvent.set(e.event, list);
122
+ }
123
+ process.stderr.write("Hook call statistics (last 24h):\n");
124
+ for (const [event, list] of byEvent) {
125
+ const blocks = list.filter(e => e.decision === "block").length;
126
+ const warns = list.filter(e => e.decision === "warn").length;
127
+ const avgMs = list.reduce((sum, e) => sum + e.duration_ms, 0) / list.length;
128
+ const parts = [`${list.length} calls`];
129
+ if (blocks > 0)
130
+ parts.push(`${blocks} blocks`);
131
+ if (warns > 0)
132
+ parts.push(`${warns} warns`);
133
+ parts.push(`avg ${avgMs.toFixed(1)}ms`);
134
+ process.stderr.write(` ${event.padEnd(18)} ${parts.join(", ")}\n`);
135
+ }
136
+ // Workflow stats
137
+ const withWorkflow = entries.filter(e => e.workflow);
138
+ if (withWorkflow.length > 0) {
139
+ const byWorkflow = new Map();
140
+ for (const e of withWorkflow) {
141
+ const key = e.workflow;
142
+ const list = byWorkflow.get(key) ?? [];
143
+ list.push(e);
144
+ byWorkflow.set(key, list);
145
+ }
146
+ process.stderr.write("\nWorkflow statistics:\n");
147
+ for (const [wf, list] of byWorkflow) {
148
+ const steps = new Set(list.map(e => e.step).filter(Boolean));
149
+ const blocks = list.filter(e => e.decision === "block").length;
150
+ process.stderr.write(` ${wf}: ${list.length} tool calls, ${steps.size} steps, ${blocks} blocks\n`);
151
+ }
152
+ }
153
+ // Block reasons TOP 3
154
+ const blockEntries = entries.filter(e => e.decision === "block");
155
+ if (blockEntries.length > 0) {
156
+ const reasons = new Map();
157
+ for (const e of blockEntries) {
158
+ const key = `${e.event}:${e.tool_name ?? "unknown"}`;
159
+ reasons.set(key, (reasons.get(key) ?? 0) + 1);
160
+ }
161
+ const sorted = [...reasons.entries()].sort((a, b) => b[1] - a[1]).slice(0, 3);
162
+ process.stderr.write("\nBlock reasons TOP 3:\n");
163
+ let rank = 0;
164
+ for (const [reason, count] of sorted) {
165
+ rank++;
166
+ process.stderr.write(` ${rank}. ${reason} x ${count}\n`);
167
+ }
168
+ }
169
+ return 0;
170
+ }
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 (--scope user|project, --yes)
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)
@@ -52,6 +53,7 @@ Examples:
52
53
  dna sync --remove --inject CLAUDE.md --hooks .claude/hooks --agents .claude/agents
53
54
  dna verify Verify synced files against .dna/lock
54
55
  dna verify --lock /path/to/.dna/lock
56
+ dna verify --stats Show hook call statistics (last 24h)
55
57
  dna import . Import existing harness configs into DNA format
56
58
  dna run --dna my.dna.json --workflow dev-pipeline --task P5.8
57
59
  dna run --dna my.dna.json --workflow dev-pipeline --task P5.8 --dry-run
@@ -119,6 +121,7 @@ async function main() {
119
121
  args: rest,
120
122
  options: {
121
123
  lock: { type: "string", default: ".dna/lock" },
124
+ stats: { type: "boolean", default: false },
122
125
  },
123
126
  allowPositionals: true,
124
127
  strict: false,
@@ -126,6 +129,7 @@ async function main() {
126
129
  const { runVerify } = await import("./commands/verify.js");
127
130
  const code = await runVerify({
128
131
  lockFile: verifyValues.lock,
132
+ stats: verifyValues.stats,
129
133
  });
130
134
  process.exit(code);
131
135
  break;
@@ -355,6 +359,28 @@ async function main() {
355
359
  process.exit(code);
356
360
  break;
357
361
  }
362
+ case "setup": {
363
+ const { values: setupValues } = parseArgs({
364
+ args: rest,
365
+ options: {
366
+ scope: { type: "string", short: "s", default: "user" },
367
+ yes: { type: "boolean", short: "y", default: false },
368
+ },
369
+ strict: false,
370
+ });
371
+ const setupScope = setupValues.scope;
372
+ if (setupScope !== "user" && setupScope !== "project") {
373
+ process.stderr.write(`Error: --scope must be 'user' or 'project', got '${setupScope}'\n`);
374
+ process.exit(2);
375
+ }
376
+ const { runSetup } = await import("./commands/setup.js");
377
+ const code = await runSetup({
378
+ scope: setupScope,
379
+ yes: setupValues.yes,
380
+ });
381
+ process.exit(code);
382
+ break;
383
+ }
358
384
  default:
359
385
  process.stderr.write(`Unknown command: ${command}\n\n`);
360
386
  process.stderr.write(HELP);
@@ -212,6 +212,45 @@ 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
+ const handoffChain = [];
222
+ for (const step of wf.steps ?? []) {
223
+ if (!activeRoles.includes(step.role)) {
224
+ activeRoles.push(step.role);
225
+ }
226
+ if (step.checkpoints && step.checkpoints.length > 0) {
227
+ wfStepCheckpoints.push({
228
+ step_role: step.role,
229
+ step_id: step.id,
230
+ checkpoints: [...step.checkpoints],
231
+ });
232
+ }
233
+ // Collect handoff chain entries
234
+ if (step.handoff) {
235
+ handoffChain.push({
236
+ step_id: step.id,
237
+ produces: step.handoff.produces,
238
+ consumes: step.handoff.consumes,
239
+ });
240
+ }
241
+ }
242
+ // Derive namespace from workflow key: "ns_wfname" → "ns", "wfname" → ""
243
+ const underscoreIdx = wfKey.indexOf("_");
244
+ const namespace = underscoreIdx > 0 ? wfKey.slice(0, underscoreIdx) : "";
245
+ workflowsIR.push({
246
+ workflow_name: wfKey,
247
+ namespace,
248
+ step_checkpoints: wfStepCheckpoints,
249
+ active_roles: activeRoles,
250
+ handoff_chain: handoffChain,
251
+ });
252
+ }
253
+ }
215
254
  // Sort directives by priority
216
255
  const priorityOrder = { high: 0, medium: 1, low: 2 };
217
256
  directives.sort((a, b) => priorityOrder[a.priority] - priorityOrder[b.priority]);
@@ -229,5 +268,6 @@ export function compileDNA(activated, cascaded) {
229
268
  role_scope: roleScope,
230
269
  roles_scope_map: rolesScopeMap.length > 0 ? rolesScopeMap : undefined,
231
270
  step_checkpoints: stepCheckpoints.length > 0 ? stepCheckpoints : undefined,
271
+ workflows_ir: workflowsIR.length > 0 ? workflowsIR : undefined,
232
272
  };
233
273
  }
@@ -173,6 +173,7 @@ function toWorkflowStep(def) {
173
173
  prompt: def.prompt ?? null,
174
174
  completion: def.completion && def.completion.length > 0 ? [...def.completion] : null,
175
175
  checkpoints: def.checkpoints && def.checkpoints.length > 0 ? [...def.checkpoints] : null,
176
+ handoff: def.handoff ?? null,
176
177
  };
177
178
  }
178
179
  /**