intentdna 1.6.5 → 1.7.4

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 (82) hide show
  1. package/.claude-plugin/marketplace.json +2 -2
  2. package/.claude-plugin/plugin.json +1 -1
  3. package/README.md +26 -15
  4. package/dist/cli/commands/compile.js +2 -47
  5. package/dist/cli/commands/context.d.ts +8 -0
  6. package/dist/cli/commands/context.js +63 -0
  7. package/dist/cli/commands/feedback.d.ts +3 -2
  8. package/dist/cli/commands/feedback.js +16 -5
  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 +191 -186
  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.d.ts +32 -0
  18. package/dist/cli/commands/verify.js +270 -31
  19. package/dist/cli/index.js +78 -11
  20. package/dist/compiler/activate.js +11 -6
  21. package/dist/compiler/cascade.d.ts +5 -1
  22. package/dist/compiler/cascade.js +74 -1
  23. package/dist/compiler/compile.js +38 -0
  24. package/dist/compiler/diagnostics.d.ts +17 -0
  25. package/dist/compiler/diagnostics.js +30 -0
  26. package/dist/compiler/index.d.ts +7 -0
  27. package/dist/compiler/index.js +13 -13
  28. package/dist/compiler/input-resolver.d.ts +32 -0
  29. package/dist/compiler/input-resolver.js +281 -0
  30. package/dist/compiler/provenance.d.ts +8 -0
  31. package/dist/compiler/provenance.js +127 -0
  32. package/dist/governance/index.d.ts +4 -3
  33. package/dist/governance/index.js +3 -4
  34. package/dist/governance/runtime-decision-event.d.ts +85 -0
  35. package/dist/governance/runtime-decision-event.js +231 -0
  36. package/dist/governance/types.d.ts +3 -3
  37. package/dist/governance/types.js +3 -3
  38. package/dist/hooks/cli.d.ts +10 -1
  39. package/dist/hooks/cli.js +199 -35
  40. package/dist/hooks/state.d.ts +5 -10
  41. package/dist/hooks/state.js +170 -2
  42. package/dist/index.d.ts +1 -0
  43. package/dist/index.js +1 -0
  44. package/dist/mcp/index.js +2 -0
  45. package/dist/mcp/tools-compile.js +18 -49
  46. package/dist/mcp/tools-context.d.ts +2 -0
  47. package/dist/mcp/tools-context.js +85 -0
  48. package/dist/mcp/tools-enforce.d.ts +2 -2
  49. package/dist/mcp/tools-enforce.js +19 -49
  50. package/dist/mcp/tools-observability.js +26 -2
  51. package/dist/mcp/tools-state.d.ts +1 -1
  52. package/dist/mcp/tools-state.js +17 -7
  53. package/dist/report/kernel-report.d.ts +27 -0
  54. package/dist/report/kernel-report.js +60 -19
  55. package/dist/report/kernel-signals.d.ts +5 -2
  56. package/dist/report/kernel-signals.js +87 -2
  57. package/dist/report/report-package.d.ts +64 -0
  58. package/dist/report/report-package.js +90 -0
  59. package/dist/runtime/agent-md.d.ts +1 -0
  60. package/dist/runtime/agent-md.js +21 -3
  61. package/dist/runtime/claude-sdk.d.ts +9 -4
  62. package/dist/runtime/claude-sdk.js +9 -0
  63. package/dist/runtime/context-sources.d.ts +14 -0
  64. package/dist/runtime/context-sources.js +60 -0
  65. package/dist/runtime/plugin-adapter.d.ts +5 -1
  66. package/dist/runtime/plugin-adapter.js +2 -0
  67. package/dist/runtime/skill-adapter.d.ts +32 -4
  68. package/dist/runtime/skill-adapter.js +184 -9
  69. package/dist/runtime/workflow-runner.d.ts +1 -1
  70. package/dist/runtime/workflow-runner.js +1 -1
  71. package/dist/schema/types.d.ts +84 -0
  72. package/dist/schema/validate.js +156 -2
  73. package/dist/schema/validators/controllers.js +16 -0
  74. package/dist/signals/index.d.ts +10 -0
  75. package/dist/signals/index.js +90 -5
  76. package/dist/templates/catalog.d.ts +19 -0
  77. package/dist/templates/catalog.js +57 -0
  78. package/dist/templates/flutter-rewrite.dna.yaml +2 -2
  79. package/package.json +1 -1
  80. package/spec/README.md +1 -1
  81. package/spec/foundation-hardening.md +2 -1
  82. package/spec/schema-spec.md +78 -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, expandDNAInputs, 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,12 +298,27 @@ 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)
