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.
- package/.claude-plugin/hooks/hooks.json +12 -0
- package/.claude-plugin/plugin.json +5 -0
- package/dist/cli/commands/setup.d.ts +9 -0
- package/dist/cli/commands/setup.js +42 -0
- package/dist/cli/commands/sync.d.ts +12 -0
- package/dist/cli/commands/sync.js +115 -15
- package/dist/cli/index.js +7 -0
- package/dist/compiler/compile.js +30 -0
- package/dist/hooks/cli.js +25 -3
- package/dist/hooks/enforce.d.ts +38 -0
- package/dist/hooks/enforce.js +99 -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/schema/types.d.ts +19 -0
- package/dist/schema/validate.js +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,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:
|
|
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
|
+
}
|
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);
|
package/dist/compiler/compile.js
CHANGED
|
@@ -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
|
|
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
|
}
|
package/dist/hooks/enforce.d.ts
CHANGED
|
@@ -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
|
/**
|
package/dist/hooks/enforce.js
CHANGED
|
@@ -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)
|
package/dist/hooks/index.d.ts
CHANGED
|
@@ -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";
|
package/dist/hooks/index.js
CHANGED
|
@@ -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";
|
package/dist/hooks/protocol.d.ts
CHANGED
|
@@ -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
|
|
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";
|
package/dist/schema/types.d.ts
CHANGED
|
@@ -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 {
|
package/dist/schema/validate.js
CHANGED
|
@@ -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.
|
|
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
|
],
|