@bevel-software/platform-core-backend 0.22.0 → 0.24.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 (466) 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 +46 -13
  7. package/dist/core/create-core-server.js.map +1 -1
  8. package/dist/core/create-core-services.d.ts +12 -2
  9. package/dist/core/create-core-services.d.ts.map +1 -1
  10. package/dist/core/create-core-services.js +137 -24
  11. package/dist/core/create-core-services.js.map +1 -1
  12. package/dist/core/lifecycle.d.ts +46 -1
  13. package/dist/core/lifecycle.d.ts.map +1 -1
  14. package/dist/core/lifecycle.js +99 -13
  15. package/dist/core/lifecycle.js.map +1 -1
  16. package/dist/core-config.d.ts +16 -12
  17. package/dist/core-config.d.ts.map +1 -1
  18. package/dist/core-config.js +28 -13
  19. package/dist/core-config.js.map +1 -1
  20. package/dist/index.d.ts +6 -4
  21. package/dist/index.d.ts.map +1 -1
  22. package/dist/index.js +12 -3
  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.routes.d.ts.map +1 -1
  31. package/dist/modules/access/access.routes.js +17 -16
  32. package/dist/modules/access/access.routes.js.map +1 -1
  33. package/dist/modules/access/directory-sync-bot.d.ts.map +1 -1
  34. package/dist/modules/access/directory-sync-bot.js +7 -3
  35. package/dist/modules/access/directory-sync-bot.js.map +1 -1
  36. package/dist/modules/agent-instructions/agent-instructions.routes.d.ts +8 -1
  37. package/dist/modules/agent-instructions/agent-instructions.routes.d.ts.map +1 -1
  38. package/dist/modules/agent-instructions/agent-instructions.routes.js +8 -2
  39. package/dist/modules/agent-instructions/agent-instructions.routes.js.map +1 -1
  40. package/dist/modules/agent-instructions/compose.d.ts +38 -6
  41. package/dist/modules/agent-instructions/compose.d.ts.map +1 -1
  42. package/dist/modules/agent-instructions/compose.js +39 -6
  43. package/dist/modules/agent-instructions/compose.js.map +1 -1
  44. package/dist/modules/agent-instructions/index.d.ts +2 -1
  45. package/dist/modules/agent-instructions/index.d.ts.map +1 -1
  46. package/dist/modules/agent-instructions/index.js +2 -1
  47. package/dist/modules/agent-instructions/index.js.map +1 -1
  48. package/dist/modules/agent-instructions/shared-file-rules.d.ts +115 -0
  49. package/dist/modules/agent-instructions/shared-file-rules.d.ts.map +1 -0
  50. package/dist/modules/agent-instructions/shared-file-rules.js +272 -0
  51. package/dist/modules/agent-instructions/shared-file-rules.js.map +1 -0
  52. package/dist/modules/audit/agent-audit.service.d.ts.map +1 -1
  53. package/dist/modules/audit/agent-audit.service.js +4 -2
  54. package/dist/modules/audit/agent-audit.service.js.map +1 -1
  55. package/dist/modules/auth/account-admission.d.ts +58 -7
  56. package/dist/modules/auth/account-admission.d.ts.map +1 -1
  57. package/dist/modules/auth/account-admission.js +44 -1
  58. package/dist/modules/auth/account-admission.js.map +1 -1
  59. package/dist/modules/auth/account-erasure.service.d.ts.map +1 -1
  60. package/dist/modules/auth/account-erasure.service.js +57 -16
  61. package/dist/modules/auth/account-erasure.service.js.map +1 -1
  62. package/dist/modules/auth/account.routes.d.ts +1 -1
  63. package/dist/modules/auth/account.routes.d.ts.map +1 -1
  64. package/dist/modules/auth/account.routes.js +61 -5
  65. package/dist/modules/auth/account.routes.js.map +1 -1
  66. package/dist/modules/auth/auth.middleware.d.ts +1 -1
  67. package/dist/modules/auth/auth.middleware.d.ts.map +1 -1
  68. package/dist/modules/auth/auth.middleware.js +18 -7
  69. package/dist/modules/auth/auth.middleware.js.map +1 -1
  70. package/dist/modules/auth/auth.routes.d.ts.map +1 -1
  71. package/dist/modules/auth/auth.routes.js +8 -0
  72. package/dist/modules/auth/auth.routes.js.map +1 -1
  73. package/dist/modules/auth/auth.service.d.ts +70 -2
  74. package/dist/modules/auth/auth.service.d.ts.map +1 -1
  75. package/dist/modules/auth/auth.service.js +197 -18
  76. package/dist/modules/auth/auth.service.js.map +1 -1
  77. package/dist/modules/auth/oidc-auth-provider.d.ts.map +1 -1
  78. package/dist/modules/auth/oidc-auth-provider.js +7 -2
  79. package/dist/modules/auth/oidc-auth-provider.js.map +1 -1
  80. package/dist/modules/code-mode/code-mode.tool.d.ts +20 -2
  81. package/dist/modules/code-mode/code-mode.tool.d.ts.map +1 -1
  82. package/dist/modules/code-mode/code-mode.tool.js +66 -35
  83. package/dist/modules/code-mode/code-mode.tool.js.map +1 -1
  84. package/dist/modules/database/connection.d.ts +16 -0
  85. package/dist/modules/database/connection.d.ts.map +1 -1
  86. package/dist/modules/database/connection.js +117 -0
  87. package/dist/modules/database/connection.js.map +1 -1
  88. package/dist/modules/database/core-schema.d.ts +337 -120
  89. package/dist/modules/database/core-schema.d.ts.map +1 -1
  90. package/dist/modules/database/core-schema.js +116 -58
  91. package/dist/modules/database/core-schema.js.map +1 -1
  92. package/dist/modules/database/migrate.d.ts +8 -0
  93. package/dist/modules/database/migrate.d.ts.map +1 -1
  94. package/dist/modules/database/migrate.js +271 -1
  95. package/dist/modules/database/migrate.js.map +1 -1
  96. package/dist/modules/kb-fs/branch-name.d.ts.map +1 -1
  97. package/dist/modules/kb-fs/branch-name.js +12 -2
  98. package/dist/modules/kb-fs/branch-name.js.map +1 -1
  99. package/dist/modules/kb-fs/repo-path.d.ts +11 -16
  100. package/dist/modules/kb-fs/repo-path.d.ts.map +1 -1
  101. package/dist/modules/kb-fs/repo-path.js +79 -0
  102. package/dist/modules/kb-fs/repo-path.js.map +1 -1
  103. package/dist/modules/kb-sync/kb-sync.routes.d.ts +2 -2
  104. package/dist/modules/kb-sync/kb-sync.routes.d.ts.map +1 -1
  105. package/dist/modules/kb-sync/kb-sync.routes.js +14 -3
  106. package/dist/modules/kb-sync/kb-sync.routes.js.map +1 -1
  107. package/dist/modules/kb-sync/sync-auth.d.ts +3 -1
  108. package/dist/modules/kb-sync/sync-auth.d.ts.map +1 -1
  109. package/dist/modules/kb-sync/sync-auth.js +1 -1
  110. package/dist/modules/kb-sync/sync-auth.js.map +1 -1
  111. package/dist/modules/mcp/mcp-auth.middleware.d.ts.map +1 -1
  112. package/dist/modules/mcp/mcp-auth.middleware.js +21 -6
  113. package/dist/modules/mcp/mcp-auth.middleware.js.map +1 -1
  114. package/dist/modules/mcp/mcp.service.d.ts +8 -0
  115. package/dist/modules/mcp/mcp.service.d.ts.map +1 -1
  116. package/dist/modules/mcp/mcp.service.js +38 -8
  117. package/dist/modules/mcp/mcp.service.js.map +1 -1
  118. package/dist/modules/mcp/oauth/bevel-oauth-provider.d.ts.map +1 -1
  119. package/dist/modules/mcp/oauth/bevel-oauth-provider.js +3 -2
  120. package/dist/modules/mcp/oauth/bevel-oauth-provider.js.map +1 -1
  121. package/dist/modules/plugins/join-request-records.store.d.ts.map +1 -1
  122. package/dist/modules/plugins/join-request-records.store.js +7 -4
  123. package/dist/modules/plugins/join-request-records.store.js.map +1 -1
  124. package/dist/modules/settings/deployment-settings.service.d.ts.map +1 -1
  125. package/dist/modules/settings/deployment-settings.service.js +8 -3
  126. package/dist/modules/settings/deployment-settings.service.js.map +1 -1
  127. package/dist/modules/settings/setup.routes.d.ts +52 -1
  128. package/dist/modules/settings/setup.routes.d.ts.map +1 -1
  129. package/dist/modules/settings/setup.routes.js +207 -19
  130. package/dist/modules/settings/setup.routes.js.map +1 -1
  131. package/dist/modules/skills/skills.contract.d.ts +90 -18
  132. package/dist/modules/skills/skills.contract.d.ts.map +1 -1
  133. package/dist/modules/skills/skills.contract.js +4 -1
  134. package/dist/modules/skills/skills.contract.js.map +1 -1
  135. package/dist/modules/skills/skills.service.d.ts +98 -9
  136. package/dist/modules/skills/skills.service.d.ts.map +1 -1
  137. package/dist/modules/skills/skills.service.js +239 -37
  138. package/dist/modules/skills/skills.service.js.map +1 -1
  139. package/dist/modules/skills/skills.tools.d.ts +7 -0
  140. package/dist/modules/skills/skills.tools.d.ts.map +1 -1
  141. package/dist/modules/skills/skills.tools.js +71 -7
  142. package/dist/modules/skills/skills.tools.js.map +1 -1
  143. package/dist/modules/tool-auth/external-api-key.service.d.ts.map +1 -1
  144. package/dist/modules/tool-auth/external-api-key.service.js +12 -4
  145. package/dist/modules/tool-auth/external-api-key.service.js.map +1 -1
  146. package/dist/modules/tool-auth/internal-token.service.d.ts +3 -3
  147. package/dist/modules/tool-auth/tool-auth.middleware.d.ts +16 -7
  148. package/dist/modules/tool-auth/tool-auth.middleware.d.ts.map +1 -1
  149. package/dist/modules/tool-auth/tool-auth.middleware.js +34 -12
  150. package/dist/modules/tool-auth/tool-auth.middleware.js.map +1 -1
  151. package/dist/modules/tool-helpers/tool-handler.d.ts +2 -1
  152. package/dist/modules/tool-helpers/tool-handler.d.ts.map +1 -1
  153. package/dist/modules/tool-helpers/tool-handler.js +25 -4
  154. package/dist/modules/tool-helpers/tool-handler.js.map +1 -1
  155. package/dist/modules/tool-helpers/tool.contract.d.ts +3 -2
  156. package/dist/modules/tool-helpers/tool.contract.d.ts.map +1 -1
  157. package/dist/modules/tool-helpers/tool.contract.js.map +1 -1
  158. package/dist/modules/tool-helpers/validate-token.js +1 -1
  159. package/dist/modules/tool-helpers/validate-token.js.map +1 -1
  160. package/dist/modules/tool-manuals/tool-manuals.tools.d.ts.map +1 -1
  161. package/dist/modules/tool-manuals/tool-manuals.tools.js +42 -31
  162. package/dist/modules/tool-manuals/tool-manuals.tools.js.map +1 -1
  163. package/dist/modules/tool-registry/description-length.d.ts +80 -0
  164. package/dist/modules/tool-registry/description-length.d.ts.map +1 -0
  165. package/dist/modules/tool-registry/description-length.js +108 -0
  166. package/dist/modules/tool-registry/description-length.js.map +1 -0
  167. package/dist/modules/workflow/agent-tools/change-request-summary.d.ts +98 -0
  168. package/dist/modules/workflow/agent-tools/change-request-summary.d.ts.map +1 -0
  169. package/dist/modules/workflow/agent-tools/change-request-summary.js +81 -0
  170. package/dist/modules/workflow/agent-tools/change-request-summary.js.map +1 -0
  171. package/dist/modules/workflow/agent-tools/workflow.tools.d.ts.map +1 -1
  172. package/dist/modules/workflow/agent-tools/workflow.tools.js +75 -35
  173. package/dist/modules/workflow/agent-tools/workflow.tools.js.map +1 -1
  174. package/dist/modules/workflow/file-lock.service.d.ts +24 -0
  175. package/dist/modules/workflow/file-lock.service.d.ts.map +1 -1
  176. package/dist/modules/workflow/file-lock.service.js +30 -0
  177. package/dist/modules/workflow/file-lock.service.js.map +1 -1
  178. package/dist/modules/workflow/git/git.service.d.ts +94 -0
  179. package/dist/modules/workflow/git/git.service.d.ts.map +1 -1
  180. package/dist/modules/workflow/git/git.service.js +193 -3
  181. package/dist/modules/workflow/git/git.service.js.map +1 -1
  182. package/dist/modules/workflow/pending-commits.service.d.ts +38 -0
  183. package/dist/modules/workflow/pending-commits.service.d.ts.map +1 -1
  184. package/dist/modules/workflow/pending-commits.service.js +60 -1
  185. package/dist/modules/workflow/pending-commits.service.js.map +1 -1
  186. package/dist/modules/workflow/recovery-bot.d.ts.map +1 -1
  187. package/dist/modules/workflow/recovery-bot.js +7 -3
  188. package/dist/modules/workflow/recovery-bot.js.map +1 -1
  189. package/dist/modules/workflow/review-workflow/review-workflow.service.d.ts.map +1 -1
  190. package/dist/modules/workflow/review-workflow/review-workflow.service.js +12 -3
  191. package/dist/modules/workflow/review-workflow/review-workflow.service.js.map +1 -1
  192. package/dist/modules/workflow/workflow-hooks.d.ts +54 -32
  193. package/dist/modules/workflow/workflow-hooks.d.ts.map +1 -1
  194. package/dist/modules/workflow/workflow-hooks.js +16 -1
  195. package/dist/modules/workflow/workflow-hooks.js.map +1 -1
  196. package/dist/modules/workflow/workflow.service.d.ts +60 -6
  197. package/dist/modules/workflow/workflow.service.d.ts.map +1 -1
  198. package/dist/modules/workflow/workflow.service.js +115 -5
  199. package/dist/modules/workflow/workflow.service.js.map +1 -1
  200. package/dist/modules/workspace/agent-access.gate.d.ts +94 -0
  201. package/dist/modules/workspace/agent-access.gate.d.ts.map +1 -0
  202. package/dist/modules/workspace/agent-access.gate.js +123 -0
  203. package/dist/modules/workspace/agent-access.gate.js.map +1 -0
  204. package/dist/modules/workspace/agent-upload.routes.d.ts +77 -0
  205. package/dist/modules/workspace/agent-upload.routes.d.ts.map +1 -0
  206. package/dist/modules/workspace/agent-upload.routes.js +210 -0
  207. package/dist/modules/workspace/agent-upload.routes.js.map +1 -0
  208. package/dist/modules/workspace/agent-upload.store.d.ts +284 -0
  209. package/dist/modules/workspace/agent-upload.store.d.ts.map +1 -0
  210. package/dist/modules/workspace/agent-upload.store.js +553 -0
  211. package/dist/modules/workspace/agent-upload.store.js.map +1 -0
  212. package/dist/modules/workspace/routine-write-policy.d.ts +5 -6
  213. package/dist/modules/workspace/routine-write-policy.d.ts.map +1 -1
  214. package/dist/modules/workspace/routine-write-policy.js +5 -6
  215. package/dist/modules/workspace/routine-write-policy.js.map +1 -1
  216. package/dist/modules/workspace/session-sink.d.ts +5 -5
  217. package/dist/modules/workspace/set-aside-clone.d.ts +46 -0
  218. package/dist/modules/workspace/set-aside-clone.d.ts.map +1 -0
  219. package/dist/modules/workspace/set-aside-clone.js +92 -0
  220. package/dist/modules/workspace/set-aside-clone.js.map +1 -0
  221. package/dist/modules/workspace/startup/kb-startup-runner.d.ts +59 -9
  222. package/dist/modules/workspace/startup/kb-startup-runner.d.ts.map +1 -1
  223. package/dist/modules/workspace/startup/kb-startup-runner.js +65 -24
  224. package/dist/modules/workspace/startup/kb-startup-runner.js.map +1 -1
  225. package/dist/modules/workspace/startup/steps/seed-tree.d.ts.map +1 -1
  226. package/dist/modules/workspace/startup/steps/seed-tree.js +3 -3
  227. package/dist/modules/workspace/startup/steps/seed-tree.js.map +1 -1
  228. package/dist/modules/workspace/startup/steps/template-source.d.ts +40 -0
  229. package/dist/modules/workspace/startup/steps/template-source.d.ts.map +1 -1
  230. package/dist/modules/workspace/startup/steps/template-source.js +46 -4
  231. package/dist/modules/workspace/startup/steps/template-source.js.map +1 -1
  232. package/dist/modules/workspace/upload-limits.d.ts +13 -0
  233. package/dist/modules/workspace/upload-limits.d.ts.map +1 -0
  234. package/dist/modules/workspace/upload-limits.js +13 -0
  235. package/dist/modules/workspace/upload-limits.js.map +1 -0
  236. package/dist/modules/workspace/workspace.routes.d.ts.map +1 -1
  237. package/dist/modules/workspace/workspace.routes.js +93 -4
  238. package/dist/modules/workspace/workspace.routes.js.map +1 -1
  239. package/dist/modules/workspace/workspace.service.d.ts +128 -5
  240. package/dist/modules/workspace/workspace.service.d.ts.map +1 -1
  241. package/dist/modules/workspace/workspace.service.js +316 -58
  242. package/dist/modules/workspace/workspace.service.js.map +1 -1
  243. package/dist/modules/workspace/workspace.tools.d.ts +13 -11
  244. package/dist/modules/workspace/workspace.tools.d.ts.map +1 -1
  245. package/dist/modules/workspace/workspace.tools.js +791 -198
  246. package/dist/modules/workspace/workspace.tools.js.map +1 -1
  247. package/dist/modules/workspace/write-denial.d.ts +0 -6
  248. package/dist/modules/workspace/write-denial.d.ts.map +1 -1
  249. package/dist/modules/workspace/write-denial.js +0 -6
  250. package/dist/modules/workspace/write-denial.js.map +1 -1
  251. package/dist/modules/workspace/zip-entry-rules.d.ts +114 -0
  252. package/dist/modules/workspace/zip-entry-rules.d.ts.map +1 -0
  253. package/dist/modules/workspace/zip-entry-rules.js +154 -0
  254. package/dist/modules/workspace/zip-entry-rules.js.map +1 -0
  255. package/dist/modules/write-access/write-access.d.ts +60 -0
  256. package/dist/modules/write-access/write-access.d.ts.map +1 -0
  257. package/dist/modules/write-access/write-access.js +129 -0
  258. package/dist/modules/write-access/write-access.js.map +1 -0
  259. package/dist/shared/column-crypto.d.ts +194 -0
  260. package/dist/shared/column-crypto.d.ts.map +1 -0
  261. package/dist/shared/column-crypto.js +144 -0
  262. package/dist/shared/column-crypto.js.map +1 -0
  263. package/dist/shared/domain-errors.d.ts +25 -0
  264. package/dist/shared/domain-errors.d.ts.map +1 -1
  265. package/dist/shared/domain-errors.js +28 -0
  266. package/dist/shared/domain-errors.js.map +1 -1
  267. package/dist/shared/git.contract.d.ts +20 -0
  268. package/dist/shared/git.contract.d.ts.map +1 -1
  269. package/dist/shared/git.contract.js +26 -0
  270. package/dist/shared/git.contract.js.map +1 -1
  271. package/dist/shared/token-crypto.d.ts.map +1 -1
  272. package/dist/shared/token-crypto.js +25 -1
  273. package/dist/shared/token-crypto.js.map +1 -1
  274. package/dist/tenancy/static-tenant-source.d.ts +0 -1
  275. package/dist/tenancy/static-tenant-source.d.ts.map +1 -1
  276. package/dist/tenancy/static-tenant-source.js +1 -2
  277. package/dist/tenancy/static-tenant-source.js.map +1 -1
  278. package/dist/tenancy/tenant-secrets.d.ts +5 -1
  279. package/dist/tenancy/tenant-secrets.d.ts.map +1 -1
  280. package/dist/tenancy/tenant-secrets.js +4 -0
  281. package/dist/tenancy/tenant-secrets.js.map +1 -1
  282. package/kb-template/AGENTS.md +2 -0
  283. package/migrations/0014_change_request_closed_reason.sql +1 -0
  284. package/migrations/0015_account_deactivation.sql +3 -0
  285. package/migrations/0016_pii_encryption.sql +20 -0
  286. package/migrations/meta/0014_snapshot.json +2265 -0
  287. package/migrations/meta/0015_snapshot.json +2271 -0
  288. package/migrations/meta/0016_snapshot.json +2327 -0
  289. package/migrations/meta/_journal.json +21 -0
  290. package/package.json +3 -3
  291. package/src/__tests__/kb-layout-config.test.ts +0 -2
  292. package/src/__tests__/retired-settings.test.ts +96 -0
  293. package/src/core/__tests__/gated-boot.test.ts +132 -0
  294. package/src/core/__tests__/lifecycle.test.ts +241 -5
  295. package/src/core/__tests__/set-aside-root-is-one-place.test.ts +63 -0
  296. package/src/core/core-ports.ts +11 -1
  297. package/src/core/create-core-server.ts +52 -16
  298. package/src/core/create-core-services.ts +156 -24
  299. package/src/core/lifecycle.ts +117 -13
  300. package/src/core-config.ts +31 -14
  301. package/src/index.ts +30 -1
  302. package/src/modules/access/__tests__/access-control.preview-relocation.test.ts +385 -0
  303. package/src/modules/access/__tests__/access-control.prospective.test.ts +94 -18
  304. package/src/modules/access/__tests__/access.routes.prospective.test.ts +6 -7
  305. package/src/modules/access/__tests__/users-db-double.ts +21 -12
  306. package/src/modules/access/access-control.interface.ts +43 -12
  307. package/src/modules/access/access-control.service.ts +234 -36
  308. package/src/modules/access/access.routes.ts +17 -16
  309. package/src/modules/access/directory-sync-bot.ts +7 -3
  310. package/src/modules/agent-instructions/__tests__/agent-instructions.route.test.ts +11 -5
  311. package/src/modules/agent-instructions/__tests__/compose.test.ts +19 -10
  312. package/src/modules/agent-instructions/__tests__/shared-file-rules.test.ts +238 -0
  313. package/src/modules/agent-instructions/agent-instructions.routes.ts +12 -2
  314. package/src/modules/agent-instructions/compose.ts +50 -7
  315. package/src/modules/agent-instructions/index.ts +12 -0
  316. package/src/modules/agent-instructions/shared-file-rules.ts +314 -0
  317. package/src/modules/audit/agent-audit.service.ts +4 -2
  318. package/src/modules/auth/__tests__/account-deactivation.test.ts +253 -0
  319. package/src/modules/auth/__tests__/account-erasure.approval-gate.test.ts +6 -2
  320. package/src/modules/auth/__tests__/account.routes.test.ts +87 -6
  321. package/src/modules/auth/__tests__/auth.middleware.test.ts +45 -22
  322. package/src/modules/auth/__tests__/auth.service.test.ts +4 -2
  323. package/src/modules/auth/account-admission.ts +78 -9
  324. package/src/modules/auth/account-erasure.service.ts +69 -18
  325. package/src/modules/auth/account.routes.ts +63 -7
  326. package/src/modules/auth/auth.middleware.ts +19 -8
  327. package/src/modules/auth/auth.routes.ts +8 -0
  328. package/src/modules/auth/auth.service.ts +206 -23
  329. package/src/modules/auth/oidc-auth-provider.ts +7 -2
  330. package/src/modules/code-mode/__tests__/chain-runtime.e2e.test.ts +335 -0
  331. package/src/modules/code-mode/__tests__/code-mode.tool.test.ts +46 -2
  332. package/src/modules/code-mode/code-mode.tool.ts +80 -34
  333. package/src/modules/database/__tests__/connection.test.ts +12 -0
  334. package/src/modules/database/__tests__/pii-backfill.pg.test.ts +561 -0
  335. package/src/modules/database/connection.ts +117 -0
  336. package/src/modules/database/core-schema.ts +116 -58
  337. package/src/modules/database/migrate.ts +353 -1
  338. package/src/modules/kb-fs/__tests__/branch-name.test.ts +10 -0
  339. package/src/modules/kb-fs/__tests__/repo-path.test.ts +123 -0
  340. package/src/modules/kb-fs/branch-name.ts +14 -1
  341. package/src/modules/kb-fs/repo-path.ts +85 -0
  342. package/src/modules/kb-sync/__tests__/kb-sync.routes.test.ts +26 -1
  343. package/src/modules/kb-sync/kb-sync.routes.ts +14 -4
  344. package/src/modules/kb-sync/sync-auth.ts +2 -2
  345. package/src/modules/mcp/__tests__/mcp-auth.middleware.test.ts +60 -3
  346. package/src/modules/mcp/__tests__/mcp.e2e.test.ts +4 -3
  347. package/src/modules/mcp/__tests__/mcp.service.test.ts +115 -13
  348. package/src/modules/mcp/mcp-auth.middleware.ts +20 -6
  349. package/src/modules/mcp/mcp.service.ts +46 -7
  350. package/src/modules/mcp/oauth/bevel-oauth-provider.ts +3 -1
  351. package/src/modules/plugins/__tests__/plugins.tools.test.ts +2 -1
  352. package/src/modules/plugins/join-request-records.store.ts +7 -4
  353. package/src/modules/settings/__tests__/deployment-settings.service.test.ts +16 -0
  354. package/src/modules/settings/__tests__/setup.routes.git-mode.test.ts +91 -58
  355. package/src/modules/settings/__tests__/setup.routes.github-app.test.ts +25 -8
  356. package/src/modules/settings/__tests__/setup.routes.managed-phase.test.ts +14 -11
  357. package/src/modules/settings/__tests__/setup.routes.repository-change.test.ts +390 -0
  358. package/src/modules/settings/__tests__/setup.routes.test.ts +3 -0
  359. package/src/modules/settings/deployment-settings.service.ts +8 -3
  360. package/src/modules/settings/setup.routes.ts +263 -19
  361. package/src/modules/skills/__tests__/allowed-tools-warn.tools.test.ts +2 -1
  362. package/src/modules/skills/__tests__/branch-skills.tools.test.ts +218 -0
  363. package/src/modules/skills/__tests__/skills.service.test.ts +229 -5
  364. package/src/modules/skills/skills.contract.ts +91 -18
  365. package/src/modules/skills/skills.service.ts +278 -41
  366. package/src/modules/skills/skills.tools.ts +80 -8
  367. package/src/modules/tool-auth/__tests__/manual-auth.middleware.test.ts +14 -1
  368. package/src/modules/tool-auth/external-api-key.service.ts +12 -4
  369. package/src/modules/tool-auth/internal-token.service.ts +3 -3
  370. package/src/modules/tool-auth/tool-auth.middleware.ts +34 -11
  371. package/src/modules/tool-helpers/__tests__/agent-roles-write.test.ts +5 -3
  372. package/src/modules/tool-helpers/__tests__/phase4-tools.test.ts +3 -3
  373. package/src/modules/tool-helpers/__tests__/validate-token.test.ts +11 -1
  374. package/src/modules/tool-helpers/tool-handler.ts +24 -4
  375. package/src/modules/tool-helpers/tool.contract.ts +3 -2
  376. package/src/modules/tool-helpers/validate-token.ts +1 -1
  377. package/src/modules/tool-manuals/tool-manuals.tools.ts +44 -31
  378. package/src/modules/tool-registry/__tests__/tool-description-length.test.ts +378 -0
  379. package/src/modules/tool-registry/description-length.ts +111 -0
  380. package/src/modules/workflow/__tests__/pending-commits.repository-replaced.test.ts +111 -0
  381. package/src/modules/workflow/__tests__/workflow.routes.history-read-gate.test.ts +25 -0
  382. package/src/modules/workflow/__tests__/workflow.service.repository-replaced.test.ts +249 -0
  383. package/src/modules/workflow/agent-tools/__tests__/workflow.tools.test.ts +264 -6
  384. package/src/modules/workflow/agent-tools/change-request-summary.ts +182 -0
  385. package/src/modules/workflow/agent-tools/workflow.tools.ts +86 -35
  386. package/src/modules/workflow/file-lock.service.ts +31 -0
  387. package/src/modules/workflow/git/__tests__/git.service.fileBytesAtCommit.test.ts +260 -0
  388. package/src/modules/workflow/git/__tests__/git.service.prFetchFailure.test.ts +170 -0
  389. package/src/modules/workflow/git/git.service.ts +215 -1
  390. package/src/modules/workflow/pending-commits.service.ts +64 -2
  391. package/src/modules/workflow/recovery-bot.ts +7 -3
  392. package/src/modules/workflow/review-workflow/__tests__/carry-approvals-forward.test.ts +4 -0
  393. package/src/modules/workflow/review-workflow/__tests__/erase-approver.test.ts +25 -3
  394. package/src/modules/workflow/review-workflow/review-workflow.service.ts +12 -3
  395. package/src/modules/workflow/workflow-hooks.ts +64 -26
  396. package/src/modules/workflow/workflow.service.ts +125 -5
  397. package/src/modules/workspace/__tests__/agent-access.gate.test.ts +208 -0
  398. package/src/modules/workspace/__tests__/agent-uploads.test.ts +1604 -0
  399. package/src/modules/workspace/__tests__/escape-sequences.routes.test.ts +394 -0
  400. package/src/modules/workspace/__tests__/file-stat-access.test.ts +2 -1
  401. package/src/modules/workspace/__tests__/git-internals.security.test.ts +2 -1
  402. package/src/modules/workspace/__tests__/set-aside-clone.test.ts +52 -0
  403. package/src/modules/workspace/__tests__/workspace.routes.at-ref.test.ts +305 -0
  404. package/src/modules/workspace/__tests__/workspace.routes.download.test.ts +2 -0
  405. package/src/modules/workspace/__tests__/workspace.service.any-workspace-credentials.test.ts +156 -0
  406. package/src/modules/workspace/__tests__/workspace.service.forget-clone-races.test.ts +147 -0
  407. package/src/modules/workspace/__tests__/workspace.service.replaced-repository.test.ts +402 -0
  408. package/src/modules/workspace/__tests__/workspace.service.test.ts +77 -9
  409. package/src/modules/workspace/__tests__/workspace.tools.agents-file.test.ts +41 -39
  410. package/src/modules/workspace/__tests__/workspace.tools.branch-errors.test.ts +2 -3
  411. package/src/modules/workspace/__tests__/workspace.tools.test.ts +766 -56
  412. package/src/modules/workspace/agent-access.gate.ts +164 -0
  413. package/src/modules/workspace/agent-upload.routes.ts +214 -0
  414. package/src/modules/workspace/agent-upload.store.ts +668 -0
  415. package/src/modules/workspace/routine-write-policy.ts +5 -6
  416. package/src/modules/workspace/session-sink.ts +5 -5
  417. package/src/modules/workspace/set-aside-clone.ts +96 -0
  418. package/src/modules/workspace/startup/__tests__/kb-startup-runner.test.ts +86 -0
  419. package/src/modules/workspace/startup/kb-startup-runner.ts +115 -34
  420. package/src/modules/workspace/startup/steps/__tests__/steps.test.ts +9 -9
  421. package/src/modules/workspace/startup/steps/seed-tree.ts +3 -6
  422. package/src/modules/workspace/startup/steps/template-source.ts +53 -5
  423. package/src/modules/workspace/upload-limits.ts +12 -0
  424. package/src/modules/workspace/workspace.routes.ts +96 -5
  425. package/src/modules/workspace/workspace.service.ts +319 -61
  426. package/src/modules/workspace/workspace.tools.ts +894 -214
  427. package/src/modules/workspace/write-denial.ts +0 -8
  428. package/src/modules/workspace/zip-entry-rules.ts +173 -0
  429. package/src/modules/write-access/__tests__/write-access.test.ts +248 -0
  430. package/src/modules/write-access/write-access.ts +153 -0
  431. package/src/shared/__tests__/column-crypto.test.ts +217 -0
  432. package/src/shared/column-crypto.ts +218 -0
  433. package/src/shared/domain-errors.ts +31 -0
  434. package/src/shared/git.contract.ts +27 -0
  435. package/src/shared/token-crypto.ts +28 -1
  436. package/src/tenancy/__tests__/static-tenant-source.test.ts +4 -1
  437. package/src/tenancy/static-tenant-source.ts +1 -3
  438. package/src/tenancy/tenant-secrets.ts +5 -1
  439. package/dist/modules/workflow/session-ontology.policy.d.ts +0 -52
  440. package/dist/modules/workflow/session-ontology.policy.d.ts.map +0 -1
  441. package/dist/modules/workflow/session-ontology.policy.js +0 -62
  442. package/dist/modules/workflow/session-ontology.policy.js.map +0 -1
  443. package/dist/modules/workflow/session-ontology.service.d.ts +0 -105
  444. package/dist/modules/workflow/session-ontology.service.d.ts.map +0 -1
  445. package/dist/modules/workflow/session-ontology.service.js +0 -147
  446. package/dist/modules/workflow/session-ontology.service.js.map +0 -1
  447. package/dist/modules/workspace/session-ontology.gate.d.ts +0 -114
  448. package/dist/modules/workspace/session-ontology.gate.d.ts.map +0 -1
  449. package/dist/modules/workspace/session-ontology.gate.js +0 -161
  450. package/dist/modules/workspace/session-ontology.gate.js.map +0 -1
  451. package/dist/shared/kb-layout.d.ts +0 -39
  452. package/dist/shared/kb-layout.d.ts.map +0 -1
  453. package/dist/shared/kb-layout.js +0 -103
  454. package/dist/shared/kb-layout.js.map +0 -1
  455. package/dist/shared/kb-layout.test.d.ts +0 -2
  456. package/dist/shared/kb-layout.test.d.ts.map +0 -1
  457. package/dist/shared/kb-layout.test.js +0 -75
  458. package/dist/shared/kb-layout.test.js.map +0 -1
  459. package/src/modules/workflow/__tests__/session-ontology.policy.test.ts +0 -62
  460. package/src/modules/workflow/__tests__/session-ontology.service.test.ts +0 -201
  461. package/src/modules/workflow/session-ontology.policy.ts +0 -70
  462. package/src/modules/workflow/session-ontology.service.ts +0 -183
  463. package/src/modules/workspace/__tests__/session-ontology.gate.test.ts +0 -239
  464. package/src/modules/workspace/session-ontology.gate.ts +0 -191
  465. package/src/shared/kb-layout.test.ts +0 -98
  466. package/src/shared/kb-layout.ts +0 -102
