add-coder 0.3.34 → 0.3.35

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 (147) hide show
  1. package/dist/index.js +21 -3
  2. package/package.json +3 -3
  3. package/templates/.add-coder-src-hash.json +144 -90
  4. package/templates/adapters/claude/hooks/doc-format-guard.mjs +172 -88
  5. package/templates/adapters/claude/hooks/notification.mjs +0 -4
  6. package/templates/adapters/claude/hooks/permission-denied.mjs +0 -4
  7. package/templates/adapters/claude/hooks/permission-gate.mjs +0 -4
  8. package/templates/adapters/claude/hooks/post-tool-use.mjs +59 -5
  9. package/templates/adapters/claude/hooks/pre-compact.mjs +0 -4
  10. package/templates/adapters/claude/hooks/pre-tool-use.mjs +0 -4
  11. package/templates/adapters/claude/hooks/prompt-submit.mjs +72 -4
  12. package/templates/adapters/claude/hooks/session-end.mjs +0 -4
  13. package/templates/adapters/claude/hooks/session-start.mjs +65 -5
  14. package/templates/adapters/claude/hooks/stop-check.mjs +6 -87
  15. package/templates/adapters/claude/hooks/stop-failure.mjs +0 -4
  16. package/templates/adapters/claude/hooks/subagent-guard.mjs +0 -4
  17. package/templates/adapters/claude/hooks/subagent-stop.mjs +0 -4
  18. package/templates/adapters/codex/hooks/doc-format-guard.mjs +172 -88
  19. package/templates/adapters/codex/hooks/notification.mjs +0 -4
  20. package/templates/adapters/codex/hooks/permission-gate.mjs +0 -4
  21. package/templates/adapters/codex/hooks/post-tool-failure.mjs +0 -4
  22. package/templates/adapters/codex/hooks/post-tool-use.mjs +59 -5
  23. package/templates/adapters/codex/hooks/pre-compact.mjs +0 -4
  24. package/templates/adapters/codex/hooks/pre-tool-use.mjs +0 -4
  25. package/templates/adapters/codex/hooks/prompt-submit.mjs +72 -4
  26. package/templates/adapters/codex/hooks/session-end.mjs +0 -4
  27. package/templates/adapters/codex/hooks/session-start.mjs +65 -5
  28. package/templates/adapters/codex/hooks/stop-check.mjs +6 -87
  29. package/templates/adapters/codex/hooks/subagent-guard.mjs +0 -4
  30. package/templates/adapters/codex/hooks/subagent-stop.mjs +0 -4
  31. package/templates/adapters/qoder/hooks/doc-format-guard.mjs +172 -88
  32. package/templates/adapters/qoder/hooks/notification.mjs +0 -4
  33. package/templates/adapters/qoder/hooks/permission-gate.mjs +0 -4
  34. package/templates/adapters/qoder/hooks/post-tool-failure.mjs +0 -4
  35. package/templates/adapters/qoder/hooks/post-tool-use.mjs +59 -5
  36. package/templates/adapters/qoder/hooks/pre-compact.mjs +0 -4
  37. package/templates/adapters/qoder/hooks/pre-tool-use.mjs +0 -4
  38. package/templates/adapters/qoder/hooks/prompt-submit.mjs +72 -4
  39. package/templates/adapters/qoder/hooks/review-checklist.mjs +0 -4
  40. package/templates/adapters/qoder/hooks/session-end.mjs +0 -4
  41. package/templates/adapters/qoder/hooks/session-start.mjs +67 -5
  42. package/templates/adapters/qoder/hooks/stop-check.mjs +6 -87
  43. package/templates/adapters/qoder/hooks/subagent-guard.mjs +0 -4
  44. package/templates/adapters/qoder/hooks/subagent-stop.mjs +0 -4
  45. package/templates/adapters/trae/hooks/doc-format-guard.mjs +172 -88
  46. package/templates/adapters/trae/hooks/notification.mjs +0 -4
  47. package/templates/adapters/trae/hooks/permission-gate.mjs +0 -4
  48. package/templates/adapters/trae/hooks/post-tool-failure.mjs +0 -4
  49. package/templates/adapters/trae/hooks/post-tool-use.mjs +59 -5
  50. package/templates/adapters/trae/hooks/pre-compact.mjs +0 -4
  51. package/templates/adapters/trae/hooks/pre-tool-use.mjs +0 -4
  52. package/templates/adapters/trae/hooks/prompt-submit.mjs +72 -4
  53. package/templates/adapters/trae/hooks/session-end.mjs +0 -4
  54. package/templates/adapters/trae/hooks/session-start.mjs +65 -5
  55. package/templates/adapters/trae/hooks/stop-check.mjs +6 -87
  56. package/templates/adapters/trae/hooks/subagent-guard.mjs +0 -4
  57. package/templates/adapters/trae/hooks/subagent-stop.mjs +0 -4
  58. package/templates/adapters/vscode/hooks/doc-format-guard.mjs +172 -88
  59. package/templates/adapters/vscode/hooks/notification.mjs +0 -4
  60. package/templates/adapters/vscode/hooks/permission-gate.mjs +0 -4
  61. package/templates/adapters/vscode/hooks/post-tool-failure.mjs +0 -4
  62. package/templates/adapters/vscode/hooks/post-tool-use.mjs +59 -5
  63. package/templates/adapters/vscode/hooks/pre-compact.mjs +0 -4
  64. package/templates/adapters/vscode/hooks/pre-tool-use.mjs +0 -4
  65. package/templates/adapters/vscode/hooks/prompt-submit.mjs +72 -4
  66. package/templates/adapters/vscode/hooks/session-end.mjs +0 -4
  67. package/templates/adapters/vscode/hooks/session-start.mjs +65 -5
  68. package/templates/adapters/vscode/hooks/stop-check.mjs +6 -87
  69. package/templates/adapters/vscode/hooks/subagent-guard.mjs +0 -4
  70. package/templates/adapters/vscode/hooks/subagent-stop.mjs +0 -4
  71. package/templates/core/governance/doc-format-guard.ts +29 -112
  72. package/templates/core/governance/post-tool-router.ts +33 -1
  73. package/templates/core/governance/prompt-router.ts +47 -0
  74. package/templates/core/governance/rules.ts +0 -4
  75. package/templates/core/governance/session-start-guard.ts +48 -1
  76. package/templates/core/governance/stop-router.ts +7 -101
  77. package/templates/core/prisma/add.prisma +203 -0
  78. package/templates/core/scripts/db-ensure.sh +61 -2
  79. package/templates/core/scripts/mcp-server/shared/db-types.ts +119 -0
  80. package/templates/core/scripts/mcp-server/shared/hitl-create-policy.ts +27 -0
  81. package/templates/core/scripts/mcp-server/shared/hitl-proposal-content.ts +110 -0
  82. package/templates/core/scripts/mcp-server/shared/hitl-widget-instance.ts +85 -0
  83. package/templates/core/scripts/mcp-server/shared/memory/calibration/batch-fit.ts +250 -0
  84. package/templates/core/scripts/mcp-server/shared/memory/calibration/feedback-stats.ts +101 -0
  85. package/templates/core/scripts/mcp-server/shared/memory/calibration/unit-state.ts +224 -0
  86. package/templates/core/scripts/mcp-server/shared/memory/domain/conflicts.ts +59 -0
  87. package/templates/core/scripts/mcp-server/shared/memory/domain/dedup.ts +45 -0
  88. package/templates/core/scripts/mcp-server/shared/memory/domain/errors.ts +33 -0
  89. package/templates/core/scripts/mcp-server/shared/memory/domain/handoff-digest.ts +92 -0
  90. package/templates/core/scripts/mcp-server/shared/memory/domain/metric-candidate.ts +79 -0
  91. package/templates/core/scripts/mcp-server/shared/memory/domain/scope.ts +94 -0
  92. package/templates/core/scripts/mcp-server/shared/memory/domain/secrets.ts +50 -0
  93. package/templates/core/scripts/mcp-server/shared/memory/domain/state-machine.ts +90 -0
  94. package/templates/core/scripts/mcp-server/shared/memory/embedding/index.ts +117 -0
  95. package/templates/core/scripts/mcp-server/shared/memory/embedding/local-onnx.ts +105 -0
  96. package/templates/core/scripts/mcp-server/shared/memory/embedding/openai-compatible.ts +87 -0
  97. package/templates/core/scripts/mcp-server/shared/memory/jobs/consolidation.ts +226 -0
  98. package/templates/core/scripts/mcp-server/shared/memory/jobs/evidence-collector.ts +153 -0
  99. package/templates/core/scripts/mcp-server/shared/memory/jobs/snapshot.ts +114 -0
  100. package/templates/core/scripts/mcp-server/shared/memory/metrics/gate-recall.ts +134 -0
  101. package/templates/core/scripts/mcp-server/shared/memory/metrics/gate-writer.ts +217 -0
  102. package/templates/core/scripts/mcp-server/shared/memory/metrics/stage-words.ts +69 -0
  103. package/templates/core/scripts/mcp-server/shared/memory/retrieval/context-builder.ts +89 -0
  104. package/templates/core/scripts/mcp-server/shared/memory/retrieval/fts/pg.ts +139 -0
  105. package/templates/core/scripts/mcp-server/shared/memory/retrieval/fts/sqlite-fts5.sql +29 -0
  106. package/templates/core/scripts/mcp-server/shared/memory/retrieval/fts/sqlite.ts +106 -0
  107. package/templates/core/scripts/mcp-server/shared/memory/retrieval/fusion.ts +43 -0
  108. package/templates/core/scripts/mcp-server/shared/memory/retrieval/pipeline.ts +285 -0
  109. package/templates/core/scripts/mcp-server/shared/memory/retrieval/query-terms.ts +31 -0
  110. package/templates/core/scripts/mcp-server/shared/memory/retrieval/recall-writer.ts +87 -0
  111. package/templates/core/scripts/mcp-server/shared/memory/retrieval/reranker.ts +116 -0
  112. package/templates/core/scripts/mcp-server/shared/memory/retrieval/types.ts +52 -0
  113. package/templates/core/scripts/mcp-server/shared/memory/retrieval/vector/pgvector.ts +143 -0
  114. package/templates/core/scripts/mcp-server/shared/memory/retrieval/vector/sqlite-vec.ts +118 -0
  115. package/templates/core/scripts/mcp-server/shared/memory/switches.ts +39 -0
  116. package/templates/core/scripts/mcp-server/shared/review-files.ts +22 -0
  117. package/templates/core/scripts/mcp-server/shared/runtime-freshness.ts +235 -0
  118. package/templates/core/scripts/mcp-server/tools/gateway/check_dps.ts +41 -3
  119. package/templates/core/scripts/mcp-server/tools/gateway/check_rahs.ts +40 -1
  120. package/templates/core/scripts/mcp-server/tools/gateway/check_spec_sync.ts +2 -2
  121. package/templates/core/scripts/mcp-server/tools/hitl.ts +108 -42
  122. package/templates/core/scripts/mcp-server/tools/index.ts +7 -1
  123. package/templates/core/scripts/mcp-server/tools/memory-compat.ts +258 -0
  124. package/templates/core/scripts/mcp-server/tools/memory.ts +654 -0
  125. package/templates/core/scripts/mcp-server/tools/plan.ts +8 -3
  126. package/templates/core/scripts/mcp-server/tools/review.ts +10 -7
  127. package/templates/core/scripts/mcp-server.ts +36 -0
  128. package/templates/core/templates/checklist-template.md +13 -0
  129. package/templates/core/templates/review-implementation-template.md +24 -0
  130. package/templates/core/templates/review-template.md +16 -0
  131. package/templates/core/validation/index.ts +136 -0
  132. package/templates/core/validation/policy.ts +91 -0
  133. package/templates/core/validation/registry.ts +61 -0
  134. package/templates/core/validation/schema-validator.ts +277 -0
  135. package/templates/core/validation/validators/add-route.ts +32 -0
  136. package/templates/core/validation/validators/checklist.ts +48 -0
  137. package/templates/core/validation/validators/handoff.ts +46 -0
  138. package/templates/core/validation/validators/hitl.ts +22 -0
  139. package/templates/core/validation/validators/index.ts +52 -0
  140. package/templates/core/validation/validators/plan.ts +20 -0
  141. package/templates/core/validation/validators/report.ts +16 -0
  142. package/templates/core/validation/validators/review.ts +30 -0
  143. package/templates/core/validation/validators/spec.ts +25 -0
  144. package/templates/core/validation/validators/tasks.ts +40 -0
  145. package/templates/core/validation/validators/types.ts +32 -0
  146. package/templates/core/vocabulary/add-governance-vocabulary.md +18 -0
  147. package/templates/core/scripts/mcp-server/shared/dps-spec-ref.ts +0 -17
