@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
|
@@ -21,14 +21,14 @@ import { promises as fs } from 'node:fs';
|
|
|
21
21
|
import { execFile } from 'node:child_process';
|
|
22
22
|
import { promisify } from 'node:util';
|
|
23
23
|
import { isProtectedBranch, DEFAULT_BRANCH } from '@bevel-software/platform-shared';
|
|
24
|
-
import { and, eq } from 'drizzle-orm';
|
|
24
|
+
import { and, eq, or } from 'drizzle-orm';
|
|
25
25
|
import { changeRequests } from '../database/schema.js';
|
|
26
26
|
import { workspaceIdForBranch, branchForWorkspaceId } from '../../shared/workspace-id.js';
|
|
27
27
|
import { sanitizeError } from './sanitize-error.js';
|
|
28
28
|
import { WorkflowHooks } from './workflow-hooks.js';
|
|
29
29
|
import { WorkspaceMutex } from '../kb-fs/mutex.js';
|
|
30
30
|
import { hashEmail } from '../../shared/hash-email.js';
|
|
31
|
-
import { ChangeRequestConflictsError, DuplicateChangeRequestError, RolesYamlPreservationError, PullRebaseConflictError, PushNeedsAgentResolutionError, WorkflowDomainError, WorkflowValidationError, } from '../../shared/domain-errors.js';
|
|
31
|
+
import { ChangeRequestConflictsError, DuplicateChangeRequestError, RolesYamlPreservationError, PullRebaseConflictError, PushNeedsAgentResolutionError, RemoteBranchGoneError, WorkflowDomainError, WorkflowValidationError, } from '../../shared/domain-errors.js';
|
|
32
32
|
import { RECOVERY_BOT_EMAIL, RECOVERY_BOT_NAME } from './recovery-bot.js';
|
|
33
33
|
import { AccessDeniedError } from '../access-model/access-errors.js';
|
|
34
34
|
const execFileAsync = promisify(execFile);
|
|
@@ -58,6 +58,28 @@ function isOpenPairViolation(err) {
|
|
|
58
58
|
const message = fieldOf(err, 'message') ?? fieldOf(cause, 'message');
|
|
59
59
|
return typeof message === 'string' && message.includes(OPEN_PAIR_CONSTRAINT);
|
|
60
60
|
}
|
|
61
|
+
/**
|
|
62
|
+
* The one sentence a remote-sync conflict is reported with — in the sync
|
|
63
|
+
* response, on the branch's banner and in the log — so whoever sees any of
|
|
64
|
+
* the three reads the same thing: which branch, which files, that automatic
|
|
65
|
+
* recovery is under way, and what to do if it does not clear.
|
|
66
|
+
*/
|
|
67
|
+
export function syncConflictMessage(branch, conflictedPaths) {
|
|
68
|
+
// Bounded: the same sentence goes to the response, the banner and the log,
|
|
69
|
+
// and it must be one sentence in all three whatever the conflict's size.
|
|
70
|
+
// The full path list rides beside it as `conflictedPaths`.
|
|
71
|
+
const shown = conflictedPaths.slice(0, SYNC_CONFLICT_FILES_NAMED);
|
|
72
|
+
const more = conflictedPaths.length - shown.length;
|
|
73
|
+
const files = conflictedPaths.length === 0
|
|
74
|
+
? 'some files'
|
|
75
|
+
: more > 0
|
|
76
|
+
? `${shown.join(', ')} and ${more} more file${more === 1 ? '' : 's'}`
|
|
77
|
+
: shown.join(', ');
|
|
78
|
+
return (`${branch} is not in sync yet: ${files} changed both in Hexis and on the git host. ` +
|
|
79
|
+
`Recovery is queued; if this stays, open the files on ${branch} in Hexis, keep what you want, and save.`);
|
|
80
|
+
}
|
|
81
|
+
/** How many conflicted files the sync-conflict sentence names before "and N more". */
|
|
82
|
+
const SYNC_CONFLICT_FILES_NAMED = 3;
|
|
61
83
|
/** Redact the shared GitHub token from any error string before surfacing it. */
|
|
62
84
|
function redactTokens(msg) {
|
|
63
85
|
const tokens = [process.env.GITHUB_TOKEN, process.env.GH_TOKEN].filter((t) => !!t && t.length > 0);
|
|
@@ -222,7 +244,12 @@ export class WorkflowService {
|
|
|
222
244
|
return branches;
|
|
223
245
|
}
|
|
224
246
|
createBranch(workspaceId, name, fromBase) {
|
|
225
|
-
|
|
247
|
+
// Under the branch's lifecycle lock, like `deleteBranch` and the
|
|
248
|
+
// retirement of a clone the host deleted: creating a branch is the one
|
|
249
|
+
// operation that can bring a "gone" branch back, and the retirement's
|
|
250
|
+
// "still gone on origin?" check is only a guarantee if nothing can
|
|
251
|
+
// recreate the branch between that check and the delete.
|
|
252
|
+
return this.branchLifecycle.run(`branch:${name}`, () => this.git.createBranch(workspaceId, name, fromBase));
|
|
226
253
|
}
|
|
227
254
|
/**
|
|
228
255
|
* Serialises everything that decides a branch's fate — opening a change
|
|
@@ -234,6 +261,71 @@ export class WorkflowService {
|
|
|
234
261
|
* one server process owns the workspaces directory.
|
|
235
262
|
*/
|
|
236
263
|
branchLifecycle = new WorkspaceMutex();
|
|
264
|
+
/**
|
|
265
|
+
* Hold the lifecycle lock of BOTH branches a change request names, so a
|
|
266
|
+
* check-then-insert spanning them excludes either one being deleted.
|
|
267
|
+
*
|
|
268
|
+
* ATOMICALLY, via `runAll` — not `run(a, () => run(b, …))`. Taking the keys
|
|
269
|
+
* one at a time leaves a gap between them in which a delete of the second
|
|
270
|
+
* branch can start, pass its "is any open request using this?" check
|
|
271
|
+
* (the row does not exist yet) and finish, so the request lands pointing at
|
|
272
|
+
* a branch that was removed while it was being opened. That is the same
|
|
273
|
+
* failure this PR is about, reintroduced one level down — and it would have
|
|
274
|
+
* been a REGRESSION on the source branch, which the previous single-key
|
|
275
|
+
* lock held for the whole sequence.
|
|
276
|
+
*
|
|
277
|
+
* Reserving the set in one step also removes the deadlock question that
|
|
278
|
+
* sequential acquisition raises: two requests may legally be open in
|
|
279
|
+
* opposite directions (`A -> B` and `B -> A`), and nothing is ever held
|
|
280
|
+
* while waiting for the other key.
|
|
281
|
+
*
|
|
282
|
+
* A PROTECTED target's key is NOT reserved. No path in the system can
|
|
283
|
+
* delete a protected branch (`git.deleteBranch` throws
|
|
284
|
+
* `ProtectedBranchError` before any lock, `retireMergedSourceBranch`
|
|
285
|
+
* returns early on one), so the key would exclude nothing. It would only
|
|
286
|
+
* cost: nearly every request targets the protected default branch, so one
|
|
287
|
+
* shared key there would serialize every open on the hottest path for the
|
|
288
|
+
* duration of a fetch + merge + push + diff. The source needs no such
|
|
289
|
+
* carve-out; `openChangeRequest` refuses protected sources before locking.
|
|
290
|
+
*/
|
|
291
|
+
runOnBranchPair(source, target, fn) {
|
|
292
|
+
const keys = [`branch:${source}`];
|
|
293
|
+
if (!isProtectedBranch(target))
|
|
294
|
+
keys.push(`branch:${target}`);
|
|
295
|
+
return this.branchLifecycle.runAll(keys, fn);
|
|
296
|
+
}
|
|
297
|
+
/**
|
|
298
|
+
* The open change requests that deleting `branch` would strand — every one
|
|
299
|
+
* that NAMES it, at EITHER end.
|
|
300
|
+
*
|
|
301
|
+
* Both ends, not just the source. A change request is a proposal to move
|
|
302
|
+
* commits FROM one branch INTO another and it needs both to exist: the
|
|
303
|
+
* detail read resolves the target BEFORE the source, so a request whose
|
|
304
|
+
* target has been deleted cannot be read, declined, or deleted either. It
|
|
305
|
+
* is also the worse half to lose — a stranded source is closed by the boot
|
|
306
|
+
* sweep, while a stranded target used to survive every restart, because
|
|
307
|
+
* every guard here asked only "is this branch anybody's SOURCE?".
|
|
308
|
+
*
|
|
309
|
+
* That question is what let a merge retire a branch a second, still-open
|
|
310
|
+
* request was pointing INTO: the pair `X -> B` (merged, B retired) and
|
|
311
|
+
* `Y -> X` (open, now unusable forever).
|
|
312
|
+
*
|
|
313
|
+
* Returns the first such request's number and which end of it names the
|
|
314
|
+
* branch, or null when the branch is free. The end matters to the caller's
|
|
315
|
+
* error message: "withdraw your request" is only actionable when the asker
|
|
316
|
+
* owns the request, and a request proposing INTO the branch is someone
|
|
317
|
+
* else's.
|
|
318
|
+
*/
|
|
319
|
+
async openChangeRequestOn(branch) {
|
|
320
|
+
const [row] = await this.db
|
|
321
|
+
.select({ number: changeRequests.number, sourceBranch: changeRequests.sourceBranch })
|
|
322
|
+
.from(changeRequests)
|
|
323
|
+
.where(and(or(eq(changeRequests.sourceBranch, branch), eq(changeRequests.targetBranch, branch)), eq(changeRequests.state, 'open')))
|
|
324
|
+
.limit(1);
|
|
325
|
+
if (!row)
|
|
326
|
+
return null;
|
|
327
|
+
return { number: row.number, end: row.sourceBranch === branch ? 'source' : 'target' };
|
|
328
|
+
}
|
|
237
329
|
async deleteBranch(workspaceId, name, user, opts) {
|
|
238
330
|
return this.branchLifecycle.run(`branch:${name}`, async () => {
|
|
239
331
|
// A branch backing an OPEN change request is load-bearing: deleting it
|
|
@@ -243,13 +335,18 @@ export class WorkflowService {
|
|
|
243
335
|
// its own open-request check under this same lock, and the legacy
|
|
244
336
|
// `onlyIfNoRemote` prune only fires when origin no longer has the ref.
|
|
245
337
|
if (!opts?.systemCleanup && !opts?.onlyIfNoRemote) {
|
|
246
|
-
const open = await this.
|
|
247
|
-
|
|
248
|
-
.
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
throw new WorkflowValidationError(
|
|
338
|
+
const open = await this.openChangeRequestOn(name);
|
|
339
|
+
if (open !== null) {
|
|
340
|
+
// Two different situations, two different actors. A request FROM
|
|
341
|
+
// this branch is the deleter's own to withdraw; a request INTO it
|
|
342
|
+
// belongs to someone else, and telling the branch owner to
|
|
343
|
+
// "withdraw" it points them at a verb they cannot use.
|
|
344
|
+
throw new WorkflowValidationError(open.end === 'source'
|
|
345
|
+
? `Branch "${name}" has an open change request (#${open.number}). ` +
|
|
346
|
+
`Withdraw or decline it before deleting the branch.`
|
|
347
|
+
: `Open change request #${open.number} proposes changes into "${name}". ` +
|
|
348
|
+
`Its author can withdraw it, or an admin can decline it; ` +
|
|
349
|
+
`after that the branch can be deleted.`);
|
|
253
350
|
}
|
|
254
351
|
}
|
|
255
352
|
await this.deleteBranchUnlocked(workspaceId, name, user, opts);
|
|
@@ -291,9 +388,40 @@ export class WorkflowService {
|
|
|
291
388
|
refreshRemotes(workspaceId) {
|
|
292
389
|
return this.git.fetch(workspaceId);
|
|
293
390
|
}
|
|
391
|
+
/**
|
|
392
|
+
* `git pull` on a workspace, plus the one announcement a pull owes the rest
|
|
393
|
+
* of the process.
|
|
394
|
+
*
|
|
395
|
+
* A pull rewrites a working tree without going through the write routes or
|
|
396
|
+
* the commit pipeline, so it emits neither of the signals those produce —
|
|
397
|
+
* and the catalogs that scan the DEFAULT branch's tree (skills, tool
|
|
398
|
+
* manuals, the plugin index) are keyed to exactly that tree. Every path that
|
|
399
|
+
* pulls the default workspace therefore has to say so, or those catalogs
|
|
400
|
+
* keep serving the pre-pull scan for the rest of their TTL: a merge landing
|
|
401
|
+
* an approved skill, the recovery ladder after a non-fast-forward push, and
|
|
402
|
+
* a user's update-from-remote alike. Routing every pull through here makes
|
|
403
|
+
* that ONE mechanism — "the default branch's tree changed" — instead of a
|
|
404
|
+
* separate special case per event. (The remote sync, `syncWorkspaceFromRemote`,
|
|
405
|
+
* pulls directly: it owes the same announcement to every branch and per
|
|
406
|
+
* file, and makes it from the same `treeChanged` verdict.)
|
|
407
|
+
*
|
|
408
|
+
* Announced only after the pull RESOLVES, and only when it CHANGED the
|
|
409
|
+
* tree's content: a pull that threw left the tree as it was, and so did an
|
|
410
|
+
* "already up to date" pull or one that landed only content-identical
|
|
411
|
+
* commits — dropping every catalog and making every attached browser
|
|
412
|
+
* refetch its file tree for a change that did not happen buys a re-scan
|
|
413
|
+
* and nothing else. The pull itself reports the change, since it is the
|
|
414
|
+
* only code that can observe it under the workspace mutex.
|
|
415
|
+
*/
|
|
416
|
+
async pullWorkspace(workspaceId) {
|
|
417
|
+
const { treeChanged } = await this.git.pull(workspaceId);
|
|
418
|
+
if (treeChanged && branchForWorkspaceId(workspaceId) === DEFAULT_BRANCH) {
|
|
419
|
+
this.events?.emit({ kind: 'fs-tree-changed', workspaceId, branch: DEFAULT_BRANCH });
|
|
420
|
+
}
|
|
421
|
+
}
|
|
294
422
|
async updateFromRemote(workspaceId, user) {
|
|
295
423
|
try {
|
|
296
|
-
await this.
|
|
424
|
+
await this.pullWorkspace(workspaceId);
|
|
297
425
|
}
|
|
298
426
|
catch (err) {
|
|
299
427
|
if (err instanceof PullRebaseConflictError) {
|
|
@@ -302,6 +430,158 @@ export class WorkflowService {
|
|
|
302
430
|
throw err;
|
|
303
431
|
}
|
|
304
432
|
}
|
|
433
|
+
async retireRemoteGoneClone(workspaceId) {
|
|
434
|
+
const branch = branchForWorkspaceId(workspaceId);
|
|
435
|
+
const id = workspaceIdForBranch(branch);
|
|
436
|
+
// The lock `createBranch` and `deleteBranch` take: no Hexis operation can
|
|
437
|
+
// bring this branch back into being while the clone is examined and
|
|
438
|
+
// removed. The sync's own hold on the clone ended when its outcome was
|
|
439
|
+
// returned, so the branch is asked about AGAIN here rather than trusted
|
|
440
|
+
// from then — a recreate that landed in between keeps its clone. A clone
|
|
441
|
+
// being bootstrapped right now is left alone too: it can only be cloning
|
|
442
|
+
// a branch that exists, so the retirement is already moot. What remains
|
|
443
|
+
// is a recreate pushed from OUTSIDE Hexis in the milliseconds between the
|
|
444
|
+
// origin check and the delete, with a bootstrap starting in that same
|
|
445
|
+
// window; the next sync of that branch clones it fresh.
|
|
446
|
+
return this.branchLifecycle.run(`branch:${branch}`, async () => {
|
|
447
|
+
if (!(await this.workspaceService.hasBootstrappedWorkspace(id)))
|
|
448
|
+
return false;
|
|
449
|
+
if (this.workspaceService.isBootstrapInFlight(branch))
|
|
450
|
+
return false;
|
|
451
|
+
if (await this.git.remoteBranchExists(branch, branch))
|
|
452
|
+
return false;
|
|
453
|
+
await this.workspaceService.deleteWorkspace(id);
|
|
454
|
+
// Whatever a sync still owed that clone is owed to nothing now.
|
|
455
|
+
this.owedAnnouncements.delete(id);
|
|
456
|
+
return true;
|
|
457
|
+
});
|
|
458
|
+
}
|
|
459
|
+
/**
|
|
460
|
+
* Cap on per-path `file-changed` events one sync announces for a branch.
|
|
461
|
+
* Past it the tree event alone carries the news — a bulk import that
|
|
462
|
+
* rewrote hundreds of files would otherwise fan hundreds of events out to
|
|
463
|
+
* every open browser to say what one refresh says.
|
|
464
|
+
*/
|
|
465
|
+
static SYNC_FILE_EVENT_CAP = 200;
|
|
466
|
+
/**
|
|
467
|
+
* Announcements a sync still OWES, per workspace: a pull landed but the
|
|
468
|
+
* events for it could not be sent. The pull is not redone on retry — HEAD
|
|
469
|
+
* has not moved since — so the announcement cannot be re-derived from sha
|
|
470
|
+
* movement; it is remembered here and delivered by the next sync of that
|
|
471
|
+
* workspace, merged with whatever that sync finds.
|
|
472
|
+
*/
|
|
473
|
+
owedAnnouncements = new Map();
|
|
474
|
+
async syncWorkspaceFromRemote(workspaceId) {
|
|
475
|
+
const branch = branchForWorkspaceId(workspaceId);
|
|
476
|
+
const id = workspaceIdForBranch(branch);
|
|
477
|
+
// Two spellings of one workspace id are in circulation — the encoded
|
|
478
|
+
// directory name (`ali%2Fx`, what the clone listing yields) and the
|
|
479
|
+
// Express-decoded form (`ali/x`, what every HTTP route hands the git
|
|
480
|
+
// layer) — and the git layer's mutex keys on the raw string. Speaking the
|
|
481
|
+
// routes' spelling to git is what puts a webhook's rebase in the same
|
|
482
|
+
// queue as the author's concurrent save on a slashed branch; the event
|
|
483
|
+
// bus gets the encoded form, which every SSE consumer canonicalises.
|
|
484
|
+
const gitId = branch;
|
|
485
|
+
// Every failure below is an OUTCOME, never a throw: the caller runs this
|
|
486
|
+
// over many clones, and one broken clone must not abort the others.
|
|
487
|
+
let pulled;
|
|
488
|
+
try {
|
|
489
|
+
pulled = await this.git.syncFromRemote(gitId);
|
|
490
|
+
}
|
|
491
|
+
catch (err) {
|
|
492
|
+
if (err instanceof RemoteBranchGoneError) {
|
|
493
|
+
// Not a failure: the host deleted the branch, so there is nothing to
|
|
494
|
+
// sync and the caller retires the stale clone. No banner — nobody is
|
|
495
|
+
// editing a branch that no longer exists. An announcement still owed
|
|
496
|
+
// for that clone dies with it: a branch recreated under the same name
|
|
497
|
+
// is a different tree, and replaying the old paths against it would
|
|
498
|
+
// announce changes that never happened there.
|
|
499
|
+
this.owedAnnouncements.delete(id);
|
|
500
|
+
console.log(`[sync] branch "${branch}" no longer exists on the host`);
|
|
501
|
+
return { branch, outcome: 'remote-gone' };
|
|
502
|
+
}
|
|
503
|
+
if (err instanceof PullRebaseConflictError) {
|
|
504
|
+
// Same path as `updateFromRemote`: the rebase was aborted inside
|
|
505
|
+
// the pull (the clone is exactly what it was) and the divergence goes
|
|
506
|
+
// to the retry → recovery-agent → escalate ladder, attributed to the
|
|
507
|
+
// recovery bot since no person triggered this. It is still reported
|
|
508
|
+
// as a conflict because it is not resolved at request time — the
|
|
509
|
+
// caller's retry, or the ladder, settles it; the banner clears on the
|
|
510
|
+
// next clean pull or push.
|
|
511
|
+
await this.queuePullConflictRecovery(gitId, err);
|
|
512
|
+
const message = syncConflictMessage(branch, err.conflictedPaths);
|
|
513
|
+
console.warn(`[sync] ${message}`);
|
|
514
|
+
this.noteGitSyncFailed(gitId, branch, err, { paths: err.conflictedPaths, message });
|
|
515
|
+
return { branch, outcome: 'conflict', conflictedPaths: err.conflictedPaths, error: message };
|
|
516
|
+
}
|
|
517
|
+
const message = sanitizeError(err);
|
|
518
|
+
console.warn(`[sync] pull failed for branch "${branch}": ${message}`);
|
|
519
|
+
this.noteGitSyncFailed(gitId, branch, err);
|
|
520
|
+
return { branch, outcome: 'error', error: message };
|
|
521
|
+
}
|
|
522
|
+
// A clean pull is proof origin is reachable and this clone rebases onto
|
|
523
|
+
// it — the condition a `git-sync-failed` banner on this branch was
|
|
524
|
+
// waiting for, whether a sync or a push raised it.
|
|
525
|
+
this.noteGitSyncOk(gitId, branch);
|
|
526
|
+
const { before, after, treeChanged, changedPaths } = pulled;
|
|
527
|
+
// What this branch reports about ITS clone is decided by the pull alone:
|
|
528
|
+
// `updated` when HEAD moved, `up-to-date` otherwise.
|
|
529
|
+
const outcome = after === null || after === before
|
|
530
|
+
? { branch, outcome: 'up-to-date', to: after ?? '' }
|
|
531
|
+
: { branch, outcome: 'updated', from: before, to: after };
|
|
532
|
+
// What it ANNOUNCES is what has changed since the last announcement that
|
|
533
|
+
// was actually delivered: this pull's content change, if any, plus one an
|
|
534
|
+
// earlier sync landed but could not send. The announcement keys on the
|
|
535
|
+
// pull's verdict about CONTENT — commits that left the tree identical are
|
|
536
|
+
// nothing for a browser to refetch or a catalog to drop.
|
|
537
|
+
const owed = this.owedAnnouncements.get(id);
|
|
538
|
+
if (!treeChanged && !owed)
|
|
539
|
+
return outcome;
|
|
540
|
+
const announce = {
|
|
541
|
+
after: after ?? owed?.after ?? '',
|
|
542
|
+
changedPaths: [...new Set([...(owed?.changedPaths ?? []), ...changedPaths])],
|
|
543
|
+
};
|
|
544
|
+
try {
|
|
545
|
+
// Announce the new tree. The tree event alone already refreshes every
|
|
546
|
+
// open file explorer and, on the default branch, drops the skill / tool /
|
|
547
|
+
// plugin catalogues (`catalog-cache-invalidation.ts`); the CR list
|
|
548
|
+
// caches the touched paths it resolved against the old tree, so drop
|
|
549
|
+
// that too.
|
|
550
|
+
this.prs.invalidateListCache();
|
|
551
|
+
this.events?.emit({ kind: 'fs-tree-changed', workspaceId: id, branch });
|
|
552
|
+
// Per-file events so an open tab on a file the host rewrote refetches
|
|
553
|
+
// instead of showing stale bytes until its next tree refresh. The paths
|
|
554
|
+
// were observed under the same hold as the pull, so they are exactly
|
|
555
|
+
// this sync's — never a save that landed alongside. Capped: a bulk
|
|
556
|
+
// import would otherwise fan hundreds of events out to say what one
|
|
557
|
+
// refresh says.
|
|
558
|
+
if (announce.changedPaths.length <= WorkflowService.SYNC_FILE_EVENT_CAP) {
|
|
559
|
+
for (const p of announce.changedPaths) {
|
|
560
|
+
this.events?.emit({
|
|
561
|
+
kind: 'file-changed',
|
|
562
|
+
workspaceId: id,
|
|
563
|
+
branch,
|
|
564
|
+
path: `${this.kbDirName}/${p}`,
|
|
565
|
+
newSha: announce.after,
|
|
566
|
+
byUserId: 'system',
|
|
567
|
+
byUserName: 'Git sync',
|
|
568
|
+
});
|
|
569
|
+
}
|
|
570
|
+
}
|
|
571
|
+
}
|
|
572
|
+
catch (err) {
|
|
573
|
+
// The pull landed, so origin is fine and no banner is raised — but the
|
|
574
|
+
// caller must hear that this branch's announcement did not happen, and
|
|
575
|
+
// the announcement stays owed: the retry the error invites will not
|
|
576
|
+
// move HEAD again, so it must find the debt here rather than in the shas.
|
|
577
|
+
this.owedAnnouncements.set(id, announce);
|
|
578
|
+
const message = sanitizeError(err);
|
|
579
|
+
console.warn(`[sync] pulled "${branch}" but could not announce it: ${message}`);
|
|
580
|
+
return { branch, outcome: 'error', error: message };
|
|
581
|
+
}
|
|
582
|
+
this.owedAnnouncements.delete(id);
|
|
583
|
+
return outcome;
|
|
584
|
+
}
|
|
305
585
|
/**
|
|
306
586
|
* A pull hit a rebase conflict — the workspace carries local commits that
|
|
307
587
|
* conflict with origin, a state that never resolves on its own (every
|
|
@@ -549,7 +829,7 @@ export class WorkflowService {
|
|
|
549
829
|
let recoveryError = null;
|
|
550
830
|
if (looksLikeNonFastForward) {
|
|
551
831
|
try {
|
|
552
|
-
await this.
|
|
832
|
+
await this.pullWorkspace(workspaceId);
|
|
553
833
|
await this.git.push(workspaceId, user);
|
|
554
834
|
recovered = true;
|
|
555
835
|
this.noteGitSyncOk(workspaceId, branch);
|
|
@@ -760,7 +1040,29 @@ export class WorkflowService {
|
|
|
760
1040
|
* machinery is unaffected — all this adds is that someone finds out, rather
|
|
761
1041
|
* than the deployment looking healthy while nothing reaches the remote.
|
|
762
1042
|
*/
|
|
763
|
-
noteGitSyncFailed(workspaceId, branch, err
|
|
1043
|
+
noteGitSyncFailed(workspaceId, branch, err,
|
|
1044
|
+
/**
|
|
1045
|
+
* The files a person must reconcile, and the sentence we composed about
|
|
1046
|
+
* them. That sentence is ours (branch name + repo paths, no git stderr),
|
|
1047
|
+
* so it goes out verbatim — the same string the sync response and the
|
|
1048
|
+
* log carry — rather than through the sanitiser, whose 200-character
|
|
1049
|
+
* clip would make the banner disagree with them. Callers that already
|
|
1050
|
+
* hold the sentence pass it; otherwise it is derived below.
|
|
1051
|
+
*/
|
|
1052
|
+
explicitConflict) {
|
|
1053
|
+
// A rebase conflict is a conflict whichever path ran into it. The remote
|
|
1054
|
+
// sync names its files on purpose; the push-recovery paths (autosave, the
|
|
1055
|
+
// lock release, the queued retries) raise the same banner with the raw
|
|
1056
|
+
// error — and the banner keeps the LATEST failure per branch, so a retry
|
|
1057
|
+
// landing after the sync would replace the conflict variant, with the
|
|
1058
|
+
// files as links, by the generic "check the server logs" one. Seen on
|
|
1059
|
+
// staging: the sync's 409 named the file, the worker's retry a moment
|
|
1060
|
+
// later took the links away. Derive the paths from the error itself so
|
|
1061
|
+
// every path agrees.
|
|
1062
|
+
const conflict = explicitConflict ??
|
|
1063
|
+
(err instanceof PullRebaseConflictError
|
|
1064
|
+
? { paths: err.conflictedPaths, message: syncConflictMessage(branch, err.conflictedPaths) }
|
|
1065
|
+
: undefined);
|
|
764
1066
|
// Same canonicalization as `noteGitSyncOk` — the pair must agree on keys.
|
|
765
1067
|
const id = branchForWorkspaceId(workspaceId);
|
|
766
1068
|
this.gitSyncFailing.add(id);
|
|
@@ -770,7 +1072,8 @@ export class WorkflowService {
|
|
|
770
1072
|
branch,
|
|
771
1073
|
// Git stderr can quote a credentialed URL; the banner is user-facing and
|
|
772
1074
|
// the string also lands in client logs, so sanitize before it leaves.
|
|
773
|
-
reason: sanitizeError(err),
|
|
1075
|
+
reason: conflict ? conflict.message : sanitizeError(err),
|
|
1076
|
+
...(conflict ? { conflictedPaths: conflict.paths } : {}),
|
|
774
1077
|
});
|
|
775
1078
|
}
|
|
776
1079
|
/**
|
|
@@ -819,7 +1122,7 @@ export class WorkflowService {
|
|
|
819
1122
|
let recoveryError = null;
|
|
820
1123
|
if (looksLikeNonFastForward) {
|
|
821
1124
|
try {
|
|
822
|
-
await this.
|
|
1125
|
+
await this.pullWorkspace(workspaceId);
|
|
823
1126
|
await this.git.push(workspaceId, user, opts);
|
|
824
1127
|
recovered = true;
|
|
825
1128
|
this.noteGitSyncOk(workspaceId, branch);
|
|
@@ -1019,12 +1322,17 @@ export class WorkflowService {
|
|
|
1019
1322
|
throw new WorkflowValidationError(`Cannot open a change request *from* a protected branch ("${input.sourceBranch}").`, { kind: 'source-is-protected', sourceBranch: input.sourceBranch });
|
|
1020
1323
|
}
|
|
1021
1324
|
// The whole open sequence — uniqueness check through row insert — runs
|
|
1022
|
-
// under the
|
|
1023
|
-
//
|
|
1024
|
-
// cleanup can never race into the
|
|
1025
|
-
// method's "no open request" answer and its
|
|
1026
|
-
// branch out from under a request being opened.
|
|
1027
|
-
|
|
1325
|
+
// under the lifecycle lock of BOTH branches. `retireMergedSourceBranch`
|
|
1326
|
+
// and `deleteBranch` take the lock of the branch they are removing around
|
|
1327
|
+
// their own check-then-delete, so a cleanup can never race into the
|
|
1328
|
+
// multi-second gap between this method's "no open request" answer and its
|
|
1329
|
+
// row landing, and delete a branch out from under a request being opened.
|
|
1330
|
+
//
|
|
1331
|
+
// BOTH, because that gap is symmetric: a request is unusable if EITHER of
|
|
1332
|
+
// its branches is retired inside the window, and holding only the source
|
|
1333
|
+
// left a `Y -> X` request open to X being deleted under it — the very
|
|
1334
|
+
// failure the widened guards exist to prevent.
|
|
1335
|
+
return this.runOnBranchPair(input.sourceBranch, input.targetBranch, async () => {
|
|
1028
1336
|
// Uniqueness rule — A→B blocks A→B (the spec is explicit that B→A in
|
|
1029
1337
|
// parallel is still allowed). `listOpenPrs` is cached for 30s so we
|
|
1030
1338
|
// force-fresh to catch a CR that opened just now.
|
|
@@ -1236,7 +1544,7 @@ export class WorkflowService {
|
|
|
1236
1544
|
// refs, but the restore commits from the working tree — a stale tree
|
|
1237
1545
|
// would push non-fast-forward and fail loudly anyway; this just makes
|
|
1238
1546
|
// that rare.
|
|
1239
|
-
await this.
|
|
1547
|
+
await this.pullWorkspace(ws.id).catch(() => undefined);
|
|
1240
1548
|
// The verb acts on the request as it is NOW — never a cached file list.
|
|
1241
1549
|
const paths = await this.git.changedPathsForPr(ws.id, baseBranch, headBranch);
|
|
1242
1550
|
if (!paths.includes(repoRelPath)) {
|
|
@@ -1292,9 +1600,47 @@ export class WorkflowService {
|
|
|
1292
1600
|
if (summary.state === 'merged') {
|
|
1293
1601
|
throw new WorkflowDomainError('This change request has already been applied.', 422);
|
|
1294
1602
|
}
|
|
1295
|
-
|
|
1296
|
-
|
|
1297
|
-
|
|
1603
|
+
// Which `roles.yaml` authorizes this? The base branch's, exactly as every
|
|
1604
|
+
// other admin check here. When the base branch itself is gone, this
|
|
1605
|
+
// bootstrap (or the at-ref check below) fails; that failure is NOT proven
|
|
1606
|
+
// absence, so it never re-keys authorization to some other branch. It
|
|
1607
|
+
// kicks the deleted-branch sweep instead: the one mechanism that proves a
|
|
1608
|
+
// branch is missing (fresh fetch, then a set lookup) and closes every
|
|
1609
|
+
// request stranded by it, this one included. This call still fails; by
|
|
1610
|
+
// the next attempt the sweep has either closed the row (nothing left to
|
|
1611
|
+
// delete) or shown the base alive, meaning the failure was transient and
|
|
1612
|
+
// a retry can succeed.
|
|
1613
|
+
let ws;
|
|
1614
|
+
let isAdmin;
|
|
1615
|
+
try {
|
|
1616
|
+
ws = await this.workspaceService.getOrCreateForBranch(summary.base);
|
|
1617
|
+
// STRICT: the admin check below reads roles.yaml at `origin/<base>`,
|
|
1618
|
+
// and on a swallowed fetch failure that ref is whatever the clone last
|
|
1619
|
+
// saw — a user whose rights were since revoked would still pass
|
|
1620
|
+
// against the stale tree. Authorization never runs against refs of
|
|
1621
|
+
// unknown age.
|
|
1622
|
+
try {
|
|
1623
|
+
await this.workspaceService.ensureRemotesFetched(ws.id, { strict: true });
|
|
1624
|
+
}
|
|
1625
|
+
catch {
|
|
1626
|
+
throw new WorkflowDomainError(`Could not verify the base branch "${summary.base}" — origin was unreachable. Retry in a moment.`, 409);
|
|
1627
|
+
}
|
|
1628
|
+
isAdmin = await this.accessControl.canWriteAtRef(ws.id, `origin/${summary.base}`, user.email, 'roles.yaml');
|
|
1629
|
+
}
|
|
1630
|
+
catch (err) {
|
|
1631
|
+
this.kickDeletedBranchSweep();
|
|
1632
|
+
throw err;
|
|
1633
|
+
}
|
|
1634
|
+
// null is "the ref did not resolve", not "denied" — and saying "only an
|
|
1635
|
+
// admin can do this" to an admin whose base branch is merely unfetchable
|
|
1636
|
+
// sent people chasing their role instead of the branch. Kick the sweep
|
|
1637
|
+
// (it proves absence itself, with a strict fetch, and closes the
|
|
1638
|
+
// stranded rows) and fail with the honest condition; by the next
|
|
1639
|
+
// attempt the sweep has either closed the row or shown the base alive.
|
|
1640
|
+
if (isAdmin === null) {
|
|
1641
|
+
this.kickDeletedBranchSweep();
|
|
1642
|
+
throw new WorkflowDomainError(`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.`, 409);
|
|
1643
|
+
}
|
|
1298
1644
|
if (isAdmin !== true) {
|
|
1299
1645
|
throw new WorkflowDomainError('Only an admin can delete a change request.', 403);
|
|
1300
1646
|
}
|
|
@@ -1364,17 +1710,23 @@ export class WorkflowService {
|
|
|
1364
1710
|
return true;
|
|
1365
1711
|
}
|
|
1366
1712
|
/**
|
|
1367
|
-
* Close every open change request whose
|
|
1713
|
+
* Close every open change request either of whose branches no longer exists.
|
|
1368
1714
|
*
|
|
1369
|
-
* A change request is a proposal to merge
|
|
1370
|
-
* gone — deleted after a manual merge, pruned as
|
|
1371
|
-
* by a feature that has itself been removed — the
|
|
1372
|
-
* reviewed, applied or declined. It is not a pending
|
|
1373
|
-
* tombstone, and it costs more than tidiness: `listOpenPrs`
|
|
1374
|
-
* open request's changed paths, so every one of these throws
|
|
1715
|
+
* A change request is a proposal to merge one branch into another, so it
|
|
1716
|
+
* needs BOTH. When either is gone — deleted after a manual merge, pruned as
|
|
1717
|
+
* abandoned, or left behind by a feature that has itself been removed — the
|
|
1718
|
+
* request cannot be read, reviewed, applied or declined. It is not a pending
|
|
1719
|
+
* decision, it is a tombstone, and it costs more than tidiness: `listOpenPrs`
|
|
1720
|
+
* resolves each open request's changed paths, so every one of these throws
|
|
1375
1721
|
* `unknown branch` on every poll, filling the log and slowing the list it
|
|
1376
1722
|
* appears in.
|
|
1377
1723
|
*
|
|
1724
|
+
* The TARGET half is the one that used to be missed, and it was the worse
|
|
1725
|
+
* half: a missing source could only ever arrive by a path that also closed
|
|
1726
|
+
* the request, while a request whose target had been retired under it was
|
|
1727
|
+
* unreadable, undeclinable, undeletable — and invisible here, so it survived
|
|
1728
|
+
* every restart.
|
|
1729
|
+
*
|
|
1378
1730
|
* CLOSED, NOT DELETED. The row is evidence — who proposed what, when, and
|
|
1379
1731
|
* that it was never applied — and it is the only remaining trace once the
|
|
1380
1732
|
* branch is gone. Closing takes it out of every open list (which is the
|
|
@@ -1383,7 +1735,7 @@ export class WorkflowService {
|
|
|
1383
1735
|
* operation in the workflow that destroys history without a human asking.
|
|
1384
1736
|
*
|
|
1385
1737
|
* ONE fetch, then a set lookup. The freshness matters more than the cost:
|
|
1386
|
-
* `changedPathsForPr` fails when
|
|
1738
|
+
* `changedPathsForPr` fails when a branch is missing from THAT
|
|
1387
1739
|
* workspace's refs, which is not the same thing as missing from origin —
|
|
1388
1740
|
* a clone that never fetched a branch someone else created reports exactly
|
|
1389
1741
|
* the same error. Closing on that signal would eat live requests. So the
|
|
@@ -1399,7 +1751,11 @@ export class WorkflowService {
|
|
|
1399
1751
|
*/
|
|
1400
1752
|
async closeChangeRequestsWithDeletedBranches() {
|
|
1401
1753
|
const open = await this.db
|
|
1402
|
-
.select({
|
|
1754
|
+
.select({
|
|
1755
|
+
number: changeRequests.number,
|
|
1756
|
+
sourceBranch: changeRequests.sourceBranch,
|
|
1757
|
+
targetBranch: changeRequests.targetBranch,
|
|
1758
|
+
})
|
|
1403
1759
|
.from(changeRequests)
|
|
1404
1760
|
.where(eq(changeRequests.state, 'open'));
|
|
1405
1761
|
if (open.length === 0)
|
|
@@ -1408,6 +1764,11 @@ export class WorkflowService {
|
|
|
1408
1764
|
try {
|
|
1409
1765
|
const branches = await this.git.listBranches(workspaceIdForBranch(DEFAULT_BRANCH), {
|
|
1410
1766
|
freshFetch: true,
|
|
1767
|
+
// The list is used to PROVE absence, and the listing's normal
|
|
1768
|
+
// degrade-to-stale behaviour would report every branch created since
|
|
1769
|
+
// the last good fetch as missing — closing live requests. Strict:
|
|
1770
|
+
// a failed fetch throws into the catch below, which closes nothing.
|
|
1771
|
+
strictFetch: true,
|
|
1411
1772
|
});
|
|
1412
1773
|
live = new Set(branches.map((b) => b.name));
|
|
1413
1774
|
}
|
|
@@ -1423,7 +1784,15 @@ export class WorkflowService {
|
|
|
1423
1784
|
}
|
|
1424
1785
|
let closed = 0;
|
|
1425
1786
|
for (const cr of open) {
|
|
1426
|
-
|
|
1787
|
+
// Either end being gone makes the request unusable — the detail read
|
|
1788
|
+
// resolves the target first, so a missing target fails it just as hard
|
|
1789
|
+
// as a missing source, and no other path ever closes those.
|
|
1790
|
+
const missing = !live.has(cr.sourceBranch)
|
|
1791
|
+
? cr.sourceBranch
|
|
1792
|
+
: !live.has(cr.targetBranch)
|
|
1793
|
+
? cr.targetBranch
|
|
1794
|
+
: null;
|
|
1795
|
+
if (!missing)
|
|
1427
1796
|
continue;
|
|
1428
1797
|
const now = new Date();
|
|
1429
1798
|
// Guarded on `state = 'open'`, so a merge or withdraw racing this call
|
|
@@ -1437,11 +1806,40 @@ export class WorkflowService {
|
|
|
1437
1806
|
continue;
|
|
1438
1807
|
this.prs.invalidateDetailCache(cr.number);
|
|
1439
1808
|
this.events?.emit({ kind: 'change-request-rejected', number: cr.number });
|
|
1440
|
-
console.log(`[cr] closed change request #${cr.number}:
|
|
1809
|
+
console.log(`[cr] closed change request #${cr.number}: branch "${missing}" no longer exists`);
|
|
1441
1810
|
closed++;
|
|
1442
1811
|
}
|
|
1443
1812
|
return closed;
|
|
1444
1813
|
}
|
|
1814
|
+
/**
|
|
1815
|
+
* The sweep above, on demand. Boot runs it once (`create-core-server`),
|
|
1816
|
+
* which clears requests stranded BEFORE the process started; a branch that
|
|
1817
|
+
* goes missing mid-uptime would otherwise keep its requests broken until
|
|
1818
|
+
* the next restart. Verbs that hit a failure which MIGHT mean a missing
|
|
1819
|
+
* branch kick it here.
|
|
1820
|
+
*
|
|
1821
|
+
* Detached, because the kicking verb is already surfacing its own error
|
|
1822
|
+
* and must not hold its response on a fetch. Coalesced, because the kick
|
|
1823
|
+
* sites are user actions that cluster when something is stranded (retry,
|
|
1824
|
+
* decline, delete) and one sweep answers all of them. The sweep proves
|
|
1825
|
+
* absence itself and fails safe by closing nothing, so a spurious kick
|
|
1826
|
+
* costs one fetch and never closes a live request.
|
|
1827
|
+
*/
|
|
1828
|
+
sweepKick = null;
|
|
1829
|
+
kickDeletedBranchSweep() {
|
|
1830
|
+
if (this.sweepKick)
|
|
1831
|
+
return;
|
|
1832
|
+
this.sweepKick = this.closeChangeRequestsWithDeletedBranches()
|
|
1833
|
+
.then((n) => {
|
|
1834
|
+
if (n > 0) {
|
|
1835
|
+
console.log(`[cr] on-demand sweep closed ${n} stranded change request${n === 1 ? '' : 's'}`);
|
|
1836
|
+
}
|
|
1837
|
+
})
|
|
1838
|
+
.catch((err) => console.warn('[cr] on-demand deleted-branch sweep failed:', err))
|
|
1839
|
+
.finally(() => {
|
|
1840
|
+
this.sweepKick = null;
|
|
1841
|
+
});
|
|
1842
|
+
}
|
|
1445
1843
|
/**
|
|
1446
1844
|
* Reject (close-without-merging) a change request. Per PLAN.md spec, the
|
|
1447
1845
|
* authorized set is:
|
|
@@ -1467,15 +1865,27 @@ export class WorkflowService {
|
|
|
1467
1865
|
// The path list comes from the PR detail — fetch fresh so the rule
|
|
1468
1866
|
// applies against the current head (a stale list would let a user
|
|
1469
1867
|
// reject a CR whose files they no longer all own).
|
|
1470
|
-
|
|
1471
|
-
|
|
1472
|
-
|
|
1473
|
-
|
|
1474
|
-
|
|
1475
|
-
|
|
1476
|
-
|
|
1477
|
-
|
|
1478
|
-
|
|
1868
|
+
let detail;
|
|
1869
|
+
try {
|
|
1870
|
+
detail = await this.prs.getPrDetail(number, {
|
|
1871
|
+
fresh: true,
|
|
1872
|
+
workspaceId,
|
|
1873
|
+
viewerEmail: user.email,
|
|
1874
|
+
// Only `files[].path` is read below; a patch per file would be one
|
|
1875
|
+
// git subprocess each, generated for nothing, on every reject by a
|
|
1876
|
+
// non-author.
|
|
1877
|
+
patches: false,
|
|
1878
|
+
});
|
|
1879
|
+
}
|
|
1880
|
+
catch (err) {
|
|
1881
|
+
// The detail read resolves both branch tips (`resolvePrShas`), so a
|
|
1882
|
+
// request one of whose branches is gone fails HERE, on the verb that
|
|
1883
|
+
// exists to get rid of it. Kick the sweep (which proves the absence
|
|
1884
|
+
// and closes the stranded rows) and surface the failure; see
|
|
1885
|
+
// `kickDeletedBranchSweep`.
|
|
1886
|
+
this.kickDeletedBranchSweep();
|
|
1887
|
+
throw err;
|
|
1888
|
+
}
|
|
1479
1889
|
const paths = detail?.files.map((f) => f.path) ?? [];
|
|
1480
1890
|
if (paths.length > 0) {
|
|
1481
1891
|
const writeMap = await this.accessControl.canWriteBatchAtRef(workspaceId, `origin/${baseBranch}`, user.email, paths);
|
|
@@ -1563,7 +1973,7 @@ export class WorkflowService {
|
|
|
1563
1973
|
try {
|
|
1564
1974
|
const targetWorkspace = await this.workspaceService.getOrCreateForBranch(baseBranch);
|
|
1565
1975
|
targetWorkspaceId = targetWorkspace.id;
|
|
1566
|
-
await this.
|
|
1976
|
+
await this.pullWorkspace(targetWorkspace.id);
|
|
1567
1977
|
}
|
|
1568
1978
|
catch (err) {
|
|
1569
1979
|
// Still best-effort for the merge response (the merge already landed
|
|
@@ -1617,12 +2027,12 @@ export class WorkflowService {
|
|
|
1617
2027
|
// until retirement finishes (and fails loudly on the missing branch,
|
|
1618
2028
|
// retryable, instead of losing the branch mid-open).
|
|
1619
2029
|
await this.branchLifecycle.run(`branch:${sourceBranch}`, async () => {
|
|
1620
|
-
|
|
1621
|
-
|
|
1622
|
-
|
|
1623
|
-
|
|
1624
|
-
|
|
1625
|
-
if (stillOpen
|
|
2030
|
+
// EITHER end (see `openChangeRequestOn`). The request being merged is
|
|
2031
|
+
// already `merged` by now, so it never matches itself; anything left
|
|
2032
|
+
// is a live request that still needs this branch — including one
|
|
2033
|
+
// proposing INTO it, which the source-only question used to miss.
|
|
2034
|
+
const stillOpen = await this.openChangeRequestOn(sourceBranch);
|
|
2035
|
+
if (stillOpen !== null)
|
|
1626
2036
|
return;
|
|
1627
2037
|
const targetWs = await this.workspaceService.getOrCreateForBranch(baseBranch);
|
|
1628
2038
|
// The remote-tracking ref must be current, or deleteBranch's
|