@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.
- package/LICENSE +202 -202
- package/THIRD-PARTY-NOTICES.md +428 -454
- package/dist/core/core-ports.d.ts +8 -1
- package/dist/core/core-ports.d.ts.map +1 -1
- package/dist/core/core-ports.js.map +1 -1
- package/dist/core/create-core-server.d.ts.map +1 -1
- package/dist/core/create-core-server.js +75 -18
- package/dist/core/create-core-server.js.map +1 -1
- package/dist/core/create-core-services.d.ts +12 -5
- package/dist/core/create-core-services.d.ts.map +1 -1
- package/dist/core/create-core-services.js +69 -36
- package/dist/core/create-core-services.js.map +1 -1
- package/dist/modules/access/access-control.interface.d.ts +8 -7
- package/dist/modules/access/access-control.interface.d.ts.map +1 -1
- package/dist/modules/access/access-control.service.d.ts +1 -1
- package/dist/modules/access/access-control.service.d.ts.map +1 -1
- package/dist/modules/access/access-control.service.js +6 -6
- package/dist/modules/access/access-control.service.js.map +1 -1
- package/dist/modules/access/access-declarations.d.ts +5 -5
- package/dist/modules/access/access-declarations.js +3 -3
- package/dist/modules/access/access-mutation.service.d.ts +3 -3
- package/dist/modules/access/access-mutation.service.d.ts.map +1 -1
- package/dist/modules/access/access-mutation.service.js +3 -3
- package/dist/modules/access/access-mutation.service.js.map +1 -1
- package/dist/modules/access/access-splice.js +4 -4
- package/dist/modules/access/access-splice.js.map +1 -1
- package/dist/modules/access/access.routes.d.ts +3 -1
- package/dist/modules/access/access.routes.d.ts.map +1 -1
- package/dist/modules/access/access.routes.js +22 -20
- package/dist/modules/access/access.routes.js.map +1 -1
- package/dist/modules/access/creator-access.d.ts +2 -2
- package/dist/modules/access/creator-access.js +5 -5
- package/dist/modules/access/creator-access.js.map +1 -1
- package/dist/modules/access/render-roles-yaml.d.ts +22 -0
- package/dist/modules/access/render-roles-yaml.d.ts.map +1 -0
- package/dist/modules/access/render-roles-yaml.js +56 -0
- package/dist/modules/access/render-roles-yaml.js.map +1 -0
- package/dist/modules/access/roles-admin.service.d.ts +33 -5
- package/dist/modules/access/roles-admin.service.d.ts.map +1 -1
- package/dist/modules/access/roles-admin.service.js +44 -19
- package/dist/modules/access/roles-admin.service.js.map +1 -1
- package/dist/modules/code-mode/code-mode-names.d.ts +5 -13
- package/dist/modules/code-mode/code-mode-names.d.ts.map +1 -1
- package/dist/modules/code-mode/code-mode-names.js +5 -27
- package/dist/modules/code-mode/code-mode-names.js.map +1 -1
- package/dist/modules/code-mode/code-mode.tool.d.ts.map +1 -1
- package/dist/modules/code-mode/code-mode.tool.js +29 -7
- package/dist/modules/code-mode/code-mode.tool.js.map +1 -1
- package/dist/modules/mcp/mcp.service.d.ts +11 -52
- package/dist/modules/mcp/mcp.service.d.ts.map +1 -1
- package/dist/modules/mcp/mcp.service.js +33 -395
- package/dist/modules/mcp/mcp.service.js.map +1 -1
- package/dist/modules/plugins/index.d.ts +7 -0
- package/dist/modules/plugins/index.d.ts.map +1 -0
- package/dist/modules/plugins/index.js +6 -0
- package/dist/modules/plugins/index.js.map +1 -0
- package/dist/modules/plugins/join-proposals.d.ts +53 -0
- package/dist/modules/plugins/join-proposals.d.ts.map +1 -0
- package/dist/modules/plugins/join-proposals.js +67 -0
- package/dist/modules/plugins/join-proposals.js.map +1 -0
- package/dist/modules/plugins/join-requests.service.d.ts +81 -0
- package/dist/modules/plugins/join-requests.service.d.ts.map +1 -0
- package/dist/modules/plugins/join-requests.service.js +135 -0
- package/dist/modules/plugins/join-requests.service.js.map +1 -0
- package/dist/modules/plugins/plugin-provision.service.d.ts +134 -0
- package/dist/modules/plugins/plugin-provision.service.d.ts.map +1 -0
- package/dist/modules/plugins/plugin-provision.service.js +344 -0
- package/dist/modules/plugins/plugin-provision.service.js.map +1 -0
- package/dist/modules/plugins/plugins.contract.d.ts +106 -0
- package/dist/modules/plugins/plugins.contract.d.ts.map +1 -0
- package/dist/modules/plugins/plugins.contract.js +36 -0
- package/dist/modules/plugins/plugins.contract.js.map +1 -0
- package/dist/modules/plugins/plugins.routes.d.ts +42 -0
- package/dist/modules/plugins/plugins.routes.d.ts.map +1 -0
- package/dist/modules/plugins/plugins.routes.js +379 -0
- package/dist/modules/plugins/plugins.routes.js.map +1 -0
- package/dist/modules/plugins/plugins.service.d.ts +60 -0
- package/dist/modules/plugins/plugins.service.d.ts.map +1 -0
- package/dist/modules/plugins/plugins.service.js +172 -0
- package/dist/modules/plugins/plugins.service.js.map +1 -0
- package/dist/modules/settings/setup.routes.d.ts +10 -1
- package/dist/modules/settings/setup.routes.d.ts.map +1 -1
- package/dist/modules/settings/setup.routes.js +111 -6
- package/dist/modules/settings/setup.routes.js.map +1 -1
- package/dist/modules/skills/pending-skills.service.d.ts +2 -2
- package/dist/modules/skills/pending-skills.service.js +7 -7
- package/dist/modules/skills/pending-skills.service.js.map +1 -1
- package/dist/modules/skills/skills.contract.d.ts +4 -4
- package/dist/modules/skills/skills.contract.d.ts.map +1 -1
- package/dist/modules/skills/skills.contract.js +1 -1
- package/dist/modules/skills/skills.service.js +5 -5
- package/dist/modules/skills/skills.service.js.map +1 -1
- package/dist/modules/tool-manuals/mcp-json-discovery.d.ts +65 -0
- package/dist/modules/tool-manuals/mcp-json-discovery.d.ts.map +1 -0
- package/dist/modules/tool-manuals/mcp-json-discovery.js +276 -0
- package/dist/modules/tool-manuals/mcp-json-discovery.js.map +1 -0
- package/dist/modules/tool-manuals/mcp-server-edit.service.d.ts +92 -0
- package/dist/modules/tool-manuals/mcp-server-edit.service.d.ts.map +1 -0
- package/dist/modules/tool-manuals/mcp-server-edit.service.js +328 -0
- package/dist/modules/tool-manuals/mcp-server-edit.service.js.map +1 -0
- package/dist/modules/tool-manuals/tool-manuals.contract.d.ts +38 -12
- package/dist/modules/tool-manuals/tool-manuals.contract.d.ts.map +1 -1
- package/dist/modules/tool-manuals/tool-manuals.contract.js +1 -1
- package/dist/modules/tool-manuals/tool-manuals.routes.d.ts +13 -2
- package/dist/modules/tool-manuals/tool-manuals.routes.d.ts.map +1 -1
- package/dist/modules/tool-manuals/tool-manuals.routes.js +233 -2
- package/dist/modules/tool-manuals/tool-manuals.routes.js.map +1 -1
- package/dist/modules/tool-manuals/tool-manuals.service.d.ts.map +1 -1
- package/dist/modules/tool-manuals/tool-manuals.service.js +74 -37
- package/dist/modules/tool-manuals/tool-manuals.service.js.map +1 -1
- package/dist/modules/tool-manuals/tool-manuals.tools.js +6 -3
- package/dist/modules/tool-manuals/tool-manuals.tools.js.map +1 -1
- package/dist/modules/workflow/git/git.service.js +2 -2
- package/dist/modules/workflow/git/git.service.js.map +1 -1
- package/dist/modules/workspace/kb-seed.service.d.ts +2 -2
- package/dist/modules/workspace/kb-seed.service.d.ts.map +1 -1
- package/dist/modules/workspace/kb-seed.service.js +43 -10
- package/dist/modules/workspace/kb-seed.service.js.map +1 -1
- package/dist/modules/workspace/plugins-migration.d.ts +50 -0
- package/dist/modules/workspace/plugins-migration.d.ts.map +1 -0
- package/dist/modules/workspace/plugins-migration.js +379 -0
- package/dist/modules/workspace/plugins-migration.js.map +1 -0
- package/dist/modules/workspace/startup/kb-git.d.ts +23 -0
- package/dist/modules/workspace/startup/kb-git.d.ts.map +1 -0
- package/dist/modules/workspace/startup/kb-git.js +86 -0
- package/dist/modules/workspace/startup/kb-git.js.map +1 -0
- package/dist/modules/workspace/startup/kb-startup-runner.d.ts +74 -0
- package/dist/modules/workspace/startup/kb-startup-runner.d.ts.map +1 -0
- package/dist/modules/workspace/startup/kb-startup-runner.js +528 -0
- package/dist/modules/workspace/startup/kb-startup-runner.js.map +1 -0
- package/dist/modules/workspace/startup/on-server-start.d.ts +105 -0
- package/dist/modules/workspace/startup/on-server-start.d.ts.map +1 -0
- package/dist/modules/workspace/startup/on-server-start.js +21 -0
- package/dist/modules/workspace/startup/on-server-start.js.map +1 -0
- package/dist/modules/workspace/startup/steps/groups-to-plugins.step.d.ts +46 -0
- package/dist/modules/workspace/startup/steps/groups-to-plugins.step.d.ts.map +1 -0
- package/dist/modules/workspace/startup/steps/groups-to-plugins.step.js +492 -0
- package/dist/modules/workspace/startup/steps/groups-to-plugins.step.js.map +1 -0
- package/dist/modules/workspace/startup/steps/roles-yaml.step.d.ts +23 -0
- package/dist/modules/workspace/startup/steps/roles-yaml.step.d.ts.map +1 -0
- package/dist/modules/workspace/startup/steps/roles-yaml.step.js +69 -0
- package/dist/modules/workspace/startup/steps/roles-yaml.step.js.map +1 -0
- package/dist/modules/workspace/startup/steps/seed-tree.d.ts +17 -0
- package/dist/modules/workspace/startup/steps/seed-tree.d.ts.map +1 -0
- package/dist/modules/workspace/startup/steps/seed-tree.js +109 -0
- package/dist/modules/workspace/startup/steps/seed-tree.js.map +1 -0
- package/dist/modules/workspace/startup/steps/template-files.step.d.ts +103 -0
- package/dist/modules/workspace/startup/steps/template-files.step.d.ts.map +1 -0
- package/dist/modules/workspace/startup/steps/template-files.step.js +337 -0
- package/dist/modules/workspace/startup/steps/template-files.step.js.map +1 -0
- package/dist/modules/workspace/workspace.routes.js +3 -3
- package/dist/modules/workspace/workspace.routes.js.map +1 -1
- package/dist/modules/workspace/workspace.service.d.ts +0 -9
- package/dist/modules/workspace/workspace.service.d.ts.map +1 -1
- package/dist/modules/workspace/workspace.service.js +2 -30
- package/dist/modules/workspace/workspace.service.js.map +1 -1
- package/dist/shared/kb-layout.test.js +3 -3
- package/dist/shared/kb-layout.test.js.map +1 -1
- package/dist/shared/utcp-namespace.d.ts +6 -27
- package/dist/shared/utcp-namespace.d.ts.map +1 -1
- package/dist/shared/utcp-namespace.js +6 -63
- package/dist/shared/utcp-namespace.js.map +1 -1
- package/dist/shared/variable-refs.d.ts +42 -0
- package/dist/shared/variable-refs.d.ts.map +1 -0
- package/dist/shared/variable-refs.js +60 -0
- package/dist/shared/variable-refs.js.map +1 -0
- package/kb-template/.bevelignore +1 -1
- package/kb-template/AGENTS.md +88 -35
- package/kb-template/KnowledgeBase/How to get started.md +10 -10
- package/kb-template/access.md +36 -36
- package/kb-template/gitignore.template +16 -0
- package/migrations/meta/0000_snapshot.json +1479 -1479
- package/package.json +5 -4
- package/src/assets.ts +25 -25
- package/src/core/core-ports.ts +113 -106
- package/src/core/create-core-server.ts +85 -20
- package/src/core/create-core-services.ts +83 -42
- package/src/index.ts +69 -69
- package/src/modules/access/__tests__/access-control.atref-batch.test.ts +98 -98
- package/src/modules/access/__tests__/access-declarations.test.ts +28 -28
- package/src/modules/access/__tests__/access-md-format.test.ts +18 -18
- package/src/modules/access/__tests__/access-mutation.service.test.ts +5 -5
- package/src/modules/access/__tests__/access-splice.test.ts +2 -2
- package/src/modules/access/__tests__/access.routes.overrides.test.ts +16 -16
- package/src/modules/access/__tests__/grant-sources.test.ts +12 -12
- package/src/modules/access/__tests__/roles-admin.service.test.ts +36 -2
- package/src/modules/access/access-control.interface.ts +8 -7
- package/src/modules/access/access-control.service.ts +7 -7
- package/src/modules/access/access-declarations.ts +5 -5
- package/src/modules/access/access-mutation.service.ts +3 -3
- package/src/modules/access/access-splice.ts +4 -4
- package/src/modules/access/access.routes.ts +23 -20
- package/src/modules/access/creator-access.ts +5 -5
- package/src/modules/access/render-roles-yaml.ts +65 -0
- package/src/modules/access/roles-admin.service.ts +45 -16
- package/src/modules/admin/admin-access.routes.ts +29 -29
- package/src/modules/auth/__tests__/auth.routes.test.ts +91 -91
- package/src/modules/auth/__tests__/rate-limit.test.ts +36 -36
- package/src/modules/auth/rate-limit.ts +45 -45
- package/src/modules/code-mode/__tests__/code-mode.tool.test.ts +67 -0
- package/src/modules/code-mode/code-mode-names.ts +10 -36
- package/src/modules/code-mode/code-mode.tool.ts +27 -7
- package/src/modules/database/connection.ts +15 -15
- package/src/modules/database/schema.ts +11 -11
- package/src/modules/diff/__tests__/diff.routes.rejectPathsLocked.test.ts +150 -150
- package/src/modules/mcp/mcp.service.ts +57 -435
- package/src/modules/{groups → plugins}/__tests__/join-proposals.test.ts +1 -1
- package/src/modules/{groups → plugins}/__tests__/join-requests.service.test.ts +7 -7
- package/src/modules/{groups/__tests__/group-index.service.test.ts → plugins/__tests__/plugin-index.service.test.ts} +41 -41
- package/src/modules/plugins/__tests__/plugin-provision.service.test.ts +312 -0
- package/src/modules/{groups/__tests__/groups.routes.test.ts → plugins/__tests__/plugins.routes.test.ts} +100 -100
- package/src/modules/plugins/index.ts +17 -0
- package/src/modules/{groups → plugins}/join-proposals.ts +2 -2
- package/src/modules/{groups → plugins}/join-requests.service.ts +8 -8
- package/src/modules/{groups/group-provision.service.ts → plugins/plugin-provision.service.ts} +139 -69
- package/src/modules/{groups/groups.contract.ts → plugins/plugins.contract.ts} +26 -26
- package/src/modules/{groups/groups.routes.ts → plugins/plugins.routes.ts} +102 -102
- package/src/modules/{groups/groups.service.ts → plugins/plugins.service.ts} +43 -43
- package/src/modules/secrets-vault/__tests__/tool-owner-gate.route.test.ts +143 -143
- package/src/modules/settings/__tests__/setup.routes.test.ts +124 -3
- package/src/modules/settings/setup.routes.ts +112 -5
- package/src/modules/skills/__tests__/pending-skills.service.test.ts +14 -14
- package/src/modules/skills/__tests__/skills.service.test.ts +13 -13
- package/src/modules/skills/pending-skills.service.ts +7 -7
- package/src/modules/skills/skills.contract.ts +4 -4
- package/src/modules/skills/skills.service.ts +5 -5
- package/src/modules/tool-auth/llm-usage-meter.ts +19 -19
- package/src/modules/tool-manuals/__tests__/mcp-json-discovery.test.ts +198 -0
- package/src/modules/tool-manuals/__tests__/mcp-server-edit.service.test.ts +346 -0
- package/src/modules/tool-manuals/__tests__/tool-manuals.archive.route.test.ts +101 -0
- package/src/modules/tool-manuals/__tests__/tool-manuals.detail.route.test.ts +3 -3
- package/src/modules/tool-manuals/__tests__/tool-manuals.mcp-oauth.test.ts +2 -2
- package/src/modules/tool-manuals/__tests__/tool-manuals.service.test.ts +27 -27
- package/src/modules/tool-manuals/__tests__/tool-manuals.tools.test.ts +3 -3
- package/src/modules/tool-manuals/mcp-json-discovery.ts +328 -0
- package/src/modules/tool-manuals/mcp-server-edit.service.ts +434 -0
- package/src/modules/tool-manuals/tool-manuals.contract.ts +35 -12
- package/src/modules/tool-manuals/tool-manuals.routes.ts +222 -1
- package/src/modules/tool-manuals/tool-manuals.service.ts +82 -42
- package/src/modules/tool-manuals/tool-manuals.tools.ts +6 -3
- package/src/modules/workflow/__tests__/workflow.service.facade.test.ts +1 -1
- package/src/modules/workflow/git/__tests__/branch-name.test.ts +3 -3
- package/src/modules/workflow/git/__tests__/git.service.accessGating.test.ts +3 -3
- package/src/modules/workflow/git/__tests__/git.service.commitFile.test.ts +13 -8
- package/src/modules/workflow/git/__tests__/git.service.deleteBranch.test.ts +1 -1
- package/src/modules/workflow/git/__tests__/pull-request.service.test.ts +1 -1
- package/src/modules/workflow/git/git.service.ts +2 -2
- package/src/modules/workflow/review-workflow/__tests__/approval-states.test.ts +1 -1
- package/src/modules/workflow/review-workflow/__tests__/cancel-pr.test.ts +1 -1
- package/src/modules/workflow/workflow-hooks.ts +101 -101
- package/src/modules/workspace/__tests__/session-ontology.gate.test.ts +237 -237
- package/src/modules/workspace/__tests__/workspace.routes.create-grant.test.ts +236 -236
- package/src/modules/workspace/__tests__/workspace.routes.delete.test.ts +179 -179
- package/src/modules/workspace/__tests__/workspace.routes.download.test.ts +320 -320
- package/src/modules/workspace/__tests__/workspace.routes.read-gate.test.ts +337 -337
- package/src/modules/workspace/__tests__/workspace.service.test.ts +27 -0
- package/src/modules/workspace/bevel-ignore.ts +66 -66
- package/src/modules/workspace/session-sink.ts +25 -25
- package/src/modules/workspace/startup/__tests__/kb-startup-runner.test.ts +495 -0
- package/src/modules/workspace/startup/kb-git.ts +94 -0
- package/src/modules/workspace/startup/kb-startup-runner.ts +597 -0
- package/src/modules/workspace/startup/on-server-start.ts +97 -0
- package/src/modules/workspace/startup/steps/__tests__/steps.test.ts +636 -0
- package/src/modules/workspace/startup/steps/groups-to-plugins.step.ts +561 -0
- package/src/modules/workspace/startup/steps/roles-yaml.step.ts +71 -0
- package/src/modules/workspace/startup/steps/seed-tree.ts +115 -0
- package/src/modules/workspace/startup/steps/template-files.step.ts +360 -0
- package/src/modules/workspace/workspace.routes.ts +3 -3
- package/src/modules/workspace/workspace.service.ts +2 -35
- package/src/modules/workspace/workspace.tools.ts +922 -922
- package/src/shared/__tests__/join-request.test.ts +13 -13
- package/src/shared/__tests__/kb-layout.plugin.test.ts +45 -0
- package/src/shared/kb-layout.test.ts +3 -3
- package/src/shared/utcp-namespace.ts +10 -68
- package/src/shared/variable-refs.ts +64 -0
- package/src/modules/groups/__tests__/group-provision.service.test.ts +0 -247
- package/src/modules/groups/index.ts +0 -17
- package/src/modules/workspace/__tests__/kb-seed.service.test.ts +0 -444
- package/src/modules/workspace/kb-seed.interface.ts +0 -36
- package/src/modules/workspace/kb-seed.service.ts +0 -555
- package/src/shared/__tests__/kb-layout.group.test.ts +0 -45
- /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
|
-
|
|
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:
|
|
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: ['
|
|
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:
|
|
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
|
-
'
|
|
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: '
|
|
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
|
|
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: '
|
|
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
|
-
'
|
|
160
|
-
'
|
|
161
|
-
'
|
|
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:
|
|
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: ['
|
|
225
|
+
touchedNodePaths: ['Plugins/Engineering/older/SKILL.md'],
|
|
226
226
|
}),
|
|
227
227
|
],
|
|
228
228
|
branchFiles: {
|
|
229
|
-
'agent/weekly-newsletter:
|
|
230
|
-
'agent/older:
|
|
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
|
-
'
|
|
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, '
|
|
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 (
|
|
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('
|
|
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('
|
|
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('
|
|
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, '
|
|
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(['
|
|
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('
|
|
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, '
|
|
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('
|
|
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('
|
|
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(['
|
|
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('
|
|
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
|
-
|
|
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
|
-
*
|
|
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
|
|
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
|
|
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
|
-
* `
|
|
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. `
|
|
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] ===
|
|
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 `
|
|
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. `
|
|
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. `
|
|
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
|
|
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,
|
|
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
|
|
107
|
-
// failure (no workspace, no
|
|
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. `
|
|
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,
|
|
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
|
+
};
|