@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
@@ -35,10 +35,12 @@ import {
35
35
  ProtectedBranchError,
36
36
  PullRebaseConflictError,
37
37
  RemoteBranchGoneError,
38
+ VersionNotOnBranchError,
38
39
  isMissingRemoteBranchFailure,
39
40
  } from '../../../shared/domain-errors.js';
40
41
  import {
41
42
  GitRunError,
43
+ assertOneSpecPerLine,
42
44
  isGitTimeout,
43
45
  redactGitToken,
44
46
  type GitCredentials,
@@ -1760,7 +1762,12 @@ export class GitService implements IGitService {
1760
1762
  * conflict into the typed error the workflow layer queues recovery for.
1761
1763
  * Shared by `pull` and `syncFromRemote` so the two cannot drift.
1762
1764
  */
1763
- private async rebaseOntoRemote(cwd: string, branch: string, remoteRef: string): Promise<void> {
1765
+ private async rebaseOntoRemote(
1766
+ cwd: string,
1767
+ branch: string,
1768
+ remoteRef: string,
1769
+ opts: { preserveMerges?: boolean } = {},
1770
+ ): Promise<void> {
1764
1771
  // An UNBORN HEAD (a clone of an upstream that was empty when cloned) has
1765
1772
  // no commit to replay and `git rebase` refuses to start from it, so the
1766
1773
  // branch is moved to the remote commit instead. That is only safe while
@@ -1798,7 +1805,20 @@ export class GitService implements IGitService {
1798
1805
  // the local commits onto origin, then reapplies them, so the retry
1799
1806
  // push can fast-forward. Untracked files don't block rebase and are
1800
1807
  // left untouched.
1801
- await this.git(cwd, ['rebase', '--autostash', remoteRef]);
1808
+ // `--rebase-merges` is what keeps an unpushed MERGE a merge. Plain
1809
+ // rebase replays `--no-merges upstream..HEAD`, which turns a merge
1810
+ // commit the clone holds but origin has not seen into cherry-picks of
1811
+ // the commits it merged — the merged branch stops being a parent, so
1812
+ // "does the published head contain the target's head" answers no
1813
+ // however many times the update runs. Only the change-request update
1814
+ // asks for it; every other pull replays plain saves, where the two
1815
+ // spellings produce the same history.
1816
+ await this.git(cwd, [
1817
+ 'rebase',
1818
+ '--autostash',
1819
+ ...(opts.preserveMerges ? ['--rebase-merges'] : []),
1820
+ remoteRef,
1821
+ ]);
1802
1822
  } catch (err) {
1803
1823
  // Capture the contested paths BEFORE the abort wipes the rebase
1804
1824
  // state — `--diff-filter=U` lists exactly the files whose replay
@@ -1922,7 +1942,10 @@ export class GitService implements IGitService {
1922
1942
  });
1923
1943
  }
1924
1944
 
1925
- async pull(workspaceId: string): Promise<{ treeChanged: boolean }> {
1945
+ async pull(
1946
+ workspaceId: string,
1947
+ opts: { preserveMerges?: boolean } = {},
1948
+ ): Promise<{ treeChanged: boolean }> {
1926
1949
  return this.mutex.run(workspaceId, async () => {
1927
1950
  const cwd = await this.repoDir(workspaceId);
1928
1951
  const branch = await this.checkedOutBranch(cwd);
@@ -1935,7 +1958,7 @@ export class GitService implements IGitService {
1935
1958
  // Refresh WITHOUT `git pull`, and without touching `FETCH_HEAD` — see
1936
1959
  // `refreshRemoteBranchRef` for why both halves are load-bearing.
1937
1960
  const remoteRef = await this.refreshRemoteBranchRef(cwd, branch);
1938
- await this.rebaseOntoRemote(cwd, branch, remoteRef);
1961
+ await this.rebaseOntoRemote(cwd, branch, remoteRef, opts);
1939
1962
  this.accessControl?.invalidate(workspaceId);
1940
1963
  const treeAfter = (await this.revParseOrNull(cwd, 'HEAD^{tree}')) ?? '';
1941
1964
  return { treeChanged: treeAfter !== treeBefore };
@@ -2310,6 +2333,10 @@ export class GitService implements IGitService {
2310
2333
  const repoRelativePath = this.stripRepoPrefix(relativePath);
2311
2334
  return this.mutex.run(workspaceId, async () => {
2312
2335
  const cwd = await this.repoDir(workspaceId);
2336
+ // Branch-scoped, like the bytes route: a patch is a past version too,
2337
+ // and leaving this route open would let a sha refused there be read
2338
+ // here instead.
2339
+ await this.assertOnBranchHistory(cwd, sha);
2313
2340
  // BOTH sides: at a commit that DELETES a directory the path is absent
2314
2341
  // at `sha` but a tree at `sha^` — and the diff of that deletion is
2315
2342
  // every child's content, exactly what the per-file gate never checked.
@@ -2361,6 +2388,10 @@ export class GitService implements IGitService {
2361
2388
  const repoRelativePath = this.stripRepoPrefix(relativePath);
2362
2389
  return this.mutex.run(workspaceId, async () => {
2363
2390
  const cwd = await this.repoDir(workspaceId);
2391
+ // Same branch rule as the patch and the bytes: this route serves the
2392
+ // FULL text of both sides, so it is the most generous of the three and
2393
+ // the least safe one to leave ungated.
2394
+ await this.assertOnBranchHistory(cwd, sha);
2364
2395
  await this.assertNotTreeAtRef(cwd, sha, repoRelativePath, relativePath);
2365
2396
  await this.assertNotTreeAtRef(cwd, `${sha}^`, repoRelativePath, relativePath);
2366
2397
  const current = await this.readFileAtRef(workspaceId, sha, repoRelativePath);
@@ -3248,6 +3279,143 @@ export class GitService implements IGitService {
3248
3279
  }
3249
3280
  }
3250
3281
 
3282
+ /**
3283
+ * A repo-relative file's BYTES at an arbitrary git ref — the binary sibling
3284
+ * of {@link readFileAtRef}, with the blob's object id alongside them.
3285
+ *
3286
+ * `readFileAtRef` decodes stdout as UTF-8, which for a PNG or a PDF is not a
3287
+ * lossless round trip: every byte sequence that is not valid UTF-8 comes
3288
+ * back as U+FFFD and the file is ruined. A past version has to be delivered
3289
+ * byte for byte, so this reads through `cat-file --batch`, whose framing is
3290
+ * in BYTES (`<oid> <type> <size>\n<size bytes>\n`) rather than characters.
3291
+ *
3292
+ * One invocation, and absence is a PROTOCOL answer (`<spec> missing`) rather
3293
+ * than a parsed error message — which also covers `<sha>^` of a root commit,
3294
+ * where there is no "before" side at all. `null` therefore means exactly
3295
+ * "nothing is at that path on that side".
3296
+ *
3297
+ * The `oid` is the content's own hash, so it is what the HTTP route uses as
3298
+ * an ETag: cheaper than hashing the body again, and stable for a version
3299
+ * that by definition can never change.
3300
+ */
3301
+ private async blobAtRef(
3302
+ cwd: string,
3303
+ ref: string,
3304
+ repoRelativePath: string,
3305
+ ): Promise<{ bytes: Buffer; blobId: string } | null> {
3306
+ // The spec has to be ONE line: the path is refused a control character
3307
+ // twice over before it gets here (at the route's normaliser and again at
3308
+ // `assertValidRelativePath`), and this is the fence at the protocol —
3309
+ // `--batch` would otherwise read `<sha>:Docs/x.md\nzzz` as two object
3310
+ // names and answer about the first, under the gates of the second.
3311
+ const spec = `${ref}:${repoRelativePath}`;
3312
+ assertOneSpecPerLine([spec]);
3313
+ const { stdout } = await this.gitBytes(cwd, ['cat-file', '--batch'], {
3314
+ input: `${spec}\n`,
3315
+ });
3316
+ const nl = stdout.indexOf(0x0a);
3317
+ if (nl < 0) throw new Error('unexpected end of git cat-file output');
3318
+ // The spec may itself contain spaces (a KB file routinely does), hence the
3319
+ // endsWith checks rather than a split — same protocol reading as the
3320
+ // access resolver's `catFileBatch`.
3321
+ const header = stdout.subarray(0, nl).toString('utf-8');
3322
+ if (header.endsWith(' missing') || header.endsWith(' ambiguous')) return null;
3323
+ const [blobId, type, rawSize] = header.split(' ');
3324
+ // A tree here would be the per-file gate bypassed, and `assertNotTreeAtRef`
3325
+ // is what refuses it before this runs; anything else non-blob (a tag, a
3326
+ // commit) is not a file either. Nothing to serve in either case.
3327
+ if (type !== 'blob' || blobId === undefined) return null;
3328
+ const size = Number(rawSize);
3329
+ if (!Number.isSafeInteger(size) || size < 0) {
3330
+ throw new Error(`unexpected git cat-file header: ${header}`);
3331
+ }
3332
+ return { bytes: stdout.subarray(nl + 1, nl + 1 + size), blobId };
3333
+ }
3334
+
3335
+ /**
3336
+ * Refuse a sha that is not in the history of the branch this workspace has
3337
+ * checked out.
3338
+ *
3339
+ * A file's past is served per BRANCH: the history panel lists what `git log`
3340
+ * on this workspace's own branch touched, and every read of a past save has
3341
+ * to stay inside that list. Without this, a sha lifted from another branch —
3342
+ * or from a change request nobody approved — would be readable through the
3343
+ * workspace of a branch whose history never contained it, which is the
3344
+ * access rules of the viewed branch bypassed by a query parameter.
3345
+ *
3346
+ * `merge-base --is-ancestor` is the exact question, and a commit is its own
3347
+ * ancestor, so the branch head passes. Exit 1 is git's answer for "resolved,
3348
+ * and not an ancestor". An unresolvable object name — a made-up id — gets
3349
+ * the SAME refusal on purpose (see {@link VersionNotOnBranchError}), but only
3350
+ * when git's complaint NAMES that id; every other failure propagates, a
3351
+ * broken repository included. A guard that fails open on its own errors is
3352
+ * not a guard, and one that reports them as a refusal is worse than no
3353
+ * message at all.
3354
+ */
3355
+ private async assertOnBranchHistory(cwd: string, sha: string): Promise<void> {
3356
+ try {
3357
+ await this.git(cwd, ['merge-base', '--is-ancestor', sha, 'HEAD']);
3358
+ } catch (err) {
3359
+ if (err instanceof GitRunError && !err.timedOut) {
3360
+ if (err.exitCode === 1) throw new VersionNotOnBranchError();
3361
+ const stderr = err.stderr ?? err.message;
3362
+ if (
3363
+ // The complaint has to be about THE SHA WE ASKED FOR. `HEAD` is the
3364
+ // other argument, and git spells its own failures the same way: a
3365
+ // repository with no commits answers "ambiguous argument 'HEAD'",
3366
+ // which is a broken workspace, not a version off the branch.
3367
+ // Refusing it as "not in this file's history" would hide an
3368
+ // infrastructure failure behind a sentence about access.
3369
+ stderr.toLowerCase().includes(sha.toLowerCase()) &&
3370
+ // git's wording for "I cannot resolve that" varies by subcommand and
3371
+ // by version — `merge-base` says "Not a valid commit name", others
3372
+ // "not a valid object name" / "unknown revision" — so every spelling
3373
+ // that means the same thing is listed rather than one guessed at.
3374
+ /not a valid (object|commit) name|no such commit|bad object|unknown revision|malformed object name|ambiguous argument/i.test(
3375
+ stderr,
3376
+ )
3377
+ ) {
3378
+ throw new VersionNotOnBranchError();
3379
+ }
3380
+ }
3381
+ throw err;
3382
+ }
3383
+ }
3384
+
3385
+ /**
3386
+ * One file's BYTES at a save, exactly as they were — the `after` side of
3387
+ * `sha`, or the `before` side (`<sha>^`) for the save that deleted it.
3388
+ *
3389
+ * Every guard the text history surfaces apply, in the same order: the sha is
3390
+ * a sha, the save is on this branch, and the pathspec is not a directory at
3391
+ * that ref (a per-file gate upstream authorized one path, and a tree read
3392
+ * would hand over every child). `null` means the file was not there on the
3393
+ * side asked for.
3394
+ *
3395
+ * Runs inside the workspace's git turn — one turn for the whole read, so the
3396
+ * ancestry check and the bytes it authorizes cannot be separated by another
3397
+ * caller's checkout.
3398
+ */
3399
+ async fileBytesAtCommit(
3400
+ workspaceId: string,
3401
+ relativePath: string,
3402
+ sha: string,
3403
+ side: 'after' | 'before',
3404
+ ): Promise<{ bytes: Buffer; blobId: string } | null> {
3405
+ assertValidRelativePath(relativePath);
3406
+ if (!/^[a-f0-9]{7,40}$/i.test(sha)) {
3407
+ throw new WorkflowValidationError('invalid commit sha');
3408
+ }
3409
+ const repoRelativePath = this.stripRepoPrefix(relativePath);
3410
+ return this.mutex.run(workspaceId, async () => {
3411
+ const cwd = await this.repoDir(workspaceId);
3412
+ await this.assertOnBranchHistory(cwd, sha);
3413
+ const ref = side === 'before' ? `${sha}^` : sha;
3414
+ await this.assertNotTreeAtRef(cwd, ref, repoRelativePath, relativePath);
3415
+ return this.blobAtRef(cwd, ref, repoRelativePath);
3416
+ });
3417
+ }
3418
+
3251
3419
  /**
3252
3420
  * The commits on `ref` that touched `repoRelativePath`, newest first, at
3253
3421
  * most `limit` of them. What a skill's `version` is looked up through: each
@@ -3356,6 +3524,27 @@ export class GitService implements IGitService {
3356
3524
  env: { GIT_LITERAL_PATHSPECS: '1', ...opts?.env },
3357
3525
  });
3358
3526
  }
3527
+
3528
+ /**
3529
+ * {@link git}, with stdout as raw BYTES.
3530
+ *
3531
+ * A sibling rather than an `encoding` option on `git()` itself: every other
3532
+ * caller in this service reads text, and the two return types (`string` vs
3533
+ * `Buffer`) are not interchangeable at any call site. Same
3534
+ * `GIT_LITERAL_PATHSPECS` and same runner deadline — the only difference is
3535
+ * that the bytes are not decoded.
3536
+ */
3537
+ private gitBytes(
3538
+ cwd: string,
3539
+ args: string[],
3540
+ opts?: Omit<GitRunOptions, 'encoding'>,
3541
+ ): Promise<GitRunResult<Buffer>> {
3542
+ return this.gitRunner.run(cwd, args, {
3543
+ ...opts,
3544
+ encoding: 'buffer',
3545
+ env: { GIT_LITERAL_PATHSPECS: '1', ...opts?.env },
3546
+ });
3547
+ }
3359
3548
  }
3360
3549
 
3361
3550
  /** Map a git status letter (`git diff --name-status`) to the DTO's `PrFileStatus`. */
@@ -250,6 +250,12 @@ export class NodeGitRunner implements IGitRunner {
250
250
  const timers: NodeJS.Timeout[] = [];
251
251
  let timedOut = false;
252
252
 
253
+ // A credential that expires is renewed before it is read. Renewing is
254
+ // the credential's own business and its failure is not this call's:
255
+ // plenty of git never leaves the disk, and a call that does is told by
256
+ // the host what the token in hand is worth.
257
+ await this.credentials.prepare?.().catch(() => undefined);
258
+
253
259
  try {
254
260
  const { promise: pending, child } = spawnGit(args, {
255
261
  cwd,
@@ -366,6 +366,60 @@ export class PullRequestService implements IPullRequestService {
366
366
  return this.summaryOf(row, workspaceId);
367
367
  }
368
368
 
369
+ /**
370
+ * Did the target change, since the fork point, any file this request also
371
+ * changes? The question behind `needsUpdate`.
372
+ *
373
+ * The intersection is taken over BOTH names of every rename on both sides,
374
+ * because a wrong "no" is the expensive answer: it would open a request on
375
+ * a diff read against text that has since moved, with Approve live. So
376
+ * `pathsChangedBetween` runs without rename detection (a rename on the
377
+ * TARGET reports the path it left as well as the one it arrived at), and
378
+ * the request's own side contributes each file's `previousPath` alongside
379
+ * its `path`.
380
+ *
381
+ * Answers true without asking git in the two cases where nothing may be
382
+ * assumed: no fork point (the branches share no history, so there is no
383
+ * range to diff) and a failed diff (an infra error must not read as "this
384
+ * request is fine"). Answers false without asking git when the request is
385
+ * not behind at all, or changes no files — the common case, and the one
386
+ * that must stay free.
387
+ */
388
+ private async targetTouchesRequestFiles(
389
+ workspaceId: string,
390
+ forkPoint: { mergeBaseSha: string | null; behind: boolean },
391
+ baseSha: string,
392
+ files: PullRequestFile[],
393
+ ): Promise<boolean> {
394
+ if (!forkPoint.behind) return false;
395
+ if (files.length === 0) return false;
396
+ // Diverged with no common ancestor: the target's whole history is
397
+ // "changed since the fork point", so anything this request touches is
398
+ // touched. Nothing to intersect against, and nothing to skip for.
399
+ if (!forkPoint.mergeBaseSha) return true;
400
+ const requestPaths = new Set<string>();
401
+ for (const f of files) {
402
+ requestPaths.add(f.path);
403
+ if (f.previousPath) requestPaths.add(f.previousPath);
404
+ }
405
+ try {
406
+ const onTarget = await this.gitService.pathsChangedBetween(
407
+ workspaceId,
408
+ forkPoint.mergeBaseSha,
409
+ // The target tip the WHOLE detail is pinned to, so this range ends
410
+ // exactly where `behind` and the file list were computed.
411
+ baseSha,
412
+ );
413
+ return onTarget.some((path) => requestPaths.has(path));
414
+ } catch (err) {
415
+ log.warn(
416
+ 'could not compare the target against a change request\'s files; treating it as needing an update',
417
+ { err },
418
+ );
419
+ return true;
420
+ }
421
+ }
422
+
369
423
  async getPrDetail(
370
424
  prNumber: number,
371
425
  opts: { fresh?: boolean; workspaceId?: string; viewerEmail?: string; patches?: boolean } = {},
@@ -392,6 +446,8 @@ export class PullRequestService implements IPullRequestService {
392
446
  mergeBaseSha: null,
393
447
  behind: false,
394
448
  };
449
+ /** Did the target change, since the fork point, a file this request changes? */
450
+ let targetChangedShared = false;
395
451
  if (workspaceId) {
396
452
  const shas = await this.gitService.resolvePrShas(
397
453
  workspaceId,
@@ -417,6 +473,17 @@ export class PullRequestService implements IPullRequestService {
417
473
  // Pinned to the same two commits as the file list, so "needs updating"
418
474
  // and the diff it qualifies can never describe different heads.
419
475
  forkPoint = await this.gitService.forkPointForPr(workspaceId, { baseSha, headSha });
476
+ // Only a target that moved in a file THIS request also changes makes the
477
+ // proposal's diff describe text that has moved. One extra
478
+ // `diff --name-only` (and only when the branches have diverged at all)
479
+ // buys the difference between opening instantly and paying for a merge,
480
+ // a push and a second detail read.
481
+ targetChangedShared = await this.targetTouchesRequestFiles(
482
+ workspaceId,
483
+ forkPoint,
484
+ baseSha,
485
+ files,
486
+ );
420
487
  }
421
488
 
422
489
  // Validated cache hit: TTL fresh AND head SHA unchanged since we cached.
@@ -511,6 +578,22 @@ export class PullRequestService implements IPullRequestService {
511
578
  approvals,
512
579
  });
513
580
 
581
+ // Hashed here, not on the client: the detail carries an author HASH and no
582
+ // author email, so the comparison can only happen where the viewer's email
583
+ // is already known.
584
+ const viewerIsAuthor = !!(
585
+ opts.viewerEmail &&
586
+ summary.authorId &&
587
+ summary.authorId === hashEmail(opts.viewerEmail)
588
+ );
589
+
590
+ const viewerCanDelete = computeViewerCanDelete({
591
+ state: summary.state,
592
+ authorId: summary.authorId,
593
+ viewerEmail: opts.viewerEmail,
594
+ viewerCanBypassMerge,
595
+ });
596
+
514
597
  const detail: PullRequestDetail = {
515
598
  ...summary,
516
599
  body: row.body,
@@ -526,7 +609,12 @@ export class PullRequestService implements IPullRequestService {
526
609
  viewerCanCancel,
527
610
  mergeBaseSha: forkPoint.mergeBaseSha,
528
611
  behind: summary.state === 'open' && forkPoint.behind,
612
+ // `behind` AND the divergence actually reaches this request's files.
613
+ // Both halves, in this order, so a closed request is never either.
614
+ needsUpdate: summary.state === 'open' && forkPoint.behind && targetChangedShared,
529
615
  viewerCanUpdate,
616
+ viewerIsAuthor,
617
+ viewerCanDelete,
530
618
  };
531
619
 
532
620
  // A patch-less detail is an internal read; it must not be served to the
@@ -631,7 +719,38 @@ export function computeViewerCanUpdate(input: {
631
719
  return viewerIsAuthor || input.viewerCanBypassMerge || viewerMayApply;
632
720
  }
633
721
 
722
+ /**
723
+ * Pure predicate for the `viewerCanDelete` hint — who may delete a request
724
+ * outright, closing it AND retiring its branch. The author (their own
725
+ * proposal and their own branch) or an admin (`viewerCanBypassMerge` is the
726
+ * proxy — the same `canWriteAtRef('roles.yaml')` predicate the DELETE route
727
+ * enforces). Mirrors `deleteChangeRequest` exactly, which is why it differs
728
+ * from its two siblings in both directions:
729
+ *
730
+ * - It does NOT grant the owner of every changed file, as
731
+ * `computeViewerCanCancel` does. Declining someone else's request leaves
732
+ * their branch to rework; deleting it destroys their text.
733
+ * - It bars only `merged`, not everything that is not `open`. A request
734
+ * withdrawn in another tab is `closed` and still has a leftover branch the
735
+ * server will retire, so the button must survive it. Applied history is
736
+ * nobody's to delete.
737
+ *
738
+ * Fail-closed: no viewer email → false, whatever the grants say.
739
+ */
740
+ export function computeViewerCanDelete(input: {
741
+ state: PullRequestState;
742
+ authorId: string | undefined;
743
+ viewerEmail: string | undefined;
744
+ viewerCanBypassMerge: boolean;
745
+ }): boolean {
746
+ if (input.state === 'merged') return false;
747
+ if (!input.viewerEmail) return false;
748
+ const viewerIsAuthor = !!(input.authorId && input.authorId === hashEmail(input.viewerEmail));
749
+ return viewerIsAuthor || input.viewerCanBypassMerge;
750
+ }
751
+
634
752
  export const __testing = {
635
753
  computeViewerCanCancel,
636
754
  computeViewerCanUpdate,
755
+ computeViewerCanDelete,
637
756
  };
@@ -34,7 +34,7 @@
34
34
  * (status='needs_attention')
35
35
  */
36
36
 
37
- import { and, eq, inArray, isNull, min, or, sql } from 'drizzle-orm';
37
+ import { and, eq, inArray, isNull, min, or, sql, type SQL } from 'drizzle-orm';
38
38
  import { logger } from '../../shared/logging.js';
39
39
 
40
40
  const log = logger('pending-commits');
@@ -384,6 +384,64 @@ export class PendingCommitsService {
384
384
  .where(eq(pendingCommits.id, id));
385
385
  }
386
386
 
387
+ /**
388
+ * Take every commit still queued for `branch` out of the worker's reach and
389
+ * onto the admin surface, and answer how many moved.
390
+ *
391
+ * The one caller is the knowledge-base repository being REPLACED. A release
392
+ * ENQUEUES the bytes and only then drops its lock, so the moment the locks
393
+ * on a branch are dropped there may be rows already waiting — bytes that
394
+ * were written against a working copy of the repository that is gone. Left
395
+ * `pending`, the worker would pick them up after the re-clone and commit
396
+ * them onto a same-named branch of a DIFFERENT repository: someone else's
397
+ * file, overwritten by a change nobody made to it.
398
+ *
399
+ * `needs_attention`, not deleted — the same rule as everything else about a
400
+ * replacement. The bytes are still in the row, the reason is on it, and they
401
+ * show up on the pending-commit admin surface for a person to decide about.
402
+ * `running` rows go too: the worker checks the row back in when it finishes,
403
+ * and a row it is mid-flight with is exactly one whose destination just
404
+ * changed under it.
405
+ */
406
+ async markNeedsAttentionOnBranch(branch: string, error: string): Promise<number> {
407
+ return this.escalateQueued(eq(pendingCommits.branch, branch), error);
408
+ }
409
+
410
+ /** Every commit still queued that `which` selects goes to a person, with the reason. Answers how many. */
411
+ private async escalateQueued(which: SQL, error: string): Promise<number> {
412
+ const moved = await this.db
413
+ .update(pendingCommits)
414
+ .set({
415
+ status: 'needs_attention',
416
+ lastError: error,
417
+ lastAttemptedAt: new Date(),
418
+ })
419
+ .where(and(which, inArray(pendingCommits.status, ['pending', 'running'])))
420
+ .returning({ id: pendingCommits.id });
421
+ return moved.length;
422
+ }
423
+
424
+ /**
425
+ * The same, for every commit still queued against ONE WORKING COPY, and
426
+ * answer how many moved.
427
+ *
428
+ * The caller is the startup phase setting a working copy aside: a clone of
429
+ * a repository the configured one shares no history with. Whatever was
430
+ * queued for it was written against that clone, on any branch, the default
431
+ * one included, and the directory the worker would write to is now a fresh
432
+ * clone of another repository. Closing change requests covers the branches
433
+ * of the requests that were closed and nothing else, which is why this is
434
+ * keyed on the working copy: it is the thing that was replaced.
435
+ *
436
+ * A working copy that was only pointed at a new address (the same
437
+ * repository, moved) keeps its queue, and is never passed here.
438
+ */
439
+ async markNeedsAttentionInWorkspace(workspaceId: string, error: string): Promise<number> {
440
+ // The canonical form `enqueue` stores (see `canonicalWorkspaceId`), so the
441
+ // caller's spelling of the id cannot make this match nothing.
442
+ return this.escalateQueued(eq(pendingCommits.workspaceId, canonicalWorkspaceId(workspaceId)), error);
443
+ }
444
+
387
445
  /**
388
446
  * When the oldest commit still waiting to land was queued, or null when
389
447
  * nothing waits — the readiness answer's one number. `pending` and
@@ -1,14 +1,13 @@
1
- import type { ValidationReport } from '@bevel-software/platform-shared';
1
+ import type { AuthUser, ValidationReport } from '@bevel-software/platform-shared';
2
2
 
3
3
  /**
4
4
  * Workflow lifecycle hooks — the generic seam that replaced the
5
- * constructor-injected KB validator (and the core-owned ontology write block).
6
- * Core code invokes the hooks at fixed lifecycle points; the modules that OWN
7
- * the behavior (the enterprise kb/kb-graph modules today) register handlers in
8
- * the composition root, right after `createCoreServices` returns. Core
9
- * registers none, so a core-only deployment runs every hook point as a no-op.
5
+ * constructor-injected KB validator. Core code invokes the hooks at fixed
6
+ * lifecycle points; the modules that OWN the behavior register handlers in the
7
+ * composition root, right after `createCoreServices` returns. Core registers
8
+ * none, so a core-only deployment runs every hook point as a no-op.
10
9
  *
11
- * Two hook kinds with deliberately different failure semantics:
10
+ * Three hook kinds, with deliberately different failure semantics:
12
11
  *
13
12
  * - `commitValidation` — ADVISORY. Runs at commit time in `GitService`
14
13
  * (`commit` / `commitFile`), exactly where the injected `IKbValidator`
@@ -18,12 +17,18 @@ import type { ValidationReport } from '@bevel-software/platform-shared';
18
17
  * `runValidation(workspaceId)` shape so `KbValidatorService` registers
19
18
  * with a one-line adapter.
20
19
  *
21
- * - `preWrite` — BLOCKING. Runs before a gated agent write (file-write
22
- * tools) or a write-capable shell command; a hook that throws REJECTS the
23
- * operation (the error propagates to the tool caller). This is where the
24
- * enterprise ontology-session block registers; core registers nothing, so
25
- * core never blocks (touch TRACKING stays core — see
26
- * `session-ontology.gate.ts`).
20
+ * - `agentRead` — BLOCKING. Runs before a gated agent READ of a
21
+ * knowledge-base path (reads, `delete_file`, `delete_folder`, and each
22
+ * file a `grep` walk opens). A hook that throws REFUSES the read.
23
+ *
24
+ * - `preWrite` — BLOCKING. Runs before a gated agent WRITE (the file-write
25
+ * tools, once per path) or a write-capable shell command, which carries no
26
+ * path. A hook that throws REFUSES the operation, and the caller sees the
27
+ * hook's own message and status.
28
+ *
29
+ * The two blocking hooks are the seam a deployment builds a per-conversation
30
+ * rule on: they carry the session id the call named, and core itself neither
31
+ * decides nor records anything (see `workspace/agent-access.gate.ts`).
27
32
  */
28
33
 
29
34
  /** What `GitService` knows at the advisory commit-validation point. */
@@ -47,19 +52,35 @@ export type CommitValidationHook = (
47
52
  ) => Promise<ValidationReport | void>;
48
53
 
49
54
  /**
50
- * What the pre-write gate knows when it consults the blocking hooks. The core
51
- * gate has already resolved all skip conditions (boundary flag off, neutral
52
- * path, non-agent caller, recovery bot, missing session id — see
53
- * `resolveGatedSession`), so by hook time there is always a session id.
55
+ * What an agent operation looks like to the blocking hooks. The core gate has
56
+ * already resolved the only two skips it owns (the caller is not an agent, or
57
+ * is the recovery bot), so every hook call describes a real agent operation.
58
+ *
59
+ * `sessionId` is present only when the call carried one — core does NOT fail
60
+ * closed on its absence, because whether a path needs a session is the
61
+ * registering deployment's rule, not core's.
54
62
  */
55
- export type PreWriteContext =
56
- /** A path-addressed content write (write_file/edit_file/mkdir/move/copy/unzip…). */
57
- | { kind: 'file-write'; sessionId: string; wsPath: string }
58
- /** A write-capable operation with no resolvable target path (execute_command). */
59
- | { kind: 'shell'; sessionId: string };
63
+ export interface AgentOperationContext {
64
+ /** The conversation the call belongs to, when it named one. */
65
+ sessionId?: string;
66
+ /**
67
+ * The workspace-relative path the operation targets. Absent for a
68
+ * write-capable operation with no resolvable target (`execute_command`).
69
+ */
70
+ wsPath?: string;
71
+ /** The branch (draft) whose workspace the operation acts on. */
72
+ branch: string;
73
+ /** The calling user. */
74
+ user: AuthUser;
75
+ /** Whether the caller is the in-app agent (`internal`) or an external one. */
76
+ source: 'internal' | 'external';
77
+ }
60
78
 
61
- /** Blocking pre-write hook: throw to reject the write; return to allow. */
62
- export type PreWriteHook = (ctx: PreWriteContext) => Promise<void>;
79
+ /** Blocking pre-read hook: throw to refuse the read; return to allow. */
80
+ export type AgentReadHook = (ctx: AgentOperationContext) => Promise<void>;
81
+
82
+ /** Blocking pre-write hook: throw to refuse the write; return to allow. */
83
+ export type PreWriteHook = (ctx: AgentOperationContext) => Promise<void>;
63
84
 
64
85
  /**
65
86
  * The registry instance. ONE per composition (created in
@@ -68,6 +89,7 @@ export type PreWriteHook = (ctx: PreWriteContext) => Promise<void>;
68
89
  */
69
90
  export class WorkflowHooks {
70
91
  private readonly commitValidation: CommitValidationHook[] = [];
92
+ private readonly agentRead: AgentReadHook[] = [];
71
93
  private readonly preWrite: PreWriteHook[] = [];
72
94
 
73
95
  /** Register an advisory commit-time validation hook. */
@@ -75,6 +97,11 @@ export class WorkflowHooks {
75
97
  this.commitValidation.push(hook);
76
98
  }
77
99
 
100
+ /** Register a blocking pre-read hook. */
101
+ onAgentRead(hook: AgentReadHook): void {
102
+ this.agentRead.push(hook);
103
+ }
104
+
78
105
  /** Register a blocking pre-write hook. */
79
106
  onPreWrite(hook: PreWriteHook): void {
80
107
  this.preWrite.push(hook);
@@ -88,12 +115,23 @@ export class WorkflowHooks {
88
115
  return this.commitValidation;
89
116
  }
90
117
 
118
+ /**
119
+ * Run every blocking pre-read hook in registration order. The first throw
120
+ * propagates and refuses the read; with no hooks registered (core-only)
121
+ * this resolves immediately.
122
+ */
123
+ async runAgentRead(ctx: AgentOperationContext): Promise<void> {
124
+ for (const hook of this.agentRead) {
125
+ await hook(ctx);
126
+ }
127
+ }
128
+
91
129
  /**
92
130
  * Run every blocking pre-write hook in registration order. The first throw
93
- * propagates and rejects the write; with no hooks registered (core-only)
131
+ * propagates and refuses the write; with no hooks registered (core-only)
94
132
  * this resolves immediately.
95
133
  */
96
- async runPreWrite(ctx: PreWriteContext): Promise<void> {
134
+ async runPreWrite(ctx: AgentOperationContext): Promise<void> {
97
135
  for (const hook of this.preWrite) {
98
136
  await hook(ctx);
99
137
  }
@@ -790,7 +790,9 @@ export function createWorkflowRoutes(
790
790
 
791
791
  /**
792
792
  * Delete a change request outright — close it and retire its source
793
- * branch. Admin-only; the moderation verb for a shared deployment.
793
+ * branch. The request's author or an admin: their own proposal to throw
794
+ * away, and the moderation verb for a shared deployment. The changed files'
795
+ * owners are NOT included — they keep reject, which leaves the branch.
794
796
  */
795
797
  router.delete('/workflow/change-requests/:number', async (req, res) => {
796
798
  const num = parsePrNumber(req.params.number);