intentdna 1.6.5 → 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 (58) 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/init.js +11 -63
  9. package/dist/cli/commands/run.js +5 -4
  10. package/dist/cli/commands/show.js +2 -38
  11. package/dist/cli/commands/sync.d.ts +9 -6
  12. package/dist/cli/commands/sync.js +186 -184
  13. package/dist/cli/commands/templates.d.ts +10 -1
  14. package/dist/cli/commands/templates.js +50 -1
  15. package/dist/cli/commands/validate.js +15 -9
  16. package/dist/cli/commands/verify.js +97 -25
  17. package/dist/cli/index.js +76 -11
  18. package/dist/compiler/cascade.d.ts +3 -1
  19. package/dist/compiler/cascade.js +51 -0
  20. package/dist/compiler/compile.js +37 -0
  21. package/dist/compiler/diagnostics.d.ts +17 -0
  22. package/dist/compiler/diagnostics.js +30 -0
  23. package/dist/compiler/index.d.ts +3 -0
  24. package/dist/compiler/index.js +8 -11
  25. package/dist/compiler/input-resolver.d.ts +25 -0
  26. package/dist/compiler/input-resolver.js +175 -0
  27. package/dist/hooks/cli.d.ts +10 -1
  28. package/dist/hooks/cli.js +37 -22
  29. package/dist/hooks/state.d.ts +2 -0
  30. package/dist/hooks/state.js +23 -2
  31. package/dist/mcp/index.js +2 -0
  32. package/dist/mcp/tools-compile.js +18 -49
  33. package/dist/mcp/tools-context.d.ts +2 -0
  34. package/dist/mcp/tools-context.js +85 -0
  35. package/dist/mcp/tools-enforce.d.ts +2 -2
  36. package/dist/mcp/tools-enforce.js +19 -49
  37. package/dist/mcp/tools-observability.js +24 -0
  38. package/dist/report/kernel-signals.js +3 -0
  39. package/dist/report/report-package.d.ts +56 -0
  40. package/dist/report/report-package.js +85 -0
  41. package/dist/runtime/agent-md.d.ts +1 -0
  42. package/dist/runtime/agent-md.js +21 -3
  43. package/dist/runtime/context-sources.d.ts +14 -0
  44. package/dist/runtime/context-sources.js +60 -0
  45. package/dist/runtime/skill-adapter.d.ts +32 -4
  46. package/dist/runtime/skill-adapter.js +184 -9
  47. package/dist/runtime/workflow-runner.d.ts +1 -1
  48. package/dist/runtime/workflow-runner.js +1 -1
  49. package/dist/schema/types.d.ts +33 -0
  50. package/dist/schema/validate.js +156 -2
  51. package/dist/schema/validators/controllers.js +16 -0
  52. package/dist/signals/index.d.ts +10 -0
  53. package/dist/signals/index.js +90 -5
  54. package/dist/templates/catalog.d.ts +19 -0
  55. package/dist/templates/catalog.js +57 -0
  56. package/dist/templates/flutter-rewrite.dna.yaml +2 -2
  57. package/package.json +1 -1
  58. package/spec/foundation-hardening.md +2 -1
@@ -10,19 +10,20 @@
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
21
  import { syncInjectMarkdownTargets } from "../../runtime/markdown-target-registry.js";
22
22
  import { getSyncOutputArtifactMetadata } from "../../runtime/output-artifact-registry.js";
23
+ import { compileWorkflow } from "../../compiler/workflow.js";
23
24
  import { compileAllRolesToAgentMD, writeAgentMDFiles, removeAgentMDFiles } from "../../runtime/agent-md.js";
24
25
  import { removeWorkflowScripts } from "../../runtime/workflow-runner.js";
25
- import { compileWorkflowToSkill, compileControllerToSkill, writeSkillFiles } from "../../runtime/skill-adapter.js";
26
+ import { compileWorkflowToSkill, compileControllerToSkill, writeSkillFiles, removeSkillFiles } from "../../runtime/skill-adapter.js";
26
27
  import { compilePluginSettings, detectEnabledEvents, mergeSettingsFile, recommendHookTimeouts } from "../../runtime/settings-adapter.js";
