add-coder 0.3.26 → 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 +112 -31
- 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 +121 -39
- 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 +119 -38
- 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 +120 -38
- 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 +112 -31
- 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 +120 -38
- 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
|
@@ -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
|
+
}
|
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
import { randomUUID } from "crypto"
|
|
2
|
+
import * as z from "zod/v4"
|
|
3
|
+
import type { RuntimeContextKey } from "./runtime-context.js"
|
|
4
|
+
|
|
5
|
+
export const PLAN_LIFECYCLE_CHANNEL = "add_plan_lifecycle_changed_v1"
|
|
6
|
+
|
|
7
|
+
export const PlanLifecycleChangedEnvelopeSchema = z.object({
|
|
8
|
+
schemaVersion: z.literal(1),
|
|
9
|
+
eventId: z.string().min(1),
|
|
10
|
+
projectKey: z.string().min(1),
|
|
11
|
+
adapterKey: z.string().min(1),
|
|
12
|
+
planId: z.string().min(1),
|
|
13
|
+
revision: z.number().int().nonnegative(),
|
|
14
|
+
})
|
|
15
|
+
|
|
16
|
+
export type PlanLifecycleChangedEnvelope = z.infer<typeof PlanLifecycleChangedEnvelopeSchema>
|
|
17
|
+
|
|
18
|
+
export interface PrismaNotifyTransaction {
|
|
19
|
+
$executeRaw(query: TemplateStringsArray, ...values: unknown[]): Promise<number>
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
export function createPlanLifecycleChangedEnvelope(input: {
|
|
23
|
+
context: RuntimeContextKey
|
|
24
|
+
planId: string
|
|
25
|
+
revision: number
|
|
26
|
+
eventId?: string
|
|
27
|
+
}): PlanLifecycleChangedEnvelope {
|
|
28
|
+
return {
|
|
29
|
+
schemaVersion: 1,
|
|
30
|
+
eventId: input.eventId ?? randomUUID(),
|
|
31
|
+
projectKey: input.context.projectKey,
|
|
32
|
+
adapterKey: input.context.adapterKey,
|
|
33
|
+
planId: input.planId,
|
|
34
|
+
revision: input.revision,
|
|
35
|
+
}
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
export function parsePlanLifecycleChangedEnvelope(payload: string | undefined): PlanLifecycleChangedEnvelope | null {
|
|
39
|
+
if (!payload) return null
|
|
40
|
+
try {
|
|
41
|
+
const parsed: unknown = JSON.parse(payload)
|
|
42
|
+
const result = PlanLifecycleChangedEnvelopeSchema.safeParse(parsed)
|
|
43
|
+
return result.success ? result.data : null
|
|
44
|
+
} catch {
|
|
45
|
+
return null
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
export function isLifecycleEventForContext(
|
|
50
|
+
envelope: PlanLifecycleChangedEnvelope,
|
|
51
|
+
context: RuntimeContextKey,
|
|
52
|
+
): boolean {
|
|
53
|
+
return envelope.projectKey === context.projectKey && envelope.adapterKey === context.adapterKey
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
export async function publishPlanLifecycleChanged(
|
|
57
|
+
tx: PrismaNotifyTransaction,
|
|
58
|
+
envelope: PlanLifecycleChangedEnvelope,
|
|
59
|
+
): Promise<void> {
|
|
60
|
+
const payload = JSON.stringify(envelope)
|
|
61
|
+
await tx.$executeRaw`SELECT pg_notify(${PLAN_LIFECYCLE_CHANNEL}, ${payload})`
|
|
62
|
+
}
|
|
@@ -0,0 +1,78 @@
|
|
|
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 interface PlanLifecycleMutationRow {
|
|
10
|
+
id: string
|
|
11
|
+
projectKey: string
|
|
12
|
+
adapterKey: string
|
|
13
|
+
planName: string
|
|
14
|
+
lifecycle: PlanLifecycleStatus
|
|
15
|
+
revision: number
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
export interface PlanLifecycleTransaction extends PrismaNotifyTransaction {
|
|
19
|
+
planRecord: {
|
|
20
|
+
findFirst(args: { where: Record<string, unknown> }): Promise<PlanLifecycleMutationRow | null>
|
|
21
|
+
update(args: {
|
|
22
|
+
where: Record<string, unknown>
|
|
23
|
+
data: { lifecycle: PlanLifecycleStatus; revision: { increment: 1 } }
|
|
24
|
+
}): Promise<PlanLifecycleMutationRow>
|
|
25
|
+
}
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
export interface PlanLifecycleDatabase {
|
|
29
|
+
$transaction<T>(work: (tx: PlanLifecycleTransaction) => Promise<T>): Promise<T>
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
export interface TransitionPlanLifecycleInput {
|
|
33
|
+
context: RuntimeContextKey
|
|
34
|
+
planName: string
|
|
35
|
+
to: PlanLifecycleStatus
|
|
36
|
+
expectedRevision?: number
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
export async function transitionPlanLifecycle(
|
|
40
|
+
database: PlanLifecycleDatabase,
|
|
41
|
+
input: TransitionPlanLifecycleInput,
|
|
42
|
+
): Promise<PlanLifecycleMutationRow> {
|
|
43
|
+
return database.$transaction(async (tx) => {
|
|
44
|
+
const current = await tx.planRecord.findFirst({
|
|
45
|
+
where: {
|
|
46
|
+
projectKey: input.context.projectKey,
|
|
47
|
+
adapterKey: input.context.adapterKey,
|
|
48
|
+
planName: input.planName,
|
|
49
|
+
},
|
|
50
|
+
})
|
|
51
|
+
if (!current) {
|
|
52
|
+
throw new Error(`当前 RuntimeContextKey 下不存在 Plan: ${input.planName}`)
|
|
53
|
+
}
|
|
54
|
+
if (current.projectKey !== input.context.projectKey || current.adapterKey !== input.context.adapterKey) {
|
|
55
|
+
throw new Error("拒绝迁移 scope 外 Plan lifecycle")
|
|
56
|
+
}
|
|
57
|
+
if (input.expectedRevision !== undefined && current.revision !== input.expectedRevision) {
|
|
58
|
+
throw new Error(`Plan lifecycle revision 冲突: expected=${input.expectedRevision}, actual=${current.revision}`)
|
|
59
|
+
}
|
|
60
|
+
assertLifecycleTransition(current.lifecycle, input.to)
|
|
61
|
+
if (current.lifecycle === input.to) return current
|
|
62
|
+
|
|
63
|
+
const updated = await tx.planRecord.update({
|
|
64
|
+
where: { id: current.id, revision: current.revision },
|
|
65
|
+
data: { lifecycle: input.to, revision: { increment: 1 } },
|
|
66
|
+
})
|
|
67
|
+
if (updated.revision !== current.revision + 1) {
|
|
68
|
+
throw new Error(`Plan lifecycle revision 未按预期递增: ${current.revision} → ${updated.revision}`)
|
|
69
|
+
}
|
|
70
|
+
const envelope = createPlanLifecycleChangedEnvelope({
|
|
71
|
+
context: input.context,
|
|
72
|
+
planId: updated.id,
|
|
73
|
+
revision: updated.revision,
|
|
74
|
+
})
|
|
75
|
+
await publishPlanLifecycleChanged(tx, envelope)
|
|
76
|
+
return updated
|
|
77
|
+
})
|
|
78
|
+
}
|