@bevel-software/platform-core-backend 0.7.4 → 0.8.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 (246) hide show
  1. package/LICENSE +202 -202
  2. package/THIRD-PARTY-NOTICES.md +428 -454
  3. package/dist/core/core-ports.d.ts +1 -1
  4. package/dist/core/create-core-server.d.ts.map +1 -1
  5. package/dist/core/create-core-server.js +51 -7
  6. package/dist/core/create-core-server.js.map +1 -1
  7. package/dist/core/create-core-services.d.ts +5 -3
  8. package/dist/core/create-core-services.d.ts.map +1 -1
  9. package/dist/core/create-core-services.js +38 -21
  10. package/dist/core/create-core-services.js.map +1 -1
  11. package/dist/modules/access/access-control.interface.d.ts +8 -7
  12. package/dist/modules/access/access-control.interface.d.ts.map +1 -1
  13. package/dist/modules/access/access-control.service.d.ts +1 -1
  14. package/dist/modules/access/access-control.service.d.ts.map +1 -1
  15. package/dist/modules/access/access-control.service.js +6 -6
  16. package/dist/modules/access/access-control.service.js.map +1 -1
  17. package/dist/modules/access/access-declarations.d.ts +5 -5
  18. package/dist/modules/access/access-declarations.js +3 -3
  19. package/dist/modules/access/access-mutation.service.d.ts +3 -3
  20. package/dist/modules/access/access-mutation.service.d.ts.map +1 -1
  21. package/dist/modules/access/access-mutation.service.js +3 -3
  22. package/dist/modules/access/access-mutation.service.js.map +1 -1
  23. package/dist/modules/access/access-splice.js +4 -4
  24. package/dist/modules/access/access-splice.js.map +1 -1
  25. package/dist/modules/access/access.routes.js +19 -19
  26. package/dist/modules/access/access.routes.js.map +1 -1
  27. package/dist/modules/access/creator-access.d.ts +2 -2
  28. package/dist/modules/access/creator-access.js +5 -5
  29. package/dist/modules/access/creator-access.js.map +1 -1
  30. package/dist/modules/access/roles-admin.service.d.ts +18 -4
  31. package/dist/modules/access/roles-admin.service.d.ts.map +1 -1
  32. package/dist/modules/access/roles-admin.service.js +14 -4
  33. package/dist/modules/access/roles-admin.service.js.map +1 -1
  34. package/dist/modules/code-mode/code-mode-names.d.ts +5 -13
  35. package/dist/modules/code-mode/code-mode-names.d.ts.map +1 -1
  36. package/dist/modules/code-mode/code-mode-names.js +5 -27
  37. package/dist/modules/code-mode/code-mode-names.js.map +1 -1
  38. package/dist/modules/code-mode/code-mode.tool.d.ts.map +1 -1
  39. package/dist/modules/code-mode/code-mode.tool.js +29 -7
  40. package/dist/modules/code-mode/code-mode.tool.js.map +1 -1
  41. package/dist/modules/mcp/mcp.service.d.ts +11 -52
  42. package/dist/modules/mcp/mcp.service.d.ts.map +1 -1
  43. package/dist/modules/mcp/mcp.service.js +33 -395
  44. package/dist/modules/mcp/mcp.service.js.map +1 -1
  45. package/dist/modules/plugins/index.d.ts +7 -0
  46. package/dist/modules/plugins/index.d.ts.map +1 -0
  47. package/dist/modules/plugins/index.js +6 -0
  48. package/dist/modules/plugins/index.js.map +1 -0
  49. package/dist/modules/plugins/join-proposals.d.ts +53 -0
  50. package/dist/modules/plugins/join-proposals.d.ts.map +1 -0
  51. package/dist/modules/plugins/join-proposals.js +67 -0
  52. package/dist/modules/plugins/join-proposals.js.map +1 -0
  53. package/dist/modules/plugins/join-requests.service.d.ts +81 -0
  54. package/dist/modules/plugins/join-requests.service.d.ts.map +1 -0
  55. package/dist/modules/plugins/join-requests.service.js +135 -0
  56. package/dist/modules/plugins/join-requests.service.js.map +1 -0
  57. package/dist/modules/plugins/plugin-provision.service.d.ts +134 -0
  58. package/dist/modules/plugins/plugin-provision.service.d.ts.map +1 -0
  59. package/dist/modules/plugins/plugin-provision.service.js +344 -0
  60. package/dist/modules/plugins/plugin-provision.service.js.map +1 -0
  61. package/dist/modules/plugins/plugins.contract.d.ts +106 -0
  62. package/dist/modules/plugins/plugins.contract.d.ts.map +1 -0
  63. package/dist/modules/plugins/plugins.contract.js +36 -0
  64. package/dist/modules/plugins/plugins.contract.js.map +1 -0
  65. package/dist/modules/plugins/plugins.routes.d.ts +42 -0
  66. package/dist/modules/plugins/plugins.routes.d.ts.map +1 -0
  67. package/dist/modules/plugins/plugins.routes.js +379 -0
  68. package/dist/modules/plugins/plugins.routes.js.map +1 -0
  69. package/dist/modules/plugins/plugins.service.d.ts +60 -0
  70. package/dist/modules/plugins/plugins.service.d.ts.map +1 -0
  71. package/dist/modules/plugins/plugins.service.js +172 -0
  72. package/dist/modules/plugins/plugins.service.js.map +1 -0
  73. package/dist/modules/skills/pending-skills.service.d.ts +2 -2
  74. package/dist/modules/skills/pending-skills.service.js +7 -7
  75. package/dist/modules/skills/pending-skills.service.js.map +1 -1
  76. package/dist/modules/skills/skills.contract.d.ts +4 -4
  77. package/dist/modules/skills/skills.contract.d.ts.map +1 -1
  78. package/dist/modules/skills/skills.contract.js +1 -1
  79. package/dist/modules/skills/skills.service.js +5 -5
  80. package/dist/modules/skills/skills.service.js.map +1 -1
  81. package/dist/modules/tool-manuals/mcp-json-discovery.d.ts +65 -0
  82. package/dist/modules/tool-manuals/mcp-json-discovery.d.ts.map +1 -0
  83. package/dist/modules/tool-manuals/mcp-json-discovery.js +276 -0
  84. package/dist/modules/tool-manuals/mcp-json-discovery.js.map +1 -0
  85. package/dist/modules/tool-manuals/mcp-server-edit.service.d.ts +92 -0
  86. package/dist/modules/tool-manuals/mcp-server-edit.service.d.ts.map +1 -0
  87. package/dist/modules/tool-manuals/mcp-server-edit.service.js +328 -0
  88. package/dist/modules/tool-manuals/mcp-server-edit.service.js.map +1 -0
  89. package/dist/modules/tool-manuals/tool-manuals.contract.d.ts +38 -12
  90. package/dist/modules/tool-manuals/tool-manuals.contract.d.ts.map +1 -1
  91. package/dist/modules/tool-manuals/tool-manuals.contract.js +1 -1
  92. package/dist/modules/tool-manuals/tool-manuals.routes.d.ts +13 -2
  93. package/dist/modules/tool-manuals/tool-manuals.routes.d.ts.map +1 -1
  94. package/dist/modules/tool-manuals/tool-manuals.routes.js +233 -2
  95. package/dist/modules/tool-manuals/tool-manuals.routes.js.map +1 -1
  96. package/dist/modules/tool-manuals/tool-manuals.service.d.ts.map +1 -1
  97. package/dist/modules/tool-manuals/tool-manuals.service.js +74 -37
  98. package/dist/modules/tool-manuals/tool-manuals.service.js.map +1 -1
  99. package/dist/modules/tool-manuals/tool-manuals.tools.js +6 -3
  100. package/dist/modules/tool-manuals/tool-manuals.tools.js.map +1 -1
  101. package/dist/modules/workflow/git/git.service.js +2 -2
  102. package/dist/modules/workflow/git/git.service.js.map +1 -1
  103. package/dist/modules/workflow/pending-commits.service.d.ts +11 -0
  104. package/dist/modules/workflow/pending-commits.service.d.ts.map +1 -1
  105. package/dist/modules/workflow/pending-commits.service.js +37 -9
  106. package/dist/modules/workflow/pending-commits.service.js.map +1 -1
  107. package/dist/modules/workflow/pending-commits.worker.d.ts +17 -4
  108. package/dist/modules/workflow/pending-commits.worker.d.ts.map +1 -1
  109. package/dist/modules/workflow/pending-commits.worker.js +63 -9
  110. package/dist/modules/workflow/pending-commits.worker.js.map +1 -1
  111. package/dist/modules/workflow/workflow.service.d.ts +1 -0
  112. package/dist/modules/workflow/workflow.service.d.ts.map +1 -1
  113. package/dist/modules/workflow/workflow.service.js +6 -1
  114. package/dist/modules/workflow/workflow.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/workspace.routes.js +3 -3
  124. package/dist/modules/workspace/workspace.routes.js.map +1 -1
  125. package/dist/modules/workspace/workspace.service.d.ts +26 -0
  126. package/dist/modules/workspace/workspace.service.d.ts.map +1 -1
  127. package/dist/modules/workspace/workspace.service.js +83 -12
  128. package/dist/modules/workspace/workspace.service.js.map +1 -1
  129. package/dist/shared/kb-layout.test.js +3 -3
  130. package/dist/shared/kb-layout.test.js.map +1 -1
  131. package/dist/shared/utcp-namespace.d.ts +6 -27
  132. package/dist/shared/utcp-namespace.d.ts.map +1 -1
  133. package/dist/shared/utcp-namespace.js +6 -63
  134. package/dist/shared/utcp-namespace.js.map +1 -1
  135. package/dist/shared/variable-refs.d.ts +42 -0
  136. package/dist/shared/variable-refs.d.ts.map +1 -0
  137. package/dist/shared/variable-refs.js +60 -0
  138. package/dist/shared/variable-refs.js.map +1 -0
  139. package/kb-template/.bevelignore +1 -1
  140. package/kb-template/AGENTS.md +88 -35
  141. package/kb-template/KnowledgeBase/How to get started.md +10 -10
  142. package/kb-template/access.md +36 -36
  143. package/migrations/meta/0000_snapshot.json +1479 -1479
  144. package/package.json +5 -4
  145. package/src/assets.ts +25 -25
  146. package/src/core/core-ports.ts +106 -106
  147. package/src/core/create-core-server.ts +55 -9
  148. package/src/core/create-core-services.ts +48 -24
  149. package/src/index.ts +69 -69
  150. package/src/modules/access/__tests__/access-control.atref-batch.test.ts +98 -98
  151. package/src/modules/access/__tests__/access-declarations.test.ts +28 -28
  152. package/src/modules/access/__tests__/access-md-format.test.ts +18 -18
  153. package/src/modules/access/__tests__/access-mutation.service.test.ts +5 -5
  154. package/src/modules/access/__tests__/access-splice.test.ts +2 -2
  155. package/src/modules/access/__tests__/access.routes.overrides.test.ts +16 -16
  156. package/src/modules/access/__tests__/grant-sources.test.ts +12 -12
  157. package/src/modules/access/__tests__/roles-admin.service.test.ts +13 -1
  158. package/src/modules/access/access-control.interface.ts +8 -7
  159. package/src/modules/access/access-control.service.ts +7 -7
  160. package/src/modules/access/access-declarations.ts +5 -5
  161. package/src/modules/access/access-mutation.service.ts +3 -3
  162. package/src/modules/access/access-splice.ts +4 -4
  163. package/src/modules/access/access.routes.ts +20 -20
  164. package/src/modules/access/creator-access.ts +5 -5
  165. package/src/modules/access/roles-admin.service.ts +13 -2
  166. package/src/modules/admin/admin-access.routes.ts +29 -29
  167. package/src/modules/auth/__tests__/auth.routes.test.ts +91 -91
  168. package/src/modules/auth/__tests__/rate-limit.test.ts +36 -36
  169. package/src/modules/auth/rate-limit.ts +45 -45
  170. package/src/modules/code-mode/__tests__/code-mode.tool.test.ts +67 -0
  171. package/src/modules/code-mode/code-mode-names.ts +10 -36
  172. package/src/modules/code-mode/code-mode.tool.ts +27 -7
  173. package/src/modules/database/connection.ts +15 -15
  174. package/src/modules/database/schema.ts +11 -11
  175. package/src/modules/diff/__tests__/diff.routes.rejectPathsLocked.test.ts +150 -150
  176. package/src/modules/mcp/mcp.service.ts +57 -435
  177. package/src/modules/{groups → plugins}/__tests__/join-proposals.test.ts +1 -1
  178. package/src/modules/{groups → plugins}/__tests__/join-requests.service.test.ts +7 -7
  179. package/src/modules/{groups/__tests__/group-index.service.test.ts → plugins/__tests__/plugin-index.service.test.ts} +41 -41
  180. package/src/modules/plugins/__tests__/plugin-provision.service.test.ts +312 -0
  181. package/src/modules/{groups/__tests__/groups.routes.test.ts → plugins/__tests__/plugins.routes.test.ts} +100 -100
  182. package/src/modules/plugins/index.ts +17 -0
  183. package/src/modules/{groups → plugins}/join-proposals.ts +2 -2
  184. package/src/modules/{groups → plugins}/join-requests.service.ts +8 -8
  185. package/src/modules/{groups/group-provision.service.ts → plugins/plugin-provision.service.ts} +139 -69
  186. package/src/modules/{groups/groups.contract.ts → plugins/plugins.contract.ts} +26 -26
  187. package/src/modules/{groups/groups.routes.ts → plugins/plugins.routes.ts} +102 -102
  188. package/src/modules/{groups/groups.service.ts → plugins/plugins.service.ts} +43 -43
  189. package/src/modules/secrets-vault/__tests__/tool-owner-gate.route.test.ts +143 -143
  190. package/src/modules/skills/__tests__/pending-skills.service.test.ts +14 -14
  191. package/src/modules/skills/__tests__/skills.service.test.ts +13 -13
  192. package/src/modules/skills/pending-skills.service.ts +7 -7
  193. package/src/modules/skills/skills.contract.ts +4 -4
  194. package/src/modules/skills/skills.service.ts +5 -5
  195. package/src/modules/tool-auth/llm-usage-meter.ts +19 -19
  196. package/src/modules/tool-manuals/__tests__/mcp-json-discovery.test.ts +198 -0
  197. package/src/modules/tool-manuals/__tests__/mcp-server-edit.service.test.ts +346 -0
  198. package/src/modules/tool-manuals/__tests__/tool-manuals.archive.route.test.ts +101 -0
  199. package/src/modules/tool-manuals/__tests__/tool-manuals.detail.route.test.ts +3 -3
  200. package/src/modules/tool-manuals/__tests__/tool-manuals.mcp-oauth.test.ts +2 -2
  201. package/src/modules/tool-manuals/__tests__/tool-manuals.service.test.ts +27 -27
  202. package/src/modules/tool-manuals/__tests__/tool-manuals.tools.test.ts +3 -3
  203. package/src/modules/tool-manuals/mcp-json-discovery.ts +328 -0
  204. package/src/modules/tool-manuals/mcp-server-edit.service.ts +434 -0
  205. package/src/modules/tool-manuals/tool-manuals.contract.ts +35 -12
  206. package/src/modules/tool-manuals/tool-manuals.routes.ts +222 -1
  207. package/src/modules/tool-manuals/tool-manuals.service.ts +82 -42
  208. package/src/modules/tool-manuals/tool-manuals.tools.ts +6 -3
  209. package/src/modules/workflow/__tests__/pending-commits.worker.test.ts +118 -0
  210. package/src/modules/workflow/__tests__/workflow.service.facade.test.ts +1 -1
  211. package/src/modules/workflow/git/__tests__/branch-name.test.ts +3 -3
  212. package/src/modules/workflow/git/__tests__/git.service.accessGating.test.ts +3 -3
  213. package/src/modules/workflow/git/__tests__/git.service.commitFile.test.ts +13 -8
  214. package/src/modules/workflow/git/__tests__/git.service.deleteBranch.test.ts +1 -1
  215. package/src/modules/workflow/git/__tests__/pull-request.service.test.ts +1 -1
  216. package/src/modules/workflow/git/git.service.ts +2 -2
  217. package/src/modules/workflow/pending-commits.service.ts +48 -12
  218. package/src/modules/workflow/pending-commits.worker.ts +64 -8
  219. package/src/modules/workflow/review-workflow/__tests__/approval-states.test.ts +1 -1
  220. package/src/modules/workflow/review-workflow/__tests__/cancel-pr.test.ts +1 -1
  221. package/src/modules/workflow/workflow-hooks.ts +101 -101
  222. package/src/modules/workflow/workflow.service.ts +13 -2
  223. package/src/modules/workspace/__tests__/kb-seed.service.test.ts +81 -13
  224. package/src/modules/workspace/__tests__/plugins-migration.test.ts +427 -0
  225. package/src/modules/workspace/__tests__/session-ontology.gate.test.ts +237 -237
  226. package/src/modules/workspace/__tests__/workspace.routes.create-grant.test.ts +236 -236
  227. package/src/modules/workspace/__tests__/workspace.routes.delete.test.ts +179 -179
  228. package/src/modules/workspace/__tests__/workspace.routes.download.test.ts +320 -320
  229. package/src/modules/workspace/__tests__/workspace.routes.read-gate.test.ts +337 -337
  230. package/src/modules/workspace/__tests__/workspace.service.test.ts +116 -0
  231. package/src/modules/workspace/bevel-ignore.ts +66 -66
  232. package/src/modules/workspace/kb-seed.service.ts +38 -9
  233. package/src/modules/workspace/plugins-migration.ts +479 -0
  234. package/src/modules/workspace/session-sink.ts +25 -25
  235. package/src/modules/workspace/workspace.routes.ts +3 -3
  236. package/src/modules/workspace/workspace.service.ts +85 -14
  237. package/src/modules/workspace/workspace.tools.ts +922 -922
  238. package/src/shared/__tests__/join-request.test.ts +13 -13
  239. package/src/shared/__tests__/kb-layout.plugin.test.ts +45 -0
  240. package/src/shared/kb-layout.test.ts +3 -3
  241. package/src/shared/utcp-namespace.ts +10 -68
  242. package/src/shared/variable-refs.ts +64 -0
  243. package/src/modules/groups/__tests__/group-provision.service.test.ts +0 -247
  244. package/src/modules/groups/index.ts +0 -17
  245. package/src/shared/__tests__/kb-layout.group.test.ts +0 -45
  246. /package/kb-template/{Groups → Plugins}/.gitkeep +0 -0