@@ -0,0 +1,249 @@
1
+ import { describe, it, expect, vi } from 'vitest';
2
+ import { testKbContext } from '../../../__tests__/kb-context.js';
3
+ import type { GitService } from '../git/git.service.js';
4
+ import type { PullRequestService } from '../git/pull-request.service.js';
5
+ import type { IReviewWorkflowService } from '../review-workflow/review-workflow.interface.js';
6
+ import type { WorkspaceService } from '../../workspace/workspace.service.js';
7
+ import type { IAccessControl } from '../../access/access-control.interface.js';
8
+ import type { FileLockService } from '../file-lock.service.js';
9
+ import type { PendingCommitsService } from '../pending-commits.service.js';
10
+ import type { Database } from '../../database/connection.js';
11
+ import { WorkflowService } from '../workflow.service.js';
12
+ import { openChangeGate } from '../../../__tests__/open-change-gate.js';
13
+
14
+ /**
15
+ * The CLOSE half of the admin's choice when the knowledge-base repository is
16
+ * replaced: the requests point at branches that are not in the new
17
+ * repository, so they are closed — with a reason, releasing the locks held on
18
+ * those branches and taking any commit still queued for them off the worker,
19
+ * and without deleting a single row.
20
+ */
21
+
22
+ interface ClosedRow {
23
+ number: number;
24
+ sourceBranch: string;
25
+ }
26
+
27
+ /**
28
+ * DB stub shaped like the real close: ONE `update … where state = 'open'
29
+ * returning` per pass, answering with the rows that pass actually closed.
30
+ *
31
+ * `passes` is therefore what the statement RETURNS, pass by pass — which is
32
+ * how a request already closed by a merge or a withdrawal is expressed (it is
33
+ * simply not in the returned set) and how a request opened BETWEEN passes is
34
+ * expressed (it turns up in a later one). A pass past the end answers nothing,
35
+ * which is what stops the loop.
36
+ */
37
+ function makeDb(passes: ClosedRow[][], sets: Record<string, unknown>[], openCount: number) {
38
+ let pass = 0;
39
+ const db = {
40
+ // The count is a SQL aggregate: one row, one integer.
41
+ select: () => ({ from: () => ({ where: async () => [{ open: openCount }] }) }),
42
+ update: () => ({
43
+ set: (values: Record<string, unknown>) => {
44
+ sets.push(values);
45
+ return { where: () => ({ returning: async () => passes[pass++] ?? [] }) };
46
+ },
47
+ }),
48
+ // Nothing here may ever delete a change request.
49
+ delete: () => {
50
+ throw new Error('a change request must never be deleted');
51
+ },
52
+ };
53
+ return db as unknown as Database;
54
+ }
55
+
56
+ function makeService(passes: ClosedRow[][], openCount = passes.flat().length) {
57
+ const sets: Record<string, unknown>[] = [];
58
+ const unlocked: string[] = [];
59
+ const dequeued: string[] = [];
60
+ const invalidated: number[] = [];
61
+ const emitted: unknown[] = [];
62
+ const fileLocks = {
63
+ releaseAllOnBranch: vi.fn(async (branch: string) => {
64
+ unlocked.push(branch);
65
+ return 2;
66
+ }),
67
+ } as unknown as FileLockService;
68
+ const pendingCommits = {
69
+ markNeedsAttentionOnBranch: vi.fn(async (branch: string) => {
70
+ dequeued.push(branch);
71
+ return 1;
72
+ }),
73
+ } as unknown as PendingCommitsService;
74
+ const prs = {
75
+ invalidateDetailCache: vi.fn((n: number) => invalidated.push(n)),
76
+ } as unknown as PullRequestService;
77
+ const svc = new WorkflowService(
78
+ makeDb(passes, sets, openCount),
79
+ {} as GitService,
80
+ prs,
81
+ {} as IReviewWorkflowService,
82
+ {} as WorkspaceService,
83
+ {} as IAccessControl,
84
+ fileLocks,
85
+ pendingCommits,
86
+ testKbContext(),
87
+ openChangeGate(),
88
+ );
89
+ (svc as unknown as { events?: { emit(e: unknown): void } }).events = {
90
+ emit: (e: unknown) => emitted.push(e),
91
+ };
92
+ return { svc, sets, unlocked, dequeued, invalidated, emitted, fileLocks, pendingCommits };
93
+ }
94
+
95
+ describe('closeOpenChangeRequestsAsRepositoryReplaced', () => {
96
+ it('closes every open request with the reason, and deletes nothing', async () => {
97
+ const { svc, sets, invalidated, emitted } = makeService([
98
+ [
99
+ { number: 7, sourceBranch: 'alice/draft' },
100
+ { number: 9, sourceBranch: 'bob/draft' },
101
+ ],
102
+ ]);
103
+
104
+ await expect(svc.closeOpenChangeRequestsAsRepositoryReplaced()).resolves.toBe(2);
105
+
106
+ // ONE statement closed both: a read-then-write loop would have missed any
107
+ // request opened while it ran.
108
+ expect(sets).toHaveLength(2); // the closing pass, then the pass that found nothing
109
+ expect(sets[0]?.state).toBe('closed');
110
+ // The reason outlives the admin who chose it — that is the whole point
111
+ // of a column rather than a log line.
112
+ expect(sets[0]?.closedReason).toBe('repository-replaced');
113
+ expect(sets[0]?.closedAt).toBeInstanceOf(Date);
114
+ expect(invalidated).toEqual([7, 9]);
115
+ expect(emitted).toEqual([
116
+ { kind: 'change-request-rejected', number: 7 },
117
+ { kind: 'change-request-rejected', number: 9 },
118
+ ]);
119
+ });
120
+
121
+ it('closes a request opened while it was closing the others', async () => {
122
+ // The deployment is still live — the address has not been saved yet — so
123
+ // `POST /change-requests` is still being served. The first pass closed two;
124
+ // #11 arrived after it and is closed by the next one.
125
+ const { svc, invalidated } = makeService([
126
+ [
127
+ { number: 7, sourceBranch: 'alice/draft' },
128
+ { number: 9, sourceBranch: 'bob/draft' },
129
+ ],
130
+ [{ number: 11, sourceBranch: 'carol/draft' }],
131
+ ]);
132
+
133
+ await expect(svc.closeOpenChangeRequestsAsRepositoryReplaced()).resolves.toBe(3);
134
+ expect(invalidated).toEqual([7, 9, 11]);
135
+ });
136
+
137
+ it('gives up after a bounded number of passes rather than holding the save open', async () => {
138
+ // A client opening requests in a loop must not be able to keep the save
139
+ // from ever returning. What survives the last pass was opened after the
140
+ // repository was replaced, and the deleted-branch sweep owns it.
141
+ const endless = Array.from({ length: 50 }, (_, i) => [
142
+ { number: i + 1, sourceBranch: `writer-${i}/draft` },
143
+ ]);
144
+ const { svc } = makeService(endless, 50);
145
+ const closed = await svc.closeOpenChangeRequestsAsRepositoryReplaced();
146
+ expect(closed).toBeGreaterThan(0);
147
+ expect(closed).toBeLessThan(50);
148
+ });
149
+
150
+ it('skips a request a merge or a withdrawal closed first', async () => {
151
+ // The guard is the statement's own `where state = 'open'`: #9 was closed by
152
+ // somebody else between the count and this call, so it is not in the
153
+ // returned set — and nothing is done on its behalf.
154
+ const { svc, unlocked, dequeued, invalidated, emitted } = makeService(
155
+ [[{ number: 7, sourceBranch: 'alice/draft' }]],
156
+ 2,
157
+ );
158
+
159
+ await expect(svc.closeOpenChangeRequestsAsRepositoryReplaced()).resolves.toBe(1);
160
+
161
+ expect(unlocked).toEqual(['alice/draft']);
162
+ expect(dequeued).toEqual(['alice/draft']);
163
+ expect(invalidated).toEqual([7]);
164
+ expect(emitted).toEqual([{ kind: 'change-request-rejected', number: 7 }]);
165
+ });
166
+
167
+ it('releases the file locks held on the branches it closes', async () => {
168
+ const { svc, unlocked } = makeService([
169
+ [
170
+ { number: 7, sourceBranch: 'alice/draft' },
171
+ { number: 9, sourceBranch: 'bob/draft' },
172
+ ],
173
+ ]);
174
+
175
+ await svc.closeOpenChangeRequestsAsRepositoryReplaced();
176
+
177
+ // Nobody can publish those bytes or release the lock by finishing: the
178
+ // branch belongs to a repository this deployment no longer has.
179
+ expect(unlocked).toEqual(['alice/draft', 'bob/draft']);
180
+ });
181
+
182
+ it('takes the commits already queued for those branches off the worker', async () => {
183
+ const { svc, dequeued, pendingCommits } = makeService([
184
+ [{ number: 7, sourceBranch: 'alice/draft' }],
185
+ ]);
186
+
187
+ await svc.closeOpenChangeRequestsAsRepositoryReplaced();
188
+
189
+ // A release ENQUEUES the bytes and only then drops its lock, so dropping
190
+ // the locks can leave rows waiting. Left pending, the worker would commit
191
+ // them onto a same-named branch of a different repository after the
192
+ // re-clone.
193
+ expect(dequeued).toEqual(['alice/draft']);
194
+ expect(vi.mocked(pendingCommits.markNeedsAttentionOnBranch).mock.calls[0]?.[1]).toMatch(
195
+ /repository was replaced/,
196
+ );
197
+ });
198
+
199
+ it('still closes the requests when a lock release fails', async () => {
200
+ const { svc, fileLocks } = makeService([[{ number: 7, sourceBranch: 'alice/draft' }]]);
201
+ vi.mocked(fileLocks.releaseAllOnBranch).mockRejectedValueOnce(new Error('lock table is down'));
202
+ vi.spyOn(console, 'warn').mockImplementation(() => {});
203
+ try {
204
+ // The close is the decision the admin made; a lock expires on its own
205
+ // within the minute, so it is not worth half-closing the set over.
206
+ await expect(svc.closeOpenChangeRequestsAsRepositoryReplaced()).resolves.toBe(1);
207
+ } finally {
208
+ vi.restoreAllMocks();
209
+ }
210
+ });
211
+
212
+ it('still closes the requests when the queued commits cannot be set aside', async () => {
213
+ const { svc, pendingCommits, unlocked } = makeService([
214
+ [{ number: 7, sourceBranch: 'alice/draft' }],
215
+ ]);
216
+ vi.mocked(pendingCommits.markNeedsAttentionOnBranch).mockRejectedValueOnce(
217
+ new Error('pending_commits is down'),
218
+ );
219
+ vi.spyOn(console, 'warn').mockImplementation(() => {});
220
+ try {
221
+ await expect(svc.closeOpenChangeRequestsAsRepositoryReplaced()).resolves.toBe(1);
222
+ expect(unlocked).toEqual(['alice/draft']);
223
+ } finally {
224
+ vi.restoreAllMocks();
225
+ }
226
+ });
227
+
228
+ it('closes nothing, and asks nothing of the lock table, when no request is open', async () => {
229
+ const { svc, unlocked, dequeued, emitted } = makeService([]);
230
+ await expect(svc.closeOpenChangeRequestsAsRepositoryReplaced()).resolves.toBe(0);
231
+ expect(unlocked).toEqual([]);
232
+ expect(dequeued).toEqual([]);
233
+ expect(emitted).toEqual([]);
234
+ });
235
+ });
236
+
237
+ describe('countOpenChangeRequests', () => {
238
+ it('answers how many are open, so the confirmation can say what it is about', async () => {
239
+ const { svc } = makeService([], 2);
240
+ // A SQL aggregate: the question is an integer, and materializing one row
241
+ // per open request to answer it would scale with the wrong thing.
242
+ await expect(svc.countOpenChangeRequests()).resolves.toBe(2);
243
+ });
244
+
245
+ it('answers zero when the aggregate comes back empty', async () => {
246
+ const { svc } = makeService([], 0);
247
+ await expect(svc.countOpenChangeRequests()).resolves.toBe(0);
248
+ });
249
+ });
@@ -9,11 +9,76 @@ import { createToolContextResolver } from '../../../tool-helpers/tool-context.js
9
9
  import { createToolHandlerFactory } from '../../../tool-helpers/tool-handler.js';
