add-coder 0.3.24 → 0.3.25
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/README.en.md +10 -10
- package/README.md +10 -10
- package/dist/index.js +0 -3
- package/package.json +1 -1
- package/templates/.add-coder-src-hash.json +12 -14
- package/templates/core/scripts/mcp-server/shared/db-types.ts +208 -0
- package/templates/core/scripts/mcp-server/tools/audit.ts +10 -7
- package/templates/core/scripts/mcp-server/tools/context.ts +19 -0
- package/templates/core/scripts/mcp-server/tools/contract.ts +6 -4
- package/templates/core/scripts/mcp-server/tools/hitl.ts +116 -34
- package/templates/core/scripts/mcp-server/tools/plan.ts +33 -16
- package/templates/core/scripts/mcp-server/tools/review.ts +12 -9
- package/templates/core/templates/simple-plan-template.md +13 -0
- package/templates/core/templates/simple-plan-template.schema.json +10 -0
- package/templates/core/templates/standard-plan-template.md +13 -0
- package/templates/core/templates/standard-plan-template.schema.json +10 -0
- package/templates/core/plans/2026-07/08/add-coder-npm-package-add-route-v1.md +0 -331
- package/templates/core/plans/2026-07/08/add-coder-npm-package-handoff-v1.md +0 -686
- package/templates/core/plans/2026-07/08/add-coder-npm-package-plan-v1.md +0 -895
|
@@ -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
|
|
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
|
-
|
|
28
|
-
const
|
|
29
|
-
|
|
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
|
-
|
|
43
|
+
const lines = [
|
|
32
44
|
`⚠️ 当前环境(${MAGIC_DIR})客户端不支持 elicitation 弹框,交互模式已裁决为 genui。请按以下流程完成:`,
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
`
|
|
36
|
-
|
|
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
|
-
})
|
|
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
|
-
})
|
|
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
|
-
|
|
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
|
|
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
|
-
})
|
|
461
|
+
})
|
|
393
462
|
if (!rows.length) return errorResponse(`未找到 HITL 记录: planName=${planName}, type=${type}`)
|
|
394
463
|
const r = rows[0]
|
|
395
|
-
const prevStatus = r.status
|
|
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:
|
|
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(
|
|
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
|
-
})
|
|
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
|
|
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: ${
|
|
485
|
-
r.approvedAt ? `approvedAt: ${
|
|
486
|
-
r.rejectedAt ? `rejectedAt: ${
|
|
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 ${
|
|
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" })`)
|
|
@@ -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
|
|
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 } })
|
|
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 } })
|
|
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
|
|
126
|
-
const d = plan.doneTasks
|
|
127
|
-
const ct = plan.checklistT
|
|
128
|
-
const ctd = plan.checklistTDone
|
|
129
|
-
const cr = plan.checklistR
|
|
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 } })
|
|
175
|
+
const plan = await db.plan.findFirst({ where: { planName } })
|
|
159
176
|
if (!plan) return errorResponse(`PlanRecord 未找到: ${planName}`)
|
|
160
|
-
const planPath = plan.planPath
|
|
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
|
|
164
|
-
const d = plan.doneTasks
|
|
165
|
-
const ct = plan.checklistT
|
|
166
|
-
const ctd = plan.checklistTDone
|
|
167
|
-
const cr = plan.checklistR
|
|
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
|
|
11
|
-
get plan() { return prisma.planRecord
|
|
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
|
-
|
|
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
|
-
})
|
|
73
|
+
})
|
|
71
74
|
const actualPlanName = planMatch
|
|
72
|
-
?
|
|
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
|
-
})
|
|
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
|
-
})
|
|
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 } })
|
|
161
|
+
const rows = await db.review.findMany({ where: { planName } })
|
|
159
162
|
|
|
160
163
|
if (!rows.length) return errorResponse(`无 ReviewRecord: ${planName}`)
|
|
161
164
|
|
|
@@ -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": "## 一、背景与目标",
|