add-coder 0.3.25-0 → 0.3.26

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.
Files changed (33) hide show
  1. package/README.en.md +10 -10
  2. package/README.md +20 -46
  3. package/dist/index.js +6 -69
  4. package/package.json +1 -1
  5. package/templates/.add-coder-src-hash.json +24 -28
  6. package/templates/adapters/claude/hooks/doc-format-guard.sh +2 -2
  7. package/templates/adapters/codex/hooks/doc-format-guard.sh +2 -2
  8. package/templates/adapters/qoder/hooks/doc-format-guard.sh +2 -2
  9. package/templates/adapters/trae/hooks/doc-format-guard.sh +2 -2
  10. package/templates/adapters/vscode/hooks/doc-format-guard.sh +2 -2
  11. package/templates/core/hooks/doc-format-guard.sh +2 -2
  12. package/templates/core/scripts/db-ensure.sh +0 -8
  13. package/templates/core/scripts/mcp-server/shared/db-types.ts +208 -0
  14. package/templates/core/scripts/mcp-server/shared/response.ts +2 -3
  15. package/templates/core/scripts/mcp-server/tools/audit.ts +10 -7
  16. package/templates/core/scripts/mcp-server/tools/context.ts +19 -0
  17. package/templates/core/scripts/mcp-server/tools/contract.ts +6 -4
  18. package/templates/core/scripts/mcp-server/tools/gateway/check_rahs.ts +2 -69
  19. package/templates/core/scripts/mcp-server/tools/gateway/check_spec_sync.ts +1 -1
  20. package/templates/core/scripts/mcp-server/tools/hitl.ts +116 -34
  21. package/templates/core/scripts/mcp-server/tools/index.ts +1 -66
  22. package/templates/core/scripts/mcp-server/tools/plan.ts +33 -16
  23. package/templates/core/scripts/mcp-server/tools/review.ts +12 -9
  24. package/templates/core/scripts/mcp-server.ts +1 -2
  25. package/templates/core/templates/simple-plan-template.md +13 -0
  26. package/templates/core/templates/simple-plan-template.schema.json +10 -0
  27. package/templates/core/templates/standard-plan-template.md +13 -0
  28. package/templates/core/templates/standard-plan-template.schema.json +10 -0
  29. package/templates/adapters/codex/config.toml.example +0 -20
  30. package/templates/core/plans/2026-07/08/add-coder-npm-package-add-route-v1.md +0 -331
  31. package/templates/core/plans/2026-07/08/add-coder-npm-package-handoff-v1.md +0 -686
  32. package/templates/core/plans/2026-07/08/add-coder-npm-package-plan-v1.md +0 -895
  33. package/templates/core/scripts/mcp-server/shared/redact.ts +0 -28
@@ -2,14 +2,18 @@ import * as z from "zod/v4"
2
2
  import { inputRequired, acceptedContent } from "@modelcontextprotocol/server"
3
3
  import type { ToolRegistrar } from "./registrar.js"
4
4
  import { writeFileSync, mkdirSync, existsSync, readFileSync } from "fs"
5
- import { join, basename } from "path"
5
+ import { join, basename, relative } from "path"
6
6
  import { textResponse, errorResponse } from "../shared/response.js"
7
7
  import { PROJECT_ROOT, MAGIC_DIR } from "../shared/fs.js"
8
8
  import { prisma } from "../shared/prisma.js"
9
9
  import { HITL_INTERACTION_CONFIG } from "../shared/hitl-interaction.strategy.js"
10
+ import type { HitlRow, PlanRow } from "../shared/db-types.js"
11
+ import { HitlRowSchema, PlanRowSchema, validatedDelegate } from "../shared/db-types.js"
10
12
 
13
+ // 无类型边界单点(zod 托管):动态加载的 prisma client 在此一次性转为运行期校验的泛型委托
11
14
  const db = {
12
- get hitl() { return prisma.hitlRecord as unknown as Record<string, (...a: unknown[]) => unknown> },
15
+ get hitl() { return validatedDelegate<HitlRow>(prisma.hitlRecord, HitlRowSchema, "HitlRecord") },
16
+ get plan() { return validatedDelegate<PlanRow>(prisma.planRecord, PlanRowSchema, "PlanRecord") },
13
17
  }
14
18
 