27
28
  import { createCompiledIR, writeCompiledIR } from "../../runtime/plugin-adapter.js";
28
29
  import { cascadeDNA } from "../../compiler/cascade.js";
@@ -73,32 +74,6 @@ const LEGACY_BASH_HOOKS = [
73
74
  "dna-pre-compact.sh",
74
75
  "dna-notification.sh",
75
76
  ];
76
- const TEMPLATES_DIR = resolve(dirname(fileURLToPath(import.meta.url)), "..", "..", "templates");
77
- function calculateTemplateHash(content) {
78
- return createHash("sha256").update(content, "utf-8").digest("hex").slice(0, 16);
79
- }
80
- async function findTemplateFile(templateName) {
81
- // Try direct filename match first
82
- const directPath = resolve(TEMPLATES_DIR, `${templateName}.dna.yaml`);
83
- if (await fileExists(directPath))
84
- return directPath;
85
- // Fallback: search by id or name field
86
- try {
87
- const files = await readdir(TEMPLATES_DIR);
88
- for (const file of files) {
89
- if (!file.endsWith(".dna.yaml") && !file.endsWith(".dna.yml"))
90
- continue;
91
- const fullPath = resolve(TEMPLATES_DIR, file);
92
- const content = await readFile(fullPath, "utf-8");
93
- const data = parseYAML(content);
94
- if (data.id === templateName || data.name === templateName) {
95
- return fullPath;
96
- }
97
- }
98
- }
99
- catch { /* fail-open */ }
100
- return null;
101
- }
102
77
  /**
103
78
  * Remove legacy bash hook scripts written by previous Intent DNA versions.
104
79
  * Only removes files that contain the sentinel comment.
@@ -129,11 +104,11 @@ export async function cleanStaleDNAFiles(dir, type, activeNamespaces) {
129
104
  const entries = await readdir(dir, { withFileTypes: true });
130
105
  for (const entry of entries) {
131
106
  const fullPath = resolve(dir, entry.name);
132
- // Match active namespace prefixes directly so multi-hyphen names like
133
- // dna-frw-fix-loop still resolve to namespace frw.
134
- 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)
135
111
  continue;
136
- }
137
112
  if (type === "agents" && entry.isFile() && entry.name.endsWith(".md")) {
138
113
  const content = await readFile(fullPath, "utf-8").catch(() => "");
139
114
  if (content.includes(DNA_SENTINEL)) {
@@ -171,7 +146,7 @@ function promptVariable(description, defaultValue) {
171
146
  * prompt user interactively. If already plain strings, use as-is.
172
147
  * Saves resolved values back to config file.
173
148
  */
