@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
@@ -1,6 +1,12 @@
1
1
  import fs from 'node:fs/promises';
2
2
  import path from 'node:path';
3
- import { KNOWLEDGE_BASE_DIR, PLUGINS_DIR } from '@bevel-software/platform-shared';
3
+ import {
4
+ KNOWLEDGE_BASE_DIR,
5
+ PLUGINS_DIR,
6
+ SKILLS_DIR,
7
+ renderKbLayoutPlaceholders,
8
+ validateKbRootName,
9
+ } from '@bevel-software/platform-shared';
4
10
  import { IGNORE_FILENAME } from '../../bevel-ignore.js';
5
11
  import type { KbBranch, OnServerStart, ServerStartContext, StepResult } from '../on-server-start.js';
6
12
 
@@ -43,8 +49,8 @@ export const TEMPLATE_SOURCE_FALLBACKS: Readonly<Record<string, string>> = {
43
49
  };
44
50
 
45
51
  /**
46
- * The two roots CORE gives a knowledge base: the ontologies, and the plugins
47
- * that hold skills and tools.
52
+ * The three roots CORE gives a knowledge base: the ontologies, the shared
53
+ * skills, and the plugins that hold tools and link the skills.
48
54
  *
49
55
  * `Data/`, `Agents/` and `Pipelines/` are deliberately absent. They scaffold
50
56
  * the agentic execution layer, which is not part of this platform — a core
@@ -54,8 +60,13 @@ export const TEMPLATE_SOURCE_FALLBACKS: Readonly<Record<string, string>> = {
54
60
  * READMEs); the names stay reserved in `kb-layout.ts` either way, so a KB
55
61
  * that has them still renders them as roots rather than folding them into
56
62
  * Knowledge.
63
+ *
64
+ * A function: the three names are deployment-configurable live bindings, and
65
+ * a module-scope array would snapshot the defaults before configuration.
57
66
  */
58
- const CORE_REQUIRED_DIRS: readonly string[] = [KNOWLEDGE_BASE_DIR, PLUGINS_DIR];
67
+ function coreRequiredDirs(): readonly string[] {
68
+ return [KNOWLEDGE_BASE_DIR, SKILLS_DIR, PLUGINS_DIR];
69
+ }
59
70
 