15
19
  export function registerHitlTools(server: ToolRegistrar) {
@@ -22,18 +26,32 @@ export function registerHitlTools(server: ToolRegistrar) {
22
26
 
23
27
  // genui 模式客户端(如 Qoder)未声明 elicitation capability,弹框必然失败 → 返回 genui 流程引导
24
28
  function _genuiGuide(recallStep: string): string {
29
+ // D2(Review 回流):widget 模板多候选探测——sync 实际落脚含 core/ 层级,
30
+ // toml 原值为源仓路径(消费方不存在),逐候选回退,全 miss 显式告警。
31
+ // 输出必须为 workspace 相对路径:genui show_widget 实测拒绝绝对路径(2026-08-11 验证)。
25
32
  let widgetPath = ""
26
33
  if (_interaction.widget_path) {
27
- // genui show_widget 仅接受 workspace 相对路径,绝对路径会被拒
28
- const bakedRel = join(MAGIC_DIR, "templates", basename(_interaction.widget_path))
29
- widgetPath = existsSync(join(PROJECT_ROOT, bakedRel)) ? bakedRel : _interaction.widget_path
34
+ const base = basename(_interaction.widget_path)
35
+ const candidates = [
36
+ join(PROJECT_ROOT, MAGIC_DIR, "templates", "core", "templates", base),
37
+ join(PROJECT_ROOT, MAGIC_DIR, "templates", base),
38
+ join(PROJECT_ROOT, _interaction.widget_path),
39
+ ]
40
+ const abs = candidates.find((p) => existsSync(p)) ?? ""
41
+ widgetPath = abs ? relative(PROJECT_ROOT, abs) : ""
30
42
  }
31
- return [
43
+ const lines = [
32
44
  `⚠️ 当前环境(${MAGIC_DIR})客户端不支持 elicitation 弹框,交互模式已裁决为 genui。请按以下流程完成:`,
33
- `1. 调用 genui show_widget 渲染审批表单${widgetPath ? `(widget_path: ${widgetPath},workspace 相对路径,维度列表经 data 注入)` : ""}`,
34
- `2. 用户在 widget 中逐项拍板`,
35
- `3. ${recallStep}`,
36
- ].join("\n")
45
+ ]
46
+ if (widgetPath) {
47
+ lines.push(`1. 调用 genui show_widget 渲染审批表单(widget_path: ${widgetPath},workspace 相对路径,维度列表经 data 注入)`)
48
+ } else {
49
+ lines.push(`1. ⚠️ widget 模板缺失(hitl-approval-widget.html 未同步到 ${MAGIC_DIR}/templates/),请先执行 add-coder sync 补齐,再走 genui show_widget 流程`)
50
+ lines.push(` - 兜底可用 inline widget_code 渲染;数据契约: window.__WIDGET_DATA__ = { planName, type, dimensions: [{name, content}] }`)
51
+ }
52
+ lines.push(`2. 用户在 widget 中逐项拍板`)
53
+ lines.push(`3. ${recallStep}`)
54
+ return lines.join("\n")
37
55
  }
38
56
 
39
57
  // ═══════════════ 辅助:解析 inputRequired 响应 ═══════════════
@@ -115,6 +133,19 @@ export function registerHitlTools(server: ToolRegistrar) {
115
133
  try {
116
134
  const { planName, type, dimensions, _fallback, _use_genui } = args as { planName: string; type: string; dimensions?: { name: string; content?: string }[]; _fallback?: boolean; _use_genui?: boolean }
117
135
 
136
+ // ── planName 入口强校验(弱模型友好:不合规返回可照抄修正调用,而非让错误漂移) ──
137
+ const _pnValid = /-(plan|collab-contract)-v\d+$/.test(planName)
138
+ || /-review(-v\d+|-implementation(-v\d+)?|-runtime(-v\d+)?)?$/.test(planName)
139
+ || planName.endsWith(".hitl")
140
+ if (!_pnValid) {
141
+ const suggested = type === "COLLAB_CONTRACT" ? `${planName}-collab-contract-v1` : `${planName}-plan-v1`
142
+ return errorResponse(
143
+ `planName 格式不合规: "${planName}"\n` +
144
+ `要求: 以 -plan-v{n} / -collab-contract-v{n} / -review-* 结尾(哨兵命名依赖此后缀剥离)\n` +
145
+ `可照抄修正: create_hitl({ planName: "${suggested}", type: "${type}" })`
146
+ )
147
+ }
148
+
118
149
  // ── 最终维度内容(从弹框结果合并) ──
119
150
  let finalDims: { name: string; content: string }[] = []
120
151
 
@@ -210,21 +241,44 @@ export function registerHitlTools(server: ToolRegistrar) {
210
241
  }
211
242
 
212
243
  // ── 原始业务逻辑(创建 DB + 生成 hitl.md。降级模式也走此路) ──
244
+ const now = new Date()
245
+ const yyyyMM = `${now.getFullYear()}-${String(now.getMonth()+1).padStart(2,"0")}`
246
+ const dd = String(now.getDate()).padStart(2,"0")
247
+ const plansDir = join(PROJECT_ROOT, MAGIC_DIR, "plans", yyyyMM, dd)
248
+ const proposalPath = join(plansDir, `${planName}.hitl.md`)
249
+
250
+ // ── FK 断环修复:HitlRecord.planName 外键要求 PlanRecord 先行。
251
+ // 全新 Plan 首次审批时自动预置占位行(约定式 planPath,文件存在性即指纹),
252
+ // Plan 文件写出后由 plan_track 回刷真实路径——机器占位机器回刷,不消耗 LLM 认知。
253
+ // D5(Review 回流):占位路径按 type 分流——PLAN_REVIEW 指向 reviews/ 目录,
254
+ // review 文档写出后由 review_track/plan_track 回刷消解(避免永久悬空)。 ──
255
+ let planProvisioned = false
256
+ const existingPlan = await db.plan.findFirst({ where: { planName } })
257
+ if (!existingPlan) {
258
+ const placeholderPath = type === "PLAN_REVIEW"
259
+ ? join(PROJECT_ROOT, MAGIC_DIR, "reviews", yyyyMM, dd, `${planName}.md`)
260
+ : join(plansDir, `${planName}.md`)
261
+ await db.plan.create({
262
+ data: {
263
+ planName,
264
+ planPath: placeholderPath,
265
+ planKeyword: String(planName).replace(/-(plan|collab-contract|review)-v\d+$/, ""),
266
+ totalTasks: 0, doneTasks: 0, checklistT: 0, checklistTDone: 0, checklistR: 0,
267
+ },
268
+ })
269
+ planProvisioned = true
270
+ }
271
+
213
272
  const rows = await db.hitl.findMany({
214
273
  where: { planName },
215
274
  orderBy: { round: "desc" },
216
275
  take: 1,
217
- }) as { round: number | null }[]
276
+ })
218
277
  const round = (rows[0]?.round ?? 0) + 1
219
278
  const record = await db.hitl.create({
220
- data: { planName, round, type, status: "DRAFT" },
221
- }) as { id: string }
279
+ data: { planName, round, type: type as HitlRow["type"], status: "DRAFT" },
280
+ })
222
281
  // 生成 hitl.md
