@bevel-software/platform-core-backend 0.26.0 → 0.27.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (613) hide show
  1. package/agent-guide/access-control.md +17 -0
  2. package/agent-guide/conventions.md +8 -0
  3. package/agent-guide/html-views.md +83 -0
  4. package/agent-guide/new-knowledge-base.md +24 -0
  5. package/agent-guide/tool-manuals.md +10 -0
  6. package/dist/assets.d.ts +19 -0
  7. package/dist/assets.d.ts.map +1 -1
  8. package/dist/assets.js +26 -2
  9. package/dist/assets.js.map +1 -1
  10. package/dist/core/catalog-revision.d.ts +19 -1
  11. package/dist/core/catalog-revision.d.ts.map +1 -1
  12. package/dist/core/catalog-revision.js +14 -3
  13. package/dist/core/catalog-revision.js.map +1 -1
  14. package/dist/core/create-core-server.d.ts +5 -0
  15. package/dist/core/create-core-server.d.ts.map +1 -1
  16. package/dist/core/create-core-server.js +54 -3
  17. package/dist/core/create-core-server.js.map +1 -1
  18. package/dist/core/create-core-services.d.ts +26 -0
  19. package/dist/core/create-core-services.d.ts.map +1 -1
  20. package/dist/core/create-core-services.js +100 -8
  21. package/dist/core/create-core-services.js.map +1 -1
  22. package/dist/core/lifecycle.d.ts +16 -1
  23. package/dist/core/lifecycle.d.ts.map +1 -1
  24. package/dist/core/lifecycle.js +18 -1
  25. package/dist/core/lifecycle.js.map +1 -1
  26. package/dist/core-config.d.ts +21 -0
  27. package/dist/core-config.d.ts.map +1 -1
  28. package/dist/core-config.js +22 -1
  29. package/dist/core-config.js.map +1 -1
  30. package/dist/index.d.ts +1 -1
  31. package/dist/index.d.ts.map +1 -1
  32. package/dist/index.js +2 -2
  33. package/dist/index.js.map +1 -1
  34. package/dist/modules/access/access-control.interface.d.ts +8 -0
  35. package/dist/modules/access/access-control.interface.d.ts.map +1 -1
  36. package/dist/modules/access/access-control.service.d.ts +1 -0
  37. package/dist/modules/access/access-control.service.d.ts.map +1 -1
  38. package/dist/modules/access/access-control.service.js +11 -0
  39. package/dist/modules/access/access-control.service.js.map +1 -1
  40. package/dist/modules/access/directory-sync-bot.js +1 -1
  41. package/dist/modules/access/directory-sync-bot.js.map +1 -1
  42. package/dist/modules/agent-guide/agent-guide.d.ts +6 -0
  43. package/dist/modules/agent-guide/agent-guide.d.ts.map +1 -1
  44. package/dist/modules/agent-guide/agent-guide.js +8 -0
  45. package/dist/modules/agent-guide/agent-guide.js.map +1 -1
  46. package/dist/modules/agent-guide/index.d.ts +1 -1
  47. package/dist/modules/agent-guide/index.d.ts.map +1 -1
  48. package/dist/modules/agent-guide/index.js +1 -1
  49. package/dist/modules/agent-guide/index.js.map +1 -1
  50. package/dist/modules/agent-instructions/compose.d.ts +5 -0
  51. package/dist/modules/agent-instructions/compose.d.ts.map +1 -1
  52. package/dist/modules/agent-instructions/compose.js +19 -2
  53. package/dist/modules/agent-instructions/compose.js.map +1 -1
  54. package/dist/modules/agent-instructions/shared-file-rules.js +19 -19
  55. package/dist/modules/agent-instructions/shared-file-rules.js.map +1 -1
  56. package/dist/modules/audit/agent-audit.service.d.ts +21 -2
  57. package/dist/modules/audit/agent-audit.service.d.ts.map +1 -1
  58. package/dist/modules/audit/agent-audit.service.js +43 -2
  59. package/dist/modules/audit/agent-audit.service.js.map +1 -1
  60. package/dist/modules/audit/agent-connection.routes.d.ts +34 -0
  61. package/dist/modules/audit/agent-connection.routes.d.ts.map +1 -0
  62. package/dist/modules/audit/agent-connection.routes.js +38 -0
  63. package/dist/modules/audit/agent-connection.routes.js.map +1 -0
  64. package/dist/modules/audit/audit.contract.d.ts +24 -0
  65. package/dist/modules/audit/audit.contract.d.ts.map +1 -1
  66. package/dist/modules/audit/audit.contract.js.map +1 -1
  67. package/dist/modules/audit/index.d.ts +1 -0
  68. package/dist/modules/audit/index.d.ts.map +1 -1
  69. package/dist/modules/audit/index.js +1 -0
  70. package/dist/modules/audit/index.js.map +1 -1
  71. package/dist/modules/auth/account.routes.d.ts.map +1 -1
  72. package/dist/modules/auth/account.routes.js +10 -3
  73. package/dist/modules/auth/account.routes.js.map +1 -1
  74. package/dist/modules/auth/auth.middleware.d.ts +7 -0
  75. package/dist/modules/auth/auth.middleware.d.ts.map +1 -1
  76. package/dist/modules/auth/auth.middleware.js +1 -1
  77. package/dist/modules/auth/auth.middleware.js.map +1 -1
  78. package/dist/modules/code-mode/code-mode.tool.d.ts.map +1 -1
  79. package/dist/modules/code-mode/code-mode.tool.js +11 -3
  80. package/dist/modules/code-mode/code-mode.tool.js.map +1 -1
  81. package/dist/modules/database/core-schema.d.ts +96 -0
  82. package/dist/modules/database/core-schema.d.ts.map +1 -1
  83. package/dist/modules/database/core-schema.js +32 -3
  84. package/dist/modules/database/core-schema.js.map +1 -1
  85. package/dist/modules/embed/account-link.service.d.ts +42 -0
  86. package/dist/modules/embed/account-link.service.d.ts.map +1 -0
  87. package/dist/modules/embed/account-link.service.js +50 -0
  88. package/dist/modules/embed/account-link.service.js.map +1 -0
  89. package/dist/modules/embed/embed-link.d.ts +58 -0
  90. package/dist/modules/embed/embed-link.d.ts.map +1 -0
  91. package/dist/modules/embed/embed-link.js +160 -0
  92. package/dist/modules/embed/embed-link.js.map +1 -0
  93. package/dist/modules/embed/embed.errors.d.ts +30 -0
  94. package/dist/modules/embed/embed.errors.d.ts.map +1 -0
  95. package/dist/modules/embed/embed.errors.js +42 -0
  96. package/dist/modules/embed/embed.errors.js.map +1 -0
  97. package/dist/modules/embed/embed.interface.d.ts +204 -0
  98. package/dist/modules/embed/embed.interface.d.ts.map +1 -0
  99. package/dist/modules/embed/embed.interface.js +2 -0
  100. package/dist/modules/embed/embed.interface.js.map +1 -0
  101. package/dist/modules/embed/embed.routes.d.ts +59 -0
  102. package/dist/modules/embed/embed.routes.d.ts.map +1 -0
  103. package/dist/modules/embed/embed.routes.js +343 -0
  104. package/dist/modules/embed/embed.routes.js.map +1 -0
  105. package/dist/modules/embed/embed.service.d.ts +169 -0
  106. package/dist/modules/embed/embed.service.d.ts.map +1 -0
  107. package/dist/modules/embed/embed.service.js +627 -0
  108. package/dist/modules/embed/embed.service.js.map +1 -0
  109. package/dist/modules/embed/embed.tools.d.ts +51 -0
  110. package/dist/modules/embed/embed.tools.d.ts.map +1 -0
  111. package/dist/modules/embed/embed.tools.js +170 -0
  112. package/dist/modules/embed/embed.tools.js.map +1 -0
  113. package/dist/modules/embed/mcp-app.d.ts +64 -0
  114. package/dist/modules/embed/mcp-app.d.ts.map +1 -0
  115. package/dist/modules/embed/mcp-app.js +147 -0
  116. package/dist/modules/embed/mcp-app.js.map +1 -0
  117. package/dist/modules/embed/mcp-app.routes.d.ts +21 -0
  118. package/dist/modules/embed/mcp-app.routes.d.ts.map +1 -0
  119. package/dist/modules/embed/mcp-app.routes.js +36 -0
  120. package/dist/modules/embed/mcp-app.routes.js.map +1 -0
  121. package/dist/modules/github-app/github-app.connection.d.ts +15 -0
  122. package/dist/modules/github-app/github-app.connection.d.ts.map +1 -1
  123. package/dist/modules/github-app/github-app.connection.js +34 -3
  124. package/dist/modules/github-app/github-app.connection.js.map +1 -1
  125. package/dist/modules/kb-fs/locking-filesystem.d.ts +65 -0
  126. package/dist/modules/kb-fs/locking-filesystem.d.ts.map +1 -1
  127. package/dist/modules/kb-fs/locking-filesystem.js +300 -0
  128. package/dist/modules/kb-fs/locking-filesystem.js.map +1 -1
  129. package/dist/modules/kb-sync/kb-sync.interface.d.ts +5 -0
  130. package/dist/modules/kb-sync/kb-sync.interface.d.ts.map +1 -1
  131. package/dist/modules/kb-sync/kb-sync.service.d.ts.map +1 -1
  132. package/dist/modules/kb-sync/kb-sync.service.js +6 -0
  133. package/dist/modules/kb-sync/kb-sync.service.js.map +1 -1
  134. package/dist/modules/mcp/mcp-auth.middleware.d.ts.map +1 -1
  135. package/dist/modules/mcp/mcp-auth.middleware.js +2 -1
  136. package/dist/modules/mcp/mcp-auth.middleware.js.map +1 -1
  137. package/dist/modules/mcp/mcp.service.d.ts +28 -1
  138. package/dist/modules/mcp/mcp.service.d.ts.map +1 -1
  139. package/dist/modules/mcp/mcp.service.js +64 -8
  140. package/dist/modules/mcp/mcp.service.js.map +1 -1
  141. package/dist/modules/mcp/oauth/bevel-oauth-provider.d.ts +24 -0
  142. package/dist/modules/mcp/oauth/bevel-oauth-provider.d.ts.map +1 -1
  143. package/dist/modules/mcp/oauth/bevel-oauth-provider.js +32 -4
  144. package/dist/modules/mcp/oauth/bevel-oauth-provider.js.map +1 -1
  145. package/dist/modules/onboarding/onboarding.contract.d.ts +80 -0
  146. package/dist/modules/onboarding/onboarding.contract.d.ts.map +1 -0
  147. package/dist/modules/onboarding/onboarding.contract.js +7 -0
  148. package/dist/modules/onboarding/onboarding.contract.js.map +1 -0
  149. package/dist/modules/onboarding/onboarding.routes.d.ts +18 -0
  150. package/dist/modules/onboarding/onboarding.routes.d.ts.map +1 -0
  151. package/dist/modules/onboarding/onboarding.routes.js +81 -0
  152. package/dist/modules/onboarding/onboarding.routes.js.map +1 -0
  153. package/dist/modules/onboarding/starter-pack.service.d.ts +213 -0
  154. package/dist/modules/onboarding/starter-pack.service.d.ts.map +1 -0
  155. package/dist/modules/onboarding/starter-pack.service.js +492 -0
  156. package/dist/modules/onboarding/starter-pack.service.js.map +1 -0
  157. package/dist/modules/onboarding/starter-packs.d.ts +78 -0
  158. package/dist/modules/onboarding/starter-packs.d.ts.map +1 -0
  159. package/dist/modules/onboarding/starter-packs.js +156 -0
  160. package/dist/modules/onboarding/starter-packs.js.map +1 -0
  161. package/dist/modules/plugins/index.d.ts +1 -1
  162. package/dist/modules/plugins/index.d.ts.map +1 -1
  163. package/dist/modules/plugins/index.js +1 -1
  164. package/dist/modules/plugins/index.js.map +1 -1
  165. package/dist/modules/plugins/plugin-provision.service.d.ts +19 -0
  166. package/dist/modules/plugins/plugin-provision.service.d.ts.map +1 -1
  167. package/dist/modules/plugins/plugin-provision.service.js +18 -1
  168. package/dist/modules/plugins/plugin-provision.service.js.map +1 -1
  169. package/dist/modules/plugins/plugins.routes.d.ts +0 -36
  170. package/dist/modules/plugins/plugins.routes.d.ts.map +1 -1
  171. package/dist/modules/plugins/plugins.routes.js +31 -0
  172. package/dist/modules/plugins/plugins.routes.js.map +1 -1
  173. package/dist/modules/settings/deployment-settings.service.d.ts +34 -0
  174. package/dist/modules/settings/deployment-settings.service.d.ts.map +1 -1
  175. package/dist/modules/settings/deployment-settings.service.js +134 -1
  176. package/dist/modules/settings/deployment-settings.service.js.map +1 -1
  177. package/dist/modules/settings/repository-source.d.ts +1 -0
  178. package/dist/modules/settings/repository-source.d.ts.map +1 -1
  179. package/dist/modules/settings/repository-source.js +3 -3
  180. package/dist/modules/settings/repository-source.js.map +1 -1
  181. package/dist/modules/skills/skills.tools.d.ts.map +1 -1
  182. package/dist/modules/skills/skills.tools.js +50 -32
  183. package/dist/modules/skills/skills.tools.js.map +1 -1
  184. package/dist/modules/tool-auth/connection-key-rejection.d.ts +10 -1
  185. package/dist/modules/tool-auth/connection-key-rejection.d.ts.map +1 -1
  186. package/dist/modules/tool-auth/connection-key-rejection.js +11 -3
  187. package/dist/modules/tool-auth/connection-key-rejection.js.map +1 -1
  188. package/dist/modules/tool-auth/external-api-key.interface.d.ts +8 -0
  189. package/dist/modules/tool-auth/external-api-key.interface.d.ts.map +1 -1
  190. package/dist/modules/tool-auth/external-api-key.service.d.ts +24 -2
  191. package/dist/modules/tool-auth/external-api-key.service.d.ts.map +1 -1
  192. package/dist/modules/tool-auth/external-api-key.service.js +59 -2
  193. package/dist/modules/tool-auth/external-api-key.service.js.map +1 -1
  194. package/dist/modules/tool-auth/tool-auth.middleware.d.ts.map +1 -1
  195. package/dist/modules/tool-auth/tool-auth.middleware.js +9 -4
  196. package/dist/modules/tool-auth/tool-auth.middleware.js.map +1 -1
  197. package/dist/modules/tool-helpers/index.d.ts +28 -1
  198. package/dist/modules/tool-helpers/index.d.ts.map +1 -1
  199. package/dist/modules/tool-helpers/index.js +28 -1
  200. package/dist/modules/tool-helpers/index.js.map +1 -1
  201. package/dist/modules/tool-helpers/route-argument-check.d.ts +10 -0
  202. package/dist/modules/tool-helpers/route-argument-check.d.ts.map +1 -0
  203. package/dist/modules/tool-helpers/route-argument-check.js +132 -0
  204. package/dist/modules/tool-helpers/route-argument-check.js.map +1 -0
  205. package/dist/modules/tool-helpers/route-tool-schemas.d.ts +89 -0
  206. package/dist/modules/tool-helpers/route-tool-schemas.d.ts.map +1 -0
  207. package/dist/modules/tool-helpers/route-tool-schemas.js +113 -0
  208. package/dist/modules/tool-helpers/route-tool-schemas.js.map +1 -0
  209. package/dist/modules/tool-helpers/tool-def.d.ts +71 -0
  210. package/dist/modules/tool-helpers/tool-def.d.ts.map +1 -1
  211. package/dist/modules/tool-helpers/tool-def.js +70 -6
  212. package/dist/modules/tool-helpers/tool-def.js.map +1 -1
  213. package/dist/modules/tool-helpers/tool-handler.d.ts +46 -6
  214. package/dist/modules/tool-helpers/tool-handler.d.ts.map +1 -1
  215. package/dist/modules/tool-helpers/tool-handler.js +157 -8
  216. package/dist/modules/tool-helpers/tool-handler.js.map +1 -1
  217. package/dist/modules/tool-helpers/tool.contract.d.ts +9 -0
  218. package/dist/modules/tool-helpers/tool.contract.d.ts.map +1 -1
  219. package/dist/modules/tool-helpers/tool.contract.js.map +1 -1
  220. package/dist/modules/tool-manuals/tool-manuals.tools.d.ts.map +1 -1
  221. package/dist/modules/tool-manuals/tool-manuals.tools.js +14 -5
  222. package/dist/modules/tool-manuals/tool-manuals.tools.js.map +1 -1
  223. package/dist/modules/tool-registry/description-length.d.ts +6 -2
  224. package/dist/modules/tool-registry/description-length.d.ts.map +1 -1
  225. package/dist/modules/tool-registry/description-length.js +27 -5
  226. package/dist/modules/tool-registry/description-length.js.map +1 -1
  227. package/dist/modules/tool-registry/guide-first.d.ts +4 -0
  228. package/dist/modules/tool-registry/guide-first.d.ts.map +1 -1
  229. package/dist/modules/tool-registry/guide-first.js +9 -1
  230. package/dist/modules/tool-registry/guide-first.js.map +1 -1
  231. package/dist/modules/workflow/agent-tools/change-request-read.tools.d.ts +53 -0
  232. package/dist/modules/workflow/agent-tools/change-request-read.tools.d.ts.map +1 -1
  233. package/dist/modules/workflow/agent-tools/change-request-read.tools.js +97 -68
  234. package/dist/modules/workflow/agent-tools/change-request-read.tools.js.map +1 -1
  235. package/dist/modules/workflow/agent-tools/workflow.tools.d.ts +8 -1
  236. package/dist/modules/workflow/agent-tools/workflow.tools.d.ts.map +1 -1
  237. package/dist/modules/workflow/agent-tools/workflow.tools.js +124 -37
  238. package/dist/modules/workflow/agent-tools/workflow.tools.js.map +1 -1
  239. package/dist/modules/workflow/file-lock.service.d.ts +29 -5
  240. package/dist/modules/workflow/file-lock.service.d.ts.map +1 -1
  241. package/dist/modules/workflow/file-lock.service.js +62 -5
  242. package/dist/modules/workflow/file-lock.service.js.map +1 -1
  243. package/dist/modules/workflow/git/git.service.d.ts +72 -1
  244. package/dist/modules/workflow/git/git.service.d.ts.map +1 -1
  245. package/dist/modules/workflow/git/git.service.js +172 -36
  246. package/dist/modules/workflow/git/git.service.js.map +1 -1
  247. package/dist/modules/workflow/git/node-git-runner.d.ts +15 -0
  248. package/dist/modules/workflow/git/node-git-runner.d.ts.map +1 -1
  249. package/dist/modules/workflow/git/node-git-runner.js +78 -6
  250. package/dist/modules/workflow/git/node-git-runner.js.map +1 -1
  251. package/dist/modules/workflow/pending-commits.service.d.ts +15 -0
  252. package/dist/modules/workflow/pending-commits.service.d.ts.map +1 -1
  253. package/dist/modules/workflow/pending-commits.service.js +25 -1
  254. package/dist/modules/workflow/pending-commits.service.js.map +1 -1
  255. package/dist/modules/workflow/pending-commits.worker.d.ts +6 -1
  256. package/dist/modules/workflow/pending-commits.worker.d.ts.map +1 -1
  257. package/dist/modules/workflow/pending-commits.worker.js +23 -6
  258. package/dist/modules/workflow/pending-commits.worker.js.map +1 -1
  259. package/dist/modules/workflow/recovery-bot.js +1 -1
  260. package/dist/modules/workflow/recovery-bot.js.map +1 -1
  261. package/dist/modules/workflow/review-workflow/review-workflow.service.d.ts.map +1 -1
  262. package/dist/modules/workflow/review-workflow/review-workflow.service.js +2 -1
  263. package/dist/modules/workflow/review-workflow/review-workflow.service.js.map +1 -1
  264. package/dist/modules/workflow/sanitize-error.d.ts +16 -0
  265. package/dist/modules/workflow/sanitize-error.d.ts.map +1 -1
  266. package/dist/modules/workflow/sanitize-error.js +45 -0
  267. package/dist/modules/workflow/sanitize-error.js.map +1 -1
  268. package/dist/modules/workflow/workflow.service.d.ts +183 -15
  269. package/dist/modules/workflow/workflow.service.d.ts.map +1 -1
  270. package/dist/modules/workflow/workflow.service.js +805 -106
  271. package/dist/modules/workflow/workflow.service.js.map +1 -1
  272. package/dist/modules/workspace/agent-download.builder.d.ts +105 -0
  273. package/dist/modules/workspace/agent-download.builder.d.ts.map +1 -0
  274. package/dist/modules/workspace/agent-download.builder.js +238 -0
  275. package/dist/modules/workspace/agent-download.builder.js.map +1 -0
  276. package/dist/modules/workspace/agent-download.routes.d.ts +77 -0
  277. package/dist/modules/workspace/agent-download.routes.d.ts.map +1 -0
  278. package/dist/modules/workspace/agent-download.routes.js +156 -0
  279. package/dist/modules/workspace/agent-download.routes.js.map +1 -0
  280. package/dist/modules/workspace/agent-download.store.d.ts +225 -0
  281. package/dist/modules/workspace/agent-download.store.d.ts.map +1 -0
  282. package/dist/modules/workspace/agent-download.store.js +423 -0
  283. package/dist/modules/workspace/agent-download.store.js.map +1 -0
  284. package/dist/modules/workspace/agent-upload.store.d.ts +10 -1
  285. package/dist/modules/workspace/agent-upload.store.d.ts.map +1 -1
  286. package/dist/modules/workspace/agent-upload.store.js +16 -6
  287. package/dist/modules/workspace/agent-upload.store.js.map +1 -1
  288. package/dist/modules/workspace/file-readers/raw-mime.d.ts +6 -0
  289. package/dist/modules/workspace/file-readers/raw-mime.d.ts.map +1 -0
  290. package/dist/modules/workspace/file-readers/raw-mime.js +38 -0
  291. package/dist/modules/workspace/file-readers/raw-mime.js.map +1 -0
  292. package/dist/modules/workspace/file-readers/text-reader.d.ts +5 -5
  293. package/dist/modules/workspace/file-readers/text-reader.d.ts.map +1 -1
  294. package/dist/modules/workspace/file-readers/text-reader.js +5 -8
  295. package/dist/modules/workspace/file-readers/text-reader.js.map +1 -1
  296. package/dist/modules/workspace/first-run.d.ts +111 -0
  297. package/dist/modules/workspace/first-run.d.ts.map +1 -0
  298. package/dist/modules/workspace/first-run.js +181 -0
  299. package/dist/modules/workspace/first-run.js.map +1 -0
  300. package/dist/modules/workspace/move-links.d.ts +92 -0
  301. package/dist/modules/workspace/move-links.d.ts.map +1 -0
  302. package/dist/modules/workspace/move-links.js +173 -0
  303. package/dist/modules/workspace/move-links.js.map +1 -0
  304. package/dist/modules/workspace/startup/steps/seed-tree.d.ts.map +1 -1
  305. package/dist/modules/workspace/startup/steps/seed-tree.js +3 -13
  306. package/dist/modules/workspace/startup/steps/seed-tree.js.map +1 -1
  307. package/dist/modules/workspace/workspace.routes.d.ts.map +1 -1
  308. package/dist/modules/workspace/workspace.routes.js +63 -22
  309. package/dist/modules/workspace/workspace.routes.js.map +1 -1
  310. package/dist/modules/workspace/workspace.service.d.ts +75 -2
  311. package/dist/modules/workspace/workspace.service.d.ts.map +1 -1
  312. package/dist/modules/workspace/workspace.service.js +197 -24
  313. package/dist/modules/workspace/workspace.service.js.map +1 -1
  314. package/dist/modules/workspace/workspace.tools.d.ts +59 -1
  315. package/dist/modules/workspace/workspace.tools.d.ts.map +1 -1
  316. package/dist/modules/workspace/workspace.tools.js +443 -65
  317. package/dist/modules/workspace/workspace.tools.js.map +1 -1
  318. package/dist/modules/write-access/write-access.d.ts.map +1 -1
  319. package/dist/modules/write-access/write-access.js +9 -0
  320. package/dist/modules/write-access/write-access.js.map +1 -1
  321. package/dist/shared/domain-errors.d.ts +73 -1
  322. package/dist/shared/domain-errors.d.ts.map +1 -1
  323. package/dist/shared/domain-errors.js +86 -12
  324. package/dist/shared/domain-errors.js.map +1 -1
  325. package/dist/shared/git.contract.d.ts +10 -1
  326. package/dist/shared/git.contract.d.ts.map +1 -1
  327. package/dist/shared/git.contract.js.map +1 -1
  328. package/dist/shared/utf8-text.d.ts +15 -0
  329. package/dist/shared/utf8-text.d.ts.map +1 -0
  330. package/dist/shared/utf8-text.js +20 -0
  331. package/dist/shared/utf8-text.js.map +1 -0
  332. package/dist/tenancy/static-tenant-source.d.ts +2 -1
  333. package/dist/tenancy/static-tenant-source.d.ts.map +1 -1
  334. package/dist/tenancy/static-tenant-source.js +9 -2
  335. package/dist/tenancy/static-tenant-source.js.map +1 -1
  336. package/mcp-app/page.html +349 -0
  337. package/migrations/0017_atlassian_account_links.sql +37 -0
  338. package/migrations/meta/0017_snapshot.json +2396 -0
  339. package/migrations/meta/_journal.json +8 -1
  340. package/package.json +5 -3
  341. package/src/__tests__/packaged-assets-ship.test.ts +16 -4
  342. package/src/assets.ts +28 -2
  343. package/src/core/__tests__/catalog-revision.test.ts +40 -0
  344. package/src/core/__tests__/gated-boot.test.ts +1 -0
  345. package/src/core/__tests__/lifecycle.test.ts +14 -0
  346. package/src/core/catalog-revision.ts +15 -2
  347. package/src/core/create-core-server.ts +67 -3
  348. package/src/core/create-core-services.ts +140 -6
  349. package/src/core/lifecycle.ts +28 -2
  350. package/src/core-config.ts +24 -1
  351. package/src/index.ts +2 -2
  352. package/src/modules/access/__tests__/access-control.service.test.ts +19 -0
  353. package/src/modules/access/access-control.interface.ts +13 -0
  354. package/src/modules/access/access-control.service.ts +16 -0
  355. package/src/modules/access/directory-sync-bot.ts +1 -1
  356. package/src/modules/agent-guide/__tests__/agent-guide.test.ts +58 -0
  357. package/src/modules/agent-guide/__tests__/agent-guide.tools.test.ts +19 -1
  358. package/src/modules/agent-guide/agent-guide.ts +9 -0
  359. package/src/modules/agent-guide/index.ts +1 -0
  360. package/src/modules/agent-instructions/__tests__/shared-file-rules.test.ts +14 -0
  361. package/src/modules/agent-instructions/compose.ts +17 -2
  362. package/src/modules/agent-instructions/shared-file-rules.ts +19 -19
  363. package/src/modules/audit/__tests__/agent-audit.service.test.ts +64 -4
  364. package/src/modules/audit/__tests__/agent-connection.routes.test.ts +102 -0
  365. package/src/modules/audit/agent-audit.service.ts +58 -3
  366. package/src/modules/audit/agent-connection.routes.ts +56 -0
  367. package/src/modules/audit/audit.contract.ts +26 -0
  368. package/src/modules/audit/index.ts +1 -0
  369. package/src/modules/auth/__tests__/account.routes.test.ts +5 -3
  370. package/src/modules/auth/account.routes.ts +10 -3
  371. package/src/modules/auth/auth.middleware.ts +1 -1
  372. package/src/modules/code-mode/__tests__/code-mode.tool.test.ts +18 -2
  373. package/src/modules/code-mode/code-mode.tool.ts +13 -2
  374. package/src/modules/database/core-schema.ts +33 -3
  375. package/src/modules/embed/__tests__/account-links.migration.test.ts +98 -0
  376. package/src/modules/embed/__tests__/embed-link.test.ts +122 -0
  377. package/src/modules/embed/__tests__/embed.routes.test.ts +362 -0
  378. package/src/modules/embed/__tests__/embed.service.lock.test.ts +193 -0
  379. package/src/modules/embed/__tests__/embed.service.test.ts +805 -0
  380. package/src/modules/embed/__tests__/embed.tools.test.ts +304 -0
  381. package/src/modules/embed/__tests__/mcp-app.test.ts +207 -0
  382. package/src/modules/embed/account-link.service.ts +76 -0
  383. package/src/modules/embed/embed-link.ts +180 -0
  384. package/src/modules/embed/embed.errors.ts +43 -0
  385. package/src/modules/embed/embed.interface.ts +203 -0
  386. package/src/modules/embed/embed.routes.ts +363 -0
  387. package/src/modules/embed/embed.service.ts +705 -0
  388. package/src/modules/embed/embed.tools.ts +206 -0
  389. package/src/modules/embed/mcp-app.routes.ts +41 -0
  390. package/src/modules/embed/mcp-app.ts +173 -0
  391. package/src/modules/github-app/__tests__/github-app.test.ts +76 -0
  392. package/src/modules/github-app/github-app.connection.ts +38 -4
  393. package/src/modules/kb-fs/__tests__/move-with-edits.test.ts +304 -0
  394. package/src/modules/kb-fs/locking-filesystem.ts +287 -0
  395. package/src/modules/kb-sync/__tests__/kb-sync.service.test.ts +35 -0
  396. package/src/modules/kb-sync/kb-sync.interface.ts +2 -0
  397. package/src/modules/kb-sync/kb-sync.service.ts +6 -0
  398. package/src/modules/mcp/__tests__/bevel-oauth-provider.test.ts +68 -11
  399. package/src/modules/mcp/__tests__/mcp-proxy-helpers.test.ts +37 -2
  400. package/src/modules/mcp/__tests__/mcp.app.e2e.test.ts +200 -0
  401. package/src/modules/mcp/__tests__/mcp.e2e.test.ts +3 -1
  402. package/src/modules/mcp/__tests__/mcp.service.test.ts +23 -13
  403. package/src/modules/mcp/__tests__/tool-call-interface.e2e.test.ts +499 -0
  404. package/src/modules/mcp/mcp-auth.middleware.ts +2 -1
  405. package/src/modules/mcp/mcp.service.ts +81 -12
  406. package/src/modules/mcp/oauth/bevel-oauth-provider.ts +42 -4
  407. package/src/modules/onboarding/__tests__/onboarding.routes.test.ts +163 -0
  408. package/src/modules/onboarding/__tests__/starter-pack.service.test.ts +635 -0
  409. package/src/modules/onboarding/__tests__/starter-packs.test.ts +169 -0
  410. package/src/modules/onboarding/onboarding.contract.ts +85 -0
  411. package/src/modules/onboarding/onboarding.routes.ts +85 -0
  412. package/src/modules/onboarding/starter-pack.service.ts +580 -0
  413. package/src/modules/onboarding/starter-packs.ts +182 -0
  414. package/src/modules/plugins/__tests__/plugins.tools.test.ts +25 -2
  415. package/src/modules/plugins/index.ts +1 -0
  416. package/src/modules/plugins/plugin-provision.service.ts +22 -1
  417. package/src/modules/plugins/plugins.routes.ts +29 -0
  418. package/src/modules/settings/__tests__/deployment-settings.service.test.ts +150 -1
  419. package/src/modules/settings/__tests__/repository-source.test.ts +9 -0
  420. package/src/modules/settings/deployment-settings.service.ts +131 -1
  421. package/src/modules/settings/repository-source.ts +4 -4
  422. package/src/modules/skills/__tests__/skills.tools.description.test.ts +5 -1
  423. package/src/modules/skills/skills.tools.ts +55 -35
  424. package/src/modules/tool-auth/__tests__/connection-key-rejection.test.ts +32 -0
  425. package/src/modules/tool-auth/__tests__/external-api-key.service.test.ts +65 -0
  426. package/src/modules/tool-auth/connection-key-rejection.ts +13 -3
  427. package/src/modules/tool-auth/external-api-key.interface.ts +9 -0
  428. package/src/modules/tool-auth/external-api-key.service.ts +61 -4
  429. package/src/modules/tool-auth/tool-auth.middleware.ts +13 -4
  430. package/src/modules/tool-helpers/__tests__/branch-declaration.test.ts +502 -0
  431. package/src/modules/tool-helpers/__tests__/route-argument-check.test.ts +337 -0
  432. package/src/modules/tool-helpers/index.ts +34 -1
  433. package/src/modules/tool-helpers/route-argument-check.ts +149 -0
  434. package/src/modules/tool-helpers/route-tool-schemas.ts +171 -0
  435. package/src/modules/tool-helpers/tool-def.ts +132 -7
  436. package/src/modules/tool-helpers/tool-handler.ts +196 -8
  437. package/src/modules/tool-helpers/tool.contract.ts +9 -0
  438. package/src/modules/tool-manuals/tool-manuals.tools.ts +15 -5
  439. package/src/modules/tool-registry/__tests__/own-tool-schemas.test.ts +14 -1
  440. package/src/modules/tool-registry/__tests__/tool-description-length.test.ts +23 -10
  441. package/src/modules/tool-registry/description-length.ts +34 -7
  442. package/src/modules/tool-registry/guide-first.ts +8 -1
  443. package/src/modules/workflow/__tests__/file-lock.service.deletion-gate.test.ts +104 -0
  444. package/src/modules/workflow/__tests__/pending-commits.repository-replaced.test.ts +66 -0
  445. package/src/modules/workflow/__tests__/pending-commits.worker.test.ts +31 -2
  446. package/src/modules/workflow/__tests__/preserve-roles-yaml.test.ts +110 -6
  447. package/src/modules/workflow/__tests__/sanitize-error.test.ts +89 -1
  448. package/src/modules/workflow/__tests__/workflow.routes.push-refused.test.ts +202 -0
  449. package/src/modules/workflow/__tests__/workflow.service.branch-in-use.test.ts +6 -6
  450. package/src/modules/workflow/__tests__/workflow.service.delete-branch.test.ts +1026 -0
  451. package/src/modules/workflow/__tests__/workflow.service.facade.test.ts +468 -10
  452. package/src/modules/workflow/__tests__/workflow.service.folder-proposals.test.ts +4 -0
  453. package/src/modules/workflow/__tests__/workflow.service.releaseLock.test.ts +5 -4
  454. package/src/modules/workflow/__tests__/workflow.service.update-from-target.test.ts +54 -2
  455. package/src/modules/workflow/agent-tools/__tests__/change-request-read.tools.test.ts +41 -5
  456. package/src/modules/workflow/agent-tools/__tests__/workflow.tools.test.ts +127 -2
  457. package/src/modules/workflow/agent-tools/change-request-read.tools.ts +157 -122
  458. package/src/modules/workflow/agent-tools/workflow.tools.ts +129 -35
  459. package/src/modules/workflow/file-lock.service.ts +67 -5
  460. package/src/modules/workflow/git/__tests__/git.service.deleteBranch.test.ts +164 -0
  461. package/src/modules/workflow/git/__tests__/git.service.pull.test.ts +48 -1
  462. package/src/modules/workflow/git/__tests__/node-git-runner.test.ts +231 -2
  463. package/src/modules/workflow/git/git.service.ts +176 -38
  464. package/src/modules/workflow/git/node-git-runner.ts +82 -7
  465. package/src/modules/workflow/pending-commits.service.ts +29 -1
  466. package/src/modules/workflow/pending-commits.worker.ts +24 -6
  467. package/src/modules/workflow/recovery-bot.ts +1 -1
  468. package/src/modules/workflow/review-workflow/review-workflow.service.ts +2 -1
  469. package/src/modules/workflow/sanitize-error.ts +50 -0
  470. package/src/modules/workflow/workflow.service.ts +879 -112
  471. package/src/modules/workspace/__tests__/agent-downloads.test.ts +823 -0
  472. package/src/modules/workspace/__tests__/first-run.test.ts +234 -0
  473. package/src/modules/workspace/__tests__/git-internals.security.test.ts +5 -2
  474. package/src/modules/workspace/__tests__/md-links.test.ts +353 -0
  475. package/src/modules/workspace/__tests__/move-links.test.ts +261 -0
  476. package/src/modules/workspace/__tests__/rest-tool-arguments.test.ts +219 -0
  477. package/src/modules/workspace/__tests__/workspace-paths-inside-the-repo.test.ts +3 -0
  478. package/src/modules/workspace/__tests__/workspace.routes.delete.test.ts +102 -0
  479. package/src/modules/workspace/__tests__/workspace.routes.download.test.ts +52 -5
  480. package/src/modules/workspace/__tests__/workspace.routes.folder-zip-access.test.ts +190 -0
  481. package/src/modules/workspace/__tests__/workspace.service.forget-clone-races.test.ts +8 -4
  482. package/src/modules/workspace/__tests__/workspace.service.test.ts +50 -8
  483. package/src/modules/workspace/__tests__/workspace.tools.branch-errors.test.ts +44 -1
  484. package/src/modules/workspace/__tests__/workspace.tools.test.ts +409 -12
  485. package/src/modules/workspace/agent-download.builder.ts +308 -0
  486. package/src/modules/workspace/agent-download.routes.ts +183 -0
  487. package/src/modules/workspace/agent-download.store.ts +533 -0
  488. package/src/modules/workspace/agent-upload.store.ts +23 -7
  489. package/src/modules/workspace/file-readers/raw-mime.ts +40 -0
  490. package/src/modules/workspace/file-readers/text-reader.ts +5 -8
  491. package/src/modules/workspace/first-run.ts +222 -0
  492. package/src/modules/workspace/move-links.ts +237 -0
  493. package/src/modules/workspace/startup/steps/seed-tree.ts +3 -12
  494. package/src/modules/workspace/workspace.routes.ts +64 -22
  495. package/src/modules/workspace/workspace.service.ts +202 -25
  496. package/src/modules/workspace/workspace.tools.ts +492 -63
  497. package/src/modules/write-access/__tests__/write-access.test.ts +13 -0
  498. package/src/modules/write-access/write-access.ts +9 -0
  499. package/src/shared/domain-errors.ts +108 -11
  500. package/src/shared/git.contract.ts +8 -1
  501. package/src/shared/utf8-text.ts +21 -0
  502. package/src/tenancy/__tests__/static-tenant-source.test.ts +1 -0
  503. package/src/tenancy/static-tenant-source.ts +10 -2
  504. package/starter-packs/README.md +103 -0
  505. package/starter-packs/engineering/KnowledgeBase/About us.md +9 -0
  506. package/starter-packs/engineering/KnowledgeBase/Architecture.md +9 -0
  507. package/starter-packs/engineering/KnowledgeBase/Glossary.md +9 -0
  508. package/starter-packs/engineering/KnowledgeBase/How we ship.md +9 -0
  509. package/starter-packs/engineering/Plugins/engineering-starter/LICENSE +21 -0
  510. package/starter-packs/engineering/Plugins/engineering-starter/SOURCE.md +20 -0
  511. package/starter-packs/engineering/Plugins/engineering-starter/access.md +19 -0
  512. package/starter-packs/engineering/Plugins/engineering-starter/plugin.json +10 -0
  513. package/starter-packs/engineering/Plugins/engineering-starter/skills/office-hours/SKILL.md +171 -0
  514. package/starter-packs/engineering/Plugins/engineering-starter/skills/office-hours/sections/phase-2a-startup-diagnostic.md +157 -0
  515. package/starter-packs/engineering/Plugins/engineering-starter/skills/office-hours/sections/phase-2b-builder-brainstorm.md +39 -0
  516. package/starter-packs/engineering/Plugins/engineering-starter/skills/plan-ceo-review/SKILL.md +144 -0
  517. package/starter-packs/engineering/Plugins/engineering-starter/skills/plan-eng-review/SKILL.md +144 -0
  518. package/starter-packs/engineering/Plugins/engineering-starter/skills/review/SKILL.md +80 -0
  519. package/starter-packs/engineering/Plugins/engineering-starter/skills/review/checklist.md +184 -0
  520. package/starter-packs/engineering/pack.yaml +11 -0
  521. package/starter-packs/general/KnowledgeBase/About us.md +9 -0
  522. package/starter-packs/general/KnowledgeBase/Glossary.md +9 -0
  523. package/starter-packs/general/KnowledgeBase/How we work.md +9 -0
  524. package/starter-packs/general/Plugins/general-starter/LICENSE +202 -0
  525. package/starter-packs/general/Plugins/general-starter/SOURCE.md +10 -0
  526. package/starter-packs/general/Plugins/general-starter/access.md +19 -0
  527. package/starter-packs/general/Plugins/general-starter/plugin.json +10 -0
  528. package/starter-packs/general/Plugins/general-starter/skills/memory-management/SKILL.md +332 -0
  529. package/starter-packs/general/Plugins/general-starter/skills/productivity-start/SKILL.md +163 -0
  530. package/starter-packs/general/Plugins/general-starter/skills/productivity-update/SKILL.md +167 -0
  531. package/starter-packs/general/Plugins/general-starter/skills/task-management/SKILL.md +83 -0
  532. package/starter-packs/general/pack.yaml +10 -0
  533. package/starter-packs/operations/KnowledgeBase/Glossary.md +9 -0
  534. package/starter-packs/operations/KnowledgeBase/How we work.md +9 -0
  535. package/starter-packs/operations/KnowledgeBase/Processes.md +9 -0
  536. package/starter-packs/operations/KnowledgeBase/Tools we use.md +9 -0
  537. package/starter-packs/operations/KnowledgeBase/Vendors.md +9 -0
  538. package/starter-packs/operations/Plugins/operations-starter/LICENSE +202 -0
  539. package/starter-packs/operations/Plugins/operations-starter/SOURCE.md +10 -0
  540. package/starter-packs/operations/Plugins/operations-starter/access.md +19 -0
  541. package/starter-packs/operations/Plugins/operations-starter/plugin.json +10 -0
  542. package/starter-packs/operations/Plugins/operations-starter/skills/capacity-plan/SKILL.md +111 -0
  543. package/starter-packs/operations/Plugins/operations-starter/skills/change-request/SKILL.md +119 -0
  544. package/starter-packs/operations/Plugins/operations-starter/skills/compliance-tracking/SKILL.md +44 -0
  545. package/starter-packs/operations/Plugins/operations-starter/skills/process-doc/SKILL.md +80 -0
  546. package/starter-packs/operations/Plugins/operations-starter/skills/process-optimization/SKILL.md +39 -0
  547. package/starter-packs/operations/Plugins/operations-starter/skills/risk-assessment/SKILL.md +40 -0
  548. package/starter-packs/operations/Plugins/operations-starter/skills/runbook/SKILL.md +85 -0
  549. package/starter-packs/operations/Plugins/operations-starter/skills/status-report/SKILL.md +75 -0
  550. package/starter-packs/operations/Plugins/operations-starter/skills/vendor-review/SKILL.md +102 -0
  551. package/starter-packs/operations/pack.yaml +12 -0
  552. package/starter-packs/sales/KnowledgeBase/About us.md +9 -0
  553. package/starter-packs/sales/KnowledgeBase/Customers.md +9 -0
  554. package/starter-packs/sales/KnowledgeBase/Glossary.md +9 -0
  555. package/starter-packs/sales/KnowledgeBase/How we sell.md +9 -0
  556. package/starter-packs/sales/KnowledgeBase/Objections.md +9 -0
  557. package/starter-packs/sales/KnowledgeBase/Pricing.md +9 -0
  558. package/starter-packs/sales/Plugins/sales-starter/LICENSE +202 -0
  559. package/starter-packs/sales/Plugins/sales-starter/SOURCE.md +10 -0
  560. package/starter-packs/sales/Plugins/sales-starter/access.md +19 -0
  561. package/starter-packs/sales/Plugins/sales-starter/plugin.json +10 -0
  562. package/starter-packs/sales/Plugins/sales-starter/skills/account-context/SKILL.md +94 -0
  563. package/starter-packs/sales/Plugins/sales-starter/skills/account-plan/SKILL.md +95 -0
  564. package/starter-packs/sales/Plugins/sales-starter/skills/account-research/SKILL.md +99 -0
  565. package/starter-packs/sales/Plugins/sales-starter/skills/account-tiering/SKILL.md +101 -0
  566. package/starter-packs/sales/Plugins/sales-starter/skills/call-prep/SKILL.md +111 -0
  567. package/starter-packs/sales/Plugins/sales-starter/skills/call-summary/SKILL.md +111 -0
  568. package/starter-packs/sales/Plugins/sales-starter/skills/close-plan/SKILL.md +106 -0
  569. package/starter-packs/sales/Plugins/sales-starter/skills/competitive-intelligence/SKILL.md +102 -0
  570. package/starter-packs/sales/Plugins/sales-starter/skills/create-an-asset/SKILL.md +105 -0
  571. package/starter-packs/sales/Plugins/sales-starter/skills/crm-hygiene-check/SKILL.md +103 -0
  572. package/starter-packs/sales/Plugins/sales-starter/skills/customer-health/SKILL.md +108 -0
  573. package/starter-packs/sales/Plugins/sales-starter/skills/customer-voice/SKILL.md +94 -0
  574. package/starter-packs/sales/Plugins/sales-starter/skills/daily-briefing/SKILL.md +61 -0
  575. package/starter-packs/sales/Plugins/sales-starter/skills/deal-advance-gap/SKILL.md +102 -0
  576. package/starter-packs/sales/Plugins/sales-starter/skills/deal-review/SKILL.md +105 -0
  577. package/starter-packs/sales/Plugins/sales-starter/skills/deal-signals/SKILL.md +96 -0
  578. package/starter-packs/sales/Plugins/sales-starter/skills/deal-slip-scenario/SKILL.md +104 -0
  579. package/starter-packs/sales/Plugins/sales-starter/skills/draft-outreach/SKILL.md +116 -0
  580. package/starter-packs/sales/Plugins/sales-starter/skills/end-of-day/SKILL.md +114 -0
  581. package/starter-packs/sales/Plugins/sales-starter/skills/expansion-whitespace/SKILL.md +97 -0
  582. package/starter-packs/sales/Plugins/sales-starter/skills/forecast/SKILL.md +100 -0
  583. package/starter-packs/sales/Plugins/sales-starter/skills/handle-objection/SKILL.md +93 -0
  584. package/starter-packs/sales/Plugins/sales-starter/skills/inbox-sweep/SKILL.md +114 -0
  585. package/starter-packs/sales/Plugins/sales-starter/skills/lead-triage/SKILL.md +106 -0
  586. package/starter-packs/sales/Plugins/sales-starter/skills/log-activity/SKILL.md +111 -0
  587. package/starter-packs/sales/Plugins/sales-starter/skills/pipeline-review/SKILL.md +97 -0
  588. package/starter-packs/sales/Plugins/sales-starter/skills/renewal-radar/SKILL.md +95 -0
  589. package/starter-packs/sales/Plugins/sales-starter/skills/rep-context/SKILL.md +90 -0
  590. package/starter-packs/sales/Plugins/sales-starter/skills/route-lead/SKILL.md +111 -0
  591. package/starter-packs/sales/Plugins/sales-starter/skills/sales-setup/SKILL.md +200 -0
  592. package/starter-packs/sales/Plugins/sales-starter/skills/schedule-meeting/SKILL.md +99 -0
  593. package/starter-packs/sales/Plugins/sales-starter/skills/stakeholder-map/SKILL.md +107 -0
  594. package/starter-packs/sales/Plugins/sales-starter/skills/team-pipeline/SKILL.md +105 -0
  595. package/starter-packs/sales/Plugins/sales-starter/skills/update-opportunity/SKILL.md +112 -0
  596. package/starter-packs/sales/Plugins/sales-starter/skills/weekly-wrap/SKILL.md +90 -0
  597. package/starter-packs/sales/Plugins/sales-starter/skills/win-loss-review/SKILL.md +88 -0
  598. package/starter-packs/sales/pack.yaml +13 -0
  599. package/starter-packs/support/KnowledgeBase/Escalations.md +9 -0
  600. package/starter-packs/support/KnowledgeBase/Glossary.md +9 -0
  601. package/starter-packs/support/KnowledgeBase/Known issues.md +9 -0
  602. package/starter-packs/support/KnowledgeBase/Products.md +9 -0
  603. package/starter-packs/support/KnowledgeBase/Tone of voice.md +9 -0
  604. package/starter-packs/support/Plugins/support-starter/LICENSE +202 -0
  605. package/starter-packs/support/Plugins/support-starter/SOURCE.md +10 -0
  606. package/starter-packs/support/Plugins/support-starter/access.md +19 -0
  607. package/starter-packs/support/Plugins/support-starter/plugin.json +10 -0
  608. package/starter-packs/support/Plugins/support-starter/skills/customer-escalation/SKILL.md +246 -0
  609. package/starter-packs/support/Plugins/support-starter/skills/customer-research/SKILL.md +250 -0
  610. package/starter-packs/support/Plugins/support-starter/skills/draft-response/SKILL.md +416 -0
  611. package/starter-packs/support/Plugins/support-starter/skills/kb-article/SKILL.md +350 -0
  612. package/starter-packs/support/Plugins/support-starter/skills/ticket-triage/SKILL.md +273 -0
  613. package/starter-packs/support/pack.yaml +12 -0
