add-coder 0.3.25 → 0.3.27

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 (132) hide show
  1. package/README.md +36 -10
  2. package/dist/index.js +481 -209
  3. package/package.json +14 -4
  4. package/templates/.add-coder-src-hash.json +129 -106
  5. package/templates/adapters/claude/hooks/doc-format-guard.sh +114 -33
  6. package/templates/adapters/claude/hooks/lib/common.sh +46 -87
  7. package/templates/adapters/claude/hooks/lib/context-inject.sh +2 -2
  8. package/templates/adapters/claude/hooks/lib/notify.sh +6 -4
  9. package/templates/adapters/claude/hooks/lib/preload-templates.sh +27 -2
  10. package/templates/adapters/claude/hooks/lib/state-detect.sh +8 -112
  11. package/templates/adapters/claude/hooks/lib/vocabulary.sh +13 -7
  12. package/templates/adapters/claude/hooks/prompt-submit.sh +1 -1
  13. package/templates/adapters/claude/hooks/stop-check.sh +9 -0
  14. package/templates/adapters/codex/config.toml.example +12 -0
  15. package/templates/adapters/codex/hooks/doc-format-guard.sh +123 -41
  16. package/templates/adapters/codex/hooks/lib/common.sh +47 -99
  17. package/templates/adapters/codex/hooks/lib/context-inject.sh +4 -4
  18. package/templates/adapters/codex/hooks/lib/notify.sh +6 -4
  19. package/templates/adapters/codex/hooks/lib/preload-templates.sh +27 -6
  20. package/templates/adapters/codex/hooks/lib/state-detect.sh +5 -114
  21. package/templates/adapters/codex/hooks/lib/vocabulary.sh +3 -8
  22. package/templates/adapters/codex/hooks/notification.sh +3 -18
  23. package/templates/adapters/codex/hooks/post-tool-use.sh +24 -119
  24. package/templates/adapters/codex/hooks/pre-tool-use.sh +97 -184
  25. package/templates/adapters/codex/hooks/prompt-submit.sh +8 -3
  26. package/templates/adapters/codex/hooks/session-start.sh +19 -9
  27. package/templates/adapters/codex/hooks/stop-check.sh +44 -18
  28. package/templates/adapters/codex/hooks.json +67 -1
  29. package/templates/adapters/qoder/hooks/doc-format-guard.sh +121 -40
  30. package/templates/adapters/qoder/hooks/lib/common.sh +49 -87
  31. package/templates/adapters/qoder/hooks/lib/context-inject.sh +2 -2
  32. package/templates/adapters/qoder/hooks/lib/notify.sh +6 -4
  33. package/templates/adapters/qoder/hooks/lib/preload-templates.sh +27 -2
  34. package/templates/adapters/qoder/hooks/lib/state-detect.sh +8 -112
  35. package/templates/adapters/qoder/hooks/lib/vocabulary.sh +13 -7
  36. package/templates/adapters/qoder/hooks/prompt-submit.sh +1 -1
  37. package/templates/adapters/qoder/hooks/session-start.sh +1 -1
  38. package/templates/adapters/qoder/hooks/stop-check.sh +11 -0
  39. package/templates/adapters/trae/hooks/doc-format-guard.sh +122 -40
  40. package/templates/adapters/trae/hooks/lib/common.sh +46 -87
  41. package/templates/adapters/trae/hooks/lib/context-inject.sh +2 -2
  42. package/templates/adapters/trae/hooks/lib/notify.sh +5 -3
  43. package/templates/adapters/trae/hooks/lib/preload-templates.sh +27 -2
  44. package/templates/adapters/trae/hooks/lib/state-detect.sh +8 -115
  45. package/templates/adapters/trae/hooks/lib/vocabulary.sh +13 -7
  46. package/templates/adapters/trae/hooks/pre-tool-use.sh +14 -8
  47. package/templates/adapters/trae/hooks/stop-check.sh +9 -0
  48. package/templates/adapters/vscode/hooks/doc-format-guard.sh +114 -33
  49. package/templates/adapters/vscode/hooks/lib/common.sh +46 -87
  50. package/templates/adapters/vscode/hooks/lib/context-inject.sh +2 -2
  51. package/templates/adapters/vscode/hooks/lib/notify.sh +6 -4
  52. package/templates/adapters/vscode/hooks/lib/preload-templates.sh +27 -2
  53. package/templates/adapters/vscode/hooks/lib/state-detect.sh +8 -112
  54. package/templates/adapters/vscode/hooks/lib/vocabulary.sh +13 -7
  55. package/templates/adapters/vscode/hooks/notification.sh +3 -5
  56. package/templates/adapters/vscode/hooks/prompt-submit.sh +1 -1
  57. package/templates/adapters/vscode/hooks/stop-check.sh +9 -0
  58. package/templates/core/docs/ADD-governance-claude-code.md +1 -1
  59. package/templates/core/docs/ADD-governance-codex.md +1 -1
  60. package/templates/core/docs/ADD-governance-qoder-cn.md +1 -1
  61. package/templates/core/docs/ADD-governance-trae.md +1 -1
  62. package/templates/core/docs/ADD-governance-vscode-copilot.md +1 -1
  63. package/templates/core/hooks/doc-format-guard.sh +122 -40
  64. package/templates/core/hooks/lib/common.sh +46 -87
  65. package/templates/core/hooks/lib/context-inject.sh +2 -2
  66. package/templates/core/hooks/lib/notify.sh +5 -3
  67. package/templates/core/hooks/lib/preload-templates.sh +27 -2
  68. package/templates/core/hooks/lib/state-detect.sh +8 -115
  69. package/templates/core/hooks/lib/vocabulary.sh +13 -7
  70. package/templates/core/hooks/pre-tool-use.sh +14 -8
  71. package/templates/core/hooks/stop-check.sh +9 -0
  72. package/templates/core/prisma/add.prisma +140 -82
  73. package/templates/core/rules/project_rules.md +27 -24
  74. package/templates/core/scripts/db-ensure.sh +8 -0
  75. package/templates/core/scripts/gen-plan-index.sh +4 -15
  76. package/templates/core/scripts/mcp-server/notifications/hitl.ts +9 -1
  77. package/templates/core/scripts/mcp-server/notifications/hook.ts +25 -9
  78. package/templates/core/scripts/mcp-server/resources/hitl-approval-widget.ts +34 -0
  79. package/templates/core/scripts/mcp-server/resources/hook-events-report.ts +14 -2
  80. package/templates/core/scripts/mcp-server/resources/index.ts +2 -0
  81. package/templates/core/scripts/mcp-server/sampling/review.ts +25 -10
  82. package/templates/core/scripts/mcp-server/shared/db-types.ts +33 -8
  83. package/templates/core/scripts/mcp-server/shared/dps-scoring.strategy.ts +1 -1
  84. package/templates/core/scripts/mcp-server/shared/env.ts +28 -8
  85. package/templates/core/scripts/mcp-server/shared/fs.ts +3 -0
  86. package/templates/core/scripts/mcp-server/shared/hitl-interaction.strategy.ts +2 -3
  87. package/templates/core/scripts/mcp-server/shared/hitl-lifecycle-mutation.ts +129 -0
  88. package/templates/core/scripts/mcp-server/shared/hitl-proposal-mutation.ts +98 -0
  89. package/templates/core/scripts/mcp-server/shared/hitl-ui.ts +3 -0
  90. package/templates/core/scripts/mcp-server/shared/plan-lifecycle-events.ts +62 -0
  91. package/templates/core/scripts/mcp-server/shared/plan-lifecycle-mutation.ts +78 -0
  92. package/templates/core/scripts/mcp-server/shared/plan-lifecycle-subscriber.ts +145 -0
  93. package/templates/core/scripts/mcp-server/shared/plan-lifecycle.ts +153 -0
  94. package/templates/core/scripts/mcp-server/shared/plan-round-events.ts +56 -0
  95. package/templates/core/scripts/mcp-server/shared/plan-round-mutation.ts +143 -0
  96. package/templates/core/scripts/mcp-server/shared/plan-round-store.ts +47 -0
  97. package/templates/core/scripts/mcp-server/shared/plan-round-subscriber.ts +99 -0
  98. package/templates/core/scripts/mcp-server/shared/plan-status-store.ts +37 -0
  99. package/templates/core/scripts/mcp-server/shared/plan-tracking-mutation.ts +71 -0
  100. package/templates/core/scripts/mcp-server/shared/prisma.ts +12 -6
  101. package/templates/core/scripts/mcp-server/shared/project-root-strategy.ts +1 -2
  102. package/templates/core/scripts/mcp-server/shared/quant-recheck.strategy.ts +16 -0
  103. package/templates/core/scripts/mcp-server/shared/redact.ts +28 -0
  104. package/templates/core/scripts/mcp-server/shared/response.ts +3 -2
  105. package/templates/core/scripts/mcp-server/shared/runtime-context.ts +77 -0
  106. package/templates/core/scripts/mcp-server/shared/schema-topology.ts +159 -0
  107. package/templates/core/scripts/mcp-server/tools/audit.ts +49 -7
  108. package/templates/core/scripts/mcp-server/tools/context.ts +117 -121
  109. package/templates/core/scripts/mcp-server/tools/contract.ts +34 -9
  110. package/templates/core/scripts/mcp-server/tools/gateway/check_add_route_status.ts +50 -3
  111. package/templates/core/scripts/mcp-server/tools/gateway/check_doc_similarity.ts +218 -0
  112. package/templates/core/scripts/mcp-server/tools/gateway/check_dps.ts +22 -7
  113. package/templates/core/scripts/mcp-server/tools/gateway/check_rahs.ts +75 -4
  114. package/templates/core/scripts/mcp-server/tools/gateway/check_spec_sync.ts +19 -6
  115. package/templates/core/scripts/mcp-server/tools/gateway/index.ts +2 -0
  116. package/templates/core/scripts/mcp-server/tools/hitl.ts +160 -75
  117. package/templates/core/scripts/mcp-server/tools/hook-event-report.ts +9 -2
  118. package/templates/core/scripts/mcp-server/tools/index.ts +69 -3
  119. package/templates/core/scripts/mcp-server/tools/plan.ts +140 -15
  120. package/templates/core/scripts/mcp-server/tools/review.ts +44 -15
  121. package/templates/core/scripts/mcp-server.ts +60 -1
  122. package/templates/core/scripts/plan-status-bridge.ts +26 -0
  123. package/templates/core/skills/add-paradigm/SKILL.md +2 -2
  124. package/templates/core/templates/add-route-template.schema.json +24 -56
  125. package/templates/core/templates/collab-contract-template.md +1 -1
  126. package/templates/core/templates/handoff-multi-round-template.schema.json +48 -0
  127. package/templates/core/templates/handoff-single-round-template.schema.json +92 -0
  128. package/templates/core/templates/handoff-single-round.schema.json +1 -1
  129. package/templates/core/templates/hitl-approval-widget.html +222 -37
  130. package/templates/core/templates/spec-template.schema.json +9 -46
  131. package/templates/core/templates/standard-plan-template.schema.json +1 -1
  132. package/templates/core/templates/tasks-template.schema.json +13 -54
