intentdna 1.6.4 → 1.7.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (76) hide show
  1. package/.claude-plugin/marketplace.json +2 -2
  2. package/.claude-plugin/plugin.json +1 -1
  3. package/dist/cli/commands/compile.js +2 -47
  4. package/dist/cli/commands/context.d.ts +8 -0
  5. package/dist/cli/commands/context.js +63 -0
  6. package/dist/cli/commands/feedback.d.ts +1 -0
  7. package/dist/cli/commands/feedback.js +11 -0
  8. package/dist/cli/commands/generate.js +5 -33
  9. package/dist/cli/commands/init.js +11 -63
  10. package/dist/cli/commands/run.js +5 -4
  11. package/dist/cli/commands/show.js +2 -38
  12. package/dist/cli/commands/sync.d.ts +9 -6
  13. package/dist/cli/commands/sync.js +209 -190
  14. package/dist/cli/commands/templates.d.ts +10 -1
  15. package/dist/cli/commands/templates.js +50 -1
  16. package/dist/cli/commands/validate.js +15 -9
  17. package/dist/cli/commands/verify.js +97 -25
  18. package/dist/cli/index.js +76 -11
  19. package/dist/compiler/cascade.d.ts +3 -1
  20. package/dist/compiler/cascade.js +51 -0
  21. package/dist/compiler/compile.js +37 -0
  22. package/dist/compiler/diagnostics.d.ts +17 -0
  23. package/dist/compiler/diagnostics.js +30 -0
  24. package/dist/compiler/index.d.ts +3 -0
  25. package/dist/compiler/index.js +8 -11
  26. package/dist/compiler/input-resolver.d.ts +25 -0
  27. package/dist/compiler/input-resolver.js +175 -0
  28. package/dist/hooks/cli.d.ts +10 -1
  29. package/dist/hooks/cli.js +39 -26
  30. package/dist/hooks/event-registry.d.ts +17 -0
  31. package/dist/hooks/event-registry.js +89 -0
  32. package/dist/hooks/protocol.d.ts +1 -1
  33. package/dist/hooks/schema.js +5 -1
  34. package/dist/hooks/state.d.ts +2 -0
  35. package/dist/hooks/state.js +23 -2
  36. package/dist/mcp/index.js +2 -0
  37. package/dist/mcp/tools-compile.js +18 -49
  38. package/dist/mcp/tools-context.d.ts +2 -0
  39. package/dist/mcp/tools-context.js +85 -0
  40. package/dist/mcp/tools-enforce.d.ts +2 -2
  41. package/dist/mcp/tools-enforce.js +21 -53
  42. package/dist/mcp/tools-observability.js +24 -0
  43. package/dist/report/kernel-signals.js +3 -0
  44. package/dist/report/report-package.d.ts +56 -0
  45. package/dist/report/report-package.js +85 -0
  46. package/dist/runtime/agent-md.d.ts +1 -0
  47. package/dist/runtime/agent-md.js +21 -3
  48. package/dist/runtime/context-sources.d.ts +14 -0
  49. package/dist/runtime/context-sources.js +60 -0
  50. package/dist/runtime/markdown-target-registry.d.ts +17 -0
  51. package/dist/runtime/markdown-target-registry.js +37 -0
  52. package/dist/runtime/markdown.d.ts +2 -1
  53. package/dist/runtime/markdown.js +9 -12
  54. package/dist/runtime/output-artifact-registry.d.ts +11 -0
  55. package/dist/runtime/output-artifact-registry.js +30 -0
  56. package/dist/runtime/settings-adapter.d.ts +2 -1
  57. package/dist/runtime/settings-adapter.js +4 -12
  58. package/dist/runtime/skill-adapter.d.ts +32 -4
  59. package/dist/runtime/skill-adapter.js +187 -10
  60. package/dist/runtime/workflow-runner.d.ts +1 -1
  61. package/dist/runtime/workflow-runner.js +1 -1
  62. package/dist/schema/controller-registry.d.ts +29 -0
  63. package/dist/schema/controller-registry.js +35 -0
  64. package/dist/schema/types.d.ts +33 -0
  65. package/dist/schema/validate.js +157 -138
  66. package/dist/schema/validators/controllers.d.ts +3 -0
  67. package/dist/schema/validators/controllers.js +156 -0
  68. package/dist/signals/index.d.ts +10 -0
  69. package/dist/signals/index.js +90 -5
  70. package/dist/templates/catalog.d.ts +19 -0
  71. package/dist/templates/catalog.js +57 -0
  72. package/dist/templates/flutter-rewrite.dna.yaml +2 -2
  73. package/dist/templates/metadata.d.ts +6 -0
  74. package/dist/templates/metadata.js +32 -0
  75. package/package.json +1 -1
  76. package/spec/foundation-hardening.md +2 -1
@@ -10,22 +10,25 @@
10
10
  *
11
11
  * Targets: claude-md, soul-md, cursorrules, system-prompt
12
12
  */
13
- import { resolve, dirname, basename } from "node:path";
13
+ import { resolve, dirname } from "node:path";
14
14
  import { readFile, writeFile as writeFileAsync, stat, mkdir, readdir, unlink, rm } from "node:fs/promises";
