@bevel-software/platform-core-backend 0.13.6 → 0.15.1
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/THIRD-PARTY-NOTICES.md +6 -6
- package/dist/core/catalog-cache-invalidation.d.ts +29 -0
- package/dist/core/catalog-cache-invalidation.d.ts.map +1 -0
- package/dist/core/catalog-cache-invalidation.js +71 -0
- package/dist/core/catalog-cache-invalidation.js.map +1 -0
- package/dist/core/create-core-server.d.ts.map +1 -1
- package/dist/core/create-core-server.js +101 -9
- package/dist/core/create-core-server.js.map +1 -1
- package/dist/core/create-core-services.d.ts +20 -1
- package/dist/core/create-core-services.d.ts.map +1 -1
- package/dist/core/create-core-services.js +105 -43
- package/dist/core/create-core-services.js.map +1 -1
- package/dist/core-config.d.ts.map +1 -1
- package/dist/core-config.js +12 -5
- package/dist/core-config.js.map +1 -1
- package/dist/modules/access/access-control.interface.d.ts +18 -6
- package/dist/modules/access/access-control.interface.d.ts.map +1 -1
- package/dist/modules/access/access-control.service.d.ts +26 -1
- package/dist/modules/access/access-control.service.d.ts.map +1 -1
- package/dist/modules/access/access-control.service.js +213 -71
- package/dist/modules/access/access-control.service.js.map +1 -1
- package/dist/modules/access/access.routes.d.ts.map +1 -1
- package/dist/modules/access/access.routes.js +59 -7
- package/dist/modules/access/access.routes.js.map +1 -1
- package/dist/modules/access-model/access-grammar.d.ts +51 -1
- package/dist/modules/access-model/access-grammar.d.ts.map +1 -1
- package/dist/modules/access-model/access-grammar.js +95 -1
- package/dist/modules/access-model/access-grammar.js.map +1 -1
- package/dist/modules/access-model/group-files.d.ts.map +1 -1
- package/dist/modules/access-model/group-files.js +4 -1
- package/dist/modules/access-model/group-files.js.map +1 -1
- package/dist/modules/access-model/kb-read-filter.d.ts +12 -0
- package/dist/modules/access-model/kb-read-filter.d.ts.map +1 -1
- package/dist/modules/access-model/kb-read-filter.js +15 -0
- package/dist/modules/access-model/kb-read-filter.js.map +1 -1
- package/dist/modules/access-model/plugin-principals.d.ts +49 -0
- package/dist/modules/access-model/plugin-principals.d.ts.map +1 -0
- package/dist/modules/access-model/plugin-principals.js +125 -0
- package/dist/modules/access-model/plugin-principals.js.map +1 -0
- package/dist/modules/connection-probe/connection-probe.contract.d.ts +43 -0
- package/dist/modules/connection-probe/connection-probe.contract.d.ts.map +1 -0
- package/dist/modules/connection-probe/connection-probe.contract.js +2 -0
- package/dist/modules/connection-probe/connection-probe.contract.js.map +1 -0
- package/dist/modules/connection-probe/connection-probe.service.d.ts +94 -0
- package/dist/modules/connection-probe/connection-probe.service.d.ts.map +1 -0
- package/dist/modules/connection-probe/connection-probe.service.js +684 -0
- package/dist/modules/connection-probe/connection-probe.service.js.map +1 -0
- package/dist/modules/connection-probe/index.d.ts +3 -0
- package/dist/modules/connection-probe/index.d.ts.map +1 -0
- package/dist/modules/connection-probe/index.js +3 -0
- package/dist/modules/connection-probe/index.js.map +1 -0
- package/dist/modules/database/core-schema.d.ts +326 -0
- package/dist/modules/database/core-schema.d.ts.map +1 -1
- package/dist/modules/database/core-schema.js +52 -0
- package/dist/modules/database/core-schema.js.map +1 -1
- package/dist/modules/diff/diff.routes.d.ts.map +1 -1
- package/dist/modules/diff/diff.routes.js +3 -5
- package/dist/modules/diff/diff.routes.js.map +1 -1
- package/dist/modules/kb-fs/mutex.d.ts +37 -0
- package/dist/modules/kb-fs/mutex.d.ts.map +1 -1
- package/dist/modules/kb-fs/mutex.js +48 -5
- package/dist/modules/kb-fs/mutex.js.map +1 -1
- package/dist/modules/kb-sync/index.d.ts +6 -0
- package/dist/modules/kb-sync/index.d.ts.map +1 -0
- package/dist/modules/kb-sync/index.js +5 -0
- package/dist/modules/kb-sync/index.js.map +1 -0
- package/dist/modules/kb-sync/kb-sync.interface.d.ts +77 -0
- package/dist/modules/kb-sync/kb-sync.interface.d.ts.map +1 -0
- package/dist/modules/kb-sync/kb-sync.interface.js +2 -0
- package/dist/modules/kb-sync/kb-sync.interface.js.map +1 -0
- package/dist/modules/kb-sync/kb-sync.routes.d.ts +70 -0
- package/dist/modules/kb-sync/kb-sync.routes.d.ts.map +1 -0
- package/dist/modules/kb-sync/kb-sync.routes.js +135 -0
- package/dist/modules/kb-sync/kb-sync.routes.js.map +1 -0
- package/dist/modules/kb-sync/kb-sync.service.d.ts +45 -0
- package/dist/modules/kb-sync/kb-sync.service.d.ts.map +1 -0
- package/dist/modules/kb-sync/kb-sync.service.js +183 -0
- package/dist/modules/kb-sync/kb-sync.service.js.map +1 -0
- package/dist/modules/kb-sync/sync-auth.d.ts +59 -0
- package/dist/modules/kb-sync/sync-auth.d.ts.map +1 -0
- package/dist/modules/kb-sync/sync-auth.js +62 -0
- package/dist/modules/kb-sync/sync-auth.js.map +1 -0
- package/dist/modules/kb-sync/sync-payload.d.ts +32 -0
- package/dist/modules/kb-sync/sync-payload.d.ts.map +1 -0
- package/dist/modules/kb-sync/sync-payload.js +105 -0
- package/dist/modules/kb-sync/sync-payload.js.map +1 -0
- package/dist/modules/marketplace/git-http.routes.d.ts +42 -0
- package/dist/modules/marketplace/git-http.routes.d.ts.map +1 -0
- package/dist/modules/marketplace/git-http.routes.js +190 -0
- package/dist/modules/marketplace/git-http.routes.js.map +1 -0
- package/dist/modules/marketplace/github-facade/github-facade-admin.routes.d.ts +23 -0
- package/dist/modules/marketplace/github-facade/github-facade-admin.routes.d.ts.map +1 -0
- package/dist/modules/marketplace/github-facade/github-facade-admin.routes.js +59 -0
- package/dist/modules/marketplace/github-facade/github-facade-admin.routes.js.map +1 -0
- package/dist/modules/marketplace/github-facade/github-facade-codes.store.d.ts +59 -0
- package/dist/modules/marketplace/github-facade/github-facade-codes.store.d.ts.map +1 -0
- package/dist/modules/marketplace/github-facade/github-facade-codes.store.js +96 -0
- package/dist/modules/marketplace/github-facade/github-facade-codes.store.js.map +1 -0
- package/dist/modules/marketplace/github-facade/github-facade-credentials.service.d.ts +99 -0
- package/dist/modules/marketplace/github-facade/github-facade-credentials.service.d.ts.map +1 -0
- package/dist/modules/marketplace/github-facade/github-facade-credentials.service.js +147 -0
- package/dist/modules/marketplace/github-facade/github-facade-credentials.service.js.map +1 -0
- package/dist/modules/marketplace/github-facade/github-facade.routes.d.ts +47 -0
- package/dist/modules/marketplace/github-facade/github-facade.routes.d.ts.map +1 -0
- package/dist/modules/marketplace/github-facade/github-facade.routes.js +228 -0
- package/dist/modules/marketplace/github-facade/github-facade.routes.js.map +1 -0
- package/dist/modules/marketplace/github-facade/github-facade.service.d.ts +109 -0
- package/dist/modules/marketplace/github-facade/github-facade.service.d.ts.map +1 -0
- package/dist/modules/marketplace/github-facade/github-facade.service.js +172 -0
- package/dist/modules/marketplace/github-facade/github-facade.service.js.map +1 -0
- package/dist/modules/marketplace/github-facade/index.d.ts +6 -0
- package/dist/modules/marketplace/github-facade/index.d.ts.map +1 -0
- package/dist/modules/marketplace/github-facade/index.js +6 -0
- package/dist/modules/marketplace/github-facade/index.js.map +1 -0
- package/dist/modules/marketplace/index.d.ts +4 -0
- package/dist/modules/marketplace/index.d.ts.map +1 -0
- package/dist/modules/marketplace/index.js +4 -0
- package/dist/modules/marketplace/index.js.map +1 -0
- package/dist/modules/marketplace/marketplace-repo.service.d.ts +122 -0
- package/dist/modules/marketplace/marketplace-repo.service.d.ts.map +1 -0
- package/dist/modules/marketplace/marketplace-repo.service.js +300 -0
- package/dist/modules/marketplace/marketplace-repo.service.js.map +1 -0
- package/dist/modules/mcp/oauth/bevel-oauth-provider.d.ts.map +1 -1
- package/dist/modules/mcp/oauth/bevel-oauth-provider.js +4 -0
- package/dist/modules/mcp/oauth/bevel-oauth-provider.js.map +1 -1
- package/dist/modules/mcp/oauth/oauth-consent.routes.d.ts +14 -8
- package/dist/modules/mcp/oauth/oauth-consent.routes.d.ts.map +1 -1
- package/dist/modules/mcp/oauth/oauth-consent.routes.js +13 -1
- package/dist/modules/mcp/oauth/oauth-consent.routes.js.map +1 -1
- package/dist/modules/mcp/oauth/oauth-state.d.ts +13 -2
- package/dist/modules/mcp/oauth/oauth-state.d.ts.map +1 -1
- package/dist/modules/mcp/oauth/oauth-state.js +3 -1
- package/dist/modules/mcp/oauth/oauth-state.js.map +1 -1
- package/dist/modules/plugins/compile/compile-marketplace.d.ts +97 -0
- package/dist/modules/plugins/compile/compile-marketplace.d.ts.map +1 -0
- package/dist/modules/plugins/compile/compile-marketplace.js +240 -0
- package/dist/modules/plugins/compile/compile-marketplace.js.map +1 -0
- package/dist/modules/plugins/compile/marketplace-compiler.service.d.ts +66 -0
- package/dist/modules/plugins/compile/marketplace-compiler.service.d.ts.map +1 -0
- package/dist/modules/plugins/compile/marketplace-compiler.service.js +132 -0
- package/dist/modules/plugins/compile/marketplace-compiler.service.js.map +1 -0
- package/dist/modules/plugins/discovery/bundle-dialect/bundle.source.d.ts +37 -0
- package/dist/modules/plugins/discovery/bundle-dialect/bundle.source.d.ts.map +1 -0
- package/dist/modules/plugins/discovery/bundle-dialect/bundle.source.js +135 -0
- package/dist/modules/plugins/discovery/bundle-dialect/bundle.source.js.map +1 -0
- package/dist/modules/plugins/discovery/bundle-dialect/registry.d.ts +45 -0
- package/dist/modules/plugins/discovery/bundle-dialect/registry.d.ts.map +1 -0
- package/dist/modules/plugins/discovery/bundle-dialect/registry.js +147 -0
- package/dist/modules/plugins/discovery/bundle-dialect/registry.js.map +1 -0
- package/dist/modules/plugins/discovery/kb-plugin-source.d.ts +37 -0
- package/dist/modules/plugins/discovery/kb-plugin-source.d.ts.map +1 -0
- package/dist/modules/plugins/discovery/kb-plugin-source.js +107 -0
- package/dist/modules/plugins/discovery/kb-plugin-source.js.map +1 -0
- package/dist/modules/plugins/discovery/native.source.d.ts +15 -0
- package/dist/modules/plugins/discovery/native.source.d.ts.map +1 -0
- package/dist/modules/plugins/discovery/native.source.js +102 -0
- package/dist/modules/plugins/discovery/native.source.js.map +1 -0
- package/dist/modules/plugins/discovery/plugin-source.d.ts +96 -0
- package/dist/modules/plugins/discovery/plugin-source.d.ts.map +1 -0
- package/dist/modules/plugins/discovery/plugin-source.js +2 -0
- package/dist/modules/plugins/discovery/plugin-source.js.map +1 -0
- package/dist/modules/plugins/index.d.ts +10 -1
- package/dist/modules/plugins/index.d.ts.map +1 -1
- package/dist/modules/plugins/index.js +8 -0
- package/dist/modules/plugins/index.js.map +1 -1
- package/dist/modules/plugins/plugin-links.d.ts +66 -0
- package/dist/modules/plugins/plugin-links.d.ts.map +1 -0
- package/dist/modules/plugins/plugin-links.js +160 -0
- package/dist/modules/plugins/plugin-links.js.map +1 -0
- package/dist/modules/plugins/plugin-links.service.d.ts +114 -0
- package/dist/modules/plugins/plugin-links.service.d.ts.map +1 -0
- package/dist/modules/plugins/plugin-links.service.js +304 -0
- package/dist/modules/plugins/plugin-links.service.js.map +1 -0
- package/dist/modules/plugins/plugin-provision.service.d.ts +39 -3
- package/dist/modules/plugins/plugin-provision.service.d.ts.map +1 -1
- package/dist/modules/plugins/plugin-provision.service.js +152 -52
- package/dist/modules/plugins/plugin-provision.service.js.map +1 -1
- package/dist/modules/plugins/plugin-rename.service.d.ts +69 -0
- package/dist/modules/plugins/plugin-rename.service.d.ts.map +1 -0
- package/dist/modules/plugins/plugin-rename.service.js +245 -0
- package/dist/modules/plugins/plugin-rename.service.js.map +1 -0
- package/dist/modules/plugins/plugins.contract.d.ts +27 -1
- package/dist/modules/plugins/plugins.contract.d.ts.map +1 -1
- package/dist/modules/plugins/plugins.routes.d.ts +7 -1
- package/dist/modules/plugins/plugins.routes.d.ts.map +1 -1
- package/dist/modules/plugins/plugins.routes.js +120 -12
- package/dist/modules/plugins/plugins.routes.js.map +1 -1
- package/dist/modules/plugins/plugins.service.d.ts +50 -3
- package/dist/modules/plugins/plugins.service.d.ts.map +1 -1
- package/dist/modules/plugins/plugins.service.js +108 -47
- package/dist/modules/plugins/plugins.service.js.map +1 -1
- package/dist/modules/secrets-vault/db-secrets-vault.service.js +1 -1
- package/dist/modules/secrets-vault/db-secrets-vault.service.js.map +1 -1
- package/dist/modules/secrets-vault/secrets-vault.routes.d.ts +6 -0
- package/dist/modules/secrets-vault/secrets-vault.routes.d.ts.map +1 -1
- package/dist/modules/secrets-vault/secrets-vault.routes.js +31 -1
- package/dist/modules/secrets-vault/secrets-vault.routes.js.map +1 -1
- package/dist/modules/settings/deployment-settings.service.d.ts +9 -0
- package/dist/modules/settings/deployment-settings.service.d.ts.map +1 -1
- package/dist/modules/settings/deployment-settings.service.js +82 -2
- package/dist/modules/settings/deployment-settings.service.js.map +1 -1
- package/dist/modules/settings/setup.routes.d.ts +26 -0
- package/dist/modules/settings/setup.routes.d.ts.map +1 -1
- package/dist/modules/settings/setup.routes.js +9 -1
- package/dist/modules/settings/setup.routes.js.map +1 -1
- package/dist/modules/skills/index.d.ts +1 -0
- package/dist/modules/skills/index.d.ts.map +1 -1
- package/dist/modules/skills/index.js +1 -0
- package/dist/modules/skills/index.js.map +1 -1
- package/dist/modules/skills/pending-skills.service.d.ts.map +1 -1
- package/dist/modules/skills/pending-skills.service.js +9 -4
- package/dist/modules/skills/pending-skills.service.js.map +1 -1
- package/dist/modules/skills/skill-access-requests.routes.d.ts +38 -0
- package/dist/modules/skills/skill-access-requests.routes.d.ts.map +1 -0
- package/dist/modules/skills/skill-access-requests.routes.js +168 -0
- package/dist/modules/skills/skill-access-requests.routes.js.map +1 -0
- package/dist/modules/skills/skills.contract.d.ts +31 -0
- package/dist/modules/skills/skills.contract.d.ts.map +1 -1
- package/dist/modules/skills/skills.routes.d.ts +28 -3
- package/dist/modules/skills/skills.routes.d.ts.map +1 -1
- package/dist/modules/skills/skills.routes.js +42 -3
- package/dist/modules/skills/skills.routes.js.map +1 -1
- package/dist/modules/skills/skills.service.d.ts +4 -0
- package/dist/modules/skills/skills.service.d.ts.map +1 -1
- package/dist/modules/skills/skills.service.js +83 -22
- package/dist/modules/skills/skills.service.js.map +1 -1
- package/dist/modules/tool-auth/external-api-key.interface.d.ts +18 -1
- package/dist/modules/tool-auth/external-api-key.interface.d.ts.map +1 -1
- package/dist/modules/tool-auth/external-api-key.interface.js +2 -1
- package/dist/modules/tool-auth/external-api-key.interface.js.map +1 -1
- package/dist/modules/tool-auth/external-api-key.service.d.ts +13 -4
- package/dist/modules/tool-auth/external-api-key.service.d.ts.map +1 -1
- package/dist/modules/tool-auth/external-api-key.service.js +26 -9
- package/dist/modules/tool-auth/external-api-key.service.js.map +1 -1
- package/dist/modules/tool-manuals/mcp-json-discovery.d.ts +36 -0
- package/dist/modules/tool-manuals/mcp-json-discovery.d.ts.map +1 -1
- package/dist/modules/tool-manuals/mcp-json-discovery.js +130 -83
- package/dist/modules/tool-manuals/mcp-json-discovery.js.map +1 -1
- package/dist/modules/tool-manuals/mcp-server-edit.service.d.ts.map +1 -1
- package/dist/modules/tool-manuals/mcp-server-edit.service.js +10 -4
- package/dist/modules/tool-manuals/mcp-server-edit.service.js.map +1 -1
- package/dist/modules/tool-manuals/tool-manuals.contract.d.ts +83 -0
- package/dist/modules/tool-manuals/tool-manuals.contract.d.ts.map +1 -1
- package/dist/modules/tool-manuals/tool-manuals.service.d.ts +15 -2
- package/dist/modules/tool-manuals/tool-manuals.service.d.ts.map +1 -1
- package/dist/modules/tool-manuals/tool-manuals.service.js +199 -38
- package/dist/modules/tool-manuals/tool-manuals.service.js.map +1 -1
- package/dist/modules/workflow/git/git.service.d.ts +58 -3
- package/dist/modules/workflow/git/git.service.d.ts.map +1 -1
- package/dist/modules/workflow/git/git.service.js +325 -60
- package/dist/modules/workflow/git/git.service.js.map +1 -1
- package/dist/modules/workflow/git/pull-request.service.d.ts +6 -0
- package/dist/modules/workflow/git/pull-request.service.d.ts.map +1 -1
- package/dist/modules/workflow/git/pull-request.service.js +8 -0
- package/dist/modules/workflow/git/pull-request.service.js.map +1 -1
- package/dist/modules/workflow/workflow.routes.d.ts +2 -1
- package/dist/modules/workflow/workflow.routes.d.ts.map +1 -1
- package/dist/modules/workflow/workflow.routes.js +98 -9
- package/dist/modules/workflow/workflow.routes.js.map +1 -1
- package/dist/modules/workflow/workflow.service.d.ts +133 -9
- package/dist/modules/workflow/workflow.service.d.ts.map +1 -1
- package/dist/modules/workflow/workflow.service.js +462 -52
- package/dist/modules/workflow/workflow.service.js.map +1 -1
- package/dist/modules/workspace/startup/steps/plugin-manifests.step.d.ts +35 -0
- package/dist/modules/workspace/startup/steps/plugin-manifests.step.d.ts.map +1 -0
- package/dist/modules/workspace/startup/steps/plugin-manifests.step.js +121 -0
- package/dist/modules/workspace/startup/steps/plugin-manifests.step.js.map +1 -0
- package/dist/modules/workspace/startup/steps/seed-tree.d.ts.map +1 -1
- package/dist/modules/workspace/startup/steps/seed-tree.js +46 -2
- package/dist/modules/workspace/startup/steps/seed-tree.js.map +1 -1
- package/dist/modules/workspace/startup/steps/template-files.step.d.ts.map +1 -1
- package/dist/modules/workspace/startup/steps/template-files.step.js +112 -25
- package/dist/modules/workspace/startup/steps/template-files.step.js.map +1 -1
- package/dist/modules/workspace/workspace.routes.d.ts.map +1 -1
- package/dist/modules/workspace/workspace.routes.js +11 -14
- package/dist/modules/workspace/workspace.routes.js.map +1 -1
- package/dist/modules/workspace/workspace.service.d.ts +38 -1
- package/dist/modules/workspace/workspace.service.d.ts.map +1 -1
- package/dist/modules/workspace/workspace.service.js +131 -20
- package/dist/modules/workspace/workspace.service.js.map +1 -1
- package/dist/shared/domain-errors.d.ts +20 -0
- package/dist/shared/domain-errors.d.ts.map +1 -1
- package/dist/shared/domain-errors.js +29 -0
- package/dist/shared/domain-errors.js.map +1 -1
- package/dist/shared/fs-errors.d.ts +8 -0
- package/dist/shared/fs-errors.d.ts.map +1 -0
- package/dist/shared/fs-errors.js +11 -0
- package/dist/shared/fs-errors.js.map +1 -0
- package/dist/shared/fs-walk.d.ts +20 -6
- package/dist/shared/fs-walk.d.ts.map +1 -1
- package/dist/shared/fs-walk.js +35 -27
- package/dist/shared/fs-walk.js.map +1 -1
- package/dist/shared/kb-walk.d.ts +52 -0
- package/dist/shared/kb-walk.d.ts.map +1 -0
- package/dist/shared/kb-walk.js +76 -0
- package/dist/shared/kb-walk.js.map +1 -0
- package/dist/shared/path-order.d.ts +18 -0
- package/dist/shared/path-order.d.ts.map +1 -0
- package/dist/shared/path-order.js +35 -0
- package/dist/shared/path-order.js.map +1 -0
- package/dist/shared/token-crypto.d.ts +17 -2
- package/dist/shared/token-crypto.d.ts.map +1 -1
- package/dist/shared/token-crypto.js +25 -8
- package/dist/shared/token-crypto.js.map +1 -1
- package/dist/shared/ttl-cache.d.ts +28 -1
- package/dist/shared/ttl-cache.d.ts.map +1 -1
- package/dist/shared/ttl-cache.js +33 -1
- package/dist/shared/ttl-cache.js.map +1 -1
- package/kb-template/.bevelignore +1 -1
- package/kb-template/AGENTS.md +95 -30
- package/kb-template/Skills/.gitkeep +0 -0
- package/migrations/0005_github_facade.sql +25 -0
- package/migrations/meta/0005_snapshot.json +1717 -0
- package/migrations/meta/_journal.json +7 -0
- package/package.json +6 -6
- package/src/__tests__/core-config.admin.test.ts +21 -0
- package/src/__tests__/kb-layout-config.test.ts +110 -0
- package/src/core/__tests__/catalog-cache-invalidation.test.ts +146 -0
- package/src/core/catalog-cache-invalidation.ts +92 -0
- package/src/core/create-core-server.ts +126 -7
- package/src/core/create-core-services.ts +173 -41
- package/src/core-config.ts +14 -7
- package/src/modules/access/__tests__/access-control.invalidate-race.test.ts +86 -0
- package/src/modules/access/__tests__/access-control.service.test.ts +19 -3
- package/src/modules/access/__tests__/access-plugin-principals.test.ts +368 -0
- package/src/modules/access/__tests__/groups-admin.service.test.ts +8 -0
- package/src/modules/access/access-control.interface.ts +20 -9
- package/src/modules/access/access-control.service.ts +240 -78
- package/src/modules/access/access.routes.ts +96 -14
- package/src/modules/access-model/__tests__/access-grammar.test.ts +18 -0
- package/src/modules/access-model/__tests__/plugin-token-canonical.test.ts +40 -0
- package/src/modules/access-model/access-grammar.ts +114 -2
- package/src/modules/access-model/group-files.ts +4 -0
- package/src/modules/access-model/kb-read-filter.ts +22 -0
- package/src/modules/access-model/plugin-principals.ts +137 -0
- package/src/modules/connection-probe/__tests__/connection-probe.service.test.ts +685 -0
- package/src/modules/connection-probe/connection-probe.contract.ts +44 -0
- package/src/modules/connection-probe/connection-probe.service.ts +734 -0
- package/src/modules/connection-probe/index.ts +2 -0
- package/src/modules/database/core-schema.ts +54 -0
- package/src/modules/diff/diff.routes.ts +9 -5
- package/src/modules/kb-fs/__tests__/mutex.test.ts +107 -0
- package/src/modules/kb-fs/mutex.ts +50 -5
- package/src/modules/kb-sync/__tests__/kb-sync.routes.test.ts +297 -0
- package/src/modules/kb-sync/__tests__/kb-sync.service.test.ts +295 -0
- package/src/modules/kb-sync/__tests__/sync-auth.test.ts +118 -0
- package/src/modules/kb-sync/__tests__/sync-payload.test.ts +148 -0
- package/src/modules/kb-sync/index.ts +19 -0
- package/src/modules/kb-sync/kb-sync.interface.ts +79 -0
- package/src/modules/kb-sync/kb-sync.routes.ts +197 -0
- package/src/modules/kb-sync/kb-sync.service.ts +201 -0
- package/src/modules/kb-sync/sync-auth.ts +118 -0
- package/src/modules/kb-sync/sync-payload.ts +143 -0
- package/src/modules/marketplace/__tests__/github-facade.test.ts +443 -0
- package/src/modules/marketplace/__tests__/marketplace-git.test.ts +179 -0
- package/src/modules/marketplace/git-http.routes.ts +193 -0
- package/src/modules/marketplace/github-facade/github-facade-admin.routes.ts +78 -0
- package/src/modules/marketplace/github-facade/github-facade-codes.store.ts +145 -0
- package/src/modules/marketplace/github-facade/github-facade-credentials.service.ts +208 -0
- package/src/modules/marketplace/github-facade/github-facade.routes.ts +268 -0
- package/src/modules/marketplace/github-facade/github-facade.service.ts +221 -0
- package/src/modules/marketplace/github-facade/index.ts +25 -0
- package/src/modules/marketplace/index.ts +3 -0
- package/src/modules/marketplace/marketplace-repo.service.ts +337 -0
- package/src/modules/mcp/oauth/bevel-oauth-provider.ts +3 -0
- package/src/modules/mcp/oauth/oauth-consent.routes.ts +24 -2
- package/src/modules/mcp/oauth/oauth-state.ts +15 -3
- package/src/modules/plugins/__tests__/bundle-dialect.e2e.test.ts +163 -0
- package/src/modules/plugins/__tests__/compile-marketplace.test.ts +294 -0
- package/src/modules/plugins/__tests__/plugin-index.service.test.ts +56 -12
- package/src/modules/plugins/__tests__/plugin-links.service.test.ts +287 -0
- package/src/modules/plugins/__tests__/plugin-provision.service.test.ts +205 -5
- package/src/modules/plugins/__tests__/plugin-rename.service.test.ts +394 -0
- package/src/modules/plugins/__tests__/plugins.routes.test.ts +80 -33
- package/src/modules/plugins/compile/compile-marketplace.ts +403 -0
- package/src/modules/plugins/compile/marketplace-compiler.service.ts +150 -0
- package/src/modules/plugins/discovery/__tests__/kb-plugin-source.test.ts +329 -0
- package/src/modules/plugins/discovery/bundle-dialect/bundle.source.ts +140 -0
- package/src/modules/plugins/discovery/bundle-dialect/registry.ts +175 -0
- package/src/modules/plugins/discovery/kb-plugin-source.ts +114 -0
- package/src/modules/plugins/discovery/native.source.ts +122 -0
- package/src/modules/plugins/discovery/plugin-source.ts +99 -0
- package/src/modules/plugins/index.ts +10 -0
- package/src/modules/plugins/plugin-links.service.ts +363 -0
- package/src/modules/plugins/plugin-links.ts +184 -0
- package/src/modules/plugins/plugin-provision.service.ts +160 -54
- package/src/modules/plugins/plugin-rename.service.ts +298 -0
- package/src/modules/plugins/plugins.contract.ts +28 -1
- package/src/modules/plugins/plugins.routes.ts +128 -11
- package/src/modules/plugins/plugins.service.ts +106 -53
- package/src/modules/secrets-vault/__tests__/connect-pending.route.test.ts +12 -0
- package/src/modules/secrets-vault/__tests__/oauth-return-to.route.test.ts +10 -3
- package/src/modules/secrets-vault/__tests__/tool-owner-gate.route.test.ts +26 -0
- package/src/modules/secrets-vault/db-secrets-vault.service.ts +1 -1
- package/src/modules/secrets-vault/secrets-vault.routes.ts +35 -1
- package/src/modules/settings/__tests__/deployment-settings.service.test.ts +57 -0
- package/src/modules/settings/__tests__/setup.routes.sync-status.test.ts +77 -0
- package/src/modules/settings/deployment-settings.service.ts +95 -2
- package/src/modules/settings/setup.routes.ts +24 -0
- package/src/modules/skills/__tests__/skill-access-requests.routes.test.ts +128 -0
- package/src/modules/skills/__tests__/skills.service.test.ts +85 -0
- package/src/modules/skills/index.ts +1 -0
- package/src/modules/skills/pending-skills.service.ts +7 -5
- package/src/modules/skills/skill-access-requests.routes.ts +199 -0
- package/src/modules/skills/skills.contract.ts +32 -0
- package/src/modules/skills/skills.routes.ts +59 -3
- package/src/modules/skills/skills.service.ts +84 -22
- package/src/modules/tool-auth/external-api-key.interface.ts +20 -1
- package/src/modules/tool-auth/external-api-key.service.ts +31 -13
- package/src/modules/tool-manuals/__tests__/mcp-json-discovery.test.ts +35 -1
- package/src/modules/tool-manuals/__tests__/tool-manuals.cli.test.ts +2 -0
- package/src/modules/tool-manuals/__tests__/tool-manuals.health-check.test.ts +104 -0
- package/src/modules/tool-manuals/__tests__/tool-manuals.service.test.ts +56 -0
- package/src/modules/tool-manuals/mcp-json-discovery.ts +154 -76
- package/src/modules/tool-manuals/mcp-server-edit.service.ts +13 -4
- package/src/modules/tool-manuals/tool-manuals.contract.ts +89 -0
- package/src/modules/tool-manuals/tool-manuals.service.ts +210 -37
- package/src/modules/workflow/__tests__/preserve-roles-yaml.test.ts +1 -1
- package/src/modules/workflow/__tests__/workflow.routes.history-read-gate.test.ts +139 -0
- package/src/modules/workflow/__tests__/workflow.service.branch-in-use.test.ts +337 -0
- package/src/modules/workflow/__tests__/workflow.service.commitFileWhileLocked.test.ts +1 -0
- package/src/modules/workflow/__tests__/workflow.service.deleted-branch-sweep.test.ts +7 -1
- package/src/modules/workflow/__tests__/workflow.service.facade.test.ts +97 -10
- package/src/modules/workflow/__tests__/workflow.service.releaseLock.test.ts +119 -2
- package/src/modules/workflow/__tests__/workflow.service.sync.test.ts +437 -0
- package/src/modules/workflow/git/__tests__/git.service.history-guards.test.ts +86 -0
- package/src/modules/workflow/git/__tests__/git.service.postMergePull.test.ts +3 -3
- package/src/modules/workflow/git/__tests__/git.service.pull.test.ts +47 -4
- package/src/modules/workflow/git/__tests__/git.service.syncFromRemote.test.ts +253 -0
- package/src/modules/workflow/git/git.service.ts +354 -63
- package/src/modules/workflow/git/pull-request.service.ts +9 -0
- package/src/modules/workflow/workflow.routes.ts +104 -4
- package/src/modules/workflow/workflow.service.ts +505 -64
- package/src/modules/workspace/__tests__/workspace.routes.read-gate.test.ts +6 -7
- package/src/modules/workspace/__tests__/workspace.service.list-cloned.test.ts +93 -0
- package/src/modules/workspace/__tests__/workspace.service.missing-branch.test.ts +28 -0
- package/src/modules/workspace/__tests__/workspace.service.read-filter.test.ts +42 -12
- package/src/modules/workspace/startup/steps/__tests__/steps.test.ts +219 -4
- package/src/modules/workspace/startup/steps/plugin-manifests.step.ts +129 -0
- package/src/modules/workspace/startup/steps/seed-tree.ts +44 -2
- package/src/modules/workspace/startup/steps/template-files.step.ts +131 -27
- package/src/modules/workspace/workspace.routes.ts +17 -21
- package/src/modules/workspace/workspace.service.ts +128 -19
- package/src/shared/__tests__/filename.test.ts +0 -0
- package/src/shared/__tests__/fs-walk.test.ts +22 -1
- package/src/shared/__tests__/kb-walk.test.ts +107 -0
- package/src/shared/__tests__/path-order.test.ts +24 -0
- package/src/shared/__tests__/token-crypto.test.ts +34 -0
- package/src/shared/__tests__/ttl-cache.test.ts +55 -0
- package/src/shared/domain-errors.ts +29 -0
- package/src/shared/fs-errors.ts +10 -0
- package/src/shared/fs-walk.ts +38 -23
- package/src/shared/kb-walk.ts +95 -0
- package/src/shared/path-order.ts +33 -0
- package/src/shared/token-crypto.ts +28 -10
- package/src/shared/ttl-cache.ts +33 -1
|
@@ -25,6 +25,7 @@ import type {
|
|
|
25
25
|
AcquireLockResult,
|
|
26
26
|
AuthUser,
|
|
27
27
|
Branch,
|
|
28
|
+
BranchSyncOutcome,
|
|
28
29
|
BranchWorkspaceStatus,
|
|
29
30
|
CancelChangeRequestResult,
|
|
30
31
|
Change,
|
|
@@ -40,9 +41,10 @@ import type {
|
|
|
40
41
|
MergeChangeRequestOutcome,
|
|
41
42
|
OpenChangeRequestInput,
|
|
42
43
|
PostChangeRequestCommentInput,
|
|
44
|
+
RemoteSyncPullResult,
|
|
43
45
|
} from '@bevel-software/platform-shared';
|
|
44
46
|
import { isProtectedBranch, DEFAULT_BRANCH } from '@bevel-software/platform-shared';
|
|
45
|
-
import { and, eq } from 'drizzle-orm';
|
|
47
|
+
import { and, eq, or } from 'drizzle-orm';
|
|
46
48
|
import type { Database } from '../database/connection.js';
|
|
47
49
|
import { changeRequests } from '../database/schema.js';
|
|
48
50
|
import type { GitService } from './git/git.service.js';
|
|
@@ -65,6 +67,7 @@ import {
|
|
|
65
67
|
RolesYamlPreservationError,
|
|
66
68
|
PullRebaseConflictError,
|
|
67
69
|
PushNeedsAgentResolutionError,
|
|
70
|
+
RemoteBranchGoneError,
|
|
68
71
|
WorkflowDomainError,
|
|
69
72
|
WorkflowValidationError,
|
|
70
73
|
} from '../../shared/domain-errors.js';
|
|
@@ -100,6 +103,33 @@ function isOpenPairViolation(err: unknown): boolean {
|
|
|
100
103
|
return typeof message === 'string' && message.includes(OPEN_PAIR_CONSTRAINT);
|
|
101
104
|
}
|
|
102
105
|
|
|
106
|
+
/**
|
|
107
|
+
* The one sentence a remote-sync conflict is reported with — in the sync
|
|
108
|
+
* response, on the branch's banner and in the log — so whoever sees any of
|
|
109
|
+
* the three reads the same thing: which branch, which files, that automatic
|
|
110
|
+
* recovery is under way, and what to do if it does not clear.
|
|
111
|
+
*/
|
|
112
|
+
export function syncConflictMessage(branch: string, conflictedPaths: string[]): string {
|
|
113
|
+
// Bounded: the same sentence goes to the response, the banner and the log,
|
|
114
|
+
// and it must be one sentence in all three whatever the conflict's size.
|
|
115
|
+
// The full path list rides beside it as `conflictedPaths`.
|
|
116
|
+
const shown = conflictedPaths.slice(0, SYNC_CONFLICT_FILES_NAMED);
|
|
117
|
+
const more = conflictedPaths.length - shown.length;
|
|
118
|
+
const files =
|
|
119
|
+
conflictedPaths.length === 0
|
|
120
|
+
? 'some files'
|
|
121
|
+
: more > 0
|
|
122
|
+
? `${shown.join(', ')} and ${more} more file${more === 1 ? '' : 's'}`
|
|
123
|
+
: shown.join(', ');
|
|
124
|
+
return (
|
|
125
|
+
`${branch} is not in sync yet: ${files} changed both in Hexis and on the git host. ` +
|
|
126
|
+
`Recovery is queued; if this stays, open the files on ${branch} in Hexis, keep what you want, and save.`
|
|
127
|
+
);
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
/** How many conflicted files the sync-conflict sentence names before "and N more". */
|
|
131
|
+
const SYNC_CONFLICT_FILES_NAMED = 3;
|
|
132
|
+
|
|
103
133
|
/** Redact the shared GitHub token from any error string before surfacing it. */
|
|
104
134
|
function redactTokens(msg: string): string {
|
|
105
135
|
const tokens = [process.env.GITHUB_TOKEN, process.env.GH_TOKEN].filter(
|
|
@@ -246,7 +276,10 @@ export class WorkflowService implements IWorkflowService {
|
|
|
246
276
|
*/
|
|
247
277
|
private orphanSweepStarted = false;
|
|
248
278
|
|
|
249
|
-
async listBranches(
|
|
279
|
+
async listBranches(
|
|
280
|
+
workspaceId: string,
|
|
281
|
+
opts?: { freshFetch?: boolean; strictFetch?: boolean },
|
|
282
|
+
): Promise<Branch[]> {
|
|
250
283
|
const branches = await this.git.listBranches(workspaceId, opts);
|
|
251
284
|
if (!this.orphanSweepStarted) {
|
|
252
285
|
this.orphanSweepStarted = true;
|
|
@@ -268,7 +301,14 @@ export class WorkflowService implements IWorkflowService {
|
|
|
268
301
|
}
|
|
269
302
|
|
|
270
303
|
createBranch(workspaceId: string, name: string, fromBase?: string): Promise<Branch> {
|
|
271
|
-
|
|
304
|
+
// Under the branch's lifecycle lock, like `deleteBranch` and the
|
|
305
|
+
// retirement of a clone the host deleted: creating a branch is the one
|
|
306
|
+
// operation that can bring a "gone" branch back, and the retirement's
|
|
307
|
+
// "still gone on origin?" check is only a guarantee if nothing can
|
|
308
|
+
// recreate the branch between that check and the delete.
|
|
309
|
+
return this.branchLifecycle.run(`branch:${name}`, () =>
|
|
310
|
+
this.git.createBranch(workspaceId, name, fromBase),
|
|
311
|
+
);
|
|
272
312
|
}
|
|
273
313
|
|
|
274
314
|
/**
|
|
@@ -282,6 +322,85 @@ export class WorkflowService implements IWorkflowService {
|
|
|
282
322
|
*/
|
|
283
323
|
private readonly branchLifecycle = new WorkspaceMutex();
|
|
284
324
|
|
|
325
|
+
/**
|
|
326
|
+
* Hold the lifecycle lock of BOTH branches a change request names, so a
|
|
327
|
+
* check-then-insert spanning them excludes either one being deleted.
|
|
328
|
+
*
|
|
329
|
+
* ATOMICALLY, via `runAll` — not `run(a, () => run(b, …))`. Taking the keys
|
|
330
|
+
* one at a time leaves a gap between them in which a delete of the second
|
|
331
|
+
* branch can start, pass its "is any open request using this?" check
|
|
332
|
+
* (the row does not exist yet) and finish, so the request lands pointing at
|
|
333
|
+
* a branch that was removed while it was being opened. That is the same
|
|
334
|
+
* failure this PR is about, reintroduced one level down — and it would have
|
|
335
|
+
* been a REGRESSION on the source branch, which the previous single-key
|
|
336
|
+
* lock held for the whole sequence.
|
|
337
|
+
*
|
|
338
|
+
* Reserving the set in one step also removes the deadlock question that
|
|
339
|
+
* sequential acquisition raises: two requests may legally be open in
|
|
340
|
+
* opposite directions (`A -> B` and `B -> A`), and nothing is ever held
|
|
341
|
+
* while waiting for the other key.
|
|
342
|
+
*
|
|
343
|
+
* A PROTECTED target's key is NOT reserved. No path in the system can
|
|
344
|
+
* delete a protected branch (`git.deleteBranch` throws
|
|
345
|
+
* `ProtectedBranchError` before any lock, `retireMergedSourceBranch`
|
|
346
|
+
* returns early on one), so the key would exclude nothing. It would only
|
|
347
|
+
* cost: nearly every request targets the protected default branch, so one
|
|
348
|
+
* shared key there would serialize every open on the hottest path for the
|
|
349
|
+
* duration of a fetch + merge + push + diff. The source needs no such
|
|
350
|
+
* carve-out; `openChangeRequest` refuses protected sources before locking.
|
|
351
|
+
*/
|
|
352
|
+
private runOnBranchPair<T>(
|
|
353
|
+
source: string,
|
|
354
|
+
target: string,
|
|
355
|
+
fn: () => Promise<T>,
|
|
356
|
+
): Promise<T> {
|
|
357
|
+
const keys = [`branch:${source}`];
|
|
358
|
+
if (!isProtectedBranch(target)) keys.push(`branch:${target}`);
|
|
359
|
+
return this.branchLifecycle.runAll(keys, fn);
|
|
360
|
+
}
|
|
361
|
+
|
|
362
|
+
/**
|
|
363
|
+
* The open change requests that deleting `branch` would strand — every one
|
|
364
|
+
* that NAMES it, at EITHER end.
|
|
365
|
+
*
|
|
366
|
+
* Both ends, not just the source. A change request is a proposal to move
|
|
367
|
+
* commits FROM one branch INTO another and it needs both to exist: the
|
|
368
|
+
* detail read resolves the target BEFORE the source, so a request whose
|
|
369
|
+
* target has been deleted cannot be read, declined, or deleted either. It
|
|
370
|
+
* is also the worse half to lose — a stranded source is closed by the boot
|
|
371
|
+
* sweep, while a stranded target used to survive every restart, because
|
|
372
|
+
* every guard here asked only "is this branch anybody's SOURCE?".
|
|
373
|
+
*
|
|
374
|
+
* That question is what let a merge retire a branch a second, still-open
|
|
375
|
+
* request was pointing INTO: the pair `X -> B` (merged, B retired) and
|
|
376
|
+
* `Y -> X` (open, now unusable forever).
|
|
377
|
+
*
|
|
378
|
+
* Returns the first such request's number and which end of it names the
|
|
379
|
+
* branch, or null when the branch is free. The end matters to the caller's
|
|
380
|
+
* error message: "withdraw your request" is only actionable when the asker
|
|
381
|
+
* owns the request, and a request proposing INTO the branch is someone
|
|
382
|
+
* else's.
|
|
383
|
+
*/
|
|
384
|
+
private async openChangeRequestOn(
|
|
385
|
+
branch: string,
|
|
386
|
+
): Promise<{ number: number; end: 'source' | 'target' } | null> {
|
|
387
|
+
const [row] = await this.db
|
|
388
|
+
.select({ number: changeRequests.number, sourceBranch: changeRequests.sourceBranch })
|
|
389
|
+
.from(changeRequests)
|
|
390
|
+
.where(
|
|
391
|
+
and(
|
|
392
|
+
or(
|
|
393
|
+
eq(changeRequests.sourceBranch, branch),
|
|
394
|
+
eq(changeRequests.targetBranch, branch),
|
|
395
|
+
),
|
|
396
|
+
eq(changeRequests.state, 'open'),
|
|
397
|
+
),
|
|
398
|
+
)
|
|
399
|
+
.limit(1);
|
|
400
|
+
if (!row) return null;
|
|
401
|
+
return { number: row.number, end: row.sourceBranch === branch ? 'source' : 'target' };
|
|
402
|
+
}
|
|
403
|
+
|
|
285
404
|
async deleteBranch(
|
|
286
405
|
workspaceId: string,
|
|
287
406
|
name: string,
|
|
@@ -296,16 +415,19 @@ export class WorkflowService implements IWorkflowService {
|
|
|
296
415
|
// its own open-request check under this same lock, and the legacy
|
|
297
416
|
// `onlyIfNoRemote` prune only fires when origin no longer has the ref.
|
|
298
417
|
if (!opts?.systemCleanup && !opts?.onlyIfNoRemote) {
|
|
299
|
-
const open = await this.
|
|
300
|
-
|
|
301
|
-
.
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
.limit(1);
|
|
306
|
-
if (open.length > 0) {
|
|
418
|
+
const open = await this.openChangeRequestOn(name);
|
|
419
|
+
if (open !== null) {
|
|
420
|
+
// Two different situations, two different actors. A request FROM
|
|
421
|
+
// this branch is the deleter's own to withdraw; a request INTO it
|
|
422
|
+
// belongs to someone else, and telling the branch owner to
|
|
423
|
+
// "withdraw" it points them at a verb they cannot use.
|
|
307
424
|
throw new WorkflowValidationError(
|
|
308
|
-
|
|
425
|
+
open.end === 'source'
|
|
426
|
+
? `Branch "${name}" has an open change request (#${open.number}). ` +
|
|
427
|
+
`Withdraw or decline it before deleting the branch.`
|
|
428
|
+
: `Open change request #${open.number} proposes changes into "${name}". ` +
|
|
429
|
+
`Its author can withdraw it, or an admin can decline it; ` +
|
|
430
|
+
`after that the branch can be deleted.`,
|
|
309
431
|
);
|
|
310
432
|
}
|
|
311
433
|
}
|
|
@@ -363,9 +485,41 @@ export class WorkflowService implements IWorkflowService {
|
|
|
363
485
|
return this.git.fetch(workspaceId);
|
|
364
486
|
}
|
|
365
487
|
|
|
488
|
+
/**
|
|
489
|
+
* `git pull` on a workspace, plus the one announcement a pull owes the rest
|
|
490
|
+
* of the process.
|
|
491
|
+
*
|
|
492
|
+
* A pull rewrites a working tree without going through the write routes or
|
|
493
|
+
* the commit pipeline, so it emits neither of the signals those produce —
|
|
494
|
+
* and the catalogs that scan the DEFAULT branch's tree (skills, tool
|
|
495
|
+
* manuals, the plugin index) are keyed to exactly that tree. Every path that
|
|
496
|
+
* pulls the default workspace therefore has to say so, or those catalogs
|
|
497
|
+
* keep serving the pre-pull scan for the rest of their TTL: a merge landing
|
|
498
|
+
* an approved skill, the recovery ladder after a non-fast-forward push, and
|
|
499
|
+
* a user's update-from-remote alike. Routing every pull through here makes
|
|
500
|
+
* that ONE mechanism — "the default branch's tree changed" — instead of a
|
|
501
|
+
* separate special case per event. (The remote sync, `syncWorkspaceFromRemote`,
|
|
502
|
+
* pulls directly: it owes the same announcement to every branch and per
|
|
503
|
+
* file, and makes it from the same `treeChanged` verdict.)
|
|
504
|
+
*
|
|
505
|
+
* Announced only after the pull RESOLVES, and only when it CHANGED the
|
|
506
|
+
* tree's content: a pull that threw left the tree as it was, and so did an
|
|
507
|
+
* "already up to date" pull or one that landed only content-identical
|
|
508
|
+
* commits — dropping every catalog and making every attached browser
|
|
509
|
+
* refetch its file tree for a change that did not happen buys a re-scan
|
|
510
|
+
* and nothing else. The pull itself reports the change, since it is the
|
|
511
|
+
* only code that can observe it under the workspace mutex.
|
|
512
|
+
*/
|
|
513
|
+
private async pullWorkspace(workspaceId: string): Promise<void> {
|
|
514
|
+
const { treeChanged } = await this.git.pull(workspaceId);
|
|
515
|
+
if (treeChanged && branchForWorkspaceId(workspaceId) === DEFAULT_BRANCH) {
|
|
516
|
+
this.events?.emit({ kind: 'fs-tree-changed', workspaceId, branch: DEFAULT_BRANCH });
|
|
517
|
+
}
|
|
518
|
+
}
|
|
519
|
+
|
|
366
520
|
async updateFromRemote(workspaceId: string, user?: AuthUser): Promise<void> {
|
|
367
521
|
try {
|
|
368
|
-
await this.
|
|
522
|
+
await this.pullWorkspace(workspaceId);
|
|
369
523
|
} catch (err) {
|
|
370
524
|
if (err instanceof PullRebaseConflictError) {
|
|
371
525
|
await this.queuePullConflictRecovery(workspaceId, err, user);
|
|
@@ -374,6 +528,157 @@ export class WorkflowService implements IWorkflowService {
|
|
|
374
528
|
}
|
|
375
529
|
}
|
|
376
530
|
|
|
531
|
+
async retireRemoteGoneClone(workspaceId: string): Promise<boolean> {
|
|
532
|
+
const branch = branchForWorkspaceId(workspaceId);
|
|
533
|
+
const id = workspaceIdForBranch(branch);
|
|
534
|
+
// The lock `createBranch` and `deleteBranch` take: no Hexis operation can
|
|
535
|
+
// bring this branch back into being while the clone is examined and
|
|
536
|
+
// removed. The sync's own hold on the clone ended when its outcome was
|
|
537
|
+
// returned, so the branch is asked about AGAIN here rather than trusted
|
|
538
|
+
// from then — a recreate that landed in between keeps its clone. A clone
|
|
539
|
+
// being bootstrapped right now is left alone too: it can only be cloning
|
|
540
|
+
// a branch that exists, so the retirement is already moot. What remains
|
|
541
|
+
// is a recreate pushed from OUTSIDE Hexis in the milliseconds between the
|
|
542
|
+
// origin check and the delete, with a bootstrap starting in that same
|
|
543
|
+
// window; the next sync of that branch clones it fresh.
|
|
544
|
+
return this.branchLifecycle.run(`branch:${branch}`, async () => {
|
|
545
|
+
if (!(await this.workspaceService.hasBootstrappedWorkspace(id))) return false;
|
|
546
|
+
if (this.workspaceService.isBootstrapInFlight(branch)) return false;
|
|
547
|
+
if (await this.git.remoteBranchExists(branch, branch)) return false;
|
|
548
|
+
await this.workspaceService.deleteWorkspace(id);
|
|
549
|
+
// Whatever a sync still owed that clone is owed to nothing now.
|
|
550
|
+
this.owedAnnouncements.delete(id);
|
|
551
|
+
return true;
|
|
552
|
+
});
|
|
553
|
+
}
|
|
554
|
+
|
|
555
|
+
/**
|
|
556
|
+
* Cap on per-path `file-changed` events one sync announces for a branch.
|
|
557
|
+
* Past it the tree event alone carries the news — a bulk import that
|
|
558
|
+
* rewrote hundreds of files would otherwise fan hundreds of events out to
|
|
559
|
+
* every open browser to say what one refresh says.
|
|
560
|
+
*/
|
|
561
|
+
private static readonly SYNC_FILE_EVENT_CAP = 200;
|
|
562
|
+
|
|
563
|
+
/**
|
|
564
|
+
* Announcements a sync still OWES, per workspace: a pull landed but the
|
|
565
|
+
* events for it could not be sent. The pull is not redone on retry — HEAD
|
|
566
|
+
* has not moved since — so the announcement cannot be re-derived from sha
|
|
567
|
+
* movement; it is remembered here and delivered by the next sync of that
|
|
568
|
+
* workspace, merged with whatever that sync finds.
|
|
569
|
+
*/
|
|
570
|
+
private readonly owedAnnouncements = new Map<string, { after: string; changedPaths: string[] }>();
|
|
571
|
+
|
|
572
|
+
async syncWorkspaceFromRemote(workspaceId: string): Promise<BranchSyncOutcome> {
|
|
573
|
+
const branch = branchForWorkspaceId(workspaceId);
|
|
574
|
+
const id = workspaceIdForBranch(branch);
|
|
575
|
+
// Two spellings of one workspace id are in circulation — the encoded
|
|
576
|
+
// directory name (`ali%2Fx`, what the clone listing yields) and the
|
|
577
|
+
// Express-decoded form (`ali/x`, what every HTTP route hands the git
|
|
578
|
+
// layer) — and the git layer's mutex keys on the raw string. Speaking the
|
|
579
|
+
// routes' spelling to git is what puts a webhook's rebase in the same
|
|
580
|
+
// queue as the author's concurrent save on a slashed branch; the event
|
|
581
|
+
// bus gets the encoded form, which every SSE consumer canonicalises.
|
|
582
|
+
const gitId = branch;
|
|
583
|
+
// Every failure below is an OUTCOME, never a throw: the caller runs this
|
|
584
|
+
// over many clones, and one broken clone must not abort the others.
|
|
585
|
+
let pulled: RemoteSyncPullResult;
|
|
586
|
+
try {
|
|
587
|
+
pulled = await this.git.syncFromRemote(gitId);
|
|
588
|
+
} catch (err) {
|
|
589
|
+
if (err instanceof RemoteBranchGoneError) {
|
|
590
|
+
// Not a failure: the host deleted the branch, so there is nothing to
|
|
591
|
+
// sync and the caller retires the stale clone. No banner — nobody is
|
|
592
|
+
// editing a branch that no longer exists. An announcement still owed
|
|
593
|
+
// for that clone dies with it: a branch recreated under the same name
|
|
594
|
+
// is a different tree, and replaying the old paths against it would
|
|
595
|
+
// announce changes that never happened there.
|
|
596
|
+
this.owedAnnouncements.delete(id);
|
|
597
|
+
console.log(`[sync] branch "${branch}" no longer exists on the host`);
|
|
598
|
+
return { branch, outcome: 'remote-gone' };
|
|
599
|
+
}
|
|
600
|
+
if (err instanceof PullRebaseConflictError) {
|
|
601
|
+
// Same path as `updateFromRemote`: the rebase was aborted inside
|
|
602
|
+
// the pull (the clone is exactly what it was) and the divergence goes
|
|
603
|
+
// to the retry → recovery-agent → escalate ladder, attributed to the
|
|
604
|
+
// recovery bot since no person triggered this. It is still reported
|
|
605
|
+
// as a conflict because it is not resolved at request time — the
|
|
606
|
+
// caller's retry, or the ladder, settles it; the banner clears on the
|
|
607
|
+
// next clean pull or push.
|
|
608
|
+
await this.queuePullConflictRecovery(gitId, err);
|
|
609
|
+
const message = syncConflictMessage(branch, err.conflictedPaths);
|
|
610
|
+
console.warn(`[sync] ${message}`);
|
|
611
|
+
this.noteGitSyncFailed(gitId, branch, err, { paths: err.conflictedPaths, message });
|
|
612
|
+
return { branch, outcome: 'conflict', conflictedPaths: err.conflictedPaths, error: message };
|
|
613
|
+
}
|
|
614
|
+
const message = sanitizeError(err);
|
|
615
|
+
console.warn(`[sync] pull failed for branch "${branch}": ${message}`);
|
|
616
|
+
this.noteGitSyncFailed(gitId, branch, err);
|
|
617
|
+
return { branch, outcome: 'error', error: message };
|
|
618
|
+
}
|
|
619
|
+
// A clean pull is proof origin is reachable and this clone rebases onto
|
|
620
|
+
// it — the condition a `git-sync-failed` banner on this branch was
|
|
621
|
+
// waiting for, whether a sync or a push raised it.
|
|
622
|
+
this.noteGitSyncOk(gitId, branch);
|
|
623
|
+
const { before, after, treeChanged, changedPaths } = pulled;
|
|
624
|
+
// What this branch reports about ITS clone is decided by the pull alone:
|
|
625
|
+
// `updated` when HEAD moved, `up-to-date` otherwise.
|
|
626
|
+
const outcome: BranchSyncOutcome =
|
|
627
|
+
after === null || after === before
|
|
628
|
+
? { branch, outcome: 'up-to-date', to: after ?? '' }
|
|
629
|
+
: { branch, outcome: 'updated', from: before, to: after };
|
|
630
|
+
// What it ANNOUNCES is what has changed since the last announcement that
|
|
631
|
+
// was actually delivered: this pull's content change, if any, plus one an
|
|
632
|
+
// earlier sync landed but could not send. The announcement keys on the
|
|
633
|
+
// pull's verdict about CONTENT — commits that left the tree identical are
|
|
634
|
+
// nothing for a browser to refetch or a catalog to drop.
|
|
635
|
+
const owed = this.owedAnnouncements.get(id);
|
|
636
|
+
if (!treeChanged && !owed) return outcome;
|
|
637
|
+
const announce = {
|
|
638
|
+
after: after ?? owed?.after ?? '',
|
|
639
|
+
changedPaths: [...new Set([...(owed?.changedPaths ?? []), ...changedPaths])],
|
|
640
|
+
};
|
|
641
|
+
try {
|
|
642
|
+
// Announce the new tree. The tree event alone already refreshes every
|
|
643
|
+
// open file explorer and, on the default branch, drops the skill / tool /
|
|
644
|
+
// plugin catalogues (`catalog-cache-invalidation.ts`); the CR list
|
|
645
|
+
// caches the touched paths it resolved against the old tree, so drop
|
|
646
|
+
// that too.
|
|
647
|
+
this.prs.invalidateListCache();
|
|
648
|
+
this.events?.emit({ kind: 'fs-tree-changed', workspaceId: id, branch });
|
|
649
|
+
// Per-file events so an open tab on a file the host rewrote refetches
|
|
650
|
+
// instead of showing stale bytes until its next tree refresh. The paths
|
|
651
|
+
// were observed under the same hold as the pull, so they are exactly
|
|
652
|
+
// this sync's — never a save that landed alongside. Capped: a bulk
|
|
653
|
+
// import would otherwise fan hundreds of events out to say what one
|
|
654
|
+
// refresh says.
|
|
655
|
+
if (announce.changedPaths.length <= WorkflowService.SYNC_FILE_EVENT_CAP) {
|
|
656
|
+
for (const p of announce.changedPaths) {
|
|
657
|
+
this.events?.emit({
|
|
658
|
+
kind: 'file-changed',
|
|
659
|
+
workspaceId: id,
|
|
660
|
+
branch,
|
|
661
|
+
path: `${this.kbDirName}/${p}`,
|
|
662
|
+
newSha: announce.after,
|
|
663
|
+
byUserId: 'system',
|
|
664
|
+
byUserName: 'Git sync',
|
|
665
|
+
});
|
|
666
|
+
}
|
|
667
|
+
}
|
|
668
|
+
} catch (err) {
|
|
669
|
+
// The pull landed, so origin is fine and no banner is raised — but the
|
|
670
|
+
// caller must hear that this branch's announcement did not happen, and
|
|
671
|
+
// the announcement stays owed: the retry the error invites will not
|
|
672
|
+
// move HEAD again, so it must find the debt here rather than in the shas.
|
|
673
|
+
this.owedAnnouncements.set(id, announce);
|
|
674
|
+
const message = sanitizeError(err);
|
|
675
|
+
console.warn(`[sync] pulled "${branch}" but could not announce it: ${message}`);
|
|
676
|
+
return { branch, outcome: 'error', error: message };
|
|
677
|
+
}
|
|
678
|
+
this.owedAnnouncements.delete(id);
|
|
679
|
+
return outcome;
|
|
680
|
+
}
|
|
681
|
+
|
|
377
682
|
/**
|
|
378
683
|
* A pull hit a rebase conflict — the workspace carries local commits that
|
|
379
684
|
* conflict with origin, a state that never resolves on its own (every
|
|
@@ -700,7 +1005,7 @@ export class WorkflowService implements IWorkflowService {
|
|
|
700
1005
|
let recoveryError: unknown = null;
|
|
701
1006
|
if (looksLikeNonFastForward) {
|
|
702
1007
|
try {
|
|
703
|
-
await this.
|
|
1008
|
+
await this.pullWorkspace(workspaceId);
|
|
704
1009
|
await this.git.push(workspaceId, user);
|
|
705
1010
|
recovered = true;
|
|
706
1011
|
this.noteGitSyncOk(workspaceId, branch);
|
|
@@ -951,7 +1256,34 @@ export class WorkflowService implements IWorkflowService {
|
|
|
951
1256
|
* machinery is unaffected — all this adds is that someone finds out, rather
|
|
952
1257
|
* than the deployment looking healthy while nothing reaches the remote.
|
|
953
1258
|
*/
|
|
954
|
-
private noteGitSyncFailed(
|
|
1259
|
+
private noteGitSyncFailed(
|
|
1260
|
+
workspaceId: string,
|
|
1261
|
+
branch: string,
|
|
1262
|
+
err: unknown,
|
|
1263
|
+
/**
|
|
1264
|
+
* The files a person must reconcile, and the sentence we composed about
|
|
1265
|
+
* them. That sentence is ours (branch name + repo paths, no git stderr),
|
|
1266
|
+
* so it goes out verbatim — the same string the sync response and the
|
|
1267
|
+
* log carry — rather than through the sanitiser, whose 200-character
|
|
1268
|
+
* clip would make the banner disagree with them. Callers that already
|
|
1269
|
+
* hold the sentence pass it; otherwise it is derived below.
|
|
1270
|
+
*/
|
|
1271
|
+
explicitConflict?: { paths: string[]; message: string },
|
|
1272
|
+
): void {
|
|
1273
|
+
// A rebase conflict is a conflict whichever path ran into it. The remote
|
|
1274
|
+
// sync names its files on purpose; the push-recovery paths (autosave, the
|
|
1275
|
+
// lock release, the queued retries) raise the same banner with the raw
|
|
1276
|
+
// error — and the banner keeps the LATEST failure per branch, so a retry
|
|
1277
|
+
// landing after the sync would replace the conflict variant, with the
|
|
1278
|
+
// files as links, by the generic "check the server logs" one. Seen on
|
|
1279
|
+
// staging: the sync's 409 named the file, the worker's retry a moment
|
|
1280
|
+
// later took the links away. Derive the paths from the error itself so
|
|
1281
|
+
// every path agrees.
|
|
1282
|
+
const conflict =
|
|
1283
|
+
explicitConflict ??
|
|
1284
|
+
(err instanceof PullRebaseConflictError
|
|
1285
|
+
? { paths: err.conflictedPaths, message: syncConflictMessage(branch, err.conflictedPaths) }
|
|
1286
|
+
: undefined);
|
|
955
1287
|
// Same canonicalization as `noteGitSyncOk` — the pair must agree on keys.
|
|
956
1288
|
const id = branchForWorkspaceId(workspaceId);
|
|
957
1289
|
this.gitSyncFailing.add(id);
|
|
@@ -961,7 +1293,8 @@ export class WorkflowService implements IWorkflowService {
|
|
|
961
1293
|
branch,
|
|
962
1294
|
// Git stderr can quote a credentialed URL; the banner is user-facing and
|
|
963
1295
|
// the string also lands in client logs, so sanitize before it leaves.
|
|
964
|
-
reason: sanitizeError(err),
|
|
1296
|
+
reason: conflict ? conflict.message : sanitizeError(err),
|
|
1297
|
+
...(conflict ? { conflictedPaths: conflict.paths } : {}),
|
|
965
1298
|
});
|
|
966
1299
|
}
|
|
967
1300
|
|
|
@@ -1020,7 +1353,7 @@ export class WorkflowService implements IWorkflowService {
|
|
|
1020
1353
|
let recoveryError: unknown = null;
|
|
1021
1354
|
if (looksLikeNonFastForward) {
|
|
1022
1355
|
try {
|
|
1023
|
-
await this.
|
|
1356
|
+
await this.pullWorkspace(workspaceId);
|
|
1024
1357
|
await this.git.push(workspaceId, user, opts);
|
|
1025
1358
|
recovered = true;
|
|
1026
1359
|
this.noteGitSyncOk(workspaceId, branch);
|
|
@@ -1284,12 +1617,17 @@ export class WorkflowService implements IWorkflowService {
|
|
|
1284
1617
|
}
|
|
1285
1618
|
|
|
1286
1619
|
// The whole open sequence — uniqueness check through row insert — runs
|
|
1287
|
-
// under the
|
|
1288
|
-
//
|
|
1289
|
-
// cleanup can never race into the
|
|
1290
|
-
// method's "no open request" answer and its
|
|
1291
|
-
// branch out from under a request being opened.
|
|
1292
|
-
|
|
1620
|
+
// under the lifecycle lock of BOTH branches. `retireMergedSourceBranch`
|
|
1621
|
+
// and `deleteBranch` take the lock of the branch they are removing around
|
|
1622
|
+
// their own check-then-delete, so a cleanup can never race into the
|
|
1623
|
+
// multi-second gap between this method's "no open request" answer and its
|
|
1624
|
+
// row landing, and delete a branch out from under a request being opened.
|
|
1625
|
+
//
|
|
1626
|
+
// BOTH, because that gap is symmetric: a request is unusable if EITHER of
|
|
1627
|
+
// its branches is retired inside the window, and holding only the source
|
|
1628
|
+
// left a `Y -> X` request open to X being deleted under it — the very
|
|
1629
|
+
// failure the widened guards exist to prevent.
|
|
1630
|
+
return this.runOnBranchPair(input.sourceBranch, input.targetBranch, async () => {
|
|
1293
1631
|
// Uniqueness rule — A→B blocks A→B (the spec is explicit that B→A in
|
|
1294
1632
|
// parallel is still allowed). `listOpenPrs` is cached for 30s so we
|
|
1295
1633
|
// force-fresh to catch a CR that opened just now.
|
|
@@ -1623,7 +1961,7 @@ export class WorkflowService implements IWorkflowService {
|
|
|
1623
1961
|
// refs, but the restore commits from the working tree — a stale tree
|
|
1624
1962
|
// would push non-fast-forward and fail loudly anyway; this just makes
|
|
1625
1963
|
// that rare.
|
|
1626
|
-
await this.
|
|
1964
|
+
await this.pullWorkspace(ws.id).catch(() => undefined);
|
|
1627
1965
|
|
|
1628
1966
|
// The verb acts on the request as it is NOW — never a cached file list.
|
|
1629
1967
|
const paths = await this.git.changedPathsForPr(ws.id, baseBranch, headBranch);
|
|
@@ -1701,14 +2039,56 @@ export class WorkflowService implements IWorkflowService {
|
|
|
1701
2039
|
throw new WorkflowDomainError('This change request has already been applied.', 422);
|
|
1702
2040
|
}
|
|
1703
2041
|
|
|
1704
|
-
|
|
1705
|
-
|
|
1706
|
-
|
|
1707
|
-
|
|
1708
|
-
|
|
1709
|
-
|
|
1710
|
-
|
|
1711
|
-
|
|
2042
|
+
// Which `roles.yaml` authorizes this? The base branch's, exactly as every
|
|
2043
|
+
// other admin check here. When the base branch itself is gone, this
|
|
2044
|
+
// bootstrap (or the at-ref check below) fails; that failure is NOT proven
|
|
2045
|
+
// absence, so it never re-keys authorization to some other branch. It
|
|
2046
|
+
// kicks the deleted-branch sweep instead: the one mechanism that proves a
|
|
2047
|
+
// branch is missing (fresh fetch, then a set lookup) and closes every
|
|
2048
|
+
// request stranded by it, this one included. This call still fails; by
|
|
2049
|
+
// the next attempt the sweep has either closed the row (nothing left to
|
|
2050
|
+
// delete) or shown the base alive, meaning the failure was transient and
|
|
2051
|
+
// a retry can succeed.
|
|
2052
|
+
let ws: { id: string };
|
|
2053
|
+
let isAdmin: boolean | null;
|
|
2054
|
+
try {
|
|
2055
|
+
ws = await this.workspaceService.getOrCreateForBranch(summary.base);
|
|
2056
|
+
// STRICT: the admin check below reads roles.yaml at `origin/<base>`,
|
|
2057
|
+
// and on a swallowed fetch failure that ref is whatever the clone last
|
|
2058
|
+
// saw — a user whose rights were since revoked would still pass
|
|
2059
|
+
// against the stale tree. Authorization never runs against refs of
|
|
2060
|
+
// unknown age.
|
|
2061
|
+
try {
|
|
2062
|
+
await this.workspaceService.ensureRemotesFetched(ws.id, { strict: true });
|
|
2063
|
+
} catch {
|
|
2064
|
+
throw new WorkflowDomainError(
|
|
2065
|
+
`Could not verify the base branch "${summary.base}" — origin was unreachable. Retry in a moment.`,
|
|
2066
|
+
409,
|
|
2067
|
+
);
|
|
2068
|
+
}
|
|
2069
|
+
isAdmin = await this.accessControl.canWriteAtRef(
|
|
2070
|
+
ws.id,
|
|
2071
|
+
`origin/${summary.base}`,
|
|
2072
|
+
user.email,
|
|
2073
|
+
'roles.yaml',
|
|
2074
|
+
);
|
|
2075
|
+
} catch (err) {
|
|
2076
|
+
this.kickDeletedBranchSweep();
|
|
2077
|
+
throw err;
|
|
2078
|
+
}
|
|
2079
|
+
// null is "the ref did not resolve", not "denied" — and saying "only an
|
|
2080
|
+
// admin can do this" to an admin whose base branch is merely unfetchable
|
|
2081
|
+
// sent people chasing their role instead of the branch. Kick the sweep
|
|
2082
|
+
// (it proves absence itself, with a strict fetch, and closes the
|
|
2083
|
+
// stranded rows) and fail with the honest condition; by the next
|
|
2084
|
+
// attempt the sweep has either closed the row or shown the base alive.
|
|
2085
|
+
if (isAdmin === null) {
|
|
2086
|
+
this.kickDeletedBranchSweep();
|
|
2087
|
+
throw new WorkflowDomainError(
|
|
2088
|
+
`The base branch "${summary.base}" could not be resolved — it may have been deleted, or origin was unreachable. A background check is running; retry in a moment.`,
|
|
2089
|
+
409,
|
|
2090
|
+
);
|
|
2091
|
+
}
|
|
1712
2092
|
if (isAdmin !== true) {
|
|
1713
2093
|
throw new WorkflowDomainError('Only an admin can delete a change request.', 403);
|
|
1714
2094
|
}
|
|
@@ -1780,17 +2160,23 @@ export class WorkflowService implements IWorkflowService {
|
|
|
1780
2160
|
}
|
|
1781
2161
|
|
|
1782
2162
|
/**
|
|
1783
|
-
* Close every open change request whose
|
|
2163
|
+
* Close every open change request either of whose branches no longer exists.
|
|
1784
2164
|
*
|
|
1785
|
-
* A change request is a proposal to merge
|
|
1786
|
-
* gone — deleted after a manual merge, pruned as
|
|
1787
|
-
* by a feature that has itself been removed — the
|
|
1788
|
-
* reviewed, applied or declined. It is not a pending
|
|
1789
|
-
* tombstone, and it costs more than tidiness: `listOpenPrs`
|
|
1790
|
-
* open request's changed paths, so every one of these throws
|
|
2165
|
+
* A change request is a proposal to merge one branch into another, so it
|
|
2166
|
+
* needs BOTH. When either is gone — deleted after a manual merge, pruned as
|
|
2167
|
+
* abandoned, or left behind by a feature that has itself been removed — the
|
|
2168
|
+
* request cannot be read, reviewed, applied or declined. It is not a pending
|
|
2169
|
+
* decision, it is a tombstone, and it costs more than tidiness: `listOpenPrs`
|
|
2170
|
+
* resolves each open request's changed paths, so every one of these throws
|
|
1791
2171
|
* `unknown branch` on every poll, filling the log and slowing the list it
|
|
1792
2172
|
* appears in.
|
|
1793
2173
|
*
|
|
2174
|
+
* The TARGET half is the one that used to be missed, and it was the worse
|
|
2175
|
+
* half: a missing source could only ever arrive by a path that also closed
|
|
2176
|
+
* the request, while a request whose target had been retired under it was
|
|
2177
|
+
* unreadable, undeclinable, undeletable — and invisible here, so it survived
|
|
2178
|
+
* every restart.
|
|
2179
|
+
*
|
|
1794
2180
|
* CLOSED, NOT DELETED. The row is evidence — who proposed what, when, and
|
|
1795
2181
|
* that it was never applied — and it is the only remaining trace once the
|
|
1796
2182
|
* branch is gone. Closing takes it out of every open list (which is the
|
|
@@ -1799,7 +2185,7 @@ export class WorkflowService implements IWorkflowService {
|
|
|
1799
2185
|
* operation in the workflow that destroys history without a human asking.
|
|
1800
2186
|
*
|
|
1801
2187
|
* ONE fetch, then a set lookup. The freshness matters more than the cost:
|
|
1802
|
-
* `changedPathsForPr` fails when
|
|
2188
|
+
* `changedPathsForPr` fails when a branch is missing from THAT
|
|
1803
2189
|
* workspace's refs, which is not the same thing as missing from origin —
|
|
1804
2190
|
* a clone that never fetched a branch someone else created reports exactly
|
|
1805
2191
|
* the same error. Closing on that signal would eat live requests. So the
|
|
@@ -1815,7 +2201,11 @@ export class WorkflowService implements IWorkflowService {
|
|
|
1815
2201
|
*/
|
|
1816
2202
|
async closeChangeRequestsWithDeletedBranches(): Promise<number> {
|
|
1817
2203
|
const open = await this.db
|
|
1818
|
-
.select({
|
|
2204
|
+
.select({
|
|
2205
|
+
number: changeRequests.number,
|
|
2206
|
+
sourceBranch: changeRequests.sourceBranch,
|
|
2207
|
+
targetBranch: changeRequests.targetBranch,
|
|
2208
|
+
})
|
|
1819
2209
|
.from(changeRequests)
|
|
1820
2210
|
.where(eq(changeRequests.state, 'open'));
|
|
1821
2211
|
if (open.length === 0) return 0;
|
|
@@ -1824,6 +2214,11 @@ export class WorkflowService implements IWorkflowService {
|
|
|
1824
2214
|
try {
|
|
1825
2215
|
const branches = await this.git.listBranches(workspaceIdForBranch(DEFAULT_BRANCH), {
|
|
1826
2216
|
freshFetch: true,
|
|
2217
|
+
// The list is used to PROVE absence, and the listing's normal
|
|
2218
|
+
// degrade-to-stale behaviour would report every branch created since
|
|
2219
|
+
// the last good fetch as missing — closing live requests. Strict:
|
|
2220
|
+
// a failed fetch throws into the catch below, which closes nothing.
|
|
2221
|
+
strictFetch: true,
|
|
1827
2222
|
});
|
|
1828
2223
|
live = new Set(branches.map((b) => b.name));
|
|
1829
2224
|
} catch (err) {
|
|
@@ -1839,7 +2234,15 @@ export class WorkflowService implements IWorkflowService {
|
|
|
1839
2234
|
|
|
1840
2235
|
let closed = 0;
|
|
1841
2236
|
for (const cr of open) {
|
|
1842
|
-
|
|
2237
|
+
// Either end being gone makes the request unusable — the detail read
|
|
2238
|
+
// resolves the target first, so a missing target fails it just as hard
|
|
2239
|
+
// as a missing source, and no other path ever closes those.
|
|
2240
|
+
const missing = !live.has(cr.sourceBranch)
|
|
2241
|
+
? cr.sourceBranch
|
|
2242
|
+
: !live.has(cr.targetBranch)
|
|
2243
|
+
? cr.targetBranch
|
|
2244
|
+
: null;
|
|
2245
|
+
if (!missing) continue;
|
|
1843
2246
|
const now = new Date();
|
|
1844
2247
|
// Guarded on `state = 'open'`, so a merge or withdraw racing this call
|
|
1845
2248
|
// wins and this becomes a no-op.
|
|
@@ -1852,13 +2255,45 @@ export class WorkflowService implements IWorkflowService {
|
|
|
1852
2255
|
this.prs.invalidateDetailCache(cr.number);
|
|
1853
2256
|
this.events?.emit({ kind: 'change-request-rejected', number: cr.number });
|
|
1854
2257
|
console.log(
|
|
1855
|
-
`[cr] closed change request #${cr.number}:
|
|
2258
|
+
`[cr] closed change request #${cr.number}: branch "${missing}" no longer exists`,
|
|
1856
2259
|
);
|
|
1857
2260
|
closed++;
|
|
1858
2261
|
}
|
|
1859
2262
|
return closed;
|
|
1860
2263
|
}
|
|
1861
2264
|
|
|
2265
|
+
/**
|
|
2266
|
+
* The sweep above, on demand. Boot runs it once (`create-core-server`),
|
|
2267
|
+
* which clears requests stranded BEFORE the process started; a branch that
|
|
2268
|
+
* goes missing mid-uptime would otherwise keep its requests broken until
|
|
2269
|
+
* the next restart. Verbs that hit a failure which MIGHT mean a missing
|
|
2270
|
+
* branch kick it here.
|
|
2271
|
+
*
|
|
2272
|
+
* Detached, because the kicking verb is already surfacing its own error
|
|
2273
|
+
* and must not hold its response on a fetch. Coalesced, because the kick
|
|
2274
|
+
* sites are user actions that cluster when something is stranded (retry,
|
|
2275
|
+
* decline, delete) and one sweep answers all of them. The sweep proves
|
|
2276
|
+
* absence itself and fails safe by closing nothing, so a spurious kick
|
|
2277
|
+
* costs one fetch and never closes a live request.
|
|
2278
|
+
*/
|
|
2279
|
+
private sweepKick: Promise<void> | null = null;
|
|
2280
|
+
private kickDeletedBranchSweep(): void {
|
|
2281
|
+
if (this.sweepKick) return;
|
|
2282
|
+
this.sweepKick = this.closeChangeRequestsWithDeletedBranches()
|
|
2283
|
+
.then((n) => {
|
|
2284
|
+
if (n > 0) {
|
|
2285
|
+
console.log(
|
|
2286
|
+
`[cr] on-demand sweep closed ${n} stranded change request${n === 1 ? '' : 's'}`,
|
|
2287
|
+
);
|
|
2288
|
+
}
|
|
2289
|
+
})
|
|
2290
|
+
.catch((err) => console.warn('[cr] on-demand deleted-branch sweep failed:', err))
|
|
2291
|
+
.finally(() => {
|
|
2292
|
+
this.sweepKick = null;
|
|
2293
|
+
});
|
|
2294
|
+
}
|
|
2295
|
+
|
|
2296
|
+
|
|
1862
2297
|
/**
|
|
1863
2298
|
* Reject (close-without-merging) a change request. Per PLAN.md spec, the
|
|
1864
2299
|
* authorized set is:
|
|
@@ -1892,15 +2327,26 @@ export class WorkflowService implements IWorkflowService {
|
|
|
1892
2327
|
// The path list comes from the PR detail — fetch fresh so the rule
|
|
1893
2328
|
// applies against the current head (a stale list would let a user
|
|
1894
2329
|
// reject a CR whose files they no longer all own).
|
|
1895
|
-
|
|
1896
|
-
|
|
1897
|
-
|
|
1898
|
-
|
|
1899
|
-
|
|
1900
|
-
|
|
1901
|
-
|
|
1902
|
-
|
|
1903
|
-
|
|
2330
|
+
let detail;
|
|
2331
|
+
try {
|
|
2332
|
+
detail = await this.prs.getPrDetail(number, {
|
|
2333
|
+
fresh: true,
|
|
2334
|
+
workspaceId,
|
|
2335
|
+
viewerEmail: user.email,
|
|
2336
|
+
// Only `files[].path` is read below; a patch per file would be one
|
|
2337
|
+
// git subprocess each, generated for nothing, on every reject by a
|
|
2338
|
+
// non-author.
|
|
2339
|
+
patches: false,
|
|
2340
|
+
});
|
|
2341
|
+
} catch (err) {
|
|
2342
|
+
// The detail read resolves both branch tips (`resolvePrShas`), so a
|
|
2343
|
+
// request one of whose branches is gone fails HERE, on the verb that
|
|
2344
|
+
// exists to get rid of it. Kick the sweep (which proves the absence
|
|
2345
|
+
// and closes the stranded rows) and surface the failure; see
|
|
2346
|
+
// `kickDeletedBranchSweep`.
|
|
2347
|
+
this.kickDeletedBranchSweep();
|
|
2348
|
+
throw err;
|
|
2349
|
+
}
|
|
1904
2350
|
const paths = detail?.files.map((f) => f.path) ?? [];
|
|
1905
2351
|
if (paths.length > 0) {
|
|
1906
2352
|
const writeMap = await this.accessControl.canWriteBatchAtRef(
|
|
@@ -2026,7 +2472,7 @@ export class WorkflowService implements IWorkflowService {
|
|
|
2026
2472
|
try {
|
|
2027
2473
|
const targetWorkspace = await this.workspaceService.getOrCreateForBranch(baseBranch);
|
|
2028
2474
|
targetWorkspaceId = targetWorkspace.id;
|
|
2029
|
-
await this.
|
|
2475
|
+
await this.pullWorkspace(targetWorkspace.id);
|
|
2030
2476
|
} catch (err) {
|
|
2031
2477
|
// Still best-effort for the merge response (the merge already landed
|
|
2032
2478
|
// on origin) — but a rebase CONFLICT here means the target workspace
|
|
@@ -2086,17 +2532,12 @@ export class WorkflowService implements IWorkflowService {
|
|
|
2086
2532
|
// until retirement finishes (and fails loudly on the missing branch,
|
|
2087
2533
|
// retryable, instead of losing the branch mid-open).
|
|
2088
2534
|
await this.branchLifecycle.run(`branch:${sourceBranch}`, async () => {
|
|
2089
|
-
|
|
2090
|
-
|
|
2091
|
-
|
|
2092
|
-
|
|
2093
|
-
|
|
2094
|
-
|
|
2095
|
-
eq(changeRequests.state, 'open'),
|
|
2096
|
-
),
|
|
2097
|
-
)
|
|
2098
|
-
.limit(1);
|
|
2099
|
-
if (stillOpen.length > 0) return;
|
|
2535
|
+
// EITHER end (see `openChangeRequestOn`). The request being merged is
|
|
2536
|
+
// already `merged` by now, so it never matches itself; anything left
|
|
2537
|
+
// is a live request that still needs this branch — including one
|
|
2538
|
+
// proposing INTO it, which the source-only question used to miss.
|
|
2539
|
+
const stillOpen = await this.openChangeRequestOn(sourceBranch);
|
|
2540
|
+
if (stillOpen !== null) return;
|
|
2100
2541
|
|
|
2101
2542
|
const targetWs = await this.workspaceService.getOrCreateForBranch(baseBranch);
|
|
2102
2543
|
// The remote-tracking ref must be current, or deleteBranch's
|