@bevel-software/platform-core-backend 0.13.6 → 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 (456) 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 +101 -9
  8. package/dist/core/create-core-server.js.map +1 -1
  9. package/dist/core/create-core-services.d.ts +20 -1
  10. package/dist/core/create-core-services.d.ts.map +1 -1
  11. package/dist/core/create-core-services.js +105 -43
  12. package/dist/core/create-core-services.js.map +1 -1
  13. package/dist/core-config.d.ts.map +1 -1
  14. package/dist/core-config.js +12 -5
  15. package/dist/core-config.js.map +1 -1
  16. package/dist/modules/access/access-control.interface.d.ts +18 -6
  17. package/dist/modules/access/access-control.interface.d.ts.map +1 -1
  18. package/dist/modules/access/access-control.service.d.ts +26 -1
  19. package/dist/modules/access/access-control.service.d.ts.map +1 -1
  20. package/dist/modules/access/access-control.service.js +213 -71
  21. package/dist/modules/access/access-control.service.js.map +1 -1
  22. package/dist/modules/access/access.routes.d.ts.map +1 -1
  23. package/dist/modules/access/access.routes.js +59 -7
  24. package/dist/modules/access/access.routes.js.map +1 -1
  25. package/dist/modules/access-model/access-grammar.d.ts +51 -1
  26. package/dist/modules/access-model/access-grammar.d.ts.map +1 -1
  27. package/dist/modules/access-model/access-grammar.js +95 -1
  28. package/dist/modules/access-model/access-grammar.js.map +1 -1
  29. package/dist/modules/access-model/group-files.d.ts.map +1 -1
  30. package/dist/modules/access-model/group-files.js +4 -1
  31. package/dist/modules/access-model/group-files.js.map +1 -1
  32. package/dist/modules/access-model/kb-read-filter.d.ts +12 -0
  33. package/dist/modules/access-model/kb-read-filter.d.ts.map +1 -1
  34. package/dist/modules/access-model/kb-read-filter.js +15 -0
  35. package/dist/modules/access-model/kb-read-filter.js.map +1 -1
  36. package/dist/modules/access-model/plugin-principals.d.ts +49 -0
  37. package/dist/modules/access-model/plugin-principals.d.ts.map +1 -0
  38. package/dist/modules/access-model/plugin-principals.js +125 -0
  39. package/dist/modules/access-model/plugin-principals.js.map +1 -0
  40. package/dist/modules/connection-probe/connection-probe.contract.d.ts +43 -0
  41. package/dist/modules/connection-probe/connection-probe.contract.d.ts.map +1 -0
  42. package/dist/modules/connection-probe/connection-probe.contract.js +2 -0
  43. package/dist/modules/connection-probe/connection-probe.contract.js.map +1 -0
  44. package/dist/modules/connection-probe/connection-probe.service.d.ts +94 -0
  45. package/dist/modules/connection-probe/connection-probe.service.d.ts.map +1 -0
  46. package/dist/modules/connection-probe/connection-probe.service.js +684 -0
  47. package/dist/modules/connection-probe/connection-probe.service.js.map +1 -0
  48. package/dist/modules/connection-probe/index.d.ts +3 -0
  49. package/dist/modules/connection-probe/index.d.ts.map +1 -0
  50. package/dist/modules/connection-probe/index.js +3 -0
  51. package/dist/modules/connection-probe/index.js.map +1 -0
  52. package/dist/modules/database/core-schema.d.ts +326 -0
  53. package/dist/modules/database/core-schema.d.ts.map +1 -1
  54. package/dist/modules/database/core-schema.js +52 -0
  55. package/dist/modules/database/core-schema.js.map +1 -1
  56. package/dist/modules/diff/diff.routes.d.ts.map +1 -1
  57. package/dist/modules/diff/diff.routes.js +3 -5
  58. package/dist/modules/diff/diff.routes.js.map +1 -1
  59. package/dist/modules/kb-fs/mutex.d.ts +37 -0
  60. package/dist/modules/kb-fs/mutex.d.ts.map +1 -1
  61. package/dist/modules/kb-fs/mutex.js +48 -5
  62. package/dist/modules/kb-fs/mutex.js.map +1 -1
  63. package/dist/modules/kb-sync/index.d.ts +6 -0
  64. package/dist/modules/kb-sync/index.d.ts.map +1 -0
  65. package/dist/modules/kb-sync/index.js +5 -0
  66. package/dist/modules/kb-sync/index.js.map +1 -0
  67. package/dist/modules/kb-sync/kb-sync.interface.d.ts +77 -0
  68. package/dist/modules/kb-sync/kb-sync.interface.d.ts.map +1 -0
  69. package/dist/modules/kb-sync/kb-sync.interface.js +2 -0
  70. package/dist/modules/kb-sync/kb-sync.interface.js.map +1 -0
  71. package/dist/modules/kb-sync/kb-sync.routes.d.ts +70 -0
  72. package/dist/modules/kb-sync/kb-sync.routes.d.ts.map +1 -0
  73. package/dist/modules/kb-sync/kb-sync.routes.js +135 -0
  74. package/dist/modules/kb-sync/kb-sync.routes.js.map +1 -0
  75. package/dist/modules/kb-sync/kb-sync.service.d.ts +45 -0
  76. package/dist/modules/kb-sync/kb-sync.service.d.ts.map +1 -0
  77. package/dist/modules/kb-sync/kb-sync.service.js +183 -0
  78. package/dist/modules/kb-sync/kb-sync.service.js.map +1 -0
  79. package/dist/modules/kb-sync/sync-auth.d.ts +59 -0
  80. package/dist/modules/kb-sync/sync-auth.d.ts.map +1 -0
  81. package/dist/modules/kb-sync/sync-auth.js +62 -0
  82. package/dist/modules/kb-sync/sync-auth.js.map +1 -0
  83. package/dist/modules/kb-sync/sync-payload.d.ts +32 -0
  84. package/dist/modules/kb-sync/sync-payload.d.ts.map +1 -0
  85. package/dist/modules/kb-sync/sync-payload.js +105 -0
  86. package/dist/modules/kb-sync/sync-payload.js.map +1 -0
  87. package/dist/modules/marketplace/git-http.routes.d.ts +42 -0
  88. package/dist/modules/marketplace/git-http.routes.d.ts.map +1 -0
  89. package/dist/modules/marketplace/git-http.routes.js +190 -0
  90. package/dist/modules/marketplace/git-http.routes.js.map +1 -0
  91. package/dist/modules/marketplace/github-facade/github-facade-admin.routes.d.ts +23 -0
  92. package/dist/modules/marketplace/github-facade/github-facade-admin.routes.d.ts.map +1 -0
  93. package/dist/modules/marketplace/github-facade/github-facade-admin.routes.js +59 -0
  94. package/dist/modules/marketplace/github-facade/github-facade-admin.routes.js.map +1 -0
  95. package/dist/modules/marketplace/github-facade/github-facade-codes.store.d.ts +59 -0
  96. package/dist/modules/marketplace/github-facade/github-facade-codes.store.d.ts.map +1 -0
  97. package/dist/modules/marketplace/github-facade/github-facade-codes.store.js +96 -0
  98. package/dist/modules/marketplace/github-facade/github-facade-codes.store.js.map +1 -0
  99. package/dist/modules/marketplace/github-facade/github-facade-credentials.service.d.ts +99 -0
  100. package/dist/modules/marketplace/github-facade/github-facade-credentials.service.d.ts.map +1 -0
  101. package/dist/modules/marketplace/github-facade/github-facade-credentials.service.js +147 -0
  102. package/dist/modules/marketplace/github-facade/github-facade-credentials.service.js.map +1 -0
  103. package/dist/modules/marketplace/github-facade/github-facade.routes.d.ts +47 -0
  104. package/dist/modules/marketplace/github-facade/github-facade.routes.d.ts.map +1 -0
  105. package/dist/modules/marketplace/github-facade/github-facade.routes.js +228 -0
  106. package/dist/modules/marketplace/github-facade/github-facade.routes.js.map +1 -0
  107. package/dist/modules/marketplace/github-facade/github-facade.service.d.ts +109 -0
  108. package/dist/modules/marketplace/github-facade/github-facade.service.d.ts.map +1 -0
  109. package/dist/modules/marketplace/github-facade/github-facade.service.js +172 -0
  110. package/dist/modules/marketplace/github-facade/github-facade.service.js.map +1 -0
  111. package/dist/modules/marketplace/github-facade/index.d.ts +6 -0
  112. package/dist/modules/marketplace/github-facade/index.d.ts.map +1 -0
  113. package/dist/modules/marketplace/github-facade/index.js +6 -0
  114. package/dist/modules/marketplace/github-facade/index.js.map +1 -0
  115. package/dist/modules/marketplace/index.d.ts +4 -0
  116. package/dist/modules/marketplace/index.d.ts.map +1 -0
  117. package/dist/modules/marketplace/index.js +4 -0
  118. package/dist/modules/marketplace/index.js.map +1 -0
  119. package/dist/modules/marketplace/marketplace-repo.service.d.ts +122 -0
  120. package/dist/modules/marketplace/marketplace-repo.service.d.ts.map +1 -0
  121. package/dist/modules/marketplace/marketplace-repo.service.js +300 -0
  122. package/dist/modules/marketplace/marketplace-repo.service.js.map +1 -0
  123. package/dist/modules/mcp/oauth/bevel-oauth-provider.d.ts.map +1 -1
  124. package/dist/modules/mcp/oauth/bevel-oauth-provider.js +4 -0
  125. package/dist/modules/mcp/oauth/bevel-oauth-provider.js.map +1 -1
  126. package/dist/modules/mcp/oauth/oauth-consent.routes.d.ts +14 -8
  127. package/dist/modules/mcp/oauth/oauth-consent.routes.d.ts.map +1 -1
  128. package/dist/modules/mcp/oauth/oauth-consent.routes.js +13 -1
  129. package/dist/modules/mcp/oauth/oauth-consent.routes.js.map +1 -1
  130. package/dist/modules/mcp/oauth/oauth-state.d.ts +13 -2
  131. package/dist/modules/mcp/oauth/oauth-state.d.ts.map +1 -1
  132. package/dist/modules/mcp/oauth/oauth-state.js +3 -1
  133. package/dist/modules/mcp/oauth/oauth-state.js.map +1 -1
  134. package/dist/modules/plugins/compile/compile-marketplace.d.ts +97 -0
  135. package/dist/modules/plugins/compile/compile-marketplace.d.ts.map +1 -0
  136. package/dist/modules/plugins/compile/compile-marketplace.js +240 -0
  137. package/dist/modules/plugins/compile/compile-marketplace.js.map +1 -0
  138. package/dist/modules/plugins/compile/marketplace-compiler.service.d.ts +66 -0
  139. package/dist/modules/plugins/compile/marketplace-compiler.service.d.ts.map +1 -0
  140. package/dist/modules/plugins/compile/marketplace-compiler.service.js +132 -0
  141. package/dist/modules/plugins/compile/marketplace-compiler.service.js.map +1 -0
  142. package/dist/modules/plugins/discovery/bundle-dialect/bundle.source.d.ts +37 -0
  143. package/dist/modules/plugins/discovery/bundle-dialect/bundle.source.d.ts.map +1 -0
  144. package/dist/modules/plugins/discovery/bundle-dialect/bundle.source.js +135 -0
  145. package/dist/modules/plugins/discovery/bundle-dialect/bundle.source.js.map +1 -0
  146. package/dist/modules/plugins/discovery/bundle-dialect/registry.d.ts +45 -0
  147. package/dist/modules/plugins/discovery/bundle-dialect/registry.d.ts.map +1 -0
  148. package/dist/modules/plugins/discovery/bundle-dialect/registry.js +147 -0
  149. package/dist/modules/plugins/discovery/bundle-dialect/registry.js.map +1 -0
  150. package/dist/modules/plugins/discovery/kb-plugin-source.d.ts +37 -0
  151. package/dist/modules/plugins/discovery/kb-plugin-source.d.ts.map +1 -0
  152. package/dist/modules/plugins/discovery/kb-plugin-source.js +107 -0
  153. package/dist/modules/plugins/discovery/kb-plugin-source.js.map +1 -0
  154. package/dist/modules/plugins/discovery/native.source.d.ts +15 -0
  155. package/dist/modules/plugins/discovery/native.source.d.ts.map +1 -0
  156. package/dist/modules/plugins/discovery/native.source.js +102 -0
  157. package/dist/modules/plugins/discovery/native.source.js.map +1 -0
  158. package/dist/modules/plugins/discovery/plugin-source.d.ts +96 -0
  159. package/dist/modules/plugins/discovery/plugin-source.d.ts.map +1 -0
  160. package/dist/modules/plugins/discovery/plugin-source.js +2 -0
  161. package/dist/modules/plugins/discovery/plugin-source.js.map +1 -0
  162. package/dist/modules/plugins/index.d.ts +10 -1
  163. package/dist/modules/plugins/index.d.ts.map +1 -1
  164. package/dist/modules/plugins/index.js +8 -0
  165. package/dist/modules/plugins/index.js.map +1 -1
  166. package/dist/modules/plugins/plugin-links.d.ts +66 -0
  167. package/dist/modules/plugins/plugin-links.d.ts.map +1 -0
  168. package/dist/modules/plugins/plugin-links.js +160 -0
  169. package/dist/modules/plugins/plugin-links.js.map +1 -0
  170. package/dist/modules/plugins/plugin-links.service.d.ts +114 -0
  171. package/dist/modules/plugins/plugin-links.service.d.ts.map +1 -0
  172. package/dist/modules/plugins/plugin-links.service.js +304 -0
  173. package/dist/modules/plugins/plugin-links.service.js.map +1 -0
  174. package/dist/modules/plugins/plugin-provision.service.d.ts +39 -3
  175. package/dist/modules/plugins/plugin-provision.service.d.ts.map +1 -1
  176. package/dist/modules/plugins/plugin-provision.service.js +152 -52
  177. package/dist/modules/plugins/plugin-provision.service.js.map +1 -1
  178. package/dist/modules/plugins/plugin-rename.service.d.ts +69 -0
  179. package/dist/modules/plugins/plugin-rename.service.d.ts.map +1 -0
  180. package/dist/modules/plugins/plugin-rename.service.js +245 -0
  181. package/dist/modules/plugins/plugin-rename.service.js.map +1 -0
  182. package/dist/modules/plugins/plugins.contract.d.ts +27 -1
  183. package/dist/modules/plugins/plugins.contract.d.ts.map +1 -1
  184. package/dist/modules/plugins/plugins.routes.d.ts +7 -1
  185. package/dist/modules/plugins/plugins.routes.d.ts.map +1 -1
  186. package/dist/modules/plugins/plugins.routes.js +120 -12
  187. package/dist/modules/plugins/plugins.routes.js.map +1 -1
  188. package/dist/modules/plugins/plugins.service.d.ts +50 -3
  189. package/dist/modules/plugins/plugins.service.d.ts.map +1 -1
  190. package/dist/modules/plugins/plugins.service.js +108 -47
  191. package/dist/modules/plugins/plugins.service.js.map +1 -1
  192. package/dist/modules/secrets-vault/db-secrets-vault.service.js +1 -1
  193. package/dist/modules/secrets-vault/db-secrets-vault.service.js.map +1 -1
  194. package/dist/modules/secrets-vault/secrets-vault.routes.d.ts +6 -0
  195. package/dist/modules/secrets-vault/secrets-vault.routes.d.ts.map +1 -1
  196. package/dist/modules/secrets-vault/secrets-vault.routes.js +31 -1
  197. package/dist/modules/secrets-vault/secrets-vault.routes.js.map +1 -1
  198. package/dist/modules/settings/deployment-settings.service.d.ts +9 -0
  199. package/dist/modules/settings/deployment-settings.service.d.ts.map +1 -1
  200. package/dist/modules/settings/deployment-settings.service.js +82 -2
  201. package/dist/modules/settings/deployment-settings.service.js.map +1 -1
  202. package/dist/modules/settings/setup.routes.d.ts +26 -0
  203. package/dist/modules/settings/setup.routes.d.ts.map +1 -1
  204. package/dist/modules/settings/setup.routes.js +9 -1
  205. package/dist/modules/settings/setup.routes.js.map +1 -1
  206. package/dist/modules/skills/index.d.ts +1 -0
  207. package/dist/modules/skills/index.d.ts.map +1 -1
  208. package/dist/modules/skills/index.js +1 -0
  209. package/dist/modules/skills/index.js.map +1 -1
  210. package/dist/modules/skills/pending-skills.service.d.ts.map +1 -1
  211. package/dist/modules/skills/pending-skills.service.js +9 -4
  212. package/dist/modules/skills/pending-skills.service.js.map +1 -1
  213. package/dist/modules/skills/skill-access-requests.routes.d.ts +38 -0
  214. package/dist/modules/skills/skill-access-requests.routes.d.ts.map +1 -0
  215. package/dist/modules/skills/skill-access-requests.routes.js +168 -0
  216. package/dist/modules/skills/skill-access-requests.routes.js.map +1 -0
  217. package/dist/modules/skills/skills.contract.d.ts +31 -0
  218. package/dist/modules/skills/skills.contract.d.ts.map +1 -1
  219. package/dist/modules/skills/skills.routes.d.ts +28 -3
  220. package/dist/modules/skills/skills.routes.d.ts.map +1 -1
  221. package/dist/modules/skills/skills.routes.js +42 -3
  222. package/dist/modules/skills/skills.routes.js.map +1 -1
  223. package/dist/modules/skills/skills.service.d.ts +4 -0
  224. package/dist/modules/skills/skills.service.d.ts.map +1 -1
  225. package/dist/modules/skills/skills.service.js +83 -22
  226. package/dist/modules/skills/skills.service.js.map +1 -1
  227. package/dist/modules/tool-auth/external-api-key.interface.d.ts +18 -1
  228. package/dist/modules/tool-auth/external-api-key.interface.d.ts.map +1 -1
  229. package/dist/modules/tool-auth/external-api-key.interface.js +2 -1
  230. package/dist/modules/tool-auth/external-api-key.interface.js.map +1 -1
  231. package/dist/modules/tool-auth/external-api-key.service.d.ts +13 -4
  232. package/dist/modules/tool-auth/external-api-key.service.d.ts.map +1 -1
  233. package/dist/modules/tool-auth/external-api-key.service.js +26 -9
  234. package/dist/modules/tool-auth/external-api-key.service.js.map +1 -1
  235. package/dist/modules/tool-manuals/mcp-json-discovery.d.ts +36 -0
  236. package/dist/modules/tool-manuals/mcp-json-discovery.d.ts.map +1 -1
  237. package/dist/modules/tool-manuals/mcp-json-discovery.js +130 -83
  238. package/dist/modules/tool-manuals/mcp-json-discovery.js.map +1 -1
  239. package/dist/modules/tool-manuals/mcp-server-edit.service.d.ts.map +1 -1
  240. package/dist/modules/tool-manuals/mcp-server-edit.service.js +10 -4
  241. package/dist/modules/tool-manuals/mcp-server-edit.service.js.map +1 -1
  242. package/dist/modules/tool-manuals/tool-manuals.contract.d.ts +83 -0
  243. package/dist/modules/tool-manuals/tool-manuals.contract.d.ts.map +1 -1
  244. package/dist/modules/tool-manuals/tool-manuals.service.d.ts +15 -2
  245. package/dist/modules/tool-manuals/tool-manuals.service.d.ts.map +1 -1
  246. package/dist/modules/tool-manuals/tool-manuals.service.js +199 -38
  247. package/dist/modules/tool-manuals/tool-manuals.service.js.map +1 -1
  248. package/dist/modules/workflow/git/git.service.d.ts +58 -3
  249. package/dist/modules/workflow/git/git.service.d.ts.map +1 -1
  250. package/dist/modules/workflow/git/git.service.js +325 -60
  251. package/dist/modules/workflow/git/git.service.js.map +1 -1
  252. package/dist/modules/workflow/git/pull-request.service.d.ts +6 -0
  253. package/dist/modules/workflow/git/pull-request.service.d.ts.map +1 -1
  254. package/dist/modules/workflow/git/pull-request.service.js +8 -0
  255. package/dist/modules/workflow/git/pull-request.service.js.map +1 -1
  256. package/dist/modules/workflow/workflow.routes.d.ts +2 -1
  257. package/dist/modules/workflow/workflow.routes.d.ts.map +1 -1
  258. package/dist/modules/workflow/workflow.routes.js +98 -9
  259. package/dist/modules/workflow/workflow.routes.js.map +1 -1
  260. package/dist/modules/workflow/workflow.service.d.ts +133 -9
  261. package/dist/modules/workflow/workflow.service.d.ts.map +1 -1
  262. package/dist/modules/workflow/workflow.service.js +462 -52
  263. package/dist/modules/workflow/workflow.service.js.map +1 -1
  264. package/dist/modules/workspace/startup/steps/plugin-manifests.step.d.ts +35 -0
  265. package/dist/modules/workspace/startup/steps/plugin-manifests.step.d.ts.map +1 -0
  266. package/dist/modules/workspace/startup/steps/plugin-manifests.step.js +121 -0
  267. package/dist/modules/workspace/startup/steps/plugin-manifests.step.js.map +1 -0
  268. package/dist/modules/workspace/startup/steps/seed-tree.d.ts.map +1 -1
  269. package/dist/modules/workspace/startup/steps/seed-tree.js +46 -2
  270. package/dist/modules/workspace/startup/steps/seed-tree.js.map +1 -1
  271. package/dist/modules/workspace/startup/steps/template-files.step.d.ts.map +1 -1
  272. package/dist/modules/workspace/startup/steps/template-files.step.js +112 -25
  273. package/dist/modules/workspace/startup/steps/template-files.step.js.map +1 -1
  274. package/dist/modules/workspace/workspace.routes.d.ts.map +1 -1
  275. package/dist/modules/workspace/workspace.routes.js +11 -14
  276. package/dist/modules/workspace/workspace.routes.js.map +1 -1
  277. package/dist/modules/workspace/workspace.service.d.ts +38 -1
  278. package/dist/modules/workspace/workspace.service.d.ts.map +1 -1
  279. package/dist/modules/workspace/workspace.service.js +131 -20
  280. package/dist/modules/workspace/workspace.service.js.map +1 -1
  281. package/dist/shared/domain-errors.d.ts +20 -0
  282. package/dist/shared/domain-errors.d.ts.map +1 -1
  283. package/dist/shared/domain-errors.js +29 -0
  284. package/dist/shared/domain-errors.js.map +1 -1
  285. package/dist/shared/fs-errors.d.ts +8 -0
  286. package/dist/shared/fs-errors.d.ts.map +1 -0
  287. package/dist/shared/fs-errors.js +11 -0
  288. package/dist/shared/fs-errors.js.map +1 -0
  289. package/dist/shared/fs-walk.d.ts +20 -6
  290. package/dist/shared/fs-walk.d.ts.map +1 -1
  291. package/dist/shared/fs-walk.js +35 -27
  292. package/dist/shared/fs-walk.js.map +1 -1
  293. package/dist/shared/kb-walk.d.ts +52 -0
  294. package/dist/shared/kb-walk.d.ts.map +1 -0
  295. package/dist/shared/kb-walk.js +76 -0
  296. package/dist/shared/kb-walk.js.map +1 -0
  297. package/dist/shared/path-order.d.ts +18 -0
  298. package/dist/shared/path-order.d.ts.map +1 -0
  299. package/dist/shared/path-order.js +35 -0
  300. package/dist/shared/path-order.js.map +1 -0
  301. package/dist/shared/token-crypto.d.ts +17 -2
  302. package/dist/shared/token-crypto.d.ts.map +1 -1
  303. package/dist/shared/token-crypto.js +25 -8
  304. package/dist/shared/token-crypto.js.map +1 -1
  305. package/dist/shared/ttl-cache.d.ts +28 -1
  306. package/dist/shared/ttl-cache.d.ts.map +1 -1
  307. package/dist/shared/ttl-cache.js +33 -1
  308. package/dist/shared/ttl-cache.js.map +1 -1
  309. package/kb-template/.bevelignore +1 -1
  310. package/kb-template/AGENTS.md +95 -30
  311. package/kb-template/Skills/.gitkeep +0 -0
  312. package/migrations/0005_github_facade.sql +25 -0
  313. package/migrations/meta/0005_snapshot.json +1717 -0
  314. package/migrations/meta/_journal.json +7 -0
  315. package/package.json +6 -6
  316. package/src/__tests__/core-config.admin.test.ts +21 -0
  317. package/src/__tests__/kb-layout-config.test.ts +110 -0
  318. package/src/core/__tests__/catalog-cache-invalidation.test.ts +146 -0
  319. package/src/core/catalog-cache-invalidation.ts +92 -0
  320. package/src/core/create-core-server.ts +126 -7
  321. package/src/core/create-core-services.ts +173 -41
  322. package/src/core-config.ts +14 -7
  323. package/src/modules/access/__tests__/access-control.invalidate-race.test.ts +86 -0
  324. package/src/modules/access/__tests__/access-control.service.test.ts +19 -3
  325. package/src/modules/access/__tests__/access-plugin-principals.test.ts +368 -0
  326. package/src/modules/access/__tests__/groups-admin.service.test.ts +8 -0
  327. package/src/modules/access/access-control.interface.ts +20 -9
  328. package/src/modules/access/access-control.service.ts +240 -78
  329. package/src/modules/access/access.routes.ts +96 -14
  330. package/src/modules/access-model/__tests__/access-grammar.test.ts +18 -0
  331. package/src/modules/access-model/__tests__/plugin-token-canonical.test.ts +40 -0
  332. package/src/modules/access-model/access-grammar.ts +114 -2
  333. package/src/modules/access-model/group-files.ts +4 -0
  334. package/src/modules/access-model/kb-read-filter.ts +22 -0
  335. package/src/modules/access-model/plugin-principals.ts +137 -0
  336. package/src/modules/connection-probe/__tests__/connection-probe.service.test.ts +685 -0
  337. package/src/modules/connection-probe/connection-probe.contract.ts +44 -0
  338. package/src/modules/connection-probe/connection-probe.service.ts +734 -0
  339. package/src/modules/connection-probe/index.ts +2 -0
  340. package/src/modules/database/core-schema.ts +54 -0
  341. package/src/modules/diff/diff.routes.ts +9 -5
  342. package/src/modules/kb-fs/__tests__/mutex.test.ts +107 -0
  343. package/src/modules/kb-fs/mutex.ts +50 -5
  344. package/src/modules/kb-sync/__tests__/kb-sync.routes.test.ts +297 -0
  345. package/src/modules/kb-sync/__tests__/kb-sync.service.test.ts +295 -0
  346. package/src/modules/kb-sync/__tests__/sync-auth.test.ts +118 -0
  347. package/src/modules/kb-sync/__tests__/sync-payload.test.ts +148 -0
  348. package/src/modules/kb-sync/index.ts +19 -0
  349. package/src/modules/kb-sync/kb-sync.interface.ts +79 -0
  350. package/src/modules/kb-sync/kb-sync.routes.ts +197 -0
  351. package/src/modules/kb-sync/kb-sync.service.ts +201 -0
  352. package/src/modules/kb-sync/sync-auth.ts +118 -0
  353. package/src/modules/kb-sync/sync-payload.ts +143 -0
  354. package/src/modules/marketplace/__tests__/github-facade.test.ts +443 -0
  355. package/src/modules/marketplace/__tests__/marketplace-git.test.ts +179 -0
  356. package/src/modules/marketplace/git-http.routes.ts +193 -0
  357. package/src/modules/marketplace/github-facade/github-facade-admin.routes.ts +78 -0
  358. package/src/modules/marketplace/github-facade/github-facade-codes.store.ts +145 -0
  359. package/src/modules/marketplace/github-facade/github-facade-credentials.service.ts +208 -0
  360. package/src/modules/marketplace/github-facade/github-facade.routes.ts +268 -0
  361. package/src/modules/marketplace/github-facade/github-facade.service.ts +221 -0
  362. package/src/modules/marketplace/github-facade/index.ts +25 -0
  363. package/src/modules/marketplace/index.ts +3 -0
  364. package/src/modules/marketplace/marketplace-repo.service.ts +337 -0
  365. package/src/modules/mcp/oauth/bevel-oauth-provider.ts +3 -0
  366. package/src/modules/mcp/oauth/oauth-consent.routes.ts +24 -2
  367. package/src/modules/mcp/oauth/oauth-state.ts +15 -3
  368. package/src/modules/plugins/__tests__/bundle-dialect.e2e.test.ts +163 -0
  369. package/src/modules/plugins/__tests__/compile-marketplace.test.ts +294 -0
  370. package/src/modules/plugins/__tests__/plugin-index.service.test.ts +56 -12
  371. package/src/modules/plugins/__tests__/plugin-links.service.test.ts +287 -0
  372. package/src/modules/plugins/__tests__/plugin-provision.service.test.ts +205 -5
  373. package/src/modules/plugins/__tests__/plugin-rename.service.test.ts +394 -0
  374. package/src/modules/plugins/__tests__/plugins.routes.test.ts +80 -33
  375. package/src/modules/plugins/compile/compile-marketplace.ts +403 -0
  376. package/src/modules/plugins/compile/marketplace-compiler.service.ts +150 -0
  377. package/src/modules/plugins/discovery/__tests__/kb-plugin-source.test.ts +329 -0
  378. package/src/modules/plugins/discovery/bundle-dialect/bundle.source.ts +140 -0
  379. package/src/modules/plugins/discovery/bundle-dialect/registry.ts +175 -0
  380. package/src/modules/plugins/discovery/kb-plugin-source.ts +114 -0
  381. package/src/modules/plugins/discovery/native.source.ts +122 -0
  382. package/src/modules/plugins/discovery/plugin-source.ts +99 -0
  383. package/src/modules/plugins/index.ts +10 -0
  384. package/src/modules/plugins/plugin-links.service.ts +363 -0
  385. package/src/modules/plugins/plugin-links.ts +184 -0
  386. package/src/modules/plugins/plugin-provision.service.ts +160 -54
  387. package/src/modules/plugins/plugin-rename.service.ts +298 -0
  388. package/src/modules/plugins/plugins.contract.ts +28 -1
  389. package/src/modules/plugins/plugins.routes.ts +128 -11
  390. package/src/modules/plugins/plugins.service.ts +106 -53
  391. package/src/modules/secrets-vault/__tests__/connect-pending.route.test.ts +12 -0
  392. package/src/modules/secrets-vault/__tests__/oauth-return-to.route.test.ts +10 -3
  393. package/src/modules/secrets-vault/__tests__/tool-owner-gate.route.test.ts +26 -0
  394. package/src/modules/secrets-vault/db-secrets-vault.service.ts +1 -1
  395. package/src/modules/secrets-vault/secrets-vault.routes.ts +35 -1
  396. package/src/modules/settings/__tests__/deployment-settings.service.test.ts +57 -0
  397. package/src/modules/settings/__tests__/setup.routes.sync-status.test.ts +77 -0
  398. package/src/modules/settings/deployment-settings.service.ts +95 -2
  399. package/src/modules/settings/setup.routes.ts +24 -0
  400. package/src/modules/skills/__tests__/skill-access-requests.routes.test.ts +128 -0
  401. package/src/modules/skills/__tests__/skills.service.test.ts +85 -0
  402. package/src/modules/skills/index.ts +1 -0
  403. package/src/modules/skills/pending-skills.service.ts +7 -5
  404. package/src/modules/skills/skill-access-requests.routes.ts +199 -0
  405. package/src/modules/skills/skills.contract.ts +32 -0
  406. package/src/modules/skills/skills.routes.ts +59 -3
  407. package/src/modules/skills/skills.service.ts +84 -22
  408. package/src/modules/tool-auth/external-api-key.interface.ts +20 -1
  409. package/src/modules/tool-auth/external-api-key.service.ts +31 -13
  410. package/src/modules/tool-manuals/__tests__/mcp-json-discovery.test.ts +35 -1
  411. package/src/modules/tool-manuals/__tests__/tool-manuals.cli.test.ts +2 -0
  412. package/src/modules/tool-manuals/__tests__/tool-manuals.health-check.test.ts +104 -0
  413. package/src/modules/tool-manuals/__tests__/tool-manuals.service.test.ts +56 -0
  414. package/src/modules/tool-manuals/mcp-json-discovery.ts +154 -76
  415. package/src/modules/tool-manuals/mcp-server-edit.service.ts +13 -4
  416. package/src/modules/tool-manuals/tool-manuals.contract.ts +89 -0
  417. package/src/modules/tool-manuals/tool-manuals.service.ts +210 -37
  418. package/src/modules/workflow/__tests__/preserve-roles-yaml.test.ts +1 -1
  419. package/src/modules/workflow/__tests__/workflow.routes.history-read-gate.test.ts +139 -0
  420. package/src/modules/workflow/__tests__/workflow.service.branch-in-use.test.ts +337 -0
  421. package/src/modules/workflow/__tests__/workflow.service.commitFileWhileLocked.test.ts +1 -0
  422. package/src/modules/workflow/__tests__/workflow.service.deleted-branch-sweep.test.ts +7 -1
  423. package/src/modules/workflow/__tests__/workflow.service.facade.test.ts +97 -10
  424. package/src/modules/workflow/__tests__/workflow.service.releaseLock.test.ts +119 -2
  425. package/src/modules/workflow/__tests__/workflow.service.sync.test.ts +437 -0
  426. package/src/modules/workflow/git/__tests__/git.service.history-guards.test.ts +86 -0
  427. package/src/modules/workflow/git/__tests__/git.service.postMergePull.test.ts +3 -3
  428. package/src/modules/workflow/git/__tests__/git.service.pull.test.ts +47 -4
  429. package/src/modules/workflow/git/__tests__/git.service.syncFromRemote.test.ts +253 -0
  430. package/src/modules/workflow/git/git.service.ts +354 -63
  431. package/src/modules/workflow/git/pull-request.service.ts +9 -0
  432. package/src/modules/workflow/workflow.routes.ts +104 -4
  433. package/src/modules/workflow/workflow.service.ts +505 -64
  434. package/src/modules/workspace/__tests__/workspace.routes.read-gate.test.ts +6 -7
  435. package/src/modules/workspace/__tests__/workspace.service.list-cloned.test.ts +93 -0
  436. package/src/modules/workspace/__tests__/workspace.service.missing-branch.test.ts +28 -0
  437. package/src/modules/workspace/__tests__/workspace.service.read-filter.test.ts +42 -12
  438. package/src/modules/workspace/startup/steps/__tests__/steps.test.ts +219 -4
  439. package/src/modules/workspace/startup/steps/plugin-manifests.step.ts +129 -0
  440. package/src/modules/workspace/startup/steps/seed-tree.ts +44 -2
  441. package/src/modules/workspace/startup/steps/template-files.step.ts +131 -27
  442. package/src/modules/workspace/workspace.routes.ts +17 -21
  443. package/src/modules/workspace/workspace.service.ts +128 -19
  444. package/src/shared/__tests__/filename.test.ts +0 -0
  445. package/src/shared/__tests__/fs-walk.test.ts +22 -1
  446. package/src/shared/__tests__/kb-walk.test.ts +107 -0
  447. package/src/shared/__tests__/path-order.test.ts +24 -0
  448. package/src/shared/__tests__/token-crypto.test.ts +34 -0
  449. package/src/shared/__tests__/ttl-cache.test.ts +55 -0
  450. package/src/shared/domain-errors.ts +29 -0
  451. package/src/shared/fs-errors.ts +10 -0
  452. package/src/shared/fs-walk.ts +38 -23
  453. package/src/shared/kb-walk.ts +95 -0
  454. package/src/shared/path-order.ts +33 -0
  455. package/src/shared/token-crypto.ts +28 -10
  456. package/src/shared/ttl-cache.ts +33 -1
