@bevel-software/platform-core-backend 0.26.0 → 0.27.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 (613) hide show
  1. package/agent-guide/access-control.md +17 -0
  2. package/agent-guide/conventions.md +8 -0
  3. package/agent-guide/html-views.md +83 -0
  4. package/agent-guide/new-knowledge-base.md +24 -0
  5. package/agent-guide/tool-manuals.md +10 -0
  6. package/dist/assets.d.ts +19 -0
  7. package/dist/assets.d.ts.map +1 -1
  8. package/dist/assets.js +26 -2
  9. package/dist/assets.js.map +1 -1
  10. package/dist/core/catalog-revision.d.ts +19 -1
  11. package/dist/core/catalog-revision.d.ts.map +1 -1
  12. package/dist/core/catalog-revision.js +14 -3
  13. package/dist/core/catalog-revision.js.map +1 -1
  14. package/dist/core/create-core-server.d.ts +5 -0
  15. package/dist/core/create-core-server.d.ts.map +1 -1
  16. package/dist/core/create-core-server.js +54 -3
  17. package/dist/core/create-core-server.js.map +1 -1
  18. package/dist/core/create-core-services.d.ts +26 -0
  19. package/dist/core/create-core-services.d.ts.map +1 -1
  20. package/dist/core/create-core-services.js +100 -8
  21. package/dist/core/create-core-services.js.map +1 -1
  22. package/dist/core/lifecycle.d.ts +16 -1
  23. package/dist/core/lifecycle.d.ts.map +1 -1
  24. package/dist/core/lifecycle.js +18 -1
  25. package/dist/core/lifecycle.js.map +1 -1
  26. package/dist/core-config.d.ts +21 -0
  27. package/dist/core-config.d.ts.map +1 -1
  28. package/dist/core-config.js +22 -1
  29. package/dist/core-config.js.map +1 -1
  30. package/dist/index.d.ts +1 -1
  31. package/dist/index.d.ts.map +1 -1
  32. package/dist/index.js +2 -2
  33. package/dist/index.js.map +1 -1
  34. package/dist/modules/access/access-control.interface.d.ts +8 -0
  35. package/dist/modules/access/access-control.interface.d.ts.map +1 -1
  36. package/dist/modules/access/access-control.service.d.ts +1 -0
  37. package/dist/modules/access/access-control.service.d.ts.map +1 -1
  38. package/dist/modules/access/access-control.service.js +11 -0
  39. package/dist/modules/access/access-control.service.js.map +1 -1
  40. package/dist/modules/access/directory-sync-bot.js +1 -1
  41. package/dist/modules/access/directory-sync-bot.js.map +1 -1
  42. package/dist/modules/agent-guide/agent-guide.d.ts +6 -0
  43. package/dist/modules/agent-guide/agent-guide.d.ts.map +1 -1
  44. package/dist/modules/agent-guide/agent-guide.js +8 -0
  45. package/dist/modules/agent-guide/agent-guide.js.map +1 -1
  46. package/dist/modules/agent-guide/index.d.ts +1 -1
  47. package/dist/modules/agent-guide/index.d.ts.map +1 -1
  48. package/dist/modules/agent-guide/index.js +1 -1
  49. package/dist/modules/agent-guide/index.js.map +1 -1
  50. package/dist/modules/agent-instructions/compose.d.ts +5 -0
  51. package/dist/modules/agent-instructions/compose.d.ts.map +1 -1
  52. package/dist/modules/agent-instructions/compose.js +19 -2
  53. package/dist/modules/agent-instructions/compose.js.map +1 -1
  54. package/dist/modules/agent-instructions/shared-file-rules.js +19 -19
  55. package/dist/modules/agent-instructions/shared-file-rules.js.map +1 -1
  56. package/dist/modules/audit/agent-audit.service.d.ts +21 -2
  57. package/dist/modules/audit/agent-audit.service.d.ts.map +1 -1
  58. package/dist/modules/audit/agent-audit.service.js +43 -2
  59. package/dist/modules/audit/agent-audit.service.js.map +1 -1
  60. package/dist/modules/audit/agent-connection.routes.d.ts +34 -0
  61. package/dist/modules/audit/agent-connection.routes.d.ts.map +1 -0
  62. package/dist/modules/audit/agent-connection.routes.js +38 -0
  63. package/dist/modules/audit/agent-connection.routes.js.map +1 -0
  64. package/dist/modules/audit/audit.contract.d.ts +24 -0
  65. package/dist/modules/audit/audit.contract.d.ts.map +1 -1
  66. package/dist/modules/audit/audit.contract.js.map +1 -1
  67. package/dist/modules/audit/index.d.ts +1 -0
  68. package/dist/modules/audit/index.d.ts.map +1 -1
  69. package/dist/modules/audit/index.js +1 -0
  70. package/dist/modules/audit/index.js.map +1 -1
  71. package/dist/modules/auth/account.routes.d.ts.map +1 -1
  72. package/dist/modules/auth/account.routes.js +10 -3
  73. package/dist/modules/auth/account.routes.js.map +1 -1
  74. package/dist/modules/auth/auth.middleware.d.ts +7 -0
  75. package/dist/modules/auth/auth.middleware.d.ts.map +1 -1
  76. package/dist/modules/auth/auth.middleware.js +1 -1
  77. package/dist/modules/auth/auth.middleware.js.map +1 -1
  78. package/dist/modules/code-mode/code-mode.tool.d.ts.map +1 -1
  79. package/dist/modules/code-mode/code-mode.tool.js +11 -3
  80. package/dist/modules/code-mode/code-mode.tool.js.map +1 -1
  81. package/dist/modules/database/core-schema.d.ts +96 -0
  82. package/dist/modules/database/core-schema.d.ts.map +1 -1
  83. package/dist/modules/database/core-schema.js +32 -3
  84. package/dist/modules/database/core-schema.js.map +1 -1
  85. package/dist/modules/embed/account-link.service.d.ts +42 -0
  86. package/dist/modules/embed/account-link.service.d.ts.map +1 -0
  87. package/dist/modules/embed/account-link.service.js +50 -0
  88. package/dist/modules/embed/account-link.service.js.map +1 -0
  89. package/dist/modules/embed/embed-link.d.ts +58 -0
  90. package/dist/modules/embed/embed-link.d.ts.map +1 -0
  91. package/dist/modules/embed/embed-link.js +160 -0
  92. package/dist/modules/embed/embed-link.js.map +1 -0
  93. package/dist/modules/embed/embed.errors.d.ts +30 -0
  94. package/dist/modules/embed/embed.errors.d.ts.map +1 -0
  95. package/dist/modules/embed/embed.errors.js +42 -0
  96. package/dist/modules/embed/embed.errors.js.map +1 -0
  97. package/dist/modules/embed/embed.interface.d.ts +204 -0
  98. package/dist/modules/embed/embed.interface.d.ts.map +1 -0
  99. package/dist/modules/embed/embed.interface.js +2 -0
  100. package/dist/modules/embed/embed.interface.js.map +1 -0
  101. package/dist/modules/embed/embed.routes.d.ts +59 -0
  102. package/dist/modules/embed/embed.routes.d.ts.map +1 -0
  103. package/dist/modules/embed/embed.routes.js +343 -0
  104. package/dist/modules/embed/embed.routes.js.map +1 -0
  105. package/dist/modules/embed/embed.service.d.ts +169 -0
  106. package/dist/modules/embed/embed.service.d.ts.map +1 -0
  107. package/dist/modules/embed/embed.service.js +627 -0
  108. package/dist/modules/embed/embed.service.js.map +1 -0
  109. package/dist/modules/embed/embed.tools.d.ts +51 -0
  110. package/dist/modules/embed/embed.tools.d.ts.map +1 -0
  111. package/dist/modules/embed/embed.tools.js +170 -0
  112. package/dist/modules/embed/embed.tools.js.map +1 -0
  113. package/dist/modules/embed/mcp-app.d.ts +64 -0
  114. package/dist/modules/embed/mcp-app.d.ts.map +1 -0
  115. package/dist/modules/embed/mcp-app.js +147 -0
  116. package/dist/modules/embed/mcp-app.js.map +1 -0
  117. package/dist/modules/embed/mcp-app.routes.d.ts +21 -0
  118. package/dist/modules/embed/mcp-app.routes.d.ts.map +1 -0
  119. package/dist/modules/embed/mcp-app.routes.js +36 -0
  120. package/dist/modules/embed/mcp-app.routes.js.map +1 -0
  121. package/dist/modules/github-app/github-app.connection.d.ts +15 -0
  122. package/dist/modules/github-app/github-app.connection.d.ts.map +1 -1
  123. package/dist/modules/github-app/github-app.connection.js +34 -3
  124. package/dist/modules/github-app/github-app.connection.js.map +1 -1
  125. package/dist/modules/kb-fs/locking-filesystem.d.ts +65 -0
  126. package/dist/modules/kb-fs/locking-filesystem.d.ts.map +1 -1
  127. package/dist/modules/kb-fs/locking-filesystem.js +300 -0
  128. package/dist/modules/kb-fs/locking-filesystem.js.map +1 -1
  129. package/dist/modules/kb-sync/kb-sync.interface.d.ts +5 -0
  130. package/dist/modules/kb-sync/kb-sync.interface.d.ts.map +1 -1
  131. package/dist/modules/kb-sync/kb-sync.service.d.ts.map +1 -1
  132. package/dist/modules/kb-sync/kb-sync.service.js +6 -0
  133. package/dist/modules/kb-sync/kb-sync.service.js.map +1 -1
  134. package/dist/modules/mcp/mcp-auth.middleware.d.ts.map +1 -1
  135. package/dist/modules/mcp/mcp-auth.middleware.js +2 -1
  136. package/dist/modules/mcp/mcp-auth.middleware.js.map +1 -1
  137. package/dist/modules/mcp/mcp.service.d.ts +28 -1
  138. package/dist/modules/mcp/mcp.service.d.ts.map +1 -1
  139. package/dist/modules/mcp/mcp.service.js +64 -8
  140. package/dist/modules/mcp/mcp.service.js.map +1 -1
  141. package/dist/modules/mcp/oauth/bevel-oauth-provider.d.ts +24 -0
  142. package/dist/modules/mcp/oauth/bevel-oauth-provider.d.ts.map +1 -1
  143. package/dist/modules/mcp/oauth/bevel-oauth-provider.js +32 -4
  144. package/dist/modules/mcp/oauth/bevel-oauth-provider.js.map +1 -1
  145. package/dist/modules/onboarding/onboarding.contract.d.ts +80 -0
  146. package/dist/modules/onboarding/onboarding.contract.d.ts.map +1 -0
  147. package/dist/modules/onboarding/onboarding.contract.js +7 -0
  148. package/dist/modules/onboarding/onboarding.contract.js.map +1 -0
  149. package/dist/modules/onboarding/onboarding.routes.d.ts +18 -0
  150. package/dist/modules/onboarding/onboarding.routes.d.ts.map +1 -0
  151. package/dist/modules/onboarding/onboarding.routes.js +81 -0
  152. package/dist/modules/onboarding/onboarding.routes.js.map +1 -0
  153. package/dist/modules/onboarding/starter-pack.service.d.ts +213 -0
  154. package/dist/modules/onboarding/starter-pack.service.d.ts.map +1 -0
  155. package/dist/modules/onboarding/starter-pack.service.js +492 -0
  156. package/dist/modules/onboarding/starter-pack.service.js.map +1 -0
  157. package/dist/modules/onboarding/starter-packs.d.ts +78 -0
  158. package/dist/modules/onboarding/starter-packs.d.ts.map +1 -0
  159. package/dist/modules/onboarding/starter-packs.js +156 -0
  160. package/dist/modules/onboarding/starter-packs.js.map +1 -0
  161. package/dist/modules/plugins/index.d.ts +1 -1
  162. package/dist/modules/plugins/index.d.ts.map +1 -1
  163. package/dist/modules/plugins/index.js +1 -1
  164. package/dist/modules/plugins/index.js.map +1 -1
  165. package/dist/modules/plugins/plugin-provision.service.d.ts +19 -0
  166. package/dist/modules/plugins/plugin-provision.service.d.ts.map +1 -1
  167. package/dist/modules/plugins/plugin-provision.service.js +18 -1
  168. package/dist/modules/plugins/plugin-provision.service.js.map +1 -1
  169. package/dist/modules/plugins/plugins.routes.d.ts +0 -36
  170. package/dist/modules/plugins/plugins.routes.d.ts.map +1 -1
  171. package/dist/modules/plugins/plugins.routes.js +31 -0
  172. package/dist/modules/plugins/plugins.routes.js.map +1 -1
  173. package/dist/modules/settings/deployment-settings.service.d.ts +34 -0
  174. package/dist/modules/settings/deployment-settings.service.d.ts.map +1 -1
  175. package/dist/modules/settings/deployment-settings.service.js +134 -1
  176. package/dist/modules/settings/deployment-settings.service.js.map +1 -1
  177. package/dist/modules/settings/repository-source.d.ts +1 -0
  178. package/dist/modules/settings/repository-source.d.ts.map +1 -1
  179. package/dist/modules/settings/repository-source.js +3 -3
  180. package/dist/modules/settings/repository-source.js.map +1 -1
  181. package/dist/modules/skills/skills.tools.d.ts.map +1 -1
  182. package/dist/modules/skills/skills.tools.js +50 -32
  183. package/dist/modules/skills/skills.tools.js.map +1 -1
  184. package/dist/modules/tool-auth/connection-key-rejection.d.ts +10 -1
  185. package/dist/modules/tool-auth/connection-key-rejection.d.ts.map +1 -1
  186. package/dist/modules/tool-auth/connection-key-rejection.js +11 -3
  187. package/dist/modules/tool-auth/connection-key-rejection.js.map +1 -1
  188. package/dist/modules/tool-auth/external-api-key.interface.d.ts +8 -0
  189. package/dist/modules/tool-auth/external-api-key.interface.d.ts.map +1 -1
  190. package/dist/modules/tool-auth/external-api-key.service.d.ts +24 -2
  191. package/dist/modules/tool-auth/external-api-key.service.d.ts.map +1 -1
  192. package/dist/modules/tool-auth/external-api-key.service.js +59 -2
  193. package/dist/modules/tool-auth/external-api-key.service.js.map +1 -1
  194. package/dist/modules/tool-auth/tool-auth.middleware.d.ts.map +1 -1
  195. package/dist/modules/tool-auth/tool-auth.middleware.js +9 -4
  196. package/dist/modules/tool-auth/tool-auth.middleware.js.map +1 -1
  197. package/dist/modules/tool-helpers/index.d.ts +28 -1
  198. package/dist/modules/tool-helpers/index.d.ts.map +1 -1
  199. package/dist/modules/tool-helpers/index.js +28 -1
  200. package/dist/modules/tool-helpers/index.js.map +1 -1
  201. package/dist/modules/tool-helpers/route-argument-check.d.ts +10 -0
  202. package/dist/modules/tool-helpers/route-argument-check.d.ts.map +1 -0
  203. package/dist/modules/tool-helpers/route-argument-check.js +132 -0
  204. package/dist/modules/tool-helpers/route-argument-check.js.map +1 -0
  205. package/dist/modules/tool-helpers/route-tool-schemas.d.ts +89 -0
  206. package/dist/modules/tool-helpers/route-tool-schemas.d.ts.map +1 -0
  207. package/dist/modules/tool-helpers/route-tool-schemas.js +113 -0
  208. package/dist/modules/tool-helpers/route-tool-schemas.js.map +1 -0
  209. package/dist/modules/tool-helpers/tool-def.d.ts +71 -0
  210. package/dist/modules/tool-helpers/tool-def.d.ts.map +1 -1
  211. package/dist/modules/tool-helpers/tool-def.js +70 -6
  212. package/dist/modules/tool-helpers/tool-def.js.map +1 -1
  213. package/dist/modules/tool-helpers/tool-handler.d.ts +46 -6
  214. package/dist/modules/tool-helpers/tool-handler.d.ts.map +1 -1
  215. package/dist/modules/tool-helpers/tool-handler.js +157 -8
  216. package/dist/modules/tool-helpers/tool-handler.js.map +1 -1
  217. package/dist/modules/tool-helpers/tool.contract.d.ts +9 -0
  218. package/dist/modules/tool-helpers/tool.contract.d.ts.map +1 -1
  219. package/dist/modules/tool-helpers/tool.contract.js.map +1 -1
  220. package/dist/modules/tool-manuals/tool-manuals.tools.d.ts.map +1 -1
  221. package/dist/modules/tool-manuals/tool-manuals.tools.js +14 -5
  222. package/dist/modules/tool-manuals/tool-manuals.tools.js.map +1 -1
  223. package/dist/modules/tool-registry/description-length.d.ts +6 -2
  224. package/dist/modules/tool-registry/description-length.d.ts.map +1 -1
  225. package/dist/modules/tool-registry/description-length.js +27 -5
  226. package/dist/modules/tool-registry/description-length.js.map +1 -1
  227. package/dist/modules/tool-registry/guide-first.d.ts +4 -0
  228. package/dist/modules/tool-registry/guide-first.d.ts.map +1 -1
  229. package/dist/modules/tool-registry/guide-first.js +9 -1
  230. package/dist/modules/tool-registry/guide-first.js.map +1 -1
  231. package/dist/modules/workflow/agent-tools/change-request-read.tools.d.ts +53 -0
  232. package/dist/modules/workflow/agent-tools/change-request-read.tools.d.ts.map +1 -1
  233. package/dist/modules/workflow/agent-tools/change-request-read.tools.js +97 -68
  234. package/dist/modules/workflow/agent-tools/change-request-read.tools.js.map +1 -1
  235. package/dist/modules/workflow/agent-tools/workflow.tools.d.ts +8 -1
  236. package/dist/modules/workflow/agent-tools/workflow.tools.d.ts.map +1 -1
  237. package/dist/modules/workflow/agent-tools/workflow.tools.js +124 -37
  238. package/dist/modules/workflow/agent-tools/workflow.tools.js.map +1 -1
  239. package/dist/modules/workflow/file-lock.service.d.ts +29 -5
  240. package/dist/modules/workflow/file-lock.service.d.ts.map +1 -1
  241. package/dist/modules/workflow/file-lock.service.js +62 -5
  242. package/dist/modules/workflow/file-lock.service.js.map +1 -1
  243. package/dist/modules/workflow/git/git.service.d.ts +72 -1
  244. package/dist/modules/workflow/git/git.service.d.ts.map +1 -1
  245. package/dist/modules/workflow/git/git.service.js +172 -36
  246. package/dist/modules/workflow/git/git.service.js.map +1 -1
  247. package/dist/modules/workflow/git/node-git-runner.d.ts +15 -0
  248. package/dist/modules/workflow/git/node-git-runner.d.ts.map +1 -1
  249. package/dist/modules/workflow/git/node-git-runner.js +78 -6
  250. package/dist/modules/workflow/git/node-git-runner.js.map +1 -1
  251. package/dist/modules/workflow/pending-commits.service.d.ts +15 -0
  252. package/dist/modules/workflow/pending-commits.service.d.ts.map +1 -1
  253. package/dist/modules/workflow/pending-commits.service.js +25 -1
  254. package/dist/modules/workflow/pending-commits.service.js.map +1 -1
  255. package/dist/modules/workflow/pending-commits.worker.d.ts +6 -1
  256. package/dist/modules/workflow/pending-commits.worker.d.ts.map +1 -1
  257. package/dist/modules/workflow/pending-commits.worker.js +23 -6
  258. package/dist/modules/workflow/pending-commits.worker.js.map +1 -1
  259. package/dist/modules/workflow/recovery-bot.js +1 -1
  260. package/dist/modules/workflow/recovery-bot.js.map +1 -1
  261. package/dist/modules/workflow/review-workflow/review-workflow.service.d.ts.map +1 -1
  262. package/dist/modules/workflow/review-workflow/review-workflow.service.js +2 -1
  263. package/dist/modules/workflow/review-workflow/review-workflow.service.js.map +1 -1
  264. package/dist/modules/workflow/sanitize-error.d.ts +16 -0
  265. package/dist/modules/workflow/sanitize-error.d.ts.map +1 -1
  266. package/dist/modules/workflow/sanitize-error.js +45 -0
  267. package/dist/modules/workflow/sanitize-error.js.map +1 -1
  268. package/dist/modules/workflow/workflow.service.d.ts +183 -15
  269. package/dist/modules/workflow/workflow.service.d.ts.map +1 -1
  270. package/dist/modules/workflow/workflow.service.js +805 -106
  271. package/dist/modules/workflow/workflow.service.js.map +1 -1
  272. package/dist/modules/workspace/agent-download.builder.d.ts +105 -0
  273. package/dist/modules/workspace/agent-download.builder.d.ts.map +1 -0
  274. package/dist/modules/workspace/agent-download.builder.js +238 -0
  275. package/dist/modules/workspace/agent-download.builder.js.map +1 -0
  276. package/dist/modules/workspace/agent-download.routes.d.ts +77 -0
  277. package/dist/modules/workspace/agent-download.routes.d.ts.map +1 -0
  278. package/dist/modules/workspace/agent-download.routes.js +156 -0
  279. package/dist/modules/workspace/agent-download.routes.js.map +1 -0
  280. package/dist/modules/workspace/agent-download.store.d.ts +225 -0
  281. package/dist/modules/workspace/agent-download.store.d.ts.map +1 -0
  282. package/dist/modules/workspace/agent-download.store.js +423 -0
  283. package/dist/modules/workspace/agent-download.store.js.map +1 -0
  284. package/dist/modules/workspace/agent-upload.store.d.ts +10 -1
  285. package/dist/modules/workspace/agent-upload.store.d.ts.map +1 -1
  286. package/dist/modules/workspace/agent-upload.store.js +16 -6
  287. package/dist/modules/workspace/agent-upload.store.js.map +1 -1
  288. package/dist/modules/workspace/file-readers/raw-mime.d.ts +6 -0
  289. package/dist/modules/workspace/file-readers/raw-mime.d.ts.map +1 -0
  290. package/dist/modules/workspace/file-readers/raw-mime.js +38 -0
  291. package/dist/modules/workspace/file-readers/raw-mime.js.map +1 -0
  292. package/dist/modules/workspace/file-readers/text-reader.d.ts +5 -5
  293. package/dist/modules/workspace/file-readers/text-reader.d.ts.map +1 -1
  294. package/dist/modules/workspace/file-readers/text-reader.js +5 -8
  295. package/dist/modules/workspace/file-readers/text-reader.js.map +1 -1
  296. package/dist/modules/workspace/first-run.d.ts +111 -0
  297. package/dist/modules/workspace/first-run.d.ts.map +1 -0
  298. package/dist/modules/workspace/first-run.js +181 -0
  299. package/dist/modules/workspace/first-run.js.map +1 -0
  300. package/dist/modules/workspace/move-links.d.ts +92 -0
  301. package/dist/modules/workspace/move-links.d.ts.map +1 -0
  302. package/dist/modules/workspace/move-links.js +173 -0
  303. package/dist/modules/workspace/move-links.js.map +1 -0
  304. package/dist/modules/workspace/startup/steps/seed-tree.d.ts.map +1 -1
  305. package/dist/modules/workspace/startup/steps/seed-tree.js +3 -13
  306. package/dist/modules/workspace/startup/steps/seed-tree.js.map +1 -1
  307. package/dist/modules/workspace/workspace.routes.d.ts.map +1 -1
  308. package/dist/modules/workspace/workspace.routes.js +63 -22
  309. package/dist/modules/workspace/workspace.routes.js.map +1 -1
  310. package/dist/modules/workspace/workspace.service.d.ts +75 -2
  311. package/dist/modules/workspace/workspace.service.d.ts.map +1 -1
  312. package/dist/modules/workspace/workspace.service.js +197 -24
  313. package/dist/modules/workspace/workspace.service.js.map +1 -1
  314. package/dist/modules/workspace/workspace.tools.d.ts +59 -1
  315. package/dist/modules/workspace/workspace.tools.d.ts.map +1 -1
  316. package/dist/modules/workspace/workspace.tools.js +443 -65
  317. package/dist/modules/workspace/workspace.tools.js.map +1 -1
  318. package/dist/modules/write-access/write-access.d.ts.map +1 -1
  319. package/dist/modules/write-access/write-access.js +9 -0
  320. package/dist/modules/write-access/write-access.js.map +1 -1
  321. package/dist/shared/domain-errors.d.ts +73 -1
  322. package/dist/shared/domain-errors.d.ts.map +1 -1
  323. package/dist/shared/domain-errors.js +86 -12
  324. package/dist/shared/domain-errors.js.map +1 -1
  325. package/dist/shared/git.contract.d.ts +10 -1
  326. package/dist/shared/git.contract.d.ts.map +1 -1
  327. package/dist/shared/git.contract.js.map +1 -1
  328. package/dist/shared/utf8-text.d.ts +15 -0
  329. package/dist/shared/utf8-text.d.ts.map +1 -0
  330. package/dist/shared/utf8-text.js +20 -0
  331. package/dist/shared/utf8-text.js.map +1 -0
  332. package/dist/tenancy/static-tenant-source.d.ts +2 -1
  333. package/dist/tenancy/static-tenant-source.d.ts.map +1 -1
  334. package/dist/tenancy/static-tenant-source.js +9 -2
  335. package/dist/tenancy/static-tenant-source.js.map +1 -1
  336. package/mcp-app/page.html +349 -0
  337. package/migrations/0017_atlassian_account_links.sql +37 -0
  338. package/migrations/meta/0017_snapshot.json +2396 -0
  339. package/migrations/meta/_journal.json +8 -1
  340. package/package.json +5 -3
  341. package/src/__tests__/packaged-assets-ship.test.ts +16 -4
  342. package/src/assets.ts +28 -2
  343. package/src/core/__tests__/catalog-revision.test.ts +40 -0
  344. package/src/core/__tests__/gated-boot.test.ts +1 -0
  345. package/src/core/__tests__/lifecycle.test.ts +14 -0
  346. package/src/core/catalog-revision.ts +15 -2
  347. package/src/core/create-core-server.ts +67 -3
  348. package/src/core/create-core-services.ts +140 -6
  349. package/src/core/lifecycle.ts +28 -2
  350. package/src/core-config.ts +24 -1
  351. package/src/index.ts +2 -2
  352. package/src/modules/access/__tests__/access-control.service.test.ts +19 -0
  353. package/src/modules/access/access-control.interface.ts +13 -0
  354. package/src/modules/access/access-control.service.ts +16 -0
  355. package/src/modules/access/directory-sync-bot.ts +1 -1
  356. package/src/modules/agent-guide/__tests__/agent-guide.test.ts +58 -0
  357. package/src/modules/agent-guide/__tests__/agent-guide.tools.test.ts +19 -1
  358. package/src/modules/agent-guide/agent-guide.ts +9 -0
  359. package/src/modules/agent-guide/index.ts +1 -0
  360. package/src/modules/agent-instructions/__tests__/shared-file-rules.test.ts +14 -0
  361. package/src/modules/agent-instructions/compose.ts +17 -2
  362. package/src/modules/agent-instructions/shared-file-rules.ts +19 -19
  363. package/src/modules/audit/__tests__/agent-audit.service.test.ts +64 -4
  364. package/src/modules/audit/__tests__/agent-connection.routes.test.ts +102 -0
  365. package/src/modules/audit/agent-audit.service.ts +58 -3
  366. package/src/modules/audit/agent-connection.routes.ts +56 -0
  367. package/src/modules/audit/audit.contract.ts +26 -0
  368. package/src/modules/audit/index.ts +1 -0
  369. package/src/modules/auth/__tests__/account.routes.test.ts +5 -3
  370. package/src/modules/auth/account.routes.ts +10 -3
  371. package/src/modules/auth/auth.middleware.ts +1 -1
  372. package/src/modules/code-mode/__tests__/code-mode.tool.test.ts +18 -2
  373. package/src/modules/code-mode/code-mode.tool.ts +13 -2
  374. package/src/modules/database/core-schema.ts +33 -3
  375. package/src/modules/embed/__tests__/account-links.migration.test.ts +98 -0
  376. package/src/modules/embed/__tests__/embed-link.test.ts +122 -0
  377. package/src/modules/embed/__tests__/embed.routes.test.ts +362 -0
  378. package/src/modules/embed/__tests__/embed.service.lock.test.ts +193 -0
  379. package/src/modules/embed/__tests__/embed.service.test.ts +805 -0
  380. package/src/modules/embed/__tests__/embed.tools.test.ts +304 -0
  381. package/src/modules/embed/__tests__/mcp-app.test.ts +207 -0
  382. package/src/modules/embed/account-link.service.ts +76 -0
  383. package/src/modules/embed/embed-link.ts +180 -0
  384. package/src/modules/embed/embed.errors.ts +43 -0
  385. package/src/modules/embed/embed.interface.ts +203 -0
  386. package/src/modules/embed/embed.routes.ts +363 -0
  387. package/src/modules/embed/embed.service.ts +705 -0
  388. package/src/modules/embed/embed.tools.ts +206 -0
  389. package/src/modules/embed/mcp-app.routes.ts +41 -0
  390. package/src/modules/embed/mcp-app.ts +173 -0
  391. package/src/modules/github-app/__tests__/github-app.test.ts +76 -0
  392. package/src/modules/github-app/github-app.connection.ts +38 -4
  393. package/src/modules/kb-fs/__tests__/move-with-edits.test.ts +304 -0
  394. package/src/modules/kb-fs/locking-filesystem.ts +287 -0
  395. package/src/modules/kb-sync/__tests__/kb-sync.service.test.ts +35 -0
  396. package/src/modules/kb-sync/kb-sync.interface.ts +2 -0
  397. package/src/modules/kb-sync/kb-sync.service.ts +6 -0
  398. package/src/modules/mcp/__tests__/bevel-oauth-provider.test.ts +68 -11
  399. package/src/modules/mcp/__tests__/mcp-proxy-helpers.test.ts +37 -2
  400. package/src/modules/mcp/__tests__/mcp.app.e2e.test.ts +200 -0
  401. package/src/modules/mcp/__tests__/mcp.e2e.test.ts +3 -1
  402. package/src/modules/mcp/__tests__/mcp.service.test.ts +23 -13
  403. package/src/modules/mcp/__tests__/tool-call-interface.e2e.test.ts +499 -0
  404. package/src/modules/mcp/mcp-auth.middleware.ts +2 -1
  405. package/src/modules/mcp/mcp.service.ts +81 -12
  406. package/src/modules/mcp/oauth/bevel-oauth-provider.ts +42 -4
  407. package/src/modules/onboarding/__tests__/onboarding.routes.test.ts +163 -0
  408. package/src/modules/onboarding/__tests__/starter-pack.service.test.ts +635 -0
  409. package/src/modules/onboarding/__tests__/starter-packs.test.ts +169 -0
  410. package/src/modules/onboarding/onboarding.contract.ts +85 -0
  411. package/src/modules/onboarding/onboarding.routes.ts +85 -0
  412. package/src/modules/onboarding/starter-pack.service.ts +580 -0
  413. package/src/modules/onboarding/starter-packs.ts +182 -0
  414. package/src/modules/plugins/__tests__/plugins.tools.test.ts +25 -2
  415. package/src/modules/plugins/index.ts +1 -0
  416. package/src/modules/plugins/plugin-provision.service.ts +22 -1
  417. package/src/modules/plugins/plugins.routes.ts +29 -0
  418. package/src/modules/settings/__tests__/deployment-settings.service.test.ts +150 -1
  419. package/src/modules/settings/__tests__/repository-source.test.ts +9 -0
  420. package/src/modules/settings/deployment-settings.service.ts +131 -1
  421. package/src/modules/settings/repository-source.ts +4 -4
  422. package/src/modules/skills/__tests__/skills.tools.description.test.ts +5 -1
  423. package/src/modules/skills/skills.tools.ts +55 -35
  424. package/src/modules/tool-auth/__tests__/connection-key-rejection.test.ts +32 -0
  425. package/src/modules/tool-auth/__tests__/external-api-key.service.test.ts +65 -0
  426. package/src/modules/tool-auth/connection-key-rejection.ts +13 -3
  427. package/src/modules/tool-auth/external-api-key.interface.ts +9 -0
  428. package/src/modules/tool-auth/external-api-key.service.ts +61 -4
  429. package/src/modules/tool-auth/tool-auth.middleware.ts +13 -4
  430. package/src/modules/tool-helpers/__tests__/branch-declaration.test.ts +502 -0
  431. package/src/modules/tool-helpers/__tests__/route-argument-check.test.ts +337 -0
  432. package/src/modules/tool-helpers/index.ts +34 -1
  433. package/src/modules/tool-helpers/route-argument-check.ts +149 -0
  434. package/src/modules/tool-helpers/route-tool-schemas.ts +171 -0
  435. package/src/modules/tool-helpers/tool-def.ts +132 -7
  436. package/src/modules/tool-helpers/tool-handler.ts +196 -8
  437. package/src/modules/tool-helpers/tool.contract.ts +9 -0
  438. package/src/modules/tool-manuals/tool-manuals.tools.ts +15 -5
  439. package/src/modules/tool-registry/__tests__/own-tool-schemas.test.ts +14 -1
  440. package/src/modules/tool-registry/__tests__/tool-description-length.test.ts +23 -10
  441. package/src/modules/tool-registry/description-length.ts +34 -7
  442. package/src/modules/tool-registry/guide-first.ts +8 -1
  443. package/src/modules/workflow/__tests__/file-lock.service.deletion-gate.test.ts +104 -0
  444. package/src/modules/workflow/__tests__/pending-commits.repository-replaced.test.ts +66 -0
  445. package/src/modules/workflow/__tests__/pending-commits.worker.test.ts +31 -2
  446. package/src/modules/workflow/__tests__/preserve-roles-yaml.test.ts +110 -6
  447. package/src/modules/workflow/__tests__/sanitize-error.test.ts +89 -1
  448. package/src/modules/workflow/__tests__/workflow.routes.push-refused.test.ts +202 -0
  449. package/src/modules/workflow/__tests__/workflow.service.branch-in-use.test.ts +6 -6
  450. package/src/modules/workflow/__tests__/workflow.service.delete-branch.test.ts +1026 -0
  451. package/src/modules/workflow/__tests__/workflow.service.facade.test.ts +468 -10
  452. package/src/modules/workflow/__tests__/workflow.service.folder-proposals.test.ts +4 -0
  453. package/src/modules/workflow/__tests__/workflow.service.releaseLock.test.ts +5 -4
  454. package/src/modules/workflow/__tests__/workflow.service.update-from-target.test.ts +54 -2
  455. package/src/modules/workflow/agent-tools/__tests__/change-request-read.tools.test.ts +41 -5
  456. package/src/modules/workflow/agent-tools/__tests__/workflow.tools.test.ts +127 -2
  457. package/src/modules/workflow/agent-tools/change-request-read.tools.ts +157 -122
  458. package/src/modules/workflow/agent-tools/workflow.tools.ts +129 -35
  459. package/src/modules/workflow/file-lock.service.ts +67 -5
  460. package/src/modules/workflow/git/__tests__/git.service.deleteBranch.test.ts +164 -0
  461. package/src/modules/workflow/git/__tests__/git.service.pull.test.ts +48 -1
  462. package/src/modules/workflow/git/__tests__/node-git-runner.test.ts +231 -2
  463. package/src/modules/workflow/git/git.service.ts +176 -38
  464. package/src/modules/workflow/git/node-git-runner.ts +82 -7
  465. package/src/modules/workflow/pending-commits.service.ts +29 -1
  466. package/src/modules/workflow/pending-commits.worker.ts +24 -6
  467. package/src/modules/workflow/recovery-bot.ts +1 -1
  468. package/src/modules/workflow/review-workflow/review-workflow.service.ts +2 -1
  469. package/src/modules/workflow/sanitize-error.ts +50 -0
  470. package/src/modules/workflow/workflow.service.ts +879 -112
  471. package/src/modules/workspace/__tests__/agent-downloads.test.ts +823 -0
  472. package/src/modules/workspace/__tests__/first-run.test.ts +234 -0
  473. package/src/modules/workspace/__tests__/git-internals.security.test.ts +5 -2
  474. package/src/modules/workspace/__tests__/md-links.test.ts +353 -0
  475. package/src/modules/workspace/__tests__/move-links.test.ts +261 -0
  476. package/src/modules/workspace/__tests__/rest-tool-arguments.test.ts +219 -0
  477. package/src/modules/workspace/__tests__/workspace-paths-inside-the-repo.test.ts +3 -0
  478. package/src/modules/workspace/__tests__/workspace.routes.delete.test.ts +102 -0
  479. package/src/modules/workspace/__tests__/workspace.routes.download.test.ts +52 -5
  480. package/src/modules/workspace/__tests__/workspace.routes.folder-zip-access.test.ts +190 -0
  481. package/src/modules/workspace/__tests__/workspace.service.forget-clone-races.test.ts +8 -4
  482. package/src/modules/workspace/__tests__/workspace.service.test.ts +50 -8
  483. package/src/modules/workspace/__tests__/workspace.tools.branch-errors.test.ts +44 -1
  484. package/src/modules/workspace/__tests__/workspace.tools.test.ts +409 -12
  485. package/src/modules/workspace/agent-download.builder.ts +308 -0
  486. package/src/modules/workspace/agent-download.routes.ts +183 -0
  487. package/src/modules/workspace/agent-download.store.ts +533 -0
  488. package/src/modules/workspace/agent-upload.store.ts +23 -7
  489. package/src/modules/workspace/file-readers/raw-mime.ts +40 -0
  490. package/src/modules/workspace/file-readers/text-reader.ts +5 -8
  491. package/src/modules/workspace/first-run.ts +222 -0
  492. package/src/modules/workspace/move-links.ts +237 -0
  493. package/src/modules/workspace/startup/steps/seed-tree.ts +3 -12
  494. package/src/modules/workspace/workspace.routes.ts +64 -22
  495. package/src/modules/workspace/workspace.service.ts +202 -25
  496. package/src/modules/workspace/workspace.tools.ts +492 -63
  497. package/src/modules/write-access/__tests__/write-access.test.ts +13 -0
  498. package/src/modules/write-access/write-access.ts +9 -0
  499. package/src/shared/domain-errors.ts +108 -11
  500. package/src/shared/git.contract.ts +8 -1
  501. package/src/shared/utf8-text.ts +21 -0
  502. package/src/tenancy/__tests__/static-tenant-source.test.ts +1 -0
  503. package/src/tenancy/static-tenant-source.ts +10 -2
  504. package/starter-packs/README.md +103 -0
  505. package/starter-packs/engineering/KnowledgeBase/About us.md +9 -0
  506. package/starter-packs/engineering/KnowledgeBase/Architecture.md +9 -0
  507. package/starter-packs/engineering/KnowledgeBase/Glossary.md +9 -0
  508. package/starter-packs/engineering/KnowledgeBase/How we ship.md +9 -0
  509. package/starter-packs/engineering/Plugins/engineering-starter/LICENSE +21 -0
  510. package/starter-packs/engineering/Plugins/engineering-starter/SOURCE.md +20 -0
  511. package/starter-packs/engineering/Plugins/engineering-starter/access.md +19 -0
  512. package/starter-packs/engineering/Plugins/engineering-starter/plugin.json +10 -0
  513. package/starter-packs/engineering/Plugins/engineering-starter/skills/office-hours/SKILL.md +171 -0
  514. package/starter-packs/engineering/Plugins/engineering-starter/skills/office-hours/sections/phase-2a-startup-diagnostic.md +157 -0
  515. package/starter-packs/engineering/Plugins/engineering-starter/skills/office-hours/sections/phase-2b-builder-brainstorm.md +39 -0
  516. package/starter-packs/engineering/Plugins/engineering-starter/skills/plan-ceo-review/SKILL.md +144 -0
  517. package/starter-packs/engineering/Plugins/engineering-starter/skills/plan-eng-review/SKILL.md +144 -0
  518. package/starter-packs/engineering/Plugins/engineering-starter/skills/review/SKILL.md +80 -0
  519. package/starter-packs/engineering/Plugins/engineering-starter/skills/review/checklist.md +184 -0
  520. package/starter-packs/engineering/pack.yaml +11 -0
  521. package/starter-packs/general/KnowledgeBase/About us.md +9 -0
  522. package/starter-packs/general/KnowledgeBase/Glossary.md +9 -0
  523. package/starter-packs/general/KnowledgeBase/How we work.md +9 -0
  524. package/starter-packs/general/Plugins/general-starter/LICENSE +202 -0
  525. package/starter-packs/general/Plugins/general-starter/SOURCE.md +10 -0
  526. package/starter-packs/general/Plugins/general-starter/access.md +19 -0
  527. package/starter-packs/general/Plugins/general-starter/plugin.json +10 -0
  528. package/starter-packs/general/Plugins/general-starter/skills/memory-management/SKILL.md +332 -0
  529. package/starter-packs/general/Plugins/general-starter/skills/productivity-start/SKILL.md +163 -0
  530. package/starter-packs/general/Plugins/general-starter/skills/productivity-update/SKILL.md +167 -0
  531. package/starter-packs/general/Plugins/general-starter/skills/task-management/SKILL.md +83 -0
  532. package/starter-packs/general/pack.yaml +10 -0
  533. package/starter-packs/operations/KnowledgeBase/Glossary.md +9 -0
  534. package/starter-packs/operations/KnowledgeBase/How we work.md +9 -0
  535. package/starter-packs/operations/KnowledgeBase/Processes.md +9 -0
  536. package/starter-packs/operations/KnowledgeBase/Tools we use.md +9 -0
  537. package/starter-packs/operations/KnowledgeBase/Vendors.md +9 -0
  538. package/starter-packs/operations/Plugins/operations-starter/LICENSE +202 -0
  539. package/starter-packs/operations/Plugins/operations-starter/SOURCE.md +10 -0
  540. package/starter-packs/operations/Plugins/operations-starter/access.md +19 -0
  541. package/starter-packs/operations/Plugins/operations-starter/plugin.json +10 -0
  542. package/starter-packs/operations/Plugins/operations-starter/skills/capacity-plan/SKILL.md +111 -0
  543. package/starter-packs/operations/Plugins/operations-starter/skills/change-request/SKILL.md +119 -0
  544. package/starter-packs/operations/Plugins/operations-starter/skills/compliance-tracking/SKILL.md +44 -0
  545. package/starter-packs/operations/Plugins/operations-starter/skills/process-doc/SKILL.md +80 -0
  546. package/starter-packs/operations/Plugins/operations-starter/skills/process-optimization/SKILL.md +39 -0
  547. package/starter-packs/operations/Plugins/operations-starter/skills/risk-assessment/SKILL.md +40 -0
  548. package/starter-packs/operations/Plugins/operations-starter/skills/runbook/SKILL.md +85 -0
  549. package/starter-packs/operations/Plugins/operations-starter/skills/status-report/SKILL.md +75 -0
  550. package/starter-packs/operations/Plugins/operations-starter/skills/vendor-review/SKILL.md +102 -0
  551. package/starter-packs/operations/pack.yaml +12 -0
  552. package/starter-packs/sales/KnowledgeBase/About us.md +9 -0
  553. package/starter-packs/sales/KnowledgeBase/Customers.md +9 -0
  554. package/starter-packs/sales/KnowledgeBase/Glossary.md +9 -0
  555. package/starter-packs/sales/KnowledgeBase/How we sell.md +9 -0
  556. package/starter-packs/sales/KnowledgeBase/Objections.md +9 -0
  557. package/starter-packs/sales/KnowledgeBase/Pricing.md +9 -0
  558. package/starter-packs/sales/Plugins/sales-starter/LICENSE +202 -0
  559. package/starter-packs/sales/Plugins/sales-starter/SOURCE.md +10 -0
  560. package/starter-packs/sales/Plugins/sales-starter/access.md +19 -0
  561. package/starter-packs/sales/Plugins/sales-starter/plugin.json +10 -0
  562. package/starter-packs/sales/Plugins/sales-starter/skills/account-context/SKILL.md +94 -0
  563. package/starter-packs/sales/Plugins/sales-starter/skills/account-plan/SKILL.md +95 -0
  564. package/starter-packs/sales/Plugins/sales-starter/skills/account-research/SKILL.md +99 -0
  565. package/starter-packs/sales/Plugins/sales-starter/skills/account-tiering/SKILL.md +101 -0
  566. package/starter-packs/sales/Plugins/sales-starter/skills/call-prep/SKILL.md +111 -0
  567. package/starter-packs/sales/Plugins/sales-starter/skills/call-summary/SKILL.md +111 -0
  568. package/starter-packs/sales/Plugins/sales-starter/skills/close-plan/SKILL.md +106 -0
  569. package/starter-packs/sales/Plugins/sales-starter/skills/competitive-intelligence/SKILL.md +102 -0
  570. package/starter-packs/sales/Plugins/sales-starter/skills/create-an-asset/SKILL.md +105 -0
  571. package/starter-packs/sales/Plugins/sales-starter/skills/crm-hygiene-check/SKILL.md +103 -0
  572. package/starter-packs/sales/Plugins/sales-starter/skills/customer-health/SKILL.md +108 -0
  573. package/starter-packs/sales/Plugins/sales-starter/skills/customer-voice/SKILL.md +94 -0
  574. package/starter-packs/sales/Plugins/sales-starter/skills/daily-briefing/SKILL.md +61 -0
  575. package/starter-packs/sales/Plugins/sales-starter/skills/deal-advance-gap/SKILL.md +102 -0
  576. package/starter-packs/sales/Plugins/sales-starter/skills/deal-review/SKILL.md +105 -0
  577. package/starter-packs/sales/Plugins/sales-starter/skills/deal-signals/SKILL.md +96 -0
  578. package/starter-packs/sales/Plugins/sales-starter/skills/deal-slip-scenario/SKILL.md +104 -0
  579. package/starter-packs/sales/Plugins/sales-starter/skills/draft-outreach/SKILL.md +116 -0
  580. package/starter-packs/sales/Plugins/sales-starter/skills/end-of-day/SKILL.md +114 -0
  581. package/starter-packs/sales/Plugins/sales-starter/skills/expansion-whitespace/SKILL.md +97 -0
  582. package/starter-packs/sales/Plugins/sales-starter/skills/forecast/SKILL.md +100 -0
  583. package/starter-packs/sales/Plugins/sales-starter/skills/handle-objection/SKILL.md +93 -0
  584. package/starter-packs/sales/Plugins/sales-starter/skills/inbox-sweep/SKILL.md +114 -0
  585. package/starter-packs/sales/Plugins/sales-starter/skills/lead-triage/SKILL.md +106 -0
  586. package/starter-packs/sales/Plugins/sales-starter/skills/log-activity/SKILL.md +111 -0
  587. package/starter-packs/sales/Plugins/sales-starter/skills/pipeline-review/SKILL.md +97 -0
  588. package/starter-packs/sales/Plugins/sales-starter/skills/renewal-radar/SKILL.md +95 -0
  589. package/starter-packs/sales/Plugins/sales-starter/skills/rep-context/SKILL.md +90 -0
  590. package/starter-packs/sales/Plugins/sales-starter/skills/route-lead/SKILL.md +111 -0
  591. package/starter-packs/sales/Plugins/sales-starter/skills/sales-setup/SKILL.md +200 -0
  592. package/starter-packs/sales/Plugins/sales-starter/skills/schedule-meeting/SKILL.md +99 -0
  593. package/starter-packs/sales/Plugins/sales-starter/skills/stakeholder-map/SKILL.md +107 -0
  594. package/starter-packs/sales/Plugins/sales-starter/skills/team-pipeline/SKILL.md +105 -0
  595. package/starter-packs/sales/Plugins/sales-starter/skills/update-opportunity/SKILL.md +112 -0
  596. package/starter-packs/sales/Plugins/sales-starter/skills/weekly-wrap/SKILL.md +90 -0
  597. package/starter-packs/sales/Plugins/sales-starter/skills/win-loss-review/SKILL.md +88 -0
  598. package/starter-packs/sales/pack.yaml +13 -0
  599. package/starter-packs/support/KnowledgeBase/Escalations.md +9 -0
  600. package/starter-packs/support/KnowledgeBase/Glossary.md +9 -0
  601. package/starter-packs/support/KnowledgeBase/Known issues.md +9 -0
  602. package/starter-packs/support/KnowledgeBase/Products.md +9 -0
  603. package/starter-packs/support/KnowledgeBase/Tone of voice.md +9 -0
  604. package/starter-packs/support/Plugins/support-starter/LICENSE +202 -0
  605. package/starter-packs/support/Plugins/support-starter/SOURCE.md +10 -0
  606. package/starter-packs/support/Plugins/support-starter/access.md +19 -0
  607. package/starter-packs/support/Plugins/support-starter/plugin.json +10 -0
  608. package/starter-packs/support/Plugins/support-starter/skills/customer-escalation/SKILL.md +246 -0
  609. package/starter-packs/support/Plugins/support-starter/skills/customer-research/SKILL.md +250 -0
  610. package/starter-packs/support/Plugins/support-starter/skills/draft-response/SKILL.md +416 -0
  611. package/starter-packs/support/Plugins/support-starter/skills/kb-article/SKILL.md +350 -0
  612. package/starter-packs/support/Plugins/support-starter/skills/ticket-triage/SKILL.md +273 -0
  613. package/starter-packs/support/pack.yaml +12 -0
