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,16 +7,26 @@ import { textResponse, errorResponse } from "../shared/response.js"
7
7
  import { PROJECT_ROOT, MAGIC_DIR } from "../shared/fs.js"
8
8
  import { prisma } from "../shared/prisma.js"
9
9
  import { HITL_INTERACTION_CONFIG } from "../shared/hitl-interaction.strategy.js"
10
- import type { HitlRow, PlanRow } from "../shared/db-types.js"
11
- import { HitlRowSchema, PlanRowSchema, validatedDelegate } from "../shared/db-types.js"
10
+ import type { HitlRow } from "../shared/db-types.js"
11
+ import { HitlRowSchema, validatedDelegate } from "../shared/db-types.js"
12
+ import { getRuntimeContext } from "../shared/env.js"
13
+ import {
14
+ decideHitlAndPublish,
15
+ type HitlLifecycleDatabase,
16
+ } from "../shared/hitl-lifecycle-mutation.js"
17
+ import {
18
+ createHitlProposalAndPublish,
19
+ type HitlProposalDatabase,
20
+ } from "../shared/hitl-proposal-mutation.js"
21
+ import { HITL_APPROVAL_WIDGET_URI } from "../shared/hitl-ui.js"
12
22
 
13
23
  // 无类型边界单点(zod 托管):动态加载的 prisma client 在此一次性转为运行期校验的泛型委托
14
24
  const db = {
15
25
  get hitl() { return validatedDelegate<HitlRow>(prisma.hitlRecord, HitlRowSchema, "HitlRecord") },
16
- get plan() { return validatedDelegate<PlanRow>(prisma.planRecord, PlanRowSchema, "PlanRecord") },
17
26
  }
18
27
 
