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