@@ -0,0 +1,635 @@
1
+ import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest';
2
+ import fs from 'node:fs/promises';
3
+ import os from 'node:os';
4
+ import path from 'node:path';
5
+ import { DEFAULT_KB_LAYOUT, type AuthUser, type IWorkflowService, type KbLayout } from '@bevel-software/platform-shared';
6
+ import { testKbContext } from '../../../__tests__/kb-context.js';
7
+ import { NodeFs } from '../../kb-fs/node-fs.js';
8
+ import { WorkspaceMutex } from '../../kb-fs/mutex.js';
9
+ import { KbPluginSource } from '../../plugins/index.js';
10
+ import { CLAIM_TTL_MS, StarterPackError, StarterPackService, commitSubjectOf, summaryOf } from '../starter-pack.service.js';
11
+
12
+ /**
13
+ * The starter-pack question end to end against a real checkout folder, with
14
+ * the lock/commit pipeline and the admin check stubbed at their seams:
15
+ * who is asked and when, that a choice is ONE commit of only what is absent,
16
+ * under the deployment's own folder names, with the plugin run by the admin
17
+ * who chose it — and that the question is not asked twice.
18
+ */
19
+
20
+ const KB = 'knowledge-base';
21
+ const ADMIN: AuthUser = { id: 'u-admin', email: 'ada@example.com', name: 'Ada Admin' } as AuthUser;
22
+ const MEMBER: AuthUser = { id: 'u-member', email: 'mo@example.com', name: 'Mo Member' } as AuthUser;
23
+
24
+ const SHIPPED_ACCESS = ['---', 'read:', ' - everyone', '---', 'read:', ' - everyone', 'write:', ' - Admin', ''].join('\n');
25
+
26
+ let packsDir = '';
27
+ let wsDir = '';
28
+
29
+ async function put(base: string, rel: string, content: string): Promise<void> {
30
+ const abs = path.join(base, ...rel.split('/'));
31
+ await fs.mkdir(path.dirname(abs), { recursive: true });
32
+ await fs.writeFile(abs, content);
33
+ }
34
+
35
+ /**
36
+ * `store` stands for the deployment-settings table: pass one to two
37
+ * harnesses and they are two replicas sharing a database.
38
+ */
39
+ function harness({
40
+ layout = DEFAULT_KB_LAYOUT as KbLayout,
41
+ admins = [ADMIN.email],
42
+ store = {} as Record<string, string>,
43
+ } = {}) {
44
+ const settings = {
45
+ reload: vi.fn(async (key: string) => store[key] ?? ''),
46
+ record: vi.fn(async (entries: Record<string, string>) => {
47
+ Object.assign(store, entries);
48
+ }),
49
+ recordIfAbsent: vi.fn(async (key: string, value: string) => {
50
+ if (key in store) return false;
51
+ store[key] = value;
52
+ return true;
53
+ }),
54
+ swapIfValue: vi.fn(async (key: string, expected: string, next: string | null) => {
55
+ if (store[key] !== expected) return false;
56
+ if (next === null) delete store[key];
57
+ else store[key] = next;
58
+ return true;
59
+ }),
60
+ };
61
+ const kb = testKbContext({ kbDirName: KB, layout });
62
+ // The plugin creation's identity lock, as `PluginProvisionService.withIdentities` keys it.
63
+ const locks = new WorkspaceMutex();
64
+ /** The keys the service asked for, in order — noted before the lock is granted. */
65
+ const lockRequests: string[] = [];
66
+ const pluginLocks = {
67
+ withIdentities: <T>(names: string[], fn: () => Promise<T>) => {
68
+ const keys = names.map((n) => `plugin:${n.toLowerCase()}`);
69
+ lockRequests.push(...keys);
70
+ return locks.runAll(keys, fn);
71
+ },
72
+ };
73
+ const workflow = {
74
+ acquireLock: vi.fn(async () => ({ acquired: true, lock: { holderName: 'x' } })),
75
+ releaseLock: vi.fn(async () => null),
76
+ releaseLockNoCommit: vi.fn(async () => undefined),
77
+ releaseLockUntouched: vi.fn(async () => undefined),
78
+ commitChanges: vi.fn(async () => ({ sha: 'abc' })),
79
+ };
80
+ const events = { emit: vi.fn() };
81
+ /**
82
+ * Who may read what: everything, unless a test narrows it by repo-relative
83
+ * path — a folder's name closes what is below it, as the folder chain does
84
+ * in the real verdict.
85
+ */
86
+ const unreadable = new Set<string>();
87
+ const denied = (p: string) => [...unreadable].some((u) => p === u || p.startsWith(`${u}/`));
88
+ const accessControl = {
89
+ invalidate: vi.fn(),
90
+ canReadBatch: vi.fn(async (_ws: string, _email: string, paths: string[]) => new Map(paths.map((p) => [p, !denied(p)]))),
91
+ };
92
+ const svc = new StarterPackService({
93
+ packsDir,
94
+ kb,
95
+ disk: new NodeFs(),
96
+ pluginSource: new KbPluginSource(new NodeFs(), kb),
97
+ pluginLocks,
98
+ workspaceService: {
99
+ getOrCreateForBranch: vi.fn(async () => ({ id: 'ws-main' })),
100
+ getWorkspacePath: vi.fn(async () => wsDir),
101
+ hasBootstrappedWorkspace: vi.fn(async () => true),
102
+ } as never,
103
+ workflow: workflow as unknown as IWorkflowService,
104
+ adminAccess: { isAdmin: async (email) => admins.includes(email ?? '') },
105
+ settings: settings as never,
106
+ accessControl,
107
+ events,
108
+ choosing: new WorkspaceMutex(),
109
+ });
110
+ return { svc, store, settings, locks, lockRequests, workflow, events, accessControl, unreadable };
111
+ }
112
+
113
+ beforeEach(async () => {
114
+ packsDir = await fs.mkdtemp(path.join(os.tmpdir(), 'bevel-packs-'));
115
+ wsDir = await fs.mkdtemp(path.join(os.tmpdir(), 'bevel-pack-ws-'));
116
+ await put(
117
+ packsDir,
118
+ 'sales/pack.yaml',
119
+ [
120
+ 'id: sales',
121
+ 'name: Sales',
122
+ 'description: Pages about what you sell.',
123
+ 'order: 2',
124
+ 'firstPagePrompt: Fill in the Customers page.',
125
+ 'suggestedPages: [About us, Customers]',
126
+ ].join('\n'),
127
+ );
128
+ await put(packsDir, 'sales/KnowledgeBase/About us.md', '# About us\n\nAsk your agent: _Draft this_\n');
129
+ await put(packsDir, 'sales/KnowledgeBase/Customers.md', '# Customers\n');
130
+ await put(packsDir, 'sales/Plugins/sales-starter/plugin.json', '{"name":"sales-starter","displayName":"Sales starter"}\n');
131
+ await put(packsDir, 'sales/Plugins/sales-starter/access.md', SHIPPED_ACCESS);
132
+ await put(packsDir, 'sales/Plugins/sales-starter/LICENSE', 'Apache-2.0\n');
133
+ await put(packsDir, 'sales/Plugins/sales-starter/skills/call-prep/SKILL.md', '---\nname: call-prep\n---\n');
134
+ await put(packsDir, 'sales/Plugins/sales-starter/skills/forecast/SKILL.md', '---\nname: forecast\n---\n');
135
+ // The seeded knowledge base: nothing but the starter guide.
136
+ await put(wsDir, `${KB}/KnowledgeBase/How to get started.md`, '# How to get started\n');
137
+ await put(wsDir, `${KB}/Plugins/.gitkeep`, '');
138
+ });
139
+
140
+ afterEach(async () => {
141
+ await fs.rm(packsDir, { recursive: true, force: true });
142
+ await fs.rm(wsDir, { recursive: true, force: true });
143
+ });
144
+
145
+ describe('who is asked', () => {
146
+ it('an admin of a new knowledge base, with the packs as chips', async () => {
147
+ const { svc } = harness();
148
+ expect(await svc.status(ADMIN)).toEqual({
149
+ offered: true,
150
+ chosen: null,
151
+ packs: [{ id: 'sales', name: 'Sales', description: 'Pages about what you sell.', order: 2 }],
152
+ chosenPack: null,
153
+ });
154
+ });
155
+
156
+ it('never a member, who is not shown the packs either', async () => {
157
+ const { svc } = harness();
158
+ expect(await svc.status(MEMBER)).toMatchObject({ offered: false, packs: [] });
159
+ });
160
+
161
+ it('still asks an admin who may not read the one page there is: it is not theirs to know of', async () => {
162
+ const { svc, unreadable } = harness();
163
+ await put(wsDir, `${KB}/KnowledgeBase/Leadership/Plan.md`, '# Plan\n');
164
+ unreadable.add('KnowledgeBase/Leadership');
165
+ expect((await svc.status(ADMIN)).offered).toBe(true);
166
+ unreadable.clear();
167
+ expect((await svc.status(ADMIN)).offered).toBe(false);
168
+ });
169
+
170
+ it('nobody who may not read the knowledge folder itself, admin or not', async () => {
171
+ const { svc, unreadable } = harness();
172
+ unreadable.add('KnowledgeBase');
173
+ expect((await svc.status(ADMIN)).offered).toBe(false);
174
+ await expect(svc.choose(ADMIN, 'sales')).rejects.toMatchObject({ status: 409 });
175
+ });
176
+
177
+ it('nobody once the knowledge base has a page', async () => {
178
+ await put(wsDir, `${KB}/KnowledgeBase/Team/Roadmap.md`, '# Roadmap\n');
179
+ const { svc } = harness();
180
+ expect((await svc.status(ADMIN)).offered).toBe(false);
181
+ });
182
+
183
+ it('nobody once the question was answered, skip included', async () => {
184
+ const { svc, store } = harness();
185
+ store.starterPack = 'none';
186
+ expect(await svc.status(ADMIN)).toMatchObject({ offered: false, chosen: 'none', chosenPack: null });
187
+ });
188
+
189
+ it('nobody when there are no packs to offer', async () => {
190
+ await fs.rm(packsDir, { recursive: true, force: true });
191
+ const { svc } = harness();
192
+ expect((await svc.status(ADMIN)).offered).toBe(false);
193
+ });
194
+ });
195
+
196
+ describe('choosing a pack', () => {
197
+ it('adds the pack in ONE commit by the admin, records the choice, and refreshes the tree', async () => {
198
+ const { svc, store, settings, workflow, events, accessControl } = harness();
199
+
200
+ const applied = await svc.choose(ADMIN, 'sales');
201
+
202
+ // The claim is taken BEFORE the write, so another replica's is found and
203
+ // nothing is written twice; it is released once the choice is recorded.
204
+ expect(settings.recordIfAbsent).toHaveBeenCalledWith('starterPackClaim', expect.stringMatching(/^u-admin \d+$/), ADMIN.id);
205
+ expect(settings.recordIfAbsent.mock.invocationCallOrder[0]!).toBeLessThan(
206
+ workflow.commitChanges.mock.invocationCallOrder[0]!,
207
+ );
208
+ // The answer is recorded after the commit, and only where none is.
209
+ const answered = settings.recordIfAbsent.mock.calls.findIndex(([key]) => key === 'starterPack');
210
+ expect(settings.recordIfAbsent.mock.invocationCallOrder[answered]!).toBeGreaterThan(workflow.commitChanges.mock.invocationCallOrder[0]!);
211
+ expect(settings.record).not.toHaveBeenCalled();
212
+ expect(store.starterPackClaim).toBeUndefined();
213
+
214
+ expect(applied).toEqual({ id: 'sales', name: 'Sales', pages: 2, skills: 2, summary: 'Added 2 pages and 2 skills for Sales.' });
215
+ expect(workflow.commitChanges).toHaveBeenCalledTimes(1);
216
+ const [wsId, author, subject, paths] = workflow.commitChanges.mock.calls[0] as unknown as [string, AuthUser, string, string[]];
217
+ expect([wsId, author, subject]).toEqual(['ws-main', ADMIN, 'Add starter pages and skills for Sales']);
218
+ expect(paths).toEqual([
219
+ `${KB}/KnowledgeBase/About us.md`,
220
+ `${KB}/KnowledgeBase/Customers.md`,
221
+ `${KB}/Plugins/sales-starter/LICENSE`,
222
+ `${KB}/Plugins/sales-starter/access.md`,
223
+ `${KB}/Plugins/sales-starter/plugin.json`,
224
+ `${KB}/Plugins/sales-starter/skills/call-prep/SKILL.md`,
225
+ `${KB}/Plugins/sales-starter/skills/forecast/SKILL.md`,
226
+ ]);
227
+ expect(store.starterPack).toBe('sales');
228
+ expect(accessControl.invalidate).toHaveBeenCalledWith('ws-main');
229
+ expect(events.emit).toHaveBeenCalledWith({ kind: 'fs-tree-changed', workspaceId: 'ws-main', branch: 'target-company-state' });
230
+ });
231
+
232
+ it('runs the plugin by the admin who chose it, on top of the rules the pack ships', async () => {
233
+ const { svc } = harness();
234
+ await svc.choose(ADMIN, 'sales');
235
+
236
+ const access = await fs.readFile(path.join(wsDir, KB, 'Plugins/sales-starter/access.md'), 'utf8');
237
+ const body = access.split('---').pop()!;
238
+ // The team still uses it; the admin reads, changes and owns it.
239
+ expect(body).toMatch(/read:\n(\s+- .+\n)*\s+- everyone/);
240
+ for (const verb of ['read', 'write', 'owner']) {
241
+ expect(body).toMatch(new RegExp(`${verb}:\\n(\\s+- .+\\n)*\\s+- Ada Admin <ada@example.com>`));
242
+ }
243
+ });
244
+
245
+ it('gives a pack plugin without rules or a manifest the ones "Create a plugin" writes', async () => {
246
+ await fs.rm(path.join(packsDir, 'sales/Plugins/sales-starter/access.md'));
247
+ await fs.rm(path.join(packsDir, 'sales/Plugins/sales-starter/plugin.json'));
248
+ const { svc } = harness();
249
+ await svc.choose(ADMIN, 'sales');
250
+
251
+ const manifest = JSON.parse(await fs.readFile(path.join(wsDir, KB, 'Plugins/sales-starter/plugin.json'), 'utf8'));
252
+ expect(manifest).toMatchObject({ name: 'sales-starter', displayName: 'sales-starter' });
253
+ const access = await fs.readFile(path.join(wsDir, KB, 'Plugins/sales-starter/access.md'), 'utf8');
254
+ expect(access).toContain('Ada Admin <ada@example.com>');
255
+ });
256
+
257
+ it('writes only what is absent: a page made meanwhile is never replaced', async () => {
258
+ const { svc, workflow } = harness();
259
+ // Between the question and the locks, someone writes About us.
260
+ workflow.acquireLock.mockImplementationOnce(async () => {
261
+ await put(wsDir, `${KB}/KnowledgeBase/About us.md`, '# Ours\n');
262
+ return { acquired: true, lock: { holderName: 'x' } };
263
+ });
264
+
265
+ const applied = await svc.choose(ADMIN, 'sales');
266
+
267
+ expect(await fs.readFile(path.join(wsDir, KB, 'KnowledgeBase/About us.md'), 'utf8')).toBe('# Ours\n');
268
+ const paths = (workflow.commitChanges.mock.calls[0] as unknown as [string, AuthUser, string, string[]])[3];
269
+ expect(paths).not.toContain(`${KB}/KnowledgeBase/About us.md`);
270
+ expect(applied.pages).toBe(1);
271
+ });
272
+
273
+ it('leaves a plugin that is already there alone, files and rules', async () => {
274
+ await put(wsDir, `${KB}/Plugins/Sales-Starter/access.md`, '# theirs\n');
275
+ const { svc, workflow } = harness();
276
+
277
+ const applied = await svc.choose(ADMIN, 'sales');
278
+
279
+ const paths = (workflow.commitChanges.mock.calls[0] as unknown as [string, AuthUser, string, string[]])[3];
280
+ expect(paths.filter((p) => p.includes('/Plugins/'))).toEqual([]);
281
+ expect(applied).toMatchObject({ pages: 2, skills: 0 });
282
+ });
283
+
284
+ it('leaves alone a plugin of the same name that lives in a grouping folder', async () => {
285
+ // Not at the plugins root: discovery lists it, a folder listing would not.
286
+ await put(wsDir, `${KB}/Plugins/teams/Sales Starter/plugin.json`, '{"name":"sales-starter"}\n');
287
+ await put(wsDir, `${KB}/Plugins/teams/Sales Starter/access.md`, '# theirs\n');
288
+ const { svc, workflow } = harness();
289
+
290
+ const applied = await svc.choose(ADMIN, 'sales');
291
+
292
+ const paths = (workflow.commitChanges.mock.calls[0] as unknown as [string, AuthUser, string, string[]])[3];
293
+ expect(paths.filter((p) => p.includes('/Plugins/'))).toEqual([]);
294
+ expect(applied).toMatchObject({ pages: 2, skills: 0 });
295
+ });
296
+
297
+ it('writes under the folder names this deployment chose', async () => {
298
+ await fs.rm(path.join(wsDir, KB), { recursive: true });
299
+ await put(wsDir, `${KB}/Wiki/How to get started.md`, '# How to get started\n');
300
+ const { svc, workflow } = harness({ layout: { ...DEFAULT_KB_LAYOUT, knowledgeBaseDir: 'Wiki', pluginsDir: 'Teams' } });
301
+
302
+ await svc.choose(ADMIN, 'sales');
303
+
304
+ const paths = (workflow.commitChanges.mock.calls[0] as unknown as [string, AuthUser, string, string[]])[3];
305
+ expect(paths).toContain(`${KB}/Wiki/About us.md`);
306
+ expect(paths).toContain(`${KB}/Teams/sales-starter/skills/forecast/SKILL.md`);
307
+ expect(paths.some((p) => p.includes('KnowledgeBase/') || p.includes('/Plugins/'))).toBe(false);
308
+ });
309
+
310
+ it('skipping records "none" and writes nothing', async () => {
311
+ const { svc, store, workflow } = harness();
312
+
313
+ expect(await svc.choose(ADMIN, 'none')).toEqual({ id: 'none', name: null, pages: 0, skills: 0, summary: '' });
314
+ expect(store.starterPack).toBe('none');
315
+ expect(workflow.commitChanges).not.toHaveBeenCalled();
316
+ });
317
+
318
+ it('is asked once: a second choice is 409, and so is one on a knowledge base with pages', async () => {
319
+ const { svc, workflow } = harness();
320
+ await svc.choose(ADMIN, 'none');
321
+ await expect(svc.choose(ADMIN, 'sales')).rejects.toMatchObject({ status: 409 });
322
+
323
+ const fresh = harness();
324
+ await put(wsDir, `${KB}/KnowledgeBase/Ours.md`, '# Ours\n');
325
+ await expect(fresh.svc.choose(ADMIN, 'sales')).rejects.toMatchObject({ status: 409 });
326
+ expect(workflow.commitChanges).not.toHaveBeenCalled();
327
+ expect(fresh.workflow.commitChanges).not.toHaveBeenCalled();
328
+ });
329
+
330
+ it('judges the question under the claim: an answer recorded meanwhile is found, not written over', async () => {
331
+ const store: Record<string, string> = {};
332
+ const { svc, settings, workflow } = harness({ store });
333
+ // Another replica answered `none` and released while this call was on its way to its claim.
334
+ settings.recordIfAbsent.mockImplementationOnce(async (key: string, value: string) => {
335
+ store[key] = value;
336
+ store.starterPack = 'none';
337
+ return true;
338
+ });
339
+ await expect(svc.choose(ADMIN, 'sales')).rejects.toMatchObject({ status: 409 });
340
+ expect(settings.recordIfAbsent.mock.invocationCallOrder[0]!).toBeLessThan(
341
+ settings.reload.mock.invocationCallOrder.find((_, i) => settings.reload.mock.calls[i]![0] === 'starterPack')!,
342
+ );
343
+ expect(workflow.commitChanges).not.toHaveBeenCalled();
344
+ expect(store.starterPack).toBe('none');
345
+ expect(store.starterPackClaim).toBeUndefined();
346
+ });
347
+
348
+ it('two quick clicks add the pack once', async () => {
349
+ const { svc, workflow } = harness();
350
+ const results = await Promise.allSettled([svc.choose(ADMIN, 'sales'), svc.choose(ADMIN, 'sales')]);
351
+ expect(results.map((r) => r.status).sort()).toEqual(['fulfilled', 'rejected']);
352
+ expect(workflow.commitChanges).toHaveBeenCalledTimes(1);
353
+ });
354
+
355
+ it('two replicas choosing at once add the pack once: the second finds the first’s claim', async () => {
356
+ const store: Record<string, string> = {};
357
+ const one = harness({ store });
358
+ const two = harness({ store });
359
+ const results = await Promise.allSettled([one.svc.choose(ADMIN, 'sales'), two.svc.choose(ADMIN, 'sales')]);
360
+ expect(results.map((r) => r.status).sort()).toEqual(['fulfilled', 'rejected']);
361
+ const refused = results.find((r) => r.status === 'rejected') as PromiseRejectedResult;
362
+ expect(refused.reason).toMatchObject({ status: 409 });
363
+ expect(one.workflow.commitChanges.mock.calls.length + two.workflow.commitChanges.mock.calls.length).toBe(1);
364
+ expect(store.starterPack).toBe('sales');
365
+ expect(store.starterPackClaim).toBeUndefined();
366
+ });
367
+
368
+ it('refuses while another admin’s claim is live, and takes over one left by a process that died', async () => {
369
+ const store: Record<string, string> = { starterPackClaim: `u-other ${Date.now()}` };
370
+ const { svc, workflow } = harness({ store });
371
+ await expect(svc.choose(ADMIN, 'sales')).rejects.toMatchObject({ status: 409 });
372
+ await expect(svc.choose(ADMIN, 'sales')).rejects.toThrow(/being added right now/);
373
+ expect(workflow.commitChanges).not.toHaveBeenCalled();
374
+
375
+ store.starterPackClaim = `u-other ${Date.now() - CLAIM_TTL_MS - 1}`;
376
+ await expect(svc.choose(ADMIN, 'sales')).resolves.toMatchObject({ id: 'sales' });
377
+ expect(workflow.commitChanges).toHaveBeenCalledTimes(1);
378
+ expect(store.starterPack).toBe('sales');
379
+ });
380
+
381
+ it('takes over an expired claim by replacing THAT claim, so two replicas cannot both take it', async () => {
382
+ const stale = `u-other ${Date.now() - CLAIM_TTL_MS - 1}`;
383
+ const store: Record<string, string> = { starterPackClaim: stale };
384
+ const { svc, settings, workflow } = harness({ store });
385
+ // Another replica took the expired claim over between this one's read and its take-over.
386
+ settings.reload.mockImplementation(async (key: string) => {
387
+ const held = store[key] ?? '';
388
+ if (key === 'starterPackClaim') store[key] = `u-fast ${Date.now()}`;
389
+ return held;
390
+ });
391
+ await expect(svc.choose(ADMIN, 'sales')).rejects.toThrow(/being added right now/);
392
+ expect(settings.swapIfValue).toHaveBeenCalledWith('starterPackClaim', stale, expect.stringMatching(/^u-admin /), ADMIN.id);
393
+ expect(workflow.commitChanges).not.toHaveBeenCalled();
394
+ expect(store.starterPackClaim).toMatch(/^u-fast /);
395
+ });
396
+
397
+ it('claims an expired claim whose holder finished and released it meanwhile, rather than refusing', async () => {
398
+ const stale = `u-other ${Date.now() - CLAIM_TTL_MS - 1}`;
399
+ const store: Record<string, string> = { starterPackClaim: stale };
400
+ const { svc, settings, workflow } = harness({ store });
401
+ // Between this replica's read and its take-over, the holder finished and released: the row is gone.
402
+ settings.reload.mockImplementation(async (key: string) => {
403
+ const held = store[key] ?? '';
404
+ if (key === 'starterPackClaim') delete store[key];
405
+ return held;
406
+ });
407
+ await expect(svc.choose(ADMIN, 'sales')).resolves.toMatchObject({ id: 'sales' });
408
+ // The refused first try, the one at the empty row — and the answer itself.
409
+ expect(settings.recordIfAbsent.mock.calls.map(([key]) => key)).toEqual(['starterPackClaim', 'starterPackClaim', 'starterPack']);
410
+ expect(workflow.commitChanges).toHaveBeenCalledTimes(1);
411
+ expect(store.starterPack).toBe('sales');
412
+ });
413
+
414
+ it('releases only its own claim: one taken over after it expired is left to its new holder', async () => {
415
+ const store: Record<string, string> = {};
416
+ const { svc, workflow } = harness({ store });
417
+ // While this write was failing — past the fence, in the commit — its
418
+ // claim expired and another replica took it over.
419
+ workflow.commitChanges.mockImplementationOnce(async () => {
420
+ store.starterPackClaim = `u-other ${Date.now()}`;
421
+ throw new Error('disk full');
422
+ });
423
+ await expect(svc.choose(ADMIN, 'sales')).rejects.toThrow('disk full');
424
+ expect(store.starterPackClaim).toMatch(/^u-other /);
425
+ });
426
+
427
+ it('checks its claim at the last moment: one taken over mid-write adds nothing, and leaves the answer the new holder gave', async () => {
428
+ const store: Record<string, string> = {};
429
+ const { svc, settings, workflow } = harness({ store });
430
+ // The clone took longer than the claim lives; another replica took the
431
+ // claim over and answered `none` while this one was taking its locks.
432
+ workflow.acquireLock.mockImplementationOnce(async () => {
433
+ store.starterPackClaim = 'u-other 1';
434
+ store.starterPack = 'none';
435
+ return { acquired: true, lock: { holderName: 'x' } };
436
+ });
437
+ await expect(svc.choose(ADMIN, 'sales')).rejects.toMatchObject({ status: 409 });
438
+ expect(workflow.commitChanges).not.toHaveBeenCalled();
439
+ expect(store.starterPack).toBe('none');
440
+ expect(store.starterPackClaim).toBe('u-other 1');
441
+ // Asked the database with the locks held, not before them: the renewal
442
+ // of its own claim, refused because the claim is no longer its own.
443
+ const renewals = settings.swapIfValue.mock.calls
444
+ .map(([key, expected, next], i) => ({ key, expected, next, at: settings.swapIfValue.mock.invocationCallOrder[i]! }))
445
+ .filter((c) => c.key === 'starterPackClaim' && c.next !== null);
446
+ expect(renewals).toHaveLength(1);
447
+ expect(renewals[0]!.expected).toMatch(/^u-admin /);
448
+ expect(renewals[0]!.at).toBeGreaterThan(workflow.acquireLock.mock.invocationCallOrder[0]!);
449
+ });
450
+
451
+ it('renews its claim as it checks it, so the commit that follows has the whole lease to itself', async () => {
452
+ const store: Record<string, string> = {};
453
+ const { svc, settings, workflow } = harness({ store });
454
+ let claimed = '';
455
+ settings.recordIfAbsent.mockImplementationOnce(async (key: string, value: string) => {
456
+ store[key] = value;
457
+ claimed = value;
458
+ return true;
459
+ });
460
+ // The work before the commit takes a moment, so a renewal is dated later than the claim.
461
+ workflow.acquireLock.mockImplementationOnce(async () => {
462
+ await new Promise((r) => setTimeout(r, 5));
463
+ return { acquired: true, lock: { holderName: 'x' } };
464
+ });
465
+ let claimAtCommit = '';
466
+ workflow.commitChanges.mockImplementationOnce(async () => {
467
+ claimAtCommit = store.starterPackClaim!;
468
+ return { sha: 'abc' };
469
+ });
470
+ await expect(svc.choose(ADMIN, 'sales')).resolves.toMatchObject({ id: 'sales' });
471
+ const dated = (claim: string) => Number(claim.split(' ').pop());
472
+ expect(claimAtCommit.startsWith(`${ADMIN.id} `)).toBe(true);
473
+ expect(dated(claimAtCommit)).toBeGreaterThan(dated(claimed));
474
+ // Released by its renewed value, so the row is gone.
475
+ expect(store.starterPackClaim).toBeUndefined();
476
+ });
477
+
478
+ it('releases a renewal whose outcome it never learned: the row is not left closed until it expires', async () => {
479
+ const store: Record<string, string> = {};
480
+ const { svc, settings, workflow } = harness({ store });
481
+ // The renewal reached the database and landed, but its answer did not come back.
482
+ settings.swapIfValue.mockImplementationOnce(async (key: string, expected: string, next: string | null) => {
483
+ if (store[key] === expected && next !== null) store[key] = next;
484
+ throw new Error('connection reset');
485
+ });
486
+ await expect(svc.choose(ADMIN, 'sales')).rejects.toThrow('connection reset');
487
+ expect(workflow.commitChanges).not.toHaveBeenCalled();
488
+ expect(store.starterPackClaim).toBeUndefined();
489
+ // Open again at once, not in ten minutes.
490
+ await expect(svc.choose(ADMIN, 'sales')).resolves.toMatchObject({ id: 'sales' });
491
+ });
492
+
493
+ it('records a skip only where nothing is answered: an answer that landed meanwhile stands', async () => {
494
+ const store: Record<string, string> = {};
495
+ const { svc, settings } = harness({ store });
496
+ // Between the fence's read of the answer — the second — and this call's
497
+ // record, another replica's pack landed and was recorded.
498
+ let reads = 0;
499
+ settings.reload.mockImplementation(async (key: string) => {
500
+ const held = store[key] ?? '';
501
+ if (key === 'starterPack' && ++reads === 2) store.starterPack = 'sales';
502
+ return held;
503
+ });
504
+ await expect(svc.choose(ADMIN, 'none')).rejects.toMatchObject({ status: 409 });
505
+ expect(store.starterPack).toBe('sales');
506
+ // The fence passed; it was the insert-if-absent that found the answer.
507
+ expect(settings.recordIfAbsent).toHaveBeenCalledWith('starterPack', 'none', ADMIN.id);
508
+ expect(settings.record).not.toHaveBeenCalled();
509
+ });
510
+
511
+ it('never writes its answer over another: a pack added as another replica answered keeps that answer', async () => {
512
+ const store: Record<string, string> = {};
513
+ const { svc, workflow } = harness({ store });
514
+ // The commit landed; in the moment before the record, another replica answered.
515
+ workflow.commitChanges.mockImplementationOnce(async () => {
516
+ store.starterPack = 'none';
517
+ return { sha: 'abc' };
518
+ });
519
+ await expect(svc.choose(ADMIN, 'sales')).resolves.toMatchObject({ id: 'sales' });
520
+ expect(store.starterPack).toBe('none');
521
+ expect(store.starterPackClaim).toBeUndefined();
522
+ });
523
+
524
+ it('waits for a plugin creation of the same name, on the lock creations take', async () => {
525
+ const { svc, locks, lockRequests, workflow } = harness();
526
+ let released = false;
527
+ let finishCreating!: () => void;
528
+ const creating = locks.run(
529
+ 'plugin:sales-starter',
530
+ () =>
531
+ new Promise<void>((resolve) => {
532
+ finishCreating = () => {
533
+ released = true;
534
+ resolve();
535
+ };
536
+ }),
537
+ );
538
+ const commits: string[] = [];
539
+ workflow.commitChanges.mockImplementation(async () => {
540
+ commits.push(released ? 'after the creation released the lock' : 'while the creation held the lock');
541
+ return { sha: 'abc' };
542
+ });
543
+
544
+ const choosing = svc.choose(ADMIN, 'sales');
545
+ // The pack has asked for the lock the creation holds, and nothing is
546
+ // committed until that creation lets go.
547
+ await vi.waitFor(() => expect(lockRequests).toEqual(['plugin:sales-starter']));
548
+ expect(workflow.commitChanges).not.toHaveBeenCalled();
549
+ finishCreating();
550
+ await creating;
551
+ await expect(choosing).resolves.toMatchObject({ id: 'sales' });
552
+ expect(commits).toEqual(['after the creation released the lock']);
553
+ });
554
+
555
+ it('adds the pack even when recording the choice fails afterwards: the pages close the question', async () => {
556
+ const { svc, store, workflow, settings } = harness();
557
+ settings.recordIfAbsent.mockImplementation(async (key: string, value: string) => {
558
+ if (key === 'starterPack') throw new Error('database unreachable');
559
+ if (key in store) return false;
560
+ store[key] = value;
561
+ return true;
562
+ });
563
+ await expect(svc.choose(ADMIN, 'sales')).resolves.toMatchObject({ id: 'sales', pages: 2 });
564
+ expect(workflow.commitChanges).toHaveBeenCalledTimes(1);
565
+ expect(store.starterPack).toBeUndefined();
566
+ expect(store.starterPackClaim).toBeUndefined();
567
+ expect((await svc.status(ADMIN)).offered).toBe(false);
568
+ await expect(svc.choose(ADMIN, 'sales')).rejects.toMatchObject({ status: 409 });
569
+ });
570
+
571
+ it('refuses a member (403) and a pack that does not exist (404), recording nothing', async () => {
572
+ const { svc, store } = harness();
573
+ await expect(svc.choose(MEMBER, 'sales')).rejects.toBeInstanceOf(StarterPackError);
574
+ await expect(svc.choose(MEMBER, 'sales')).rejects.toMatchObject({ status: 403 });
575
+ await expect(svc.choose(ADMIN, 'astronomy')).rejects.toMatchObject({ status: 404 });
576
+ expect(store.starterPack).toBeUndefined();
577
+ });
578
+
579
+ it('leaves the question open when the commit fails: the claim is taken back', async () => {
580
+ const { svc, store, settings, workflow } = harness();
581
+ workflow.commitChanges.mockRejectedValueOnce(new Error('disk full'));
582
+ await expect(svc.choose(ADMIN, 'sales')).rejects.toThrow('disk full');
583
+ expect(settings.recordIfAbsent).toHaveBeenCalledTimes(1);
584
+ expect(settings.swapIfValue).toHaveBeenCalledWith('starterPackClaim', expect.stringMatching(/^u-admin /), null, null);
585
+ expect(store.starterPack).toBeUndefined();
586
+ expect(store.starterPackClaim).toBeUndefined();
587
+ });
588
+ });
589
+
590
+ describe('after the choice', () => {
591
+ it('the chosen pack carries its prompt and the pages still as it wrote them', async () => {
592
+ const { svc } = harness();
593
+ await svc.choose(ADMIN, 'sales');
594
+ await fs.writeFile(path.join(wsDir, KB, 'KnowledgeBase/Customers.md'), '# Customers\n\nAcme, Globex.\n');
595
+
596
+ expect((await svc.status(MEMBER)).chosenPack).toEqual({
597
+ id: 'sales',
598
+ name: 'Sales',
599
+ firstPagePrompt: 'Fill in the Customers page.',
600
+ starterPages: [`${KB}/KnowledgeBase/About us.md`],
601
+ });
602
+ });
603
+
604
+ it('names no untouched page the caller may not read: the list would say the page exists and what it holds', async () => {
605
+ const { svc, unreadable, accessControl } = harness();
606
+ await svc.choose(ADMIN, 'sales');
607
+ unreadable.add('KnowledgeBase/About us.md');
608
+
609
+ expect((await svc.status(MEMBER)).chosenPack?.starterPages).toEqual([`${KB}/KnowledgeBase/Customers.md`]);
610
+ // Asked for the caller, by the pages' repo-relative paths, on the default branch's workspace.
611
+ expect(accessControl.canReadBatch).toHaveBeenLastCalledWith(expect.any(String), MEMBER.email, [
612
+ 'KnowledgeBase/About us.md',
613
+ 'KnowledgeBase/Customers.md',
614
+ ]);
615
+ });
616
+
617
+ it("the agent's first-run note learns the pack's suggestions and its pages", async () => {
618
+ const { svc, store } = harness();
619
+ expect(await svc.firstRunStarter()).toBeNull();
620
+ store.starterPack = 'sales';
621
+ const starter = await svc.firstRunStarter();
622
+ expect(starter?.name).toBe('Sales');
623
+ expect(starter?.suggestedPages).toEqual(['About us', 'Customers']);
624
+ expect([...(starter?.pages.keys() ?? [])]).toEqual(['About us.md', 'Customers.md']);
625
+ });
626
+ });
627
+
628
+ describe('wording', () => {
629
+ it('names the team, except for the catch-all pack whose name is the chip "Something else"', () => {
630
+ expect(commitSubjectOf({ id: 'sales', name: 'Sales' })).toBe('Add starter pages and skills for Sales');
631
+ expect(commitSubjectOf({ id: 'general', name: 'Something else' })).toBe('Add starter pages and skills');
632
+ expect(summaryOf({ id: 'general', name: 'Something else' }, 3, 1)).toBe('Added 3 pages and 1 skill.');
633
+ expect(summaryOf({ id: 'sales', name: 'Sales' }, 0, 0)).toBe('Everything in this pack was already here.');
634
+ });
635
+ });