@@ -0,0 +1,235 @@
1
+ /*
2
+ * 产物-进程新鲜度(Plan hitl-widget-runtime-gap §Freshness)
3
+ *
4
+ * 背景:`npm run sync` 会重写 `{magicDir}/scripts/mcp-server.ts` 等产物,但不会重启正在运行的
5
+ * server 进程 → 出现「产物是新的、跑的是旧代码」的静默陈旧(实测:进程 Sep 12 11:49 vs 产物 09-13 09:47)。
6
+ *
7
+ * 覆盖面:**6 个 adapter 各自都有 server 副本**,因此检测范围是全部 magic 目录,不限于 .codex。
8
+ * 判定不确定时一律返回 "unknown"(宁可漏报,不误报"需重启")。
9
+ */
10
+ import { existsSync, readFileSync, rmSync, statSync, writeFileSync } from "node:fs"
11
+ import { join } from "node:path"
12
+ import { execSync } from "node:child_process"
13
+
14
+ export const RESTART_MARKER_FILE = ".mcp-restart-required"
15
+ export const DEFAULT_MAGIC_DIRS = [".codex", ".add", ".qoder", ".claude", ".vscode", ".trae"] as const
16
+
17
+ export interface RunningServer {
18
+ magicDir: string
19
+ pid: number
20
+ ppid: number
21
+ command: string
22
+ startedAt?: Date
23
+ /**
24
+ * 启动者已死(ppid 指向 init/systemd)——**残留族**(2026-09-14 新增):
25
+ * 实测成因:IDE/app 退出时通常只杀直接子进程(`npx`),
26
+ * `npm exec → sh -c → tsx → node` 这几代被 reparent 到 systemd 继续存活
27
+ * (同一逻辑 server 5 个进程;旧实例的 server 在 app 重启后仍活了 20+ 分钟)。
28
+ * 这类进程的 stdio 对端已消失,属可回收垃圾;也不应参与新鲜度判定。
29
+ */
30
+ orphan?: boolean
31
+ }
32
+
33
+ export interface FreshnessInfo {
34
+ stale: boolean | "unknown"
35
+ magicDir: string
36
+ processStartedAt?: string
37
+ artifactMtime?: string
38
+ markerPath?: string
39
+ detail: string
40
+ }
41
+
42
+ export interface FreshnessDeps {
43
+ /** 测试注入:ps 输出(`ps -eo pid=,etimes=,args=` 形式) */
44
+ psOutput?: string
45
+ listServers?: () => RunningServer[]
46
+ mtimeOf?: (path: string) => Date | null
47
+ markerExists?: (path: string) => boolean
48
+ now?: () => Date
49
+ }
50
+
51
+ /** 扫描全部 adapter 的运行中 MCP server(解析失败返回空数组,不抛) */
52
+ export function listRunningMcpServers(deps: Pick<FreshnessDeps, "psOutput" | "now"> = {}): RunningServer[] {
53
+ let out = deps.psOutput
54
+ if (out === undefined) {
55
+ try {
56
+ out = execSync("ps -eo pid=,ppid=,etimes=,args=", { encoding: "utf-8", stdio: ["ignore", "pipe", "ignore"] })
57
+ } catch {
58
+ return []
59
+ }
60
+ }
61
+ const now = (deps.now ?? (() => new Date()))()
62
+ const lines = out.split("\n")
63
+ // 启动者已死的判定依据:ppid 落在 init / systemd 上(reparent 的结果)
64
+ const supervisorPids = new Set<number>()
65
+ const allPids = new Set<number>()
66
+ for (const line of lines) {
67
+ const m = line.trim().match(/^(\d+)\s+(\d+)\s+(\d+)\s+(.*)$/)
68
+ if (!m) continue
69
+ allPids.add(Number(m[1]))
70
+ if (/(^|\/)(systemd|init)(\s|$)/.test(m[4])) supervisorPids.add(Number(m[1]))
71
+ }
72
+ const servers: RunningServer[] = []
73
+ for (const line of lines) {
74
+ if (!line.includes("mcp-server.ts")) continue
75
+ if (line.includes("mcp-restart-notice")) continue // 跳过本脚本自身
76
+ const m = line.trim().match(/^(\d+)\s+(\d+)\s+(\d+)\s+(.*)$/)
77
+ if (!m) continue
78
+ const ppid = Number(m[2])
79
+ const cmd = m[4]
80
+ /*
81
+ * 噪声过滤(2026-09-14 收窄):
82
+ * 旧实现是 `cmd.includes("node_modules") → skip`,把**我们自己的 tsx 启动层**也一并挡掉
83
+ * (`node …/node_modules/.bin/../tsx/dist/cli.mjs <magicDir>/scripts/mcp-server.ts`)——
84
+ * 后果是回收只能杀掉表层 npx/npm,深层 node/tsx 残部继续存活(实测:回收后仍留
85
+ * `135292/135310`、`135897/135913` 两族残部)。
86
+ * 现在只排除「安装副本」:dot 目录直接位于 node_modules 树内
87
+ * (如 `node_modules/<pkg>/.codex/scripts/mcp-server.ts`),其余照常纳入。
88
+ */
89
+ if (/(^|[/\s])node_modules\/(?:[^/\s]+\/)*\.[a-z][a-z0-9-]*\/scripts\/mcp-server\.ts/.test(cmd)) continue
90
+ // magic 段必须以点开头(.codex/.qoder…),且路径后是行尾或参数分隔
91
+ const magic = cmd.match(/(?:^|[/\s])(\.[a-z][a-z0-9-]*)\/scripts\/mcp-server\.ts(?:\s|$)/)
92
+ if (!magic) continue
93
+ const etimes = Number(m[3])
94
+ servers.push({
95
+ magicDir: magic[1],
96
+ pid: Number(m[1]),
97
+ ppid,
98
+ command: cmd,
99
+ startedAt: Number.isFinite(etimes) ? new Date(now.getTime() - etimes * 1000) : undefined,
100
+ orphan: false, // 先按 o 标记,稍后按"整族"传播(见下)
101
+ })
102
+ }
103
+ /*
104
+ * 孤儿按**整族**传播:链上后几代(npm exec / sh -c / tsx / node)的 ppid 指向族内进程,
105
+ * 单看自身 ppid 判不出来 —— 只有族根(其 ppid 不在族内)能反映"启动者是否还在"。
106
+ * 族根判定为孤儿时,整族标记(含 rootPpid 不存在=父进程已消失的情形)。
107
+ */
108
+ const byPid = new Map(servers.map((s) => [s.pid, s]))
109
+ const childrenOf = new Map<number, number[]>()
110
+ for (const s of servers) {
111
+ if (!byPid.has(s.ppid)) continue
112
+ childrenOf.set(s.ppid, [...(childrenOf.get(s.ppid) ?? []), s.pid])
113
+ }
114
+ for (const s of servers) {
115
+ if (byPid.has(s.ppid)) continue // 不是族根
116
+ const rootParentGone = s.ppid === 1 || supervisorPids.has(s.ppid) || !allPids.has(s.ppid)
117
+ if (!rootParentGone) continue
118
+ const stack = [s.pid]
119
+ while (stack.length > 0) {
120
+ const pid = stack.pop() as number
121
+ const node = byPid.get(pid)
122
+ if (!node || node.orphan) continue
123
+ node.orphan = true
124
+ stack.push(...(childrenOf.get(pid) ?? []))
125
+ }
126
+ }
127
+ return servers
128
+ }
129
+
130
+ /**
131
+ * 残留族清单(供 `scripts/mcp-restart-notice.ts` 回收):
132
+ * 只按 adapter 取**全部孤儿 pid**(含 npx/npm/sh/tsx/node 各代),非孤儿一律不动。
133
+ */
134
+ export function orphanPidsToReap(servers: readonly RunningServer[]): number[] {
135
+ return [...new Set(servers.filter((s) => s.orphan).map((s) => s.pid))].sort((a, b) => a - b)
136
+ }
137
+
138
+ /**
139
+ * 按 adapter 聚合:同一 adapter 常同时存在多条进程记录(npx / npm exec / sh -c / tsx 包装链)。
140
+ * 取**最早的启动时间**(最保守判定)合成一条,避免一个逻辑 server 被报成十几条。
141
+ */
142
+ export function earliestPerAdapter(servers: RunningServer[]): Map<string, RunningServer> {
143
+ const out = new Map<string, RunningServer>()
144
+ for (const s of servers) {
145
+ const prev = out.get(s.magicDir)
146
+ if (!prev) {
147
+ out.set(s.magicDir, s)
148
+ continue
149
+ }
150
+ const prevAt = prev.startedAt?.getTime() ?? Number.POSITIVE_INFINITY
151
+ const curAt = s.startedAt?.getTime() ?? Number.POSITIVE_INFINITY
152
+ if (curAt < prevAt) out.set(s.magicDir, s)
153
+ }
154
+ return out
155
+ }
156
+
157
+ /** 判定单个 adapter 的新鲜度(未知一律 "unknown") */
158
+ export function computeFreshness(
159
+ magicDir: string,
160
+ projectRoot: string,
161
+ deps: FreshnessDeps = {},
162
+ ): FreshnessInfo {
163
+ const artifactPath = join(projectRoot, magicDir, "scripts", "mcp-server.ts")
164
+ const markerPath = join(projectRoot, magicDir, RESTART_MARKER_FILE)
165
+ const mtime = deps.mtimeOf
166
+ ? deps.mtimeOf(artifactPath)
167
+ : existsSync(artifactPath)
168
+ ? statSync(artifactPath).mtime
169
+ : null
170
+ const hasMarker = deps.markerExists ? deps.markerExists(markerPath) : existsSync(markerPath)
171
+ const base: Omit<FreshnessInfo, "stale" | "detail"> = {
172
+ magicDir,
173
+ ...(mtime ? { artifactMtime: mtime.toISOString() } : {}),
174
+ ...(hasMarker ? { markerPath } : {}),
175
+ }
176
+ if (!mtime) {
177
+ return { ...base, stale: "unknown", detail: "产物不存在,无法判定新鲜度" }
178
+ }
179
+ const allServers = deps.listServers ? deps.listServers() : listRunningMcpServers()
180
+ // 残留族(启动者已死)不参与新鲜度判定:否则"孤儿进程"会把判定带偏
181
+ // (实测:旧实例的 server 让新鲜度报出错误的 pid / 错误的 stale 值)
182
+ const perAdapter = earliestPerAdapter(allServers.filter((s) => s.magicDir === magicDir && !s.orphan))
183
+ const server = perAdapter.get(magicDir)
184
+ if (!server) {
185
+ return { ...base, stale: false, detail: `${magicDir} 无运行中 server` }
186
+ }
187
+ if (!server.startedAt) {
188
+ return { ...base, stale: "unknown", detail: `进程 pid=${server.pid} 启动时间不可得` }
189
+ }
190
+ const stale = server.startedAt.getTime() < mtime.getTime()
191
+ return {
192
+ ...base,
193
+ processStartedAt: server.startedAt.toISOString(),
194
+ stale,
195
+ detail: stale
196
+ ? `${magicDir} server(pid ${server.pid}) 启动早于产物更新,需重启`
197
+ : `${magicDir} server(pid ${server.pid}) 晚于产物,已最新`,
198
+ }
199
+ }
200
+
201
+ /** 写重启标记(失败返回 null,调用方 fail-open) */
202
+ export function writeRestartRequiredMarker(
203
+ magicDir: string,
204
+ projectRoot: string,
205
+ info: { reason: string; pid?: number; artifactMtime?: string },
206
+ ): string | null {
207
+ const markerPath = join(projectRoot, magicDir, RESTART_MARKER_FILE)
208
+ try {
209
+ writeFileSync(
210
+ markerPath,
211
+ JSON.stringify({ magicDir, at: new Date().toISOString(), ...info }, null, 2),
212
+ "utf-8",
213
+ )
214
+ return markerPath
215
+ } catch {
216
+ return null
217
+ }
218
+ }
219
+
220
+ export function readRestartRequiredMarker(magicDir: string, projectRoot: string): string | null {
221
+ const markerPath = join(projectRoot, magicDir, RESTART_MARKER_FILE)
222
+ try {
223
+ return existsSync(markerPath) ? readFileSync(markerPath, "utf-8") : null
224
+ } catch {
225
+ return null
226
+ }
227
+ }
228
+
229
+ export function clearRestartRequiredMarker(magicDir: string, projectRoot: string): void {
230
+ try {
231
+ rmSync(join(projectRoot, magicDir, RESTART_MARKER_FILE), { force: true })
232
+ } catch {
233
+ /* fail-open */
234
+ }
235
+ }
@@ -29,7 +29,14 @@ import {
29
29
  getEmbeddings,
30
30
  } from "./helpers.js";
