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.
Files changed (132) hide show
  1. package/README.md +36 -10
  2. package/dist/index.js +481 -209
  3. package/package.json +14 -4
  4. package/templates/.add-coder-src-hash.json +129 -106
  5. package/templates/adapters/claude/hooks/doc-format-guard.sh +112 -31
  6. package/templates/adapters/claude/hooks/lib/common.sh +46 -87
  7. package/templates/adapters/claude/hooks/lib/context-inject.sh +2 -2
  8. package/templates/adapters/claude/hooks/lib/notify.sh +6 -4
  9. package/templates/adapters/claude/hooks/lib/preload-templates.sh +27 -2
  10. package/templates/adapters/claude/hooks/lib/state-detect.sh +8 -112
  11. package/templates/adapters/claude/hooks/lib/vocabulary.sh +13 -7
  12. package/templates/adapters/claude/hooks/prompt-submit.sh +1 -1
  13. package/templates/adapters/claude/hooks/stop-check.sh +9 -0
  14. package/templates/adapters/codex/config.toml.example +12 -0
  15. package/templates/adapters/codex/hooks/doc-format-guard.sh +121 -39
  16. package/templates/adapters/codex/hooks/lib/common.sh +47 -99
  17. package/templates/adapters/codex/hooks/lib/context-inject.sh +4 -4
  18. package/templates/adapters/codex/hooks/lib/notify.sh +6 -4
  19. package/templates/adapters/codex/hooks/lib/preload-templates.sh +27 -6
  20. package/templates/adapters/codex/hooks/lib/state-detect.sh +5 -114
  21. package/templates/adapters/codex/hooks/lib/vocabulary.sh +3 -8
  22. package/templates/adapters/codex/hooks/notification.sh +3 -18
  23. package/templates/adapters/codex/hooks/post-tool-use.sh +24 -119
  24. package/templates/adapters/codex/hooks/pre-tool-use.sh +97 -184
  25. package/templates/adapters/codex/hooks/prompt-submit.sh +8 -3
  26. package/templates/adapters/codex/hooks/session-start.sh +19 -9
  27. package/templates/adapters/codex/hooks/stop-check.sh +44 -18
  28. package/templates/adapters/codex/hooks.json +67 -1
  29. package/templates/adapters/qoder/hooks/doc-format-guard.sh +119 -38
  30. package/templates/adapters/qoder/hooks/lib/common.sh +49 -87
  31. package/templates/adapters/qoder/hooks/lib/context-inject.sh +2 -2
  32. package/templates/adapters/qoder/hooks/lib/notify.sh +6 -4
  33. package/templates/adapters/qoder/hooks/lib/preload-templates.sh +27 -2
  34. package/templates/adapters/qoder/hooks/lib/state-detect.sh +8 -112
  35. package/templates/adapters/qoder/hooks/lib/vocabulary.sh +13 -7
  36. package/templates/adapters/qoder/hooks/prompt-submit.sh +1 -1
  37. package/templates/adapters/qoder/hooks/session-start.sh +1 -1
  38. package/templates/adapters/qoder/hooks/stop-check.sh +11 -0
  39. package/templates/adapters/trae/hooks/doc-format-guard.sh +120 -38
  40. package/templates/adapters/trae/hooks/lib/common.sh +46 -87
  41. package/templates/adapters/trae/hooks/lib/context-inject.sh +2 -2
  42. package/templates/adapters/trae/hooks/lib/notify.sh +5 -3
  43. package/templates/adapters/trae/hooks/lib/preload-templates.sh +27 -2
  44. package/templates/adapters/trae/hooks/lib/state-detect.sh +8 -115
  45. package/templates/adapters/trae/hooks/lib/vocabulary.sh +13 -7
  46. package/templates/adapters/trae/hooks/pre-tool-use.sh +14 -8
  47. package/templates/adapters/trae/hooks/stop-check.sh +9 -0
  48. package/templates/adapters/vscode/hooks/doc-format-guard.sh +112 -31
  49. package/templates/adapters/vscode/hooks/lib/common.sh +46 -87
  50. package/templates/adapters/vscode/hooks/lib/context-inject.sh +2 -2
  51. package/templates/adapters/vscode/hooks/lib/notify.sh +6 -4
  52. package/templates/adapters/vscode/hooks/lib/preload-templates.sh +27 -2
  53. package/templates/adapters/vscode/hooks/lib/state-detect.sh +8 -112
  54. package/templates/adapters/vscode/hooks/lib/vocabulary.sh +13 -7
  55. package/templates/adapters/vscode/hooks/notification.sh +3 -5
  56. package/templates/adapters/vscode/hooks/prompt-submit.sh +1 -1
  57. package/templates/adapters/vscode/hooks/stop-check.sh +9 -0
  58. package/templates/core/docs/ADD-governance-claude-code.md +1 -1
  59. package/templates/core/docs/ADD-governance-codex.md +1 -1
  60. package/templates/core/docs/ADD-governance-qoder-cn.md +1 -1
  61. package/templates/core/docs/ADD-governance-trae.md +1 -1
  62. package/templates/core/docs/ADD-governance-vscode-copilot.md +1 -1
  63. package/templates/core/hooks/doc-format-guard.sh +120 -38
  64. package/templates/core/hooks/lib/common.sh +46 -87
  65. package/templates/core/hooks/lib/context-inject.sh +2 -2
  66. package/templates/core/hooks/lib/notify.sh +5 -3
  67. package/templates/core/hooks/lib/preload-templates.sh +27 -2
  68. package/templates/core/hooks/lib/state-detect.sh +8 -115
  69. package/templates/core/hooks/lib/vocabulary.sh +13 -7
  70. package/templates/core/hooks/pre-tool-use.sh +14 -8
  71. package/templates/core/hooks/stop-check.sh +9 -0
  72. package/templates/core/prisma/add.prisma +140 -82
  73. package/templates/core/rules/project_rules.md +27 -24
  74. package/templates/core/scripts/db-ensure.sh +8 -0
  75. package/templates/core/scripts/gen-plan-index.sh +4 -15
  76. package/templates/core/scripts/mcp-server/notifications/hitl.ts +9 -1
  77. package/templates/core/scripts/mcp-server/notifications/hook.ts +25 -9
  78. package/templates/core/scripts/mcp-server/resources/hitl-approval-widget.ts +34 -0
  79. package/templates/core/scripts/mcp-server/resources/hook-events-report.ts +14 -2
  80. package/templates/core/scripts/mcp-server/resources/index.ts +2 -0
  81. package/templates/core/scripts/mcp-server/sampling/review.ts +25 -10
  82. package/templates/core/scripts/mcp-server/shared/db-types.ts +33 -8
  83. package/templates/core/scripts/mcp-server/shared/dps-scoring.strategy.ts +1 -1
  84. package/templates/core/scripts/mcp-server/shared/env.ts +28 -8
  85. package/templates/core/scripts/mcp-server/shared/fs.ts +3 -0
  86. package/templates/core/scripts/mcp-server/shared/hitl-interaction.strategy.ts +2 -3
  87. package/templates/core/scripts/mcp-server/shared/hitl-lifecycle-mutation.ts +129 -0
  88. package/templates/core/scripts/mcp-server/shared/hitl-proposal-mutation.ts +98 -0
  89. package/templates/core/scripts/mcp-server/shared/hitl-ui.ts +3 -0
  90. package/templates/core/scripts/mcp-server/shared/plan-lifecycle-events.ts +62 -0
  91. package/templates/core/scripts/mcp-server/shared/plan-lifecycle-mutation.ts +78 -0
  92. package/templates/core/scripts/mcp-server/shared/plan-lifecycle-subscriber.ts +145 -0
  93. package/templates/core/scripts/mcp-server/shared/plan-lifecycle.ts +153 -0
  94. package/templates/core/scripts/mcp-server/shared/plan-round-events.ts +56 -0
  95. package/templates/core/scripts/mcp-server/shared/plan-round-mutation.ts +143 -0
  96. package/templates/core/scripts/mcp-server/shared/plan-round-store.ts +47 -0
  97. package/templates/core/scripts/mcp-server/shared/plan-round-subscriber.ts +99 -0
  98. package/templates/core/scripts/mcp-server/shared/plan-status-store.ts +37 -0
  99. package/templates/core/scripts/mcp-server/shared/plan-tracking-mutation.ts +71 -0
  100. package/templates/core/scripts/mcp-server/shared/prisma.ts +12 -6
  101. package/templates/core/scripts/mcp-server/shared/project-root-strategy.ts +1 -2
  102. package/templates/core/scripts/mcp-server/shared/quant-recheck.strategy.ts +16 -0
  103. package/templates/core/scripts/mcp-server/shared/redact.ts +28 -0
  104. package/templates/core/scripts/mcp-server/shared/response.ts +3 -2
  105. package/templates/core/scripts/mcp-server/shared/runtime-context.ts +77 -0
  106. package/templates/core/scripts/mcp-server/shared/schema-topology.ts +159 -0
  107. package/templates/core/scripts/mcp-server/tools/audit.ts +49 -7
  108. package/templates/core/scripts/mcp-server/tools/context.ts +117 -121
  109. package/templates/core/scripts/mcp-server/tools/contract.ts +34 -9
  110. package/templates/core/scripts/mcp-server/tools/gateway/check_add_route_status.ts +50 -3
  111. package/templates/core/scripts/mcp-server/tools/gateway/check_doc_similarity.ts +218 -0
  112. package/templates/core/scripts/mcp-server/tools/gateway/check_dps.ts +22 -7
  113. package/templates/core/scripts/mcp-server/tools/gateway/check_rahs.ts +75 -4
  114. package/templates/core/scripts/mcp-server/tools/gateway/check_spec_sync.ts +19 -6
  115. package/templates/core/scripts/mcp-server/tools/gateway/index.ts +2 -0
  116. package/templates/core/scripts/mcp-server/tools/hitl.ts +160 -75
  117. package/templates/core/scripts/mcp-server/tools/hook-event-report.ts +9 -2
  118. package/templates/core/scripts/mcp-server/tools/index.ts +69 -3
  119. package/templates/core/scripts/mcp-server/tools/plan.ts +140 -15
  120. package/templates/core/scripts/mcp-server/tools/review.ts +44 -15
  121. package/templates/core/scripts/mcp-server.ts +60 -1
  122. package/templates/core/scripts/plan-status-bridge.ts +26 -0
  123. package/templates/core/skills/add-paradigm/SKILL.md +2 -2
  124. package/templates/core/templates/add-route-template.schema.json +24 -56
  125. package/templates/core/templates/collab-contract-template.md +1 -1
  126. package/templates/core/templates/handoff-multi-round-template.schema.json +48 -0
  127. package/templates/core/templates/handoff-single-round-template.schema.json +92 -0
  128. package/templates/core/templates/handoff-single-round.schema.json +1 -1
  129. package/templates/core/templates/hitl-approval-widget.html +222 -37
  130. package/templates/core/templates/spec-template.schema.json +9 -46
  131. package/templates/core/templates/standard-plan-template.schema.json +1 -1
  132. 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
