add-coder 0.2.8 → 0.3.0

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 (58) hide show
  1. package/README.md +66 -27
  2. package/dist/index.js +158 -11
  3. package/package.json +9 -12
  4. package/templates/.add-coder-src-hash.json +256 -0
  5. package/templates/adapters/claude/hooks/doc-format-guard.sh +17 -0
  6. package/templates/adapters/claude/hooks/lib/notify.sh +34 -0
  7. package/templates/adapters/claude/hooks/pre-tool-use.sh +26 -7
  8. package/templates/adapters/claude/hooks/prompt-submit.sh +16 -0
  9. package/templates/adapters/codex/hooks/doc-format-guard.sh +17 -0
  10. package/templates/adapters/codex/hooks/lib/notify.sh +34 -0
  11. package/templates/adapters/codex/hooks/pre-tool-use.sh +48 -18
  12. package/templates/adapters/codex/hooks/prompt-submit.sh +16 -0
  13. package/templates/adapters/qoder/hooks/doc-format-guard.sh +17 -0
  14. package/templates/adapters/qoder/hooks/lib/notify.sh +34 -0
  15. package/templates/adapters/qoder/hooks/pre-tool-use.sh +28 -9
  16. package/templates/adapters/qoder/hooks/prompt-submit.sh +18 -1
  17. package/templates/adapters/trae/hooks/doc-format-guard.sh +17 -0
  18. package/templates/adapters/trae/hooks/lib/notify.sh +34 -0
  19. package/templates/adapters/trae/hooks/pre-tool-use.sh +48 -18
  20. package/templates/adapters/trae/hooks/prompt-submit.sh +16 -0
  21. package/templates/adapters/vscode/hooks/doc-format-guard.sh +16 -0
  22. package/templates/adapters/vscode/hooks/lib/notify.sh +34 -0
  23. package/templates/adapters/vscode/hooks/pre-tool-use.sh +26 -7
  24. package/templates/adapters/vscode/hooks/prompt-submit.sh +16 -0
  25. package/templates/core/hooks/doc-format-guard.sh +17 -0
  26. package/templates/core/hooks/lib/notify.sh +34 -0
  27. package/templates/core/hooks/pre-tool-use.sh +48 -18
  28. package/templates/core/hooks/prompt-submit.sh +16 -0
  29. package/templates/core/reviews/.gitkeep +0 -0
  30. package/templates/core/scripts/mcp-server/elicitation/confirm.ts +30 -0
  31. package/templates/core/scripts/mcp-server/elicitation/index.ts +1 -0
  32. package/templates/core/scripts/mcp-server/index.ts +15 -0
  33. package/templates/core/scripts/mcp-server/notifications/hitl.ts +49 -0
  34. package/templates/core/scripts/mcp-server/notifications/hook.ts +268 -0
  35. package/templates/core/scripts/mcp-server/notifications/index.ts +8 -0
  36. package/templates/core/scripts/mcp-server/resources/add-coder-version.ts +25 -0
  37. package/templates/core/scripts/mcp-server/resources/add-state.ts +44 -0
  38. package/templates/core/scripts/mcp-server/resources/hook-events-report.ts +104 -0
  39. package/templates/core/scripts/mcp-server/resources/index.ts +12 -0
  40. package/templates/core/scripts/mcp-server/resources/round-task.ts +22 -0
  41. package/templates/core/scripts/mcp-server/sampling/index.ts +1 -0
  42. package/templates/core/scripts/mcp-server/sampling/review.ts +47 -0
  43. package/templates/core/scripts/mcp-server/shared/env.ts +26 -0
  44. package/templates/core/scripts/mcp-server/shared/fs.ts +40 -0
  45. package/templates/core/scripts/mcp-server/shared/prisma.ts +35 -0
  46. package/templates/core/scripts/mcp-server/shared/response.ts +9 -0
  47. package/templates/core/scripts/mcp-server/tasks/index.ts +2 -0
  48. package/templates/core/scripts/mcp-server/tasks/runner.ts +20 -0
  49. package/templates/core/scripts/mcp-server/tasks/store.ts +20 -0
  50. package/templates/core/scripts/mcp-server/tools/audit.ts +73 -0
  51. package/templates/core/scripts/mcp-server/tools/context.ts +337 -0
  52. package/templates/core/scripts/mcp-server/tools/docs.ts +42 -0
  53. package/templates/core/scripts/mcp-server/tools/gateway.ts +135 -0
  54. package/templates/core/scripts/mcp-server/tools/hook-event-report.ts +92 -0
  55. package/templates/core/scripts/mcp-server/tools/index.ts +17 -0
  56. package/templates/core/scripts/mcp-server/tools/quality.ts +93 -0
  57. package/templates/core/scripts/mcp-server/types.ts +7 -0
  58. package/templates/core/scripts/mcp-server.ts +8 -3455
