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.
@@ -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.3",
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.3"
28
+ "version": "1.6.5"
29
29
  }
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "intentdna",
3
- "version": "1.6.3",
3
+ "version": "1.6.5",
4
4
  "description": "Declarative policy layer for AI agent behavior",
5
5
  "author": {
6
6
  "name": "Samuel"
@@ -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 [template, keywords] of Object.entries(TEMPLATE_KEYWORDS)) {
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 = ["claude-md", "soul-md", "cursorrules", "system-prompt"];
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.push(resolve(opts.inject));
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.push(resolve(irPath));
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.push(resolve(opts.settingsPath));
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
- trackedOutputs.push(...written.map(f => resolve(f)));
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.push(resolve(written));
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
- trackedOutputs.push(...written.map(f => resolve(f)));
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
+ }
@@ -6,7 +6,7 @@
6
6
  *
7
7
  * Protocol reference: docs/references/cc-source-analysis.md §2
8
8
  */
9
- export type HookEvent = "PreToolUse" | "PostToolUse" | "UserPromptSubmit" | "SubagentStop" | "PreCompact" | "Notification" | "Stop" | "SessionStart";
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;
@@ -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
- errors.push(`Unknown event type: ${event}`);
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
- export type MarkdownTarget = "claude-md" | "soul-md" | "cursorrules" | "system-prompt" | "agents-md";
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
  */
@@ -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
- switch (target) {
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
- export type HookEventKey = "preToolUse" | "postToolUse" | "userPromptSubmit" | "subagentStop" | "preCompact" | "notification" | "stop";
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 = HOOK_EVENT_MAP[key];
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 eventName of Object.values(HOOK_EVENT_MAP)) {
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) => {