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.
- package/dist/index.js +21 -3
- package/package.json +3 -3
- package/templates/.add-coder-src-hash.json +144 -90
- package/templates/adapters/claude/hooks/doc-format-guard.mjs +172 -88
- package/templates/adapters/claude/hooks/notification.mjs +0 -4
- package/templates/adapters/claude/hooks/permission-denied.mjs +0 -4
- package/templates/adapters/claude/hooks/permission-gate.mjs +0 -4
- package/templates/adapters/claude/hooks/post-tool-use.mjs +59 -5
- package/templates/adapters/claude/hooks/pre-compact.mjs +0 -4
- package/templates/adapters/claude/hooks/pre-tool-use.mjs +0 -4
- package/templates/adapters/claude/hooks/prompt-submit.mjs +72 -4
- package/templates/adapters/claude/hooks/session-end.mjs +0 -4
- package/templates/adapters/claude/hooks/session-start.mjs +65 -5
- package/templates/adapters/claude/hooks/stop-check.mjs +6 -87
- package/templates/adapters/claude/hooks/stop-failure.mjs +0 -4
- package/templates/adapters/claude/hooks/subagent-guard.mjs +0 -4
- package/templates/adapters/claude/hooks/subagent-stop.mjs +0 -4
- package/templates/adapters/codex/hooks/doc-format-guard.mjs +172 -88
- package/templates/adapters/codex/hooks/notification.mjs +0 -4
- package/templates/adapters/codex/hooks/permission-gate.mjs +0 -4
- package/templates/adapters/codex/hooks/post-tool-failure.mjs +0 -4
- package/templates/adapters/codex/hooks/post-tool-use.mjs +59 -5
- package/templates/adapters/codex/hooks/pre-compact.mjs +0 -4
- package/templates/adapters/codex/hooks/pre-tool-use.mjs +0 -4
- package/templates/adapters/codex/hooks/prompt-submit.mjs +72 -4
- package/templates/adapters/codex/hooks/session-end.mjs +0 -4
- package/templates/adapters/codex/hooks/session-start.mjs +65 -5
- package/templates/adapters/codex/hooks/stop-check.mjs +6 -87
- package/templates/adapters/codex/hooks/subagent-guard.mjs +0 -4
- package/templates/adapters/codex/hooks/subagent-stop.mjs +0 -4
- package/templates/adapters/qoder/hooks/doc-format-guard.mjs +172 -88
- package/templates/adapters/qoder/hooks/notification.mjs +0 -4
- package/templates/adapters/qoder/hooks/permission-gate.mjs +0 -4
- package/templates/adapters/qoder/hooks/post-tool-failure.mjs +0 -4
- package/templates/adapters/qoder/hooks/post-tool-use.mjs +59 -5
- package/templates/adapters/qoder/hooks/pre-compact.mjs +0 -4
- package/templates/adapters/qoder/hooks/pre-tool-use.mjs +0 -4
- package/templates/adapters/qoder/hooks/prompt-submit.mjs +72 -4
- package/templates/adapters/qoder/hooks/review-checklist.mjs +0 -4
- package/templates/adapters/qoder/hooks/session-end.mjs +0 -4
- package/templates/adapters/qoder/hooks/session-start.mjs +67 -5
- package/templates/adapters/qoder/hooks/stop-check.mjs +6 -87
- package/templates/adapters/qoder/hooks/subagent-guard.mjs +0 -4
- package/templates/adapters/qoder/hooks/subagent-stop.mjs +0 -4
- package/templates/adapters/trae/hooks/doc-format-guard.mjs +172 -88
- package/templates/adapters/trae/hooks/notification.mjs +0 -4
- package/templates/adapters/trae/hooks/permission-gate.mjs +0 -4
- package/templates/adapters/trae/hooks/post-tool-failure.mjs +0 -4
- package/templates/adapters/trae/hooks/post-tool-use.mjs +59 -5
- package/templates/adapters/trae/hooks/pre-compact.mjs +0 -4
- package/templates/adapters/trae/hooks/pre-tool-use.mjs +0 -4
- package/templates/adapters/trae/hooks/prompt-submit.mjs +72 -4
- package/templates/adapters/trae/hooks/session-end.mjs +0 -4
- package/templates/adapters/trae/hooks/session-start.mjs +65 -5
- package/templates/adapters/trae/hooks/stop-check.mjs +6 -87
- package/templates/adapters/trae/hooks/subagent-guard.mjs +0 -4
- package/templates/adapters/trae/hooks/subagent-stop.mjs +0 -4
- package/templates/adapters/vscode/hooks/doc-format-guard.mjs +172 -88
- package/templates/adapters/vscode/hooks/notification.mjs +0 -4
- package/templates/adapters/vscode/hooks/permission-gate.mjs +0 -4
- package/templates/adapters/vscode/hooks/post-tool-failure.mjs +0 -4
- package/templates/adapters/vscode/hooks/post-tool-use.mjs +59 -5
- package/templates/adapters/vscode/hooks/pre-compact.mjs +0 -4
- package/templates/adapters/vscode/hooks/pre-tool-use.mjs +0 -4
- package/templates/adapters/vscode/hooks/prompt-submit.mjs +72 -4
- package/templates/adapters/vscode/hooks/session-end.mjs +0 -4
- package/templates/adapters/vscode/hooks/session-start.mjs +65 -5
- package/templates/adapters/vscode/hooks/stop-check.mjs +6 -87
- package/templates/adapters/vscode/hooks/subagent-guard.mjs +0 -4
- package/templates/adapters/vscode/hooks/subagent-stop.mjs +0 -4
- package/templates/core/governance/doc-format-guard.ts +29 -112
- package/templates/core/governance/post-tool-router.ts +33 -1
- package/templates/core/governance/prompt-router.ts +47 -0
- package/templates/core/governance/rules.ts +0 -4
- package/templates/core/governance/session-start-guard.ts +48 -1
- package/templates/core/governance/stop-router.ts +7 -101
- package/templates/core/prisma/add.prisma +203 -0
- package/templates/core/scripts/db-ensure.sh +61 -2
- package/templates/core/scripts/mcp-server/shared/db-types.ts +119 -0
- package/templates/core/scripts/mcp-server/shared/hitl-create-policy.ts +27 -0
- package/templates/core/scripts/mcp-server/shared/hitl-proposal-content.ts +110 -0
- package/templates/core/scripts/mcp-server/shared/hitl-widget-instance.ts +85 -0
- package/templates/core/scripts/mcp-server/shared/memory/calibration/batch-fit.ts +250 -0
- package/templates/core/scripts/mcp-server/shared/memory/calibration/feedback-stats.ts +101 -0
- package/templates/core/scripts/mcp-server/shared/memory/calibration/unit-state.ts +224 -0
- package/templates/core/scripts/mcp-server/shared/memory/domain/conflicts.ts +59 -0
- package/templates/core/scripts/mcp-server/shared/memory/domain/dedup.ts +45 -0
- package/templates/core/scripts/mcp-server/shared/memory/domain/errors.ts +33 -0
- package/templates/core/scripts/mcp-server/shared/memory/domain/handoff-digest.ts +92 -0
- package/templates/core/scripts/mcp-server/shared/memory/domain/metric-candidate.ts +79 -0
- package/templates/core/scripts/mcp-server/shared/memory/domain/scope.ts +94 -0
- package/templates/core/scripts/mcp-server/shared/memory/domain/secrets.ts +50 -0
- package/templates/core/scripts/mcp-server/shared/memory/domain/state-machine.ts +90 -0
- package/templates/core/scripts/mcp-server/shared/memory/embedding/index.ts +117 -0
- package/templates/core/scripts/mcp-server/shared/memory/embedding/local-onnx.ts +105 -0
- package/templates/core/scripts/mcp-server/shared/memory/embedding/openai-compatible.ts +87 -0
- package/templates/core/scripts/mcp-server/shared/memory/jobs/consolidation.ts +226 -0
- package/templates/core/scripts/mcp-server/shared/memory/jobs/evidence-collector.ts +153 -0
- package/templates/core/scripts/mcp-server/shared/memory/jobs/snapshot.ts +114 -0
- package/templates/core/scripts/mcp-server/shared/memory/metrics/gate-recall.ts +134 -0
- package/templates/core/scripts/mcp-server/shared/memory/metrics/gate-writer.ts +217 -0
- package/templates/core/scripts/mcp-server/shared/memory/metrics/stage-words.ts +69 -0
- package/templates/core/scripts/mcp-server/shared/memory/retrieval/context-builder.ts +89 -0
- package/templates/core/scripts/mcp-server/shared/memory/retrieval/fts/pg.ts +139 -0
- package/templates/core/scripts/mcp-server/shared/memory/retrieval/fts/sqlite-fts5.sql +29 -0
- package/templates/core/scripts/mcp-server/shared/memory/retrieval/fts/sqlite.ts +106 -0
- package/templates/core/scripts/mcp-server/shared/memory/retrieval/fusion.ts +43 -0
- package/templates/core/scripts/mcp-server/shared/memory/retrieval/pipeline.ts +285 -0
- package/templates/core/scripts/mcp-server/shared/memory/retrieval/query-terms.ts +31 -0
- package/templates/core/scripts/mcp-server/shared/memory/retrieval/recall-writer.ts +87 -0
- package/templates/core/scripts/mcp-server/shared/memory/retrieval/reranker.ts +116 -0
- package/templates/core/scripts/mcp-server/shared/memory/retrieval/types.ts +52 -0
- package/templates/core/scripts/mcp-server/shared/memory/retrieval/vector/pgvector.ts +143 -0
- package/templates/core/scripts/mcp-server/shared/memory/retrieval/vector/sqlite-vec.ts +118 -0
- package/templates/core/scripts/mcp-server/shared/memory/switches.ts +39 -0
- package/templates/core/scripts/mcp-server/shared/review-files.ts +22 -0
- package/templates/core/scripts/mcp-server/shared/runtime-freshness.ts +235 -0
- package/templates/core/scripts/mcp-server/tools/gateway/check_dps.ts +41 -3
- package/templates/core/scripts/mcp-server/tools/gateway/check_rahs.ts +40 -1
- package/templates/core/scripts/mcp-server/tools/gateway/check_spec_sync.ts +2 -2
- package/templates/core/scripts/mcp-server/tools/hitl.ts +108 -42
- package/templates/core/scripts/mcp-server/tools/index.ts +7 -1
- package/templates/core/scripts/mcp-server/tools/memory-compat.ts +258 -0
- package/templates/core/scripts/mcp-server/tools/memory.ts +654 -0
- package/templates/core/scripts/mcp-server/tools/plan.ts +8 -3
- package/templates/core/scripts/mcp-server/tools/review.ts +10 -7
- package/templates/core/scripts/mcp-server.ts +36 -0
- package/templates/core/templates/checklist-template.md +13 -0
- package/templates/core/templates/review-implementation-template.md +24 -0
- package/templates/core/templates/review-template.md +16 -0
- package/templates/core/validation/index.ts +136 -0
- package/templates/core/validation/policy.ts +91 -0
- package/templates/core/validation/registry.ts +61 -0
- package/templates/core/validation/schema-validator.ts +277 -0
- package/templates/core/validation/validators/add-route.ts +32 -0
- package/templates/core/validation/validators/checklist.ts +48 -0
- package/templates/core/validation/validators/handoff.ts +46 -0
- package/templates/core/validation/validators/hitl.ts +22 -0
- package/templates/core/validation/validators/index.ts +52 -0
- package/templates/core/validation/validators/plan.ts +20 -0
- package/templates/core/validation/validators/report.ts +16 -0
- package/templates/core/validation/validators/review.ts +30 -0
- package/templates/core/validation/validators/spec.ts +25 -0
- package/templates/core/validation/validators/tasks.ts +40 -0
- package/templates/core/validation/validators/types.ts +32 -0
- package/templates/core/vocabulary/add-governance-vocabulary.md +18 -0
- 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 {
|
|
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
|
|
84
|
-
|
|
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
|
|
80
|
-
const appendixFiles = (arContent.match(/`[^`]+\.(ts|js|sh|md|tsx|json|yml|yaml
|
|
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
|
-
|
|
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
|
|
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
|
|
247
|
-
|
|
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 (!
|
|
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,
|
|
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:
|
|
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:
|
|
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
|
|
366
|
-
|
|
367
|
-
|
|
368
|
-
|
|
369
|
-
|
|
370
|
-
|
|
371
|
-
|
|
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,
|
|
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:
|
|
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:
|
|
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 = [
|