@bevel-software/platform-core-backend 0.14.0 → 0.15.1

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 (385) hide show
  1. package/THIRD-PARTY-NOTICES.md +6 -6
  2. package/dist/core/catalog-cache-invalidation.d.ts +29 -0
  3. package/dist/core/catalog-cache-invalidation.d.ts.map +1 -0
  4. package/dist/core/catalog-cache-invalidation.js +71 -0
  5. package/dist/core/catalog-cache-invalidation.js.map +1 -0
  6. package/dist/core/create-core-server.d.ts.map +1 -1
  7. package/dist/core/create-core-server.js +99 -8
  8. package/dist/core/create-core-server.js.map +1 -1
  9. package/dist/core/create-core-services.d.ts +18 -1
  10. package/dist/core/create-core-services.d.ts.map +1 -1
  11. package/dist/core/create-core-services.js +96 -43
  12. package/dist/core/create-core-services.js.map +1 -1
  13. package/dist/modules/access/access-control.interface.d.ts +18 -6
  14. package/dist/modules/access/access-control.interface.d.ts.map +1 -1
  15. package/dist/modules/access/access-control.service.d.ts +26 -1
  16. package/dist/modules/access/access-control.service.d.ts.map +1 -1
  17. package/dist/modules/access/access-control.service.js +213 -71
  18. package/dist/modules/access/access-control.service.js.map +1 -1
  19. package/dist/modules/access/access.routes.d.ts.map +1 -1
  20. package/dist/modules/access/access.routes.js +59 -7
  21. package/dist/modules/access/access.routes.js.map +1 -1
  22. package/dist/modules/access-model/access-grammar.d.ts +51 -1
  23. package/dist/modules/access-model/access-grammar.d.ts.map +1 -1
  24. package/dist/modules/access-model/access-grammar.js +95 -1
  25. package/dist/modules/access-model/access-grammar.js.map +1 -1
  26. package/dist/modules/access-model/group-files.d.ts.map +1 -1
  27. package/dist/modules/access-model/group-files.js +4 -1
  28. package/dist/modules/access-model/group-files.js.map +1 -1
  29. package/dist/modules/access-model/plugin-principals.d.ts +49 -0
  30. package/dist/modules/access-model/plugin-principals.d.ts.map +1 -0
  31. package/dist/modules/access-model/plugin-principals.js +125 -0
  32. package/dist/modules/access-model/plugin-principals.js.map +1 -0
  33. package/dist/modules/database/core-schema.d.ts +326 -0
  34. package/dist/modules/database/core-schema.d.ts.map +1 -1
  35. package/dist/modules/database/core-schema.js +52 -0
  36. package/dist/modules/database/core-schema.js.map +1 -1
  37. package/dist/modules/kb-sync/index.d.ts +6 -0
  38. package/dist/modules/kb-sync/index.d.ts.map +1 -0
  39. package/dist/modules/kb-sync/index.js +5 -0
  40. package/dist/modules/kb-sync/index.js.map +1 -0
  41. package/dist/modules/kb-sync/kb-sync.interface.d.ts +77 -0
  42. package/dist/modules/kb-sync/kb-sync.interface.d.ts.map +1 -0
  43. package/dist/modules/kb-sync/kb-sync.interface.js +2 -0
  44. package/dist/modules/kb-sync/kb-sync.interface.js.map +1 -0
  45. package/dist/modules/kb-sync/kb-sync.routes.d.ts +70 -0
  46. package/dist/modules/kb-sync/kb-sync.routes.d.ts.map +1 -0
  47. package/dist/modules/kb-sync/kb-sync.routes.js +135 -0
  48. package/dist/modules/kb-sync/kb-sync.routes.js.map +1 -0
  49. package/dist/modules/kb-sync/kb-sync.service.d.ts +45 -0
  50. package/dist/modules/kb-sync/kb-sync.service.d.ts.map +1 -0
  51. package/dist/modules/kb-sync/kb-sync.service.js +183 -0
  52. package/dist/modules/kb-sync/kb-sync.service.js.map +1 -0
  53. package/dist/modules/kb-sync/sync-auth.d.ts +59 -0
  54. package/dist/modules/kb-sync/sync-auth.d.ts.map +1 -0
  55. package/dist/modules/kb-sync/sync-auth.js +62 -0
  56. package/dist/modules/kb-sync/sync-auth.js.map +1 -0
  57. package/dist/modules/kb-sync/sync-payload.d.ts +32 -0
  58. package/dist/modules/kb-sync/sync-payload.d.ts.map +1 -0
  59. package/dist/modules/kb-sync/sync-payload.js +105 -0
  60. package/dist/modules/kb-sync/sync-payload.js.map +1 -0
  61. package/dist/modules/marketplace/git-http.routes.d.ts +42 -0
  62. package/dist/modules/marketplace/git-http.routes.d.ts.map +1 -0
  63. package/dist/modules/marketplace/git-http.routes.js +190 -0
  64. package/dist/modules/marketplace/git-http.routes.js.map +1 -0
  65. package/dist/modules/marketplace/github-facade/github-facade-admin.routes.d.ts +23 -0
  66. package/dist/modules/marketplace/github-facade/github-facade-admin.routes.d.ts.map +1 -0
  67. package/dist/modules/marketplace/github-facade/github-facade-admin.routes.js +59 -0
  68. package/dist/modules/marketplace/github-facade/github-facade-admin.routes.js.map +1 -0
  69. package/dist/modules/marketplace/github-facade/github-facade-codes.store.d.ts +59 -0
  70. package/dist/modules/marketplace/github-facade/github-facade-codes.store.d.ts.map +1 -0
  71. package/dist/modules/marketplace/github-facade/github-facade-codes.store.js +96 -0
  72. package/dist/modules/marketplace/github-facade/github-facade-codes.store.js.map +1 -0
  73. package/dist/modules/marketplace/github-facade/github-facade-credentials.service.d.ts +99 -0
  74. package/dist/modules/marketplace/github-facade/github-facade-credentials.service.d.ts.map +1 -0
  75. package/dist/modules/marketplace/github-facade/github-facade-credentials.service.js +147 -0
  76. package/dist/modules/marketplace/github-facade/github-facade-credentials.service.js.map +1 -0
  77. package/dist/modules/marketplace/github-facade/github-facade.routes.d.ts +47 -0
  78. package/dist/modules/marketplace/github-facade/github-facade.routes.d.ts.map +1 -0
  79. package/dist/modules/marketplace/github-facade/github-facade.routes.js +228 -0
  80. package/dist/modules/marketplace/github-facade/github-facade.routes.js.map +1 -0
  81. package/dist/modules/marketplace/github-facade/github-facade.service.d.ts +109 -0
  82. package/dist/modules/marketplace/github-facade/github-facade.service.d.ts.map +1 -0
  83. package/dist/modules/marketplace/github-facade/github-facade.service.js +172 -0
  84. package/dist/modules/marketplace/github-facade/github-facade.service.js.map +1 -0
  85. package/dist/modules/marketplace/github-facade/index.d.ts +6 -0
  86. package/dist/modules/marketplace/github-facade/index.d.ts.map +1 -0
  87. package/dist/modules/marketplace/github-facade/index.js +6 -0
  88. package/dist/modules/marketplace/github-facade/index.js.map +1 -0
  89. package/dist/modules/marketplace/index.d.ts +4 -0
  90. package/dist/modules/marketplace/index.d.ts.map +1 -0
  91. package/dist/modules/marketplace/index.js +4 -0
  92. package/dist/modules/marketplace/index.js.map +1 -0
  93. package/dist/modules/marketplace/marketplace-repo.service.d.ts +122 -0
  94. package/dist/modules/marketplace/marketplace-repo.service.d.ts.map +1 -0
  95. package/dist/modules/marketplace/marketplace-repo.service.js +300 -0
  96. package/dist/modules/marketplace/marketplace-repo.service.js.map +1 -0
  97. package/dist/modules/mcp/oauth/bevel-oauth-provider.d.ts.map +1 -1
  98. package/dist/modules/mcp/oauth/bevel-oauth-provider.js +4 -0
  99. package/dist/modules/mcp/oauth/bevel-oauth-provider.js.map +1 -1
  100. package/dist/modules/mcp/oauth/oauth-consent.routes.d.ts +14 -8
  101. package/dist/modules/mcp/oauth/oauth-consent.routes.d.ts.map +1 -1
  102. package/dist/modules/mcp/oauth/oauth-consent.routes.js +13 -1
  103. package/dist/modules/mcp/oauth/oauth-consent.routes.js.map +1 -1
  104. package/dist/modules/mcp/oauth/oauth-state.d.ts +13 -2
  105. package/dist/modules/mcp/oauth/oauth-state.d.ts.map +1 -1
  106. package/dist/modules/mcp/oauth/oauth-state.js +3 -1
  107. package/dist/modules/mcp/oauth/oauth-state.js.map +1 -1
  108. package/dist/modules/plugins/compile/compile-marketplace.d.ts +97 -0
  109. package/dist/modules/plugins/compile/compile-marketplace.d.ts.map +1 -0
  110. package/dist/modules/plugins/compile/compile-marketplace.js +240 -0
  111. package/dist/modules/plugins/compile/compile-marketplace.js.map +1 -0
  112. package/dist/modules/plugins/compile/marketplace-compiler.service.d.ts +66 -0
  113. package/dist/modules/plugins/compile/marketplace-compiler.service.d.ts.map +1 -0
  114. package/dist/modules/plugins/compile/marketplace-compiler.service.js +132 -0
  115. package/dist/modules/plugins/compile/marketplace-compiler.service.js.map +1 -0
  116. package/dist/modules/plugins/discovery/bundle-dialect/bundle.source.d.ts +37 -0
  117. package/dist/modules/plugins/discovery/bundle-dialect/bundle.source.d.ts.map +1 -0
  118. package/dist/modules/plugins/discovery/bundle-dialect/bundle.source.js +135 -0
  119. package/dist/modules/plugins/discovery/bundle-dialect/bundle.source.js.map +1 -0
  120. package/dist/modules/plugins/discovery/bundle-dialect/registry.d.ts +45 -0
  121. package/dist/modules/plugins/discovery/bundle-dialect/registry.d.ts.map +1 -0
  122. package/dist/modules/plugins/discovery/bundle-dialect/registry.js +147 -0
  123. package/dist/modules/plugins/discovery/bundle-dialect/registry.js.map +1 -0
  124. package/dist/modules/plugins/discovery/kb-plugin-source.d.ts +37 -0
  125. package/dist/modules/plugins/discovery/kb-plugin-source.d.ts.map +1 -0
  126. package/dist/modules/plugins/discovery/kb-plugin-source.js +107 -0
  127. package/dist/modules/plugins/discovery/kb-plugin-source.js.map +1 -0
  128. package/dist/modules/plugins/discovery/native.source.d.ts +15 -0
  129. package/dist/modules/plugins/discovery/native.source.d.ts.map +1 -0
  130. package/dist/modules/plugins/discovery/native.source.js +102 -0
  131. package/dist/modules/plugins/discovery/native.source.js.map +1 -0
  132. package/dist/modules/plugins/discovery/plugin-source.d.ts +96 -0
  133. package/dist/modules/plugins/discovery/plugin-source.d.ts.map +1 -0
  134. package/dist/modules/plugins/discovery/plugin-source.js +2 -0
  135. package/dist/modules/plugins/discovery/plugin-source.js.map +1 -0
  136. package/dist/modules/plugins/index.d.ts +10 -1
  137. package/dist/modules/plugins/index.d.ts.map +1 -1
  138. package/dist/modules/plugins/index.js +8 -0
  139. package/dist/modules/plugins/index.js.map +1 -1
  140. package/dist/modules/plugins/plugin-links.d.ts +66 -0
  141. package/dist/modules/plugins/plugin-links.d.ts.map +1 -0
  142. package/dist/modules/plugins/plugin-links.js +160 -0
  143. package/dist/modules/plugins/plugin-links.js.map +1 -0
  144. package/dist/modules/plugins/plugin-links.service.d.ts +114 -0
  145. package/dist/modules/plugins/plugin-links.service.d.ts.map +1 -0
  146. package/dist/modules/plugins/plugin-links.service.js +304 -0
  147. package/dist/modules/plugins/plugin-links.service.js.map +1 -0
  148. package/dist/modules/plugins/plugin-provision.service.d.ts +39 -3
  149. package/dist/modules/plugins/plugin-provision.service.d.ts.map +1 -1
  150. package/dist/modules/plugins/plugin-provision.service.js +152 -52
  151. package/dist/modules/plugins/plugin-provision.service.js.map +1 -1
  152. package/dist/modules/plugins/plugin-rename.service.d.ts +69 -0
  153. package/dist/modules/plugins/plugin-rename.service.d.ts.map +1 -0
  154. package/dist/modules/plugins/plugin-rename.service.js +245 -0
  155. package/dist/modules/plugins/plugin-rename.service.js.map +1 -0
  156. package/dist/modules/plugins/plugins.contract.d.ts +27 -1
  157. package/dist/modules/plugins/plugins.contract.d.ts.map +1 -1
  158. package/dist/modules/plugins/plugins.routes.d.ts +7 -1
  159. package/dist/modules/plugins/plugins.routes.d.ts.map +1 -1
  160. package/dist/modules/plugins/plugins.routes.js +120 -12
  161. package/dist/modules/plugins/plugins.routes.js.map +1 -1
  162. package/dist/modules/plugins/plugins.service.d.ts +50 -3
  163. package/dist/modules/plugins/plugins.service.d.ts.map +1 -1
  164. package/dist/modules/plugins/plugins.service.js +108 -47
  165. package/dist/modules/plugins/plugins.service.js.map +1 -1
  166. package/dist/modules/settings/deployment-settings.service.d.ts +9 -0
  167. package/dist/modules/settings/deployment-settings.service.d.ts.map +1 -1
  168. package/dist/modules/settings/deployment-settings.service.js +82 -2
  169. package/dist/modules/settings/deployment-settings.service.js.map +1 -1
  170. package/dist/modules/settings/setup.routes.d.ts +26 -0
  171. package/dist/modules/settings/setup.routes.d.ts.map +1 -1
  172. package/dist/modules/settings/setup.routes.js +9 -1
  173. package/dist/modules/settings/setup.routes.js.map +1 -1
  174. package/dist/modules/skills/index.d.ts +1 -0
  175. package/dist/modules/skills/index.d.ts.map +1 -1
  176. package/dist/modules/skills/index.js +1 -0
  177. package/dist/modules/skills/index.js.map +1 -1
  178. package/dist/modules/skills/pending-skills.service.d.ts.map +1 -1
  179. package/dist/modules/skills/pending-skills.service.js +9 -4
  180. package/dist/modules/skills/pending-skills.service.js.map +1 -1
  181. package/dist/modules/skills/skill-access-requests.routes.d.ts +38 -0
  182. package/dist/modules/skills/skill-access-requests.routes.d.ts.map +1 -0
  183. package/dist/modules/skills/skill-access-requests.routes.js +168 -0
  184. package/dist/modules/skills/skill-access-requests.routes.js.map +1 -0
  185. package/dist/modules/skills/skills.contract.d.ts +31 -0
  186. package/dist/modules/skills/skills.contract.d.ts.map +1 -1
  187. package/dist/modules/skills/skills.routes.d.ts +28 -3
  188. package/dist/modules/skills/skills.routes.d.ts.map +1 -1
  189. package/dist/modules/skills/skills.routes.js +42 -3
  190. package/dist/modules/skills/skills.routes.js.map +1 -1
  191. package/dist/modules/skills/skills.service.d.ts +4 -0
  192. package/dist/modules/skills/skills.service.d.ts.map +1 -1
  193. package/dist/modules/skills/skills.service.js +83 -22
  194. package/dist/modules/skills/skills.service.js.map +1 -1
  195. package/dist/modules/tool-auth/external-api-key.interface.d.ts +18 -1
  196. package/dist/modules/tool-auth/external-api-key.interface.d.ts.map +1 -1
  197. package/dist/modules/tool-auth/external-api-key.interface.js +2 -1
  198. package/dist/modules/tool-auth/external-api-key.interface.js.map +1 -1
  199. package/dist/modules/tool-auth/external-api-key.service.d.ts +13 -4
  200. package/dist/modules/tool-auth/external-api-key.service.d.ts.map +1 -1
  201. package/dist/modules/tool-auth/external-api-key.service.js +26 -9
  202. package/dist/modules/tool-auth/external-api-key.service.js.map +1 -1
  203. package/dist/modules/tool-manuals/mcp-json-discovery.d.ts +36 -0
  204. package/dist/modules/tool-manuals/mcp-json-discovery.d.ts.map +1 -1
  205. package/dist/modules/tool-manuals/mcp-json-discovery.js +130 -92
  206. package/dist/modules/tool-manuals/mcp-json-discovery.js.map +1 -1
  207. package/dist/modules/tool-manuals/tool-manuals.service.d.ts +6 -1
  208. package/dist/modules/tool-manuals/tool-manuals.service.d.ts.map +1 -1
  209. package/dist/modules/tool-manuals/tool-manuals.service.js +18 -25
  210. package/dist/modules/tool-manuals/tool-manuals.service.js.map +1 -1
  211. package/dist/modules/workflow/git/git.service.d.ts +40 -2
  212. package/dist/modules/workflow/git/git.service.d.ts.map +1 -1
  213. package/dist/modules/workflow/git/git.service.js +227 -54
  214. package/dist/modules/workflow/git/git.service.js.map +1 -1
  215. package/dist/modules/workflow/git/pull-request.service.d.ts +6 -0
  216. package/dist/modules/workflow/git/pull-request.service.d.ts.map +1 -1
  217. package/dist/modules/workflow/git/pull-request.service.js +8 -0
  218. package/dist/modules/workflow/git/pull-request.service.js.map +1 -1
  219. package/dist/modules/workflow/workflow.service.d.ts +52 -1
  220. package/dist/modules/workflow/workflow.service.d.ts.map +1 -1
  221. package/dist/modules/workflow/workflow.service.js +242 -9
  222. package/dist/modules/workflow/workflow.service.js.map +1 -1
  223. package/dist/modules/workspace/startup/steps/plugin-manifests.step.d.ts +35 -0
  224. package/dist/modules/workspace/startup/steps/plugin-manifests.step.d.ts.map +1 -0
  225. package/dist/modules/workspace/startup/steps/plugin-manifests.step.js +121 -0
  226. package/dist/modules/workspace/startup/steps/plugin-manifests.step.js.map +1 -0
  227. package/dist/modules/workspace/startup/steps/seed-tree.d.ts.map +1 -1
  228. package/dist/modules/workspace/startup/steps/seed-tree.js +46 -2
  229. package/dist/modules/workspace/startup/steps/seed-tree.js.map +1 -1
  230. package/dist/modules/workspace/startup/steps/template-files.step.d.ts.map +1 -1
  231. package/dist/modules/workspace/startup/steps/template-files.step.js +112 -25
  232. package/dist/modules/workspace/startup/steps/template-files.step.js.map +1 -1
  233. package/dist/modules/workspace/workspace.routes.d.ts.map +1 -1
  234. package/dist/modules/workspace/workspace.routes.js +9 -9
  235. package/dist/modules/workspace/workspace.routes.js.map +1 -1
  236. package/dist/modules/workspace/workspace.service.d.ts +33 -0
  237. package/dist/modules/workspace/workspace.service.d.ts.map +1 -1
  238. package/dist/modules/workspace/workspace.service.js +110 -16
  239. package/dist/modules/workspace/workspace.service.js.map +1 -1
  240. package/dist/shared/domain-errors.d.ts +20 -0
  241. package/dist/shared/domain-errors.d.ts.map +1 -1
  242. package/dist/shared/domain-errors.js +29 -0
  243. package/dist/shared/domain-errors.js.map +1 -1
  244. package/dist/shared/fs-errors.d.ts +8 -0
  245. package/dist/shared/fs-errors.d.ts.map +1 -0
  246. package/dist/shared/fs-errors.js +11 -0
  247. package/dist/shared/fs-errors.js.map +1 -0
  248. package/dist/shared/fs-walk.d.ts +20 -6
  249. package/dist/shared/fs-walk.d.ts.map +1 -1
  250. package/dist/shared/fs-walk.js +35 -27
  251. package/dist/shared/fs-walk.js.map +1 -1
  252. package/dist/shared/kb-walk.d.ts +52 -0
  253. package/dist/shared/kb-walk.d.ts.map +1 -0
  254. package/dist/shared/kb-walk.js +76 -0
  255. package/dist/shared/kb-walk.js.map +1 -0
  256. package/dist/shared/path-order.d.ts +18 -0
  257. package/dist/shared/path-order.d.ts.map +1 -0
  258. package/dist/shared/path-order.js +35 -0
  259. package/dist/shared/path-order.js.map +1 -0
  260. package/dist/shared/ttl-cache.d.ts +28 -1
  261. package/dist/shared/ttl-cache.d.ts.map +1 -1
  262. package/dist/shared/ttl-cache.js +33 -1
  263. package/dist/shared/ttl-cache.js.map +1 -1
  264. package/kb-template/.bevelignore +1 -1
  265. package/kb-template/AGENTS.md +95 -30
  266. package/kb-template/Skills/.gitkeep +0 -0
  267. package/migrations/0005_github_facade.sql +25 -0
  268. package/migrations/meta/0005_snapshot.json +1717 -0
  269. package/migrations/meta/_journal.json +7 -0
  270. package/package.json +6 -6
  271. package/src/__tests__/kb-layout-config.test.ts +110 -0
  272. package/src/core/__tests__/catalog-cache-invalidation.test.ts +146 -0
  273. package/src/core/catalog-cache-invalidation.ts +92 -0
  274. package/src/core/create-core-server.ts +123 -7
  275. package/src/core/create-core-services.ts +162 -41
  276. package/src/modules/access/__tests__/access-control.invalidate-race.test.ts +86 -0
  277. package/src/modules/access/__tests__/access-control.service.test.ts +19 -3
  278. package/src/modules/access/__tests__/access-plugin-principals.test.ts +368 -0
  279. package/src/modules/access/__tests__/groups-admin.service.test.ts +8 -0
  280. package/src/modules/access/access-control.interface.ts +20 -9
  281. package/src/modules/access/access-control.service.ts +240 -78
  282. package/src/modules/access/access.routes.ts +96 -14
  283. package/src/modules/access-model/__tests__/access-grammar.test.ts +18 -0
  284. package/src/modules/access-model/__tests__/plugin-token-canonical.test.ts +40 -0
  285. package/src/modules/access-model/access-grammar.ts +114 -2
  286. package/src/modules/access-model/group-files.ts +4 -0
  287. package/src/modules/access-model/plugin-principals.ts +137 -0
  288. package/src/modules/database/core-schema.ts +54 -0
  289. package/src/modules/kb-sync/__tests__/kb-sync.routes.test.ts +297 -0
  290. package/src/modules/kb-sync/__tests__/kb-sync.service.test.ts +295 -0
  291. package/src/modules/kb-sync/__tests__/sync-auth.test.ts +118 -0
  292. package/src/modules/kb-sync/__tests__/sync-payload.test.ts +148 -0
  293. package/src/modules/kb-sync/index.ts +19 -0
  294. package/src/modules/kb-sync/kb-sync.interface.ts +79 -0
  295. package/src/modules/kb-sync/kb-sync.routes.ts +197 -0
  296. package/src/modules/kb-sync/kb-sync.service.ts +201 -0
  297. package/src/modules/kb-sync/sync-auth.ts +118 -0
  298. package/src/modules/kb-sync/sync-payload.ts +143 -0
  299. package/src/modules/marketplace/__tests__/github-facade.test.ts +443 -0
  300. package/src/modules/marketplace/__tests__/marketplace-git.test.ts +179 -0
  301. package/src/modules/marketplace/git-http.routes.ts +193 -0
  302. package/src/modules/marketplace/github-facade/github-facade-admin.routes.ts +78 -0
  303. package/src/modules/marketplace/github-facade/github-facade-codes.store.ts +145 -0
  304. package/src/modules/marketplace/github-facade/github-facade-credentials.service.ts +208 -0
  305. package/src/modules/marketplace/github-facade/github-facade.routes.ts +268 -0
  306. package/src/modules/marketplace/github-facade/github-facade.service.ts +221 -0
  307. package/src/modules/marketplace/github-facade/index.ts +25 -0
  308. package/src/modules/marketplace/index.ts +3 -0
  309. package/src/modules/marketplace/marketplace-repo.service.ts +337 -0
  310. package/src/modules/mcp/oauth/bevel-oauth-provider.ts +3 -0
  311. package/src/modules/mcp/oauth/oauth-consent.routes.ts +24 -2
  312. package/src/modules/mcp/oauth/oauth-state.ts +15 -3
  313. package/src/modules/plugins/__tests__/bundle-dialect.e2e.test.ts +163 -0
  314. package/src/modules/plugins/__tests__/compile-marketplace.test.ts +294 -0
  315. package/src/modules/plugins/__tests__/plugin-index.service.test.ts +56 -12
  316. package/src/modules/plugins/__tests__/plugin-links.service.test.ts +287 -0
  317. package/src/modules/plugins/__tests__/plugin-provision.service.test.ts +205 -5
  318. package/src/modules/plugins/__tests__/plugin-rename.service.test.ts +394 -0
  319. package/src/modules/plugins/__tests__/plugins.routes.test.ts +80 -33
  320. package/src/modules/plugins/compile/compile-marketplace.ts +403 -0
  321. package/src/modules/plugins/compile/marketplace-compiler.service.ts +150 -0
  322. package/src/modules/plugins/discovery/__tests__/kb-plugin-source.test.ts +329 -0
  323. package/src/modules/plugins/discovery/bundle-dialect/bundle.source.ts +140 -0
  324. package/src/modules/plugins/discovery/bundle-dialect/registry.ts +175 -0
  325. package/src/modules/plugins/discovery/kb-plugin-source.ts +114 -0
  326. package/src/modules/plugins/discovery/native.source.ts +122 -0
  327. package/src/modules/plugins/discovery/plugin-source.ts +99 -0
  328. package/src/modules/plugins/index.ts +10 -0
  329. package/src/modules/plugins/plugin-links.service.ts +363 -0
  330. package/src/modules/plugins/plugin-links.ts +184 -0
  331. package/src/modules/plugins/plugin-provision.service.ts +160 -54
  332. package/src/modules/plugins/plugin-rename.service.ts +298 -0
  333. package/src/modules/plugins/plugins.contract.ts +28 -1
  334. package/src/modules/plugins/plugins.routes.ts +128 -11
  335. package/src/modules/plugins/plugins.service.ts +106 -53
  336. package/src/modules/settings/__tests__/deployment-settings.service.test.ts +57 -0
  337. package/src/modules/settings/__tests__/setup.routes.sync-status.test.ts +77 -0
  338. package/src/modules/settings/deployment-settings.service.ts +95 -2
  339. package/src/modules/settings/setup.routes.ts +24 -0
  340. package/src/modules/skills/__tests__/skill-access-requests.routes.test.ts +128 -0
  341. package/src/modules/skills/__tests__/skills.service.test.ts +85 -0
  342. package/src/modules/skills/index.ts +1 -0
  343. package/src/modules/skills/pending-skills.service.ts +7 -5
  344. package/src/modules/skills/skill-access-requests.routes.ts +199 -0
  345. package/src/modules/skills/skills.contract.ts +32 -0
  346. package/src/modules/skills/skills.routes.ts +59 -3
  347. package/src/modules/skills/skills.service.ts +84 -22
  348. package/src/modules/tool-auth/external-api-key.interface.ts +20 -1
  349. package/src/modules/tool-auth/external-api-key.service.ts +31 -13
  350. package/src/modules/tool-manuals/__tests__/mcp-json-discovery.test.ts +24 -1
  351. package/src/modules/tool-manuals/__tests__/tool-manuals.cli.test.ts +2 -0
  352. package/src/modules/tool-manuals/mcp-json-discovery.ts +154 -88
  353. package/src/modules/tool-manuals/tool-manuals.service.ts +14 -23
  354. package/src/modules/workflow/__tests__/preserve-roles-yaml.test.ts +1 -1
  355. package/src/modules/workflow/__tests__/workflow.service.commitFileWhileLocked.test.ts +1 -0
  356. package/src/modules/workflow/__tests__/workflow.service.facade.test.ts +2 -2
  357. package/src/modules/workflow/__tests__/workflow.service.releaseLock.test.ts +119 -2
  358. package/src/modules/workflow/__tests__/workflow.service.sync.test.ts +437 -0
  359. package/src/modules/workflow/git/__tests__/git.service.postMergePull.test.ts +3 -3
  360. package/src/modules/workflow/git/__tests__/git.service.pull.test.ts +47 -4
  361. package/src/modules/workflow/git/__tests__/git.service.syncFromRemote.test.ts +253 -0
  362. package/src/modules/workflow/git/git.service.ts +239 -56
  363. package/src/modules/workflow/git/pull-request.service.ts +9 -0
  364. package/src/modules/workflow/workflow.service.ts +260 -9
  365. package/src/modules/workspace/__tests__/workspace.routes.read-gate.test.ts +6 -7
  366. package/src/modules/workspace/__tests__/workspace.service.list-cloned.test.ts +93 -0
  367. package/src/modules/workspace/__tests__/workspace.service.missing-branch.test.ts +28 -0
  368. package/src/modules/workspace/__tests__/workspace.service.read-filter.test.ts +42 -12
  369. package/src/modules/workspace/startup/steps/__tests__/steps.test.ts +219 -4
  370. package/src/modules/workspace/startup/steps/plugin-manifests.step.ts +129 -0
  371. package/src/modules/workspace/startup/steps/seed-tree.ts +44 -2
  372. package/src/modules/workspace/startup/steps/template-files.step.ts +131 -27
  373. package/src/modules/workspace/workspace.routes.ts +9 -17
  374. package/src/modules/workspace/workspace.service.ts +107 -16
  375. package/src/shared/__tests__/filename.test.ts +0 -0
  376. package/src/shared/__tests__/fs-walk.test.ts +22 -1
  377. package/src/shared/__tests__/kb-walk.test.ts +107 -0
  378. package/src/shared/__tests__/path-order.test.ts +24 -0
  379. package/src/shared/__tests__/ttl-cache.test.ts +55 -0
  380. package/src/shared/domain-errors.ts +29 -0
  381. package/src/shared/fs-errors.ts +10 -0
  382. package/src/shared/fs-walk.ts +38 -23
  383. package/src/shared/kb-walk.ts +95 -0
  384. package/src/shared/path-order.ts +33 -0
  385. package/src/shared/ttl-cache.ts +33 -1
