intentdna 1.5.12 → 1.5.14

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.
@@ -9,7 +9,7 @@
9
9
  {
10
10
  "name": "intentdna",
11
11
  "description": "DNA template compilation + runtime enforcement",
12
- "version": "1.5.12",
12
+ "version": "1.5.14",
13
13
  "source": "./"
14
14
  }
15
15
  ]
@@ -1,5 +1,5 @@
1
1
  {
2
2
  "name": "intentdna",
3
- "version": "1.5.12",
3
+ "version": "1.5.14",
4
4
  "description": "Declarative policy layer for AI agent governance"
5
5
  }
@@ -11,4 +11,8 @@ export interface InitOptions {
11
11
  register?: string;
12
12
  upgrade?: string | boolean;
13
13
  }
14
+ /**
15
+ * Calculate SHA256 hash of template content (truncated to 16 hex chars).
16
+ */
17
+ export declare function calculateTemplateHash(content: string): string;
14
18
  export declare function runInit(opts: InitOptions): Promise<number>;
@@ -6,8 +6,9 @@
6
6
  import { writeFile, readFile, readdir, mkdir, copyFile, access } from "node:fs/promises";
7
7
  import { resolve, dirname, basename } from "node:path";
8
8
  import { fileURLToPath } from "node:url";
9
+ import { createHash } from "node:crypto";
9
10
  import { parseYAML } from "../../schema/yaml-parser.js";
10
- import { getPackageVersion, sanitizeTemplateName } from "../util/version.js";
11
+ import { sanitizeTemplateName } from "../util/version.js";
11
12
  import { validateDNA } from "../../schema/validate.js";
12
13
  import { askString, askChoice, askNumber, askYesNo, closePrompt } from "../util/prompt.js";
13
14
  const TEMPLATES_DIR = resolve(dirname(fileURLToPath(import.meta.url)), "..", "..", "templates");
@@ -65,17 +66,24 @@ async function listTemplates() {
65
66
  return results;
66
67
  }
67
68
  /**
68
- * Inject _source_template and _template_version fields after the namespace/type line in YAML content.
69
+ * Calculate SHA256 hash of template content (truncated to 16 hex chars).
69
70
  */