@@ -435,6 +435,122 @@ describe('WorkspaceService — clone bootstrap & sibling reference', () => {
435
435
  await svc.getOrCreateForBranch('alice/draft');
436
436
  expect(cloned).toEqual([workspaceIdForBranch('alice/draft')]);
437
437
  });
438
+
439
+ // An upgraded deployment REUSES the persistent clone, so a top-up bound to
440
+ // fresh clones alone never runs a new build's scaffolding or migrations —
441
+ // the Groups→Plugins rename sat out an upgrade exactly this way. Every boot
442
+ // must offer the top-up to an existing clone once.
443
+ it('offers the scaffolding top-up to an existing clone once per process', async () => {
444
+ const seed = {
445
+ ensureRemoteSeeded: vi.fn(async () => {}),
446
+ topUpWorkspace: vi.fn(async () => {}),
447
+ };
448
+ const firstBoot = new WorkspaceService(workspacesRoot, upstream, 'knowledge-base');
449
+ firstBoot.setSeedService(seed);
450
+ await firstBoot.getOrCreateForBranch('target-company-state');
451
+ expect(seed.topUpWorkspace).toHaveBeenCalledTimes(1); // the fresh clone
452
+
453
+ // A new process over the same workspaces dir — the deployed-upgrade case:
454
+ // the clone exists, and the top-up must still run, once, not per access.
455
+ const nextBoot = new WorkspaceService(workspacesRoot, upstream, 'knowledge-base');
456
+ nextBoot.setSeedService(seed);
457
+ await nextBoot.getOrCreateForBranch('target-company-state');
458
+ await nextBoot.getOrCreateForBranch('target-company-state');
459
+ expect(seed.topUpWorkspace).toHaveBeenCalledTimes(2);
460
+ });
461
+ });
462
+
463
+ describe('WorkspaceService — scaffolding top-up on restart-survivor clones', () => {
464
+ let root: string;
465
+
466
+ beforeEach(async () => {
467
+ root = await mkTmpRoot();
468
+ // The fake `.git` in seedBranchWorkspace makes normalizeCloneTracking's
469
+ // git calls fail; that path only warns, which is noise here.
470
+ vi.spyOn(console, 'warn').mockImplementation(() => {});
471
+ });
472
+
473
+ afterEach(async () => {
474
+ vi.restoreAllMocks();
475
+ await fs.rm(root, { recursive: true, force: true });
476
+ });
477
+
478
+ // The top-up MOVES files (the Groups→Plugins migration runs inside it), so a
479
+ // caller handed the workspace mid-run reads a tree with both halves missing.
480
+ it('makes a concurrent opener wait out the in-flight top-up instead of returning mid-migration', async () => {
481
+ await seedBranchWorkspace(root, 'target-company-state');
482
+ const svc = new WorkspaceService(root, 'https://github.com/Bevel-Software/knowledge-base.git', 'knowledge-base');
483
+ let release!: () => void;
484
+ const gate = new Promise<void>((r) => { release = r; });
485
+ const seed = {
486
+ ensureRemoteSeeded: vi.fn(async () => {}),
487
+ topUpWorkspace: vi.fn(() => gate),
488
+ };
489
+ svc.setSeedService(seed);
490
+
491
+ let aDone = false;
492
+ let bDone = false;
493
+ const a = svc.getOrCreateForBranch('target-company-state').then((v) => { aDone = true; return v; });
494
+ const b = svc.getOrCreateForBranch('target-company-state').then((v) => { bDone = true; return v; });
495
+ // Wait until the top-up has started (the fake clone makes the preceding
496
+ // git-config repair fail slowly, so poll rather than sleep), then give
497
+ // both callers time to settle against the gate.
498
+ await vi.waitFor(() => expect(seed.topUpWorkspace).toHaveBeenCalledTimes(1));
499
+ await new Promise((r) => setTimeout(r, 30));
500
+ // Neither caller has been handed the workspace while the top-up runs —
501
+ // and the second did not start a rival run.
502
+ expect(aDone).toBe(false);
503
+ expect(bDone).toBe(false);
504
+ expect(seed.topUpWorkspace).toHaveBeenCalledTimes(1);
505
+
506
+ release();
507
+ const [infoA, infoB] = await Promise.all([a, b]);
508
+ expect(infoA.id).toBe(infoB.id);
509
+ });
510
+
511
+ it('re-offers the top-up after deleteWorkspace — the claim died with the clone', async () => {
512
+ await seedBranchWorkspace(root, 'target-company-state');
513
+ const svc = new WorkspaceService(root, 'https://github.com/Bevel-Software/knowledge-base.git', 'knowledge-base');
514
+ const seed = {
515
+ ensureRemoteSeeded: vi.fn(async () => {}),
516
+ topUpWorkspace: vi.fn(async () => {}),
517
+ };
518
+ svc.setSeedService(seed);
519
+
520
+ await svc.getOrCreateForBranch('target-company-state');
521
+ expect(seed.topUpWorkspace).toHaveBeenCalledTimes(1);
522
+
523
+ await svc.deleteWorkspace(workspaceIdForBranch('target-company-state'));
524
+ // A later bootstrap re-creates the clone (seeded by hand here); it must
525
+ // be offered the top-up afresh — the old claim was about a clone that no
526
+ // longer exists.
527
+ await seedBranchWorkspace(root, 'target-company-state');
528
+ await svc.getOrCreateForBranch('target-company-state');
529
+ expect(seed.topUpWorkspace).toHaveBeenCalledTimes(2);
530
+ });
531
+
532
+ it('re-offers the top-up after the orphan sweep removes the clone — same eviction rule', async () => {
533
+ await seedBranchWorkspace(root, 'target-company-state');
534
+ const svc = new WorkspaceService(root, 'https://github.com/Bevel-Software/knowledge-base.git', 'knowledge-base');
535
+ const seed = {
536
+ ensureRemoteSeeded: vi.fn(async () => {}),
537
+ topUpWorkspace: vi.fn(async () => {}),
538
+ };
539
+ svc.setSeedService(seed);
540
+
541
+ await svc.getOrCreateForBranch('target-company-state');
542
+ expect(seed.topUpWorkspace).toHaveBeenCalledTimes(1);
543
+
544
+ // The branch vanishes from the known set; the sweep reclaims its clone.
545
+ const { removed } = await svc.sweepOrphanedWorkspaces([]);
546
+ expect(removed).toContain(workspaceIdForBranch('target-company-state'));
547
+
548
+ // Re-created before any restart: the fresh clone must still get its one
549
+ // top-up — the claim died with the directory the sweep removed.
550
+ await seedBranchWorkspace(root, 'target-company-state');
551
+ await svc.getOrCreateForBranch('target-company-state');
552
+ expect(seed.topUpWorkspace).toHaveBeenCalledTimes(2);
553
+ });
438
554
  });
