intentdna 1.5.13 → 1.5.15
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/dist/cli/commands/init.d.ts +4 -0
- package/dist/cli/commands/init.js +24 -11
- package/dist/cli/commands/sync.d.ts +9 -4
- package/dist/cli/commands/sync.js +61 -23
- package/dist/hooks/cli.d.ts +30 -0
- package/dist/hooks/cli.js +168 -59
- package/dist/hooks/enforce.d.ts +13 -12
- package/dist/hooks/enforce.js +61 -46
- package/dist/hooks/protocol.d.ts +11 -0
- package/dist/hooks/schema.d.ts +22 -0
- package/dist/hooks/schema.js +86 -0
- package/dist/hooks/state-manager.d.ts +48 -0
- package/dist/hooks/state-manager.js +135 -0
- package/dist/hooks/state.d.ts +8 -0
- package/dist/hooks/state.js +8 -0
- package/dist/mcp/tools-enforce.js +6 -2
- package/dist/templates/flutter-rewrite.dna.yaml +22 -17
- package/package.json +3 -2
- package/spec/hooks-infra-harness-hardening.md +72 -26
- package/spec/template-version-namespace-fix.md +653 -0
- package/.claude-plugin/hooks/hooks.json +0 -12
- package/.claude-plugin/marketplace.json +0 -16
- package/.claude-plugin/plugin.json +0 -5
- package/LICENSE +0 -69
|
@@ -0,0 +1,653 @@
|
|
|
1
|
+
# Template Version + Namespace Cleanup Fix
|
|
2
|
+
|
|
3
|
+
## Metadata
|
|
4
|
+
- Generated: 2026-04-19
|
|
5
|
+
- Status: APPROVED
|
|
6
|
+
- 预计工期: 2-3 小时
|
|
7
|
+
- 发版类型: patch
|
|
8
|
+
|
|
9
|
+
## 背景
|
|
10
|
+
|
|
11
|
+
两个独立 bug 需要一起修:
|
|
12
|
+
|
|
13
|
+
### Bug 1: _template_version 跟 package 版本走
|
|
14
|
+
**问题**: 改了 flutter-rewrite 模板 → 发版 1.5.13 → 所有模板的 _template_version 都变成 1.5.13,但只有 flutter-rewrite 真正改了内容。
|
|
15
|
+
|
|
16
|
+
**根因**: `injectSourceTemplate()` 用 `getPackageVersion()` 而不是模板自身的 `version` 字段。
|
|
17
|
+
|
|
18
|
+
### Bug 2: cleanStaleDNAFiles 不区分 namespace
|
|
19
|
+
**问题**: 项目用 frw + be 两个模板,`dna sync frw` 会把 be 的 agent/skill 也删掉。
|
|
20
|
+
|
|
21
|
+
**根因**: `cleanStaleDNAFiles()` 只看 `intentdna:managed` 标记,不看 namespace。
|
|
22
|
+
|
|
23
|
+
---
|
|
24
|
+
|
|
25
|
+
## 方案
|
|
26
|
+
|
|
27
|
+
### 方案 A: Template Content Hash Tracking
|
|
28
|
+
|
|
29
|
+
每个模板用 content hash 追踪版本,不依赖手动 bump version 字段。
|
|
30
|
+
|
|
31
|
+
**流程**:
|
|
32
|
+
1. `dna init <template>` 时:
|
|
33
|
+
- 计算模板文件内容的 SHA256 hash
|
|
34
|
+
- 写入项目 config: `_template_content_hash: "abc123..."`
|
|
35
|
+
- 保留 `_template_version` 字段(从模板的 `version` 读取,不是 pkg 版本)
|
|
36
|
+
|
|
37
|
+
2. `dna sync` 时:
|
|
38
|
+
- 读当前模板文件,计算 hash
|
|
39
|
+
- 对比项目 config 的 `_template_content_hash`
|
|
40
|
+
- 不同 → 提示 "Template content changed. Run: dna init --upgrade <template>"
|
|
41
|
+
|
|
42
|
+
3. `dna init --upgrade <template>` 时:
|
|
43
|
+
- 更新 `_template_content_hash` 为新 hash
|
|
44
|
+
- 更新 `_template_version` 为模板的 `version` 字段
|
|
45
|
+
|
|
46
|
+
**优点**:
|
|
47
|
+
- 不依赖手动 bump version
|
|
48
|
+
- 任何模板内容变化都能检测到
|
|
49
|
+
- 不同模板独立追踪
|
|
50
|
+
|
|
51
|
+
**缺点**:
|
|
52
|
+
- 用户手动改了项目 config(加自定义 gene)也会触发"模板变化"提示
|
|
53
|
+
- 需要明确告知用户:项目 config 是"模板实例",改动后不再追踪上游模板更新
|
|
54
|
+
|
|
55
|
+
### 方案 B: Namespace-aware Cleanup
|
|
56
|
+
|
|
57
|
+
`cleanStaleDNAFiles()` 只删当前 sync 的 namespace 对应的文件。
|
|
58
|
+
|
|
59
|
+
**实现**:
|
|
60
|
+
```typescript
|
|
61
|
+
// 当前
|
|
62
|
+
async function cleanStaleDNAFiles(dir: string, type: "agent" | "skill"): Promise<number>
|
|
63
|
+
|
|
64
|
+
// 修改后
|
|
65
|
+
async function cleanStaleDNAFiles(
|
|
66
|
+
dir: string,
|
|
67
|
+
type: "agent" | "skill",
|
|
68
|
+
activeNamespaces: string[] // 当前 sync 的模板的 namespace 列表
|
|
69
|
+
): Promise<number>
|
|
70
|
+
```
|
|
71
|
+
|
|
72
|
+
**逻辑**:
|
|
73
|
+
1. 扫描 `.claude/agents/` 或 `.claude/skills/`
|
|
74
|
+
2. 对每个文件/目录:
|
|
75
|
+
- 读内容检查 `intentdna:managed` 标记
|
|
76
|
+
- 如果有标记 → 提取文件名前缀(`dna-frw-*` → `frw`)
|
|
77
|
+
- 如果前缀在 `activeNamespaces` 中 → 删除
|
|
78
|
+
- 否则保留
|
|
79
|
+
|
|
80
|
+
**文件名规则**:
|
|
81
|
+
- Agent MD: `dna-{namespace}-{role}.md`
|
|
82
|
+
- Skill dir: `dna-{namespace}-{workflow}/`
|
|
83
|
+
|
|
84
|
+
**边界情况**:
|
|
85
|
+
- 文件名不符合 `dna-{ns}-*` 格式 → 保留(可能是用户手写的)
|
|
86
|
+
- 没有 `intentdna:managed` 标记 → 保留
|
|
87
|
+
- namespace 不在 activeNamespaces → 保留
|
|
88
|
+
|
|
89
|
+
---
|
|
90
|
+
|
|
91
|
+
## 实施步骤
|
|
92
|
+
|
|
93
|
+
### Step 1: Template Content Hash (Bug 1)
|
|
94
|
+
|
|
95
|
+
#### 1.1 新增 hash 计算函数
|
|
96
|
+
|
|
97
|
+
文件:`src/cli/commands/init.ts`
|
|
98
|
+
|
|
99
|
+
```typescript
|
|
100
|
+
import { createHash } from "crypto";
|
|
101
|
+
|
|
102
|
+
/**
|
|
103
|
+
* Calculate SHA256 hash of template content.
|
|
104
|
+
* Used for tracking template changes independent of version field.
|
|
105
|
+
*/
|
|
106
|
+
function calculateTemplateHash(content: string): string {
|
|
107
|
+
return createHash("sha256").update(content, "utf-8").digest("hex").slice(0, 16);
|
|
108
|
+
}
|
|
109
|
+
```
|
|
110
|
+
|
|
111
|
+
#### 1.2 修改 injectSourceTemplate
|
|
112
|
+
|
|
113
|
+
文件:`src/cli/commands/init.ts:91-100`
|
|
114
|
+
|
|
115
|
+
```typescript
|
|
116
|
+
// 之前
|
|
117
|
+
function injectSourceTemplate(content: string, templateName: string, version?: string): string {
|
|
118
|
+
const marker = /^(namespace:\s*.+)$/m;
|
|
119
|
+
const typeMarker = /^(type:\s*.+)$/m;
|
|
120
|
+
const match = content.match(marker) || content.match(typeMarker);
|
|
121
|
+
const versionLine = version ? `\n_template_version: "${version}"` : "";
|
|
122
|
+
if (match) {
|
|
123
|
+
return content.replace(match[0], `${match[0]}\n_source_template: ${templateName}${versionLine}`);
|
|
124
|
+
}
|
|
125
|
+
return `_source_template: ${templateName}${versionLine}\n${content}`;
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
// 之后
|
|
129
|
+
function injectSourceTemplate(
|
|
130
|
+
content: string,
|
|
131
|
+
templateName: string,
|
|
132
|
+
templateVersion?: string,
|
|
133
|
+
templateHash?: string
|
|
134
|
+
): string {
|
|
135
|
+
const marker = /^(namespace:\s*.+)$/m;
|
|
136
|
+
const typeMarker = /^(type:\s*.+)$/m;
|
|
137
|
+
const match = content.match(marker) || content.match(typeMarker);
|
|
138
|
+
|
|
139
|
+
const versionLine = templateVersion ? `\n_template_version: "${templateVersion}"` : "";
|
|
140
|
+
const hashLine = templateHash ? `\n_template_content_hash: "${templateHash}"` : "";
|
|
141
|
+
const metadata = `\n_source_template: ${templateName}${versionLine}${hashLine}`;
|
|
142
|
+
|
|
143
|
+
if (match) {
|
|
144
|
+
return content.replace(match[0], `${match[0]}${metadata}`);
|
|
145
|
+
}
|
|
146
|
+
return `_source_template: ${templateName}${versionLine}${hashLine}\n${content}`;
|
|
147
|
+
}
|
|
148
|
+
```
|
|
149
|
+
|
|
150
|
+
#### 1.3 修改 init 命令调用点
|
|
151
|
+
|
|
152
|
+
文件:`src/cli/commands/init.ts`
|
|
153
|
+
|
|
154
|
+
找到两处 `injectSourceTemplate()` 调用:
|
|
155
|
+
|
|
156
|
+
**调用点 1(约 194 行,--upgrade 路径)**:
|
|
157
|
+
```typescript
|
|
158
|
+
// 之前
|
|
159
|
+
const pkgVersion = await getPackageVersion();
|
|
160
|
+
newContent = injectSourceTemplate(newContent, found.name, pkgVersion ?? undefined);
|
|
161
|
+
|
|
162
|
+
// 之后
|
|
163
|
+
const templateData = parseYAML(newContent) as Record<string, unknown>;
|
|
164
|
+
const templateVersion = typeof templateData.version === "string" ? templateData.version : undefined;
|
|
165
|
+
const templateHash = calculateTemplateHash(newContent);
|
|
166
|
+
newContent = injectSourceTemplate(newContent, found.name, templateVersion, templateHash);
|
|
167
|
+
```
|
|
168
|
+
|
|
169
|
+
**调用点 2(约 340 行,新建路径)**:
|
|
170
|
+
```typescript
|
|
171
|
+
// 之前
|
|
172
|
+
const pkgVersion = await getPackageVersion();
|
|
173
|
+
const contentWithSource = injectSourceTemplate(content, found.name, pkgVersion ?? undefined);
|
|
174
|
+
|
|
175
|
+
// 之后
|
|
176
|
+
const templateData = parseYAML(content) as Record<string, unknown>;
|
|
177
|
+
const templateVersion = typeof templateData.version === "string" ? templateData.version : undefined;
|
|
178
|
+
const templateHash = calculateTemplateHash(content);
|
|
179
|
+
const contentWithSource = injectSourceTemplate(content, found.name, templateVersion, templateHash);
|
|
180
|
+
```
|
|
181
|
+
|
|
182
|
+
#### 1.4 修改 checkTemplateVersions
|
|
183
|
+
|
|
184
|
+
文件:`src/cli/commands/sync.ts:334-370`
|
|
185
|
+
|
|
186
|
+
```typescript
|
|
187
|
+
// 之前
|
|
188
|
+
export async function checkTemplateVersions(configPaths: string[]): Promise<TemplateUpgradeInfo[]> {
|
|
189
|
+
const pkgVersion = await getPackageVersion();
|
|
190
|
+
if (!pkgVersion) return [];
|
|
191
|
+
|
|
192
|
+
const upgrades: TemplateUpgradeInfo[] = [];
|
|
193
|
+
|
|
194
|
+
for (const configPath of configPaths) {
|
|
195
|
+
try {
|
|
196
|
+
const content = await readFile(configPath, "utf-8");
|
|
197
|
+
const data = parseYAML(content) as Record<string, unknown>;
|
|
198
|
+
const tmplVersion = typeof data._template_version === "string" ? data._template_version : undefined;
|
|
199
|
+
const tmplName = typeof data._source_template === "string" ? data._source_template : undefined;
|
|
200
|
+
|
|
201
|
+
if (tmplName && !tmplVersion) {
|
|
202
|
+
// Old config without version tracking — suggest upgrade
|
|
203
|
+
upgrades.push({
|
|
204
|
+
configPath,
|
|
205
|
+
templateName: tmplName,
|
|
206
|
+
currentVersion: "unknown",
|
|
207
|
+
availableVersion: pkgVersion,
|
|
208
|
+
});
|
|
209
|
+
process.stderr.write(
|
|
210
|
+
`Upgrade available: "${tmplName}" has no version tag. Run: dna init --upgrade ${tmplName}\n`,
|
|
211
|
+
);
|
|
212
|
+
} else if (tmplVersion && tmplName && compareSemver(pkgVersion, tmplVersion) > 0) {
|
|
213
|
+
// Package is strictly newer than template — suggest upgrade
|
|
214
|
+
upgrades.push({
|
|
215
|
+
configPath,
|
|
216
|
+
templateName: tmplName,
|
|
217
|
+
currentVersion: tmplVersion,
|
|
218
|
+
availableVersion: pkgVersion,
|
|
219
|
+
});
|
|
220
|
+
process.stderr.write(
|
|
221
|
+
`Upgrade available: "${tmplName}" ${tmplVersion} → ${pkgVersion}. Run: dna init --upgrade ${tmplName}\n`,
|
|
222
|
+
);
|
|
223
|
+
}
|
|
224
|
+
} catch {
|
|
225
|
+
// Fail-open
|
|
226
|
+
}
|
|
227
|
+
}
|
|
228
|
+
|
|
229
|
+
return upgrades;
|
|
230
|
+
}
|
|
231
|
+
|
|
232
|
+
// 之后
|
|
233
|
+
export async function checkTemplateVersions(configPaths: string[]): Promise<TemplateUpgradeInfo[]> {
|
|
234
|
+
const upgrades: TemplateUpgradeInfo[] = [];
|
|
235
|
+
|
|
236
|
+
for (const configPath of configPaths) {
|
|
237
|
+
try {
|
|
238
|
+
const content = await readFile(configPath, "utf-8");
|
|
239
|
+
const data = parseYAML(content) as Record<string, unknown>;
|
|
240
|
+
const tmplName = typeof data._source_template === "string" ? data._source_template : undefined;
|
|
241
|
+
const tmplHash = typeof data._template_content_hash === "string" ? data._template_content_hash : undefined;
|
|
242
|
+
const tmplVersion = typeof data._template_version === "string" ? data._template_version : undefined;
|
|
243
|
+
|
|
244
|
+
if (!tmplName) continue;
|
|
245
|
+
|
|
246
|
+
// Find current template file
|
|
247
|
+
const templatePath = await findTemplateFile(tmplName);
|
|
248
|
+
if (!templatePath) continue;
|
|
249
|
+
|
|
250
|
+
const currentTemplateContent = await readFile(templatePath, "utf-8");
|
|
251
|
+
const currentHash = calculateTemplateHash(currentTemplateContent);
|
|
252
|
+
|
|
253
|
+
if (!tmplHash) {
|
|
254
|
+
// Old config without hash tracking — suggest upgrade
|
|
255
|
+
upgrades.push({
|
|
256
|
+
configPath,
|
|
257
|
+
templateName: tmplName,
|
|
258
|
+
currentVersion: tmplVersion ?? "unknown",
|
|
259
|
+
availableVersion: "content-changed",
|
|
260
|
+
});
|
|
261
|
+
process.stderr.write(
|
|
262
|
+
`Upgrade available: "${tmplName}" has no content hash. Run: dna init --upgrade ${tmplName}\n`,
|
|
263
|
+
);
|
|
264
|
+
} else if (currentHash !== tmplHash) {
|
|
265
|
+
// Template content changed
|
|
266
|
+
upgrades.push({
|
|
267
|
+
configPath,
|
|
268
|
+
templateName: tmplName,
|
|
269
|
+
currentVersion: tmplVersion ?? "unknown",
|
|
270
|
+
availableVersion: "content-changed",
|
|
271
|
+
});
|
|
272
|
+
process.stderr.write(
|
|
273
|
+
`Template content changed: "${tmplName}". Run: dna init --upgrade ${tmplName}\n`,
|
|
274
|
+
);
|
|
275
|
+
}
|
|
276
|
+
} catch {
|
|
277
|
+
// Fail-open
|
|
278
|
+
}
|
|
279
|
+
}
|
|
280
|
+
|
|
281
|
+
return upgrades;
|
|
282
|
+
}
|
|
283
|
+
|
|
284
|
+
/**
|
|
285
|
+
* Find template file path by template name.
|
|
286
|
+
* Searches in src/templates/ directory.
|
|
287
|
+
*/
|
|
288
|
+
async function findTemplateFile(templateName: string): Promise<string | null> {
|
|
289
|
+
const templatesDir = resolve(__dirname, "..", "..", "templates");
|
|
290
|
+
try {
|
|
291
|
+
const files = await readdir(templatesDir);
|
|
292
|
+
for (const file of files) {
|
|
293
|
+
if (file.endsWith(".dna.yaml") || file.endsWith(".dna.yml")) {
|
|
294
|
+
const fullPath = resolve(templatesDir, file);
|
|
295
|
+
const content = await readFile(fullPath, "utf-8");
|
|
296
|
+
const data = parseYAML(content) as Record<string, unknown>;
|
|
297
|
+
const id = typeof data.id === "string" ? data.id : undefined;
|
|
298
|
+
const name = typeof data.name === "string" ? data.name : undefined;
|
|
299
|
+
if (id === templateName || name === templateName) {
|
|
300
|
+
return fullPath;
|
|
301
|
+
}
|
|
302
|
+
}
|
|
303
|
+
}
|
|
304
|
+
} catch {
|
|
305
|
+
// Fail-open
|
|
306
|
+
}
|
|
307
|
+
return null;
|
|
308
|
+
}
|
|
309
|
+
```
|
|
310
|
+
|
|
311
|
+
需要在文件顶部 import:
|
|
312
|
+
```typescript
|
|
313
|
+
import { createHash } from "crypto";
|
|
314
|
+
```
|
|
315
|
+
|
|
316
|
+
并把 `calculateTemplateHash` 函数从 init.ts 移到共享位置或复制一份。
|
|
317
|
+
|
|
318
|
+
---
|
|
319
|
+
|
|
320
|
+
### Step 2: Namespace-aware Cleanup (Bug 2)
|
|
321
|
+
|
|
322
|
+
#### 2.1 修改 cleanStaleDNAFiles 签名
|
|
323
|
+
|
|
324
|
+
文件:`src/cli/commands/sync.ts:126-141`
|
|
325
|
+
|
|
326
|
+
```typescript
|
|
327
|
+
// 之前
|
|
328
|
+
async function cleanStaleDNAFiles(dir: string, type: "agent" | "skill"): Promise<number>
|
|
329
|
+
|
|
330
|
+
// 之后
|
|
331
|
+
async function cleanStaleDNAFiles(
|
|
332
|
+
dir: string,
|
|
333
|
+
type: "agent" | "skill",
|
|
334
|
+
activeNamespaces: string[]
|
|
335
|
+
): Promise<number>
|
|
336
|
+
```
|
|
337
|
+
|
|
338
|
+
#### 2.2 修改 cleanStaleDNAFiles 实现
|
|
339
|
+
|
|
340
|
+
```typescript
|
|
341
|
+
async function cleanStaleDNAFiles(
|
|
342
|
+
dir: string,
|
|
343
|
+
type: "agent" | "skill",
|
|
344
|
+
activeNamespaces: string[]
|
|
345
|
+
): Promise<number> {
|
|
346
|
+
let removed = 0;
|
|
347
|
+
try {
|
|
348
|
+
const entries = await readdir(dir, { withFileTypes: true });
|
|
349
|
+
for (const entry of entries) {
|
|
350
|
+
const fullPath = resolve(dir, entry.name);
|
|
351
|
+
|
|
352
|
+
// Extract namespace from filename
|
|
353
|
+
// Agent: dna-{ns}-{role}.md
|
|
354
|
+
// Skill: dna-{ns}-{workflow}/
|
|
355
|
+
const match = entry.name.match(/^dna-([a-z0-9_-]+)-/);
|
|
356
|
+
if (!match) continue; // Not DNA-managed naming pattern
|
|
357
|
+
|
|
358
|
+
const namespace = match[1];
|
|
359
|
+
if (!activeNamespaces.includes(namespace)) {
|
|
360
|
+
// Not in current sync scope, skip
|
|
361
|
+
continue;
|
|
362
|
+
}
|
|
363
|
+
|
|
364
|
+
// Check for intentdna:managed sentinel
|
|
365
|
+
let hasManaged = false;
|
|
366
|
+
if (entry.isFile()) {
|
|
367
|
+
const content = await readFile(fullPath, "utf-8");
|
|
368
|
+
hasManaged = content.includes("intentdna:managed");
|
|
369
|
+
} else if (entry.isDirectory()) {
|
|
370
|
+
// Check SKILL.md inside skill directory
|
|
371
|
+
const skillMd = resolve(fullPath, "SKILL.md");
|
|
372
|
+
try {
|
|
373
|
+
const content = await readFile(skillMd, "utf-8");
|
|
374
|
+
hasManaged = content.includes("intentdna:managed");
|
|
375
|
+
} catch {
|
|
376
|
+
// No SKILL.md or unreadable
|
|
377
|
+
}
|
|
378
|
+
}
|
|
379
|
+
|
|
380
|
+
if (hasManaged) {
|
|
381
|
+
if (entry.isDirectory()) {
|
|
382
|
+
await rm(fullPath, { recursive: true, force: true });
|
|
383
|
+
} else {
|
|
384
|
+
await unlink(fullPath);
|
|
385
|
+
}
|
|
386
|
+
removed++;
|
|
387
|
+
}
|
|
388
|
+
}
|
|
389
|
+
} catch {
|
|
390
|
+
// Fail-open
|
|
391
|
+
}
|
|
392
|
+
return removed;
|
|
393
|
+
}
|
|
394
|
+
```
|
|
395
|
+
|
|
396
|
+
#### 2.3 修改调用点
|
|
397
|
+
|
|
398
|
+
文件:`src/cli/commands/sync.ts`
|
|
399
|
+
|
|
400
|
+
找到 `cleanStaleDNAFiles` 的调用点(约 600-650 行),传入 activeNamespaces:
|
|
401
|
+
|
|
402
|
+
```typescript
|
|
403
|
+
// 之前
|
|
404
|
+
if (opts.agentsDir) {
|
|
405
|
+
const removed = await cleanStaleDNAFiles(opts.agentsDir, "agent");
|
|
406
|
+
if (removed > 0) process.stderr.write(`Cleaned ${removed} stale agent file(s)\n`);
|
|
407
|
+
}
|
|
408
|
+
if (opts.skillsDir) {
|
|
409
|
+
const removed = await cleanStaleDNAFiles(opts.skillsDir, "skill");
|
|
410
|
+
if (removed > 0) process.stderr.write(`Cleaned ${removed} stale skill dir(s)\n`);
|
|
411
|
+
}
|
|
412
|
+
|
|
413
|
+
// 之后
|
|
414
|
+
// Extract namespaces from all loaded DNAs
|
|
415
|
+
const activeNamespaces = dnas
|
|
416
|
+
.map(d => typeof d.namespace === "string" ? d.namespace : null)
|
|
417
|
+
.filter((ns): ns is string => ns !== null);
|
|
418
|
+
|
|
419
|
+
if (opts.agentsDir) {
|
|
420
|
+
const removed = await cleanStaleDNAFiles(opts.agentsDir, "agent", activeNamespaces);
|
|
421
|
+
if (removed > 0) process.stderr.write(`Cleaned ${removed} stale agent file(s)\n`);
|
|
422
|
+
}
|
|
423
|
+
if (opts.skillsDir) {
|
|
424
|
+
const removed = await cleanStaleDNAFiles(opts.skillsDir, "skill", activeNamespaces);
|
|
425
|
+
if (removed > 0) process.stderr.write(`Cleaned ${removed} stale skill dir(s)\n`);
|
|
426
|
+
}
|
|
427
|
+
```
|
|
428
|
+
|
|
429
|
+
---
|
|
430
|
+
|
|
431
|
+
### Step 3: 测试
|
|
432
|
+
|
|
433
|
+
#### 3.1 单元测试
|
|
434
|
+
|
|
435
|
+
新建 `test/template-version-hash.test.ts`:
|
|
436
|
+
|
|
437
|
+
```typescript
|
|
438
|
+
import { describe, it, expect } from "vitest";
|
|
439
|
+
import { calculateTemplateHash } from "../src/cli/commands/init.js";
|
|
440
|
+
|
|
441
|
+
describe("calculateTemplateHash", () => {
|
|
442
|
+
it("returns consistent hash for same content", () => {
|
|
443
|
+
const content = "namespace: test\nversion: 0.1.0";
|
|
444
|
+
const hash1 = calculateTemplateHash(content);
|
|
445
|
+
const hash2 = calculateTemplateHash(content);
|
|
446
|
+
expect(hash1).toBe(hash2);
|
|
447
|
+
});
|
|
448
|
+
|
|
449
|
+
it("returns different hash for different content", () => {
|
|
450
|
+
const content1 = "namespace: test\nversion: 0.1.0";
|
|
451
|
+
const content2 = "namespace: test\nversion: 0.2.0";
|
|
452
|
+
const hash1 = calculateTemplateHash(content1);
|
|
453
|
+
const hash2 = calculateTemplateHash(content2);
|
|
454
|
+
expect(hash1).not.toBe(hash2);
|
|
455
|
+
});
|
|
456
|
+
|
|
457
|
+
it("returns 16-char hex string", () => {
|
|
458
|
+
const hash = calculateTemplateHash("test");
|
|
459
|
+
expect(hash).toMatch(/^[a-f0-9]{16}$/);
|
|
460
|
+
});
|
|
461
|
+
});
|
|
462
|
+
```
|
|
463
|
+
|
|
464
|
+
新建 `test/namespace-cleanup.test.ts`:
|
|
465
|
+
|
|
466
|
+
```typescript
|
|
467
|
+
import { describe, it, expect, beforeEach, afterEach } from "vitest";
|
|
468
|
+
import { mkdir, writeFile, readdir, rm } from "fs/promises";
|
|
469
|
+
import { resolve } from "path";
|
|
470
|
+
import { cleanStaleDNAFiles } from "../src/cli/commands/sync.js";
|
|
471
|
+
|
|
472
|
+
describe("cleanStaleDNAFiles namespace filtering", () => {
|
|
473
|
+
const testDir = resolve(__dirname, ".test-cleanup");
|
|
474
|
+
const agentsDir = resolve(testDir, "agents");
|
|
475
|
+
|
|
476
|
+
beforeEach(async () => {
|
|
477
|
+
await mkdir(agentsDir, { recursive: true });
|
|
478
|
+
});
|
|
479
|
+
|
|
480
|
+
afterEach(async () => {
|
|
481
|
+
await rm(testDir, { recursive: true, force: true });
|
|
482
|
+
});
|
|
483
|
+
|
|
484
|
+
it("only removes files matching active namespaces", async () => {
|
|
485
|
+
// Create files for two namespaces
|
|
486
|
+
await writeFile(
|
|
487
|
+
resolve(agentsDir, "dna-frw-surgeon.md"),
|
|
488
|
+
"<!-- intentdna:managed -->\nFRW surgeon"
|
|
489
|
+
);
|
|
490
|
+
await writeFile(
|
|
491
|
+
resolve(agentsDir, "dna-be-handler.md"),
|
|
492
|
+
"<!-- intentdna:managed -->\nBE handler"
|
|
493
|
+
);
|
|
494
|
+
|
|
495
|
+
// Clean only frw namespace
|
|
496
|
+
const removed = await cleanStaleDNAFiles(agentsDir, "agent", ["frw"]);
|
|
497
|
+
|
|
498
|
+
expect(removed).toBe(1);
|
|
499
|
+
const remaining = await readdir(agentsDir);
|
|
500
|
+
expect(remaining).toEqual(["dna-be-handler.md"]);
|
|
501
|
+
});
|
|
502
|
+
|
|
503
|
+
it("preserves files without intentdna:managed", async () => {
|
|
504
|
+
await writeFile(
|
|
505
|
+
resolve(agentsDir, "dna-frw-custom.md"),
|
|
506
|
+
"User-written agent"
|
|
507
|
+
);
|
|
508
|
+
|
|
509
|
+
const removed = await cleanStaleDNAFiles(agentsDir, "agent", ["frw"]);
|
|
510
|
+
|
|
511
|
+
expect(removed).toBe(0);
|
|
512
|
+
const remaining = await readdir(agentsDir);
|
|
513
|
+
expect(remaining).toEqual(["dna-frw-custom.md"]);
|
|
514
|
+
});
|
|
515
|
+
|
|
516
|
+
it("preserves files not matching dna-{ns}- pattern", async () => {
|
|
517
|
+
await writeFile(
|
|
518
|
+
resolve(agentsDir, "custom-agent.md"),
|
|
519
|
+
"<!-- intentdna:managed -->\nCustom"
|
|
520
|
+
);
|
|
521
|
+
|
|
522
|
+
const removed = await cleanStaleDNAFiles(agentsDir, "agent", ["frw"]);
|
|
523
|
+
|
|
524
|
+
expect(removed).toBe(0);
|
|
525
|
+
const remaining = await readdir(agentsDir);
|
|
526
|
+
expect(remaining).toEqual(["custom-agent.md"]);
|
|
527
|
+
});
|
|
528
|
+
});
|
|
529
|
+
```
|
|
530
|
+
|
|
531
|
+
#### 3.2 集成测试
|
|
532
|
+
|
|
533
|
+
在 intentdna 项目:
|
|
534
|
+
```bash
|
|
535
|
+
npm test # 全绿
|
|
536
|
+
```
|
|
537
|
+
|
|
538
|
+
在 lwk_flutter_v2-rewrite 项目(单模板场景):
|
|
539
|
+
```bash
|
|
540
|
+
dna sync
|
|
541
|
+
# 验证:不报错,agent/skill 正常生成
|
|
542
|
+
```
|
|
543
|
+
|
|
544
|
+
创建测试项目(多模板场景):
|
|
545
|
+
```bash
|
|
546
|
+
mkdir /tmp/multi-template-test && cd /tmp/multi-template-test
|
|
547
|
+
dna init flutter-rewrite
|
|
548
|
+
dna init backend-api # 假设有这个模板,或用另一个
|
|
549
|
+
dna sync
|
|
550
|
+
# 验证:两个模板的 agent/skill 都保留
|
|
551
|
+
```
|
|
552
|
+
|
|
553
|
+
---
|
|
554
|
+
|
|
555
|
+
### Step 4: 文档更新
|
|
556
|
+
|
|
557
|
+
#### 4.1 CHANGELOG.md
|
|
558
|
+
|
|
559
|
+
```markdown
|
|
560
|
+
## [1.5.14] - 2026-04-19
|
|
561
|
+
|
|
562
|
+
### Fixed
|
|
563
|
+
- Template version tracking now uses template's own `version` field instead of package version
|
|
564
|
+
- Added `_template_content_hash` to detect template changes independent of version bumps
|
|
565
|
+
- `dna sync` now only cleans stale files from active namespaces, fixing multi-template projects
|
|
566
|
+
- Namespace-aware cleanup prevents accidental deletion of other templates' generated files
|
|
567
|
+
|
|
568
|
+
### Changed
|
|
569
|
+
- `dna init` now injects `_template_content_hash` alongside `_template_version`
|
|
570
|
+
- `dna sync` detects template content changes via hash comparison
|
|
571
|
+
```
|
|
572
|
+
|
|
573
|
+
#### 4.2 docs/multi-template.md(新建)
|
|
574
|
+
|
|
575
|
+
```markdown
|
|
576
|
+
# Multi-Template Projects
|
|
577
|
+
|
|
578
|
+
Intent DNA supports using multiple templates in a single project.
|
|
579
|
+
|
|
580
|
+
## Setup
|
|
581
|
+
|
|
582
|
+
```bash
|
|
583
|
+
dna init flutter-rewrite
|
|
584
|
+
dna init backend-api
|
|
585
|
+
```
|
|
586
|
+
|
|
587
|
+
Each template must have a unique `namespace` field. Namespace collisions are detected and blocked.
|
|
588
|
+
|
|
589
|
+
## Sync Behavior
|
|
590
|
+
|
|
591
|
+
`dna sync` processes all templates in `.dna/configs/*.yaml` and only cleans files belonging to active namespaces.
|
|
592
|
+
|
|
593
|
+
Example:
|
|
594
|
+
- `dna-frw-*` files belong to `frw` namespace
|
|
595
|
+
- `dna-be-*` files belong to `be` namespace
|
|
596
|
+
- Syncing `frw` template will not delete `be` files
|
|
597
|
+
|
|
598
|
+
## Template Updates
|
|
599
|
+
|
|
600
|
+
Each template tracks its content independently via `_template_content_hash`. When template content changes:
|
|
601
|
+
|
|
602
|
+
```bash
|
|
603
|
+
dna sync
|
|
604
|
+
# Output: Template content changed: "flutter-rewrite". Run: dna init --upgrade flutter-rewrite
|
|
605
|
+
```
|
|
606
|
+
|
|
607
|
+
Run upgrade to update:
|
|
608
|
+
```bash
|
|
609
|
+
dna init --upgrade flutter-rewrite
|
|
610
|
+
```
|
|
611
|
+
```
|
|
612
|
+
|
|
613
|
+
---
|
|
614
|
+
|
|
615
|
+
### Step 5: 发版
|
|
616
|
+
|
|
617
|
+
```bash
|
|
618
|
+
cd /Users/samuel/lawark/intentdna
|
|
619
|
+
npm test # 全绿
|
|
620
|
+
/dna-release patch "fix: template version tracking + namespace-aware cleanup for multi-template projects"
|
|
621
|
+
```
|
|
622
|
+
|
|
623
|
+
---
|
|
624
|
+
|
|
625
|
+
## 验收清单
|
|
626
|
+
|
|
627
|
+
### Bug 1: Template Version
|
|
628
|
+
- [ ] `calculateTemplateHash()` 实现
|
|
629
|
+
- [ ] `injectSourceTemplate()` 接受 templateVersion + templateHash 参数
|
|
630
|
+
- [ ] init 命令两处调用点更新(读模板 version 字段,不读 pkg 版本)
|
|
631
|
+
- [ ] `checkTemplateVersions()` 改为 hash 对比
|
|
632
|
+
- [ ] `findTemplateFile()` 辅助函数实现
|
|
633
|
+
- [ ] 单元测试通过
|
|
634
|
+
|
|
635
|
+
### Bug 2: Namespace Cleanup
|
|
636
|
+
- [ ] `cleanStaleDNAFiles()` 接受 activeNamespaces 参数
|
|
637
|
+
- [ ] 文件名 namespace 提取逻辑(`dna-{ns}-*` 正则)
|
|
638
|
+
- [ ] 只删匹配 namespace 的文件
|
|
639
|
+
- [ ] sync 调用点传入 activeNamespaces
|
|
640
|
+
- [ ] 单元测试通过
|
|
641
|
+
|
|
642
|
+
### 集成验证
|
|
643
|
+
- [ ] intentdna 项目 npm test 全绿
|
|
644
|
+
- [ ] lwk_flutter_v2-rewrite dna sync 正常
|
|
645
|
+
- [ ] 多模板测试项目验证(如果有第二个模板)
|
|
646
|
+
|
|
647
|
+
### 文档
|
|
648
|
+
- [ ] CHANGELOG.md 更新
|
|
649
|
+
- [ ] docs/multi-template.md 新建(可选)
|
|
650
|
+
|
|
651
|
+
### 发版
|
|
652
|
+
- [ ] /dna-release patch 执行
|
|
653
|
+
- [ ] v1.5.14 发布
|
|
@@ -1,12 +0,0 @@
|
|
|
1
|
-
{
|
|
2
|
-
"hooks": {
|
|
3
|
-
"PreToolUse": [{ "matcher": "*", "hooks": [{ "type": "command", "command": "dna-hook PreToolUse", "timeout": 5 }] }],
|
|
4
|
-
"PostToolUse": [{ "matcher": "*", "hooks": [{ "type": "command", "command": "dna-hook PostToolUse", "timeout": 3 }] }],
|
|
5
|
-
"UserPromptSubmit": [{ "matcher": "*", "hooks": [{ "type": "command", "command": "dna-hook UserPromptSubmit", "timeout": 5 }] }],
|
|
6
|
-
"SubagentStop": [{ "matcher": "*", "hooks": [{ "type": "command", "command": "dna-hook SubagentStop", "timeout": 3 }] }],
|
|
7
|
-
"PreCompact": [{ "matcher": "*", "hooks": [{ "type": "command", "command": "dna-hook PreCompact", "timeout": 3 }] }],
|
|
8
|
-
"Notification": [{ "matcher": "*", "hooks": [{ "type": "command", "command": "dna-hook Notification", "timeout": 3 }] }],
|
|
9
|
-
"Stop": [{ "matcher": "*", "hooks": [{ "type": "command", "command": "dna-hook Stop", "timeout": 3 }] }],
|
|
10
|
-
"SessionStart": [{ "matcher": "*", "hooks": [{ "type": "command", "command": "dna-hook SessionStart", "timeout": 5 }] }]
|
|
11
|
-
}
|
|
12
|
-
}
|
|
@@ -1,16 +0,0 @@
|
|
|
1
|
-
{
|
|
2
|
-
"name": "intentdna",
|
|
3
|
-
"description": "Declarative policy layer for AI agent governance",
|
|
4
|
-
"owner": {
|
|
5
|
-
"name": "Samuel",
|
|
6
|
-
"email": "wanglushan3344@gmail.com"
|
|
7
|
-
},
|
|
8
|
-
"plugins": [
|
|
9
|
-
{
|
|
10
|
-
"name": "intentdna",
|
|
11
|
-
"description": "DNA template compilation + runtime enforcement",
|
|
12
|
-
"version": "1.5.13",
|
|
13
|
-
"source": "./"
|
|
14
|
-
}
|
|
15
|
-
]
|
|
16
|
-
}
|