add-coder 0.1.5 → 0.1.8

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.
@@ -11,6 +11,7 @@
11
11
  - ADD Route: `.qoder/plans/2026-07/08/farm-agent-add-coder-npm-package-add-route-v1.md` ✅
12
12
  - Handoff: `.qoder/plans/2026-07/08/farm-agent-add-coder-npm-package-handoff-v1.md` ✅
13
13
  - Review: `.qoder/reviews/farm-agent-add-coder-npm-package-review-v1.md` ✅
14
+ - Review v2: `.qoder/reviews/farm-agent-add-coder-npm-package-review-v2.md` ✅ — 代码实现评审,2 条 P0(spawnSync catch 失能、ask 重复定义)
14
15
  - **ADD-7 审计策略**:
15
16
 
16
17
  | 文件 | targetType | action | beforeState | afterState | 状态 |
@@ -242,10 +243,11 @@ packages/add-coder/
242
243
  │ │ ├── skills/ # add-paradigm/SKILL.md, session-init/SKILL.md
243
244
  │ │ ├── docs/ # 15 个 .md 模板(plan/spec/tasks/checklist/handoff/review/add-route 等)
244
245
  │ │ ├── docs-schema/ # 15 个 .schema.json
246
+ │ │ ├── plans/ # 示例 plans(含 add-coder 6轮范例)
247
+ │ │ ├── specs/ # 示例 specs(含 add-coder spec/tasks/checklist)
245
248
  │ │ ├── rules/ # project_rules.md, theory-practice-map.toml
246
249
  │ │ ├── vocabulary/ # add-governance-vocabulary.md
247
250
  │ │ ├── scripts/ # mcp-server.ts, add-coder-mcp-server.ts
248
- │ │ ├── prisma/ # add.prisma(ADD 治理模型,init 时复制到用户项目)
249
251
  │ │ ├── reports/ # 7 个 report 模板
250
252
  │ │ └── tools/ # README.md
251
253
  │ │
@@ -273,6 +275,90 @@ packages/add-coder/
273
275
  └── README.md
