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
|
@@ -1,9 +1,21 @@
|
|
|
1
1
|
import type { McpServer } from "@modelcontextprotocol/server"
|
|
2
2
|
import { join } from "path"
|
|
3
|
-
import {
|
|
3
|
+
import {
|
|
4
|
+
appendFileSync,
|
|
5
|
+
closeSync,
|
|
6
|
+
existsSync,
|
|
7
|
+
mkdirSync,
|
|
8
|
+
openSync,
|
|
9
|
+
readSync,
|
|
10
|
+
statSync,
|
|
11
|
+
watch,
|
|
12
|
+
writeFileSync,
|
|
13
|
+
} from "fs"
|
|
4
14
|
import { readFile } from "fs/promises"
|
|
15
|
+
import { createHash } from "crypto"
|
|
5
16
|
import { PROJECT_ROOT, MAGIC_DIR } from "../shared/fs.js"
|
|
6
17
|
import { prisma } from "../shared/prisma.js"
|
|
18
|
+
import { getRuntimeContext } from "../shared/env.js"
|
|
7
19
|
|
|
8
20
|
// ── 类型定义 ──
|
|
9
21
|
interface HookEvent {
|
|
@@ -29,6 +41,7 @@ interface QueueState {
|
|
|
29
41
|
const REPORT_DIR = join(PROJECT_ROOT, MAGIC_DIR, "reports")
|
|
30
42
|
const JSONL_FILE = join(REPORT_DIR, "hook-events.jsonl")
|
|
31
43
|
const OVERFLOW_FILE = join(REPORT_DIR, "hook-events-overflow.jsonl")
|
|
44
|
+
const runtimeContext = getRuntimeContext()
|
|
32
45
|
|
|
33
46
|
// ── 内存缓冲队列 ──
|
|
34
47
|
const MAX_QUEUE = 50
|
|
@@ -98,6 +111,13 @@ async function flushToDB(events: HookEvent[]): Promise<number> {
|
|
|
98
111
|
const ops = prisma.devOperation as Record<string, (...a: unknown[]) => unknown>
|
|
99
112
|
const data = unique.map(e => ({
|
|
100
113
|
userId,
|
|
114
|
+
projectKey: runtimeContext.projectKey,
|
|
115
|
+
producerAdapterKey: runtimeContext.adapterKey,
|
|
116
|
+
contextId: runtimeContext.contextId,
|
|
117
|
+
toolName: "hook-event-notification",
|
|
118
|
+
operationKey: createHash("sha256")
|
|
119
|
+
.update(`${runtimeContext.contextId}\0${dedupKey(e)}\0${e.decision}\0${e.cmd}`)
|
|
120
|
+
.digest("hex"),
|
|
101
121
|
action: "HOOK_INTERCEPT",
|
|
102
122
|
targetType: e.hook,
|
|
103
123
|
targetId: e.cmd.substring(0, 500),
|
|
@@ -106,7 +126,7 @@ async function flushToDB(events: HookEvent[]): Promise<number> {
|
|
|
106
126
|
afterState: JSON.stringify({ planStatus: e.planStatus }),
|
|
107
127
|
}))
|
|
108
128
|
|
|
109
|
-
await ops.createMany({ data })
|
|
129
|
+
await ops.createMany({ data, skipDuplicates: true })
|
|
110
130
|
return unique.length
|
|
111
131
|
} catch (err) {
|
|
112
132
|
console.error("[hook-notify] flushToDB 失败:", err instanceof Error ? err.message : String(err))
|
|
@@ -125,7 +145,6 @@ async function drainOverflowFile(): Promise<HookEvent[]> {
|
|
|
125
145
|
if (ev) events.push(ev)
|
|
126
146
|
}
|
|
127
147
|
// 清空溢出文件
|
|
128
|
-
const { writeFileSync } = await import("fs")
|
|
129
148
|
writeFileSync(OVERFLOW_FILE, "", "utf-8")
|
|
130
149
|
} catch {
|
|
131
150
|
// 忽略,下次 flush 重试
|
|
@@ -145,7 +164,6 @@ async function doFlush(): Promise<void> {
|
|
|
145
164
|
|
|
146
165
|
const count = await flushToDB(allEvents)
|
|
147
166
|
if (count > 0 && serverRef) {
|
|
148
|
-
const noPlanCount = allEvents.filter(e => e.planKeyword === "no-active-plan").length
|
|
149
167
|
const msg = `[Hook] ${count} 条拦截事件已审计落库(计划: ${[...new Set(allEvents.map(e => e.planKeyword))].join(", ")})`
|
|
150
168
|
serverRef.sendLoggingMessage({ level: "warning" as const, data: msg }).catch(() => {})
|
|
151
169
|
}
|
|
@@ -156,7 +174,6 @@ function enqueue(event: HookEvent): void {
|
|
|
156
174
|
if (q.events.length >= MAX_QUEUE) {
|
|
157
175
|
// 降级写入溢出文件
|
|
158
176
|
try {
|
|
159
|
-
const { appendFileSync } = require("fs") as typeof import("fs")
|
|
160
177
|
appendFileSync(OVERFLOW_FILE, JSON.stringify(event) + "\n", "utf-8")
|
|
161
178
|
} catch { /* 静默 */ }
|
|
162
179
|
return
|
|
@@ -191,10 +208,10 @@ function readNewLines(): void {
|
|
|
191
208
|
if (st.size <= q.bytesRead) return
|
|
192
209
|
|
|
193
210
|
// 只读增量部分
|
|
194
|
-
const fd =
|
|
211
|
+
const fd = openSync(JSONL_FILE, "r")
|
|
195
212
|
const buf = Buffer.alloc(st.size - q.bytesRead)
|
|
196
|
-
|
|
197
|
-
|
|
213
|
+
readSync(fd, buf, 0, buf.length, q.bytesRead)
|
|
214
|
+
closeSync(fd)
|
|
198
215
|
|
|
199
216
|
const text = buf.toString("utf-8")
|
|
200
217
|
for (const line of text.split("\n")) {
|
|
@@ -242,7 +259,6 @@ export function registerHookNotifications(server: McpServer) {
|
|
|
242
259
|
void initialScan().then(() => {
|
|
243
260
|
// 确保 jsonl 文件存在(fs.watch 要求文件已存在)
|
|
244
261
|
try {
|
|
245
|
-
const { mkdirSync, writeFileSync } = require("fs") as typeof import("fs")
|
|
246
262
|
mkdirSync(REPORT_DIR, { recursive: true })
|
|
247
263
|
if (!existsSync(JSONL_FILE)) writeFileSync(JSONL_FILE, "", "utf-8")
|
|
248
264
|
} catch { /* ok */ }
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
import type { McpServer } from "@modelcontextprotocol/server"
|
|
2
|
+
import { existsSync, readFileSync } from "fs"
|
|
3
|
+
import { join } from "path"
|
|
4
|
+
import { PROJECT_ROOT, MAGIC_DIR } from "../shared/fs.js"
|
|
5
|
+
import {
|
|
6
|
+
HITL_APPROVAL_WIDGET_FILE,
|
|
7
|
+
HITL_APPROVAL_WIDGET_MIME,
|
|
8
|
+
HITL_APPROVAL_WIDGET_URI,
|
|
9
|
+
} from "../shared/hitl-ui.js"
|
|
10
|
+
|
|
11
|
+
export function registerHitlApprovalWidgetResource(server: McpServer) {
|
|
12
|
+
server.registerResource(
|
|
13
|
+
"hitl-approval-widget",
|
|
14
|
+
HITL_APPROVAL_WIDGET_URI,
|
|
15
|
+
{
|
|
16
|
+
title: "ADD HITL Approval",
|
|
17
|
+
description: "逐项审核 ADD HITL 提案的 core 标准 widget",
|
|
18
|
+
mimeType: HITL_APPROVAL_WIDGET_MIME,
|
|
19
|
+
},
|
|
20
|
+
(uri) => {
|
|
21
|
+
const widgetPath = join(PROJECT_ROOT, MAGIC_DIR, "templates", HITL_APPROVAL_WIDGET_FILE)
|
|
22
|
+
if (!existsSync(widgetPath)) {
|
|
23
|
+
throw new Error(`HITL widget 缺失: ${widgetPath}。请重新执行 add-coder sync。`)
|
|
24
|
+
}
|
|
25
|
+
return {
|
|
26
|
+
contents: [{
|
|
27
|
+
uri: uri.href,
|
|
28
|
+
mimeType: HITL_APPROVAL_WIDGET_MIME,
|
|
29
|
+
text: readFileSync(widgetPath, "utf-8"),
|
|
30
|
+
}],
|
|
31
|
+
}
|
|
32
|
+
},
|
|
33
|
+
)
|
|
34
|
+
}
|
|
@@ -1,7 +1,9 @@
|
|
|
1
1
|
import type { McpServer } from "@modelcontextprotocol/server"
|
|
2
2
|
import { prisma } from "../shared/prisma.js"
|
|
3
|
+
import { getRuntimeContext } from "../shared/env.js"
|
|
3
4
|
|
|
4
5
|
export function registerHookEventResources(server: McpServer) {
|
|
6
|
+
const runtimeContext = getRuntimeContext()
|
|
5
7
|
|
|
6
8
|
// ── 日报 Resource ──
|
|
7
9
|
server.registerResource("hook-events-daily", "add-coder://report/hook-events/daily",
|
|
@@ -11,7 +13,12 @@ export function registerHookEventResources(server: McpServer) {
|
|
|
11
13
|
const ops = prisma.devOperation as Record<string, (...a: unknown[]) => unknown>
|
|
12
14
|
const since = new Date(Date.now() - 24 * 60 * 60 * 1000)
|
|
13
15
|
const logs = await ops.findMany({
|
|
14
|
-
where: {
|
|
16
|
+
where: {
|
|
17
|
+
action: "HOOK_INTERCEPT",
|
|
18
|
+
projectKey: runtimeContext.projectKey,
|
|
19
|
+
producerAdapterKey: runtimeContext.adapterKey,
|
|
20
|
+
createdAt: { gte: since },
|
|
21
|
+
},
|
|
15
22
|
orderBy: { createdAt: "desc" },
|
|
16
23
|
take: 500,
|
|
17
24
|
}) as Array<Record<string, unknown>>
|
|
@@ -60,7 +67,12 @@ export function registerHookEventResources(server: McpServer) {
|
|
|
60
67
|
const ops = prisma.devOperation as Record<string, (...a: unknown[]) => unknown>
|
|
61
68
|
const since = new Date(Date.now() - 7 * 24 * 60 * 60 * 1000)
|
|
62
69
|
const logs = await ops.findMany({
|
|
63
|
-
where: {
|
|
70
|
+
where: {
|
|
71
|
+
action: "HOOK_INTERCEPT",
|
|
72
|
+
projectKey: runtimeContext.projectKey,
|
|
73
|
+
producerAdapterKey: runtimeContext.adapterKey,
|
|
74
|
+
createdAt: { gte: since },
|
|
75
|
+
},
|
|
64
76
|
orderBy: { createdAt: "desc" },
|
|
65
77
|
take: 2000,
|
|
66
78
|
}) as Array<Record<string, unknown>>
|
|
@@ -3,10 +3,12 @@ import { registerAddStateResources } from "./add-state.js"
|
|
|
3
3
|
import { registerRoundTaskResources } from "./round-task.js"
|
|
4
4
|
import { registerVersionResource } from "./add-coder-version.js"
|
|
5
5
|
import { registerHookEventResources } from "./hook-events-report.js"
|
|
6
|
+
import { registerHitlApprovalWidgetResource } from "./hitl-approval-widget.js"
|
|
6
7
|
|
|
7
8
|
export function registerAllResources(server: McpServer) {
|
|
8
9
|
registerAddStateResources(server)
|
|
9
10
|
registerRoundTaskResources(server)
|
|
10
11
|
registerVersionResource(server)
|
|
11
12
|
registerHookEventResources(server) // 2 resources: hook-events/{daily,weekly}
|
|
13
|
+
registerHitlApprovalWidgetResource(server)
|
|
12
14
|
}
|
|
@@ -6,7 +6,8 @@ import { readFileSafe, PROJECT_ROOT, MAGIC_DIR } from "../shared/fs.js"
|
|
|
6
6
|
* HITL Review 触发
|
|
7
7
|
* ADD-9 方向验证 / ADD-10 语义对齐 / ADD-11 证据持久化
|
|
8
8
|
*
|
|
9
|
-
*
|
|
9
|
+
* 方案/实现 Review: 读取模板 → 整理 dimensions → create_hitl → update_hitl → status_hitl → 写入正式 Review
|
|
10
|
+
* Runtime Review: [T] 全部通过后按模板直接生成,不走 HITL
|
|
10
11
|
* 参考: add-paradigm SKILL Step 0.6.5(Review 结论回流至 Plan 与 Specs)
|
|
11
12
|
*/
|
|
12
13
|
export async function createReviewRequest(planKeyword: string, reviewType: "plan" | "implementation" | "runtime" = "plan"): Promise<InputRequiredResult> {
|
|
@@ -18,23 +19,37 @@ export async function createReviewRequest(planKeyword: string, reviewType: "plan
|
|
|
18
19
|
|
|
19
20
|
const templatePath = join(PROJECT_ROOT, MAGIC_DIR, "templates", templateFile)
|
|
20
21
|
const template = await readFileSafe(templatePath)
|
|
22
|
+
const requiresHitl = reviewType !== "runtime"
|
|
23
|
+
const reviewPlanName = reviewType === "plan"
|
|
24
|
+
? `${planKeyword}-review-v1`
|
|
25
|
+
: `${planKeyword}-review-${reviewType}`
|
|
21
26
|
|
|
22
|
-
const
|
|
23
|
-
## HITL
|
|
27
|
+
const workflowGuide = requiresHitl ? `
|
|
28
|
+
## HITL 审核流程
|
|
24
29
|
|
|
25
|
-
1.
|
|
26
|
-
2.
|
|
27
|
-
3.
|
|
30
|
+
1. 根据模板与审查证据,将待拍板发现整理为 dimensions
|
|
31
|
+
2. 调用 create_hitl({ planName: "${reviewPlanName}", type: "PLAN_REVIEW", dimensions })
|
|
32
|
+
3. 人类审核提案后调用 update_hitl 完成 TONGYI/BOHUI 拍板
|
|
33
|
+
4. 通过 status_hitl 确认 TONGYI 后,生成完整 Review 写入 ${MAGIC_DIR}/reviews/
|
|
34
|
+
5. 将 Review 结论回流至 Plan 与 Specs(Step 0.6.5)
|
|
28
35
|
|
|
29
36
|
模板:
|
|
30
37
|
${template?.slice(0, 3000) ?? `标准 ${reviewType} Review 模板`}
|
|
38
|
+
` : `
|
|
39
|
+
## Runtime Review 流程
|
|
40
|
+
|
|
41
|
+
1. 确认 implementation checklist 的 [T] 项已全部通过
|
|
42
|
+
2. 按模板直接生成 Runtime Review 写入 ${MAGIC_DIR}/reviews/
|
|
43
|
+
3. 持久化运行时证据,并将发现回流至 Plan、Specs 或 runtime-fix Plan
|
|
44
|
+
|
|
45
|
+
模板:
|
|
46
|
+
${template?.slice(0, 3000) ?? "标准 runtime Review 模板"}
|
|
31
47
|
`
|
|
32
48
|
|
|
33
|
-
const prompt = `请为 Plan "${planKeyword}"
|
|
34
|
-
先读取 ${MAGIC_DIR}/
|
|
35
|
-
然后按 HITL 两步法:先写 temporary.md → 人类拍板 → 生成完整 Review。
|
|
49
|
+
const prompt = `请为 Plan "${planKeyword}" 生成 Review(类型: ${reviewType})。
|
|
50
|
+
先读取 ${MAGIC_DIR}/templates/${templateFile},再严格执行下述流程。
|
|
36
51
|
|
|
37
|
-
${
|
|
52
|
+
${workflowGuide}`
|
|
38
53
|
|
|
39
54
|
const sampleRequest = inputRequired.createMessage({
|
|
40
55
|
messages: [{ role: "user" as const, content: { type: "text" as const, text: prompt } }],
|
|
@@ -20,7 +20,11 @@ import * as z from "zod/v4"
|
|
|
20
20
|
|
|
21
21
|
export const PlanRowSchema = z.looseObject({
|
|
22
22
|
id: z.string(),
|
|
23
|
+
projectKey: z.string(),
|
|
24
|
+
adapterKey: z.string(),
|
|
23
25
|
planName: z.string(),
|
|
26
|
+
lifecycle: z.enum(["DRAFT", "ACTIVE", "BLOCKED", "REJECTED", "CLOSED", "ABANDONED"]),
|
|
27
|
+
revision: z.number(),
|
|
24
28
|
planPath: z.string(),
|
|
25
29
|
planKeyword: z.string().nullable(),
|
|
26
30
|
specPath: z.string().nullable(),
|
|
@@ -45,6 +49,8 @@ export const PlanRowSchema = z.looseObject({
|
|
|
45
49
|
|
|
46
50
|
export const HitlRowSchema = z.looseObject({
|
|
47
51
|
id: z.string(),
|
|
52
|
+
projectKey: z.string(),
|
|
53
|
+
adapterKey: z.string(),
|
|
48
54
|
planName: z.string(),
|
|
49
55
|
round: z.number(),
|
|
50
56
|
type: z.enum(["PLAN", "PLAN_REVIEW", "COLLAB_CONTRACT"]),
|
|
@@ -58,6 +64,8 @@ export const HitlRowSchema = z.looseObject({
|
|
|
58
64
|
|
|
59
65
|
export const ReviewRowSchema = z.looseObject({
|
|
60
66
|
id: z.string(),
|
|
67
|
+
projectKey: z.string(),
|
|
68
|
+
adapterKey: z.string(),
|
|
61
69
|
planName: z.string(),
|
|
62
70
|
type: z.enum(["PLAN_REVIEW", "IMPLEMENTATION", "RUNTIME"]),
|
|
63
71
|
reviewPath: z.string().nullable(),
|
|
@@ -70,6 +78,9 @@ export const ReviewRowSchema = z.looseObject({
|
|
|
70
78
|
|
|
71
79
|
export const AuditLogRowSchema = z.looseObject({
|
|
72
80
|
id: z.string(),
|
|
81
|
+
projectKey: z.string(),
|
|
82
|
+
producerAdapterKey: z.string(),
|
|
83
|
+
contextId: z.string(),
|
|
73
84
|
action: z.string(),
|
|
74
85
|
targetType: z.string(),
|
|
75
86
|
targetId: z.string(),
|
|
@@ -83,6 +94,11 @@ export const AuditLogRowSchema = z.looseObject({
|
|
|
83
94
|
export const DevOperationRowSchema = z.looseObject({
|
|
84
95
|
id: z.string(),
|
|
85
96
|
userId: z.string(),
|
|
97
|
+
projectKey: z.string(),
|
|
98
|
+
producerAdapterKey: z.string(),
|
|
99
|
+
contextId: z.string(),
|
|
100
|
+
toolName: z.string(),
|
|
101
|
+
operationKey: z.string(),
|
|
86
102
|
planKeyword: z.string(),
|
|
87
103
|
action: z.string(),
|
|
88
104
|
targetType: z.string(),
|
|
@@ -103,9 +119,13 @@ export const AddUserRowSchema = z.looseObject({
|
|
|
103
119
|
|
|
104
120
|
export const CollabContractRowSchema = z.looseObject({
|
|
105
121
|
id: z.string(),
|
|
122
|
+
projectKey: z.string(),
|
|
123
|
+
ownerAdapterKey: z.string(),
|
|
106
124
|
contractName: z.string(),
|
|
107
125
|
contractPath: z.string(),
|
|
108
126
|
masterPlanName: z.string(),
|
|
127
|
+
masterProjectKey: z.string(),
|
|
128
|
+
masterAdapterKey: z.string(),
|
|
109
129
|
participants: z.unknown(),
|
|
110
130
|
abilityMatrix: z.unknown().nullable(),
|
|
111
131
|
stages: z.unknown(),
|
|
@@ -151,6 +171,7 @@ export interface TableDelegate<T> {
|
|
|
151
171
|
findMany(args?: QueryArgs<T>): Promise<T[]>
|
|
152
172
|
findUnique(args: { where: Record<string, unknown>; select?: Record<string, unknown> }): Promise<T | null>
|
|
153
173
|
create(args: { data: Partial<T> }): Promise<T>
|
|
174
|
+
upsert(args: { where: Record<string, unknown>; create: Partial<T>; update: Partial<T> }): Promise<T>
|
|
154
175
|
update(args: { where: Partial<T>; data: Partial<T> }): Promise<T>
|
|
155
176
|
updateMany(args: { where: Record<string, unknown>; data: Partial<T> }): Promise<BatchResult>
|
|
156
177
|
delete(args: { where: Partial<T> }): Promise<T>
|
|
@@ -187,22 +208,26 @@ export function validatedDelegate<T>(
|
|
|
187
208
|
|
|
188
209
|
return {
|
|
189
210
|
async findFirst(args) {
|
|
190
|
-
const row = await raw.findFirst(args
|
|
211
|
+
const row = await raw.findFirst(args)
|
|
191
212
|
return row === null ? null : parseRow(row)
|
|
192
213
|
},
|
|
193
214
|
async findMany(args) {
|
|
194
|
-
const rows = await raw.findMany(args
|
|
215
|
+
const rows = await raw.findMany(args)
|
|
195
216
|
return rows.map(parseRow)
|
|
196
217
|
},
|
|
197
218
|
async findUnique(args) {
|
|
198
219
|
if (!raw.findUnique) throw new Error(`表 ${tableName} 不支持 findUnique`)
|
|
199
|
-
const row = await raw.findUnique(args
|
|
220
|
+
const row = await raw.findUnique(args)
|
|
200
221
|
return row === null ? null : parseRow(row)
|
|
201
222
|
},
|
|
202
|
-
async create(args) { return parseRow(await raw.create(args
|
|
203
|
-
async
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
223
|
+
async create(args) { return parseRow(await raw.create(args)) },
|
|
224
|
+
async upsert(args) {
|
|
225
|
+
if (!raw.upsert) throw new Error(`表 ${tableName} 不支持 upsert`)
|
|
226
|
+
return parseRow(await raw.upsert(args))
|
|
227
|
+
},
|
|
228
|
+
async update(args) { return parseRow(await raw.update(args)) },
|
|
229
|
+
async updateMany(args) { return raw.updateMany(args) },
|
|
230
|
+
async delete(args) { return parseRow(await raw.delete(args)) },
|
|
231
|
+
async deleteMany(args) { return raw.deleteMany(args) },
|
|
207
232
|
}
|
|
208
233
|
}
|
|
@@ -1,29 +1,49 @@
|
|
|
1
1
|
import dotenv from "dotenv"
|
|
2
|
-
import { dirname, resolve, basename } from "path"
|
|
3
|
-
import { fileURLToPath } from "url"
|
|
2
|
+
import { dirname, resolve, basename, join } from "path"
|
|
4
3
|
import { existsSync } from "fs"
|
|
5
|
-
import { resolveProjectRoot } from "./project-root-strategy.js"
|
|
4
|
+
import { resolveProjectRoot } from "./project-root-strategy.js";
|
|
5
|
+
import { createRuntimeContext, type RuntimeContextKey } from "./runtime-context.js"
|
|
6
6
|
|
|
7
|
-
const
|
|
8
|
-
const __dirname = dirname(__filename)
|
|
7
|
+
const __dirname = import.meta.dirname
|
|
9
8
|
|
|
10
9
|
// 集中裁决层驱动:PROJECT_ROOT 推导由 project-root-rules.toml → project-root-strategy.ts 定义
|
|
11
10
|
export const PROJECT_ROOT = resolveProjectRoot(__dirname)
|
|
12
11
|
|
|
13
|
-
|
|
12
|
+
// find-up 锚点(轮次 3):向上找含 scripts/mcp-server 结构的目录 = magicDir(不手算层级)
|
|
13
|
+
// magicDir 结构探测(轮次 3):从当前文件位置向上逐级找首个 magic 结构目录(.xxx 且含 scripts/mcp-server)
|
|
14
|
+
// —— 命中当前运行副本,多副本共存无歧义,不 hardcode 目录名
|
|
15
|
+
export const MAGIC_DIR = process.env.MAGIC_DIR || (() => {
|
|
16
|
+
try {
|
|
17
|
+
let d = __dirname;
|
|
18
|
+
for (;;) {
|
|
19
|
+
const base = basename(d);
|
|
20
|
+
if (base.startsWith(".") && existsSync(join(d, "scripts", "mcp-server"))) return base;
|
|
21
|
+
const parent = dirname(d);
|
|
22
|
+
if (parent === d) break;
|
|
23
|
+
d = parent;
|
|
24
|
+
}
|
|
25
|
+
} catch { /* 探测失败走兜底 */ }
|
|
26
|
+
return basename(resolve(__dirname, "..", "..", "..", ".."));
|
|
27
|
+
})()
|
|
14
28
|
export const PROJECT_ID = basename(PROJECT_ROOT)
|
|
15
29
|
|
|
30
|
+
let runtimeContext: Readonly<RuntimeContextKey> | undefined
|
|
31
|
+
export function getRuntimeContext(): Readonly<RuntimeContextKey> {
|
|
32
|
+
runtimeContext ??= createRuntimeContext(PROJECT_ROOT, MAGIC_DIR)
|
|
33
|
+
return runtimeContext
|
|
34
|
+
}
|
|
35
|
+
|
|
16
36
|
const ENV_CANDIDATES = [".env.development.local", ".env.development", ".env.local", ".env"]
|
|
17
37
|
let loaded = false
|
|
18
38
|
for (const base of [PROJECT_ROOT, process.cwd()]) {
|
|
19
39
|
if (loaded) break
|
|
20
40
|
for (const f of ENV_CANDIDATES) {
|
|
21
41
|
const p = resolve(base, f)
|
|
22
|
-
if (existsSync(p)) { dotenv.config({ path: p, override: true }); loaded = true; break }
|
|
42
|
+
if (existsSync(p)) { dotenv.config({ path: p, override: true, quiet: true }); loaded = true; break }
|
|
23
43
|
}
|
|
24
44
|
}
|
|
25
45
|
|
|
26
|
-
export const DATABASE_URL = process.env.DATABASE_URL
|
|
46
|
+
export const DATABASE_URL: string = process.env.DATABASE_URL ?? ""
|
|
27
47
|
if (!DATABASE_URL) {
|
|
28
48
|
throw new Error("DATABASE_URL 未设置,请在 .env 中配置数据库连接串")
|
|
29
49
|
}
|
|
@@ -43,3 +43,6 @@ export async function readdirRecursive(baseDir: string): Promise<string[]> {
|
|
|
43
43
|
}
|
|
44
44
|
|
|
45
45
|
export { PROJECT_ROOT, MAGIC_DIR }
|
|
46
|
+
|
|
47
|
+
/** magic 目录前缀(git diff 过滤等场景用):当前环境 magicDir + 分发镜像(与 sync 分发同源) */
|
|
48
|
+
export const MAGIC_PREFIXES = [MAGIC_DIR, ".qoder", ".claude", ".vscode", ".trae", ".codex", ".add", ".backup"];
|
|
@@ -8,11 +8,10 @@ export const HITL_INTERACTION_CONFIG = {
|
|
|
8
8
|
claude: { mode: "inputRequired" as const },
|
|
9
9
|
vscode: { mode: "inputRequired" as const },
|
|
10
10
|
trae: { mode: "inputRequired" as const },
|
|
11
|
-
codex: { mode: "
|
|
11
|
+
codex: { mode: "mcpApps" as const }
|
|
12
12
|
} as const;
|
|
13
13
|
|
|
14
14
|
export type HitlInteractionMode = (typeof HITL_INTERACTION_CONFIG)[keyof typeof HITL_INTERACTION_CONFIG]["mode"];
|
|
15
|
-
// <<< CAIJUE GENERATED END
|
|
16
|
-
// >>> USER CODE >>>
|
|
15
|
+
// <<< CAIJUE GENERATED END <<<// >>> USER CODE >>>
|
|
17
16
|
// 在此添加自定义配置覆盖
|
|
18
17
|
// <<< USER CODE <<<
|
|
@@ -0,0 +1,129 @@
|
|
|
1
|
+
import type { RuntimeContextKey } from "./runtime-context.js"
|
|
2
|
+
import { assertLifecycleTransition, type PlanLifecycleStatus } from "./plan-lifecycle.js"
|
|
3
|
+
import {
|
|
4
|
+
createPlanLifecycleChangedEnvelope,
|
|
5
|
+
publishPlanLifecycleChanged,
|
|
6
|
+
type PrismaNotifyTransaction,
|
|
7
|
+
} from "./plan-lifecycle-events.js"
|
|
8
|
+
|
|
9
|
+
export type HitlDecisionStatus = "SUBMITTED" | "TONGYI" | "BOHUI"
|
|
10
|
+
export type HitlDecisionType = "PLAN" | "PLAN_REVIEW" | "COLLAB_CONTRACT"
|
|
11
|
+
|
|
12
|
+
export interface HitlMutationRow {
|
|
13
|
+
id: string
|
|
14
|
+
projectKey: string
|
|
15
|
+
adapterKey: string
|
|
16
|
+
planName: string
|
|
17
|
+
round: number
|
|
18
|
+
type: HitlDecisionType
|
|
19
|
+
status: "DRAFT" | HitlDecisionStatus
|
|
20
|
+
approvedAt: Date | null
|
|
21
|
+
rejectedAt: Date | null
|
|
22
|
+
rejectReason: string | null
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
export interface HitlPlanMutationRow {
|
|
26
|
+
id: string
|
|
27
|
+
projectKey: string
|
|
28
|
+
adapterKey: string
|
|
29
|
+
planName: string
|
|
30
|
+
lifecycle: PlanLifecycleStatus
|
|
31
|
+
revision: number
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
export interface HitlLifecycleTransaction extends PrismaNotifyTransaction {
|
|
35
|
+
hitlRecord: {
|
|
36
|
+
findFirst(args: { where: Record<string, unknown>; orderBy: Record<string, unknown> }): Promise<HitlMutationRow | null>
|
|
37
|
+
update(args: { where: Record<string, unknown>; data: Record<string, unknown> }): Promise<HitlMutationRow>
|
|
38
|
+
}
|
|
39
|
+
planRecord: {
|
|
40
|
+
findFirst(args: { where: Record<string, unknown> }): Promise<HitlPlanMutationRow | null>
|
|
41
|
+
update(args: { where: Record<string, unknown>; data: Record<string, unknown> }): Promise<HitlPlanMutationRow>
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
export interface HitlLifecycleDatabase {
|
|
46
|
+
$transaction<T>(work: (tx: HitlLifecycleTransaction) => Promise<T>): Promise<T>
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
export interface DecideHitlInput {
|
|
50
|
+
context: RuntimeContextKey
|
|
51
|
+
planName: string
|
|
52
|
+
type: HitlDecisionType
|
|
53
|
+
status: HitlDecisionStatus
|
|
54
|
+
reason?: string
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
export interface DecideHitlResult {
|
|
58
|
+
previousStatus: HitlMutationRow["status"]
|
|
59
|
+
hitl: HitlMutationRow
|
|
60
|
+
plan: HitlPlanMutationRow
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
function targetLifecycle(
|
|
64
|
+
type: HitlDecisionType,
|
|
65
|
+
status: HitlDecisionStatus,
|
|
66
|
+
current: PlanLifecycleStatus,
|
|
67
|
+
): PlanLifecycleStatus {
|
|
68
|
+
if (type !== "PLAN" || status === "SUBMITTED") return current
|
|
69
|
+
return status === "TONGYI" ? "ACTIVE" : "REJECTED"
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
/**
|
|
73
|
+
* 审批写入、Plan revision/lifecycle 与 NOTIFY 必须在同一 Prisma transaction。
|
|
74
|
+
* NOTIFY payload 只是唤醒提示;订阅端收到后必须再按自身 RuntimeContextKey 查库。
|
|
75
|
+
*/
|
|
76
|
+
export async function decideHitlAndPublish(
|
|
77
|
+
database: HitlLifecycleDatabase,
|
|
78
|
+
input: DecideHitlInput,
|
|
79
|
+
): Promise<DecideHitlResult> {
|
|
80
|
+
return database.$transaction(async (tx) => {
|
|
81
|
+
const scope = {
|
|
82
|
+
projectKey: input.context.projectKey,
|
|
83
|
+
adapterKey: input.context.adapterKey,
|
|
84
|
+
planName: input.planName,
|
|
85
|
+
}
|
|
86
|
+
const current = await tx.hitlRecord.findFirst({
|
|
87
|
+
where: { ...scope, type: input.type },
|
|
88
|
+
orderBy: { round: "desc" },
|
|
89
|
+
})
|
|
90
|
+
if (!current) throw new Error(`未找到当前 RuntimeContextKey 的 HITL: ${input.planName}/${input.type}`)
|
|
91
|
+
if (current.status === "TONGYI" || current.status === "BOHUI") {
|
|
92
|
+
throw new Error(`HITL 已终态(${current.status}),不可再次更新`)
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
const plan = await tx.planRecord.findFirst({ where: scope })
|
|
96
|
+
if (!plan) throw new Error(`未找到当前 RuntimeContextKey 的 PlanRecord: ${input.planName}`)
|
|
97
|
+
if (plan.projectKey !== input.context.projectKey || plan.adapterKey !== input.context.adapterKey) {
|
|
98
|
+
throw new Error("拒绝修改 scope 外 Plan")
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
const now = new Date()
|
|
102
|
+
const hitlData: Record<string, unknown> = { status: input.status }
|
|
103
|
+
if (input.status === "TONGYI") hitlData.approvedAt = now
|
|
104
|
+
if (input.status === "BOHUI") {
|
|
105
|
+
hitlData.rejectedAt = now
|
|
106
|
+
hitlData.rejectReason = input.reason ?? null
|
|
107
|
+
}
|
|
108
|
+
const hitl = await tx.hitlRecord.update({ where: { id: current.id }, data: hitlData })
|
|
109
|
+
|
|
110
|
+
const nextLifecycle = targetLifecycle(input.type, input.status, plan.lifecycle)
|
|
111
|
+
assertLifecycleTransition(plan.lifecycle, nextLifecycle)
|
|
112
|
+
const planData: Record<string, unknown> = { revision: { increment: 1 } }
|
|
113
|
+
if (nextLifecycle !== plan.lifecycle) planData.lifecycle = nextLifecycle
|
|
114
|
+
const updatedPlan = await tx.planRecord.update({
|
|
115
|
+
where: { id: plan.id, revision: plan.revision },
|
|
116
|
+
data: planData,
|
|
117
|
+
})
|
|
118
|
+
if (updatedPlan.revision !== plan.revision + 1) {
|
|
119
|
+
throw new Error(`Plan revision 未按预期递增: ${plan.revision} → ${updatedPlan.revision}`)
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
await publishPlanLifecycleChanged(tx, createPlanLifecycleChangedEnvelope({
|
|
123
|
+
context: input.context,
|
|
124
|
+
planId: updatedPlan.id,
|
|
125
|
+
revision: updatedPlan.revision,
|
|
126
|
+
}))
|
|
127
|
+
return { previousStatus: current.status, hitl, plan: updatedPlan }
|
|
128
|
+
})
|
|
129
|
+
}
|
|
@@ -0,0 +1,98 @@
|
|
|
1
|
+
import type { RuntimeContextKey } from "./runtime-context.js"
|
|
2
|
+
import type { PlanLifecycleStatus } from "./plan-lifecycle.js"
|
|
3
|
+
import {
|
|
4
|
+
createPlanLifecycleChangedEnvelope,
|
|
5
|
+
publishPlanLifecycleChanged,
|
|
6
|
+
type PrismaNotifyTransaction,
|
|
7
|
+
} from "./plan-lifecycle-events.js"
|
|
8
|
+
import type { HitlDecisionType } from "./hitl-lifecycle-mutation.js"
|
|
9
|
+
|
|
10
|
+
export interface HitlProposalPlanRow {
|
|
11
|
+
id: string
|
|
12
|
+
projectKey: string
|
|
13
|
+
adapterKey: string
|
|
14
|
+
planName: string
|
|
15
|
+
lifecycle: PlanLifecycleStatus
|
|
16
|
+
revision: number
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
export interface HitlProposalRow {
|
|
20
|
+
id: string
|
|
21
|
+
projectKey: string
|
|
22
|
+
adapterKey: string
|
|
23
|
+
planName: string
|
|
24
|
+
round: number
|
|
25
|
+
type: HitlDecisionType
|
|
26
|
+
status: "DRAFT"
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
export interface HitlProposalTransaction extends PrismaNotifyTransaction {
|
|
30
|
+
planRecord: {
|
|
31
|
+
findFirst(args: { where: Record<string, unknown> }): Promise<HitlProposalPlanRow | null>
|
|
32
|
+
create(args: { data: Record<string, unknown> }): Promise<HitlProposalPlanRow>
|
|
33
|
+
update(args: { where: Record<string, unknown>; data: Record<string, unknown> }): Promise<HitlProposalPlanRow>
|
|
34
|
+
}
|
|
35
|
+
hitlRecord: {
|
|
36
|
+
findFirst(args: { where: Record<string, unknown>; orderBy: Record<string, unknown> }): Promise<HitlProposalRow | null>
|
|
37
|
+
create(args: { data: Record<string, unknown> }): Promise<HitlProposalRow>
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
export interface HitlProposalDatabase {
|
|
42
|
+
$transaction<T>(work: (tx: HitlProposalTransaction) => Promise<T>): Promise<T>
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
/** Advisory xact lock makes max(round)+1 atomic per scoped plan/type. */
|
|
46
|
+
export async function createHitlProposalAndPublish(
|
|
47
|
+
database: HitlProposalDatabase,
|
|
48
|
+
input: {
|
|
49
|
+
context: RuntimeContextKey
|
|
50
|
+
planName: string
|
|
51
|
+
planPath: string
|
|
52
|
+
planKeyword: string
|
|
53
|
+
type: HitlDecisionType
|
|
54
|
+
},
|
|
55
|
+
): Promise<{ planProvisioned: boolean; plan: HitlProposalPlanRow; hitl: HitlProposalRow }> {
|
|
56
|
+
return database.$transaction(async (tx) => {
|
|
57
|
+
const lockKey = `${input.context.contextId}:${input.planName}:${input.type}`
|
|
58
|
+
await tx.$executeRaw`SELECT pg_advisory_xact_lock(hashtext(${lockKey}))`
|
|
59
|
+
const scope = {
|
|
60
|
+
projectKey: input.context.projectKey,
|
|
61
|
+
adapterKey: input.context.adapterKey,
|
|
62
|
+
planName: input.planName,
|
|
63
|
+
}
|
|
64
|
+
const existing = await tx.planRecord.findFirst({ where: scope })
|
|
65
|
+
const plan = existing
|
|
66
|
+
? await tx.planRecord.update({
|
|
67
|
+
where: { id: existing.id, revision: existing.revision },
|
|
68
|
+
data: { revision: { increment: 1 } },
|
|
69
|
+
})
|
|
70
|
+
: await tx.planRecord.create({
|
|
71
|
+
data: {
|
|
72
|
+
...scope,
|
|
73
|
+
planPath: input.planPath,
|
|
74
|
+
planKeyword: input.planKeyword,
|
|
75
|
+
lifecycle: "DRAFT",
|
|
76
|
+
revision: 0,
|
|
77
|
+
},
|
|
78
|
+
})
|
|
79
|
+
const latest = await tx.hitlRecord.findFirst({
|
|
80
|
+
where: { ...scope, type: input.type },
|
|
81
|
+
orderBy: { round: "desc" },
|
|
82
|
+
})
|
|
83
|
+
const hitl = await tx.hitlRecord.create({
|
|
84
|
+
data: {
|
|
85
|
+
...scope,
|
|
86
|
+
type: input.type,
|
|
87
|
+
round: (latest?.round ?? 0) + 1,
|
|
88
|
+
status: "DRAFT",
|
|
89
|
+
},
|
|
90
|
+
})
|
|
91
|
+
await publishPlanLifecycleChanged(tx, createPlanLifecycleChangedEnvelope({
|
|
92
|
+
context: input.context,
|
|
93
|
+
planId: plan.id,
|
|
94
|
+
revision: plan.revision,
|
|
95
|
+
}))
|
|
96
|
+
return { planProvisioned: existing === null, plan, hitl }
|
|
97
|
+
})
|
|
98
|
+
}
|