@wardby/cli 0.5.3 → 0.6.0

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 (370) hide show
  1. package/.env.example +33 -0
  2. package/README.md +25 -14
  3. package/dist/claude-coding-worker/context.d.ts +22 -0
  4. package/dist/claude-coding-worker/context.js +92 -0
  5. package/dist/claude-coding-worker/driver.d.ts +7 -1
  6. package/dist/claude-coding-worker/driver.js +38 -5
  7. package/dist/claude-coding-worker/sdk.js +13 -4
  8. package/dist/cli-help.d.ts +1 -1
  9. package/dist/cli-help.js +5 -2
  10. package/dist/cli-native-mode.d.ts +9 -0
  11. package/dist/cli-native-mode.js +20 -0
  12. package/dist/cli.d.ts +3 -1
  13. package/dist/cli.js +141 -5
  14. package/dist/coding/claude-context.d.ts +45 -0
  15. package/dist/coding/claude-context.js +434 -0
  16. package/dist/coding/profile.d.ts +12 -0
  17. package/dist/coding/profile.js +6 -0
  18. package/dist/coding/protocol.d.ts +104 -0
  19. package/dist/coding/protocol.js +87 -3
  20. package/dist/coding-worker/artifact.d.ts +8 -0
  21. package/dist/coding-worker/artifact.js +2 -2
  22. package/dist/coding-worker/driver.js +2 -0
  23. package/dist/coding-worker/sdk.d.ts +6 -0
  24. package/dist/coding-worker/sdk.js +1 -0
  25. package/dist/coding-worker/skills.d.ts +23 -0
  26. package/dist/coding-worker/skills.js +117 -0
  27. package/dist/coding-worker/types.d.ts +2 -0
  28. package/dist/config/github-allowed-accounts.d.ts +15 -0
  29. package/dist/config/github-allowed-accounts.js +36 -0
  30. package/dist/config/low-power.d.ts +12 -0
  31. package/dist/config/low-power.js +18 -0
  32. package/dist/config/mobile.d.ts +12 -0
  33. package/dist/config/mobile.js +18 -0
  34. package/dist/config/providers.d.ts +53 -0
  35. package/dist/config/providers.js +92 -6
  36. package/dist/core/db.d.ts +16 -6
  37. package/dist/core/db.js +12 -10
  38. package/dist/core/dispatch.d.ts +11 -0
  39. package/dist/core/dispatch.js +36 -1
  40. package/dist/core/engine-native.js +9 -5
  41. package/dist/core/host-events.d.ts +49 -3
  42. package/dist/core/host-events.js +289 -15
  43. package/dist/core/issue-bridge.js +42 -2
  44. package/dist/core/issue-events.js +12 -1
  45. package/dist/core/issue-status.js +14 -3
  46. package/dist/core/merge-order-check.d.ts +129 -0
  47. package/dist/core/merge-order-check.js +336 -0
  48. package/dist/core/notification-dispatcher.d.ts +70 -0
  49. package/dist/core/notification-dispatcher.js +368 -0
  50. package/dist/core/notification-templates.d.ts +25 -0
  51. package/dist/core/notification-templates.js +105 -0
  52. package/dist/core/notifications.d.ts +10 -0
  53. package/dist/core/notifications.js +38 -0
  54. package/dist/core/pg-auth.d.ts +22 -0
  55. package/dist/core/pg-auth.js +152 -0
  56. package/dist/core/power-composition.d.ts +16 -0
  57. package/dist/core/power-composition.js +37 -0
  58. package/dist/core/power-idle.d.ts +14 -0
  59. package/dist/core/power-idle.js +30 -0
  60. package/dist/core/power.d.ts +71 -0
  61. package/dist/core/power.js +202 -0
  62. package/dist/core/reconciler.d.ts +4 -2
  63. package/dist/core/reconciler.js +29 -5
  64. package/dist/core/related-pull-requests.d.ts +48 -4
  65. package/dist/core/related-pull-requests.js +113 -11
  66. package/dist/core/repo-access.d.ts +15 -0
  67. package/dist/core/repo-access.js +66 -0
  68. package/dist/core/review-fix.d.ts +37 -0
  69. package/dist/core/review-fix.js +207 -6
  70. package/dist/core/review-host-tools.d.ts +20 -0
  71. package/dist/core/review-host-tools.js +58 -2
  72. package/dist/core/runner.d.ts +336 -3
  73. package/dist/core/runner.js +894 -317
  74. package/dist/core/secret-broker-config.d.ts +103 -0
  75. package/dist/core/secret-broker-config.js +112 -0
  76. package/dist/core/secrets.d.ts +39 -4
  77. package/dist/core/secrets.js +148 -40
  78. package/dist/core/workflow-event-recorder.d.ts +14 -0
  79. package/dist/core/workflow-event-recorder.js +120 -0
  80. package/dist/core/workflow-events.d.ts +82 -0
  81. package/dist/core/workflow-events.js +44 -0
  82. package/dist/core/workflow-run-events.d.ts +16 -0
  83. package/dist/core/workflow-run-events.js +40 -0
  84. package/dist/generated/prisma/browser.d.ts +58 -0
  85. package/dist/generated/prisma/client.d.ts +58 -0
  86. package/dist/generated/prisma/commonInputTypes.d.ts +150 -0
  87. package/dist/generated/prisma/enums.d.ts +27 -0
  88. package/dist/generated/prisma/enums.js +23 -0
  89. package/dist/generated/prisma/internal/class.d.ts +88 -0
  90. package/dist/generated/prisma/internal/class.js +4 -4
  91. package/dist/generated/prisma/internal/prismaNamespace.d.ts +772 -1
  92. package/dist/generated/prisma/internal/prismaNamespace.js +135 -4
  93. package/dist/generated/prisma/internal/prismaNamespaceBrowser.d.ts +139 -0
  94. package/dist/generated/prisma/internal/prismaNamespaceBrowser.js +135 -4
  95. package/dist/generated/prisma/models/Agent.d.ts +360 -1
  96. package/dist/generated/prisma/models/CodingAgentProfile.d.ts +73 -1
  97. package/dist/generated/prisma/models/CodingRun.d.ts +205 -1
  98. package/dist/generated/prisma/models/DeferredReview.d.ts +78 -1
  99. package/dist/generated/prisma/models/NativeGatewayCall.d.ts +1675 -0
  100. package/dist/generated/prisma/models/NativeGatewayCall.js +1 -0
  101. package/dist/generated/prisma/models/NativeGatewaySession.d.ts +1517 -0
  102. package/dist/generated/prisma/models/NativeGatewaySession.js +1 -0
  103. package/dist/generated/prisma/models/NativeWarmWorker.d.ts +1296 -0
  104. package/dist/generated/prisma/models/NativeWarmWorker.js +1 -0
  105. package/dist/generated/prisma/models/NotificationChannel.d.ts +1543 -0
  106. package/dist/generated/prisma/models/NotificationChannel.js +1 -0
  107. package/dist/generated/prisma/models/NotificationDelivery.d.ts +1502 -0
  108. package/dist/generated/prisma/models/NotificationDelivery.js +1 -0
  109. package/dist/generated/prisma/models/NotificationThread.d.ts +1090 -0
  110. package/dist/generated/prisma/models/NotificationThread.js +1 -0
  111. package/dist/generated/prisma/models/Run.d.ts +826 -1
  112. package/dist/generated/prisma/models/RunHostCheck.d.ts +36 -1
  113. package/dist/generated/prisma/models/Secret.d.ts +37 -1
  114. package/dist/generated/prisma/models/SecretBrokerChange.d.ts +1101 -0
  115. package/dist/generated/prisma/models/SecretBrokerChange.js +1 -0
  116. package/dist/generated/prisma/models/WorkflowEvent.d.ts +1422 -0
  117. package/dist/generated/prisma/models/WorkflowEvent.js +1 -0
  118. package/dist/generated/prisma/models.d.ts +8 -0
  119. package/dist/help/build.js +4 -2
  120. package/dist/help/catalog.d.ts +1 -1
  121. package/dist/help/catalog.js +31 -6
  122. package/dist/help-index.json +1363 -176
  123. package/dist/import/create.js +17 -0
  124. package/dist/import/neutral-schema.d.ts +6 -2
  125. package/dist/import/neutral-schema.js +2 -0
  126. package/dist/mcp/auth/access.d.ts +1 -0
  127. package/dist/mcp/auth/repo-authorization.js +14 -0
  128. package/dist/mcp/auth/resource-server.d.ts +2 -1
  129. package/dist/mcp/auth/resource-server.js +7 -2
  130. package/dist/mcp/auth/self-hosted/browser.d.ts +1 -1
  131. package/dist/mcp/auth/self-hosted/browser.js +19 -6
  132. package/dist/mcp/context.d.ts +6 -0
  133. package/dist/mcp/host-events/github-ingress.d.ts +23 -1
  134. package/dist/mcp/host-events/github-ingress.js +68 -0
  135. package/dist/mcp/index.d.ts +13 -0
  136. package/dist/mcp/index.js +59 -10
  137. package/dist/mcp/tools/agents.js +82 -11
  138. package/dist/mcp/tools/notification-channels.d.ts +2 -0
  139. package/dist/mcp/tools/notification-channels.js +293 -0
  140. package/dist/mcp/tools/power.d.ts +2 -0
  141. package/dist/mcp/tools/power.js +77 -0
  142. package/dist/mcp/tools/runs.d.ts +0 -6
  143. package/dist/mcp/tools/runs.js +7 -2
  144. package/dist/mcp/tools/secret-broker-compat.d.ts +12 -0
  145. package/dist/mcp/tools/secret-broker-compat.js +23 -0
  146. package/dist/mcp/tools/secret-elicitation-form.d.ts +22 -2
  147. package/dist/mcp/tools/secret-elicitation-form.js +200 -10
  148. package/dist/mcp/tools/secret-elicitation-server.js +8 -1
  149. package/dist/mcp/tools/secret-elicitation.d.ts +38 -3
  150. package/dist/mcp/tools/secret-elicitation.js +110 -12
  151. package/dist/mcp/tools/secrets.js +184 -7
  152. package/dist/mcp/tools/tools.d.ts +1 -1
  153. package/dist/mcp/tools/tools.js +22 -2
  154. package/dist/native-worker/composition.d.ts +23 -0
  155. package/dist/native-worker/composition.js +77 -0
  156. package/dist/native-worker/docker-isolation.d.ts +65 -0
  157. package/dist/native-worker/docker-isolation.js +183 -0
  158. package/dist/native-worker/docker-launcher.d.ts +74 -0
  159. package/dist/native-worker/docker-launcher.js +237 -0
  160. package/dist/native-worker/gateway-server.d.ts +39 -0
  161. package/dist/native-worker/gateway-server.js +315 -0
  162. package/dist/native-worker/gateway.d.ts +44 -0
  163. package/dist/native-worker/gateway.js +197 -0
  164. package/dist/native-worker/http-server.d.ts +22 -0
  165. package/dist/native-worker/http-server.js +109 -0
  166. package/dist/native-worker/http-transport.d.ts +17 -0
  167. package/dist/native-worker/http-transport.js +88 -0
  168. package/dist/native-worker/kubernetes-isolation.d.ts +90 -0
  169. package/dist/native-worker/kubernetes-isolation.js +218 -0
  170. package/dist/native-worker/kubernetes-launcher.d.ts +102 -0
  171. package/dist/native-worker/kubernetes-launcher.js +335 -0
  172. package/dist/native-worker/launch.d.ts +45 -0
  173. package/dist/native-worker/launch.js +44 -0
  174. package/dist/native-worker/ledger.d.ts +109 -0
  175. package/dist/native-worker/ledger.js +192 -0
  176. package/dist/native-worker/loopback.d.ts +12 -0
  177. package/dist/native-worker/loopback.js +49 -0
  178. package/dist/native-worker/main.d.ts +7 -0
  179. package/dist/native-worker/main.js +74 -0
  180. package/dist/native-worker/protocol.d.ts +744 -0
  181. package/dist/native-worker/protocol.js +200 -0
  182. package/dist/native-worker/sandbox-executor.d.ts +68 -0
  183. package/dist/native-worker/sandbox-executor.js +210 -0
  184. package/dist/native-worker/stdio.d.ts +29 -0
  185. package/dist/native-worker/stdio.js +188 -0
  186. package/dist/native-worker/warm-delivery.d.ts +16 -0
  187. package/dist/native-worker/warm-delivery.js +33 -0
  188. package/dist/native-worker/warm-pool-ledger.d.ts +39 -0
  189. package/dist/native-worker/warm-pool-ledger.js +73 -0
  190. package/dist/native-worker/warm-pool.d.ts +107 -0
  191. package/dist/native-worker/warm-pool.js +283 -0
  192. package/dist/native-worker/worker.d.ts +17 -0
  193. package/dist/native-worker/worker.js +115 -0
  194. package/dist/providers/auth/authorization-server.d.ts +3 -0
  195. package/dist/providers/auth/index.js +2 -0
  196. package/dist/providers/auth/self-hosted.d.ts +13 -1
  197. package/dist/providers/auth/self-hosted.js +54 -4
  198. package/dist/providers/chat/fake.d.ts +46 -0
  199. package/dist/providers/chat/fake.js +39 -0
  200. package/dist/providers/chat/index.d.ts +4 -0
  201. package/dist/providers/chat/index.js +8 -0
  202. package/dist/providers/chat/slack.d.ts +37 -0
  203. package/dist/providers/chat/slack.js +105 -0
  204. package/dist/providers/chat/types.d.ts +42 -0
  205. package/dist/providers/chat/types.js +19 -0
  206. package/dist/providers/engine/types.d.ts +19 -0
  207. package/dist/providers/engine/types.js +22 -0
  208. package/dist/providers/executor/composition.d.ts +24 -1
  209. package/dist/providers/executor/composition.js +39 -6
  210. package/dist/providers/executor/container.d.ts +24 -1
  211. package/dist/providers/executor/container.js +101 -11
  212. package/dist/providers/executor/dbos.d.ts +42 -0
  213. package/dist/providers/executor/dbos.js +118 -12
  214. package/dist/providers/executor/deferred.d.ts +56 -0
  215. package/dist/providers/executor/deferred.js +119 -0
  216. package/dist/providers/executor/routing.d.ts +37 -10
  217. package/dist/providers/executor/routing.js +73 -32
  218. package/dist/providers/executor/types.d.ts +12 -0
  219. package/dist/providers/jobs/docker.js +3 -3
  220. package/dist/providers/jobs/fake-kubernetes-api.d.ts +21 -2
  221. package/dist/providers/jobs/fake-kubernetes-api.js +42 -1
  222. package/dist/providers/jobs/kubernetes-api.d.ts +17 -1
  223. package/dist/providers/jobs/kubernetes-api.js +7 -0
  224. package/dist/providers/jobs/kubernetes-client.d.ts +10 -1
  225. package/dist/providers/jobs/kubernetes-client.js +52 -3
  226. package/dist/providers/jobs/kubernetes-dry-run-fixture.d.ts +6 -1
  227. package/dist/providers/jobs/kubernetes-dry-run-fixture.js +6 -3
  228. package/dist/providers/jobs/kubernetes-isolation.d.ts +3 -1
  229. package/dist/providers/jobs/kubernetes-isolation.js +6 -0
  230. package/dist/providers/jobs/kubernetes-platform.d.ts +4 -1
  231. package/dist/providers/jobs/kubernetes-platform.js +38 -1
  232. package/dist/providers/jobs/kubernetes-preflight.d.ts +2 -1
  233. package/dist/providers/jobs/kubernetes-preflight.js +4 -2
  234. package/dist/providers/jobs/kubernetes.d.ts +36 -1
  235. package/dist/providers/jobs/kubernetes.js +74 -4
  236. package/dist/providers/jobs/metadata-probes.d.ts +11 -0
  237. package/dist/providers/jobs/metadata-probes.js +13 -0
  238. package/dist/providers/jobs/types.d.ts +9 -0
  239. package/dist/providers/review-host/github.d.ts +11 -1
  240. package/dist/providers/review-host/github.js +50 -0
  241. package/dist/providers/review-host/types.d.ts +23 -0
  242. package/dist/providers/vcs/github.d.ts +32 -6
  243. package/dist/providers/vcs/github.js +74 -16
  244. package/dist/quickstart/coding-seed.d.ts +10 -3
  245. package/dist/quickstart/coding-seed.js +74 -3
  246. package/dist/quickstart/images.d.ts +2 -0
  247. package/dist/quickstart/images.js +7 -1
  248. package/dist/quickstart/reviewer-prompt.d.ts +1 -1
  249. package/dist/quickstart/reviewer-prompt.js +1 -1
  250. package/dist/quickstart-images.json +1 -1
  251. package/dist/sandbox/brokered-fetch.d.ts +23 -0
  252. package/dist/sandbox/brokered-fetch.js +37 -0
  253. package/dist/sandbox/host-functions.d.ts +34 -0
  254. package/dist/sandbox/host-functions.js +144 -80
  255. package/dist/sandbox/prelude.js +7 -1
  256. package/dist/sandbox/run-in-sandbox.d.ts +19 -0
  257. package/dist/sandbox/run-in-sandbox.js +28 -10
  258. package/dist/sandbox/safe-fetch.d.ts +2 -0
  259. package/dist/sandbox/safe-fetch.js +3 -1
  260. package/dist/sandbox/secret-broker-sigv4.d.ts +2 -0
  261. package/dist/sandbox/secret-broker-sigv4.js +49 -0
  262. package/dist/sandbox/secret-broker.d.ts +34 -0
  263. package/dist/sandbox/secret-broker.js +162 -0
  264. package/dist/sandbox/user-tool.d.ts +14 -0
  265. package/dist/sandbox/user-tool.js +32 -0
  266. package/dist/serve.js +18 -0
  267. package/dist/viewer/api-schema.d.ts +210 -14
  268. package/dist/viewer/api-schema.js +22 -0
  269. package/dist/viewer/build-schemas.js +4 -1
  270. package/dist/viewer/event-bus.d.ts +6 -0
  271. package/dist/viewer/event-bus.js +16 -7
  272. package/dist/viewer/graph.d.ts +5 -2
  273. package/dist/viewer/graph.js +7 -2
  274. package/dist/viewer/http.d.ts +0 -1
  275. package/dist/viewer/http.js +2 -2
  276. package/dist/viewer/infra.js +25 -2
  277. package/dist/viewer/limits.d.ts +4 -0
  278. package/dist/viewer/limits.js +4 -0
  279. package/dist/viewer/openapi.d.ts +15 -0
  280. package/dist/viewer/openapi.js +172 -0
  281. package/docs/README.md +7 -0
  282. package/docs/agent-recipes.md +57 -8
  283. package/docs/architecture-runtime.md +16 -1
  284. package/docs/code-review-agents.md +139 -5
  285. package/docs/coding-agent-setup.md +113 -5
  286. package/docs/coding-packages.md +4 -2
  287. package/docs/coding-services.md +3 -1
  288. package/docs/coding-worker-byo-images.md +198 -6
  289. package/docs/coding-worker-isolation.md +68 -14
  290. package/docs/getting-started-gke.md +225 -9
  291. package/docs/getting-started-identity-provider.md +21 -10
  292. package/docs/getting-started.md +5 -4
  293. package/docs/jira-agents.md +5 -1
  294. package/docs/native-sandbox.md +635 -0
  295. package/docs/release-verification.md +9 -0
  296. package/docs/security-deployment.md +113 -12
  297. package/docs/slack-notifications.md +252 -0
  298. package/docs/tool-secrets.md +252 -0
  299. package/docs/viewer-api.md +46 -5
  300. package/docs/viewer-api.openapi.yml +1404 -0
  301. package/help/admin-viewer.md +6 -2
  302. package/help/agent-recipes.md +27 -0
  303. package/help/architecture-agent.md +1 -1
  304. package/help/brokered-secrets.md +79 -0
  305. package/help/build-claude-code-image.md +131 -0
  306. package/help/build-worker-image.md +3 -6
  307. package/help/code-review-agents.md +68 -6
  308. package/help/deploy-gke.md +8 -1
  309. package/help/deployment-targets.md +25 -14
  310. package/help/errors/coding-provider-not-configured.md +2 -2
  311. package/help/errors/database-auth-invalid.md +28 -0
  312. package/help/errors/github-account-not-allowed.md +25 -0
  313. package/help/errors/low-power-disabled.md +29 -0
  314. package/help/errors/low-power-scale-forbidden.md +35 -0
  315. package/help/errors/low-power-wake-timeout.md +30 -0
  316. package/help/errors/mobile-disabled.md +31 -0
  317. package/help/errors/native-sandbox-capacity.md +22 -0
  318. package/help/errors/native-sandbox-deadline-exceeded.md +19 -0
  319. package/help/errors/native-sandbox-docker-failed.md +20 -0
  320. package/help/errors/native-sandbox-gateway-unavailable.md +21 -0
  321. package/help/errors/native-sandbox-image-not-pinned.md +19 -0
  322. package/help/errors/native-sandbox-isolation-mismatch.md +20 -0
  323. package/help/errors/native-sandbox-network-unenforced.md +22 -0
  324. package/help/errors/native-sandbox-requires-catalog.md +31 -0
  325. package/help/errors/native-sandbox-unavailable.md +19 -0
  326. package/help/errors/native-sandbox-warm-delivery-failed.md +21 -0
  327. package/help/errors/native-sandbox-worker-exited.md +20 -0
  328. package/help/errors/native-sandbox-worker-lost.md +20 -0
  329. package/help/errors/native-sandbox-worker-unready.md +22 -0
  330. package/help/errors/secret-broker-body-invalid.md +26 -0
  331. package/help/errors/secret-broker-changed.md +20 -0
  332. package/help/errors/secret-broker-config-invalid.md +30 -0
  333. package/help/errors/secret-broker-conflict.md +27 -0
  334. package/help/errors/secret-broker-destination-denied.md +28 -0
  335. package/help/errors/secret-broker-value-invalid.md +28 -0
  336. package/help/errors/secret-brokered.md +24 -0
  337. package/help/errors/secret-not-brokered.md +25 -0
  338. package/help/errors/slack-auth-failed.md +48 -0
  339. package/help/errors/slack-channel-unreachable.md +39 -0
  340. package/help/errors/slack-not-configured.md +33 -0
  341. package/help/getting-started.md +4 -5
  342. package/help/github.md +18 -1
  343. package/help/identity-and-access.md +5 -1
  344. package/help/local-repositories.md +2 -3
  345. package/help/low-power-mode.md +125 -0
  346. package/help/merge-order-check.md +117 -0
  347. package/help/mobile-app.md +54 -0
  348. package/help/native-sandbox.md +136 -0
  349. package/help/observability.md +8 -0
  350. package/help/related-pull-requests.md +70 -9
  351. package/help/repo-instructions-and-skills.md +91 -0
  352. package/help/review-fix-rounds.md +10 -0
  353. package/help/slack-notifications.md +62 -0
  354. package/help/troubleshooting/coding-workers.md +21 -0
  355. package/package.json +11 -4
  356. package/prisma/migrations/20261009000003_native_execution_mode/migration.sql +12 -0
  357. package/prisma/migrations/20261009000004_native_gateway_ledger/migration.sql +65 -0
  358. package/prisma/migrations/20261009000005_native_gateway_call_child_agent/migration.sql +5 -0
  359. package/prisma/migrations/20261009000006_run_deferred_start_stop/migration.sql +14 -0
  360. package/prisma/migrations/20261009000007_native_gateway_network_ready/migration.sql +5 -0
  361. package/prisma/migrations/20261010000001_workflow_notifications/migration.sql +99 -0
  362. package/prisma/migrations/20261011000001_native_gateway_session_status_index/migration.sql +2 -0
  363. package/prisma/migrations/20261012000001_native_warm_worker/migration.sql +32 -0
  364. package/prisma/migrations/20261013000001_coding_run_merge_order/migration.sql +2 -0
  365. package/prisma/migrations/20261013000001_secret_broker/migration.sql +17 -0
  366. package/prisma/migrations/20261013000002_deferred_review_request/migration.sql +4 -0
  367. package/prisma/migrations/20261014000000_repo_skills/migration.sql +15 -0
  368. package/prisma/migrations/20261015000001_coding_run_merge_order_index/migration.sql +2 -0
  369. package/prisma/migrations/20261015000002_no_change_rereview/migration.sql +2 -0
  370. package/prisma/schema.prisma +362 -91