406
319
  .map((output) => output.path);
407
320
  }
408
- async function generateLockFile(lockPath, dnaSources, outputFiles) {
321
+ async function generateLockFile(lockPath, dnaSources, outputFiles, provenance, compiledIrHash) {
409
322
  const outputs = {};
410
323
  for (const f of outputFiles) {
411
324
  outputs[f] = { sha256: await computeFileHash(f) };
@@ -414,6 +327,8 @@ async function generateLockFile(lockPath, dnaSources, outputFiles) {
414
327
  synced_at: new Date().toISOString(),
415
328
  dna_sources: dnaSources,
416
329
  outputs,
330
+ compiled_ir_hash: compiledIrHash,
331
+ provenance,
417
332
  };
418
333
  await mkdir(dirname(lockPath), { recursive: true });
419
334
  await writeFileAsync(lockPath, JSON.stringify(lock, null, 2) + "\n", "utf-8");
@@ -432,10 +347,8 @@ export async function runSync(opts) {
432
347
  }
433
348
  // Namespace collision check (multi-config)
434
349
  if (autoFiles.length > 1) {
435
- await checkNamespaceCollisions(autoFiles);
350
+ await assertNoNamespaceCollisions(autoFiles);
436
351
  }
437
- // Template version check
438
- await checkTemplateVersions(autoFiles);
439
352
  }
440
353
  }
441
354
  if (!opts.remove && !opts.inject && !opts.hooksDir && !opts.agentsDir && !opts.workflowDir && !opts.skillsDir && !opts.settingsPath) {
@@ -451,18 +364,17 @@ export async function runSync(opts) {
451
364
  process.stderr.write(`Auto-detect: Claude Code → CLAUDE.md + agents + skills + settings\n`);
452
365
  }
453
366
  }
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
- }
367
+ const requestedMode = opts.mode ?? (opts.plugin === true ? "plugin" : opts.plugin === false ? "bin" : "auto");
368
+ let resolvedMode = requestedMode === "auto" ? "bin" : requestedMode;
369
+ if (!opts.remove && requestedMode === "auto") {
370
+ resolvedMode = await detectMode();
371
+ process.stderr.write(`Auto-detect: ${resolvedMode} mode${resolvedMode === "plugin" ? " (intentdna registered as Claude Code plugin)" : ""}\n`);
461
372
  }
373
+ opts.plugin = resolvedMode === "plugin";
462
374
  // Handle --remove
463
375
  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");
376
+ if (!opts.inject && !opts.hooksDir && !opts.agentsDir && !opts.workflowDir && !opts.skillsDir) {
377
+ process.stderr.write("Error: --inject, --hooks, --agents, --workflow, or --skills is required with --remove\n");
466
378
  return 2;
467
379
  }
468
380
  if (opts.inject) {
@@ -501,6 +413,15 @@ export async function runSync(opts) {
501
413
  process.stderr.write(`No Intent DNA workflow scripts found in ${opts.workflowDir}\n`);
502
414
  }
503
415
  }
416
+ if (opts.skillsDir) {
417
+ const count = await removeSkillFiles(opts.skillsDir);
418
+ if (count > 0) {
419
+ process.stderr.write(`Removed ${count} Intent DNA skill dir(s) from ${opts.skillsDir}\n`);
420
+ }
421
+ else {
422
+ process.stderr.write(`No Intent DNA skill dirs found in ${opts.skillsDir}\n`);
423
+ }
424
+ }
504
425
  return 0;
505
426
  }
