add-coder 0.3.26 → 0.3.28

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 (323) hide show
  1. package/README.md +282 -155
  2. package/dist/index.js +481 -209
  3. package/package.json +15 -4
  4. package/templates/.add-coder-src-hash.json +186 -185
  5. package/templates/adapters/claude/hooks/doc-format-guard.ts +32 -0
  6. package/templates/adapters/claude/hooks/lib/claude-env.ts +7 -0
  7. package/templates/adapters/claude/hooks/notification.ts +24 -0
  8. package/templates/adapters/claude/hooks/permission-denied.ts +33 -0
  9. package/templates/adapters/claude/hooks/permission-gate.ts +12 -0
  10. package/templates/adapters/claude/hooks/post-tool-failure.ts +4 -0
  11. package/templates/adapters/claude/hooks/post-tool-use.ts +17 -0
  12. package/templates/adapters/claude/hooks/pre-compact.ts +10 -0
  13. package/templates/adapters/claude/hooks/pre-tool-use.ts +81 -0
  14. package/templates/adapters/claude/hooks/prompt-submit.ts +82 -0
  15. package/templates/adapters/claude/hooks/review-checklist.ts +4 -0
  16. package/templates/adapters/claude/hooks/session-end.ts +10 -0
  17. package/templates/adapters/claude/hooks/session-start.ts +24 -0
  18. package/templates/adapters/claude/hooks/stop-check.ts +15 -0
  19. package/templates/adapters/claude/hooks/stop-failure.ts +34 -0
  20. package/templates/adapters/claude/hooks/subagent-guard.ts +25 -0
  21. package/templates/adapters/claude/hooks/subagent-stop.ts +12 -0
  22. package/templates/adapters/claude/settings.json +13 -13
  23. package/templates/adapters/codex/config.toml.example +12 -0
  24. package/templates/adapters/codex/hooks/doc-format-guard.ts +30 -0
  25. package/templates/adapters/codex/hooks/notification.ts +26 -0
  26. package/templates/adapters/codex/hooks/permission-gate.ts +15 -0
  27. package/templates/adapters/codex/hooks/post-tool-failure.ts +14 -0
  28. package/templates/adapters/codex/hooks/post-tool-use.ts +85 -0
  29. package/templates/adapters/codex/hooks/pre-compact.ts +20 -0
  30. package/templates/adapters/codex/hooks/pre-tool-use.ts +141 -0
  31. package/templates/adapters/codex/hooks/prompt-submit.ts +38 -0
  32. package/templates/adapters/codex/hooks/review-checklist.ts +5 -0
  33. package/templates/adapters/codex/hooks/session-end.ts +19 -0
  34. package/templates/adapters/codex/hooks/session-start.ts +45 -0
  35. package/templates/adapters/codex/hooks/stop-check.ts +59 -0
  36. package/templates/adapters/codex/hooks/subagent-guard.ts +22 -0
  37. package/templates/adapters/codex/hooks/subagent-stop.ts +20 -0
  38. package/templates/adapters/codex/hooks.json +67 -1
  39. package/templates/adapters/qoder/hooks/doc-format-guard.ts +34 -0
  40. package/templates/adapters/qoder/hooks/lib/context-inject.ts +61 -0
  41. package/templates/adapters/qoder/hooks/lib/qoder-env.ts +23 -0
  42. package/templates/adapters/qoder/hooks/lib/review-checklist.ts +5 -0
  43. package/templates/adapters/qoder/hooks/notification.ts +34 -0
  44. package/templates/adapters/qoder/hooks/permission-gate.ts +26 -0
  45. package/templates/adapters/qoder/hooks/post-tool-failure.ts +30 -0
  46. package/templates/adapters/qoder/hooks/post-tool-use.ts +32 -0
  47. package/templates/adapters/qoder/hooks/pre-compact.ts +16 -0
  48. package/templates/adapters/qoder/hooks/pre-tool-use.ts +112 -0
  49. package/templates/adapters/qoder/hooks/prompt-submit.ts +66 -0
  50. package/templates/adapters/qoder/hooks/review-checklist.ts +24 -0
  51. package/templates/adapters/qoder/hooks/session-end.ts +27 -0
  52. package/templates/adapters/qoder/hooks/session-start.ts +83 -0
  53. package/templates/adapters/qoder/hooks/stop-check.ts +69 -0
  54. package/templates/adapters/qoder/hooks/subagent-guard.ts +16 -0
  55. package/templates/adapters/qoder/hooks/subagent-stop.ts +33 -0
  56. package/templates/adapters/qoder/settings.json +13 -13
  57. package/templates/adapters/trae/hooks/doc-format-guard.ts +30 -0
  58. package/templates/adapters/trae/hooks/notification.ts +22 -0
  59. package/templates/adapters/trae/hooks/permission-gate.ts +15 -0
  60. package/templates/adapters/trae/hooks/post-tool-failure.ts +14 -0
  61. package/templates/adapters/trae/hooks/post-tool-use.ts +24 -0
  62. package/templates/adapters/trae/hooks/pre-compact.ts +15 -0
  63. package/templates/adapters/trae/hooks/pre-tool-use.ts +40 -0
  64. package/templates/adapters/trae/hooks/prompt-submit.ts +29 -0
  65. package/templates/adapters/trae/hooks/review-checklist.ts +5 -0
  66. package/templates/adapters/trae/hooks/session-end.ts +16 -0
  67. package/templates/adapters/trae/hooks/session-start.ts +15 -0
  68. package/templates/adapters/trae/hooks/stop-check.ts +20 -0
  69. package/templates/adapters/trae/hooks/subagent-guard.ts +22 -0
  70. package/templates/adapters/trae/hooks/subagent-stop.ts +15 -0
  71. package/templates/adapters/trae/hooks.json +1 -1
  72. package/templates/adapters/vscode/hooks/doc-format-guard.ts +27 -0
  73. package/templates/adapters/vscode/hooks/notification.ts +22 -0
  74. package/templates/adapters/vscode/hooks/permission-gate.ts +15 -0
  75. package/templates/adapters/vscode/hooks/post-tool-failure.ts +32 -0
  76. package/templates/adapters/vscode/hooks/post-tool-use.ts +25 -0
  77. package/templates/adapters/vscode/hooks/pre-compact.ts +15 -0
  78. package/templates/adapters/vscode/hooks/pre-tool-use.ts +78 -0
  79. package/templates/adapters/vscode/hooks/prompt-submit.ts +74 -0
  80. package/templates/adapters/vscode/hooks/review-checklist.ts +5 -0
  81. package/templates/adapters/vscode/hooks/session-end.ts +16 -0
  82. package/templates/adapters/vscode/hooks/session-start.ts +24 -0
  83. package/templates/adapters/vscode/hooks/stop-check.ts +21 -0
  84. package/templates/adapters/vscode/hooks/subagent-guard.ts +21 -0
  85. package/templates/adapters/vscode/hooks/subagent-stop.ts +15 -0
  86. package/templates/core/docs/ADD-governance-claude-code.md +4 -4
  87. package/templates/core/docs/ADD-governance-codex.md +6 -6
  88. package/templates/core/docs/ADD-governance-qoder-cn.md +5 -5
  89. package/templates/core/docs/ADD-governance-trae.md +3 -3
  90. package/templates/core/docs/ADD-governance-vscode-copilot.md +22 -23
  91. package/templates/core/governance/audit-bridge.ts +47 -0
  92. package/templates/core/governance/common.ts +299 -0
  93. package/templates/core/governance/context-inject.ts +53 -0
  94. package/templates/core/governance/doc-format-guard.ts +440 -0
  95. package/templates/core/governance/notification-router.ts +62 -0
  96. package/templates/core/governance/notify.ts +66 -0
  97. package/templates/core/governance/permission-gate-router.ts +42 -0
  98. package/templates/core/governance/post-tool-failure-router.ts +56 -0
  99. package/templates/core/governance/post-tool-router.ts +212 -0
  100. package/templates/core/governance/pre-compact-guard.ts +41 -0
  101. package/templates/core/governance/pre-tool-guard.ts +309 -0
  102. package/templates/core/governance/preload-templates.ts +241 -0
  103. package/templates/core/governance/prompt-router.ts +222 -0
  104. package/templates/core/governance/review-checklist-guard.ts +186 -0
  105. package/templates/core/governance/rules.ts +720 -0
  106. package/templates/core/governance/session-end.ts +88 -0
  107. package/templates/core/governance/session-start-guard.ts +99 -0
  108. package/templates/core/governance/state-detect.ts +6 -0
  109. package/templates/core/governance/stop-router.ts +140 -0
  110. package/templates/core/governance/subagent-guard-router.ts +35 -0
  111. package/templates/core/governance/subagent-stop-router.ts +93 -0
  112. package/templates/core/governance/subagent-stop.ts +75 -0
  113. package/templates/core/governance/vocabulary.ts +109 -0
  114. package/templates/core/hooks/doc-format-guard.ts +21 -0
  115. package/templates/core/hooks/notification.ts +11 -0
  116. package/templates/core/hooks/permission-gate.ts +7 -0
  117. package/templates/core/hooks/post-tool-failure.ts +7 -0
  118. package/templates/core/hooks/post-tool-use.ts +23 -0
  119. package/templates/core/hooks/pre-compact.ts +9 -0
  120. package/templates/core/hooks/pre-tool-use.ts +30 -0
  121. package/templates/core/hooks/prompt-submit.ts +11 -0
  122. package/templates/core/hooks/review-checklist.ts +4 -0
  123. package/templates/core/hooks/session-end.ts +10 -0
  124. package/templates/core/hooks/session-start.ts +9 -0
  125. package/templates/core/hooks/stop-check.ts +14 -0
  126. package/templates/core/hooks/subagent-guard.ts +8 -0
  127. package/templates/core/hooks/subagent-stop.ts +9 -0
  128. package/templates/core/prisma/add.prisma +140 -82
  129. package/templates/core/rules/project_rules.md +27 -24
  130. package/templates/core/scripts/db-ensure.sh +8 -0
  131. package/templates/core/scripts/gen-plan-index.sh +4 -15
  132. package/templates/core/scripts/mcp-server/notifications/hitl.ts +9 -1
  133. package/templates/core/scripts/mcp-server/notifications/hook.ts +25 -9
  134. package/templates/core/scripts/mcp-server/resources/hitl-approval-widget.ts +34 -0
  135. package/templates/core/scripts/mcp-server/resources/hook-events-report.ts +14 -2
  136. package/templates/core/scripts/mcp-server/resources/index.ts +2 -0
  137. package/templates/core/scripts/mcp-server/sampling/review.ts +25 -10
  138. package/templates/core/scripts/mcp-server/shared/db-types.ts +33 -8
  139. package/templates/core/scripts/mcp-server/shared/dps-scoring.strategy.ts +1 -1
  140. package/templates/core/scripts/mcp-server/shared/env.ts +28 -8
  141. package/templates/core/scripts/mcp-server/shared/fs.ts +3 -0
  142. package/templates/core/scripts/mcp-server/shared/hitl-interaction.strategy.ts +2 -3
  143. package/templates/core/scripts/mcp-server/shared/hitl-lifecycle-mutation.ts +129 -0
  144. package/templates/core/scripts/mcp-server/shared/hitl-proposal-mutation.ts +98 -0
  145. package/templates/core/scripts/mcp-server/shared/hitl-ui.ts +3 -0
  146. package/templates/core/scripts/mcp-server/shared/plan-lifecycle-events.ts +62 -0
  147. package/templates/core/scripts/mcp-server/shared/plan-lifecycle-mutation.ts +78 -0
  148. package/templates/core/scripts/mcp-server/shared/plan-lifecycle-subscriber.ts +145 -0
  149. package/templates/core/scripts/mcp-server/shared/plan-lifecycle.ts +153 -0
  150. package/templates/core/scripts/mcp-server/shared/plan-round-events.ts +56 -0
  151. package/templates/core/scripts/mcp-server/shared/plan-round-mutation.ts +143 -0
  152. package/templates/core/scripts/mcp-server/shared/plan-round-store.ts +47 -0
  153. package/templates/core/scripts/mcp-server/shared/plan-round-subscriber.ts +99 -0
  154. package/templates/core/scripts/mcp-server/shared/plan-status-store.ts +37 -0
  155. package/templates/core/scripts/mcp-server/shared/plan-tracking-mutation.ts +71 -0
  156. package/templates/core/scripts/mcp-server/shared/prisma.ts +12 -6
  157. package/templates/core/scripts/mcp-server/shared/project-root-strategy.ts +1 -2
  158. package/templates/core/scripts/mcp-server/shared/quant-recheck.strategy.ts +16 -0
  159. package/templates/core/scripts/mcp-server/shared/redact.ts +28 -0
  160. package/templates/core/scripts/mcp-server/shared/response.ts +3 -2
  161. package/templates/core/scripts/mcp-server/shared/runtime-context.ts +77 -0
  162. package/templates/core/scripts/mcp-server/shared/schema-topology.ts +159 -0
  163. package/templates/core/scripts/mcp-server/tools/audit.ts +49 -7
  164. package/templates/core/scripts/mcp-server/tools/context.ts +117 -121
  165. package/templates/core/scripts/mcp-server/tools/contract.ts +34 -9
  166. package/templates/core/scripts/mcp-server/tools/gateway/check_add_route_status.ts +50 -3
  167. package/templates/core/scripts/mcp-server/tools/gateway/check_doc_similarity.ts +218 -0
  168. package/templates/core/scripts/mcp-server/tools/gateway/check_dps.ts +22 -7
  169. package/templates/core/scripts/mcp-server/tools/gateway/check_rahs.ts +86 -4
  170. package/templates/core/scripts/mcp-server/tools/gateway/check_spec_sync.ts +19 -6
  171. package/templates/core/scripts/mcp-server/tools/gateway/index.ts +2 -0
  172. package/templates/core/scripts/mcp-server/tools/hitl.ts +160 -75
  173. package/templates/core/scripts/mcp-server/tools/hook-event-report.ts +9 -2
  174. package/templates/core/scripts/mcp-server/tools/index.ts +69 -3
  175. package/templates/core/scripts/mcp-server/tools/plan.ts +152 -17
  176. package/templates/core/scripts/mcp-server/tools/review.ts +44 -15
  177. package/templates/core/scripts/mcp-server.ts +60 -1
  178. package/templates/core/scripts/plan-status-bridge.ts +26 -0
  179. package/templates/core/skills/add-paradigm/SKILL.md +2 -2
  180. package/templates/core/templates/add-route-template.schema.json +24 -56
  181. package/templates/core/templates/collab-contract-template.md +1 -1
  182. package/templates/core/templates/handoff-multi-round-template.schema.json +48 -0
  183. package/templates/core/templates/handoff-single-round-template.schema.json +92 -0
  184. package/templates/core/templates/handoff-single-round.schema.json +1 -1
  185. package/templates/core/templates/hitl-approval-widget.html +222 -37
  186. package/templates/core/templates/spec-template.schema.json +9 -46
  187. package/templates/core/templates/standard-plan-template.schema.json +1 -1
  188. package/templates/core/templates/tasks-template.schema.json +13 -54
  189. package/templates/adapters/claude/hooks/doc-format-guard.sh +0 -202
  190. package/templates/adapters/claude/hooks/lib/common.sh +0 -261
  191. package/templates/adapters/claude/hooks/lib/context-inject.sh +0 -96
  192. package/templates/adapters/claude/hooks/lib/notify.sh +0 -34
  193. package/templates/adapters/claude/hooks/lib/preload-templates.sh +0 -187
  194. package/templates/adapters/claude/hooks/lib/session-end.sh +0 -76
  195. package/templates/adapters/claude/hooks/lib/state-detect.sh +0 -117
  196. package/templates/adapters/claude/hooks/lib/subagent-stop.sh +0 -90
  197. package/templates/adapters/claude/hooks/lib/vocabulary.sh +0 -56
  198. package/templates/adapters/claude/hooks/notification.sh +0 -45
  199. package/templates/adapters/claude/hooks/permission-denied.sh +0 -42
  200. package/templates/adapters/claude/hooks/permission-gate.sh +0 -18
  201. package/templates/adapters/claude/hooks/post-tool-failure.sh +0 -10
  202. package/templates/adapters/claude/hooks/post-tool-use.sh +0 -35
  203. package/templates/adapters/claude/hooks/pre-compact.sh +0 -37
  204. package/templates/adapters/claude/hooks/pre-tool-use.sh +0 -153
  205. package/templates/adapters/claude/hooks/prompt-submit.sh +0 -99
  206. package/templates/adapters/claude/hooks/review-checklist.sh +0 -10
  207. package/templates/adapters/claude/hooks/session-end.sh +0 -21
  208. package/templates/adapters/claude/hooks/session-start.sh +0 -36
  209. package/templates/adapters/claude/hooks/stop-check.sh +0 -45
  210. package/templates/adapters/claude/hooks/stop-failure.sh +0 -44
  211. package/templates/adapters/claude/hooks/subagent-guard.sh +0 -36
  212. package/templates/adapters/claude/hooks/subagent-stop.sh +0 -20
  213. package/templates/adapters/codex/hooks/doc-format-guard.sh +0 -261
  214. package/templates/adapters/codex/hooks/lib/common.sh +0 -261
  215. package/templates/adapters/codex/hooks/lib/context-inject.sh +0 -96
  216. package/templates/adapters/codex/hooks/lib/notify.sh +0 -34
  217. package/templates/adapters/codex/hooks/lib/preload-templates.sh +0 -187
  218. package/templates/adapters/codex/hooks/lib/session-end.sh +0 -76
  219. package/templates/adapters/codex/hooks/lib/state-detect.sh +0 -117
  220. package/templates/adapters/codex/hooks/lib/subagent-stop.sh +0 -90
  221. package/templates/adapters/codex/hooks/lib/vocabulary.sh +0 -56
  222. package/templates/adapters/codex/hooks/notification.sh +0 -45
  223. package/templates/adapters/codex/hooks/permission-gate.sh +0 -18
  224. package/templates/adapters/codex/hooks/post-tool-failure.sh +0 -10
  225. package/templates/adapters/codex/hooks/post-tool-use.sh +0 -138
  226. package/templates/adapters/codex/hooks/pre-compact.sh +0 -37
  227. package/templates/adapters/codex/hooks/pre-tool-use.sh +0 -217
  228. package/templates/adapters/codex/hooks/prompt-submit.sh +0 -87
  229. package/templates/adapters/codex/hooks/review-checklist.sh +0 -10
  230. package/templates/adapters/codex/hooks/session-end.sh +0 -38
  231. package/templates/adapters/codex/hooks/session-start.sh +0 -53
  232. package/templates/adapters/codex/hooks/stop-check.sh +0 -45
  233. package/templates/adapters/codex/hooks/subagent-guard.sh +0 -15
  234. package/templates/adapters/codex/hooks/subagent-stop.sh +0 -47
  235. package/templates/adapters/qoder/hooks/doc-format-guard.sh +0 -260
  236. package/templates/adapters/qoder/hooks/lib/common.sh +0 -261
  237. package/templates/adapters/qoder/hooks/lib/context-inject.sh +0 -96
  238. package/templates/adapters/qoder/hooks/lib/notify.sh +0 -34
  239. package/templates/adapters/qoder/hooks/lib/preload-templates.sh +0 -187
  240. package/templates/adapters/qoder/hooks/lib/session-end.sh +0 -76
  241. package/templates/adapters/qoder/hooks/lib/state-detect.sh +0 -117
  242. package/templates/adapters/qoder/hooks/lib/subagent-stop.sh +0 -90
  243. package/templates/adapters/qoder/hooks/lib/vocabulary.sh +0 -56
  244. package/templates/adapters/qoder/hooks/notification.sh +0 -25
  245. package/templates/adapters/qoder/hooks/permission-gate.sh +0 -8
  246. package/templates/adapters/qoder/hooks/post-tool-failure.sh +0 -8
  247. package/templates/adapters/qoder/hooks/post-tool-use.sh +0 -121
  248. package/templates/adapters/qoder/hooks/pre-compact.sh +0 -37
  249. package/templates/adapters/qoder/hooks/pre-tool-use.sh +0 -154
  250. package/templates/adapters/qoder/hooks/prompt-submit.sh +0 -96
  251. package/templates/adapters/qoder/hooks/review-checklist.sh +0 -157
  252. package/templates/adapters/qoder/hooks/session-end.sh +0 -24
  253. package/templates/adapters/qoder/hooks/session-start.sh +0 -45
  254. package/templates/adapters/qoder/hooks/stop-check.sh +0 -48
  255. package/templates/adapters/qoder/hooks/subagent-guard.sh +0 -27
  256. package/templates/adapters/qoder/hooks/subagent-stop.sh +0 -26
  257. package/templates/adapters/trae/hooks/doc-format-guard.sh +0 -261
  258. package/templates/adapters/trae/hooks/lib/common.sh +0 -261
  259. package/templates/adapters/trae/hooks/lib/context-inject.sh +0 -96
  260. package/templates/adapters/trae/hooks/lib/notify.sh +0 -34
  261. package/templates/adapters/trae/hooks/lib/preload-templates.sh +0 -187
  262. package/templates/adapters/trae/hooks/lib/session-end.sh +0 -76
  263. package/templates/adapters/trae/hooks/lib/state-detect.sh +0 -117
  264. package/templates/adapters/trae/hooks/lib/subagent-stop.sh +0 -90
  265. package/templates/adapters/trae/hooks/lib/vocabulary.sh +0 -56
  266. package/templates/adapters/trae/hooks/notification.sh +0 -45
  267. package/templates/adapters/trae/hooks/permission-gate.sh +0 -18
  268. package/templates/adapters/trae/hooks/post-tool-failure.sh +0 -10
  269. package/templates/adapters/trae/hooks/post-tool-use.sh +0 -138
  270. package/templates/adapters/trae/hooks/pre-compact.sh +0 -37
  271. package/templates/adapters/trae/hooks/pre-tool-use.sh +0 -217
  272. package/templates/adapters/trae/hooks/prompt-submit.sh +0 -87
  273. package/templates/adapters/trae/hooks/review-checklist.sh +0 -10
  274. package/templates/adapters/trae/hooks/session-end.sh +0 -38
  275. package/templates/adapters/trae/hooks/session-start.sh +0 -53
  276. package/templates/adapters/trae/hooks/stop-check.sh +0 -45
  277. package/templates/adapters/trae/hooks/subagent-guard.sh +0 -15
  278. package/templates/adapters/trae/hooks/subagent-stop.sh +0 -47
  279. package/templates/adapters/vscode/hooks/doc-format-guard.sh +0 -201
  280. package/templates/adapters/vscode/hooks/lib/common.sh +0 -261
  281. package/templates/adapters/vscode/hooks/lib/context-inject.sh +0 -96
  282. package/templates/adapters/vscode/hooks/lib/notify.sh +0 -34
  283. package/templates/adapters/vscode/hooks/lib/preload-templates.sh +0 -187
  284. package/templates/adapters/vscode/hooks/lib/session-end.sh +0 -76
  285. package/templates/adapters/vscode/hooks/lib/state-detect.sh +0 -117
  286. package/templates/adapters/vscode/hooks/lib/subagent-stop.sh +0 -90
  287. package/templates/adapters/vscode/hooks/lib/vocabulary.sh +0 -56
  288. package/templates/adapters/vscode/hooks/notification.sh +0 -45
  289. package/templates/adapters/vscode/hooks/permission-gate.sh +0 -18
  290. package/templates/adapters/vscode/hooks/post-tool-failure.sh +0 -28
  291. package/templates/adapters/vscode/hooks/post-tool-use.sh +0 -35
  292. package/templates/adapters/vscode/hooks/pre-compact.sh +0 -37
  293. package/templates/adapters/vscode/hooks/pre-tool-use.sh +0 -153
  294. package/templates/adapters/vscode/hooks/prompt-submit.sh +0 -99
  295. package/templates/adapters/vscode/hooks/review-checklist.sh +0 -10
  296. package/templates/adapters/vscode/hooks/session-end.sh +0 -13
  297. package/templates/adapters/vscode/hooks/session-start.sh +0 -35
  298. package/templates/adapters/vscode/hooks/stop-check.sh +0 -53
  299. package/templates/adapters/vscode/hooks/subagent-guard.sh +0 -37
  300. package/templates/adapters/vscode/hooks/subagent-stop.sh +0 -12
  301. package/templates/core/hooks/doc-format-guard.sh +0 -261
  302. package/templates/core/hooks/lib/common.sh +0 -261
  303. package/templates/core/hooks/lib/context-inject.sh +0 -96
  304. package/templates/core/hooks/lib/notify.sh +0 -34
  305. package/templates/core/hooks/lib/preload-templates.sh +0 -187
  306. package/templates/core/hooks/lib/session-end.sh +0 -76
  307. package/templates/core/hooks/lib/state-detect.sh +0 -117
  308. package/templates/core/hooks/lib/subagent-stop.sh +0 -90
  309. package/templates/core/hooks/lib/vocabulary.sh +0 -56
  310. package/templates/core/hooks/notification.sh +0 -45
  311. package/templates/core/hooks/permission-gate.sh +0 -18
  312. package/templates/core/hooks/post-tool-failure.sh +0 -10
  313. package/templates/core/hooks/post-tool-use.sh +0 -138
  314. package/templates/core/hooks/pre-compact.sh +0 -37
  315. package/templates/core/hooks/pre-tool-use.sh +0 -217
  316. package/templates/core/hooks/prompt-submit.sh +0 -87
  317. package/templates/core/hooks/review-checklist.sh +0 -10
  318. package/templates/core/hooks/session-end.sh +0 -38
  319. package/templates/core/hooks/session-start.sh +0 -53
  320. package/templates/core/hooks/stop-check.sh +0 -45
  321. package/templates/core/hooks/subagent-guard.sh +0 -15
  322. package/templates/core/hooks/subagent-stop.sh +0 -47
  323. package/templates/shared/hooks-lib/common.sh +0 -22
