@bevel-software/platform-core-backend 0.21.0 → 0.23.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 (434) hide show
  1. package/dist/core/core-ports.d.ts +11 -1
  2. package/dist/core/core-ports.d.ts.map +1 -1
  3. package/dist/core/core-ports.js.map +1 -1
  4. package/dist/core/create-core-server.d.ts +3 -3
  5. package/dist/core/create-core-server.d.ts.map +1 -1
  6. package/dist/core/create-core-server.js +62 -13
  7. package/dist/core/create-core-server.js.map +1 -1
  8. package/dist/core/create-core-services.d.ts +22 -3
  9. package/dist/core/create-core-services.d.ts.map +1 -1
  10. package/dist/core/create-core-services.js +158 -23
  11. package/dist/core/create-core-services.js.map +1 -1
  12. package/dist/core/lifecycle.d.ts +34 -1
  13. package/dist/core/lifecycle.d.ts.map +1 -1
  14. package/dist/core/lifecycle.js +89 -13
  15. package/dist/core/lifecycle.js.map +1 -1
  16. package/dist/core-config.d.ts +0 -12
  17. package/dist/core-config.d.ts.map +1 -1
  18. package/dist/core-config.js +11 -13
  19. package/dist/core-config.js.map +1 -1
  20. package/dist/index.d.ts +3 -2
  21. package/dist/index.d.ts.map +1 -1
  22. package/dist/index.js +2 -1
  23. package/dist/index.js.map +1 -1
  24. package/dist/modules/access/access-control.interface.d.ts +42 -12
  25. package/dist/modules/access/access-control.interface.d.ts.map +1 -1
  26. package/dist/modules/access/access-control.service.d.ts +67 -10
  27. package/dist/modules/access/access-control.service.d.ts.map +1 -1
  28. package/dist/modules/access/access-control.service.js +214 -35
  29. package/dist/modules/access/access-control.service.js.map +1 -1
  30. package/dist/modules/access/access-requests.contract.d.ts +75 -0
  31. package/dist/modules/access/access-requests.contract.d.ts.map +1 -0
  32. package/dist/modules/access/access-requests.contract.js +20 -0
  33. package/dist/modules/access/access-requests.contract.js.map +1 -0
  34. package/dist/modules/access/access-requests.routes.d.ts +35 -0
  35. package/dist/modules/access/access-requests.routes.d.ts.map +1 -0
  36. package/dist/modules/access/access-requests.routes.js +237 -0
  37. package/dist/modules/access/access-requests.routes.js.map +1 -0
  38. package/dist/modules/access/access-requests.service.d.ts +123 -0
  39. package/dist/modules/access/access-requests.service.d.ts.map +1 -0
  40. package/dist/modules/access/access-requests.service.js +337 -0
  41. package/dist/modules/access/access-requests.service.js.map +1 -0
  42. package/dist/modules/access/access.routes.d.ts.map +1 -1
  43. package/dist/modules/access/access.routes.js +13 -15
  44. package/dist/modules/access/access.routes.js.map +1 -1
  45. package/dist/modules/access-model/access-grammar.d.ts +12 -0
  46. package/dist/modules/access-model/access-grammar.d.ts.map +1 -1
  47. package/dist/modules/access-model/access-grammar.js +24 -0
  48. package/dist/modules/access-model/access-grammar.js.map +1 -1
  49. package/dist/modules/auth/account-admission.d.ts +58 -7
  50. package/dist/modules/auth/account-admission.d.ts.map +1 -1
  51. package/dist/modules/auth/account-admission.js +44 -1
  52. package/dist/modules/auth/account-admission.js.map +1 -1
  53. package/dist/modules/auth/account.routes.d.ts +1 -1
  54. package/dist/modules/auth/account.routes.d.ts.map +1 -1
  55. package/dist/modules/auth/account.routes.js +61 -5
  56. package/dist/modules/auth/account.routes.js.map +1 -1
  57. package/dist/modules/auth/auth.middleware.d.ts +1 -1
  58. package/dist/modules/auth/auth.middleware.d.ts.map +1 -1
  59. package/dist/modules/auth/auth.middleware.js +18 -7
  60. package/dist/modules/auth/auth.middleware.js.map +1 -1
  61. package/dist/modules/auth/auth.routes.d.ts.map +1 -1
  62. package/dist/modules/auth/auth.routes.js +8 -0
  63. package/dist/modules/auth/auth.routes.js.map +1 -1
  64. package/dist/modules/auth/auth.service.d.ts +96 -3
  65. package/dist/modules/auth/auth.service.d.ts.map +1 -1
  66. package/dist/modules/auth/auth.service.js +187 -9
  67. package/dist/modules/auth/auth.service.js.map +1 -1
  68. package/dist/modules/auth/oidc-auth-provider.d.ts.map +1 -1
  69. package/dist/modules/auth/oidc-auth-provider.js +7 -2
  70. package/dist/modules/auth/oidc-auth-provider.js.map +1 -1
  71. package/dist/modules/database/core-schema.d.ts +41 -24
  72. package/dist/modules/database/core-schema.d.ts.map +1 -1
  73. package/dist/modules/database/core-schema.js +35 -27
  74. package/dist/modules/database/core-schema.js.map +1 -1
  75. package/dist/modules/github-app/github-app.client.d.ts +101 -0
  76. package/dist/modules/github-app/github-app.client.d.ts.map +1 -0
  77. package/dist/modules/github-app/github-app.client.js +222 -0
  78. package/dist/modules/github-app/github-app.client.js.map +1 -0
  79. package/dist/modules/github-app/github-app.connection.d.ts +102 -0
  80. package/dist/modules/github-app/github-app.connection.d.ts.map +1 -0
  81. package/dist/modules/github-app/github-app.connection.js +185 -0
  82. package/dist/modules/github-app/github-app.connection.js.map +1 -0
  83. package/dist/modules/github-app/github-app.routes.d.ts +59 -0
  84. package/dist/modules/github-app/github-app.routes.d.ts.map +1 -0
  85. package/dist/modules/github-app/github-app.routes.js +323 -0
  86. package/dist/modules/github-app/github-app.routes.js.map +1 -0
  87. package/dist/modules/github-app/index.d.ts +4 -0
  88. package/dist/modules/github-app/index.d.ts.map +1 -0
  89. package/dist/modules/github-app/index.js +4 -0
  90. package/dist/modules/github-app/index.js.map +1 -0
  91. package/dist/modules/kb-fs/branch-name.d.ts.map +1 -1
  92. package/dist/modules/kb-fs/branch-name.js +12 -2
  93. package/dist/modules/kb-fs/branch-name.js.map +1 -1
  94. package/dist/modules/kb-fs/remote-url.d.ts +22 -0
  95. package/dist/modules/kb-fs/remote-url.d.ts.map +1 -0
  96. package/dist/modules/kb-fs/remote-url.js +35 -0
  97. package/dist/modules/kb-fs/remote-url.js.map +1 -0
  98. package/dist/modules/kb-fs/repo-path.d.ts +11 -16
  99. package/dist/modules/kb-fs/repo-path.d.ts.map +1 -1
  100. package/dist/modules/kb-fs/repo-path.js +79 -0
  101. package/dist/modules/kb-fs/repo-path.js.map +1 -1
  102. package/dist/modules/kb-sync/kb-sync.routes.d.ts +2 -2
  103. package/dist/modules/kb-sync/kb-sync.routes.d.ts.map +1 -1
  104. package/dist/modules/kb-sync/kb-sync.routes.js +14 -3
  105. package/dist/modules/kb-sync/kb-sync.routes.js.map +1 -1
  106. package/dist/modules/kb-sync/sync-auth.d.ts +3 -1
  107. package/dist/modules/kb-sync/sync-auth.d.ts.map +1 -1
  108. package/dist/modules/kb-sync/sync-auth.js +1 -1
  109. package/dist/modules/kb-sync/sync-auth.js.map +1 -1
  110. package/dist/modules/mcp/mcp-auth.middleware.d.ts.map +1 -1
  111. package/dist/modules/mcp/mcp-auth.middleware.js +21 -6
  112. package/dist/modules/mcp/mcp-auth.middleware.js.map +1 -1
  113. package/dist/modules/mcp/oauth/bevel-oauth-provider.d.ts.map +1 -1
  114. package/dist/modules/mcp/oauth/bevel-oauth-provider.js +3 -2
  115. package/dist/modules/mcp/oauth/bevel-oauth-provider.js.map +1 -1
  116. package/dist/modules/plugins/join-proposals.d.ts +17 -5
  117. package/dist/modules/plugins/join-proposals.d.ts.map +1 -1
  118. package/dist/modules/plugins/join-proposals.js +76 -22
  119. package/dist/modules/plugins/join-proposals.js.map +1 -1
  120. package/dist/modules/plugins/join-requests.service.d.ts +111 -18
  121. package/dist/modules/plugins/join-requests.service.d.ts.map +1 -1
  122. package/dist/modules/plugins/join-requests.service.js +173 -29
  123. package/dist/modules/plugins/join-requests.service.js.map +1 -1
  124. package/dist/modules/plugins/plugins.routes.d.ts.map +1 -1
  125. package/dist/modules/plugins/plugins.routes.js +3 -2
  126. package/dist/modules/plugins/plugins.routes.js.map +1 -1
  127. package/dist/modules/settings/deployment-settings.service.d.ts +22 -1
  128. package/dist/modules/settings/deployment-settings.service.d.ts.map +1 -1
  129. package/dist/modules/settings/deployment-settings.service.js +101 -7
  130. package/dist/modules/settings/deployment-settings.service.js.map +1 -1
  131. package/dist/modules/settings/managed-repository.d.ts +43 -0
  132. package/dist/modules/settings/managed-repository.d.ts.map +1 -0
  133. package/dist/modules/settings/managed-repository.js +60 -0
  134. package/dist/modules/settings/managed-repository.js.map +1 -0
  135. package/dist/modules/settings/repository-source.d.ts +128 -0
  136. package/dist/modules/settings/repository-source.d.ts.map +1 -0
  137. package/dist/modules/settings/repository-source.js +150 -0
  138. package/dist/modules/settings/repository-source.js.map +1 -0
  139. package/dist/modules/settings/setup.routes.d.ts +78 -4
  140. package/dist/modules/settings/setup.routes.d.ts.map +1 -1
  141. package/dist/modules/settings/setup.routes.js +379 -17
  142. package/dist/modules/settings/setup.routes.js.map +1 -1
  143. package/dist/modules/skills/skill-access-requests.routes.d.ts +6 -8
  144. package/dist/modules/skills/skill-access-requests.routes.d.ts.map +1 -1
  145. package/dist/modules/skills/skill-access-requests.routes.js +24 -63
  146. package/dist/modules/skills/skill-access-requests.routes.js.map +1 -1
  147. package/dist/modules/skills/skills.contract.d.ts +90 -18
  148. package/dist/modules/skills/skills.contract.d.ts.map +1 -1
  149. package/dist/modules/skills/skills.contract.js +4 -1
  150. package/dist/modules/skills/skills.contract.js.map +1 -1
  151. package/dist/modules/skills/skills.service.d.ts +98 -9
  152. package/dist/modules/skills/skills.service.d.ts.map +1 -1
  153. package/dist/modules/skills/skills.service.js +239 -37
  154. package/dist/modules/skills/skills.service.js.map +1 -1
  155. package/dist/modules/skills/skills.tools.d.ts +7 -0
  156. package/dist/modules/skills/skills.tools.d.ts.map +1 -1
  157. package/dist/modules/skills/skills.tools.js +71 -7
  158. package/dist/modules/skills/skills.tools.js.map +1 -1
  159. package/dist/modules/tool-auth/external-api-key.service.d.ts.map +1 -1
  160. package/dist/modules/tool-auth/external-api-key.service.js +4 -2
  161. package/dist/modules/tool-auth/external-api-key.service.js.map +1 -1
  162. package/dist/modules/tool-auth/internal-token.service.d.ts +3 -3
  163. package/dist/modules/tool-auth/tool-auth.middleware.d.ts +16 -7
  164. package/dist/modules/tool-auth/tool-auth.middleware.d.ts.map +1 -1
  165. package/dist/modules/tool-auth/tool-auth.middleware.js +34 -12
  166. package/dist/modules/tool-auth/tool-auth.middleware.js.map +1 -1
  167. package/dist/modules/tool-helpers/tool-context.d.ts.map +1 -1
  168. package/dist/modules/tool-helpers/tool-context.js +15 -0
  169. package/dist/modules/tool-helpers/tool-context.js.map +1 -1
  170. package/dist/modules/tool-helpers/tool-handler.d.ts +2 -1
  171. package/dist/modules/tool-helpers/tool-handler.d.ts.map +1 -1
  172. package/dist/modules/tool-helpers/tool-handler.js +25 -4
  173. package/dist/modules/tool-helpers/tool-handler.js.map +1 -1
  174. package/dist/modules/tool-helpers/tool.contract.d.ts +3 -2
  175. package/dist/modules/tool-helpers/tool.contract.d.ts.map +1 -1
  176. package/dist/modules/tool-helpers/tool.contract.js.map +1 -1
  177. package/dist/modules/tool-helpers/validate-token.js +1 -1
  178. package/dist/modules/tool-helpers/validate-token.js.map +1 -1
  179. package/dist/modules/workflow/agent-tools/change-request-summary.d.ts +98 -0
  180. package/dist/modules/workflow/agent-tools/change-request-summary.d.ts.map +1 -0
  181. package/dist/modules/workflow/agent-tools/change-request-summary.js +81 -0
  182. package/dist/modules/workflow/agent-tools/change-request-summary.js.map +1 -0
  183. package/dist/modules/workflow/agent-tools/workflow.tools.d.ts.map +1 -1
  184. package/dist/modules/workflow/agent-tools/workflow.tools.js +113 -37
  185. package/dist/modules/workflow/agent-tools/workflow.tools.js.map +1 -1
  186. package/dist/modules/workflow/file-lock.service.d.ts +24 -0
  187. package/dist/modules/workflow/file-lock.service.d.ts.map +1 -1
  188. package/dist/modules/workflow/file-lock.service.js +30 -0
  189. package/dist/modules/workflow/file-lock.service.js.map +1 -1
  190. package/dist/modules/workflow/git/git.service.d.ts +72 -1
  191. package/dist/modules/workflow/git/git.service.d.ts.map +1 -1
  192. package/dist/modules/workflow/git/git.service.js +170 -6
  193. package/dist/modules/workflow/git/git.service.js.map +1 -1
  194. package/dist/modules/workflow/git/node-git-runner.d.ts.map +1 -1
  195. package/dist/modules/workflow/git/node-git-runner.js +5 -0
  196. package/dist/modules/workflow/git/node-git-runner.js.map +1 -1
  197. package/dist/modules/workflow/git/pull-request.service.d.ts +45 -0
  198. package/dist/modules/workflow/git/pull-request.service.d.ts.map +1 -1
  199. package/dist/modules/workflow/git/pull-request.service.js +99 -0
  200. package/dist/modules/workflow/git/pull-request.service.js.map +1 -1
  201. package/dist/modules/workflow/pending-commits.service.d.ts +38 -0
  202. package/dist/modules/workflow/pending-commits.service.d.ts.map +1 -1
  203. package/dist/modules/workflow/pending-commits.service.js +55 -0
  204. package/dist/modules/workflow/pending-commits.service.js.map +1 -1
  205. package/dist/modules/workflow/workflow-hooks.d.ts +54 -32
  206. package/dist/modules/workflow/workflow-hooks.d.ts.map +1 -1
  207. package/dist/modules/workflow/workflow-hooks.js +16 -1
  208. package/dist/modules/workflow/workflow-hooks.js.map +1 -1
  209. package/dist/modules/workflow/workflow.routes.d.ts.map +1 -1
  210. package/dist/modules/workflow/workflow.routes.js +3 -1
  211. package/dist/modules/workflow/workflow.routes.js.map +1 -1
  212. package/dist/modules/workflow/workflow.service.d.ts +94 -13
  213. package/dist/modules/workflow/workflow.service.d.ts.map +1 -1
  214. package/dist/modules/workflow/workflow.service.js +258 -51
  215. package/dist/modules/workflow/workflow.service.js.map +1 -1
  216. package/dist/modules/workspace/agent-access.gate.d.ts +94 -0
  217. package/dist/modules/workspace/agent-access.gate.d.ts.map +1 -0
  218. package/dist/modules/workspace/agent-access.gate.js +123 -0
  219. package/dist/modules/workspace/agent-access.gate.js.map +1 -0
  220. package/dist/modules/workspace/routine-write-policy.d.ts +5 -6
  221. package/dist/modules/workspace/routine-write-policy.d.ts.map +1 -1
  222. package/dist/modules/workspace/routine-write-policy.js +5 -6
  223. package/dist/modules/workspace/routine-write-policy.js.map +1 -1
  224. package/dist/modules/workspace/session-sink.d.ts +5 -5
  225. package/dist/modules/workspace/set-aside-clone.d.ts +46 -0
  226. package/dist/modules/workspace/set-aside-clone.d.ts.map +1 -0
  227. package/dist/modules/workspace/set-aside-clone.js +92 -0
  228. package/dist/modules/workspace/set-aside-clone.js.map +1 -0
  229. package/dist/modules/workspace/startup/kb-startup-runner.d.ts +117 -1
  230. package/dist/modules/workspace/startup/kb-startup-runner.d.ts.map +1 -1
  231. package/dist/modules/workspace/startup/kb-startup-runner.js +190 -1
  232. package/dist/modules/workspace/startup/kb-startup-runner.js.map +1 -1
  233. package/dist/modules/workspace/workspace.routes.d.ts.map +1 -1
  234. package/dist/modules/workspace/workspace.routes.js +92 -3
  235. package/dist/modules/workspace/workspace.routes.js.map +1 -1
  236. package/dist/modules/workspace/workspace.service.d.ts +115 -5
  237. package/dist/modules/workspace/workspace.service.d.ts.map +1 -1
  238. package/dist/modules/workspace/workspace.service.js +272 -26
  239. package/dist/modules/workspace/workspace.service.js.map +1 -1
  240. package/dist/modules/workspace/workspace.tools.d.ts +2 -2
  241. package/dist/modules/workspace/workspace.tools.d.ts.map +1 -1
  242. package/dist/modules/workspace/workspace.tools.js +337 -117
  243. package/dist/modules/workspace/workspace.tools.js.map +1 -1
  244. package/dist/modules/write-access/write-access.d.ts +60 -0
  245. package/dist/modules/write-access/write-access.d.ts.map +1 -0
  246. package/dist/modules/write-access/write-access.js +129 -0
  247. package/dist/modules/write-access/write-access.js.map +1 -0
  248. package/dist/shared/domain-errors.d.ts +64 -0
  249. package/dist/shared/domain-errors.d.ts.map +1 -1
  250. package/dist/shared/domain-errors.js +83 -0
  251. package/dist/shared/domain-errors.js.map +1 -1
  252. package/dist/shared/git.contract.d.ts +30 -0
  253. package/dist/shared/git.contract.d.ts.map +1 -1
  254. package/dist/shared/git.contract.js +26 -0
  255. package/dist/shared/git.contract.js.map +1 -1
  256. package/dist/tenancy/static-tenant-source.d.ts +0 -1
  257. package/dist/tenancy/static-tenant-source.d.ts.map +1 -1
  258. package/dist/tenancy/static-tenant-source.js +0 -2
  259. package/dist/tenancy/static-tenant-source.js.map +1 -1
  260. package/migrations/0014_change_request_closed_reason.sql +1 -0
  261. package/migrations/0015_account_deactivation.sql +3 -0
  262. package/migrations/meta/0014_snapshot.json +2265 -0
  263. package/migrations/meta/0015_snapshot.json +2271 -0
  264. package/migrations/meta/_journal.json +14 -0
  265. package/package.json +3 -3
  266. package/src/__tests__/kb-layout-config.test.ts +0 -2
  267. package/src/__tests__/retired-settings.test.ts +96 -0
  268. package/src/core/__tests__/gated-boot.test.ts +132 -0
  269. package/src/core/__tests__/lifecycle.test.ts +170 -1
  270. package/src/core/__tests__/set-aside-root-is-one-place.test.ts +63 -0
  271. package/src/core/core-ports.ts +11 -1
  272. package/src/core/create-core-server.ts +69 -15
  273. package/src/core/create-core-services.ts +190 -27
  274. package/src/core/lifecycle.ts +98 -13
  275. package/src/core-config.ts +13 -14
  276. package/src/index.ts +12 -1
  277. package/src/modules/access/__tests__/access-control.preview-relocation.test.ts +385 -0
  278. package/src/modules/access/__tests__/access-control.prospective.test.ts +94 -18
  279. package/src/modules/access/__tests__/access-requests.recut.test.ts +134 -0
  280. package/src/modules/access/__tests__/access-requests.routes.test.ts +610 -0
  281. package/src/modules/access/__tests__/access.routes.prospective.test.ts +6 -7
  282. package/src/modules/access/access-control.interface.ts +43 -12
  283. package/src/modules/access/access-control.service.ts +234 -36
  284. package/src/modules/access/access-requests.contract.ts +93 -0
  285. package/src/modules/access/access-requests.routes.ts +286 -0
  286. package/src/modules/access/access-requests.service.ts +420 -0
  287. package/src/modules/access/access.routes.ts +13 -15
  288. package/src/modules/access-model/access-grammar.ts +22 -0
  289. package/src/modules/agent-instructions/__tests__/agent-instructions.route.test.ts +3 -1
  290. package/src/modules/auth/__tests__/account-deactivation.test.ts +252 -0
  291. package/src/modules/auth/__tests__/account.routes.test.ts +81 -3
  292. package/src/modules/auth/__tests__/auth.middleware.test.ts +45 -22
  293. package/src/modules/auth/__tests__/auth.service.test.ts +39 -1
  294. package/src/modules/auth/account-admission.ts +78 -9
  295. package/src/modules/auth/account.routes.ts +63 -7
  296. package/src/modules/auth/auth.middleware.ts +19 -8
  297. package/src/modules/auth/auth.routes.ts +8 -0
  298. package/src/modules/auth/auth.service.ts +207 -7
  299. package/src/modules/auth/oidc-auth-provider.ts +7 -2
  300. package/src/modules/database/core-schema.ts +35 -27
  301. package/src/modules/github-app/__tests__/github-app.test.ts +848 -0
  302. package/src/modules/github-app/github-app.client.ts +268 -0
  303. package/src/modules/github-app/github-app.connection.ts +204 -0
  304. package/src/modules/github-app/github-app.routes.ts +359 -0
  305. package/src/modules/github-app/index.ts +19 -0
  306. package/src/modules/kb-fs/__tests__/branch-name.test.ts +10 -0
  307. package/src/modules/kb-fs/__tests__/remote-url.test.ts +38 -0
  308. package/src/modules/kb-fs/__tests__/repo-path.test.ts +123 -0
  309. package/src/modules/kb-fs/branch-name.ts +14 -1
  310. package/src/modules/kb-fs/remote-url.ts +35 -0
  311. package/src/modules/kb-fs/repo-path.ts +85 -0
  312. package/src/modules/kb-sync/__tests__/kb-sync.routes.test.ts +26 -1
  313. package/src/modules/kb-sync/kb-sync.routes.ts +14 -4
  314. package/src/modules/kb-sync/sync-auth.ts +2 -2
  315. package/src/modules/mcp/__tests__/mcp-auth.middleware.test.ts +60 -3
  316. package/src/modules/mcp/__tests__/mcp.service.test.ts +68 -6
  317. package/src/modules/mcp/mcp-auth.middleware.ts +20 -6
  318. package/src/modules/mcp/oauth/bevel-oauth-provider.ts +3 -1
  319. package/src/modules/plugins/__tests__/join-proposals.test.ts +100 -19
  320. package/src/modules/plugins/__tests__/join-requests.service.test.ts +27 -11
  321. package/src/modules/plugins/__tests__/join-requests.settlement.test.ts +371 -0
  322. package/src/modules/plugins/__tests__/plugins.routes.test.ts +1 -1
  323. package/src/modules/plugins/__tests__/plugins.tools.test.ts +2 -1
  324. package/src/modules/plugins/join-proposals.ts +87 -19
  325. package/src/modules/plugins/join-requests.service.ts +199 -39
  326. package/src/modules/plugins/plugins.routes.ts +3 -2
  327. package/src/modules/settings/__tests__/deployment-settings.service.test.ts +16 -0
  328. package/src/modules/settings/__tests__/repository-source.test.ts +191 -0
  329. package/src/modules/settings/__tests__/setup.routes.git-mode.test.ts +386 -0
  330. package/src/modules/settings/__tests__/setup.routes.github-app.test.ts +299 -0
  331. package/src/modules/settings/__tests__/setup.routes.managed-phase.test.ts +236 -0
  332. package/src/modules/settings/__tests__/setup.routes.repository-change.test.ts +390 -0
  333. package/src/modules/settings/__tests__/setup.routes.test.ts +3 -0
  334. package/src/modules/settings/deployment-settings.service.ts +117 -6
  335. package/src/modules/settings/managed-repository.ts +68 -0
  336. package/src/modules/settings/repository-source.ts +197 -0
  337. package/src/modules/settings/setup.routes.ts +463 -15
  338. package/src/modules/skills/__tests__/allowed-tools-warn.tools.test.ts +2 -1
  339. package/src/modules/skills/__tests__/branch-skills.tools.test.ts +218 -0
  340. package/src/modules/skills/__tests__/skill-access-requests.routes.test.ts +5 -1
  341. package/src/modules/skills/__tests__/skills.service.test.ts +229 -5
  342. package/src/modules/skills/skill-access-requests.routes.ts +25 -75
  343. package/src/modules/skills/skills.contract.ts +91 -18
  344. package/src/modules/skills/skills.service.ts +278 -41
  345. package/src/modules/skills/skills.tools.ts +80 -8
  346. package/src/modules/tool-auth/__tests__/manual-auth.middleware.test.ts +14 -1
  347. package/src/modules/tool-auth/external-api-key.service.ts +4 -2
  348. package/src/modules/tool-auth/internal-token.service.ts +3 -3
  349. package/src/modules/tool-auth/tool-auth.middleware.ts +34 -11
  350. package/src/modules/tool-helpers/__tests__/agent-roles-write.test.ts +5 -3
  351. package/src/modules/tool-helpers/__tests__/phase4-tools.test.ts +3 -3
  352. package/src/modules/tool-helpers/__tests__/validate-token.test.ts +11 -1
  353. package/src/modules/tool-helpers/tool-context.ts +15 -0
  354. package/src/modules/tool-helpers/tool-handler.ts +24 -4
  355. package/src/modules/tool-helpers/tool.contract.ts +3 -2
  356. package/src/modules/tool-helpers/validate-token.ts +1 -1
  357. package/src/modules/workflow/__tests__/apply-failure.test.ts +9 -1
  358. package/src/modules/workflow/__tests__/pending-commits.repository-replaced.test.ts +111 -0
  359. package/src/modules/workflow/__tests__/workflow.routes.history-read-gate.test.ts +25 -0
  360. package/src/modules/workflow/__tests__/workflow.service.facade.test.ts +159 -8
  361. package/src/modules/workflow/__tests__/workflow.service.repository-replaced.test.ts +249 -0
  362. package/src/modules/workflow/__tests__/workflow.service.update-from-target.test.ts +146 -40
  363. package/src/modules/workflow/agent-tools/__tests__/workflow.tools.test.ts +405 -7
  364. package/src/modules/workflow/agent-tools/change-request-summary.ts +182 -0
  365. package/src/modules/workflow/agent-tools/workflow.tools.ts +135 -38
  366. package/src/modules/workflow/file-lock.service.ts +31 -0
  367. package/src/modules/workflow/git/__tests__/git.service.fileBytesAtCommit.test.ts +260 -0
  368. package/src/modules/workflow/git/__tests__/git.service.pull.test.ts +121 -0
  369. package/src/modules/workflow/git/__tests__/pull-request.service.getPrDetail.test.ts +145 -2
  370. package/src/modules/workflow/git/__tests__/pull-request.service.viewer-can-delete.test.ts +166 -0
  371. package/src/modules/workflow/git/git.service.ts +193 -4
  372. package/src/modules/workflow/git/node-git-runner.ts +6 -0
  373. package/src/modules/workflow/git/pull-request.service.ts +119 -0
  374. package/src/modules/workflow/pending-commits.service.ts +59 -1
  375. package/src/modules/workflow/workflow-hooks.ts +64 -26
  376. package/src/modules/workflow/workflow.routes.ts +3 -1
  377. package/src/modules/workflow/workflow.service.ts +290 -55
  378. package/src/modules/workspace/__tests__/agent-access.gate.test.ts +208 -0
  379. package/src/modules/workspace/__tests__/escape-sequences.routes.test.ts +384 -0
  380. package/src/modules/workspace/__tests__/file-stat-access.test.ts +2 -1
  381. package/src/modules/workspace/__tests__/git-internals.security.test.ts +2 -1
  382. package/src/modules/workspace/__tests__/set-aside-clone.test.ts +52 -0
  383. package/src/modules/workspace/__tests__/workspace.routes.at-ref.test.ts +305 -0
  384. package/src/modules/workspace/__tests__/workspace.routes.download.test.ts +2 -0
  385. package/src/modules/workspace/__tests__/workspace.service.forget-clone-races.test.ts +147 -0
  386. package/src/modules/workspace/__tests__/workspace.service.replaced-repository.test.ts +326 -0
  387. package/src/modules/workspace/__tests__/workspace.service.test.ts +20 -9
  388. package/src/modules/workspace/__tests__/workspace.service.unknown-branch.test.ts +43 -0
  389. package/src/modules/workspace/__tests__/workspace.tools.agents-file.test.ts +2 -3
  390. package/src/modules/workspace/__tests__/workspace.tools.branch-errors.test.ts +235 -10
  391. package/src/modules/workspace/__tests__/workspace.tools.test.ts +720 -17
  392. package/src/modules/workspace/agent-access.gate.ts +164 -0
  393. package/src/modules/workspace/routine-write-policy.ts +5 -6
  394. package/src/modules/workspace/session-sink.ts +5 -5
  395. package/src/modules/workspace/set-aside-clone.ts +96 -0
  396. package/src/modules/workspace/startup/__tests__/kb-startup-runner.test.ts +297 -0
  397. package/src/modules/workspace/startup/kb-startup-runner.ts +242 -2
  398. package/src/modules/workspace/workspace.routes.ts +95 -3
  399. package/src/modules/workspace/workspace.service.ts +273 -25
  400. package/src/modules/workspace/workspace.tools.ts +373 -119
  401. package/src/modules/write-access/__tests__/write-access.test.ts +248 -0
  402. package/src/modules/write-access/write-access.ts +153 -0
  403. package/src/shared/domain-errors.ts +89 -0
  404. package/src/shared/git.contract.ts +37 -0
  405. package/src/tenancy/__tests__/static-tenant-source.test.ts +0 -1
  406. package/src/tenancy/static-tenant-source.ts +0 -3
  407. package/dist/modules/workflow/session-ontology.policy.d.ts +0 -52
  408. package/dist/modules/workflow/session-ontology.policy.d.ts.map +0 -1
  409. package/dist/modules/workflow/session-ontology.policy.js +0 -62
  410. package/dist/modules/workflow/session-ontology.policy.js.map +0 -1
  411. package/dist/modules/workflow/session-ontology.service.d.ts +0 -105
  412. package/dist/modules/workflow/session-ontology.service.d.ts.map +0 -1
  413. package/dist/modules/workflow/session-ontology.service.js +0 -147
  414. package/dist/modules/workflow/session-ontology.service.js.map +0 -1
  415. package/dist/modules/workspace/session-ontology.gate.d.ts +0 -114
  416. package/dist/modules/workspace/session-ontology.gate.d.ts.map +0 -1
  417. package/dist/modules/workspace/session-ontology.gate.js +0 -161
  418. package/dist/modules/workspace/session-ontology.gate.js.map +0 -1
  419. package/dist/shared/kb-layout.d.ts +0 -39
  420. package/dist/shared/kb-layout.d.ts.map +0 -1
  421. package/dist/shared/kb-layout.js +0 -103
  422. package/dist/shared/kb-layout.js.map +0 -1
  423. package/dist/shared/kb-layout.test.d.ts +0 -2
  424. package/dist/shared/kb-layout.test.d.ts.map +0 -1
  425. package/dist/shared/kb-layout.test.js +0 -75
  426. package/dist/shared/kb-layout.test.js.map +0 -1
  427. package/src/modules/workflow/__tests__/session-ontology.policy.test.ts +0 -62
  428. package/src/modules/workflow/__tests__/session-ontology.service.test.ts +0 -201
  429. package/src/modules/workflow/session-ontology.policy.ts +0 -70
  430. package/src/modules/workflow/session-ontology.service.ts +0 -183
  431. package/src/modules/workspace/__tests__/session-ontology.gate.test.ts +0 -239
  432. package/src/modules/workspace/session-ontology.gate.ts +0 -191
  433. package/src/shared/kb-layout.test.ts +0 -98
  434. package/src/shared/kb-layout.ts +0 -102
