add-coder 0.2.8 → 0.2.10

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 (57) hide show
  1. package/README.md +22 -17
  2. package/dist/index.js +10 -0
  3. package/package.json +4 -11
  4. package/templates/adapters/claude/hooks/doc-format-guard.sh +17 -0
  5. package/templates/adapters/claude/hooks/lib/notify.sh +34 -0
  6. package/templates/adapters/claude/hooks/pre-tool-use.sh +26 -7
  7. package/templates/adapters/claude/hooks/prompt-submit.sh +16 -0
  8. package/templates/adapters/codex/hooks/doc-format-guard.sh +17 -0
  9. package/templates/adapters/codex/hooks/lib/notify.sh +34 -0
  10. package/templates/adapters/codex/hooks/pre-tool-use.sh +48 -18
  11. package/templates/adapters/codex/hooks/prompt-submit.sh +16 -0
  12. package/templates/adapters/qoder/hooks/doc-format-guard.sh +17 -0
  13. package/templates/adapters/qoder/hooks/lib/notify.sh +34 -0
  14. package/templates/adapters/qoder/hooks/pre-tool-use.sh +28 -9
  15. package/templates/adapters/qoder/hooks/prompt-submit.sh +18 -1
  16. package/templates/adapters/trae/hooks/doc-format-guard.sh +17 -0
  17. package/templates/adapters/trae/hooks/lib/notify.sh +34 -0
  18. package/templates/adapters/trae/hooks/pre-tool-use.sh +48 -18
  19. package/templates/adapters/trae/hooks/prompt-submit.sh +16 -0
  20. package/templates/adapters/vscode/hooks/doc-format-guard.sh +16 -0
  21. package/templates/adapters/vscode/hooks/lib/notify.sh +34 -0
  22. package/templates/adapters/vscode/hooks/pre-tool-use.sh +26 -7
  23. package/templates/adapters/vscode/hooks/prompt-submit.sh +16 -0
  24. package/templates/core/hooks/doc-format-guard.sh +17 -0
  25. package/templates/core/hooks/lib/notify.sh +34 -0
  26. package/templates/core/hooks/pre-tool-use.sh +48 -18
  27. package/templates/core/hooks/prompt-submit.sh +16 -0
  28. package/templates/core/reviews/.gitkeep +0 -0
  29. package/templates/core/scripts/mcp-server/elicitation/confirm.ts +30 -0
  30. package/templates/core/scripts/mcp-server/elicitation/index.ts +1 -0
  31. package/templates/core/scripts/mcp-server/index.ts +15 -0
  32. package/templates/core/scripts/mcp-server/notifications/hitl.ts +49 -0
  33. package/templates/core/scripts/mcp-server/notifications/hook.ts +268 -0
  34. package/templates/core/scripts/mcp-server/notifications/index.ts +8 -0
  35. package/templates/core/scripts/mcp-server/resources/add-coder-version.ts +25 -0
  36. package/templates/core/scripts/mcp-server/resources/add-state.ts +44 -0
  37. package/templates/core/scripts/mcp-server/resources/hook-events-report.ts +104 -0
  38. package/templates/core/scripts/mcp-server/resources/index.ts +12 -0
  39. package/templates/core/scripts/mcp-server/resources/round-task.ts +22 -0
  40. package/templates/core/scripts/mcp-server/sampling/index.ts +1 -0
  41. package/templates/core/scripts/mcp-server/sampling/review.ts +47 -0
  42. package/templates/core/scripts/mcp-server/shared/env.ts +26 -0
  43. package/templates/core/scripts/mcp-server/shared/fs.ts +40 -0
  44. package/templates/core/scripts/mcp-server/shared/prisma.ts +35 -0
  45. package/templates/core/scripts/mcp-server/shared/response.ts +9 -0
  46. package/templates/core/scripts/mcp-server/tasks/index.ts +2 -0
  47. package/templates/core/scripts/mcp-server/tasks/runner.ts +20 -0
  48. package/templates/core/scripts/mcp-server/tasks/store.ts +20 -0
  49. package/templates/core/scripts/mcp-server/tools/audit.ts +73 -0
  50. package/templates/core/scripts/mcp-server/tools/context.ts +337 -0
  51. package/templates/core/scripts/mcp-server/tools/docs.ts +42 -0
  52. package/templates/core/scripts/mcp-server/tools/gateway.ts +135 -0
  53. package/templates/core/scripts/mcp-server/tools/hook-event-report.ts +92 -0
  54. package/templates/core/scripts/mcp-server/tools/index.ts +17 -0
  55. package/templates/core/scripts/mcp-server/tools/quality.ts +93 -0
  56. package/templates/core/scripts/mcp-server/types.ts +7 -0
  57. package/templates/core/scripts/mcp-server.ts +8 -3455
