intentdna 1.8.4 → 1.8.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.8.4",
12
+ "version": "1.8.5",
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.5"
29
29
  }
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "intentdna",
3
- "version": "1.8.4",
3
+ "version": "1.8.5",
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) {
@@ -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",
@@ -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),
@@ -238,6 +239,7 @@ async function registerDNAMCPServer(projectDir) {
238
239
  }
239
240
  export async function executeClaudeCoreSyncTargetPlan(plan, projectDir) {
240
241
  const messages = [];
242
+ let settingsHookCleanup;
241
243
  const writtenIrPath = await writeCompiledIR(projectDir, plan.compiledIR);
242
244
  messages.push(`Wrote compiled IR: ${writtenIrPath}`);
243
245
  const removedLegacyHooks = await removeLegacyBashHooks(plan.legacyHooksDir);
@@ -253,9 +255,19 @@ export async function executeClaudeCoreSyncTargetPlan(plan, projectDir) {
253
255
  messages.push(`Bin mode: registered ${plan.settingsHookRegistration.enabledEvents.length} hook event(s) in ${plan.settingsHookRegistration.path}`);
254
256
  }
255
257
  else if (plan.pluginSkipsSettings) {
256
- messages.push("Plugin mode: hooks managed by plugin framework (skipping settings.json)");
258
+ settingsHookCleanup = await removeIntentDNASettingsHooks(plan.pluginSettingsPath);
259
+ if (settingsHookCleanup.removedCommands > 0) {
260
+ const action = settingsHookCleanup.removedFile ? "removed" : "cleaned";
261
+ messages.push(`Plugin mode: ${action} ${settingsHookCleanup.removedCommands} stale bin-mode dna-hook command(s) from ${plan.pluginSettingsPath}; hooks are managed by plugin framework`);
262
+ }
263
+ else {
264
+ messages.push("Plugin mode: hooks managed by plugin framework (settings.json not written)");
265
+ }
266
+ if (settingsHookCleanup.warning) {
267
+ messages.push(`Plugin mode: preserved settings.json without cleanup (${settingsHookCleanup.warning})`);
268
+ }
257
269
  }
258
- return { messages, removedLegacyHooks };
270
+ return { messages, removedLegacyHooks, settingsHookCleanup };
259
271
  }
260
272
  export function formatClaudeCoreSyncTargetPlanDryRun(plan) {
261
273
  const lines = [
@@ -267,7 +279,7 @@ export function formatClaudeCoreSyncTargetPlanDryRun(plan) {
267
279
  lines.push(`Dry-run: would register ${plan.settingsHookRegistration.enabledEvents.length} hook event(s) in ${plan.settingsHookRegistration.path}`);
268
280
  }
269
281
  else if (plan.pluginSkipsSettings) {
270
- lines.push("Plugin mode: hooks managed by plugin framework (skipping settings.json)");
282
+ 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
283
  }
272
284
  const capabilityEntries = Object.entries(plan.capabilityReport.capabilities)
273
285
  .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.5",
4
4
  "description": "Intent DNA — Declarative policy layer for AI agent behavior",
5
5
  "type": "module",
6
6
  "main": "dist/index.js",