@@ -182,11 +182,10 @@ describe('read-permission gates on human read routes', () => {
182
182
 
183
183
  it('keeps the structural root folders visible even when nothing is readable', async () => {
184
184
  // A user with no read grants: the access batch denies everything. The
185
- // structural top-level folders (KnowledgeBase, Data, Agents, Pipelines,
186
- // Plugins) must still resolve readable (so the explorer renders its
187
- // section view), while their contents stay gated. The retired Skills/
188
- // and Tools/ roots get NO structural treatment — they are ordinary
189
- // (denied) folders now.
185
+ // structural top-level folders (KnowledgeBase, Skills, Data, Agents,
186
+ // Pipelines, Plugins) must still resolve readable (so the explorer renders
187
+ // its section view), while their contents stay gated. The retired Tools/
188
+ // root gets NO structural treatment — it is an ordinary (denied) folder.
190
189
  h = await makeHarness({ allowFn: () => false });
191
190
  expect((await get('/files')).status).toBe(200);
192
191
  const filter = h.listFilesFilter();
@@ -208,8 +207,8 @@ describe('read-permission gates on human read routes', () => {
208
207
  expect(verdict.get(`${KB}/Agents`)).toBe(true);
209
208
  expect(verdict.get(`${KB}/Pipelines`)).toBe(true);
210
209
  expect(verdict.get(`${KB}/Plugins`)).toBe(true);
211
- // Retired roots: no override, the deny-all batch verdict stands.
212
- expect(verdict.get(`${KB}/Skills`)).toBe(false);
210
+ expect(verdict.get(`${KB}/Skills`)).toBe(true);
211
+ // Retired root: no override, the deny-all batch verdict stands.
213
212
  expect(verdict.get(`${KB}/Tools`)).toBe(false);
214
213
  // Their contents: still gated (the override matches only the exact folder).
215
214
  expect(verdict.get(`${KB}/KnowledgeBase/Open/a.md`)).toBe(false);
@@ -0,0 +1,93 @@
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 { WorkspaceService } from '../workspace.service.js';
6
+
7
+ /**
8
+ * `listClonedWorkspaces` is what the remote sync enumerates. It must read the
9
+ * DISK, not the in-memory bootstrap cache: clones survive a restart, and a
10
+ * hook that fires before anyone has opened a branch must still find them.
11
+ */
12
+ describe('WorkspaceService.listClonedWorkspaces', () => {
13
+ let root: string;
14
+
15
+ beforeEach(async () => {
16
+ root = await fs.mkdtemp(path.join(os.tmpdir(), 'bevel-ws-list-'));
17
+ });
18
+
19
+ afterEach(async () => {
20
+ await fs.rm(root, { recursive: true, force: true });
21
+ });
22
+
23
+ async function clone(dirName: string, withGit = true) {
24
+ const repo = path.join(root, dirName, 'knowledge-base');
25
+ await fs.mkdir(withGit ? path.join(repo, '.git') : repo, { recursive: true });
26
+ }
27
+
28
+ it('finds every finished clone on disk without any branch having been opened this process', async () => {
29
+ await clone('main');
30
+ await clone('ali%2Fnew-skill');
31
+ // Half-built: the directory exists but the clone never finished.
32
+ await clone('juan%2Fabandoned', false);
33
+ // Not one of ours: the name is not the encoding of any branch.
34
+ await clone('weird%zz');
35
+ // A stray file at the root is ignored.
36
+ await fs.writeFile(path.join(root, 'notes.txt'), 'x');
37
+
38
+ const svc = new WorkspaceService(root, 'https://example.test/kb.git', 'knowledge-base');
39
+ const found = await svc.listClonedWorkspaces();
40
+ expect(found.sort((a, b) => a.branch.localeCompare(b.branch))).toEqual([
41
+ { id: 'ali%2Fnew-skill', branch: 'ali/new-skill' },
42
+ { id: 'main', branch: 'main' },
43
+ ]);
44
+ });
45
+
46
+ it('is empty when the workspaces root does not exist yet', async () => {
47
+ const svc = new WorkspaceService(path.join(root, 'missing'), 'https://example.test/kb.git', 'knowledge-base');
48
+ expect(await svc.listClonedWorkspaces()).toEqual([]);
49
+ });
50
+ });
51
+
52
+ describe('WorkspaceService.listClonedWorkspaces — what is not a clone', () => {
53
+ let root: string;
54
+ beforeEach(async () => {
55
+ root = await fs.mkdtemp(path.join(os.tmpdir(), 'bevel-ws-list-'));
56
+ });
57
+ afterEach(async () => {
58
+ await fs.rm(root, { recursive: true, force: true });
59
+ });
60
+
61
+ it('a directory whose name round-trips but is not a branch git accepts', async () => {
62
+ // `foo!`, `-leading-dash` and `a..b` survive encodeURIComponent unchanged, so
63
+ // only the validator can exclude them; `has space` does not round-trip and
64
+ // is excluded one step earlier. Both guards have to hold.
65
+ for (const name of ['main', 'foo!', '-leading-dash', 'a..b', 'has space']) {
66
+ await fs.mkdir(path.join(root, name, 'knowledge-base', '.git'), { recursive: true });
67
+ }
68
+ const svc = new WorkspaceService(root, 'https://example.test/kb.git', 'knowledge-base');
69
+ expect(await svc.listClonedWorkspaces()).toEqual([{ id: 'main', branch: 'main' }]);
70
+ });
71
+
72
+ it('a branch whose bootstrap is in flight right now', async () => {
73
+ await fs.mkdir(path.join(root, 'main', 'knowledge-base', '.git'), { recursive: true });
74
+ await fs.mkdir(path.join(root, 'ali%2Fx', 'knowledge-base', '.git'), { recursive: true });
75
+ const svc = new WorkspaceService(root, 'https://example.test/kb.git', 'knowledge-base');
76
+ // While a clone runs, `.git` exists but the tree is not checked out yet.
77
+ // The listing asks the public predicate, which is stubbed here rather than
78
+ // the tracker behind it — no public seam can hold a real clone open.
79
+ vi.spyOn(svc, 'isBootstrapInFlight').mockImplementation((branch) => branch === 'ali/x');
80
+ expect(await svc.listClonedWorkspaces()).toEqual([{ id: 'main', branch: 'main' }]);
81
+ });
82
+
83
+ it('a root that is a file counts as no root, not as a clone list', async () => {
84
+ // A FILE where the root should be: readdir fails with ENOTDIR on POSIX and
85
+ // ENOENT on Windows — both "no root", so an empty list. A permission
86
+ // failure is the case that must propagate, and there is no portable way
87
+ // to stage one in a temp dir; the code path is the same `throw err`.
88
+ const file = path.join(root, 'not-a-dir');
89
+ await fs.writeFile(file, 'x');
90
+ const svc = new WorkspaceService(file, 'https://example.test/kb.git', 'knowledge-base');
91
+ expect(await svc.listClonedWorkspaces()).toEqual([]);
92
+ });
93
+ });
@@ -0,0 +1,28 @@
1
+ import { describe, expect, it } from 'vitest';
2
+ import { isMissingRemoteBranchFailure } from '../../../shared/domain-errors.js';
3
+
4
+ /**
5
+ * The one line that decides whether a failed bootstrap is "this branch does
6
+ * not exist" (410, the browser says so) or "something is wrong" (500). Git's
7
+ * two wordings for a missing ref are matched; an unreachable host or a
8
+ * refused credential must NOT be, or a deployment outage would read as
9
+ * "your branch is gone".
10
+ */
11
+ describe('isMissingRemoteBranchFailure', () => {
12
+ it('recognises both of git’s wordings for a ref origin does not have', () => {
13
+ expect(
14
+ isMissingRemoteBranchFailure(
15
+ "Command failed: git clone -b sync-test/conflict-2 https://[REDACTED]@github.com/x/y.git /w\nfatal: Remote branch sync-test/conflict-2 not found in upstream origin",
16
+ ),
17
+ ).toBe(true);
18
+ expect(isMissingRemoteBranchFailure("fatal: couldn't find remote ref refs/heads/ali/x")).toBe(true);
19
+ });
20
+
21
+ it('does not mistake an unreachable host or a refused credential for a missing branch', () => {
22
+ expect(
23
+ isMissingRemoteBranchFailure("fatal: unable to access 'https://github.com/x/y.git/': Could not resolve host: github.com"),
24
+ ).toBe(false);
25
+ expect(isMissingRemoteBranchFailure('remote: Invalid username or password.\nfatal: Authentication failed')).toBe(false);
26
+ expect(isMissingRemoteBranchFailure('fatal: repository not found')).toBe(false);
27
+ });
28
+ });
@@ -38,8 +38,14 @@ describe('WorkspaceService.listFiles — read filter', () => {
38
38
  await mkFile(workspaceDir, 'Open/a.md');
39
39
  await mkFile(workspaceDir, 'Open/b.md');
40
40
  await mkFile(workspaceDir, 'Secret/s1.md');
41
+ await mkFile(workspaceDir, 'Secret/Vault/v1.md');
41
42
  await mkFile(workspaceDir, 'Mixed/hidden.md');
42
43
  await mkFile(workspaceDir, 'top.md');
44
+ // A denied folder with a grant below it: Scopes/ itself is closed, but
45
+ // Scopes/Deploy/ is shared (a linked skill's folder opened to a plugin's
46
+ // readers, say), and its files are readable.
47
+ await mkFile(workspaceDir, 'Scopes/Deploy/SKILL.md');
48
+ await mkFile(workspaceDir, 'Scopes/other.md');
43
49
  svc = new WorkspaceService(root, 'https://example.invalid/repo.git', KB);
44
50
  await svc.getWorkspacePath(workspaceId);
45
51
  });
@@ -64,27 +70,51 @@ describe('WorkspaceService.listFiles — read filter', () => {
64
70
  expect(filtered).toEqual(unfiltered);
65
71
  });
66
72
 
67
- it('drops denied files and denied directories (subtree skipped)', async () => {
68
- const asked: string[] = [];
69
- const filter: ReadTreeFilter = async (ps) => {
70
- asked.push(...ps);
71
- return new Map(ps.map((p) => [p, !(p.includes('Secret') || p.endsWith('hidden.md'))]));
72
- };
73
- const tree = await svc.listFiles(workspaceId, filter);
74
- const all = paths(tree);
73
+ it('drops denied files, and a denied directory with nothing readable beneath it', async () => {
74
+ const filter: ReadTreeFilter = async (ps) =>
75
+ new Map(ps.map((p) => [p, !(p.includes('Secret') || p.includes('Scopes') || p.endsWith('hidden.md'))]));
76
+ const all = paths(await svc.listFiles(workspaceId, filter));
75
77
 
76
78
  // Readable files survive.
77
79
  expect(all).toContain('Open/a.md');
78
80
  expect(all).toContain('Open/b.md');
79
81
  expect(all).toContain('top.md');
80
- // Denied directory and its whole subtree are gone.
82
+ // A denied directory whose whole subtree is denied is gone, with the
83
+ // subtree — including a nested folder the walk did look into.
81
84
  expect(all).not.toContain('Secret');
82
85
  expect(all).not.toContain('Secret/s1.md');
86
+ expect(all).not.toContain('Secret/Vault');
87
+ expect(all).not.toContain('Scopes');
83
88
  // Denied file is gone.
84
89
  expect(all).not.toContain('Mixed/hidden.md');
85
- // The denied directory's subtree is never walked — the filter is never
86
- // asked about a path inside Secret/.
87
- expect(asked.some((p) => p.startsWith('Secret/'))).toBe(false);
90
+ });
91
+
92
+ it('keeps a denied directory as the way to what is readable beneath it', async () => {
93
+ // Scopes/ is closed; Scopes/Deploy/ and its file are shared. The closed
94
+ // folder shows as a container — the reader can reach the skill through
95
+ // the tree — with its own contents still filtered (other.md is gone).
96
+ const filter: ReadTreeFilter = async (ps) =>
97
+ new Map(ps.map((p) => [p, p.startsWith('Scopes/Deploy') || (!p.includes('Secret') && !p.includes('Scopes') && !p.endsWith('hidden.md'))]));
98
+ const all = paths(await svc.listFiles(workspaceId, filter));
99
+ expect(all).toContain('Scopes');
100
+ expect(all).toContain('Scopes/Deploy');
101
+ expect(all).toContain('Scopes/Deploy/SKILL.md');
102
+ expect(all).not.toContain('Scopes/other.md');
103
+ // Unchanged for a subtree with nothing readable in it.
104
+ expect(all).not.toContain('Secret');
105
+ });
106
+
107
+ it('keeps a closed folder whose only readable content is an EMPTY sub-folder', async () => {
108
+ // A readable folder is kept on its own verdict, children or not — so the
109
+ // closed folder above it has a child, and stays as the way there.
110
+ await fs.mkdir(path.join(workspaceDir, 'Scopes', 'Empty'), { recursive: true });
111
+ const filter: ReadTreeFilter = async (ps) =>
112
+ new Map(ps.map((p) => [p, p === 'Scopes/Empty' || (!p.includes('Secret') && !p.includes('Scopes') && !p.endsWith('hidden.md'))]));
113
+ const all = paths(await svc.listFiles(workspaceId, filter));
114
+ expect(all).toContain('Scopes');
115
+ expect(all).toContain('Scopes/Empty');
116
+ expect(all).not.toContain('Scopes/Deploy');
117
+ expect(all).not.toContain('Scopes/other.md');
88
118
  });
89
119
 
90
120
  it('keeps a readable directory left empty after filtering (D4)', async () => {
@@ -7,11 +7,13 @@ import path from 'node:path';
7
7
  import { KbStartupRunner } from '../../kb-startup-runner.js';
8
8
  import type { OnServerStart, ServerStartContext, StepResult } from '../../on-server-start.js';
9
9
  import { GroupsToPluginsStep } from '../groups-to-plugins.step.js';
10
+ import { PluginManifestsStep } from '../plugin-manifests.step.js';
10
11
  import { RolesYamlStep } from '../roles-yaml.step.js';
11
12
  import { renderRolesYaml } from '../../../../access-model/render-roles-yaml.js';
12
13
  import { TemplateFilesStep } from '../template-files.step.js';
13
14
  import { buildSeedTree } from '../seed-tree.js';
14
15
  import { defaultKbTemplateDir } from '../../../../../assets.js';
16
+ import { DEFAULT_KB_LAYOUT, configureKbLayout, renderKbLayoutPlaceholders } from '@bevel-software/platform-shared';
15
17
 
16
18
  const execFileAsync = promisify(execFile);
17
19
 
@@ -54,6 +56,7 @@ beforeEach(async () => {
54
56
 
55
57
  afterEach(async () => {
56
58
  vi.restoreAllMocks();
59
+ configureKbLayout({ ...DEFAULT_KB_LAYOUT });
57
60
  await fs.rm(root, { recursive: true, force: true });
58
61
  });
59
62
 
@@ -109,8 +112,9 @@ async function exists(dir: string, rel: string): Promise<boolean> {
109
112
 
110
113
  const norm = (text: string) => text.replace(/\r\n?/g, '\n');
111
114
 
115
+ /** The template as the step writes it under the default layout — placeholders rendered. */
112
116
  async function template(name: string): Promise<string> {
113
- return fs.readFile(path.join(TEMPLATE_DIR, name), 'utf8');
117
+ return renderKbLayoutPlaceholders(await fs.readFile(path.join(TEMPLATE_DIR, name), 'utf8'), DEFAULT_KB_LAYOUT);
114
118
  }
115
119
 
116
120
  /** Every required file + reserved root already present, from the real template. */
@@ -122,6 +126,7 @@ async function fullScaffold(): Promise<Record<string, string>> {
122
126
  '.gitignore': await template('gitignore.template'),
123
127
  'KnowledgeBase/.gitkeep': '',
124
128
  'Plugins/.gitkeep': '',
129
+ 'Skills/.gitkeep': '',
125
130
  };
126
131
  }
127
132
 
@@ -144,12 +149,39 @@ describe('TemplateFilesStep', () => {
144
149
  // Reserved roots materialize as <dir>/.gitkeep.
145
150
  expect(await exists(dir, 'KnowledgeBase/.gitkeep')).toBe(true);
146
151
  expect(await exists(dir, 'Plugins/.gitkeep')).toBe(true);
152
+ expect(await exists(dir, 'Skills/.gitkeep')).toBe(true);
147
153
  const subject = (await git(dir, ['log', '--format=%s', '-1'])).trim();
148
154
  expect(subject).toMatch(/^Add missing KB scaffolding: /);
149
155
  expect(subject).toContain('.gitignore');
150
156
  }
151
157
  });
152
158
 
159
+ it('renders the managed files with the deployment\'s own root names', async () => {
160
+ // A deployment that renamed its roots must hand the agent a guide naming
161
+ // the folders it will find, and an ignore file hiding the real ones.
162
+ configureKbLayout({ knowledgeBaseDir: 'docs', skillsDir: 'skills', pluginsDir: 'plugins' });
163
+ await seedUpstream({ 'marker.txt': 'seeded' });
164
+ await makeRunner([new TemplateFilesStep()]).runAll();
165
+
166
+ const dir = await checkout(DEFAULT_BRANCH);
167
+ const agents = norm(await fs.readFile(path.join(dir, 'AGENTS.md'), 'utf8'));
168
+ expect(agents).toContain('plugins/<Plugin>/plugin.json');
169
+ expect(agents).toContain('`docs/`');
170
+ expect(agents).not.toContain('{{');
171
+ expect(agents).not.toContain('KnowledgeBase/');
172
+ const ignore = norm(await fs.readFile(path.join(dir, '.bevelignore'), 'utf8')).split('\n').map((l) => l.trim());
173
+ expect(ignore).toContain('plugins/');
174
+ // The skills root stays VISIBLE: the Skills & Tools sidebar reads it from the workspace tree.
175
+ expect(ignore).not.toContain('skills/');
176
+ expect(ignore).not.toContain('Plugins/');
177
+ expect(await exists(dir, 'docs/.gitkeep')).toBe(true);
178
+
179
+ // And a second boot sees the rendered guide as current: no churn commit.
180
+ await makeRunner([new TemplateFilesStep()]).runAll();
181
+ const again = await checkout(DEFAULT_BRANCH);
182
+ expect((await git(again, ['rev-list', '--count', 'HEAD'])).trim()).toBe('2'); // init + scaffolding
183
+ });
184
+
153
185
  it('replaces a drifted AGENTS.md, and says so when that is the only change', async () => {
154
186
  await seedUpstream({ ...(await fullScaffold()), 'AGENTS.md': 'stale conventions\n' });
155
187
  await makeRunner([new TemplateFilesStep()]).runAll();
@@ -184,20 +216,160 @@ describe('TemplateFilesStep', () => {
184
216
  const lines = norm(await fs.readFile(path.join(dir, '.bevelignore'), 'utf8')).split('\n').map((l) => l.trim());
185
217
  expect(lines).toContain('MyStuff/'); // the operator's rules survive
186
218
  expect(lines).toContain('AGENTS.md'); // the platform's rule was appended
219
+ expect(lines).not.toContain('Skills/'); // never the skills root — the Library's tree needs it
220
+ });
221
+
222
+ it('drops the Skills/ rule an earlier release appended, and its comment, keeping every other rule', async () => {
223
+ // A knowledge base whose ignore file was topped up by the release that
224
+ // hid the skills root: the platform's comment + line sit at the end. The
225
+ // Skills & Tools sidebar reads that root from the workspace tree now, so
226
+ // the rule comes out exactly as it went in — nothing of the operator's moves.
227
+ const scaffold = await fullScaffold();
228
+ scaffold['.bevelignore'] =
229
+ '# mine\n.git/\nAGENTS.md\nPlugins/\n\n# Added by the platform: the conventions doc is not node content.\nSkills/\n';
230
+ await seedUpstream(scaffold);
231
+
232
+ await makeRunner([new TemplateFilesStep()]).runAll();
233
+
234
+ const dir = await checkout(DEFAULT_BRANCH);
235
+ const text = norm(await fs.readFile(path.join(dir, '.bevelignore'), 'utf8'));
236
+ expect(text).toBe('# mine\n.git/\nAGENTS.md\nPlugins/\n');
237
+ // Idempotent: a second boot has nothing to change.
238
+ await makeRunner([new TemplateFilesStep()]).runAll();
239
+ const again = await checkout(DEFAULT_BRANCH);
240
+ expect(norm(await fs.readFile(path.join(again, '.bevelignore'), 'utf8'))).toBe(text);
241
+ });
242
+
243
+ it("drops the template's own Skills/ rule from a KB seeded by that release, comment included — whatever root the comment named", async () => {
244
+ // The previous template listed the rule under its own explanatory line,
245
+ // ending with the plugins root's name of the day; a deployment may have
246
+ // renamed that root since, so the line is known by its opening.
247
+ const scaffold = await fullScaffold();
248
+ scaffold['.bevelignore'] =
249
+ 'AGENTS.md\nPlugins/\n# The shared-skills root is rendered by the Skills & Tools app, like Groups/.\nSkills/\n';
250
+ await seedUpstream(scaffold);
251
+
252
+ await makeRunner([new TemplateFilesStep()]).runAll();
253
+
254
+ const dir = await checkout(DEFAULT_BRANCH);
255
+ expect(norm(await fs.readFile(path.join(dir, '.bevelignore'), 'utf8'))).toBe('AGENTS.md\nPlugins/\n');
256
+ });
257
+
258
+ it('recognises the legacy comment for a renamed plugins root with a space in its name', async () => {
259
+ // The name between the fixed opening and closing is judged by the one
260
+ // root-name rule the platform has, so every name it could have rendered
261
+ // there is recognised — and nothing a root cannot be called is.
262
+ const scaffold = await fullScaffold();
263
+ scaffold['.bevelignore'] =
264
+ 'AGENTS.md\nPlugins/\n# The shared-skills root is rendered by the Skills & Tools app, like My Plugins/.\nSkills/\n';
265
+ await seedUpstream(scaffold);
266
+
267
+ await makeRunner([new TemplateFilesStep()]).runAll();
268
+
269
+ const dir = await checkout(DEFAULT_BRANCH);
270
+ expect(norm(await fs.readFile(path.join(dir, '.bevelignore'), 'utf8'))).toBe('AGENTS.md\nPlugins/\n');
271
+ });
272
+
273
+ it("keeps an operator's Skills/ rule whose own comment merely opens like the platform's", async () => {
274
+ // Provenance is the platform's EXACT comment. A comment that begins the
275
+ // same way and goes on differently was never written by the platform.
276
+ const scaffold = await fullScaffold();
277
+ const text =
278
+ 'AGENTS.md\nPlugins/\n# The shared-skills root is rendered by the Skills & Tools app, and I hide it anyway\nSkills/\n';
279
+ scaffold['.bevelignore'] = text;
280
+ await seedUpstream(scaffold);
281
+
282
+ await makeRunner([new TemplateFilesStep()]).runAll();
283
+
284
+ const dir = await checkout(DEFAULT_BRANCH);
285
+ expect(norm(await fs.readFile(path.join(dir, '.bevelignore'), 'utf8'))).toBe(text);
286
+ });
287
+
288
+ it("keeps a Skills/ rule the operator wrote themselves — provenance is the platform's comment", async () => {
289
+ const scaffold = await fullScaffold();
290
+ scaffold['.bevelignore'] = 'AGENTS.md\nPlugins/\n# I hide skills on purpose\nSkills/\n';
291
+ await seedUpstream(scaffold);
292
+
293
+ await makeRunner([new TemplateFilesStep()]).runAll();
294
+
295
+ const dir = await checkout(DEFAULT_BRANCH);
296
+ expect(norm(await fs.readFile(path.join(dir, '.bevelignore'), 'utf8'))).toBe(
297
+ 'AGENTS.md\nPlugins/\n# I hide skills on purpose\nSkills/\n',
298
+ );
299
+ });
300
+
301
+ it('seeds a binary template file byte for byte and keeps a script executable — text is what decodes', async () => {
302
+ const customTemplate = path.join(root, 'custom-template-bytes');
303
+ // The packaged template as a base, plus two files it does not ship.
304
+ await fs.cp(TEMPLATE_DIR, customTemplate, { recursive: true });
305
+ await fs.mkdir(path.join(customTemplate, 'assets'), { recursive: true });
306
+ await fs.mkdir(path.join(customTemplate, 'scripts'), { recursive: true });
307
+ // Not UTF-8, and holding a NUL: text by no reading of the bytes. A
308
+ // name-based rule once sent this through the decoder and changed it.
309
+ const binary = Buffer.from([0x89, 0x50, 0x00, 0xff, 0xfe, 0x7b, 0x7b, 0x7d, 0x7d]);
310
+ await fs.writeFile(path.join(customTemplate, 'assets', '.logo.bin'), binary);
311
+ await fs.writeFile(path.join(customTemplate, 'scripts', 'run.sh'), '#!/bin/sh\necho {{skillsDir}}\n', { mode: 0o755 });
312
+ // The empty-remote seed is the one path that copies a whole template.
313
+ const dir = path.join(root, 'seeded-bytes');
314
+ await fs.mkdir(dir, { recursive: true });
315
+ await buildSeedTree(customTemplate, [], ['admin@example.com'])(dir);
316
+
317
+ expect(await fs.readFile(path.join(dir, 'assets', '.logo.bin'))).toEqual(binary);
318
+ expect(norm(await fs.readFile(path.join(dir, 'scripts', 'run.sh'), 'utf8'))).toBe('#!/bin/sh\necho Skills\n');
319
+ if (process.platform !== 'win32') {
320
+ expect((await fs.stat(path.join(dir, 'scripts', 'run.sh'))).mode & 0o111).not.toBe(0);
321
+ }
322
+ });
323
+
324
+ it("declares a custom template's ignore file without the stale Skills/ rule it still ships", async () => {
325
+ // A KB with NO ignore file gets the template's copy — and a distribution's
326
+ // template may still carry the rule the previous release had. The on-disk
327
+ // reconciliation never runs on an absent file, so the declared content
328
+ // must arrive already reconciled.
329
+ const customTemplate = path.join(root, 'custom-template-stale');
330
+ await fs.mkdir(customTemplate, { recursive: true });
331
+ await fs.writeFile(path.join(customTemplate, 'AGENTS.md'), await template('AGENTS.md'), 'utf8');
332
+ await fs.writeFile(
333
+ path.join(customTemplate, '.bevelignore'),
334
+ '# custom\nMyStuff/\n# The shared-skills root is rendered by the Skills & Tools app, like {{pluginsDir}}/.\n{{skillsDir}}/\n',
335
+ 'utf8',
336
+ );
337
+ const scaffold = await fullScaffold();
338
+ delete scaffold['.bevelignore'];
339
+ await seedUpstream(scaffold);
340
+
341
+ await makeRunner([new TemplateFilesStep()], customTemplate).runAll();
342
+
343
+ const dir = await checkout(DEFAULT_BRANCH);
344
+ const text = norm(await fs.readFile(path.join(dir, '.bevelignore'), 'utf8'));
345
+ expect(text).toContain('MyStuff/');
346
+ expect(text.split('\n').map((l) => l.trim())).not.toContain('Skills/');
347
+ expect(text).not.toContain('shared-skills root');
348
+ });
349
+
350
+ it("leaves an operator's !Skills/ negation alone — there is nothing of the platform's to remove", async () => {
351
+ const scaffold = await fullScaffold();
352
+ scaffold['.bevelignore'] = 'AGENTS.md\nPlugins/\n!Skills/\n';
353
+ await seedUpstream(scaffold);
354
+
355
+ await makeRunner([new TemplateFilesStep()]).runAll();
356
+
357
+ const dir = await checkout(DEFAULT_BRANCH);
358
+ expect(norm(await fs.readFile(path.join(dir, '.bevelignore'), 'utf8'))).toBe('AGENTS.md\nPlugins/\n!Skills/\n');
187
359
  });
188
360
 
189
361
  it('respects an explicit !AGENTS.md negation — hiding the doc is a default, not a mandate', async () => {
190
362
  // Appending the positive rule after the negation would WIN under ordered
191
363
  // matching and silently defeat the operator's stated choice to show it.
192
364
  const scaffold = await fullScaffold();
193
- scaffold['.bevelignore'] = '# operator wants the doc visible\n!AGENTS.md\n';
365
+ scaffold['.bevelignore'] = '# operator wants the doc visible\n!AGENTS.md\nPlugins/\n';
194
366
  await seedUpstream(scaffold);
195
367
 
196
368
  await makeRunner([new TemplateFilesStep()]).runAll();
197
369
 
198
370
  const dir = await checkout(DEFAULT_BRANCH);
199
371
  expect(norm(await fs.readFile(path.join(dir, '.bevelignore'), 'utf8'))).toBe(
200
- '# operator wants the doc visible\n!AGENTS.md\n',
372
+ '# operator wants the doc visible\n!AGENTS.md\nPlugins/\n',
201
373
  );
202
374
  });
203
375
 
@@ -294,11 +466,54 @@ describe('buildSeedTree', () => {
294
466
  expect(await fs.readFile(path.join(dest, 'docs/guide.md'), 'utf8')).toBe('guide');
295
467
  expect(await fs.readFile(path.join(dest, 'access.md'), 'utf8')).toBe('policy');
296
468
  // The generated paths — what the runner force-adds past a template .gitignore.
297
- expect(generated.sort()).toEqual(['KnowledgeBase/.gitkeep', 'Plugins/.gitkeep', 'roles.yaml']);
469
+ expect(generated.sort()).toEqual(['KnowledgeBase/.gitkeep', 'Plugins/.gitkeep', 'Skills/.gitkeep', 'roles.yaml']);
298
470
  expect(await exists(dest, 'roles.yaml')).toBe(true);
299
471
  });
300
472
  });
301
473
 
474
+ describe('PluginManifestsStep', () => {
475
+ it('writes plugin.json into legacy plugin folders on every branch, and leaves scopes and real plugins alone', async () => {
476
+ const scaffold = await fullScaffold();
477
+ await seedUpstream({
478
+ ...scaffold,
479
+ // Legacy shapes: access.md only; mcp.json only; a bare skill tree.
480
+ 'Plugins/GTM/access.md': '---\n---\nread:\n - everyone\n',
481
+ 'Plugins/GTM/outreach/SKILL.md': '---\ndescription: x\n---\n',
482
+ 'Plugins/Servers/mcp.json': '{"mcpServers":{}}',
483
+ 'Plugins/Bare/deploy/SKILL.md': '---\ndescription: y\n---\n',
484
+ // Already a plugin, both shapes.
485
+ 'Plugins/Modern/plugin.json': '{"name":"modern"}',
486
+ 'Plugins/functional/cluster/example/plugin.bundle.json': '{"name":"example"}',
487
+ // A scope with rules of its own above a bundle: not a plugin.
488
+ 'Plugins/functional/access.md': '---\n---\nread:\n - everyone\n',
489
+ // Nothing plugin-shaped at all.
490
+ 'Plugins/notes/README.md': 'just a folder',
491
+ });
492
+
493
+ await makeRunner([new PluginManifestsStep()]).runAll();
494
+
495
+ for (const branch of PROTECTED) {
496
+ const dir = await checkout(branch);
497
+ for (const legacy of ['GTM', 'Servers', 'Bare']) {
498
+ const manifest = JSON.parse(await fs.readFile(path.join(dir, `Plugins/${legacy}/plugin.json`), 'utf8'));
499
+ expect(manifest.name).toBe(legacy.toLowerCase());
500
+ }
501
+ expect(await exists(dir, 'Plugins/functional/plugin.json')).toBe(false);
502
+ expect(await exists(dir, 'Plugins/notes/plugin.json')).toBe(false);
503
+ expect(await fs.readFile(path.join(dir, 'Plugins/Modern/plugin.json'), 'utf8')).toBe('{"name":"modern"}');
504
+ }
505
+ const dir = await checkout(DEFAULT_BRANCH);
506
+ const log = (await git(dir, ['log', '-1', '--format=%B'])).trim();
507
+ expect(log).toContain('Add plugin manifests to 3 legacy plugin folders');
508
+ expect(log).toContain('Plugins/GTM: plugin.json written');
509
+
510
+ // Idempotent: nothing left to write on the next boot.
511
+ await makeRunner([new PluginManifestsStep()]).runAll();
512
+ const again = await checkout(DEFAULT_BRANCH);
513
+ expect((await git(again, ['rev-list', '--count', 'HEAD'])).trim()).toBe('2'); // init + one migration commit
514
+ });
515
+ });
516
+
302
517
  describe('GroupsToPluginsStep', () => {
303
518
  it('migrates a Groups/ tree to the Plugins layout — on the protected branches AND a draft', async () => {
304
519
  const seed = await seedUpstream({
@@ -0,0 +1,129 @@
1
+ import fs from 'node:fs/promises';
2
+ import path from 'node:path';
3
+ import {
4
+ HEXIS_EXTENSION_NS,
5
+ PLUGINS_DIR,
6
+ PLUGIN_MANIFEST_FILE,
7
+ PLUGIN_MCP_FILE,
8
+ PLUGIN_SKILLS_DIR,
9
+ renderPluginManifest,
10
+ } from '@bevel-software/platform-shared';
11
+ import { BUNDLE_FILE } from '../../../plugins/discovery/bundle-dialect/bundle.source.js';
12
+ import { isAbsence } from '../../../../shared/fs-errors.js';
13
+ import type { KbBranch, OnServerStart, ServerStartContext, StepResult } from '../on-server-start.js';
14
+
15
+ /**
16
+ * Give every legacy plugin folder its manifest, as an {@link OnServerStart}
17
+ * step.
18
+ *
19
+ * A plugin IS a folder carrying `plugin.json` (or a `plugin.bundle.json`, the
20
+ * read-only customer dialect) — discovery reads nothing else, at any depth.
21
+ * Folders from before the manifest existed have `access.md`, an `mcp.json`,
22
+ * a `skills/` tree or `.tool` manuals and no `plugin.json` beside them; the
23
+ * old scanners read those by position (directly under the plugins root),
24
+ * and this step is what retires that rule: it writes the minimal manifest
25
+ * into each such folder, once, so the position never has to mean anything
26
+ * again.
27
+ *
28
+ * WHICH folders: walk the plugins root; a folder holding either file is a
29
+ * plugin and is not entered. Any other folder is entered first. A folder
30
+ * DIRECTLY under the root — the only place the legacy layout ever put a
31
+ * plugin — becomes one when nothing plugin-shaped lives beneath it and its
32
+ * own content is legacy content: a scope folder (the dialect's
33
+ * `plugins/functional/…`) can carry an `access.md` of its own and must stay
34
+ * a scope, and a skill folder inside a legacy plugin must not become a
35
+ * plugin of its own. "Legacy content" is the set of things provisioning and
36
+ * the old migration ever put in a plugin folder: `access.md`, `mcp.json`,
37
+ * `skills/`, the hexis extension directory, a `.tool` file, or a `SKILL.md`
38
+ * anywhere beneath (the pre-`skills/` shape).
39
+ *
40
+ * Every branch, drafts included, like the Groups→Plugins migration and for
41
+ * the same reason: a draft migrated alongside its target diffs by the user's
42
+ * own changes only. Idempotent: a folder that has its manifest is skipped.
43
+ */
44
+ export class PluginManifestsStep implements OnServerStart {
45
+ readonly name = 'plugin-manifests';
46
+
47
+ async run(ctx: ServerStartContext): Promise<StepResult> {
48
+ for (const branch of await ctx.allBranches()) {
49
+ await addManifests(branch);
50
+ }
51
+ return { outcome: 'ok' };
52
+ }
53
+ }
54
+
55
+ async function addManifests(branch: KbBranch): Promise<void> {
56
+ const repoDir = await branch.repoDir();
57
+ const root = path.join(repoDir, PLUGINS_DIR);
58
+ const added: string[] = [];
59
+
60
+ /** Resolves to how many plugins sit at or beneath `dir`. */
61
+ const visit = async (dir: string, rel: string): Promise<number> => {
62
+ if (rel && ((await isFile(path.join(dir, PLUGIN_MANIFEST_FILE))) || (await isFile(path.join(dir, BUNDLE_FILE))))) {
63
+ return 1;
64
+ }
65
+ let entries: import('node:fs').Dirent[];
66
+ try {
67
+ entries = await fs.readdir(dir, { withFileTypes: true });
68
+ } catch (err) {
69
+ // Absence is the plugins root not existing yet; anything else must stop
70
+ // the boot rather than quietly leave legacy plugins without manifests.
71
+ if (isAbsence(err)) return 0;
72
+ throw err;
73
+ }
74
+ let beneath = 0;
75
+ for (const entry of entries.filter((e) => e.isDirectory() && !e.name.startsWith('.'))) {
76
+ beneath += await visit(path.join(dir, entry.name), rel ? `${rel}/${entry.name}` : entry.name);
77
+ }
78
+ if (beneath > 0 || !rel || rel.includes('/')) return beneath;
79
+ if (!(await looksLikeLegacyPlugin(dir, entries))) return 0;
80
+ const manifestRel = `${PLUGINS_DIR}/${rel}/${PLUGIN_MANIFEST_FILE}`;
81
+ branch.write(manifestRel, renderPluginManifest(path.posix.basename(rel)));
82
+ added.push(rel);
83
+ return 1;
84
+ };
85
+
86
+ await visit(root, '');
87
+ if (added.length === 0) return;
88
+ branch.note(`Add plugin manifests to ${added.length === 1 ? 'a legacy plugin folder' : `${added.length} legacy plugin folders`}`);
89
+ for (const rel of added) branch.note(`${PLUGINS_DIR}/${rel}: ${PLUGIN_MANIFEST_FILE} written`);
90
+ }
91
+
92
+ async function looksLikeLegacyPlugin(dir: string, entries: import('node:fs').Dirent[]): Promise<boolean> {
93
+ for (const entry of entries) {
94
+ if (entry.isFile() && (entry.name === 'access.md' || entry.name === PLUGIN_MCP_FILE || entry.name.toLowerCase().endsWith('.tool'))) {
95
+ return true;
96
+ }
97
+ if (entry.isDirectory() && (entry.name === PLUGIN_SKILLS_DIR || entry.name === HEXIS_EXTENSION_NS)) return true;
98
+ }
99
+ return hasSkillBeneath(dir);
100
+ }
101
+
102
+ /** The pre-`skills/` shape: `Plugins/<Plugin>/<skill>/SKILL.md`, at any depth. */
103
+ async function hasSkillBeneath(dir: string): Promise<boolean> {
104
+ let entries: import('node:fs').Dirent[];
105
+ try {
106
+ entries = await fs.readdir(dir, { withFileTypes: true });
107
+ } catch (err) {
108
+ if (isAbsence(err)) return false;
109
+ throw err;
110
+ }
111
+ if (entries.some((e) => e.isFile() && e.name === 'SKILL.md')) return true;
112
+ for (const entry of entries) {
113
+ if (entry.isDirectory() && !entry.name.startsWith('.') && (await hasSkillBeneath(path.join(dir, entry.name)))) {
114
+ return true;
115
+ }
116
+ }
117
+ return false;
118
+ }
119
+
120
+ async function isFile(abs: string): Promise<boolean> {
121
+ return fs.stat(abs).then(
122
+ (s) => s.isFile(),
123
+ (err: unknown) => {
124
+ if (isAbsence(err)) return false;
125
+ throw err; // a probe that fails for another reason is not "no manifest"
126
+ },
127
+ );
128
+ }
129
+