@@ -0,0 +1,337 @@
1
+ import * as z from "zod/v4"
2
+ import type { McpServer } from "@modelcontextprotocol/server"
3
+ import { readFile } from "fs/promises"
4
+ import { readdir } from "fs/promises"
5
+ import { stat } from "fs/promises"
6
+ import { existsSync } from "fs"
7
+ import { join, relative } from "path"
8
+ import { textResponse, errorResponse } from "../shared/response.js"
9
+ import { readFileSafe, readdirRecursive } from "../shared/fs.js"
10
+ import { PROJECT_ROOT, MAGIC_DIR } from "../shared/fs.js"
11
+
12
+ export function registerContextTools(server: McpServer) {
13
+
14
+ // ===== get_project_context (L166-430) =====
15
+ server.registerTool(
16
+ "get_project_context",
17
+ {
18
+ description: `获取项目的完整上下文信息:目录结构、技术栈、包管理信息、项目规则(ADD 范式约束)、${MAGIC_DIR}/ 工作流产物(templates/specs/reviews/plans)。AI 助手在 MCP-1(上下文优先)中应在生成代码前调用此工具获取项目真实信息,避免幻觉。\n\nscope='add-state' 返回 ADD 工作流状态快照:当前活跃 Plan、Review 未闭环问题、DPS 门禁状态、待执行操作清单。用于空白对话开局时快速介入 ADD 范式。`,
19
+ inputSchema: z.object({
20
+ scope: z.string().optional().describe("获取信息的范围: 'structure' 仅目录结构, 'rules' 仅项目规则, 'package' 仅包信息, 'add-state' ADD 工作流状态, 'all' 全部"),
21
+ }),
22
+ },
23
+ async (args: { scope?: string }) => {
24
+ try {
25
+ const scope = args?.scope || "all"
26
+ const parts: string[] = []
27
+
28
+ if (scope === "all" || scope === "package") {
29
+ const pkg = await readFileSafe(join(PROJECT_ROOT, "package.json"))
30
+ if (pkg) {
31
+ const parsed = JSON.parse(pkg)
32
+ parts.push("=== 项目信息 ===")
33
+ parts.push(`名称: ${parsed.name}`)
34
+ parts.push(`版本: ${parsed.version}`)
35
+ parts.push(`技术栈: Next.js ${parsed.dependencies?.next || "unknown"} + TypeScript + Prisma + LangGraph`)
36
+ parts.push("")
37
+ parts.push("核心依赖:")
38
+ const keyDeps = [
39
+ "next", "@langchain/langgraph", "@prisma/client", "prisma",
40
+ "zustand", "@tanstack/react-query", "chromadb", "zod",
41
+ ]
42
+ for (const dep of keyDeps) {
43
+ const ver = parsed.dependencies?.[dep] || parsed.devDependencies?.[dep]
44
+ if (ver) parts.push(` ${dep}: ${ver}`)
45
+ }
46
+ parts.push("")
47
+ parts.push("可用脚本:")
48
+ for (const [name, script] of Object.entries(parsed.scripts || {})) {
49
+ parts.push(` ${name}: ${String(script)}`)
50
+ }
51
+ parts.push("")
52
+ }
53
+ }
54
+
55
+ if (scope === "all" || scope === "rules") {
56
+ const rules = await readFileSafe(join(PROJECT_ROOT, MAGIC_DIR, "rules", "project_rules.md"))
57
+ if (rules) {
58
+ parts.push("=== 项目规则 (ADD 范式强制约束) ===")
59
+ const lines = rules.split("\n")
60
+ let inCodeBlock = false
61
+ for (const line of lines) {
62
+ if (line.startsWith("```")) { inCodeBlock = !inCodeBlock; continue }
63
+ if (inCodeBlock) continue
64
+ if (line.startsWith("## ADD-") || line.startsWith("## 项目")) { parts.push(""); parts.push(line) }
65
+ else if (line.startsWith("###") || line.startsWith("####")) { parts.push(line) }
66
+ }
67
+ parts.push("")
68
+ }
69
+ }
70
+
71
+ if (scope === "all" || scope === "structure" || scope === "add-state") {
72
+ parts.push("=== ADD 工作流状态 ===")
73
+ const plansDir = join(PROJECT_ROOT, MAGIC_DIR, "plans")
74
+ const reviewsDir = join(PROJECT_ROOT, MAGIC_DIR, "reviews")
75
+ const specsDir = join(PROJECT_ROOT, MAGIC_DIR, "specs")
76
+
77
+ let activePlan = ""
78
+ let activePlanPath = ""
79
+ if (existsSync(plansDir)) {
80
+ const planFiles = (await readdirRecursive(plansDir))
81
+ .filter(f => f.endsWith(".md") && !f.includes("add-route") && !f.includes("handoff"))
82
+ .sort()
83
+ if (planFiles.length > 0) {
84
+ activePlan = planFiles[planFiles.length - 1]
85
+ activePlanPath = join(plansDir, activePlan)
86
+ parts.push(`最近 Plan: ${activePlan}`)
87
+ const allPlanFiles = await readdirRecursive(plansDir)
88
+ const addRouteFile = allPlanFiles.find(f => f.includes("add-route"))
89
+ const planKeyword = activePlan.replace(/-plan-v\d+\.md$/, "")
90
+ const handoffFiles = allPlanFiles.filter(f => f.includes("handoff"))
91
+ const hasHandoff = handoffFiles.some(f => f.toLowerCase().includes(planKeyword.toLowerCase()))
92
+ parts.push(` add-route: ${addRouteFile ? "✅ " + addRouteFile : "❌ 缺失(需回退 Step 0.5)"}`)
93
+ parts.push(` handoff: ${hasHandoff ? "✅ 已生成" : "❌ 缺失(需回退 Step 0.5)"}`)
94
+ } else { parts.push("最近 Plan: 无") }
95
+ } else { parts.push("最近 Plan: 无") }
96
+
97
+ let reviewFileName = ""; let reviewP0Count = 0; let reviewP1Count = 0; let reviewBackflowRate = 0
98
+ if (existsSync(reviewsDir) && activePlan) {
99
+ const reviewFiles = await readdir(reviewsDir)
100
+ const planKeyword = activePlan.replace(/-plan-v\d+\.md$/, "")
101
+ const matchingReview = reviewFiles.find(f => f.toLowerCase().includes(planKeyword.toLowerCase()) && f.includes("-review-v"))
102
+ || reviewFiles.find(f => f.toLowerCase().includes(planKeyword.toLowerCase()))
103
+ if (matchingReview) {
104
+ reviewFileName = matchingReview
105
+ const reviewContent = await readFileSafe(join(reviewsDir, matchingReview)) || ""
106
+ const reviewLines = reviewContent.split("\n")
107
+ let inP0 = false, inP1 = false
108
+ for (const line of reviewLines) {
109
+ if (line.match(/P0|ADD.*合规|阻断/)) { inP0 = true; inP1 = false; continue }
110
+ if (line.match(/P1|架构设计.*缺口/)) { inP0 = false; inP1 = true; continue }
111
+ if (line.match(/P2|中等|影响评估|决策结论|方案对比/)) { inP0 = false; inP1 = false; continue }
112
+ if ((inP0 || inP1) && line.trim().startsWith("|") && !line.includes("---")) {
113
+ const cols = line.split("|").map(c => c.trim()).filter(Boolean)
114
+ if (cols.length >= 4 && cols[0].match(/^\d+$/)) {
115
+ if (inP0) reviewP0Count++; else if (inP1) reviewP1Count++
116
+ }
117
+ }
118
+ }
119
+ const planContent = await readFileSafe(activePlanPath) || ""
120
+ if ((reviewP0Count + reviewP1Count) > 0 && planContent) {
121
+ const indicators = ["add-route", "specs/", "handoff", "Task Group", "perExpertTopK", "agri_tech", "ChromaCollectionManager", "迁移路线图", "8 个 Expert", "冗余策略"]
122
+ let hit = 0
123
+ for (const ind of indicators) { if (planContent.toLowerCase().includes(ind.toLowerCase())) hit++ }
124
+ reviewBackflowRate = Math.min(100, Math.round((hit / Math.max(reviewP0Count + reviewP1Count, 1)) * 100))
125
+ }
126
+ parts.push(""); parts.push(`关联 Review: ${matchingReview}`)
127
+ parts.push(` P0 问题: ${reviewP0Count} 个, P1 问题: ${reviewP1Count} 个`)
128
+ parts.push(` 回流状态: 约 ${reviewBackflowRate}%`)
129
+ parts.push(reviewBackflowRate < 70 ? " ⚠️ Review 结论未充分回流至 Plan — 需执行 0.6.5 卡位" : " ✅ Review 结论基本回流至 Plan")
130
+ } else { parts.push(""); parts.push("关联 Review: 无(该 Plan 尚未生成方案评审)") }
131
+ }
132
+
133
+ if (activePlan) {
134
+ const planKeyword = activePlan.replace(/-plan-v\d+\.md$/, "")
135
+ const specDirs = existsSync(specsDir) ? await readdir(specsDir) : []
136
+ const matchingSpec = specDirs.find(d => d.toLowerCase().includes(planKeyword.toLowerCase()))
137
+ parts.push("")
138
+ if (matchingSpec) {
139
+ const hasSpec = existsSync(join(specsDir, matchingSpec, "spec.md"))
140
+ const hasTasks = existsSync(join(specsDir, matchingSpec, "tasks.md"))
141
+ const hasChecklist = existsSync(join(specsDir, matchingSpec, "checklist.md"))
142
+ parts.push(`Specs: ${matchingSpec}/`)
143
+ parts.push(` spec.md: ${hasSpec ? "✅" : "❌"}`)
144
+ parts.push(` tasks.md: ${hasTasks ? "✅" : "❌"}`)
145
+ parts.push(` checklist.md: ${hasChecklist ? "✅" : "❌"}`)
146
+ } else { parts.push("Specs: ❌ 缺失") }
147
+ }
148
+
149
+ parts.push(""); parts.push("=== 待执行 ADD 操作(按流程顺序) ===")
150
+ const todoItems: string[] = []
151
+ if (!activePlan) { todoItems.push("1. [未开始] 用户提出需求 → 生成 Plan") }
152
+ else {
153
+ if (!reviewFileName) { todoItems.push("1. [Step 0] 生成 Plan Review(ADD-9 方案评审)") }
154
+ else if (reviewP0Count + reviewP1Count > 0 && reviewBackflowRate < 70) {
155
+ todoItems.push("1. [0.6.5] Review 结论回流至 Plan — P0/P1 未写入 Plan 体")
156
+ todoItems.push("2. [check_dps] DPS 门禁预计不通过(回流完整度 < 70%)")
157
+ }
158
+ const allPlanDir = await readdirRecursive(plansDir)
159
+ const hasAddRoute = allPlanDir.some(f => f.includes("add-route"))
160
+ if (!hasAddRoute && reviewBackflowRate >= 70) { todoItems.push("2. [Step 0.5] 生成 add-route") }
161
+ const planKw = activePlan.replace(/-plan-v\d+\.md$/, "")
162
+ const sd = existsSync(specsDir) ? await readdir(specsDir) : []
163
+ if (!sd.some(d => d.toLowerCase().includes(planKw.toLowerCase())) && reviewBackflowRate >= 70) { todoItems.push("3. [Step 0] 生成 Specs 三元组") }
164
+ if (todoItems.length === 0) { todoItems.push("✅ ADD 就绪 — check_dps ≥ 85 后可进入 Step 1") }
165
+ }
166
+ for (const item of todoItems) parts.push(item)
167
+ parts.push(""); parts.push("快速指令: 说「执行 add-paradigm Step 0」进入文档先行流程")
168
+ parts.push(" 说「将 Review 结论回流至 Plan」触发 0.6.5 卡位")
169
+ }
170
+
171
+ if (scope === "all" || scope === "structure") {
172
+ parts.push("=== 项目目录结构(顶层) ===")
173
+ const topDirs = [MAGIC_DIR, "src", "prisma", "scripts", "docs", "data", "public"]
174
+ for (const dir of topDirs) {
175
+ const fullPath = join(PROJECT_ROOT, dir)
176
+ if (existsSync(fullPath)) { const entries = await readdir(fullPath); parts.push(` ${dir}/ (${entries.length} 项)`) }
177
+ }
178
+ parts.push(""); parts.push("=== src/ 子目录结构 ===")
179
+ const srcDirs = ["agents", "app", "components", "lib", "services", "stores", "types"]
180
+ for (const dir of srcDirs) {
181
+ const fullPath = join(PROJECT_ROOT, "src", dir)
182
+ if (existsSync(fullPath)) {
183
+ const entries = await readdir(fullPath)
184
+ const items = (await Promise.all(entries.slice(0, 15).map(async (e) => { const s = await stat(join(fullPath, e)); return s.isDirectory() ? `${e}/` : e }))).join(", ")
185
+ parts.push(` src/${dir}/ (${entries.length} 项): ${items})`)
186
+ }
187
+ }
188
+ parts.push(""); parts.push(`=== ${MAGIC_DIR}/ ADD 工作流产物 ===`)
189
+ const magicDirs = [
190
+ { dir: "templates", desc: "ADD 文档模板(11 个)" }, { dir: "specs", desc: "specs 三元组(spec+tasks+checklist)" },
191
+ { dir: "reviews", desc: "方案审查 + 实现审查 + 运行时审查" }, { dir: "plans", desc: "Plan + handoff 交接手册" },
192
+ { dir: "rules", desc: "项目规则 + 理论→实践映射" }, { dir: "skills", desc: "SKILL 行为定义(add-paradigm / session-init)" },
193
+ { dir: "scripts", desc: "工具脚本 + MCP 服务器" },
194
+ ]
195
+ for (const { dir, desc } of magicDirs) {
196
+ const fullPath = join(PROJECT_ROOT, MAGIC_DIR, dir)
197
+ if (existsSync(fullPath)) { const entries = await readdir(fullPath); parts.push(` ${MAGIC_DIR}/${dir}/ (${entries.length} 项) — ${desc}`) }
198
+ }
199
+ }
200
+ return textResponse(parts.join("\n"))
201
+ } catch (error) { return errorResponse(`获取项目上下文失败: ${error instanceof Error ? error.message : String(error)}`) }
202
+ }
203
+ )
204
+
205
+ // ===== get_db_schema (L433-522) =====
206
+ server.registerTool(
207
+ "get_db_schema",
208
+ {
209
+ description: "获取 Prisma 数据库 Schema 定义(MCP-1 上下文优先)。返回指定模型的结构、字段、关系。AI 助手在编写数据库查询代码时应调用此工具获取真实的 Schema 信息,避免凭记忆假设。",
210
+ inputSchema: z.object({ model: z.string().optional().describe("可选的模型名称(不区分大小写)。不指定则返回所有模型概况。指定则返回该模型的完整字段定义。") }),
211
+ },
212
+ async (args: { model?: string }) => {
213
+ try {
214
+ const schemaPath = join(PROJECT_ROOT, "prisma", "schema.prisma")
215
+ const schema = await readFileSafe(schemaPath)
216
+ if (!schema) return errorResponse("未找到 prisma/schema.prisma 文件")
217
+ const modelName = args?.model?.toLowerCase()
218
+ if (modelName) {
219
+ const modelRegex = new RegExp(`model\\s+${modelName}\\s*\\{`, "i")
220
+ const match = schema.match(modelRegex)
221
+ if (match) {
222
+ const startIdx = match.index ?? 0; const braceIdx = schema.indexOf("{", startIdx)
223
+ if (braceIdx !== -1) { let depth = 1; let endIdx = braceIdx + 1; while (depth > 0 && endIdx < schema.length) { if (schema[endIdx] === "{") depth++; else if (schema[endIdx] === "}") depth--; endIdx++ }; return textResponse(`=== Model: ${args.model} ===\n\nmodel ${args.model} ${schema.slice(braceIdx, endIdx)}`) }
224
+ }
225
+ const enumMatch = schema.match(new RegExp(`enum\\s+${modelName}\\s*\\{`, "i"))
226
+ if (enumMatch) return textResponse(`=== Enum: ${args.model} ===\n\n可用的枚举值见不传参调用结果。`)
227
+ return errorResponse(`未找到模型或枚举: ${args.model}。可用模型见不传参调用结果。`)
228
+ }
229
+ const models: Array<{ name: string; fieldCount: number }> = []
230
+ const modelRegex = /model\s+(\w+)\s*\{/g; let m
231
+ while ((m = modelRegex.exec(schema)) !== null) {
232
+ const startIdx = m.index; const braceIdx = schema.indexOf("{", startIdx)
233
+ if (braceIdx !== -1) { let depth = 1; let endIdx = braceIdx + 1; while (depth > 0 && endIdx < schema.length) { if (schema[endIdx] === "{") depth++; else if (schema[endIdx] === "}") depth--; endIdx++ }; const body = schema.slice(braceIdx, endIdx); const fieldCount = body.split("\n").filter((l: string) => l.trim() && !l.trim().startsWith("//") && !l.trim().startsWith("@@")).length; models.push({ name: m[1], fieldCount }) }
234
+ }
235
+ const enums: string[] = []; const enumRegex = /enum\s+(\w+)\s*\{/g
236
+ while ((m = enumRegex.exec(schema)) !== null) { enums.push(m[1]) }
237
+ const parts = ["=== Prisma Schema 概况 ===", "", `模型 (${models.length} 个):`]
238
+ for (const model of models) parts.push(` ${model.name} (${model.fieldCount} 字段)`)
239
+ if (enums.length > 0) { parts.push(""); parts.push(`枚举 (${enums.length} 个): ${enums.join(", ")}`) }
240
+ parts.push("", '提示: 指定 model 参数获取完整字段定义,例如: get_db_schema({ model: "User" })')
241
+ return textResponse(parts.join("\n"))
242
+ } catch (error) { return errorResponse(`获取 Schema 失败: ${error instanceof Error ? error.message : String(error)}`) }
243
+ }
244
+ )
245
+
246
+ // ===== get_audit_logger_pattern (L524-612) =====
247
+ server.registerTool(
248
+ "get_audit_logger_pattern",
249
+ {
250
+ description: "获取指定域的审计日志器完整代码或模式(MCP-1 上下文优先)。历史域('knowledge-base', 'agent')返回已有的混合式日志器代码;新域返回三层分离式模板(ADD-4 三层可插拔架构):\n- Layer 1 开发审计(dev-logger): console + file + DB metadata,可插拔\n- Layer 2 运行时审计(audit): console + AuditLog 表,始终开启\n- Layer 3 调试日志: console only,LOG_LEVEL 控制\n新业务域必须使用三层分离模式。",
251
+ inputSchema: z.object({ domain: z.string().describe("审计日志器域: 'knowledge-base', 'agent'(历史混合式),或新域如 'personnel'(三层分离式)") }),
252
+ },
253
+ async (args: { domain: string }) => {
254
+ try {
255
+ const domain = args?.domain; if (!domain) return errorResponse("domain 参数不能为空")
256
+ const filePath = (() => { switch(domain) { case "knowledge-base": return join(PROJECT_ROOT, "src", "lib", "audit-logger.ts"); case "agent": return join(PROJECT_ROOT, "src", "lib", "agent-audit-logger.ts"); default: return "" } })()
257
+ if (filePath) {
258
+ const content = await readFileSafe(filePath); if (!content) return errorResponse(`未找到 ${domain} 审计日志器文件: ${filePath}`)
259
+ const meta: Record<string, { prefix: string; logDir: string; logFile: string }> = { "knowledge-base": { prefix: "[KB-AUDIT]", logDir: "logs/knowledge-base/", logFile: "kb-audit.log" }, "agent": { prefix: "[AGENT-AUDIT]", logDir: "logs/agent/", logFile: "agent-audit.log" } }
260
+ const m = meta[domain]
261
+ const parts = [`=== ${domain} 审计日志器(历史混合式,Layer 1 + Layer 2 未分离) ===`, `前缀: ${m.prefix}`, `日志目录: ${m.logDir}`, `日志文件: ${m.logFile}`, `文件路径: ${relative(PROJECT_ROOT, filePath)}`, "", "=== 完整代码 ===", content, "", "=== 模式要点 ===", "1. PREFIX 常量: [DOMAIN-AUDIT] 格式", `2. LOG_DIR: logs/domain/ 目录 (当前: ${m.logDir})`, "3. AuditPhase 类型: 枚举所有业务阶段", "4. audit() / auditPhaseStart() / auditPhaseEnd() 三函数", "5. readRecentLogs() / clearLogs() 读写函数", "6. ENABLE_FILE_LOG 环境变量控制,开发环境默认启用", "7. 三通道输出: console.log + fs.appendFile + 数据库回写", "", "⚠️ 注意: 这是历史混合式模式。新建业务域应使用三层分离模式(调用 generate_audit_logger 生成)。"]
262
+ return textResponse(parts.join("\n"))
263
+ }
264
+ const featureCap = domain.split("-").map((s: string) => s.charAt(0).toUpperCase() + s.slice(1)).join("")
265
+ const fnPrefix = featureCap.charAt(0).toLowerCase() + featureCap.slice(1)
266
+ return textResponse(`=== ${domain} 三层分离式审计日志器(新模式,ADD-4) ===\n\n新建 ${domain} 业务域应调用 generate_audit_logger 生成两个文件:\n\n--- 文件1: src/lib/${domain}-dev-logger.ts (Layer 1 开发审计,可插拔) ---\n仅在 NODE_ENV=development 时生效,用于 AI 合规检查。\nADD-4 三通道输出:\n 1. console.log — ${fnPrefix}Audit / ${fnPrefix}AuditPhaseStart / ${fnPrefix}AuditPhaseEnd\n 2. fs.appendFile — writeToFile(自动)\n 3. DB metadata — build${featureCap}DevAudit() 构建记录 → 业务服务层 saveAuditData 写入\n包含函数: auditPhaseStart / auditPhaseEnd / audit / buildXxxDevAudit / readRecentLogs / clearLogs\n\n--- 文件2: src/lib/${domain}-audit.ts (Layer 2 运行时业务审计,不可插拔) ---\n始终开启,用于业务记录和前端查询。\nADD-4 三通道输出:\n 1. console.log — record${featureCap}Audit\n 2. fs.appendFile — (AuditLog 表替代文件写入)\n 3. DB AuditLog 表 — prisma.auditLog.create\n包含函数: record${featureCap}Audit()\n\n新建业务域必须使用三层分离模式(而非历史混合式)。详见项目规则 ADD-4「三层可插拔架构」。`)
267
+ } catch (error) { return errorResponse(`获取审计日志器失败: ${error instanceof Error ? error.message : String(error)}`) }
268
+ }
269
+ )
270
+
271
+ // ===== get_add_template (L1718-1780) =====
272
+ server.registerTool(
273
+ "get_add_template",
274
+ {
275
+ description: "获取 ADD 工作流的文档模板(ADD-0.1 广义文档先行)。ADD 范式要求每次生成文档产物前必须先读取对应模板,禁止凭记忆生成——模板可能在迭代中已更新。\n\n可用模板(12 个):\n- plan-template.md — 需求方案\n- add-route-template.md — Plan→ADD 九阶段执行映射(轻量模式,适合前端/小项目)\n- add-route-template-heavyweight.md — Plan→ADD 九阶段执行映射(重型模式,适合后端/管线/合规场景,强制 check_spec_sync 文档-代码交叉校验)\n- spec-template.md — 功能规格\n- tasks-template.md — 任务拆分\n- checklist-template.md — 验收清单\n- review-template.md — 方案审查(ADD-9)\n- review-implementation-template.md — 实现审查(ADD-10)\n- review-runtime-template.md — 运行时纠偏(ADD-11)\n- handoff-template.md — 交接总览索引\n- handoff-single-round-template.md — 单轮交接(9 章节)\n- handoff-multi-round-template.md — 多轮交接(13 子章节/轮)\n\n本项目默认使用重型 add-route 模板。",
276
+ inputSchema: z.object({ template: z.string().describe("模板名称(不含 .md 后缀),如 'plan-template', 'add-route-template', 'spec-template', 'review-template'。传 'list' 获取所有模板列表。") }),
277
+ },
278
+ async (args: { template: string }) => {
279
+ try {
280
+ const { template } = args; if (!template) return errorResponse("template 参数不能为空")
281
+ const templatesDir = join(PROJECT_ROOT, MAGIC_DIR, "templates")
282
+ if (template === "list") {
283
+ const entries = await readdir(templatesDir); const mdFiles = entries.filter(f => f.endsWith(".md"))
284
+ const parts = [`=== ADD 模板列表(${mdFiles.length} 个) ===`, ""]
285
+ for (const f of mdFiles) { const content = await readFileSafe(join(templatesDir, f)); const firstLine = content?.split("\n")[0] || ""; const title = firstLine.replace(/^#+\s*/, "").trim() || f; parts.push(` ${f.replace(".md", "")} — ${title}`) }
286
+ parts.push("", '用法: get_add_template({ template: "plan-template" })'); return textResponse(parts.join("\n"))
287
+ }
288
+ const fileName = template.endsWith(".md") ? template : `${template}.md`
289
+ const filePath = join(templatesDir, fileName)
290
+ if (!existsSync(filePath)) return errorResponse(`未找到模板: ${fileName}\n可用模板请调用: get_add_template({ template: "list" })`)
291
+ const content = await readFile(filePath, "utf-8")
292
+ return textResponse([`=== ADD 模板: ${fileName} ===`, `路径: ${MAGIC_DIR}/templates/${fileName}`, "", content, "", "=== 使用提示 ===", "1. 复制模板到目标路径,替换 {占位符} 为实际内容", "2. 禁止凭记忆生成——模板可能在迭代中已更新", "3. 生成文档后调用 record_dev_operation 记录(targetType 视产物类型而定)"].join("\n"))
293
+ } catch (error) { return errorResponse(`获取 ADD 模板失败: ${error instanceof Error ? error.message : String(error)}`) }
294
+ }
295
+ )
296
+
297
+ // ===== get_spec_context (L1782-1873) =====
298
+ server.registerTool(
299
+ "get_spec_context",
300
+ {
301
+ description: `获取 ${MAGIC_DIR}/specs/ 下指定任务的 specs 三元组上下文(spec.md + tasks.md + checklist.md)。ADD 工作流中每个原子事务对应一个 specs 三元组目录,形成「需求→执行→验收」闭环。\n\nAI 助手在 ADD 范式 Step 3 执行业务逻辑时,应调用此工具获取当前任务的 spec/tasks/checklist 上下文,确保实现与规格一致。`,
302
+ inputSchema: z.object({
303
+ task: z.string().describe(`任务名(即 ${MAGIC_DIR}/specs/ 下的目录名),如 'co-agent-response-strategy'。传 'list' 获取所有任务列表。`),
304
+ file: z.string().optional().describe("指定读取三元组中的某个文件: 'spec', 'tasks', 'checklist'。不传则返回全部三个文件。"),
305
+ }),
306
+ },
307
+ async (args: { task: string; file?: string }) => {
308
+ try {
309
+ const { task, file } = args; if (!task) return errorResponse("task 参数不能为空")
310
+ const specsDir = join(PROJECT_ROOT, MAGIC_DIR, "specs")
311
+ if (task === "list") {
312
+ const entries = await readdir(specsDir, { withFileTypes: true }); const dirs = entries.filter(e => e.isDirectory()).map(e => e.name)
313
+ const parts = [`=== Specs 任务列表(${dirs.length} 个) ===`, ""]
314
+ for (const dir of dirs) {
315
+ const specPath = join(specsDir, dir, "spec.md"); const content = await readFileSafe(specPath)
316
+ const firstLine = content?.split("\n")[0] || ""; const title = firstLine.replace(/^#+\s*/, "").trim() || dir
317
+ const hasTasks = existsSync(join(specsDir, dir, "tasks.md")); const hasChecklist = existsSync(join(specsDir, dir, "checklist.md"))
318
+ parts.push(` ${(hasTasks && hasChecklist) ? "✅" : "⚠️ 不完整"} ${dir} — ${title}`)
319
+ }
320
+ parts.push("", '用法: get_spec_context({ task: "co-agent-response-strategy" })'); return textResponse(parts.join("\n"))
321
+ }
322
+ const taskDir = join(specsDir, task)
323
+ if (!existsSync(taskDir)) return errorResponse(`未找到 specs 任务: ${task}\n可用任务请调用: get_spec_context({ task: "list" })`)
324
+ const trinity: Record<string, string> = { spec: "spec.md", tasks: "tasks.md", checklist: "checklist.md" }
325
+ const parts = [`=== Specs 三元组: ${task} ===`, `路径: ${MAGIC_DIR}/specs/${task}/`, ""]
326
+ if (file && trinity[file]) {
327
+ const fp = join(taskDir, trinity[file]); const content = await readFileSafe(fp)
328
+ if (content) { parts.push(`=== ${trinity[file]} ===`); parts.push(content) } else { parts.push(`⚠️ ${trinity[file]} 不存在`) }
329
+ } else {
330
+ for (const [_key, fn] of Object.entries(trinity)) { const fp = join(taskDir, fn); const content = await readFileSafe(fp); if (content) { parts.push(`=== ${fn} ===`); parts.push(content); parts.push("") } else { parts.push(`⚠️ ${fn} 不存在(三元组不完整)`); parts.push("") } }
331
+ }
332
+ return textResponse(parts.join("\n"))
333
+ } catch (error) { return errorResponse(`获取 Spec 上下文失败: ${error instanceof Error ? error.message : String(error)}`) }
334
+ }
335
+ )
336
+
337
+ }
@@ -0,0 +1,42 @@
1
+ import * as z from "zod/v4"
2
+ import type { McpServer } from "@modelcontextprotocol/server"
3
+ import { readdir } from "fs/promises"
4
+ import { existsSync } from "fs"
5
+ import { join } from "path"
6
+ import { textResponse, errorResponse } from "../shared/response.js"
7
+ import { readFileSafe } from "../shared/fs.js"
8
+ import { PROJECT_ROOT, MAGIC_DIR } from "../shared/fs.js"
9
+
10
+ export function registerDocsTools(server: McpServer) {
11
+
12
+ server.registerTool("find_related_docs", {
13
+ description: `搜索与当前变更相关的项目文档(ADD-0.1 广义文档先行)。搜索范围:\n1. docs/ 目录 — 需求文档、架构文档、规范文档\n2. ${MAGIC_DIR}/ 产物 — plans/(Plan+add-route+handoff)、specs/(三元组)、reviews/(方案审查+实现审查+运行时审查)\n\nAI 助手在 ADD 范式 Step 0 中应调用此工具查找需要更新的 docs/ 文档。`,
14
+ inputSchema: z.object({
15
+ query: z.string().describe("搜索关键词"),
16
+ category: z.string().optional().describe("文档类别过滤: 'requirement', 'architecture', 'standard', 'plan', 'add-route', 'spec', 'review', 'handoff'"),
17
+ }),
18
+ }, async (args, _ctx) => {
19
+ try {
20
+ const { query, category } = args
21
+ const docsDir = join(PROJECT_ROOT, "docs")
22
+ const catPrefixes: Record<string, string[]> = { requirement: ["00-需求"], architecture: ["01-架构","02-架构"], standard: ["02-规范","03-规范"] }
23
+ const magicCat: Record<string, string> = { plans: "plan", specs: "spec", reviews: "review" }
24
+ const allFiles: Array<{ path: string; relativePath: string; category: string }> = []
25
+ const walk = async (dir: string, rel: string, src: "docs" | "magic" = "docs") => {
26
+ try { for (const e of await readdir(dir, { withFileTypes: true })) { const fp = join(dir, e.name); const rp = rel ? `${rel}/${e.name}` : e.name; if (e.isDirectory()) await walk(fp, rp, src); else if (e.name.endsWith(".md") || e.name.endsWith(".html")) { let c = "unknown"; if (src === "magic") { const td = rp.split("/")[0]; c = magicCat[td] || "unknown"; if (td === "plans" && e.name.includes("handoff")) c = "handoff"; if (td === "plans" && e.name.includes("add-route")) c = "add-route" } else { for (const [cat, pf] of Object.entries(catPrefixes)) if (pf.some(p => rp.includes(p))) { c = cat; break } }; allFiles.push({ path: fp, relativePath: rp, category: c }) } } } catch { /* fallthrough */ }
27
+ }
28
+ await walk(docsDir, "", "docs")
29
+ for (const md of ["plans","specs","reviews"]) { const p = join(PROJECT_ROOT, MAGIC_DIR, md); if (existsSync(p)) await walk(p, md, "magic") }
30
+ let filtered = allFiles
31
+ if (category) { const ac: Record<string, string[]> = { ...catPrefixes, plan: ["plan"], "add-route": ["add-route"], spec: ["spec"], review: ["review"], handoff: ["handoff"] }; if (ac[category]) filtered = allFiles.filter(f => f.category === category) }
32
+ const ql = query.toLowerCase(); const matches: Array<{ path: string; relativePath: string; category: string; title: string; relevance: number }> = []
33
+ for (const d of filtered) { let r = 0; if (d.relativePath.toLowerCase().includes(ql)) r += 3; let t = ""; try { const c = await readFileSafe(d.path); if (c) { const m = c.match(/^#\s+(.+)/m); t = m ? m[1].trim() : c.split("\n")[0].replace(/^#+\s*/,"").replace(/[#*]/g,"").trim(); if (c.toLowerCase().includes(ql)) r += 2 } } catch { /* fallthrough */ }; if (r > 0) matches.push({ ...d, title: t || d.relativePath.split("/").pop()!, relevance: r }) }
34
+ matches.sort((a,b) => b.relevance - a.relevance)
35
+ const p: string[] = [`=== 项目文档搜索: "${query}" ===`, `匹配文档数: ${matches.length}`, ""]
36
+ if (!matches.length) { p.push("未找到匹配的文档。"); p.push("=== 可用文档列表 ==="); for (const d of allFiles) p.push(` [${d.category}] ${d.relativePath}`) }
37
+ else { for (const d of matches) p.push(` [相关度 ${d.relevance}] ${d.relativePath}\n 标题: ${d.title}\n`) }
38
+ return textResponse(p.join("\n"))
39
+ } catch (error) { return errorResponse(`搜索文档失败: ${error instanceof Error ? error.message : String(error)}`) }
40
+ })
41
+
42
+ }
@@ -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
+ }