223
- const now = new Date()
224
- const yyyyMM = `${now.getFullYear()}-${String(now.getMonth()+1).padStart(2,"0")}`
225
- const dd = String(now.getDate()).padStart(2,"0")
226
- const plansDir = join(PROJECT_ROOT, MAGIC_DIR, "plans", yyyyMM, dd)
227
- const proposalPath = join(plansDir, `${planName}.hitl.md`)
228
282
  const isoNow = now.toISOString()
229
283
 
230
284
  // 动态生成维度表格行
@@ -270,10 +324,18 @@ export function registerHitlTools(server: ToolRegistrar) {
270
324
  `recordId: ${record.id}`,
271
325
  ]
272
326
  if (finalDims.length > 0) lines.push(`dimensions: ${finalDims.length} 项`)
327
+ if (planProvisioned) lines.push(`PlanRecord: 自动预置(占位行,Plan 文件写出后 plan_track 回刷真实路径)`)
273
328
  if (_fallback) lines.push(`mode: _fallback (跳过 dialog,原始代码降级)`)
274
329
  return textResponse(lines.join("\n"))
275
330
  } catch (e) {
276
- return errorResponse(`create_hitl 失败: ${e instanceof Error ? e.message : String(e)}`)
331
+ const msg = e instanceof Error ? e.message : String(e)
332
+ if (msg.includes("Foreign key")) {
333
+ return errorResponse(
334
+ `create_hitl 外键异常(PlanRecord 自动预置可能失败): ${msg}\n` +
335
+ `处方: 检查数据库连接与 PlanRecord 表结构,或改用 plan_track({ planName }) 先建立追踪记录后重试`
336
+ )
337
+ }
338
+ return errorResponse(`create_hitl 失败: ${msg}`)
277
339
  }
278
340
  })
279
341
 
@@ -296,7 +358,13 @@ export function registerHitlTools(server: ToolRegistrar) {
296
358
  }),
