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.
- package/README.md +36 -10
- package/dist/index.js +481 -209
- package/package.json +14 -4
- package/templates/.add-coder-src-hash.json +129 -106
- package/templates/adapters/claude/hooks/doc-format-guard.sh +114 -33
- package/templates/adapters/claude/hooks/lib/common.sh +46 -87
- package/templates/adapters/claude/hooks/lib/context-inject.sh +2 -2
- package/templates/adapters/claude/hooks/lib/notify.sh +6 -4
- package/templates/adapters/claude/hooks/lib/preload-templates.sh +27 -2
- package/templates/adapters/claude/hooks/lib/state-detect.sh +8 -112
- package/templates/adapters/claude/hooks/lib/vocabulary.sh +13 -7
- package/templates/adapters/claude/hooks/prompt-submit.sh +1 -1
- package/templates/adapters/claude/hooks/stop-check.sh +9 -0
- package/templates/adapters/codex/config.toml.example +12 -0
- package/templates/adapters/codex/hooks/doc-format-guard.sh +123 -41
- package/templates/adapters/codex/hooks/lib/common.sh +47 -99
- package/templates/adapters/codex/hooks/lib/context-inject.sh +4 -4
- package/templates/adapters/codex/hooks/lib/notify.sh +6 -4
- package/templates/adapters/codex/hooks/lib/preload-templates.sh +27 -6
- package/templates/adapters/codex/hooks/lib/state-detect.sh +5 -114
- package/templates/adapters/codex/hooks/lib/vocabulary.sh +3 -8
- package/templates/adapters/codex/hooks/notification.sh +3 -18
- package/templates/adapters/codex/hooks/post-tool-use.sh +24 -119
- package/templates/adapters/codex/hooks/pre-tool-use.sh +97 -184
- package/templates/adapters/codex/hooks/prompt-submit.sh +8 -3
- package/templates/adapters/codex/hooks/session-start.sh +19 -9
- package/templates/adapters/codex/hooks/stop-check.sh +44 -18
- package/templates/adapters/codex/hooks.json +67 -1
- package/templates/adapters/qoder/hooks/doc-format-guard.sh +121 -40
- package/templates/adapters/qoder/hooks/lib/common.sh +49 -87
- package/templates/adapters/qoder/hooks/lib/context-inject.sh +2 -2
- package/templates/adapters/qoder/hooks/lib/notify.sh +6 -4
- package/templates/adapters/qoder/hooks/lib/preload-templates.sh +27 -2
- package/templates/adapters/qoder/hooks/lib/state-detect.sh +8 -112
- package/templates/adapters/qoder/hooks/lib/vocabulary.sh +13 -7
- package/templates/adapters/qoder/hooks/prompt-submit.sh +1 -1
- package/templates/adapters/qoder/hooks/session-start.sh +1 -1
- package/templates/adapters/qoder/hooks/stop-check.sh +11 -0
- package/templates/adapters/trae/hooks/doc-format-guard.sh +122 -40
- package/templates/adapters/trae/hooks/lib/common.sh +46 -87
- package/templates/adapters/trae/hooks/lib/context-inject.sh +2 -2
- package/templates/adapters/trae/hooks/lib/notify.sh +5 -3
- package/templates/adapters/trae/hooks/lib/preload-templates.sh +27 -2
- package/templates/adapters/trae/hooks/lib/state-detect.sh +8 -115
- package/templates/adapters/trae/hooks/lib/vocabulary.sh +13 -7
- package/templates/adapters/trae/hooks/pre-tool-use.sh +14 -8
- package/templates/adapters/trae/hooks/stop-check.sh +9 -0
- package/templates/adapters/vscode/hooks/doc-format-guard.sh +114 -33
- package/templates/adapters/vscode/hooks/lib/common.sh +46 -87
- package/templates/adapters/vscode/hooks/lib/context-inject.sh +2 -2
- package/templates/adapters/vscode/hooks/lib/notify.sh +6 -4
- package/templates/adapters/vscode/hooks/lib/preload-templates.sh +27 -2
- package/templates/adapters/vscode/hooks/lib/state-detect.sh +8 -112
- package/templates/adapters/vscode/hooks/lib/vocabulary.sh +13 -7
- package/templates/adapters/vscode/hooks/notification.sh +3 -5
- package/templates/adapters/vscode/hooks/prompt-submit.sh +1 -1
- package/templates/adapters/vscode/hooks/stop-check.sh +9 -0
- package/templates/core/docs/ADD-governance-claude-code.md +1 -1
- package/templates/core/docs/ADD-governance-codex.md +1 -1
- package/templates/core/docs/ADD-governance-qoder-cn.md +1 -1
- package/templates/core/docs/ADD-governance-trae.md +1 -1
- package/templates/core/docs/ADD-governance-vscode-copilot.md +1 -1
- package/templates/core/hooks/doc-format-guard.sh +122 -40
- package/templates/core/hooks/lib/common.sh +46 -87
- package/templates/core/hooks/lib/context-inject.sh +2 -2
- package/templates/core/hooks/lib/notify.sh +5 -3
- package/templates/core/hooks/lib/preload-templates.sh +27 -2
- package/templates/core/hooks/lib/state-detect.sh +8 -115
- package/templates/core/hooks/lib/vocabulary.sh +13 -7
- package/templates/core/hooks/pre-tool-use.sh +14 -8
- package/templates/core/hooks/stop-check.sh +9 -0
- package/templates/core/prisma/add.prisma +140 -82
- package/templates/core/rules/project_rules.md +27 -24
- package/templates/core/scripts/db-ensure.sh +8 -0
- package/templates/core/scripts/gen-plan-index.sh +4 -15
- package/templates/core/scripts/mcp-server/notifications/hitl.ts +9 -1
- package/templates/core/scripts/mcp-server/notifications/hook.ts +25 -9
- package/templates/core/scripts/mcp-server/resources/hitl-approval-widget.ts +34 -0
- package/templates/core/scripts/mcp-server/resources/hook-events-report.ts +14 -2
- package/templates/core/scripts/mcp-server/resources/index.ts +2 -0
- package/templates/core/scripts/mcp-server/sampling/review.ts +25 -10
- package/templates/core/scripts/mcp-server/shared/db-types.ts +33 -8
- package/templates/core/scripts/mcp-server/shared/dps-scoring.strategy.ts +1 -1
- package/templates/core/scripts/mcp-server/shared/env.ts +28 -8
- package/templates/core/scripts/mcp-server/shared/fs.ts +3 -0
- package/templates/core/scripts/mcp-server/shared/hitl-interaction.strategy.ts +2 -3
- package/templates/core/scripts/mcp-server/shared/hitl-lifecycle-mutation.ts +129 -0
- package/templates/core/scripts/mcp-server/shared/hitl-proposal-mutation.ts +98 -0
- package/templates/core/scripts/mcp-server/shared/hitl-ui.ts +3 -0
- package/templates/core/scripts/mcp-server/shared/plan-lifecycle-events.ts +62 -0
- package/templates/core/scripts/mcp-server/shared/plan-lifecycle-mutation.ts +78 -0
- package/templates/core/scripts/mcp-server/shared/plan-lifecycle-subscriber.ts +145 -0
- package/templates/core/scripts/mcp-server/shared/plan-lifecycle.ts +153 -0
- package/templates/core/scripts/mcp-server/shared/plan-round-events.ts +56 -0
- package/templates/core/scripts/mcp-server/shared/plan-round-mutation.ts +143 -0
- package/templates/core/scripts/mcp-server/shared/plan-round-store.ts +47 -0
- package/templates/core/scripts/mcp-server/shared/plan-round-subscriber.ts +99 -0
- package/templates/core/scripts/mcp-server/shared/plan-status-store.ts +37 -0
- package/templates/core/scripts/mcp-server/shared/plan-tracking-mutation.ts +71 -0
- package/templates/core/scripts/mcp-server/shared/prisma.ts +12 -6
- package/templates/core/scripts/mcp-server/shared/project-root-strategy.ts +1 -2
- package/templates/core/scripts/mcp-server/shared/quant-recheck.strategy.ts +16 -0
- package/templates/core/scripts/mcp-server/shared/redact.ts +28 -0
- package/templates/core/scripts/mcp-server/shared/response.ts +3 -2
- package/templates/core/scripts/mcp-server/shared/runtime-context.ts +77 -0
- package/templates/core/scripts/mcp-server/shared/schema-topology.ts +159 -0
- package/templates/core/scripts/mcp-server/tools/audit.ts +49 -7
- package/templates/core/scripts/mcp-server/tools/context.ts +117 -121
- package/templates/core/scripts/mcp-server/tools/contract.ts +34 -9
- package/templates/core/scripts/mcp-server/tools/gateway/check_add_route_status.ts +50 -3
- package/templates/core/scripts/mcp-server/tools/gateway/check_doc_similarity.ts +218 -0
- package/templates/core/scripts/mcp-server/tools/gateway/check_dps.ts +22 -7
- package/templates/core/scripts/mcp-server/tools/gateway/check_rahs.ts +75 -4
- package/templates/core/scripts/mcp-server/tools/gateway/check_spec_sync.ts +19 -6
- package/templates/core/scripts/mcp-server/tools/gateway/index.ts +2 -0
- package/templates/core/scripts/mcp-server/tools/hitl.ts +160 -75
- package/templates/core/scripts/mcp-server/tools/hook-event-report.ts +9 -2
- package/templates/core/scripts/mcp-server/tools/index.ts +69 -3
- package/templates/core/scripts/mcp-server/tools/plan.ts +140 -15
- package/templates/core/scripts/mcp-server/tools/review.ts +44 -15
- package/templates/core/scripts/mcp-server.ts +60 -1
- package/templates/core/scripts/plan-status-bridge.ts +26 -0
- package/templates/core/skills/add-paradigm/SKILL.md +2 -2
- package/templates/core/templates/add-route-template.schema.json +24 -56
- package/templates/core/templates/collab-contract-template.md +1 -1
- package/templates/core/templates/handoff-multi-round-template.schema.json +48 -0
- package/templates/core/templates/handoff-single-round-template.schema.json +92 -0
- package/templates/core/templates/handoff-single-round.schema.json +1 -1
- package/templates/core/templates/hitl-approval-widget.html +222 -37
- package/templates/core/templates/spec-template.schema.json +9 -46
- package/templates/core/templates/standard-plan-template.schema.json +1 -1
- 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
|
|
11
|
-
import { HitlRowSchema,
|
|
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
|
|
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
|
|
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
|
|
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
|
|
304
|
+
finalDims = dimensions.map(d => ({ name: d.name, content: d.content || "" }))
|
|
211
305
|
} else {
|
|
212
306
|
// 逐项处理:合并调整内容
|
|
213
|
-
finalDims = dimensions
|
|
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
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
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
|
|
362
|
-
const planName =
|
|
363
|
-
const 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
|
|
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
|
|
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
|
|
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
|
|
458
|
-
|
|
459
|
-
|
|
460
|
-
|
|
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
|
-
|
|
463
|
-
const
|
|
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
|
-
|
|
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: {
|
|
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> = {
|
|
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
|
|
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
|
-
|
|
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) //
|
|
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:
|
|
107
|
+
// Total: 30 tools
|
|
42
108
|
}
|