@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
@@ -30,9 +30,40 @@ const execFileAsync = promisify(execFile);
30
30
  export function createSetupRoutes(
31
31
  settings: DeploymentSettingsService,
32
32
  adminAccess: IAdminAccessService,
33
+ /**
34
+ * The KB startup phase, invoked at its SECOND quiet moment: the save that
35
+ * completes first-time setup. The app is gated shut until exactly then
36
+ * (`isComplete`), so no session can be holding a working clone the phase
37
+ * would race.
38
+ */
39
+ kbStartupRunner: { runAll(): Promise<void> },
33
40
  ): express.Router {
34
41
  const router = express.Router();
35
42
 
43
+ /**
44
+ * Whether the last setup-time run of the KB startup phase FAILED. While
45
+ * true the deployment stays GATED: the settings are saved but the KB was
46
+ * never initialized, and reporting setup complete would open the app over
47
+ * an unmaintained (possibly unseeded) knowledge base. Saving the setup
48
+ * form again retries the phase; a server restart retries it at boot; a
49
+ * success clears the flag. Per-process state, like the gate itself.
50
+ */
51
+ let kbInitFailed = false;
52
+ /** The failure's message, surfaced to the ADMIN on the status endpoint. */
53
+ let kbInitError: string | null = null;
54
+ /**
55
+ * The setup-time run currently executing, if any. Its jobs: (a) keeping the
56
+ * status gate SHUT while a run executes (the settings read complete the
57
+ * moment they save, but the phase is still mutating branch trees — no
58
+ * workspace request may start yet), and (b) defense in depth via the `??=`
59
+ * at the run site, should a second invoker ever appear. It is NOT what
60
+ * serializes saves — the `saveTurn` chain below runs handlers strictly one
61
+ * at a time, phase included, so no second run can start while one executes.
62
+ */
63
+ let kbInitInFlight: Promise<void> | null = null;
64
+ /** The app-gate answer: settings complete AND the KB phase settled clean. */
65
+ const kbReady = () => isComplete(settings) && !kbInitFailed && kbInitInFlight === null;
66
+
36
67
  const requireAdmin: express.RequestHandler = async (req, res, next) => {
37
68
  if (!(await adminAccess.isAdmin(req.userEmail))) {
38
69
  res.status(403).json({ error: 'Admins only' });
@@ -51,25 +82,51 @@ export function createSetupRoutes(
51
82
  * secret's value.
52
83
  */
53
84
  router.get('/setup/status', async (req, res) => {
54
- const complete = isComplete(settings);
85
+ // A failed OR still-running setup-time KB initialization keeps setup
86
+ // INCOMPLETE: the frontend gates the app on this answer, and opening it
87
+ // over an uninitialized (or mid-mutation) KB would be worse than keeping
88
+ // the setup screen up.
89
+ //
90
+ // `kbReady()` is read fresh AFTER the admin check, immediately before
91
+ // building each response: the check awaits, a completion save can start
92
+ // the phase during that await, and a snapshot taken before it would
93
+ // resurrect a pre-phase `true` — opening the gate mid-mutation.
55
94
  if (!(await adminAccess.isAdmin(req.userEmail))) {
56
- res.json({ complete, isAdmin: false });
95
+ res.json({ complete: kbReady(), isAdmin: false });
57
96
  return;
58
97
  }
59
98
  res.json({
60
- complete,
99
+ complete: kbReady(),
61
100
  awaitingRestart: awaitingRestart(settings),
62
101
  isAdmin: true,
63
102
  settings: settings.describe(),
103
+ ...(kbInitFailed ? { kbInitError } : {}),
64
104
  });
65
105
  });
66
106
 
107
+ /**
108
+ * Setup saves run strictly ONE AT A TIME, phase included: `settings.save`
109
+ * updates the live cache write by write, and the completing save runs the
110
+ * KB startup phase, which reads its configuration through live getters —
111
+ * a save interleaving with either would hand half-updated state to the
112
+ * other. A plain promise chain is enough: saves are a setup-screen rarity,
113
+ * not a hot path, and a save arriving mid-run simply waits the previous
114
+ * save (and its phase) out before proceeding.
115
+ */
116
+ let saveTurn: Promise<unknown> = Promise.resolve();
117
+
67
118
  /**
68
119
  * Save settings. Validated and written as ONE batch — a repository URL
69
120
  * stored without the token that reads it is a deployment that fails at its
70
121
  * first clone, so a partial write is never better than none.
71
122
  */
72
123
  router.post('/setup/settings', requireAdmin, async (req, res) => {
124
+ const turn = saveTurn.then(() => handleSave(req, res));
125
+ saveTurn = turn.catch(() => {});
126
+ await turn;
127
+ });
128
+
129
+ async function handleSave(req: express.Request, res: express.Response): Promise<void> {
73
130
  const body = (req.body ?? {}) as { settings?: Record<string, unknown> };
74
131
  const entries: Record<string, string> = {};
75
132
  for (const [key, value] of Object.entries(body.settings ?? {})) {
@@ -80,6 +137,11 @@ export function createSetupRoutes(
80
137
  entries[key] = value;
81
138
  }
82
139
  try {
140
+ // Completion is a TRANSITION, so it is measured BEFORE the save: the
141
+ // save that flips it false→true — whichever field arrives last — is the
142
+ // one that must run the KB startup phase, regardless of which save
143
+ // configured the branch model.
144
+ const wasComplete = isComplete(settings);
83
145
  const { restartRequired } = await settings.save(entries, req.userId ?? null);
84
146
  /**
85
147
  * Apply the branch model to THIS process, so pressing Save finishes
@@ -102,10 +164,55 @@ export function createSetupRoutes(
102
164
  };
103
165
  if (!validateBranchModel(model)) configureBranchModel(model);
104
166
  }
167
+ /**
168
+ * The save that COMPLETES setup is the KB startup phase's SECOND quiet
169
+ * moment (the other is boot): the app was gated shut until this very
170
+ * response, so the trees are provably quiet. Run the phase now —
171
+ * seeding the remote, scaffolding and migrating every branch — so the
172
+ * first workspace request that follows finds a maintained KB.
173
+ *
174
+ * Runs on the false→true completion transition — including when the
175
+ * branch model was configured by an EARLIER save and the repository
176
+ * URL arrives on a later one — and again on any save while a previous
177
+ * setup-time run stands failed (`kbInitFailed`), so saving the form is
178
+ * the retry. Never on a re-save of a complete, healthy setup: with the
179
+ * gate open, sessions may be live and that is no longer a quiet moment.
180
+ */
181
+ if ((!wasComplete || kbInitFailed) && isComplete(settings)) {
182
+ try {
183
+ // One run at a time. The save chain already serializes handlers
184
+ // whole, so no second run can start while one executes; the `??=`
185
+ // is defense in depth should another invoker ever appear.
186
+ kbInitInFlight ??= kbStartupRunner.runAll();
187
+ await kbInitInFlight;
188
+ // Under KB_SAFE_BOOT a run that abandoned the phase still resolves
189
+ // and opens the gate DELIBERATELY — booting unmaintained so the
190
+ // operator can get in and fix things is exactly what the
191
+ // break-glass is for.
192
+ kbInitFailed = false;
193
+ kbInitError = null;
194
+ } catch (initErr) {
195
+ // The settings ARE saved — only the KB initialization failed. The
196
+ // deployment stays gated (see the status endpoint) until a retry
197
+ // succeeds. Logged in full, returned actionable.
198
+ const msg = initErr instanceof Error ? initErr.message : String(initErr);
199
+ console.error('[setup] KB initialization failed after setup completed:', msg);
200
+ kbInitFailed = true;
201
+ kbInitError = msg;
202
+ res.status(500).json({
203
+ error:
204
+ 'Settings saved, but the knowledge base could not be initialized. ' +
205
+ 'Saving the setup form again retries; restarting the server retries too.',
206
+ });
207
+ return;
208
+ } finally {
209
+ kbInitInFlight = null;
210
+ }
211
+ }
105
212
  res.json({
106
213
  ok: true,
107
214
  restartRequired,
108
- complete: isComplete(settings),
215
+ complete: kbReady(),
109
216
  awaitingRestart: awaitingRestart(settings),
110
217
  settings: settings.describe(),
111
218
  });
@@ -119,7 +226,7 @@ export function createSetupRoutes(
119
226
  console.error('[setup] save failed:', err instanceof Error ? err.message : String(err));
120
227
  res.status(500).json({ error: 'Could not save these settings.' });
121
228
  }
122
- });
229
+ }
123
230
 
124
231
  /**
125
232
  * Try the credentials against the real remote, BEFORE anything is saved.
@@ -40,7 +40,7 @@ function cr(over: Partial<ChangeRequest> = {}): ChangeRequest {
40
40
  base: 'main',
41
41
  state: 'open',
42
42
  createdAt: '2026-08-06T09:00:00.000Z',
43
- touchedNodePaths: ['Groups/Engineering/weekly-newsletter/SKILL.md'],
43
+ touchedNodePaths: ['Plugins/Engineering/weekly-newsletter/SKILL.md'],
44
44
  review: { approvals: 0, changesRequested: 0, pendingLogins: [] },
45
45
  url: '/change-requests/7',
46
46
  ...over,
@@ -56,7 +56,7 @@ function harness(opts: {
56
56
  writers?: Record<string, string[]>;
57
57
  }) {
58
58
  const branchFiles = opts.branchFiles ?? {
59
- 'agent/weekly-newsletter:Groups/Engineering/weekly-newsletter/SKILL.md': SKILL_MD,
59
+ 'agent/weekly-newsletter:Plugins/Engineering/weekly-newsletter/SKILL.md': SKILL_MD,
60
60
  };
61
61
  const workspaceService = {
62
62
  ensureRemotesFetched: async () => undefined,
@@ -81,7 +81,7 @@ function harness(opts: {
81
81
  }
82
82
 
83
83
  const ADMIN_WRITES = {
84
- 'Groups/Engineering/weekly-newsletter/SKILL.md': [ADMIN],
84
+ 'Plugins/Engineering/weekly-newsletter/SKILL.md': [ADMIN],
85
85
  };
86
86
 
87
87
  describe('PendingSkillsService', () => {
@@ -93,7 +93,7 @@ describe('PendingSkillsService', () => {
93
93
  name: 'weekly-newsletter',
94
94
  description: 'Drafts the Friday newsletter.',
95
95
  version: '0.1.0',
96
- path: 'Groups/Engineering/weekly-newsletter',
96
+ path: 'Plugins/Engineering/weekly-newsletter',
97
97
  changeRequestNumber: 7,
98
98
  branch: 'agent/weekly-newsletter',
99
99
  authorName: 'Ali Raza',
@@ -110,7 +110,7 @@ describe('PendingSkillsService', () => {
110
110
 
111
111
  /**
112
112
  * The decision that shapes this surface: a proposal is between its author and
113
- * the people who can approve it, and nobody else. A member of the group who
113
+ * the people who can approve it, and nobody else. A member of the plugin who
114
114
  * cannot write the folder is not one of those people.
115
115
  */
116
116
  test('hides it from everyone else', async () => {
@@ -143,7 +143,7 @@ describe('PendingSkillsService', () => {
143
143
  {
144
144
  name: 'weekly-newsletter',
145
145
  description: 'Drafts the Friday newsletter.',
146
- path: 'Groups/Engineering/weekly-newsletter',
146
+ path: 'Plugins/Engineering/weekly-newsletter',
147
147
  },
148
148
  ],
149
149
  writers: ADMIN_WRITES,
@@ -156,9 +156,9 @@ describe('PendingSkillsService', () => {
156
156
  crs: [
157
157
  cr({
158
158
  touchedNodePaths: [
159
- 'Groups/Engineering/access.md',
160
- 'Groups/Engineering/SKILL.md', // a group folder is not a skill
161
- 'Groups/Engineering/weekly-newsletter/reference.md',
159
+ 'Plugins/Engineering/access.md',
160
+ 'Plugins/Engineering/SKILL.md', // a plugin folder is not a skill
161
+ 'Plugins/Engineering/weekly-newsletter/reference.md',
162
162
  ],
163
163
  }),
164
164
  ],
@@ -185,7 +185,7 @@ describe('PendingSkillsService', () => {
185
185
  const svc = harness({
186
186
  crs: [cr()],
187
187
  branchFiles: {
188
- 'agent/weekly-newsletter:Groups/Engineering/weekly-newsletter/SKILL.md':
188
+ 'agent/weekly-newsletter:Plugins/Engineering/weekly-newsletter/SKILL.md':
189
189
  '---\ndescription: No name declared.\n---\n\n# Body\n',
190
190
  },
191
191
  writers: ADMIN_WRITES,
@@ -222,17 +222,17 @@ describe('PendingSkillsService', () => {
222
222
  number: 3,
223
223
  createdAt: '2026-08-01T12:00:00.000Z',
224
224
  branch: 'agent/older',
225
- touchedNodePaths: ['Groups/Engineering/older/SKILL.md'],
225
+ touchedNodePaths: ['Plugins/Engineering/older/SKILL.md'],
226
226
  }),
227
227
  ],
228
228
  branchFiles: {
229
- 'agent/weekly-newsletter:Groups/Engineering/weekly-newsletter/SKILL.md': SKILL_MD,
230
- 'agent/older:Groups/Engineering/older/SKILL.md':
229
+ 'agent/weekly-newsletter:Plugins/Engineering/weekly-newsletter/SKILL.md': SKILL_MD,
230
+ 'agent/older:Plugins/Engineering/older/SKILL.md':
231
231
  '---\nname: older\ndescription: Older.\n---\n',
232
232
  },
233
233
  writers: {
234
234
  ...ADMIN_WRITES,
235
- 'Groups/Engineering/older/SKILL.md': [ADMIN],
235
+ 'Plugins/Engineering/older/SKILL.md': [ADMIN],
236
236
  },
237
237
  });
238
238
  expect((await svc.listPendingSkills(ADMIN)).map((p) => p.changeRequestNumber)).toEqual([3, 9]);
@@ -50,14 +50,14 @@ describe('SkillService', () => {
50
50
 
51
51
  beforeEach(async () => {
52
52
  root = await mkdtemp(join(tmpdir(), 'skills-'));
53
- const skills = join(root, wsId, KB_DIR, 'Groups');
53
+ const skills = join(root, wsId, KB_DIR, 'Plugins');
54
54
  await mkdir(join(skills, 'rfi', 'scripts'), { recursive: true });
55
55
  await writeFile(join(skills, 'rfi', 'SKILL.md'), RFI_SKILL);
56
56
  await writeFile(join(skills, 'rfi', 'scripts', 'build_xlsx.py'), 'print("xlsx")\n');
57
57
  // A folder with no SKILL.md is not a skill.
58
58
  await mkdir(join(skills, 'not-a-skill'), { recursive: true });
59
59
  await writeFile(join(skills, 'not-a-skill', 'README.md'), '# nope\n');
60
- // A skill nested in a category subfolder (Groups/Development/coding-guidelines).
60
+ // A skill nested in a category subfolder (Plugins/Development/coding-guidelines).
61
61
  await mkdir(join(skills, 'Development', 'coding-guidelines'), { recursive: true });
62
62
  await writeFile(
63
63
  join(skills, 'Development', 'coding-guidelines', 'SKILL.md'),
@@ -72,7 +72,7 @@ describe('SkillService', () => {
72
72
  expect(skills).toHaveLength(2);
73
73
  const rfi = skills.find((s) => s.name === 'rfi')!;
74
74
  expect(rfi).toBeDefined();
75
- expect(rfi.path).toBe('Groups/rfi');
75
+ expect(rfi.path).toBe('Plugins/rfi');
76
76
  expect(rfi.version).toBe('1.4.0');
77
77
  expect(rfi.description).not.toBe('|');
78
78
  expect(rfi.description).toContain('Specialist RFI responder');
@@ -83,19 +83,19 @@ describe('SkillService', () => {
83
83
  const skills = await svc().listSkills();
84
84
  const cg = skills.find((s) => s.name === 'coding-guidelines')!;
85
85
  expect(cg).toBeDefined();
86
- expect(cg.path).toBe('Groups/Development/coding-guidelines');
86
+ expect(cg.path).toBe('Plugins/Development/coding-guidelines');
87
87
  expect(cg.description).toBe('Coding guidelines.');
88
88
 
89
89
  const res = await svc().getSkill('user@x.eu', 'coding-guidelines');
90
90
  expect(res.ok).toBe(true);
91
91
  if (res.ok && res.kind === 'skill') {
92
- expect(res.skill.path).toBe('Groups/Development/coding-guidelines');
92
+ expect(res.skill.path).toBe('Plugins/Development/coding-guidelines');
93
93
  expect(res.skill.body).toContain('# Guidelines');
94
94
  }
95
95
  });
96
96
 
97
97
  test('refuses a colliding id (no auto-suffix) — the shared dedup rule', async () => {
98
- const skills = join(root, wsId, KB_DIR, 'Groups');
98
+ const skills = join(root, wsId, KB_DIR, 'Plugins');
99
99
  // Two skills resolve to the same id `dup` (no frontmatter id/name → folder name).
100
100
  await mkdir(join(skills, 'Zeta', 'dup'), { recursive: true });
101
101
  await writeFile(join(skills, 'Zeta', 'dup', 'SKILL.md'), '---\ndescription: zeta dup\n---\n\n# Z\n');
@@ -104,28 +104,28 @@ describe('SkillService', () => {
104
104
 
105
105
  const list = await svc().listSkills();
106
106
  // Only the first (smallest path) survives; the duplicate is dropped, not suffixed.
107
- expect(list.filter((s) => s.name === 'dup').map((s) => s.path)).toEqual(['Groups/Alpha/dup']);
107
+ expect(list.filter((s) => s.name === 'dup').map((s) => s.path)).toEqual(['Plugins/Alpha/dup']);
108
108
  expect(list.find((s) => s.name === 'dup2')).toBeUndefined();
109
109
 
110
110
  const a = await svc().getSkill('user@x.eu', 'dup');
111
- expect(a.ok && a.kind === 'skill' && a.skill.path).toBe('Groups/Alpha/dup');
111
+ expect(a.ok && a.kind === 'skill' && a.skill.path).toBe('Plugins/Alpha/dup');
112
112
  });
113
113
 
114
114
  test('frontmatter `id`/`name` overrides the folder name for identity', async () => {
115
- const skills = join(root, wsId, KB_DIR, 'Groups');
115
+ const skills = join(root, wsId, KB_DIR, 'Plugins');
116
116
  await mkdir(join(skills, 'folderx'), { recursive: true });
117
117
  await writeFile(join(skills, 'folderx', 'SKILL.md'), '---\nid: my_skill\ndescription: d\n---\n\n# X\n');
118
118
  const list = await svc().listSkills();
119
- expect(list.find((s) => s.name === 'my_skill')?.path).toBe('Groups/folderx');
119
+ expect(list.find((s) => s.name === 'my_skill')?.path).toBe('Plugins/folderx');
120
120
  });
121
121
 
122
122
  test('getSkill returns body, folder path, files and allowed-tools', async () => {
123
123
  const res = await svc().getSkill('user@x.eu', 'rfi');
124
124
  expect(res.ok).toBe(true);
125
125
  if (res.ok && res.kind === 'skill') {
126
- expect(res.skill.path).toBe('Groups/rfi');
126
+ expect(res.skill.path).toBe('Plugins/rfi');
127
127
  expect(res.skill.body).toContain('# /rfi: Specialist RFI Responder');
128
- expect(res.skill.files).toEqual(['Groups/rfi/scripts/build_xlsx.py']);
128
+ expect(res.skill.files).toEqual(['Plugins/rfi/scripts/build_xlsx.py']);
129
129
  expect(res.skill.allowedTools).toEqual(['Bash', 'Read']);
130
130
  }
131
131
  });
@@ -134,7 +134,7 @@ describe('SkillService', () => {
134
134
  const res = await svc().getSkill('user@x.eu', 'rfi', 'scripts/build_xlsx.py');
135
135
  expect(res.ok).toBe(true);
136
136
  if (res.ok && res.kind === 'file') {
137
- expect(res.file.path).toBe('Groups/rfi/scripts/build_xlsx.py');
137
+ expect(res.file.path).toBe('Plugins/rfi/scripts/build_xlsx.py');
138
138
  expect(res.file.content).toContain('print("xlsx")');
139
139
  }
140
140
  });
@@ -1,6 +1,6 @@
1
1
  import {
2
2
  DEFAULT_BRANCH,
3
- GROUPS_DIR,
3
+ PLUGINS_DIR,
4
4
  type ChangeRequest,
5
5
  type IWorkflowService,
6
6
  } from '@bevel-software/platform-shared';
@@ -27,9 +27,9 @@ const SKILL_DOC = 'SKILL.md';
27
27
  *
28
28
  * WHO MAY SEE ONE is narrower than who may see the catalog: the author, and
29
29
  * whoever could approve it. "Could approve it" is not re-stated here as a
30
- * group-admin check — it is `canWrite` on the SKILL.md the request would
30
+ * plugin-admin check — it is `canWrite` on the SKILL.md the request would
31
31
  * create. The new folder carries no `access.md` of its own, so that verdict
32
- * inherits from the group folder, which IS the group's admin; asking the access
32
+ * inherits from the plugin folder, which IS the plugin's admin; asking the access
33
33
  * tree keeps this surface and the merge gate answering with one voice, and it
34
34
  * keeps working for skills nested in a category subfolder that carries rules of
35
35
  * its own.
@@ -80,7 +80,7 @@ export class PendingSkillsService implements IPendingSkillService {
80
80
  const authorHash = hashEmail(email);
81
81
 
82
82
  // One access load for every candidate. Resolved on the DEFAULT branch —
83
- // the tree as it stands is what says who owns the group the skill is
83
+ // the tree as it stands is what says who owns the plugin the skill is
84
84
  // arriving into; the branch's own copy is written by the proposer and must
85
85
  // not get a say in who reviews them.
86
86
  const writable = await this.accessControl
@@ -148,16 +148,16 @@ export class PendingSkillsService implements IPendingSkillService {
148
148
  /**
149
149
  * Is this touched path a skill's own SKILL.md?
150
150
  *
151
- * `Groups/<group>/<skill>/SKILL.md` is the shallowest shape, hence four
151
+ * `Plugins/<plugin>/<skill>/SKILL.md` is the shallowest shape, hence four
152
152
  * segments; deeper ones are skills nested in a category subfolder, which the
153
- * catalog already supports. `Groups/<group>/SKILL.md` is NOT a skill — a group
153
+ * catalog already supports. `Plugins/<plugin>/SKILL.md` is NOT a skill — a plugin
154
154
  * folder is not itself one.
155
155
  */
156
156
  function isSkillDoc(repoRelPath: string): boolean {
157
157
  const segments = repoRelPath.split('/');
158
158
  return (
159
159
  segments.length >= 4 &&
160
- segments[0] === GROUPS_DIR &&
160
+ segments[0] === PLUGINS_DIR &&
161
161
  segments[segments.length - 1] === SKILL_DOC
162
162
  );
163
163
  }
@@ -1,5 +1,5 @@
1
1
  /**
2
- * Skills are reusable specialist instructions living under `Groups/<group>/<name>/SKILL.md`
2
+ * Skills are reusable specialist instructions living under `Plugins/<plugin>/<name>/SKILL.md`
3
3
  * in the KB repo. Discovery/loading is pinned to the DEFAULT branch — the catalog
4
4
  * is a single global, released set (a skill on a draft isn't discoverable until
5
5
  * merged). Progressive disclosure: `listSkills` = name + description (level 1),
@@ -12,7 +12,7 @@ export interface SkillSummary {
12
12
  name: string;
13
13
  description: string;
14
14
  version?: string;
15
- /** Repo-root-relative skill folder, e.g. `Groups/Everyone/rfi`. */
15
+ /** Repo-root-relative skill folder, e.g. `Plugins/Everyone/rfi`. */
16
16
  path: string;
17
17
  }
18
18
 
@@ -21,7 +21,7 @@ export interface Skill extends SkillSummary {
21
21
  body: string;
22
22
  /** Pre-approved tools from the `allowed-tools` frontmatter, if declared. */
23
23
  allowedTools?: string[];
24
- /** Repo-root-relative bundled file paths, e.g. `Groups/Everyone/rfi/scripts/build_xlsx.py`. */
24
+ /** Repo-root-relative bundled file paths, e.g. `Plugins/Everyone/rfi/scripts/build_xlsx.py`. */
25
25
  files: string[];
26
26
  }
27
27
 
@@ -48,7 +48,7 @@ export type GetSkillResult =
48
48
  *
49
49
  * Who may see one is narrower than who may see the catalog: its author, and
50
50
  * whoever could approve it. See `PendingSkillsService` for why that predicate
51
- * is the access tree's answer rather than a group-admin check spelled out
51
+ * is the access tree's answer rather than a plugin-admin check spelled out
52
52
  * again here.
53
53
  */
54
54
  export interface PendingSkill extends SkillSummary {
@@ -1,7 +1,7 @@
1
1
  import path from 'node:path';
2
2
  import fs from 'node:fs/promises';
3
3
  import { parseDocument } from 'yaml';
4
- import { DEFAULT_BRANCH, GROUPS_DIR } from '@bevel-software/platform-shared';
4
+ import { DEFAULT_BRANCH, PLUGINS_DIR } from '@bevel-software/platform-shared';
5
5
  import type { WorkspaceService } from '../workspace/workspace.service.js';
6
6
  import { workspaceIdForBranch } from '../workspace/workspace.service.js';
7
7
  import type { IAccessControl } from '../access/access-control.interface.js';
@@ -103,8 +103,8 @@ export class SkillService implements ISkillService {
103
103
  }
104
104
 
105
105
  private async scanDisk(): Promise<ParsedSkill[]> {
106
- // Ensure the default-branch clone exists, then scan its Groups/ dir. Any
107
- // failure (no workspace, no Groups/ dir) degrades to an empty catalog — the
106
+ // Ensure the default-branch clone exists, then scan its Plugins/ dir. Any
107
+ // failure (no workspace, no Plugins/ dir) degrades to an empty catalog — the
108
108
  // manual/tools must never break because skills can't be read.
109
109
  let wsId: string;
110
110
  try {
@@ -119,7 +119,7 @@ export class SkillService implements ISkillService {
119
119
  // tree and treat every folder that directly contains a SKILL.md as a skill;
120
120
  // don't descend past it — its inner files are bundled assets, not nested
121
121
  // skills. The skill name is the leaf folder name; its path is the full
122
- // repo-relative folder (e.g. `Groups/Development/coding-guidelines`).
122
+ // repo-relative folder (e.g. `Plugins/Development/coding-guidelines`).
123
123
  const out: ParsedSkill[] = [];
124
124
  const walk = async (dir: string, relFolder: string): Promise<void> => {
125
125
  let entries: import('node:fs').Dirent[];
@@ -156,7 +156,7 @@ export class SkillService implements ISkillService {
156
156
  await walk(path.join(dir, entry.name), `${relFolder}/${entry.name}`);
157
157
  }
158
158
  };
159
- await walk(path.join(kbRoot, GROUPS_DIR), GROUPS_DIR);
159
+ await walk(path.join(kbRoot, PLUGINS_DIR), PLUGINS_DIR);
160
160
  // A skill's id (frontmatter `id`/`name`, else folder name) is how getSkill()
161
161
  // resolves it, so it must be unique. Sort by (name, path) for a deterministic
162
162
  // winner, then REFUSE later duplicates via the shared dedup — the same rule
@@ -1,19 +1,19 @@
1
- /**
2
- * Metering view over connection-key LLM usage, consumed by the connection-key
3
- * management routes (mcp) to show each key's spend against the daily budget.
4
- * The enterprise LLM proxy's `LlmUsageService` satisfies this structurally; a
5
- * core-only deployment (no LLM proxy, nothing to meter) passes
6
- * {@link unmeteredLlmUsage}.
7
- */
8
- export interface ILlmUsageMeter {
9
- /** Daily per-key token cap shown in the UI, or null when unmetered. */
10
- readonly dailyCap: number | null;
11
- /** Today's usage per key id, as `{ [tokenId]: tokens }` — missing keys mean 0. */
12
- usageForTokens(tokenIds: string[]): Promise<Record<string, number>>;
13
- }
14
-
15
- /** Core default: no proxy, no metering — every key reads as unused/uncapped. */
16
- export const unmeteredLlmUsage: ILlmUsageMeter = {
17
- dailyCap: null,
18
- usageForTokens: async () => ({}),
19
- };
1
+ /**
2
+ * Metering view over connection-key LLM usage, consumed by the connection-key
3
+ * management routes (mcp) to show each key's spend against the daily budget.
4
+ * The enterprise LLM proxy's `LlmUsageService` satisfies this structurally; a
5
+ * core-only deployment (no LLM proxy, nothing to meter) passes
6
+ * {@link unmeteredLlmUsage}.
7
+ */
8
+ export interface ILlmUsageMeter {
9
+ /** Daily per-key token cap shown in the UI, or null when unmetered. */
10
+ readonly dailyCap: number | null;
11
+ /** Today's usage per key id, as `{ [tokenId]: tokens }` — missing keys mean 0. */
12
+ usageForTokens(tokenIds: string[]): Promise<Record<string, number>>;
13
+ }
14
+
15
+ /** Core default: no proxy, no metering — every key reads as unused/uncapped. */
16
+ export const unmeteredLlmUsage: ILlmUsageMeter = {
17
+ dailyCap: null,
18
+ usageForTokens: async () => ({}),
19
+ };