intentdna 1.8.4 → 1.8.6

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.8.4",
12
+ "version": "1.8.6",
13
13
  "author": {
14
14
  "name": "Samuel"
15
15
  },
@@ -25,5 +25,5 @@
25
25
  ]
26
26
  }
27
27
  ],
28
- "version": "1.8.4"
28
+ "version": "1.8.6"
29
29
  }
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "intentdna",
3
- "version": "1.8.4",
3
+ "version": "1.8.6",
4
4
  "description": "Declarative policy layer for AI agent behavior",
5
5
  "author": {
6
6
  "name": "Samuel"
package/README.md CHANGED
@@ -17,11 +17,11 @@ IntentDNA is not a model company, generic agent framework, prompt library, canva
17
17
  ## Install or update
18
18
 
19
19
  ```bash
20
- npm install --global intentdna@1.8.4
20
+ npm install --global intentdna@1.8.5
21
21
  dna --version
22
22
  ```
23
23
 
24
- Version `1.8.4` makes delegated workflow completion gates enforceable at the real Claude handoff boundary, returns bounded validator diagnostics to retries, and links C5-backed suggestions to concrete capture event IDs. Online generates copyable local commands only; you still run and review them in your own project.
24
+ Version `1.8.5` keeps Claude plugin Hook delivery in `hooks/hooks.json`: plugin sync does not register project `settings.json` Hooks, retracts exact stale bin-mode `dna-hook` entries once, and leaves user settings untouched in steady state. Online generates copyable local commands only; you still run and review them in your own project.
25
25
 
26
26
  ## Product Shape
27
27
 
@@ -612,6 +612,13 @@ export async function runSync(opts) {
612
612
  opts.skillsDir = resolve(projectDir, ".agents", "skills");
613
613
  process.stderr.write("Codex workflow projection: .agents/skills\n");
614
614
  }
615
+ const requestedMode = opts.mode ?? (opts.plugin === true ? "plugin" : opts.plugin === false ? "bin" : "auto");
616
+ let resolvedMode = requestedMode === "auto" ? "bin" : requestedMode;
617
+ if (!opts.remove && requestedMode === "auto") {
618
+ resolvedMode = await detectMode();
619
+ process.stderr.write(`Auto-detect: ${resolvedMode} mode${resolvedMode === "plugin" ? " (intentdna registered as Claude Code plugin)" : ""}\n`);
620
+ }
621
+ opts.plugin = resolvedMode === "plugin";
615
622
  if (!opts.remove && !opts.inject && !opts.hooksDir && !opts.agentsDir && !opts.workflowDir && !opts.skillsDir && !opts.settingsPath && !opts.codex) {
616
623
  // Auto-detect Claude Code environment → fill all outputs
617
624
  const hasClaudeDir = await fileExists(resolve(projectDir, ".claude"));
@@ -621,16 +628,11 @@ export async function runSync(opts) {
621
628
  opts.agentsDir = resolve(projectDir, ".claude", "agents");
622
629
  opts.skillsDir = resolve(projectDir, ".claude", "skills");
623
630
  opts.settingsPath = resolve(projectDir, ".claude", "settings.json");
624
- process.stderr.write(`Auto-detect: Claude Code → CLAUDE.md + agents + skills + settings\n`);
631
+ process.stderr.write(resolvedMode === "plugin"
632
+ ? "Auto-detect: Claude Code → CLAUDE.md + agents + skills; plugin hooks (no settings injection)\n"
633
+ : "Auto-detect: Claude Code → CLAUDE.md + agents + skills + settings\n");
625
634
  }
626
635
  }
627
- const requestedMode = opts.mode ?? (opts.plugin === true ? "plugin" : opts.plugin === false ? "bin" : "auto");
628
- let resolvedMode = requestedMode === "auto" ? "bin" : requestedMode;
629
- if (!opts.remove && requestedMode === "auto") {
630
- resolvedMode = await detectMode();
631
- process.stderr.write(`Auto-detect: ${resolvedMode} mode${resolvedMode === "plugin" ? " (intentdna registered as Claude Code plugin)" : ""}\n`);
632
- }
633
- opts.plugin = resolvedMode === "plugin";
634
636
  // Handle --remove
635
637
  if (opts.remove) {
636
638
  if (!opts.inject && !opts.hooksDir && !opts.agentsDir && !opts.workflowDir && !opts.skillsDir && !opts.codex) {
@@ -919,7 +921,7 @@ export async function runSync(opts) {
919
921
  }
920
922
  const configPath = opts.files.length === 1 ? opts.files[0] : null;
921
923
  const variables = await resolveVariables(rawVars, configPath, { dryRun: opts.dryRun });
922
- // Step 8: Generate .claude/.mcp.json only for Claude Code skills.
924
+ // Step 8: Generate project-root .mcp.json only for Claude Code skills.
923
925
  if (skillSurface === "claude_code") {
924
926
  const mcpDeps = {};
925
927
  for (const d of dnas) {
@@ -927,7 +929,7 @@ export async function runSync(opts) {
927
929
  Object.assign(mcpDeps, d.mcp);
928
930
  }
929
931
  if (Object.keys(mcpDeps).length > 0) {
930
- const mcpJsonPath = resolve(projectDir, ".claude", ".mcp.json");
932
+ const mcpJsonPath = resolve(projectDir, ".mcp.json");
931
933
  if (opts.dryRun) {
932
934
  process.stderr.write(`Dry-run: would write ${Object.keys(mcpDeps).length} MCP server(s) to ${mcpJsonPath}\n`);
933
935
  }
@@ -1061,7 +1063,7 @@ export async function runSync(opts) {
1061
1063
  }
1062
1064
  }
1063
1065
  /**
1064
- * Write MCP server configuration to .claude/.mcp.json.
1066
+ * Write MCP server configuration to the Claude project-root .mcp.json.
1065
1067
  * Merges with existing config — DNA-declared servers are added/updated,
1066
1068
  * but existing user-configured servers are NOT overwritten.
1067
1069
  */
@@ -1072,7 +1074,12 @@ async function writeMCPConfig(mcpDeps, mcpJsonPath, variables) {
1072
1074
  const raw = await readFile(mcpJsonPath, "utf-8");
1073
1075
  existing = JSON.parse(raw);
1074
1076
  }
1075
- catch { /* doesn't exist yet */ }
1077
+ catch (error) {
1078
+ const code = error && typeof error === "object" && "code" in error ? error.code : undefined;
1079
+ if (code !== "ENOENT") {
1080
+ throw new Error(`Cannot read Claude MCP config: ${mcpJsonPath}`);
1081
+ }
1082
+ }
1076
1083
  const mcpServers = (existing.mcpServers ?? {});
1077
1084
  for (const [name, server] of Object.entries(mcpDeps)) {
1078
1085
  // Don't overwrite existing user-configured servers
@@ -524,7 +524,7 @@ export async function runHealth() {
524
524
  }
525
525
  catch {
526
526
  // Check for plugin registration instead
527
- const mcpPath = resolve(cwd, ".claude", ".mcp.json");
527
+ const mcpPath = resolve(cwd, ".mcp.json");
528
528
  try {
529
529
  const raw = await rf(mcpPath, "utf-8");
530
530
  const mcp = JSON.parse(raw);
@@ -1,7 +1,7 @@
1
1
  import type { ConstraintIR, RoleDef } from "../schema/types.js";
2
2
  import type { SyncOutputArtifactId } from "./output-artifact-registry.js";
3
3
  import { type CompiledIRFile } from "./plugin-adapter.js";
4
- import { type DNASettings, type HookEventKey } from "./settings-adapter.js";
4
+ import { type DNASettings, type HookEventKey, type IntentDNASettingsHookCleanupResult } from "./settings-adapter.js";
5
5
  import type { AdapterCapabilityReport, CapabilityReportEntry, PlannedArtifact, SyncTargetPlan } from "./sync-target-plan.js";
6
6
  export type ClaudeSyncMode = "plugin" | "bin";
7
7
  export interface ClaudeCorePlannedArtifact extends PlannedArtifact {
@@ -31,6 +31,7 @@ export interface ClaudeCoreSyncTargetPlan extends SyncTargetPlan {
31
31
  legacyHooksDir: string;
32
32
  mcpConfigPath: string;
33
33
  settingsHookRegistration?: ClaudeSettingsHookRegistration;
34
+ pluginSettingsPath?: string;
34
35
  pluginSkipsSettings: boolean;
35
36
  plannedArtifacts: ClaudeCorePlannedArtifact[];
36
37
  capabilityReport: ClaudeCoreAdapterCapabilityReport;
@@ -38,6 +39,7 @@ export interface ClaudeCoreSyncTargetPlan extends SyncTargetPlan {
38
39
  export interface ExecuteClaudeCoreSyncTargetPlanResult {
39
40
  messages: string[];
40
41
  removedLegacyHooks: string[];
42
+ settingsHookCleanup?: IntentDNASettingsHookCleanupResult;
41
43
  }
42
44
  /**
43
45
  * Remove legacy bash hook scripts written by previous Intent DNA versions.
@@ -1,7 +1,7 @@
1
1
  import { mkdir, readFile, unlink, writeFile } from "node:fs/promises";
2
2
  import { dirname, resolve } from "node:path";
3
3
  import { createCompiledIR, writeCompiledIR } from "./plugin-adapter.js";
4
- import { compilePluginSettings, detectEnabledEvents, mergeSettingsFile, recommendHookTimeouts } from "./settings-adapter.js";
4
+ import { compilePluginSettings, detectEnabledEvents, mergeSettingsFile, recommendHookTimeouts, removeIntentDNASettingsHooks, } from "./settings-adapter.js";
5
5
  import { formatSyncTargetPlanDiagnostics } from "./sync-target-plan.js";
6
6
  const LEGACY_BASH_HOOKS = [
7
7
  "dna-pre-tool-use.sh",
@@ -46,7 +46,7 @@ export function createClaudeCoreSyncTargetPlan(options) {
46
46
  const compiledIR = createCompiledIR(options.ir, options.roles);
47
47
  const compiledIRPath = resolve(options.projectDir, ".dna", "compiled", "ir.json");
48
48
  const legacyHooksDir = resolve(options.projectDir, ".claude", "hooks");
49
- const mcpConfigPath = resolve(options.projectDir, ".claude", ".mcp.json");
49
+ const mcpConfigPath = resolve(options.projectDir, ".mcp.json");
50
50
  const settingsHookRegistration = createSettingsHookRegistration(options.ir, options.mode, options.settingsPath);
51
51
  const plannedArtifacts = [
52
52
  {
@@ -83,6 +83,7 @@ export function createClaudeCoreSyncTargetPlan(options) {
83
83
  legacyHooksDir,
84
84
  mcpConfigPath,
85
85
  settingsHookRegistration,
86
+ pluginSettingsPath: options.mode === "plugin" ? options.settingsPath : undefined,
86
87
  pluginSkipsSettings: options.mode === "plugin" && !!options.settingsPath,
87
88
  plannedArtifacts,
88
89
  capabilityReport: createClaudeCapabilityReport(plannedArtifacts, options.mode, settingsHookRegistration),
@@ -217,13 +218,18 @@ function createClaudeEvidencePlan(plannedArtifacts) {
217
218
  };
218
219
  }
219
220
  async function registerDNAMCPServer(projectDir) {
220
- const mcpJsonPath = resolve(projectDir, ".claude", ".mcp.json");
221
+ const mcpJsonPath = resolve(projectDir, ".mcp.json");
221
222
  let existing = {};
222
223
  try {
223
224
  const raw = await readFile(mcpJsonPath, "utf-8");
224
225
  existing = JSON.parse(raw);
225
226
  }
226
- catch { /* doesn't exist yet */ }
227
+ catch (error) {
228
+ const code = error && typeof error === "object" && "code" in error ? error.code : undefined;
229
+ if (code !== "ENOENT") {
230
+ throw new Error(`Cannot read Claude MCP config: ${mcpJsonPath}`);
231
+ }
232
+ }
227
233
  const mcpServers = (existing.mcpServers ?? {});
228
234
  if (mcpServers["intentdna"])
229
235
  return { path: mcpJsonPath, registered: false };
@@ -238,6 +244,7 @@ async function registerDNAMCPServer(projectDir) {
238
244
  }
239
245
  export async function executeClaudeCoreSyncTargetPlan(plan, projectDir) {
240
246
  const messages = [];
247
+ let settingsHookCleanup;
241
248
  const writtenIrPath = await writeCompiledIR(projectDir, plan.compiledIR);
242
249
  messages.push(`Wrote compiled IR: ${writtenIrPath}`);
243
250
  const removedLegacyHooks = await removeLegacyBashHooks(plan.legacyHooksDir);
@@ -253,9 +260,19 @@ export async function executeClaudeCoreSyncTargetPlan(plan, projectDir) {
253
260
  messages.push(`Bin mode: registered ${plan.settingsHookRegistration.enabledEvents.length} hook event(s) in ${plan.settingsHookRegistration.path}`);
254
261
  }
255
262
  else if (plan.pluginSkipsSettings) {
256
- messages.push("Plugin mode: hooks managed by plugin framework (skipping settings.json)");
263
+ settingsHookCleanup = await removeIntentDNASettingsHooks(plan.pluginSettingsPath);
264
+ if (settingsHookCleanup.removedCommands > 0) {
265
+ const action = settingsHookCleanup.removedFile ? "removed" : "cleaned";
266
+ messages.push(`Plugin mode: ${action} ${settingsHookCleanup.removedCommands} stale bin-mode dna-hook command(s) from ${plan.pluginSettingsPath}; hooks are managed by plugin framework`);
267
+ }
268
+ else {
269
+ messages.push("Plugin mode: hooks managed by plugin framework (settings.json not written)");
270
+ }
271
+ if (settingsHookCleanup.warning) {
272
+ messages.push(`Plugin mode: preserved settings.json without cleanup (${settingsHookCleanup.warning})`);
273
+ }
257
274
  }
258
- return { messages, removedLegacyHooks };
275
+ return { messages, removedLegacyHooks, settingsHookCleanup };
259
276
  }
260
277
  export function formatClaudeCoreSyncTargetPlanDryRun(plan) {
261
278
  const lines = [
@@ -267,7 +284,7 @@ export function formatClaudeCoreSyncTargetPlanDryRun(plan) {
267
284
  lines.push(`Dry-run: would register ${plan.settingsHookRegistration.enabledEvents.length} hook event(s) in ${plan.settingsHookRegistration.path}`);
268
285
  }
269
286
  else if (plan.pluginSkipsSettings) {
270
- lines.push("Plugin mode: hooks managed by plugin framework (skipping settings.json)");
287
+ lines.push("Plugin mode: hooks managed by plugin framework; would remove stale bin-mode dna-hook commands if present and would not register settings hooks");
271
288
  }
272
289
  const capabilityEntries = Object.entries(plan.capabilityReport.capabilities)
273
290
  .sort(([left], [right]) => left.localeCompare(right));
@@ -18,6 +18,13 @@ export interface SettingsHookEntry {
18
18
  export interface DNASettings {
19
19
  hooks: Record<string, SettingsHookEntry[]>;
20
20
  }
21
+ export interface IntentDNASettingsHookCleanupResult {
22
+ changed: boolean;
23
+ removedCommands: number;
24
+ removedEvents: string[];
25
+ removedFile: boolean;
26
+ warning?: string;
27
+ }
21
28
  /**
22
29
  * Compile settings.json hook configuration for dna-hook binary.
23
30
  * Uses `dna-hook <event>` commands.
@@ -34,3 +41,8 @@ export declare function detectEnabledEvents(ir: import("../schema/types.js").Con
34
41
  * Replaces DNA hooks (identified by "dna-" prefix), keeps user hooks.
35
42
  */
36
43
  export declare function mergeSettingsFile(dnaSettings: DNASettings, settingsPath: string): Promise<void>;
44
+ /**
45
+ * Remove bin-mode IntentDNA hook commands when Claude plugin hooks own dispatch.
46
+ * Invalid or user-owned settings are preserved without rewriting the file.
47
+ */
48
+ export declare function removeIntentDNASettingsHooks(settingsPath: string): Promise<IntentDNASettingsHookCleanupResult>;
@@ -6,9 +6,9 @@
6
6
  *
7
7
  * See docs/archive/2026H1/references/cc-source-analysis.md §2 for the full hook event catalog.
8
8
  */
9
- import { readFile, writeFile } from "node:fs/promises";
9
+ import { readFile, unlink, writeFile } from "node:fs/promises";
10
10
  import { getHookEventBySettingsKey, settingsHookEventKeys } from "../hooks/event-registry.js";
11
- // ── Plugin Mode (Node.js hooks via dna-hook binary) ───────
11
+ // ── Bin Mode (project settings via dna-hook binary) ───────
12
12
  /**
13
13
  * Compile settings.json hook configuration for dna-hook binary.
14
14
  * Uses `dna-hook <event>` commands.
@@ -131,3 +131,120 @@ export async function mergeSettingsFile(dnaSettings, settingsPath) {
131
131
  existing.hooks = mergedHooks;
132
132
  await writeFile(settingsPath, JSON.stringify(existing, null, 2) + "\n", "utf-8");
133
133
  }
134
+ const INTENTDNA_SETTINGS_HOOK_COMMANDS = new Set(settingsHookEventKeys().map((key) => `dna-hook ${getHookEventBySettingsKey(key).event}`));
135
+ function isIntentDNASettingsHookCommand(value, eventName) {
136
+ if (!value || typeof value !== "object" || Array.isArray(value))
137
+ return false;
138
+ const hook = value;
139
+ const expectedCommand = `dna-hook ${eventName}`;
140
+ return hook.type === "command"
141
+ && typeof hook.command === "string"
142
+ && hook.command === expectedCommand
143
+ && INTENTDNA_SETTINGS_HOOK_COMMANDS.has(expectedCommand);
144
+ }
145
+ function isGeneratedIntentDNASettingsHookEntry(entry) {
146
+ return entry.matcher === ""
147
+ && Object.keys(entry).every((key) => key === "matcher" || key === "hooks");
148
+ }
149
+ /**
150
+ * Remove bin-mode IntentDNA hook commands when Claude plugin hooks own dispatch.
151
+ * Invalid or user-owned settings are preserved without rewriting the file.
152
+ */
153
+ export async function removeIntentDNASettingsHooks(settingsPath) {
154
+ const unchanged = (warning) => ({
155
+ changed: false,
156
+ removedCommands: 0,
157
+ removedEvents: [],
158
+ removedFile: false,
159
+ ...(warning ? { warning } : {}),
160
+ });
161
+ let raw;
162
+ try {
163
+ raw = await readFile(settingsPath, "utf-8");
164
+ }
165
+ catch (error) {
166
+ const code = error && typeof error === "object" && "code" in error ? error.code : undefined;
167
+ return code === "ENOENT" ? unchanged() : unchanged("could not read settings.json");
168
+ }
169
+ let parsed;
170
+ try {
171
+ parsed = JSON.parse(raw);
172
+ }
173
+ catch {
174
+ return unchanged("settings.json is not valid JSON");
175
+ }
176
+ if (!parsed || typeof parsed !== "object" || Array.isArray(parsed)) {
177
+ return unchanged("settings.json root is not an object");
178
+ }
179
+ const settings = parsed;
180
+ const rawHooks = settings.hooks;
181
+ if (!rawHooks || typeof rawHooks !== "object" || Array.isArray(rawHooks)) {
182
+ return unchanged();
183
+ }
184
+ const cleanedHooks = {};
185
+ const removedEvents = [];
186
+ let removedCommands = 0;
187
+ for (const [eventName, rawEntries] of Object.entries(rawHooks)) {
188
+ if (!Array.isArray(rawEntries)) {
189
+ cleanedHooks[eventName] = rawEntries;
190
+ continue;
191
+ }
192
+ let eventRemoved = false;
193
+ const cleanedEntries = [];
194
+ for (const rawEntry of rawEntries) {
195
+ if (!rawEntry || typeof rawEntry !== "object" || Array.isArray(rawEntry)) {
196
+ cleanedEntries.push(rawEntry);
197
+ continue;
198
+ }
199
+ const entry = rawEntry;
200
+ if (!Array.isArray(entry.hooks)) {
201
+ cleanedEntries.push(rawEntry);
202
+ continue;
203
+ }
204
+ let entryRemovedCommands = 0;
205
+ const retainedCommands = entry.hooks.filter((hook) => {
206
+ const owned = isIntentDNASettingsHookCommand(hook, eventName);
207
+ if (owned) {
208
+ removedCommands += 1;
209
+ entryRemovedCommands += 1;
210
+ eventRemoved = true;
211
+ }
212
+ return !owned;
213
+ });
214
+ if (entryRemovedCommands === 0) {
215
+ cleanedEntries.push(rawEntry);
216
+ }
217
+ else if (retainedCommands.length > 0 || !isGeneratedIntentDNASettingsHookEntry(entry)) {
218
+ cleanedEntries.push({ ...entry, hooks: retainedCommands });
219
+ }
220
+ }
221
+ if (eventRemoved)
222
+ removedEvents.push(eventName);
223
+ if (!eventRemoved || cleanedEntries.length > 0)
224
+ cleanedHooks[eventName] = cleanedEntries;
225
+ }
226
+ if (removedCommands === 0)
227
+ return unchanged();
228
+ if (Object.keys(cleanedHooks).length > 0) {
229
+ settings.hooks = cleanedHooks;
230
+ }
231
+ else {
232
+ delete settings.hooks;
233
+ }
234
+ if (Object.keys(settings).length === 0) {
235
+ await unlink(settingsPath);
236
+ return {
237
+ changed: true,
238
+ removedCommands,
239
+ removedEvents,
240
+ removedFile: true,
241
+ };
242
+ }
243
+ await writeFile(settingsPath, JSON.stringify(settings, null, 2) + "\n", "utf-8");
244
+ return {
245
+ changed: true,
246
+ removedCommands,
247
+ removedEvents,
248
+ removedFile: false,
249
+ };
250
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "intentdna",
3
- "version": "1.8.4",
3
+ "version": "1.8.6",
4
4
  "description": "Intent DNA — Declarative policy layer for AI agent behavior",
5
5
  "type": "module",
6
6
  "main": "dist/index.js",