15
15
  import { createHash } from "node:crypto";
16
- import { fileURLToPath } from "node:url";
17
16
  import { createInterface } from "node:readline";
18
- import { loadDNA, compileFromFiles } from "../../compiler/index.js";
17
+ import { assertNoNamespaceCollisions, compileFromFiles, detectDNAConfigs, expandDNAInputFiles, loadDNA, } from "../../compiler/index.js";
19
18
  import { parseYAML } from "../../schema/yaml-parser.js";
19
+ import { findTemplateCatalogEntry } from "../../templates/catalog.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";
23
+ import { compileWorkflow } from "../../compiler/workflow.js";
21
24
  import { compileAllRolesToAgentMD, writeAgentMDFiles, removeAgentMDFiles } from "../../runtime/agent-md.js";
22
25
  import { removeWorkflowScripts } from "../../runtime/workflow-runner.js";
23
- import { compileWorkflowToSkill, compileControllerToSkill, writeSkillFiles } from "../../runtime/skill-adapter.js";
26
+ import { compileWorkflowToSkill, compileControllerToSkill, writeSkillFiles, removeSkillFiles } from "../../runtime/skill-adapter.js";
24
27
  import { compilePluginSettings, detectEnabledEvents, mergeSettingsFile, recommendHookTimeouts } from "../../runtime/settings-adapter.js";
25
28
  import { createCompiledIR, writeCompiledIR } from "../../runtime/plugin-adapter.js";
26
29
  import { cascadeDNA } from "../../compiler/cascade.js";
27
30
  import { EvolutionEngine } from "../../evolution/index.js";
28
- const VALID_TARGETS = ["claude-md", "soul-md", "cursorrules", "system-prompt"];
31
+ const VALID_TARGETS = syncInjectMarkdownTargets();
29
32
  /**
30
33
  * Detect whether intentdna is running as a Claude Code plugin or standalone binary.
31
34
  *
@@ -71,32 +74,6 @@ const LEGACY_BASH_HOOKS = [
71
74
  "dna-pre-compact.sh",
72
75
  "dna-notification.sh",
73
76
  ];
74
- const TEMPLATES_DIR = resolve(dirname(fileURLToPath(import.meta.url)), "..", "..", "templates");
75
- function calculateTemplateHash(content) {
76
- return createHash("sha256").update(content, "utf-8").digest("hex").slice(0, 16);
77
- }
78
- async function findTemplateFile(templateName) {
79
- // Try direct filename match first
80
- const directPath = resolve(TEMPLATES_DIR, `${templateName}.dna.yaml`);
81
- if (await fileExists(directPath))
82
- return directPath;
83
- // Fallback: search by id or name field
84
- try {
85
- const files = await readdir(TEMPLATES_DIR);
86
- for (const file of files) {
87
- if (!file.endsWith(".dna.yaml") && !file.endsWith(".dna.yml"))
88
- continue;
89
- const fullPath = resolve(TEMPLATES_DIR, file);
90
- const content = await readFile(fullPath, "utf-8");
91
- const data = parseYAML(content);
92
- if (data.id === templateName || data.name === templateName) {
93
- return fullPath;
94
- }
95
- }
96
- }
97
- catch { /* fail-open */ }
98
- return null;
99
- }
100
77
  /**
101
78
  * Remove legacy bash hook scripts written by previous Intent DNA versions.
102
79
  * Only removes files that contain the sentinel comment.
@@ -127,11 +104,11 @@ export async function cleanStaleDNAFiles(dir, type, activeNamespaces) {
127
104
  const entries = await readdir(dir, { withFileTypes: true });
128
105
  for (const entry of entries) {
129
106
  const fullPath = resolve(dir, entry.name);
130
- // Match active namespace prefixes directly so multi-hyphen names like
131
- // dna-frw-fix-loop still resolve to namespace frw.
132
- if (!activeNamespaces.some(ns => entry.name.startsWith(`dna-${ns}-`))) {
107
+ if (activeNamespaces.length === 0)
108
+ continue;
109
+ const matchesActiveScope = activeNamespaces.some(ns => entry.name.startsWith(`dna-${ns}-`));
110
+ if (!matchesActiveScope)
133
111
  continue;
134
- }
135
112
  if (type === "agents" && entry.isFile() && entry.name.endsWith(".md")) {
136
113
  const content = await readFile(fullPath, "utf-8").catch(() => "");
137
114
  if (content.includes(DNA_SENTINEL)) {
@@ -169,7 +146,7 @@ function promptVariable(description, defaultValue) {
169
146
  * prompt user interactively. If already plain strings, use as-is.
170
147
  * Saves resolved values back to config file.
171
148
  */
