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.
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 +114 -33
  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 +123 -41
  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 +121 -40
  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 +122 -40
  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 +114 -33
  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 +122 -40
  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
@@ -1,9 +1,21 @@
1
1
  import type { McpServer } from "@modelcontextprotocol/server"
2
2
  import { join } from "path"
3
- import { existsSync, watch, readFileSync, statSync } from "fs"
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 = require("fs").openSync(JSONL_FILE, "r")
211
+ const fd = openSync(JSONL_FILE, "r")
195
212
  const buf = Buffer.alloc(st.size - q.bytesRead)
196
- require("fs").readSync(fd, buf, 0, buf.length, q.bytesRead)
197
- require("fs").closeSync(fd)
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: { action: "HOOK_INTERCEPT", createdAt: { gte: since } },
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: { action: "HOOK_INTERCEPT", createdAt: { gte: since } },
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
- * 流程: 读取 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"