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.
- package/.claude-plugin/marketplace.json +2 -2
- package/.claude-plugin/plugin.json +1 -1
- package/README.md +26 -15
- 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 +3 -2
- package/dist/cli/commands/feedback.js +16 -5
- 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 +191 -186
- 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.d.ts +32 -0
- package/dist/cli/commands/verify.js +270 -31
- package/dist/cli/index.js +78 -11
- package/dist/compiler/activate.js +11 -6
- package/dist/compiler/cascade.d.ts +5 -1
- package/dist/compiler/cascade.js +74 -1
- package/dist/compiler/compile.js +38 -0
- package/dist/compiler/diagnostics.d.ts +17 -0
- package/dist/compiler/diagnostics.js +30 -0
- package/dist/compiler/index.d.ts +7 -0
- package/dist/compiler/index.js +13 -13
- package/dist/compiler/input-resolver.d.ts +32 -0
- package/dist/compiler/input-resolver.js +281 -0
- package/dist/compiler/provenance.d.ts +8 -0
- package/dist/compiler/provenance.js +127 -0
- package/dist/governance/index.d.ts +4 -3
- package/dist/governance/index.js +3 -4
- package/dist/governance/runtime-decision-event.d.ts +85 -0
- package/dist/governance/runtime-decision-event.js +231 -0
- package/dist/governance/types.d.ts +3 -3
- package/dist/governance/types.js +3 -3
- package/dist/hooks/cli.d.ts +10 -1
- package/dist/hooks/cli.js +199 -35
- package/dist/hooks/state.d.ts +5 -10
- package/dist/hooks/state.js +170 -2
- package/dist/index.d.ts +1 -0
- package/dist/index.js +1 -0
- 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 +26 -2
- package/dist/mcp/tools-state.d.ts +1 -1
- package/dist/mcp/tools-state.js +17 -7
- package/dist/report/kernel-report.d.ts +27 -0
- package/dist/report/kernel-report.js +60 -19
- package/dist/report/kernel-signals.d.ts +5 -2
- package/dist/report/kernel-signals.js +87 -2
- package/dist/report/report-package.d.ts +64 -0
- package/dist/report/report-package.js +90 -0
- package/dist/runtime/agent-md.d.ts +1 -0
- package/dist/runtime/agent-md.js +21 -3
- package/dist/runtime/claude-sdk.d.ts +9 -4
- package/dist/runtime/claude-sdk.js +9 -0
- package/dist/runtime/context-sources.d.ts +14 -0
- package/dist/runtime/context-sources.js +60 -0
- package/dist/runtime/plugin-adapter.d.ts +5 -1
- package/dist/runtime/plugin-adapter.js +2 -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 +84 -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/README.md +1 -1
- package/spec/foundation-hardening.md +2 -1
- 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
|
|
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, 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
|
-
|
|
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,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
|
|
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
|
-
|
|
455
|
-
|
|
456
|
-
|
|
457
|
-
|
|
458
|
-
|
|
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 --
|
|
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
|
|
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(
|
|
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
|
-
|
|
550
|
-
|
|
551
|
-
|
|
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
|
-
|
|
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 =
|
|
567
|
-
|
|
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
|
-
|
|
572
|
-
|
|
573
|
-
|
|
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 =
|
|
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
|
-
|
|
586
|
-
|
|
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
|
-
|
|
601
|
-
|
|
602
|
-
|
|
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 =
|
|
613
|
-
if (
|
|
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 =
|
|
653
|
-
|
|
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
|
-
|
|
663
|
-
|
|
664
|
-
|
|
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
|
-
|
|
690
|
-
|
|
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
|
-
|
|
710
|
-
|
|
711
|
-
|
|
712
|
-
|
|
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
|
-
|
|
728
|
-
|
|
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
|
|
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>;
|