@@ -29,7 +29,7 @@ import { promises as fs } from 'node:fs';
29
29
  import { execFile } from 'node:child_process';
30
30
  import { promisify } from 'node:util';
31
31
  import { isFolderPlaceholder, folderPlaceholderPath, isPlatformRestoreShape, } from '@bevel-software/platform-shared';
32
- import { and, eq, inArray, isNotNull, isNull, lt, or } from 'drizzle-orm';
32
+ import { and, desc, eq, inArray, isNotNull, isNull, lt, ne, or, sql } from 'drizzle-orm';
33
33
  import { changeRequests } from '../database/schema.js';
34
34
  import { redactSecret } from '../../shared/redact-secret.js';
35
35
  import { workspaceIdForBranch, branchForWorkspaceId } from '../../shared/workspace-id.js';
@@ -249,9 +249,9 @@ export class WorkflowService {
249
249
  * the composition root registers module-owned behavior against it
250
250
  * (`core.workflowService.hooks.onCommitValidation(…)` /
251
251
  * `.onPreWrite(…)`) after `createCoreServices` returns. The composition
252
- * root passes the SAME instance `GitService` and the session-ontology
253
- * gate consult; the default keeps test constructions (which don't
254
- * exercise hooks) unchanged.
252
+ * root passes the SAME instance `GitService` and the agent-access gate
253
+ * consult; the default keeps test constructions (which don't exercise
254
+ * hooks) unchanged.
255
255
  */
256
256
  hooks = new WorkflowHooks()) {
257
257
  this.db = db;
@@ -470,8 +470,8 @@ export class WorkflowService {
470
470
  * and nothing else. The pull itself reports the change, since it is the
471
471
  * only code that can observe it under the workspace mutex.
472
472
  */
473
- async pullWorkspace(workspaceId) {
474
- const { treeChanged } = await this.git.pull(workspaceId);
473
+ async pullWorkspace(workspaceId, opts = {}) {
474
+ const { treeChanged } = await this.git.pull(workspaceId, opts);
475
475
  const defaultBranch = this.kb.defaultBranch;
476
476
  if (treeChanged && branchForWorkspaceId(workspaceId) === defaultBranch) {
477
477
  this.events?.emit({ kind: 'fs-tree-changed', workspaceId, branch: defaultBranch });
@@ -756,6 +756,16 @@ export class WorkflowService {
756
756
  fileAtChange(workspaceId, path, sha) {
757
757
  return this.git.fileContentsAtCommit(workspaceId, path, sha);
758
758
  }
759
+ /**
760
+ * The BYTES of one file at a change — what the file page's byte-reading
761
+ * viewers (pdf, image, docx, pptx, xlsx, email) need to show a past save,
762
+ * and what "Download this version" hands over. `fileAtChange` above cannot
763
+ * serve them: it decodes to UTF-8, which ruins every format that is not
764
+ * text.
765
+ */
766
+ fileBytesAtChange(workspaceId, path, sha, side) {
767
+ return this.git.fileBytesAtCommit(workspaceId, path, sha, side);
768
+ }
759
769
  /**
760
770
  * Resolve write permission for `targetPath` against the access tree at
761
771
  * `HEAD` of `branch`. Throws `AccessDeniedError` on denial with the
@@ -1427,6 +1437,30 @@ export class WorkflowService {
1427
1437
  getChangeRequest(number) {
1428
1438
  return this.prs.getPr(number);
1429
1439
  }
1440
+ /**
1441
+ * The newest non-open change request from `sourceBranch` by `authorEmail`.
1442
+ *
1443
+ * Read straight off `change_requests` rather than through the PR listing,
1444
+ * which serves OPEN requests only: the whole point here is the closed ones.
1445
+ * `author_email` is stored lowercased at insert, so the needle is too.
1446
+ * Ordered by `closed_at` with `created_at` as the tiebreak, because a row
1447
+ * closed before the column existed (or by a path that forgot to stamp it)
1448
+ * still has a creation time and must not sort as the oldest.
1449
+ */
1450
+ async latestClosedChangeRequest(authorEmail, sourceBranch) {
1451
+ const email = authorEmail.trim().toLowerCase();
1452
+ if (!email || !sourceBranch)
1453
+ return null;
1454
+ const [row] = await this.db
1455
+ .select({ number: changeRequests.number, state: changeRequests.state })
1456
+ .from(changeRequests)
1457
+ .where(and(eq(changeRequests.authorEmail, email), eq(changeRequests.sourceBranch, sourceBranch), ne(changeRequests.state, 'open')))
1458
+ .orderBy(desc(changeRequests.closedAt), desc(changeRequests.createdAt))
1459
+ .limit(1);
1460
+ if (!row)
1461
+ return null;
1462
+ return { number: row.number, state: row.state };
1463
+ }
1430
1464
  getChangeRequestDetail(number, opts) {
1431
1465
  return this.prs.getPrDetail(number, opts);
1432
1466
  }
@@ -1594,9 +1628,25 @@ export class WorkflowService {
1594
1628
  * the enforcement read one predicate. A conflicting merge is aborted
1595
1629
  * inside `mergeFromOrigin`: nothing is committed or pushed, and the branch
1596
1630
  * is exactly what it was.
1631
+ *
1632
+ * ONE detail read after the merge. The read before it exists only to
1633
+ * decide authority and to name the two branches, so it asks for no patches
1634
+ * — that is one git subprocess per changed file it does not spend. The
1635
+ * approvals carry-forward runs BEFORE the final read rather than after it,
1636
+ * against a head resolved with `resolvePrShas` (two rev-parses) instead of
1637
+ * a whole second detail, so the detail that goes back already describes the
1638
+ * approvals it carried. What used to be three detail reads is now one.
1597
1639
  */
1598
1640
  async updateFromTarget(workspaceId, user, number) {
1599
- const detail = await this.prs.getPrDetail(number, { fresh: true, workspaceId, viewerEmail: user.email });
1641
+ // No patches: all this read decides is authority, state and the two
1642
+ // branch names. Asking for patch text here would diff every changed file
1643
+ // in a request that is about to be merged and read in full anyway.
1644
+ const detail = await this.prs.getPrDetail(number, {
1645
+ fresh: true,
1646
+ workspaceId,
1647
+ viewerEmail: user.email,
1648
+ patches: false,
1649
+ });
1600
1650
  if (!detail) {
1601
1651
  throw new WorkflowValidationError(`Change request #${number} not found.`, { kind: 'change-request-not-found', number });
1602
1652
  }
@@ -1615,8 +1665,15 @@ export class WorkflowService {
1615
1665
  // merge commit that then cannot push, stranding it in the workspace. A
1616
1666
  // rebase conflict hands the stranded saves to recovery, as every other
1617
1667
  // pull does, before the refusal reaches the caller.
1668
+ //
1669
+ // `preserveMerges`: this is the one pull whose unpushed commit may be a
1670
+ // MERGE — the update that ran before this one and could not push. A plain
1671
+ // rebase replays it as cherry-picks of the target's commits, which drops
1672
+ // the target as a parent, so the branch never contains the target's head
1673
+ // however many times the update runs. Replayed with `--rebase-merges` it
1674
+ // stays a merge and the next push ends the loop.
1618
1675
  try {
1619
- await this.pullWorkspace(workspaceId);
1676
+ await this.pullWorkspace(workspaceId, { preserveMerges: true });
1620
1677
  }
1621
1678
  catch (err) {
1622
1679
  if (err instanceof PullRebaseConflictError) {
@@ -1648,21 +1705,47 @@ export class WorkflowService {
1648
1705
  if (outcome.kind === 'conflicts') {
1649
1706
  throw new ChangeRequestConflictsError(detail.branch, detail.base, outcome.paths);
1650
1707
  }
1651
- // If a new merge commit landed, push so the CR picks it up. When
1652
- // already up to date there's nothing to share — short-circuit the push.
1653
- if (!outcome.alreadyUpToDate) {
1708
+ // Push whenever the CLONE holds a commit origin has not seen — not when
1709
+ // THIS merge authored one. The two differ in exactly the case that keeps
1710
+ // a request behind for good: an earlier update merged, its push failed,
1711
+ // and the merge is sitting in the workspace. That merge already contains
1712
+ // the target, so this call's `mergeFromOrigin` reports
1713
+ // `alreadyUpToDate` and the old gate skipped the push — leaving the
1714
+ // request behind on the remote, and the next open with the same work to
1715
+ // do. Asking git what is unpushed retries it instead.
1716
+ //
1717
+ // A failed probe falls back to the old gate rather than to "push
1718
+ // anyway": `trackedPush` on a clone with nothing to push is a wasted
1719
+ // round trip on every single update.
1720
+ const hasUnpushed = await this.git
1721
+ .hasUnpushedCommits(workspaceId)
1722
+ .catch((err) => {
1723
+ log.warn(`could not tell whether change request #${number} has unpushed commits; falling back to whether this merge authored one`, { err });
1724
+ return !outcome.alreadyUpToDate;
1725
+ });
1726
+ if (hasUnpushed) {
1654
1727
  await this.trackedPush(workspaceId, user);
1655
- // The source head moved: the refusal described a revision that is gone.
1656
- await this.clearApplyFailure(number, { recordedBefore: headMovedAfter });
1657
1728
  }
1658
- this.prs.invalidateDetailCache(number);
1659
- const refreshed = await this.prs.getPrDetail(number, {
1660
- fresh: true,
1661
- workspaceId,
1662
- viewerEmail: user.email,
1663
- });
1664
- if (!refreshed) {
1665
- throw new WorkflowValidationError(`Refreshed change request #${number} but could not re-fetch its detail.`);
1729
+ // Where the branch ended up, as PUBLISHED — two rev-parses, not a second
1730
+ // detail read. Everything below needs only the sha, and the detail that
1731
+ // goes back to the caller is read once, at the end, after the approvals
1732
+ // it should describe have been carried.
1733
+ let headAfterMerge = headBeforeMerge;
1734
+ try {
1735
+ const at = await this.git.resolvePrShas(workspaceId, detail.base, detail.branch);
1736
+ headAfterMerge = at.headSha;
1737
+ }
1738
+ catch (err) {
1739
+ log.warn(`could not resolve the published head of change request #${number} after updating it from target`, { err });
1740
+ }
1741
+ const headMoved = Boolean(headBeforeMerge) && headAfterMerge !== headBeforeMerge;
1742
+ // The source head moved: the refusal described a revision that is gone.
1743
+ // `hasUnpushed` counts as well as the sha comparison, and deliberately —
1744
+ // a push that just landed moved the published head by definition, and
1745
+ // reading the sha to confirm it is a call that can fail. A refusal left
1746
+ // standing over a revision nobody can apply any more is the worse error.
1747
+ if (headMoved || hasUnpushed) {
1748
+ await this.clearApplyFailure(number, { recordedBefore: headMovedAfter });
1666
1749
  }
1667
1750
  // The merge is a commit the APPROVERS did not make. Re-pin their
1668
1751
  // approvals onto the new head for every file whose bytes it left alone —
@@ -1671,35 +1754,48 @@ export class WorkflowService {
1671
1754
  // than the bookkeeping, so a failure here is logged and the fresh detail
1672
1755
  // still goes back.
1673
1756
  //
1674
- // `alreadyUpToDate` is the real gate: no merge commit, no head to compare,
1675
- // nothing moved under anyone. The other two conditions are belt-and-braces
1676
- // over a sha that is not a sha — a resolve that failed onto a detail with
1677
- // no head, or two heads that somehow read equal — both of which
1678
- // `pathsChangedBetween` and `carryApprovalsForward` would refuse anyway.
1679
- // A merge that authored a commit cannot leave the head where it was.
1680
- if (!outcome.alreadyUpToDate && headBeforeMerge && refreshed.headSha !== headBeforeMerge) {
1757
+ // The head moving is the real gate: nothing moved, nothing moved under
1758
+ // anyone. It also covers the sha that is not a sha — a resolve that
1759
+ // failed onto a detail with no head — which `pathsChangedBetween` and
1760
+ // `carryApprovalsForward` would refuse anyway.
1761
+ //
1762
+ // This runs BEFORE the detail read, not after it. The rows it writes are
1763
+ // what the caller's `approvals` must describe, so doing it first means
1764
+ // the one read at the end is already correct — where carrying afterwards
1765
+ // needed a THIRD read to correct a detail assembled before the rows
1766
+ // existed.
1767
+ //
1768
+ // The same list answers the caller's `updatedPaths`: "which files did
1769
+ // this merge change" is one question, and the two answers must not be
1770
+ // allowed to differ — a file whose approval was carried (so: untouched)
1771
+ // that the dialog then re-read would be the dialog discarding content the
1772
+ // server just called unchanged.
1773
+ let updatedPaths = [];
1774
+ if (headMoved) {
1681
1775
  try {
1682
- const changedPaths = await this.git.pathsChangedBetween(workspaceId, headBeforeMerge, refreshed.headSha);
1683
- const carried = await this.reviewWorkflow.carryApprovalsForward(number, headBeforeMerge, refreshed.headSha, changedPaths);
1684
- // The detail above was assembled before those rows existed, so its
1685
- // `approvals` (and the merge gate derived from them) still describe
1686
- // the pre-merge head. Only re-read when something actually carried.
1687
- if (carried > 0) {
1688
- this.prs.invalidateDetailCache(number);
1689
- const withApprovals = await this.prs.getPrDetail(number, {
1690
- fresh: true,
1691
- workspaceId,
1692
- viewerEmail: user.email,
1693
- });
1694
- if (withApprovals)
1695
- return withApprovals;
1696
- }
1776
+ updatedPaths = await this.git.pathsChangedBetween(workspaceId, headBeforeMerge, headAfterMerge);
1777
+ await this.reviewWorkflow.carryApprovalsForward(number, headBeforeMerge, headAfterMerge, updatedPaths);
1697
1778
  }
1698
1779
  catch (err) {
1699
1780
  log.warn(`could not carry approvals forward on change request #${number} after updating from target`, { err });
1781
+ // The diff is what failed, so which files moved is unknown. Say so as
1782
+ // "every file this request had", which is every file a caller can
1783
+ // already be showing: slow beats a dialog that goes on presenting
1784
+ // pre-merge text as current. Anything the merge ADDED is not on the
1785
+ // list and does not need to be — no caller has read it yet.
1786
+ updatedPaths = (detail.files ?? []).map((f) => f.path);
1700
1787
  }
1701
1788
  }
1702
- return refreshed;
1789
+ this.prs.invalidateDetailCache(number);
1790
+ const refreshed = await this.prs.getPrDetail(number, {
1791
+ fresh: true,
1792
+ workspaceId,
1793
+ viewerEmail: user.email,
1794
+ });
1795
+ if (!refreshed) {
1796
+ throw new WorkflowValidationError(`Refreshed change request #${number} but could not re-fetch its detail.`);
1797
+ }
1798
+ return { ...refreshed, updatedPaths };
1703
1799
  }
1704
1800
  listComments(number) {
1705
1801
  return this.reviewWorkflow.listComments(number);
@@ -2126,11 +2222,16 @@ export class WorkflowService {
2126
2222
  /**
2127
2223
  * DELETE a change request outright: close it (whatever its diff says) and
2128
2224
  * retire its source branch — the request, its proposal, and the branch that
2129
- * carried it are gone in one verb. Admin-only, resolved the same way every
2130
- * other admin check here is (write on `roles.yaml` at `origin/<base>`,
2131
- * never a local ref): this is the moderation verb for a shared deployment,
2132
- * stronger than reject (which the author or the files' owners can do, and
2133
- * which leaves the branch for a second round).
2225
+ * carried it are gone in one verb. The request's AUTHOR or an admin, and
2226
+ * nobody else: it is the author's own proposal and their own branch to throw
2227
+ * away, and it is the moderation verb for a shared deployment. Admin rights
2228
+ * resolve the same way every other admin check here does (write on
2229
+ * `roles.yaml` at `origin/<base>`, never a local ref); authorship is the
2230
+ * STORED `authorId` hash, never anything the caller sends.
2231
+ *
2232
+ * Stronger than reject, which the changed files' owners may also do and which
2233
+ * leaves the branch for a second round. They keep reject and do not get this:
2234
+ * an owner must not be able to destroy someone else's text and branch.
2134
2235
  */
2135
2236
  async deleteChangeRequest(number, user) {
2136
2237
  const summary = await this.prs.getPr(number);
@@ -2180,8 +2281,17 @@ export class WorkflowService {
2180
2281
  this.kickDeletedBranchSweep();
2181
2282
  throw new WorkflowDomainError(`The base branch "${summary.base}" could not be resolved — it may have been deleted, or origin was unreachable. A background check is running; retry in a moment.`, 409);
2182
2283
  }
2183
- if (isAdmin !== true) {
2184
- throw new WorkflowDomainError('Only an admin can delete a change request.', 403);
2284
+ // Authorship is decided from the STORED author hash, never from anything
2285
+ // the caller sends. Note what is deliberately NOT short-circuited for an
2286
+ // author: the strict fetch and the `null` check above still run. An
2287
+ // author's grant needs no `roles.yaml` read, so skipping them would look
2288
+ // free — but it would let a delete close the request while the branch
2289
+ // removal silently failed against an unreachable origin, and "the shared
2290
+ // repository was down, so nothing changed" is the one refusal that has to
2291
+ // stay honest.
2292
+ const callerIsAuthor = !!(summary.authorId && summary.authorId === hashEmail(user.email));
2293
+ if (!callerIsAuthor && isAdmin !== true) {
2294
+ throw new WorkflowDomainError("Only the request's author or an admin can delete it.", 403);
2185
2295
  }
2186
2296
  // Close first (idempotent: an already-closed request just skips to the
2187
2297
  // branch retirement). Guarded on `open` so a concurrent merge wins.
@@ -2375,6 +2485,103 @@ export class WorkflowService {
2375
2485
  }
2376
2486
  return closed;
2377
2487
  }
2488
+ /**
2489
+ * How many change requests are open right now.
2490
+ *
2491
+ * Read by the setup screen's repository-change confirmation, which has to
2492
+ * say how many requests the admin is deciding about BEFORE the address is
2493
+ * saved. A plain count — nothing here reaches the repository, so it answers
2494
+ * just as well for a repository that is already gone.
2495
+ */
2496
+ async countOpenChangeRequests() {
2497
+ const [row] = await this.db
2498
+ .select({ open: sql `count(*)::int` })
2499
+ .from(changeRequests)
2500
+ .where(eq(changeRequests.state, 'open'));
2501
+ return row?.open ?? 0;
2502
+ }
2503
+ /**
2504
+ * Close every open change request because the knowledge-base REPOSITORY was
2505
+ * replaced, and answer how many were closed.
2506
+ *
2507
+ * The admin's own choice on the setup screen: a repository that merely
2508
+ * MOVED keeps its requests (the branches came along), while a repository
2509
+ * that was REPLACED has none of those branches, so every request points at
2510
+ * something this deployment can no longer fetch. Nothing is deleted — the
2511
+ * rows stay, closed, carrying `closed_reason = 'repository-replaced'` so
2512
+ * the reason survives the person who chose it.
2513
+ *
2514
+ * Locks held on those branches go with them. Their holder cannot publish
2515
+ * the bytes (the branch is not in the new repository) and cannot release
2516
+ * the lock by finishing (the request is closed), so the row would sit there
2517
+ * refusing the path until its TTL ran out — and a still-connected client's
2518
+ * heartbeat keeps pushing that out.
2519
+ *
2520
+ * The close is ONE statement per pass — `update … where state = 'open'
2521
+ * returning` — not a count followed by a row-at-a-time loop. Read first and
2522
+ * then written, every request opened while the loop ran would be missed and
2523
+ * left open against a branch the new repository does not have. One
2524
+ * set-shaped statement cannot miss a row that exists when it runs, and the
2525
+ * guard is the statement's own `where`, so a merge or a withdrawal racing it
2526
+ * wins and its request is simply not in the returned set.
2527
+ *
2528
+ * Repeated while a pass still finds something, because a request can be
2529
+ * opened BETWEEN passes: the caller has not saved the new address yet, so
2530
+ * the deployment is still live and `POST /change-requests` is still being
2531
+ * served. Bounded, so a client opening requests in a loop cannot hold the
2532
+ * save open forever — what survives the last pass is a request opened after
2533
+ * the repository was replaced, which the deleted-branch sweep closes on its
2534
+ * own terms.
2535
+ */
2536
+ async closeOpenChangeRequestsAsRepositoryReplaced() {
2537
+ let closed = 0;
2538
+ for (let pass = 0; pass < 5; pass++) {
2539
+ const now = new Date();
2540
+ const justClosed = await this.db
2541
+ .update(changeRequests)
2542
+ .set({ state: 'closed', closedAt: now, updatedAt: now, closedReason: 'repository-replaced' })
2543
+ .where(eq(changeRequests.state, 'open'))
2544
+ .returning({ number: changeRequests.number, sourceBranch: changeRequests.sourceBranch });
2545
+ if (justClosed.length === 0)
2546
+ break;
2547
+ for (const cr of justClosed) {
2548
+ // Best effort, per branch: a lock table that will not answer must not
2549
+ // leave the requests half closed — the close is the decision the admin
2550
+ // made, and the locks expire on their own within the minute.
2551
+ try {
2552
+ const released = await this.fileLocks.releaseAllOnBranch(cr.sourceBranch);
2553
+ if (released > 0) {
2554
+ crLog.info(`released ${released} file lock(s) held on "${cr.sourceBranch}"`);
2555
+ }
2556
+ }
2557
+ catch (err) {
2558
+ crLog.warn(`could not release the file locks on "${cr.sourceBranch}":`, { err });
2559
+ }
2560
+ // The bytes a release already enqueued belong to the repository that
2561
+ // is gone — see `markNeedsAttentionOnBranch`. Taken out of the
2562
+ // worker's reach in the same breath as the locks, because dropping a
2563
+ // lock is precisely what leaves a row behind.
2564
+ try {
2565
+ const quarantined = await this.pendingCommits.markNeedsAttentionOnBranch(cr.sourceBranch, 'The knowledge-base repository was replaced while this commit was still queued, so it was never ' +
2566
+ 'written. The bytes are kept here: the branch it was meant for belongs to the previous repository.');
2567
+ if (quarantined > 0) {
2568
+ crLog.warn(`${quarantined} queued commit(s) on "${cr.sourceBranch}" need attention: the repository was replaced ` +
2569
+ 'before they landed, so they were not written to the new one.');
2570
+ }
2571
+ }
2572
+ catch (err) {
2573
+ crLog.warn(`could not set aside the queued commits on "${cr.sourceBranch}":`, { err });
2574
+ }
2575
+ this.prs.invalidateDetailCache(cr.number);
2576
+ this.events?.emit({ kind: 'change-request-rejected', number: cr.number });
2577
+ closed++;
2578
+ }
2579
+ }
2580
+ if (closed > 0) {
2581
+ crLog.info(`closed ${closed} change request(s): the knowledge-base repository was replaced`);
2582
+ }
2583
+ return closed;
2584
+ }
2378
2585
  /**
2379
2586
  * The sweep above, on demand. Boot runs it once (`create-core-server`),
2380
2587
  * which clears requests stranded BEFORE the process started; a branch that