@bevel-software/platform-core-backend 0.25.2 → 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 (733) hide show
  1. package/agent-guide/access-control.md +251 -0
  2. package/agent-guide/conventions.md +35 -0
  3. package/agent-guide/directory-structure.md +145 -0
  4. package/agent-guide/finding-things.md +7 -0
  5. package/agent-guide/html-views.md +83 -0
  6. package/agent-guide/introduction.md +27 -0
  7. package/agent-guide/new-knowledge-base.md +24 -0
  8. package/agent-guide/skills.md +47 -0
  9. package/agent-guide/tool-manuals.md +227 -0
  10. package/agent-guide/where-a-new-file-goes.md +36 -0
  11. package/dist/assets.d.ts +26 -0
  12. package/dist/assets.d.ts.map +1 -1
  13. package/dist/assets.js +35 -2
  14. package/dist/assets.js.map +1 -1
  15. package/dist/core/catalog-revision.d.ts +19 -1
  16. package/dist/core/catalog-revision.d.ts.map +1 -1
  17. package/dist/core/catalog-revision.js +14 -3
  18. package/dist/core/catalog-revision.js.map +1 -1
  19. package/dist/core/core-ports.d.ts +11 -0
  20. package/dist/core/core-ports.d.ts.map +1 -1
  21. package/dist/core/core-ports.js.map +1 -1
  22. package/dist/core/create-core-server.d.ts +5 -0
  23. package/dist/core/create-core-server.d.ts.map +1 -1
  24. package/dist/core/create-core-server.js +65 -3
  25. package/dist/core/create-core-server.js.map +1 -1
  26. package/dist/core/create-core-services.d.ts +35 -0
  27. package/dist/core/create-core-services.d.ts.map +1 -1
  28. package/dist/core/create-core-services.js +114 -12
  29. package/dist/core/create-core-services.js.map +1 -1
  30. package/dist/core/lifecycle.d.ts +16 -1
  31. package/dist/core/lifecycle.d.ts.map +1 -1
  32. package/dist/core/lifecycle.js +18 -1
  33. package/dist/core/lifecycle.js.map +1 -1
  34. package/dist/core-config.d.ts +21 -0
  35. package/dist/core-config.d.ts.map +1 -1
  36. package/dist/core-config.js +22 -1
  37. package/dist/core-config.js.map +1 -1
  38. package/dist/index.d.ts +1 -1
  39. package/dist/index.d.ts.map +1 -1
  40. package/dist/index.js +2 -2
  41. package/dist/index.js.map +1 -1
  42. package/dist/modules/access/access-control.interface.d.ts +17 -0
  43. package/dist/modules/access/access-control.interface.d.ts.map +1 -1
  44. package/dist/modules/access/access-control.service.d.ts +2 -0
  45. package/dist/modules/access/access-control.service.d.ts.map +1 -1
  46. package/dist/modules/access/access-control.service.js +27 -0
  47. package/dist/modules/access/access-control.service.js.map +1 -1
  48. package/dist/modules/access/directory-sync-bot.js +1 -1
  49. package/dist/modules/access/directory-sync-bot.js.map +1 -1
  50. package/dist/modules/agent-guide/agent-guide.d.ts +145 -0
  51. package/dist/modules/agent-guide/agent-guide.d.ts.map +1 -0
  52. package/dist/modules/agent-guide/agent-guide.js +199 -0
  53. package/dist/modules/agent-guide/agent-guide.js.map +1 -0
  54. package/dist/modules/agent-guide/agent-guide.tools.d.ts +24 -0
  55. package/dist/modules/agent-guide/agent-guide.tools.d.ts.map +1 -0
  56. package/dist/modules/agent-guide/agent-guide.tools.js +100 -0
  57. package/dist/modules/agent-guide/agent-guide.tools.js.map +1 -0
  58. package/dist/modules/agent-guide/index.d.ts +4 -0
  59. package/dist/modules/agent-guide/index.d.ts.map +1 -0
  60. package/dist/modules/agent-guide/index.js +4 -0
  61. package/dist/modules/agent-guide/index.js.map +1 -0
  62. package/dist/modules/agent-instructions/agent-instructions.routes.d.ts +3 -2
  63. package/dist/modules/agent-instructions/agent-instructions.routes.d.ts.map +1 -1
  64. package/dist/modules/agent-instructions/agent-instructions.routes.js +3 -2
  65. package/dist/modules/agent-instructions/agent-instructions.routes.js.map +1 -1
  66. package/dist/modules/agent-instructions/compose.d.ts +14 -6
  67. package/dist/modules/agent-instructions/compose.d.ts.map +1 -1
  68. package/dist/modules/agent-instructions/compose.js +28 -8
  69. package/dist/modules/agent-instructions/compose.js.map +1 -1
  70. package/dist/modules/agent-instructions/index.d.ts +1 -1
  71. package/dist/modules/agent-instructions/index.d.ts.map +1 -1
  72. package/dist/modules/agent-instructions/index.js +1 -1
  73. package/dist/modules/agent-instructions/index.js.map +1 -1
  74. package/dist/modules/agent-instructions/shared-file-rules.d.ts +10 -50
  75. package/dist/modules/agent-instructions/shared-file-rules.d.ts.map +1 -1
  76. package/dist/modules/agent-instructions/shared-file-rules.js +51 -104
  77. package/dist/modules/agent-instructions/shared-file-rules.js.map +1 -1
  78. package/dist/modules/audit/agent-audit.service.d.ts +21 -2
  79. package/dist/modules/audit/agent-audit.service.d.ts.map +1 -1
  80. package/dist/modules/audit/agent-audit.service.js +43 -2
  81. package/dist/modules/audit/agent-audit.service.js.map +1 -1
  82. package/dist/modules/audit/agent-connection.routes.d.ts +34 -0
  83. package/dist/modules/audit/agent-connection.routes.d.ts.map +1 -0
  84. package/dist/modules/audit/agent-connection.routes.js +38 -0
  85. package/dist/modules/audit/agent-connection.routes.js.map +1 -0
  86. package/dist/modules/audit/audit.contract.d.ts +24 -0
  87. package/dist/modules/audit/audit.contract.d.ts.map +1 -1
  88. package/dist/modules/audit/audit.contract.js.map +1 -1
  89. package/dist/modules/audit/index.d.ts +1 -0
  90. package/dist/modules/audit/index.d.ts.map +1 -1
  91. package/dist/modules/audit/index.js +1 -0
  92. package/dist/modules/audit/index.js.map +1 -1
  93. package/dist/modules/auth/account.routes.d.ts.map +1 -1
  94. package/dist/modules/auth/account.routes.js +10 -3
  95. package/dist/modules/auth/account.routes.js.map +1 -1
  96. package/dist/modules/auth/auth.middleware.d.ts +7 -0
  97. package/dist/modules/auth/auth.middleware.d.ts.map +1 -1
  98. package/dist/modules/auth/auth.middleware.js +1 -1
  99. package/dist/modules/auth/auth.middleware.js.map +1 -1
  100. package/dist/modules/code-mode/code-mode.tool.d.ts.map +1 -1
  101. package/dist/modules/code-mode/code-mode.tool.js +11 -3
  102. package/dist/modules/code-mode/code-mode.tool.js.map +1 -1
  103. package/dist/modules/database/core-schema.d.ts +96 -0
  104. package/dist/modules/database/core-schema.d.ts.map +1 -1
  105. package/dist/modules/database/core-schema.js +32 -3
  106. package/dist/modules/database/core-schema.js.map +1 -1
  107. package/dist/modules/embed/account-link.service.d.ts +42 -0
  108. package/dist/modules/embed/account-link.service.d.ts.map +1 -0
  109. package/dist/modules/embed/account-link.service.js +50 -0
  110. package/dist/modules/embed/account-link.service.js.map +1 -0
  111. package/dist/modules/embed/embed-link.d.ts +58 -0
  112. package/dist/modules/embed/embed-link.d.ts.map +1 -0
  113. package/dist/modules/embed/embed-link.js +160 -0
  114. package/dist/modules/embed/embed-link.js.map +1 -0
  115. package/dist/modules/embed/embed.errors.d.ts +30 -0
  116. package/dist/modules/embed/embed.errors.d.ts.map +1 -0
  117. package/dist/modules/embed/embed.errors.js +42 -0
  118. package/dist/modules/embed/embed.errors.js.map +1 -0
  119. package/dist/modules/embed/embed.interface.d.ts +204 -0
  120. package/dist/modules/embed/embed.interface.d.ts.map +1 -0
  121. package/dist/modules/embed/embed.interface.js +2 -0
  122. package/dist/modules/embed/embed.interface.js.map +1 -0
  123. package/dist/modules/embed/embed.routes.d.ts +59 -0
  124. package/dist/modules/embed/embed.routes.d.ts.map +1 -0
  125. package/dist/modules/embed/embed.routes.js +343 -0
  126. package/dist/modules/embed/embed.routes.js.map +1 -0
  127. package/dist/modules/embed/embed.service.d.ts +169 -0
  128. package/dist/modules/embed/embed.service.d.ts.map +1 -0
  129. package/dist/modules/embed/embed.service.js +627 -0
  130. package/dist/modules/embed/embed.service.js.map +1 -0
  131. package/dist/modules/embed/embed.tools.d.ts +51 -0
  132. package/dist/modules/embed/embed.tools.d.ts.map +1 -0
  133. package/dist/modules/embed/embed.tools.js +170 -0
  134. package/dist/modules/embed/embed.tools.js.map +1 -0
  135. package/dist/modules/embed/mcp-app.d.ts +64 -0
  136. package/dist/modules/embed/mcp-app.d.ts.map +1 -0
  137. package/dist/modules/embed/mcp-app.js +147 -0
  138. package/dist/modules/embed/mcp-app.js.map +1 -0
  139. package/dist/modules/embed/mcp-app.routes.d.ts +21 -0
  140. package/dist/modules/embed/mcp-app.routes.d.ts.map +1 -0
  141. package/dist/modules/embed/mcp-app.routes.js +36 -0
  142. package/dist/modules/embed/mcp-app.routes.js.map +1 -0
  143. package/dist/modules/github-app/github-app.connection.d.ts +15 -0
  144. package/dist/modules/github-app/github-app.connection.d.ts.map +1 -1
  145. package/dist/modules/github-app/github-app.connection.js +34 -3
  146. package/dist/modules/github-app/github-app.connection.js.map +1 -1
  147. package/dist/modules/kb-fs/locking-filesystem.d.ts +65 -0
  148. package/dist/modules/kb-fs/locking-filesystem.d.ts.map +1 -1
  149. package/dist/modules/kb-fs/locking-filesystem.js +300 -0
  150. package/dist/modules/kb-fs/locking-filesystem.js.map +1 -1
  151. package/dist/modules/kb-sync/kb-sync.interface.d.ts +5 -0
  152. package/dist/modules/kb-sync/kb-sync.interface.d.ts.map +1 -1
  153. package/dist/modules/kb-sync/kb-sync.service.d.ts.map +1 -1
  154. package/dist/modules/kb-sync/kb-sync.service.js +6 -0
  155. package/dist/modules/kb-sync/kb-sync.service.js.map +1 -1
  156. package/dist/modules/mcp/mcp-auth.middleware.d.ts.map +1 -1
  157. package/dist/modules/mcp/mcp-auth.middleware.js +2 -1
  158. package/dist/modules/mcp/mcp-auth.middleware.js.map +1 -1
  159. package/dist/modules/mcp/mcp.service.d.ts +57 -3
  160. package/dist/modules/mcp/mcp.service.d.ts.map +1 -1
  161. package/dist/modules/mcp/mcp.service.js +177 -24
  162. package/dist/modules/mcp/mcp.service.js.map +1 -1
  163. package/dist/modules/mcp/oauth/bevel-oauth-provider.d.ts +24 -0
  164. package/dist/modules/mcp/oauth/bevel-oauth-provider.d.ts.map +1 -1
  165. package/dist/modules/mcp/oauth/bevel-oauth-provider.js +32 -4
  166. package/dist/modules/mcp/oauth/bevel-oauth-provider.js.map +1 -1
  167. package/dist/modules/mcp/tool-schema-guard.d.ts +105 -0
  168. package/dist/modules/mcp/tool-schema-guard.d.ts.map +1 -0
  169. package/dist/modules/mcp/tool-schema-guard.js +171 -0
  170. package/dist/modules/mcp/tool-schema-guard.js.map +1 -0
  171. package/dist/modules/onboarding/onboarding.contract.d.ts +80 -0
  172. package/dist/modules/onboarding/onboarding.contract.d.ts.map +1 -0
  173. package/dist/modules/onboarding/onboarding.contract.js +7 -0
  174. package/dist/modules/onboarding/onboarding.contract.js.map +1 -0
  175. package/dist/modules/onboarding/onboarding.routes.d.ts +18 -0
  176. package/dist/modules/onboarding/onboarding.routes.d.ts.map +1 -0
  177. package/dist/modules/onboarding/onboarding.routes.js +81 -0
  178. package/dist/modules/onboarding/onboarding.routes.js.map +1 -0
  179. package/dist/modules/onboarding/starter-pack.service.d.ts +213 -0
  180. package/dist/modules/onboarding/starter-pack.service.d.ts.map +1 -0
  181. package/dist/modules/onboarding/starter-pack.service.js +492 -0
  182. package/dist/modules/onboarding/starter-pack.service.js.map +1 -0
  183. package/dist/modules/onboarding/starter-packs.d.ts +78 -0
  184. package/dist/modules/onboarding/starter-packs.d.ts.map +1 -0
  185. package/dist/modules/onboarding/starter-packs.js +156 -0
  186. package/dist/modules/onboarding/starter-packs.js.map +1 -0
  187. package/dist/modules/plugins/index.d.ts +1 -1
  188. package/dist/modules/plugins/index.d.ts.map +1 -1
  189. package/dist/modules/plugins/index.js +1 -1
  190. package/dist/modules/plugins/index.js.map +1 -1
  191. package/dist/modules/plugins/plugin-provision.service.d.ts +19 -0
  192. package/dist/modules/plugins/plugin-provision.service.d.ts.map +1 -1
  193. package/dist/modules/plugins/plugin-provision.service.js +18 -1
  194. package/dist/modules/plugins/plugin-provision.service.js.map +1 -1
  195. package/dist/modules/plugins/plugins.routes.d.ts +0 -36
  196. package/dist/modules/plugins/plugins.routes.d.ts.map +1 -1
  197. package/dist/modules/plugins/plugins.routes.js +31 -0
  198. package/dist/modules/plugins/plugins.routes.js.map +1 -1
  199. package/dist/modules/plugins/plugins.tools.d.ts +36 -2
  200. package/dist/modules/plugins/plugins.tools.d.ts.map +1 -1
  201. package/dist/modules/plugins/plugins.tools.js +71 -14
  202. package/dist/modules/plugins/plugins.tools.js.map +1 -1
  203. package/dist/modules/settings/deployment-settings.service.d.ts +34 -7
  204. package/dist/modules/settings/deployment-settings.service.d.ts.map +1 -1
  205. package/dist/modules/settings/deployment-settings.service.js +148 -54
  206. package/dist/modules/settings/deployment-settings.service.js.map +1 -1
  207. package/dist/modules/settings/repository-source.d.ts +1 -0
  208. package/dist/modules/settings/repository-source.d.ts.map +1 -1
  209. package/dist/modules/settings/repository-source.js +3 -3
  210. package/dist/modules/settings/repository-source.js.map +1 -1
  211. package/dist/modules/settings/setup.routes.d.ts.map +1 -1
  212. package/dist/modules/settings/setup.routes.js +3 -6
  213. package/dist/modules/settings/setup.routes.js.map +1 -1
  214. package/dist/modules/skills/skills.tools.d.ts.map +1 -1
  215. package/dist/modules/skills/skills.tools.js +103 -43
  216. package/dist/modules/skills/skills.tools.js.map +1 -1
  217. package/dist/modules/tool-auth/connection-key-rejection.d.ts +10 -1
  218. package/dist/modules/tool-auth/connection-key-rejection.d.ts.map +1 -1
  219. package/dist/modules/tool-auth/connection-key-rejection.js +11 -3
  220. package/dist/modules/tool-auth/connection-key-rejection.js.map +1 -1
  221. package/dist/modules/tool-auth/external-api-key.interface.d.ts +8 -0
  222. package/dist/modules/tool-auth/external-api-key.interface.d.ts.map +1 -1
  223. package/dist/modules/tool-auth/external-api-key.service.d.ts +24 -2
  224. package/dist/modules/tool-auth/external-api-key.service.d.ts.map +1 -1
  225. package/dist/modules/tool-auth/external-api-key.service.js +59 -2
  226. package/dist/modules/tool-auth/external-api-key.service.js.map +1 -1
  227. package/dist/modules/tool-auth/tool-auth.middleware.d.ts.map +1 -1
  228. package/dist/modules/tool-auth/tool-auth.middleware.js +9 -4
  229. package/dist/modules/tool-auth/tool-auth.middleware.js.map +1 -1
  230. package/dist/modules/tool-helpers/index.d.ts +28 -1
  231. package/dist/modules/tool-helpers/index.d.ts.map +1 -1
  232. package/dist/modules/tool-helpers/index.js +28 -1
  233. package/dist/modules/tool-helpers/index.js.map +1 -1
  234. package/dist/modules/tool-helpers/route-argument-check.d.ts +10 -0
  235. package/dist/modules/tool-helpers/route-argument-check.d.ts.map +1 -0
  236. package/dist/modules/tool-helpers/route-argument-check.js +132 -0
  237. package/dist/modules/tool-helpers/route-argument-check.js.map +1 -0
  238. package/dist/modules/tool-helpers/route-tool-schemas.d.ts +89 -0
  239. package/dist/modules/tool-helpers/route-tool-schemas.d.ts.map +1 -0
  240. package/dist/modules/tool-helpers/route-tool-schemas.js +113 -0
  241. package/dist/modules/tool-helpers/route-tool-schemas.js.map +1 -0
  242. package/dist/modules/tool-helpers/tool-def.d.ts +71 -0
  243. package/dist/modules/tool-helpers/tool-def.d.ts.map +1 -1
  244. package/dist/modules/tool-helpers/tool-def.js +70 -6
  245. package/dist/modules/tool-helpers/tool-def.js.map +1 -1
  246. package/dist/modules/tool-helpers/tool-handler.d.ts +46 -6
  247. package/dist/modules/tool-helpers/tool-handler.d.ts.map +1 -1
  248. package/dist/modules/tool-helpers/tool-handler.js +157 -8
  249. package/dist/modules/tool-helpers/tool-handler.js.map +1 -1
  250. package/dist/modules/tool-helpers/tool.contract.d.ts +9 -0
  251. package/dist/modules/tool-helpers/tool.contract.d.ts.map +1 -1
  252. package/dist/modules/tool-helpers/tool.contract.js.map +1 -1
  253. package/dist/modules/tool-manuals/tool-manuals.contract.d.ts +23 -4
  254. package/dist/modules/tool-manuals/tool-manuals.contract.d.ts.map +1 -1
  255. package/dist/modules/tool-manuals/tool-manuals.contract.js.map +1 -1
  256. package/dist/modules/tool-manuals/tool-manuals.service.d.ts +4 -0
  257. package/dist/modules/tool-manuals/tool-manuals.service.d.ts.map +1 -1
  258. package/dist/modules/tool-manuals/tool-manuals.service.js +14 -0
  259. package/dist/modules/tool-manuals/tool-manuals.service.js.map +1 -1
  260. package/dist/modules/tool-manuals/tool-manuals.tools.d.ts +7 -0
  261. package/dist/modules/tool-manuals/tool-manuals.tools.d.ts.map +1 -1
  262. package/dist/modules/tool-manuals/tool-manuals.tools.js +77 -38
  263. package/dist/modules/tool-manuals/tool-manuals.tools.js.map +1 -1
  264. package/dist/modules/tool-registry/description-length.d.ts +20 -16
  265. package/dist/modules/tool-registry/description-length.d.ts.map +1 -1
  266. package/dist/modules/tool-registry/description-length.js +50 -30
  267. package/dist/modules/tool-registry/description-length.js.map +1 -1
  268. package/dist/modules/tool-registry/guide-first.d.ts +27 -0
  269. package/dist/modules/tool-registry/guide-first.d.ts.map +1 -0
  270. package/dist/modules/tool-registry/guide-first.js +40 -0
  271. package/dist/modules/tool-registry/guide-first.js.map +1 -0
  272. package/dist/modules/tool-registry/tool-registry.d.ts +6 -0
  273. package/dist/modules/tool-registry/tool-registry.d.ts.map +1 -1
  274. package/dist/modules/tool-registry/tool-registry.js +9 -2
  275. package/dist/modules/tool-registry/tool-registry.js.map +1 -1
  276. package/dist/modules/workflow/agent-tools/change-request-read-shape.d.ts +449 -0
  277. package/dist/modules/workflow/agent-tools/change-request-read-shape.d.ts.map +1 -0
  278. package/dist/modules/workflow/agent-tools/change-request-read-shape.js +481 -0
  279. package/dist/modules/workflow/agent-tools/change-request-read-shape.js.map +1 -0
  280. package/dist/modules/workflow/agent-tools/change-request-read.tools.d.ts +126 -0
  281. package/dist/modules/workflow/agent-tools/change-request-read.tools.d.ts.map +1 -0
  282. package/dist/modules/workflow/agent-tools/change-request-read.tools.js +611 -0
  283. package/dist/modules/workflow/agent-tools/change-request-read.tools.js.map +1 -0
  284. package/dist/modules/workflow/agent-tools/change-request-summary.d.ts +12 -1
  285. package/dist/modules/workflow/agent-tools/change-request-summary.d.ts.map +1 -1
  286. package/dist/modules/workflow/agent-tools/change-request-summary.js +5 -1
  287. package/dist/modules/workflow/agent-tools/change-request-summary.js.map +1 -1
  288. package/dist/modules/workflow/agent-tools/workflow.tools.d.ts +8 -1
  289. package/dist/modules/workflow/agent-tools/workflow.tools.d.ts.map +1 -1
  290. package/dist/modules/workflow/agent-tools/workflow.tools.js +133 -37
  291. package/dist/modules/workflow/agent-tools/workflow.tools.js.map +1 -1
  292. package/dist/modules/workflow/file-lock.service.d.ts +29 -5
  293. package/dist/modules/workflow/file-lock.service.d.ts.map +1 -1
  294. package/dist/modules/workflow/file-lock.service.js +62 -5
  295. package/dist/modules/workflow/file-lock.service.js.map +1 -1
  296. package/dist/modules/workflow/git/git.service.d.ts +282 -14
  297. package/dist/modules/workflow/git/git.service.d.ts.map +1 -1
  298. package/dist/modules/workflow/git/git.service.js +627 -126
  299. package/dist/modules/workflow/git/git.service.js.map +1 -1
  300. package/dist/modules/workflow/git/merge-commit.d.ts +73 -0
  301. package/dist/modules/workflow/git/merge-commit.d.ts.map +1 -0
  302. package/dist/modules/workflow/git/merge-commit.js +89 -0
  303. package/dist/modules/workflow/git/merge-commit.js.map +1 -0
  304. package/dist/modules/workflow/git/node-git-runner.d.ts +15 -0
  305. package/dist/modules/workflow/git/node-git-runner.d.ts.map +1 -1
  306. package/dist/modules/workflow/git/node-git-runner.js +78 -6
  307. package/dist/modules/workflow/git/node-git-runner.js.map +1 -1
  308. package/dist/modules/workflow/git/pull-request.service.d.ts +94 -1
  309. package/dist/modules/workflow/git/pull-request.service.d.ts.map +1 -1
  310. package/dist/modules/workflow/git/pull-request.service.js +332 -37
  311. package/dist/modules/workflow/git/pull-request.service.js.map +1 -1
  312. package/dist/modules/workflow/pending-commits.service.d.ts +15 -0
  313. package/dist/modules/workflow/pending-commits.service.d.ts.map +1 -1
  314. package/dist/modules/workflow/pending-commits.service.js +25 -1
  315. package/dist/modules/workflow/pending-commits.service.js.map +1 -1
  316. package/dist/modules/workflow/pending-commits.worker.d.ts +6 -1
  317. package/dist/modules/workflow/pending-commits.worker.d.ts.map +1 -1
  318. package/dist/modules/workflow/pending-commits.worker.js +23 -6
  319. package/dist/modules/workflow/pending-commits.worker.js.map +1 -1
  320. package/dist/modules/workflow/recovery-bot.js +1 -1
  321. package/dist/modules/workflow/recovery-bot.js.map +1 -1
  322. package/dist/modules/workflow/review-workflow/review-workflow.service.d.ts +35 -0
  323. package/dist/modules/workflow/review-workflow/review-workflow.service.d.ts.map +1 -1
  324. package/dist/modules/workflow/review-workflow/review-workflow.service.js +180 -13
  325. package/dist/modules/workflow/review-workflow/review-workflow.service.js.map +1 -1
  326. package/dist/modules/workflow/sanitize-error.d.ts +16 -0
  327. package/dist/modules/workflow/sanitize-error.d.ts.map +1 -1
  328. package/dist/modules/workflow/sanitize-error.js +45 -0
  329. package/dist/modules/workflow/sanitize-error.js.map +1 -1
  330. package/dist/modules/workflow/workflow.routes.d.ts +6 -2
  331. package/dist/modules/workflow/workflow.routes.d.ts.map +1 -1
  332. package/dist/modules/workflow/workflow.routes.js +7 -2
  333. package/dist/modules/workflow/workflow.routes.js.map +1 -1
  334. package/dist/modules/workflow/workflow.service.d.ts +187 -15
  335. package/dist/modules/workflow/workflow.service.d.ts.map +1 -1
  336. package/dist/modules/workflow/workflow.service.js +808 -106
  337. package/dist/modules/workflow/workflow.service.js.map +1 -1
  338. package/dist/modules/workspace/agent-download.builder.d.ts +105 -0
  339. package/dist/modules/workspace/agent-download.builder.d.ts.map +1 -0
  340. package/dist/modules/workspace/agent-download.builder.js +238 -0
  341. package/dist/modules/workspace/agent-download.builder.js.map +1 -0
  342. package/dist/modules/workspace/agent-download.routes.d.ts +77 -0
  343. package/dist/modules/workspace/agent-download.routes.d.ts.map +1 -0
  344. package/dist/modules/workspace/agent-download.routes.js +156 -0
  345. package/dist/modules/workspace/agent-download.routes.js.map +1 -0
  346. package/dist/modules/workspace/agent-download.store.d.ts +225 -0
  347. package/dist/modules/workspace/agent-download.store.d.ts.map +1 -0
  348. package/dist/modules/workspace/agent-download.store.js +423 -0
  349. package/dist/modules/workspace/agent-download.store.js.map +1 -0
  350. package/dist/modules/workspace/agent-upload.store.d.ts +10 -1
  351. package/dist/modules/workspace/agent-upload.store.d.ts.map +1 -1
  352. package/dist/modules/workspace/agent-upload.store.js +16 -6
  353. package/dist/modules/workspace/agent-upload.store.js.map +1 -1
  354. package/dist/modules/workspace/file-readers/raw-mime.d.ts +6 -0
  355. package/dist/modules/workspace/file-readers/raw-mime.d.ts.map +1 -0
  356. package/dist/modules/workspace/file-readers/raw-mime.js +38 -0
  357. package/dist/modules/workspace/file-readers/raw-mime.js.map +1 -0
  358. package/dist/modules/workspace/file-readers/text-reader.d.ts +5 -5
  359. package/dist/modules/workspace/file-readers/text-reader.d.ts.map +1 -1
  360. package/dist/modules/workspace/file-readers/text-reader.js +5 -8
  361. package/dist/modules/workspace/file-readers/text-reader.js.map +1 -1
  362. package/dist/modules/workspace/first-run.d.ts +111 -0
  363. package/dist/modules/workspace/first-run.d.ts.map +1 -0
  364. package/dist/modules/workspace/first-run.js +181 -0
  365. package/dist/modules/workspace/first-run.js.map +1 -0
  366. package/dist/modules/workspace/move-links.d.ts +92 -0
  367. package/dist/modules/workspace/move-links.d.ts.map +1 -0
  368. package/dist/modules/workspace/move-links.js +173 -0
  369. package/dist/modules/workspace/move-links.js.map +1 -0
  370. package/dist/modules/workspace/startup/steps/seed-tree.d.ts.map +1 -1
  371. package/dist/modules/workspace/startup/steps/seed-tree.js +24 -39
  372. package/dist/modules/workspace/startup/steps/seed-tree.js.map +1 -1
  373. package/dist/modules/workspace/startup/steps/template-files.step.d.ts +58 -52
  374. package/dist/modules/workspace/startup/steps/template-files.step.d.ts.map +1 -1
  375. package/dist/modules/workspace/startup/steps/template-files.step.js +209 -223
  376. package/dist/modules/workspace/startup/steps/template-files.step.js.map +1 -1
  377. package/dist/modules/workspace/startup/steps/template-source.d.ts +5 -3
  378. package/dist/modules/workspace/startup/steps/template-source.d.ts.map +1 -1
  379. package/dist/modules/workspace/startup/steps/template-source.js +5 -3
  380. package/dist/modules/workspace/startup/steps/template-source.js.map +1 -1
  381. package/dist/modules/workspace/workspace.routes.d.ts.map +1 -1
  382. package/dist/modules/workspace/workspace.routes.js +63 -22
  383. package/dist/modules/workspace/workspace.routes.js.map +1 -1
  384. package/dist/modules/workspace/workspace.service.d.ts +75 -2
  385. package/dist/modules/workspace/workspace.service.d.ts.map +1 -1
  386. package/dist/modules/workspace/workspace.service.js +197 -24
  387. package/dist/modules/workspace/workspace.service.js.map +1 -1
  388. package/dist/modules/workspace/workspace.tools.d.ts +68 -1
  389. package/dist/modules/workspace/workspace.tools.d.ts.map +1 -1
  390. package/dist/modules/workspace/workspace.tools.js +642 -71
  391. package/dist/modules/workspace/workspace.tools.js.map +1 -1
  392. package/dist/modules/write-access/write-access.d.ts.map +1 -1
  393. package/dist/modules/write-access/write-access.js +9 -0
  394. package/dist/modules/write-access/write-access.js.map +1 -1
  395. package/dist/shared/domain-errors.d.ts +84 -1
  396. package/dist/shared/domain-errors.d.ts.map +1 -1
  397. package/dist/shared/domain-errors.js +100 -12
  398. package/dist/shared/domain-errors.js.map +1 -1
  399. package/dist/shared/git.contract.d.ts +10 -1
  400. package/dist/shared/git.contract.d.ts.map +1 -1
  401. package/dist/shared/git.contract.js.map +1 -1
  402. package/dist/shared/hidden-tools.d.ts +44 -0
  403. package/dist/shared/hidden-tools.d.ts.map +1 -0
  404. package/dist/shared/hidden-tools.js +13 -0
  405. package/dist/shared/hidden-tools.js.map +1 -0
  406. package/dist/shared/utf8-text.d.ts +15 -0
  407. package/dist/shared/utf8-text.d.ts.map +1 -0
  408. package/dist/shared/utf8-text.js +20 -0
  409. package/dist/shared/utf8-text.js.map +1 -0
  410. package/dist/tenancy/static-tenant-source.d.ts +2 -1
  411. package/dist/tenancy/static-tenant-source.d.ts.map +1 -1
  412. package/dist/tenancy/static-tenant-source.js +9 -2
  413. package/dist/tenancy/static-tenant-source.js.map +1 -1
  414. package/kb-template/.bevelignore +0 -5
  415. package/mcp-app/page.html +349 -0
  416. package/migrations/0017_atlassian_account_links.sql +37 -0
  417. package/migrations/meta/0017_snapshot.json +2396 -0
  418. package/migrations/meta/_journal.json +8 -1
  419. package/package.json +6 -3
  420. package/src/__tests__/kb-layout-config.test.ts +10 -100
  421. package/src/__tests__/packaged-assets-ship.test.ts +66 -0
  422. package/src/assets.ts +38 -2
  423. package/src/core/__tests__/catalog-revision.test.ts +40 -0
  424. package/src/core/__tests__/gated-boot.test.ts +1 -0
  425. package/src/core/__tests__/lifecycle.test.ts +14 -0
  426. package/src/core/catalog-revision.ts +15 -2
  427. package/src/core/core-ports.ts +11 -0
  428. package/src/core/create-core-server.ts +78 -3
  429. package/src/core/create-core-services.ts +168 -10
  430. package/src/core/lifecycle.ts +28 -2
  431. package/src/core-config.ts +24 -1
  432. package/src/index.ts +2 -2
  433. package/src/modules/access/__tests__/access-control.atref-batch.test.ts +58 -0
  434. package/src/modules/access/__tests__/access-control.platform-restore.test.ts +8 -7
  435. package/src/modules/access/__tests__/access-control.service.test.ts +19 -0
  436. package/src/modules/access/__tests__/access-personal-plugin.test.ts +1 -18
  437. package/src/modules/access/access-control.interface.ts +28 -0
  438. package/src/modules/access/access-control.service.ts +37 -0
  439. package/src/modules/access/directory-sync-bot.ts +1 -1
  440. package/src/modules/agent-guide/__tests__/agent-guide.test.ts +386 -0
  441. package/src/modules/agent-guide/__tests__/agent-guide.tools.test.ts +207 -0
  442. package/src/modules/agent-guide/agent-guide.tools.ts +122 -0
  443. package/src/modules/agent-guide/agent-guide.ts +300 -0
  444. package/src/modules/agent-guide/index.ts +22 -0
  445. package/src/modules/agent-instructions/__tests__/shared-file-rules.test.ts +39 -118
  446. package/src/modules/agent-instructions/agent-instructions.routes.ts +3 -2
  447. package/src/modules/agent-instructions/compose.ts +26 -8
  448. package/src/modules/agent-instructions/index.ts +0 -3
  449. package/src/modules/agent-instructions/shared-file-rules.ts +50 -112
  450. package/src/modules/audit/__tests__/agent-audit.service.test.ts +64 -4
  451. package/src/modules/audit/__tests__/agent-connection.routes.test.ts +102 -0
  452. package/src/modules/audit/agent-audit.service.ts +58 -3
  453. package/src/modules/audit/agent-connection.routes.ts +56 -0
  454. package/src/modules/audit/audit.contract.ts +26 -0
  455. package/src/modules/audit/index.ts +1 -0
  456. package/src/modules/auth/__tests__/account.routes.test.ts +5 -3
  457. package/src/modules/auth/account.routes.ts +10 -3
  458. package/src/modules/auth/auth.middleware.ts +1 -1
  459. package/src/modules/code-mode/__tests__/code-mode.tool.test.ts +18 -2
  460. package/src/modules/code-mode/code-mode.tool.ts +13 -2
  461. package/src/modules/database/core-schema.ts +33 -3
  462. package/src/modules/embed/__tests__/account-links.migration.test.ts +98 -0
  463. package/src/modules/embed/__tests__/embed-link.test.ts +122 -0
  464. package/src/modules/embed/__tests__/embed.routes.test.ts +362 -0
  465. package/src/modules/embed/__tests__/embed.service.lock.test.ts +193 -0
  466. package/src/modules/embed/__tests__/embed.service.test.ts +805 -0
  467. package/src/modules/embed/__tests__/embed.tools.test.ts +304 -0
  468. package/src/modules/embed/__tests__/mcp-app.test.ts +207 -0
  469. package/src/modules/embed/account-link.service.ts +76 -0
  470. package/src/modules/embed/embed-link.ts +180 -0
  471. package/src/modules/embed/embed.errors.ts +43 -0
  472. package/src/modules/embed/embed.interface.ts +203 -0
  473. package/src/modules/embed/embed.routes.ts +363 -0
  474. package/src/modules/embed/embed.service.ts +705 -0
  475. package/src/modules/embed/embed.tools.ts +206 -0
  476. package/src/modules/embed/mcp-app.routes.ts +41 -0
  477. package/src/modules/embed/mcp-app.ts +173 -0
  478. package/src/modules/github-app/__tests__/github-app.test.ts +76 -0
  479. package/src/modules/github-app/github-app.connection.ts +38 -4
  480. package/src/modules/kb-fs/__tests__/move-with-edits.test.ts +304 -0
  481. package/src/modules/kb-fs/locking-filesystem.ts +287 -0
  482. package/src/modules/kb-sync/__tests__/kb-sync.service.test.ts +35 -0
  483. package/src/modules/kb-sync/kb-sync.interface.ts +2 -0
  484. package/src/modules/kb-sync/kb-sync.service.ts +6 -0
  485. package/src/modules/mcp/__tests__/bevel-oauth-provider.test.ts +68 -11
  486. package/src/modules/mcp/__tests__/fake-downstream-mcp-server.ts +14 -3
  487. package/src/modules/mcp/__tests__/mcp-proxy-helpers.test.ts +37 -2
  488. package/src/modules/mcp/__tests__/mcp.app.e2e.test.ts +200 -0
  489. package/src/modules/mcp/__tests__/mcp.e2e.test.ts +252 -0
  490. package/src/modules/mcp/__tests__/mcp.service.test.ts +42 -24
  491. package/src/modules/mcp/__tests__/tool-call-interface.e2e.test.ts +499 -0
  492. package/src/modules/mcp/__tests__/tool-schema-guard.test.ts +266 -0
  493. package/src/modules/mcp/mcp-auth.middleware.ts +2 -1
  494. package/src/modules/mcp/mcp.service.ts +218 -31
  495. package/src/modules/mcp/oauth/bevel-oauth-provider.ts +42 -4
  496. package/src/modules/mcp/tool-schema-guard.ts +196 -0
  497. package/src/modules/onboarding/__tests__/onboarding.routes.test.ts +163 -0
  498. package/src/modules/onboarding/__tests__/starter-pack.service.test.ts +635 -0
  499. package/src/modules/onboarding/__tests__/starter-packs.test.ts +169 -0
  500. package/src/modules/onboarding/onboarding.contract.ts +85 -0
  501. package/src/modules/onboarding/onboarding.routes.ts +85 -0
  502. package/src/modules/onboarding/starter-pack.service.ts +580 -0
  503. package/src/modules/onboarding/starter-packs.ts +182 -0
  504. package/src/modules/plugins/__tests__/plugins.tools.test.ts +177 -4
  505. package/src/modules/plugins/index.ts +1 -0
  506. package/src/modules/plugins/plugin-provision.service.ts +22 -1
  507. package/src/modules/plugins/plugins.routes.ts +29 -0
  508. package/src/modules/plugins/plugins.tools.ts +75 -15
  509. package/src/modules/settings/__tests__/deployment-settings.service.test.ts +176 -56
  510. package/src/modules/settings/__tests__/repository-source.test.ts +9 -0
  511. package/src/modules/settings/deployment-settings.service.ts +144 -55
  512. package/src/modules/settings/repository-source.ts +4 -4
  513. package/src/modules/settings/setup.routes.ts +3 -6
  514. package/src/modules/skills/__tests__/skills.tools.description.test.ts +95 -0
  515. package/src/modules/skills/skills.tools.ts +112 -46
  516. package/src/modules/tool-auth/__tests__/connection-key-rejection.test.ts +32 -0
  517. package/src/modules/tool-auth/__tests__/external-api-key.service.test.ts +65 -0
  518. package/src/modules/tool-auth/connection-key-rejection.ts +13 -3
  519. package/src/modules/tool-auth/external-api-key.interface.ts +9 -0
  520. package/src/modules/tool-auth/external-api-key.service.ts +61 -4
  521. package/src/modules/tool-auth/tool-auth.middleware.ts +13 -4
  522. package/src/modules/tool-helpers/__tests__/branch-declaration.test.ts +502 -0
  523. package/src/modules/tool-helpers/__tests__/route-argument-check.test.ts +337 -0
  524. package/src/modules/tool-helpers/index.ts +34 -1
  525. package/src/modules/tool-helpers/route-argument-check.ts +149 -0
  526. package/src/modules/tool-helpers/route-tool-schemas.ts +171 -0
  527. package/src/modules/tool-helpers/tool-def.ts +132 -7
  528. package/src/modules/tool-helpers/tool-handler.ts +196 -8
  529. package/src/modules/tool-helpers/tool.contract.ts +9 -0
  530. package/src/modules/tool-manuals/__tests__/tool-manuals.detail.route.test.ts +57 -0
  531. package/src/modules/tool-manuals/__tests__/tool-manuals.tools.test.ts +73 -4
  532. package/src/modules/tool-manuals/tool-manuals.contract.ts +24 -4
  533. package/src/modules/tool-manuals/tool-manuals.service.ts +17 -0
  534. package/src/modules/tool-manuals/tool-manuals.tools.ts +86 -38
  535. package/src/modules/tool-registry/__tests__/own-tool-schemas.test.ts +173 -0
  536. package/src/modules/tool-registry/__tests__/tool-description-length.test.ts +81 -66
  537. package/src/modules/tool-registry/description-length.ts +57 -32
  538. package/src/modules/tool-registry/guide-first.ts +41 -0
  539. package/src/modules/tool-registry/tool-registry.ts +9 -2
  540. package/src/modules/workflow/__tests__/apply-failure.test.ts +6 -1
  541. package/src/modules/workflow/__tests__/file-lock.service.deletion-gate.test.ts +104 -0
  542. package/src/modules/workflow/__tests__/pending-commits.repository-replaced.test.ts +66 -0
  543. package/src/modules/workflow/__tests__/pending-commits.worker.test.ts +31 -2
  544. package/src/modules/workflow/__tests__/preserve-roles-yaml.test.ts +110 -6
  545. package/src/modules/workflow/__tests__/sanitize-error.test.ts +89 -1
  546. package/src/modules/workflow/__tests__/workflow.routes.push-refused.test.ts +202 -0
  547. package/src/modules/workflow/__tests__/workflow.service.branch-in-use.test.ts +6 -6
  548. package/src/modules/workflow/__tests__/workflow.service.delete-branch.test.ts +1026 -0
  549. package/src/modules/workflow/__tests__/workflow.service.facade.test.ts +468 -10
  550. package/src/modules/workflow/__tests__/workflow.service.folder-proposals.test.ts +4 -0
  551. package/src/modules/workflow/__tests__/workflow.service.releaseLock.test.ts +5 -4
  552. package/src/modules/workflow/__tests__/workflow.service.update-from-target.test.ts +54 -2
  553. package/src/modules/workflow/agent-tools/__tests__/change-request-read-shape.test.ts +705 -0
  554. package/src/modules/workflow/agent-tools/__tests__/change-request-read.tools.test.ts +1554 -0
  555. package/src/modules/workflow/agent-tools/__tests__/workflow.tools.test.ts +150 -4
  556. package/src/modules/workflow/agent-tools/change-request-read-shape.ts +712 -0
  557. package/src/modules/workflow/agent-tools/change-request-read.tools.ts +759 -0
  558. package/src/modules/workflow/agent-tools/change-request-summary.ts +5 -1
  559. package/src/modules/workflow/agent-tools/workflow.tools.ts +137 -35
  560. package/src/modules/workflow/file-lock.service.ts +67 -5
  561. package/src/modules/workflow/git/__tests__/git.service.appliedChange.test.ts +285 -0
  562. package/src/modules/workflow/git/__tests__/git.service.changedFilesForPr.test.ts +124 -0
  563. package/src/modules/workflow/git/__tests__/git.service.deleteBranch.test.ts +164 -0
  564. package/src/modules/workflow/git/__tests__/git.service.mergeChangeRequest.test.ts +334 -0
  565. package/src/modules/workflow/git/__tests__/git.service.pull.test.ts +48 -1
  566. package/src/modules/workflow/git/__tests__/node-git-runner.test.ts +231 -2
  567. package/src/modules/workflow/git/__tests__/pull-request.service.list-fetch.test.ts +72 -2
  568. package/src/modules/workflow/git/__tests__/pull-request.service.placeholder.test.ts +24 -2
  569. package/src/modules/workflow/git/__tests__/pull-request.service.test.ts +620 -1
  570. package/src/modules/workflow/git/git.service.ts +714 -133
  571. package/src/modules/workflow/git/merge-commit.ts +88 -0
  572. package/src/modules/workflow/git/node-git-runner.ts +82 -7
  573. package/src/modules/workflow/git/pull-request.service.ts +380 -54
  574. package/src/modules/workflow/pending-commits.service.ts +29 -1
  575. package/src/modules/workflow/pending-commits.worker.ts +24 -6
  576. package/src/modules/workflow/recovery-bot.ts +1 -1
  577. package/src/modules/workflow/review-workflow/__tests__/approval-states.test.ts +7 -1
  578. package/src/modules/workflow/review-workflow/__tests__/merge-records-own-commit.test.ts +407 -0
  579. package/src/modules/workflow/review-workflow/review-workflow.service.ts +191 -12
  580. package/src/modules/workflow/sanitize-error.ts +50 -0
  581. package/src/modules/workflow/workflow.routes.ts +7 -2
  582. package/src/modules/workflow/workflow.service.ts +886 -112
  583. package/src/modules/workspace/__tests__/agent-downloads.test.ts +823 -0
  584. package/src/modules/workspace/__tests__/escape-sequences.routes.test.ts +4 -3
  585. package/src/modules/workspace/__tests__/first-run.test.ts +234 -0
  586. package/src/modules/workspace/__tests__/git-internals.security.test.ts +5 -2
  587. package/src/modules/workspace/__tests__/md-links.test.ts +353 -0
  588. package/src/modules/workspace/__tests__/move-links.test.ts +261 -0
  589. package/src/modules/workspace/__tests__/rest-tool-arguments.test.ts +219 -0
  590. package/src/modules/workspace/__tests__/workspace-paths-inside-the-repo.test.ts +3 -0
  591. package/src/modules/workspace/__tests__/workspace.routes.delete.test.ts +102 -0
  592. package/src/modules/workspace/__tests__/workspace.routes.download.test.ts +52 -5
  593. package/src/modules/workspace/__tests__/workspace.routes.folder-zip-access.test.ts +190 -0
  594. package/src/modules/workspace/__tests__/workspace.routes.move-platform-files.test.ts +21 -10
  595. package/src/modules/workspace/__tests__/workspace.service.forget-clone-races.test.ts +8 -4
  596. package/src/modules/workspace/__tests__/workspace.service.test.ts +50 -8
  597. package/src/modules/workspace/__tests__/workspace.tools.agents-file.test.ts +33 -55
  598. package/src/modules/workspace/__tests__/workspace.tools.branch-errors.test.ts +44 -1
  599. package/src/modules/workspace/__tests__/workspace.tools.test.ts +664 -34
  600. package/src/modules/workspace/agent-download.builder.ts +308 -0
  601. package/src/modules/workspace/agent-download.routes.ts +183 -0
  602. package/src/modules/workspace/agent-download.store.ts +533 -0
  603. package/src/modules/workspace/agent-upload.store.ts +23 -7
  604. package/src/modules/workspace/file-readers/raw-mime.ts +40 -0
  605. package/src/modules/workspace/file-readers/text-reader.ts +5 -8
  606. package/src/modules/workspace/first-run.ts +222 -0
  607. package/src/modules/workspace/move-links.ts +237 -0
  608. package/src/modules/workspace/startup/steps/__tests__/steps.test.ts +191 -489
  609. package/src/modules/workspace/startup/steps/seed-tree.ts +23 -38
  610. package/src/modules/workspace/startup/steps/template-files.step.ts +217 -249
  611. package/src/modules/workspace/startup/steps/template-source.ts +5 -3
  612. package/src/modules/workspace/workspace.routes.ts +64 -22
  613. package/src/modules/workspace/workspace.service.ts +202 -25
  614. package/src/modules/workspace/workspace.tools.ts +707 -68
  615. package/src/modules/write-access/__tests__/write-access.test.ts +13 -0
  616. package/src/modules/write-access/write-access.ts +9 -0
  617. package/src/shared/domain-errors.ts +123 -11
  618. package/src/shared/git.contract.ts +8 -1
  619. package/src/shared/hidden-tools.ts +45 -0
  620. package/src/shared/utf8-text.ts +21 -0
  621. package/src/tenancy/__tests__/static-tenant-source.test.ts +1 -0
  622. package/src/tenancy/static-tenant-source.ts +10 -2
  623. package/starter-packs/README.md +103 -0
  624. package/starter-packs/engineering/KnowledgeBase/About us.md +9 -0
  625. package/starter-packs/engineering/KnowledgeBase/Architecture.md +9 -0
  626. package/starter-packs/engineering/KnowledgeBase/Glossary.md +9 -0
  627. package/starter-packs/engineering/KnowledgeBase/How we ship.md +9 -0
  628. package/starter-packs/engineering/Plugins/engineering-starter/LICENSE +21 -0
  629. package/starter-packs/engineering/Plugins/engineering-starter/SOURCE.md +20 -0
  630. package/starter-packs/engineering/Plugins/engineering-starter/access.md +19 -0
  631. package/starter-packs/engineering/Plugins/engineering-starter/plugin.json +10 -0
  632. package/starter-packs/engineering/Plugins/engineering-starter/skills/office-hours/SKILL.md +171 -0
  633. package/starter-packs/engineering/Plugins/engineering-starter/skills/office-hours/sections/phase-2a-startup-diagnostic.md +157 -0
  634. package/starter-packs/engineering/Plugins/engineering-starter/skills/office-hours/sections/phase-2b-builder-brainstorm.md +39 -0
  635. package/starter-packs/engineering/Plugins/engineering-starter/skills/plan-ceo-review/SKILL.md +144 -0
  636. package/starter-packs/engineering/Plugins/engineering-starter/skills/plan-eng-review/SKILL.md +144 -0
  637. package/starter-packs/engineering/Plugins/engineering-starter/skills/review/SKILL.md +80 -0
  638. package/starter-packs/engineering/Plugins/engineering-starter/skills/review/checklist.md +184 -0
  639. package/starter-packs/engineering/pack.yaml +11 -0
  640. package/starter-packs/general/KnowledgeBase/About us.md +9 -0
  641. package/starter-packs/general/KnowledgeBase/Glossary.md +9 -0
  642. package/starter-packs/general/KnowledgeBase/How we work.md +9 -0
  643. package/starter-packs/general/Plugins/general-starter/LICENSE +202 -0
  644. package/starter-packs/general/Plugins/general-starter/SOURCE.md +10 -0
  645. package/starter-packs/general/Plugins/general-starter/access.md +19 -0
  646. package/starter-packs/general/Plugins/general-starter/plugin.json +10 -0
  647. package/starter-packs/general/Plugins/general-starter/skills/memory-management/SKILL.md +332 -0
  648. package/starter-packs/general/Plugins/general-starter/skills/productivity-start/SKILL.md +163 -0
  649. package/starter-packs/general/Plugins/general-starter/skills/productivity-update/SKILL.md +167 -0
  650. package/starter-packs/general/Plugins/general-starter/skills/task-management/SKILL.md +83 -0
  651. package/starter-packs/general/pack.yaml +10 -0
  652. package/starter-packs/operations/KnowledgeBase/Glossary.md +9 -0
  653. package/starter-packs/operations/KnowledgeBase/How we work.md +9 -0
  654. package/starter-packs/operations/KnowledgeBase/Processes.md +9 -0
  655. package/starter-packs/operations/KnowledgeBase/Tools we use.md +9 -0
  656. package/starter-packs/operations/KnowledgeBase/Vendors.md +9 -0
  657. package/starter-packs/operations/Plugins/operations-starter/LICENSE +202 -0
  658. package/starter-packs/operations/Plugins/operations-starter/SOURCE.md +10 -0
  659. package/starter-packs/operations/Plugins/operations-starter/access.md +19 -0
  660. package/starter-packs/operations/Plugins/operations-starter/plugin.json +10 -0
  661. package/starter-packs/operations/Plugins/operations-starter/skills/capacity-plan/SKILL.md +111 -0
  662. package/starter-packs/operations/Plugins/operations-starter/skills/change-request/SKILL.md +119 -0
  663. package/starter-packs/operations/Plugins/operations-starter/skills/compliance-tracking/SKILL.md +44 -0
  664. package/starter-packs/operations/Plugins/operations-starter/skills/process-doc/SKILL.md +80 -0
  665. package/starter-packs/operations/Plugins/operations-starter/skills/process-optimization/SKILL.md +39 -0
  666. package/starter-packs/operations/Plugins/operations-starter/skills/risk-assessment/SKILL.md +40 -0
  667. package/starter-packs/operations/Plugins/operations-starter/skills/runbook/SKILL.md +85 -0
  668. package/starter-packs/operations/Plugins/operations-starter/skills/status-report/SKILL.md +75 -0
  669. package/starter-packs/operations/Plugins/operations-starter/skills/vendor-review/SKILL.md +102 -0
  670. package/starter-packs/operations/pack.yaml +12 -0
  671. package/starter-packs/sales/KnowledgeBase/About us.md +9 -0
  672. package/starter-packs/sales/KnowledgeBase/Customers.md +9 -0
  673. package/starter-packs/sales/KnowledgeBase/Glossary.md +9 -0
  674. package/starter-packs/sales/KnowledgeBase/How we sell.md +9 -0
  675. package/starter-packs/sales/KnowledgeBase/Objections.md +9 -0
  676. package/starter-packs/sales/KnowledgeBase/Pricing.md +9 -0
  677. package/starter-packs/sales/Plugins/sales-starter/LICENSE +202 -0
  678. package/starter-packs/sales/Plugins/sales-starter/SOURCE.md +10 -0
  679. package/starter-packs/sales/Plugins/sales-starter/access.md +19 -0
  680. package/starter-packs/sales/Plugins/sales-starter/plugin.json +10 -0
  681. package/starter-packs/sales/Plugins/sales-starter/skills/account-context/SKILL.md +94 -0
  682. package/starter-packs/sales/Plugins/sales-starter/skills/account-plan/SKILL.md +95 -0
  683. package/starter-packs/sales/Plugins/sales-starter/skills/account-research/SKILL.md +99 -0
  684. package/starter-packs/sales/Plugins/sales-starter/skills/account-tiering/SKILL.md +101 -0
  685. package/starter-packs/sales/Plugins/sales-starter/skills/call-prep/SKILL.md +111 -0
  686. package/starter-packs/sales/Plugins/sales-starter/skills/call-summary/SKILL.md +111 -0
  687. package/starter-packs/sales/Plugins/sales-starter/skills/close-plan/SKILL.md +106 -0
  688. package/starter-packs/sales/Plugins/sales-starter/skills/competitive-intelligence/SKILL.md +102 -0
  689. package/starter-packs/sales/Plugins/sales-starter/skills/create-an-asset/SKILL.md +105 -0
  690. package/starter-packs/sales/Plugins/sales-starter/skills/crm-hygiene-check/SKILL.md +103 -0
  691. package/starter-packs/sales/Plugins/sales-starter/skills/customer-health/SKILL.md +108 -0
  692. package/starter-packs/sales/Plugins/sales-starter/skills/customer-voice/SKILL.md +94 -0
  693. package/starter-packs/sales/Plugins/sales-starter/skills/daily-briefing/SKILL.md +61 -0
  694. package/starter-packs/sales/Plugins/sales-starter/skills/deal-advance-gap/SKILL.md +102 -0
  695. package/starter-packs/sales/Plugins/sales-starter/skills/deal-review/SKILL.md +105 -0
  696. package/starter-packs/sales/Plugins/sales-starter/skills/deal-signals/SKILL.md +96 -0
  697. package/starter-packs/sales/Plugins/sales-starter/skills/deal-slip-scenario/SKILL.md +104 -0
  698. package/starter-packs/sales/Plugins/sales-starter/skills/draft-outreach/SKILL.md +116 -0
  699. package/starter-packs/sales/Plugins/sales-starter/skills/end-of-day/SKILL.md +114 -0
  700. package/starter-packs/sales/Plugins/sales-starter/skills/expansion-whitespace/SKILL.md +97 -0
  701. package/starter-packs/sales/Plugins/sales-starter/skills/forecast/SKILL.md +100 -0
  702. package/starter-packs/sales/Plugins/sales-starter/skills/handle-objection/SKILL.md +93 -0
  703. package/starter-packs/sales/Plugins/sales-starter/skills/inbox-sweep/SKILL.md +114 -0
  704. package/starter-packs/sales/Plugins/sales-starter/skills/lead-triage/SKILL.md +106 -0
  705. package/starter-packs/sales/Plugins/sales-starter/skills/log-activity/SKILL.md +111 -0
  706. package/starter-packs/sales/Plugins/sales-starter/skills/pipeline-review/SKILL.md +97 -0
  707. package/starter-packs/sales/Plugins/sales-starter/skills/renewal-radar/SKILL.md +95 -0
  708. package/starter-packs/sales/Plugins/sales-starter/skills/rep-context/SKILL.md +90 -0
  709. package/starter-packs/sales/Plugins/sales-starter/skills/route-lead/SKILL.md +111 -0
  710. package/starter-packs/sales/Plugins/sales-starter/skills/sales-setup/SKILL.md +200 -0
  711. package/starter-packs/sales/Plugins/sales-starter/skills/schedule-meeting/SKILL.md +99 -0
  712. package/starter-packs/sales/Plugins/sales-starter/skills/stakeholder-map/SKILL.md +107 -0
  713. package/starter-packs/sales/Plugins/sales-starter/skills/team-pipeline/SKILL.md +105 -0
  714. package/starter-packs/sales/Plugins/sales-starter/skills/update-opportunity/SKILL.md +112 -0
  715. package/starter-packs/sales/Plugins/sales-starter/skills/weekly-wrap/SKILL.md +90 -0
  716. package/starter-packs/sales/Plugins/sales-starter/skills/win-loss-review/SKILL.md +88 -0
  717. package/starter-packs/sales/pack.yaml +13 -0
  718. package/starter-packs/support/KnowledgeBase/Escalations.md +9 -0
  719. package/starter-packs/support/KnowledgeBase/Glossary.md +9 -0
  720. package/starter-packs/support/KnowledgeBase/Known issues.md +9 -0
  721. package/starter-packs/support/KnowledgeBase/Products.md +9 -0
  722. package/starter-packs/support/KnowledgeBase/Tone of voice.md +9 -0
  723. package/starter-packs/support/Plugins/support-starter/LICENSE +202 -0
  724. package/starter-packs/support/Plugins/support-starter/SOURCE.md +10 -0
  725. package/starter-packs/support/Plugins/support-starter/access.md +19 -0
  726. package/starter-packs/support/Plugins/support-starter/plugin.json +10 -0
  727. package/starter-packs/support/Plugins/support-starter/skills/customer-escalation/SKILL.md +246 -0
  728. package/starter-packs/support/Plugins/support-starter/skills/customer-research/SKILL.md +250 -0
  729. package/starter-packs/support/Plugins/support-starter/skills/draft-response/SKILL.md +416 -0
  730. package/starter-packs/support/Plugins/support-starter/skills/kb-article/SKILL.md +350 -0
  731. package/starter-packs/support/Plugins/support-starter/skills/ticket-triage/SKILL.md +273 -0
  732. package/starter-packs/support/pack.yaml +12 -0
  733. package/kb-template/AGENTS.md +0 -730
