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.
- package/.claude-plugin/hooks/hooks.json +12 -0
- package/.claude-plugin/marketplace.json +12 -0
- package/.claude-plugin/plugin.json +5 -0
- package/dist/cli/commands/setup.d.ts +21 -0
- package/dist/cli/commands/setup.js +192 -0
- package/dist/cli/commands/sync.d.ts +12 -0
- package/dist/cli/commands/sync.js +115 -15
- package/dist/cli/commands/verify.d.ts +1 -0
- package/dist/cli/commands/verify.js +69 -0
- package/dist/cli/index.js +26 -0
- package/dist/compiler/compile.js +40 -0
- package/dist/compiler/workflow.js +1 -0
- package/dist/hooks/cli.js +70 -4
- package/dist/hooks/enforce.d.ts +56 -1
- package/dist/hooks/enforce.js +160 -0
- package/dist/hooks/index.d.ts +2 -2
- package/dist/hooks/index.js +1 -1
- package/dist/hooks/protocol.d.ts +4 -1
- package/dist/hooks/state.d.ts +40 -1
- package/dist/hooks/state.js +137 -2
- package/dist/runtime/skill-adapter.js +21 -0
- package/dist/schema/types.d.ts +54 -0
- package/dist/schema/validate.js +29 -0
- package/dist/templates/code-review-pipeline.dna.yaml +12 -0
- package/dist/templates/flutter-refactoring-rescue.dna.yaml +20 -0
- package/dist/templates/flutter-rewrite.dna.yaml +39 -0
- package/dist/templates/full-pipeline.dna.yaml +29 -0
- package/package.json +2 -1
|
@@ -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,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:
|
|
344
|
-
|
|
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(`
|
|
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(`
|
|
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
|
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);
|
package/dist/compiler/compile.js
CHANGED
|
@@ -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
|
/**
|