506
427
  if (opts.files.length === 0) {
@@ -522,11 +443,19 @@ export async function runSync(opts) {
522
443
  // Step 0: Expand and pre-validate all DNA files before any work
523
444
  // loadDNA() calls validateDNA() internally — validation failures throw here,
524
445
  // before evolve/compile/inject waste any work.
525
- const expandedFiles = await expandFiles(opts.files);
446
+ const expandedInputs = await expandDNAInputs(opts.files);
447
+ const expandedFiles = expandedInputs.files;
448
+ await checkTemplateVersions(expandedFiles);
526
449
  const loadedDNAs = await Promise.all(expandedFiles.map(loadDNA));
450
+ if (opts.dryRun) {
451
+ process.stderr.write("Dry-run: no files will be written or removed.\n");
452
+ }
527
453
  // Step 1: Evolve if requested
528
454
  let epigeneticMarkers = undefined;
529
- if (opts.evolve) {
455
+ if (opts.evolve && opts.dryRun) {
456
+ process.stderr.write("Dry-run: would run evolution before compiling\n");
457
+ }
458
+ else if (opts.evolve) {
530
459
  const dnas = loadedDNAs;
531
460
  // Use the highest-priority DNA's ID
532
461
  const primaryDna = dnas.reduce((a, b) => a.cascade.priority >= b.cascade.priority ? a : b);
@@ -538,7 +467,7 @@ export async function runSync(opts) {
538
467
  }
539
468
  }
540
469
  // Step 2: Compile
541
- const ir = await compileFromFiles(expandedFiles, {
470
+ const ir = await compileFromFiles(opts.files, {
542
471
  context: opts.context,
543
472
  role: opts.role,
544
473
  epigenetic_markers: epigeneticMarkers,
@@ -546,13 +475,19 @@ export async function runSync(opts) {
546
475
  // Step 3: Render and inject markdown
547
476
  if (opts.inject) {
548
477
  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`);
478
+ if (opts.dryRun) {
479
+ compileToMarkdown(ir, target);
480
+ process.stderr.write(`Dry-run: would sync ${opts.inject} (${target})\n`);
553
481
  }
554
482
  else {
555
- process.stderr.write(`No changes needed in ${opts.inject}\n`);
483
+ const markdown = compileToMarkdown(ir, target);
484
+ const modified = await injectIntoFile(opts.inject, markdown);
485
+ if (modified) {
486
+ process.stderr.write(`Synced to ${opts.inject} (${target})\n`);
487
+ }
488
+ else {
489
+ process.stderr.write(`No changes needed in ${opts.inject}\n`);
490
+ }
556
491
  }
557
492
  trackSyncOutput(trackedOutputs, "markdown-injection", opts.inject);
558
493
  }
@@ -563,17 +498,35 @@ export async function runSync(opts) {
563
498
  const cascadedForIR = cascadeDNA(dnas);
564
499
  const roles = cascadedForIR.roles;
565
500
  const compiled = createCompiledIR(ir, roles);
566
- const irPath = await writeCompiledIR(cwd, compiled);
567
- process.stderr.write(`Wrote compiled IR: ${irPath}\n`);
501
+ const irPath = resolve(cwd, ".dna", "compiled", "ir.json");
502
+ if (opts.dryRun) {
503
+ JSON.stringify(compiled);
504
+ process.stderr.write(`Dry-run: would write compiled IR: ${irPath}\n`);
505
+ }
506
+ else {
507
+ const writtenIrPath = await writeCompiledIR(cwd, compiled);
508
+ process.stderr.write(`Wrote compiled IR: ${writtenIrPath}\n`);
509
+ }
568
510
  trackSyncOutput(trackedOutputs, "compiled-ir", irPath);
569
511
  // Clean up old bash hooks if they exist
570
512
  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`);
513
+ if (opts.dryRun) {
514
+ process.stderr.write(`Dry-run: would clean legacy bash hooks in ${oldHooksDir}\n`);
515
+ }
516
+ else {
517
+ const removed = await removeLegacyBashHooks(oldHooksDir);
518
+ if (removed > 0) {
519
+ process.stderr.write(`Cleaned ${removed} legacy bash hook(s) from ${oldHooksDir}\n`);
520
+ }
574
521
  }
575
522
  // G2/G3: Register built-in DNA MCP server in .claude/.mcp.json
576
- const mcpPath = await registerDNAMCPServer(cwd);
523
+ const mcpPath = resolve(cwd, ".claude", ".mcp.json");
524
+ if (opts.dryRun) {
525
+ process.stderr.write(`Dry-run: would register dna-mcp server in ${mcpPath}\n`);
526
+ }
527
+ else {
528
+ await registerDNAMCPServer(cwd);
529
+ }
577
530
  trackSyncOutput(trackedOutputs, "mcp-config", mcpPath);
578
531
  }
579
532
  // Step 3.6: Register hooks in settings.json (bin mode only)
@@ -582,8 +535,14 @@ export async function runSync(opts) {
582
535
  const enabledEvents = detectEnabledEvents(ir);
583
536
  if (enabledEvents.length > 0) {
584
537
  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`);
538
+ if (opts.dryRun) {
539
+ JSON.stringify(pluginSettings);
540
+ process.stderr.write(`Dry-run: would register ${enabledEvents.length} hook event(s) in ${opts.settingsPath}\n`);
541
+ }
542
+ else {
543
+ await mergeSettingsFile(pluginSettings, opts.settingsPath);
544
+ process.stderr.write(`Bin mode: registered ${enabledEvents.length} hook event(s) in ${opts.settingsPath}\n`);
545
+ }
587
546
  trackSyncOutput(trackedOutputs, "settings-hooks", opts.settingsPath);
588
547
  }
589
548
  }
@@ -597,9 +556,14 @@ export async function runSync(opts) {
597
556
  // Step 5: Generate agent MD files
598
557
  if (opts.agentsDir) {
599
558
  // 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`);
559
+ if (opts.dryRun) {
560
+ process.stderr.write(`Dry-run: would clean stale agent file(s) in ${opts.agentsDir}\n`);
561
+ }
562
+ else {
563
+ const staleAgents = await cleanStaleDNAFiles(opts.agentsDir, "agents", activeNamespaces);
564
+ if (staleAgents.length > 0) {
565
+ process.stderr.write(`Cleaned ${staleAgents.length} stale agent file(s)\n`);
566
+ }
603
567
  }
604
568
  const dnas = loadedDNAs;
605
569
  const cascaded = cascadeDNA(dnas);
@@ -609,15 +573,19 @@ export async function runSync(opts) {
609
573
  }
610
574
  else {
611
575
  const results = compileAllRolesToAgentMD(roles, ir);
612
- const written = await writeAgentMDFiles(results, opts.agentsDir);
613
- if (written.length > 0) {
576
+ const written = results.map((result) => resolve(opts.agentsDir, result.fileName));
577
+ if (opts.dryRun) {
578
+ process.stderr.write(`Dry-run: would generate ${written.length} agent file(s) in ${opts.agentsDir}\n`);
579
+ }
580
+ else {
581
+ await writeAgentMDFiles(results, opts.agentsDir);
614
582
  process.stderr.write(`Generated ${written.length} agent file(s) in ${opts.agentsDir}\n`);
615
583
  for (const f of written) {
616
584
  process.stderr.write(` ${f}\n`);
617
585
  }
618
- for (const file of written)
619
- trackSyncOutput(trackedOutputs, "agents", file);
620
586
  }
587
+ for (const file of written)
588
+ trackSyncOutput(trackedOutputs, "agents", file);
621
589
  }
622
590
  }
623
591
  // Step 6: Generate workflow script(s)
@@ -629,7 +597,6 @@ export async function runSync(opts) {
629
597
  process.stderr.write("No workflow defined in DNA files\n");
630
598
  }
631
599
  else {
632
- const { compileWorkflow } = await import("../../compiler/workflow.js");
633
600
  const { compileWorkflowToShell, writeWorkflowScript } = await import("../../runtime/workflow-runner.js");
634
601
  // If --workflow-name specified, only compile that workflow; otherwise compile all
635
602
  const targetWorkflows = opts.workflowName
@@ -649,8 +616,14 @@ export async function runSync(opts) {
649
616
  }
650
617
  else {
651
618
  const shellResult = compileWorkflowToShell(result.plan);
652
- const written = await writeWorkflowScript(shellResult, opts.workflowDir);
653
- process.stderr.write(`Generated workflow script: ${written}\n`);
619
+ const written = resolve(opts.workflowDir, shellResult.fileName);
620
+ if (opts.dryRun) {
621
+ process.stderr.write(`Dry-run: would generate workflow script: ${written}\n`);
622
+ }
623
+ else {
624
+ await writeWorkflowScript(shellResult, opts.workflowDir);
625
+ process.stderr.write(`Generated workflow script: ${written}\n`);
626
+ }
654
627
  trackSyncOutput(trackedOutputs, "workflows", written);
655
628
  }
656
629
  }
@@ -659,9 +632,14 @@ export async function runSync(opts) {
659
632
  // Step 7: Generate skill files from workflows
660
633
  if (opts.skillsDir) {
661
634
  // 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`);
635
+ if (opts.dryRun) {
636
+ process.stderr.write(`Dry-run: would clean stale skill dir(s) in ${opts.skillsDir}\n`);
637
+ }
638
+ else {
639
+ const staleSkills = await cleanStaleDNAFiles(opts.skillsDir, "skills", activeNamespaces);
640
+ if (staleSkills.length > 0) {
641
+ process.stderr.write(`Cleaned ${staleSkills.length} stale skill dir(s)\n`);
642
+ }
665
643
  }
666
644
  const dnas = loadedDNAs;
667
645
  const cascadedForSkills = cascadeDNA(dnas);
@@ -675,7 +653,7 @@ export async function runSync(opts) {
675
653
  Object.assign(rawVars, d.variables);
676
654
  }
677
655
  const configPath = opts.files.length === 1 ? opts.files[0] : null;
678
- const variables = await resolveVariables(rawVars, configPath);
656
+ const variables = await resolveVariables(rawVars, configPath, { dryRun: opts.dryRun });
679
657
  // Step 8: Generate .claude/.mcp.json from MCP dependencies (inside skillsDir block, shares variables)
680
658
  {
681
659
  const mcpDeps = {};
@@ -686,15 +664,19 @@ export async function runSync(opts) {
686
664
  if (Object.keys(mcpDeps).length > 0) {
687
665
  const cwd = process.cwd();
688
666
  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`);
667
+ if (opts.dryRun) {
668
+ process.stderr.write(`Dry-run: would write ${Object.keys(mcpDeps).length} MCP server(s) to ${mcpJsonPath}\n`);
669
+ }
670
+ else {
671
+ await writeMCPConfig(mcpDeps, mcpJsonPath, variables);
672
+ process.stderr.write(`MCP: wrote ${Object.keys(mcpDeps).length} server(s) to ${mcpJsonPath}\n`);
673
+ }
691
674
  }
692
675
  }
693
676
  if (Object.keys(workflows).length === 0 && Object.keys(controllers).length === 0) {
694
677
  process.stderr.write("No workflow or controller defined — no skills to generate\n");
695
678
  }
696
679
  else {
697
- const { compileWorkflow } = await import("../../compiler/workflow.js");
698
680
  const skillResults = [];
699
681
  for (const [name, wfDef] of Object.entries(workflows)) {
700
682
  const result = compileWorkflow(wfDef, { workflow_key: name });
@@ -706,10 +688,24 @@ export async function runSync(opts) {
706
688
  skillResults.push(compileControllerToSkill(name, controllerDef, variables, workflows));
707
689
  }
708
690
  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`);
691
+ let written = skillResults.flatMap((result) => {
692
+ const files = [resolve(opts.skillsDir, result.dirName, "SKILL.md")];
693
+ if (result.bodyFileName)
694
+ files.push(resolve(opts.skillsDir, result.dirName, "skill-bodies", result.bodyFileName));
695
+ return files;
696
+ });
697
+ if (skillResults.some((result) => result.mapEntry)) {
698
+ written.push(resolve(opts.skillsDir, ".intentdna-skill-map.json"));
699
+ }
700
+ if (opts.dryRun) {
701
+ process.stderr.write(`Dry-run: would generate ${written.length} skill artifact(s) in ${opts.skillsDir}\n`);
702
+ }
703
+ else {
704
+ written = await writeSkillFiles(skillResults, opts.skillsDir);
705
+ process.stderr.write(`Generated ${written.length} skill artifact(s) in ${opts.skillsDir}\n`);
706
+ for (const f of written) {
707
+ process.stderr.write(` ${f}\n`);
708
+ }
713
709
  }
714
710
  for (const file of written)
715
711
  trackSyncOutput(trackedOutputs, "skills-controllers", file);
@@ -724,8 +720,17 @@ export async function runSync(opts) {
724
720
  // Step 9: Generate .dna/lock file (only when --lock is specified or outputs exist)
725
721
  if (opts.lockFile && trackedOutputs.length > 0) {
726
722
  const lockPath = resolve(opts.lockFile);
727
- await generateLockFile(lockPath, expandedFiles, lockTrackedOutputPaths(trackedOutputs));
728
- process.stderr.write(`Lock file: ${lockPath}\n`);
723
+ if (opts.dryRun) {
724
+ process.stderr.write(`Dry-run: would write lock file: ${lockPath}\n`);
725
+ }
726
+ else {
727
+ await generateLockFile(lockPath, opts.files, lockTrackedOutputPaths(trackedOutputs), ir.provenance, ir.compiled_ir_hash);
728
+ process.stderr.write(`Lock file: ${lockPath}\n`);
729
+ }
730
+ trackSyncOutput(trackedOutputs, "lock-file", lockPath);
731
+ }
732
+ if (opts.dryRun || opts.postSummary) {
733
+ summarizeSyncOutputs(trackedOutputs, opts.dryRun === true);
729
734
  }
730
735
  return 0;
731
736
  }
@@ -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>;