@@ -195,6 +195,121 @@ export function validatedVariables(raw: unknown): ToolVariable[] | null {
195
195
  * materialization is a later phase — so a stdio server relying on them will
196
196
  * fail to spawn until then; bare-command servers (`npx …`) work today.
197
197
  */
198
+ /**
199
+ * The ONE judgement of an `mcp.json` server entry — its name and its
200
+ * transport — shared by the manual scanner (which layers the platform's
201
+ * extension data and its reachability gate on top), the bundle dialect's
202
+ * registry (whose configs become entries) and the marketplace compiler
203
+ * (which ships only what a client could run). An entry that fails here fails
204
+ * everywhere, for one reason; an entry that passes is returned NORMALISED to
205
+ * the portable shape every consumer emits.
206
+ */
207
+ export type McpEntryVerdict =
208
+ | { ok: true; transport: 'stdio'; entry: PortableStdioEntry }
209
+ | { ok: true; transport: 'streamable-http'; entry: PortableHttpEntry }
210
+ | { ok: false; reason: string };
211
+
212
+ /** A stdio server as a client launches it — every field already the type the launcher needs. */
213
+ export interface PortableStdioEntry {
214
+ type: 'stdio';
215
+ command: string;
216
+ args: string[];
217
+ env?: Record<string, string>;
218
+ cwd?: string;
219
+ }
220
+ /** An http server as a client dials it — an endpoint, never a credential. */
221
+ export interface PortableHttpEntry {
222
+ type: 'streamable-http';
223
+ url: string;
224
+ headers?: Record<string, string>;
225
+ }
226
+
227
+ /**
228
+ * A record whose every value is a string, or null — no coercion, no cast.
229
+ * Built without a prototype: the keys are HTTP header and environment
230
+ * variable names, and a plain object would silently swallow one spelled
231
+ * `__proto__` (a prototype assignment, not a property) — every accepted key
232
+ * must come out the other side.
233
+ */
234
+ function stringMap(v: unknown): Record<string, string> | null {
235
+ if (!isRecord(v)) return null;
236
+ const out: Record<string, string> = Object.create(null) as Record<string, string>;
237
+ for (const [k, val] of Object.entries(v)) {
238
+ if (typeof val !== 'string') return null;
239
+ out[k] = val;
240
+ }
241
+ return out;
242
+ }
243
+
244
+ export function judgeMcpServerEntry(name: string, raw: unknown): McpEntryVerdict {
245
+ // The name rule (lowercase, alphanumeric first) also keeps every object
246
+ // prototype key — `__proto__`, `constructor` — out of the plain-object
247
+ // maps consumers build from these names.
248
+ if (!SERVER_NAME_RE.test(name)) {
249
+ return {
250
+ ok: false,
251
+ reason: 'the name is the secret namespace and route slug, so it must be lowercase alphanumeric with `_`/`-`',
252
+ };
253
+ }
254
+ if (!isRecord(raw) || typeof raw.type !== 'string') return { ok: false, reason: 'no transport type' };
255
+ if (raw.type === 'stdio') {
256
+ if (typeof raw.command !== 'string' || raw.command.length === 0) return { ok: false, reason: 'no command' };
257
+ const entry: PortableStdioEntry = { type: 'stdio', command: raw.command, args: [] };
258
+ if (raw.args !== undefined) {
259
+ // A launcher gets the strings it was given, never a stringified object.
260
+ if (!Array.isArray(raw.args) || !raw.args.every((a) => typeof a === 'string')) {
261
+ return { ok: false, reason: 'args must be a list of strings' };
262
+ }
263
+ entry.args = raw.args;
264
+ }
265
+ if (raw.env !== undefined) {
266
+ const env = stringMap(raw.env);
267
+ if (env === null) return { ok: false, reason: 'env must be a map of strings' };
268
+ entry.env = env;
269
+ }
270
+ if (raw.cwd !== undefined) {
271
+ if (typeof raw.cwd !== 'string') return { ok: false, reason: 'cwd must be a string' };
272
+ entry.cwd = raw.cwd;
273
+ }
274
+ return { ok: true, transport: 'stdio', entry };
275
+ }
276
+ if (raw.type === 'sse') {
277
+ // The pinned `@utcp/mcp` speaks `stdio` and streamable `http` — there
278
+ // is no sse transport in its schema, so a declaration claiming one either
279
+ // fails validation or, worse, dials a handshake the server does not
280
+ // speak. Refusing names the fix; silently rebuilding as http used to
281
+ // configure exactly that wrong handshake.
282
+ return {
283
+ ok: false,
284
+ reason: 'the MCP client has no `sse` transport — declare the server as `streamable-http` if it supports it',
285
+ };
286
+ }
287
+ if (raw.type === 'streamable-http') {
288
+ if (typeof raw.url !== 'string' || raw.url.length === 0) return { ok: false, reason: 'no url' };
289
+ let parsed: URL;
290
+ try {
291
+ parsed = new URL(raw.url);
292
+ } catch {
293
+ return { ok: false, reason: 'url must be http(s)' };
294
+ }
295
+ if (parsed.protocol !== 'http:' && parsed.protocol !== 'https:') return { ok: false, reason: 'url must be http(s)' };
296
+ // A portable entry is an endpoint. A credential belongs in the vault and
297
+ // reaches a client through the platform, never verbatim in a config that
298
+ // is compiled into a marketplace and cloned onto every member's machine.
299
+ if (parsed.username || parsed.password) return { ok: false, reason: 'url must not carry credentials' };
300
+ const entry: PortableHttpEntry = { type: 'streamable-http', url: raw.url };
301
+ if (raw.headers !== undefined) {
302
+ const headers = stringMap(raw.headers);
303
+ if (headers === null) return { ok: false, reason: 'headers must be a map of strings' };
304
+ entry.headers = headers;
305
+ }
306
+ return { ok: true, transport: 'streamable-http', entry };
307
+ }
308
+ // Unknown transport: the spec says an unknown `type` invalidates the
309
+ // ENTRY, not the file — the caller skips it and keeps its siblings.
310
+ return { ok: false, reason: `unknown type "${raw.type}"` };
311
+ }
312
+
198
313
  export function descriptorsFromMcpJson(
199
314
  pluginFolder: string,
200
315
  mcpJsonText: string,
@@ -227,18 +342,14 @@ export function descriptorsFromMcpJson(
227
342
  const mcpJsonPath = `${PLUGINS_DIR}/${pluginFolder}/${PLUGIN_MCP_FILE}`;
228
343
 
229
344
  const out: ToolManualDescriptor[] = [];
230
- for (const [name, raw] of Object.entries(mcp.mcpServers)) {
231
- if (!SERVER_NAME_RE.test(name)) {
232
- console.warn(
233
- `[tool-manuals] skipping mcp server "${name}" in ${mcpJsonPath}: the name is the secret ` +
234
- 'namespace and route slug, so it must be lowercase alphanumeric with `_`/`-`.',
235
- );
236
- continue;
237
- }
238
- if (!isRecord(raw) || typeof raw.type !== 'string') {
239
- console.warn(`[tool-manuals] skipping mcp server "${name}" in ${mcpJsonPath}: no transport type.`);
345
+ for (const [name, rawEntry] of Object.entries(mcp.mcpServers)) {
346
+ const verdict = judgeMcpServerEntry(name, rawEntry);
347
+ if (!verdict.ok) {
348
+ console.warn(`[tool-manuals] skipping mcp server "${name}" in ${mcpJsonPath}: ${verdict.reason}.`);
240
349
  continue;
241
350
  }
351
+ // Narrowed by the verdict: an entry that passed is a record.
352
+ const raw = rawEntry as Record<string, unknown>;
242
353
  // The extension entry is knowledge-base content too — same zero-trust
243
354
  // parse as the rest: a non-object entry reads as "no extension data".
244
355
  const ext: HexisMcpServerExtension = isRecord(extensions[name])
@@ -274,91 +385,46 @@ export function descriptorsFromMcpJson(
274
385
  ...(variables.length > 0 ? { variables } : {}),
275
386
  };
276
387
 
277
- if (raw.type === 'stdio') {
278
- if (typeof raw.command !== 'string' || raw.command.length === 0) {
279
- console.warn(`[tool-manuals] skipping stdio server "${name}" in ${mcpJsonPath}: no command.`);
280
- continue;
281
- }
282
- out.push({
283
- ...shared,
284
- remote: false,
285
- stdio: {
286
- command: raw.command,
287
- args: Array.isArray(raw.args) ? raw.args.map(String) : [],
288
- env: isRecord(raw.env) ? (raw.env as Record<string, string>) : undefined,
289
- cwd: typeof raw.cwd === 'string' ? raw.cwd : undefined,
290
- },
291
- });
292
- continue;
293
- }
294
-
295
- if (raw.type === 'sse') {
296
- // The pinned `@utcp/mcp` speaks `stdio` and streamable `http` — there
297
- // is no sse transport in its schema, so a template claiming one either
298
- // fails validation or, worse, dials a handshake the server does not
299
- // speak. Refusing here names the fix; silently rebuilding as http used
300
- // to configure exactly that wrong handshake.
301
- console.warn(
302
- `[tool-manuals] skipping mcp server "${name}" in ${mcpJsonPath}: the MCP client has no \`sse\` transport — declare the server as \`streamable-http\` if it supports it.`,
303
- );
388
+ if (verdict.transport === 'stdio') {
389
+ const { command, args, env, cwd } = verdict.entry;
390
+ out.push({ ...shared, remote: false, stdio: { command, args, env, cwd } });
304
391
  continue;
305
392
  }
306
393
 
307
- if (raw.type === 'streamable-http') {
308
- if (typeof raw.url !== 'string' || raw.url.length === 0) {
309
- console.warn(`[tool-manuals] skipping mcp server "${name}" in ${mcpJsonPath}: no url.`);
310
- continue;
311
- }
312
- // Remote-capable servers get the same SSRF gate `.tool` urls pass —
313
- // otherwise mcp.json becomes the way to point the backend at loopback,
314
- // private ranges, or the cloud metadata endpoint. A `local: true` entry
315
- // is exempt because loopback is exactly what local MEANS, and only the
316
- // user's own machine ever dials it.
317
- // Scheme first, for EVERYONE: `local: true` exempts a server from the
318
- // private-network reachability policy, not from being http(s) at all.
319
- let schemeOk = false;
394
+ const url = verdict.entry.url;
395
+ // Remote-capable servers get the same SSRF gate `.tool` urls pass —
396
+ // otherwise mcp.json becomes the way to point the backend at loopback,
397
+ // private ranges, or the cloud metadata endpoint. A `local: true` entry
398
+ // is exempt because loopback is exactly what local MEANS, and only the
399
+ // user's own machine ever dials it. (The scheme itself was judged for
400
+ // everyone above: `local: true` exempts a server from the reachability
401
+ // policy, not from being http(s) at all.)
402
+ if (ext.local !== true) {
320
403
  try {
321
- const u = new URL(raw.url);
322
- schemeOk = u.protocol === 'http:' || u.protocol === 'https:';
323
- } catch {
324
- schemeOk = false;
325
- }
326
- if (!schemeOk) {
327
- console.warn(`[tool-manuals] skipping mcp server "${name}" in ${mcpJsonPath}: url must be http(s).`);
404
+ assertSafeFetchUrl(url, { label: `mcp server "${name}" url` });
405
+ } catch (err) {
406
+ console.warn(
407
+ `[tool-manuals] skipping mcp server "${name}" in ${mcpJsonPath}: ` +
408
+ `${err instanceof Error ? err.message : String(err)} (declare it \`local: true\` if it is deliberately private).`,
409
+ );
328
410
  continue;
329
411
  }
330
- if (ext.local !== true) {
331
- try {
332
- assertSafeFetchUrl(raw.url, { label: `mcp server "${name}" url` });
333
- } catch (err) {
334
- console.warn(
335
- `[tool-manuals] skipping mcp server "${name}" in ${mcpJsonPath}: ` +
336
- `${err instanceof Error ? err.message : String(err)} (declare it \`local: true\` if it is deliberately private).`,
337
- );
338
- continue;
339
- }
340
- }
341
- // Extension headers (auth, `${VAR}` refs) win over mcp.json's literal
342
- // ones on a key collision: the portable file cannot carry a credential,
343
- // so when both name the same header the extension is the operative one.
344
- // Both sides pass the isRecord gate — spreading a malformed non-object
345
- // value (a string, say) would scatter its indices into header keys.
346
- const headers = {
347
- ...(isRecord(raw.headers) ? (raw.headers as Record<string, string>) : {}),
348
- ...(isRecord(ext.headers) ? (ext.headers as Record<string, string>) : {}),
349
- };
350
- out.push({
351
- ...shared,
352
- url: raw.url,
353
- ...(Object.keys(headers).length > 0 ? { headers } : {}),
354
- ...(ext.local === true ? { remote: false } : {}),
355
- });
356
- continue;
357
412
  }
358
-
359
- // Unknown transport: the spec says an unknown `type` invalidates the
360
- // ENTRY, not the file — skip it, keep its siblings.
361
- console.warn(`[tool-manuals] skipping mcp server "${name}" in ${mcpJsonPath}: unknown type "${raw.type}".`);
413
+ // Extension headers (auth, `${VAR}` refs) win over mcp.json's literal
414
+ // ones on a key collision: the portable file cannot carry a credential,
415
+ // so when both name the same header the extension is the operative one.
416
+ // Both sides pass the isRecord gate — spreading a malformed non-object
417
+ // value (a string, say) would scatter its indices into header keys.
418
+ const headers = {
419
+ ...(verdict.entry.headers ?? {}),
420
+ ...(isRecord(ext.headers) ? (ext.headers as Record<string, string>) : {}),
421
+ };
422
+ out.push({
423
+ ...shared,
424
+ url,
425
+ ...(Object.keys(headers).length > 0 ? { headers } : {}),
426
+ ...(ext.local === true ? { remote: false } : {}),
427
+ });
362
428
  }
363
429
  return out;
364
430
  }
@@ -15,10 +15,10 @@ import {
15
15
  import {
16
16
  DEFAULT_BRANCH,
17
17
  PLUGINS_DIR,
18
- PLUGIN_MANIFEST_FILE,
19
- PLUGIN_MCP_FILE,
20
18
  } from '@bevel-software/platform-shared';
21
19
  import { descriptorsFromMcpJson } from './mcp-json-discovery.js';
20
+ import type { PluginSource } from '../plugins/discovery/plugin-source.js';
21
+ import { KbPluginSource } from '../plugins/discovery/kb-plugin-source.js';
22
22
  import type { WorkspaceService } from '../workspace/workspace.service.js';
23
23
  import { workspaceIdForBranch } from '../../shared/workspace-id.js';
24
24
  import type { IAccessControl } from '../access/access-control.interface.js';
@@ -150,6 +150,8 @@ export class ToolManualService implements IToolManualService {
150
150
  private readonly accessControl: IAccessControl,
151
151
  private readonly kbDirName: string,
152
152
  now: () => number = Date.now,
153
+ /** Where plugins (and their MCP servers) come from — native manifests unless a dialect is configured. */
154
+ private readonly source: PluginSource = new KbPluginSource(),
153
155
  ) {
154
156
  this.cache = new TtlCache(CACHE_TTL_MS, now);
155
157
  }
@@ -425,12 +427,15 @@ export class ToolManualService implements IToolManualService {
425
427
  private async scan(): Promise<ToolManualDescriptor[]> {
426
428
  const cached = this.cache.get();
427
429
  if (cached) return cached;
430
+ // See `TtlCache.begin`: taken before the read so an `invalidate()` that
431
+ // lands mid-scan discards this result instead of being overwritten by it.
432
+ const token = this.cache.begin();
428
433
  const manuals = await this.scanDisk();
429
434
  await this.decorateMcpOAuth(manuals);
430
435
  // AFTER the oauth decoration, so an injected `${MCP_OAUTH}` header ref is
431
436
  // already declared and isn't re-surfaced as a bare admin key.
432
437
  for (const m of manuals) this.surfaceReferencedVariables(m);
433
- this.cache.set(manuals);
438
+ this.cache.set(manuals, token);
434
439
  return manuals;
435
440
  }
436
441
 
@@ -654,27 +659,13 @@ export class ToolManualService implements IToolManualService {
654
659
  // shape. Listed BEFORE the `.tool` files: on a name collision (a legacy
655
660
  // mcp `.tool` the migration has not converted yet), the shared dedup keeps
656
661
  // the first occurrence, and the authoritative source must be the one kept.
662
+ // The plugins (and their mcp.json text) come from the configured source, so
663
+ // a dialect that expands servers from a registry lands here unchanged.
657
664
  const parsed: ToolManualDescriptor[] = [];
658
- let pluginFolders: string[] = [];
659
- try {
660
- pluginFolders = (await fs.readdir(root, { withFileTypes: true }))
661
- .filter((e) => e.isDirectory() && !e.name.startsWith('.'))
662
- .map((e) => e.name)
663
- .sort();
664
- } catch {
665
- /* no Plugins/ root — nothing to scan */
666
- }
667
- for (const folder of pluginFolders) {
668
- let mcpJson: string;
669
- try {
670
- mcpJson = await fs.readFile(path.join(root, folder, PLUGIN_MCP_FILE), 'utf-8');
671
- } catch {
672
- continue; // no mcp.json is the common case, not an error
673
- }
674
- const pluginJson = await fs
675
- .readFile(path.join(root, folder, PLUGIN_MANIFEST_FILE), 'utf-8')
676
- .catch(() => null);
677
- parsed.push(...descriptorsFromMcpJson(folder, mcpJson, pluginJson));
665
+ const discovered = await this.source.discover(kbRoot);
666
+ for (const plugin of discovered.plugins) {
667
+ if (plugin.mcpJsonText === null) continue; // no servers is the common case, not an error
668
+ parsed.push(...descriptorsFromMcpJson(plugin.relFolder, plugin.mcpJsonText, plugin.manifestText));
678
669
  }
679
670
 
680
671
  for (const f of files) {
@@ -80,7 +80,7 @@ describe('mergeChangeRequest — roles.yaml preservation guard', () => {
80
80
 
81
81
  const git = {
82
82
  fetch: vi.fn().mockResolvedValue(undefined),
83
- pull: vi.fn().mockResolvedValue(undefined),
83
+ pull: vi.fn().mockResolvedValue({ treeChanged: true }),
84
84
  // Resolve roles.yaml at origin/<ref> from the per-test content.
85
85
  readFileAtRef: vi.fn(async (_ws: string, ref: string) =>
86
86
  ref === `origin/${BASE}` ? opts.baseRoles : ref === `origin/${HEAD}` ? opts.headRoles : null,
@@ -83,6 +83,7 @@ function makeGit(overrides: Partial<{
83
83
  }),
84
84
  pull: vi.fn().mockImplementation(async () => {
85
85
  if (pullBehavior === 'fail') throw new Error('git pull failed: merge conflict');
86
+ return { treeChanged: true };
86
87
  }),
87
88
  } as unknown as GitService;
88
89
  }
@@ -182,7 +182,7 @@ function makeGit(): GitService {
182
182
  commit: vi.fn(),
183
183
  push: vi.fn(),
184
184
  fetch: vi.fn(),
185
- pull: vi.fn(),
185
+ pull: vi.fn().mockResolvedValue({ treeChanged: true }),
186
186
  diffStat: vi.fn(),
187
187
  pendingChanges: vi.fn(),
188
188
  resolveForkBase: vi.fn(),
@@ -559,7 +559,7 @@ describe('WorkflowService — revertChangeRequestFile / closeEmptyChangeRequest'
559
559
 
560
560
  function makeRevertGit(overrides: Record<string, unknown> = {}): GitService {
561
561
  return Object.assign(makeGit(), {
562
- pull: vi.fn().mockResolvedValue(undefined),
562
+ pull: vi.fn().mockResolvedValue({ treeChanged: true }),
563
563
  changedPathsForPr: vi.fn().mockResolvedValue(['Docs/a.md', 'Docs/b.md']),
564
564
  mergeBaseForPr: vi.fn().mockResolvedValue('mb-sha'),
565
565
  restorePathFromRef: vi.fn().mockResolvedValue(undefined),
@@ -8,9 +8,10 @@ import type { IAccessControl } from '../../access/access-control.interface.js';
8
8
  import { FileLockService } from '../file-lock.service.js';
9
9
  import { PendingCommitsService } from '../pending-commits.service.js';
10
10
  import { WorkflowEventBus } from '../event-bus.js';
11
- import { WorkflowService } from '../workflow.service.js';
11
+ import { WorkflowService, syncConflictMessage } from '../workflow.service.js';
12
12
  import type { Database } from '../../database/connection.js';
13
13
  import {
14
+ PullRebaseConflictError,
14
15
  PullRebaseConflictError,
15
16
  PushNeedsAgentResolutionError,
16
17
  WorkflowValidationError,
@@ -87,7 +88,7 @@ function makePending(): PendingCommitsService {
87
88
  function makeGit(overrides: Partial<{
88
89
  commitFile: Change | null;
89
90
  pushBehavior: 'ok' | 'nff' | 'auth-fail';
90
- pullBehavior: 'ok' | 'fail';
91
+ pullBehavior: 'ok' | 'fail' | 'conflict';
91
92
  pushAfterPullBehavior: 'ok' | 'fail';
92
93
  hasUnpushedCommits: boolean;
93
94
  }> = {}): GitService {
@@ -120,6 +121,8 @@ function makeGit(overrides: Partial<{
120
121
  }),
121
122
  pull: vi.fn().mockImplementation(async () => {
122
123
  if (pullBehavior === 'fail') throw new Error('git pull failed: merge conflict');
124
+ if (pullBehavior === 'conflict') throw new PullRebaseConflictError('feat/x', ['foo.md'], 'CONFLICT (content)');
125
+ return { treeChanged: true };
123
126
  }),
124
127
  } as unknown as GitService;
125
128
  }
@@ -460,6 +463,29 @@ describe('WorkflowService.runPendingCommit — worker entry point', () => {
460
463
  ]);
461
464
  });
462
465
 
466
+ it('a rebase CONFLICT on the recovery path raises the banner WITH its files, like the remote sync does', async () => {
467
+ // The banner keeps the latest failure per branch. The remote sync raises
468
+ // the conflict variant (files as links); if this path then raised the
469
+ // generic variant for the same conflict, the retry would take the links
470
+ // away — seen on staging. Every path names the files.
471
+ const git = makeGit({
472
+ commitFile: null,
473
+ hasUnpushedCommits: true,
474
+ pushBehavior: 'nff',
475
+ pullBehavior: 'conflict',
476
+ });
477
+ const svc = makeFacade(git, makeFileLocks(USER.id), makePending(), events);
478
+ await expect(svc.runPendingCommit('ws-1', 'feat/x', 'foo.md', USER)).rejects.toBeInstanceOf(
479
+ PushNeedsAgentResolutionError,
480
+ );
481
+ const failed = emitSpy.mock.calls.map((c) => c[0] as Record<string, unknown>).find((e) => e.kind === 'git-sync-failed');
482
+ expect(failed).toMatchObject({
483
+ branch: 'feat/x',
484
+ conflictedPaths: ['foo.md'],
485
+ reason: syncConflictMessage('feat/x', ['foo.md']),
486
+ });
487
+ });
488
+
463
489
  it('recovers from non-fast-forward push via pull --rebase + retry, emits file-changed exactly once', async () => {
464
490
  // Teammate pushed to the same branch between our commit and our
465
491
  // push. The cooperative path pulls + retries — this is the case
@@ -750,3 +776,94 @@ describe('WorkflowService.updateFromRemote — pull-conflict recovery dispatch',
750
776
  await expect(svc.updateFromRemote('main', USER)).rejects.toBe(CONFLICT);
751
777
  });
752
778
  });
779
+
780
+ describe('WorkflowService.updateFromRemote — the tree-change announcement', () => {
781
+ /**
782
+ * The announcement under test is what keeps the DEFAULT branch's catalog
783
+ * caches (skills, tool manuals, plugin index) honest: a pull is the one way
784
+ * a tree changes without passing the write routes, so `pullWorkspace` owes
785
+ * the process an `fs-tree-changed` — and owes it ONLY when the pull moved
786
+ * HEAD, on the default branch, after the pull resolved. Asserted through
787
+ * `updateFromRemote`, the public verb that wraps `pullWorkspace`.
788
+ */
789
+ const DEFAULT_WS = 'target-company-state'; // == the test env's DEFAULT_BRANCH
790
+
791
+ function makePullingGit(result: { treeChanged: boolean } | Error): {
792
+ git: GitService;
793
+ pullSettled: () => boolean;
794
+ } {
795
+ let settled = false;
796
+ const git = {
797
+ pull: vi.fn().mockImplementation(async () => {
798
+ // A microtask gap, so an emit issued before the await would be
799
+ // observably premature rather than coincidentally ordered.
800
+ await Promise.resolve();
801
+ settled = true;
802
+ if (result instanceof Error) throw result;
803
+ return result;
804
+ }),
805
+ } as unknown as GitService;
806
+ return { git, pullSettled: () => settled };
807
+ }
808
+
809
+ function treeChangedEvents(emitSpy: MockInstance): unknown[] {
810
+ return emitSpy.mock.calls
811
+ .map((c) => c[0] as { kind: string })
812
+ .filter((e) => e.kind === 'fs-tree-changed');
813
+ }
814
+
815
+ it('emits fs-tree-changed for the default workspace once the pull has changed the tree', async () => {
816
+ const events = new WorkflowEventBus();
817
+ const emitSpy = vi.spyOn(events, 'emit');
818
+ const { git, pullSettled } = makePullingGit({ treeChanged: true });
819
+ let settledAtEmit: boolean | undefined;
820
+ emitSpy.mockImplementation(() => {
821
+ settledAtEmit = pullSettled();
822
+ });
823
+ const svc = makeFacade(git, makeFileLocks(USER.id), makePending(), events);
824
+
825
+ await svc.updateFromRemote(DEFAULT_WS, USER);
826
+
827
+ expect(treeChangedEvents(emitSpy)).toEqual([
828
+ { kind: 'fs-tree-changed', workspaceId: DEFAULT_WS, branch: DEFAULT_WS },
829
+ ]);
830
+ // After the pull RESOLVED — an emit for a pull that later failed would
831
+ // make other clients refetch a tree that never changed.
832
+ expect(settledAtEmit).toBe(true);
833
+ });
834
+
835
+ it('stays silent when the pull found nothing new (treeChanged: false)', async () => {
836
+ const events = new WorkflowEventBus();
837
+ const emitSpy = vi.spyOn(events, 'emit');
838
+ const { git } = makePullingGit({ treeChanged: false });
839
+ const svc = makeFacade(git, makeFileLocks(USER.id), makePending(), events);
840
+
841
+ await svc.updateFromRemote(DEFAULT_WS, USER);
842
+
843
+ // An "already up to date" sync must not drop the catalogs or make every
844
+ // attached browser refetch its file tree.
845
+ expect(treeChangedEvents(emitSpy)).toEqual([]);
846
+ });
847
+
848
+ it('stays silent for a non-default workspace even when the tree changed', async () => {
849
+ const events = new WorkflowEventBus();
850
+ const emitSpy = vi.spyOn(events, 'emit');
851
+ const { git } = makePullingGit({ treeChanged: true });
852
+ const svc = makeFacade(git, makeFileLocks(USER.id), makePending(), events);
853
+
854
+ await svc.updateFromRemote(encodeURIComponent('alice/draft'), USER);
855
+
856
+ expect(treeChangedEvents(emitSpy)).toEqual([]);
857
+ });
858
+
859
+ it('stays silent when the pull throws', async () => {
860
+ const events = new WorkflowEventBus();
861
+ const emitSpy = vi.spyOn(events, 'emit');
862
+ const { git } = makePullingGit(new Error('git fetch failed: could not resolve host'));
863
+ const svc = makeFacade(git, makeFileLocks(USER.id), makePending(), events);
864
+
865
+ await expect(svc.updateFromRemote(DEFAULT_WS, USER)).rejects.toThrow();
866
+
867
+ expect(treeChangedEvents(emitSpy)).toEqual([]);
868
+ });
869
+ });