@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
@@ -0,0 +1,394 @@
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 { DEFAULT_BRANCH, type AuthUser } from '@bevel-software/platform-shared';
6
+
7
+ import type { WorkspaceService } from '../../workspace/workspace.service.js';
8
+ import { AccessControlService } from '../../access/access-control.service.js';
9
+ import { PushNeedsAgentResolutionError } from '../../../shared/domain-errors.js';
10
+ import { workspaceIdForBranch } from '../../../shared/workspace-id.js';
11
+ import { KbPluginSource } from '../discovery/kb-plugin-source.js';
12
+ import { PluginRenameError, PluginRenameService, renamePluginPrincipalInText } from '../plugin-rename.service.js';
13
+
14
+ // The one walk of the checkout, with a switch that makes it report a hole —
15
+ // the way a folder it cannot list would — to every listener on it.
16
+ const walkMock = vi.hoisted(() => ({ holeInTheWalk: false }));
17
+ vi.mock('../../../shared/kb-walk.js', async (importOriginal) => {
18
+ const actual = await importOriginal<typeof import('../../../shared/kb-walk.js')>();
19
+ return {
20
+ ...actual,
21
+ walkKb: async (root: string, listeners: readonly import('../../../shared/kb-walk.js').KbWalkListener[]) => {
22
+ const result = await actual.walkKb(root, listeners);
23
+ if (walkMock.holeInTheWalk) {
24
+ const err = Object.assign(new Error('EACCES: permission denied'), { code: 'EACCES' });
25
+ for (const l of listeners) await l.onHole?.('KnowledgeBase/Notes', err);
26
+ result.holes.push('KnowledgeBase/Notes');
27
+ }
28
+ return result;
29
+ },
30
+ };
31
+ });
32
+
33
+ /**
34
+ * Renaming over a real tree: the real resolver decides who may rename and
35
+ * who still reads afterwards, real discovery finds the plugin, and the commit
36
+ * driver is the only thing stubbed (it records what would land).
37
+ */
38
+
39
+ const KB_DIR = 'knowledge-base';
40
+ const wsId = workspaceIdForBranch(DEFAULT_BRANCH);
41
+
42
+ const manager: AuthUser = { id: 'u-mia', email: 'mia@x.io', name: 'Mia' } as AuthUser;
43
+ const member: AuthUser = { id: 'u-sam', email: 'sam@x.io', name: 'Sam' } as AuthUser;
44
+
45
+ const GTM_RULES = '---\nread:\n - everyone\n---\nread:\n - Sam <sam@x.io>\nwrite:\n - Mia <mia@x.io>\nowner:\n - Mia <mia@x.io>\n';
46
+ const GTM_MANIFEST = '{\n "name": "gtm",\n "version": "1.0.0"\n}\n';
47
+ // The linked skill's grants, in BOTH spellings a knowledge base may hold:
48
+ // the folder's casing from before the manifest became the identity, and the
49
+ // identifier itself.
50
+ const DEPLOY_RULES = '---\n---\nread:\n - plugin/GTM/read\nwrite:\n - plugin/gtm/write\n';
51
+
52
+ describe('PluginRenameService', () => {
53
+ let root: string;
54
+ let repo: string;
55
+ let commits: { summary: string; paths: string[] }[];
56
+ let failNextCommit: Error | null;
57
+ let failWriteMatching: string | null;
58
+ let invalidated: number;
59
+ let access: AccessControlService;
60
+ let svc: PluginRenameService;
61
+
62
+ const write = async (rel: string, text: string) => {
63
+ const abs = path.join(repo, rel);
64
+ await fs.mkdir(path.dirname(abs), { recursive: true });
65
+ await fs.writeFile(abs, text);
66
+ };
67
+ const read = (rel: string) => fs.readFile(path.join(repo, rel), 'utf-8');
68
+ const manifest = async () => JSON.parse(await read('Plugins/GTM/plugin.json'));
69
+
70
+ beforeEach(async () => {
71
+ root = await fs.mkdtemp(path.join(os.tmpdir(), 'bevel-rename-'));
72
+ repo = path.join(root, wsId, KB_DIR);
73
+ commits = [];
74
+ failNextCommit = null;
75
+ failWriteMatching = null;
76
+ walkMock.holeInTheWalk = false;
77
+ invalidated = 0;
78
+ const workspaceService = {
79
+ getOrCreateForBranch: async () => ({ id: wsId }),
80
+ getWorkspacePath: async (id: string) => path.join(root, id),
81
+ readFile: async (id: string, rel: string) => fs.readFile(path.join(root, id, rel), 'utf-8'),
82
+ writeFile: async (id: string, rel: string, text: string) => {
83
+ if (failWriteMatching && rel.endsWith(failWriteMatching)) {
84
+ failWriteMatching = null;
85
+ throw new Error('disk full');
86
+ }
87
+ const abs = path.join(root, id, rel);
88
+ await fs.mkdir(path.dirname(abs), { recursive: true });
89
+ await fs.writeFile(abs, text);
90
+ },
91
+ ensureRemotesFetched: async () => undefined,
92
+ } as unknown as WorkspaceService;
93
+ const driver = {
94
+ commitChanges: async (_ws: string, _user: AuthUser, summary: string, paths: string[]) => {
95
+ if (failNextCommit) {
96
+ const err = failNextCommit;
97
+ failNextCommit = null;
98
+ throw err;
99
+ }
100
+ commits.push({ summary, paths });
101
+ },
102
+ };
103
+
104
+ await write('roles.yaml', 'roles:\n Admin:\n - admin@x.io\n');
105
+ await write('access.md', '---\nwrite:\n - Admin\n---\n');
106
+ await write('Plugins/GTM/access.md', GTM_RULES);
107
+ await write('Plugins/GTM/plugin.json', GTM_MANIFEST);
108
+ // A scope Mia edits, holding the skill GTM is granted on.
109
+ await write('Skills/Eng/access.md', '---\n---\nwrite:\n - Mia <mia@x.io>\n');
110
+ await write('Skills/Eng/deploy/access.md', DEPLOY_RULES);
111
+ await write('Skills/Eng/deploy/SKILL.md', '---\ndescription: Ship it.\n---\n');
112
+
113
+ access = new AccessControlService(workspaceService, KB_DIR);
114
+ svc = new PluginRenameService(workspaceService, driver, access, new KbPluginSource(), KB_DIR, undefined, () => {
115
+ invalidated += 1;
116
+ });
117
+ });
118
+ afterEach(() => fs.rm(root, { recursive: true, force: true }));
119
+
120
+ it('refuses to claim a name against a listing with a hole in it — an unreadable folder may hold that very plugin', async () => {
121
+ // Discovery as the walker reports it when a folder exists but could not
122
+ // be read: the plugins it did see, plus the hole.
123
+ const real = new KbPluginSource();
124
+ const holed = {
125
+ dialect: 'kb',
126
+ discover: async (root: string) => ({ ...(await real.discover(root)), unreadable: ['Plugins/Hidden'] }),
127
+ };
128
+ const svcOverHole = new PluginRenameService(
129
+ { getWorkspacePath: async (id: string) => path.join(root, id) } as unknown as WorkspaceService,
130
+ { commitChanges: async () => { throw new Error('must not commit'); } },
131
+ access,
132
+ holed,
133
+ KB_DIR,
134
+ );
135
+ await expect(svcOverHole.rename(manager, 'gtm', { name: 'go-to-market' })).rejects.toMatchObject({
136
+ status: 503,
137
+ payload: { kind: 'incomplete-discovery', unreadable: ['Plugins/Hidden'] },
138
+ });
139
+ expect(await read('Plugins/GTM/plugin.json')).toBe(GTM_MANIFEST);
140
+ expect(await read('Skills/Eng/deploy/access.md')).toBe(DEPLOY_RULES);
141
+ });
142
+
143
+ it('a display-name change claims nothing, so a hole elsewhere does not stop it', async () => {
144
+ walkMock.holeInTheWalk = true;
145
+ await svc.rename(manager, 'gtm', { displayName: 'Go To Market' });
146
+ expect((await manifest()).displayName).toBe('Go To Market');
147
+ expect(commits).toHaveLength(1);
148
+ // The same hole still refuses an identifier change.
149
+ await expect(svc.rename(manager, 'gtm', { name: 'go-to-market' })).rejects.toMatchObject({ status: 503 });
150
+ });
151
+
152
+ it('tells a NON-manager nothing about the tree — a holed discovery is still just "unknown plugin" to them', async () => {
153
+ const real = new KbPluginSource();
154
+ const holed = {
155
+ dialect: 'kb',
156
+ discover: async (root: string) => ({ ...(await real.discover(root)), unreadable: ['Plugins/Hidden'] }),
157
+ };
158
+ const svcOverHole = new PluginRenameService(
159
+ { getWorkspacePath: async (id: string) => path.join(root, id) } as unknown as WorkspaceService,
160
+ { commitChanges: async () => { throw new Error('must not commit'); } },
161
+ access,
162
+ holed,
163
+ KB_DIR,
164
+ );
165
+ const refusal = await svcOverHole.rename(member, 'gtm', { name: 'go-to-market' }).catch((e: unknown) => e);
166
+ expect(refusal).toMatchObject({ status: 404, payload: { kind: 'unknown-plugin' } });
167
+ expect(JSON.stringify(refusal)).not.toContain('Hidden');
168
+ });
169
+
170
+ it('a grant file it cannot open is the same hole as a folder it cannot list: refused before any write', async () => {
171
+ const tool = 'Plugins/GTM/software.bevel.hexis/tools/web.tool';
172
+ await write(tool, '---\nname: web\nread:\n - plugin/gtm/read\n---\nbody\n');
173
+ const realReadFile = fs.readFile;
174
+ const spy = vi.spyOn(fs, 'readFile').mockImplementation(((file: string, opts: unknown) =>
175
+ String(file).endsWith('web.tool')
176
+ ? Promise.reject(Object.assign(new Error('EACCES: permission denied'), { code: 'EACCES' }))
177
+ : (realReadFile as (f: string, o: unknown) => Promise<unknown>).call(fs, file, opts)) as never);
178
+ try {
179
+ await expect(svc.rename(manager, 'gtm', { name: 'go-to-market' })).rejects.toMatchObject({
180
+ status: 503,
181
+ payload: { kind: 'incomplete-discovery', unreadable: [tool] },
182
+ });
183
+ } finally {
184
+ spy.mockRestore();
185
+ }
186
+ expect(await read('Plugins/GTM/plugin.json')).toBe(GTM_MANIFEST);
187
+ expect(await read('Skills/Eng/deploy/access.md')).toBe(DEPLOY_RULES);
188
+ expect(commits).toEqual([]);
189
+ });
190
+
191
+ it('renames the identifier: the manifest and every grant that spells the old one, in ONE commit', async () => {
192
+ expect(await access.canRead(wsId, member.email, 'Skills/Eng/deploy/SKILL.md')).toBe(true);
193
+
194
+ const result = await svc.rename(manager, 'gtm', { name: 'go-to-market' });
195
+ expect(result).toEqual({ name: 'go-to-market', displayName: 'GTM', rewritten: ['Skills/Eng/deploy/access.md'] });
196
+
197
+ // The manifest keeps everything else it had.
198
+ expect(await manifest()).toEqual({ name: 'go-to-market', version: '1.0.0' });
199
+ // Both spellings became the one new one; the file's shape is otherwise untouched.
200
+ expect(await read('Skills/Eng/deploy/access.md')).toBe(
201
+ '---\n---\nread:\n - plugin/go-to-market/read\nwrite:\n - plugin/go-to-market/write\n',
202
+ );
203
+ expect(commits).toEqual([
204
+ {
205
+ summary: 'Rename plugin gtm to go-to-market',
206
+ paths: [`${KB_DIR}/Plugins/GTM/plugin.json`, `${KB_DIR}/Skills/Eng/deploy/access.md`],
207
+ },
208
+ ]);
209
+ // The point of it all: the same people hold the same verbs through the new spelling.
210
+ expect(await access.canRead(wsId, member.email, 'Skills/Eng/deploy/SKILL.md')).toBe(true);
211
+ expect(await access.canWrite(wsId, manager.email, 'Skills/Eng/deploy/SKILL.md')).toBe(true);
212
+ expect(invalidated).toBe(1);
213
+ // The old spelling now names nobody: a stale grant somewhere else grants nothing.
214
+ await write('Skills/Eng/deploy/access.md', DEPLOY_RULES);
215
+ access.invalidate(wsId);
216
+ expect(await access.canRead(wsId, member.email, 'Skills/Eng/deploy/SKILL.md')).toBe(false);
217
+ });
218
+
219
+ it('changes the display name without touching a single grant, and stores none that equals the folder', async () => {
220
+ await svc.rename(manager, 'gtm', { displayName: ' Go To Market ' });
221
+ expect(await manifest()).toEqual({ name: 'gtm', version: '1.0.0', displayName: 'Go To Market' });
222
+ expect(await read('Skills/Eng/deploy/access.md')).toBe(DEPLOY_RULES);
223
+ expect(commits).toEqual([{ summary: 'Rename plugin gtm: display name', paths: [`${KB_DIR}/Plugins/GTM/plugin.json`] }]);
224
+
225
+ // The folder name is the default label — writing it down would only be noise.
226
+ await svc.rename(manager, 'gtm', { displayName: 'GTM' });
227
+ expect(await manifest()).toEqual({ name: 'gtm', version: '1.0.0' });
228
+ });
229
+
230
+ it('judges only a NEW identifier: a plugin whose manifest already wears the reserved prefix can still change its display name', async () => {
231
+ // Hand-written before the rule existed; the folder is not a personal shelf.
232
+ await write('Plugins/Legacy/plugin.json', '{"name":"personal-legacy"}');
233
+ await write('Plugins/Legacy/access.md', GTM_RULES);
234
+ await svc.rename(manager, 'personal-legacy', { displayName: 'Legacy Team' });
235
+ expect(JSON.parse(await read('Plugins/Legacy/plugin.json'))).toEqual({ name: 'personal-legacy', displayName: 'Legacy Team' });
236
+ // Moving it to ANOTHER reserved name is still refused; moving it out of the namespace is fine.
237
+ await expect(svc.rename(manager, 'personal-legacy', { name: 'personal-other' })).rejects.toMatchObject({ payload: { kind: 'bad-name' } });
238
+ await svc.rename(manager, 'personal-legacy', { name: 'legacy' });
239
+ expect(JSON.parse(await read('Plugins/Legacy/plugin.json')).name).toBe('legacy');
240
+ });
241
+
242
+ it('is fail-closed: a member, an unknown name, and the folder spelled as a name all get the same 404', async () => {
243
+ for (const [user, name] of [
244
+ [member, 'gtm'],
245
+ [manager, 'ghost'],
246
+ [manager, 'GTM'],
247
+ ] as const) {
248
+ await expect(svc.rename(user, name, { name: 'x' })).rejects.toMatchObject({ status: 404, payload: { kind: 'unknown-plugin' } });
249
+ }
250
+ expect(commits).toEqual([]);
251
+ });
252
+
253
+ it('refuses an identifier that is not one, a personal one, or one already taken — writing nothing', async () => {
254
+ await write('Plugins/Ops/plugin.json', '{"name":"ops"}');
255
+ await write('Plugins/Ops/access.md', GTM_RULES);
256
+ for (const [name, kind] of [
257
+ ['Sales Team', 'bad-name'],
258
+ ['sales--team', 'bad-name'],
259
+ ['personal-mia', 'bad-name'],
260
+ // Kebab-case but longer than the slug the marketplace keys on: not its own slug.
261
+ [`${'a'.repeat(60)}-${'b'.repeat(10)}`, 'bad-name'],
262
+ ['ops', 'name-taken'],
263
+ ] as const) {
264
+ await expect(svc.rename(manager, 'gtm', { name })).rejects.toMatchObject({ payload: { kind } });
265
+ }
266
+ expect(await read('Plugins/GTM/plugin.json')).toBe(GTM_MANIFEST);
267
+ expect(commits).toEqual([]);
268
+ });
269
+
270
+ it('refuses — before writing anything — when a grant sits in a file the caller cannot edit, and names it', async () => {
271
+ // A knowledge page whose own frontmatter names the plugin; only admins write there.
272
+ await write('KnowledgeBase/Notes/access.md', '---\nread:\n - plugin/gtm/read\n---\n');
273
+ access.invalidate(wsId);
274
+
275
+ await expect(svc.rename(manager, 'gtm', { name: 'go-to-market' })).rejects.toMatchObject({
276
+ status: 409,
277
+ payload: { kind: 'needs-write', files: ['KnowledgeBase/Notes/access.md'] },
278
+ });
279
+ expect(await read('Plugins/GTM/plugin.json')).toBe(GTM_MANIFEST);
280
+ expect(await read('Skills/Eng/deploy/access.md')).toBe(DEPLOY_RULES);
281
+ expect(commits).toEqual([]);
282
+ });
283
+
284
+ it('puts every file back when the commit is refused, so a retry starts from what origin has', async () => {
285
+ failNextCommit = new Error('push refused');
286
+ await expect(svc.rename(manager, 'gtm', { name: 'go-to-market' })).rejects.toThrow('push refused');
287
+ expect(await read('Plugins/GTM/plugin.json')).toBe(GTM_MANIFEST);
288
+ expect(await read('Skills/Eng/deploy/access.md')).toBe(DEPLOY_RULES);
289
+ expect(invalidated).toBe(0);
290
+
291
+ await svc.rename(manager, 'gtm', { name: 'go-to-market' });
292
+ expect((await manifest()).name).toBe('go-to-market');
293
+ });
294
+
295
+ it('a name is taken by SLUG: a bundle spelled "Sales Team" already holds sales-team', async () => {
296
+ await write('Plugins/Ext/plugin.bundle.json', '{"name":"Sales Team"}');
297
+ await expect(svc.rename(manager, 'gtm', { name: 'sales-team' })).rejects.toMatchObject({
298
+ status: 409,
299
+ payload: { kind: 'name-taken' },
300
+ });
301
+ expect(commits).toEqual([]);
302
+ });
303
+
304
+ it('rewrites the grant in every file kind the resolver reads from — a .tool frontmatter included', async () => {
305
+ const tool = 'Plugins/GTM/software.bevel.hexis/tools/web.tool';
306
+ await write(tool, '---\nname: web\nread:\n - plugin/gtm/read\n---\nbody\n');
307
+ const result = await svc.rename(manager, 'gtm', { name: 'go-to-market' });
308
+ expect(result.rewritten).toEqual([tool, 'Skills/Eng/deploy/access.md']);
309
+ expect(await read(tool)).toBe('---\nname: web\nread:\n - plugin/go-to-market/read\n---\nbody\n');
310
+ });
311
+
312
+ it('walks exactly what the resolver reads: a grant under node_modules is never rewritten, because the walk never enters it', async () => {
313
+ await write('node_modules/some-dep/access.md', '---\n---\nread:\n - plugin/gtm/read\n');
314
+ const result = await svc.rename(manager, 'gtm', { name: 'go-to-market' });
315
+ // The resolver never reads that file, so the rename neither rewrites it
316
+ // nor counts it — the same walk, the same skip rule.
317
+ expect(result.rewritten).toEqual(['Skills/Eng/deploy/access.md']);
318
+ expect(await read('node_modules/some-dep/access.md')).toContain('plugin/gtm/read');
319
+ });
320
+
321
+ it('a folder it cannot list stops the rename before a byte is written', async () => {
322
+ walkMock.holeInTheWalk = true;
323
+ // The same refusal as a discovery hole — a 503 naming the folder, never a raw errno.
324
+ await expect(svc.rename(manager, 'gtm', { name: 'go-to-market' })).rejects.toMatchObject({
325
+ status: 503,
326
+ payload: { kind: 'incomplete-discovery', unreadable: ['KnowledgeBase/Notes'] },
327
+ });
328
+ expect(await read('Plugins/GTM/plugin.json')).toBe(GTM_MANIFEST);
329
+ expect(await read('Skills/Eng/deploy/access.md')).toBe(DEPLOY_RULES);
330
+ expect(commits).toEqual([]);
331
+ });
332
+
333
+ it('puts every file back when a write in the batch fails — the ones already written included', async () => {
334
+ failWriteMatching = 'Skills/Eng/deploy/access.md';
335
+ await expect(svc.rename(manager, 'gtm', { name: 'go-to-market' })).rejects.toThrow('disk full');
336
+ expect(await read('Plugins/GTM/plugin.json')).toBe(GTM_MANIFEST);
337
+ expect(await read('Skills/Eng/deploy/access.md')).toBe(DEPLOY_RULES);
338
+ expect(commits).toEqual([]);
339
+ });
340
+
341
+ it('leaves a rename that COMMITTED but could not push alone — the recovery flow owns that state', async () => {
342
+ failNextCommit = new PushNeedsAgentResolutionError(DEFAULT_BRANCH, 'Plugins/GTM/plugin.json', 'rejected', 'n/a');
343
+ await expect(svc.rename(manager, 'gtm', { name: 'go-to-market' })).rejects.toBeInstanceOf(PushNeedsAgentResolutionError);
344
+ // Restoring the old bytes here would stack an uncommitted inverse on a
345
+ // real commit; the working tree stays as the commit left it — and since
346
+ // it changed, the caches keyed on the old identity are dropped as on success.
347
+ expect((await manifest()).name).toBe('go-to-market');
348
+ expect(await read('Skills/Eng/deploy/access.md')).toContain('plugin/go-to-market/read');
349
+ expect(invalidated).toBe(1);
350
+ expect(await access.canRead(wsId, member.email, 'Skills/Eng/deploy/SKILL.md')).toBe(true);
351
+ });
352
+
353
+ it('will not rename a plugin read from an external format — that repository owns its name', async () => {
354
+ await write('Plugins/Ext/plugin.bundle.json', '{"name":"ext"}');
355
+ await write('Plugins/Ext/access.md', GTM_RULES);
356
+ await expect(svc.rename(manager, 'ext', { name: 'ext-2' })).rejects.toBeInstanceOf(PluginRenameError);
357
+ await expect(svc.rename(manager, 'ext', { name: 'ext-2' })).rejects.toMatchObject({ status: 409, payload: { kind: 'read-only' } });
358
+ });
359
+ });
360
+
361
+ describe('renamePluginPrincipalInText', () => {
362
+ it('rewrites every access entry naming the plugin, in any accepted spelling, and nothing else', () => {
363
+ const text = [
364
+ '---',
365
+ 'read:',
366
+ ' - plugin/GTM/read',
367
+ ' - deny plugin/gtm/write',
368
+ '---',
369
+ 'read:',
370
+ ' - plugin/gtm-x/read',
371
+ ' - Sam <sam@x.io>',
372
+ 'Mention plugin/gtm/read in prose, and leave it.',
373
+ '',
374
+ ].join('\n');
375
+ expect(renamePluginPrincipalInText(text, 'gtm', 'go-to-market')).toBe(
376
+ [
377
+ '---',
378
+ 'read:',
379
+ ' - plugin/go-to-market/read',
380
+ ' - deny plugin/go-to-market/write',
381
+ '---',
382
+ 'read:',
383
+ ' - plugin/gtm-x/read',
384
+ ' - Sam <sam@x.io>',
385
+ 'Mention plugin/gtm/read in prose, and leave it.',
386
+ '',
387
+ ].join('\n'),
388
+ );
389
+ });
390
+
391
+ it('keeps the line endings it was given', () => {
392
+ expect(renamePluginPrincipalInText('read:\r\n - plugin/gtm/read\r\n', 'gtm', 'g2')).toBe('read:\r\n - plugin/g2/read\r\n');
393
+ });
394
+ });