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