274
276
  ```
275
277
 
278
+
279
+ # ──── npx add-coder init 后的用户项目结构 ────
280
+
281
+ ```
282
+ 用户项目/
283
+ ├── .add/ # ★ ADD 蓝图(推荐路径)
284
+ │ ├── agents/ # ← templates/core/agents/
285
+ │ ├── skills/ # ← templates/core/skills/
286
+ │ ├── templates/ # ← templates/core/docs/(.md 模板 + .schema.json,扁平)
287
+ │ ├── rules/ # ← templates/core/rules/
288
+ │ ├── vocabulary/ # ← templates/core/vocabulary/
289
+ │ ├── scripts/ # ← templates/core/scripts/
290
+ │ ├── hooks/ # ← templates/adapters/{target}/hooks/
291
+ │ ├── plans/ # ← templates/core/plans/
292
+ │ ├── specs/ # ← templates/core/specs/
293
+ │ ├── reports/ # ← templates/core/reports/
294
+ │ ├── tools/ # ← templates/core/tools/
295
+ │ ├── debug-dump/ # ← templates/shared/debug-dump/(空目录占位)
296
+ │ ├── repowiki/ # ← templates/shared/repowiki/(空目录占位)
297
+ │ ├── reviews/ # ← 用户自建(init 后空目录)
298
+ │ ├── mcp.json # ← templates/adapters/{target}/mcp.json
299
+ │ ├── settings.json # ← templates/adapters/{target}/settings.json
300
+ │ └── sync-policy.json # ← templates/adapters/qoder/sync-policy.json
301
+
302
+ ├── .qoder/ # ★ Qoder IDE 配置
303
+ │ ├── agents/ # ← 同 .add/agents/
304
+ │ ├── skills/ # ← 同 .add/skills/
305
+ │ ├── templates/ # ← 同 .add/templates/
306
+ │ ├── rules/ # ← 同 .add/rules/
307
+ │ ├── vocabulary/ # ← 同 .add/vocabulary/
308
+ │ ├── scripts/ # ← 同 .add/scripts/
309
+ │ ├── hooks/ # ← adapter/qoder/hooks/(Qoder 专属 matcher)
310
+ │ ├── plans/ # ← 同 .add/plans/
311
+ │ ├── specs/ # ← 同 .add/specs/
312
+ │ ├── reports/ # ← 同 .add/reports/
313
+ │ ├── tools/ # ← 同 .add/tools/
314
+ │ ├── debug-dump/ # ← 同 .add/debug-dump/
315
+ │ ├── repowiki/ # ← 同 .add/repowiki/
316
+ │ ├── reviews/ # ← 同 .add/reviews/
317
+ │ ├── mcp.json # ← adapter/qoder/mcp.json
318
+ │ ├── settings.json # ← adapter/qoder/settings.json
319
+ │ └── sync-policy.json # ← adapter/qoder/sync-policy.json
320
+
321
+ ├── .claude/ # ★ Claude Code 配置
322
+ │ ├── agents/ # ← 同 .add/agents/
323
+ │ ├── skills/ # ← 同 .add/skills/
324
+ │ ├── templates/ # ← 同 .add/templates/
325
+ │ ├── rules/ # ← 同 .add/rules/
326
+ │ ├── vocabulary/ # ← 同 .add/vocabulary/
327
+ │ ├── scripts/ # ← 同 .add/scripts/
328
+ │ ├── hooks/ # ← adapter/claude/hooks/(Claude 标准工具名)
329
+ │ ├── plans/ # ← 同 .add/plans/
330
+ │ ├── specs/ # ← 同 .add/specs/
331
+ │ ├── reports/ # ← 同 .add/reports/
332
+ │ ├── tools/ # ← 同 .add/tools/
333
+ │ ├── debug-dump/ # ← 同 .add/debug-dump/
334
+ │ ├── repowiki/ # ← 同 .add/repowiki/
335
+ │ ├── reviews/ # ← 同 .add/reviews/
336
+ │ ├── mcp.json # ← adapter/claude/mcp.json
337
+ │ └── settings.json # ← adapter/claude/settings.json
338
+
339
+ ├── .vscode/ # ★ VS Code 配置
340
+ │ ├── settings.json # ← templates/adapters/vscode/settings.json
341
+ │ ├── launch.json # ← templates/adapters/vscode/launch.json
342
+ │ ├── tasks.json # ← templates/adapters/vscode/tasks.json
343
+ │ └── extensions.json # ← templates/adapters/vscode/extensions.json
344
+
345
+ ├── prisma/
346
+ │ ├── schema.prisma # 用户已有或 prisma init 创建
347
+ │ └── add.prisma # injectPrisma 注入(来自 npm 包内置)
348
+
349
+ └── .env.development # prisma init 创建,用户配置 DATABASE_URL
350
+ ```
351
+
352
+ > **部署策略**:`templates/core/` 内容复制到 `.add/`、`.qoder/`、`.claude/` 三个目录。
353
+ > IDE 只认自身的 magic path(`.qoder/` / `.claude/`),必须各自包含完整内容。
354
+ > `.add/` 保留作为 add-coder 推荐蓝图,供未来工具链引用。
355
+
356
+ > **⚠️ 改造待执行**:当前 `renderCore()` 仅输出到 `.add/`,需改造为三目录并行输出:
357
+ > 1. `src/core/renderer.ts`:`renderCore()` 返回值改为 `Map<string, string>[]`(三份)
358
+ > 2. `src/cli/commands/init.ts`:core 文件写入 `.add/` `.qoder/` `.claude/` 三个目标
359
+ > 3. adapter renderers:不再单独部署 core 内容,只处理 adapter 专属文件(hooks/mcp/settings)
360
+ > 4. 写入去重:三目录同名文件只写一次,后两次 skip(内容相同)
361
+
276
362
  **关键命名决策**:旧的 `templates/templates/` 嵌套命名 → 改为 `templates/core/docs/`,避免 `templates/templates/` 这种令人困惑的路径。
277
363
 
278
364
  **渲染器路径约定**:
@@ -366,6 +452,25 @@ const adapterDir = path.join(TEMPLATES_ROOT, 'adapters', target)
366
452
  > **add-coder 作为通用转录引擎**:类似 Prisma 的 `prisma generate`——读 caijue.toml,生成策略代码。
367
453
  > 引擎本身不关心 caijue.toml 的内容来源,只负责"TOML → TS"的编译时转录。
368
454
 
455
+ #### 3.5.1 实际实现架构(偏离原 Plan §3.5.2,以 handoff §7 为准)
456
+
457
+ ```
458
+ caijue.toml(索引) *-rules.toml(规则参数) strategies/*.ts
459
+ ───────────────── ──────────────────── ┌──────────────────┐
460
+ [[caijue]] detect-rules.toml → │ GENERATED (转录) │
461
+ rules = "detect-rules" prisma-rules.toml → │ DETECT_RULES=[] │
462
+ impl = "strategies/..." adapter-rules.toml → │ PRISMA_CONFIG={} │
463
+ writer-rules.toml → ├──────────────────┤
464
+ │ USER CODE (手写) │
465
+ 转录流程: │ detectIDE() │
466
+ npm run generate → transcribe.ts → GENERATED 区块 │ injectPrisma() │
467
+ └──────────────────┘
468
+
469
+ - caijue.toml 只做索引,不耦合规则逻辑
470
+ - *-rules.toml 定义规则参数,transcribe.ts 产出 GENERATED 区块
471
+ - USER CODE 手写业务逻辑,npm run generate 不覆盖
472
+ ```
473
+
369
474
  #### 3.5.1 裁决点清单
370
475
 
371
476
  | 裁决点 | 当前位置 | 决策内容 | 归入 caijue |
@@ -525,6 +630,8 @@ packages/add-coder/
525
630
 
526
631
  **验收**:在空白 Prisma 项目中执行 `prisma migrate dev --schema=prisma/` 成功创建 DevOperation 和 AuditLog 表
527
632
 
633
+ > **Review v2 回流(P0-1)**:`prisma-injector.ts` 中 `spawnSync` 返回状态须检查 `r.status`,不可依赖 try/catch。迁移失败时 `add.prisma` 回滚删除。
634
+
528
635
  #### Task 1: 清理硬编码 + 参数化 core 模板
529
636
 
530
637
  **范围**:`packages/add-coder/templates/` 下所有文件(共 ~70 个文件)
@@ -741,6 +848,9 @@ packages/add-coder/
741
848
 
742
849
  **回流机制**:
743
850
  - Review 发现的 P0/P1 问题回写至 Plan 对应章节
851
+ - Review v2 P0-1(spawnSync catch 失能):已在 Task 0 补充验收项
852
+ - Review v2 P0-2(ask 重复定义):已记录至 Task 1(CaijueHub 重构时一并提取到 utils)
853
+ - Review v2 P1-1/2/3(unlinkSync 缺口、gitignore):已记录至 CI 基建清单
744
854
  - 回流完成后方可进入 Step 1 实施
745
855
  - P2 问题在 Step 3 实施前解决或记录为 Known Issue
746
856
 
@@ -23,9 +23,9 @@ import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js"
23
23
  import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js"
24
24
  import { z } from "zod"
25
25
  import { readFile, readdir, stat, mkdir, writeFile } from "fs/promises"
26
- import { join, relative, basename } from "path"
26
+ import { join, relative } from "path"
27
27
  import { existsSync } from "fs"
28
- import { execSync } from "child_process"
28
+ import { spawnSync } from "child_process"
29
29
  import { PrismaClient, Prisma } from "@prisma/client"
30
30
 
31
31
  const prisma = new PrismaClient({
@@ -51,6 +51,31 @@ async function readFileSafe(filePath: string): Promise<string | null> {
51
51
  }
52
52
  }
53
53
 
54
+ // ── ADD 文档模板校验(共享函数,供 create_plan / check_add_compliance / check_spec_sync 等复用)──
55
+
56
+ interface GuardResult { ok: boolean; issues: string }
57
+
58
+ async function validateDocWithGuard(filePath: string): Promise<GuardResult> {
59
+ const guardScript = join(PROJECT_ROOT, MAGIC_DIR, "hooks", "doc-format-guard.sh")
60
+ if (!existsSync(guardScript)) return { ok: true, issues: "" }
61
+ const content = await readFileSafe(filePath)
62
+ if (!content) return { ok: false, issues: "文件无法读取" }
63
+ const guardInput = JSON.stringify({
64
+ tool_input: { file_path: filePath, file_content: content },
65
+ })
66
+ const guard = spawnSync("/bin/bash", [guardScript], {
67
+ input: guardInput,
68
+ encoding: "utf-8",
69
+ timeout: 15000,
70
+ cwd: PROJECT_ROOT,
71
+ env: { ...process.env, MAGIC_DIR },
72
+ })
73
+ if (guard.status === 2 || guard.stderr?.includes("校验不通过")) {
74
+ return { ok: false, issues: guard.stderr || guard.stdout || "校验失败" }
75
+ }
76
+ return { ok: true, issues: "" }
77
+ }
78
+
54
79
  /** 递归读取目录下所有文件(扁平化返回),支持 ${MAGIC_DIR}/plans/2026-06/05/ 等分层结构 */