70
- function injectSourceTemplate(content, templateName, version) {
71
+ export function calculateTemplateHash(content) {
72
+ return createHash("sha256").update(content, "utf-8").digest("hex").slice(0, 16);
73
+ }
74
+ /**
75
+ * Inject _source_template, _template_version, and _template_content_hash fields after the namespace/type line in YAML content.
76
+ */
77
+ function injectSourceTemplate(content, templateName, templateVersion, templateHash) {
71
78
  const marker = /^(namespace:\s*.+)$/m;
72
79
  const typeMarker = /^(type:\s*.+)$/m;
73
80
  const match = content.match(marker) || content.match(typeMarker);
74
- const versionLine = version ? `\n_template_version: "${version}"` : "";
81
+ const versionLine = templateVersion ? `\n_template_version: "${templateVersion}"` : "";
82
+ const hashLine = templateHash ? `\n_template_content_hash: "${templateHash}"` : "";
75
83
  if (match) {
76
- return content.replace(match[0], `${match[0]}\n_source_template: ${templateName}${versionLine}`);
84
+ return content.replace(match[0], `${match[0]}\n_source_template: ${templateName}${versionLine}${hashLine}`);
77
85
  }
78
- return `_source_template: ${templateName}${versionLine}\n${content}`;
86
+ return `_source_template: ${templateName}${versionLine}${hashLine}\n${content}`;
79
87
  }
80
88
  /**
81
89
  * Check namespace collision against existing configs in .dna/configs/.
@@ -164,9 +172,11 @@ async function upgradeConfig(configPath, templateName) {
164
172
  const srcDir = found.source === "project" ? PROJECT_TEMPLATES_DIR : TEMPLATES_DIR;
165
173
  const srcPath = resolve(srcDir, `${found.name}.dna.yaml`);
166
174
  let newContent = await readFile(srcPath, "utf-8");
167
- // 4. Inject _source_template and _template_version
168
- const pkgVersion = await getPackageVersion();
169
- newContent = injectSourceTemplate(newContent, found.name, pkgVersion ?? undefined);
175
+ // 4. Inject _source_template, _template_version, and _template_content_hash
176
+ const templateData = parseYAML(newContent);
177
+ const templateVersion = typeof templateData.version === "string" ? templateData.version : undefined;
178
+ const templateHash = calculateTemplateHash(newContent);
179
+ newContent = injectSourceTemplate(newContent, found.name, templateVersion, templateHash);
170
180
  // 5. Replace VariableDef entries with user values where they exist
171
181
  for (const [key, value] of Object.entries(userVars)) {
172
182
  // Match multi-line VariableDef: "key:\n description: ...\n default: ..."
@@ -290,8 +300,11 @@ export async function runInit(opts) {
290
300
  const srcPath = resolve(srcDir, `${found.name}.dna.yaml`);
291
301
  const content = await readFile(srcPath, "utf-8");
292
302
  // Inject _source_template and _template_version metadata for upgrade tracking
293
- const pkgVersion = await getPackageVersion();
294
- const contentWithSource = injectSourceTemplate(content, found.name, pkgVersion ?? undefined);
303
+ // Inject _source_template, _template_version, and _template_content_hash for upgrade tracking
304
+ const templateData = parseYAML(content);
305
+ const templateVersion = typeof templateData.version === "string" ? templateData.version : undefined;
306
+ const templateHash = calculateTemplateHash(content);
307
+ const contentWithSource = injectSourceTemplate(content, found.name, templateVersion, templateHash);
295
308
  // Check namespace collision against existing configs
296
309
  const srcData = parseYAML(content);
297
310
  const srcNs = typeof srcData.namespace === "string" ? srcData.namespace : undefined;
@@ -40,6 +40,12 @@ export interface SyncOptions {
40
40
  * → ALSO write settings.json to register dna-hook events
41
41
  */
42
42
  export declare function detectMode(): Promise<"plugin" | "bin">;
43
+ /**
44
+ * Remove stale DNA-managed files from a directory.
45
+ * Only removes files matching activeNamespaces (dna-{ns}-*).
46
+ * Returns list of removed paths.
47
+ */
48
+ export declare function cleanStaleDNAFiles(dir: string, type: "agents" | "skills", activeNamespaces: string[]): Promise<string[]>;
43
49
  /**
44
50
  * Auto-detect DNA configs: multi-config (.dna/configs/*.yaml) or legacy single-config.
45
51
  * Returns array of absolute paths.
@@ -70,11 +76,10 @@ export interface TemplateUpgradeInfo {
70
76
  availableVersion: string;
71
77
  }
72
78
  /**
73
- * Check template versions against current package version.
74
- * Returns upgrade info for outdated configs and prints human-readable suggestions.
79
+ * Check template versions against current template content hashes.
80
+ * Returns upgrade info for changed configs and prints human-readable suggestions.
75
81
  *
76
- * U1: Semver-aware comparison only suggests upgrades when package is strictly newer.
77
- * Handles missing _template_version (old configs created before versioning).
82
+ * Uses content hash comparison instead of semver detects any template content change.
78
83
  */
79
84
  export declare function checkTemplateVersions(configPaths: string[]): Promise<TemplateUpgradeInfo[]>;
80
85
  export declare function runSync(opts: SyncOptions): Promise<number>;
@@ -17,7 +17,6 @@ import { fileURLToPath } from "node:url";
17
17
  import { createInterface } from "node:readline";
18
18
  import { loadDNA, compileFromFiles } from "../../compiler/index.js";
19
19
  import { parseYAML } from "../../schema/yaml-parser.js";
20
- import { getPackageVersion } from "../util/version.js";
21
20
  import { compileToMarkdown, injectIntoFile, removeFromFile } from "../../runtime/markdown.js";
22
21
  import { compileAllRolesToAgentMD, writeAgentMDFiles, removeAgentMDFiles } from "../../runtime/agent-md.js";
23
22
  import { removeWorkflowScripts } from "../../runtime/workflow-runner.js";
@@ -72,6 +71,32 @@ const LEGACY_BASH_HOOKS = [
72
71
  "dna-pre-compact.sh",
73
72
  "dna-notification.sh",
74
73
  ];
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
+ }
75
100
  /**
76
101
  * Remove legacy bash hook scripts written by previous Intent DNA versions.
77
102
  * Only removes files that contain the sentinel comment.
@@ -93,15 +118,22 @@ async function removeLegacyBashHooks(hooksDir) {
93
118
  }
94
119
  /**
95
120
  * Remove stale DNA-managed files from a directory.
96
- * Scans for files/dirs containing the sentinel marker and deletes them.
121
+ * Only removes files matching activeNamespaces (dna-{ns}-*).
97
122
  * Returns list of removed paths.
98
123
  */