60
71
  /**
61
72
  * A reserved root must be ONE path segment — `Data`, not `Data/x`, `../x` or
@@ -102,7 +113,7 @@ export function reservedRootDirs(extraRootDirs: readonly string[]): readonly str
102
113
  );
103
114
  }
104
115
  }
105
- return [...CORE_REQUIRED_DIRS, ...extraRootDirs];
116
+ return [...coreRequiredDirs(), ...extraRootDirs];
106
117
  }
107
118
 
108
119
  /**
@@ -200,14 +211,18 @@ export class TemplateFilesStep implements OnServerStart {
200
211
  'Remove or rename it — the platform requires this name to be a readable file.',
201
212
  );
202
213
  }
203
- let content: Uint8Array | string = await readTemplate(templateDir, rel);
214
+ let content = await readTemplate(templateDir, rel);
204
215
  // The on-disk merge below only runs against an EXISTING ignore file; a
205
216
  // freshly-declared one was merely assumed to carry the AGENTS.md rule —
206
217
  // true of the packaged template, not necessarily of a distribution's
207
218
  // custom one. Make it true here, so the managed conventions doc is
208
219
  // hidden from the file tree from the first boot either way.
220
+ // …and a template still shipping the skills rule an earlier release
221
+ // had (a distribution's copy, a stale packaged one) must not declare
222
+ // it: the on-disk reconciliation below never sees a file that was
223
+ // absent, so the declared content is reconciled here instead.
209
224
  if (rel === IGNORE_FILENAME) {
210
- content = withIgnorePattern(new TextDecoder().decode(content), 'AGENTS.md');
225
+ content = withoutPlatformIgnorePattern(withIgnorePattern(content, 'AGENTS.md'), `${SKILLS_DIR}/`);
211
226
  }
212
227
  branch.write(rel, content);
213
228
  added.push(rel);
@@ -224,7 +239,20 @@ export class TemplateFilesStep implements OnServerStart {
224
239
  // LINE PRESENCE, not effective outcome: a later `!AGENTS.md` negation is
225
240
  // the operator explicitly choosing to SHOW the file, and hiding it is a
226
241
  // default this step provides, not a mandate it re-imposes every boot.
227
- added.push(...(await mergeIgnorePattern(repoDir, branch, 'AGENTS.md')));
242
+ //
243
+ // The shared-skills root goes the OTHER way. An earlier release hid it
244
+ // like `Plugins/`; the Skills & Tools sidebar now renders it as a file
245
+ // tree read from the workspace tree, which the ignore file filters — so a
246
+ // KB still carrying that rule would show an empty Skills section. The
247
+ // line that release wrote comes out, recognised by the PLATFORM'S OWN
248
+ // COMMENT above it — an operator who wrote the same rule by hand keeps
249
+ // it, for the same reason the negation above is kept: the file is
250
+ // theirs. The Knowledge explorer never rendered the root and still does
251
+ // not. Spelled with the CONFIGURED root name, since a deployment may
252
+ // have renamed it. ONE read-modify-write for both rules: two passes
253
+ // would each read the on-disk file and the second declared write would
254
+ // lose the first's.
255
+ added.push(...(await reconcileIgnoreRules(repoDir, branch, { add: ['AGENTS.md'], drop: [`${SKILLS_DIR}/`] })));
228
256
 
229
257
  // AGENTS.md is MANAGED, not merely seeded: the platform owns its content,
230
258
  // and a stale copy is replaced with the packaged template's every startup
@@ -293,59 +321,135 @@ export class TemplateFilesStep implements OnServerStart {
293
321
  }
294
322
  }
295
323
 
296
- /** The template's content for `relPath`, bytes as shipped. */
297
- async function readTemplate(templateDir: string, relPath: string): Promise<Uint8Array> {
298
- return fs.readFile(await templateSource(templateDir, relPath));
324
+ /**
325
+ * The template's content for `relPath`, RENDERED: the managed files name the
326
+ * three root folders, and a deployment may have renamed those, so the
327
+ * placeholders the template carries (`{{pluginsDir}}` …) are filled with the
328
+ * names in effect. Every required file is text; a template without
329
+ * placeholders passes through unchanged.
330
+ */
331
+ async function readTemplate(templateDir: string, relPath: string): Promise<string> {
332
+ return renderKbLayoutPlaceholders(await fs.readFile(await templateSource(templateDir, relPath), 'utf8'));
299
333
  }
300
334
 
301
335
  /**
302
- * Whether the repo's copy of `relPath` differs from the template's, modulo
303
- * line endings — a CRLF checkout of identical content must read as "same",
304
- * or the managed-file refresh would commit churn on every boot forever.
336
+ * Whether the repo's copy of `relPath` differs from the RENDERED template's,
337
+ * modulo line endings — a CRLF checkout of identical content must read as
338
+ * "same", or the managed-file refresh would commit churn on every boot
339
+ * forever. Rendered, so a renamed root is compared against the guide that
340
+ * names it, not against the placeholders.
305
341
  */
306
342
  async function templateDiffers(templateDir: string, repoDir: string, relPath: string): Promise<boolean> {
307
343
  const norm = (text: string) => text.replace(/\r\n?/g, '\n');
308
344
  const [current, template] = await Promise.all([
309
345
  fs.readFile(path.join(repoDir, relPath), 'utf8'),
310
- templateSource(templateDir, relPath).then((from) => fs.readFile(from, 'utf8')),
346
+ readTemplate(templateDir, relPath),
311
347
  ]);
312
348
  return norm(current) !== norm(template);
313
349
  }
314
350
 
315
351
  /**
316
- * Ensure `.bevelignore` carries `pattern`, declaring the appended content when
317
- * absent. Returns the paths changed, for the note.
352
+ * Reconcile the platform's OWN rules in `.bevelignore`: every `add` pattern
353
+ * guaranteed present as a line, every `drop` pattern taken out. Returns the
354
+ * paths changed, for the note.
318
355
  *
319
- * APPENDS — never rewrites. The file is the operator's, and every rule
320
- * already in it is theirs to keep; this adds one line under a comment saying
321
- * where it came from. Absent file, or a file that already lists the pattern,
322
- * is a no-op — an absent file means the template's copy (declared in the same
323
- * step) arrives with the pattern in it.
356
+ * Never rewrites the rest. The file is the operator's, and every rule already
357
+ * in it is theirs to keep: adding puts one line under a comment saying where
358
+ * it came from, and dropping removes exactly the line (and the comment) an
359
+ * earlier release put there — never a line the operator wrote. Absent file is
360
+ * a no-op — it means the template's copy (declared in the same step, and
361
+ * reconciled the same way at declaration) arrives with the right rules in it.
324
362
  *
325
363
  * Matched line-wise rather than by substring: a rule for `Plugins/AGENTS.md`
326
364
  * is not a rule for the root `AGENTS.md`, and treating it as one would leave
327
365
  * the mismatch this exists to close.
328
366
  */
329
- async function mergeIgnorePattern(repoDir: string, branch: KbBranch, pattern: string): Promise<string[]> {
367
+ async function reconcileIgnoreRules(
368
+ repoDir: string,
369
+ branch: KbBranch,
370
+ rules: { add: string[]; drop: string[] },
371
+ ): Promise<string[]> {
330
372
  let current: string;
331
373
  try {
332
374
  current = await fs.readFile(path.join(repoDir, IGNORE_FILENAME), 'utf8');
333
375
  } catch {
334
376
  // No ignore file — the copy declared from the template arrives with the
335
- // pattern in it (guaranteed at declaration time, see the required-files
336
- // loop above).
377
+ // right rules in it (guaranteed at declaration time, see the
378
+ // required-files loop above).
337
379
  return [];
338
380
  }
339
- const merged = withIgnorePattern(current, pattern);
381
+ const added = rules.add.reduce((text, pattern) => withIgnorePattern(text, pattern), current);
382
+ const merged = rules.drop.reduce((text, pattern) => withoutPlatformIgnorePattern(text, pattern), added);
340
383
  if (merged === current) return [];
341
384
  branch.write(IGNORE_FILENAME, merged);
342
385
  return [IGNORE_FILENAME];
343
386
  }
344
387
 
388
+ /** The comment `withIgnorePattern` writes above a line it appends. */
389
+ const PLATFORM_RULE_COMMENT = '# Added by the platform: the conventions doc is not node content.';
390
+
391
+ /**
392
+ * The template line an earlier release shipped above the shared-skills rule,
393
+ * split around its one variable: the plugins root's name, which a deployment
394
+ * may have renamed since. Everything else is fixed.
395
+ */
396
+ const LEGACY_SKILLS_RULE_COMMENT_OPENING = '# The shared-skills root is rendered by the Skills & Tools app, like ';
397
+ const LEGACY_SKILLS_RULE_COMMENT_CLOSING = '/.';
398
+
399
+ /**
400
+ * Whether a line is EXACTLY a comment the platform wrote above a rule it
401
+ * added. For the legacy template line that means the fixed opening, the
402
+ * fixed closing, and between them a name the platform could have rendered
403
+ * there — judged by the ONE rule that decides what a root may be called
404
+ * (`validateKbRootName`), not by a second grammar written here: a hand-made
405
+ * character class either admits names the validator refuses, or refuses
406
+ * names it admits (a space, say), and either way a line the platform did
407
+ * write would be left standing. A looser match (an opening, a substring)
408
+ * errs the other way and takes a line the operator wrote.
409
+ */
410
+ function isPlatformRuleComment(line: string): boolean {
411
+ const trimmed = line.trim();
412
+ if (trimmed === PLATFORM_RULE_COMMENT) return true;
413
+ if (
414
+ !trimmed.startsWith(LEGACY_SKILLS_RULE_COMMENT_OPENING) ||
415
+ !trimmed.endsWith(LEGACY_SKILLS_RULE_COMMENT_CLOSING)
416
+ ) {
417
+ return false;
418
+ }
419
+ const name = trimmed.slice(
420
+ LEGACY_SKILLS_RULE_COMMENT_OPENING.length,
421
+ trimmed.length - LEGACY_SKILLS_RULE_COMMENT_CLOSING.length,
422
+ );
423
+ return name === name.trim() && validateKbRootName(name) === null;
424
+ }
425
+
426
+ /**
427
+ * `text` without the `pattern` lines THE PLATFORM WROTE — the ones sitting
428
+ * directly under its own comment (the one `withIgnorePattern` writes, or the
429
+ * template line an earlier release shipped) — and without that comment, plus
430
+ * the blank line that opened an appended block. Provenance is the comment:
431
+ * an identical line with no platform comment above it is the operator's and
432
+ * stays, as does a `!pattern` negation. Nothing else moves.
433
+ */
434
+ function withoutPlatformIgnorePattern(text: string, pattern: string): string {
435
+ const lines = text.split('\n');
436
+ const kept: string[] = [];
437
+ for (const line of lines) {
438
+ const above = kept[kept.length - 1];
439
+ if (line.trim() !== pattern || above === undefined || !isPlatformRuleComment(above)) {
440
+ kept.push(line);
441
+ continue;
442
+ }
443
+ kept.pop();
444
+ if (above.trim() === PLATFORM_RULE_COMMENT && kept.length > 1 && kept[kept.length - 1]?.trim() === '') kept.pop();
445
+ }
446
+ return kept.join('\n');
447
+ }
448
+
345
449
  /**
346
450
  * `text` with `pattern` guaranteed present as a LINE — appended under a
347
451
  * comment naming its origin when absent, returned unchanged when present.
348
- * Line-wise match, same rationale as {@link mergeIgnorePattern}.
452
+ * Line-wise match, same rationale as {@link mergeIgnorePatterns}.
349
453
  *
350
454
  * An explicit `!pattern` line also returns the text unchanged: that is the
351
455
  * operator choosing to SHOW the file, and ordered matching means a positive
@@ -4,15 +4,7 @@ import { IGNORE_FILENAME } from './bevel-ignore.js';
4
4
  import type { IAdminAccessService } from '../admin/admin.interface.js';
5
5
  import express from 'express';
6
6
  import type { AuthUser, IWorkflowService } from '@bevel-software/platform-shared';
7
- import {
8
- AGENTS_DIR,
9
- DATA_DIR,
10
- DEFAULT_BRANCH,
11
- KNOWLEDGE_BASE_DIR,
12
- KNOWLEDGE_DIR,
13
- PIPELINES_DIR,
14
- PLUGINS_DIR,
15
- } from '@bevel-software/platform-shared';
7
+ import { DEFAULT_BRANCH, KNOWLEDGE_DIR, reservedRootDirNames } from '@bevel-software/platform-shared';
16
8
  import { FolderTooLargeError, type ReadTreeFilter } from './workspace.service.js';
17
9
  import { branchForWorkspaceId } from '../../shared/workspace-id.js';
18
10
  import type { WorkspaceService } from './workspace.service.js';
@@ -290,6 +282,13 @@ export function createWorkspaceRoutes(
290
282
  );
291
283
  res.json({ workspace, fileTree });
292
284
  } catch (error) {
285
+ // A typed domain answer — a branch origin no longer has (410), a name
286
+ // git refuses (400) — is the client's to act on, not a failure of ours;
287
+ // it keeps its status so the browser can say what happened.
288
+ if (error instanceof WorkflowDomainError) {
289
+ sendError(res, error);
290
+ return;
291
+ }
293
292
  // Log the full stack so the next 500 isn't a guessing game — the
294
293
  // bare `error.message` we returned before lost most diagnostic
295
294
  // signal (cause chain, stack frames, error class).
@@ -476,14 +475,7 @@ export function createWorkspaceRoutes(
476
475
  // same treatment so legacy clones don't collapse. Only the folders
477
476
  // themselves are forced visible — their contents stay gated by the
478
477
  // verdict above.
479
- const structuralRoots = new Set([
480
- KNOWLEDGE_BASE_DIR,
481
- DATA_DIR,
482
- AGENTS_DIR,
483
- PIPELINES_DIR,
484
- PLUGINS_DIR,
485
- KNOWLEDGE_DIR,
486
- ]);
478
+ const structuralRoots = new Set([...reservedRootDirNames(), KNOWLEDGE_DIR]);
487
479
  for (const wp of wsRelPaths) {
488
480
  const rel = toKbRelative(wp, kbDirName);
489
481
  if (rel !== null && structuralRoots.has(rel)) verdict.set(wp, true);
@@ -7,7 +7,11 @@ import type { AuthUser, IWorkspaceService, WorkspaceInfo, FileTreeEntry } from '
7
7
  import { assertValidRelativePath, validateFilename, DEFAULT_BRANCH } from '@bevel-software/platform-shared';
8
8
  import { BevelIgnoreStack } from './bevel-ignore.js';
9
9
  import { workspaceIdForBranch, branchForWorkspaceId } from '../../shared/workspace-id.js';
10
- import { WorkflowDomainError } from '../../shared/domain-errors.js';
10
+ import {
11
+ RemoteBranchGoneError,
12
+ WorkflowDomainError,
13
+ isMissingRemoteBranchFailure,
14
+ } from '../../shared/domain-errors.js';
11
15
  import { assertValidBranchName } from '../kb-fs/branch-name.js';
12
16
  import {
13
17
  cloneCredentialArgs,
@@ -248,6 +252,73 @@ export class WorkspaceService implements IWorkspaceService {
248
252
  return null;
249
253
  }
250
254
 
255
+ /**
256
+ * Every branch with a finished clone on disk, whether or not this process
257
+ * has touched it yet. The remote sync reads this: clones survive a restart,
258
+ * and a hook that fires before anyone opens a branch must still find its
259
+ * clone. Same disk scan as `findAnyWorkspaceId`.
260
+ *
261
+ * What is listed: directories holding a `<kbDirName>/.git`, whose name is
262
+ * the encoding of a branch git would accept, and whose bootstrap is not in
263
+ * flight right now. The in-flight exclusion matters because `git clone`
264
+ * creates `.git` long before the working tree is checked out — pulling a
265
+ * clone mid-bootstrap would run git against a half-written tree — and a
266
+ * clone that is being created this instant is current by definition. A
267
+ * clone left behind by a crash mid-checkout is NOT distinguished here: the
268
+ * rest of this service treats a directory with `.git` as a clone too, and
269
+ * the pull's own errors are how such a tree surfaces.
270
+ *
271
+ * A missing root means no clones (nothing has been bootstrapped yet); any
272
+ * other failure to read it is propagated, so a sync cannot report success
273
+ * over a root it could not see. One entry that cannot be probed (an ACL, a
274
+ * half-deleted directory) is returned with `unreadable` set rather than
275
+ * thrown, so it fails as one branch and not as the whole listing.
276
+ */
277
+ async listClonedWorkspaces(): Promise<Array<{ id: string; branch: string; unreadable?: string }>> {
278
+ let entries: Array<{ name: string; isDirectory: () => boolean }>;
279
+ try {
280
+ entries = await fs.readdir(this.workspacesRoot, { withFileTypes: true });
281
+ } catch (err) {
282
+ const code = (err as NodeJS.ErrnoException | null)?.code;
283
+ if (code === 'ENOENT' || code === 'ENOTDIR') return [];
284
+ throw err;
285
+ }
286
+ const cloned: Array<{ id: string; branch: string; unreadable?: string }> = [];
287
+ for (const entry of entries) {
288
+ if (!entry.isDirectory()) continue;
289
+ const branch = branchForWorkspaceId(entry.name);
290
+ // Not one of ours unless the name is the encoding of a branch git
291
+ // accepts: `branchForWorkspaceId` returns a malformed name unchanged, so
292
+ // the round-trip catches those, and the validator catches a stray
293
+ // directory whose name merely round-trips.
294
+ if (workspaceIdForBranch(branch) !== entry.name) continue;
295
+ try {
296
+ assertValidBranchName(branch);
297
+ } catch {
298
+ continue;
299
+ }
300
+ try {
301
+ await fs.access(path.join(this.workspacesRoot, entry.name, this.kbDirName, '.git'));
302
+ } catch (err) {
303
+ const code = (err as NodeJS.ErrnoException | null)?.code;
304
+ if (code === 'ENOENT' || code === 'ENOTDIR') continue;
305
+ cloned.push({
306
+ id: entry.name,
307
+ branch,
308
+ unreadable: `Could not read this branch's clone: ${redactError(err)}`,
309
+ });
310
+ continue;
311
+ }
312
+ // AFTER the probe, with no await in between: a bootstrap registers
313
+ // itself before it creates `.git`, so any clone whose `.git` was found
314
+ // while its bootstrap is running is in this map by now. Checking before
315
+ // the probe left a window where the clone started in between.
316
+ if (this.isBootstrapInFlight(branch)) continue;
317
+ cloned.push({ id: entry.name, branch });
318
+ }
319
+ return cloned;
320
+ }
321
+
251
322
  /**
252
323
  * Run the clone. When a `referenceRepo` is supplied and the referenced
253
324
  * clone fails (e.g. the sibling's object store is corrupt or mid-write),
@@ -408,6 +479,15 @@ export class WorkspaceService implements IWorkspaceService {
408
479
  }
409
480
  }
410
481
 
482
+ /**
483
+ * Whether a clone of `branch` is being created at this moment. Read by the
484
+ * retirement of a clone the host deleted: a bootstrap in flight means the
485
+ * branch exists again, so there is nothing stale to remove.
486
+ */
487
+ isBootstrapInFlight(branch: string): boolean {
488
+ return this.inFlightBootstraps.has(branch);
489
+ }
490
+
411
491
  /**
412
492
  * Bring an adopted clone's config back to what a fresh clone would carry.
413
493
  *
@@ -620,9 +700,18 @@ export class WorkspaceService implements IWorkspaceService {
620
700
  }
621
701
  } catch (err) {
622
702
  const redacted = redactError(err);
623
- console.error(`[workspace] Failed to clone for branch "${branch}":`, redacted);
624
703
  // Roll back partial state so the next bootstrap retries cleanly.
625
704
  await fs.rm(targetDir, { recursive: true, force: true }).catch(() => {});
705
+ // A branch origin does not have is a fact about the branch, not a
706
+ // failure of ours: the host deleted it (and the sync retired the
707
+ // clone), or the link was to a branch that never existed. Typed, so the
708
+ // routes answer 410 with the branch named and the browser can say "this
709
+ // branch no longer exists" instead of "something went wrong".
710
+ if (isMissingRemoteBranchFailure(redacted)) {
711
+ console.log(`[workspace] branch "${branch}" does not exist on origin — nothing to clone`);
712
+ throw new RemoteBranchGoneError(branch);
713
+ }
714
+ console.error(`[workspace] Failed to clone for branch "${branch}":`, redacted);
626
715
  throw new Error(`Failed to clone process map: ${redacted}`);
627
716
  }
628
717
  }
@@ -1433,23 +1522,25 @@ export class WorkspaceService implements IWorkspaceService {
1433
1522
  });
1434
1523
  }
1435
1524
 
1436
- // Read-permission filter: ONE batched check per directory. Drops files AND
1437
- // directories the caller can't read; a hidden directory's subtree is never
1438
- // walked (skip-and-don't-recurse). A readable directory left empty after
1439
- // filtering stays visible (the folder itself is readable). Fail-closed:
1440
- // anything not explicitly readable is dropped. No filter → identical to the
1441
- // pre-feature tree (regression-safe).
1442
- let visible = candidates;
1443
- if (readFilter && candidates.length > 0) {
1444
- const verdict = await readFilter(candidates.map((c) => c.rel));
1445
- visible = candidates.filter((c) => verdict.get(c.rel) === true);
1446
- }
1525
+ // Read-permission filter: ONE batched check per directory. Drops files the
1526
+ // caller can't read. A directory is kept when the caller can read it — a
1527
+ // readable directory left empty after filtering stays visible, the folder
1528
+ // itself is readable — OR when something readable survives beneath it: a
1529
+ // grant below (a linked skill's folder opened to a plugin's readers, a
1530
+ // sub-folder shared on its own) makes the folders above it the way there,
1531
+ // shown as containers whose own contents stay filtered. So an unreadable
1532
+ // directory is walked, not skipped, and dropped only when the walk finds
1533
+ // nothing. Fail-closed: anything not explicitly readable is dropped. No
1534
+ // filter → identical to the pre-feature tree (regression-safe).
1535
+ const verdict = readFilter && candidates.length > 0 ? await readFilter(candidates.map((c) => c.rel)) : null;
1536
+ const readable = (rel: string) => verdict === null || verdict.get(rel) === true;
1447
1537
 
1448
1538
  const children: FileTreeEntry[] = [];
1449
- for (const { entry, entryPath, rel } of visible) {
1539
+ for (const { entry, entryPath, rel } of candidates) {
1450
1540
  if (entry.isDirectory()) {
1451
- children.push(await this.buildFileTree(entryPath, workspaceRoot, ignoreStack, readFilter));
1452
- } else {
1541
+ const sub = await this.buildFileTree(entryPath, workspaceRoot, ignoreStack, readFilter);
1542
+ if (readable(rel) || (sub.children?.length ?? 0) > 0) children.push(sub);
1543
+ } else if (readable(rel)) {
1453
1544
  children.push({ name: entry.name, relativePath: rel, type: 'file' });
1454
1545
  }
1455
1546
  }
@@ -1,4 +1,4 @@
1
- import { describe, it, expect, beforeEach, afterEach } from 'vitest';
1
+ import { describe, it, expect, beforeEach, afterEach, vi } from 'vitest';
2
2
  import fs from 'node:fs/promises';
3
3
  import path from 'node:path';
4
4
  import os from 'node:os';
@@ -39,4 +39,25 @@ describe('walkFiles', () => {
39
39
 
40
40
  expect(await walkFiles(root, (n) => n.endsWith('.tool'))).toEqual(['visible.tool']);
41
41
  });
42
+
43
+ it('skips a directory it cannot list by default, and throws for a strict caller', async () => {
44
+ await fs.mkdir(path.join(root, 'locked'), { recursive: true });
45
+ await fs.writeFile(path.join(root, 'locked/a.tool'), '');
46
+ await fs.writeFile(path.join(root, 'b.tool'), '');
47
+ const real = fs.readdir;
48
+ const spy = vi.spyOn(fs, 'readdir').mockImplementation(((dir: string, opts: unknown) => {
49
+ if (String(dir).endsWith('locked')) {
50
+ return Promise.reject(Object.assign(new Error('EACCES: permission denied'), { code: 'EACCES' }));
51
+ }
52
+ return (real as (d: string, o: unknown) => Promise<unknown>).call(fs, dir, opts);
53
+ }) as never);
54
+ try {
55
+ // A catalog shows what it can…
56
+ expect(await walkFiles(root, (n) => n.endsWith('.tool'))).toEqual(['b.tool']);
57
+ // …a caller that must see everything gets the error, not a list with a hole.
58
+ await expect(walkFiles(root, (n) => n.endsWith('.tool'), { strict: true })).rejects.toThrow('EACCES');
59
+ } finally {
60
+ spy.mockRestore();
61
+ }
62
+ });
42
63
  });
@@ -0,0 +1,107 @@
1
+ import { describe, it, expect, beforeEach, afterEach, vi } from 'vitest';
2
+ import fs from 'node:fs/promises';
3
+ import path from 'node:path';
4
+ import os from 'node:os';
5
+ import { walkKb, type KbWalkListener } from '../kb-walk.js';
6
+
7
+ /**
8
+ * The one walk: what it skips, in what order it visits, what it calls a hole
9
+ * — and that every listener on it is told exactly the same things.
10
+ */
11
+ describe('walkKb', () => {
12
+ let root: string;
13
+ const write = async (rel: string, text = '') => {
14
+ const abs = path.join(root, rel);
15
+ await fs.mkdir(path.dirname(abs), { recursive: true });
16
+ await fs.writeFile(abs, text);
17
+ };
18
+ /** A listener that records every event it is told, in order. */
19
+ const recorder = () => {
20
+ const events: string[] = [];
21
+ const listener: KbWalkListener = {
22
+ onDir: (rel, entries) => void events.push(`dir ${rel || '.'} [${entries.map((e) => e.name).join(' ')}]`),
23
+ onFile: (dir, name) => void events.push(`file ${dir ? `${dir}/${name}` : name}`),
24
+ onHole: (rel) => void events.push(`hole ${rel || '.'}`),
25
+ };
26
+ return { events, listener };
27
+ };
28
+
29
+ beforeEach(async () => {
30
+ root = await fs.mkdtemp(path.join(os.tmpdir(), 'kb-walk-'));
31
+ });
32
+ afterEach(() => fs.rm(root, { recursive: true, force: true }));
33
+
34
+ it('visits every folder once, in component order, skipping dot-entries and node_modules', async () => {
35
+ await write('Plugins/a-b/plugin.json');
36
+ await write('Plugins/a/b/plugin.json');
37
+ await write('KnowledgeBase/x.md');
38
+ await write('.git/HEAD');
39
+ await write('node_modules/dep/index.js');
40
+ await write('Plugins/.parked/plugin.json');
41
+ const { events, listener } = recorder();
42
+ const { holes } = await walkKb(root, [listener]);
43
+ expect(holes).toEqual([]);
44
+ expect(events).toEqual([
45
+ 'dir . [KnowledgeBase Plugins]',
46
+ 'dir KnowledgeBase [x.md]',
47
+ 'file KnowledgeBase/x.md',
48
+ 'dir Plugins [a a-b]', // "a" before "a-b": the walk order every consumer agrees on
49
+ 'dir Plugins/a [b]',
50
+ 'dir Plugins/a/b [plugin.json]',
51
+ 'file Plugins/a/b/plugin.json',
52
+ 'dir Plugins/a-b [plugin.json]',
53
+ 'file Plugins/a-b/plugin.json',
54
+ ]);
55
+ });
56
+
57
+ it('tells every listener the same things', async () => {
58
+ await write('Plugins/GTM/plugin.json');
59
+ await write('Skills/Eng/deploy/SKILL.md');
60
+ const a = recorder();
61
+ const b = recorder();
62
+ await walkKb(root, [a.listener, b.listener]);
63
+ expect(a.events.length).toBeGreaterThan(0);
64
+ expect(b.events).toEqual(a.events);
65
+ });
66
+
67
+ it('a folder that exists but cannot be listed is a hole — reported to every listener and in the result', async () => {
68
+ await write('Plugins/locked/plugin.json');
69
+ await write('Plugins/open/plugin.json');
70
+ const real = fs.readdir;
71
+ const spy = vi.spyOn(fs, 'readdir').mockImplementation(((dir: string, opts: unknown) =>
72
+ String(dir).endsWith('locked')
73
+ ? Promise.reject(Object.assign(new Error('EACCES: permission denied'), { code: 'EACCES' }))
74
+ : (real as (d: string, o: unknown) => Promise<unknown>).call(fs, dir, opts)) as never);
75
+ try {
76
+ const { events, listener } = recorder();
77
+ const { holes } = await walkKb(root, [listener]);
78
+ expect(holes).toEqual(['Plugins/locked']);
79
+ expect(events).toContain('hole Plugins/locked');
80
+ // The rest of the tree is still walked.
81
+ expect(events).toContain('file Plugins/open/plugin.json');
82
+ } finally {
83
+ spy.mockRestore();
84
+ }
85
+ });
86
+
87
+ it('a missing root is an empty walk; a folder that vanished between listing and visiting is not a hole', async () => {
88
+ const empty = recorder();
89
+ expect(await walkKb(path.join(root, 'nope'), [empty.listener])).toEqual({ holes: [] });
90
+ expect(empty.events).toEqual([]);
91
+
92
+ await write('Plugins/gone/plugin.json');
93
+ const real = fs.readdir;
94
+ const spy = vi.spyOn(fs, 'readdir').mockImplementation(((dir: string, opts: unknown) =>
95
+ String(dir).endsWith('gone')
96
+ ? Promise.reject(Object.assign(new Error('ENOENT: no such file'), { code: 'ENOENT' }))
97
+ : (real as (d: string, o: unknown) => Promise<unknown>).call(fs, dir, opts)) as never);
98
+ try {
99
+ const { events, listener } = recorder();
100
+ const { holes } = await walkKb(root, [listener]);
101
+ expect(holes).toEqual([]);
102
+ expect(events.some((e) => e.startsWith('hole'))).toBe(false);
103
+ } finally {
104
+ spy.mockRestore();
105
+ }
106
+ });
107
+ });
@@ -0,0 +1,24 @@
1
+ import { describe, it, expect } from 'vitest';
2
+ import { comparePathComponents } from '../path-order.js';
3
+
4
+ describe('comparePathComponents', () => {
5
+ it('orders component by component, the way a sorted directory walk visits paths', () => {
6
+ // A whole-string compare would weigh "-" against "/" and put a-b/x first.
7
+ expect(comparePathComponents('a/b/x', 'a-b/x')).toBeLessThan(0);
8
+ expect(comparePathComponents('Plugins/GTM', 'Plugins/GTM/skills')).toBeLessThan(0);
9
+ expect(comparePathComponents('Plugins/GTM', 'Plugins/GTM')).toBe(0);
10
+ });
11
+
12
+ it('never calls two distinct paths equal: a collation tie falls through to code-unit order', () => {
13
+ // Precomposed é and its decomposed spelling collate as equal; two folders
14
+ // so named must still have ONE first, or discovery and principal synthesis
15
+ // could each claim a different one for the same slug.
16
+ const composed = 'Plugins/café';
17
+ const decomposed = 'Plugins/café';
18
+ // (Whether the host's collation equates them is the host's business —
19
+ // the property under test holds either way: distinct paths never tie.)
20
+ const order = comparePathComponents(composed, decomposed);
21
+ expect(order).not.toBe(0);
22
+ expect(comparePathComponents(decomposed, composed)).toBe(-order);
23
+ });
24
+ });