174
- async function resolveVariables(rawVars, configPath) {
149
+ async function resolveVariables(rawVars, configPath, options) {
175
150
  const resolved = {};
176
151
  const needsPrompt = [];
177
152
  for (const [key, val] of Object.entries(rawVars)) {
@@ -182,7 +157,7 @@ async function resolveVariables(rawVars, configPath) {
182
157
  needsPrompt.push({ key, desc: val.description, def: val.default });
183
158
  }
184
159
  }
185
- if (needsPrompt.length > 0 && process.stdin.isTTY) {
160
+ if (needsPrompt.length > 0 && process.stdin.isTTY && !options?.dryRun) {
186
161
  process.stderr.write("\nProject variables (press Enter for default):\n");
187
162
  for (const v of needsPrompt) {
188
163
  const value = await promptVariable(v.desc, v.def);
@@ -218,42 +193,8 @@ async function resolveVariables(rawVars, configPath) {
218
193
  * Auto-detect DNA config file in current directory (legacy single-config).
219
194
  * Priority: .dna/config.yaml > .dna/config.yml > .dna/config.json > .dna.yaml > .dna.yml > .dna.json
220
195
  */
221
- async function autoDetectDNA() {
222
- const cwd = process.cwd();
223
- const candidates = [
224
- ".dna/config.yaml", ".dna/config.yml", ".dna/config.json",
225
- ".dna.yaml", ".dna.yml", ".dna.json",
226
- ];
227
- for (const name of candidates) {
228
- const path = resolve(cwd, name);
229
- if (await fileExists(path))
230
- return path;
231
- }
232
- return null;
233
- }
234
- /**
235
- * Auto-detect DNA configs: multi-config (.dna/configs/*.yaml) or legacy single-config.
236
- * Returns array of absolute paths.
237
- */
238
196
  export async function autoDetectConfigs() {
239
- const cwd = process.cwd();
240
- // Priority 1: .dna/configs/*.yaml (multi-config)
241
- const configsDir = resolve(cwd, ".dna", "configs");
242
- try {
243
- const files = await readdir(configsDir);
244
- const yamls = files
245
- .filter(f => f.endsWith(".yaml") || f.endsWith(".yml"))
246
- .sort()
247
- .map(f => resolve(configsDir, f));
248
- if (yamls.length > 0)
249
- return yamls;
250
- }
251
- catch { /* dir doesn't exist */ }
252
- // Fallback: legacy single-config paths
253
- const legacy = await autoDetectDNA();
254
- if (legacy)
255
- return [legacy];
256
- return [];
197
+ return detectDNAConfigs(process.cwd());
257
198
  }
258
199
  /**
259
200
  * Read the current package version from package.json.
@@ -264,26 +205,7 @@ export { getPackageVersion } from "../util/version.js";
264
205
  * Throws if two configs declare the same namespace.
265
206
  */
266
207
  export async function checkNamespaceCollisions(configPaths) {
267
- const namespaces = new Map();
268
- for (const configPath of configPaths) {
269
- try {
270
- const content = await readFile(configPath, "utf-8");
271
- const data = parseYAML(content);
272
- const ns = typeof data.namespace === "string" ? data.namespace : undefined;
273
- if (ns) {
274
- const file = basename(configPath);
275
- if (namespaces.has(ns)) {
276
- throw new Error(`Namespace "${ns}" conflict: ${namespaces.get(ns)} and ${file}. Each template must have a unique namespace.`);
277
- }
278
- namespaces.set(ns, file);
279
- }
280
- }
281
- catch (err) {
282
- if (err instanceof Error && err.message.includes("conflict"))
283
- throw err;
284
- // Parse error — will be caught later during compilation
285
- }
286
- }
208
+ await assertNoNamespaceCollisions(configPaths);
287
209
  }
288
210
  /**
289
211
  * Compare two semver strings. Returns:
@@ -308,10 +230,8 @@ export function compareSemver(a, b) {
308
230
  return 0;
309
231
  }
310
232
  /**
311
- * Check template versions against current template content hashes.
312
- * Returns upgrade info for changed configs and prints human-readable suggestions.
313
- *
314
- * 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.
315
235
  */
316
236
  export async function checkTemplateVersions(configPaths) {
317
237
  const upgrades = [];
@@ -324,26 +244,37 @@ export async function checkTemplateVersions(configPaths) {
324
244
  const tmplVersion = typeof data._template_version === "string" ? data._template_version : undefined;
325
245
  if (!tmplName)
326
246
  continue;
327
- const templatePath = await findTemplateFile(tmplName);
328
- 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`);
329
259
  continue;
330
- const currentTemplateContent = await readFile(templatePath, "utf-8");
331
- const currentHash = calculateTemplateHash(currentTemplateContent);
260
+ }
332
261
  if (!tmplHash) {
333
262
  upgrades.push({
334
263
  configPath,
335
264
  templateName: tmplName,
336
265
  currentVersion: tmplVersion ?? "unknown",
337
- availableVersion: "content-changed",
266
+ availableVersion: template.version ?? "content-changed",
267
+ reason: "missing-hash",
338
268
  });
339
269
  process.stderr.write(`Upgrade available: "${tmplName}" has no content hash. Run: dna init --upgrade ${tmplName}\n`);
340
270
  }
341
- else if (currentHash !== tmplHash) {
271
+ else if (template.contentHash !== tmplHash) {
342
272
  upgrades.push({
343
273
  configPath,
344
274
  templateName: tmplName,
345
275
  currentVersion: tmplVersion ?? "unknown",
346
276
  availableVersion: "content-changed",
277
+ reason: "content-drift",
347
278
  });
348
279
  process.stderr.write(`Template content changed: "${tmplName}". Run: dna init --upgrade ${tmplName}\n`);
349
280
  }
@@ -354,39 +285,6 @@ export async function checkTemplateVersions(configPaths) {
354
285
  }
355
286
  return upgrades;
356
287
  }
357
- function resolveSpeciesPath(ref) {
358
- if (!ref.startsWith("species:"))
359
- return null;
360
- const name = ref.slice("species:".length);
361
- const thisDir = dirname(fileURLToPath(import.meta.url));
362
- return resolve(thisDir, "..", "..", "species", `${name}.dna.json`);
363
- }
364
- async function expandFiles(files) {
365
- const expanded = [];
366
- const seen = new Set();
367
- for (const file of files) {
368
- const abs = resolve(file);
369
- if (seen.has(abs))
370
- continue;
371
- seen.add(abs);
372
- expanded.push(abs);
373
- try {
374
- const raw = await readFile(abs, "utf-8");
375
- const dna = JSON.parse(raw);
376
- for (const ref of dna.cascade?.inherits ?? []) {
377
- const speciesPath = resolveSpeciesPath(ref);
378
- if (speciesPath && !seen.has(speciesPath)) {
379
- seen.add(speciesPath);
380
- expanded.unshift(speciesPath);
381
- }
382
- }
383
- }
384
- catch {
385
- // Will fail later with proper error
386
- }
387
- }
388
- return expanded;
389
- }
390
288
  function fileSha256(content) {
391
289
  return createHash("sha256").update(content, "utf-8").digest("hex");
392
290
  }
@@ -400,6 +298,21 @@ function trackSyncOutput(trackedOutputs, artifact, path) {
400
298
  }
401
299
  trackedOutputs.push({ artifact, path: resolve(path) });
402
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
+ }
403
316
  function lockTrackedOutputPaths(trackedOutputs) {
404
317
  return trackedOutputs
405
318
  .filter((output) => getSyncOutputArtifactMetadata(output.artifact)?.trackedByLock)
@@ -432,10 +345,8 @@ export async function runSync(opts) {
432
345
  }
433
346
  // Namespace collision check (multi-config)
434
347
  if (autoFiles.length > 1) {
435
- await checkNamespaceCollisions(autoFiles);
348
+ await assertNoNamespaceCollisions(autoFiles);
436
349
  }
437
- // Template version check
438
- await checkTemplateVersions(autoFiles);
439
350
  }
440
351
  }
441
352
  if (!opts.remove && !opts.inject && !opts.hooksDir && !opts.agentsDir && !opts.workflowDir && !opts.skillsDir && !opts.settingsPath) {
@@ -451,18 +362,17 @@ export async function runSync(opts) {
451
362
  process.stderr.write(`Auto-detect: Claude Code → CLAUDE.md + agents + skills + settings\n`);
452
363
  }
453
364
  }
454
- // Auto-detect plugin vs bin mode if --plugin not explicitly set
455
- if (!opts.remove && opts.plugin === undefined) {
456
- const mode = await detectMode();
457
- if (mode === "plugin") {
458
- opts.plugin = true;
459
- process.stderr.write("Auto-detect: plugin mode (intentdna registered as Claude Code plugin)\n");
460
- }
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`);
461
370
  }
371
+ opts.plugin = resolvedMode === "plugin";
462
372
  // Handle --remove
463
373
  if (opts.remove) {
464
- if (!opts.inject && !opts.hooksDir && !opts.agentsDir && !opts.workflowDir) {
465
- 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");
466
376
  return 2;
467
377
  }
468
378
  if (opts.inject) {
@@ -501,6 +411,15 @@ export async function runSync(opts) {
501
411
  process.stderr.write(`No Intent DNA workflow scripts found in ${opts.workflowDir}\n`);
502
412
  }
503
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
+ }
504
423
  return 0;
505
424
  }
506
425
  if (opts.files.length === 0) {
@@ -522,11 +441,18 @@ export async function runSync(opts) {
522
441
  // Step 0: Expand and pre-validate all DNA files before any work
523
442
  // loadDNA() calls validateDNA() internally — validation failures throw here,
524
443
  // before evolve/compile/inject waste any work.
525
- const expandedFiles = await expandFiles(opts.files);
444
+ const expandedFiles = await expandDNAInputFiles(opts.files);
445
+ await checkTemplateVersions(expandedFiles);
526
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
+ }
527
450
  // Step 1: Evolve if requested
528
451
  let epigeneticMarkers = undefined;
529
- 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) {
530
456
  const dnas = loadedDNAs;
531
457
  // Use the highest-priority DNA's ID
532
458
  const primaryDna = dnas.reduce((a, b) => a.cascade.priority >= b.cascade.priority ? a : b);
@@ -546,13 +472,19 @@ export async function runSync(opts) {
546
472
  // Step 3: Render and inject markdown
547
473
  if (opts.inject) {
548
474
  const target = opts.target;
549
- const markdown = compileToMarkdown(ir, target);
550
- const modified = await injectIntoFile(opts.inject, markdown);
551
- if (modified) {
552
- 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`);
553
478
  }
554
479
  else {
555
- 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
+ }
556
488
  }
557
489
  trackSyncOutput(trackedOutputs, "markdown-injection", opts.inject);
558
490
  }
@@ -563,17 +495,35 @@ export async function runSync(opts) {
563
495
  const cascadedForIR = cascadeDNA(dnas);
564
496
  const roles = cascadedForIR.roles;
565
497
  const compiled = createCompiledIR(ir, roles);
566
- const irPath = await writeCompiledIR(cwd, compiled);
567
- process.stderr.write(`Wrote compiled IR: ${irPath}\n`);
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
+ }
568
507
  trackSyncOutput(trackedOutputs, "compiled-ir", irPath);
569
508
  // Clean up old bash hooks if they exist
570
509
  const oldHooksDir = resolve(cwd, ".claude", "hooks");
571
- const removed = await removeLegacyBashHooks(oldHooksDir);
572
- if (removed > 0) {
573
- 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
+ }
574
518
  }
575
519
  // G2/G3: Register built-in DNA MCP server in .claude/.mcp.json
576
- const mcpPath = 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
+ }
577
527
  trackSyncOutput(trackedOutputs, "mcp-config", mcpPath);
578
528
  }
579
529
  // Step 3.6: Register hooks in settings.json (bin mode only)
@@ -582,8 +532,14 @@ export async function runSync(opts) {
582
532
  const enabledEvents = detectEnabledEvents(ir);
583
533
  if (enabledEvents.length > 0) {
584
534
  const pluginSettings = compilePluginSettings(enabledEvents, 10, recommendHookTimeouts(ir));
585
- await mergeSettingsFile(pluginSettings, opts.settingsPath);
586
- process.stderr.write(`Bin mode: registered ${enabledEvents.length} hook event(s) in ${opts.settingsPath}\n`);
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
+ }
587
543
  trackSyncOutput(trackedOutputs, "settings-hooks", opts.settingsPath);
588
544
  }
589
545
  }
@@ -597,9 +553,14 @@ export async function runSync(opts) {
597
553
  // Step 5: Generate agent MD files
598
554
  if (opts.agentsDir) {
599
555
  // Clean stale DNA-managed agents before regenerating
600
- const staleAgents = await cleanStaleDNAFiles(opts.agentsDir, "agents", activeNamespaces);
601
- if (staleAgents.length > 0) {
602
- 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
+ }
603
564
  }
604
565
  const dnas = loadedDNAs;
605
566
  const cascaded = cascadeDNA(dnas);
@@ -609,15 +570,19 @@ export async function runSync(opts) {
609
570
  }
610
571
  else {
611
572
  const results = compileAllRolesToAgentMD(roles, ir);
612
- const written = await writeAgentMDFiles(results, opts.agentsDir);
613
- 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);
614
579
  process.stderr.write(`Generated ${written.length} agent file(s) in ${opts.agentsDir}\n`);
615
580
  for (const f of written) {
616
581
  process.stderr.write(` ${f}\n`);
617
582
  }
618
- for (const file of written)
619
- trackSyncOutput(trackedOutputs, "agents", file);
620
583
  }
584
+ for (const file of written)
585
+ trackSyncOutput(trackedOutputs, "agents", file);
621
586
  }
622
587
  }
623
588
  // Step 6: Generate workflow script(s)
@@ -629,7 +594,6 @@ export async function runSync(opts) {
629
594
  process.stderr.write("No workflow defined in DNA files\n");
630
595
  }
631
596
  else {
632
- const { compileWorkflow } = await import("../../compiler/workflow.js");
633
597
  const { compileWorkflowToShell, writeWorkflowScript } = await import("../../runtime/workflow-runner.js");
634
598
  // If --workflow-name specified, only compile that workflow; otherwise compile all
635
599
  const targetWorkflows = opts.workflowName
@@ -649,8 +613,14 @@ export async function runSync(opts) {
649
613
  }
650
614
  else {
651
615
  const shellResult = compileWorkflowToShell(result.plan);
652
- const written = await writeWorkflowScript(shellResult, opts.workflowDir);
653
- process.stderr.write(`Generated workflow script: ${written}\n`);
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
+ }
654
624
  trackSyncOutput(trackedOutputs, "workflows", written);
655
625
  }
656
626
  }
@@ -659,9 +629,14 @@ export async function runSync(opts) {
659
629
  // Step 7: Generate skill files from workflows
660
630
  if (opts.skillsDir) {
661
631
  // Clean stale DNA-managed skills before regenerating
662
- const staleSkills = await cleanStaleDNAFiles(opts.skillsDir, "skills", activeNamespaces);
663
- if (staleSkills.length > 0) {
664
- 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
+ }
665
640
  }
666
641
  const dnas = loadedDNAs;
667
642
  const cascadedForSkills = cascadeDNA(dnas);
@@ -675,7 +650,7 @@ export async function runSync(opts) {
675
650
  Object.assign(rawVars, d.variables);
676
651
  }
677
652
  const configPath = opts.files.length === 1 ? opts.files[0] : null;
678
- const variables = await resolveVariables(rawVars, configPath);
653
+ const variables = await resolveVariables(rawVars, configPath, { dryRun: opts.dryRun });
679
654
  // Step 8: Generate .claude/.mcp.json from MCP dependencies (inside skillsDir block, shares variables)
680
655
  {
681
656
  const mcpDeps = {};
@@ -686,15 +661,19 @@ export async function runSync(opts) {
686
661
  if (Object.keys(mcpDeps).length > 0) {
687
662
  const cwd = process.cwd();
688
663
  const mcpJsonPath = resolve(cwd, ".claude", ".mcp.json");
689
- await writeMCPConfig(mcpDeps, mcpJsonPath, variables);
690
- 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
+ }
691
671
  }
692
672
  }
693
673
  if (Object.keys(workflows).length === 0 && Object.keys(controllers).length === 0) {
694
674
  process.stderr.write("No workflow or controller defined — no skills to generate\n");
695
675
  }
696
676
  else {
697
- const { compileWorkflow } = await import("../../compiler/workflow.js");
698
677
  const skillResults = [];
699
678
  for (const [name, wfDef] of Object.entries(workflows)) {
700
679
  const result = compileWorkflow(wfDef, { workflow_key: name });
@@ -706,10 +685,24 @@ export async function runSync(opts) {
706
685
  skillResults.push(compileControllerToSkill(name, controllerDef, variables, workflows));
707
686
  }
708
687
  if (skillResults.length > 0) {
709
- const written = await writeSkillFiles(skillResults, opts.skillsDir);
710
- process.stderr.write(`Generated ${written.length} skill file(s) in ${opts.skillsDir}\n`);
711
- for (const f of written) {
712
- 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
+ }
713
706
  }
714
707
  for (const file of written)
715
708
  trackSyncOutput(trackedOutputs, "skills-controllers", file);
@@ -724,8 +717,17 @@ export async function runSync(opts) {
724
717
  // Step 9: Generate .dna/lock file (only when --lock is specified or outputs exist)
725
718
  if (opts.lockFile && trackedOutputs.length > 0) {
726
719
  const lockPath = resolve(opts.lockFile);
727
- await generateLockFile(lockPath, expandedFiles, lockTrackedOutputPaths(trackedOutputs));
728
- 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);
729
731
  }
730
732
  return 0;
731
733
  }
@@ -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>;