intentdna 1.4.8 → 1.5.1

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.
@@ -130,7 +130,7 @@ export async function appendCompletedArtifact(projectDir, stepId, artifact, sess
130
130
  }
131
131
  // ── Internal ───────────────────────────────────────────────
132
132
  /** Atomic write: write to temp file then rename. */
133
- async function atomicWrite(filePath, data) {
133
+ export async function atomicWrite(filePath, data) {
134
134
  const tmpPath = filePath + ".tmp." + process.pid;
135
135
  await mkdir(dirname(filePath), { recursive: true });
136
136
  await writeFile(tmpPath, data, "utf-8");
@@ -139,17 +139,29 @@ async function atomicWrite(filePath, data) {
139
139
  const TRACE_DIR = "trace";
140
140
  const MAX_TRACE_SIZE = 10 * 1024 * 1024; // 10MB
141
141
  const TRACE_RETENTION_DAYS = 7;
142
+ /**
143
+ * Build trace file name.
144
+ * With session isolation: `trace-{date}-{sessionId}.jsonl`
145
+ * Without: `trace-{date}.jsonl` (backward compat / merged view)
146
+ */
147
+ function traceFileName(date, sessionId) {
148
+ if (sessionId) {
149
+ return `trace-${date}-${sessionId}.jsonl`;
150
+ }
151
+ return `trace-${date}.jsonl`;
152
+ }
142
153
  /**
143
154
  * Append a trace entry to the daily trace file.
144
- * File: `.dna/state/trace/trace-YYYY-MM-DD.jsonl`
155
+ * With sessionId: `.dna/state/trace/trace-YYYY-MM-DD-{sessionId}.jsonl`
156
+ * Without: `.dna/state/trace/trace-YYYY-MM-DD.jsonl` (backward compat)
145
157
  * Fail-open: never throws.
146
158
  */
147
- export async function appendTrace(projectDir, entry) {
159
+ export async function appendTrace(projectDir, entry, sessionId) {
148
160
  try {
149
161
  const traceDir = join(projectDir, ".dna", "state", TRACE_DIR);
150
162
  await mkdir(traceDir, { recursive: true });
151
163
  const date = entry.timestamp.slice(0, 10);
152
- const tracePath = join(traceDir, `trace-${date}.jsonl`);
164
+ const tracePath = join(traceDir, traceFileName(date, sessionId));
153
165
  // Check file size — rotate if over limit
154
166
  try {
155
167
  const stats = await stat(tracePath);
@@ -165,9 +177,11 @@ export async function appendTrace(projectDir, entry) {
165
177
  }
166
178
  /**
167
179
  * Read trace entries from the last N days.
180
+ * With sessionId: reads only that session's trace files.
181
+ * Without: reads all trace files (merged view).
168
182
  * Returns parsed entries sorted by timestamp.
169
183
  */
170
- export async function readTraces(projectDir, days = 1) {
184
+ export async function readTraces(projectDir, days = 1, sessionId) {
171
185
  const traceDir = join(projectDir, ".dna", "state", TRACE_DIR);
172
186
  const entries = [];
173
187
  const cutoff = new Date();
@@ -178,8 +192,16 @@ export async function readTraces(projectDir, days = 1) {
178
192
  const traceFiles = files
179
193
  .filter(f => f.startsWith("trace-") && f.endsWith(".jsonl"))
180
194
  .filter(f => {
181
- const fileDate = f.slice(6, 16); // "trace-YYYY-MM-DD.jsonl" "YYYY-MM-DD"
182
- return fileDate >= cutoffDate;
195
+ // Extract date from filename: trace-YYYY-MM-DD.jsonl or trace-YYYY-MM-DD-{sessionId}.jsonl
196
+ const fileDate = f.slice(6, 16); // "trace-YYYY-MM-DD..."
197
+ if (fileDate < cutoffDate)
198
+ return false;
199
+ // Session filter: only include files for this session (or shared files)
200
+ if (sessionId) {
201
+ const suffix = f.slice(16); // "-{sessionId}.jsonl" or ".jsonl"
202
+ return suffix === `-${sessionId}.jsonl` || suffix === ".jsonl";
203
+ }
204
+ return true;
183
205
  })
184
206
  .sort();
185
207
  for (const file of traceFiles) {
@@ -226,3 +248,68 @@ export async function rotateTraces(projectDir) {
226
248
  }
227
249
  return removed;
228
250
  }
251
+ // ── Stale State Cleanup ──────────────────────────────────
252
+ /**
253
+ * Clean up stale state from `.dna/state/sessions/` and root state.
254
+ * Removes workflow.json files older than DEFAULT_STALENESS_MS (2h).
255
+ * Called on SessionStart to prevent state accumulation.
256
+ * Fail-open: never throws.
257
+ */
258
+ export async function cleanStaleState(projectDir) {
259
+ let removed = 0;
260
+ const sessionsDir = join(projectDir, ".dna", "state", "sessions");
261
+ try {
262
+ const sessions = await readdir(sessionsDir);
263
+ for (const sessionDir of sessions) {
264
+ const wfPath = join(sessionsDir, sessionDir, WORKFLOW_FILE);
265
+ try {
266
+ const raw = await readFile(wfPath, "utf-8");
267
+ const state = JSON.parse(raw);
268
+ if (state.started_at) {
269
+ const age = Date.now() - new Date(state.started_at).getTime();
270
+ if (age > DEFAULT_STALENESS_MS) {
271
+ // Remove stale session directory
272
+ const { rm } = await import("node:fs/promises");
273
+ await rm(join(sessionsDir, sessionDir), { recursive: true, force: true });
274
+ removed++;
275
+ }
276
+ }
277
+ }
278
+ catch {
279
+ // Malformed or missing workflow.json — clean up the directory
280
+ try {
281
+ const dirStat = await stat(join(sessionsDir, sessionDir));
282
+ if (dirStat.isDirectory()) {
283
+ const dirAge = Date.now() - dirStat.mtimeMs;
284
+ if (dirAge > DEFAULT_STALENESS_MS) {
285
+ const { rm } = await import("node:fs/promises");
286
+ await rm(join(sessionsDir, sessionDir), { recursive: true, force: true });
287
+ removed++;
288
+ }
289
+ }
290
+ }
291
+ catch { /* skip */ }
292
+ }
293
+ }
294
+ }
295
+ catch {
296
+ // No sessions dir — nothing to clean
297
+ }
298
+ // Also clean root-level stale workflow state
299
+ const rootWfPath = join(projectDir, ".dna", "state", WORKFLOW_FILE);
300
+ try {
301
+ const raw = await readFile(rootWfPath, "utf-8");
302
+ const state = JSON.parse(raw);
303
+ if (state.started_at) {
304
+ const age = Date.now() - new Date(state.started_at).getTime();
305
+ if (age > DEFAULT_STALENESS_MS) {
306
+ await unlink(rootWfPath).catch(() => { });
307
+ removed++;
308
+ }
309
+ }
310
+ }
311
+ catch {
312
+ // No root workflow state
313
+ }
314
+ return removed;
315
+ }
@@ -6,8 +6,9 @@
6
6
  *
7
7
  * This is the bridge between compile-time (dna sync) and runtime (hook execution).
8
8
  */
9
- import { readFile, writeFile, mkdir } from "node:fs/promises";
9
+ import { readFile, mkdir } from "node:fs/promises";
10
10
  import { join } from "node:path";
11
+ import { atomicWrite } from "../hooks/state.js";
11
12
  // ── Serialize ──────────────────────────────────────────────
12
13
  /**
13
14
  * Create a CompiledIRFile wrapper around a ConstraintIR.
@@ -30,7 +31,7 @@ export async function writeCompiledIR(projectDir, compiled) {
30
31
  const outputDir = join(projectDir, ".dna", "compiled");
31
32
  await mkdir(outputDir, { recursive: true });
32
33
  const outputPath = join(outputDir, "ir.json");
33
- await writeFile(outputPath, JSON.stringify(compiled, null, 2) + "\n", "utf-8");
34
+ await atomicWrite(outputPath, JSON.stringify(compiled, null, 2) + "\n");
34
35
  return outputPath;
35
36
  }
36
37
  /**
@@ -258,11 +258,13 @@ function generateGroupExecution(group, stepMap, options, plan) {
258
258
  }
259
259
  // Merge worktrees back (escalate on conflict)
260
260
  lines.push("");
261
- lines.push(" # Merge worktrees back to main branch");
261
+ lines.push(" # Scope gate + merge worktrees back to main branch");
262
262
  for (const id of group.step_ids) {
263
+ const step = stepMap.get(id);
263
264
  const varId = sanitizeForBash(id);
264
265
  const safeId = sanitizeForShell(id);
265
266
  lines.push(` if [ "$STEP_${varId}_STATUS" -eq 0 ]; then`);
267
+ lines.push(` scope_gate "${safeId}" "dna-wt-${safeId}" "${step.role}" "${safeId}"`);
266
268
  lines.push(` merge_worktree "dna-wt-${safeId}" "${safeId}"`);
267
269
  lines.push(" fi");
268
270
  }
@@ -336,6 +338,77 @@ function generateMergeWorktreeFn() {
336
338
  "",
337
339
  ];
338
340
  }
341
+ /**
342
+ * Generate the scope_gate bash helper function.
343
+ * Filters out-of-scope file changes from a worktree branch before merging.
344
+ * Reads role write scope from .dna/compiled/ir.json, reverts unauthorized files,
345
+ * and writes trace entries for each violation.
346
+ * Fail-open: never prevents merge from proceeding.
347
+ */
348
+ function generateScopeGateFn() {
349
+ return [
350
+ "# Scope gate: filter out-of-scope changes before merge (three-layer defense L3)",
351
+ "scope_gate() {",
352
+ ' local name="$1" branch="$2" role="$3" step_id="$4"',
353
+ ' local ir_path="${PROJECT_ROOT}/.dna/compiled/ir.json"',
354
+ "",
355
+ ' [ ! -f "$ir_path" ] && return 0',
356
+ "",
357
+ " # Read write scope globs for this role from IR",
358
+ " local allowed_globs",
359
+ " allowed_globs=$(jq -r --arg role \"$role\" \\",
360
+ " '.roles_scope_map[]? | select(.role_name == $role) | .scope.write[]?' \\",
361
+ ' "$ir_path" 2>/dev/null)',
362
+ "",
363
+ " # No scope defined — allow all changes",
364
+ ' [ -z "$allowed_globs" ] && return 0',
365
+ "",
366
+ " # Get files changed by this branch vs main",
367
+ " local changed",
368
+ ' changed=$(git diff --name-only "$MAIN_BRANCH...$branch" 2>/dev/null)',
369
+ ' [ -z "$changed" ] && return 0',
370
+ "",
371
+ " local rejected=()",
372
+ ' while IFS= read -r file; do',
373
+ ' [ -z "$file" ] && continue',
374
+ " local allowed=false",
375
+ ' while IFS= read -r glob; do',
376
+ ' [ -z "$glob" ] && continue',
377
+ " # Prefix matching (consistent with enforce.ts globToPrefix)",
378
+ ' local prefix="${glob%%\\**}"',
379
+ ' if [ -z "$prefix" ] || [[ "$file" == "${prefix}"* ]]; then',
380
+ " allowed=true",
381
+ " break",
382
+ " fi",
383
+ ' done <<< "$allowed_globs"',
384
+ ' if [ "$allowed" = false ]; then',
385
+ ' rejected+=("$file")',
386
+ " fi",
387
+ ' done <<< "$changed"',
388
+ "",
389
+ ' [ ${#rejected[@]} -eq 0 ] && return 0',
390
+ "",
391
+ " echo \"[Intent DNA] Scope gate: ${#rejected[@]} file(s) outside role '${role}' scope, reverted:\"",
392
+ ' printf " - %s\\n" "${rejected[@]}"',
393
+ "",
394
+ " # Revert out-of-scope files in worktree branch",
395
+ ' for file in "${rejected[@]}"; do',
396
+ ' (cd ".dna/worktrees/${name}" && git checkout "$MAIN_BRANCH" -- "$file") 2>/dev/null || true',
397
+ " done",
398
+ ' (cd ".dna/worktrees/${name}" && git add -A && git commit --amend --no-edit) 2>/dev/null || true',
399
+ "",
400
+ " # Write trace entries for scope violations",
401
+ ' local trace_dir="${PROJECT_ROOT}/.dna/state/trace"',
402
+ ' mkdir -p "$trace_dir" 2>/dev/null || true',
403
+ ' local trace_file="${trace_dir}/trace-$(date +%Y-%m-%d).jsonl"',
404
+ ' for file in "${rejected[@]}"; do',
405
+ " printf '{\"trace_id\":\"%s\",\"event\":\"ScopeGate\",\"agent_type\":\"dna-%s\",\"step\":\"%s\",\"decision\":\"warn\",\"reason\":\"Merge-time scope gate reverted out-of-scope file\",\"target_path\":\"%s\",\"duration_ms\":0,\"timestamp\":\"%s\"}\\n' \\",
406
+ " \"$(uuidgen | tr '[:upper:]' '[:lower:]')\" \"$role\" \"$step_id\" \"$file\" \"$(date -u +%Y-%m-%dT%H:%M:%S.000Z)\" >> \"$trace_file\"",
407
+ " done",
408
+ "}",
409
+ "",
410
+ ];
411
+ }
339
412
  /**
340
413
  * Generate transition check code after all groups execute.
341
414
  */
@@ -404,9 +477,10 @@ export function compileWorkflowToShell(plan, options) {
404
477
  // run_agent function
405
478
  lines.push(...generateRunAgentFn(opts));
406
479
  lines.push("");
407
- // merge_worktree function (only if needed)
480
+ // merge_worktree + scope_gate functions (only if needed)
408
481
  if (needsWorktreeSupport(plan)) {
409
482
  lines.push(...generateMergeWorktreeFn());
483
+ lines.push(...generateScopeGateFn());
410
484
  }
411
485
  // Main execution
412
486
  if (plan.retry.max_retries > 0) {
@@ -235,6 +235,16 @@ function validateWorkflow(workflow, roleNames, pathPrefix) {
235
235
  if (step.prompt !== undefined && (typeof step.prompt !== "string" || !step.prompt)) {
236
236
  errors.push({ path: `${stepPath}.prompt`, message: "prompt must be a non-empty string" });
237
237
  }
238
+ // isolation: must be a valid enum value
239
+ if (step.isolation !== undefined) {
240
+ const validIsolation = ["none", "worktree", "auto"];
241
+ if (!validIsolation.includes(step.isolation)) {
242
+ errors.push({
243
+ path: `${stepPath}.isolation`,
244
+ message: `must be one of: ${validIsolation.join(", ")}`,
245
+ });
246
+ }
247
+ }
238
248
  // checkpoints validation
239
249
  if (step.checkpoints) {
240
250
  if (!Array.isArray(step.checkpoints)) {
@@ -283,6 +293,26 @@ function validateWorkflow(workflow, roleNames, pathPrefix) {
283
293
  }
284
294
  }
285
295
  }
296
+ // Validate default_isolation
297
+ if (workflow.default_isolation !== undefined) {
298
+ const validIsolation = ["none", "worktree", "auto"];
299
+ if (!validIsolation.includes(workflow.default_isolation)) {
300
+ errors.push({
301
+ path: `${path}.default_isolation`,
302
+ message: `must be one of: ${validIsolation.join(", ")}`,
303
+ });
304
+ }
305
+ }
306
+ // Validate merge_strategy
307
+ if (workflow.merge_strategy !== undefined) {
308
+ const validStrategies = ["escalate"];
309
+ if (!validStrategies.includes(workflow.merge_strategy)) {
310
+ errors.push({
311
+ path: `${path}.merge_strategy`,
312
+ message: `must be one of: ${validStrategies.join(", ")}`,
313
+ });
314
+ }
315
+ }
286
316
  // Validate transitions
287
317
  const validConditions = ["pass", "fail", "always", "error"];
288
318
  for (let i = 0; i < (workflow.transitions?.length ?? 0); i++) {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "intentdna",
3
- "version": "1.4.8",
3
+ "version": "1.5.1",
4
4
  "description": "Intent DNA — Declarative policy layer for AI agent behavior",
5
5
  "type": "module",
6
6
  "main": "dist/index.js",
@@ -0,0 +1,177 @@
1
+ # Spec: DNA 基础设施加固
2
+
3
+ ## Metadata
4
+ - Interview: 5 rounds, final ambiguity 20%
5
+ - Date: 2026-04-15
6
+ - Status: 设计完成
7
+
8
+ ## Goal
9
+
10
+ 全面审视 DNA 基础设施的薄弱点,从可靠性 → 治理能力 → 工具体验三个维度加固,对标 OMC 补齐差距,为组织级治理奠基。
11
+
12
+ ## 成功标准
13
+
14
+ - 零已知缺陷
15
+ - 关键路径测试覆盖 >90%
16
+ - 可交付给他人使用(不需要作者盯着)
17
+ - 架构可扩展(后续治理功能不需要重构基础)
18
+
19
+ ## 优先级: 可靠性 → 治理能力 → 工具体验
20
+
21
+ ---
22
+
23
+ ## 第一阶段: 可靠性加固
24
+
25
+ ### R1: Config 编译前验证
26
+
27
+ **问题**: `dna compile` 绕过 `validateDNA()`,无效 DNA 静默编译。
28
+
29
+ **修复**: sync/compile 入口统一调用 `validateDNA()`,验证失败 → 报错退出。
30
+
31
+ ### R2: Trace 并发安全
32
+
33
+ **问题**: 多 session 同时 `appendFile` 写 trace.jsonl,可能 corrupt。
34
+
35
+ **修复**:
36
+ - 方案 A: trace 文件按 session 隔离 → `trace-{date}-{sessionId}.jsonl`
37
+ - 方案 B: 使用 `O_APPEND` 原子追加(POSIX 保证 ≤ PIPE_BUF 的 write 原子性)
38
+ - 推荐 A(简单、和 OMC 一致)
39
+
40
+ ### R3: IR 版本迁移
41
+
42
+ **问题**: `ir_version` 字段存在但无迁移逻辑。破坏性 IR 变更让旧 hook 静默失败。
43
+
44
+ **修复**:
45
+ - dna-hook 检测 `ir_version` 不匹配 → 输出明确错误 "IR version mismatch, run dna sync"
46
+ - 保留 fail-open(不 block,但警告)
47
+
48
+ ### R4: 原子写入
49
+
50
+ **问题**: state 写入使用普通 `writeFile`,进程中断可能 corrupt。
51
+
52
+ **修复**: 参考 OMC 的 temp+rename 模式(state.ts 已有,确认所有写入路径都用)
53
+
54
+ ### R5: Stale 状态清理
55
+
56
+ **问题**: 无超时机制,残留 state 文件永久存在。
57
+
58
+ **修复**: 参考 OMC 的 2h stale 超时。SessionStart 时检查并清理过期 state。
59
+
60
+ ---
61
+
62
+ ## 第二阶段: 治理能力
63
+
64
+ ### G1: Session 隔离
65
+
66
+ **问题**: `.dna/state/` 无 session 目录,多 session 状态冲突。
67
+
68
+ **修复**:
69
+ ```
70
+ .dna/state/
71
+ sessions/
72
+ {sessionId}/
73
+ workflow.json
74
+ trace.jsonl ← session 级 trace
75
+ trace/
76
+ trace-{date}.jsonl ← 全局 trace(合并视图)
77
+ ```
78
+
79
+ ### G2: MCP Server — 状态管理工具
80
+
81
+ DNA 的第一个 MCP server,提供:
82
+ - `dna_state_read(mode)` — 读 workflow/enforce 状态
83
+ - `dna_state_write(mode, data)` — 写状态
84
+ - `dna_trace_query(filters)` — 查询 trace 数据
85
+ - `dna_workflow_status()` — 当前 workflow 进度
86
+
87
+ 实现方式: 参考 OMC 的 `createSdkMcpServer()`,注册为 Claude Code plugin 的 MCP server。
88
+
89
+ ### G3: MCP Server — 编译工具链
90
+
91
+ - `dna_compile(config_path)` — 编译 DNA 到 IR
92
+ - `dna_sync()` — 一键同步(等同 CLI dna sync)
93
+ - `dna_generate(description)` — 从描述生成 DNA
94
+
95
+ 让 LLM 直接操作 DNA,不需要 Bash 中转。
96
+
97
+ ### G4: 状态驱动 Enforce
98
+
99
+ **问题**: enforce 只读静态 IR,不能根据 workflow 运行时状态调整。
100
+
101
+ **修复**: enforce 接受 state 参数,支持条件规则:
102
+ - "rescue 第 3 轮后放宽 scope"
103
+ - "scan 步骤只读,fix 步骤可写"
104
+ - 当前 WorkflowState 已传入 enforce,但只用于 checkpoint。扩展为通用条件。
105
+
106
+ ### G5: MCP Server — 远程治理通信(后续)
107
+
108
+ - 上报审计数据到中央服务器
109
+ - 拉取企业级 DNA 策略
110
+ - 接收策略更新推送
111
+
112
+ 这是 Phase 7 的企业治理基础,当前只做架构预留。
113
+
114
+ ---
115
+
116
+ ## 第三阶段: 工具体验
117
+
118
+ ### U1: dna sync 自动检测升级
119
+
120
+ sync 时对比 `_template_version`,有更新 → 提示升级。
121
+
122
+ ### U2: 冷启动优化
123
+
124
+ **问题**: 每次 hook 调用 spawn Node.js ~50ms。
125
+
126
+ **方案**:
127
+ - 短期: IR 缓存到内存(MCP server 常驻进程时自然解决)
128
+ - 长期: MCP server 内置 enforce,hook 调用走 MCP 而非 spawn
129
+
130
+ ### U3: 错误信息优化
131
+
132
+ hook 报错时输出人类可读的提示而非 JSON。`dna verify` 输出友好的健康报告。
133
+
134
+ ---
135
+
136
+ ## 与 OMC 差距对标
137
+
138
+ | 差距 | 对应任务 | 阶段 |
139
+ |------|---------|------|
140
+ | 无 session 隔离 | G1 | 二 |
141
+ | 无 MCP 工具 | G2 + G3 | 二 |
142
+ | 每次冷启动 | U2 | 三 |
143
+ | 纯静态 enforce | G4 | 二 |
144
+ | trace 并发 | R2 | 一 |
145
+ | 无原子写入 | R4 | 一 |
146
+ | 无 stale 清理 | R5 | 一 |
147
+
148
+ ## DNA 已有优势(保持)
149
+
150
+ | 优势 | 不要丢 |
151
+ |------|--------|
152
+ | 纯函数 enforce | 不引入 I/O 到 enforce.ts |
153
+ | 编译时 IR | 保持预编译模式,MCP 是补充不是替代 |
154
+ | Fail-open | 所有错误 → allow |
155
+ | 确定性 | 同 IR + 同 input = 同 output |
156
+
157
+ ## Acceptance Criteria
158
+
159
+ ### 第一阶段
160
+ - [ ] dna compile/sync 编译前调用 validateDNA()
161
+ - [ ] trace 写入 session 隔离或原子追加
162
+ - [ ] IR 版本不匹配 → 明确警告
163
+ - [ ] 所有 state 写入使用 temp+rename
164
+ - [ ] SessionStart 清理 >2h stale state
165
+ - [ ] 测试覆盖所有修复
166
+
167
+ ### 第二阶段
168
+ - [ ] .dna/state/sessions/{id}/ 目录结构
169
+ - [ ] MCP server 提供 state_read/write/trace_query
170
+ - [ ] MCP server 提供 compile/sync/generate
171
+ - [ ] enforce 支持 WorkflowState 条件规则
172
+ - [ ] 远程通信架构预留(接口定义,不实现)
173
+
174
+ ### 第三阶段
175
+ - [ ] dna sync 自动检测模板版本 + 提示升级
176
+ - [ ] MCP server 常驻进程解决冷启动
177
+ - [ ] 错误信息人类可读
@@ -0,0 +1,172 @@
1
+ # Spec: Multi-Config 模板管理
2
+
3
+ ## Metadata
4
+ - Date: 2026-04-15
5
+ - Phase: 6 (持续改进 C2/C3)
6
+ - Status: 设计完成,待实现
7
+
8
+ ## Goal
9
+
10
+ 支持一个项目同时使用多个 DNA 模板,每个模板有独立的 namespace、roles、workflows。统一编译到一个 IR,运行时透明。
11
+
12
+ ## 目录结构
13
+
14
+ ```
15
+ .dna/
16
+ configs/ # 多模板配置目录
17
+ flutter-rewrite.yaml # namespace: frw
18
+ secure-dev.yaml # namespace: sec
19
+ code-review.yaml # namespace: cr
20
+ compiled/
21
+ ir.json # 合并后的单一 IR
22
+ state/
23
+ trace/
24
+ lock # 追踪所有 configs 的 hash
25
+ ```
26
+
27
+ ## 核心设计决策
28
+
29
+ | 决策 | 结论 | 理由 |
30
+ |------|------|------|
31
+ | 目录结构 | `.dna/configs/*.yaml` | 独立目录,清晰分离 |
32
+ | IR 输出 | 合并为单一 ir.json | hooks 只读一个 IR,运行时透明 |
33
+ | Namespace 冲突 | 编译时报错 | 一个 namespace 只属于一个模板,静默覆盖 = 数据丢失 |
34
+ | Gene 冲突 | 合并 codons + 警告 | gene 是项目级共享概念,additive 合并最合理 |
35
+ | 向后兼容 | 暂不考虑 | 新项目直接用 configs/,老项目手动迁移 |
36
+ | 性能 | 无影响 | YAML parse 5 个文件 ~25ms,ir.json KB 级,hook 运行时无差异 |
37
+
38
+ ## Namespace 冲突处理
39
+
40
+ 编译时扫描 `configs/*.yaml`,检测 namespace 重复:
41
+
42
+ ```typescript
43
+ // sync.ts
44
+ const namespaces = new Map<string, string>(); // namespace → filename
45
+ for (const config of configs) {
46
+ if (namespaces.has(config.namespace)) {
47
+ throw new Error(
48
+ `Namespace "${config.namespace}" conflict: ` +
49
+ `${namespaces.get(config.namespace)} and ${config.filename}. ` +
50
+ `Each template must have a unique namespace.`
51
+ );
52
+ }
53
+ namespaces.set(config.namespace, config.filename);
54
+ }
55
+ ```
56
+
57
+ ## Gene 冲突处理
58
+
59
+ 同名 gene 来自不同模板时,合并 codons 并输出警告:
60
+
61
+ ```typescript
62
+ // cascade.ts
63
+ if (mergedGenes[geneName] && sourceNamespace !== existingNamespace) {
64
+ // Different templates define same gene — merge codons
65
+ mergedGenes[geneName].codons.push(...gene.codons);
66
+ warnings.push(
67
+ `Gene "${geneName}" defined in both ${existingNamespace} and ${sourceNamespace}. ` +
68
+ `Codons merged. Verify this is intentional.`
69
+ );
70
+ } else {
71
+ // Same namespace or new gene — normal cascade (higher priority wins)
72
+ mergedGenes[geneName] = gene;
73
+ }
74
+ ```
75
+
76
+ ## 模板版本追踪
77
+
78
+ 每个 config 文件增加 `_template_version` 字段:
79
+
80
+ ```yaml
81
+ _source_template: flutter-rewrite
82
+ _template_version: "1.4.9" # npm 包版本时写入
83
+ ```
84
+
85
+ `dna sync` 时对比:
86
+ 1. 读每个 config 的 `_source_template` + `_template_version`
87
+ 2. 查 npm 包里对应模板的当前版本
88
+ 3. 版本不一致 → 提示 "flutter-rewrite 有新版本 (1.4.9 → 1.5.0),是否升级?(Y/n)"
89
+ 4. 确认 → `upgradeConfig()` 全量替换 + 回写 variables
90
+
91
+ ## dna sync 流程变化
92
+
93
+ ```
94
+ 当前:
95
+ autoDetectDNA() → 找 .dna/config.yaml → 读一个文件 → 编译
96
+
97
+ 新:
98
+ autoDetectConfigs() → 扫描 .dna/configs/*.yaml
99
+ → 读所有文件
100
+ → 检测 namespace 冲突(报错)
101
+ → 检测 gene 冲突(合并 + 警告)
102
+ → 检测模板版本(提示升级)
103
+ → cascadeDNA(all) → 合并为一个 IR
104
+ → 生成全部产物
105
+ ```
106
+
107
+ ## dna init 流程变化
108
+
109
+ ```
110
+ 当前:
111
+ dna init --template flutter-rewrite
112
+ → 写 .dna/config.yaml
113
+
114
+ 新:
115
+ dna init --template flutter-rewrite
116
+ → mkdir -p .dna/configs/
117
+ → 写 .dna/configs/flutter-rewrite.yaml
118
+
119
+ dna init --template secure-dev (追加第二个模板)
120
+ → 检测 namespace 冲突
121
+ → 写 .dna/configs/secure-dev.yaml
122
+ ```
123
+
124
+ ## dna init --upgrade 流程变化
125
+
126
+ ```
127
+ 当前:
128
+ 读 .dna/config.yaml → 升级一个
129
+
130
+ 新:
131
+ dna init --upgrade (升级所有)
132
+ → 扫描 configs/*.yaml
133
+ → 逐个检测版本,逐个升级
134
+
135
+ dna init --upgrade flutter-rewrite (升级指定)
136
+ → 只升级 configs/flutter-rewrite.yaml
137
+ ```
138
+
139
+ ## 对现有功能的影响
140
+
141
+ | 功能 | 改动 |
142
+ |------|------|
143
+ | **autoDetectDNA()** | 改为 `autoDetectConfigs()`,扫描 `configs/*.yaml` |
144
+ | **cascadeDNA()** | 无改动(已支持多 DNA 输入) |
145
+ | **ir.json** | 内容更大(更多 roles/workflows),格式不变 |
146
+ | **hooks (dna-hook)** | 无改动(只读 ir.json) |
147
+ | **agents/*.md** | 无改动(namespace 前缀天然隔离) |
148
+ | **skills/*.md** | 无改动(namespace 前缀天然隔离) |
149
+ | **settings.json** | 无改动(hook 注册逻辑不变) |
150
+ | **CLAUDE.md** | 所有模板的 directives 合并注入 |
151
+ | **lock 文件** | 追踪所有 configs/*.yaml 的 hash |
152
+ | **dna init** | 写到 `configs/` 目录 |
153
+ | **dna init --upgrade** | 支持全量升级和指定升级 |
154
+
155
+ ## 不做
156
+
157
+ - 向后兼容自动迁移(老项目手动 `mv config.yaml configs/`)
158
+ - config 间的 gene 优先级控制(MVP 用合并 + 警告)
159
+ - 运行时动态加载 config(全部编译时处理)
160
+
161
+ ## Acceptance Criteria
162
+
163
+ - [ ] `dna init --template X` 写到 `.dna/configs/X.yaml`
164
+ - [ ] `dna sync` 自动扫描 `configs/*.yaml`
165
+ - [ ] namespace 重复 → 编译报错
166
+ - [ ] 同名 gene 不同模板 → codons 合并 + 警告
167
+ - [ ] `_template_version` 写入 config,sync 时对比提示升级
168
+ - [ ] `dna init --upgrade` 支持全量和指定模板
169
+ - [ ] 多模板编译为单一 ir.json
170
+ - [ ] agents/skills 文件 namespace 隔离正确
171
+ - [ ] lock 文件追踪所有 configs
172
+ - [ ] 测试覆盖:namespace 冲突、gene 合并、多模板编译、升级流程