297
359
  }, async (args: Record<string, unknown>, ctx: Record<string, unknown>) => {
298
360
  try {
299
- const { planName, type, status, reason, _fallback, _use_genui } = args as Record<string, string | boolean | undefined>
361
+ const _raw = args as Record<string, string | boolean | undefined>
362
+ const planName = String(_raw.planName ?? "")
363
+ const type = String(_raw.type ?? "")
364
+ const status = typeof _raw.status === "string" ? _raw.status : undefined
365
+ const reason = typeof _raw.reason === "string" ? _raw.reason : undefined
366
+ const _fallback = _raw._fallback === true
367
+ const _use_genui = _raw._use_genui === true
300
368
 
301
369
  // ── 交互式确认(非降级模式且非 genui 模式) ──
302
370
  let effectiveStatus = status as string | undefined
@@ -384,26 +452,38 @@ export function registerHitlTools(server: ToolRegistrar) {
384
452
  // 降级模式必须传 status
385
453
 
386
454
  // ── 原始业务逻辑(更新 DB + 写哨兵文件。降级模式也走此路) ──
387
- const s = effectiveStatus!
455
+ const s = effectiveStatus as HitlRow["status"]
456
+ const qType = type as HitlRow["type"]
388
457
  const rows = await db.hitl.findMany({
389
- where: { planName, type },
458
+ where: { planName, type: qType },
390
459
  orderBy: { round: "desc" },
391
460
  take: 1,
392
- }) as Record<string, unknown>[]
461
+ })
393
462
  if (!rows.length) return errorResponse(`未找到 HITL 记录: planName=${planName}, type=${type}`)
394
463
  const r = rows[0]
395
- const prevStatus = r.status as string
464
+ const prevStatus = r.status
396
465
  if (prevStatus === "TONGYI" || prevStatus === "BOHUI") {
397
466
  return errorResponse(`HITL 已终态(${prevStatus}),不可再更新。BOHUI 后请用 create_hitl 新建 round。`)
398
467
  }
399
468
  // 更新
400
- const data: Record<string, unknown> = { status: s }
469
+ const data: Partial<HitlRow> = { status: s }
401
470
  if (s === "TONGYI") data.approvedAt = new Date()
402
471
  if (s === "BOHUI") {
403
472
  data.rejectedAt = new Date()
404
- if (reason) data.rejectReason = reason
473
+ if (reason) data.rejectReason = String(reason)
405
474
  }
406
475
  await db.hitl.update({ where: { id: r.id }, data })
476
+ // ── BOHUI 清算:Plan 文件不存在 → 预置 PlanRecord 无存续理由,连同本 plan 全部 HitlRecord 删除(孤儿即时归零)。
477
+ // 已写出文件的驳回 Plan 保留记录(历史价值)。 ──
478
+ let orphanCleaned = ""
479
+ if (s === "BOHUI") {
480
+ const planRow = await db.plan.findFirst({ where: { planName } })
481
+ if (planRow && !existsSync(planRow.planPath)) {
482
+ await db.hitl.deleteMany({ where: { planName } })
483
+ await db.plan.delete({ where: { id: planRow.id } })
484
+ orphanCleaned = `预置数据已清算(Plan 文件不存在: ${planRow.planPath})`
485
+ }
486
+ }
407
487
  // 写哨兵文件(双命名:原始 planName + pre-tool-use hook §C 剥后缀推导名,保持闸门一致)
408
488
  const markerDir = join(PROJECT_ROOT, MAGIC_DIR, "hitl")
409
489
  mkdirSync(markerDir, { recursive: true })
@@ -440,7 +520,8 @@ export function registerHitlTools(server: ToolRegistrar) {
440
520
  if (s === "BOHUI") {
441
521
  lines.push(`marker: ${marker}`)
442
522
  lines.push(`reason: ${reason || "—"}`)
443
- lines.push(``, `💡 驳回后请用 create_hitl 新建 round ${Number(r.round) + 1} 重新发起审批。`)
523
+ if (orphanCleaned) lines.push(`cleanup: ${orphanCleaned}`)
524
+ lines.push(``, `💡 驳回后请用 create_hitl 新建 round ${r.round + 1} 重新发起审批。`)
444
525
  }
445
526
  return textResponse(lines.join("\n"))
446
527
  } catch (e) {
@@ -458,11 +539,12 @@ export function registerHitlTools(server: ToolRegistrar) {
458
539
  }, async (args: Record<string, unknown>) => {
459
540
  try {
460
541
  const { planName, type } = args as { planName: string; type?: string }
542
+ const qType = (type || "PLAN") as HitlRow["type"]
461
543
  const rows = await db.hitl.findMany({
462
- where: { planName, type },
544
+ where: { planName, type: qType },
463
545
  orderBy: { round: "desc" },
464
546
  take: 1,
465
- }) as Record<string, unknown>[]
547
+ })
466
548
  if (!rows.length) {
467
549
  return textResponse(
468
550
  `📋 HITL: 未发起\n` +
@@ -472,7 +554,7 @@ export function registerHitlTools(server: ToolRegistrar) {
472
554
  )
473
555
  }
474
556
  const r = rows[0]
475
- const status = r.status as string
557
+ const status = r.status
476
558
  const icons: Record<string, string> = { DRAFT: "⏳", SUBMITTED: "📤", TONGYI: "✅", BOHUI: "❌" }
477
559
  const icon = icons[status] || "❓"
478
560
  const lines = [
@@ -481,13 +563,13 @@ export function registerHitlTools(server: ToolRegistrar) {
481
563
  `planName: ${planName}`,
482
564
  `type: ${r.type}`,
483
565
  `round: ${r.round}`,
484
- r.createdAt ? `createdAt: ${(r.createdAt as Date).toISOString()}` : null,
485
- r.approvedAt ? `approvedAt: ${(r.approvedAt as Date).toISOString()}` : null,
486
- r.rejectedAt ? `rejectedAt: ${(r.rejectedAt as Date).toISOString()}` : null,
566
+ r.createdAt ? `createdAt: ${r.createdAt.toISOString()}` : null,
567
+ r.approvedAt ? `approvedAt: ${r.approvedAt.toISOString()}` : null,
568
+ r.rejectedAt ? `rejectedAt: ${r.rejectedAt.toISOString()}` : null,
487
569
  r.rejectReason ? `reason: ${r.rejectReason}` : null,
488
570
  ].filter(Boolean)
489
571
  if (status === "BOHUI") {
490
- lines.push(``, `💡 驳回后可 create_hitl 新建 round ${Number(r.round) + 1}。`)
572
+ lines.push(``, `💡 驳回后可 create_hitl 新建 round ${r.round + 1}。`)
491
573
  }
492
574
  if (status === "DRAFT" || status === "SUBMITTED") {
493
575
  lines.push(``, `操作: update_hitl({ planName: "${planName}", type: "${r.type}", status: "TONGYI|BOHUI" })`)
@@ -12,61 +12,6 @@ import { registerReviewTools } from "./review.js"
12
12
  import { PROJECT_ID } from "../shared/env.js"
13
13
  import type { ToolRegistrar } from "./registrar.js"
14
14
 
15
- // ── 读写分级信号量节流(进程层并发契约 v2 §7:Codex Parallel MCP 真并行兑底) ──
16
- // 读工具共享 8 并发;写工具共享 4 并发;超限请求排队(MCP 协议允许延迟响应,排队即天然反压)
17
- const READ_MAX = 8
18
- const WRITE_MAX = 4
19
-
20
- const READ_TOOLS = new Set([
21
- "get_project_context", "find_related_docs", "get_db_schema", "query_audit_logs",
22
- "plan_status", "review_status", "status_hitl", "contract_status",
23
- "check_dps", "check_rahs", "check_add_route_status", "check_spec_sync",
24
- "check_add_route_completeness", "check_phase_symmetry", "check_failure_path",
25
- "check_add_compliance", "get_hook_events",
26
- ])
27
-
28
- function createSemaphore(limit: number) {
29
- let active = 0
30
- const queue: (() => void)[] = []
31
- const acquire = () =>
32
- new Promise<void>((resolve) => {
33
- if (active < limit) {
34
- active++
35
- resolve()
36
- } else {
37
- queue.push(resolve)
38
- }
39
- })
40
- const release = () => {
41
- active--
42
- const next = queue.shift()
43
- if (next) {
44
- active++
45
- next()
46
- }
47
- }
48
- return { acquire, release }
49
- }
50
-
51
- // 429 指数退避重试(3 次,250ms 起步,倍率 2x):上游限流不直接透传
52
- async function withRetry<T>(fn: () => Promise<T>): Promise<T> {
53
- let lastErr: unknown
54
- for (let attempt = 1; attempt <= 3; attempt++) {
55
- try {
56
- return await fn()
57
- } catch (e) {
58
- lastErr = e
59
- const msg = e instanceof Error ? e.message : String(e)
60
- if (!/429|rate.?limit|限流/i.test(msg)) throw e
61
- await new Promise((r) => setTimeout(r, 250 * 2 ** (attempt - 1)))
62
- }
63
- }
64
- throw lastErr
65
- }
66
-
67
- const readSem = createSemaphore(READ_MAX)
68
- const writeSem = createSemaphore(WRITE_MAX)
69
-
70
15
  export function registerAllTools(server: McpServer) {
71
16
  // D9 多 MCP 路由安全:派生装饰版 registrar,为所有工具 description 注入项目身份前缀
72
17
  const origRegister = server.registerTool.bind(server)
@@ -75,20 +20,10 @@ export function registerAllTools(server: McpServer) {
75
20
  type ToolCb = Parameters<RegisterTool>[2]
76
21
  const registerTool = ((name: string, config: ToolConfig, cb: ToolCb) => {
77
22
  const c = config as { description?: string }
78
- // 读写分级节流 + 429 退避(进程层契约 v2:Codex 并行调用兜底)
79
- const sem = READ_TOOLS.has(name) ? readSem : writeSem
80
- const throttledCb = (async (...args: unknown[]) => {
81
- await sem.acquire()
82
- try {
83
- return await withRetry(async () => cb(...(args as Parameters<ToolCb>)))
84
- } finally {
85
- sem.release()
86
- }
87
- }) as ToolCb
88
23
  return origRegister(
89
24
  name,
90
25
  { ...config, description: `[项目: ${PROJECT_ID}] ${c.description ?? ""}` },
91
- throttledCb,
26
+ cb,
92
27
  )
93
28
  }) as RegisterTool
94
29
  const registrar: ToolRegistrar = { registerTool }
@@ -5,9 +5,12 @@ import { join, basename } from "path"
5
5
  import { textResponse, errorResponse } from "../shared/response.js"
6
6
  import { readFileSafe, readdirRecursive, PROJECT_ROOT, MAGIC_DIR } from "../shared/fs.js"
7
7
  import { prisma } from "../shared/prisma.js"
8
+ import type { PlanRow } from "../shared/db-types.js"
9
+ import { PlanRowSchema, validatedDelegate } from "../shared/db-types.js"
8
10
 
11
+ // 无类型边界单点(zod 托管):动态加载的 prisma client 在此一次性转为运行期校验的泛型委托
9
12
  const db = {
10
- get plan() { return prisma.planRecord as unknown as Record<string, (...a: unknown[]) => unknown> },
13
+ get plan() { return validatedDelegate<PlanRow>(prisma.planRecord, PlanRowSchema, "PlanRecord") },
11
14
  }
12
15
 
13
16
  export function registerPlanTools(server: ToolRegistrar) {
@@ -81,8 +84,8 @@ export function registerPlanTools(server: ToolRegistrar) {
81
84
  )
82
85
  const addRoutePath = addRouteFile ? join(plansDir, addRouteFile) : undefined
83
86
  // upsert PlanRecord
84
- const existing = await db.plan.findFirst({ where: { planName: t.name } }) as Record<string, unknown> | null
85
- const data = {
87
+ const existing = await db.plan.findFirst({ where: { planName: t.name } })
88
+ const data: Partial<PlanRow> = {
86
89
  planPath: t.path,
87
90
  specPath: existsSync(specPath) ? specPath : undefined,
88
91
  tasksPath: existsSync(tasksPath) ? tasksPath : undefined,
@@ -100,6 +103,20 @@ export function registerPlanTools(server: ToolRegistrar) {
100
103
  newPlans.push(t.name.replace(/-plan-v\d+$/, ""))
101
104
  }
102
105
  }
106
+ // ── 孤儿对账(全量扫描时):PlanRecord 存在但 planPath 文件缺失 → 悬空清单。
107
+ // 来源:create_hitl 预置行被中断 / 用户放弃未清理。允许暂时存在,不允许不可见。 ──
108
+ if (!pn) {
109
+ const allPlans = await db.plan.findMany({ orderBy: { createdAt: "desc" } })
110
+ const orphans = allPlans.filter(p => !existsSync(p.planPath))
111
+ if (orphans.length > 0) {
112
+ results.push("", `⚠️ 悬空 Plan(记录在、文件缺失)— 续写或清理:`)
113
+ const cutoff = Date.now() - 14 * 86400000
114
+ for (const o of orphans) {
115
+ const stale = o.createdAt.getTime() < cutoff
116
+ results.push(` ${stale ? "🔴 超 14 天,建议清理" : "·"} ${o.planName} → ${o.planPath}`)
117
+ }
118
+ }
119
+ }
103
120
  // scanAll 完成后提示用户调用 review_track 填充缺陷指标
104
121
  if (newPlans.length > 0) {
105
122
  results.push(``, `💡 以下 Plan 为新创建,ReviewRecord 暂无 P0/P1 指标:`)
@@ -120,13 +137,13 @@ export function registerPlanTools(server: ToolRegistrar) {
120
137
  }, async (args: Record<string, unknown>) => {
121
138
  try {
122
139
  const { planName } = args as { planName: string }
123
- const plan = await db.plan.findFirst({ where: { planName } }) as Record<string, unknown> | null
140
+ const plan = await db.plan.findFirst({ where: { planName } })
124
141
  if (!plan) return textResponse(`📋 Plan: 未跟踪\nplanName: ${planName}\n\n操作: plan_track({ planName: "${planName}" })`)
125
- const t = plan.totalTasks as number || 0
126
- const d = plan.doneTasks as number || 0
127
- const ct = plan.checklistT as number || 0
128
- const ctd = plan.checklistTDone as number || 0
129
- const cr = plan.checklistR as number || 0
142
+ const t = plan.totalTasks || 0
143
+ const d = plan.doneTasks || 0
144
+ const ct = plan.checklistT || 0
145
+ const ctd = plan.checklistTDone || 0
146
+ const cr = plan.checklistR || 0
130
147
  const taskPct = t > 0 ? Math.round((d / t) * 100) : 0
131
148
  const lines = [
132
149
  `📊 ${planName}`,
@@ -155,16 +172,16 @@ export function registerPlanTools(server: ToolRegistrar) {
155
172
  }, async (args: Record<string, unknown>) => {
156
173
  try {
157
174
  const { planName } = args as { planName: string }
158
- const plan = await db.plan.findFirst({ where: { planName } }) as Record<string, unknown> | null
175
+ const plan = await db.plan.findFirst({ where: { planName } })
159
176
  if (!plan) return errorResponse(`PlanRecord 未找到: ${planName}`)
160
- const planPath = plan.planPath as string
177
+ const planPath = plan.planPath
161
178
  if (!existsSync(planPath)) return errorResponse(`Plan 文件不存在: ${planPath}`)
162
179
  const content = readFileSync(planPath, "utf-8")
163
- const t = plan.totalTasks as number || 0
164
- const d = plan.doneTasks as number || 0
165
- const ct = plan.checklistT as number || 0
166
- const ctd = plan.checklistTDone as number || 0
167
- const cr = plan.checklistR as number || 0
180
+ const t = plan.totalTasks || 0
181
+ const d = plan.doneTasks || 0
182
+ const ct = plan.checklistT || 0
183
+ const ctd = plan.checklistTDone || 0
184
+ const cr = plan.checklistR || 0
168
185
  const pct = t > 0 ? Math.round((d / t) * 100) : 0
169
186
  const snapshot = [
170
187
  `## 📊 Plan 进度快照`,
@@ -5,14 +5,17 @@ import { join, basename } from "path"
5
5
  import { textResponse, errorResponse } from "../shared/response.js"
6
6
  import { readFileSafe, readdirRecursive, PROJECT_ROOT, MAGIC_DIR } from "../shared/fs.js"
7
7
  import { prisma } from "../shared/prisma.js"
8
+ import type { PlanRow, ReviewRow } from "../shared/db-types.js"
9
+ import { PlanRowSchema, ReviewRowSchema, validatedDelegate } from "../shared/db-types.js"
8
10
 
9
11
  const db = {
10
- get review() { return prisma.reviewRecord as unknown as Record<string, (...a: unknown[]) => unknown> },
11
- get plan() { return prisma.planRecord as unknown as Record<string, (...a: unknown[]) => unknown> },
12
+ get review() { return validatedDelegate<ReviewRow>(prisma.reviewRecord, ReviewRowSchema, "ReviewRecord") },
13
+ get plan() { return validatedDelegate<PlanRow>(prisma.planRecord, PlanRowSchema, "PlanRecord") },
12
14
  }
13
15
 
14
- // 检测 review 类型
15
- function detectReviewType(filename: string): string {
16
+ // 检测 review 类型(与 ReviewRowSchema.type 枚举对齐)
17
+ type ReviewTypeValue = "PLAN_REVIEW" | "IMPLEMENTATION" | "RUNTIME"
18
+ function detectReviewType(filename: string): ReviewTypeValue {
16
19
  const l = filename.toLowerCase()
17
20
  if (l.includes("implementation")) return "IMPLEMENTATION"
18
21
  if (l.includes("runtime")) return "RUNTIME"
@@ -67,9 +70,9 @@ export function registerReviewTools(server: ToolRegistrar) {
67
70
  // 需要用 contains 匹配,而不是 exact match
68
71
  const planMatch = await db.plan.findFirst({
69
72
  where: { planName: { contains: derivedPlan } },
70
- }) as Record<string, unknown> | null
73
+ })
71
74
  const actualPlanName = planMatch
72
- ? (planMatch.planName as string)
75
+ ? planMatch.planName
73
76
  : derivedPlan // fallback,让外键约束报错时提示更清晰
74
77
 
75
78
  const content = await readFileSafe(fullPath)
@@ -79,7 +82,7 @@ export function registerReviewTools(server: ToolRegistrar) {
79
82
  const stats = countIssueType(content)
80
83
  const record = await db.review.findFirst({
81
84
  where: { planName: actualPlanName, type: rtype },
82
- }) as Record<string, unknown> | null
85
+ })
83
86
 
84
87
  const data = {
85
88
  reviewPath: fullPath,
@@ -117,7 +120,7 @@ export function registerReviewTools(server: ToolRegistrar) {
117
120
  const rows = await db.review.findMany({
118
121
  where: { planName },
119
122
  orderBy: { type: "asc" },
120
- }) as Record<string, unknown>[]
123
+ })
121
124
 
122
125
  if (!rows.length) {
123
126
  return textResponse(
@@ -155,7 +158,7 @@ export function registerReviewTools(server: ToolRegistrar) {
155
158
  }, async (args: Record<string, unknown>) => {
156
159
  try {
157
160
  const { planName } = args as { planName: string }
158
- const rows = await db.review.findMany({ where: { planName } }) as Record<string, unknown>[]
161
+ const rows = await db.review.findMany({ where: { planName } })
159
162
 
160
163
  if (!rows.length) return errorResponse(`无 ReviewRecord: ${planName}`)
161
164
 
@@ -1,7 +1,6 @@
1
1
  import { McpServer } from "@modelcontextprotocol/server"
2
2
  import { StdioServerTransport } from "@modelcontextprotocol/server/stdio"
3
3
  import { registerAll } from "./mcp-server/index.js"
4
- import { redact } from "./mcp-server/shared/redact.js"
5
4
 
6
5
  async function main() {
7
6
  const server = new McpServer(
@@ -15,6 +14,6 @@ async function main() {
15
14
  }
16
15
 
17
16
  main().catch((error) => {
18
- console.error("[ADD-MCP] Fatal error:", redact(error instanceof Error ? error.message : String(error)))
17
+ console.error("[ADD-MCP] Fatal error:", error)
19
18
  process.exit(1)
20
19
  })
@@ -34,6 +34,19 @@
34
34
 
35
35
  ---
36
36
 
37
+ ## Review 回流记录
38
+
39
+ > **功能**:总览索引——记录所有 review 发现及其回流落点。读者可从表快速扫描“有哪些问题”,再跳转到对应位置看“改了什么”。
40
+ > **回流规则**:每条发现必须在 Plan 体中对应位置有落点(补充 Task / 修正描述 / 新增约束),表行与落点双向一致。
41
+
42
+ | # | 来源阶段 | 严重度 | 发现 | 回流落点 |
43
+ |---|:---:|:---:|------|------|
44
+ | R1 | Plan Review | 🔴高 | {发现简述} | Task {N} / §{章节}:{具体修改} |
45
+
46
+ > **来源阶段枚举**:`Plan Review`(Step 0.6.5 设计审查)| `Impl Review`(Step 3-7 实现审查)| `Runtime Review`(Step 8+ 运行时纠偏)
47
+
48
+ ---
49
+
37
50
  ## 一、Plan 概述
38
51
 
39
52
  - **现状**: 一句话描述当前问题
@@ -12,6 +12,16 @@
12
12
  "required": true,
13
13
  "note": "AI 必须先在此表中列出实际完整文件路径和方案决策,等待人类拍板后再执行。禁止偷懒写'等 N 个文件'。"
14
14
  },
15
+ {
16
+ "id": "review-backflow",
17
+ "heading": "## Review 回流记录",
18
+ "required": true,
19
+ "note": "总览索引表:记录所有 review 发现及其回流落点。列:'#' + '来源阶段' + '严重度' + '发现' + '回流落点'。来源阶段枚举:Plan Review / Impl Review / Runtime Review。",
20
+ "validation": {
21
+ "columns": ["#", "来源阶段", "严重度", "发现", "回流落点"],
22
+ "source_phase_enum": ["Plan Review", "Impl Review", "Runtime Review"]
23
+ }
24
+ },
15
25
  {
16
26
  "id": "changes",
17
27
  "heading": "## 二、变更范围",
@@ -37,6 +37,19 @@
37
37
 
38
38
  ---
39
39
 
40
+ ## Review 回流记录
41
+
42
+ > **功能**:总览索引——记录所有 review 发现及其回流落点。读者可从表快速扫描“有哪些问题”,再跳转到对应位置看“改了什么”。
43
+ > **回流规则**:每条发现必须在 Plan 体中对应位置有落点(补充 Task / 修正描述 / 新增约束),表行与落点双向一致。
44
+
45
+ | # | 来源阶段 | 严重度 | 发现 | 回流落点 |
46
+ |---|:---:|:---:|------|------|
47
+ | R1 | Plan Review | 🔴高 | {发现简述} | Task {N.M} / §{章节}:{具体修改} |
48
+
49
+ > **来源阶段枚举**:`Plan Review`(Step 0.6.5 设计审查)| `Impl Review`(Step 3-7 实现审查)| `Runtime Review`(Step 8+ 运行时纠偏)
50
+
51
+ ---
52
+
40
53
  ## 一、背景与目标
41
54
 
42
55
  ### 1.1 问题现状
@@ -12,6 +12,16 @@
12
12
  "required": true,
13
13
  "note": "AI 必须先在此表中列出全部关键决策,等待人类拍板后再展开详细设计。"
14
14
  },
15
+ {
16
+ "id": "review-backflow",
17
+ "heading": "## Review 回流记录",
18
+ "required": true,
19
+ "note": "总览索引表:记录所有 review 发现及其回流落点。列:'#' + '来源阶段' + '严重度' + '发现' + '回流落点'。来源阶段枚举:Plan Review / Impl Review / Runtime Review。",
20
+ "validation": {
21
+ "columns": ["#", "来源阶段", "严重度", "发现", "回流落点"],
22
+ "source_phase_enum": ["Plan Review", "Impl Review", "Runtime Review"]
23
+ }
24
+ },
15
25
  {
16
26
  "id": "background",
17
27
  "heading": "## 一、背景与目标",
@@ -1,20 +0,0 @@
1
- # Codex MCP 配置模板(真源)
2
- # 渲染后分发到 .codex/ 与各 magic dirs templates/,供 --print-mcp-config 输出
3
- # 使用方式:
4
- # npx add-coder init --adapter=codex --print-mcp-config # stdout 输出片段
5
- # npx add-coder init --adapter=codex --write-user-config # 显式确认后写入 ~/.codex/config.toml
6
- #
7
- # Windows 分支(renderer 按平台注入,无需手动修改):
8
- # win32: command = "cmd /c" args = ["npx.cmd", "tsx", "..."] # .cmd 分支
9
- # posix: command = "npx" args = ["tsx", "..."]
10
- #
11
- # 安全约定:
12
- # - env 只引用环境变量({{projectRoot}} 由渲染时注入,密码/连接串禁止写入模板)
13
- # - DATABASE_URL 请通过消费方 .env 或 IDE 环境传入,模板不包含任何连接串
14
-
15
- [mcp_servers.add_coder]
16
- command = "{{codexCommand}}"
17
- args = ["tsx", "{{magicDir}}/scripts/mcp-server.ts"]
18
- env = {
19
- PROJECT_ROOT = "{{projectRoot}}",
20
- }