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.
- package/.claude-plugin/marketplace.json +2 -2
- package/.claude-plugin/plugin.json +1 -1
- package/dist/cli/commands/compile.js +2 -47
- package/dist/cli/commands/context.d.ts +8 -0
- package/dist/cli/commands/context.js +63 -0
- package/dist/cli/commands/feedback.d.ts +1 -0
- package/dist/cli/commands/feedback.js +11 -0
- package/dist/cli/commands/init.js +11 -63
- package/dist/cli/commands/run.js +5 -4
- package/dist/cli/commands/show.js +2 -38
- package/dist/cli/commands/sync.d.ts +9 -6
- package/dist/cli/commands/sync.js +186 -184
- package/dist/cli/commands/templates.d.ts +10 -1
- package/dist/cli/commands/templates.js +50 -1
- package/dist/cli/commands/validate.js +15 -9
- package/dist/cli/commands/verify.js +97 -25
- package/dist/cli/index.js +76 -11
- package/dist/compiler/cascade.d.ts +3 -1
- package/dist/compiler/cascade.js +51 -0
- package/dist/compiler/compile.js +37 -0
- package/dist/compiler/diagnostics.d.ts +17 -0
- package/dist/compiler/diagnostics.js +30 -0
- package/dist/compiler/index.d.ts +3 -0
- package/dist/compiler/index.js +8 -11
- package/dist/compiler/input-resolver.d.ts +25 -0
- package/dist/compiler/input-resolver.js +175 -0
- package/dist/hooks/cli.d.ts +10 -1
- package/dist/hooks/cli.js +37 -22
- package/dist/hooks/state.d.ts +2 -0
- package/dist/hooks/state.js +23 -2
- package/dist/mcp/index.js +2 -0
- package/dist/mcp/tools-compile.js +18 -49
- package/dist/mcp/tools-context.d.ts +2 -0
- package/dist/mcp/tools-context.js +85 -0
- package/dist/mcp/tools-enforce.d.ts +2 -2
- package/dist/mcp/tools-enforce.js +19 -49
- package/dist/mcp/tools-observability.js +24 -0
- package/dist/report/kernel-signals.js +3 -0
- package/dist/report/report-package.d.ts +56 -0
- package/dist/report/report-package.js +85 -0
- package/dist/runtime/agent-md.d.ts +1 -0
- package/dist/runtime/agent-md.js +21 -3
- package/dist/runtime/context-sources.d.ts +14 -0
- package/dist/runtime/context-sources.js +60 -0
- package/dist/runtime/skill-adapter.d.ts +32 -4
- package/dist/runtime/skill-adapter.js +184 -9
- package/dist/runtime/workflow-runner.d.ts +1 -1
- package/dist/runtime/workflow-runner.js +1 -1
- package/dist/schema/types.d.ts +33 -0
- package/dist/schema/validate.js +156 -2
- package/dist/schema/validators/controllers.js +16 -0
- package/dist/signals/index.d.ts +10 -0
- package/dist/signals/index.js +90 -5
- package/dist/templates/catalog.d.ts +19 -0
- package/dist/templates/catalog.js +57 -0
- package/dist/templates/flutter-rewrite.dna.yaml +2 -2
- package/package.json +1 -1
- 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
|
|
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 {
|
|
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
|
-
|
|
133
|
-
|
|
134
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
312
|
-
*
|
|
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
|
|
328
|
-
if (!
|
|
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
|
-
|
|
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 (
|
|
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
|
|
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
|
-
|
|
455
|
-
|
|
456
|
-
|
|
457
|
-
|
|
458
|
-
|
|
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 --
|
|
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
|
|
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
|
-
|
|
550
|
-
|
|
551
|
-
|
|
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
|
-
|
|
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 =
|
|
567
|
-
|
|
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
|
-
|
|
572
|
-
|
|
573
|
-
|
|
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 =
|
|
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
|
-
|
|
586
|
-
|
|
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
|
-
|
|
601
|
-
|
|
602
|
-
|
|
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 =
|
|
613
|
-
if (
|
|
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 =
|
|
653
|
-
|
|
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
|
-
|
|
663
|
-
|
|
664
|
-
|
|
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
|
-
|
|
690
|
-
|
|
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
|
-
|
|
710
|
-
|
|
711
|
-
|
|
712
|
-
|
|
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
|
-
|
|
728
|
-
|
|
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
|
|
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>;
|