10
10
  import { createManualRoutes } from '../../../tool-registry/manual.routes.js';
11
11
  import { registerWorkflowTools } from '../workflow.tools.js';
12
- import { OpenChangeRequestBlocksMergeError } from '../../../../shared/domain-errors.js';
12
+ import { ChangeRequestConflictsError, OpenChangeRequestBlocksMergeError } from '../../../../shared/domain-errors.js';
13
13
 
14
14
  const WS = 'target-company-state';
15
15
 
16
+ /**
17
+ * One file of a change-request detail as the service returns it: a 15 KB
18
+ * addition, patch included. The reported failure was 27 of these — the detail
19
+ * answered ~445,000 characters and overflowed the agent's tool-result limit.
20
+ */
21
+ function addedFile(i: number): Record<string, unknown> {
22
+ const path = `KnowledgeBase/Engineering/Knowledge/Hexis/Generated-Node-${i}.md`;
23
+ return {
24
+ path,
25
+ status: 'added',
26
+ additions: 400,
27
+ deletions: 0,
28
+ // 400 × 37 characters ≈ 15 KB, as reported.
29
+ patch: `@@ -0,0 +1,400 @@\n${'+a line of generated knowledge text\n'.repeat(400)}`,
30
+ isBinary: false,
31
+ sha: `blob-${i}`,
32
+ rawUrl: `https://raw.example.com/blob-${i}`,
33
+ };
34
+ }
35
+
36
+ /** The per-file approval state the detail carries, as the app's dialog reads it. */
37
+ function approvalOf(path: string): Record<string, unknown> {
38
+ return {
39
+ path,
40
+ eligibleApprovers: {
41
+ roles: ['Admin', 'Engineering'],
42
+ users: [{ name: 'Razvan Radulescu', email: 'razvan.radulescu@bevel.software' }],
43
+ },
44
+ approvedBy: [],
45
+ eligibilityResolved: true,
46
+ isApproved: false,
47
+ inMergeGate: true,
48
+ viewerCanApprove: true,
49
+ };
50
+ }
51
+
52
+ /** A full change-request detail over the given files — what `openChangeRequest` answers. */
53
+ function detailOf(files: Record<string, unknown>[]): Record<string, unknown> {
54
+ return {
55
+ number: 7,
56
+ url: 'https://bevel.example.com/change-requests/7',
57
+ title: 'My change',
58
+ state: 'open',
59
+ branch: 'me/draft',
60
+ base: 'target-company-state',
61
+ body: 'Body shown verbatim to reviewers.',
62
+ author: { login: 'bevel-bot' },
63
+ headSha: 'head-1',
64
+ baseSha: 'base-1',
65
+ files,
66
+ comments: [],
67
+ approvals: files.map((f) => approvalOf(f.path as string)),
68
+ mergeableInBevel: false,
69
+ mergeBlockedReasons: files.map((f) => `${f.path as string} is not approved`),
70
+ mergeWarnings: files.map((f) => `${f.path as string} is not approved`),
71
+ };
72
+ }
73
+
74
+ /** The reported case: 27 new files of 15 KB each. */
75
+ const BIG = () => detailOf(Array.from({ length: 27 }, (_, i) => addedFile(i)));
76
+
16
77
  let calls: unknown[][] = [];