@@ -7,6 +7,19 @@ import { readFileSafe, readdirRecursive, PROJECT_ROOT, MAGIC_DIR } from "../shar
7
7
  import { prisma } from "../shared/prisma.js"
8
8
  import type { PlanRow } from "../shared/db-types.js"
9
9
  import { PlanRowSchema, validatedDelegate } from "../shared/db-types.js"
10
+ import { getRuntimeContext } from "../shared/env.js"
11
+ import { assertPathInRuntimeScope } from "../shared/runtime-context.js"
12
+ import { resolvePlanStatus } from "../shared/plan-lifecycle.js"
13
+ import { createPrismaPlanStatusStore } from "../shared/plan-status-store.js"
14
+ import {
15
+ trackPlanAndPublish,
16
+ type PlanTrackingDatabase,
17
+ } from "../shared/plan-tracking-mutation.js"
18
+ import {
19
+ closePlanRoundAndPublish,
20
+ type PlanRoundDatabase,
21
+ } from "../shared/plan-round-mutation.js"
22
+ import { queryPlanRounds, type PlanRoundReadClient } from "../shared/plan-round-store.js"
10
23
 
11
24
  // 无类型边界单点(zod 托管):动态加载的 prisma client 在此一次性转为运行期校验的泛型委托
12
25
  const db = {
@@ -14,6 +27,14 @@ const db = {
14
27
  }
15
28
 
16
29
  export function registerPlanTools(server: ToolRegistrar) {
30
+ const runtimeContext = getRuntimeContext()
31
+ const statusStore = createPrismaPlanStatusStore(prisma)
32
+ const parseStructuredState = (value: string, field: string): Record<string, unknown> | unknown[] => {
33
+ let parsed: unknown
34
+ try { parsed = JSON.parse(value) } catch { throw new Error(`${field} 必须是有效 JSON`) }
35
+ if (parsed === null || typeof parsed !== "object") throw new Error(`${field} 必须是非 null JSON 对象或数组`)
36
+ return parsed as Record<string, unknown> | unknown[]
37
+ }
17
38
 
18
39
  // ===== plan_track =====
19
40
  server.registerTool("plan_track", {
@@ -24,7 +45,7 @@ export function registerPlanTools(server: ToolRegistrar) {
24
45
  }),
25
46
  }, async (args: Record<string, unknown>) => {
26
47
  try {
27
- const { planName, scanAll } = args as { planName?: string; scanAll?: boolean }
48
+ const { planName } = args as { planName?: string; scanAll?: boolean }
28
49
  const pn = typeof planName === "string" ? planName : ""
29
50
  const plansDir = join(PROJECT_ROOT, MAGIC_DIR, "plans")
30
51
  const specsDir = join(PROJECT_ROOT, MAGIC_DIR, "specs")
@@ -32,7 +53,7 @@ export function registerPlanTools(server: ToolRegistrar) {
32
53
 
33
54
  if (!existsSync(plansDir)) return errorResponse(`plans 目录不存在: ${plansDir}`)
34
55
 
35
- let targets: { name: string; path: string; keyword: string }[] = []
56
+ const targets: { name: string; path: string; keyword: string }[] = []
36
57
  const newPlans: string[] = []
37
58
  const allFiles = await readdirRecursive(plansDir)
38
59
  // 排除 .hitl.md(HITL 提案不是 Plan 本体,避免 plan_track 误扫为独立 Plan)
@@ -84,7 +105,6 @@ export function registerPlanTools(server: ToolRegistrar) {
84
105
  )
85
106
  const addRoutePath = addRouteFile ? join(plansDir, addRouteFile) : undefined
86
107
  // upsert PlanRecord
87
- const existing = await db.plan.findFirst({ where: { planName: t.name } })
88
108
  const data: Partial<PlanRow> = {
89
109
  planPath: t.path,
90
110
  specPath: existsSync(specPath) ? specPath : undefined,
@@ -94,11 +114,16 @@ export function registerPlanTools(server: ToolRegistrar) {
94
114
  totalTasks, doneTasks, checklistT, checklistTDone, checklistR,
95
115
  planKeyword: keyword,
96
116
  }
97
- if (existing) {
98
- await db.plan.update({ where: { id: existing.id }, data })
117
+ assertPathInRuntimeScope(runtimeContext, t.path)
118
+ const tracked = await trackPlanAndPublish(prisma as unknown as PlanTrackingDatabase, {
119
+ context: runtimeContext,
120
+ planName: t.name,
121
+ planPath: t.path,
122
+ projection: data,
123
+ })
124
+ if (!tracked.created) {
99
125
  results.push(` ✅ 已更新 (totalTasks=${totalTasks}, done=${doneTasks})` + (addRoutePath ? `, addRoute` : ``))
100
126
  } else {
101
- await db.plan.create({ data: { ...data, planName: t.name, planPath: t.path } })
102
127
  results.push(` ✅ 已创建 (totalTasks=${totalTasks}, done=${doneTasks})` + (addRoutePath ? `, addRoute` : ``))
103
128
  newPlans.push(t.name.replace(/-plan-v\d+$/, ""))
104
129
  }
@@ -106,7 +131,13 @@ export function registerPlanTools(server: ToolRegistrar) {
106
131
  // ── 孤儿对账(全量扫描时):PlanRecord 存在但 planPath 文件缺失 → 悬空清单。
107
132
  // 来源:create_hitl 预置行被中断 / 用户放弃未清理。允许暂时存在,不允许不可见。 ──
108
133
  if (!pn) {
109
- const allPlans = await db.plan.findMany({ orderBy: { createdAt: "desc" } })
134
+ const allPlans = await db.plan.findMany({
135
+ where: {
136
+ projectKey: runtimeContext.projectKey,
137
+ adapterKey: runtimeContext.adapterKey,
138
+ },
139
+ orderBy: { createdAt: "desc" },
140
+ })
110
141
  const orphans = allPlans.filter(p => !existsSync(p.planPath))
111
142
  if (orphans.length > 0) {
112
143
  results.push("", `⚠️ 悬空 Plan(记录在、文件缺失)— 续写或清理:`)
@@ -137,17 +168,35 @@ export function registerPlanTools(server: ToolRegistrar) {
137
168
  }, async (args: Record<string, unknown>) => {
138
169
  try {
139
170
  const { planName } = args as { planName: string }
140
- const plan = await db.plan.findFirst({ where: { planName } })
141
- if (!plan) return textResponse(`📋 Plan: 未跟踪\nplanName: ${planName}\n\n操作: plan_track({ planName: "${planName}" })`)
142
- const t = plan.totalTasks || 0
143
- const d = plan.doneTasks || 0
144
- const ct = plan.checklistT || 0
145
- const ctd = plan.checklistTDone || 0
171
+ const resolution = await resolvePlanStatus(statusStore, runtimeContext, planName)
172
+ if (resolution.availability === "STATUS_UNAVAILABLE") {
173
+ return errorResponse(`Plan status 不可用(禁止按文件猜测): ${resolution.reason}`)
174
+ }
175
+ if (resolution.planName === null) {
176
+ return textResponse(`📋 Plan: 未跟踪\nplanName: ${planName}\nsource: database\ncontextId: ${runtimeContext.contextId}\n\n操作: plan_track({ planName: "${planName}" })`)
177
+ }
178
+ const plan = await db.plan.findFirst({
179
+ where: {
180
+ projectKey: runtimeContext.projectKey,
181
+ adapterKey: runtimeContext.adapterKey,
182
+ planName,
183
+ },
184
+ })
185
+ if (!plan) return errorResponse(`Plan status 解析后记录消失,请重试: ${planName}`)
186
+ const t = resolution.progress.totalTasks
187
+ const d = resolution.progress.doneTasks
188
+ const ct = resolution.progress.checklistT
189
+ const ctd = resolution.progress.checklistTDone
146
190
  const cr = plan.checklistR || 0
147
191
  const taskPct = t > 0 ? Math.round((d / t) * 100) : 0
148
192
  const lines = [
149
193
  `📊 ${planName}`,
150
194
  ``,
195
+ `lifecycle: ${resolution.lifecycle} (${resolution.isActive ? "active" : "inactive"})`,
196
+ `approval: ${resolution.approvalStatus ?? "—"}`,
197
+ `revision: ${resolution.revision}`,
198
+ `source: database`,
199
+ `contextId: ${runtimeContext.contextId}`,
151
200
  `tasks.md: ${d}/${t} (${taskPct}%) ${taskPct >= 100 ? "✅" : d > 0 ? "🔄" : "⬜"}`,
152
201
  `checklist: [T] ${ctd}/${ct} | [R] ${cr} 项`,
153
202
  `planKeyword: ${plan.planKeyword || "—"}`,
@@ -156,6 +205,7 @@ export function registerPlanTools(server: ToolRegistrar) {
156
205
  // P3 #5 契约角色展示(PlanRecord 已设置时)
157
206
  plan.contractRole ? `contractRole: ${plan.contractRole}` : null,
158
207
  plan.contractName ? `contractName: ${plan.contractName}` : null,
208
+ `machine: ${JSON.stringify(resolution)}`,
159
209
  ].filter(Boolean)
160
210
  return textResponse(lines.join("\n"))
161
211
  } catch (e) {
@@ -172,9 +222,16 @@ export function registerPlanTools(server: ToolRegistrar) {
172
222
  }, async (args: Record<string, unknown>) => {
173
223
  try {
174
224
  const { planName } = args as { planName: string }
175
- const plan = await db.plan.findFirst({ where: { planName } })
225
+ const plan = await db.plan.findFirst({
226
+ where: {
227
+ projectKey: runtimeContext.projectKey,
228
+ adapterKey: runtimeContext.adapterKey,
229
+ planName,
230
+ },
231
+ })
176
232
  if (!plan) return errorResponse(`PlanRecord 未找到: ${planName}`)
177
233
  const planPath = plan.planPath
234
+ assertPathInRuntimeScope(runtimeContext, planPath)
178
235
  if (!existsSync(planPath)) return errorResponse(`Plan 文件不存在: ${planPath}`)
179
236
  const content = readFileSync(planPath, "utf-8")
180
237
  const t = plan.totalTasks || 0
@@ -195,7 +252,6 @@ export function registerPlanTools(server: ToolRegistrar) {
195
252
  ].join("\n")
196
253
  // 用 indexOf 安全替换(不用跨行正则)
197
254
  const marker = "## 📊 Plan 进度快照"
198
- const markerEnd = "\n## 📊 Plan 进度快照"
199
255
  // 找到现有快照区块的结束位置
200
256
  const startIdx = content.indexOf(marker)
201
257
  let newContent: string
@@ -217,4 +273,73 @@ export function registerPlanTools(server: ToolRegistrar) {
217
273
  }
218
274
  })
219
275
 
276
+ // ===== plan_round_close =====
277
+ server.registerTool("plan_round_close", {
278
+ description: "关闭一个逻辑 PlanRound:在当前 RuntimeContextKey 内幂等写入 ROUND_CLOSED,并于同一 Prisma transaction 发布 PostgreSQL 唤醒通知。不会关闭整个 Plan,也不会修改 lifecycle/revision。",
279
+ inputSchema: z.object({
280
+ planName: z.string().min(1),
281
+ round: z.number().int().positive(),
282
+ beforeState: z.string().describe("非 null JSON 对象或数组字符串"),
283
+ afterState: z.string().describe("非 null JSON 对象或数组字符串,包含本轮产出摘要"),
284
+ reason: z.string().optional(),
285
+ operationKey: z.string().optional(),
286
+ }),
287
+ }, async (args: Record<string, unknown>) => {
288
+ try {
289
+ const input = args as { planName: string; round: number; beforeState: string; afterState: string; reason?: string; operationKey?: string }
290
+ const result = await closePlanRoundAndPublish(prisma as unknown as PlanRoundDatabase, {
291
+ context: runtimeContext,
292
+ planName: input.planName,
293
+ round: input.round,
294
+ beforeState: parseStructuredState(input.beforeState, "beforeState"),
295
+ afterState: parseStructuredState(input.afterState, "afterState"),
296
+ reason: input.reason,
297
+ operationKey: input.operationKey,
298
+ })
299
+ return textResponse([
300
+ "✅ PlanRound 已关闭",
301
+ `planName: ${result.plan.planName}`,
302
+ `round: ${input.round}`,
303
+ `operationId: ${result.operation.id}`,
304
+ `targetId: ${result.operation.targetId}`,
305
+ `lifecycle: ${result.plan.lifecycle} (未修改)`,
306
+ `revision: ${result.plan.revision} (未修改)`,
307
+ "source: database",
308
+ ].join("\n"))
309
+ } catch (e) {
310
+ return errorResponse(`plan_round_close 失败: ${e instanceof Error ? e.message : String(e)}`)
311
+ }
312
+ })
313
+
314
+ // ===== plan_round_status =====
315
+ server.registerTool("plan_round_status", {
316
+ description: "查询当前 RuntimeContextKey 下的 ROUND_CLOSED 记录。数据库是权威状态;通知仅负责唤醒订阅者调用本查询。",
317
+ inputSchema: z.object({
318
+ planName: z.string().min(1),
319
+ round: z.number().int().positive().optional(),
320
+ }),
321
+ }, async (args: Record<string, unknown>) => {
322
+ try {
323
+ const input = args as { planName: string; round?: number }
324
+ const snapshot = await queryPlanRounds(prisma as unknown as PlanRoundReadClient, {
325
+ context: runtimeContext,
326
+ planName: input.planName,
327
+ round: input.round,
328
+ })
329
+ if (!snapshot.planId) return textResponse(`📋 PlanRound: 未找到 scoped Plan\nplanName: ${input.planName}\ncontextId: ${runtimeContext.contextId}`)
330
+ const lines = [
331
+ `📋 PlanRound: ${snapshot.planName}`,
332
+ `contextId: ${runtimeContext.contextId}`,
333
+ `source: database`,
334
+ `records: ${snapshot.rounds.length}`,
335
+ ]
336
+ for (const record of snapshot.rounds) {
337
+ lines.push(`- ${record.targetId} | ${record.createdAt.toISOString()} | operationId=${record.id}`)
338
+ }
339
+ return textResponse(lines.join("\n"))
340
+ } catch (e) {
341
+ return errorResponse(`plan_round_status 失败: ${e instanceof Error ? e.message : String(e)}`)
342
+ }
343
+ })
344
+
220
345
  }
@@ -7,6 +7,8 @@ import { readFileSafe, readdirRecursive, PROJECT_ROOT, MAGIC_DIR } from "../shar
7
7
  import { prisma } from "../shared/prisma.js"
8
8
  import type { PlanRow, ReviewRow } from "../shared/db-types.js"
9
9
  import { PlanRowSchema, ReviewRowSchema, validatedDelegate } from "../shared/db-types.js"
10
+ import { getRuntimeContext } from "../shared/env.js"
11
+ import { assertPathInRuntimeScope } from "../shared/runtime-context.js"
10
12
 
11
13
  const db = {
12
14
  get review() { return validatedDelegate<ReviewRow>(prisma.reviewRecord, ReviewRowSchema, "ReviewRecord") },
@@ -34,6 +36,7 @@ function countIssueType(content: string): { p0: number; p1: number; backflow: nu
34
36
  }
35
37
 
36
38
  export function registerReviewTools(server: ToolRegistrar) {
39
+ const runtimeContext = getRuntimeContext()
37
40
 
38
41
  // ===== review_track =====
39
42
  server.registerTool("review_track", {
@@ -44,7 +47,7 @@ export function registerReviewTools(server: ToolRegistrar) {
44
47
  }),
45
48
  }, async (args: Record<string, unknown>) => {
46
49
  try {
47
- const { planName, scanAll } = args as { planName?: string; scanAll?: boolean }
50
+ const { planName } = args as { planName?: string; scanAll?: boolean }
48
51
  const pn = typeof planName === "string" ? planName : ""
49
52
  const reviewsDir = join(PROJECT_ROOT, MAGIC_DIR, "reviews")
50
53
  if (!existsSync(reviewsDir)) return errorResponse(`reviews 目录不存在: ${reviewsDir}`)
@@ -57,7 +60,7 @@ export function registerReviewTools(server: ToolRegistrar) {
57
60
  const fullPath = join(reviewsDir, rf)
58
61
  // 从文件名推导 planName
59
62
  // 格式: {plan-name}-review-v1.md 或 {plan-name}-review-implementation.md
60
- let derivedPlan = basename(rf, ".md")
63
+ const derivedPlan = basename(rf, ".md")
61
64
  .replace(/-review-.*$/, "")
62
65
  .replace(/-implementation.*$/, "")
63
66
  .replace(/-runtime.*$/, "")
@@ -69,7 +72,11 @@ export function registerReviewTools(server: ToolRegistrar) {
69
72
  // 而 derivedPlan 为 {planPrefix}(从 review 文件名中去掉了 -review-v{n})
70
73
  // 需要用 contains 匹配,而不是 exact match
71
74
  const planMatch = await db.plan.findFirst({
72
- where: { planName: { contains: derivedPlan } },
75
+ where: {
76
+ projectKey: runtimeContext.projectKey,
77
+ adapterKey: runtimeContext.adapterKey,
78
+ planName: { contains: derivedPlan },
79
+ },
73
80
  })
74
81
  const actualPlanName = planMatch
75
82
  ? planMatch.planName
@@ -81,7 +88,12 @@ export function registerReviewTools(server: ToolRegistrar) {
81
88
  const rtype = detectReviewType(rf)
82
89
  const stats = countIssueType(content)
83
90
  const record = await db.review.findFirst({
84
- where: { planName: actualPlanName, type: rtype },
91
+ where: {
92
+ projectKey: runtimeContext.projectKey,
93
+ adapterKey: runtimeContext.adapterKey,
94
+ planName: actualPlanName,
95
+ type: rtype,
96
+ },
85
97
  })
86
98
 
87
99
  const data = {
@@ -95,7 +107,13 @@ export function registerReviewTools(server: ToolRegistrar) {
95
107
  results.push(`🔄 ${rf} (${rtype}) P0=${stats.p0} P1=${stats.p1}`)
96
108
  } else {
97
109
  await db.review.create({
98
- data: { planName: actualPlanName, type: rtype, ...data },
110
+ data: {
111
+ projectKey: runtimeContext.projectKey,
112
+ adapterKey: runtimeContext.adapterKey,
113
+ planName: actualPlanName,
114
+ type: rtype,
115
+ ...data,
116
+ },
99
117
  })
100
118
  results.push(`✅ ${rf} (${rtype}) P0=${stats.p0} P1=${stats.p1}`)
101
119
  }
@@ -118,7 +136,11 @@ export function registerReviewTools(server: ToolRegistrar) {
118
136
  try {
119
137
  const { planName } = args as { planName: string }
120
138
  const rows = await db.review.findMany({
121
- where: { planName },
139
+ where: {
140
+ projectKey: runtimeContext.projectKey,
141
+ adapterKey: runtimeContext.adapterKey,
142
+ planName,
143
+ },
122
144
  orderBy: { type: "asc" },
123
145
  })
124
146
 
@@ -130,9 +152,9 @@ export function registerReviewTools(server: ToolRegistrar) {
130
152
 
131
153
  const lines = [`📝 Review: ${planName}`, ``]
132
154
  for (const r of rows) {
133
- const p0 = r.p0Count as number || 0
134
- const p1 = r.p1Count as number || 0
135
- const br = r.backflowRate as number || 0
155
+ const p0 = r.p0Count || 0
156
+ const p1 = r.p1Count || 0
157
+ const br = r.backflowRate || 0
136
158
  const total = p0 + p1
137
159
  lines.push(
138
160
  ` [${r.type}] P0=${p0} P1=${p1} (合计 ${total}) 回流=${br}`,
@@ -140,8 +162,8 @@ export function registerReviewTools(server: ToolRegistrar) {
140
162
  )
141
163
  }
142
164
  // 汇总
143
- const tP0 = rows.reduce((s: number, r) => s + (r.p0Count as number || 0), 0)
144
- const tP1 = rows.reduce((s: number, r) => s + (r.p1Count as number || 0), 0)
165
+ const tP0 = rows.reduce((s: number, r) => s + (r.p0Count || 0), 0)
166
+ const tP1 = rows.reduce((s: number, r) => s + (r.p1Count || 0), 0)
145
167
  lines.push(``, `总计: P0=${tP0} P1=${tP1}`)
146
168
  return textResponse(lines.join("\n"))
147
169
  } catch (e) {
@@ -158,7 +180,13 @@ export function registerReviewTools(server: ToolRegistrar) {
158
180
  }, async (args: Record<string, unknown>) => {
159
181
  try {
160
182
  const { planName } = args as { planName: string }
161
- const rows = await db.review.findMany({ where: { planName } })
183
+ const rows = await db.review.findMany({
184
+ where: {
185
+ projectKey: runtimeContext.projectKey,
186
+ adapterKey: runtimeContext.adapterKey,
187
+ planName,
188
+ },
189
+ })
162
190
 
163
191
  if (!rows.length) return errorResponse(`无 ReviewRecord: ${planName}`)
164
192
 
@@ -169,10 +197,11 @@ export function registerReviewTools(server: ToolRegistrar) {
169
197
  results.push(`⚠️ 文件不存在: ${rp || r.type}`)
170
198
  continue
171
199
  }
200
+ assertPathInRuntimeScope(runtimeContext, rp)
172
201
  const content = readFileSync(rp, "utf-8")
173
- const p0 = r.p0Count as number || 0
174
- const p1 = r.p1Count as number || 0
175
- const br = r.backflowRate as number || 0
202
+ const p0 = r.p0Count || 0
203
+ const p1 = r.p1Count || 0
204
+ const br = r.backflowRate || 0
176
205
 
177
206
  const metaLines = [
178
207
  "<!-- REVIEW_META",
@@ -1,6 +1,17 @@
1
1
  import { McpServer } from "@modelcontextprotocol/server"
2
2
  import { StdioServerTransport } from "@modelcontextprotocol/server/stdio"
3
3
  import { registerAll } from "./mcp-server/index.js"
4
+ import { redact } from "./mcp-server/shared/redact.js"
5
+ import { DATABASE_URL, getRuntimeContext } from "./mcp-server/shared/env.js"
6
+ import { prisma } from "./mcp-server/shared/prisma.js"
7
+ import { resolvePlanStatus } from "./mcp-server/shared/plan-lifecycle.js"
8
+ import { createPrismaPlanStatusStore } from "./mcp-server/shared/plan-status-store.js"
9
+ import {
10
+ createPgLifecycleListenClient,
11
+ PlanLifecycleSubscriber,
12
+ } from "./mcp-server/shared/plan-lifecycle-subscriber.js"
13
+ import { PlanRoundSubscriber } from "./mcp-server/shared/plan-round-subscriber.js"
14
+ import { queryPlanRounds, type PlanRoundReadClient } from "./mcp-server/shared/plan-round-store.js"
4
15
 
5
16
  async function main() {
6
17
  const server = new McpServer(
@@ -10,10 +21,58 @@ async function main() {
10
21
  registerAll(server)
11
22
  const transport = new StdioServerTransport()
12
23
  await server.connect(transport)
24
+ let lifecycleSubscriber: PlanLifecycleSubscriber | undefined
25
+ let planRoundSubscriber: PlanRoundSubscriber | undefined
26
+ if (/^postgres(ql)?:\/\//.test(DATABASE_URL)) {
27
+ const context = getRuntimeContext()
28
+ const store = createPrismaPlanStatusStore(prisma)
29
+ lifecycleSubscriber = new PlanLifecycleSubscriber({
30
+ context,
31
+ clientFactory: () => createPgLifecycleListenClient(DATABASE_URL),
32
+ resolveStatus: ({ context: subscriberContext, planId }) => resolvePlanStatus(
33
+ store,
34
+ subscriberContext,
35
+ planId ? { planId } : { activeOnly: true },
36
+ ),
37
+ onSnapshot: (snapshot, envelope) => {
38
+ const trigger = envelope ? `notify:${envelope.eventId}` : "initial"
39
+ console.error(`[ADD-MCP] lifecycle pull (${trigger}) ${JSON.stringify(snapshot)}`)
40
+ },
41
+ onError: (error) => console.error(`[ADD-MCP] lifecycle subscriber: ${redact(error.message)}`),
42
+ })
43
+ await lifecycleSubscriber.start().catch((error: unknown) => {
44
+ console.error(`[ADD-MCP] lifecycle initial pull failed: ${redact(error instanceof Error ? error.message : String(error))}`)
45
+ })
46
+ planRoundSubscriber = new PlanRoundSubscriber({
47
+ context,
48
+ clientFactory: () => createPgLifecycleListenClient(DATABASE_URL),
49
+ queryRounds: ({ context: subscriberContext, planId }) => queryPlanRounds(
50
+ prisma as unknown as PlanRoundReadClient,
51
+ { context: subscriberContext, planId },
52
+ ),
53
+ onSnapshot: (snapshot, envelope) => {
54
+ const trigger = envelope ? `notify:${envelope.eventId}` : "initial"
55
+ console.error(`[ADD-MCP] PlanRound pull (${trigger}) ${JSON.stringify({
56
+ contextId: snapshot.context.contextId,
57
+ planId: snapshot.planId,
58
+ planName: snapshot.planName,
59
+ records: snapshot.rounds.length,
60
+ })}`)
61
+ },
62
+ onError: (error) => console.error(`[ADD-MCP] PlanRound subscriber: ${redact(error.message)}`),
63
+ })
64
+ await planRoundSubscriber.start().catch((error: unknown) => {
65
+ console.error(`[ADD-MCP] PlanRound initial pull failed: ${redact(error instanceof Error ? error.message : String(error))}`)
66
+ })
67
+ }
68
+ process.stdin.once("end", () => {
69
+ void lifecycleSubscriber?.stop()
70
+ void planRoundSubscriber?.stop()
71
+ })
13
72
  console.error("[ADD-MCP] add-dev-tools MCP server started on stdio")
14
73
  }
15
74
 
16
75
  main().catch((error) => {
17
- console.error("[ADD-MCP] Fatal error:", error)
76
+ console.error("[ADD-MCP] Fatal error:", redact(error instanceof Error ? error.message : String(error)))
18
77
  process.exit(1)
19
78
  })
@@ -0,0 +1,26 @@
1
+ #!/usr/bin/env node
2
+ import { getRuntimeContext } from "./mcp-server/shared/env.js"
3
+ import { prisma } from "./mcp-server/shared/prisma.js"
4
+ import { resolvePlanStatus } from "./mcp-server/shared/plan-lifecycle.js"
5
+ import { createPrismaPlanStatusStore } from "./mcp-server/shared/plan-status-store.js"
6
+
7
+ async function main(): Promise<void> {
8
+ const context = getRuntimeContext()
9
+ const store = createPrismaPlanStatusStore(prisma)
10
+ const snapshot = await resolvePlanStatus(store, context, { activeOnly: true })
11
+ process.stdout.write(`${JSON.stringify(snapshot)}\n`)
12
+ if (snapshot.availability === "STATUS_UNAVAILABLE") process.exitCode = 3
13
+ }
14
+
15
+ main()
16
+ .catch((error: unknown) => {
17
+ process.stderr.write(`[ADD plan-status-bridge] ${error instanceof Error ? error.message : String(error)}\n`)
18
+ process.exitCode = 3
19
+ })
20
+ .finally(async () => {
21
+ const runtimeClient = prisma as unknown as Record<string, unknown>
22
+ const disconnect = runtimeClient.$disconnect
23
+ if (typeof disconnect === "function") {
24
+ await Reflect.apply(disconnect, prisma, []).catch(() => undefined)
25
+ }
26
+ })
@@ -379,7 +379,7 @@ Plan 级闭包: {业务功能描述}
379
379
 
380
380
  **活跃判定规则**:
381
381
 
382
- 1. **判定输入**:指定 magicDir 下的 `plans/` 目录(按当前端优先回退——以 Claude IDE 宿主环境为例:`.claude/plans/` → `.qoder/plans/` → `.add/plans/`,其他 IDE 同理取各自的 magicDir)
382
+ 1. **判定输入**:当前 adapter 自己的 `{{magicDir}}/plans/`。各 adapter Plan/Spec/Route/Review/HITL 相互独立,禁止跨 adapter fallback
383
383
  2. **索引优先**:先读 `plans/index.md` 匹配 planKeyword → 无匹配才 glob `*-plan-v*.md`
384
384
  3. **活跃定义**:读 add-route 文件 → 存在 `[ ]` 未勾选的 Step 产出项 = 活跃(进行中);全部 `[x]` = 已收敛(不活跃)
385
385
  4. **多 Plan 冲突**:多个 Plan 均有 `[ ]` → 取 add-route 文件最近修改时间(`mtime`)最大的 Plan
@@ -863,7 +863,7 @@ check_add_route_completeness({ planKeyword: "<Plan 核心关键词>" })
863
863
 
864
864
  ### 3.5.3 生成实现审查文档
865
865
 
866
- HITL temporary.md 流程执行:读取 `review-implementation-template.md` 先写 `{name}-review-implementation.temporary.md`(只含 HITL 发现总览表)→ 人类一次性拍板 逐条展开详细分析 写入 `.qoder/reviews/`(guard 放行)→ 删除 temporary。详见上方模板生成规则处的 Review HITL 磋商说明。
866
+ 读取 `review-implementation-template.md`,将实现审查发现整理为 `dimensions`,调用 `create_hitl({ planName: "{name}-review-implementation", type: "PLAN_REVIEW", dimensions })`。`create_hitl` 自动生成 `*.hitl.md` 审批提案并写入 HITL 记录;人类审核后调用 `update_hitl` 完成拍板。通过 `status_hitl` 确认状态为 `TONGYI` 后,方可逐条展开详细分析并写入 `{{magicDir}}/reviews/`;若为 `BOHUI`,修订发现后调用 `create_hitl` 新建 round。
867
867
 
868
868
  ### 3.5.4 生成运行时审查文档(所有 [T] 项通过后)
869
869
 
@@ -1,58 +1,61 @@
1
1
  {
2
+ "_generated": true,
3
+ "_generated_source": "guard-rules.toml",
2
4
  "template": "add-route-template.md",
3
- "shared_by": [
4
- "add-route-template.md",
5
- "add-route-template-heavyweight.md"
6
- ],
7
5
  "sections": [
8
6
  {
9
7
  "id": "step0",
10
- "heading": "## Step 0",
11
- "required": true
8
+ "required": true,
9
+ "heading": "## Step 0"
12
10
  },
13
11
  {
14
12
  "id": "step0_plan_track",
15
- "pattern": "plan_track\\(\\{ planName:",
16
13
  "required": true,
14
+ "anchor": "plan_track",
17
15
  "within": "## Step 0",
16
+ "pattern": "plan_track\\(\\{ planName:",
18
17
  "description": "Step 0 产出必须包含 plan_track 落库步骤"
19
18
  },
20
19
  {
21
20
  "id": "step1",
22
- "heading": "## Step 1",
23
- "required": true
21
+ "required": true,
22
+ "heading": "## Step 1"
24
23
  },
25
24
  {
26
25
  "id": "step2",
27
- "heading": "## Step 2",
28
- "required": true
26
+ "required": true,
27
+ "heading": "## Step 2"
29
28
  },
30
29
  {
31
30
  "id": "step3",
32
- "heading": "## Step 3",
33
- "required": true
31
+ "required": true,
32
+ "heading": "## Step 3"
34
33
  },
35
34
  {
36
35
  "id": "step3_5",
37
- "heading": "## Step 3.5",
38
- "required": false
36
+ "required": false,
37
+ "heading": "## Step 3.5"
39
38
  },
40
39
  {
41
40
  "id": "step4",
42
- "heading": "## Step 4",
43
- "required": true
41
+ "required": true,
42
+ "heading": "## Step 4"
44
43
  },
45
44
  {
46
45
  "id": "step5",
47
- "heading": "## Step 5",
48
- "required": true
46
+ "required": true,
47
+ "heading": "## Step 5"
49
48
  },
50
49
  {
51
50
  "id": "step8",
52
- "heading": "## Step 8",
53
- "required": true
51
+ "required": true,
52
+ "heading": "## Step 8"
54
53
  }
55
54
  ],
55
+ "shared_by": [
56
+ "add-route-template.md",
57
+ "add-route-template-heavyweight.md"
58
+ ],
56
59
  "placeholders": [
57
60
  "{需求域名}",
58
61
  "{核心内容}",
@@ -71,38 +74,3 @@
71
74
  "阶段"
72
75
  ]
73
76
  }
74
- {
75
- "template": "add-route-template.md",
76
- "shared_by": [
77
- "add-route-template.md",
78
- "add-route-template-heavyweight.md"
79
- ],
80
- "sections": [
81
- {
82
- "id": "step0",
83
- "heading": "## Step 0",
84
- "required": true
85
- },
86
- {
87
- "id": "step3",
88
- "heading": "## Step 3",
89
- "required": true
90
- },
91
- {
92
- "id": "step8",
93
- "heading": "## Step 8",
94
- "required": true
95
- }
96
- ],
97
- "placeholders": [
98
- "{需求域名}",
99
- "{核心内容}",
100
- "{版本号}"
101
- ],
102
- "forbidden_terms": [
103
- "Phase",
104
- "Round",
105
- "轮次",
106
- "阶段"
107
- ]
108
- }
@@ -169,7 +169,7 @@
169
169
  | 发起时机 | 契约新建 / 契约重大变更(参与者增减、边界重划、触发条件变更) |
170
170
  | round 机制 | 复用 HitlRecord.round(同一契约多次变更递增 round) |
171
171
  | 与子 Plan 审批关系 | 契约审批是**总控级**(planName=总控 Plan);子 Plan 审批独立进行(互不阻塞) |
172
- | 哨兵 | `.qoder/hitl/.tongyi-{contractName}`(复用现有哨兵机制) |
172
+ | 哨兵 | `{{magicDir}}/hitl/.tongyi-{contractName}`(复用当前 adapter 的哨兵机制) |
173
173
  | 审计 | 契约审批记录入 HitlRecord(type=COLLAB_CONTRACT)→ query_audit_logs 可查 |
174
174
 
175
175
  ---