55
80
  async function readdirRecursive(dir: string): Promise<string[]> {
56
81
  const results: string[] = []
@@ -2080,7 +2105,9 @@ server.registerTool(
2080
2105
  return errorResponse(`未找到匹配的 Plan 文件(关键词: ${args.planKeyword})`)
2081
2106
  }
2082
2107
  const planPath = join(plansDir, planMatch)
2108
+ const planGuard = await validateDocWithGuard(planPath)
2083
2109
  lines.push(`Plan: ${planMatch}`)
2110
+ if (!planGuard.ok) lines.push(` ⚠️ 模板校验: ${planGuard.issues.split("\n")[0]}`)
2084
2111
 
2085
2112
  // 2. 从 Plan 中提取关联的 spec 目录名
2086
2113
  const planContent = await readFileSafe(planPath)
@@ -2387,9 +2414,10 @@ server.registerTool(
2387
2414
  // 5. Git diff 统计
2388
2415
  let changedFiles: string[] = []
2389
2416
  if (isEnabled("gitDiff")) {
2390
- const { execSync } = await import("child_process")
2417
+ const { spawnSync } = await import("child_process")
2391
2418
  try {
2392
- const diffStat = execSync("git diff --name-only", { cwd: PROJECT_ROOT, encoding: "utf-8", timeout: 5000 })
2419
+ const diff = spawnSync("git", ["diff", "--name-only"], { cwd: PROJECT_ROOT, encoding: "utf-8", timeout: 5000 })
2420
+ const diffStat = diff.stdout || ""
2393
2421
  changedFiles = diffStat.trim().split("\n").filter(Boolean)
2394
2422
  } catch {
2395
2423
  lines.push("Git diff: 无法获取(可能无 git 仓库或无暂存变更)")
@@ -2576,6 +2604,9 @@ server.registerTool(
2576
2604
  return errorResponse(`add-route 完整性扫描失败:无法读取文件 ${matchedFile}`)
2577
2605
  }
2578
2606
 
2607
+ // 模板格式校验
2608
+ const routeGuard = await validateDocWithGuard(filePath)
2609
+
2579
2610
  const lines = content.split("\n")
2580
2611
 
2581
2612
  // === 3. 解析 Step 结构和勾选状态 ===
@@ -2681,6 +2712,7 @@ server.registerTool(
2681
2712
  "",
2682
2713
  `整体完成度: ${globalChecked}/${globalTotal} (${completionRate}%)`,
2683
2714
  `状态: ${isComplete ? "✅ complete — add-route 完整闭环" : "⚠️ incomplete — 存在未勾选 Step"}`,
2715
+ `模板校验: ${routeGuard.ok ? "✅ 通过" : `⚠️ ${routeGuard.issues.split("\n")[0]}`}`,
2684
2716
  "",
2685
2717
  ]
2686
2718
 
@@ -3198,8 +3230,9 @@ server.registerTool(
3198
3230
  let scopeScore = 100
3199
3231
  let changedFiles: string[] = []
3200
3232
  try {
3201
- const { execSync } = await import("child_process")
3202
- const diffStat = execSync("git diff --name-only", { cwd: PROJECT_ROOT, encoding: "utf-8", timeout: 5000 })
3233
+ const { spawnSync } = await import("child_process")
3234
+ const diff = spawnSync("git", ["diff", "--name-only"], { cwd: PROJECT_ROOT, encoding: "utf-8", timeout: 5000 })
3235
+ const diffStat = diff.stdout || ""
3203
3236
  changedFiles = diffStat.trim().split("\n").filter(Boolean)
3204
3237
  } catch { /* ignore */ }
3205
3238
 
@@ -3240,12 +3273,13 @@ server.registerTool(
3240
3273
  // ====== 维度二:类型安全(权重 20%) ======
3241
3274
  let typeScore = 100
3242
3275
  try {
3243
- const { execSync } = await import("child_process")
3244
- const tscOut = execSync("npx tsc --noEmit 2>&1 || true", {
3276
+ const { spawnSync } = await import("child_process")
3277
+ const tsc = spawnSync("npx", ["tsc", "--noEmit"], {
3245
3278
  cwd: PROJECT_ROOT,
3246
3279
  encoding: "utf-8",
3247
3280
  timeout: 30000,
3248
3281
  })
3282
+ const tscOut = (tsc.stdout || "") + (tsc.stderr || "")
3249
3283
  // 统计 error TS 行数
3250
3284
  const errorLines = tscOut.split("\n").filter(l => l.includes("error TS")).length
3251
3285
  typeScore = Math.max(0, 100 - errorLines * 10)
@@ -3431,20 +3465,28 @@ server.registerTool(
3431
3465
 
3432
3466
  // 写入 Plan 文件
3433
3467
  await writeFile(filePath, content, "utf-8")
3468
+
3469
+ // 写后校验
3470
+ const guard = await validateDocWithGuard(filePath)
3471
+ if (!guard.ok) {
3472
+ try { await import("fs/promises").then(m => m.unlink(filePath)) } catch { }
3473
+ return errorResponse(`Plan 模板校验不通过,拒绝写入:\n${guard.issues}`)
3474
+ }
3475
+
3434
3476
  parts.push(`✅ Plan 文件已创建: ${relativePath}`)
3435
3477
 
3436
3478
  // 更新 index.md
3437
3479
  const indexScript = join(PROJECT_ROOT, "scripts", "gen-plan-index.sh")
3438
3480
  if (existsSync(indexScript)) {
3439
- try {
3440
- const result = execSync(`/bin/bash "${indexScript}"`, {
3441
- cwd: PROJECT_ROOT,
3442
- encoding: "utf-8",
3443
- timeout: 10000,
3444
- })
3445
- parts.push(`📋 index.md 已更新: ${result.trim()}`)
3446
- } catch (indexErr) {
3447
- parts.push(`⚠️ index.md 更新失败(将在 crontab 或下次调用时自动更新): ${indexErr instanceof Error ? indexErr.message : String(indexErr)}`)
3481
+ const result = spawnSync("/bin/bash", [indexScript], {
3482
+ cwd: PROJECT_ROOT,
3483
+ encoding: "utf-8",
3484
+ timeout: 10000,
3485
+ })
3486
+ if (result.status === 0) {
3487
+ parts.push(`📋 index.md 已更新: ${result.stdout?.trim() || "ok"}`)
3488
+ } else {
3489
+ parts.push(`⚠️ index.md 更新失败: ${result.stderr?.trim() || result.error?.message || "unknown"}`)
3448
3490
  }
3449
3491
  } else {
3450
3492
  parts.push(`⚠️ gen-plan-index.sh 不存在,index.md 将在 crontab 自动更新`)
@@ -17,7 +17,7 @@
17
17
 
18
18
  | 轮次 | 变更概要 | 涉及文件 |
19
19
  |:--:|------|------|
20
- | 1 | Prisma 模型准备 + 清理硬编码 | `templates/core/prisma/add.prisma`、`src/cli/prisma-injector.ts`、`templates/` 下约 70 个文件 |
20
+ | 1 | Prisma 模型准备 + 清理硬编码 | `src/cli/prisma-injector.ts`(add.prisma 内置在 npm 包)、`templates/` 下约 70 个文件 |
21
21
  | 2 | 模板目录重组 + 适配器架构搭建 | `templates/core/`、`src/adapters/`、`src/core/`、旧 `templates/` 迁移 |
22
22
  | 3 | 三端适配器实现(Claude → Qoder → VS Code 串行) | 见下方 Hook 清单 |
23
23
  | 4 | 配置系统(Zod schema)+ CLI 重写 | `src/config/`、`src/cli/`、`bin/add-coder.js`、`tsup.config.ts` |
@@ -171,6 +171,17 @@
171
171
  - **WHEN** 重复执行 `add-coder init`
172
172
  - **THEN** `prisma migrate dev` 不报错(已应用的迁移自动跳过)
173
173
 
174
+ #### Scenario: 三目录部署(core 内容同步到 IDE magic path)
175
+
176
+ - **WHEN** 执行 `init` 渲染 core 模板后
177
+ - **THEN** `templates/core/` 的所有文件 SHALL 同时写入 `.add/`、`.qoder/`、`.claude/` 三个目标目录
178
+ - **AND** adapter renderers SHALL 只处理 adapter 专属文件(hooks/mcp.json/settings.json/sync-policy.json),不再单独渲染 core 内容
179
+
180
+ **改造步骤**:
181
+ 1. `src/core/renderer.ts`:`renderCore()` 返回值改为输出三份文件的映射(`.add/` + `.qoder/` + `.claude/`)
182
+ 2. `src/cli/commands/init.ts`:写入阶段对三目录同名文件去重(内容相同则 skip)
183
+ 3. `src/adapters/{claude,qoder,vscode}/renderer.ts`:删除 core 渲染逻辑,只保留 hooks + mcp + settings 等 adapter 专属文件
184
+
174
185
  ---
175
186
 
176
187
  ### Requirement: 适配器三层架构
@@ -317,10 +328,15 @@
317
328
 
318
329
  系统 SHALL 在空白项目中通过完整的 init 流程验证。
319
330
 
320
- #### Scenario: 零配置可用
331
+ #### Scenario: 空项目 init
321
332
 
322
333
  - **WHEN** 在空白项目中执行 `npx add-coder init`
323
- - **THEN** 零配置生成完整 ADD 模板(skills/agents/templates/rules/hooks 自动就位)
334
+ - **THEN** `.add/` `.qoder/` `.claude/` 三目录均含完整 ADD 内容(agents/skills/templates/rules/vocabulary/scripts/hooks/plans/specs/reports/tools),`.vscode/` 含 IDE 配置
335
+
336
+ #### Scenario: plans/specs 示例
337
+
338
+ - **WHEN** 用户首次 init
339
+ - **THEN** `.qoder/plans/` 含 add-coder 6轮范例,`.qoder/specs/` 含 spec/tasks/checklist 范例
324
340
 
325
341
  #### Scenario: 三端兼容
326
342