@@ -0,0 +1,129 @@
1
+ /**
2
+ * The `wardby merge order` check: pure state computation from a related
3
+ * pull request set's declared merge order (CodingRun.mergeOrder, see
4
+ * related-pull-requests.ts). See
5
+ * docs/private/2026-10-10-delegated-merge-order-design.md Part 3.
6
+ *
7
+ * For a member with order *k*, its dependencies are the set's members with
8
+ * a strictly lower, non-null order; equal-order peers are not dependencies,
9
+ * and a null-order member is never a dependency (and has none of its own).
10
+ * The check is posted only when the set has more than one distinct non-null
11
+ * order; otherwise (or when `self` itself has no order) there is nothing to
12
+ * gate and the verdict is "skip".
13
+ *
14
+ * syncMergeOrderChecks keeps the check current on every open pull request
15
+ * of a set: at the lead's end (syncMergeOrderChecksAfterRun), when a pull
16
+ * request closes or gets a new head (syncMergeOrderChecksForPullRequest,
17
+ * from host-events), and from the reconciler (sweepMergeOrderChecks) for
18
+ * missed events. State is read fresh from the host each time; nothing is
19
+ * stored. Best effort: none of these ever throws.
20
+ */
21
+ import type { Run } from "#prisma";
22
+ import type { PullRequestOrigin, ReviewHostRegistry, UpsertNamedCheckInput } from "../providers/review-host/types.js";
23
+ import { type Logger } from "./logger.js";
24
+ import { type RelatedPullRequestsDb } from "./related-pull-requests.js";
25
+ export declare const MERGE_ORDER_CHECK_NAME = "wardby merge order";
26
+ export interface MergeOrderMember {
27
+ repository: string;
28
+ number: number;
29
+ mergeOrder: number | null;
30
+ state: "open" | "draft" | "merged" | "closed";
31
+ url?: string;
32
+ }
33
+ export type MergeOrderVerdict = {
34
+ status: "skip";
35
+ } | {
36
+ status: "in_progress";
37
+ waitingFor: MergeOrderMember[];
38
+ } | {
39
+ status: "success";
40
+ dependencies: MergeOrderMember[];
41
+ } | {
42
+ status: "failure";
43
+ closedUnmerged: MergeOrderMember[];
44
+ };
45
+ /**
46
+ * `set` may include or omit `self`: self's own order always counts toward
47
+ * the distinct orders, and self is never its own dependency (dependencies
48
+ * have a strictly lower order). `self` need not be the same object instance
49
+ * as its entry in `set`; only `mergeOrder` is read from it.
50
+ */
51
+ export declare function mergeOrderVerdict(self: MergeOrderMember, set: readonly MergeOrderMember[]): MergeOrderVerdict;
52
+ export type MergeOrderDb = RelatedPullRequestsDb;
53
+ export interface MergeOrderSyncDeps {
54
+ db: MergeOrderDb;
55
+ hosts: ReviewHostRegistry | undefined;
56
+ log?: Logger;
57
+ }
58
+ export interface MergeOrderSyncOptions {
59
+ /** A pull request this event already read: reused instead of reading it a second time. */
60
+ origin?: {
61
+ repository: string;
62
+ number: number;
63
+ read: () => Promise<PullRequestOrigin>;
64
+ };
65
+ /** A pull request this event reports closed: its state comes from the event, and it is not read. */
66
+ closed?: {
67
+ repository: string;
68
+ number: number;
69
+ merged: boolean;
70
+ };
71
+ /**
72
+ * Sets already synced in this event or sweep pass, keyed by their sorted
73
+ * members: seeds from different roots of one set (an original request
74
+ * and a follow-up) collect the same set, which is then written once.
75
+ */
76
+ synced?: Set<string>;
77
+ }
78
+ /**
79
+ * The check run's text for one verdict: step label, then the pull requests
80
+ * it depends on, as links. While the delegating run is still running
81
+ * (`leadRunning`) it may still add or reorder steps, so a success for any
82
+ * step above the set's lowest is posted as in_progress, waiting for it.
83
+ */
84
+ export declare function mergeOrderCheckInput(self: MergeOrderMember, verdict: Exclude<MergeOrderVerdict, {
85
+ status: "skip";
86
+ }>, steps: readonly number[], headSha: string, leadRunning?: boolean): UpsertNamedCheckInput;
87
+ /**
88
+ * Recomputes and posts the `wardby merge order` check on every open (or
89
+ * draft) pull request of the set `seedRunId` belongs to (any run of the
90
+ * request's tree). Nothing happens unless the set has more than one
91
+ * distinct merge order. Each member's state is read live from the host
92
+ * (a stored "merged" is final and not re-read); the check is posted only on
93
+ * an open or draft pull request the App opened whose marker names a coding
94
+ * run this deployment recorded for that repository, on its current head.
95
+ * A member that can't be read holds back only the checks that depend on it.
96
+ * Never throws.
97
+ */
98
+ export declare function syncMergeOrderChecks(deps: MergeOrderSyncDeps, seedRunId: string, opts?: MergeOrderSyncOptions): Promise<void>;
99
+ /**
100
+ * At a run's end: a tree root (the lead) that delegated anything syncs its
101
+ * set. A run that delegated nothing costs one indexed lookup. Never throws.
102
+ */
103
+ export declare function syncMergeOrderChecksAfterRun(deps: MergeOrderSyncDeps, run: Pick<Run, "id" | "parentRunId">): Promise<void>;
104
+ /**
105
+ * A pull request closed or got a new head: syncs every set it is an
106
+ * ordered member of. Only a pull request a coding run with a merge order
107
+ * opened or continued can change any verdict (a member without an order is
108
+ * no one's dependency and gets no check), so anything else costs one query
109
+ * and no host call. Never throws.
110
+ */
111
+ export declare function syncMergeOrderChecksForPullRequest(deps: MergeOrderSyncDeps, pr: {
112
+ repository: string;
113
+ number: number;
114
+ }, opts?: MergeOrderSyncOptions): Promise<void>;
115
+ /** The sweep runs at most this often per process (the reconciler ticks every 15 s). */
116
+ export declare const MERGE_ORDER_SWEEP_INTERVAL_MS: number;
117
+ /** Coding runs with a merge order updated within this long are swept. */
118
+ export declare const MERGE_ORDER_SWEEP_MAX_AGE_MS: number;
119
+ /** Most sets synced per sweep pass; the least recently synced go first. */
120
+ export declare const MERGE_ORDER_SWEEP_BATCH = 10;
121
+ export declare function resetMergeOrderSweepForTests(): void;
122
+ /**
123
+ * The reconciler's half, for missed pr_closed/pr_updated events: syncs the
124
+ * sets of pull requests that coding runs with a merge order opened or
125
+ * continued in the last 7 days, a bounded batch per pass, at most once per
126
+ * MERGE_ORDER_SWEEP_INTERVAL_MS per process. Returns the number of sets
127
+ * synced. Never throws.
128
+ */
129
+ export declare function sweepMergeOrderChecks(deps: MergeOrderSyncDeps, now?: Date): Promise<number>;
@@ -0,0 +1,336 @@
1
+ import { CODING_CODE_PROVIDER, normalizeGitHubRepository } from "../coding/protocol.js";
2
+ import { logger } from "./logger.js";
3
+ import { collectRelatedPullRequests, runTreeRoot } from "./related-pull-requests.js";
4
+ const moduleLog = logger.child({ module: "merge-order-check" });
5
+ export const MERGE_ORDER_CHECK_NAME = "wardby merge order";
6
+ /**
7
+ * `set` may include or omit `self`: self's own order always counts toward
8
+ * the distinct orders, and self is never its own dependency (dependencies
9
+ * have a strictly lower order). `self` need not be the same object instance
10
+ * as its entry in `set`; only `mergeOrder` is read from it.
11
+ */
12
+ export function mergeOrderVerdict(self, set) {
13
+ const distinctOrders = new Set();
14
+ if (self.mergeOrder !== null)
15
+ distinctOrders.add(self.mergeOrder);
16
+ for (const m of set)
17
+ if (m.mergeOrder !== null)
18
+ distinctOrders.add(m.mergeOrder);
19
+ if (distinctOrders.size < 2)
20
+ return { status: "skip" };
21
+ if (self.mergeOrder === null)
22
+ return { status: "skip" };
23
+ const selfOrder = self.mergeOrder;
24
+ const dependencies = set.filter((m) => m.mergeOrder !== null && m.mergeOrder < selfOrder);
25
+ const closedUnmerged = dependencies.filter((m) => m.state === "closed");
26
+ if (closedUnmerged.length > 0)
27
+ return { status: "failure", closedUnmerged };
28
+ const waitingFor = dependencies.filter((m) => m.state !== "merged");
29
+ if (waitingFor.length > 0)
30
+ return { status: "in_progress", waitingFor };
31
+ return { status: "success", dependencies };
32
+ }
33
+ const prUrl = (repository, number) => `https://github.com/${repository}/pull/${number}`;
34
+ /** `repository#number` as a link: never a title or any other free text from the host. */
35
+ const prLink = (m) => `[${m.repository}#${m.number}](${prUrl(m.repository, m.number)})`;
36
+ const keyOf = (repository, number) => `${repository}#${number}`;
37
+ function safeRepository(value) {
38
+ try {
39
+ return normalizeGitHubRepository(value);
40
+ }
41
+ catch {
42
+ return undefined;
43
+ }
44
+ }
45
+ function originState(origin) {
46
+ if (origin.merged)
47
+ return "merged";
48
+ if (origin.state !== "open")
49
+ return "closed";
50
+ return origin.draft ? "draft" : "open";
51
+ }
52
+ /**
53
+ * The check run's text for one verdict: step label, then the pull requests
54
+ * it depends on, as links. While the delegating run is still running
55
+ * (`leadRunning`) it may still add or reorder steps, so a success for any
56
+ * step above the set's lowest is posted as in_progress, waiting for it.
57
+ */
58
+ export function mergeOrderCheckInput(self, verdict, steps, headSha, leadRunning = false) {
59
+ const step = `Step ${steps.indexOf(self.mergeOrder) + 1} of ${steps.length} in the merge order set by the delegating agent.`;
60
+ const base = { headSha, name: MERGE_ORDER_CHECK_NAME };
61
+ const lines = (members) => members.map(prLink).join(", ");
62
+ if (verdict.status === "success" && leadRunning && self.mergeOrder !== steps[0]) {
63
+ return {
64
+ ...base,
65
+ status: "in_progress",
66
+ title: "Waiting for the delegating run to finish",
67
+ summary: `${step}\n\nEvery earlier step has merged, but the delegating run is still running and may change ` +
68
+ `the steps; waiting for it to finish.` +
69
+ (verdict.dependencies.length > 0 ? `\n\nMerged: ${lines(verdict.dependencies)}` : ""),
70
+ };
71
+ }
72
+ switch (verdict.status) {
73
+ case "success":
74
+ return {
75
+ ...base,
76
+ status: "completed",
77
+ conclusion: "success",
78
+ title: verdict.dependencies.length > 0 ? "Every earlier step has merged" : "First step: nothing to wait for",
79
+ summary: verdict.dependencies.length > 0 ? `${step}\n\nMerged: ${lines(verdict.dependencies)}` : step,
80
+ };
81
+ case "failure":
82
+ return {
83
+ ...base,
84
+ status: "completed",
85
+ conclusion: "failure",
86
+ title: "An earlier step was closed without merging",
87
+ summary: `${step}\n\nClosed without merging: ${lines(verdict.closedUnmerged)}`,
88
+ };
89
+ case "in_progress":
90
+ return {
91
+ ...base,
92
+ status: "in_progress",
93
+ title: `Waiting for ${verdict.waitingFor.length} earlier pull request${verdict.waitingFor.length === 1 ? "" : "s"} to merge`,
94
+ summary: `${step}\n\nWaiting for: ${lines(verdict.waitingFor)}`,
95
+ };
96
+ }
97
+ }
98
+ /**
99
+ * Recomputes and posts the `wardby merge order` check on every open (or
100
+ * draft) pull request of the set `seedRunId` belongs to (any run of the
101
+ * request's tree). Nothing happens unless the set has more than one
102
+ * distinct merge order. Each member's state is read live from the host
103
+ * (a stored "merged" is final and not re-read); the check is posted only on
104
+ * an open or draft pull request the App opened whose marker names a coding
105
+ * run this deployment recorded for that repository, on its current head.
106
+ * A member that can't be read holds back only the checks that depend on it.
107
+ * Never throws.
108
+ */
109
+ export async function syncMergeOrderChecks(deps, seedRunId, opts = {}) {
110
+ const log = deps.log ?? moduleLog;
111
+ const host = deps.hosts?.[CODING_CODE_PROVIDER];
112
+ if (!host?.upsertNamedCheck || !host.pullRequestOrigin)
113
+ return;
114
+ try {
115
+ const group = await collectRelatedPullRequests(deps.db, seedRunId);
116
+ if (opts.synced) {
117
+ const setKey = group.pullRequests
118
+ .map((pr) => keyOf(pr.repository, pr.number))
119
+ .sort()
120
+ .join("\n");
121
+ if (opts.synced.has(setKey))
122
+ return;
123
+ opts.synced.add(setKey);
124
+ }
125
+ const steps = [
126
+ ...new Set(group.pullRequests.flatMap((pr) => (pr.mergeOrder !== undefined ? [pr.mergeOrder] : []))),
127
+ ].sort((a, b) => a - b);
128
+ if (steps.length < 2)
129
+ return;
130
+ // A property of the set, whichever root it was seeded from: only a native lead (a root that is
131
+ // not itself a coding run) declares merge order, so only one still running may change steps. A
132
+ // top-level coding run that continued one of the PRs never holds the check: its own later pushes
133
+ // re-sync through pr_updated, and nothing re-syncs when it ends.
134
+ const leadRunning = (group.roots ?? []).some((r) => !r.codingRun && (r.status === "pending" || r.status === "running"));
135
+ const reused = opts.origin ? safeRepository(opts.origin.repository) : undefined;
136
+ const closed = opts.closed ? safeRepository(opts.closed.repository) : undefined;
137
+ const members = [];
138
+ // Bounded by the collector's MAX_RELATED_GROUP: at most one read per member.
139
+ for (const pr of group.pullRequests) {
140
+ const base = { repository: pr.repository, number: pr.number, mergeOrder: pr.mergeOrder ?? null };
141
+ const key = keyOf(pr.repository, pr.number);
142
+ if (closed && key === keyOf(closed, opts.closed.number)) {
143
+ members.push({ member: { ...base, state: opts.closed.merged ? "merged" : "closed" }, known: true });
144
+ continue;
145
+ }
146
+ if (pr.state === "merged") {
147
+ members.push({ member: { ...base, state: "merged" }, known: true });
148
+ continue;
149
+ }
150
+ // No order: never a dependency and never checked, so its state is never needed: no read.
151
+ if (base.mergeOrder === null) {
152
+ members.push({ member: { ...base, state: "open" }, known: true });
153
+ continue;
154
+ }
155
+ try {
156
+ const origin = reused && key === keyOf(reused, opts.origin.number)
157
+ ? await opts.origin.read()
158
+ : await host.pullRequestOrigin(pr.repository, pr.number);
159
+ let headSha;
160
+ if (origin.markerRunId) {
161
+ const opener = await deps.db.codingRun.findUnique({
162
+ where: { runId: origin.markerRunId },
163
+ select: { repository: true },
164
+ });
165
+ if (opener && opener.repository.toLowerCase() === pr.repository)
166
+ headSha = origin.headSha;
167
+ }
168
+ members.push({ member: { ...base, state: originState(origin) }, known: true, ...(headSha ? { headSha } : {}) });
169
+ }
170
+ catch (err) {
171
+ log.warn({ err, seedRunId, repository: pr.repository, number: pr.number }, "could not read a pull request of a merge-ordered set");
172
+ members.push({ member: { ...base, state: "open" }, known: false });
173
+ }
174
+ }
175
+ const set = members.map((m) => m.member);
176
+ for (const target of members) {
177
+ const self = target.member;
178
+ if (!target.headSha || self.mergeOrder === null || (self.state !== "open" && self.state !== "draft"))
179
+ continue;
180
+ const order = self.mergeOrder;
181
+ if (members.some((m) => !m.known && m.member.mergeOrder !== null && m.member.mergeOrder < order)) {
182
+ log.info({ seedRunId, repository: self.repository, number: self.number }, "merge order check left as is: an earlier step could not be read");
183
+ continue;
184
+ }
185
+ const verdict = mergeOrderVerdict(self, set);
186
+ if (verdict.status === "skip")
187
+ continue;
188
+ try {
189
+ await host.upsertNamedCheck(self.repository, mergeOrderCheckInput(self, verdict, steps, target.headSha, leadRunning));
190
+ log.info({ seedRunId, repository: self.repository, number: self.number, status: verdict.status }, "merge order check posted");
191
+ }
192
+ catch (err) {
193
+ log.warn({ err, seedRunId, repository: self.repository, number: self.number }, "could not post the merge order check");
194
+ }
195
+ }
196
+ }
197
+ catch (err) {
198
+ log.warn({ err, seedRunId }, "could not sync the merge order checks");
199
+ }
200
+ }
201
+ /**
202
+ * At a run's end: a tree root (the lead) that delegated anything syncs its
203
+ * set. A run that delegated nothing costs one indexed lookup. Never throws.
204
+ */
205
+ export async function syncMergeOrderChecksAfterRun(deps, run) {
206
+ if (run.parentRunId !== null)
207
+ return;
208
+ const host = deps.hosts?.[CODING_CODE_PROVIDER];
209
+ if (!host?.upsertNamedCheck || !host.pullRequestOrigin)
210
+ return;
211
+ try {
212
+ if (!(await deps.db.run.findFirst({ where: { parentRunId: run.id }, select: { id: true } })))
213
+ return;
214
+ }
215
+ catch (err) {
216
+ (deps.log ?? moduleLog).warn({ err, runId: run.id }, "could not sync the merge order checks");
217
+ return;
218
+ }
219
+ await syncMergeOrderChecks(deps, run.id);
220
+ }
221
+ /** Most coding runs (seeds) looked up per pull request event or sweep pass. */
222
+ const MAX_SEEDS = 20;
223
+ /** Coding runs with a merge order whose outcome is pull request `repository#number`. */
224
+ async function orderedSeeds(db, repository, number) {
225
+ const rows = await db.$queryRaw `
226
+ SELECT cr."runId"
227
+ FROM "CodingRun" cr
228
+ WHERE cr."mergeOrder" IS NOT NULL
229
+ AND lower(cr."repository") = lower(${repository})
230
+ AND cr."result"->>'outcome' IN ('pull_request_opened', 'pull_request_updated')
231
+ AND lower(cr."result"->>'repository') = lower(${repository})
232
+ AND cr."result"->>'pullRequestNumber' = ${String(number)}
233
+ ORDER BY cr."updatedAt" DESC
234
+ LIMIT ${MAX_SEEDS}`;
235
+ return rows.map((r) => r.runId);
236
+ }
237
+ /**
238
+ * A pull request closed or got a new head: syncs every set it is an
239
+ * ordered member of. Only a pull request a coding run with a merge order
240
+ * opened or continued can change any verdict (a member without an order is
241
+ * no one's dependency and gets no check), so anything else costs one query
242
+ * and no host call. Never throws.
243
+ */
244
+ export async function syncMergeOrderChecksForPullRequest(deps, pr, opts = {}) {
245
+ const log = deps.log ?? moduleLog;
246
+ const host = deps.hosts?.[CODING_CODE_PROVIDER];
247
+ if (!host?.upsertNamedCheck || !host.pullRequestOrigin)
248
+ return;
249
+ const repository = safeRepository(pr.repository);
250
+ if (!repository || !Number.isSafeInteger(pr.number) || pr.number <= 0)
251
+ return;
252
+ let roots;
253
+ try {
254
+ roots = [];
255
+ for (const seed of await orderedSeeds(deps.db, repository, pr.number)) {
256
+ const root = await runTreeRoot(deps.db, seed, repository);
257
+ if (root && !roots.some((r) => r.id === root.id))
258
+ roots.push(root);
259
+ }
260
+ }
261
+ catch (err) {
262
+ log.warn({ err, repository, number: pr.number }, "could not find the merge-ordered sets of a pull request");
263
+ return;
264
+ }
265
+ const synced = new Set();
266
+ for (const root of roots)
267
+ await syncMergeOrderChecks(deps, root.id, { ...opts, synced });
268
+ }
269
+ /** The sweep runs at most this often per process (the reconciler ticks every 15 s). */
270
+ export const MERGE_ORDER_SWEEP_INTERVAL_MS = 10 * 60_000;
271
+ /** Coding runs with a merge order updated within this long are swept. */
272
+ export const MERGE_ORDER_SWEEP_MAX_AGE_MS = 7 * 24 * 60 * 60_000;
273
+ /** Most coding runs read per sweep pass. */
274
+ const MERGE_ORDER_SWEEP_SEEDS = 50;
275
+ /** Most sets synced per sweep pass; the least recently synced go first. */
276
+ export const MERGE_ORDER_SWEEP_BATCH = 10;
277
+ let lastSweepAt = Number.NEGATIVE_INFINITY;
278
+ /** Root run id -> when this process last swept it: rotates the batch across passes. */
279
+ const lastSyncedAt = new Map();
280
+ export function resetMergeOrderSweepForTests() {
281
+ lastSweepAt = Number.NEGATIVE_INFINITY;
282
+ lastSyncedAt.clear();
283
+ }
284
+ /**
285
+ * The reconciler's half, for missed pr_closed/pr_updated events: syncs the
286
+ * sets of pull requests that coding runs with a merge order opened or
287
+ * continued in the last 7 days, a bounded batch per pass, at most once per
288
+ * MERGE_ORDER_SWEEP_INTERVAL_MS per process. Returns the number of sets
289
+ * synced. Never throws.
290
+ */
291
+ export async function sweepMergeOrderChecks(deps, now = new Date()) {
292
+ const log = deps.log ?? moduleLog;
293
+ const host = deps.hosts?.[CODING_CODE_PROVIDER];
294
+ if (!host?.upsertNamedCheck || !host.pullRequestOrigin)
295
+ return 0;
296
+ if (now.getTime() - lastSweepAt < MERGE_ORDER_SWEEP_INTERVAL_MS)
297
+ return 0;
298
+ lastSweepAt = now.getTime();
299
+ const since = new Date(now.getTime() - MERGE_ORDER_SWEEP_MAX_AGE_MS);
300
+ for (const [root, at] of lastSyncedAt)
301
+ if (at < since.getTime())
302
+ lastSyncedAt.delete(root);
303
+ let roots;
304
+ try {
305
+ // Covers the MERGE_ORDER_SWEEP_SEEDS most recently updated coding runs with a merge order
306
+ // within the last 7 days; older sets rely on their pr_closed/pr_updated events alone.
307
+ const rows = await deps.db.$queryRaw `
308
+ SELECT cr."runId", cr."repository"
309
+ FROM "CodingRun" cr
310
+ WHERE cr."mergeOrder" IS NOT NULL
311
+ AND cr."updatedAt" >= ${since}
312
+ AND cr."result"->>'outcome' IN ('pull_request_opened', 'pull_request_updated')
313
+ ORDER BY cr."updatedAt" DESC
314
+ LIMIT ${MERGE_ORDER_SWEEP_SEEDS}`;
315
+ roots = [];
316
+ for (const row of rows) {
317
+ const root = await runTreeRoot(deps.db, row.runId, row.repository);
318
+ if (root && !roots.some((r) => r.id === root.id))
319
+ roots.push(root);
320
+ }
321
+ }
322
+ catch (err) {
323
+ log.warn({ err }, "merge order check sweep failed");
324
+ return 0;
325
+ }
326
+ const batch = roots
327
+ .map((root, index) => ({ root, index, at: lastSyncedAt.get(root.id) ?? Number.NEGATIVE_INFINITY }))
328
+ .sort((a, b) => a.at - b.at || a.index - b.index)
329
+ .slice(0, MERGE_ORDER_SWEEP_BATCH);
330
+ const synced = new Set();
331
+ for (const { root } of batch) {
332
+ lastSyncedAt.set(root.id, now.getTime());
333
+ await syncMergeOrderChecks(deps, root.id, { synced });
334
+ }
335
+ return batch.length;
336
+ }
@@ -0,0 +1,70 @@
1
+ /**
2
+ * Delivers pending NotificationDelivery rows to chat (Slack), one thread per
3
+ * work item (or PR, or run) per channel.
4
+ *
5
+ * Rules:
6
+ * - Lease-gated: only the holder of the "notification-dispatch" SchedulerLease
7
+ * delivers. The lease is renewed before every delivery and the pass ends
8
+ * when renewal fails; a pass also stops after PASS_BUDGET_MS so a delivery
9
+ * always starts well inside the lease TTL. Replicas therefore don't
10
+ * double-post while the lease is held. Delivery is still at-least-once: a
11
+ * failed "delivered" write after a successful post retries (and reposts).
12
+ * - Threads: the first delivery for a (provider, channel, threadKey) posts a
13
+ * parent message and stores its ts in NotificationThread; every event is a
14
+ * reply under it. The thread status is derived from its delivered events;
15
+ * NotificationThread.status is the status last rendered on the parent, which
16
+ * is re-rendered with chat.update whenever the two differ. A deleted parent
17
+ * (message_not_found on update) drops the thread row; the next event posts
18
+ * a fresh parent.
19
+ * - Ordering: within a thread, deliveries go strictly oldest-first. A thread is
20
+ * only worked when its oldest pending delivery is due, and it stops at its
21
+ * first failure in a pass, so a reply never overtakes an older one.
22
+ * - Pacing: at most one Slack call per second per channel (posts and updates).
23
+ * - Errors: rate_limited defers by Retry-After without counting an attempt and
24
+ * holds the whole channel (every thread on it) until Retry-After passes;
25
+ * transient (and any non-ChatError) backs
26
+ * off exponentially and fails at MAX_ATTEMPTS; channel_unreachable fails
27
+ * every pending delivery for the channel and stamps lastError on its links;
28
+ * auth_failed pauses the whole dispatcher for 5 minutes (no attempt counted),
29
+ * then re-checks each provider with auth.test.
30
+ * - Sender: with WARDBY_SLACK_CUSTOMIZE on, a message's display name is the
31
+ * agent the event names (picked up, review, failure), else "wardby".
32
+ * - The pause and channel holds live on `deps.state` (created on first use), not in module
33
+ * state: one dispatcher (or one test) per deps object.
34
+ */
35
+ import { Prisma, type PrismaClient } from "#prisma";
36
+ import { type ChatProviderRegistry } from "../providers/chat/types.js";
37
+ export declare const DISPATCH_LEASE_SCOPE = "notification-dispatch";
38
+ export declare const MAX_ATTEMPTS = 10;
39
+ /** Per-dispatcher mutable state; `dispatchOnce` creates it on `deps` when absent. */
40
+ export interface DispatcherState {
41
+ /** While set and in the future, passes deliver nothing. */
42
+ pausedUntil?: Date;
43
+ /** provider\0channel → no Slack call to that channel before this time (rate limit Retry-After). */
44
+ channelBlockedUntil?: Map<string, Date>;
45
+ }
46
+ export interface DispatcherDeps {
47
+ db: PrismaClient;
48
+ chat: ChatProviderRegistry;
49
+ holder: string;
50
+ now?: () => Date;
51
+ sleep?: (ms: number) => Promise<void>;
52
+ state?: DispatcherState;
53
+ /** Test-only: deliver only rows whose channelId starts with this (keeps parallel DB suites apart). */
54
+ channelIdPrefix?: string;
55
+ }
56
+ /** Exponential back-off: 5s · 2^(attempts-1), capped at 1h. */
57
+ export declare function backoffMs(attempts: number): number;
58
+ /** One pass: deliver what is due. Returns the number of deliveries attempted. */
59
+ export declare function dispatchOnce(deps: DispatcherDeps): Promise<number>;
60
+ /**
61
+ * Deletes events (and their deliveries) older than `days` with no pending
62
+ * delivery. `only` narrows the candidates further (tests scope it to their own rows).
63
+ */
64
+ export declare function pruneWorkflowEvents(db: PrismaClient, now: Date, days?: number, only?: Prisma.WorkflowEventWhereInput): Promise<number>;
65
+ export interface DispatcherHandle {
66
+ stop(): void;
67
+ }
68
+ export declare function startNotificationDispatcher(deps: DispatcherDeps & {
69
+ intervalMs?: number;
70
+ }): DispatcherHandle;