@bevel-software/platform-core-backend 0.7.5 → 0.9.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (282) hide show
  1. package/LICENSE +202 -202
  2. package/THIRD-PARTY-NOTICES.md +428 -454
  3. package/dist/core/core-ports.d.ts +8 -1
  4. package/dist/core/core-ports.d.ts.map +1 -1
  5. package/dist/core/core-ports.js.map +1 -1
  6. package/dist/core/create-core-server.d.ts.map +1 -1
  7. package/dist/core/create-core-server.js +75 -18
  8. package/dist/core/create-core-server.js.map +1 -1
  9. package/dist/core/create-core-services.d.ts +12 -5
  10. package/dist/core/create-core-services.d.ts.map +1 -1
  11. package/dist/core/create-core-services.js +69 -36
  12. package/dist/core/create-core-services.js.map +1 -1
  13. package/dist/modules/access/access-control.interface.d.ts +8 -7
  14. package/dist/modules/access/access-control.interface.d.ts.map +1 -1
  15. package/dist/modules/access/access-control.service.d.ts +1 -1
  16. package/dist/modules/access/access-control.service.d.ts.map +1 -1
  17. package/dist/modules/access/access-control.service.js +6 -6
  18. package/dist/modules/access/access-control.service.js.map +1 -1
  19. package/dist/modules/access/access-declarations.d.ts +5 -5
  20. package/dist/modules/access/access-declarations.js +3 -3
  21. package/dist/modules/access/access-mutation.service.d.ts +3 -3
  22. package/dist/modules/access/access-mutation.service.d.ts.map +1 -1
  23. package/dist/modules/access/access-mutation.service.js +3 -3
  24. package/dist/modules/access/access-mutation.service.js.map +1 -1
  25. package/dist/modules/access/access-splice.js +4 -4
  26. package/dist/modules/access/access-splice.js.map +1 -1
  27. package/dist/modules/access/access.routes.d.ts +3 -1
  28. package/dist/modules/access/access.routes.d.ts.map +1 -1
  29. package/dist/modules/access/access.routes.js +22 -20
  30. package/dist/modules/access/access.routes.js.map +1 -1
  31. package/dist/modules/access/creator-access.d.ts +2 -2
  32. package/dist/modules/access/creator-access.js +5 -5
  33. package/dist/modules/access/creator-access.js.map +1 -1
  34. package/dist/modules/access/render-roles-yaml.d.ts +22 -0
  35. package/dist/modules/access/render-roles-yaml.d.ts.map +1 -0
  36. package/dist/modules/access/render-roles-yaml.js +56 -0
  37. package/dist/modules/access/render-roles-yaml.js.map +1 -0
  38. package/dist/modules/access/roles-admin.service.d.ts +33 -5
  39. package/dist/modules/access/roles-admin.service.d.ts.map +1 -1
  40. package/dist/modules/access/roles-admin.service.js +44 -19
  41. package/dist/modules/access/roles-admin.service.js.map +1 -1
  42. package/dist/modules/code-mode/code-mode-names.d.ts +5 -13
  43. package/dist/modules/code-mode/code-mode-names.d.ts.map +1 -1
  44. package/dist/modules/code-mode/code-mode-names.js +5 -27
  45. package/dist/modules/code-mode/code-mode-names.js.map +1 -1
  46. package/dist/modules/code-mode/code-mode.tool.d.ts.map +1 -1
  47. package/dist/modules/code-mode/code-mode.tool.js +29 -7
  48. package/dist/modules/code-mode/code-mode.tool.js.map +1 -1
  49. package/dist/modules/mcp/mcp.service.d.ts +11 -52
  50. package/dist/modules/mcp/mcp.service.d.ts.map +1 -1
  51. package/dist/modules/mcp/mcp.service.js +33 -395
  52. package/dist/modules/mcp/mcp.service.js.map +1 -1
  53. package/dist/modules/plugins/index.d.ts +7 -0
  54. package/dist/modules/plugins/index.d.ts.map +1 -0
  55. package/dist/modules/plugins/index.js +6 -0
  56. package/dist/modules/plugins/index.js.map +1 -0
  57. package/dist/modules/plugins/join-proposals.d.ts +53 -0
  58. package/dist/modules/plugins/join-proposals.d.ts.map +1 -0
  59. package/dist/modules/plugins/join-proposals.js +67 -0
  60. package/dist/modules/plugins/join-proposals.js.map +1 -0
  61. package/dist/modules/plugins/join-requests.service.d.ts +81 -0
  62. package/dist/modules/plugins/join-requests.service.d.ts.map +1 -0
  63. package/dist/modules/plugins/join-requests.service.js +135 -0
  64. package/dist/modules/plugins/join-requests.service.js.map +1 -0
  65. package/dist/modules/plugins/plugin-provision.service.d.ts +134 -0
  66. package/dist/modules/plugins/plugin-provision.service.d.ts.map +1 -0
  67. package/dist/modules/plugins/plugin-provision.service.js +344 -0
  68. package/dist/modules/plugins/plugin-provision.service.js.map +1 -0
  69. package/dist/modules/plugins/plugins.contract.d.ts +106 -0
  70. package/dist/modules/plugins/plugins.contract.d.ts.map +1 -0
  71. package/dist/modules/plugins/plugins.contract.js +36 -0
  72. package/dist/modules/plugins/plugins.contract.js.map +1 -0
  73. package/dist/modules/plugins/plugins.routes.d.ts +42 -0
  74. package/dist/modules/plugins/plugins.routes.d.ts.map +1 -0
  75. package/dist/modules/plugins/plugins.routes.js +379 -0
  76. package/dist/modules/plugins/plugins.routes.js.map +1 -0
  77. package/dist/modules/plugins/plugins.service.d.ts +60 -0
  78. package/dist/modules/plugins/plugins.service.d.ts.map +1 -0
  79. package/dist/modules/plugins/plugins.service.js +172 -0
  80. package/dist/modules/plugins/plugins.service.js.map +1 -0
  81. package/dist/modules/settings/setup.routes.d.ts +10 -1
  82. package/dist/modules/settings/setup.routes.d.ts.map +1 -1
  83. package/dist/modules/settings/setup.routes.js +111 -6
  84. package/dist/modules/settings/setup.routes.js.map +1 -1
  85. package/dist/modules/skills/pending-skills.service.d.ts +2 -2
  86. package/dist/modules/skills/pending-skills.service.js +7 -7
  87. package/dist/modules/skills/pending-skills.service.js.map +1 -1
  88. package/dist/modules/skills/skills.contract.d.ts +4 -4
  89. package/dist/modules/skills/skills.contract.d.ts.map +1 -1
  90. package/dist/modules/skills/skills.contract.js +1 -1
  91. package/dist/modules/skills/skills.service.js +5 -5
  92. package/dist/modules/skills/skills.service.js.map +1 -1
  93. package/dist/modules/tool-manuals/mcp-json-discovery.d.ts +65 -0
  94. package/dist/modules/tool-manuals/mcp-json-discovery.d.ts.map +1 -0
  95. package/dist/modules/tool-manuals/mcp-json-discovery.js +276 -0
  96. package/dist/modules/tool-manuals/mcp-json-discovery.js.map +1 -0
  97. package/dist/modules/tool-manuals/mcp-server-edit.service.d.ts +92 -0
  98. package/dist/modules/tool-manuals/mcp-server-edit.service.d.ts.map +1 -0
  99. package/dist/modules/tool-manuals/mcp-server-edit.service.js +328 -0
  100. package/dist/modules/tool-manuals/mcp-server-edit.service.js.map +1 -0
  101. package/dist/modules/tool-manuals/tool-manuals.contract.d.ts +38 -12
  102. package/dist/modules/tool-manuals/tool-manuals.contract.d.ts.map +1 -1
  103. package/dist/modules/tool-manuals/tool-manuals.contract.js +1 -1
  104. package/dist/modules/tool-manuals/tool-manuals.routes.d.ts +13 -2
  105. package/dist/modules/tool-manuals/tool-manuals.routes.d.ts.map +1 -1
  106. package/dist/modules/tool-manuals/tool-manuals.routes.js +233 -2
  107. package/dist/modules/tool-manuals/tool-manuals.routes.js.map +1 -1
  108. package/dist/modules/tool-manuals/tool-manuals.service.d.ts.map +1 -1
  109. package/dist/modules/tool-manuals/tool-manuals.service.js +74 -37
  110. package/dist/modules/tool-manuals/tool-manuals.service.js.map +1 -1
  111. package/dist/modules/tool-manuals/tool-manuals.tools.js +6 -3
  112. package/dist/modules/tool-manuals/tool-manuals.tools.js.map +1 -1
  113. package/dist/modules/workflow/git/git.service.js +2 -2
  114. package/dist/modules/workflow/git/git.service.js.map +1 -1
  115. package/dist/modules/workspace/kb-seed.service.d.ts +2 -2
  116. package/dist/modules/workspace/kb-seed.service.d.ts.map +1 -1
  117. package/dist/modules/workspace/kb-seed.service.js +43 -10
  118. package/dist/modules/workspace/kb-seed.service.js.map +1 -1
  119. package/dist/modules/workspace/plugins-migration.d.ts +50 -0
  120. package/dist/modules/workspace/plugins-migration.d.ts.map +1 -0
  121. package/dist/modules/workspace/plugins-migration.js +379 -0
  122. package/dist/modules/workspace/plugins-migration.js.map +1 -0
  123. package/dist/modules/workspace/startup/kb-git.d.ts +23 -0
  124. package/dist/modules/workspace/startup/kb-git.d.ts.map +1 -0
  125. package/dist/modules/workspace/startup/kb-git.js +86 -0
  126. package/dist/modules/workspace/startup/kb-git.js.map +1 -0
  127. package/dist/modules/workspace/startup/kb-startup-runner.d.ts +74 -0
  128. package/dist/modules/workspace/startup/kb-startup-runner.d.ts.map +1 -0
  129. package/dist/modules/workspace/startup/kb-startup-runner.js +528 -0
  130. package/dist/modules/workspace/startup/kb-startup-runner.js.map +1 -0
  131. package/dist/modules/workspace/startup/on-server-start.d.ts +105 -0
  132. package/dist/modules/workspace/startup/on-server-start.d.ts.map +1 -0
  133. package/dist/modules/workspace/startup/on-server-start.js +21 -0
  134. package/dist/modules/workspace/startup/on-server-start.js.map +1 -0
  135. package/dist/modules/workspace/startup/steps/groups-to-plugins.step.d.ts +46 -0
  136. package/dist/modules/workspace/startup/steps/groups-to-plugins.step.d.ts.map +1 -0
  137. package/dist/modules/workspace/startup/steps/groups-to-plugins.step.js +492 -0
  138. package/dist/modules/workspace/startup/steps/groups-to-plugins.step.js.map +1 -0
  139. package/dist/modules/workspace/startup/steps/roles-yaml.step.d.ts +23 -0
  140. package/dist/modules/workspace/startup/steps/roles-yaml.step.d.ts.map +1 -0
  141. package/dist/modules/workspace/startup/steps/roles-yaml.step.js +69 -0
  142. package/dist/modules/workspace/startup/steps/roles-yaml.step.js.map +1 -0
  143. package/dist/modules/workspace/startup/steps/seed-tree.d.ts +17 -0
  144. package/dist/modules/workspace/startup/steps/seed-tree.d.ts.map +1 -0
  145. package/dist/modules/workspace/startup/steps/seed-tree.js +109 -0
  146. package/dist/modules/workspace/startup/steps/seed-tree.js.map +1 -0
  147. package/dist/modules/workspace/startup/steps/template-files.step.d.ts +103 -0
  148. package/dist/modules/workspace/startup/steps/template-files.step.d.ts.map +1 -0
  149. package/dist/modules/workspace/startup/steps/template-files.step.js +337 -0
  150. package/dist/modules/workspace/startup/steps/template-files.step.js.map +1 -0
  151. package/dist/modules/workspace/workspace.routes.js +3 -3
  152. package/dist/modules/workspace/workspace.routes.js.map +1 -1
  153. package/dist/modules/workspace/workspace.service.d.ts +0 -9
  154. package/dist/modules/workspace/workspace.service.d.ts.map +1 -1
  155. package/dist/modules/workspace/workspace.service.js +2 -30
  156. package/dist/modules/workspace/workspace.service.js.map +1 -1
  157. package/dist/shared/kb-layout.test.js +3 -3
  158. package/dist/shared/kb-layout.test.js.map +1 -1
  159. package/dist/shared/utcp-namespace.d.ts +6 -27
  160. package/dist/shared/utcp-namespace.d.ts.map +1 -1
  161. package/dist/shared/utcp-namespace.js +6 -63
  162. package/dist/shared/utcp-namespace.js.map +1 -1
  163. package/dist/shared/variable-refs.d.ts +42 -0
  164. package/dist/shared/variable-refs.d.ts.map +1 -0
  165. package/dist/shared/variable-refs.js +60 -0
  166. package/dist/shared/variable-refs.js.map +1 -0
  167. package/kb-template/.bevelignore +1 -1
  168. package/kb-template/AGENTS.md +88 -35
  169. package/kb-template/KnowledgeBase/How to get started.md +10 -10
  170. package/kb-template/access.md +36 -36
  171. package/kb-template/gitignore.template +16 -0
  172. package/migrations/meta/0000_snapshot.json +1479 -1479
  173. package/package.json +5 -4
  174. package/src/assets.ts +25 -25
  175. package/src/core/core-ports.ts +113 -106
  176. package/src/core/create-core-server.ts +85 -20
  177. package/src/core/create-core-services.ts +83 -42
  178. package/src/index.ts +69 -69
  179. package/src/modules/access/__tests__/access-control.atref-batch.test.ts +98 -98
  180. package/src/modules/access/__tests__/access-declarations.test.ts +28 -28
  181. package/src/modules/access/__tests__/access-md-format.test.ts +18 -18
  182. package/src/modules/access/__tests__/access-mutation.service.test.ts +5 -5
  183. package/src/modules/access/__tests__/access-splice.test.ts +2 -2
  184. package/src/modules/access/__tests__/access.routes.overrides.test.ts +16 -16
  185. package/src/modules/access/__tests__/grant-sources.test.ts +12 -12
  186. package/src/modules/access/__tests__/roles-admin.service.test.ts +36 -2
  187. package/src/modules/access/access-control.interface.ts +8 -7
  188. package/src/modules/access/access-control.service.ts +7 -7
  189. package/src/modules/access/access-declarations.ts +5 -5
  190. package/src/modules/access/access-mutation.service.ts +3 -3
  191. package/src/modules/access/access-splice.ts +4 -4
  192. package/src/modules/access/access.routes.ts +23 -20
  193. package/src/modules/access/creator-access.ts +5 -5
  194. package/src/modules/access/render-roles-yaml.ts +65 -0
  195. package/src/modules/access/roles-admin.service.ts +45 -16
  196. package/src/modules/admin/admin-access.routes.ts +29 -29
  197. package/src/modules/auth/__tests__/auth.routes.test.ts +91 -91
  198. package/src/modules/auth/__tests__/rate-limit.test.ts +36 -36
  199. package/src/modules/auth/rate-limit.ts +45 -45
  200. package/src/modules/code-mode/__tests__/code-mode.tool.test.ts +67 -0
  201. package/src/modules/code-mode/code-mode-names.ts +10 -36
  202. package/src/modules/code-mode/code-mode.tool.ts +27 -7
  203. package/src/modules/database/connection.ts +15 -15
  204. package/src/modules/database/schema.ts +11 -11
  205. package/src/modules/diff/__tests__/diff.routes.rejectPathsLocked.test.ts +150 -150
  206. package/src/modules/mcp/mcp.service.ts +57 -435
  207. package/src/modules/{groups → plugins}/__tests__/join-proposals.test.ts +1 -1
  208. package/src/modules/{groups → plugins}/__tests__/join-requests.service.test.ts +7 -7
  209. package/src/modules/{groups/__tests__/group-index.service.test.ts → plugins/__tests__/plugin-index.service.test.ts} +41 -41
  210. package/src/modules/plugins/__tests__/plugin-provision.service.test.ts +312 -0
  211. package/src/modules/{groups/__tests__/groups.routes.test.ts → plugins/__tests__/plugins.routes.test.ts} +100 -100
  212. package/src/modules/plugins/index.ts +17 -0
  213. package/src/modules/{groups → plugins}/join-proposals.ts +2 -2
  214. package/src/modules/{groups → plugins}/join-requests.service.ts +8 -8
  215. package/src/modules/{groups/group-provision.service.ts → plugins/plugin-provision.service.ts} +139 -69
  216. package/src/modules/{groups/groups.contract.ts → plugins/plugins.contract.ts} +26 -26
  217. package/src/modules/{groups/groups.routes.ts → plugins/plugins.routes.ts} +102 -102
  218. package/src/modules/{groups/groups.service.ts → plugins/plugins.service.ts} +43 -43
  219. package/src/modules/secrets-vault/__tests__/tool-owner-gate.route.test.ts +143 -143
  220. package/src/modules/settings/__tests__/setup.routes.test.ts +124 -3
  221. package/src/modules/settings/setup.routes.ts +112 -5
  222. package/src/modules/skills/__tests__/pending-skills.service.test.ts +14 -14
  223. package/src/modules/skills/__tests__/skills.service.test.ts +13 -13
  224. package/src/modules/skills/pending-skills.service.ts +7 -7
  225. package/src/modules/skills/skills.contract.ts +4 -4
  226. package/src/modules/skills/skills.service.ts +5 -5
  227. package/src/modules/tool-auth/llm-usage-meter.ts +19 -19
  228. package/src/modules/tool-manuals/__tests__/mcp-json-discovery.test.ts +198 -0
  229. package/src/modules/tool-manuals/__tests__/mcp-server-edit.service.test.ts +346 -0
  230. package/src/modules/tool-manuals/__tests__/tool-manuals.archive.route.test.ts +101 -0
  231. package/src/modules/tool-manuals/__tests__/tool-manuals.detail.route.test.ts +3 -3
  232. package/src/modules/tool-manuals/__tests__/tool-manuals.mcp-oauth.test.ts +2 -2
  233. package/src/modules/tool-manuals/__tests__/tool-manuals.service.test.ts +27 -27
  234. package/src/modules/tool-manuals/__tests__/tool-manuals.tools.test.ts +3 -3
  235. package/src/modules/tool-manuals/mcp-json-discovery.ts +328 -0
  236. package/src/modules/tool-manuals/mcp-server-edit.service.ts +434 -0
  237. package/src/modules/tool-manuals/tool-manuals.contract.ts +35 -12
  238. package/src/modules/tool-manuals/tool-manuals.routes.ts +222 -1
  239. package/src/modules/tool-manuals/tool-manuals.service.ts +82 -42
  240. package/src/modules/tool-manuals/tool-manuals.tools.ts +6 -3
  241. package/src/modules/workflow/__tests__/workflow.service.facade.test.ts +1 -1
  242. package/src/modules/workflow/git/__tests__/branch-name.test.ts +3 -3
  243. package/src/modules/workflow/git/__tests__/git.service.accessGating.test.ts +3 -3
  244. package/src/modules/workflow/git/__tests__/git.service.commitFile.test.ts +13 -8
  245. package/src/modules/workflow/git/__tests__/git.service.deleteBranch.test.ts +1 -1
  246. package/src/modules/workflow/git/__tests__/pull-request.service.test.ts +1 -1
  247. package/src/modules/workflow/git/git.service.ts +2 -2
  248. package/src/modules/workflow/review-workflow/__tests__/approval-states.test.ts +1 -1
  249. package/src/modules/workflow/review-workflow/__tests__/cancel-pr.test.ts +1 -1
  250. package/src/modules/workflow/workflow-hooks.ts +101 -101
  251. package/src/modules/workspace/__tests__/session-ontology.gate.test.ts +237 -237
  252. package/src/modules/workspace/__tests__/workspace.routes.create-grant.test.ts +236 -236
  253. package/src/modules/workspace/__tests__/workspace.routes.delete.test.ts +179 -179
  254. package/src/modules/workspace/__tests__/workspace.routes.download.test.ts +320 -320
  255. package/src/modules/workspace/__tests__/workspace.routes.read-gate.test.ts +337 -337
  256. package/src/modules/workspace/__tests__/workspace.service.test.ts +27 -0
  257. package/src/modules/workspace/bevel-ignore.ts +66 -66
  258. package/src/modules/workspace/session-sink.ts +25 -25
  259. package/src/modules/workspace/startup/__tests__/kb-startup-runner.test.ts +495 -0
  260. package/src/modules/workspace/startup/kb-git.ts +94 -0
  261. package/src/modules/workspace/startup/kb-startup-runner.ts +597 -0
  262. package/src/modules/workspace/startup/on-server-start.ts +97 -0
  263. package/src/modules/workspace/startup/steps/__tests__/steps.test.ts +636 -0
  264. package/src/modules/workspace/startup/steps/groups-to-plugins.step.ts +561 -0
  265. package/src/modules/workspace/startup/steps/roles-yaml.step.ts +71 -0
  266. package/src/modules/workspace/startup/steps/seed-tree.ts +115 -0
  267. package/src/modules/workspace/startup/steps/template-files.step.ts +360 -0
  268. package/src/modules/workspace/workspace.routes.ts +3 -3
  269. package/src/modules/workspace/workspace.service.ts +2 -35
  270. package/src/modules/workspace/workspace.tools.ts +922 -922
  271. package/src/shared/__tests__/join-request.test.ts +13 -13
  272. package/src/shared/__tests__/kb-layout.plugin.test.ts +45 -0
  273. package/src/shared/kb-layout.test.ts +3 -3
  274. package/src/shared/utcp-namespace.ts +10 -68
  275. package/src/shared/variable-refs.ts +64 -0
  276. package/src/modules/groups/__tests__/group-provision.service.test.ts +0 -247
  277. package/src/modules/groups/index.ts +0 -17
  278. package/src/modules/workspace/__tests__/kb-seed.service.test.ts +0 -444
  279. package/src/modules/workspace/kb-seed.interface.ts +0 -36
  280. package/src/modules/workspace/kb-seed.service.ts +0 -555
  281. package/src/shared/__tests__/kb-layout.group.test.ts +0 -45
  282. /package/kb-template/{Groups → Plugins}/.gitkeep +0 -0