439
555
 
440
556
  describe('WorkspaceService.readAllKbFiles', () => {
@@ -1,66 +1,66 @@
1
- import fs from 'node:fs/promises';
2
- import path from 'node:path';
3
- import ignore, { type Ignore } from 'ignore';
4
-
5
- /**
6
- * The ignore file's own name. Exported because the file-tree filter hides it
7
- * from non-admins, and both places must agree on the spelling.
8
- */
9
- export const IGNORE_FILENAME = '.bevelignore';
10
-
11
- /** One .bevelignore file's rules, scoped to the directory it lives in. */
12
- interface IgnoreLayer {
13
- /** Absolute path of the directory where the .bevelignore lives. */
14
- readonly root: string;
15
- readonly matcher: Ignore;
16
- }
17
-
18
- /**
19
- * A hierarchical stack of `.bevelignore` rule sets.
20
- *
21
- * Each `.bevelignore` file applies to paths beneath the directory it lives in,
22
- * using standard gitignore syntax (negations, `**`, directory-only patterns, anchoring).
23
- * Deeper files combine with — and can override — rules from shallower files,
24
- * mirroring how git layers `.gitignore` files.
25
- */
26
- export class BevelIgnoreStack {
27
- private constructor(private readonly layers: readonly IgnoreLayer[]) {}
28
-
29
- static empty(): BevelIgnoreStack {
30
- return new BevelIgnoreStack([]);
31
- }
32
-
33
- /**
34
- * Return a new stack that additionally includes a `.bevelignore` from `dir`
35
- * (if present). If no `.bevelignore` exists in `dir`, returns this stack unchanged.
36
- */
37
- async extendedWith(dir: string): Promise<BevelIgnoreStack> {
38
- const file = path.join(dir, IGNORE_FILENAME);
39
- let contents: string;
40
- try {
41
- contents = await fs.readFile(file, 'utf-8');
42
- } catch {
43
- return this;
44
- }
45
- const matcher = ignore().add(contents);
46
- return new BevelIgnoreStack([...this.layers, { root: dir, matcher }]);
47
- }
48
-
49
- /**
50
- * Whether the given entry should be hidden from the file tree.
51
- * @param absolutePath absolute path of the entry
52
- * @param isDirectory needed because gitignore patterns ending in `/` match directories only
53
- */
54
- isIgnored(absolutePath: string, isDirectory: boolean): boolean {
55
- for (const layer of this.layers) {
56
- const rel = path.relative(layer.root, absolutePath).replace(/\\/g, '/');
57
- // Skip entries that aren't under this layer's root (shouldn't happen in normal
58
- // recursion, but guards against absolute-path quirks on Windows).
59
- if (!rel || rel.startsWith('..')) continue;
60
- // Append trailing slash for directories so `foo/`-style rules match correctly.
61
- const probe = isDirectory ? `${rel}/` : rel;
62
- if (layer.matcher.ignores(probe)) return true;
63
- }
64
- return false;
65
- }
66
- }
1
+ import fs from 'node:fs/promises';
2
+ import path from 'node:path';
3
+ import ignore, { type Ignore } from 'ignore';
4
+
5
+ /**
6
+ * The ignore file's own name. Exported because the file-tree filter hides it
7
+ * from non-admins, and both places must agree on the spelling.
8
+ */
9
+ export const IGNORE_FILENAME = '.bevelignore';
10
+
11
+ /** One .bevelignore file's rules, scoped to the directory it lives in. */
12
+ interface IgnoreLayer {
13
+ /** Absolute path of the directory where the .bevelignore lives. */
14
+ readonly root: string;
15
+ readonly matcher: Ignore;
16
+ }
17
+
18
+ /**
19
+ * A hierarchical stack of `.bevelignore` rule sets.
20
+ *
21
+ * Each `.bevelignore` file applies to paths beneath the directory it lives in,
22
+ * using standard gitignore syntax (negations, `**`, directory-only patterns, anchoring).
23
+ * Deeper files combine with — and can override — rules from shallower files,
24
+ * mirroring how git layers `.gitignore` files.
25
+ */
26
+ export class BevelIgnoreStack {
27
+ private constructor(private readonly layers: readonly IgnoreLayer[]) {}
28
+
29
+ static empty(): BevelIgnoreStack {
30
+ return new BevelIgnoreStack([]);
31
+ }
32
+
33
+ /**
34
+ * Return a new stack that additionally includes a `.bevelignore` from `dir`
35
+ * (if present). If no `.bevelignore` exists in `dir`, returns this stack unchanged.
36
+ */
37
+ async extendedWith(dir: string): Promise<BevelIgnoreStack> {
38
+ const file = path.join(dir, IGNORE_FILENAME);
39
+ let contents: string;
40
+ try {
41
+ contents = await fs.readFile(file, 'utf-8');
42
+ } catch {
43
+ return this;
44
+ }
45
+ const matcher = ignore().add(contents);
46
+ return new BevelIgnoreStack([...this.layers, { root: dir, matcher }]);
47
+ }
48
+
49
+ /**
50
+ * Whether the given entry should be hidden from the file tree.
51
+ * @param absolutePath absolute path of the entry
52
+ * @param isDirectory needed because gitignore patterns ending in `/` match directories only
53
+ */
54
+ isIgnored(absolutePath: string, isDirectory: boolean): boolean {
55
+ for (const layer of this.layers) {
56
+ const rel = path.relative(layer.root, absolutePath).replace(/\\/g, '/');
57
+ // Skip entries that aren't under this layer's root (shouldn't happen in normal
58
+ // recursion, but guards against absolute-path quirks on Windows).
59
+ if (!rel || rel.startsWith('..')) continue;
60
+ // Append trailing slash for directories so `foo/`-style rules match correctly.
61
+ const probe = isDirectory ? `${rel}/` : rel;
62
+ if (layer.matcher.ignores(probe)) return true;
63
+ }
64
+ return false;
65
+ }
66
+ }
@@ -3,8 +3,9 @@ import os from 'node:os';
3
3
  import path from 'node:path';
4
4
  import { execFile } from 'node:child_process';
5
5
  import { promisify } from 'node:util';
6
- import { KNOWLEDGE_BASE_DIR, GROUPS_DIR } from '@bevel-software/platform-shared';
6
+ import { KNOWLEDGE_BASE_DIR, LEGACY_GROUPS_DIR, PLUGINS_DIR } from '@bevel-software/platform-shared';
7
7
  import { IGNORE_FILENAME } from './bevel-ignore.js';
8
+ import { migrateGroupsToPlugins } from './plugins-migration.js';
8
9
  import type { IKbSeedService } from './kb-seed.interface.js';
9
10
 
10
11
  const execFileAsync = promisify(execFile);
@@ -36,7 +37,7 @@ const BOT_EMAIL = 'bevel-workflow@bevel.software';
36
37
  const REQUIRED_FILES: readonly string[] = ['access.md', 'AGENTS.md', '.bevelignore', '.gitignore'];
37
38
 
38
39
  /**
39
- * The two roots CORE gives a knowledge base: the ontologies, and the groups
40
+ * The two roots CORE gives a knowledge base: the ontologies, and the plugins
40
41
  * that hold skills and tools.
41
42
  *
42
43
  * `Data/`, `Agents/` and `Pipelines/` are deliberately absent. They scaffold
@@ -47,7 +48,7 @@ const REQUIRED_FILES: readonly string[] = ['access.md', 'AGENTS.md', '.beveligno
47
48
  * the names stay reserved in `kb-layout.ts` either way, so a KB that has them
48
49
  * still renders them as roots rather than folding them into Knowledge.
49
50
  */
50
- export const CORE_REQUIRED_DIRS: readonly string[] = [KNOWLEDGE_BASE_DIR, GROUPS_DIR];
51
+ export const CORE_REQUIRED_DIRS: readonly string[] = [KNOWLEDGE_BASE_DIR, PLUGINS_DIR];
51
52
 
52
53
  /**
53
54
  * A reserved root must be ONE path segment — `Data`, not `Data/x`, `../x` or
@@ -234,6 +235,29 @@ export class KbSeedService implements IKbSeedService {
234
235
  if (!this.protectedBranches().includes(branch)) return;
235
236
  try {
236
237
  const added: string[] = [];
238
+ // BEFORE the dir top-up: `ensureRequiredDirs` would otherwise create an
239
+ // empty `Plugins/` next to the `Plugins/` that is about to become it, and
240
+ // the migration refuses a destination that already exists — so the
241
+ // scaffolding would quietly block the very migration it precedes.
242
+ const migration = await migrateGroupsToPlugins(repoDir);
243
+ // Notes are logged unconditionally: an advisory note (a manual that
244
+ // refuses to convert) is exactly the run where the operator needs to
245
+ // hear about it, and such a run changes no files.
246
+ for (const note of migration.notes) console.log(`[plugins-migration] ${note}`);
247
+ // `migrated` means FILES CHANGED — a note-only run stages nothing, or
248
+ // the commit below would fail empty on every boot with a warning about
249
+ // a migration that did nothing.
250
+ if (migration.migrated) {
251
+ // Stage the legacy root ONLY when the rename happened this run: `git
252
+ // add -A -- Groups Plugins` fails outright on a pathspec that matches
253
+ // nothing, and a reorganisation inside an existing Plugins/ tree has
254
+ // no Groups/ to stage.
255
+ if (migration.renamed) added.push(LEGACY_GROUPS_DIR);
256
+ added.push(PLUGINS_DIR);
257
+ // The rename's companion edit to the repo-root ignore file — outside
258
+ // the two roots, so it needs its own pathspec to land in the commit.
259
+ if (migration.ignoreRewritten) added.push(IGNORE_FILENAME);
260
+ }
237
261
  for (const rel of REQUIRED_FILES) {
238
262
  if (!(await this.exists(path.join(repoDir, rel)))) {
239
263
  await this.copyTemplateFile(rel, repoDir);
@@ -284,9 +308,14 @@ export class KbSeedService implements IKbSeedService {
284
308
  // wasn't configured with one (the workspace clone already sets the same
285
309
  // values, so this is a harmless no-op there).
286
310
  await this.stampIdentity(repoDir);
287
- await this.git(repoDir, ['add', '--', ...added]);
288
- const message =
289
- agentsRefreshed && added.length === 1
311
+ // `-A` so the migration's renames stage their DELETIONS as well; a plain
312
+ // `add` would commit the new tree while leaving `Plugins/` in the index.
313
+ await this.git(repoDir, ['add', '-A', '--', ...added]);
314
+ const message = migration.migrated
315
+ ? migration.renamed
316
+ ? `Move ${LEGACY_GROUPS_DIR}/ to ${PLUGINS_DIR}/ (Agent Plugins layout)`
317
+ : `Reorganise ${PLUGINS_DIR}/ to the Agent Plugins layout`
318
+ : agentsRefreshed && added.length === 1
290
319
  ? 'Update AGENTS.md to the current platform template'
291
320
  : `Add missing KB scaffolding: ${added.join(', ')}`;
292
321
  await this.git(repoDir, ['commit', '-m', message]);
@@ -427,7 +456,7 @@ export class KbSeedService implements IKbSeedService {
427
456
  * where it came from. Absent file, or a file that already lists the pattern,
428
457
  * is a no-op, so running it on every clone changes nothing after the first.
429
458
  *
430
- * Matched line-wise rather than by substring: a rule for `Groups/AGENTS.md`
459
+ * Matched line-wise rather than by substring: a rule for `Plugins/AGENTS.md`
431
460
  * is not a rule for the root `AGENTS.md`, and treating it as one would leave
432
461
  * the mismatch this exists to close.
433
462
  */
@@ -467,10 +496,10 @@ export class KbSeedService implements IKbSeedService {
467
496
  for (const rootDir of this.requiredDirs) {
468
497
  const abs = path.join(repoDir, rootDir);
469
498
  // `lstat`, not `exists`: `fs.access` answers "is there something here?",
470
- // which is true of a FILE named `Groups` — and the old skip-if-present
499
+ // which is true of a FILE named `Plugins` — and the old skip-if-present
471
500
  // check then did nothing and reported success, leaving a knowledge base
472
501
  // permanently missing a root it claims to guarantee. `lstat` rather than
473
- // `stat` so a SYMLINK is rejected too: a link named `Groups` is not a KB
502
+ // `stat` so a SYMLINK is rejected too: a link named `Plugins` is not a KB
474
503
  // layout, and one pointing outside the repo would make every later write
475
504
  // into it land somewhere nobody asked for.
476
505
  const found = await this.lstatOrNull(abs);