99
- async function cleanStaleDNAFiles(dir, type) {
124
+ export async function cleanStaleDNAFiles(dir, type, activeNamespaces) {
100
125
  const removed = [];
101
126
  try {
102
127
  const entries = await readdir(dir, { withFileTypes: true });
103
128
  for (const entry of entries) {
104
129
  const fullPath = resolve(dir, entry.name);
130
+ // Extract namespace from filename: dna-{namespace}-*
131
+ const nsMatch = entry.name.match(/^dna-([a-z0-9_-]+)-/);
132
+ const fileNamespace = nsMatch ? nsMatch[1] : null;
133
+ // Skip if namespace doesn't match active namespaces
134
+ if (!fileNamespace || !activeNamespaces.includes(fileNamespace)) {
135
+ continue;
136
+ }
105
137
  if (type === "agents" && entry.isFile() && entry.name.endsWith(".md")) {
106
138
  const content = await readFile(fullPath, "utf-8").catch(() => "");
107
139
  if (content.includes(DNA_SENTINEL)) {
@@ -276,42 +308,44 @@ export function compareSemver(a, b) {
276
308
  return 0;
277
309
  }
278
310
  /**
279
- * Check template versions against current package version.
280
- * Returns upgrade info for outdated configs and prints human-readable suggestions.
311
+ * Check template versions against current template content hashes.
312
+ * Returns upgrade info for changed configs and prints human-readable suggestions.
281
313
  *
282
- * U1: Semver-aware comparison only suggests upgrades when package is strictly newer.
283
- * Handles missing _template_version (old configs created before versioning).
314
+ * Uses content hash comparison instead of semver detects any template content change.
284
315
  */
285
316
  export async function checkTemplateVersions(configPaths) {
286
- const pkgVersion = await getPackageVersion();
287
- if (!pkgVersion)
288
- return [];
289
317
  const upgrades = [];
290
318
  for (const configPath of configPaths) {
291
319
  try {
292
320
  const content = await readFile(configPath, "utf-8");
293
321
  const data = parseYAML(content);
294
- const tmplVersion = typeof data._template_version === "string" ? data._template_version : undefined;
295
322
  const tmplName = typeof data._source_template === "string" ? data._source_template : undefined;
296
- if (tmplName && !tmplVersion) {
297
- // Old config without version tracking suggest upgrade
323
+ const tmplHash = typeof data._template_content_hash === "string" ? data._template_content_hash : undefined;
324
+ const tmplVersion = typeof data._template_version === "string" ? data._template_version : undefined;
325
+ if (!tmplName)
326
+ continue;
327
+ const templatePath = await findTemplateFile(tmplName);
328
+ if (!templatePath)
329
+ continue;
330
+ const currentTemplateContent = await readFile(templatePath, "utf-8");
331
+ const currentHash = calculateTemplateHash(currentTemplateContent);
332
+ if (!tmplHash) {
298
333
  upgrades.push({
299
334
  configPath,
300
335
  templateName: tmplName,
301
- currentVersion: "unknown",
302
- availableVersion: pkgVersion,
336
+ currentVersion: tmplVersion ?? "unknown",
337
+ availableVersion: "content-changed",
303
338
  });
304
- process.stderr.write(`Upgrade available: "${tmplName}" has no version tag. Run: dna init --upgrade ${tmplName}\n`);
339
+ process.stderr.write(`Upgrade available: "${tmplName}" has no content hash. Run: dna init --upgrade ${tmplName}\n`);
305
340
  }
306
- else if (tmplVersion && tmplName && compareSemver(pkgVersion, tmplVersion) > 0) {
307
- // Package is strictly newer than template — suggest upgrade
341
+ else if (currentHash !== tmplHash) {
308
342
  upgrades.push({
309
343
  configPath,
310
344
  templateName: tmplName,
311
- currentVersion: tmplVersion,
312
- availableVersion: pkgVersion,
345
+ currentVersion: tmplVersion ?? "unknown",
346
+ availableVersion: "content-changed",
313
347
  });
314
- process.stderr.write(`Upgrade available: "${tmplName}" ${tmplVersion} → ${pkgVersion}. Run: dna init --upgrade ${tmplName}\n`);
348
+ process.stderr.write(`Template content changed: "${tmplName}". Run: dna init --upgrade ${tmplName}\n`);
315
349
  }
316
350
  }
317
351
  catch {
@@ -544,10 +578,14 @@ export async function runSync(opts) {
544
578
  else if (opts.plugin && opts.settingsPath) {
545
579
  process.stderr.write(`Plugin mode: hooks managed by plugin framework (skipping settings.json)\n`);
546
580
  }
581
+ // Extract active namespaces from loaded DNAs for namespace-scoped cleanup
582
+ const activeNamespaces = loadedDNAs
583
+ .map(d => d.namespace)
584
+ .filter((ns) => typeof ns === "string");
547
585
  // Step 5: Generate agent MD files
548
586
  if (opts.agentsDir) {
549
587
  // Clean stale DNA-managed agents before regenerating
550
- const staleAgents = await cleanStaleDNAFiles(opts.agentsDir, "agents");
588
+ const staleAgents = await cleanStaleDNAFiles(opts.agentsDir, "agents", activeNamespaces);
551
589
  if (staleAgents.length > 0) {
552
590
  process.stderr.write(`Cleaned ${staleAgents.length} stale agent file(s)\n`);
553
591
  }
@@ -608,7 +646,7 @@ export async function runSync(opts) {
608
646
  // Step 7: Generate skill files from workflows
609
647
  if (opts.skillsDir) {
610
648
  // Clean stale DNA-managed skills before regenerating
611
- const staleSkills = await cleanStaleDNAFiles(opts.skillsDir, "skills");
649
+ const staleSkills = await cleanStaleDNAFiles(opts.skillsDir, "skills", activeNamespaces);
612
650
  if (staleSkills.length > 0) {
613
651
  process.stderr.write(`Cleaned ${staleSkills.length} stale skill dir(s)\n`);
614
652
  }
@@ -201,6 +201,7 @@ roles:
201
201
  read: ["**/*"]
202
202
  write: ["lib/**", "v2/**", "test/**"]
203
203
  instructions:
204
+ - "REQUIRED FIRST: Read context files — CLAUDE.md, docs/refactoring-workflow-v2.md, v2/docs/PROVIDER_DESIGN.md, and the diagnosis spec (.omc/specs/diagnosis-$ARGUMENTS.md). Then read v1 corresponding file before any Edit. Hook will block Edit if context files are not read."
204
205
  - Fix only the identified breakpoint or missing implementation
205
206
  - Read v1 to understand intent, rewrite in v2 framework style
206
207
  - Do not copy v1 code verbatim — adapt to v2 architecture
@@ -316,19 +317,17 @@ workflows:
316
317
  Scenario 2 (re-run/incremental):
317
318
  - Read the incremental diff from behavior doc
318
319
  - Update existing tests incrementally — do NOT rewrite all tests (preserves rescue progress)
319
- - Run ALL tests for this module (do NOT skip previously green tests)
320
- - Compare with last baseline:
321
- GREEN→RED = REGRESSION (mark HIGH_PRIORITY)
322
- RED→GREEN = FIXED
323
- NEW tests = new coverage
324
- REMOVED tests = coverage shrunk
325
- - Update baseline with full result
320
+ - Run ALL tests (do NOT skip previously green tests). Compare with previous baseline: GREEN→RED = REGRESSION (mark HIGH_PRIORITY), RED→GREEN = FIXED, NEW = new coverage. Update baseline with complete results.
326
321
  - Git commit: "behavior-lock($ARGUMENTS): N tests (X green, Y red from behavior change)"
327
322
 
328
323
  BANNED patterns:
329
324
  - Do NOT use `sleep N && check` polling — run commands in foreground
330
325
  - Do NOT use `timeout Nm flutter test` — let tests run to completion
331
326
  - Do NOT rewrite existing test files from scratch — update incrementally
327
+
328
+ Report results and STOP.
329
+
330
+ Next step for user: Run /dna-frw-diagnosis $ARGUMENTS to analyze failing tests.
332
331
  handoff:
333
332
  consumes:
334
333
  - type: file
@@ -573,6 +572,8 @@ workflows:
573
572
  blocked_items_path: "docs/behavior/blocked_items.md"
574
573
  description: "Fix issues according to diagnosis spec classifications"
575
574
  prompt: |
575
+ Read .dna/state/workflow/experience.md first if it exists. It contains previous failed approaches — do NOT repeat them.
576
+
576
577
  Read context + diagnosis spec first.
577
578
 
578
579
  Process issues in priority order: INFRA → BUG → TEST_BUG
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "intentdna",
3
- "version": "1.5.12",
3
+ "version": "1.5.14",
4
4
  "description": "Intent DNA — Declarative policy layer for AI agent behavior",
5
5
  "type": "module",
6
6
  "main": "dist/index.js",