@@ -13,18 +13,18 @@ import type { WorkspaceService } from '../../workspace/workspace.service.js';
13
13
  import type { IAccessControl } from '../../access/access-control.interface.js';
14
14
  import type { ISkillService, SkillSummary } from '../../skills/skills.contract.js';
15
15
  import type { IToolManualService, ToolManualSummary } from '../../tool-manuals/tool-manuals.contract.js';
16
- import { GroupIndexService } from '../groups.service.js';
17
- import { createGroupsRoutes } from '../groups.routes.js';
16
+ import { PluginIndexService } from '../plugins.service.js';
17
+ import { createPluginsRoutes } from '../plugins.routes.js';
18
18
  import type { JoinRequestsService } from '../join-requests.service.js';
19
- import type { GroupSummary, IGroupIndexService } from '../groups.contract.js';
19
+ import type { PluginSummary, IPluginIndexService } from '../plugins.contract.js';
20
20
 
21
21
  /**
22
- * HTTP-level contract for the group routes: the auth gate, the three-tier
22
+ * HTTP-level contract for the plugin routes: the auth gate, the three-tier
23
23
  * enumeration (member / manager / discoverable — all ordinary access
24
- * verdicts), the fail-closed omission of groups with no verdict at all, and
24
+ * verdicts), the fail-closed omission of plugins with no verdict at all, and
25
25
  * the join flow that rides on plain change requests.
26
26
  *
27
- * The group index is REAL over a temp KB; access control and the workflow are
27
+ * The plugin index is REAL over a temp KB; access control and the workflow are
28
28
  * stubs, because what's under test here is the route's use of them.
29
29
  */