172
- async function resolveVariables(rawVars, configPath) {
149
+ async function resolveVariables(rawVars, configPath, options) {
173
150
  const resolved = {};
174
151
  const needsPrompt = [];
175
152
  for (const [key, val] of Object.entries(rawVars)) {
@@ -180,7 +157,7 @@ async function resolveVariables(rawVars, configPath) {
180
157
  needsPrompt.push({ key, desc: val.description, def: val.default });
181
158
  }
182
159
  }
183
- if (needsPrompt.length > 0 && process.stdin.isTTY) {
160
+ if (needsPrompt.length > 0 && process.stdin.isTTY && !options?.dryRun) {
184
161
  process.stderr.write("\nProject variables (press Enter for default):\n");
185
162
  for (const v of needsPrompt) {
186
163
  const value = await promptVariable(v.desc, v.def);
@@ -216,42 +193,8 @@ async function resolveVariables(rawVars, configPath) {
216
193
  * Auto-detect DNA config file in current directory (legacy single-config).
217
194
  * Priority: .dna/config.yaml > .dna/config.yml > .dna/config.json > .dna.yaml > .dna.yml > .dna.json
218
195
  */
219
- async function autoDetectDNA() {
220
- const cwd = process.cwd();
221
- const candidates = [
222
- ".dna/config.yaml", ".dna/config.yml", ".dna/config.json",
223
- ".dna.yaml", ".dna.yml", ".dna.json",
224
- ];
225
- for (const name of candidates) {
226
- const path = resolve(cwd, name);
227
- if (await fileExists(path))
228
- return path;
229
- }
230
- return null;
231
- }
232
- /**
233
- * Auto-detect DNA configs: multi-config (.dna/configs/*.yaml) or legacy single-config.
234
- * Returns array of absolute paths.
235
- */
236
196
  export async function autoDetectConfigs() {
237
- const cwd = process.cwd();
238
- // Priority 1: .dna/configs/*.yaml (multi-config)
239
- const configsDir = resolve(cwd, ".dna", "configs");
240
- try {
241
- const files = await readdir(configsDir);
242
- const yamls = files
243
- .filter(f => f.endsWith(".yaml") || f.endsWith(".yml"))
244
- .sort()
245
- .map(f => resolve(configsDir, f));
246
- if (yamls.length > 0)
247
- return yamls;
248
- }
249
- catch { /* dir doesn't exist */ }
250
- // Fallback: legacy single-config paths
251
- const legacy = await autoDetectDNA();
252
- if (legacy)
253
- return [legacy];
254
- return [];
197
+ return detectDNAConfigs(process.cwd());
255
198
  }
256
199
  /**
257
200
  * Read the current package version from package.json.
@@ -262,26 +205,7 @@ export { getPackageVersion } from "../util/version.js";
262
205
  * Throws if two configs declare the same namespace.
263
206
  */
264
207
  export async function checkNamespaceCollisions(configPaths) {
265
- const namespaces = new Map();
266
- for (const configPath of configPaths) {
267
- try {
268
- const content = await readFile(configPath, "utf-8");
269
- const data = parseYAML(content);
270
- const ns = typeof data.namespace === "string" ? data.namespace : undefined;
271
- if (ns) {
272
- const file = basename(configPath);
273
- if (namespaces.has(ns)) {
274
- throw new Error(`Namespace "${ns}" conflict: ${namespaces.get(ns)} and ${file}. Each template must have a unique namespace.`);
275
- }
276
- namespaces.set(ns, file);
277
- }
278
- }
279
- catch (err) {
280
- if (err instanceof Error && err.message.includes("conflict"))
281
- throw err;
282
- // Parse error — will be caught later during compilation
283
- }
284
- }
208
+ await assertNoNamespaceCollisions(configPaths);
285
209
  }
286
210
  /**
287
211
  * Compare two semver strings. Returns:
@@ -306,10 +230,8 @@ export function compareSemver(a, b) {
306
230
  return 0;
307
231
  }
308
232
  /**
309
- * Check template versions against current template content hashes.
310
- * Returns upgrade info for changed configs and prints human-readable suggestions.
311
- *
312
- * Uses content hash comparison instead of semver — detects any template content change.
233
+ * Check stamped template metadata against the current template catalog.
234
+ * Version mismatch is the primary upgrade signal; content hash drift is a fallback.
313
235
  */
314
236
  export async function checkTemplateVersions(configPaths) {
315
237
  const upgrades = [];
@@ -322,26 +244,37 @@ export async function checkTemplateVersions(configPaths) {
322
244
  const tmplVersion = typeof data._template_version === "string" ? data._template_version : undefined;
323
245
  if (!tmplName)
324
246
  continue;
325
- const templatePath = await findTemplateFile(tmplName);
326
- if (!templatePath)
247
+ const template = await findTemplateCatalogEntry(tmplName);
248
+ if (!template)
249
+ continue;
250
+ if (tmplVersion && template.version && compareSemver(template.version, tmplVersion) > 0) {
251
+ upgrades.push({
252
+ configPath,
253
+ templateName: tmplName,
254
+ currentVersion: tmplVersion,
255
+ availableVersion: template.version,
256
+ reason: "version-update",
257
+ });
258
+ process.stderr.write(`Template update available: "${tmplName}" ${tmplVersion} → ${template.version}. Run: dna init --upgrade ${tmplName}\n`);
327
259
  continue;
328
- const currentTemplateContent = await readFile(templatePath, "utf-8");
329
- const currentHash = calculateTemplateHash(currentTemplateContent);
260
+ }
330
261
  if (!tmplHash) {
331
262
  upgrades.push({
332
263
  configPath,
333
264
  templateName: tmplName,
334
265
  currentVersion: tmplVersion ?? "unknown",
335
- availableVersion: "content-changed",
266
+ availableVersion: template.version ?? "content-changed",
267
+ reason: "missing-hash",
336
268
  });
337
269
  process.stderr.write(`Upgrade available: "${tmplName}" has no content hash. Run: dna init --upgrade ${tmplName}\n`);
338
270
  }
339
- else if (currentHash !== tmplHash) {
271
+ else if (template.contentHash !== tmplHash) {
340
272
  upgrades.push({
341
273
  configPath,
342
274
  templateName: tmplName,
343
275
  currentVersion: tmplVersion ?? "unknown",
344
276
  availableVersion: "content-changed",
277
+ reason: "content-drift",
345
278
  });
346
279
  process.stderr.write(`Template content changed: "${tmplName}". Run: dna init --upgrade ${tmplName}\n`);
347
280
  }
@@ -352,39 +285,6 @@ export async function checkTemplateVersions(configPaths) {
352
285
  }
353
286
  return upgrades;
354
287
  }
355
- function resolveSpeciesPath(ref) {
356
- if (!ref.startsWith("species:"))
357
- return null;
358
- const name = ref.slice("species:".length);
359
- const thisDir = dirname(fileURLToPath(import.meta.url));
360
- return resolve(thisDir, "..", "..", "species", `${name}.dna.json`);
361
- }
362
- async function expandFiles(files) {
363
- const expanded = [];
364
- const seen = new Set();
365
- for (const file of files) {
366
- const abs = resolve(file);
367
- if (seen.has(abs))
368
- continue;
369
- seen.add(abs);
370
- expanded.push(abs);
371
- try {
372
- const raw = await readFile(abs, "utf-8");
373
- const dna = JSON.parse(raw);
374
- for (const ref of dna.cascade?.inherits ?? []) {
375
- const speciesPath = resolveSpeciesPath(ref);
376
- if (speciesPath && !seen.has(speciesPath)) {
377
- seen.add(speciesPath);
378
- expanded.unshift(speciesPath);
379
- }
380
- }
381
- }
382
- catch {
383
- // Will fail later with proper error
384
- }
385
- }
386
- return expanded;
387
- }
388
288
  function fileSha256(content) {
389
289
  return createHash("sha256").update(content, "utf-8").digest("hex");
390
290
  }
@@ -392,6 +292,32 @@ async function computeFileHash(filePath) {
392
292
  const content = await readFile(filePath, "utf-8");
393
293
  return fileSha256(content);
394
294
  }
295
+ function trackSyncOutput(trackedOutputs, artifact, path) {
296
+ if (!getSyncOutputArtifactMetadata(artifact)) {
297
+ throw new Error(`Unknown sync output artifact: ${artifact}`);
298
+ }
299
+ trackedOutputs.push({ artifact, path: resolve(path) });
300
+ }
301
+ function summarizeSyncOutputs(trackedOutputs, dryRun) {
302
+ if (trackedOutputs.length === 0)
303
+ return;
304
+ const sorted = trackedOutputs.slice().sort((a, b) => {
305
+ const aMeta = getSyncOutputArtifactMetadata(a.artifact);
306
+ const bMeta = getSyncOutputArtifactMetadata(b.artifact);
307
+ return aMeta.order - bMeta.order || a.path.localeCompare(b.path);
308
+ });
309
+ process.stderr.write(dryRun ? "Dry-run output plan:\n" : "Sync output summary:\n");
310
+ for (const output of sorted) {
311
+ const metadata = getSyncOutputArtifactMetadata(output.artifact);
312
+ const tracked = metadata.trackedByLock ? "lock-tracked" : "untracked";
313
+ process.stderr.write(` - ${metadata.label}: ${output.path} (${tracked})\n`);
314
+ }
315
+ }
316
+ function lockTrackedOutputPaths(trackedOutputs) {
317
+ return trackedOutputs
318
+ .filter((output) => getSyncOutputArtifactMetadata(output.artifact)?.trackedByLock)
319
+ .map((output) => output.path);
320
+ }
395
321
  async function generateLockFile(lockPath, dnaSources, outputFiles) {
396
322
  const outputs = {};
397
323
  for (const f of outputFiles) {
@@ -419,10 +345,8 @@ export async function runSync(opts) {
419
345
  }
420
346
  // Namespace collision check (multi-config)
421
347
  if (autoFiles.length > 1) {
422
- await checkNamespaceCollisions(autoFiles);
348
+ await assertNoNamespaceCollisions(autoFiles);
423
349
  }
424
- // Template version check
425
- await checkTemplateVersions(autoFiles);
426
350
  }
427
351
  }
428
352
  if (!opts.remove && !opts.inject && !opts.hooksDir && !opts.agentsDir && !opts.workflowDir && !opts.skillsDir && !opts.settingsPath) {
@@ -438,18 +362,17 @@ export async function runSync(opts) {
438
362
  process.stderr.write(`Auto-detect: Claude Code → CLAUDE.md + agents + skills + settings\n`);
439
363
  }
440
364
  }
441
- // Auto-detect plugin vs bin mode if --plugin not explicitly set
442
- if (!opts.remove && opts.plugin === undefined) {
443
- const mode = await detectMode();
444
- if (mode === "plugin") {
445
- opts.plugin = true;
446
- process.stderr.write("Auto-detect: plugin mode (intentdna registered as Claude Code plugin)\n");
447
- }
365
+ const requestedMode = opts.mode ?? (opts.plugin === true ? "plugin" : opts.plugin === false ? "bin" : "auto");
366
+ let resolvedMode = requestedMode === "auto" ? "bin" : requestedMode;
367
+ if (!opts.remove && requestedMode === "auto") {
368
+ resolvedMode = await detectMode();
369
+ process.stderr.write(`Auto-detect: ${resolvedMode} mode${resolvedMode === "plugin" ? " (intentdna registered as Claude Code plugin)" : ""}\n`);
448
370
  }
371
+ opts.plugin = resolvedMode === "plugin";
449
372
  // Handle --remove
450
373
  if (opts.remove) {
451
- if (!opts.inject && !opts.hooksDir && !opts.agentsDir && !opts.workflowDir) {
452
- process.stderr.write("Error: --inject, --hooks, --agents, or --workflow is required with --remove\n");
374
+ if (!opts.inject && !opts.hooksDir && !opts.agentsDir && !opts.workflowDir && !opts.skillsDir) {
375
+ process.stderr.write("Error: --inject, --hooks, --agents, --workflow, or --skills is required with --remove\n");
453
376
  return 2;
454
377
  }
455
378
  if (opts.inject) {
@@ -488,6 +411,15 @@ export async function runSync(opts) {
488
411
  process.stderr.write(`No Intent DNA workflow scripts found in ${opts.workflowDir}\n`);
489
412
  }
490
413
  }
414
+ if (opts.skillsDir) {
415
+ const count = await removeSkillFiles(opts.skillsDir);
416
+ if (count > 0) {
417
+ process.stderr.write(`Removed ${count} Intent DNA skill dir(s) from ${opts.skillsDir}\n`);
418
+ }
419
+ else {
420
+ process.stderr.write(`No Intent DNA skill dirs found in ${opts.skillsDir}\n`);
421
+ }
422
+ }
491
423
  return 0;
492
424
  }
493
425
  if (opts.files.length === 0) {
@@ -509,11 +441,18 @@ export async function runSync(opts) {
509
441
  // Step 0: Expand and pre-validate all DNA files before any work
510
442
  // loadDNA() calls validateDNA() internally — validation failures throw here,
511
443
  // before evolve/compile/inject waste any work.
512
- const expandedFiles = await expandFiles(opts.files);
444
+ const expandedFiles = await expandDNAInputFiles(opts.files);
445
+ await checkTemplateVersions(expandedFiles);
513
446
  const loadedDNAs = await Promise.all(expandedFiles.map(loadDNA));
447
+ if (opts.dryRun) {
448
+ process.stderr.write("Dry-run: no files will be written or removed.\n");
449
+ }
514
450
  // Step 1: Evolve if requested
515
451
  let epigeneticMarkers = undefined;
516
- if (opts.evolve) {
452
+ if (opts.evolve && opts.dryRun) {
453
+ process.stderr.write("Dry-run: would run evolution before compiling\n");
454
+ }
455
+ else if (opts.evolve) {
517
456
  const dnas = loadedDNAs;
518
457
  // Use the highest-priority DNA's ID
519
458
  const primaryDna = dnas.reduce((a, b) => a.cascade.priority >= b.cascade.priority ? a : b);
@@ -533,15 +472,21 @@ export async function runSync(opts) {
533
472
  // Step 3: Render and inject markdown
534
473
  if (opts.inject) {
535
474
  const target = opts.target;
536
- const markdown = compileToMarkdown(ir, target);
537
- const modified = await injectIntoFile(opts.inject, markdown);
538
- if (modified) {
539
- process.stderr.write(`Synced to ${opts.inject} (${target})\n`);
475
+ if (opts.dryRun) {
476
+ compileToMarkdown(ir, target);
477
+ process.stderr.write(`Dry-run: would sync ${opts.inject} (${target})\n`);
540
478
  }
541
479
  else {
542
- process.stderr.write(`No changes needed in ${opts.inject}\n`);
480
+ const markdown = compileToMarkdown(ir, target);
481
+ const modified = await injectIntoFile(opts.inject, markdown);
482
+ if (modified) {
483
+ process.stderr.write(`Synced to ${opts.inject} (${target})\n`);
484
+ }
485
+ else {
486
+ process.stderr.write(`No changes needed in ${opts.inject}\n`);
487
+ }
543
488
  }
544
- trackedOutputs.push(resolve(opts.inject));
489
+ trackSyncOutput(trackedOutputs, "markdown-injection", opts.inject);
545
490
  }
546
491
  // Step 3.5: Write compiled IR (needed for both plugin and bin mode)
547
492
  {
@@ -550,17 +495,36 @@ export async function runSync(opts) {
550
495
  const cascadedForIR = cascadeDNA(dnas);
551
496
  const roles = cascadedForIR.roles;
552
497
  const compiled = createCompiledIR(ir, roles);
553
- const irPath = await writeCompiledIR(cwd, compiled);
554
- process.stderr.write(`Wrote compiled IR: ${irPath}\n`);
555
- trackedOutputs.push(resolve(irPath));
498
+ const irPath = resolve(cwd, ".dna", "compiled", "ir.json");
499
+ if (opts.dryRun) {
500
+ JSON.stringify(compiled);
501
+ process.stderr.write(`Dry-run: would write compiled IR: ${irPath}\n`);
502
+ }
503
+ else {
504
+ const writtenIrPath = await writeCompiledIR(cwd, compiled);
505
+ process.stderr.write(`Wrote compiled IR: ${writtenIrPath}\n`);
506
+ }
507
+ trackSyncOutput(trackedOutputs, "compiled-ir", irPath);
556
508
  // Clean up old bash hooks if they exist
557
509
  const oldHooksDir = resolve(cwd, ".claude", "hooks");
558
- const removed = await removeLegacyBashHooks(oldHooksDir);
559
- if (removed > 0) {
560
- process.stderr.write(`Cleaned ${removed} legacy bash hook(s) from ${oldHooksDir}\n`);
510
+ if (opts.dryRun) {
511
+ process.stderr.write(`Dry-run: would clean legacy bash hooks in ${oldHooksDir}\n`);
512
+ }
513
+ else {
514
+ const removed = await removeLegacyBashHooks(oldHooksDir);
515
+ if (removed > 0) {
516
+ process.stderr.write(`Cleaned ${removed} legacy bash hook(s) from ${oldHooksDir}\n`);
517
+ }
561
518
  }
562
519
  // G2/G3: Register built-in DNA MCP server in .claude/.mcp.json
563
- await registerDNAMCPServer(cwd);
520
+ const mcpPath = resolve(cwd, ".claude", ".mcp.json");
521
+ if (opts.dryRun) {
522
+ process.stderr.write(`Dry-run: would register dna-mcp server in ${mcpPath}\n`);
523
+ }
524
+ else {
525
+ await registerDNAMCPServer(cwd);
526
+ }
527
+ trackSyncOutput(trackedOutputs, "mcp-config", mcpPath);
564
528
  }
565
529
  // Step 3.6: Register hooks in settings.json (bin mode only)
566
530
  // Plugin mode: hooks managed by plugin framework — skip settings.json
@@ -568,9 +532,15 @@ export async function runSync(opts) {
568
532
  const enabledEvents = detectEnabledEvents(ir);
569
533
  if (enabledEvents.length > 0) {
570
534
  const pluginSettings = compilePluginSettings(enabledEvents, 10, recommendHookTimeouts(ir));
571
- await mergeSettingsFile(pluginSettings, opts.settingsPath);
572
- process.stderr.write(`Bin mode: registered ${enabledEvents.length} hook event(s) in ${opts.settingsPath}\n`);
573
- trackedOutputs.push(resolve(opts.settingsPath));
535
+ if (opts.dryRun) {
536
+ JSON.stringify(pluginSettings);
537
+ process.stderr.write(`Dry-run: would register ${enabledEvents.length} hook event(s) in ${opts.settingsPath}\n`);
538
+ }
539
+ else {
540
+ await mergeSettingsFile(pluginSettings, opts.settingsPath);
541
+ process.stderr.write(`Bin mode: registered ${enabledEvents.length} hook event(s) in ${opts.settingsPath}\n`);
542
+ }
543
+ trackSyncOutput(trackedOutputs, "settings-hooks", opts.settingsPath);
574
544
  }
575
545
  }
576
546
  else if (opts.plugin && opts.settingsPath) {
@@ -583,9 +553,14 @@ export async function runSync(opts) {
583
553
  // Step 5: Generate agent MD files
584
554
  if (opts.agentsDir) {
585
555
  // Clean stale DNA-managed agents before regenerating
586
- const staleAgents = await cleanStaleDNAFiles(opts.agentsDir, "agents", activeNamespaces);
587
- if (staleAgents.length > 0) {
588
- process.stderr.write(`Cleaned ${staleAgents.length} stale agent file(s)\n`);
556
+ if (opts.dryRun) {
557
+ process.stderr.write(`Dry-run: would clean stale agent file(s) in ${opts.agentsDir}\n`);
558
+ }
559
+ else {
560
+ const staleAgents = await cleanStaleDNAFiles(opts.agentsDir, "agents", activeNamespaces);
561
+ if (staleAgents.length > 0) {
562
+ process.stderr.write(`Cleaned ${staleAgents.length} stale agent file(s)\n`);
563
+ }
589
564
  }
590
565
  const dnas = loadedDNAs;
591
566
  const cascaded = cascadeDNA(dnas);
@@ -595,14 +570,19 @@ export async function runSync(opts) {
595
570
  }
596
571
  else {
597
572
  const results = compileAllRolesToAgentMD(roles, ir);
598
- const written = await writeAgentMDFiles(results, opts.agentsDir);
599
- if (written.length > 0) {
573
+ const written = results.map((result) => resolve(opts.agentsDir, result.fileName));
574
+ if (opts.dryRun) {
575
+ process.stderr.write(`Dry-run: would generate ${written.length} agent file(s) in ${opts.agentsDir}\n`);
576
+ }
577
+ else {
578
+ await writeAgentMDFiles(results, opts.agentsDir);
600
579
  process.stderr.write(`Generated ${written.length} agent file(s) in ${opts.agentsDir}\n`);
601
580
  for (const f of written) {
602
581
  process.stderr.write(` ${f}\n`);
603
582
  }
604
- trackedOutputs.push(...written.map(f => resolve(f)));
605
583
  }
584
+ for (const file of written)
585
+ trackSyncOutput(trackedOutputs, "agents", file);
606
586
  }
607
587
  }
608
588
  // Step 6: Generate workflow script(s)
@@ -614,7 +594,6 @@ export async function runSync(opts) {
614
594
  process.stderr.write("No workflow defined in DNA files\n");
615
595
  }
616
596
  else {
617
- const { compileWorkflow } = await import("../../compiler/workflow.js");
618
597
  const { compileWorkflowToShell, writeWorkflowScript } = await import("../../runtime/workflow-runner.js");
619
598
  // If --workflow-name specified, only compile that workflow; otherwise compile all
620
599
  const targetWorkflows = opts.workflowName
@@ -634,9 +613,15 @@ export async function runSync(opts) {
634
613
  }
635
614
  else {
636
615
  const shellResult = compileWorkflowToShell(result.plan);
637
- const written = await writeWorkflowScript(shellResult, opts.workflowDir);
638
- process.stderr.write(`Generated workflow script: ${written}\n`);
639
- trackedOutputs.push(resolve(written));
616
+ const written = resolve(opts.workflowDir, shellResult.fileName);
617
+ if (opts.dryRun) {
618
+ process.stderr.write(`Dry-run: would generate workflow script: ${written}\n`);
619
+ }
620
+ else {
621
+ await writeWorkflowScript(shellResult, opts.workflowDir);
622
+ process.stderr.write(`Generated workflow script: ${written}\n`);
623
+ }
624
+ trackSyncOutput(trackedOutputs, "workflows", written);
640
625
  }
641
626
  }
642
627
  }
@@ -644,9 +629,14 @@ export async function runSync(opts) {
644
629
  // Step 7: Generate skill files from workflows
645
630
  if (opts.skillsDir) {
646
631
  // Clean stale DNA-managed skills before regenerating
647
- const staleSkills = await cleanStaleDNAFiles(opts.skillsDir, "skills", activeNamespaces);
648
- if (staleSkills.length > 0) {
649
- process.stderr.write(`Cleaned ${staleSkills.length} stale skill dir(s)\n`);
632
+ if (opts.dryRun) {
633
+ process.stderr.write(`Dry-run: would clean stale skill dir(s) in ${opts.skillsDir}\n`);
634
+ }
635
+ else {
636
+ const staleSkills = await cleanStaleDNAFiles(opts.skillsDir, "skills", activeNamespaces);
637
+ if (staleSkills.length > 0) {
638
+ process.stderr.write(`Cleaned ${staleSkills.length} stale skill dir(s)\n`);
639
+ }
650
640
  }
651
641
  const dnas = loadedDNAs;
652
642
  const cascadedForSkills = cascadeDNA(dnas);
@@ -660,7 +650,7 @@ export async function runSync(opts) {
660
650
  Object.assign(rawVars, d.variables);
661
651
  }
662
652
  const configPath = opts.files.length === 1 ? opts.files[0] : null;
663
- const variables = await resolveVariables(rawVars, configPath);
653
+ const variables = await resolveVariables(rawVars, configPath, { dryRun: opts.dryRun });
664
654
  // Step 8: Generate .claude/.mcp.json from MCP dependencies (inside skillsDir block, shares variables)
665
655
  {
666
656
  const mcpDeps = {};
@@ -671,15 +661,19 @@ export async function runSync(opts) {
671
661
  if (Object.keys(mcpDeps).length > 0) {
672
662
  const cwd = process.cwd();
673
663
  const mcpJsonPath = resolve(cwd, ".claude", ".mcp.json");
674
- await writeMCPConfig(mcpDeps, mcpJsonPath, variables);
675
- process.stderr.write(`MCP: wrote ${Object.keys(mcpDeps).length} server(s) to ${mcpJsonPath}\n`);
664
+ if (opts.dryRun) {
665
+ process.stderr.write(`Dry-run: would write ${Object.keys(mcpDeps).length} MCP server(s) to ${mcpJsonPath}\n`);
666
+ }
667
+ else {
668
+ await writeMCPConfig(mcpDeps, mcpJsonPath, variables);
669
+ process.stderr.write(`MCP: wrote ${Object.keys(mcpDeps).length} server(s) to ${mcpJsonPath}\n`);
670
+ }
676
671
  }
677
672
  }
678
673
  if (Object.keys(workflows).length === 0 && Object.keys(controllers).length === 0) {
679
674
  process.stderr.write("No workflow or controller defined — no skills to generate\n");
680
675
  }
681
676
  else {
682
- const { compileWorkflow } = await import("../../compiler/workflow.js");
683
677
  const skillResults = [];
684
678
  for (const [name, wfDef] of Object.entries(workflows)) {
685
679
  const result = compileWorkflow(wfDef, { workflow_key: name });
@@ -691,12 +685,27 @@ export async function runSync(opts) {
691
685
  skillResults.push(compileControllerToSkill(name, controllerDef, variables, workflows));
692
686
  }
693
687
  if (skillResults.length > 0) {
694
- const written = await writeSkillFiles(skillResults, opts.skillsDir);
695
- process.stderr.write(`Generated ${written.length} skill file(s) in ${opts.skillsDir}\n`);
696
- for (const f of written) {
697
- process.stderr.write(` ${f}\n`);
688
+ let written = skillResults.flatMap((result) => {
689
+ const files = [resolve(opts.skillsDir, result.dirName, "SKILL.md")];
690
+ if (result.bodyFileName)
691
+ files.push(resolve(opts.skillsDir, result.dirName, "skill-bodies", result.bodyFileName));
692
+ return files;
693
+ });
694
+ if (skillResults.some((result) => result.mapEntry)) {
695
+ written.push(resolve(opts.skillsDir, ".intentdna-skill-map.json"));
696
+ }
697
+ if (opts.dryRun) {
698
+ process.stderr.write(`Dry-run: would generate ${written.length} skill artifact(s) in ${opts.skillsDir}\n`);
699
+ }
700
+ else {
701
+ written = await writeSkillFiles(skillResults, opts.skillsDir);
702
+ process.stderr.write(`Generated ${written.length} skill artifact(s) in ${opts.skillsDir}\n`);
703
+ for (const f of written) {
704
+ process.stderr.write(` ${f}\n`);
705
+ }
698
706
  }
699
- trackedOutputs.push(...written.map(f => resolve(f)));
707
+ for (const file of written)
708
+ trackSyncOutput(trackedOutputs, "skills-controllers", file);
700
709
  }
701
710
  }
702
711
  }
@@ -708,8 +717,17 @@ export async function runSync(opts) {
708
717
  // Step 9: Generate .dna/lock file (only when --lock is specified or outputs exist)
709
718
  if (opts.lockFile && trackedOutputs.length > 0) {
710
719
  const lockPath = resolve(opts.lockFile);
711
- await generateLockFile(lockPath, expandedFiles, trackedOutputs);
712
- process.stderr.write(`Lock file: ${lockPath}\n`);
720
+ if (opts.dryRun) {
721
+ process.stderr.write(`Dry-run: would write lock file: ${lockPath}\n`);
722
+ }
723
+ else {
724
+ await generateLockFile(lockPath, expandedFiles, lockTrackedOutputPaths(trackedOutputs));
725
+ process.stderr.write(`Lock file: ${lockPath}\n`);
726
+ }
727
+ trackSyncOutput(trackedOutputs, "lock-file", lockPath);
728
+ }
729
+ if (opts.dryRun || opts.postSummary) {
730
+ summarizeSyncOutputs(trackedOutputs, opts.dryRun === true);
713
731
  }
714
732
  return 0;
715
733
  }
@@ -781,7 +799,7 @@ async function registerDNAMCPServer(cwd) {
781
799
  const mcpServers = (existing.mcpServers ?? {});
782
800
  // Don't overwrite if user already configured it
783
801
  if (mcpServers["intentdna"])
784
- return;
802
+ return mcpJsonPath;
785
803
  mcpServers["intentdna"] = {
786
804
  command: "dna-mcp",
787
805
  args: ["--project-dir", cwd],
@@ -790,4 +808,5 @@ async function registerDNAMCPServer(cwd) {
790
808
  await mkdir(dirname(mcpJsonPath), { recursive: true });
791
809
  await writeFileAsync(mcpJsonPath, JSON.stringify(existing, null, 2) + "\n", "utf-8");
792
810
  process.stderr.write(`MCP: registered dna-mcp server in ${mcpJsonPath}\n`);
811
+ return mcpJsonPath;
793
812
  }
@@ -1,5 +1,5 @@
1
1
  /**
2
- * dna templates deploy copy local src/templates/*.dna.yaml to global intentdna install
2
+ * dna templates — template catalog and deployment commands.
3
3
  */
4
4
  export interface TemplatesDeployOptions {
5
5
  /** Override local templates dir (for testing) */
@@ -7,4 +7,13 @@ export interface TemplatesDeployOptions {
7
7
  /** Override global target dir (for testing) */
8
8
  globalDir?: string;
9
9
  }
10
+ export interface TemplatesListOptions {
11
+ projectDir?: string;
12
+ }
13
+ export interface TemplatesShowOptions {
14
+ name: string;
15
+ projectDir?: string;
16
+ }
17
+ export declare function runTemplatesList(opts?: TemplatesListOptions): Promise<number>;
18
+ export declare function runTemplatesShow(opts: TemplatesShowOptions): Promise<number>;
10
19
  export declare function runTemplatesDeploy(opts?: TemplatesDeployOptions): Promise<number>;