31
31
  import { getRuntimeContext } from "../../shared/env.js";
32
- import { extractSpecRef } from "../../shared/dps-spec-ref.js";
32
+ import {
33
+ buildGateCaptureDetail,
34
+ createGateWriterDeps,
35
+ deriveGateRunId,
36
+ formatGateCaptureSummary,
37
+ writeGateMetric,
38
+ GATE_METRIC_TYPE,
39
+ } from "../../shared/memory/metrics/gate-writer.js";
33
40
 
34
41
  export function registerCheckDps(server: ToolRegistrar) {
35
42
  const runtimeContext = getRuntimeContext();
@@ -80,8 +87,10 @@ export function registerCheckDps(server: ToolRegistrar) {
80
87
  let sn = basename(pm).replace(/-plan-v\d+\.md$/, ""),
81
88
  sc = "";
82
89
  if (pc) {
83
- const specRef = extractSpecRef(pc);
84
- if (specRef) sn = specRef.specDir;
90
+ const sr = pc.match(
91
+ /Spec[:|\s`]+\.?(qoder|claude|add|vscode)\/specs\/([^/`\s]+)/,
92
+ );
93
+ if (sr) sn = sr[2];
85
94
  }
86
95
  const sp = join(specsDir, sn, "spec.md");
87
96
  sc = (await readFileSafe(sp)) || "";
@@ -561,6 +570,35 @@ export function registerCheckDps(server: ToolRegistrar) {
561
570
  /* 非阻塞:回写失败不影响 DPS 判定 */
562
571
  }
563
572
 
573
+ // ── Gate 采证(Spec §GateMetric):评分已完成 → 独立提交 MetricSnapshot;
574
+ // runId 由被评分内容派生(同内容重放 = 重复采证);旁路不阻塞评分返回 ──
575
+ const gateSeed = `${pc}\n${sc}\n${rc}`;
576
+ const captureInput = {
577
+ gate: "check_dps" as const,
578
+ planKeyword: pp,
579
+ runId: deriveGateRunId("check_dps", pp, gateSeed),
580
+ metricType: GATE_METRIC_TYPE.check_dps,
581
+ score: dps,
582
+ repository: runtimeContext.projectKey,
583
+ dimensionScores: {
584
+ semantic: semScore,
585
+ entropy: entropyScore,
586
+ cpm: cpmScore,
587
+ structure: structFinal,
588
+ },
589
+ baseline: CFG.THRESHOLD_PASS,
590
+ unit: "score",
591
+ };
592
+ const capture = await writeGateMetric(
593
+ captureInput,
594
+ createGateWriterDeps((prisma as Record<string, unknown>).addMetricSnapshot),
595
+ );
596
+ parts.push(
597
+ "",
598
+ "=== Gate 采证(MetricSnapshot)===",
599
+ ` ${formatGateCaptureSummary(buildGateCaptureDetail(captureInput, capture))}`,
600
+ );
601
+
564
602
  return textResponse(parts.join("\n"));
565
603
  } catch (e) {
566
604
  return errorResponse(
@@ -20,6 +20,14 @@ import {
20
20
  import { prisma } from "../../shared/prisma.js";
21
21
  import { runCommand } from "../../shared/run-command.js";
22
22
  import { getRuntimeContext } from "../../shared/env.js";
23
+ import {
24
+ buildGateCaptureDetail,
25
+ createGateWriterDeps,
26
+ deriveGateRunId,
27
+ formatGateCaptureSummary,
28
+ writeGateMetric,
29
+ GATE_METRIC_TYPE,
30
+ } from "../../shared/memory/metrics/gate-writer.js";
23
31
 
24
32
  export function registerCheckRahs(server: ToolRegistrar) {
25
33
  const runtimeContext = getRuntimeContext();
@@ -50,6 +58,8 @@ export function registerCheckRahs(server: ToolRegistrar) {
50
58
  );
51
59
  if (!pm)
52
60
  return errorResponse(`未找到匹配的 Plan 文件(关键词: ${pp})`);
61
+ // Plan 正文在多个维度(Spec 引用解析、采证 seed)共用,提到此处只读一次
62
+ const planContent = (await readFileSafe(join(plansDir, pm))) || "";
53
63
  let scopeScore = 80,
54
64
  typeScore = 80,
55
65
  auditScore = 80,
@@ -115,7 +125,6 @@ export function registerCheckRahs(server: ToolRegistrar) {
115
125
  const rahsCandidates = rahsVersionSuffix
116
126
  ? [`${rahsPlanBase}-${rahsVersionSuffix}`, rahsPlanBase]
117
127
  : [rahsPlanBase];
118
- const planContent = (await readFileSafe(join(plansDir, pm))) || "";
119
128
  const rahsSpecRef = planContent.match(/specs\/([^/`\s]+)/);
120
129
  if (rahsSpecRef) rahsCandidates.push(rahsSpecRef[1]);
121
130
  const sn =
@@ -181,6 +190,36 @@ export function registerCheckRahs(server: ToolRegistrar) {
181
190
  `=== RAHS = ${rahs} ${rahs >= 90 ? "🟢 PASS" : rahs >= 70 ? "🟡 WARN" : "🔴 BLOCKED"} ===`,
182
191
  );
183
192
  if (rahs < 70) parts.push(" 动作: 注意力漂移严重,返工回退");
193
+
194
+ // ── Gate 采证(Spec §GateMetric):与评分解耦,失败旁路不阻塞返回 ──
195
+ const rahsSeed = `${planContent}\n${scopeScore}|${typeScore}|${auditScore}|${specScore}|${symScore}`;
196
+ const rahsCaptureInput = {
197
+ gate: "check_rahs" as const,
198
+ planKeyword: pp,
199
+ runId: deriveGateRunId("check_rahs", pp, rahsSeed),
200
+ metricType: GATE_METRIC_TYPE.check_rahs,
201
+ score: rahs,
202
+ repository: runtimeContext.projectKey,
203
+ dimensionScores: {
204
+ scope: scopeScore,
205
+ type: typeScore,
206
+ audit: auditScore,
207
+ spec: specScore,
208
+ symmetry: symScore,
209
+ },
210
+ baseline: 90,
211
+ unit: "score",
212
+ };
213
+ const rahsCapture = await writeGateMetric(
214
+ rahsCaptureInput,
215
+ createGateWriterDeps((prisma as Record<string, unknown>).addMetricSnapshot),
216
+ );
217
+ parts.push(
218
+ "",
219
+ "=== Gate 采证(MetricSnapshot)===",
220
+ ` ${formatGateCaptureSummary(buildGateCaptureDetail(rahsCaptureInput, rahsCapture))}`,
221
+ );
222
+
184
223
  return textResponse(parts.join("\n"));
185
224
  } catch (e) {
186
225
  return errorResponse(
@@ -76,8 +76,8 @@ export function registerCheckSpecSync(server: ToolRegistrar) {
76
76
  } else {
77
77
  lines.push(`add-route: ${arFile}`);
78
78
  const arContent = (await readFileSafe(join(plansDir, arFile))) || "";
79
- // 提取 add-route 附录文件清单(toml 纳入:sync-magic-rules.toml 等控制面文件,2026-08-18 修复)
80
- const appendixFiles = (arContent.match(/`[^`]+\.(ts|js|sh|md|tsx|json|yml|yaml|toml)`/g) || [])
79
+ // 提取 add-route 附录文件清单
80
+ const appendixFiles = (arContent.match(/`[^`]+\.(ts|js|sh|md|tsx|json|yml|yaml)`/g) || [])
81
81
  .map((f: string) => f.replace(/`/g, ""));
82
82
  lines.push(`附录文件: ${appendixFiles.length} 个`);
83
83
 
@@ -1,5 +1,5 @@
1
1
  import * as z from "zod/v4"
2
- import { inputRequired, acceptedContent } from "@modelcontextprotocol/server"
2
+ import { inputRequired, acceptedContent, type ElicitInputParams } from "@modelcontextprotocol/server"
3
3
  import type { ToolRegistrar } from "./registrar.js"
4
4
  import { writeFileSync, mkdirSync, existsSync, readFileSync } from "fs"
5
5
  import { join, basename, relative } from "path"
@@ -7,6 +7,7 @@ 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 { shouldSkipHitlCreateDialog } from "../shared/hitl-create-policy.js"
10
11
  import type { HitlRow } from "../shared/db-types.js"
11
12
  import { HitlRowSchema, validatedDelegate } from "../shared/db-types.js"
12
13
  import { getRuntimeContext } from "../shared/env.js"
@@ -18,6 +19,10 @@ import {
18
19
  createHitlProposalAndPublish,
19
20
  type HitlProposalDatabase,
20
21
  } from "../shared/hitl-proposal-mutation.js"
22
+ import {
23
+ buildHitlProposalMarkdown,
24
+ applyHitlDecisionToProposal,
25
+ } from "../shared/hitl-proposal-content.js"
21
26
  import { HITL_APPROVAL_WIDGET_URI } from "../shared/hitl-ui.js"
22
27
 
23
28
  // 无类型边界单点(zod 托管):动态加载的 prisma client 在此一次性转为运行期校验的泛型委托
@@ -25,6 +30,23 @@ const db = {
25
30
  get hitl() { return validatedDelegate<HitlRow>(prisma.hitlRecord, HitlRowSchema, "HitlRecord") },
26
31
  }
27
32
 
33
+ /**
34
+ * 表单式 elicitation 的 `requestedSchema`(2026-09-14:替换 4 处 `as any`)。
35
+ *
36
+ * 为什么需要一个函数:属性表是**运行时按维度动态构造**的(`dim_0_content`/`dim_0_decision`…),
37
+ * 而 SDK 的 `ElicitInputParams["requestedSchema"]` 是静态 JSON Schema 形状 —— 字面量直接赋值
38
+ * 会因索引签名不匹配而报错。这里把**唯一的窄化点**收在一处,类型取自 SDK 而非 `any`。
39
+ */
40
+ /** 表单里单个属性的形状(本仓库只用字符串型 + 可选枚举) */
41
+ type ElicitFormProperty = { type: "string"; enum?: string[]; description?: string }
42
+
43
+ function elicitFormSchema(
44
+ properties: Record<string, ElicitFormProperty>,
45
+ required: string[],
46
+ ): ElicitInputParams["requestedSchema"] {
47
+ return { type: "object", properties, required } as ElicitInputParams["requestedSchema"]
48
+ }
49
+
28
50
  export function registerHitlTools(server: ToolRegistrar) {
29
51
  const runtimeContext = getRuntimeContext()
30
52
 
@@ -123,9 +145,15 @@ export function registerHitlTools(server: ToolRegistrar) {
123
145
  filePath: string,
124
146
  status: HitlRow["status"],
125
147
  dimensions?: { name: string; content?: string }[],
148
+ reason?: string,
126
149
  ): void {
127
150
  let proposal = readFileSync(filePath, "utf-8")
128
- proposal = proposal.replace(/(状态:\s*)[A-Z_]+/, `$1${status}`)
151
+ // 状态行 + 「审批结论」行(2026-09-14:原实现只改状态行,裁决结论不回写文档)
152
+ proposal = applyHitlDecisionToProposal(proposal, {
153
+ status,
154
+ at: new Date().toISOString(),
155
+ reason,
156
+ })
129
157
 
130
158
  if (dimensions?.length) {
131
159
  const byName = new Map(dimensions.map((d) => [d.name.trim(), d.content ?? ""]))
@@ -187,16 +215,60 @@ export function registerHitlTools(server: ToolRegistrar) {
187
215
  if (!dimensions.length) {
188
216
  return errorResponse(`HITL 提案缺少可渲染维度: ${proposalPath ?? planName}`)
189
217
  }
218
+ // 产物-进程新鲜度(覆盖全 adapter;判定不确定 → "unknown",不误报"需重启")
219
+ const { computeFreshness } = await import("../shared/runtime-freshness.js")
220
+ const { writeHitlInstanceHtml } = await import("../shared/hitl-widget-instance.js")
221
+ const stale = computeFreshness(MAGIC_DIR, PROJECT_ROOT)
222
+ // 确定性降级入口:markdown 提案 + 已落盘实例 HTML(无 UI 能力/进程陈旧时仍可完成审批)
223
+ const toRel = (abs: string): string => abs.replace(`${PROJECT_ROOT}/`, "")
224
+ const markdownPath = proposalPath ? toRel(proposalPath) : ""
225
+ let htmlPath = ""
226
+ try {
227
+ htmlPath = toRel(
228
+ writeHitlInstanceHtml({
229
+ projectRoot: PROJECT_ROOT,
230
+ magicDir: MAGIC_DIR,
231
+ planName,
232
+ type,
233
+ round: current.round,
234
+ status: current.status,
235
+ dimensions,
236
+ }).htmlPath,
237
+ )
238
+ } catch {
239
+ htmlPath = "" // 模板缺失等 → 仍保留 markdown 降级路径(fail-open)
240
+ }
190
241
  const output = {
191
242
  planName,
192
243
  type,
193
244
  round: current.round,
194
245
  status: current.status,
195
246
  dimensions,
247
+ stale,
248
+ // 服务端无法得知客户端是否真的渲染了 widget —— 如实标注 unknown,不谎报
249
+ ui: { resourceUri: HITL_APPROVAL_WIDGET_URI, rendered: "unknown" as const },
250
+ fallback: { markdownPath, htmlPath },
196
251
  }
252
+ const fallbackHint = markdownPath || htmlPath
253
+ const text =
254
+ stale.stale === true
255
+ ? `已加载 ${planName} round ${current.round} 的 ${dimensions.length} 个审批维度。⚠️ 当前 server 进程早于产物更新(需重启);widget 若未显示,降级入口:${fallbackHint}`
256
+ : `已加载 ${planName} round ${current.round} 的 ${dimensions.length} 个审批维度。widget 若未显示,降级入口:${fallbackHint}`
197
257
  return {
198
- content: [{ type: "text" as const, text: `已加载 ${planName} round ${current.round} 的 ${dimensions.length} 个审批维度。请在 widget 中拍板。` }],
258
+ content: [{ type: "text" as const, text }],
199
259
  structuredContent: output,
260
+ /*
261
+ * 结果侧 `_meta`(Apps SDK 兼容补位,2026-09-14):
262
+ * 宿主实际读的是**工具定义**的 `_meta.ui.resourceUri`(`tools/list` 时解析,
263
+ * 实测 `thread_items` 里已带 `mcpAppResourceUri`,而 `result._meta` 仍为 null 也照常绑定);
264
+ * 这里补一份结果侧 `_meta` 不改变行为,只为兼容同时读结果位的客户端。
265
+ * 真正的渲染前提是:客户端 MCP Apps 能力开关(Codex: `/experimental` 的 enable_mcp_apps)
266
+ * + 资源 mimeType 为 `text/html;profile=mcp-app` + 资源存在 + dimensions 非空。
267
+ */
268
+ _meta: {
269
+ ui: { resourceUri: HITL_APPROVAL_WIDGET_URI },
270
+ "openai/outputTemplate": HITL_APPROVAL_WIDGET_URI,
271
+ },
200
272
  }
201
273
  } catch (e) {
202
274
  return errorResponse(`render_hitl_approval 失败: ${e instanceof Error ? e.message : String(e)}`)
@@ -208,9 +280,11 @@ export function registerHitlTools(server: ToolRegistrar) {
208
280
  description: "HITL 审批:创建提案。写入 HitlRecord(status=DRAFT,自动递增 round),并生成 hitl.md 提案文件供人工审核。\n" +
209
281
  "交互模式按安装环境自动裁决(caijuehub: hitl-interaction-rules.toml):\n" +
210
282
  "- genui 模式环境(如 Qoder,客户端未声明 elicitation capability):MUST 直接走 genui 流程——先用 genui show_widget 渲染逐项决策表单,用户拍板后以 _use_genui=true + 最终 dimensions 调用本工具。不带模式参数调用会返回 genui 引导而非弹框。\n" +
283
+ "- MCP Apps 模式环境(如 Codex):MUST NOT 展开高维 inputRequired——创建阶段直接落 DRAFT,审批走 render_hitl_approval 打开的 core widget(其回调以 update_hitl(_use_widget=true) 落库)。不带模式参数时按环境自动走该路径;也可用 _mcp_apps=true 强制。\n" +
211
284
  "- 支持 elicitation 的客户端(2026-07-28+ 协议):inputRequired 弹框,含逐项决策(LLM 传 dimensions)或简单弹框两种模式。\n" +
212
285
  "降级模式(_fallback=true):genui 与弹框均不可用时兜底,跳过弹框直接以传入的 dimensions 创建,人工审核 hitl.md。\n" +
213
286
  "_use_genui=true:genui widget 回调后使用,跳过所有弹框,直接以传入的 dimensions 创建 DB+文件。\n" +
287
+ "_mcp_apps=true:强制 MCP Apps 流程(落 DRAFT 不弹框,审批走 core widget)。\n" +
214
288
  "planName 示例: add-coder-hitl-mcp-hook-plan-v1\n" +
215
289
  "type: PLAN=计划审批, PLAN_REVIEW=方案评审",
216
290
  inputSchema: z.object({
@@ -222,10 +296,11 @@ export function registerHitlTools(server: ToolRegistrar) {
222
296
  })).optional().describe("决策维度列表(LLM 根据对话生成)。不传则按模板默认 8 维度"),
223
297
  _fallback: z.boolean().optional().default(false).describe("降级模式:跳过 inputRequired,按原始代码行为直接创建 DB 记录+hitl.md"),
224
298
  _use_genui: z.boolean().optional().default(false).describe("genui 模式:genui widget 回调后使用,跳过所有弹框直接创建(dimensions 需传最终确认值)"),
299
+ _mcp_apps: z.boolean().optional().default(false).describe("Codex MCP Apps 强制模式:落 DRAFT 不弹 inputRequired,审批走 render_hitl_approval core widget(update_hitl(_use_widget=true))"),
225
300
  }),
226
301
  }, async (args: Record<string, unknown>, ctx: Record<string, unknown>) => {
227
302
  try {
228
- const { planName, type, dimensions, _fallback, _use_genui } = args as { planName: string; type: string; dimensions?: { name: string; content?: string }[]; _fallback?: boolean; _use_genui?: boolean }
303
+ const { planName, type, dimensions, _fallback, _use_genui, _mcp_apps } = args as { planName: string; type: string; dimensions?: { name: string; content?: string }[]; _fallback?: boolean; _use_genui?: boolean; _mcp_apps?: boolean }
229
304
 
230
305
  // ── planName 入口强校验(弱模型友好:不合规返回可照抄修正调用,而非让错误漂移) ──
231
306
  const _pnValid = /-(plan|collab-contract)-v\d+$/.test(planName)
@@ -243,13 +318,14 @@ export function registerHitlTools(server: ToolRegistrar) {
243
318
  // ── 最终维度内容(从弹框结果合并) ──
244
319
  let finalDims: { name: string; content: string }[] = []
245
320
 
246
- // ── genui/降级模式:无弹框环节,直接采用传入的 dimensions(降级丢维度会退化成默认空模板) ──
247
- if (_use_genui || _fallback) {
321
+ // ── genui/降级/mcpApps 模式:无弹框环节,直接采用传入的 dimensions(降级丢维度会退化成默认空模板) ──
322
+ const skipDialog = shouldSkipHitlCreateDialog(_interaction.mode, { fallback: _fallback, useGenui: _use_genui, mcpApps: _mcp_apps })
323
+ if (skipDialog) {
248
324
  finalDims = (dimensions || []).map(d => ({ name: d.name, content: d.content || "" }))
249
325
  }
250
326
 
251
327
  // ── 交互式确认(非降级模式且非 genui 模式) ──
252
- if (!_fallback && !_use_genui) {
328
+ if (!skipDialog) {
253
329
  // 环境裁决:genui 模式下不发起注定失败的 elicitation,引导 LLM 走 widget 流程
254
330
  if (_interaction.mode === "genui") {
255
331
  const dimDesc = (dimensions || []).map((d, i) => `${i + 1}. ${d.name}: ${d.content || ""}`).join("\n")
@@ -266,7 +342,7 @@ export function registerHitlTools(server: ToolRegistrar) {
266
342
 
267
343
  if (!decResp) {
268
344
  // 首次调用 — 构建扁平逐项弹框
269
- const props: Record<string, unknown> = {
345
+ const props: Record<string, ElicitFormProperty> = {
270
346
  globalAction: {
271
347
  type: "string", enum: ["", "同意全部", "驳回全部"],
272
348
  description: "同意全部=通过所有(含已调整行); 驳回全部=驳回提案; 留空=逐项"
@@ -290,7 +366,7 @@ export function registerHitlTools(server: ToolRegistrar) {
290
366
  inputRequests: {
291
367
  confirm: inputRequired.elicit({
292
368
  message: `确认创建 HITL 提案\n\nplan: ${planName}\ntype: ${type}\n\n共 ${dimensions.length} 个决策维度:\n${dimDesc}`,
293
- requestedSchema: { type: "object", properties: props, required: requiredKeys } as any
369
+ requestedSchema: elicitFormSchema(props, requiredKeys),
294
370
  })
295
371
  }
296
372
  })
@@ -322,7 +398,10 @@ export function registerHitlTools(server: ToolRegistrar) {
322
398
  inputRequests: {
323
399
  confirm: inputRequired.elicit({
324
400
  message: `确认创建 HITL 提案?\n\nplan: ${planName}\ntype: ${type}`,
325
- requestedSchema: { type: "object", properties: { action: { type: "string", enum: ["同意", "取消"], description: "同意=确认创建提案, 取消=取消操作" } }, required: ["action"] } as any
401
+ requestedSchema: elicitFormSchema(
402
+ { action: { type: "string", enum: ["同意", "取消"], description: "同意=确认创建提案, 取消=取消操作" } },
403
+ ["action"],
404
+ ),
326
405
  })
327
406
  }
328
407
  })
@@ -361,34 +440,14 @@ export function registerHitlTools(server: ToolRegistrar) {
361
440
  // 生成 hitl.md
362
441
  const isoNow = now.toISOString()
363
442
 
364
- // 动态生成维度表格行
365
- const tableRows = finalDims.length > 0
366
- ? finalDims.map((d, i) => `| ${i + 1} | ${d.name} | ${d.content} | 同意/驳回 |`).join("\n")
367
- : [
368
- "| 1 | 实施主体 | | 同意/驳回 |",
369
- "| 2 | 数据模型 | | 同意/驳回 |",
370
- "| 3 | MCP 工具 | | 同意/驳回 |",
371
- "| 4 | 文件命名 | | 同意/驳回 |",
372
- "| 5 | 模板 + schema | | 同意/驳回 |",
373
- "| 6 | 新增依赖 | | 同意/驳回 |",
374
- "| 7 | 预计文件数 | | 同意/驳回 |",
375
- "| 8 | 预计轮次 | | 同意/驳回 |",
376
- ].join("\n")
377
-
378
- const tpl = [
379
- `# ${planName} — HITL 提案 (round ${round})`,
380
- "",
381
- `> 创建: ${isoNow} | 类型: ${type} | 状态: DRAFT`,
382
- "",
383
- "## HITL 计划总览",
384
- "",
385
- "请填写以下决策维度,人工审核后点击 update_hitl 弹框选择「同意/驳回」完成审批:",
386
- "",
387
- "| # | 维度 | 方案内容 | 决策 |",
388
- "|---|------|----------|:----:|",
389
- tableRows,
390
- "",
391
- ].join("\n")
443
+ // 生成提案文档(真源 hitl-template.md:HITL 计划总览 + 审批结论 两章节齐备)
444
+ const tpl = buildHitlProposalMarkdown({
445
+ planName: String(planName),
446
+ round,
447
+ type: String(type),
448
+ createdAt: isoNow,
449
+ dimensions: finalDims,
450
+ })
392
451
  mkdirSync(plansDir, { recursive: true })
393
452
  writeFileSync(proposalPath, tpl, "utf-8")
394
453
 
@@ -406,6 +465,10 @@ export function registerHitlTools(server: ToolRegistrar) {
406
465
  if (finalDims.length > 0) lines.push(`dimensions: ${finalDims.length} 项`)
407
466
  if (proposal.planProvisioned) lines.push(`PlanRecord: 自动预置(占位行,Plan 文件写出后 plan_track 回刷真实路径)`)
408
467
  if (_fallback) lines.push(`mode: _fallback (跳过 dialog,原始代码降级)`)
468
+ if ((_interaction.mode === "mcpApps" || _mcp_apps) && !_fallback) {
469
+ lines.push(`mode: mcpApps (Codex:不展开 inputRequired${_mcp_apps ? ",_mcp_apps 显式强制" : ""})`)
470
+ lines.push(``, `➡️ 下一步:调用 render_hitl_approval({ planName: "${planName}", type: "${type}" }) 打开审批 widget,由用户拍板。`)
471
+ }
409
472
  return textResponse(lines.join("\n"))
410
473
  } catch (e) {
411
474
  const msg = e instanceof Error ? e.message : String(e)
@@ -489,7 +552,7 @@ export function registerHitlTools(server: ToolRegistrar) {
489
552
  const decResp = _parseDecisions(ctx)
490
553
 
491
554
  if (!decResp) {
492
- const props: Record<string, unknown> = {
555
+ const props: Record<string, ElicitFormProperty> = {
493
556
  globalAction: {
494
557
  type: "string", enum: ["", "同意全部", "驳回全部"],
495
558
  description: "同意全部=通过所有维度; 驳回全部=驳回提案; 留空=逐项"
@@ -512,7 +575,7 @@ export function registerHitlTools(server: ToolRegistrar) {
512
575
  inputRequests: {
513
576
  confirm: inputRequired.elicit({
514
577
  message: `HITL 审批决策\n\nplan: ${planName}\ntype: ${type}\n\n逐项决策以下 ${dims.length} 个维度:\n${dimDesc}`,
515
- requestedSchema: { type: "object", properties: props, required: requiredKeys } as any
578
+ requestedSchema: elicitFormSchema(props, requiredKeys),
516
579
  })
517
580
  }
518
581
  })
@@ -536,7 +599,10 @@ export function registerHitlTools(server: ToolRegistrar) {
536
599
  inputRequests: {
537
600
  confirm: inputRequired.elicit({
538
601
  message: `HITL 审批决策\n\nplan: ${planName}\ntype: ${type}`,
539
- requestedSchema: { type: "object", properties: { action: { type: "string", enum: ["同意", "驳回", "取消"], description: "同意=通过审批, 驳回=驳回重做, 取消=暂不操作" } }, required: ["action"] } as any
602
+ requestedSchema: elicitFormSchema(
603
+ { action: { type: "string", enum: ["同意", "驳回", "取消"], description: "同意=通过审批, 驳回=驳回重做, 取消=暂不操作" } },
604
+ ["action"],
605
+ ),
540
606
  })
541
607
  }
542
608
  })
@@ -585,7 +651,7 @@ export function registerHitlTools(server: ToolRegistrar) {
585
651
  // P3 #6:回写 .hitl.md 提案文件状态(DRAFT → TONGYI/BOHUI),保证双通道校验一致
586
652
  const proposalPath = findHitlFile(String(planName))
587
653
  if (proposalPath) {
588
- updateHitlProposal(proposalPath, s, (_use_widget || _use_genui) ? dimensions : undefined)
654
+ updateHitlProposal(proposalPath, s, (_use_widget || _use_genui) ? dimensions : undefined, reason)
589
655
  }
590
656
  // 响应
591
657
  const lines = [