19
28
  export function registerHitlTools(server: ToolRegistrar) {
29
+ const runtimeContext = getRuntimeContext()
20
30
 
21
31
  // ═══════════════ 辅助:按安装环境裁决交互模式(caijuehub: hitl-interaction-rules.toml) ═══════════════
22
32
  const _interaction = (() => {
@@ -109,6 +119,90 @@ export function registerHitlTools(server: ToolRegistrar) {
109
119
  } catch { return [] }
110
120
  }
111
121
 
122
+ function updateHitlProposal(
123
+ filePath: string,
124
+ status: HitlRow["status"],
125
+ dimensions?: { name: string; content?: string }[],
126
+ ): void {
127
+ let proposal = readFileSync(filePath, "utf-8")
128
+ proposal = proposal.replace(/(状态:\s*)[A-Z_]+/, `$1${status}`)
129
+
130
+ if (dimensions?.length) {
131
+ const byName = new Map(dimensions.map((d) => [d.name.trim(), d.content ?? ""]))
132
+ const escapeCell = (value: string) => value.replace(/\|/g, "\\|").replace(/\r?\n/g, "<br>")
133
+ proposal = proposal.split("\n").map((line) => {
134
+ const match = line.match(/^(\|\s*\d+\s*\|\s*)(.+?)(\s*\|\s*)(.+?)(\s*\|\s*.+?\s*\|)$/)
135
+ if (!match) return line
136
+ const name = match[2].trim()
137
+ const next = byName.get(name)
138
+ return next === undefined ? line : `${match[1]}${match[2]}${match[3]}${escapeCell(next)}${match[5]}`
139
+ }).join("\n")
140
+ }
141
+
142
+ writeFileSync(filePath, proposal, "utf-8")
143
+ }
144
+
145
+ server.registerTool("render_hitl_approval", {
146
+ title: "Review ADD HITL proposal",
147
+ description: "只读渲染最新 scoped HITL 提案。Codex MUST 用此工具打开 core approval widget;最终裁决由 widget 调用 update_hitl,render 本身不修改 DB。",
148
+ inputSchema: z.object({
149
+ planName: z.string().describe("Plan 名称"),
150
+ type: z.enum(["PLAN", "PLAN_REVIEW", "COLLAB_CONTRACT"]).default("PLAN"),
151
+ }),
152
+ outputSchema: z.object({
153
+ planName: z.string(),
154
+ type: z.string(),
155
+ round: z.number(),
156
+ status: z.string(),
157
+ dimensions: z.array(z.object({ name: z.string(), content: z.string() })),
158
+ }),
159
+ annotations: { readOnlyHint: true },
160
+ _meta: {
161
+ ui: { resourceUri: HITL_APPROVAL_WIDGET_URI },
162
+ "openai/outputTemplate": HITL_APPROVAL_WIDGET_URI,
163
+ "openai/toolInvocation/invoking": "正在加载 HITL 审批表…",
164
+ "openai/toolInvocation/invoked": "HITL 审批表已加载",
165
+ },
166
+ }, async (args: Record<string, unknown>) => {
167
+ try {
168
+ const planName = typeof args.planName === "string" ? args.planName : ""
169
+ const type = (typeof args.type === "string" ? args.type : "PLAN") as HitlRow["type"]
170
+ const rows = await db.hitl.findMany({
171
+ where: {
172
+ projectKey: runtimeContext.projectKey,
173
+ adapterKey: runtimeContext.adapterKey,
174
+ planName,
175
+ type,
176
+ },
177
+ orderBy: { round: "desc" },
178
+ take: 1,
179
+ })
180
+ const current = rows[0]
181
+ if (!current) return errorResponse(`未找到 scoped HITL: ${planName} (${type})`)
182
+ if (!["DRAFT", "SUBMITTED"].includes(current.status)) {
183
+ return errorResponse(`HITL round ${current.round} 已是终态 ${current.status},不可再次审批`)
184
+ }
185
+ const proposalPath = findHitlFile(planName)
186
+ const dimensions = proposalPath ? parseHitlDimensions(proposalPath) : []
187
+ if (!dimensions.length) {
188
+ return errorResponse(`HITL 提案缺少可渲染维度: ${proposalPath ?? planName}`)
189
+ }
190
+ const output = {
191
+ planName,
192
+ type,
193
+ round: current.round,
194
+ status: current.status,
195
+ dimensions,
196
+ }
197
+ return {
198
+ content: [{ type: "text" as const, text: `已加载 ${planName} round ${current.round} 的 ${dimensions.length} 个审批维度。请在 widget 中拍板。` }],
199
+ structuredContent: output,
200
+ }
201
+ } catch (e) {
202
+ return errorResponse(`render_hitl_approval 失败: ${e instanceof Error ? e.message : String(e)}`)
203
+ }
204
+ })
205
+
112
206
  // ===== create_hitl =====
113
207
  server.registerTool("create_hitl", {
114
208
  description: "HITL 审批:创建提案。写入 HitlRecord(status=DRAFT,自动递增 round),并生成 hitl.md 提案文件供人工审核。\n" +
@@ -180,7 +274,7 @@ export function registerHitlTools(server: ToolRegistrar) {
180
274
  }
181
275
  const requiredKeys: string[] = ["globalAction"]
182
276
 
183
- dimensions!.forEach((d, i) => {
277
+ dimensions.forEach((d, i) => {
184
278
  const prefix = `dim_${i}`
185
279
  props[`${prefix}_content`] = { type: "string", description: `${d.name}: ${d.content || ""}` }
186
280
  props[`${prefix}_decision`] = {
@@ -191,11 +285,11 @@ export function registerHitlTools(server: ToolRegistrar) {
191
285
  requiredKeys.push(`${prefix}_decision`)
192
286
  })
193
287
 
194
- const dimDesc = dimensions!.map((d, i) => `${i+1}. ${d.name}${d.content ? `: ${d.content}` : ""}`).join("\n")
288
+ const dimDesc = dimensions.map((d, i) => `${i+1}. ${d.name}${d.content ? `: ${d.content}` : ""}`).join("\n")
195
289
  return inputRequired({
196
290
  inputRequests: {
197
291
  confirm: inputRequired.elicit({
198
- message: `确认创建 HITL 提案\n\nplan: ${planName}\ntype: ${type}\n\n共 ${dimensions!.length} 个决策维度:\n${dimDesc}`,
292
+ message: `确认创建 HITL 提案\n\nplan: ${planName}\ntype: ${type}\n\n共 ${dimensions.length} 个决策维度:\n${dimDesc}`,
199
293
  requestedSchema: { type: "object", properties: props, required: requiredKeys } as any
200
294
  })
201
295
  }
@@ -207,10 +301,10 @@ export function registerHitlTools(server: ToolRegistrar) {
207
301
  }
208
302
 
209
303
  if (decResp.globalAction === "同意全部") {
210
- finalDims = dimensions!.map(d => ({ name: d.name, content: d.content || "" }))
304
+ finalDims = dimensions.map(d => ({ name: d.name, content: d.content || "" }))
211
305
  } else {
212
306
  // 逐项处理:合并调整内容
213
- finalDims = dimensions!.map((d, i) => {
307
+ finalDims = dimensions.map((d, i) => {
214
308
  const decision = decResp[`dim_${i}_decision`] as string | undefined
215
309
  if (decision === "调整") {
216
310
  const adjusted = decResp[`dim_${i}_adjusted`] as string | undefined
@@ -252,32 +346,18 @@ export function registerHitlTools(server: ToolRegistrar) {
252
346
  // Plan 文件写出后由 plan_track 回刷真实路径——机器占位机器回刷,不消耗 LLM 认知。
253
347
  // D5(Review 回流):占位路径按 type 分流——PLAN_REVIEW 指向 reviews/ 目录,
254
348
  // review 文档写出后由 review_track/plan_track 回刷消解(避免永久悬空)。 ──
255
- let planProvisioned = false
256
- const existingPlan = await db.plan.findFirst({ where: { planName } })
257
- if (!existingPlan) {
258
- const placeholderPath = type === "PLAN_REVIEW"
259
- ? join(PROJECT_ROOT, MAGIC_DIR, "reviews", yyyyMM, dd, `${planName}.md`)
260
- : join(plansDir, `${planName}.md`)
261
- await db.plan.create({
262
- data: {
263
- planName,
264
- planPath: placeholderPath,
265
- planKeyword: String(planName).replace(/-(plan|collab-contract|review)-v\d+$/, ""),
266
- totalTasks: 0, doneTasks: 0, checklistT: 0, checklistTDone: 0, checklistR: 0,
267
- },
268
- })
269
- planProvisioned = true
270
- }
271
-
272
- const rows = await db.hitl.findMany({
273
- where: { planName },
274
- orderBy: { round: "desc" },
275
- take: 1,
276
- })
277
- const round = (rows[0]?.round ?? 0) + 1
278
- const record = await db.hitl.create({
279
- data: { planName, round, type: type as HitlRow["type"], status: "DRAFT" },
349
+ const placeholderPath = type === "PLAN_REVIEW"
350
+ ? join(PROJECT_ROOT, MAGIC_DIR, "reviews", yyyyMM, dd, `${planName}.md`)
351
+ : join(plansDir, `${planName}.md`)
352
+ const proposal = await createHitlProposalAndPublish(prisma as unknown as HitlProposalDatabase, {
353
+ context: runtimeContext,
354
+ planName,
355
+ planPath: placeholderPath,
356
+ planKeyword: String(planName).replace(/-(plan|collab-contract|review)-v\d+$/, ""),
357
+ type: type as HitlRow["type"],
280
358
  })
359
+ const round = proposal.hitl.round
360
+ const record = proposal.hitl
281
361
  // 生成 hitl.md
282
362
  const isoNow = now.toISOString()
283
363
 
@@ -324,7 +404,7 @@ export function registerHitlTools(server: ToolRegistrar) {
324
404
  `recordId: ${record.id}`,
325
405
  ]
326
406
  if (finalDims.length > 0) lines.push(`dimensions: ${finalDims.length} 项`)
327
- if (planProvisioned) lines.push(`PlanRecord: 自动预置(占位行,Plan 文件写出后 plan_track 回刷真实路径)`)
407
+ if (proposal.planProvisioned) lines.push(`PlanRecord: 自动预置(占位行,Plan 文件写出后 plan_track 回刷真实路径)`)
328
408
  if (_fallback) lines.push(`mode: _fallback (跳过 dialog,原始代码降级)`)
329
409
  return textResponse(lines.join("\n"))
330
410
  } catch (e) {
@@ -344,6 +424,7 @@ export function registerHitlTools(server: ToolRegistrar) {
344
424
  description: "HITL 审批:更新状态。SUBMITTED→TONGYI/BOHUI。\n" +
345
425
  "交互模式按安装环境自动裁决(caijuehub: hitl-interaction-rules.toml):\n" +
346
426
  "- genui 模式环境(如 Qoder):MUST 先用 genui show_widget 渲染审批表单(维度取自 hitl.md),用户拍板后以 _use_genui=true + status(TONGYI/BOHUI) 调用本工具。不带模式参数调用会返回 genui 引导而非弹框。\n" +
427
+ "- MCP Apps 模式环境(如 Codex):MUST 先调用 render_hitl_approval 打开 core widget,widget 以 _use_widget=true 回调本工具;不得展开高维 inputRequired。\n" +
347
428
  "- 支持 elicitation 的客户端:inputRequired 弹框确认→确认后写哨兵+更新 DB。\n" +
348
429
  "降级模式(_fallback=true):genui 与弹框均不可用时兜底,跳过弹框直接写哨兵+更新 DB。\n" +
349
430
  "_use_genui=true:genui widget 回调后使用,跳过弹框直接以传入的 status/reason 更新。\n" +
@@ -353,26 +434,45 @@ export function registerHitlTools(server: ToolRegistrar) {
353
434
  type: z.enum(["PLAN", "PLAN_REVIEW", "COLLAB_CONTRACT"]).describe("审批类型"),
354
435
  status: z.enum(["SUBMITTED", "TONGYI", "BOHUI"]).optional().describe("降级模式(_fallback)或 genui 模式(_use_genui)必填:目标状态"),
355
436
  reason: z.string().optional().describe("驳回原因(降级模式手动传,genui 模式从 widget 回调获取)"),
437
+ dimensions: z.array(z.object({
438
+ name: z.string(),
439
+ content: z.string().optional(),
440
+ })).optional().describe("widget 中由用户明确保存的维度调整;仅 _use_widget/_use_genui 路径消费"),
356
441
  _fallback: z.boolean().optional().default(false).describe("降级模式:跳过 inputRequired,按原始代码行为直接写哨兵+更新 DB"),
357
442
  _use_genui: z.boolean().optional().default(false).describe("genui 模式:genui widget 回调后使用,跳过弹框直接更新(status/reason 需传最终值)"),
443
+ _use_widget: z.boolean().optional().default(false).describe("MCP Apps widget 回调:跳过 inputRequired,按用户提交的 status/reason/dimensions 更新"),
358
444
  }),
359
445
  }, async (args: Record<string, unknown>, ctx: Record<string, unknown>) => {
360
446
  try {
361
- const _raw = args as Record<string, string | boolean | undefined>
362
- const planName = String(_raw.planName ?? "")
363
- const type = String(_raw.type ?? "")
447
+ const _raw = args
448
+ const planName = typeof _raw.planName === "string" ? _raw.planName : ""
449
+ const type = typeof _raw.type === "string" ? _raw.type : ""
364
450
  const status = typeof _raw.status === "string" ? _raw.status : undefined
365
451
  const reason = typeof _raw.reason === "string" ? _raw.reason : undefined
366
452
  const _fallback = _raw._fallback === true
367
453
  const _use_genui = _raw._use_genui === true
454
+ const _use_widget = _raw._use_widget === true
455
+ const dimensions = Array.isArray(_raw.dimensions)
456
+ ? _raw.dimensions.filter((item): item is { name: string; content?: string } => {
457
+ if (!item || typeof item !== "object") return false
458
+ const row = item as Record<string, unknown>
459
+ return typeof row.name === "string" && (row.content === undefined || typeof row.content === "string")
460
+ })
461
+ : undefined
368
462
 
369
463
  // ── 交互式确认(非降级模式且非 genui 模式) ──
370
- let effectiveStatus = status as string | undefined
464
+ let effectiveStatus = status
371
465
 
372
- if (!_fallback && !_use_genui) {
466
+ if (!_fallback && !_use_genui && !_use_widget) {
467
+ if (_interaction.mode === "mcpApps") {
468
+ return textResponse(
469
+ `Codex 高维 HITL 使用 core widget,不展开 inputRequired。\n` +
470
+ `请调用 render_hitl_approval({ planName: "${planName}", type: "${type}" }),由用户在 widget 中拍板。`,
471
+ )
472
+ }
373
473
  // 环境裁决:genui 模式下不发起注定失败的 elicitation,引导 LLM 走 widget 流程
374
474
  if (_interaction.mode === "genui") {
375
- const gPath = findHitlFile(planName as string)
475
+ const gPath = findHitlFile(planName)
376
476
  const gDims = gPath ? parseHitlDimensions(gPath) : []
377
477
  const gDesc = gDims.map((d, i) => `${i + 1}. ${d.name}: ${d.content}`).join("\n")
378
478
  return textResponse(
@@ -381,7 +481,7 @@ export function registerHitlTools(server: ToolRegistrar) {
381
481
  )
382
482
  }
383
483
  // 尝试读取 hitl.md 文件解析维度
384
- const hitlPath = findHitlFile(planName as string)
484
+ const hitlPath = findHitlFile(planName)
385
485
  const dims = hitlPath ? parseHitlDimensions(hitlPath) : []
386
486
 
387
487
  if (dims.length > 0) {
@@ -450,40 +550,22 @@ export function registerHitlTools(server: ToolRegistrar) {
450
550
  }
451
551
 
452
552
  // 降级模式必须传 status
553
+ if (!effectiveStatus || !["SUBMITTED", "TONGYI", "BOHUI"].includes(effectiveStatus)) {
554
+ return errorResponse("update_hitl 必须得到有效 status: SUBMITTED/TONGYI/BOHUI")
555
+ }
453
556
 
454
557
  // ── 原始业务逻辑(更新 DB + 写哨兵文件。降级模式也走此路) ──
455
558
  const s = effectiveStatus as HitlRow["status"]
456
559
  const qType = type as HitlRow["type"]
457
- const rows = await db.hitl.findMany({
458
- where: { planName, type: qType },
459
- orderBy: { round: "desc" },
460
- take: 1,
560
+ const decision = await decideHitlAndPublish(prisma as unknown as HitlLifecycleDatabase, {
561
+ context: runtimeContext,
562
+ planName,
563
+ type: qType,
564
+ status: s as "SUBMITTED" | "TONGYI" | "BOHUI",
565
+ reason,
461
566
  })
462
- if (!rows.length) return errorResponse(`未找到 HITL 记录: planName=${planName}, type=${type}`)
463
- const r = rows[0]
464
- const prevStatus = r.status
465
- if (prevStatus === "TONGYI" || prevStatus === "BOHUI") {
466
- return errorResponse(`HITL 已终态(${prevStatus}),不可再更新。BOHUI 后请用 create_hitl 新建 round。`)
467
- }
468
- // 更新
469
- const data: Partial<HitlRow> = { status: s }
470
- if (s === "TONGYI") data.approvedAt = new Date()
471
- if (s === "BOHUI") {
472
- data.rejectedAt = new Date()
473
- if (reason) data.rejectReason = String(reason)
474
- }
475
- await db.hitl.update({ where: { id: r.id }, data })
476
- // ── BOHUI 清算:Plan 文件不存在 → 预置 PlanRecord 无存续理由,连同本 plan 全部 HitlRecord 删除(孤儿即时归零)。
477
- // 已写出文件的驳回 Plan 保留记录(历史价值)。 ──
478
- let orphanCleaned = ""
479
- if (s === "BOHUI") {
480
- const planRow = await db.plan.findFirst({ where: { planName } })
481
- if (planRow && !existsSync(planRow.planPath)) {
482
- await db.hitl.deleteMany({ where: { planName } })
483
- await db.plan.delete({ where: { id: planRow.id } })
484
- orphanCleaned = `预置数据已清算(Plan 文件不存在: ${planRow.planPath})`
485
- }
486
- }
567
+ const r = decision.hitl
568
+ const prevStatus = decision.previousStatus
487
569
  // 写哨兵文件(双命名:原始 planName + pre-tool-use hook §C 剥后缀推导名,保持闸门一致)
488
570
  const markerDir = join(PROJECT_ROOT, MAGIC_DIR, "hitl")
489
571
  mkdirSync(markerDir, { recursive: true })
@@ -503,9 +585,7 @@ export function registerHitlTools(server: ToolRegistrar) {
503
585
  // P3 #6:回写 .hitl.md 提案文件状态(DRAFT → TONGYI/BOHUI),保证双通道校验一致
504
586
  const proposalPath = findHitlFile(String(planName))
505
587
  if (proposalPath) {
506
- let proposal = readFileSync(proposalPath, "utf-8")
507
- proposal = proposal.replace(/(状态:\s*)[A-Z_]+/, `$1${s}`)
508
- writeFileSync(proposalPath, proposal, "utf-8")
588
+ updateHitlProposal(proposalPath, s, (_use_widget || _use_genui) ? dimensions : undefined)
509
589
  }
510
590
  // 响应
511
591
  const lines = [
@@ -516,11 +596,11 @@ export function registerHitlTools(server: ToolRegistrar) {
516
596
  `status: ${prevStatus} → ${s}`,
517
597
  ]
518
598
  if (_fallback) lines.push(`mode: _fallback (跳过 dialog,原始代码降级)`)
599
+ if (_use_widget) lines.push(`mode: mcpApps widget`)
519
600
  if (s === "TONGYI") lines.push(`marker: ${marker} (stat() O(1) 兜底)`)
520
601
  if (s === "BOHUI") {
521
602
  lines.push(`marker: ${marker}`)
522
603
  lines.push(`reason: ${reason || "—"}`)
523
- if (orphanCleaned) lines.push(`cleanup: ${orphanCleaned}`)
524
604
  lines.push(``, `💡 驳回后请用 create_hitl 新建 round ${r.round + 1} 重新发起审批。`)
525
605
  }
526
606
  return textResponse(lines.join("\n"))
@@ -541,7 +621,12 @@ export function registerHitlTools(server: ToolRegistrar) {
541
621
  const { planName, type } = args as { planName: string; type?: string }
542
622
  const qType = (type || "PLAN") as HitlRow["type"]
543
623
  const rows = await db.hitl.findMany({
544
- where: { planName, type: qType },
624
+ where: {
625
+ projectKey: runtimeContext.projectKey,
626
+ adapterKey: runtimeContext.adapterKey,
627
+ planName,
628
+ type: qType,
629
+ },
545
630
  orderBy: { round: "desc" },
546
631
  take: 1,
547
632
  })
@@ -2,8 +2,11 @@ import * as z from "zod/v4"
2
2
  import type { ToolRegistrar } from "./registrar.js"
3
3
  import { textResponse, errorResponse } from "../shared/response.js"
4
4
  import { prisma } from "../shared/prisma.js"
5
+ import { getRuntimeContext } from "../shared/env.js"
5
6
 
6
7
  export function registerHookEventTools(server: ToolRegistrar) {
8
+ const runtimeContext = getRuntimeContext()
9
+ const display = (value: unknown, fallback: string): string => typeof value === "string" ? value : fallback
7
10
 
8
11
  server.registerTool("get_hook_events", {
9
12
  description:
@@ -24,7 +27,11 @@ export function registerHookEventTools(server: ToolRegistrar) {
24
27
  try {
25
28
  const { planKeyword, hook, sinceMinutes, untilMinutes, limit = 50 } = args
26
29
 
27
- const where: Record<string, unknown> = { action: "HOOK_INTERCEPT" }
30
+ const where: Record<string, unknown> = {
31
+ action: "HOOK_INTERCEPT",
32
+ projectKey: runtimeContext.projectKey,
33
+ producerAdapterKey: runtimeContext.adapterKey,
34
+ }
28
35
  if (planKeyword) where.planKeyword = planKeyword
29
36
  if (hook) where.targetType = hook
30
37
 
@@ -70,7 +77,7 @@ export function registerHookEventTools(server: ToolRegistrar) {
70
77
  for (let i = 0; i < Math.min(logs.length, 20); i++) {
71
78
  const l = logs[i]
72
79
  lines.push(
73
- ` [${(l.createdAt as Date).toISOString().slice(11, 19)}] ${l.targetType || "?"} → ${l.reason || "(无)"} | plan: ${l.planKeyword || "?"}`
80
+ ` [${(l.createdAt as Date).toISOString().slice(11, 19)}] ${display(l.targetType, "?")} → ${display(l.reason, "(无)")} | plan: ${display(l.planKeyword, "?")}`
74
81
  )
75
82
  }
76
83
 
@@ -12,6 +12,62 @@ import { registerReviewTools } from "./review.js"
12
12
  import { PROJECT_ID } from "../shared/env.js"
13
13
  import type { ToolRegistrar } from "./registrar.js"
14
14
 
15
+ // ── 读写分级信号量节流(进程层并发契约 v2 §7:Codex Parallel MCP 真并行兑底) ──
16
+ // 读工具共享 8 并发;写工具共享 4 并发;超限请求排队(MCP 协议允许延迟响应,排队即天然反压)
17
+ const READ_MAX = 8
18
+ const WRITE_MAX = 4
19
+
20
+ const READ_TOOLS = new Set([
21
+ "get_project_context", "find_related_docs", "get_db_schema", "query_audit_logs",
22
+ "plan_status", "review_status", "status_hitl", "contract_status",
23
+ "render_hitl_approval",
24
+ "check_dps", "check_rahs", "check_add_route_status", "check_spec_sync",
25
+ "check_add_route_completeness", "check_phase_symmetry", "check_failure_path",
26
+ "check_add_compliance", "get_hook_events",
27
+ ])
28
+
29
+ function createSemaphore(limit: number) {
30
+ let active = 0
31
+ const queue: (() => void)[] = []
32
+ const acquire = () =>
33
+ new Promise<void>((resolve) => {
34
+ if (active < limit) {
35
+ active++
36
+ resolve()
37
+ } else {
38
+ queue.push(resolve)
39
+ }
40
+ })
41
+ const release = () => {
42
+ active--
43
+ const next = queue.shift()
44
+ if (next) {
45
+ active++
46
+ next()
47
+ }
48
+ }
49
+ return { acquire, release }
50
+ }
51
+
52
+ // 429 指数退避重试(3 次,250ms 起步,倍率 2x):上游限流不直接透传
53
+ async function withRetry<T>(fn: () => Promise<T>): Promise<T> {
54
+ let lastErr: unknown
55
+ for (let attempt = 1; attempt <= 3; attempt++) {
56
+ try {
57
+ return await fn()
58
+ } catch (e) {
59
+ lastErr = e
60
+ const msg = e instanceof Error ? e.message : String(e)
61
+ if (!/429|rate.?limit|限流/i.test(msg)) throw e
62
+ await new Promise((r) => setTimeout(r, 250 * 2 ** (attempt - 1)))
63
+ }
64
+ }
65
+ throw lastErr
66
+ }
67
+
68
+ const readSem = createSemaphore(READ_MAX)
69
+ const writeSem = createSemaphore(WRITE_MAX)
70
+
15
71
  export function registerAllTools(server: McpServer) {
16
72
  // D9 多 MCP 路由安全:派生装饰版 registrar,为所有工具 description 注入项目身份前缀
17
73
  const origRegister = server.registerTool.bind(server)
@@ -20,10 +76,20 @@ export function registerAllTools(server: McpServer) {
20
76
  type ToolCb = Parameters<RegisterTool>[2]
21
77
  const registerTool = ((name: string, config: ToolConfig, cb: ToolCb) => {
22
78
  const c = config as { description?: string }
79
+ // 读写分级节流 + 429 退避(进程层契约 v2:Codex 并行调用兜底)
80
+ const sem = READ_TOOLS.has(name) ? readSem : writeSem
81
+ const throttledCb = (async (...args: unknown[]) => {
82
+ await sem.acquire()
83
+ try {
84
+ return await withRetry(async () => cb(...(args as Parameters<ToolCb>)))
85
+ } finally {
86
+ sem.release()
87
+ }
88
+ }) as ToolCb
23
89
  return origRegister(
24
90
  name,
25
91
  { ...config, description: `[项目: ${PROJECT_ID}] ${c.description ?? ""}` },
26
- cb,
92
+ throttledCb,
27
93
  )
28
94
  }) as RegisterTool
29
95
  const registrar: ToolRegistrar = { registerTool }
@@ -35,8 +101,8 @@ export function registerAllTools(server: McpServer) {
35
101
  registerQualityTools(registrar) // 4 tools
36
102
  registerGatewayTools(registrar) // 5 tools
37
103
  registerHookEventTools(registrar) // 1 tool: get_hook_events
38
- registerHitlTools(registrar) // 3 tools: create_hitl / update_hitl / status_hitl
104
+ registerHitlTools(registrar) // 4 tools: create_hitl / update_hitl / status_hitl / render_hitl_approval
39
105
  registerPlanTools(registrar) // 3 tools: plan_track / plan_status / plan_sync
40
106
  registerReviewTools(registrar) // 3 tools: review_track / review_status / review_sync
41
- // Total: 29 tools
107
+ // Total: 30 tools
42
108
  }