@@ -11,9 +11,11 @@ import type { PendingCommitsService } from '../pending-commits.service.js';
11
11
  import type { Database } from '../../database/connection.js';
12
12
  import { WorkflowService } from '../workflow.service.js';
13
13
  import { openChangeGate } from '../../../__tests__/open-change-gate.js';
14
+ import type { WorkflowEventBus } from '../event-bus.js';
14
15
  import {
15
16
  ChangeRequestConflictsError,
16
17
  PullRebaseConflictError,
18
+ PushNeedsAgentResolutionError,
17
19
  WorkflowDomainError,
18
20
  } from '../../../shared/domain-errors.js';
19
21
 
@@ -76,6 +78,8 @@ function harness(opts: {
76
78
  /** Does the clone hold commits origin has not seen? */
77
79
  unpushed?: boolean;
78
80
  unpushedError?: Error;
81
+ /** Each push answers the next of these in turn: an Error rejects, undefined lands. */
82
+ pushes?: Array<Error | undefined>;
79
83
  }) {
80
84
  // Two resolves per update: once after the pull (where the branch IS), once
81
85
  // after the merge (where it ended up). One mock that answers in order, so a
@@ -93,7 +97,10 @@ function harness(opts: {
93
97
  ? vi.fn().mockRejectedValue(opts.pullError)
94
98
  : vi.fn().mockResolvedValue({ treeChanged: false }),
95
99
  mergeFromOrigin: vi.fn().mockResolvedValue(opts.merge ?? { kind: 'clean', alreadyUpToDate: false }),
96
- push: vi.fn().mockResolvedValue(undefined),
100
+ push: (opts.pushes ?? []).reduce(
101
+ (fn, outcome) => (outcome ? fn.mockRejectedValueOnce(outcome) : fn.mockResolvedValueOnce(undefined)),
102
+ vi.fn().mockResolvedValue(undefined),
103
+ ),
97
104
  hasUnpushedCommits: opts.unpushedError
98
105
  ? vi.fn().mockRejectedValue(opts.unpushedError)
99
106
  : vi.fn().mockResolvedValue(opts.unpushed ?? true),
@@ -114,6 +121,7 @@ function harness(opts: {
114
121
  const reviewWorkflow = {
115
122
  carryApprovalsForward: vi.fn().mockResolvedValue(opts.carried ?? 0),
116
123
  };
124
+ const emit = vi.fn();
117
125
  const svc = new WorkflowService(
118
126
  {} as unknown as Database,
119
127
  git as unknown as GitService,
@@ -125,8 +133,9 @@ function harness(opts: {
125
133
  pendingCommits as unknown as PendingCommitsService,
126
134
  testKbContext(),
127
135
  openChangeGate(),
136
+ { emit } as unknown as WorkflowEventBus,
128
137
  );
129
- return { svc, git, prs, pendingCommits, reviewWorkflow, refreshed };
138
+ return { svc, git, prs, pendingCommits, reviewWorkflow, refreshed, emit };
130
139
  }
131
140
 
132
141
  describe('WorkflowService.updateFromTarget', () => {
@@ -385,4 +394,47 @@ describe('WorkflowService.updateFromTarget: the approvals the merge did not touc
385
394
  // slow beats a dialog that goes on presenting pre-merge text as current.
386
395
  expect(result.updatedPaths).toEqual(['Sales/Deal.md']);
387
396
  });
397
+
398
+ describe('when the repository host refuses the push', () => {
399
+ const REFUSED = new Error('git push failed: remote: Internal Server Error');
400
+
401
+ it('keeps the merge applied locally and answers 409 with the saved-locally sentence and the banner', async () => {
402
+ const h = harness({ changedPaths: ['Sales/Deal.md'], pushes: [REFUSED] });
403
+ const err = await h.svc.updateFromTarget(WS, USER, 7).catch((e: unknown) => e);
404
+
405
+ expect(err).toBeInstanceOf(PushNeedsAgentResolutionError);
406
+ expect((err as PushNeedsAgentResolutionError).status).toBe(409);
407
+ expect((err as Error).message).toContain('Saved locally on "alice/deal"');
408
+ expect(JSON.stringify((err as PushNeedsAgentResolutionError).payload)).not.toContain('Internal Server Error');
409
+ // One attempt: a refusal is not a divergence, so nothing is retried.
410
+ expect(h.git.push).toHaveBeenCalledTimes(1);
411
+ // The merge ran and stays in the clone — the next push carries it.
412
+ expect(h.git.mergeFromOrigin).toHaveBeenCalledTimes(1);
413
+ expect(h.emit).toHaveBeenCalledWith(
414
+ expect.objectContaining({ kind: 'git-sync-failed', workspaceId: 'alice/deal', branch: 'alice/deal' }),
415
+ );
416
+ });
417
+
418
+ it('the next update pushes what is still unpushed, and the banner clears', async () => {
419
+ const h = harness({
420
+ changedPaths: ['Sales/Deal.md'],
421
+ unpushed: true,
422
+ pushes: [REFUSED, undefined],
423
+ });
424
+ await expect(h.svc.updateFromTarget(WS, USER, 7)).rejects.toBeInstanceOf(PushNeedsAgentResolutionError);
425
+ await h.svc.updateFromTarget(WS, USER, 7);
426
+ const kinds = h.emit.mock.calls.map((c) => (c[0] as { kind: string }).kind).filter((k) => k.startsWith('git-sync-'));
427
+ expect(kinds).toEqual(['git-sync-failed', 'git-sync-recovered']);
428
+ });
429
+
430
+ it('a non-fast-forward recovers with a merge-preserving pull, so the merge stays a merge', async () => {
431
+ const h = harness({
432
+ changedPaths: ['Sales/Deal.md'],
433
+ pushes: [new Error('! [rejected] alice/deal -> alice/deal (non-fast-forward)'), undefined],
434
+ });
435
+ await h.svc.updateFromTarget(WS, USER, 7);
436
+ expect(h.git.push).toHaveBeenCalledTimes(2);
437
+ expect(h.git.pull).toHaveBeenLastCalledWith(WS, { preserveMerges: true });
438
+ });
439
+ });
388
440
  });
@@ -383,6 +383,25 @@ describe('list_change_requests', () => {
383
383
  expect(calls).toContainEqual(['listChangeRequestsByState', ['closed', 'merged', 'open']]);
384
384
  });
385
385
 
386
+ it('refuses a `state` it does not know instead of answering the open ones', async () => {
387
+ const base = await start();
388
+ // `merged` is what the ANSWER says for an applied request, so it is the
389
+ // word an agent is most likely to feed back in; a typo is refused too.
390
+ // The route's argument check answers first — the schema pins `state` to
391
+ // its three values — and names them in its refusal; the handler's own
392
+ // check beneath it is the floor for a caller that reaches it another way.
393
+ for (const state of ['merged', 'applied', 'OPEN', '', 7, null]) {
394
+ calls = [];
395
+ const { status, json } = await call(base, 'list_change_requests', { state });
396
+ expect(status, JSON.stringify(state)).toBe(400);
397
+ expect((json as { kind?: string }).kind, JSON.stringify(state)).toBe('arguments-do-not-match');
398
+ // The three values are named either in the mismatch line (a wrong
399
+ // string) or in the interface the refusal carries (a wrong type).
400
+ expect(JSON.stringify(json), JSON.stringify(state)).toMatch(/open.*closed.*all/);
401
+ expect(calls, JSON.stringify(state)).toEqual([]);
402
+ }
403
+ });
404
+
386
405
  it('filters by target branch and by author', async () => {
387
406
  const base = await start();
388
407
  summaries = [
@@ -619,6 +638,19 @@ describe('get_change_request', () => {
619
638
  expect(absent.json.error).toBe('Change request #99 not found.');
620
639
  });
621
640
 
641
+ // An OPEN request that proposes nothing, before the background sweep has
642
+ // closed it: still "not found" to an agent that is not its author. Closing
643
+ // empty requests changed nothing about what agents may see.
644
+ it('answers not found for an open request with no files to an agent that is not its author', async () => {
645
+ const base = await start();
646
+ details.set(12, detail({ state: 'open', files: [], approvals: [] }));
647
+ const { status, json } = await call(base, 'get_change_request', { number: 12 });
648
+ expect(status).toBe(404);
649
+ expect(json.error).toBe('Change request #12 not found.');
650
+ callerEmail = AUTHOR;
651
+ expect((await call(base, 'get_change_request', { number: 12 })).status).toBe(200);
652
+ });
653
+
622
654
  it('answers not found when the access tree at the target cannot be resolved', async () => {
623
655
  const base = await start();
624
656
  readable = null;
@@ -1033,11 +1065,15 @@ describe('a mixed-access caller is never handed a path they may not read', () =>
1033
1065
  mixedAccessRequest();
1034
1066
  summaries = [details.get(1)!];
1035
1067
  for (const tool of TOOLS) {
1036
- const { status, json } = await call(
1037
- base,
1038
- tool,
1039
- tool === 'list_change_requests' ? {} : { number: 1, include: ['patches'] },
1040
- );
1068
+ // `include` only where the tool declares it: a call naming an argument
1069
+ // its tool does not have is refused before it runs.
1070
+ const args =
1071
+ tool === 'list_change_requests'
1072
+ ? {}
1073
+ : tool === 'list_change_request_files'
1074
+ ? { number: 1, include: ['patches'] }
1075
+ : { number: 1 };
1076
+ const { status, json } = await call(base, tool, args);
1041
1077
  expect(status, tool).toBe(200);
1042
1078
  const whole = JSON.stringify(json);
1043
1079
  expect(whole, tool).not.toContain('Avi-Checkin');
@@ -9,7 +9,13 @@ 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 { ChangeRequestConflictsError, OpenChangeRequestBlocksMergeError } from '../../../../shared/domain-errors.js';
12
+ import {
13
+ BranchAuthorshipError,
14
+ BranchDeleteBlockedError,
15
+ BranchNotFoundError,
16
+ ChangeRequestConflictsError,
17
+ OpenChangeRequestBlocksMergeError,
18
+ } from '../../../../shared/domain-errors.js';
13
19
 
14
20
  const WS = 'target-company-state';
15
21
 
@@ -144,6 +150,34 @@ const workflowService = {
144
150
  }
145
151
  return { kind: 'merged', sha: 'merge-sha' };
146
152
  },
153
+ deleteBranchChecked: async (
154
+ user: { id: string },
155
+ name: string,
156
+ { maySee, ...opts }: { dryRun?: boolean; discardUnmerged?: boolean; maySee?: (n: number) => Promise<boolean> },
157
+ ) => {
158
+ calls.push(['deleteBranchChecked', user.id, name, opts]);
159
+ if (maySee) calls.push(['maySee', 12, await maySee(12)], ['maySee', 13, await maySee(13)]);
160
+ if (name === 'me/missing') throw new BranchNotFoundError(name);
161
+ if (name === 'ali/theirs') throw new BranchAuthorshipError(name);
162
+ if (name === 'me/proposed') {
163
+ throw new BranchDeleteBlockedError(
164
+ 'open-change-request',
165
+ name,
166
+ 'Change request #12 is open from "me/proposed". Ask the user to withdraw or decline it in the app; then the branch can be deleted.',
167
+ { number: 12 },
168
+ );
169
+ }
170
+ if (name === 'me/unmerged' && !(opts as { discardUnmerged?: boolean }).discardUnmerged) {
171
+ throw new BranchDeleteBlockedError('unmerged-commits', name, '"me/unmerged" holds 3 commits…', { unmergedCommits: 3 });
172
+ }
173
+ if (name === 'me/offline') {
174
+ throw new BranchDeleteBlockedError('state-unconfirmed', name, 'Could not reach the shared repository…');
175
+ }
176
+ if ((opts as { dryRun?: boolean }).dryRun) {
177
+ return { kind: 'preview', branch: name, exists: true, canDelete: true, refusals: [], unmergedCommits: 0, openChangeRequests: [], lastCommit: 'tip-1' };
178
+ }
179
+ return { kind: 'deleted', branch: name, lastCommit: 'tip-1', discardedCommits: name === 'me/unmerged' ? 3 : 0 };
180
+ },
147
181
  postComment: async (number: number, _user: unknown, input: { path?: string }) => {
148
182
  calls.push(['postComment', number, input.path]);
149
183
  return { id: 'c-1' };
@@ -169,7 +203,11 @@ async function start(): Promise<string> {
169
203
  const toolHandler = createToolHandlerFactory(resolve);
170
204
 
171
205
  const router = express.Router();
172
- registerWorkflowTools(registry, router, toolAuth, toolHandler, testKbContext());
206
+ // Request #13 is one the caller may not see.
207
+ registerWorkflowTools(registry, router, toolAuth, toolHandler, testKbContext(), async (ctx, number) => {
208
+ calls.push(['maySeeAs', ctx.user.id, number]);
209
+ return number !== 13;
210
+ });
173
211
  router.use(createManualRoutes(registry, toolAuth));
174
212
 
175
213
  const app = express();
@@ -250,6 +288,80 @@ describe('registerWorkflowTools', () => {
250
288
  expect(calls.some((c) => c[0] === 'postComment')).toBe(false);
251
289
  });
252
290
 
291
+ describe('delete_branch', () => {
292
+ it('is on both catalogs, takes name, dryRun and discardUnmerged, and is a write tool', async () => {
293
+ await start();
294
+ for (const tools of [await registryRef!.listInternal(), await registryRef!.listExternal()]) {
295
+ const tool = tools.find((t) => t.name === 'delete_branch');
296
+ expect(tool).toBeDefined();
297
+ // `toolDef` wraps a tool's own inputs under `body`.
298
+ const inputs = (tool!.inputs as { properties: { body: { properties: Record<string, unknown>; required: string[] } } })
299
+ .properties.body;
300
+ expect(Object.keys(inputs.properties).sort()).toEqual(['discardUnmerged', 'dryRun', 'name']);
301
+ expect(inputs.required).toEqual(['name']);
302
+ expect(tool!.tags).toContain('write');
303
+ // An empty request is closed by the delete; a live one refuses it, linked.
304
+ expect(tool!.description).toContain('proposes nothing is closed');
305
+ expect(tool!.description).toContain('its author withdraws it or an Admin declines it');
306
+ }
307
+ });
308
+
309
+ it('deletes as the caller and answers the last commit', async () => {
310
+ const base = await start();
311
+ const res = await post(`${base}/api/agent/tools/delete_branch`, writeTok(), { name: 'me/draft' });
312
+ expect(res.status).toBe(200);
313
+ expect(await res.json()).toEqual({ kind: 'deleted', branch: 'me/draft', lastCommit: 'tip-1', discardedCommits: 0 });
314
+ expect(calls).toContainEqual(['deleteBranchChecked', 'user-A', 'me/draft', { dryRun: false, discardUnmerged: false }]);
315
+ });
316
+
317
+ it('passes dryRun and discardUnmerged through', async () => {
318
+ const base = await start();
319
+ const preview = await post(`${base}/api/agent/tools/delete_branch`, writeTok(), { name: 'me/draft', dryRun: true });
320
+ expect(await preview.json()).toMatchObject({ kind: 'preview', canDelete: true, lastCommit: 'tip-1' });
321
+ const discarded = await post(`${base}/api/agent/tools/delete_branch`, writeTok(), { name: 'me/unmerged', discardUnmerged: true });
322
+ expect(await discarded.json()).toMatchObject({ kind: 'deleted', discardedCommits: 3 });
323
+ expect(calls).toContainEqual(['deleteBranchChecked', 'user-A', 'me/draft', { dryRun: true, discardUnmerged: false }]);
324
+ expect(calls).toContainEqual(['deleteBranchChecked', 'user-A', 'me/unmerged', { dryRun: false, discardUnmerged: true }]);
325
+ });
326
+
327
+ it('answers each refusal with its status and message', async () => {
328
+ const base = await start();
329
+ const ask = async (name: string) => {
330
+ const res = await post(`${base}/api/agent/tools/delete_branch`, writeTok(), { name });
331
+ return { status: res.status, body: (await res.json()) as Record<string, unknown> };
332
+ };
333
+ const missing = await ask('me/missing');
334
+ expect(missing.status).toBe(404);
335
+ expect(missing.body.error).toMatch(/no branch named me\/missing/);
336
+ const theirs = await ask('ali/theirs');
337
+ expect(theirs.status).toBe(403);
338
+ expect(theirs.body.error).toBe('Only the author of "ali/theirs" can delete it.');
339
+ const proposed = await ask('me/proposed');
340
+ expect(proposed.status).toBe(409);
341
+ expect(proposed.body).toMatchObject({ reason: 'open-change-request', number: 12 });
342
+ expect(proposed.body.error).toMatch(/#12.*withdraw or decline it in the app/);
343
+ const unmerged = await ask('me/unmerged');
344
+ expect(unmerged.status).toBe(409);
345
+ expect(unmerged.body).toMatchObject({ reason: 'unmerged-commits', unmergedCommits: 3 });
346
+ expect((await ask('me/offline')).status).toBe(503);
347
+ });
348
+
349
+ it('requires a name', async () => {
350
+ const base = await start();
351
+ const res = await post(`${base}/api/agent/tools/delete_branch`, writeTok(), {});
352
+ expect(res.status).toBe(400);
353
+ expect(calls.some((c) => c[0] === 'deleteBranchChecked')).toBe(false);
354
+ });
355
+
356
+ it("hands the service the caller's view of change requests, as get_change_request gates it", async () => {
357
+ const base = await start();
358
+ await post(`${base}/api/agent/tools/delete_branch`, writeTok(), { name: 'me/draft', dryRun: true });
359
+ expect(calls).toContainEqual(['maySeeAs', 'user-A', 12]);
360
+ expect(calls).toContainEqual(['maySee', 12, true]);
361
+ expect(calls).toContainEqual(['maySee', 13, false]);
362
+ });
363
+ });
364
+
253
365
  // An agent proposes and syncs; a person merges.
254
366
  describe('merging', () => {
255
367
  it('merge_change_request is gone: its old path answers who merges now, and never merges', async () => {
@@ -304,6 +416,19 @@ describe('registerWorkflowTools', () => {
304
416
  const mergeBranch = tools.find((t) => t.name === 'merge_branch')!;
305
417
  expect(mergeBranch.description).toMatch(/a person merges/);
306
418
  expect(mergeBranch.description).toMatch(/SYNC/);
419
+ // Every outcome is named, and the asynchronous commit the merge waits for.
420
+ for (const kind of ['merged', 'nothing-to-merge', 'pending-commits', 'conflicts-need-resolution']) {
421
+ expect(mergeBranch.description, kind).toContain(`\`${kind}\``);
422
+ }
423
+ expect(mergeBranch.description).toMatch(/committed asynchronously/);
424
+ expect(mergeBranch.description).toMatch(/waits up to 20s/);
425
+ const outcome = (mergeBranch.outputs as { properties: { outcome: { description: string; properties: Record<string, { enum?: string[] }> } } })
426
+ .properties.outcome;
427
+ expect(outcome.properties.kind.enum).toEqual(['merged', 'nothing-to-merge', 'pending-commits', 'conflicts-need-resolution']);
428
+ expect(Object.keys(outcome.properties)).toEqual(
429
+ expect.arrayContaining(['sha', 'conflictedPaths', 'branch', 'pending', 'needsAttention', 'message']),
430
+ );
431
+ expect(outcome.description).toMatch(/committed asynchronously and the merge waits/);
307
432
  }
308
433
  });
309
434
  });
@@ -133,14 +133,6 @@ export function registerChangeRequestReadTools(
133
133
  router.post(path.slice('/api'.length), toolAuth, toolHandler(spec.handler, { write: false }));
134
134
  };
135
135
 
136
- /**
137
- * A clone to resolve access verdicts in. All clones track the same origin and
138
- * every verdict here is read at `origin/<base>`, so any existing one answers
139
- * identically — reusing one avoids cloning a branch just to read a ref.
140
- */
141
- const repoGlobalWorkspaceId = async (ctx: ToolContext): Promise<string> =>
142
- (await ctx.workspaceService.findAnyWorkspaceId()) ?? kb.defaultWorkspaceId();
143
-
144
136
  /** A change-request number as the tools accept it. */
145
137
  const numberArg = (args: Record<string, unknown>): number => {
146
138
  const n = args.number;
@@ -153,119 +145,7 @@ export function registerChangeRequestReadTools(
153
145
  return n;
154
146
  };
155
147
 
156
- /**
157
- * Read verdicts for `paths` at `origin/<base>`, as a membership set of what
158
- * the caller MAY read. A null answer (the ref or its `roles.yaml` does not
159
- * resolve) withholds everything, which is the same reading the app's
160
- * fork-point route gives it.
161
- */
162
- const readablePaths = async (
163
- ctx: ToolContext,
164
- workspaceId: string,
165
- base: string,
166
- paths: string[],
167
- ): Promise<Set<string>> => {
168
- if (paths.length === 0) return new Set();
169
- const verdicts = await accessControl.canReadBatchAtRef(
170
- workspaceId,
171
- `origin/${base}`,
172
- ctx.user.email,
173
- [...new Set(paths)],
174
- );
175
- if (!verdicts) return new Set();
176
- return new Set([...verdicts].filter(([, allowed]) => allowed).map(([p]) => p));
177
- };
178
-
179
- /**
180
- * The detail of request `number` with the access filter applied, or a 404 —
181
- * the one place the four by-number tools get their data, so they cannot
182
- * drift on who may see what.
183
- */
184
- interface ScopedDetail {
185
- detail: ChangeRequestDetail;
186
- /**
187
- * Whether the caller may read one path of this request — the raw per-path
188
- * verdict, as the access tree gives it at `origin/<base>`.
189
- */
190
- mayRead: (path: string) => boolean;
191
- /**
192
- * Whether this request may NAME `path` to the caller — `mayRead`, minus
193
- * every name a withheld file goes by. The ONE verdict the tools below ask
194
- * about a path, so none of them can decide it a second way.
195
- *
196
- * The two differ on exactly one shape, and it is the shape that leaks: a
197
- * file moved out of a folder the caller may not read is readable under
198
- * its NEW name, yet `fileIsReadable` withholds it whole (its diff shows the
199
- * old path's content). Anything keyed on that new path — an approval, a
200
- * comment anchored to it — would otherwise say the request touches a file
201
- * the file tool refuses to list, under a name it refuses to print.
202
- */
203
- mayShow: (path: string) => boolean;
204
- /** The request's files the caller may read, in the request's own order. */
205
- readableFiles: ChangeRequestDetail['files'];
206
- /**
207
- * Every name the withheld files go by — for filtering the gate blockers and
208
- * for deciding `mayShow`. NEVER answered: use `withheldFileCount` to report
209
- * them.
210
- */
211
- withheldFilePaths: string[];
212
- /** How many files are withheld. One per file, whatever its move names. */
213
- withheldFileCount: number;
214
- viewerIsAuthor: boolean;
215
- }
216
-
217
- const scopedDetail = async (
218
- ctx: ToolContext,
219
- number: number,
220
- opts: { patches: boolean },
221
- ): Promise<ScopedDetail> => {
222
- const workspaceId = await repoGlobalWorkspaceId(ctx);
223
- const detail = await ctx.workflowService.getChangeRequestDetail(number, {
224
- workspaceId,
225
- viewerEmail: ctx.user.email,
226
- patches: opts.patches,
227
- });
228
- if (!detail) throw notFound(number);
229
- const viewerIsAuthor = isAuthor(detail, ctx.user.email);
230
- // Comment paths join the batch: a comment may name a file the request no
231
- // longer changes, and without a verdict of its own it would be withheld
232
- // from a caller who can read it perfectly well.
233
- const commentPaths = detail.comments.map((c) => c.path).filter((p): p is string => !!p);
234
- // BOTH names of every file: a move is judged on its old path as well as
235
- // its new one, because the diff of a move shows what was at the old one.
236
- const readable = await readablePaths(ctx, workspaceId, detail.base, [
237
- ...detail.files.flatMap(pathsOf),
238
- ...commentPaths,
239
- ]);
240
- const mayRead = (path: string) => readable.has(path);
241
- const readableFiles = detail.files.filter((f) => fileIsReadable(f, mayRead));
242
- if (!maySeeChangeRequest({ readableFiles: readableFiles.length, isAuthor: viewerIsAuthor })) {
243
- throw notFound(number);
244
- }
245
- // Every name a withheld file goes by, so a gate warning quoting either
246
- // spelling is caught by the blocker filter — and so `mayShow` catches
247
- // whichever spelling an approval or a comment happens to use.
248
- const withheldFilePaths = detail.files
249
- .filter((f) => !fileIsReadable(f, mayRead))
250
- .flatMap(pathsOf);
251
- const withheldNames = new Set(withheldFilePaths);
252
- return {
253
- detail,
254
- mayRead,
255
- mayShow: (path: string) => mayRead(path) && !withheldNames.has(path),
256
- readableFiles,
257
- withheldFilePaths,
258
- withheldFileCount: detail.files.length - readableFiles.length,
259
- viewerIsAuthor,
260
- };
261
- };
262
-
263
- /**
264
- * The same answer for a number that was never issued and for one the caller
265
- * may not see — requirement 7. Says nothing a probe could tell apart.
266
- */
267
- const notFound = (number: number): ToolError =>
268
- new ToolError(`Change request #${number} not found.`, 404);
148
+ const { repoGlobalWorkspaceId, readablePaths, scopedDetail } = changeRequestScope(accessControl, kb);
269
149
 
270
150
  /**
271
151
  * The changed files of a SUMMARY, as the pairs a read gate decides over.
@@ -374,7 +254,18 @@ export function registerChangeRequestReadTools(
374
254
  required: ['changeRequests', ...pagingRequired],
375
255
  },
376
256
  handler: async (args, ctx: ToolContext) => {
377
- const state = args.state === 'closed' || args.state === 'all' ? args.state : 'open';
257
+ // The answer spells a merged request `state: merged`, so an agent that
258
+ // copies that word back into the filter is asking for the applied
259
+ // ones. Quietly answering the open ones instead reads as "there are no
260
+ // merged requests" — refuse, and name the filter's own vocabulary.
261
+ const state = args.state === undefined ? 'open' : args.state;
262
+ if (state !== 'open' && state !== 'closed' && state !== 'all') {
263
+ throw new ToolError(
264
+ `Unknown \`state\` ${JSON.stringify(state)}. Use \`open\` (the default), \`closed\` ` +
265
+ '(applied and declined alike) or `all`; the answer then says `merged` or `closed` for each.',
266
+ 400,
267
+ );
268
+ }
378
269
  const head = typeof args.head === 'string' ? args.head : undefined;
379
270
  const base = typeof args.base === 'string' ? args.base : undefined;
380
271
  const author = typeof args.author === 'string' ? args.author : undefined;
@@ -722,3 +613,147 @@ export function registerChangeRequestReadTools(
722
613
  });
723
614
  }
724
615
 
616
+ /**
617
+ * The detail of request `number` with the access filter applied, or a 404 —
618
+ * the one place the four by-number tools get their data, so they cannot
619
+ * drift on who may see what.
620
+ */
621
+ export interface ScopedDetail {
622
+ detail: ChangeRequestDetail;
623
+ /**
624
+ * Whether the caller may read one path of this request — the raw per-path
625
+ * verdict, as the access tree gives it at `origin/<base>`.
626
+ */
627
+ mayRead: (path: string) => boolean;
628
+ /**
629
+ * Whether this request may NAME `path` to the caller — `mayRead`, minus
630
+ * every name a withheld file goes by. The ONE verdict the tools below ask
631
+ * about a path, so none of them can decide it a second way.
632
+ *
633
+ * The two differ on exactly one shape, and it is the shape that leaks: a
634
+ * file moved out of a folder the caller may not read is readable under
635
+ * its NEW name, yet `fileIsReadable` withholds it whole (its diff shows the
636
+ * old path's content). Anything keyed on that new path — an approval, a
637
+ * comment anchored to it — would otherwise say the request touches a file
638
+ * the file tool refuses to list, under a name it refuses to print.
639
+ */
640
+ mayShow: (path: string) => boolean;
641
+ /** The request's files the caller may read, in the request's own order. */
642
+ readableFiles: ChangeRequestDetail['files'];
643
+ /**
644
+ * Every name the withheld files go by — for filtering the gate blockers and
645
+ * for deciding `mayShow`. NEVER answered: use `withheldFileCount` to report
646
+ * them.
647
+ */
648
+ withheldFilePaths: string[];
649
+ /** How many files are withheld. One per file, whatever its move names. */
650
+ withheldFileCount: number;
651
+ viewerIsAuthor: boolean;
652
+ }
653
+
654
+ /**
655
+ * The access scope of one change request, as every by-number read tool sees
656
+ * it — and as `delete_branch` does, so a deletion's preview or refusal never
657
+ * names a request `get_change_request` answers as not found.
658
+ */
659
+ export function changeRequestScope(
660
+ accessControl: Pick<IAccessControl, 'canReadBatchAtRef'>,
661
+ kb: Pick<KbContext, 'defaultWorkspaceId'>,
662
+ ) {
663
+ /**
664
+ * A clone to resolve access verdicts in. All clones track the same origin and
665
+ * every verdict here is read at `origin/<base>`, so any existing one answers
666
+ * identically — reusing one avoids cloning a branch just to read a ref.
667
+ */
668
+ const repoGlobalWorkspaceId = async (ctx: ToolContext): Promise<string> =>
669
+ (await ctx.workspaceService.findAnyWorkspaceId()) ?? kb.defaultWorkspaceId();
670
+
671
+ /**
672
+ * Read verdicts for `paths` at `origin/<base>`, as a membership set of what
673
+ * the caller MAY read. A null answer (the ref or its `roles.yaml` does not
674
+ * resolve) withholds everything, which is the same reading the app's
675
+ * fork-point route gives it.
676
+ */
677
+ const readablePaths = async (
678
+ ctx: ToolContext,
679
+ workspaceId: string,
680
+ base: string,
681
+ paths: string[],
682
+ ): Promise<Set<string>> => {
683
+ if (paths.length === 0) return new Set();
684
+ const verdicts = await accessControl.canReadBatchAtRef(
685
+ workspaceId,
686
+ `origin/${base}`,
687
+ ctx.user.email,
688
+ [...new Set(paths)],
689
+ );
690
+ if (!verdicts) return new Set();
691
+ return new Set([...verdicts].filter(([, allowed]) => allowed).map(([p]) => p));
692
+ };
693
+
694
+ const scopedDetail = async (
695
+ ctx: ToolContext,
696
+ number: number,
697
+ opts: { patches: boolean },
698
+ ): Promise<ScopedDetail> => {
699
+ const workspaceId = await repoGlobalWorkspaceId(ctx);
700
+ const detail = await ctx.workflowService.getChangeRequestDetail(number, {
701
+ workspaceId,
702
+ viewerEmail: ctx.user.email,
703
+ patches: opts.patches,
704
+ });
705
+ if (!detail) throw notFound(number);
706
+ const viewerIsAuthor = isAuthor(detail, ctx.user.email);
707
+ // Comment paths join the batch: a comment may name a file the request no
708
+ // longer changes, and without a verdict of its own it would be withheld
709
+ // from a caller who can read it perfectly well.
710
+ const commentPaths = detail.comments.map((c) => c.path).filter((p): p is string => !!p);
711
+ // BOTH names of every file: a move is judged on its old path as well as
712
+ // its new one, because the diff of a move shows what was at the old one.
713
+ const readable = await readablePaths(ctx, workspaceId, detail.base, [
714
+ ...detail.files.flatMap(pathsOf),
715
+ ...commentPaths,
716
+ ]);
717
+ const mayRead = (path: string) => readable.has(path);
718
+ const readableFiles = detail.files.filter((f) => fileIsReadable(f, mayRead));
719
+ if (!maySeeChangeRequest({ readableFiles: readableFiles.length, isAuthor: viewerIsAuthor })) {
720
+ throw notFound(number);
721
+ }
722
+ // Every name a withheld file goes by, so a gate warning quoting either
723
+ // spelling is caught by the blocker filter — and so `mayShow` catches
724
+ // whichever spelling an approval or a comment happens to use.
725
+ const withheldFilePaths = detail.files
726
+ .filter((f) => !fileIsReadable(f, mayRead))
727
+ .flatMap(pathsOf);
728
+ const withheldNames = new Set(withheldFilePaths);
729
+ return {
730
+ detail,
731
+ mayRead,
732
+ mayShow: (path: string) => mayRead(path) && !withheldNames.has(path),
733
+ readableFiles,
734
+ withheldFilePaths,
735
+ withheldFileCount: detail.files.length - readableFiles.length,
736
+ viewerIsAuthor,
737
+ };
738
+ };
739
+
740
+ /**
741
+ * The same answer for a number that was never issued and for one the caller
742
+ * may not see — requirement 7. Says nothing a probe could tell apart.
743
+ */
744
+ const notFound = (number: number): ToolError =>
745
+ new ToolError(`Change request #${number} not found.`, 404);
746
+
747
+ /** Whether the caller may see request `number` at all: what `scopedDetail` answers 404 for, as a no. */
748
+ const maySee = async (ctx: ToolContext, number: number): Promise<boolean> => {
749
+ try {
750
+ await scopedDetail(ctx, number, { patches: false });
751
+ return true;
752
+ } catch (err) {
753
+ if (err instanceof ToolError && err.status === 404) return false;
754
+ throw err;
755
+ }
756
+ };
757
+
758
+ return { repoGlobalWorkspaceId, readablePaths, scopedDetail, notFound, maySee };
759
+ }