30
30
 
@@ -38,18 +38,18 @@ const tmpDirs: string[] = [];
38
38
 
39
39
  interface HarnessOpts {
40
40
  /**
41
- * Paths (`Groups/GTM` for the folder/member verdict, `Groups/GTM/access.md`
41
+ * Paths (`Plugins/GTM` for the folder/member verdict, `Plugins/GTM/access.md`
42
42
  * for the discover verdict) the given email may read.
43
43
  */
44
44
  readable?: Record<string, string[]>;
45
45
  /** Paths the given email may write. */
46
46
  writable?: Record<string, string[]>;
47
- /** Paths (the FOLDER, e.g. `Groups/GTM`) the given email OWNS. */
47
+ /** Paths (the FOLDER, e.g. `Plugins/GTM`) the given email OWNS. */
48
48
  owner?: Record<string, string[]>;
49
49
  /** Open CRs `listChangeRequestsAuthoredBy` returns for any caller. */
50
50
  authoredCrs?: ChangeRequest[];
51
51
  /** Override the whole index (used to force the 500 path). */
52
- index?: IGroupIndexService;
52
+ index?: IPluginIndexService;
53
53
  email?: string | null;
54
54
  skills?: SkillSummary[];
55
55
  tools?: ToolManualSummary[];
@@ -72,18 +72,18 @@ function cr(over: Partial<ChangeRequest>): ChangeRequest {
72
72
  }
73
73
 
74
74
  async function makeHarness(opts: HarnessOpts = {}) {
75
- const workspaceDir = await fs.mkdtemp(path.join(os.tmpdir(), 'bevel-groups-routes-'));
75
+ const workspaceDir = await fs.mkdtemp(path.join(os.tmpdir(), 'bevel-plugins-routes-'));
76
76
  tmpDirs.push(workspaceDir);
77
77
  const kbRoot = path.join(workspaceDir, KB);
78
- // Both carry the access.md that makes a folder a group at all.
79
- await fs.mkdir(path.join(kbRoot, 'Groups', 'GTM'), { recursive: true });
78
+ // Both carry the access.md that makes a folder a plugin at all.
79
+ await fs.mkdir(path.join(kbRoot, 'Plugins', 'GTM'), { recursive: true });
80
80
  await fs.writeFile(
81
- path.join(kbRoot, 'Groups', 'GTM', 'access.md'),
81
+ path.join(kbRoot, 'Plugins', 'GTM', 'access.md'),
82
82
  '---\nread:\n - everyone\n---\nread: []\n',
83
83
  );
84
- await fs.mkdir(path.join(kbRoot, 'Groups', 'Finance'), { recursive: true });
84
+ await fs.mkdir(path.join(kbRoot, 'Plugins', 'Finance'), { recursive: true });
85
85
  await fs.writeFile(
86
- path.join(kbRoot, 'Groups', 'Finance', 'access.md'),
86
+ path.join(kbRoot, 'Plugins', 'Finance', 'access.md'),
87
87
  '---\nread:\n - everyone\n---\nread: []\n',
88
88
  );
89
89
 
@@ -133,7 +133,7 @@ async function makeHarness(opts: HarnessOpts = {}) {
133
133
 
134
134
  const index =
135
135
  opts.index ??
136
- new GroupIndexService(workspaceService, accessControl, skillService, toolService, KB);
136
+ new PluginIndexService(workspaceService, accessControl, skillService, toolService, KB);
137
137
 
138
138
  const email = opts.email === undefined ? ALI : opts.email;
139
139
  const app = express();
@@ -153,13 +153,13 @@ async function makeHarness(opts: HarnessOpts = {}) {
153
153
  // Provisioning MECHANISM is exercised by its own service tests; the routes
154
154
  // here only need to prove what they hand it and when they refuse to.
155
155
  const provision = {
156
- createGroup: vi.fn(async () => ({ folder: 'GTM', created: true })),
157
- deleteGroup: vi.fn(async () => undefined),
156
+ createPlugin: vi.fn(async () => ({ folder: 'GTM', created: true })),
157
+ deletePlugin: vi.fn(async () => undefined),
158
158
  };
159
159
 
160
160
  app.use(
161
161
  '/api',
162
- createGroupsRoutes(
162
+ createPluginsRoutes(
163
163
  index,
164
164
  accessControl,
165
165
  workflow,
@@ -191,10 +191,10 @@ function close(s: Server): Promise<void> {
191
191
  }
192
192
 
193
193
  const MEMBER_OF_BOTH = {
194
- [ALI]: ['Groups/GTM', 'Groups/GTM/access.md', 'Groups/Finance', 'Groups/Finance/access.md'],
194
+ [ALI]: ['Plugins/GTM', 'Plugins/GTM/access.md', 'Plugins/Finance', 'Plugins/Finance/access.md'],
195
195
  };
196
196
 
197
- describe('/api/groups routes', () => {
197
+ describe('/api/plugins routes', () => {
198
198
  let server: Server | null = null;
199
199
  afterEach(async () => {
200
200
  if (server) await close(server);
@@ -202,51 +202,51 @@ describe('/api/groups routes', () => {
202
202
  await Promise.all(tmpDirs.splice(0).map((d) => fs.rm(d, { recursive: true, force: true })));
203
203
  });
204
204
 
205
- const listGroups = async (baseUrl: string) => {
206
- const res = await fetch(`${baseUrl}/api/groups`);
207
- const body = (await res.json()) as { groups: GroupSummary[] };
208
- return { status: res.status, groups: body.groups, raw: JSON.stringify(body) };
205
+ const listPlugins = async (baseUrl: string) => {
206
+ const res = await fetch(`${baseUrl}/api/plugins`);
207
+ const body = (await res.json()) as { plugins: PluginSummary[] };
208
+ return { status: res.status, plugins: body.plugins, raw: JSON.stringify(body) };
209
209
  };
210
210
 
211
211
  it('401s both endpoints when req.userEmail is absent', async () => {
212
212
  const h = await makeHarness({ email: null });
213
213
  server = h.server;
214
214
  for (const [method, url] of [
215
- ['GET', '/api/groups'],
216
- ['POST', '/api/groups/GTM/join-request'],
217
- ['DELETE', '/api/groups/GTM'],
215
+ ['GET', '/api/plugins'],
216
+ ['POST', '/api/plugins/GTM/join-request'],
217
+ ['DELETE', '/api/plugins/GTM'],
218
218
  ] as const) {
219
219
  const res = await fetch(`${h.baseUrl}${url}`, { method });
220
220
  expect(res.status, `${method} ${url}`).toBe(401);
221
221
  }
222
222
  });
223
223
 
224
- it('lists member groups sorted, counting by groupOfPath', async () => {
224
+ it('lists member plugins sorted, counting by pluginOfPath', async () => {
225
225
  const h = await makeHarness({
226
226
  readable: MEMBER_OF_BOTH,
227
- skills: [{ name: 'outreach', description: '', path: 'Groups/GTM/outreach' }],
227
+ skills: [{ name: 'outreach', description: '', path: 'Plugins/GTM/outreach' }],
228
228
  tools: [
229
- { slug: 'ledger', name: 'ledger', path: 'Groups/Finance/ledger.tool', type: 'inline' },
230
- { slug: 'slack', name: 'slack', path: 'Groups/slack.tool', type: 'inline' },
229
+ { slug: 'ledger', name: 'ledger', path: 'Plugins/Finance/ledger.tool', type: 'inline' },
230
+ { slug: 'slack', name: 'slack', path: 'Plugins/slack.tool', type: 'inline' },
231
231
  ],
232
232
  });
233
233
  server = h.server;
234
- const { status, groups } = await listGroups(h.baseUrl);
234
+ const { status, plugins } = await listPlugins(h.baseUrl);
235
235
  expect(status).toBe(200);
236
- expect(groups.map((g) => g.name)).toEqual(['Finance', 'GTM']);
237
- expect(groups[0]).toMatchObject({ canRead: true, skillCount: 0, toolCount: 1 });
238
- expect(groups[1]).toMatchObject({ canRead: true, skillCount: 1, toolCount: 0 });
236
+ expect(plugins.map((g) => g.name)).toEqual(['Finance', 'GTM']);
237
+ expect(plugins[0]).toMatchObject({ canRead: true, skillCount: 0, toolCount: 1 });
238
+ expect(plugins[1]).toMatchObject({ canRead: true, skillCount: 1, toolCount: 0 });
239
239
  });
240
240
 
241
- it('a DISCOVERABLE group (access.md readable, folder not) lists locked with hasRequested from the join CR', async () => {
241
+ it('a DISCOVERABLE plugin (access.md readable, folder not) lists locked with hasRequested from the join CR', async () => {
242
242
  const h = await makeHarness({
243
- readable: { [ALI]: ['Groups/Finance/access.md'] },
243
+ readable: { [ALI]: ['Plugins/Finance/access.md'] },
244
244
  authoredCrs: [cr({ branch: joinBranchFor(ALI, 'Finance'), number: 9 })],
245
245
  });
246
246
  server = h.server;
247
- const { groups } = await listGroups(h.baseUrl);
248
- expect(groups.map((g) => g.name)).toEqual(['Finance']);
249
- expect(groups[0]).toMatchObject({
247
+ const { plugins } = await listPlugins(h.baseUrl);
248
+ expect(plugins.map((g) => g.name)).toEqual(['Finance']);
249
+ expect(plugins[0]).toMatchObject({
250
250
  canRead: false,
251
251
  canWrite: false,
252
252
  hasRequested: true,
@@ -254,91 +254,91 @@ describe('/api/groups routes', () => {
254
254
  });
255
255
  });
256
256
 
257
- it('OMITS a group with NO verdict at all — nothing about it leaves the backend', async () => {
258
- const h = await makeHarness({ readable: { [ALI]: ['Groups/Finance/access.md'] } });
257
+ it('OMITS a plugin with NO verdict at all — nothing about it leaves the backend', async () => {
258
+ const h = await makeHarness({ readable: { [ALI]: ['Plugins/Finance/access.md'] } });
259
259
  server = h.server;
260
- const { groups, raw } = await listGroups(h.baseUrl);
261
- expect(groups.map((g) => g.name)).toEqual(['Finance']);
260
+ const { plugins, raw } = await listPlugins(h.baseUrl);
261
+ expect(plugins.map((g) => g.name)).toEqual(['Finance']);
262
262
  expect(raw).not.toContain('"name":"GTM"');
263
- expect(raw).not.toContain('Groups/GTM');
263
+ expect(raw).not.toContain('Plugins/GTM');
264
264
  });
265
265
 
266
- it("keeps a locked-out folder-writer's group listed with canWrite: true (admin-rescue)", async () => {
267
- const h = await makeHarness({ writable: { [ALI]: ['Groups/GTM/access.md'] } });
266
+ it("keeps a locked-out folder-writer's plugin listed with canWrite: true (admin-rescue)", async () => {
267
+ const h = await makeHarness({ writable: { [ALI]: ['Plugins/GTM/access.md'] } });
268
268
  server = h.server;
269
- const { groups } = await listGroups(h.baseUrl);
270
- expect(groups.map((g) => g.name)).toEqual(['GTM']);
271
- expect(groups[0]).toMatchObject({ canRead: false, canWrite: true });
269
+ const { plugins } = await listPlugins(h.baseUrl);
270
+ expect(plugins.map((g) => g.name)).toEqual(['GTM']);
271
+ expect(plugins[0]).toMatchObject({ canRead: false, canWrite: true });
272
272
  });
273
273
 
274
274
  it('a member never reports hasRequested (their stale join CR is ignored)', async () => {
275
275
  const h = await makeHarness({
276
- readable: { [ALI]: ['Groups/GTM', 'Groups/GTM/access.md'] },
276
+ readable: { [ALI]: ['Plugins/GTM', 'Plugins/GTM/access.md'] },
277
277
  authoredCrs: [cr({})],
278
278
  });
279
279
  server = h.server;
280
- const { groups } = await listGroups(h.baseUrl);
281
- expect(groups[0]).toMatchObject({ name: 'GTM', canRead: true, hasRequested: false });
280
+ const { plugins } = await listPlugins(h.baseUrl);
281
+ expect(plugins[0]).toMatchObject({ name: 'GTM', canRead: true, hasRequested: false });
282
282
  });
283
283
 
284
284
  it('lists the owner verdict per caller — the folder verdict, not the manager one', async () => {
285
285
  const h = await makeHarness({
286
286
  readable: MEMBER_OF_BOTH,
287
- owner: { [ALI]: ['Groups/GTM'] },
287
+ owner: { [ALI]: ['Plugins/GTM'] },
288
288
  });
289
289
  server = h.server;
290
- const { groups } = await listGroups(h.baseUrl);
291
- expect(groups.find((g) => g.name === 'GTM')).toMatchObject({ isOwner: true });
292
- expect(groups.find((g) => g.name === 'Finance')).toMatchObject({ isOwner: false });
290
+ const { plugins } = await listPlugins(h.baseUrl);
291
+ expect(plugins.find((g) => g.name === 'GTM')).toMatchObject({ isOwner: true });
292
+ expect(plugins.find((g) => g.name === 'Finance')).toMatchObject({ isOwner: false });
293
293
  });
294
294
 
295
295
  it('delete: 404 for unknown AND for a non-owner — a manager included (identical, fail-closed)', async () => {
296
296
  // A MANAGER (write on the access.md) and a MEMBER, but not an owner:
297
297
  // deletion is the owner's verb, and the refusal must not confirm the
298
- // group exists.
298
+ // plugin exists.
299
299
  const h = await makeHarness({
300
300
  readable: MEMBER_OF_BOTH,
301
- writable: { [ALI]: ['Groups/GTM/access.md'] },
301
+ writable: { [ALI]: ['Plugins/GTM/access.md'] },
302
302
  });
303
303
  server = h.server;
304
304
  for (const name of ['Nope', 'GTM']) {
305
- const res = await fetch(`${h.baseUrl}/api/groups/${name}`, { method: 'DELETE' });
305
+ const res = await fetch(`${h.baseUrl}/api/plugins/${name}`, { method: 'DELETE' });
306
306
  expect(res.status, name).toBe(404);
307
- expect(await res.json()).toEqual({ error: 'Unknown group', kind: 'unknown-group' });
307
+ expect(await res.json()).toEqual({ error: 'Unknown plugin', kind: 'unknown-plugin' });
308
308
  }
309
- expect(h.provision.deleteGroup).not.toHaveBeenCalled();
309
+ expect(h.provision.deletePlugin).not.toHaveBeenCalled();
310
310
  });
311
311
 
312
312
  it('delete: an OWNER deletes through the provision door, by the catalog name', async () => {
313
- const h = await makeHarness({ owner: { [ALI]: ['Groups/GTM'] } });
313
+ const h = await makeHarness({ owner: { [ALI]: ['Plugins/GTM'] } });
314
314
  server = h.server;
315
- const res = await fetch(`${h.baseUrl}/api/groups/GTM`, { method: 'DELETE' });
315
+ const res = await fetch(`${h.baseUrl}/api/plugins/GTM`, { method: 'DELETE' });
316
316
  expect(res.status).toBe(200);
317
317
  expect(await res.json()).toEqual({ ok: true });
318
- expect(h.provision.deleteGroup).toHaveBeenCalledWith(
318
+ expect(h.provision.deletePlugin).toHaveBeenCalledWith(
319
319
  expect.objectContaining({ email: ALI }),
320
320
  'GTM',
321
321
  );
322
322
  });
323
323
 
324
324
  it("delete: passes a provision refusal through with the service's own status and words", async () => {
325
- const h = await makeHarness({ owner: { [ALI]: ['Groups/GTM'] } });
326
- const { GroupProvisionError } = await import('../group-provision.service.js');
327
- h.provision.deleteGroup.mockRejectedValueOnce(new GroupProvisionError('Unknown group', 404));
325
+ const h = await makeHarness({ owner: { [ALI]: ['Plugins/GTM'] } });
326
+ const { PluginProvisionError } = await import('../plugin-provision.service.js');
327
+ h.provision.deletePlugin.mockRejectedValueOnce(new PluginProvisionError('Unknown plugin', 404));
328
328
  server = h.server;
329
- const res = await fetch(`${h.baseUrl}/api/groups/GTM`, { method: 'DELETE' });
329
+ const res = await fetch(`${h.baseUrl}/api/plugins/GTM`, { method: 'DELETE' });
330
330
  expect(res.status).toBe(404);
331
- expect(await res.json()).toEqual({ error: 'Unknown group' });
331
+ expect(await res.json()).toEqual({ error: 'Unknown plugin' });
332
332
  });
333
333
 
334
334
  it('delete: 500s with its own words when the mechanism fails', async () => {
335
335
  const error = vi.spyOn(console, 'error').mockImplementation(() => {});
336
- const h = await makeHarness({ owner: { [ALI]: ['Groups/GTM'] } });
337
- h.provision.deleteGroup.mockRejectedValueOnce(new Error('push refused'));
336
+ const h = await makeHarness({ owner: { [ALI]: ['Plugins/GTM'] } });
337
+ h.provision.deletePlugin.mockRejectedValueOnce(new Error('push refused'));
338
338
  server = h.server;
339
- const res = await fetch(`${h.baseUrl}/api/groups/GTM`, { method: 'DELETE' });
339
+ const res = await fetch(`${h.baseUrl}/api/plugins/GTM`, { method: 'DELETE' });
340
340
  expect(res.status).toBe(500);
341
- expect(await res.json()).toEqual({ error: 'Failed to delete the group' });
341
+ expect(await res.json()).toEqual({ error: 'Failed to delete the plugin' });
342
342
  error.mockRestore();
343
343
  });
344
344
 
@@ -346,33 +346,33 @@ describe('/api/groups routes', () => {
346
346
  const h = await makeHarness({});
347
347
  server = h.server;
348
348
  for (const name of ['Nope', 'GTM']) {
349
- const res = await fetch(`${h.baseUrl}/api/groups/${name}/join-request`, { method: 'POST' });
349
+ const res = await fetch(`${h.baseUrl}/api/plugins/${name}/join-request`, { method: 'POST' });
350
350
  expect(res.status, name).toBe(404);
351
- expect(await res.json()).toEqual({ error: 'Unknown group', kind: 'unknown-group' });
351
+ expect(await res.json()).toEqual({ error: 'Unknown plugin', kind: 'unknown-plugin' });
352
352
  }
353
353
  });
354
354
 
355
355
  it('join-request: 409 when the caller can already read the folder', async () => {
356
356
  const h = await makeHarness({ readable: MEMBER_OF_BOTH });
357
357
  server = h.server;
358
- const res = await fetch(`${h.baseUrl}/api/groups/Finance/join-request`, { method: 'POST' });
358
+ const res = await fetch(`${h.baseUrl}/api/plugins/Finance/join-request`, { method: 'POST' });
359
359
  expect(res.status).toBe(409);
360
360
  expect((await res.json()).kind).toBe('already-readable');
361
361
  });
362
362
 
363
363
  it('join-request: branch + splice + commit + CR, on the deterministic join branch', async () => {
364
- const h = await makeHarness({ readable: { [ALI]: ['Groups/Finance/access.md'] } });
364
+ const h = await makeHarness({ readable: { [ALI]: ['Plugins/Finance/access.md'] } });
365
365
  server = h.server;
366
- const res = await fetch(`${h.baseUrl}/api/groups/Finance/join-request`, { method: 'POST' });
366
+ const res = await fetch(`${h.baseUrl}/api/plugins/Finance/join-request`, { method: 'POST' });
367
367
  expect(res.status).toBe(200);
368
368
  expect(await res.json()).toEqual({ ok: true, number: 42 });
369
369
 
370
370
  const branch = joinBranchFor(ALI, 'Finance');
371
371
  expect(h.workflow.createBranch).toHaveBeenCalledWith(wsId, branch, DEFAULT_BRANCH);
372
- // The write went to the group's access.md and added the caller to the
372
+ // The write went to the plugin's access.md and added the caller to the
373
373
  // BODY's read list (folder rules), leaving the discovery frontmatter alone.
374
374
  const write = (h.workspaceService.writeFile as ReturnType<typeof vi.fn>).mock.calls[0];
375
- expect(write[1]).toBe(`${KB}/Groups/Finance/access.md`);
375
+ expect(write[1]).toBe(`${KB}/Plugins/Finance/access.md`);
376
376
  expect(write[2]).toContain('Ali Baba <ali@bevel.software>');
377
377
  expect((write[2] as string).indexOf('everyone')).toBeLessThan(
378
378
  (write[2] as string).indexOf('Ali Baba'),
@@ -391,11 +391,11 @@ describe('/api/groups routes', () => {
391
391
 
392
392
  it('join-request is idempotent: an existing open join CR is returned, nothing new is created', async () => {
393
393
  const h = await makeHarness({
394
- readable: { [ALI]: ['Groups/Finance/access.md'] },
394
+ readable: { [ALI]: ['Plugins/Finance/access.md'] },
395
395
  authoredCrs: [cr({ branch: joinBranchFor(ALI, 'Finance'), number: 9 })],
396
396
  });
397
397
  server = h.server;
398
- const res = await fetch(`${h.baseUrl}/api/groups/Finance/join-request`, { method: 'POST' });
398
+ const res = await fetch(`${h.baseUrl}/api/plugins/Finance/join-request`, { method: 'POST' });
399
399
  expect(res.status).toBe(200);
400
400
  expect(await res.json()).toEqual({ ok: true, number: 9 });
401
401
  expect(h.workflow.createBranch).not.toHaveBeenCalled();
@@ -403,27 +403,27 @@ describe('/api/groups routes', () => {
403
403
  });
404
404
 
405
405
  it('degrades hasRequested to false when the CR lookup throws', async () => {
406
- const h = await makeHarness({ readable: { [ALI]: ['Groups/Finance/access.md'] } });
406
+ const h = await makeHarness({ readable: { [ALI]: ['Plugins/Finance/access.md'] } });
407
407
  (h.workflow.listChangeRequestsAuthoredBy as ReturnType<typeof vi.fn>).mockRejectedValueOnce(
408
408
  new Error('gh down'),
409
409
  );
410
410
  const warn = vi.spyOn(console, 'warn').mockImplementation(() => {});
411
411
  server = h.server;
412
- const { status, groups } = await listGroups(h.baseUrl);
412
+ const { status, plugins } = await listPlugins(h.baseUrl);
413
413
  expect(status).toBe(200);
414
- expect(groups[0]).toMatchObject({ hasRequested: false });
414
+ expect(plugins[0]).toMatchObject({ hasRequested: false });
415
415
  warn.mockRestore();
416
416
  });
417
417
 
418
- it('join-requests: a MANAGER gets the service\'s list for the group', async () => {
419
- const h = await makeHarness({ writable: { [ALI]: ['Groups/GTM/access.md'] } });
418
+ it('join-requests: a MANAGER gets the service\'s list for the plugin', async () => {
419
+ const h = await makeHarness({ writable: { [ALI]: ['Plugins/GTM/access.md'] } });
420
420
  server = h.server;
421
- const res = await fetch(`${h.baseUrl}/api/groups/GTM/join-requests`);
421
+ const res = await fetch(`${h.baseUrl}/api/plugins/GTM/join-requests`);
422
422
  expect(res.status).toBe(200);
423
423
  expect(await res.json()).toEqual({ requests: [] });
424
424
  expect(h.joinRequests.list).toHaveBeenCalledWith(
425
425
  'GTM',
426
- 'Groups/GTM',
426
+ 'Plugins/GTM',
427
427
  expect.anything(),
428
428
  expect.objectContaining({ email: ALI }),
429
429
  );
@@ -434,7 +434,7 @@ describe('/api/groups routes', () => {
434
434
  // question only the server answers.
435
435
  const h = await makeHarness({ readable: MEMBER_OF_BOTH });
436
436
  server = h.server;
437
- const res = await fetch(`${h.baseUrl}/api/groups/GTM/join-requests`);
437
+ const res = await fetch(`${h.baseUrl}/api/plugins/GTM/join-requests`);
438
438
  expect(res.status).toBe(200);
439
439
  expect(await res.json()).toEqual({ requests: [] });
440
440
  expect(h.joinRequests.list).not.toHaveBeenCalled();
@@ -443,7 +443,7 @@ describe('/api/groups routes', () => {
443
443
  it('reconcile: 404 for a non-manager — indistinguishable from a missing request', async () => {
444
444
  const h = await makeHarness({ readable: MEMBER_OF_BOTH, authoredCrs: [cr({ number: 7 })] });
445
445
  server = h.server;
446
- const denied = await fetch(`${h.baseUrl}/api/groups/GTM/join-requests/7/reconcile`, {
446
+ const denied = await fetch(`${h.baseUrl}/api/plugins/GTM/join-requests/7/reconcile`, {
447
447
  method: 'POST',
448
448
  });
449
449
  expect(denied.status).toBe(404);
@@ -451,9 +451,9 @@ describe('/api/groups routes', () => {
451
451
  });
452
452
 
453
453
  it('reconcile: 404 for a manager when the change request does not exist', async () => {
454
- const h = await makeHarness({ writable: { [ALI]: ['Groups/GTM/access.md'] } });
454
+ const h = await makeHarness({ writable: { [ALI]: ['Plugins/GTM/access.md'] } });
455
455
  server = h.server;
456
- const missing = await fetch(`${h.baseUrl}/api/groups/GTM/join-requests/999/reconcile`, {
456
+ const missing = await fetch(`${h.baseUrl}/api/plugins/GTM/join-requests/999/reconcile`, {
457
457
  method: 'POST',
458
458
  });
459
459
  expect(missing.status).toBe(404);
@@ -462,19 +462,19 @@ describe('/api/groups routes', () => {
462
462
 
463
463
  it('reconcile: a manager settles an open request through the service', async () => {
464
464
  const h = await makeHarness({
465
- writable: { [ALI]: ['Groups/GTM/access.md'] },
465
+ writable: { [ALI]: ['Plugins/GTM/access.md'] },
466
466
  authoredCrs: [cr({ number: 7 })],
467
467
  });
468
468
  (h.joinRequests.reconcile as ReturnType<typeof vi.fn>).mockResolvedValue(true);
469
469
  server = h.server;
470
- const res = await fetch(`${h.baseUrl}/api/groups/GTM/join-requests/7/reconcile`, {
470
+ const res = await fetch(`${h.baseUrl}/api/plugins/GTM/join-requests/7/reconcile`, {
471
471
  method: 'POST',
472
472
  });
473
473
  expect(res.status).toBe(200);
474
474
  expect(await res.json()).toEqual({ closed: true });
475
475
  });
476
476
 
477
- it('500s with { error: "Failed to list groups" } when the index throws', async () => {
477
+ it('500s with { error: "Failed to list plugins" } when the index throws', async () => {
478
478
  const error = vi.spyOn(console, 'error').mockImplementation(() => {});
479
479
  const h = await makeHarness({
480
480
  index: {
@@ -485,9 +485,9 @@ describe('/api/groups routes', () => {
485
485
  },
486
486
  });
487
487
  server = h.server;
488
- const res = await fetch(`${h.baseUrl}/api/groups`);
488
+ const res = await fetch(`${h.baseUrl}/api/plugins`);
489
489
  expect(res.status).toBe(500);
490
- expect(await res.json()).toEqual({ error: 'Failed to list groups' });
490
+ expect(await res.json()).toEqual({ error: 'Failed to list plugins' });
491
491
  error.mockRestore();
492
492
  });
493
493
  });
@@ -0,0 +1,17 @@
1
+ export { PluginIndexService, pluginsWorkspaceId } from './plugins.service.js';
2
+ export {
3
+ PluginProvisionService,
4
+ PluginProvisionError,
5
+ pluginAccessMd,
6
+ personalAccessMd,
7
+ } from './plugin-provision.service.js';
8
+ export { JoinRequestsService, type JoinRequest } from './join-requests.service.js';
9
+ export { pendingProposals, type JoinProposal } from './join-proposals.js';
10
+ export { createPluginsRoutes } from './plugins.routes.js';
11
+ export type {
12
+ IPluginIndexService,
13
+ PluginCatalogEntry,
14
+ PluginSummary,
15
+ ResolvedPrincipals,
16
+ ResolvedReaders,
17
+ } from './plugins.contract.js';
@@ -8,9 +8,9 @@ import {
8
8
  /**
9
9
  * What a join branch is PROPOSING, relative to the default branch.
10
10
  *
11
- * A join request is a change request whose branch edits one group's
11
+ * A join request is a change request whose branch edits one plugin's
12
12
  * `access.md`. Rather than treating that change request as an all-or-nothing
13
- * merge, the group's managers see its individual proposals — "grant Ali read",
13
+ * merge, the plugin's managers see its individual proposals — "grant Ali read",
14
14
  * "grant GTM Team write" — and answer them one at a time. Approving a proposal
15
15
  * writes THAT ONE grant onto the default branch through the ordinary access
16
16
  * mutation path; the branch is never merged, so nothing else it happens to
@@ -45,15 +45,15 @@ export class JoinRequestsService {
45
45
  ) {}
46
46
 
47
47
  /**
48
- * Open join requests for `group`, each carrying only its still-pending
48
+ * Open join requests for `plugin`, each carrying only its still-pending
49
49
  * proposals. Requests with none left are reconciled away (rejected, branch
50
50
  * deleted) and omitted.
51
51
  *
52
- * `actor` is who the reconciliation acts as — a manager of the group, since
52
+ * `actor` is who the reconciliation acts as — a manager of the plugin, since
53
53
  * rejecting a change request requires write on everything it touches.
54
54
  */
55
55
  async list(
56
- group: string,
56
+ plugin: string,
57
57
  folder: string,
58
58
  crs: ChangeRequest[],
59
59
  actor: AuthUser,
@@ -63,7 +63,7 @@ export class JoinRequestsService {
63
63
  const baseText = await this.readAt(DEFAULT_BRANCH, accessPath);
64
64
  const out: JoinRequest[] = [];
65
65
  for (const cr of crs) {
66
- if (cr.state !== 'open' || !isJoinBranchFor(cr.branch, group)) continue;
66
+ if (cr.state !== 'open' || !isJoinBranchFor(cr.branch, plugin)) continue;
67
67
  const proposals = pendingProposals(
68
68
  await this.readAt(cr.branch, accessPath),
69
69
  baseText,
@@ -90,12 +90,12 @@ export class JoinRequestsService {
90
90
  * same round-trip instead of waiting for the next listing.
91
91
  */
92
92
  async reconcile(
93
- group: string,
93
+ plugin: string,
94
94
  folder: string,
95
95
  cr: ChangeRequest,
96
96
  actor: AuthUser,
97
97
  ): Promise<boolean> {
98
- if (cr.state !== 'open' || !isJoinBranchFor(cr.branch, group)) return false;
98
+ if (cr.state !== 'open' || !isJoinBranchFor(cr.branch, plugin)) return false;
99
99
  await this.refresh();
100
100
  const accessPath = `${folder}/access.md`;
101
101
  const proposals = pendingProposals(
@@ -133,7 +133,7 @@ export class JoinRequestsService {
133
133
  );
134
134
  } catch (err) {
135
135
  console.warn(
136
- `[groups] could not close settled join request #${cr.number}: ${
136
+ `[plugins] could not close settled join request #${cr.number}: ${
137
137
  err instanceof Error ? err.message : String(err)
138
138
  }`,
139
139
  );
@@ -145,7 +145,7 @@ export class JoinRequestsService {
145
145
  // The request is closed either way; a leftover branch is cosmetic and
146
146
  // the author can still delete it themselves.
147
147
  console.warn(
148
- `[groups] closed join request #${cr.number} but could not delete "${cr.branch}": ${
148
+ `[plugins] closed join request #${cr.number} but could not delete "${cr.branch}": ${
149
149
  err instanceof Error ? err.message : String(err)
150
150
  }`,
151
151
  );