- * 流程: 读取 Review 模板生成 HITL 发现总览 temporary.md人类拍板 → 写入正式 Review
9
+ * 方案/实现 Review: 读取模板整理 dimensions create_hitlupdate_hitlstatus_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 hitlGuide = `
23
- ## HITL 审核流程(两步法)
27
+ const workflowGuide = requiresHitl ? `
28
+ ## HITL 审核流程
24
29
 
25
- 1. 先写 {review-name}.temporary.md(只含 HITL 发现总览表 + 问题清单)
26
- 2. 人类拍板后 生成完整 Review 写入 ${MAGIC_DIR}/reviews/
27
- 3. Review 结论回流至 Plan(Step 0.6.5)
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}" 发起 HITL Review(类型: ${reviewType})。
34
- 先读取 ${MAGIC_DIR}/reviews/ 下已有的 Review 模板,
35
- 然后按 HITL 两步法:先写 temporary.md → 人类拍板 → 生成完整 Review。
49
+ const prompt = `请为 Plan "${planKeyword}" 生成 Review(类型: ${reviewType})。
50
+ 先读取 ${MAGIC_DIR}/templates/${templateFile},再严格执行下述流程。
36
51
 
37
- ${hitlGuide}`
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 as never)
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 as never)
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 as never)
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 as never)) },
203
- async update(args) { return parseRow(await raw.update(args as never)) },
204
- async updateMany(args) { return raw.updateMany(args as never) as Promise<BatchResult> },
205
- async delete(args) { return parseRow(await raw.delete(args as never)) },
206
- async deleteMany(args) { return raw.deleteMany(args as never) as Promise<BatchResult> },
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
  }
@@ -30,4 +30,4 @@ export const DPS_SCORING_CONFIG = {
30
30
  THRESHOLD_WARN: 65,
31
31
  EMBEDDING_MODEL: "Xenova/bge-small-zh-v1.5",
32
32
  } as const;
33
- // <<< CAIJUE GENERATED END <<<
33
+ // <<< CAIJUE GENERATED END <<<
@@ -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 __filename = fileURLToPath(import.meta.url)
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
- export const MAGIC_DIR = process.env.MAGIC_DIR || basename(resolve(__dirname, "..", "..", ".."))
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: "inputRequired" as const }
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,3 @@
1
+ export const HITL_APPROVAL_WIDGET_URI = "ui://add-coder/hitl-approval"
2
+ export const HITL_APPROVAL_WIDGET_MIME = "text/html;profile=mcp-app"
3
+ export const HITL_APPROVAL_WIDGET_FILE = "hitl-approval-widget.html"
@@ -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
+ }