intentdna 1.6.3 → 1.6.5
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/marketplace.json +2 -2
- package/.claude-plugin/plugin.json +1 -1
- package/dist/cli/commands/generate.js +5 -33
- package/dist/cli/commands/sync.js +27 -10
- package/dist/hooks/cli.js +2 -4
- package/dist/hooks/event-registry.d.ts +17 -0
- package/dist/hooks/event-registry.js +89 -0
- package/dist/hooks/protocol.d.ts +1 -1
- package/dist/hooks/schema.js +5 -1
- package/dist/mcp/tools-enforce.js +2 -4
- package/dist/runtime/markdown-target-registry.d.ts +17 -0
- package/dist/runtime/markdown-target-registry.js +37 -0
- package/dist/runtime/markdown.d.ts +2 -1
- package/dist/runtime/markdown.js +9 -12
- package/dist/runtime/output-artifact-registry.d.ts +11 -0
- package/dist/runtime/output-artifact-registry.js +30 -0
- package/dist/runtime/settings-adapter.d.ts +2 -1
- package/dist/runtime/settings-adapter.js +4 -12
- package/dist/runtime/skill-adapter.js +105 -37
- package/dist/schema/controller-registry.d.ts +29 -0
- package/dist/schema/controller-registry.js +35 -0
- package/dist/schema/types.d.ts +12 -2
- package/dist/schema/validate.js +1 -93
- package/dist/schema/validators/controllers.d.ts +3 -0
- package/dist/schema/validators/controllers.js +140 -0
- package/dist/templates/flutter-rewrite.dna.yaml +53 -4
- package/dist/templates/metadata.d.ts +6 -0
- package/dist/templates/metadata.js +32 -0
- package/package.json +1 -1
|
@@ -9,7 +9,7 @@
|
|
|
9
9
|
{
|
|
10
10
|
"name": "intentdna",
|
|
11
11
|
"description": "Declarative policy layer for AI agent behavior with plugin-managed hook runtime for Claude Code.",
|
|
12
|
-
"version": "1.6.
|
|
12
|
+
"version": "1.6.5",
|
|
13
13
|
"author": {
|
|
14
14
|
"name": "Samuel"
|
|
15
15
|
},
|
|
@@ -25,5 +25,5 @@
|
|
|
25
25
|
]
|
|
26
26
|
}
|
|
27
27
|
],
|
|
28
|
-
"version": "1.6.
|
|
28
|
+
"version": "1.6.5"
|
|
29
29
|
}
|
|
@@ -11,51 +11,23 @@
|
|
|
11
11
|
import { readFile, writeFile } from "node:fs/promises";
|
|
12
12
|
import { resolve, dirname } from "node:path";
|
|
13
13
|
import { fileURLToPath } from "node:url";
|
|
14
|
+
import { TEMPLATE_MATCH_METADATA } from "../../templates/metadata.js";
|
|
14
15
|
const SPEC_PATH = resolve(dirname(fileURLToPath(import.meta.url)), "..", "..", "..", "spec", "schema-spec.md");
|
|
15
16
|
const TEMPLATES_DIR = resolve(dirname(fileURLToPath(import.meta.url)), "..", "..", "templates");
|
|
16
|
-
/** Keyword → template name mapping for scoring */
|
|
17
|
-
const TEMPLATE_KEYWORDS = {
|
|
18
|
-
"code-review-pipeline": ["review", "code review", "pr review", "pull request", "审查"],
|
|
19
|
-
"safe-refactoring": ["refactor", "refactoring", "restructure", "rewrite safely", "重构"],
|
|
20
|
-
"tdd-strict": ["tdd", "test driven", "test first", "red green", "测试驱动"],
|
|
21
|
-
"full-pipeline": ["pipeline", "plan implement test", "full workflow", "完整流水线"],
|
|
22
|
-
"systematic-debugging": ["debug", "debugging", "bug fix", "troubleshoot", "调试"],
|
|
23
|
-
"verification-loop": ["verify", "verification", "validate", "check", "验证"],
|
|
24
|
-
"secure-dev": ["security", "secure", "owasp", "vulnerability", "安全"],
|
|
25
|
-
"enterprise-baseline": ["enterprise", "organization", "company", "corporate", "企业"],
|
|
26
|
-
"frontend-quality": ["frontend", "react", "vue", "css", "ui", "前端"],
|
|
27
|
-
"api-backend": ["api", "backend", "rest", "graphql", "server", "后端"],
|
|
28
|
-
"mobile-dev": ["mobile", "ios", "android", "react native", "移动"],
|
|
29
|
-
"documentation-writer": ["doc", "documentation", "readme", "wiki", "文档"],
|
|
30
|
-
"devops-cicd": ["devops", "ci", "cd", "deploy", "pipeline", "运维"],
|
|
31
|
-
"monorepo-governance": ["monorepo", "workspace", "packages", "governance"],
|
|
32
|
-
"flutter-behavior-lock": ["flutter", "behavior lock", "behavior spec", "行为锁定"],
|
|
33
|
-
"flutter-rewrite": ["flutter", "rewrite", "migration", "flutter rewrite"],
|
|
34
|
-
"flutter-refactoring-rescue": ["flutter", "rescue", "refactoring rescue"],
|
|
35
|
-
"multi-perspective-review": ["multi perspective", "multiple reviewer", "多角度"],
|
|
36
|
-
"subagent-parallel": ["parallel", "subagent", "concurrent", "并行"],
|
|
37
|
-
"yolo-with-guardrails": ["yolo", "fast", "guardrails", "move fast"],
|
|
38
|
-
"brainstorming-first": ["brainstorm", "explore", "ideation", "探索"],
|
|
39
|
-
"branch-finishing": ["branch", "finish", "cleanup", "pr prep"],
|
|
40
|
-
"pr-submitter": ["pr", "pull request", "submit"],
|
|
41
|
-
"migration-safety": ["migration", "migrate", "upgrade", "database migration", "dependency upgrade", "迁移"],
|
|
42
|
-
"incident-response": ["incident", "production issue", "hotfix", "outage", "postmortem", "事故"],
|
|
43
|
-
"performance-audit": ["performance", "profiling", "benchmark", "optimize", "latency", "性能"],
|
|
44
|
-
};
|
|
45
17
|
function scoreTemplates(description) {
|
|
46
18
|
const lower = description.toLowerCase();
|
|
47
19
|
const scores = [];
|
|
48
|
-
for (const
|
|
20
|
+
for (const template of TEMPLATE_MATCH_METADATA) {
|
|
49
21
|
const matched = [];
|
|
50
|
-
for (const kw of keywords) {
|
|
22
|
+
for (const kw of template.keywords) {
|
|
51
23
|
if (lower.includes(kw)) {
|
|
52
24
|
matched.push(kw);
|
|
53
25
|
}
|
|
54
26
|
}
|
|
55
27
|
if (matched.length > 0) {
|
|
56
28
|
scores.push({
|
|
57
|
-
name: template,
|
|
58
|
-
displayName: template.replace(/-/g, " "),
|
|
29
|
+
name: template.name,
|
|
30
|
+
displayName: template.name.replace(/-/g, " "),
|
|
59
31
|
score: matched.length,
|
|
60
32
|
matchedKeywords: matched,
|
|
61
33
|
});
|
|
@@ -18,6 +18,8 @@ import { createInterface } from "node:readline";
|
|
|
18
18
|
import { loadDNA, compileFromFiles } from "../../compiler/index.js";
|
|
19
19
|
import { parseYAML } from "../../schema/yaml-parser.js";
|
|
20
20
|
import { compileToMarkdown, injectIntoFile, removeFromFile } from "../../runtime/markdown.js";
|
|
21
|
+
import { syncInjectMarkdownTargets } from "../../runtime/markdown-target-registry.js";
|
|
22
|
+
import { getSyncOutputArtifactMetadata } from "../../runtime/output-artifact-registry.js";
|
|
21
23
|
import { compileAllRolesToAgentMD, writeAgentMDFiles, removeAgentMDFiles } from "../../runtime/agent-md.js";
|
|
22
24
|
import { removeWorkflowScripts } from "../../runtime/workflow-runner.js";
|
|
23
25
|
import { compileWorkflowToSkill, compileControllerToSkill, writeSkillFiles } from "../../runtime/skill-adapter.js";
|
|
@@ -25,7 +27,7 @@ import { compilePluginSettings, detectEnabledEvents, mergeSettingsFile, recommen
|
|
|
25
27
|
import { createCompiledIR, writeCompiledIR } from "../../runtime/plugin-adapter.js";
|
|
26
28
|
import { cascadeDNA } from "../../compiler/cascade.js";
|
|
27
29
|
import { EvolutionEngine } from "../../evolution/index.js";
|
|
28
|
-
const VALID_TARGETS =
|
|
30
|
+
const VALID_TARGETS = syncInjectMarkdownTargets();
|
|
29
31
|
/**
|
|
30
32
|
* Detect whether intentdna is running as a Claude Code plugin or standalone binary.
|
|
31
33
|
*
|
|
@@ -392,6 +394,17 @@ async function computeFileHash(filePath) {
|
|
|
392
394
|
const content = await readFile(filePath, "utf-8");
|
|
393
395
|
return fileSha256(content);
|
|
394
396
|
}
|
|
397
|
+
function trackSyncOutput(trackedOutputs, artifact, path) {
|
|
398
|
+
if (!getSyncOutputArtifactMetadata(artifact)) {
|
|
399
|
+
throw new Error(`Unknown sync output artifact: ${artifact}`);
|
|
400
|
+
}
|
|
401
|
+
trackedOutputs.push({ artifact, path: resolve(path) });
|
|
402
|
+
}
|
|
403
|
+
function lockTrackedOutputPaths(trackedOutputs) {
|
|
404
|
+
return trackedOutputs
|
|
405
|
+
.filter((output) => getSyncOutputArtifactMetadata(output.artifact)?.trackedByLock)
|
|
406
|
+
.map((output) => output.path);
|
|
407
|
+
}
|
|
395
408
|
async function generateLockFile(lockPath, dnaSources, outputFiles) {
|
|
396
409
|
const outputs = {};
|
|
397
410
|
for (const f of outputFiles) {
|
|
@@ -541,7 +554,7 @@ export async function runSync(opts) {
|
|
|
541
554
|
else {
|
|
542
555
|
process.stderr.write(`No changes needed in ${opts.inject}\n`);
|
|
543
556
|
}
|
|
544
|
-
trackedOutputs
|
|
557
|
+
trackSyncOutput(trackedOutputs, "markdown-injection", opts.inject);
|
|
545
558
|
}
|
|
546
559
|
// Step 3.5: Write compiled IR (needed for both plugin and bin mode)
|
|
547
560
|
{
|
|
@@ -552,7 +565,7 @@ export async function runSync(opts) {
|
|
|
552
565
|
const compiled = createCompiledIR(ir, roles);
|
|
553
566
|
const irPath = await writeCompiledIR(cwd, compiled);
|
|
554
567
|
process.stderr.write(`Wrote compiled IR: ${irPath}\n`);
|
|
555
|
-
trackedOutputs
|
|
568
|
+
trackSyncOutput(trackedOutputs, "compiled-ir", irPath);
|
|
556
569
|
// Clean up old bash hooks if they exist
|
|
557
570
|
const oldHooksDir = resolve(cwd, ".claude", "hooks");
|
|
558
571
|
const removed = await removeLegacyBashHooks(oldHooksDir);
|
|
@@ -560,7 +573,8 @@ export async function runSync(opts) {
|
|
|
560
573
|
process.stderr.write(`Cleaned ${removed} legacy bash hook(s) from ${oldHooksDir}\n`);
|
|
561
574
|
}
|
|
562
575
|
// G2/G3: Register built-in DNA MCP server in .claude/.mcp.json
|
|
563
|
-
await registerDNAMCPServer(cwd);
|
|
576
|
+
const mcpPath = await registerDNAMCPServer(cwd);
|
|
577
|
+
trackSyncOutput(trackedOutputs, "mcp-config", mcpPath);
|
|
564
578
|
}
|
|
565
579
|
// Step 3.6: Register hooks in settings.json (bin mode only)
|
|
566
580
|
// Plugin mode: hooks managed by plugin framework — skip settings.json
|
|
@@ -570,7 +584,7 @@ export async function runSync(opts) {
|
|
|
570
584
|
const pluginSettings = compilePluginSettings(enabledEvents, 10, recommendHookTimeouts(ir));
|
|
571
585
|
await mergeSettingsFile(pluginSettings, opts.settingsPath);
|
|
572
586
|
process.stderr.write(`Bin mode: registered ${enabledEvents.length} hook event(s) in ${opts.settingsPath}\n`);
|
|
573
|
-
trackedOutputs
|
|
587
|
+
trackSyncOutput(trackedOutputs, "settings-hooks", opts.settingsPath);
|
|
574
588
|
}
|
|
575
589
|
}
|
|
576
590
|
else if (opts.plugin && opts.settingsPath) {
|
|
@@ -601,7 +615,8 @@ export async function runSync(opts) {
|
|
|
601
615
|
for (const f of written) {
|
|
602
616
|
process.stderr.write(` ${f}\n`);
|
|
603
617
|
}
|
|
604
|
-
|
|
618
|
+
for (const file of written)
|
|
619
|
+
trackSyncOutput(trackedOutputs, "agents", file);
|
|
605
620
|
}
|
|
606
621
|
}
|
|
607
622
|
}
|
|
@@ -636,7 +651,7 @@ export async function runSync(opts) {
|
|
|
636
651
|
const shellResult = compileWorkflowToShell(result.plan);
|
|
637
652
|
const written = await writeWorkflowScript(shellResult, opts.workflowDir);
|
|
638
653
|
process.stderr.write(`Generated workflow script: ${written}\n`);
|
|
639
|
-
trackedOutputs
|
|
654
|
+
trackSyncOutput(trackedOutputs, "workflows", written);
|
|
640
655
|
}
|
|
641
656
|
}
|
|
642
657
|
}
|
|
@@ -696,7 +711,8 @@ export async function runSync(opts) {
|
|
|
696
711
|
for (const f of written) {
|
|
697
712
|
process.stderr.write(` ${f}\n`);
|
|
698
713
|
}
|
|
699
|
-
|
|
714
|
+
for (const file of written)
|
|
715
|
+
trackSyncOutput(trackedOutputs, "skills-controllers", file);
|
|
700
716
|
}
|
|
701
717
|
}
|
|
702
718
|
}
|
|
@@ -708,7 +724,7 @@ export async function runSync(opts) {
|
|
|
708
724
|
// Step 9: Generate .dna/lock file (only when --lock is specified or outputs exist)
|
|
709
725
|
if (opts.lockFile && trackedOutputs.length > 0) {
|
|
710
726
|
const lockPath = resolve(opts.lockFile);
|
|
711
|
-
await generateLockFile(lockPath, expandedFiles, trackedOutputs);
|
|
727
|
+
await generateLockFile(lockPath, expandedFiles, lockTrackedOutputPaths(trackedOutputs));
|
|
712
728
|
process.stderr.write(`Lock file: ${lockPath}\n`);
|
|
713
729
|
}
|
|
714
730
|
return 0;
|
|
@@ -781,7 +797,7 @@ async function registerDNAMCPServer(cwd) {
|
|
|
781
797
|
const mcpServers = (existing.mcpServers ?? {});
|
|
782
798
|
// Don't overwrite if user already configured it
|
|
783
799
|
if (mcpServers["intentdna"])
|
|
784
|
-
return;
|
|
800
|
+
return mcpJsonPath;
|
|
785
801
|
mcpServers["intentdna"] = {
|
|
786
802
|
command: "dna-mcp",
|
|
787
803
|
args: ["--project-dir", cwd],
|
|
@@ -790,4 +806,5 @@ async function registerDNAMCPServer(cwd) {
|
|
|
790
806
|
await mkdir(dirname(mcpJsonPath), { recursive: true });
|
|
791
807
|
await writeFileAsync(mcpJsonPath, JSON.stringify(existing, null, 2) + "\n", "utf-8");
|
|
792
808
|
process.stderr.write(`MCP: registered dna-mcp server in ${mcpJsonPath}\n`);
|
|
809
|
+
return mcpJsonPath;
|
|
793
810
|
}
|
package/dist/hooks/cli.js
CHANGED
|
@@ -22,13 +22,11 @@ import { readStdin, writeOutput, silentOutput, allowOutput, blockOutput, stopOut
|
|
|
22
22
|
import { validateHookInput } from "./schema.js";
|
|
23
23
|
import { enforcePreToolUse, enforcePostToolUse, enforceUserPromptSubmit, enforceSubagentStop, enforcePreCompact, enforceNotification, enforceSessionStart, enforceStop, extractBashWritePaths, checkReflectionLimit, checkContextReadiness, checkWorkflowBoundary, } from "./enforce.js";
|
|
24
24
|
import { appendAudit, readWorkflowState, appendTrace, rotateTraces, readTraces, cleanStaleState, readSurgeonAttempts, writeSurgeonAttempts, appendSessionRead, readSessionReads, appendVerifierResult, appendCompletedArtifact, artifactIdentity, buildArtifactKey, readArtifactManifest, resolveArtifactTemplate, safePathComponent, writeArtifactManifest } from "./state.js";
|
|
25
|
+
import { hookEventsForSurface } from "./event-registry.js";
|
|
25
26
|
import { writeAuditEvent } from "../audit/index.js";
|
|
26
27
|
// ── Constants ──────────────────────────────────────────────
|
|
27
28
|
const DEFAULT_IR_PATH = ".dna/compiled/ir.json";
|
|
28
|
-
const VALID_EVENTS = new Set(
|
|
29
|
-
"PreToolUse", "PostToolUse", "UserPromptSubmit",
|
|
30
|
-
"SubagentStop", "PreCompact", "Notification", "Stop", "SessionStart",
|
|
31
|
-
]);
|
|
29
|
+
const VALID_EVENTS = new Set(hookEventsForSurface("cli"));
|
|
32
30
|
export function computeSummary(traces) {
|
|
33
31
|
let blocks = 0, warns = 0;
|
|
34
32
|
const toolCounts = new Map();
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
export declare const HOOK_EVENT_NAMES: readonly ["PreToolUse", "PostToolUse", "UserPromptSubmit", "SubagentStop", "PreCompact", "Notification", "Stop", "SessionStart"];
|
|
2
|
+
export type HookEvent = typeof HOOK_EVENT_NAMES[number];
|
|
3
|
+
export declare const SETTINGS_HOOK_EVENT_KEYS: readonly ["preToolUse", "postToolUse", "userPromptSubmit", "subagentStop", "preCompact", "notification", "stop"];
|
|
4
|
+
export type SettingsHookEventKey = typeof SETTINGS_HOOK_EVENT_KEYS[number];
|
|
5
|
+
export type HookSurface = "protocol" | "cli" | "settings" | "manifest" | "mcp";
|
|
6
|
+
export interface HookEventMetadata {
|
|
7
|
+
event: HookEvent;
|
|
8
|
+
settingsKey?: SettingsHookEventKey;
|
|
9
|
+
manifestTimeout: number;
|
|
10
|
+
surfaces: Record<HookSurface, boolean>;
|
|
11
|
+
}
|
|
12
|
+
export declare const HOOK_EVENT_REGISTRY: readonly HookEventMetadata[];
|
|
13
|
+
export declare function getHookEventMetadata(event: unknown): HookEventMetadata | undefined;
|
|
14
|
+
export declare function getHookEventBySettingsKey(key: unknown): HookEventMetadata | undefined;
|
|
15
|
+
export declare function hookEventsForSurface(surface: HookSurface): HookEvent[];
|
|
16
|
+
export declare function settingsHookEventKeys(): SettingsHookEventKey[];
|
|
17
|
+
export declare function isHookEventSupportedBySurface(event: unknown, surface: HookSurface): event is HookEvent;
|
|
@@ -0,0 +1,89 @@
|
|
|
1
|
+
export const HOOK_EVENT_NAMES = [
|
|
2
|
+
"PreToolUse",
|
|
3
|
+
"PostToolUse",
|
|
4
|
+
"UserPromptSubmit",
|
|
5
|
+
"SubagentStop",
|
|
6
|
+
"PreCompact",
|
|
7
|
+
"Notification",
|
|
8
|
+
"Stop",
|
|
9
|
+
"SessionStart",
|
|
10
|
+
];
|
|
11
|
+
export const SETTINGS_HOOK_EVENT_KEYS = [
|
|
12
|
+
"preToolUse",
|
|
13
|
+
"postToolUse",
|
|
14
|
+
"userPromptSubmit",
|
|
15
|
+
"subagentStop",
|
|
16
|
+
"preCompact",
|
|
17
|
+
"notification",
|
|
18
|
+
"stop",
|
|
19
|
+
];
|
|
20
|
+
export const HOOK_EVENT_REGISTRY = [
|
|
21
|
+
{
|
|
22
|
+
event: "PreToolUse",
|
|
23
|
+
settingsKey: "preToolUse",
|
|
24
|
+
manifestTimeout: 5,
|
|
25
|
+
surfaces: { protocol: true, cli: true, settings: true, manifest: true, mcp: true },
|
|
26
|
+
},
|
|
27
|
+
{
|
|
28
|
+
event: "PostToolUse",
|
|
29
|
+
settingsKey: "postToolUse",
|
|
30
|
+
manifestTimeout: 3,
|
|
31
|
+
surfaces: { protocol: true, cli: true, settings: true, manifest: true, mcp: true },
|
|
32
|
+
},
|
|
33
|
+
{
|
|
34
|
+
event: "UserPromptSubmit",
|
|
35
|
+
settingsKey: "userPromptSubmit",
|
|
36
|
+
manifestTimeout: 5,
|
|
37
|
+
surfaces: { protocol: true, cli: true, settings: true, manifest: true, mcp: true },
|
|
38
|
+
},
|
|
39
|
+
{
|
|
40
|
+
event: "SubagentStop",
|
|
41
|
+
settingsKey: "subagentStop",
|
|
42
|
+
manifestTimeout: 3,
|
|
43
|
+
surfaces: { protocol: true, cli: true, settings: true, manifest: true, mcp: true },
|
|
44
|
+
},
|
|
45
|
+
{
|
|
46
|
+
event: "PreCompact",
|
|
47
|
+
settingsKey: "preCompact",
|
|
48
|
+
manifestTimeout: 3,
|
|
49
|
+
surfaces: { protocol: true, cli: true, settings: true, manifest: true, mcp: true },
|
|
50
|
+
},
|
|
51
|
+
{
|
|
52
|
+
event: "Notification",
|
|
53
|
+
settingsKey: "notification",
|
|
54
|
+
manifestTimeout: 3,
|
|
55
|
+
surfaces: { protocol: true, cli: true, settings: true, manifest: true, mcp: true },
|
|
56
|
+
},
|
|
57
|
+
{
|
|
58
|
+
event: "Stop",
|
|
59
|
+
settingsKey: "stop",
|
|
60
|
+
manifestTimeout: 45,
|
|
61
|
+
surfaces: { protocol: true, cli: true, settings: true, manifest: true, mcp: false },
|
|
62
|
+
},
|
|
63
|
+
{
|
|
64
|
+
event: "SessionStart",
|
|
65
|
+
manifestTimeout: 5,
|
|
66
|
+
surfaces: { protocol: true, cli: true, settings: false, manifest: true, mcp: true },
|
|
67
|
+
},
|
|
68
|
+
];
|
|
69
|
+
const EVENT_BY_NAME = new Map(HOOK_EVENT_REGISTRY.map((entry) => [entry.event, entry]));
|
|
70
|
+
const EVENT_BY_SETTINGS_KEY = new Map(HOOK_EVENT_REGISTRY.flatMap((entry) => entry.settingsKey ? [[entry.settingsKey, entry]] : []));
|
|
71
|
+
export function getHookEventMetadata(event) {
|
|
72
|
+
return typeof event === "string" ? EVENT_BY_NAME.get(event) : undefined;
|
|
73
|
+
}
|
|
74
|
+
export function getHookEventBySettingsKey(key) {
|
|
75
|
+
return typeof key === "string" ? EVENT_BY_SETTINGS_KEY.get(key) : undefined;
|
|
76
|
+
}
|
|
77
|
+
export function hookEventsForSurface(surface) {
|
|
78
|
+
return HOOK_EVENT_REGISTRY
|
|
79
|
+
.filter((entry) => entry.surfaces[surface])
|
|
80
|
+
.map((entry) => entry.event);
|
|
81
|
+
}
|
|
82
|
+
export function settingsHookEventKeys() {
|
|
83
|
+
return HOOK_EVENT_REGISTRY
|
|
84
|
+
.filter((entry) => entry.surfaces.settings && entry.settingsKey)
|
|
85
|
+
.map((entry) => entry.settingsKey);
|
|
86
|
+
}
|
|
87
|
+
export function isHookEventSupportedBySurface(event, surface) {
|
|
88
|
+
return getHookEventMetadata(event)?.surfaces[surface] === true;
|
|
89
|
+
}
|
package/dist/hooks/protocol.d.ts
CHANGED
|
@@ -6,7 +6,7 @@
|
|
|
6
6
|
*
|
|
7
7
|
* Protocol reference: docs/references/cc-source-analysis.md §2
|
|
8
8
|
*/
|
|
9
|
-
export type HookEvent
|
|
9
|
+
export type { HookEvent } from "./event-registry.js";
|
|
10
10
|
/** Base fields present in all hook inputs */
|
|
11
11
|
export interface HookInputBase {
|
|
12
12
|
cwd?: string;
|
package/dist/hooks/schema.js
CHANGED
|
@@ -3,6 +3,7 @@
|
|
|
3
3
|
* Zero external dependencies. Hand-written validators.
|
|
4
4
|
* Fail-open: invalid input → log warning + return { valid: false, ... }
|
|
5
5
|
*/
|
|
6
|
+
import { isHookEventSupportedBySurface } from "./event-registry.js";
|
|
6
7
|
/**
|
|
7
8
|
* Validate hook input against expected schema for each event type.
|
|
8
9
|
* Returns normalized input (camelCase) on success.
|
|
@@ -20,6 +21,9 @@ export function validateHookInput(event, raw) {
|
|
|
20
21
|
normalized.sessionId = sessionId;
|
|
21
22
|
delete normalized.session_id;
|
|
22
23
|
}
|
|
24
|
+
if (!isHookEventSupportedBySurface(event, "protocol")) {
|
|
25
|
+
return { valid: false, errors: [`Unknown event type: ${event}`] };
|
|
26
|
+
}
|
|
23
27
|
switch (event) {
|
|
24
28
|
case "PreToolUse":
|
|
25
29
|
if (typeof input.tool_name !== "string") {
|
|
@@ -58,7 +62,7 @@ export function validateHookInput(event, raw) {
|
|
|
58
62
|
// Minimal validation — these events have no required fields beyond base
|
|
59
63
|
break;
|
|
60
64
|
default:
|
|
61
|
-
|
|
65
|
+
break;
|
|
62
66
|
}
|
|
63
67
|
return {
|
|
64
68
|
valid: errors.length === 0,
|
|
@@ -10,6 +10,7 @@
|
|
|
10
10
|
import { resolve } from "node:path";
|
|
11
11
|
import { stat, readFile } from "node:fs/promises";
|
|
12
12
|
import { enforcePreToolUse, enforcePostToolUse, enforceUserPromptSubmit, enforceSubagentStop, enforcePreCompact, enforceNotification, enforceSessionStart, } from "../hooks/enforce.js";
|
|
13
|
+
import { hookEventsForSurface } from "../hooks/event-registry.js";
|
|
13
14
|
import { textResult, errorResult } from "./server.js";
|
|
14
15
|
// ── IR Cache ────────────────────────────────────────────────
|
|
15
16
|
/** In-memory IR cache with mtime-based invalidation. */
|
|
@@ -61,10 +62,7 @@ export class IRCache {
|
|
|
61
62
|
this.lastMtime = 0;
|
|
62
63
|
}
|
|
63
64
|
}
|
|
64
|
-
const VALID_EVENTS = new Set(
|
|
65
|
-
"PreToolUse", "PostToolUse", "UserPromptSubmit",
|
|
66
|
-
"SubagentStop", "PreCompact", "Notification", "SessionStart",
|
|
67
|
-
]);
|
|
65
|
+
const VALID_EVENTS = new Set(hookEventsForSurface("mcp"));
|
|
68
66
|
function dispatchEnforce(event, ir, input, roles) {
|
|
69
67
|
const result = dispatchEnforceInner(event, ir, input, roles);
|
|
70
68
|
return result?.output ?? { continue: true, suppressOutput: true };
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
export declare const MARKDOWN_TARGET_IDS: readonly ["claude-md", "soul-md", "cursorrules", "system-prompt", "agents-md"];
|
|
2
|
+
export type MarkdownTarget = typeof MARKDOWN_TARGET_IDS[number];
|
|
3
|
+
export type MarkdownRenderer<TIR = unknown> = (ir: TIR) => string;
|
|
4
|
+
export interface MarkdownTargetMetadata {
|
|
5
|
+
id: MarkdownTarget;
|
|
6
|
+
label: string;
|
|
7
|
+
supportsSyncInject: boolean;
|
|
8
|
+
}
|
|
9
|
+
export interface MarkdownTargetRuntime<TIR = unknown> extends MarkdownTargetMetadata {
|
|
10
|
+
render: MarkdownRenderer<TIR>;
|
|
11
|
+
}
|
|
12
|
+
export declare const MARKDOWN_TARGET_REGISTRY: readonly MarkdownTargetMetadata[];
|
|
13
|
+
export declare function getMarkdownTargetMetadata(target: unknown): MarkdownTargetMetadata | undefined;
|
|
14
|
+
export declare function isMarkdownTarget(target: unknown): target is MarkdownTarget;
|
|
15
|
+
export declare function syncInjectMarkdownTargets(): MarkdownTarget[];
|
|
16
|
+
export declare function bindMarkdownTargetRenderers<TIR>(renderers: Record<MarkdownTarget, MarkdownRenderer<TIR>>): readonly MarkdownTargetRuntime<TIR>[];
|
|
17
|
+
export declare function getMarkdownTargetRuntime<TIR>(target: unknown, renderers: Record<MarkdownTarget, MarkdownRenderer<TIR>>): MarkdownTargetRuntime<TIR> | undefined;
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
export const MARKDOWN_TARGET_IDS = [
|
|
2
|
+
"claude-md",
|
|
3
|
+
"soul-md",
|
|
4
|
+
"cursorrules",
|
|
5
|
+
"system-prompt",
|
|
6
|
+
"agents-md",
|
|
7
|
+
];
|
|
8
|
+
export const MARKDOWN_TARGET_REGISTRY = [
|
|
9
|
+
{ id: "claude-md", label: "CLAUDE.md", supportsSyncInject: true },
|
|
10
|
+
{ id: "soul-md", label: "SOUL.md", supportsSyncInject: true },
|
|
11
|
+
{ id: "cursorrules", label: ".cursorrules", supportsSyncInject: true },
|
|
12
|
+
{ id: "system-prompt", label: "System prompt", supportsSyncInject: true },
|
|
13
|
+
{ id: "agents-md", label: "AGENTS.md", supportsSyncInject: false },
|
|
14
|
+
];
|
|
15
|
+
const TARGET_BY_ID = new Map(MARKDOWN_TARGET_REGISTRY.map((entry) => [entry.id, entry]));
|
|
16
|
+
export function getMarkdownTargetMetadata(target) {
|
|
17
|
+
return typeof target === "string" ? TARGET_BY_ID.get(target) : undefined;
|
|
18
|
+
}
|
|
19
|
+
export function isMarkdownTarget(target) {
|
|
20
|
+
return getMarkdownTargetMetadata(target) !== undefined;
|
|
21
|
+
}
|
|
22
|
+
export function syncInjectMarkdownTargets() {
|
|
23
|
+
return MARKDOWN_TARGET_REGISTRY
|
|
24
|
+
.filter((entry) => entry.supportsSyncInject)
|
|
25
|
+
.map((entry) => entry.id);
|
|
26
|
+
}
|
|
27
|
+
export function bindMarkdownTargetRenderers(renderers) {
|
|
28
|
+
return MARKDOWN_TARGET_REGISTRY.map((entry) => ({
|
|
29
|
+
...entry,
|
|
30
|
+
render: renderers[entry.id],
|
|
31
|
+
}));
|
|
32
|
+
}
|
|
33
|
+
export function getMarkdownTargetRuntime(target, renderers) {
|
|
34
|
+
if (typeof target !== "string")
|
|
35
|
+
return undefined;
|
|
36
|
+
return bindMarkdownTargetRenderers(renderers).find((entry) => entry.id === target);
|
|
37
|
+
}
|
|
@@ -13,7 +13,8 @@
|
|
|
13
13
|
* <!-- intentdna:end -->
|
|
14
14
|
*/
|
|
15
15
|
import type { ConstraintIR } from "../schema/types.js";
|
|
16
|
-
|
|
16
|
+
import { type MarkdownTarget } from "./markdown-target-registry.js";
|
|
17
|
+
export type { MarkdownTarget } from "./markdown-target-registry.js";
|
|
17
18
|
/**
|
|
18
19
|
* Compile Constraint IR to a markdown block suitable for the target format.
|
|
19
20
|
*/
|
package/dist/runtime/markdown.js
CHANGED
|
@@ -13,24 +13,21 @@
|
|
|
13
13
|
* <!-- intentdna:end -->
|
|
14
14
|
*/
|
|
15
15
|
import { readFile, writeFile } from "node:fs/promises";
|
|
16
|
+
import { getMarkdownTargetRuntime } from "./markdown-target-registry.js";
|
|
16
17
|
const SENTINEL_START = "<!-- intentdna:start -->";
|
|
17
18
|
const SENTINEL_END = "<!-- intentdna:end -->";
|
|
19
|
+
const MARKDOWN_RENDERERS = {
|
|
20
|
+
"claude-md": renderClaudeMd,
|
|
21
|
+
"soul-md": renderSoulMd,
|
|
22
|
+
cursorrules: renderCursorrules,
|
|
23
|
+
"system-prompt": renderSystemPrompt,
|
|
24
|
+
"agents-md": renderAgentsMd,
|
|
25
|
+
};
|
|
18
26
|
/**
|
|
19
27
|
* Compile Constraint IR to a markdown block suitable for the target format.
|
|
20
28
|
*/
|
|
21
29
|
export function compileToMarkdown(ir, target) {
|
|
22
|
-
|
|
23
|
-
case "claude-md":
|
|
24
|
-
return renderClaudeMd(ir);
|
|
25
|
-
case "soul-md":
|
|
26
|
-
return renderSoulMd(ir);
|
|
27
|
-
case "cursorrules":
|
|
28
|
-
return renderCursorrules(ir);
|
|
29
|
-
case "system-prompt":
|
|
30
|
-
return renderSystemPrompt(ir);
|
|
31
|
-
case "agents-md":
|
|
32
|
-
return renderAgentsMd(ir);
|
|
33
|
-
}
|
|
30
|
+
return getMarkdownTargetRuntime(target, MARKDOWN_RENDERERS).render(ir);
|
|
34
31
|
}
|
|
35
32
|
/**
|
|
36
33
|
* Wrap content in sentinel comments for idempotent injection.
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
export declare const SYNC_OUTPUT_ARTIFACT_IDS: readonly ["markdown-injection", "compiled-ir", "settings-hooks", "agents", "workflows", "skills-controllers", "mcp-config", "lock-file"];
|
|
2
|
+
export type SyncOutputArtifactId = typeof SYNC_OUTPUT_ARTIFACT_IDS[number];
|
|
3
|
+
export interface SyncOutputArtifactMetadata {
|
|
4
|
+
id: SyncOutputArtifactId;
|
|
5
|
+
order: number;
|
|
6
|
+
label: string;
|
|
7
|
+
trackedByLock: boolean;
|
|
8
|
+
}
|
|
9
|
+
export declare const SYNC_OUTPUT_ARTIFACT_REGISTRY: readonly SyncOutputArtifactMetadata[];
|
|
10
|
+
export declare function getSyncOutputArtifactMetadata(id: unknown): SyncOutputArtifactMetadata | undefined;
|
|
11
|
+
export declare function syncOutputArtifactIds(): SyncOutputArtifactId[];
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
export const SYNC_OUTPUT_ARTIFACT_IDS = [
|
|
2
|
+
"markdown-injection",
|
|
3
|
+
"compiled-ir",
|
|
4
|
+
"settings-hooks",
|
|
5
|
+
"agents",
|
|
6
|
+
"workflows",
|
|
7
|
+
"skills-controllers",
|
|
8
|
+
"mcp-config",
|
|
9
|
+
"lock-file",
|
|
10
|
+
];
|
|
11
|
+
export const SYNC_OUTPUT_ARTIFACT_REGISTRY = [
|
|
12
|
+
{ id: "markdown-injection", order: 30, label: "Markdown injection", trackedByLock: true },
|
|
13
|
+
{ id: "compiled-ir", order: 35, label: "Compiled IR", trackedByLock: true },
|
|
14
|
+
{ id: "settings-hooks", order: 36, label: "Settings hooks", trackedByLock: true },
|
|
15
|
+
{ id: "agents", order: 50, label: "Agents", trackedByLock: true },
|
|
16
|
+
{ id: "workflows", order: 60, label: "Workflows", trackedByLock: true },
|
|
17
|
+
{ id: "skills-controllers", order: 70, label: "Skills and controllers", trackedByLock: true },
|
|
18
|
+
{ id: "mcp-config", order: 80, label: "MCP config", trackedByLock: false },
|
|
19
|
+
{ id: "lock-file", order: 90, label: "Lock file", trackedByLock: false },
|
|
20
|
+
];
|
|
21
|
+
const ARTIFACT_BY_ID = new Map(SYNC_OUTPUT_ARTIFACT_REGISTRY.map((entry) => [entry.id, entry]));
|
|
22
|
+
export function getSyncOutputArtifactMetadata(id) {
|
|
23
|
+
return typeof id === "string" ? ARTIFACT_BY_ID.get(id) : undefined;
|
|
24
|
+
}
|
|
25
|
+
export function syncOutputArtifactIds() {
|
|
26
|
+
return SYNC_OUTPUT_ARTIFACT_REGISTRY
|
|
27
|
+
.slice()
|
|
28
|
+
.sort((a, b) => a.order - b.order)
|
|
29
|
+
.map((entry) => entry.id);
|
|
30
|
+
}
|
|
@@ -6,7 +6,8 @@
|
|
|
6
6
|
*
|
|
7
7
|
* See docs/references/cc-source-analysis.md §2 for the full hook event catalog.
|
|
8
8
|
*/
|
|
9
|
-
|
|
9
|
+
import { type SettingsHookEventKey } from "../hooks/event-registry.js";
|
|
10
|
+
export type HookEventKey = SettingsHookEventKey;
|
|
10
11
|
export interface SettingsHookEntry {
|
|
11
12
|
matcher: string;
|
|
12
13
|
hooks: Array<{
|
|
@@ -7,17 +7,8 @@
|
|
|
7
7
|
* See docs/references/cc-source-analysis.md §2 for the full hook event catalog.
|
|
8
8
|
*/
|
|
9
9
|
import { readFile, writeFile } from "node:fs/promises";
|
|
10
|
+
import { getHookEventBySettingsKey, settingsHookEventKeys } from "../hooks/event-registry.js";
|
|
10
11
|
// ── Plugin Mode (Node.js hooks via dna-hook binary) ───────
|
|
11
|
-
/** Hook event key → Claude Code settings event name */
|
|
12
|
-
const HOOK_EVENT_MAP = {
|
|
13
|
-
preToolUse: "PreToolUse",
|
|
14
|
-
postToolUse: "PostToolUse",
|
|
15
|
-
userPromptSubmit: "UserPromptSubmit",
|
|
16
|
-
subagentStop: "SubagentStop",
|
|
17
|
-
preCompact: "PreCompact",
|
|
18
|
-
notification: "Notification",
|
|
19
|
-
stop: "Stop",
|
|
20
|
-
};
|
|
21
12
|
/**
|
|
22
13
|
* Compile settings.json hook configuration for dna-hook binary.
|
|
23
14
|
* Uses `dna-hook <event>` commands.
|
|
@@ -25,7 +16,7 @@ const HOOK_EVENT_MAP = {
|
|
|
25
16
|
export function compilePluginSettings(enabledEvents, timeout = 10, timeouts) {
|
|
26
17
|
const settings = { hooks: {} };
|
|
27
18
|
for (const key of enabledEvents) {
|
|
28
|
-
const eventName =
|
|
19
|
+
const eventName = getHookEventBySettingsKey(key)?.event;
|
|
29
20
|
if (!eventName)
|
|
30
21
|
continue;
|
|
31
22
|
const eventTimeout = timeouts?.[key] ?? timeout;
|
|
@@ -120,7 +111,8 @@ export async function mergeSettingsFile(dnaSettings, settingsPath) {
|
|
|
120
111
|
mergedHooks[eventName] = [...nonDNA, ...entries];
|
|
121
112
|
}
|
|
122
113
|
// Remove DNA hook events that are no longer generated
|
|
123
|
-
for (const
|
|
114
|
+
for (const key of settingsHookEventKeys()) {
|
|
115
|
+
const eventName = getHookEventBySettingsKey(key).event;
|
|
124
116
|
if (!dnaSettings.hooks[eventName] && mergedHooks[eventName]) {
|
|
125
117
|
const entries = mergedHooks[eventName];
|
|
126
118
|
const nonDNA = entries.filter((h) => {
|