@@ -5,7 +5,7 @@ import AdmZip from 'adm-zip';
5
5
  import type { Router, RequestHandler } from 'express';
6
6
  import type { LocalFilesystem } from '@mastra/core/workspace';
7
7
  import type { IToolRegistry, JsonSchema } from '../tool-registry/tool.contract.js';
8
- import { ToolError, type ToolContext, type ToolHandler } from '../tool-helpers/tool.contract.js';
8
+ import { hasHttpStatus, ToolError, type ToolContext, type ToolHandler } from '../tool-helpers/tool.contract.js';
9
9
  import { BRANCH_INPUT, toolDef } from '../tool-helpers/tool-def.js';
10
10
  import {
11
11
  notifyAgentRead,
@@ -17,7 +17,7 @@ import type { IRoutineWritePolicy } from './routine-write-policy.js';
17
17
  import type { ToolHandlerFactory } from '../tool-helpers/tool-handler.js';
18
18
  import { requireInternalSource, requireExternalSource } from '../tool-auth/tool-auth.middleware.js';
19
19
  import { workspaceIdForBranch } from '../../shared/workspace-id.js';
20
- import { assertBranchProvided, GitInternalsError, WorkflowValidationError } from '../../shared/domain-errors.js';
20
+ import { GitInternalsError, WorkflowValidationError } from '../../shared/domain-errors.js';
21
21
  // Leaf-level shared primitive (same exception `workspace.service.ts` already
22
22
  // relies on) — not a workflow service, so this stays inside the module boundary.
23
23
  import { assertValidBranchName } from '../kb-fs/branch-name.js';
@@ -27,19 +27,20 @@ import {
27
27
  assertRepoRootNameFreeArgs,
28
28
  isInsideRepo,
29
29
  normalizePathArgs,
30
+ normalizeWorkspacePath,
30
31
  } from '../kb-fs/repo-path.js';
31
32
  import { GitGuardedFilesystem } from '../kb-fs/git-guarded-filesystem.js';
32
33
  import { assertNoGitInternalsSegment, assertNotGitInternals, hasGitInternalsSegment } from '../../shared/git-internals.js';
33
34
  import { isRolesYamlPath } from '../access-model/roles-yaml-guard.js';
34
35
  import type { ISessionSink } from './session-sink.js';
35
- import { isAbsence } from '../../shared/fs.contract.js';
36
+ import { isAbsence, type ITreeWalker } from '../../shared/fs.contract.js';
36
37
  import type { AccessDecisionSource, AccessTargetKind, IAccessControl } from '../access/access-control.interface.js';
37
38
  import { accessRoster, resolveAccessView } from '../access/access-view.js';
38
39
  import { accessMdPathForFolder, fileCarriesAccessRules, governingFolderOf } from '../access/access-mutation.service.js';
39
40
  import { toKbRelative, resolveReadableMap } from '../access-model/kb-read-filter.js';
40
41
  import type { SpillStore } from './spill-store.js';
41
42
  import type { DocExtractService } from './file-readers/doc-extract.service.js';
42
- import { displayPath, type FileKind, type FileReaderRegistry } from './file-readers/file-reader.js';
43
+ import { displayPath, type FileKind, type FileReaderRegistry, type ReadResult } from './file-readers/file-reader.js';
43
44
  import { fileTypeOf, needsContent } from './file-readers/content-mode.js';
44
45
  import { createFileReaderRegistry } from './file-readers/file-reader.registry.js';
45
46
  import { DocumentReader } from './file-readers/document-reader.js';
@@ -53,20 +54,34 @@ import {
53
54
  platformFileRefusal,
54
55
  platformFileUploadRefusal,
55
56
  platformFolderRefusal,
57
+ isRepositoryOwnFile,
58
+ repositoryOwnFileDeleteRefusal,
56
59
  entryExistsMessage,
57
60
  type ExistingEntryKind,
58
61
  } from '@bevel-software/platform-shared';
59
62
  import type { KbContext } from '../../shared/kb-context.js';
60
63
  import { AccessDeniedError } from '../access-model/access-errors.js';
64
+ import {
65
+ AGENT_GUIDE_FILE,
66
+ isAgentGuidePath,
67
+ isManagedGuide,
68
+ withPlatformGuideAppended,
69
+ type AgentGuideReader,
70
+ } from '../agent-guide/agent-guide.js';
61
71
  import { removeEmptyDirs } from './empty-dirs.js';
72
+ import { FIRST_RUN_SECTION_ID, firstRunNote, knowledgeFolderIsNew, type FirstRunStarterSource } from './first-run.js';
73
+ import { planMoveLinks } from './move-links.js';
74
+ import { MoveLockedError, MoveRacedError, type LockingFilesystem } from '../kb-fs/locking-filesystem.js';
62
75
  import { rethrowAsWriteDenial } from './write-denial.js';
63
- import { sharedRulesPointer } from '../agent-instructions/shared-file-rules.js';
64
76
  import type { IChangeReadGate } from '../access-model/change-gate.js';
65
77
  import { notFound, orDeclaredNotFound, orNotFound } from './not-found.js';
66
78
  import { logger } from '../../shared/logging.js';
67
79
  import { printable } from '../../shared/printable.js';
68
80
  import { DestinationTakenError, inspectDestination } from '../../shared/rename-no-replace.js';
69
81
  import { AgentUploadStore, type ClaimedUpload } from './agent-upload.store.js';
82
+ import type { IAgentDownloadStore } from './agent-download.store.js';
83
+ import { buildDownload } from './agent-download.builder.js';
84
+ import { DOWNLOAD_MAX_FILES, ZIP_DOWNLOAD_MAX_BYTES } from './workspace.service.js';
70
85
  import {
71
86
  isSymlinkZipEntry,
72
87
  isZipNoiseEntry,
@@ -78,6 +93,9 @@ import {
78
93
 
79
94
  const log = logger('workspace-tools');
80
95
 
96
+ /** Types that run scripts wherever they are opened, sent by `request_file_download` as plain bytes. */
97
+ const ACTIVE_CONTENT_TYPES = new Set(['image/svg+xml', 'text/html', 'application/xhtml+xml']);
98
+
81
99
  /** The caller's verdict per access verb on one path. */
82
100
  interface AccessVerbs {
83
101
  read: boolean;
@@ -730,6 +748,8 @@ async function grepWalk(
730
748
  gate: ReadGate,
731
749
  notifyRead: (path: string) => Promise<void>,
732
750
  docs: DocGrepState,
751
+ /** A file the walk leaves to its caller: never opened, never counted against `max`. */
752
+ skip: (path: string) => boolean = () => false,
733
753
  ): Promise<void> {
734
754
  if (out.length >= max || depth > 12) return;
735
755
  let entries;
@@ -747,7 +767,9 @@ async function grepWalk(
747
767
  if (e.type !== 'directory' && isFolderPlaceholder(e.name)) continue;
748
768
  const p = dir ? `${dir}/${e.name}` : e.name;
749
769
  if (e.type === 'directory') {
750
- await grepWalk(fs, p, re, out, max, depth + 1, gate, notifyRead, docs);
770
+ await grepWalk(fs, p, re, out, max, depth + 1, gate, notifyRead, docs, skip);
771
+ } else if (skip(p)) {
772
+ continue;
751
773
  } else {
752
774
  // Opening a file is a read of it, even when the walk started at a root
753
775
  // the read hook was already told about — so every file the walk opens
@@ -807,13 +829,40 @@ const BATCH_SAVE_WARNINGS_OUTPUT: JsonSchema = {
807
829
  * the tools were built has to be written here rather than onto the shared
808
830
  * `SESSION_ID_INPUT` constant.
809
831
  */
810
- function sessionIdInputOf(def: { inputs?: unknown }): { description?: string } | undefined {
832
+ export function sessionIdInputOf(def: { inputs?: unknown }): { description?: string } | undefined {
811
833
  const inputs = def.inputs as
812
834
  | { properties?: { body?: { properties?: Record<string, { description?: string }> } } }
813
835
  | undefined;
814
836
  return inputs?.properties?.body?.properties?.sessionId;
815
837
  }
816
838
 
839
+ /**
840
+ * A read of ONE workspace file, exactly as `read_file` performs it: the read
841
+ * hook, the access gate, the not-found refusal and the per-extension reader,
842
+ * in that order. Rejects with the same `ToolError`s `read_file` rejects with.
843
+ *
844
+ * `offset`/`limit` and the `__tool_chain_spill__/…` ref are deliberately NOT
845
+ * here: they are `read_file`'s own arguments, not part of what reading a file
846
+ * means.
847
+ */
848
+ export type ReadForTool = (
849
+ branch: string,
850
+ path: string,
851
+ ctx: ToolContext,
852
+ ) => Promise<ReadResult>;
853
+
854
+ /**
855
+ * What the workspace tool registration hands back for another module to build
856
+ * on, rather than re-deriving.
857
+ *
858
+ * One member today, and it is the only kind of thing that belongs here: a
859
+ * behaviour the platform promises TWICE in the same words (`open_page`
860
+ * answers "the way `read_file` does") and must therefore implement once.
861
+ */
862
+ export interface WorkspaceToolsPorts {
863
+ readForTool: ReadForTool;
864
+ }
865
+
817
866
  /**
818
867
  * Workspace domain tools: the file primitives (replacing Mastra's auto-injected
819
868
  * Workspace tools) + unzip. Most just re-expose the SAME `LocalFilesystem`
@@ -857,7 +906,35 @@ export function registerWorkspaceTools(
857
906
  * two tools are not mounted at all rather than mounted and broken.
858
907
  */
859
908
  uploads?: AgentUploadStore,
860
- ): void {
909
+ /**
910
+ * The platform's agent guide (see `modules/agent-guide`), which `read_file`
911
+ * and `file_stat` answer at the guide's name in the repository root — after
912
+ * the knowledge base's own file of that name, when it has one. Optional for
913
+ * the harnesses that are about the file primitives; without it the two
914
+ * tools read the disk and nothing else.
915
+ */
916
+ agentGuide?: AgentGuideReader,
917
+ /**
918
+ * The download-link store behind `request_file_download` — the way an agent
919
+ * takes files OUT without their content passing through the model. Optional
920
+ * for the harnesses about the file primitives; without it the tool is not
921
+ * mounted.
922
+ */
923
+ downloads?: IAgentDownloadStore,
924
+ /**
925
+ * The starter pack the knowledge base was filled from, if any (see
926
+ * `modules/onboarding`): its untouched pages do not end the `firstRun`
927
+ * note, and the note names the pages it suggests. Optional; without it the
928
+ * note reads the knowledge folder alone.
929
+ */
930
+ starterPacks?: FirstRunStarterSource,
931
+ /**
932
+ * The one tree walk (see `shared/fs.contract.ts`), for the `firstRun`
933
+ * note's look at the knowledge folder. Optional for the harnesses about the
934
+ * file primitives; without it `start_session` answers the id alone.
935
+ */
936
+ disk?: ITreeWalker,
937
+ ): WorkspaceToolsPorts {
861
938
  const { kbDirName } = kb;
862
939
  /**
863
940
  * The one extension→reader registry every read-shaped decision routes
@@ -1225,6 +1302,24 @@ export function registerWorkspaceTools(
1225
1302
  return rel !== null && isPlatformFolder(rel, kb.layout) ? platformFolderRefusal(rel) : undefined;
1226
1303
  };
1227
1304
 
1305
+ /**
1306
+ * Why `delete_file` may not delete the FILE at `path` whoever asks, or
1307
+ * undefined when the caller's write access decides. Narrower than
1308
+ * {@link managedReason}: a nested `access.md` never moves, but whoever may
1309
+ * write it may delete it — as in the app — and its folder then follows its
1310
+ * parent's rules. The root's `access.md` and `roles.yaml` are deleted by
1311
+ * nobody. Judged on the on-disk spelling, as `managedReason` is.
1312
+ */
1313
+ const fileDeleteRefusal = (path: string): string | undefined => {
1314
+ const norm = path.replace(/^\.?\/+/, '').replace(/\/+$/, '');
1315
+ if (isGitMetadata(norm)) return managedReason(norm, 'file');
1316
+ const rel = toKbRelative(norm, kbDirName);
1317
+ if (rel === null || !isPlatformFile(rel, kb.layout)) return undefined;
1318
+ if (isRepositoryOwnFile(rel, kb.layout)) return repositoryOwnFileDeleteRefusal(rel);
1319
+ const name = rel.slice(rel.lastIndexOf('/') + 1);
1320
+ return name === 'access.md' ? undefined : `${name} is a platform file and cannot be deleted through the agent tools.`;
1321
+ };
1322
+
1228
1323
  /** The workspace root on disk for `branch`. */
1229
1324
  const workspaceRoot = (branch: string, ctx: ToolContext): Promise<string> =>
1230
1325
  ctx.workspaceService.getWorkspacePath(workspaceIdForBranch(branch));
@@ -1462,6 +1557,12 @@ export function registerWorkspaceTools(
1462
1557
  * flag adds nothing to what the agent reads.
1463
1558
  */
1464
1559
  gated?: boolean;
1560
+ /**
1561
+ * Refuse a read-only credential, as a write tool is refused, WITHOUT being
1562
+ * a write: the read-only-deployment gate does not apply. For a read a
1563
+ * read-only key may still not make (`request_file_download`).
1564
+ */
1565
+ writeScope?: boolean;
1465
1566
  /**
1466
1567
  * This tool resolves `branch` ITSELF and must not be pre-checked here.
1467
1568
  * Only `execute_command` sets it: for an internal session that leaves the
@@ -1471,6 +1572,8 @@ export function registerWorkspaceTools(
1471
1572
  * handler. Every other tool takes the check below.
1472
1573
  */
1473
1574
  resolvesBranchItself?: boolean;
1575
+ /** Arguments this tool refuses by name itself, with its own wording (see `ToolDefSpec.refusesItself`). */
1576
+ refusesItself?: string[];
1474
1577
  handler: ToolHandler;
1475
1578
  }): void => {
1476
1579
  const path = `/api/agent/tools/${spec.name}`;
@@ -1482,24 +1585,23 @@ export function registerWorkspaceTools(
1482
1585
  // description from the END, where what is specific to the tool sits. The
1483
1586
  // rules themselves are in the handshake instructions and in the managed
1484
1587
  // guide (see `shared-file-rules.ts`), stated once and from one text.
1485
- // Whether a call to this tool MUST name a branch, read off the tool's own
1486
- // declaration rather than assumed of the family. Every tool mounted here
1487
- // requires `branch` today; keying on the schema means a tool that declares
1488
- // it optional (and resolves absence itself, as `list_tool_setup` does on its
1489
- // own route) is not handed a refusal it never asked for.
1490
- const requiresBranch = ((spec.inputs as { required?: string[] }).required ?? []).includes('branch');
1491
1588
  const describe = (): string =>
1492
1589
  (typeof spec.description === 'function' ? spec.description() : spec.description) +
1493
- (spec.gated ? agentAccessGate.notes.gatedToolNote() : '') +
1494
- sharedRulesPointer(kb.layout);
1590
+ (spec.gated ? agentAccessGate.notes.gatedToolNote() : '');
1495
1591
  const def = toolDef({
1496
1592
  name: spec.name,
1497
1593
  description: describe(),
1498
1594
  path,
1499
1595
  inputs: spec.inputs,
1500
1596
  outputs: spec.outputs,
1597
+ refusesItself: spec.refusesItself,
1501
1598
  tags: spec.write ? ['workspace', 'write'] : ['workspace'],
1502
1599
  });
1600
+ // Every tool here declares `branch` required in its inputs, which is how
1601
+ // `toolDef` records it: the tool handler refuses a branch-less call before
1602
+ // the path work below and before any handler. Most of these tools would
1603
+ // meet the same refusal one layer down at `getFilesystem`, but not all —
1604
+ // `unzip` hands `branch` straight to the workspace service by id.
1503
1605
  registry.registerInternalTool(def);
1504
1606
  if (!spec.internalOnly) registry.registerExternalTool(def);
1505
1607
  /**
@@ -1548,16 +1650,6 @@ export function registerWorkspaceTools(
1548
1650
  // never reached the repository — the whole bug, spelled with a prefix.
1549
1651
  toolHandler(
1550
1652
  async (args, ctx) => {
1551
- // FIRST, before the path work and before any handler: every tool
1552
- // mounted here declares `branch` as a required, non-empty string, and
1553
- // nothing enforced that, so a call that named none was carried down
1554
- // until `workspaceIdForBranch` made a workspace directory out of the
1555
- // missing value. Most of these tools would meet the same refusal one
1556
- // layer down at `getFilesystem`, but not all of them do — `unzip`
1557
- // hands `branch` straight to the workspace service by id — so the
1558
- // check belongs on the mount every one of them shares rather than on
1559
- // the resolver only some of them reach.
1560
- if (requiresBranch && !spec.resolvesBranchItself) assertBranchProvided(args.branch);
1561
1653
  // BEFORE the normaliser: see `assertToolPathsNotGitInternals`.
1562
1654
  if (spec.fileTool !== false) await assertToolPathsNotGitInternals(args, ctx);
1563
1655
  const normalized = normalizePathArgs(
@@ -1591,7 +1683,8 @@ export function registerWorkspaceTools(
1591
1683
  );
1592
1684
  }
1593
1685
  },
1594
- { write: spec.write },
1686
+ // `execute_command` resolves its own branch (see `resolvesBranchItself`).
1687
+ { write: spec.write, writeScope: spec.writeScope, ...(spec.resolvesBranchItself ? { branch: 'own' as const } : {}) },
1595
1688
  ),
1596
1689
  );
1597
1690
  };
@@ -1623,12 +1716,17 @@ export function registerWorkspaceTools(
1623
1716
  const startSessionDef = toolDef({
1624
1717
  name: 'start_session',
1625
1718
  description:
1626
- 'Mint the id of this conversation, which the KnowledgeBase tools take as `sessionId`. Call this ONCE, at the start of your work and only once per run — minting a new id mid-run starts a second conversation as far as the server is concerned. The id is also a chat session in the app, so you can hand the SAME id to the `ask` tool: your reads and your questions are then one conversation. Pass the returned id explicitly as `sessionId` on every subsequent KnowledgeBase tool call (direct MCP calls and inside `call_tool_chain` alike). RETRYING IS SAFE: a call that fails created nothing, so retry it — there is no half-made session to clean up. If a retry lands after a success you simply hold two independent ids, which is harmless: keep passing the one id you have already used for the rest of the run and ignore the other. Returns `{ sessionId }`.',
1719
+ 'Mint this conversation\'s id: the `sessionId` KnowledgeBase tools take. Call this ONCE, at the start of your work — minting a new id mid-run starts a second conversation as far as the server is concerned. The id is also a chat session in the app, so you can hand the SAME id to the `ask` tool: your reads and your questions are then one conversation. Pass the returned id as `sessionId` on every later KnowledgeBase tool call, inside `call_tool_chain` too. RETRYING IS SAFE: a call that fails created nothing, so retry it. If a retry lands after a success you hold two independent ids, which is harmless: keep passing the one you already used and ignore the other. Returns `{ sessionId }`, plus `firstRun` while the knowledge base is still empty: a note to act on.',
1627
1720
  path: '/api/agent/tools/start_session',
1628
1721
  inputs: { type: 'object', properties: {}, additionalProperties: false },
1629
1722
  outputs: {
1630
1723
  type: 'object',
1631
- properties: { sessionId: str('The minted session id — pass it as `sessionId` on subsequent KnowledgeBase tool calls and to `ask`.') },
1724
+ properties: {
1725
+ sessionId: str('The minted session id — pass it as `sessionId` on subsequent KnowledgeBase tool calls and to `ask`.'),
1726
+ firstRun: str(
1727
+ `Present only while the knowledge base holds nothing but its starter guide (and a starter pack's untouched pages): what to offer the person (the guide's \`${FIRST_RUN_SECTION_ID}\` section says how).`,
1728
+ ),
1729
+ },
1632
1730
  required: ['sessionId'],
1633
1731
  },
1634
1732
  tags: ['workspace'],
@@ -1648,11 +1746,104 @@ export function registerWorkspaceTools(
1648
1746
  requireExternalSource,
1649
1747
  toolHandler(async (_args, ctx) => {
1650
1748
  const { sessionId } = await sessionSink.createSession(ctx.user.id, new Date());
1651
- return { sessionId };
1749
+ const firstRun = await firstRunFor(ctx);
1750
+ return firstRun ? { sessionId, firstRun } : { sessionId };
1652
1751
  }),
1653
1752
  );
1654
1753
 
1754
+ /**
1755
+ * The `firstRun` note (see `first-run.ts`) while the default branch's
1756
+ * knowledge folder holds nothing but the starter guide, else null. Asked of
1757
+ * a clone that is ALREADY there — never one this call would have to make, so
1758
+ * the first call of a conversation does no clone — and never allowed to fail
1759
+ * the call: minting the id is what `start_session` is for, and the note is a
1760
+ * courtesy on top of it.
1761
+ */
1762
+ const firstRunFor = async (ctx: ToolContext): Promise<string | null> => {
1763
+ try {
1764
+ if (!disk || !kb.isBranchModelConfigured()) return null;
1765
+ const workspaceId = kb.defaultWorkspaceId();
1766
+ if (!(await ctx.workspaceService.hasBootstrappedWorkspace(workspaceId))) return null;
1767
+ const knowledgeDir = kb.layout.knowledgeBaseDir;
1768
+ // GATED LIKE A READ. The note says what the knowledge folder holds —
1769
+ // nothing, or a pack's pages still as the pack wrote them — so it goes
1770
+ // only to a caller who may read that folder, and judges the folder as
1771
+ // THEY may see it: a page they may not read does not make it old for
1772
+ // them (`mayRead` below), since reading the folder is no leave to learn
1773
+ // what restricted pages sit in it.
1774
+ if (!(await accessControl.canRead(workspaceId, ctx.user.email, knowledgeDir))) return null;
1775
+ const root = await ctx.workspaceService.getWorkspacePath(workspaceId);
1776
+ const mayRead = async (rels: string[]) => {
1777
+ const verdicts = await accessControl.canReadBatch(workspaceId, ctx.user.email, rels.map((rel) => `${knowledgeDir}/${rel}`));
1778
+ return new Map(rels.map((rel) => [rel, verdicts.get(`${knowledgeDir}/${rel}`) === true]));
1779
+ };
1780
+ // A starter pack's pages, still as the pack wrote them, are tasks to
1781
+ // fill in rather than pages anyone wrote: they leave the note standing,
1782
+ // and the note names what the pack suggests drafting first. A pack
1783
+ // page the caller may not read keeps the note away altogether: it
1784
+ // names the pack and what it suggests drafting, which is about pages
1785
+ // this caller is not to know of — in the checkout or gone from it.
1786
+ const starter = (await starterPacks?.firstRunStarter()) ?? null;
1787
+ const pages = starter ? await readableStarterPages(ctx, workspaceId, knowledgeDir, starter.pages) : undefined;
1788
+ if (starter && pages && pages.size < starter.pages.size) return null;
1789
+ if (!(await knowledgeFolderIsNew(disk, join(root, kbDirName), knowledgeDir, pages, mayRead))) return null;
1790
+ return firstRunNote(`${kbDirName}/${knowledgeDir}`, starter ?? undefined);
1791
+ } catch (err) {
1792
+ log.debug('start_session: could not tell whether the knowledge base is new', {
1793
+ error: err instanceof Error ? err.message : String(err),
1794
+ });
1795
+ return null;
1796
+ }
1797
+ };
1798
+
1799
+ /** The starter pages (paths below the knowledge folder → text) the caller may read, and no other. */
1800
+ const readableStarterPages = async (
1801
+ ctx: ToolContext,
1802
+ workspaceId: string,
1803
+ knowledgeDir: string,
1804
+ pages: ReadonlyMap<string, string>,
1805
+ ): Promise<ReadonlyMap<string, string>> => {
1806
+ const rels = [...pages.keys()];
1807
+ const verdicts = await accessControl.canReadBatch(workspaceId, ctx.user.email, rels.map((rel) => `${knowledgeDir}/${rel}`));
1808
+ return new Map(rels.filter((rel) => verdicts.get(`${knowledgeDir}/${rel}`)).map((rel) => [rel, pages.get(rel)!]));
1809
+ };
1810
+
1655
1811
  // ── reads ──────────────────────────────────────────────────────────────
1812
+
1813
+ /**
1814
+ * What reading a workspace file ANSWERS, gate and all — `read_file`'s whole
1815
+ * behaviour minus the spill ref and the `offset`/`limit` slice, which are
1816
+ * that tool's own arguments.
1817
+ *
1818
+ * Factored out because a second tool has to answer the same way: `open_page`
1819
+ * (see `modules/embed`) promises the file's text "the way `read_file` does",
1820
+ * and the refusals `read_file` gives for a path the caller may not read or
1821
+ * one that does not exist. Any of that re-derived there would be a second
1822
+ * answer to a question with one correct answer — the access gate, the read
1823
+ * hook, the extraction and the not-found message all have to match, and a
1824
+ * copy drifts on the first change to any of them.
1825
+ */
1826
+ const readForTool: ReadForTool = async (branch, p, ctx) => {
1827
+ // The guide's name at the repository root answers with the platform's
1828
+ // guide, which is text the code owns and every agent may read: no gate
1829
+ // and no read hook for it. A file the knowledge base keeps under that
1830
+ // name is ITS OWN conventions page and is read as any file is — gated,
1831
+ // noted — and comes first, with the guide after it. A copy of the
1832
+ // guide an earlier release wrote to disk (still on a draft, say) is
1833
+ // recognised by its header and not served a second time.
1834
+ if (agentGuide && isAgentGuidePath(toKbRelative(p, kbDirName) ?? '')) {
1835
+ return { kind: 'text', text: await guideAt(branch, ctx, p) };
1836
+ }
1837
+ await notifyAgentRead(agentAccessGate, ctx, branch, p);
1838
+ await assertCanRead(readGateFor(branch, ctx), p);
1839
+ const fs = await ctx.getFilesystem(branch);
1840
+ // Reading (extraction, image and binary handling included) happens AFTER
1841
+ // the access gate and the read hook above — a document read is still a
1842
+ // KB read. ONE registry dispatch picks the reader by extension.
1843
+ const bytes = await orNotFound(p, async () => asBytes(await fs.readFile(p)));
1844
+ return readers.readerFor(p).read(bytes, p);
1845
+ };
1846
+
1656
1847
  mount({
1657
1848
  name: 'read_file',
1658
1849
  gated: true,
@@ -1689,16 +1880,13 @@ export function registerWorkspaceTools(
1689
1880
  if (spillStore.isSpillRef(p)) {
1690
1881
  return { path: p, content: await spillStore.read(p, offset, limit) };
1691
1882
  }
1692
- await notifyAgentRead(agentAccessGate, ctx, a.branch as string, p);
1693
- await assertCanRead(readGateFor(a.branch as string, ctx), p);
1694
- const fs = await ctx.getFilesystem(a.branch as string);
1695
- // Reading (extraction, image and binary handling included) happens AFTER
1696
- // the access gate and the read hook above — a document read is still a
1697
- // KB read. ONE registry dispatch picks the reader by
1698
- // extension; everything below just maps its ReadResult onto the tool's
1699
- // result shape.
1700
- const bytes = await orNotFound(p, async () => asBytes(await fs.readFile(p)));
1701
- const result = await readers.readerFor(p).read(bytes, p);
1883
+ const slice = (content: string): string => {
1884
+ const start = offset && offset > 0 ? offset : 0;
1885
+ return offset !== undefined || limit !== undefined
1886
+ ? content.slice(start, limit !== undefined ? start + limit : undefined)
1887
+ : content;
1888
+ };
1889
+ const result = await readForTool(a.branch as string, p, ctx);
1702
1890
  // Images return the picture itself as an MCP image content block, so a
1703
1891
  // multimodal model SEES it. The handler returns the `McpImageResult`
1704
1892
  // sentinel; the MCP result shaping (`toCallToolResult` in
@@ -1717,14 +1905,105 @@ export function registerWorkspaceTools(
1717
1905
  // document, unreadable binary, oversized image) IS the file's honest
1718
1906
  // textual answer, sliced like any other content.
1719
1907
  const content = result.kind === 'text' ? result.text : result.message;
1720
- const start = offset && offset > 0 ? offset : 0;
1721
- const sliced = offset !== undefined || limit !== undefined
1722
- ? content.slice(start, limit !== undefined ? start + limit : undefined)
1723
- : content;
1724
- return { path: p, content: sliced };
1908
+ return { path: p, content: slice(content) };
1725
1909
  },
1726
1910
  });
1727
1911
 
1912
+ /**
1913
+ * The knowledge base's OWN file at the guide's path, read as any file is —
1914
+ * through the read hook and the access gate — or null when there is none,
1915
+ * when the caller MAY NOT READ IT, or when what is there is a copy of the
1916
+ * platform's guide an earlier release wrote (recognised by its header),
1917
+ * which the guide served beside it would only repeat. A file that is not
1918
+ * text (a binary squatting the name) is read for what it is: its honest
1919
+ * textual answer.
1920
+ *
1921
+ * A file the caller may not read answers EXACTLY as no file does: the guide
1922
+ * alone, with nothing said. A refusal here would tell a caller the root
1923
+ * denies that a conventions file exists, which is the one thing the
1924
+ * platform never tells about a file someone may not read — a restricted
1925
+ * node is indistinguishable from an absent one on every other read.
1926
+ */
1927
+ /** What a read of the guide's path answers: the guide, after the knowledge base's own readable file when it has one. */
1928
+ const guideAt = async (branch: string, ctx: ToolContext, p: string): Promise<string> => {
1929
+ const guide = await agentGuide!();
1930
+ const own = await ownGuideFile(branch, ctx, p);
1931
+ return own === null ? guide : withPlatformGuideAppended(own, guide);
1932
+ };
1933
+
1934
+ const ownGuideFile = async (branch: string, ctx: ToolContext, p: string): Promise<string | null> => {
1935
+ const fs = await ctx.getFilesystem(branch);
1936
+ // Existence first, then the gate, then the hook and the bytes — nothing of
1937
+ // theirs is read, or noted as read, before they are allowed to read it.
1938
+ if (!(await ownGuideReadable(fs, branch, ctx, p))) return null;
1939
+ await notifyAgentRead(agentAccessGate, ctx, branch, p);
1940
+ // Gone between the probe and the read — a concurrent delete — is the
1941
+ // absent case: the guide alone, as a read a moment later would answer.
1942
+ const bytes = await fs.readFile(p).then(asBytes, (err: unknown) => {
1943
+ if (isAbsence(err)) return null;
1944
+ throw err;
1945
+ });
1946
+ if (bytes === null) return null;
1947
+ const result = await readers.readerFor(p).read(bytes, p);
1948
+ const text = result.kind === 'text' ? result.text : result.kind === 'image' ? result.note : result.message;
1949
+ return isManagedGuide(text) ? null : text;
1950
+ };
1951
+
1952
+ /**
1953
+ * Whether there is a file of the knowledge base's own at the guide's path
1954
+ * that THIS caller may read. False for nothing there and for a file the
1955
+ * access rules close to them, on purpose and without distinction (see
1956
+ * {@link ownGuideFile}).
1957
+ */
1958
+ const ownGuideReadable = async (fs: LocalFilesystem, branch: string, ctx: ToolContext, p: string): Promise<boolean> => {
1959
+ // The permission verdict BEFORE the filesystem is asked anything, as on
1960
+ // every other read: a caller the rules close the path to learns nothing
1961
+ // from it — not that something is there, and not what the filesystem
1962
+ // says about an entry it cannot stat.
1963
+ const gate = readGateFor(branch, ctx);
1964
+ const rel = toKbRelative(p, gate.kbDirName);
1965
+ if (rel !== null && !(await gate.accessControl.canRead(gate.workspaceId, gate.userEmail, rel))) return false;
1966
+ return existsAt(fs, p);
1967
+ };
1968
+
1969
+ /** Whether something is at `p` on `fs` — absence is false, any other failure is thrown. */
1970
+ const existsAt = async (fs: LocalFilesystem, p: string): Promise<boolean> =>
1971
+ fs.stat(p).then(
1972
+ () => true,
1973
+ (err: unknown) => {
1974
+ if (isAbsence(err)) return false;
1975
+ throw err;
1976
+ },
1977
+ );
1978
+
1979
+ /**
1980
+ * Whether what is at `p` is an entry of the knowledge base's own for the
1981
+ * ordinary stat to describe: anything there except a plain file that is a
1982
+ * copy of the guide an earlier release wrote (recognised by its header).
1983
+ * A folder at the guide's name is theirs and is never read — reading a
1984
+ * folder is an error, not an absence. Nothing there, at the stat or at the
1985
+ * read a moment later (a concurrent delete), is the absent case, which the
1986
+ * caller answers with the guide.
1987
+ */
1988
+ const isOwnEntryStill = async (fs: LocalFilesystem, p: string): Promise<boolean> => {
1989
+ const type = await fs.stat(p).then(
1990
+ (st) => st.type,
1991
+ (err: unknown) => {
1992
+ if (isAbsence(err)) return undefined;
1993
+ throw err;
1994
+ },
1995
+ );
1996
+ if (type === undefined) return false;
1997
+ if (type !== 'file') return true;
1998
+ const bytes = await fs.readFile(p).then(asBytes, (err: unknown) => {
1999
+ if (isAbsence(err)) return null;
2000
+ throw err;
2001
+ });
2002
+ if (bytes === null) return false;
2003
+ const result = await readers.readerFor(p).read(bytes, p);
2004
+ return !(result.kind === 'text' && isManagedGuide(result.text));
2005
+ };
2006
+
1728
2007
  mount({
1729
2008
  name: 'list_files',
1730
2009
  gated: true,
@@ -1780,8 +2059,8 @@ export function registerWorkspaceTools(
1780
2059
  'never move or delete, judged like the dry runs (on a draft branch writes are not gated); `movable` judges the SOURCE ' +
1781
2060
  'side only, so the destination still wants a `move_file` dry run. ' +
1782
2061
  'For a folder, `descendants` counts the files under it at any depth; counting stops at 10000 and ' +
1783
- '`descendantsTruncated` says so, past which `movable` and `deletable` are false — a folder that large was not judged ' +
1784
- 'in full, so run the `move_file` or `delete_folder` dry run for the real verdict.',
2062
+ '`descendantsTruncated` says so, past which `movable` and `deletable` are false (not judged in full): run the ' +
2063
+ '`move_file` or `delete_folder` dry run for the real verdict.',
1785
2064
  inputs: {
1786
2065
  type: 'object',
1787
2066
  properties: {
@@ -1849,6 +2128,11 @@ export function registerWorkspaceTools(
1849
2128
  },
1850
2129
  textEditable: { type: 'boolean', description: 'Files only: whether write_file/write_files/edit_file accept this file as it is now.' },
1851
2130
  mimeNote: str('Present when `mimeSource` is `fallback`: says the MIME type is a fallback, not a detected type.'),
2131
+ platformGuide: {
2132
+ type: 'boolean',
2133
+ description:
2134
+ "True at the agent guide's name in the repository root when the knowledge base has no file of its own there: what read_file answers is the platform's guide, which is not on disk and cannot be written, moved or deleted.",
2135
+ },
1852
2136
  },
1853
2137
  required: ['managed', 'movable', 'deletable', 'access'],
1854
2138
  additionalProperties: true,
@@ -1857,7 +2141,48 @@ export function registerWorkspaceTools(
1857
2141
  handler: async (a, ctx: ToolContext) => {
1858
2142
  const p = a.path as string;
1859
2143
  const branch = a.branch as string;
1860
- await notifyAgentRead(agentAccessGate, ctx, branch, p);
2144
+ // The guide's name with no file of the knowledge base's own under it —
2145
+ // or one the caller may not read, or a copy of the guide an earlier
2146
+ // release wrote, none of which `read_file` serves: what a read answers
2147
+ // there is the platform's guide, so stat says a text file is there to
2148
+ // read — ungated, like the read — and that nothing can be moved,
2149
+ // deleted or written at it through these tools. The three cases get
2150
+ // ONE answer on purpose: a different one for the file the caller may
2151
+ // not read would tell them it exists. A file of the knowledge base's
2152
+ // own that the caller may read is a file like any other, and the
2153
+ // ordinary answer below describes it.
2154
+ let noted = false;
2155
+ if (agentGuide && isAgentGuidePath(toKbRelative(p, kbDirName) ?? '')) {
2156
+ const fs = await ctx.getFilesystem(branch);
2157
+ const readable = await ownGuideReadable(fs, branch, ctx, p);
2158
+ // Telling the organisation's own file from a stale copy READS it, so
2159
+ // the read hook hears of it as it hears of a read_file there — after
2160
+ // the gate, never before, and once (the ordinary stat below is told).
2161
+ if (readable) {
2162
+ await notifyAgentRead(agentAccessGate, ctx, branch, p);
2163
+ noted = true;
2164
+ }
2165
+ const own = readable && (await isOwnEntryStill(fs, p));
2166
+ if (!own) {
2167
+ const guide = await agentGuide();
2168
+ return {
2169
+ name: p.slice(p.lastIndexOf('/') + 1),
2170
+ type: 'file',
2171
+ size: Buffer.byteLength(guide, 'utf8'),
2172
+ managed: true,
2173
+ movable: false,
2174
+ deletable: false,
2175
+ access: { read: true, write: false, download: false, owner: false },
2176
+ contentMode: 'text',
2177
+ kind: 'text',
2178
+ mime: 'text/markdown',
2179
+ mimeSource: 'extension',
2180
+ textEditable: false,
2181
+ platformGuide: true,
2182
+ };
2183
+ }
2184
+ }
2185
+ if (!noted) await notifyAgentRead(agentAccessGate, ctx, branch, p);
1861
2186
  await assertCanRead(readGateFor(branch, ctx), p);
1862
2187
  // Nothing there is a 404, and the placeholder — never content — gets
1863
2188
  // exactly that answer: the one every file tool gives (see not-found.ts).
@@ -1885,7 +2210,11 @@ export function registerWorkspaceTools(
1885
2210
  // `mime` below, so it is never passed through.
1886
2211
  delete stat.mimeType;
1887
2212
  const kind = stat.type === 'directory' ? 'folder' : 'file';
1888
- const managed = managedReason(await onDiskSpelling(root, p), kind) !== undefined;
2213
+ const onDisk = await onDiskSpelling(root, p);
2214
+ const managed = managedReason(onDisk, kind) !== undefined;
2215
+ // A nested `access.md` is managed — it never moves — yet deleted by
2216
+ // whoever may write it, so a FILE's delete is judged on its own rule.
2217
+ const undeletable = kind === 'file' ? fileDeleteRefusal(onDisk) !== undefined : managed;
1889
2218
  const verdicts = await accessAt(branch, ctx, p);
1890
2219
  const access =
1891
2220
  a.explainAccess === true ? { ...verdicts, ...(await explainAccessAt(branch, ctx, p, kind)) } : verdicts;
@@ -1905,7 +2234,7 @@ export function registerWorkspaceTools(
1905
2234
  // `movable`/`deletable` false rather than judging part of a folder
1906
2235
  // and calling it the whole (`delete_folder`'s dry run, which walks
1907
2236
  // uncapped, remains the authority for a folder that large).
1908
- const decided = managed || link;
2237
+ const decided = (managed && undeletable) || link;
1909
2238
  const { files, links, truncated } =
1910
2239
  kind === 'folder'
1911
2240
  ? await filesUnder(fs, p, DESCENDANTS_CAP)
@@ -1927,9 +2256,9 @@ export function registerWorkspaceTools(
1927
2256
  const out: Record<string, unknown> = {
1928
2257
  ...stat,
1929
2258
  managed,
1930
- movable: open,
2259
+ movable: open && !managed,
1931
2260
  // delete_folder also refuses a folder holding a link.
1932
- deletable: open && links.length === 0,
2261
+ deletable: open && !undeletable && links.length === 0,
1933
2262
  access,
1934
2263
  };
1935
2264
  if (kind === 'folder') {
@@ -1960,7 +2289,7 @@ export function registerWorkspaceTools(
1960
2289
  name: 'grep',
1961
2290
  gated: true,
1962
2291
  description:
1963
- 'Regex content search across the workspace. Returns `{ matches: [{ path, line, text }] }` (capped). Use to find where something is defined/referenced. `path` may name a DIRECTORY (searches the subtree) or a single FILE (searches just that file); a path with nothing at it is an error, never an empty result. Searches INSIDE Office and OpenDocument files (.docx/.pptx/.xlsx, .odt/.odp/.ods), PDFs and email files (.eml/.msg) via their extracted text — matches there carry the extraction\'s line numbers, and the `[slide N]`/`[sheet: Name]`/`[page N]`/`[from]`/`[subject]` marker lines locate them; a bounded number of not-yet-extracted documents is extracted per call, and the result notes how many were skipped (re-run to cover them).',
2292
+ 'Regex search across the workspace. Returns `{ matches: [{ path, line, text }] }` (capped). Use to find where something is defined/referenced. `path` may name a DIRECTORY (searches the subtree) or a single FILE (searches just that file); a path with nothing at it is an error, never an empty result. Searches INSIDE Office and OpenDocument files (.docx/.pptx/.xlsx, .odt/.odp/.ods), PDFs and email files (.eml/.msg) via their extracted text — matches there carry the extraction\'s line numbers, and the `[slide N]`/`[sheet: Name]`/`[page N]`/`[from]`/`[subject]` marker lines locate them; a bounded number of not-yet-extracted documents is extracted per call, and the result notes how many were skipped (re-run to cover them).',
1964
2293
  inputs: {
1965
2294
  type: 'object',
1966
2295
  properties: {
@@ -2010,10 +2339,6 @@ export function registerWorkspaceTools(
2010
2339
  // (an empty path is the handler's to explain), and here it would
2011
2340
  // otherwise name the workspace directory by another spelling.
2012
2341
  const searchRoot = typeof a.path === 'string' && a.path.length > 0 ? a.path : kbDirName;
2013
- // The search root itself goes to the read hook here; each file the walk
2014
- // actually opens goes to it per-file below, so a hook sees every path a
2015
- // grep reached rather than only the root it started from.
2016
- await notifyAgentRead(agentAccessGate, ctx, a.branch as string, searchRoot);
2017
2342
  const fs = await ctx.getFilesystem(a.branch as string);
2018
2343
  const gate = readGateFor(a.branch as string, ctx);
2019
2344
  const out: { path: string; line: number; text: string }[] = [];
@@ -2031,7 +2356,51 @@ export function registerWorkspaceTools(
2031
2356
  : await searchRootKind(fs, searchRoot);
2032
2357
  /** Why a single-file search found nothing, when "no matches" would be a lie. */
2033
2358
  let fileNote: string | undefined;
2034
- if (kind === 'directory') {
2359
+ // The guide is searched where it is read: a search of the repository
2360
+ // root covers it, and a search of its own path is a search of what
2361
+ // `read_file` answers there. The composed text is what is searched —
2362
+ // the knowledge base's own readable file first, then the platform's
2363
+ // guide — under the guide's path and with the line numbers a read of
2364
+ // it gives. The walk leaves that one file to this: a match the walk
2365
+ // made there would be the same line again, and one it COUNTED against
2366
+ // `max_results` would be a file later in the tree never searched while
2367
+ // the answer says nothing was cut. A file the caller may not read is
2368
+ // absent from it, as it is from the read.
2369
+ const guidePath = `${kbDirName}/${AGENT_GUIDE_FILE}`;
2370
+ const rel = toKbRelative(searchRoot, kbDirName);
2371
+ const coversGuide =
2372
+ agentGuide !== undefined && (kind === 'directory' ? rel === null : isAgentGuidePath(rel ?? ''));
2373
+ // The search root goes to the read hook — once, and only when it is
2374
+ // repository content: a folder, or a file of the knowledge base's own.
2375
+ // The guide's own path is not told here, because `guideAt` tells the
2376
+ // hook of the organisation's file there exactly as `read_file` does
2377
+ // (after the gate), and the platform's guide alone is nobody's file to
2378
+ // note. Each file the walk opens goes to the hook per file below, so a
2379
+ // hook sees every path a grep reached rather than only its root.
2380
+ if (!(coversGuide && kind !== 'directory')) {
2381
+ await notifyAgentRead(agentAccessGate, ctx, a.branch as string, searchRoot);
2382
+ }
2383
+ if (coversGuide) {
2384
+ if (kind === 'directory') {
2385
+ await grepWalk(
2386
+ fs,
2387
+ searchRoot,
2388
+ re,
2389
+ out,
2390
+ max,
2391
+ 0,
2392
+ gate,
2393
+ (p) => notifyAgentRead(agentAccessGate, ctx, a.branch as string, p),
2394
+ docs,
2395
+ (p) => p === guidePath,
2396
+ );
2397
+ }
2398
+ const lines = (await guideAt(a.branch as string, ctx, guidePath)).split('\n');
2399
+ for (let i = 0; i < lines.length && out.length < max; i++) {
2400
+ re.lastIndex = 0;
2401
+ if (re.test(lines[i]!)) out.push({ path: guidePath, line: i + 1, text: lines[i]!.slice(0, 300) });
2402
+ }
2403
+ } else if (kind === 'directory') {
2035
2404
  await grepWalk(
2036
2405
  fs,
2037
2406
  searchRoot,
@@ -2085,6 +2454,8 @@ export function registerWorkspaceTools(
2085
2454
  // ── writes (through the lock/commit pipeline) ───────────────────────────
2086
2455
  mount({
2087
2456
  name: 'write_file',
2457
+ // A mode that is not one of the three answers `bad_mode`, which lists them.
2458
+ refusesItself: ['mode'],
2088
2459
  gated: true,
2089
2460
  description:
2090
2461
  'Write a workspace TEXT file. The change is committed + pushed as you. Returns `{ path, bytes, outcome }`, where `outcome` is ' +
@@ -2167,6 +2538,8 @@ export function registerWorkspaceTools(
2167
2538
 
2168
2539
  mount({
2169
2540
  name: 'write_files',
2541
+ // A mode that is not one of the three answers `bad_mode`, which lists them.
2542
+ refusesItself: ['mode'],
2170
2543
  gated: true,
2171
2544
  description:
2172
2545
  'Batch-write many files in ONE commit — far faster than calling write_file once per file when ' +
@@ -2228,8 +2601,10 @@ export function registerWorkspaceTools(
2228
2601
  proposable: true,
2229
2602
  handler: async (a, ctx: ToolContext) => {
2230
2603
  const files = (a.files as Array<{ path: string; content: string }>) ?? [];
2231
- if (files.length === 0) return { count: 0, files: [] };
2604
+ // The mode is judged before anything else, so an empty batch with a mode
2605
+ // that is not one answers `bad_mode` like any other call would.
2232
2606
  const mode = modeOf(a);
2607
+ if (files.length === 0) return { count: 0, files: [] };
2233
2608
  // The POLICY gate still judges the whole batch: a restricted run is a
2234
2609
  // call that should not have been made at all, not a per-path outcome.
2235
2610
  // The write hook is asked PER PATH, below, so a path it refuses is that
@@ -2461,10 +2836,8 @@ export function registerWorkspaceTools(
2461
2836
  throw new ToolError(`"${path}" is a folder, not a file — use delete_folder to delete it and the files under it.`, 400);
2462
2837
  }
2463
2838
  const onDisk = await onDiskSpelling(root, path);
2464
- if (isGitMetadata(onDisk)) throw new ToolError(managedReason(onDisk, 'file')!, 400);
2465
- if (managedReason(onDisk, 'file') !== undefined) {
2466
- throw new ToolError(`${onDisk.slice(onDisk.lastIndexOf('/') + 1)} is a platform file and cannot be deleted through the agent tools.`, 400);
2467
- }
2839
+ const refused = fileDeleteRefusal(onDisk);
2840
+ if (refused !== undefined) throw new ToolError(refused, 400);
2468
2841
  await assertNoSymlinkOnPath(root, path, true);
2469
2842
  if ((await writeBlocked(branch, ctx, [path])).length > 0) throw await writeRefusal(branch, path);
2470
2843
  await orNotFound(path, () => fs.deleteFile(path), 'Nothing to delete');
@@ -2650,12 +3023,13 @@ export function registerWorkspaceTools(
2650
3023
  // A plain string again: what refuses a move names the guide, and that is in
2651
3024
  // the shared rules now, which are rebuilt from the layout where they live.
2652
3025
  description:
2653
- 'Move or rename a workspace FILE or FOLDER; a folder moves recursively, with everything under it. `dest` is the full new path, not the folder to move into. Lands as a delete + create, committed + pushed as you. ' +
3026
+ 'Move or rename a workspace FILE or FOLDER; a folder moves recursively, with everything under it. `dest` is the full new path, not the folder to move into. Committed + pushed as you. ' +
2654
3027
  'The destination must not exist — a move never overwrites a file or merges into a folder. Access follows the ' +
2655
3028
  'DESTINATION folder, so a move can change what you (and others) may do with the file: the dry run answers ' +
2656
3029
  '`{ src, dest, kind, descendants, access: { before, after }, accessChanges, allowed, reason? }`, where `access` is your ' +
2657
3030
  'own `{ read, write, download, owner }` at the source and at the destination AS IT WILL BE once the move has landed, ' +
2658
- 'with every `access.md` inside a moved folder counted at its new place, and a move whose `accessChanges` is true wants `confirm: true`.',
3031
+ 'with every `access.md` inside a moved folder counted at its new place, and a move whose `accessChanges` is true wants `confirm: true`. ' +
3032
+ 'Links are rewritten by default, in one commit with the move: those in the moved files and those in other markdown files pointing at them (`links` reports them). `rewriteLinks: false` turns that off. Path mentions in plain prose or code are never changed.',
2659
3033
  inputs: {
2660
3034
  type: 'object',
2661
3035
  properties: {
@@ -2664,6 +3038,7 @@ export function registerWorkspaceTools(
2664
3038
  dest: wsPath(kbDirName, 'Destination path — the full new path; must not exist yet'),
2665
3039
  dryRun: { type: 'boolean', description: 'Answer with the impact and change nothing.' },
2666
3040
  confirm: { type: 'boolean', description: 'Required when the move changes your access. Set it only after a dry run.' },
3041
+ rewriteLinks: { type: 'boolean', description: 'Rewrite the links into, out of and between the moved files (default true). `false` moves without touching any link.' },
2667
3042
  sessionId: SESSION_ID_INPUT,
2668
3043
  },
2669
3044
  required: ['branch', 'src', 'dest'],
@@ -2684,6 +3059,12 @@ export function registerWorkspaceTools(
2684
3059
  confirmationRequired: { type: 'boolean', description: 'True when the call stopped for want of `confirm: true`.' },
2685
3060
  message: str('One sentence on what happened (or did not).'),
2686
3061
  moved: { type: 'boolean', description: 'True once the move landed.' },
3062
+ links: {
3063
+ type: 'object',
3064
+ description:
3065
+ 'The link rewrite, the same on a dry run and on the move: `{ filesEdited, linksRewritten, edits: [{ path, from, to }] (the first 100), ' +
3066
+ 'notRewritten: [{ path, reason, links }], unsearched? }`. Absent with `rewriteLinks: false`.',
3067
+ },
2687
3068
  },
2688
3069
  required: ['src', 'dest', 'moved'],
2689
3070
  },
@@ -2774,6 +3155,56 @@ export function registerWorkspaceTools(
2774
3155
  : occupiedBy !== null
2775
3156
  ? entryExistsMessage(occupiedBy, dest)
2776
3157
  : undefined;
3158
+ // The links are planned only for a move that may run: a refused one
3159
+ // reads no page. The plan is the same on a dry run and on the move.
3160
+ const rewriteLinks = a.rewriteLinks !== false;
3161
+ const linkPlan = rewriteLinks && reason === undefined
3162
+ ? await planMoveLinks({
3163
+ src,
3164
+ dest,
3165
+ branch,
3166
+ kbDirName,
3167
+ allFiles: await (async () => {
3168
+ const { files, links } = await filesUnder(fs, kbDirName);
3169
+ const symlinks = new Set(links);
3170
+ return files.filter((f) => !symlinks.has(f));
3171
+ })(),
3172
+ canRead: (paths) => resolveReadableMap(
3173
+ (wid, email, rels) => accessControl.canReadBatch(wid, email, rels),
3174
+ workspaceIdForBranch(branch),
3175
+ ctx.user.email,
3176
+ kbDirName,
3177
+ paths,
3178
+ ),
3179
+ writeBlocked: (paths) => writeBlocked(branch, ctx, paths),
3180
+ // Through the guarded filesystem the tools read with, never the raw
3181
+ // disk: a page replaced by a link since the listing is refused there
3182
+ // instead of read through to wherever the link points.
3183
+ readText: async (p) => String(await fs.readFile(p, { encoding: 'utf8' })),
3184
+ // The read hook, for every page the answer would NAME — an edited
3185
+ // one, one left with its links listed — and for no page merely
3186
+ // searched: naming is the disclosure, and the hook's refusal makes
3187
+ // the page one the caller cannot read, covered by the one sentence.
3188
+ readRefused: async (path) => {
3189
+ try {
3190
+ await notifyAgentRead(agentAccessGate, ctx, branch, path);
3191
+ return false;
3192
+ } catch {
3193
+ return true;
3194
+ }
3195
+ },
3196
+ // The write hook, for a page the move would edit, at its post-move path.
3197
+ writeRefusal: async (_lockAt, path) => {
3198
+ try {
3199
+ await assertAgentWriteAllowed(agentAccessGate, ctx, branch, path);
3200
+ return null;
3201
+ } catch (err) {
3202
+ return `refused: ${err instanceof Error ? err.message : String(err)}`;
3203
+ }
3204
+ },
3205
+ })
3206
+ : undefined;
3207
+ const moveReason = reason ?? linkPlan?.overCap;
2777
3208
  const impact = {
2778
3209
  src,
2779
3210
  dest,
@@ -2781,8 +3212,9 @@ export function registerWorkspaceTools(
2781
3212
  descendants,
2782
3213
  access: { before, after },
2783
3214
  accessChanges,
2784
- allowed: reason === undefined,
2785
- ...(reason !== undefined ? { reason } : {}),
3215
+ allowed: moveReason === undefined,
3216
+ ...(moveReason !== undefined ? { reason: moveReason } : {}),
3217
+ ...(linkPlan ? { links: linkPlan.report } : {}),
2786
3218
  };
2787
3219
  if (a.dryRun === true) return { ...impact, dryRun: true, moved: false };
2788
3220
  if (managed) throw new ToolError(reason!, 400);
@@ -2794,6 +3226,7 @@ export function registerWorkspaceTools(
2794
3226
  throw await writeRefusal(branch, blocked[0], folderPath ? 'dir' : 'file');
2795
3227
  }
2796
3228
  if (collision) throw new ToolError(reason!, 409);
3229
+ if (linkPlan?.overCap !== undefined) throw new ToolError(linkPlan.overCap, 400);
2797
3230
  if (accessChanges && a.confirm !== true) {
2798
3231
  return {
2799
3232
  ...impact,
@@ -2806,7 +3239,37 @@ export function registerWorkspaceTools(
2806
3239
  // no-replace move is what guarantees it. A destination created between
2807
3240
  // the two — by another agent, or by the sidebar, which moves without
2808
3241
  // taking this lock — comes back here as a refusal, not an overwrite.
2809
- await asEntryExists(() => fs.moveFile(src, dest));
3242
+ if (linkPlan === undefined) {
3243
+ await asEntryExists(() => fs.moveFile(src, dest));
3244
+ } else {
3245
+ // The move and every link edit, one commit. Under the locks, each page
3246
+ // is checked to still hold the bytes its edit was computed from.
3247
+ const edits = linkPlan.edits.map((e) => ({ path: e.path, lockAt: e.lockAt, content: e.content }));
3248
+ const check = async () => {
3249
+ for (const e of linkPlan.edits) {
3250
+ const now = await fs.readFile(e.lockAt, { encoding: 'utf8' }).then(String, () => null);
3251
+ if (now !== e.original) {
3252
+ throw new ToolError(`"${e.lockAt}" changed while the move was being planned, so nothing was moved. Run the move again.`, 409);
3253
+ }
3254
+ }
3255
+ };
3256
+ const summary = `Move ${src} to ${dest}`.slice(0, 200);
3257
+ const locking = fs as LocalFilesystem & { moveWithEdits?: LockingFilesystem['moveWithEdits'] };
3258
+ try {
3259
+ if (typeof locking.moveWithEdits === 'function') {
3260
+ await asEntryExists(() => locking.moveWithEdits!(src, dest, edits, summary, check));
3261
+ } else {
3262
+ // A filesystem without the one-commit move (none the agent is
3263
+ // handed for writing): the move, then each edit.
3264
+ await check();
3265
+ await asEntryExists(() => fs.moveFile(src, dest));
3266
+ for (const e of edits) await fs.writeFile(e.path, e.content);
3267
+ }
3268
+ } catch (err) {
3269
+ if (err instanceof MoveLockedError || err instanceof MoveRacedError) throw new ToolError(err.message, 409);
3270
+ throw err;
3271
+ }
3272
+ }
2810
3273
  // Moving the last file — or a whole folder — out leaves the folder it
2811
3274
  // came from in place, like a delete.
2812
3275
  await keepFolderOf(fs, ctx, branch, src, kbDirName);
@@ -3264,6 +3727,8 @@ export function registerWorkspaceTools(
3264
3727
 
3265
3728
  mount({
3266
3729
  name: 'apply_file_upload',
3730
+ // A mode that is not one of the three answers `bad_mode`, which lists them.
3731
+ refusesItself: ['mode'],
3267
3732
  gated: true,
3268
3733
  description:
3269
3734
  'Land a file you have already uploaded (see `request_file_upload`) in a folder on a branch, in ONE commit, as you. ' +
@@ -3329,6 +3794,177 @@ export function registerWorkspaceTools(
3329
3794
  });
3330
3795
  }
3331
3796
 
3797
+ // ── downloads (bytes that never pass through the model, the other way) ──
3798
+ // The twin of the upload pair: `read_file` answers content INTO the
3799
+ // conversation, so an agent that needs exact bytes on its own disk had no
3800
+ // way to get them. `request_file_download` judges every file on its own,
3801
+ // captures the ones that pass, and answers a one-time link per file (and a
3802
+ // zip per requested folder) that any HTTP client can fetch.
3803
+
3804
+ /** The type a file's link answers with: the reader's, as `file_stat` reports it — but never active content. */
3805
+ const downloadContentType = (p: string, bytes: Buffer): string => {
3806
+ const reader = readers.readerFor(p);
3807
+ const mime = fileTypeOf(reader, p, needsContent(reader) ? bytes : undefined).mime;
3808
+ // SVG and HTML run scripts wherever they are opened — saved to disk and
3809
+ // re-opened under `file://`, too — so they go out as bytes, as the app's
3810
+ // own Download button sends them.
3811
+ return ACTIVE_CONTENT_TYPES.has(mime) ? 'application/octet-stream' : mime;
3812
+ };
3813
+
3814
+ if (downloads) {
3815
+ mount({
3816
+ name: 'request_file_download',
3817
+ gated: true,
3818
+ description:
3819
+ 'Copy knowledge-base files onto your own disk without their content passing through the conversation — the ' +
3820
+ 'way out, as `request_file_upload` is the way in. Give `branch` and `paths` (files or folders). Every file, ' +
3821
+ 'each one inside a folder too, is included only if you may read AND download that file. Returns ' +
3822
+ '`{ expiresAt, expiresInSeconds, files: [{ path, bytes, sha256, downloadUrl }], folders: [{ path, bytes, ' +
3823
+ 'downloadUrl, files }], refused: [{ path, reason }] }`: a link per file, with its own content type, and per ' +
3824
+ `folder a zip at full repository paths (\`apply_file_upload\` it at \`${kbDirName}/\` to put every file back). ` +
3825
+ 'Fetch with any HTTP client (`curl -o <name> "<downloadUrl>"`, or the address without its last segment and an ' +
3826
+ '`x-download-token` header). Each link works ONCE, for 15 minutes, and serves the files as they are now. ' +
3827
+ 'Refused: `not found` (missing or named-but-unreadable; folders omit unreadable files), ' +
3828
+ '`download permission required`; no link when nothing is included. At most 500 MB per request.',
3829
+ inputs: {
3830
+ type: 'object',
3831
+ properties: {
3832
+ branch: BRANCH_INPUT,
3833
+ paths: {
3834
+ type: 'array',
3835
+ minItems: 1,
3836
+ items: { type: 'string' },
3837
+ description: `Files and folders to download, under \`${kbDirName}/\` (e.g. \`${kbDirName}/KnowledgeBase/Foo.md\`), with or without a leading slash — a path without that prefix is placed under \`${kbDirName}/\`.`,
3838
+ },
3839
+ sessionId: SESSION_ID_INPUT,
3840
+ },
3841
+ required: ['branch', 'paths'],
3842
+ additionalProperties: false,
3843
+ },
3844
+ outputs: {
3845
+ type: 'object',
3846
+ properties: {
3847
+ expiresAt: {
3848
+ type: ['string', 'null'],
3849
+ description: 'ISO-8601 instant after which every link of this answer is gone; null when no link was issued.',
3850
+ },
3851
+ expiresInSeconds: int('Seconds until `expiresAt`; 0 when no link was issued.'),
3852
+ files: {
3853
+ type: 'array',
3854
+ description: 'One entry per included file, each named once however many times it was asked for.',
3855
+ items: {
3856
+ type: 'object',
3857
+ properties: {
3858
+ path: str('The workspace path.'),
3859
+ bytes: int('Size in bytes.'),
3860
+ sha256: str('SHA-256 of the bytes the link serves, hex.'),
3861
+ downloadUrl: str('One-time link to the file itself.'),
3862
+ },
3863
+ required: ['path', 'bytes', 'sha256', 'downloadUrl'],
3864
+ },
3865
+ },
3866
+ folders: {
3867
+ type: 'array',
3868
+ description: 'One entry per requested folder that kept at least one file.',
3869
+ items: {
3870
+ type: 'object',
3871
+ properties: {
3872
+ path: str('The folder, as requested.'),
3873
+ bytes: int('Uncompressed total of the files in its zip.'),
3874
+ downloadUrl: str('One-time link to the zip.'),
3875
+ files: { type: 'array', items: { type: 'string' }, description: 'The workspace paths the zip holds.' },
3876
+ },
3877
+ required: ['path', 'bytes', 'downloadUrl', 'files'],
3878
+ },
3879
+ },
3880
+ refused: {
3881
+ type: 'array',
3882
+ description: 'Every path left out, with why.',
3883
+ items: {
3884
+ type: 'object',
3885
+ properties: { path: str('The path left out.'), reason: str('Why: `not found`, `download permission required`, or the deployment\'s own words.') },
3886
+ required: ['path', 'reason'],
3887
+ },
3888
+ },
3889
+ },
3890
+ required: ['expiresAt', 'expiresInSeconds', 'files', 'folders', 'refused'],
3891
+ },
3892
+ // A download is a READ: a read-only deployment still serves it. A
3893
+ // read-only CREDENTIAL may not take bytes out, though.
3894
+ write: false,
3895
+ writeScope: true,
3896
+ handler: async (a, ctx: ToolContext) => requestFileDownload(a, ctx, downloads),
3897
+ });
3898
+ }
3899
+
3900
+ /** `request_file_download`'s handler: judge, capture, and issue the links. */
3901
+ async function requestFileDownload(
3902
+ a: Record<string, unknown>,
3903
+ ctx: ToolContext,
3904
+ store: IAgentDownloadStore,
3905
+ ): Promise<unknown> {
3906
+ const branch = a.branch as string;
3907
+ const raw = a.paths;
3908
+ if (!Array.isArray(raw) || raw.length === 0 || raw.some((p) => typeof p !== 'string' || p.trim() === '')) {
3909
+ throw new ToolError('Name at least one path to download in `paths`: an array of file and folder paths.', 400);
3910
+ }
3911
+ // At the cap, say so before a branch is resolved; the slot below counts again.
3912
+ store.assertCanIssue(ctx.user);
3913
+ // Resolves (clones, if need be) the branch's workspace, as a read does —
3914
+ // so an unknown branch is the call's refusal, not every path's.
3915
+ await ctx.getFilesystem(branch);
3916
+ const workspaceId = workspaceIdForBranch(branch);
3917
+ const refusedSpelling: { path: string; reason: string }[] = [];
3918
+ const requested: string[] = [];
3919
+ for (const p of raw as string[]) {
3920
+ try {
3921
+ // As written: a name may begin or end with a space, and trimming it
3922
+ // would ask for another file. A trailing slash is the one spelling
3923
+ // folded, so `Shared/` and `Shared` are one request, not two zips.
3924
+ requested.push(normalizeWorkspacePath(p, kbDirName).replace(/\/+$/, ''));
3925
+ } catch (err) {
3926
+ if (!hasHttpStatus(err)) throw err;
3927
+ refusedSpelling.push({ path: p, reason: err.message });
3928
+ }
3929
+ }
3930
+ // A slot is taken before anything is built (a caller at the cap is told
3931
+ // so at once) and given back when nothing is issued.
3932
+ return store.withRequestSlot(ctx.user, async (issue) => {
3933
+ const built = await buildDownload(requested, {
3934
+ kbDirName,
3935
+ maxBytes: ZIP_DOWNLOAD_MAX_BYTES,
3936
+ maxFiles: DOWNLOAD_MAX_FILES,
3937
+ candidatesAt: (p) => ctx.workspaceService.downloadCandidatesAt(workspaceId, p),
3938
+ canReadBatch: (paths) => accessControl.canReadBatch(workspaceId, ctx.user.email, paths),
3939
+ canDownloadBatch: (paths) => accessControl.canDownloadBatch(workspaceId, ctx.user.email, paths),
3940
+ // The folder-level gate the app's zip route applies, before the files.
3941
+ canDownloadFolder: (p) =>
3942
+ accessControl.canDownload(workspaceId, ctx.user.email, toKbRelative(p, kbDirName) ?? p),
3943
+ notifyRead: (p) => notifyAgentRead(agentAccessGate, ctx, branch, p),
3944
+ readFile: (p) => ctx.workspaceService.readFileBinary(workspaceId, p),
3945
+ contentTypeOf: downloadContentType,
3946
+ });
3947
+ const refused = [...refusedSpelling, ...built.refused];
3948
+ if (built.artifacts.length === 0) {
3949
+ return { expiresAt: null, expiresInSeconds: 0, files: [], folders: [], refused };
3950
+ }
3951
+ const issued = await issue(built.artifacts);
3952
+ const urls = issued.downloadUrls;
3953
+ return {
3954
+ expiresAt: issued.expiresAt,
3955
+ expiresInSeconds: issued.expiresInSeconds,
3956
+ files: built.files.map((f, i) => ({ ...f, downloadUrl: urls[i]! })),
3957
+ folders: built.folders.map((f, i) => ({
3958
+ path: f.path,
3959
+ bytes: f.bytes,
3960
+ downloadUrl: urls[built.files.length + i]!,
3961
+ files: f.files,
3962
+ })),
3963
+ refused,
3964
+ };
3965
+ });
3966
+ }
3967
+
3332
3968
  // ── shell (internal-only) ───────────────────────────────────────────────
3333
3969
  mount({
3334
3970
  name: 'execute_command',
@@ -3516,4 +4152,7 @@ export function registerWorkspaceTools(
3516
4152
  });
3517
4153
  },
3518
4154
  });
4155
+
4156
+ // The one read `open_page` borrows — see `readForTool` above.
4157
+ return { readForTool };
3519
4158
  }