@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.
- package/LICENSE +202 -202
- package/THIRD-PARTY-NOTICES.md +428 -454
- package/dist/core/core-ports.d.ts +1 -1
- package/dist/core/create-core-server.d.ts.map +1 -1
- package/dist/core/create-core-server.js +51 -7
- package/dist/core/create-core-server.js.map +1 -1
- package/dist/core/create-core-services.d.ts +5 -3
- package/dist/core/create-core-services.d.ts.map +1 -1
- package/dist/core/create-core-services.js +38 -21
- 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.js +19 -19
- 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/roles-admin.service.d.ts +18 -4
- package/dist/modules/access/roles-admin.service.d.ts.map +1 -1
- package/dist/modules/access/roles-admin.service.js +14 -4
- 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/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/workflow/pending-commits.service.d.ts +11 -0
- package/dist/modules/workflow/pending-commits.service.d.ts.map +1 -1
- package/dist/modules/workflow/pending-commits.service.js +37 -9
- package/dist/modules/workflow/pending-commits.service.js.map +1 -1
- package/dist/modules/workflow/pending-commits.worker.d.ts +17 -4
- package/dist/modules/workflow/pending-commits.worker.d.ts.map +1 -1
- package/dist/modules/workflow/pending-commits.worker.js +63 -9
- package/dist/modules/workflow/pending-commits.worker.js.map +1 -1
- package/dist/modules/workflow/workflow.service.d.ts +1 -0
- package/dist/modules/workflow/workflow.service.d.ts.map +1 -1
- package/dist/modules/workflow/workflow.service.js +6 -1
- package/dist/modules/workflow/workflow.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/workspace.routes.js +3 -3
- package/dist/modules/workspace/workspace.routes.js.map +1 -1
- package/dist/modules/workspace/workspace.service.d.ts +26 -0
- package/dist/modules/workspace/workspace.service.d.ts.map +1 -1
- package/dist/modules/workspace/workspace.service.js +83 -12
- 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/migrations/meta/0000_snapshot.json +1479 -1479
- package/package.json +5 -4
- package/src/assets.ts +25 -25
- package/src/core/core-ports.ts +106 -106
- package/src/core/create-core-server.ts +55 -9
- package/src/core/create-core-services.ts +48 -24
- 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 +13 -1
- 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 +20 -20
- package/src/modules/access/creator-access.ts +5 -5
- package/src/modules/access/roles-admin.service.ts +13 -2
- 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/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__/pending-commits.worker.test.ts +118 -0
- 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/pending-commits.service.ts +48 -12
- package/src/modules/workflow/pending-commits.worker.ts +64 -8
- 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/workflow/workflow.service.ts +13 -2
- package/src/modules/workspace/__tests__/kb-seed.service.test.ts +81 -13
- package/src/modules/workspace/__tests__/plugins-migration.test.ts +427 -0
- 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 +116 -0
- package/src/modules/workspace/bevel-ignore.ts +66 -66
- package/src/modules/workspace/kb-seed.service.ts +38 -9
- package/src/modules/workspace/plugins-migration.ts +479 -0
- package/src/modules/workspace/session-sink.ts +25 -25
- package/src/modules/workspace/workspace.routes.ts +3 -3
- package/src/modules/workspace/workspace.service.ts +85 -14
- 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/shared/__tests__/kb-layout.group.test.ts +0 -45
- /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,
|
|
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
|
|
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,
|
|
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
|
-
|
|
288
|
-
|
|
289
|
-
|
|
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 `
|
|
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 `
|
|
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 `
|
|
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);
|