@@ -0,0 +1,135 @@
1
+ import * as z from "zod/v4"
2
+ import type { McpServer } from "@modelcontextprotocol/server"
3
+ import { existsSync } from "fs"
4
+ import { join, basename } from "path"
5
+ import { textResponse, errorResponse } from "../shared/response.js"
6
+ import { readFileSafe, readdirRecursive, PROJECT_ROOT, MAGIC_DIR } from "../shared/fs.js"
7
+ import { prisma } from "../shared/prisma.js"
8
+
9
+ export function registerGatewayTools(server: McpServer) {
10
+
11
+ // ===== check_add_route_status (L1448-1716) =====
12
+ server.registerTool("check_add_route_status", {
13
+ description: `ADD 范式守卫工具:交叉校验 add-route 文件的审计日志记录与文件系统存在性,并扫描文件内容统计 Step 完成度。\n必须在 Plan 进入 Handoff 或 Step 3 前调用。\n\n返回状态:\n- 'normal' — 审计日志有记录、文件存在、Step 全部闭环\n- 'warn_step_incomplete' — 文件存在但存在未勾选的 Step 产出项\n- 'file_missing' — 审计日志有记录但文件不存在\n- 'never_generated' — 审计日志无记录且文件不存在,禁止进入 Step 3`,
14
+ inputSchema: z.object({ planKeyword: z.string().describe("Plan 文件的关键词") }),
15
+ }, async (args: Record<string, unknown>, _ctx: unknown) => {
16
+ try {
17
+ const planKeyword = args.planKeyword; if (!planKeyword) return errorResponse("planKeyword 参数不能为空")
18
+ const plansDir = join(PROJECT_ROOT, MAGIC_DIR, "plans")
19
+ let auditHasRecord = false; const auditRecords: Array<{action:string;targetId:string;createdAt:Date}> = []
20
+ try { const logs = await (prisma.auditLog as Record<string, (...a: unknown[]) => unknown>).findMany({ where: { OR: [{targetId:{contains:"add-route",mode:"insensitive"}},{targetId:{contains:planKeyword,mode:"insensitive"}},{reason:{contains:"add-route",mode:"insensitive"}}] }, orderBy:{createdAt:"desc"}, take:20 })
21
+ const pl = planKeyword.toLowerCase()
22
+ for (const l of logs) { const ti=(l.targetId||"").toLowerCase(); const r=(l.reason||"").toLowerCase(); if ((ti.includes("add-route")&&ti.includes(pl))||(r.includes("add-route")&&r.includes(pl))) { auditHasRecord=true; auditRecords.push({action:l.action,targetId:l.targetId||"unknown",createdAt:l.createdAt}) } }
23
+ } catch { /* empty */ }
24
+ let fileExists=false; const matchedFiles:string[]=[]
25
+ try { const entries=await readdirRecursive(plansDir); for(const e of entries) { const el=e.toLowerCase(); if(el.includes("add-route")&&el.includes(planKeyword.toLowerCase())) { fileExists=true; matchedFiles.push(e) } } } catch { /* empty */ }
26
+ const parts=[`=== ADD 守卫:add-route 存在性交叉校验 ===`,`Plan 关键词: "${planKeyword}"`,`预期路径: ${MAGIC_DIR}/plans/{需求域名}-{核心内容}-add-route-v1.md`,""]
27
+ const scanCheckboxes=(content:string)=>{ let t=0,c=0,u=0; const inc:string[]=[]; const ss:Array<{step:string;checked:number;unchecked:number}>=[]; let cs=""; for(const l of content.split("\n")){ const sm=l.match(/^##\s+Step\s+(\d+(?:\.\d+)?)/); if(sm){ if(cs&&(c>0||u>0)) ss.push({step:cs,checked:c,unchecked:u}); cs=sm[1]; c=0; u=0; continue } const cm=l.match(/^\s*-\s+\[([ xX])\]\s/); if(cm){ t++; if(cm[1]==="x"||cm[1]==="X") c++; else { u++; if(cs&&!inc.includes(cs)) inc.push(cs) } } } if(cs&&(c>0||u>0)) ss.push({step:cs,checked:c,unchecked:u}); return {total:t,checked:c,unchecked:u,incomplete:inc,statuses:ss} }
28
+ if(auditHasRecord&&fileExists){ const sc=scanCheckboxes(await readFileSafe(join(plansDir,matchedFiles[0]))||""); const ok=sc.unchecked===0; parts.push(`状态: ${ok?"✅ normal":"⚠️ warn_step_incomplete"}`,ok?"操作: 继续执行后续流程":"操作: ⚠️ 存在未闭环 Step",""); parts.push("=== 审计记录 ==="); for(const r of auditRecords.slice(0,5)) parts.push(` [${r.createdAt.toISOString()}] ${r.action} → ${r.targetId}`); parts.push(""); parts.push("=== 匹配文件 ==="); for(const f of matchedFiles) parts.push(` ${MAGIC_DIR}/plans/${f}`); parts.push("","=== Step 完成度扫描 ==="); if(sc.total===0) parts.push(" ⚠️ 未检测到 checkbox"); else { const rate=Math.round((sc.checked/sc.total)*100); parts.push(` 整体: ${sc.checked}/${sc.total} (${rate}%)`); for(const s of sc.statuses) parts.push(` Step ${s.step}: ${s.unchecked===0?"✅":"⬜"} ${s.checked}/${s.checked+s.unchecked}`); if(sc.incomplete.length>0){ parts.push("",` ⚠️ 未闭环 Step: ${sc.incomplete.join(", ")}`) } } return textResponse(parts.join("\n")) }
29
+ if(auditHasRecord&&!fileExists){ parts.push("状态: ❌ file_missing — add-route 文件丢失","操作: 中断推理,询问用户原因","","审计日志显示 add-route 曾经存在但文件系统中找不到。","","=== 审计记录 ==="); for(const r of auditRecords.slice(0,5)) parts.push(` [${r.createdAt.toISOString()}] ${r.action} → ${r.targetId}`); return errorResponse(parts.join("\n")) }
30
+ if(!auditHasRecord&&!fileExists){ parts.push("状态: ❌ never_generated — add-route 文件从未生成","操作: 禁止进入 Step 3,强制回退至 Step 0.5","",`在 ${MAGIC_DIR}/plans/ 下未找到包含 "${planKeyword}" 和 "add-route" 的文件。`,"","=== 必须执行的步骤 ===","1. 回退到 Step 0.5","2. 调用 get_add_template({ template: \"add-route-template\" })","3. 按模板填充",`4. 保存为 ${MAGIC_DIR}/plans/{需求域名}-{核心内容}-add-route-v1.md`,"5. 调用 record_dev_operation 记录","6. 重新调用本工具验证"); return errorResponse(parts.join("\n")) }
31
+ const scW=scanCheckboxes(await readFileSafe(join(plansDir,matchedFiles[0]))||""); const wOk=scW.unchecked===0; parts.push(`状态: ⚠️ warn${scW.total>0&&!wOk?"_step_incomplete":""} — 文件存在但审计日志无记录`,"操作: 允许继续,但建议补记录","","=== 匹配文件 ==="); for(const f of matchedFiles) parts.push(` ${MAGIC_DIR}/plans/${f}`); if(scW.total>0){ parts.push("","=== Step 完成度扫描 ===",` 整体: ${scW.checked}/${scW.total} (${Math.round((scW.checked/scW.total)*100)}%)`); if(scW.incomplete.length>0) parts.push(` ⚠️ 未闭环 Step: ${scW.incomplete.join(", ")}`) }; parts.push("","=== 建议 ===","1. 调用 record_dev_operation 补记录"); if(scW.incomplete.length>0) parts.push("2. 调用 check_add_route_completeness 获取详细清单"); return textResponse(parts.join("\n"))
32
+ } catch(e) { return errorResponse(`add-route 存在性校验失败: ${e instanceof Error?e.message:String(e)}`) }
33
+ })
34
+
35
+ // ===== check_spec_sync (L2103-2580) =====
36
+ server.registerTool("check_spec_sync", {
37
+ description: "ADD 重型模式文档-代码交叉校验工具。扫描 Plan → tasks.md → checklist.md → git diff → ADD-7 审计记录,报告四者之间的不一致。",
38
+ inputSchema: z.object({ planKeyword: z.string().describe("Plan 文件的关键词") }),
39
+ }, async (args: Record<string, unknown>, _ctx: unknown) => {
40
+ try {
41
+ const plansDir=join(PROJECT_ROOT,MAGIC_DIR,"plans"), specsDir=join(PROJECT_ROOT,MAGIC_DIR,"specs"); const lines:string[]=["=== check_spec_sync 文档-代码交叉校验 ===",""]
42
+ if(!existsSync(plansDir)) return errorResponse(`plans 目录不存在: ${plansDir}`)
43
+ const planFiles=(await readdirRecursive(plansDir)).filter(f=>f.endsWith(".md"))
44
+ let planMatch=planFiles.find(f=>f.toLowerCase().includes(args.planKeyword.toLowerCase())&&f.includes("-plan-v"))
45
+ if(!planMatch) planMatch=planFiles.find(f=>f.toLowerCase().includes(args.planKeyword.toLowerCase()))
46
+ if(!planMatch) return errorResponse(`未找到匹配的 Plan 文件(关键词: ${args.planKeyword})`)
47
+ const planPath=join(plansDir,planMatch); const planContent=await readFileSafe(planPath); lines.push(`Plan: ${planMatch}`)
48
+ let specDirName=""
49
+ if(planContent){ const sm=planContent.match(/Spec[:|\s`]+\.?(qoder|claude|add|vscode)\/specs\/([^/`\s]+)/); if(sm) specDirName=sm[2] }
50
+ if(!specDirName){ const tm=planContent?.match(/Tasks[:|\s`]+\.?(qoder|claude|add|vscode)\/specs\/([^/`\s]+)/); if(tm) specDirName=tm[2] }
51
+ if(!specDirName) specDirName=basename(planMatch).replace(/-plan-v\d+\.md$/,"")
52
+ lines.push(`Spec: ${specDirName}`,"")
53
+ let ut=0,ct=0; const tp=join(specsDir,specDirName,"tasks.md"); const tc=await readFileSafe(tp)||""; if(tc){ ut=(tc.match(/^- \[ \] Task/gm)||[]).length; ct=(tc.match(/^- \[x\] Task/gm)||[]).length; lines.push(`tasks.md: ${ct} 已完成 / ${ut} 未完成`) } else lines.push("tasks.md: 不存在")
54
+ let uc=0,cc=0; const cp=join(specsDir,specDirName,"checklist.md"); const clc=await readFileSafe(cp)||""; if(clc){ uc=(clc.match(/^- \[ \] /gm)||[]).length; cc=(clc.match(/^- \[x\] /gm)||[]).length; lines.push(`checklist.md: ${cc} 已勾选 / ${uc} 未勾选`) } else lines.push("checklist.md: 不存在")
55
+ lines.push(""); if(ut===0&&uc===0) lines.push(" ✅ tasks.md 和 checklist.md 全部项已勾选"); else { if(ut>0) lines.push(` 📝 tasks.md 有 ${ut} 个未完成 Task`); if(uc>0) lines.push(` 📝 checklist.md 有 ${uc} 个未勾选项`) }
56
+ try{ const {spawnSync}=await import("child_process"); const diff=spawnSync("git",["diff","--name-only"],{cwd:PROJECT_ROOT,encoding:"utf-8",timeout:5000}); const cf=(diff.stdout||"").trim().split("\n").filter(Boolean); lines.push(`Git diff: ${cf.length} 个变更文件`) } catch{ lines.push("Git diff: 无法获取") }
57
+ return textResponse(lines.join("\n"))
58
+ } catch(e) { return errorResponse(`check_spec_sync 失败: ${e instanceof Error?e.message:String(e)}`) }
59
+ })
60
+
61
+ // ===== check_add_route_completeness (L2582-2775) =====
62
+ server.registerTool("check_add_route_completeness", {
63
+ description: "ADD 范式守卫工具:扫描 add-route 文件的 Step 完成度。统计 add-route 中每个 Step 的 [ ] 和 [x] 勾选项数量,返回逐 Step 完成率及整体状态。",
64
+ inputSchema: z.object({ planKeyword: z.string().describe("Plan 文件的关键词") }),
65
+ }, async (args: Record<string, unknown>, _ctx: unknown) => {
66
+ try {
67
+ const pp=args.planKeyword; if(!pp) return errorResponse("planKeyword 参数不能为空")
68
+ const plansDir=join(PROJECT_ROOT,MAGIC_DIR,"plans"); if(!existsSync(plansDir)) return errorResponse(`plans 目录不存在: ${plansDir}`)
69
+ const allFiles=await readdirRecursive(plansDir); const arFile=allFiles.find(f=>f.toLowerCase().includes(pp.toLowerCase())&&f.includes("add-route"))
70
+ if(!arFile) return errorResponse(`未找到匹配的 add-route 文件(关键词: ${pp})`)
71
+ const content=await readFileSafe(join(plansDir,arFile)); if(!content) return errorResponse("add-route 文件无法读取")
72
+ const steps:Record<string,{checked:number;unchecked:number}>= {}; let cur=""; let tc=0,tu=0
73
+ for(const l of content.split("\n")){ const sm=l.match(/^##\s+Step\s+(\d+(?:\.\d+)?)/); if(sm){ cur=sm[1]; steps[cur]={checked:0,unchecked:0}; continue }; const cm=l.match(/^\s*-\s+\[([ xX])\]\s/); if(cm){ tc++; if(cm[1]==="x"||cm[1]==="X"){ tu++; if(cur) steps[cur].checked++ } else { tu=tu; if(cur) steps[cur].unchecked++ } } }
74
+ const parts=[`=== add-route Step 完成度扫描 ===`,`文件: ${MAGIC_DIR}/plans/${arFile}`,`整体: ${tu}/${tc} (${tc>0?Math.round((tu/tc)*100):0}%)`,""]
75
+ for(const [s,st] of Object.entries(steps)) parts.push(` Step ${s}: ${st.unchecked===0?"✅":"⬜"} ${st.checked}/${st.checked+st.unchecked}`)
76
+ if(tc===0) parts.push(" ⚠️ 未检测到 checkbox"); else if(tc===tu) parts.push("","✅ 所有 Step 产出项全部 [x],add-route 完整闭环")
77
+ else parts.push("","⚠️ 存在未勾选的 Step 产出项,需继续执行")
78
+ return textResponse(parts.join("\n"))
79
+ } catch(e) { return errorResponse(`add-route 完成度扫描失败: ${e instanceof Error?e.message:String(e)}`) }
80
+ })
81
+
82
+ // ===== check_dps (L2777-3103) =====
83
+ server.registerTool("check_dps", {
84
+ description: "DPS 闸门(Documentation Precision Score)。评估 Plan → Review → Specs 三级文档的精确度和覆盖度。DPS ≥ 85 可进入 Step 1。",
85
+ inputSchema: z.object({ planKeyword: z.string().describe("Plan 文件的关键词") }),
86
+ }, async (args: Record<string, unknown>, _ctx: unknown) => {
87
+ try {
88
+ const pp=args.planKeyword; if(!pp) return errorResponse("planKeyword 参数不能为空")
89
+ const plansDir=join(PROJECT_ROOT,MAGIC_DIR,"plans"), specsDir=join(PROJECT_ROOT,MAGIC_DIR,"specs"), reviewsDir=join(PROJECT_ROOT,MAGIC_DIR,"reviews")
90
+ const parts=[`=== DPS:Documentation Precision Score(上游文档质量量化)===`,`Plan 关键词: "${pp}"`,""]
91
+ if(!existsSync(plansDir)) return errorResponse(`plans 目录不存在: ${plansDir}`)
92
+ const apf=(await readdirRecursive(plansDir)).filter(f=>f.endsWith(".md")); const pm=apf.find(f=>f.toLowerCase().includes(pp.toLowerCase())&&f.includes("-plan-v"))
93
+ if(!pm) return errorResponse(`未找到匹配的 Plan 文件(关键词: ${pp})`)
94
+ const planPath=join(plansDir,pm); const pc=await readFileSafe(planPath); if(!pc) return errorResponse(`无法读取 Plan 文件: ${pm}`)
95
+ parts.push(`Plan: ${pm}`)
96
+ // Plan 粒度 (25%)
97
+ const hasPlaceholders=pc.match(/\{[^}]+\}/g); const taskCount=(pc.match(/Task\s+\d+\.\d+/g)||[]).length; let planScore=100; if(hasPlaceholders&&hasPlaceholders.length>0) planScore=80; parts.push("=== 维度一:Plan 可执行粒度(30%)===",` 分数: ${planScore}/100`,hasPlaceholders?.length?` - Plan 含占位词: "${hasPlaceholders[0]}"`: " ✅ 无扣分项")
98
+ // Review 覆盖度 (25%)
99
+ let rn="",rc=""; if(existsSync(reviewsDir)){ const rfs=await readdirRecursive(reviewsDir); rn=rfs.find(f=>f.toLowerCase().includes(pp.toLowerCase())&&f.includes("-review-v"))||""; if(rn) rc=await readFileSafe(join(reviewsDir,rn))||"" }
100
+ const rdims=["数据模型/类型定义","性能影响","存储/索引成本","兼容性/向后兼容"]; let rcov=0; for(const d of rdims) if(rc&&rc.includes(d)) rcov++; const rScore=rc?Math.round((rcov/3)*100):0; parts.push(`=== 维度二:Review 覆盖完备度(35%)===`,` 覆盖度: ${rcov}/3 (${Math.round((rcov/3)*100)}%)`,` 分数: ${rScore}/100`); if(rn) parts.push(` ${rc?"✅ 兼容性/向后兼容":""}${rc?"":""}`); else parts.push(" ⚠️ Review 文件未找到,覆盖度计为 0")
101
+ // Specs 精确度 (25%)
102
+ let sn=basename(pm).replace(/-plan-v\d+\.md$/,""),sc=""
103
+ if(pc){ const sr=pc.match(/Spec[:|\s`]+\.?(qoder|claude|add|vscode)\/specs\/([^/`\s]+)/); if(sr) sn=sr[2] }
104
+ const sp=join(specsDir,sn,"spec.md"); sc=await readFileSafe(sp)||""; const rqc=sc?(sc.match(/###\s+Requirement:/g)||[]).length:0; const sScore=sc?100:0; parts.push(`=== 维度三:Specs 精确度(35%)===`,sc?` Requirements 数: ${rqc}`:" ⚠️ Specs 目录/文件未找到,精确度计为 0",` 分数: ${sScore}/100`)
105
+ // 回流完整度 (25%)
106
+ let bScore=100; if(rc){ const rp0p1=rc.split("\n").filter(l=>/^\|\s*\d+\s*\|\s*(P0|P1)\s*\|/.test(l)).length; const pb=(pc.match(/\[回流\s*[::]/g)||[]).length; if(rp0p1>0&&pb<rp0p1) bScore=Math.round((pb/rp0p1)*100); parts.push(`=== 维度四:Review 回流完整度(25%)===`,` Review P0/P1 条目总数: ${rp0p1}`,` Plan [回流:] 标记数: ${pb}`,rp0p1===0?" ✅ Review 无 P0/P1 问题":bScore===100?` ✅ 回流完整: ${pb}/${rp0p1}`:` ⚠️ 回流不完整: ${pb}/${rp0p1}`) }
107
+ else parts.push("=== 维度四:Review 回流完整度(25%)==="," ⚠️ Review 文件未找到,回流检查不可用")
108
+ parts.push(` 分数: ${bScore}/100`)
109
+ const dps=Math.round(planScore*0.25+rScore*0.25+sScore*0.25+bScore*0.25); parts.push("","=== DPS 复合计算 ===",` Plan 粒度: ${planScore} × 0.25 = ${(planScore*0.25).toFixed(1)}`,` Review 覆盖度: ${rScore} × 0.25 = ${(rScore*0.25).toFixed(1)}`,` Specs 精确度: ${sScore} × 0.25 = ${(sScore*0.25).toFixed(1)}`,` Review 回流完整度: ${bScore} × 0.25 = ${(bScore*0.25).toFixed(1)}`," ─────────────────────────────────",` DPS = ${dps} ${dps>=85?"🟢 PASS":dps>=70?"🟡 WARN":"🔴 BLOCKED"}`)
110
+ parts.push("",`=== 判定 ===`,` 结果: ${dps>=85?"�� PASS":dps>=70?"🟡 WARN":"🔴 BLOCKED"}`,` 动作: ${dps>=85?"可进入 Step 1":dps>=70?"回退补齐短板":"回退细化 Plan 本身"}`)
111
+ return textResponse(parts.join("\n"))
112
+ } catch(e) { return errorResponse(`check_dps 失败: ${e instanceof Error?e.message:String(e)}`) }
113
+ })
114
+
115
+ // ===== check_rahs (L3105-3358) =====
116
+ server.registerTool("check_rahs", {
117
+ description: "RAHS 闸门(Runtime Architecture Health Score)。检查范围保真度、类型安全、审计完整度等。RAHS ≥ 90 通过。",
118
+ inputSchema: z.object({ planKeyword: z.string().describe("Plan 文件的关键词") }),
119
+ }, async (args: Record<string, unknown>, _ctx: unknown) => {
120
+ try {
121
+ const pp=args.planKeyword; if(!pp) return errorResponse("planKeyword 参数不能为空")
122
+ const plansDir=join(PROJECT_ROOT,MAGIC_DIR,"plans"); if(!existsSync(plansDir)) return errorResponse(`plans 目录不存在: ${plansDir}`)
123
+ const apf=(await readdirRecursive(plansDir)).filter(f=>f.endsWith(".md")); const pm=apf.find(f=>f.toLowerCase().includes(pp.toLowerCase())&&f.includes("-plan-v"))
124
+ if(!pm) return errorResponse(`未找到匹配的 Plan 文件(关键词: ${pp})`)
125
+ let scopeScore=80,typeScore=80,auditScore=80,specScore=80,symScore=80; let total=0
126
+ try{ const {spawnSync}=await import("child_process"); const tsc=spawnSync("npx",["tsc","--noEmit"],{cwd:PROJECT_ROOT,encoding:"utf-8",timeout:30000}); typeScore=tsc.status===0?100:Math.max(0,100-(tsc.stderr||"").split("\n").filter(Boolean).length*5) } catch{}
127
+ const cf=apf.filter(f=>f.toLowerCase().includes(pp.toLowerCase()))
128
+ try{ const logs = await (prisma.auditLog as Record<string, (...a: unknown[]) => unknown>).findMany({where:{OR:[{targetId:{contains:pp,mode:"insensitive"}},{reason:{contains:pp,mode:"insensitive"}}]},select:{id:true},take:20}); auditScore=Math.min(100,logs.length*10) } catch{}
129
+ const parts=["=== RAHS:Runtime Architecture Health Score ===",`Plan 关键词: "${pp}"`,"",`=== 维度 ===`,` 范围保真度: ${scopeScore}/100`,` 类型安全: ${typeScore}/100`,` 审计完整度: ${auditScore}/100`,` Spec 合规: ${specScore}/100`,` 阶段对称性: ${symScore}/100`,""]
130
+ const rahs=Math.round((scopeScore+typeScore+auditScore+specScore+symScore)/5); parts.push(`=== RAHS = ${rahs} ${rahs>=90?"🟢 PASS":rahs>=70?"🟡 WARN":"🔴 BLOCKED"} ===`); if(rahs<70) parts.push(" 动作: 注意力漂移严重,返工回退")
131
+ return textResponse(parts.join("\n"))
132
+ } catch(e) { return errorResponse(`check_rahs 失败: ${e instanceof Error?e.message:String(e)}`) }
133
+ })
134
+
135
+ }
@@ -0,0 +1,92 @@
1
+ import * as z from "zod/v4"
2
+ import type { McpServer } from "@modelcontextprotocol/server"
3
+ import { textResponse, errorResponse } from "../shared/response.js"
4
+ import { prisma } from "../shared/prisma.js"
5
+
6
+ export function registerHookEventTools(server: McpServer) {
7
+
8
+ server.registerTool("get_hook_events", {
9
+ description:
10
+ "查询 Hook 拦截事件(HOOK_INTERCEPT)。支持按 planKeyword/hook/时间区间过滤,按 planKeyword 分组聚合。" +
11
+ "\n\n典型用法:" +
12
+ "\n- get_hook_events({}) — 查最近 50 条拦截事件" +
13
+ "\n- get_hook_events({ sinceMinutes: 1440 }) — 过去 24h" +
14
+ "\n- get_hook_events({ planKeyword: \"no-active-plan\" }) — 无 Plan 违规" +
15
+ "\n- get_hook_events({ hook: \"pre-tool-use\" }) — 按 hook 类型过滤",
16
+ inputSchema: z.object({
17
+ planKeyword: z.string().optional().describe("按 Plan 关键词过滤"),
18
+ hook: z.string().optional().describe("按 hook 名称过滤: pre-tool-use / doc-format-guard / prompt-submit"),
19
+ sinceMinutes: z.number().optional().describe("时间窗口起始(分钟前),如 1440=过去24h"),
20
+ untilMinutes: z.number().optional().describe("时间窗口截止(分钟前),默认 0(现在)"),
21
+ limit: z.number().optional().default(50).describe("返回最大条数,默认 50,最大 200"),
22
+ }),
23
+ }, async (args: Record<string, string | number | undefined>, _ctx: unknown) => {
24
+ try {
25
+ const { planKeyword, hook, sinceMinutes, untilMinutes, limit = 50 } = args
26
+
27
+ const where: Record<string, unknown> = { action: "HOOK_INTERCEPT" }
28
+ if (planKeyword) where.planKeyword = planKeyword
29
+ if (hook) where.targetType = hook
30
+
31
+ // 时间范围过滤
32
+ if (sinceMinutes !== undefined || untilMinutes !== undefined) {
33
+ const now = Date.now()
34
+ const since = sinceMinutes !== undefined ? new Date(now - (sinceMinutes as number) * 60 * 1000) : new Date(0)
35
+ const until = untilMinutes !== undefined ? new Date(now - (untilMinutes as number) * 60 * 1000) : new Date()
36
+ where.createdAt = { gte: since, lte: until }
37
+ }
38
+
39
+ const ops = prisma.devOperation as Record<string, (...a: unknown[]) => unknown>
40
+ const logs = await ops.findMany({
41
+ where,
42
+ orderBy: { createdAt: "desc" },
43
+ take: Math.min(limit as number, 200),
44
+ }) as Array<Record<string, unknown>>
45
+
46
+ if (logs.length === 0) {
47
+ const f: string[] = []
48
+ if (planKeyword) f.push(`planKeyword=${planKeyword}`)
49
+ if (hook) f.push(`hook=${hook}`)
50
+ if (sinceMinutes !== undefined) f.push(`过去${sinceMinutes}分钟`)
51
+ return textResponse(
52
+ `=== Hook 拦截事件 ===\n\n未找到匹配的拦截记录(条件: ${f.length > 0 ? f.join(", ") : "无过滤条件"})。`
53
+ )
54
+ }
55
+
56
+ // 按 planKeyword 分组统计
57
+ const groupMap = new Map<string, number>()
58
+ for (const l of logs) {
59
+ const kw = (l.planKeyword as string) || "unknown"
60
+ groupMap.set(kw, (groupMap.get(kw) || 0) + 1)
61
+ }
62
+
63
+ const lines = ["=== Hook 拦截事件 ===", `共 ${logs.length} 条记录`, ""]
64
+ lines.push("## 按 Plan 分组", "")
65
+ for (const [kw, count] of [...groupMap.entries()].sort((a, b) => b[1] - a[1])) {
66
+ lines.push(` ${kw}: ${count} 次`)
67
+ }
68
+
69
+ lines.push("", "## 最近事件", "")
70
+ for (let i = 0; i < Math.min(logs.length, 20); i++) {
71
+ const l = logs[i]
72
+ lines.push(
73
+ ` [${(l.createdAt as Date).toISOString().slice(11, 19)}] ${l.targetType || "?"} → ${l.reason || "(无)"} | plan: ${l.planKeyword || "?"}`
74
+ )
75
+ }
76
+
77
+ // 治理信号:无 Plan 违规告警
78
+ const noPlanCount = groupMap.get("no-active-plan") || 0
79
+ if (noPlanCount >= 10) {
80
+ lines.push("", `⚠️ 阈值告警: 过去 ${sinceMinutes || "?"} 分钟内无 Plan 违规 ${noPlanCount} 次(≥10),建议创建 Plan 或检查 hooks 误报`)
81
+ }
82
+
83
+ lines.push("", "=== 稀疏推理建议 ===", "基于以上 hook 事件,可分析近期治理合规情况。")
84
+ return textResponse(lines.join("\n"))
85
+ } catch (error) {
86
+ return errorResponse(
87
+ `查询 hook 事件失败: ${error instanceof Error ? error.message : String(error)}\n可能原因: 数据库未运行。`
88
+ )
89
+ }
90
+ })
91
+
92
+ }
@@ -0,0 +1,17 @@
1
+ import type { McpServer } from "@modelcontextprotocol/server"
2
+ import { registerContextTools } from "./context.js"
3
+ import { registerAuditTools } from "./audit.js"
4
+ import { registerDocsTools } from "./docs.js"
5
+ import { registerQualityTools } from "./quality.js"
6
+ import { registerGatewayTools } from "./gateway.js"
7
+ import { registerHookEventTools } from "./hook-event-report.js"
8
+
9
+ export function registerAllTools(server: McpServer) {
10
+ registerContextTools(server) // 5 tools
11
+ registerAuditTools(server) // 2 tools
12
+ registerDocsTools(server) // 1 tool
13
+ registerQualityTools(server) // 4 tools
14
+ registerGatewayTools(server) // 5 tools
15
+ registerHookEventTools(server) // 1 tool: get_hook_events
16
+ // Total: 18 tools
17
+ }
@@ -0,0 +1,93 @@
1
+ import * as z from "zod/v4"
2
+ import type { McpServer } from "@modelcontextprotocol/server"
3
+ import { textResponse, errorResponse } from "../shared/response.js"
4
+
5
+ export function registerQualityTools(server: McpServer) {
6
+
7
+ // ===== check_phase_symmetry (L615-679) =====
8
+ server.registerTool("check_phase_symmetry", {
9
+ description: "检查代码中的阶段标记对称性(ADD-2 阶段标记对称)。统计 auditPhaseStart/End 配对情况。",
10
+ inputSchema: z.object({ code: z.string().describe("要检查的 TypeScript 代码文本") }),
11
+ }, (args, _ctx) => {
12
+ try {
13
+ const code = args.code; if(!code) return errorResponse("code 参数不能为空")
14
+ const sr=/auditPhaseStart\(["']([^"']+)["']/g, er=/auditPhaseEnd\(["']([^"']+)["']/g
15
+ const ss:string[]=[], es:string[]=[]; let m
16
+ while((m=sr.exec(code))!==null) ss.push(m[1]); while((m=er.exec(code))!==null) es.push(m[1])
17
+ const sc:Record<string,number>={}, ec:Record<string,number>={}
18
+ for(const s of ss) sc[s]=(sc[s]||0)+1; for(const e of es) ec[e]=(ec[e]||0)+1
19
+ const ap=new Set([...Object.keys(sc),...Object.keys(ec)]); const asym:string[]=[]
20
+ ap.forEach(p=>{ const a=sc[p]||0,b=ec[p]||0; if(a!==b) asym.push(` ⚠️ ${p}: Start=${a}, End=${b} (${a>b?"缺少 End":"缺少 Start"})`) })
21
+ const l=[`=== ADD-2 阶段标记对称性检查 ===`,`Start 总数: ${ss.length}`,`End 总数: ${es.length}`,""]
22
+ if(!asym.length) l.push("✅ 阶段标记完全对称"); else l.push(`❌ 发现 ${asym.length} 个不对称阶段:`,...asym)
23
+ l.push("","=== 所有阶段明细 ==="); ap.forEach(p=>l.push(` ${p}: Start=${sc[p]||0}, End=${ec[p]||0}`))
24
+ return textResponse(l.join("\n"))
25
+ } catch(e) { return errorResponse(`检查阶段对称性失败: ${e instanceof Error?e.message:String(e)}`) }
26
+ })
27
+
28
+ // ===== check_failure_path (L681-757) =====
29
+ server.registerTool("check_failure_path", {
30
+ description: "检查失败路径审计信息密度(ADD-6)。对比 try 块与 catch 块的 extra 字段数。",
31
+ inputSchema: z.object({ code: z.string().describe("要检查的 TypeScript 代码文本") }),
32
+ }, (args, _ctx) => {
33
+ try {
34
+ const code = args.code; if(!code) return errorResponse("code 参数不能为空")
35
+ const sec = args.code.split(/catch\s*\(/); if(sec.length<=1) return textResponse("=== ADD-6 失败路径审计检查 ===\n\n未检测到 try/catch 块。")
36
+ const l=[`=== ADD-6 失败路径审计信息密度检查 ===`,`检测到 ${sec.length-1} 个 catch 块`,""]; let ap=true
37
+ for(let i=1;i<sec.length;i++){ const cb=sec[i],ce=cb.indexOf("{"); if(ce===-1) continue
38
+ const tb=sec[i-1]; const tem=tb.match(/extra[:\s]*\{[^}]*\}/g); const tfc=tem?tem.reduce((s,m)=>s+(m.match(/\w+:/g)?.length||0),0):0
39
+ const ci=cb.indexOf("}"),cbd=cb.slice(ce,ci+1); const cem=cbd.match(/extra[:\s]*\{[^}]*\}/g); const cfc=cem?cem.reduce((s,m)=>s+(m.match(/\w+:/g)?.length||0),0):0
40
+ const ch=cbd.includes("throw"), ca=cbd.includes("audit")||cbd.includes("Audit"); const cid=cfc+(ch?2:0)+(ca?2:0)
41
+ l.push(`--- Catch 块 #${i} ---`); if(cid>=tfc&&ca) l.push(" ✅ 失败路径审计信息密度充足"); else { ap=false; if(!ca) l.push(" ❌ catch 块缺少审计调用"); if(cid<tfc) l.push(` ❌ 信息密度不足: catch=${cfc}, try=${tfc}`) }
42
+ l.push(` try extra 字段数: ${tfc}`,` catch extra 字段数: ${cfc}`,` 有审计调用: ${ca?"是":"否"}`,` 有 throw: ${ch?"是":"否"}`,"")
43
+ }
44
+ l.push(ap?"✅ 所有 catch 块满足 ADD-6":"⚠️ 部分 catch 块需补充审计信息")
45
+ return textResponse(l.join("\n"))
46
+ } catch(e) { return errorResponse(`检查失败路径失败: ${e instanceof Error?e.message:String(e)}`) }
47
+ })
48
+
49
+ // ===== generate_audit_logger (L759-990) =====
50
+ server.registerTool("generate_audit_logger", {
51
+ description: "生成符合三层分离模式的新审计日志器完整代码。遵循 ADD-1~6 原则。",
52
+ inputSchema: z.object({
53
+ domain: z.string().describe("审计日志器域名(小写中划线)"),
54
+ phases: z.string().describe("业务阶段枚举列表(逗号分隔,大写蛇形)"),
55
+ prefix: z.string().describe("审计前缀标识"),
56
+ }),
57
+ }, (args, _ctx) => {
58
+ try {
59
+ const {domain,phases,prefix} = args; if(!domain||!phases||!prefix) return errorResponse("domain, phases, prefix 参数均不能为空")
60
+ const pl=phases.split(",").map(p=>p.trim()).filter(Boolean); if(!pl.length) return errorResponse("phases 必须包含至少一个阶段")
61
+ const fn=domain.split("-").map(s=>s.charAt(0).toUpperCase()+s.slice(1)).join(""), ld=`logs/${domain}/`, lf=`${domain}.log`
62
+ const te=pl.map(p=>` | "${p}"`).join("\n"), fp=fn.charAt(0).toLowerCase()+fn.slice(1)
63
+ const dlg=`=== 三层分离式审计日志器: ${domain} ===\n域名: ${domain}\n前缀: [${prefix}]\n日志目录: ${ld}\n阶段数: ${pl.length}\n阶段列表: ${pl.join(", ")}\n\n=== 文件1: src/lib/${domain}-dev-logger.ts (Layer 1) ===\n--- 模板代码 ---\nimport * as fs from "fs/promises"\nimport * as path from "path"\nconst PREFIX = "[${prefix}]"\nconst LOG_DIR = path.join(process.cwd(), "${ld}")\nconst LOG_FILE = "${lf}"\nconst IS_DEV = process.env.NODE_ENV === "development"\ntype ${fn}AuditPhase =\n${te}\nexport function ${fp}Audit(phase: ${fn}AuditPhase, detail: string, extra?: Record<string, unknown>) { if (!IS_DEV) return; const msg = \`\${PREFIX} [\${new Date().toISOString()}] [\${phase}] \${detail}\${extra ? " | " + JSON.stringify(extra) : ""}\`; console.log(msg) }\nexport function ${fp}AuditPhaseStart(phase: ${fn}AuditPhase, description: string) { if (!IS_DEV) return; console.log(\`\${PREFIX} ═══ [\${phase}] 开始: \${description} ═══\`) }\nexport function ${fp}AuditPhaseEnd(phase: ${fn}AuditPhase, detail: string) { if (!IS_DEV) return; console.log(\`\${PREFIX} ═══ [\${phase}] 结束: \${detail} ═══\`) }\n\n=== 文件2: src/lib/${domain}-audit.ts (Layer 2) ===\nimport { prisma } from "@/lib/prisma"\nconst PREFIX2 = "[${prefix}:RUNTIME]"\ntype ${fn}AuditAction =\n${pl.map(p=>` | "${p}"`).join("\n")}\nexport type ${fn}AuditRecord = { action: ${fn}AuditAction; entityId: string; detail: Record<string, unknown>; traceId?: string }\nexport async function record${fn}Audit(record: ${fn}AuditRecord): Promise<void> { console.log(\`\${PREFIX2} [\${new Date().toISOString()}] [\${record.action}] entity=\${record.entityId}\${record.traceId ? " trace="+record.traceId : ""}\${Object.keys(record.detail).length ? " | "+JSON.stringify(record.detail) : ""}\`); try { await prisma.auditLog.create({ data: { userId: "system", action: record.action, targetType: "${fn}", targetId: record.entityId, traceId: record.traceId||null, afterState: record.detail as Record<string,unknown>, reason: \`\${record.action} on \${record.entityId}\` } }) } catch(e) { console.error(\`\${PREFIX2} Failed to write AuditLog: \${e}\`) } }\n\n=== 使用方式 ===\n开发审计: import { ${fp}AuditPhaseStart, ${fp}AuditPhaseEnd, ${fp}Audit } from "@/lib/${domain}-dev-logger"\n运行时审计: import { record${fn}Audit } from "@/lib/${domain}-audit"`
64
+ return textResponse(dlg)
65
+ } catch(e) { return errorResponse(`生成审计日志器失败: ${e instanceof Error?e.message:String(e)}`) }
66
+ })
67
+
68
+ // ===== check_add_compliance (L1875-2101) =====
69
+ server.registerTool("check_add_compliance", {
70
+ description: "综合 ADD 合规检查(ADD-1~6 一键验证)。检查阶段标记对称性、失败路径审计密度、循环体审计、审计日志器导入完整性。",
71
+ inputSchema: z.object({
72
+ code: z.string().describe("要检查的 TypeScript 代码文本"),
73
+ projectPattern: z.string().optional().default("event-based").describe("项目审计模式: 'event-based' 使用 auditPhaseStart/End, 'mixed' 同时检查 event 和 agentAudit"),
74
+ }),
75
+ }, (args, _ctx) => {
76
+ try {
77
+ const code=args.code; if(!code) return errorResponse("code 参数不能为空")
78
+ const l=["=== ADD 合规检查(ADD-1~6)===",""]
79
+ const sr=/auditPhaseStart\(["']([^"']+)["']/g, er=/auditPhaseEnd\(["']([^"']+)["']/g; const ss:string[]=[], es:string[]=[]; let m; while((m=sr.exec(code))!==null) ss.push(m[1]); while((m=er.exec(code))!==null) es.push(m[1])
80
+ const sc:Record<string,number>={}, ec:Record<string,number>={}; for(const s of ss) sc[s]=(sc[s]||0)+1; for(const e of es) ec[e]=(ec[e]||0)+1
81
+ const ap=new Set([...Object.keys(sc),...Object.keys(ec)]); let symOk=true; ap.forEach(p=>{if((sc[p]||0)!==(ec[p]||0)) symOk=false})
82
+ l.push("[ADD-2] 阶段标记对称性:"," Start: "+ss.length+", End: "+es.length,symOk?" ✅ 对称":" ❌ 不对称: "+[...ap].filter(p=>(sc[p]||0)!==(ec[p]||0)).join(", "),"")
83
+ const hasAgentAudit=code.includes("agentAudit(")
84
+ l.push("[ADD-1] 审计导入: "+ (hasAgentAudit||ss.length>0?"✅ 已导入审计调用":"⚠️ 未检测到审计调用"),"")
85
+ l.push("[ADD-3] 循环体审计: "+ (code.includes("for")||code.includes("while")||code.includes(".forEach")||code.includes(".map")? "⚠️ 循环体需逐项审计": "✅ 未检测到循环"),"")
86
+ const hasConsole=code.includes("console.log"); const hasFile=code.includes("writeToFile")||code.includes("appendFile"); const hasPrisma=code.includes("prisma.")||code.includes("AuditLog")
87
+ l.push("[ADD-4] 三通道输出: ",` console: ${hasConsole?"✅":"⚠️"}`,` file: ${hasFile?"✅":"⚠️"}`,` DB: ${hasPrisma?"✅":"⚠️"}`,"")
88
+ l.push("[ADD-5/6] 失败路径: "+(code.includes("catch")?"⚠️ catch 块需等价审计":"✅ 无异常处理"))
89
+ return textResponse(l.join("\n"))
90
+ } catch(e) { return errorResponse(`合规检查失败: ${e instanceof Error?e.message:String(e)}`) }
91
+ })
92
+
93
+ }
@@ -0,0 +1,7 @@
1
+ import type { McpServer } from "@modelcontextprotocol/server"
2
+
3
+ export type ToolResponse = Array<{ type: "text"; text: string }>
4
+
5
+ export interface GuardResult { ok: boolean; issues: string }
6
+
7
+ export type ToolRegistrar = (server: McpServer) => void