@@ -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
+ }
@@ -0,0 +1,145 @@
1
+ import type { RuntimeContextKey } from "./runtime-context.js"
2
+ import type { PlanStatusResolution } from "./plan-lifecycle.js"
3
+ import {
4
+ isLifecycleEventForContext,
5
+ parsePlanLifecycleChangedEnvelope,
6
+ PLAN_LIFECYCLE_CHANNEL,
7
+ type PlanLifecycleChangedEnvelope,
8
+ } from "./plan-lifecycle-events.js"
9
+
10
+ export interface PgNotification {
11
+ channel: string
12
+ payload?: string
13
+ }
14
+
15
+ export interface LifecycleListenClient {
16
+ connect(): Promise<unknown>
17
+ query(sql: string): Promise<unknown>
18
+ end(): Promise<void>
19
+ on(event: "notification", listener: (message: PgNotification) => void): this
20
+ on(event: "error", listener: (error: Error) => void): this
21
+ on(event: "end", listener: () => void): this
22
+ removeAllListeners(): this
23
+ }
24
+
25
+ export type LifecycleListenClientFactory = () => LifecycleListenClient | Promise<LifecycleListenClient>
26
+
27
+ export interface PlanLifecycleSubscriberOptions {
28
+ context: RuntimeContextKey
29
+ clientFactory: LifecycleListenClientFactory
30
+ resolveStatus(input: {
31
+ context: RuntimeContextKey
32
+ planId?: string
33
+ reason: "initial" | "notification" | "reconnect"
34
+ }): Promise<PlanStatusResolution>
35
+ onSnapshot(snapshot: PlanStatusResolution, envelope?: PlanLifecycleChangedEnvelope): void | Promise<void>
36
+ onError?(error: Error): void
37
+ reconnectDelayMs?: number
38
+ }
39
+
40
+ export class PlanLifecycleSubscriber {
41
+ readonly #options: PlanLifecycleSubscriberOptions
42
+ #client: LifecycleListenClient | null = null
43
+ #stopping = false
44
+ #startedOnce = false
45
+ #reconnectTimer: ReturnType<typeof setTimeout> | null = null
46
+ #refreshQueue: Promise<void> = Promise.resolve()
47
+
48
+ constructor(options: PlanLifecycleSubscriberOptions) {
49
+ this.#options = options
50
+ }
51
+
52
+ async start(): Promise<void> {
53
+ if (this.#client) return
54
+ this.#stopping = false
55
+ try {
56
+ await this.#connectAndHydrate("initial")
57
+ } catch (error) {
58
+ this.#scheduleReconnect()
59
+ throw error
60
+ }
61
+ }
62
+
63
+ async stop(): Promise<void> {
64
+ this.#stopping = true
65
+ if (this.#reconnectTimer) clearTimeout(this.#reconnectTimer)
66
+ this.#reconnectTimer = null
67
+ const client = this.#client
68
+ this.#client = null
69
+ if (!client) return
70
+ client.removeAllListeners()
71
+ try { await client.query(`UNLISTEN ${PLAN_LIFECYCLE_CHANNEL}`) } catch { /* connection may already be gone */ }
72
+ await client.end().catch(() => undefined)
73
+ await this.#refreshQueue
74
+ }
75
+
76
+ async #connectAndHydrate(reason: "initial" | "reconnect"): Promise<void> {
77
+ const client = await this.#options.clientFactory()
78
+ this.#client = client
79
+ client.on("notification", (message) => this.#handleNotification(message))
80
+ client.on("error", (error) => this.#handleDisconnect(client, error))
81
+ client.on("end", () => this.#handleDisconnect(client, new Error("PostgreSQL LISTEN session ended")))
82
+ try {
83
+ await client.connect()
84
+ await client.query(`LISTEN ${PLAN_LIFECYCLE_CHANNEL}`)
85
+ await this.#refresh(undefined, reason)
86
+ this.#startedOnce = true
87
+ } catch (error) {
88
+ if (this.#client === client) this.#client = null
89
+ client.removeAllListeners()
90
+ await client.end().catch(() => undefined)
91
+ const normalized = error instanceof Error ? error : new Error(String(error))
92
+ this.#options.onError?.(normalized)
93
+ throw normalized
94
+ }
95
+ }
96
+
97
+ #handleNotification(message: PgNotification): void {
98
+ if (this.#stopping || message.channel !== PLAN_LIFECYCLE_CHANNEL) return
99
+ const envelope = parsePlanLifecycleChangedEnvelope(message.payload)
100
+ if (!envelope || !isLifecycleEventForContext(envelope, this.#options.context)) return
101
+ this.#refreshQueue = this.#refreshQueue
102
+ .then(() => this.#refresh(envelope, "notification"))
103
+ .catch((error: unknown) => this.#options.onError?.(error instanceof Error ? error : new Error(String(error))))
104
+ }
105
+
106
+ #handleDisconnect(client: LifecycleListenClient, error: Error): void {
107
+ if (this.#stopping || this.#client !== client) return
108
+ this.#client = null
109
+ client.removeAllListeners()
110
+ this.#options.onError?.(error)
111
+ if (this.#reconnectTimer) return
112
+ this.#reconnectTimer = setTimeout(() => {
113
+ this.#reconnectTimer = null
114
+ if (this.#stopping) return
115
+ this.#connectAndHydrate(this.#startedOnce ? "reconnect" : "initial")
116
+ .catch(() => this.#scheduleReconnect())
117
+ }, this.#options.reconnectDelayMs ?? 1000)
118
+ }
119
+
120
+ #scheduleReconnect(): void {
121
+ if (this.#stopping || this.#reconnectTimer) return
122
+ this.#reconnectTimer = setTimeout(() => {
123
+ this.#reconnectTimer = null
124
+ this.#connectAndHydrate(this.#startedOnce ? "reconnect" : "initial")
125
+ .catch(() => this.#scheduleReconnect())
126
+ }, this.#options.reconnectDelayMs ?? 1000)
127
+ }
128
+
129
+ async #refresh(
130
+ envelope: PlanLifecycleChangedEnvelope | undefined,
131
+ reason: "initial" | "notification" | "reconnect",
132
+ ): Promise<void> {
133
+ const snapshot = await this.#options.resolveStatus({
134
+ context: this.#options.context,
135
+ planId: envelope?.planId,
136
+ reason,
137
+ })
138
+ await this.#options.onSnapshot(snapshot, envelope)
139
+ }
140
+ }
141
+
142
+ export async function createPgLifecycleListenClient(databaseUrl: string): Promise<LifecycleListenClient> {
143
+ const { Client } = await import("pg")
144
+ return new Client({ connectionString: databaseUrl })
145
+ }
@@ -0,0 +1,153 @@
1
+ import type { RuntimeContextKey } from "./runtime-context.js"
2
+
3
+ export const PLAN_LIFECYCLE_STATUSES = [
4
+ "DRAFT",
5
+ "ACTIVE",
6
+ "BLOCKED",
7
+ "REJECTED",
8
+ "CLOSED",
9
+ "ABANDONED",
10
+ ] as const
11
+
12
+ export type PlanLifecycleStatus = (typeof PLAN_LIFECYCLE_STATUSES)[number]
13
+ export type PlanApprovalStatus = "DRAFT" | "SUBMITTED" | "TONGYI" | "BOHUI"
14
+
15
+ export interface ScopedPlanStatusRow {
16
+ id: string
17
+ projectKey: string
18
+ adapterKey: string
19
+ planName: string
20
+ lifecycle: PlanLifecycleStatus
21
+ revision: number
22
+ doneTasks: number
23
+ totalTasks: number
24
+ checklistTDone: number
25
+ checklistT: number
26
+ }
27
+
28
+ export interface PlanStatusStore {
29
+ findPlan(input: {
30
+ context: RuntimeContextKey
31
+ planName?: string
32
+ planId?: string
33
+ activeOnly?: boolean
34
+ }): Promise<ScopedPlanStatusRow | null>
35
+ findLatestPlanApproval(input: {
36
+ context: RuntimeContextKey
37
+ planName: string
38
+ }): Promise<PlanApprovalStatus | null>
39
+ }
40
+
41
+ export interface PlanStatusSnapshot {
42
+ availability: "READY"
43
+ source: "database"
44
+ context: Pick<RuntimeContextKey, "projectKey" | "adapterKey" | "contextId">
45
+ planName: string
46
+ planId: string
47
+ lifecycle: PlanLifecycleStatus
48
+ revision: number
49
+ isActive: boolean
50
+ approvalStatus: PlanApprovalStatus | null
51
+ progress: {
52
+ doneTasks: number
53
+ totalTasks: number
54
+ checklistTDone: number
55
+ checklistT: number
56
+ }
57
+ }
58
+
59
+ export interface NoActivePlanSnapshot {
60
+ availability: "READY"
61
+ source: "database"
62
+ context: Pick<RuntimeContextKey, "projectKey" | "adapterKey" | "contextId">
63
+ planName: null
64
+ lifecycle: null
65
+ isActive: false
66
+ }
67
+
68
+ export interface PlanStatusUnavailable {
69
+ availability: "STATUS_UNAVAILABLE"
70
+ source: "database"
71
+ context: Pick<RuntimeContextKey, "projectKey" | "adapterKey" | "contextId">
72
+ reason: string
73
+ }
74
+
75
+ export type PlanStatusResolution = PlanStatusSnapshot | NoActivePlanSnapshot | PlanStatusUnavailable
76
+
77
+ const ALLOWED_TRANSITIONS: Readonly<Record<PlanLifecycleStatus, readonly PlanLifecycleStatus[]>> = {
78
+ DRAFT: ["ACTIVE", "REJECTED", "ABANDONED"],
79
+ ACTIVE: ["BLOCKED", "CLOSED", "ABANDONED"],
80
+ BLOCKED: ["ACTIVE", "CLOSED", "ABANDONED"],
81
+ REJECTED: ["DRAFT", "ACTIVE", "ABANDONED"],
82
+ CLOSED: [],
83
+ ABANDONED: [],
84
+ }
85
+
86
+ export function isActiveLifecycle(lifecycle: PlanLifecycleStatus): boolean {
87
+ return lifecycle === "ACTIVE" || lifecycle === "BLOCKED"
88
+ }
89
+
90
+ export function assertLifecycleTransition(from: PlanLifecycleStatus, to: PlanLifecycleStatus): void {
91
+ if (from === to) return
92
+ if (!ALLOWED_TRANSITIONS[from].includes(to)) {
93
+ throw new Error(`非法 Plan lifecycle 迁移: ${from} → ${to}`)
94
+ }
95
+ }
96
+
97
+ function snapshotContext(context: RuntimeContextKey) {
98
+ return { projectKey: context.projectKey, adapterKey: context.adapterKey, contextId: context.contextId }
99
+ }
100
+
101
+ export async function resolvePlanStatus(
102
+ store: PlanStatusStore,
103
+ context: RuntimeContextKey,
104
+ selector?: string | { planName?: string; planId?: string; activeOnly?: boolean },
105
+ ): Promise<PlanStatusResolution> {
106
+ try {
107
+ const selection = typeof selector === "string" ? { planName: selector } : (selector ?? {})
108
+ const plan = await store.findPlan({
109
+ context,
110
+ planName: selection.planName,
111
+ planId: selection.planId,
112
+ activeOnly: selection.activeOnly ?? (selection.planName === undefined && selection.planId === undefined),
113
+ })
114
+ if (!plan) {
115
+ return {
116
+ availability: "READY",
117
+ source: "database",
118
+ context: snapshotContext(context),
119
+ planName: null,
120
+ lifecycle: null,
121
+ isActive: false,
122
+ }
123
+ }
124
+ if (plan.projectKey !== context.projectKey || plan.adapterKey !== context.adapterKey) {
125
+ throw new Error("PlanStatusStore 返回了 scope 外记录")
126
+ }
127
+ const approvalStatus = await store.findLatestPlanApproval({ context, planName: plan.planName })
128
+ return {
129
+ availability: "READY",
130
+ source: "database",
131
+ context: snapshotContext(context),
132
+ planName: plan.planName,
133
+ planId: plan.id,
134
+ lifecycle: plan.lifecycle,
135
+ revision: plan.revision,
136
+ isActive: isActiveLifecycle(plan.lifecycle),
137
+ approvalStatus,
138
+ progress: {
139
+ doneTasks: plan.doneTasks,
140
+ totalTasks: plan.totalTasks,
141
+ checklistTDone: plan.checklistTDone,
142
+ checklistT: plan.checklistT,
143
+ },
144
+ }
145
+ } catch (error) {
146
+ return {
147
+ availability: "STATUS_UNAVAILABLE",
148
+ source: "database",
149
+ context: snapshotContext(context),
150
+ reason: error instanceof Error ? error.message : String(error),
151
+ }
152
+ }
153
+ }
@@ -0,0 +1,56 @@
1
+ import { randomUUID } from "crypto"
2
+ import * as z from "zod/v4"
3
+ import type { RuntimeContextKey } from "./runtime-context.js"
4
+ import type { PrismaNotifyTransaction } from "./plan-lifecycle-events.js"
5
+
6
+ export const PLAN_ROUND_CHANNEL = "add_plan_round_changed_v1"
7
+
8
+ export const PlanRoundChangedEnvelopeSchema = z.object({
9
+ schemaVersion: z.literal(1),
10
+ eventId: z.string().min(1),
11
+ projectKey: z.string().min(1),
12
+ adapterKey: z.string().min(1),
13
+ planId: z.string().min(1),
14
+ })
15
+
16
+ export type PlanRoundChangedEnvelope = z.infer<typeof PlanRoundChangedEnvelopeSchema>
17
+
18
+ export function createPlanRoundChangedEnvelope(input: {
19
+ context: RuntimeContextKey
20
+ planId: string
21
+ eventId?: string
22
+ }): PlanRoundChangedEnvelope {
23
+ return {
24
+ schemaVersion: 1,
25
+ eventId: input.eventId ?? randomUUID(),
26
+ projectKey: input.context.projectKey,
27
+ adapterKey: input.context.adapterKey,
28
+ planId: input.planId,
29
+ }
30
+ }
31
+
32
+ export function parsePlanRoundChangedEnvelope(payload: string | undefined): PlanRoundChangedEnvelope | null {
33
+ if (!payload) return null
34
+ try {
35
+ const parsed: unknown = JSON.parse(payload)
36
+ const result = PlanRoundChangedEnvelopeSchema.safeParse(parsed)
37
+ return result.success ? result.data : null
38
+ } catch {
39
+ return null
40
+ }
41
+ }
42
+
43
+ export function isPlanRoundEventForContext(
44
+ envelope: PlanRoundChangedEnvelope,
45
+ context: RuntimeContextKey,
46
+ ): boolean {
47
+ return envelope.projectKey === context.projectKey && envelope.adapterKey === context.adapterKey
48
+ }
49
+
50
+ export async function publishPlanRoundChanged(
51
+ tx: PrismaNotifyTransaction,
52
+ envelope: PlanRoundChangedEnvelope,
53
+ ): Promise<void> {
54
+ const payload = JSON.stringify(envelope)
55
+ await tx.$executeRaw`SELECT pg_notify(${PLAN_ROUND_CHANNEL}, ${payload})`
56
+ }