78
+ /** What the service answers `openChangeRequest` with; a test replaces it. */
79
+ let opened: Record<string, unknown> = BIG();
80
+ /** Set to make `openChangeRequest` throw instead. */
81
+ let openFails: Error | undefined;
17
82
  const externalApiKeyService = {
18
83
  looksLikeExternalApiKey: (t: string) => typeof t === 'string' && t.startsWith('bevel_'),
19
84
  verifyAndLoadToken: async (t: string) =>
@@ -38,7 +103,8 @@ const workflowService = {
38
103
  },
39
104
  openChangeRequest: async (ws: string, user: { id: string }, body: unknown) => {
40
105
  calls.push(['openChangeRequest', ws, user.id, body]);
41
- return { number: 7, url: 'https://bevel.example.com/change-requests/7' };
106
+ if (openFails) throw openFails;
107
+ return opened;
42
108
  },
43
109
  createBranch: async (ws: string, name: string) => {
44
110
  calls.push(['createBranch', ws, name]);
@@ -114,6 +180,8 @@ const post = (url: string, bearer: string, body: unknown = {}) =>
114
180
 
115
181
  beforeEach(() => {
116
182
  calls = [];
183
+ opened = BIG();
184
+ openFails = undefined;
117
185
  });
118
186
  afterEach(async () => {
119
187
  if (httpServer) await new Promise<void>((r) => httpServer!.close(() => r()));
@@ -229,7 +297,7 @@ describe('registerWorkflowTools', () => {
229
297
  expect(calls).toContainEqual(['listBranches', 'existing-ws']);
230
298
  });
231
299
 
232
- it('open_change_request returns the CR detail', async () => {
300
+ it('open_change_request acts on the SOURCE branch\'s workspace', async () => {
233
301
  const base = await start();
234
302
  const res = await post(`${base}/api/agent/tools/open_change_request`, writeTok(), {
235
303
  sourceBranch: 'me/draft',
@@ -237,9 +305,6 @@ describe('registerWorkflowTools', () => {
237
305
  title: 'My change',
238
306
  });
239
307
  expect(res.status).toBe(200);
240
- expect(await res.json()).toMatchObject({
241
- changeRequest: { number: 7, url: 'https://bevel.example.com/change-requests/7' },
242
- });
243
308
  // The workspace must be derived from the SOURCE branch (not a separate
244
309
  // `branch` arg) — encodeURIComponent('me/draft'). Regression guard for the
245
310
  // `-b undefined` clone bug when the model omitted `branch`.
@@ -251,6 +316,199 @@ describe('registerWorkflowTools', () => {
251
316
  ]);
252
317
  });
253
318
 
319
+ /**
320
+ * One test per Scenario of the Specification
321
+ * (`hx-change-request-answer-is-small-spec`). The reported failure: opening a
322
+ * request for 27 files answered ~445,000 characters, because the answer was
323
+ * the detail the app's dialog reads, every patch included. It overflowed the
324
+ * tool-result limit, so the agent could not even read the link.
325
+ */
326
+ describe('what open_change_request answers', () => {
327
+ const open = async (base: string, include?: string[]) => {
328
+ const res = await post(`${base}/api/agent/tools/open_change_request`, writeTok(), {
329
+ sourceBranch: 'me/draft',
330
+ targetBranch: 'target-company-state',
331
+ title: 'My change',
332
+ ...(include ? { include } : {}),
333
+ });
334
+ expect(res.status).toBe(200);
335
+ return (await res.json()) as Record<string, unknown>;
336
+ };
337
+
338
+ it('answers a summary for 27 new files: the link first, 25 paths of 27, approvers, blockers, no patch', async () => {
339
+ const base = await start();
340
+ const body = await open(base);
341
+
342
+ // The link is the FIRST field: the one thing the agent must hand the
343
+ // user, so it survives a truncation of anything after it.
344
+ expect(Object.keys(body)[0]).toBe('url');
345
+ expect(body).toMatchObject({
346
+ url: 'https://bevel.example.com/change-requests/7',
347
+ number: 7,
348
+ title: 'My change',
349
+ state: 'open',
350
+ sourceBranch: 'me/draft',
351
+ targetBranch: 'target-company-state',
352
+ totalFiles: 27,
353
+ });
354
+
355
+ const files = body.files as { path: string; change: string }[];
356
+ expect(files).toHaveLength(25);
357
+ expect(new Set(files.map((f) => f.change))).toEqual(new Set(['added']));
358
+ expect(files[0].path).toBe('KnowledgeBase/Engineering/Knowledge/Hexis/Generated-Node-0.md');
359
+
360
+ // Who must approve, per path — what Juan found useful and must stay.
361
+ expect(body.approvals).toHaveLength(25);
362
+ expect((body.approvals as unknown[])[0]).toEqual({
363
+ path: 'KnowledgeBase/Engineering/Knowledge/Hexis/Generated-Node-0.md',
364
+ roles: ['Admin', 'Engineering'],
365
+ users: ['Razvan Radulescu'],
366
+ approved: false,
367
+ inMergeGate: true,
368
+ });
369
+ // What blocks the merge: every reason, for cut paths too.
370
+ expect(body.mergeBlockedReasons).toHaveLength(27);
371
+
372
+ // No patch, and no file content under any other name.
373
+ const json = JSON.stringify(body);
374
+ expect(json).not.toContain('patch');
375
+ expect(json).not.toContain('a line of generated knowledge text');
376
+ expect(files.every((f) => !('patch' in f))).toBe(true);
377
+ });
378
+
379
+ it('keeps the 27-file answer under 10,000 characters', async () => {
380
+ const base = await start();
381
+ const body = await open(base);
382
+ // The detail this was shaped from is the ~445,000-character one.
383
+ expect(JSON.stringify(opened).length).toBeGreaterThan(400_000);
384
+ // 8,918 characters as this fixture stands. The three per-path lists
385
+ // (`files`, `approvals`, `mergeBlockedReasons`) are what fills it, so a
386
+ // field added to any of them eats the margin ~25 times over.
387
+ expect(JSON.stringify(body).length).toBeLessThan(10_000);
388
+ });
389
+
390
+ it('`include: ["all-paths"]` lists all 27 paths, still without patches', async () => {
391
+ const base = await start();
392
+ const body = await open(base, ['all-paths']);
393
+ expect(body.files).toHaveLength(27);
394
+ expect(body.totalFiles).toBe(27);
395
+ expect(body.approvals).toHaveLength(27);
396
+ expect(JSON.stringify(body)).not.toContain('a line of generated knowledge text');
397
+ });
398
+
399
+ it('`include: ["patches"]` gives each listed file its patch', async () => {
400
+ const base = await start();
401
+ const body = await open(base, ['patches']);
402
+ const files = body.files as { path: string; patch?: string }[];
403
+ expect(files).toHaveLength(25);
404
+ expect(files.every((f) => f.patch?.startsWith('@@ -0,0 +1,400 @@'))).toBe(true);
405
+ // Cut to 25 still: `patches` adds the diffs, it does not lift the cut.
406
+ expect(body.totalFiles).toBe(27);
407
+ });
408
+
409
+ it('both together: every path, each with its patch', async () => {
410
+ const base = await start();
411
+ const body = await open(base, ['patches', 'all-paths']);
412
+ const files = body.files as { patch?: string }[];
413
+ expect(files).toHaveLength(27);
414
+ expect(files.every((f) => typeof f.patch === 'string')).toBe(true);
415
+ });
416
+
417
+ it('reads a modification `changed` and a rename `moved`, with its old path', async () => {
418
+ const base = await start();
419
+ opened = detailOf([
420
+ { path: 'KnowledgeBase/Kept.md', status: 'modified', additions: 2, deletions: 1, patch: '@@ x', isBinary: false, sha: 'b1', rawUrl: 'r1' },
421
+ { path: 'KnowledgeBase/New-Home.md', previousPath: 'KnowledgeBase/Old-Home.md', status: 'renamed', additions: 0, deletions: 0, isBinary: false, sha: 'b2', rawUrl: 'r2' },
422
+ ]);
423
+ const body = await open(base);
424
+ expect(body.files).toEqual([
425
+ { path: 'KnowledgeBase/Kept.md', change: 'changed' },
426
+ { path: 'KnowledgeBase/New-Home.md', change: 'moved', previousPath: 'KnowledgeBase/Old-Home.md' },
427
+ ]);
428
+ expect(body.totalFiles).toBe(2);
429
+ });
430
+
431
+ it('a deletion reads `deleted`', async () => {
432
+ const base = await start();
433
+ opened = detailOf([
434
+ { path: 'KnowledgeBase/Gone.md', status: 'removed', additions: 0, deletions: 9, isBinary: false, sha: 'b3', rawUrl: 'r3' },
435
+ ]);
436
+ expect((await open(base)).files).toEqual([{ path: 'KnowledgeBase/Gone.md', change: 'deleted' }]);
437
+ });
438
+
439
+ it('says the approvers are unknown rather than implying nobody must approve', async () => {
440
+ const base = await start();
441
+ opened = detailOf([
442
+ { path: 'KnowledgeBase/Opaque.md', status: 'added', additions: 1, deletions: 0, isBinary: false, sha: 'b4', rawUrl: 'r4' },
443
+ ]);
444
+ // No usable access config on the base: the empty approver set means "not
445
+ // known", and nothing may be granted on the strength of that emptiness.
446
+ opened.approvals = [
447
+ { path: 'KnowledgeBase/Opaque.md', eligibleApprovers: { roles: [], users: [] }, approvedBy: [], eligibilityResolved: false, isApproved: false, inMergeGate: false },
448
+ ];
449
+ expect((await open(base)).approvals).toEqual([
450
+ { path: 'KnowledgeBase/Opaque.md', roles: [], users: [], approved: false, inMergeGate: false, approversUnknown: true },
451
+ ]);
452
+
453
+ // A detail that carries no verdict at all reads the same way: absent is
454
+ // not resolved, so the emptiness still means "not known".
455
+ const legacy = { ...(opened.approvals as Record<string, unknown>[])[0] };
456
+ delete legacy.eligibilityResolved;
457
+ opened.approvals = [legacy];
458
+ expect((await open(base)).approvals).toEqual([
459
+ { path: 'KnowledgeBase/Opaque.md', roles: [], users: [], approved: false, inMergeGate: false, approversUnknown: true },
460
+ ]);
461
+ });
462
+
463
+ it('surfaces the `change-request-conflicts` error as before', async () => {
464
+ const base = await start();
465
+ openFails = new ChangeRequestConflictsError('me/draft', 'target-company-state', ['KnowledgeBase/Clash.md']);
466
+ const res = await post(`${base}/api/agent/tools/open_change_request`, writeTok(), {
467
+ sourceBranch: 'me/draft',
468
+ targetBranch: 'target-company-state',
469
+ title: 'My change',
470
+ });
471
+ expect(res.status).toBe(409);
472
+ expect(await res.json()).toMatchObject({
473
+ kind: 'change-request-conflicts',
474
+ sourceBranch: 'me/draft',
475
+ targetBranch: 'target-company-state',
476
+ conflictedPaths: ['KnowledgeBase/Clash.md'],
477
+ });
478
+ });
479
+
480
+ /**
481
+ * The app's dialog reads the same detail from its own route. Shaping the
482
+ * agent's answer must not touch it — so the detail the service handed back
483
+ * still carries all 27 files and all their patches after the tool answered.
484
+ */
485
+ it('leaves the detail the app\'s dialog reads exactly as the service returned it', async () => {
486
+ const base = await start();
487
+ const before = JSON.stringify(opened);
488
+ await open(base);
489
+ expect(JSON.stringify(opened)).toBe(before);
490
+ expect((opened.files as unknown[])).toHaveLength(27);
491
+ expect((opened.files as { patch?: string }[]).every((f) => typeof f.patch === 'string')).toBe(true);
492
+ });
493
+
494
+ it('says in its description what the default answer holds and how to ask for more', async () => {
495
+ await start();
496
+ for (const tools of [await registryRef!.listInternal(), await registryRef!.listExternal()]) {
497
+ const def = tools.find((t) => t.name === 'open_change_request')!;
498
+ expect(def.description).toMatch(/SUMMARY/);
499
+ expect(def.description).toMatch(/cut to 25/);
500
+ expect(def.description).toMatch(/No patches and no file content/);
501
+ expect(def.description).toMatch(/include: \["patches"\]/);
502
+ expect(def.description).toMatch(/include: \["all-paths"\]/);
503
+ // `toolDef` wraps a tool's flat inputs under `body`.
504
+ const body = (def.inputs as { properties: { body: { properties: Record<string, { items?: { enum?: string[] }; description?: string }> } } })
505
+ .properties.body;
506
+ expect(body.properties.include.items?.enum).toEqual(['patches', 'all-paths']);
507
+ expect(body.properties.include.description).toMatch(/every changed path instead of the first 25/);
508
+ }
509
+ });
510
+ });
511
+
254
512
  it('create_branch forks in the BASE branch\'s workspace, never the new draft\'s', async () => {
255
513
  const base = await start();
256
514
  const res = await post(`${base}/api/agent/tools/create_branch`, writeTok(), {