@bevel-software/platform-core-backend 0.25.2 → 0.27.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/agent-guide/access-control.md +251 -0
- package/agent-guide/conventions.md +35 -0
- package/agent-guide/directory-structure.md +145 -0
- package/agent-guide/finding-things.md +7 -0
- package/agent-guide/html-views.md +83 -0
- package/agent-guide/introduction.md +27 -0
- package/agent-guide/new-knowledge-base.md +24 -0
- package/agent-guide/skills.md +47 -0
- package/agent-guide/tool-manuals.md +227 -0
- package/agent-guide/where-a-new-file-goes.md +36 -0
- package/dist/assets.d.ts +26 -0
- package/dist/assets.d.ts.map +1 -1
- package/dist/assets.js +35 -2
- package/dist/assets.js.map +1 -1
- package/dist/core/catalog-revision.d.ts +19 -1
- package/dist/core/catalog-revision.d.ts.map +1 -1
- package/dist/core/catalog-revision.js +14 -3
- package/dist/core/catalog-revision.js.map +1 -1
- package/dist/core/core-ports.d.ts +11 -0
- package/dist/core/core-ports.d.ts.map +1 -1
- package/dist/core/core-ports.js.map +1 -1
- package/dist/core/create-core-server.d.ts +5 -0
- package/dist/core/create-core-server.d.ts.map +1 -1
- package/dist/core/create-core-server.js +65 -3
- package/dist/core/create-core-server.js.map +1 -1
- package/dist/core/create-core-services.d.ts +35 -0
- package/dist/core/create-core-services.d.ts.map +1 -1
- package/dist/core/create-core-services.js +114 -12
- package/dist/core/create-core-services.js.map +1 -1
- package/dist/core/lifecycle.d.ts +16 -1
- package/dist/core/lifecycle.d.ts.map +1 -1
- package/dist/core/lifecycle.js +18 -1
- package/dist/core/lifecycle.js.map +1 -1
- package/dist/core-config.d.ts +21 -0
- package/dist/core-config.d.ts.map +1 -1
- package/dist/core-config.js +22 -1
- package/dist/core-config.js.map +1 -1
- package/dist/index.d.ts +1 -1
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +2 -2
- package/dist/index.js.map +1 -1
- package/dist/modules/access/access-control.interface.d.ts +17 -0
- package/dist/modules/access/access-control.interface.d.ts.map +1 -1
- package/dist/modules/access/access-control.service.d.ts +2 -0
- package/dist/modules/access/access-control.service.d.ts.map +1 -1
- package/dist/modules/access/access-control.service.js +27 -0
- package/dist/modules/access/access-control.service.js.map +1 -1
- package/dist/modules/access/directory-sync-bot.js +1 -1
- package/dist/modules/access/directory-sync-bot.js.map +1 -1
- package/dist/modules/agent-guide/agent-guide.d.ts +145 -0
- package/dist/modules/agent-guide/agent-guide.d.ts.map +1 -0
- package/dist/modules/agent-guide/agent-guide.js +199 -0
- package/dist/modules/agent-guide/agent-guide.js.map +1 -0
- package/dist/modules/agent-guide/agent-guide.tools.d.ts +24 -0
- package/dist/modules/agent-guide/agent-guide.tools.d.ts.map +1 -0
- package/dist/modules/agent-guide/agent-guide.tools.js +100 -0
- package/dist/modules/agent-guide/agent-guide.tools.js.map +1 -0
- package/dist/modules/agent-guide/index.d.ts +4 -0
- package/dist/modules/agent-guide/index.d.ts.map +1 -0
- package/dist/modules/agent-guide/index.js +4 -0
- package/dist/modules/agent-guide/index.js.map +1 -0
- package/dist/modules/agent-instructions/agent-instructions.routes.d.ts +3 -2
- package/dist/modules/agent-instructions/agent-instructions.routes.d.ts.map +1 -1
- package/dist/modules/agent-instructions/agent-instructions.routes.js +3 -2
- package/dist/modules/agent-instructions/agent-instructions.routes.js.map +1 -1
- package/dist/modules/agent-instructions/compose.d.ts +14 -6
- package/dist/modules/agent-instructions/compose.d.ts.map +1 -1
- package/dist/modules/agent-instructions/compose.js +28 -8
- package/dist/modules/agent-instructions/compose.js.map +1 -1
- package/dist/modules/agent-instructions/index.d.ts +1 -1
- package/dist/modules/agent-instructions/index.d.ts.map +1 -1
- package/dist/modules/agent-instructions/index.js +1 -1
- package/dist/modules/agent-instructions/index.js.map +1 -1
- package/dist/modules/agent-instructions/shared-file-rules.d.ts +10 -50
- package/dist/modules/agent-instructions/shared-file-rules.d.ts.map +1 -1
- package/dist/modules/agent-instructions/shared-file-rules.js +51 -104
- package/dist/modules/agent-instructions/shared-file-rules.js.map +1 -1
- package/dist/modules/audit/agent-audit.service.d.ts +21 -2
- package/dist/modules/audit/agent-audit.service.d.ts.map +1 -1
- package/dist/modules/audit/agent-audit.service.js +43 -2
- package/dist/modules/audit/agent-audit.service.js.map +1 -1
- package/dist/modules/audit/agent-connection.routes.d.ts +34 -0
- package/dist/modules/audit/agent-connection.routes.d.ts.map +1 -0
- package/dist/modules/audit/agent-connection.routes.js +38 -0
- package/dist/modules/audit/agent-connection.routes.js.map +1 -0
- package/dist/modules/audit/audit.contract.d.ts +24 -0
- package/dist/modules/audit/audit.contract.d.ts.map +1 -1
- package/dist/modules/audit/audit.contract.js.map +1 -1
- package/dist/modules/audit/index.d.ts +1 -0
- package/dist/modules/audit/index.d.ts.map +1 -1
- package/dist/modules/audit/index.js +1 -0
- package/dist/modules/audit/index.js.map +1 -1
- package/dist/modules/auth/account.routes.d.ts.map +1 -1
- package/dist/modules/auth/account.routes.js +10 -3
- package/dist/modules/auth/account.routes.js.map +1 -1
- package/dist/modules/auth/auth.middleware.d.ts +7 -0
- package/dist/modules/auth/auth.middleware.d.ts.map +1 -1
- package/dist/modules/auth/auth.middleware.js +1 -1
- package/dist/modules/auth/auth.middleware.js.map +1 -1
- package/dist/modules/code-mode/code-mode.tool.d.ts.map +1 -1
- package/dist/modules/code-mode/code-mode.tool.js +11 -3
- package/dist/modules/code-mode/code-mode.tool.js.map +1 -1
- package/dist/modules/database/core-schema.d.ts +96 -0
- package/dist/modules/database/core-schema.d.ts.map +1 -1
- package/dist/modules/database/core-schema.js +32 -3
- package/dist/modules/database/core-schema.js.map +1 -1
- package/dist/modules/embed/account-link.service.d.ts +42 -0
- package/dist/modules/embed/account-link.service.d.ts.map +1 -0
- package/dist/modules/embed/account-link.service.js +50 -0
- package/dist/modules/embed/account-link.service.js.map +1 -0
- package/dist/modules/embed/embed-link.d.ts +58 -0
- package/dist/modules/embed/embed-link.d.ts.map +1 -0
- package/dist/modules/embed/embed-link.js +160 -0
- package/dist/modules/embed/embed-link.js.map +1 -0
- package/dist/modules/embed/embed.errors.d.ts +30 -0
- package/dist/modules/embed/embed.errors.d.ts.map +1 -0
- package/dist/modules/embed/embed.errors.js +42 -0
- package/dist/modules/embed/embed.errors.js.map +1 -0
- package/dist/modules/embed/embed.interface.d.ts +204 -0
- package/dist/modules/embed/embed.interface.d.ts.map +1 -0
- package/dist/modules/embed/embed.interface.js +2 -0
- package/dist/modules/embed/embed.interface.js.map +1 -0
- package/dist/modules/embed/embed.routes.d.ts +59 -0
- package/dist/modules/embed/embed.routes.d.ts.map +1 -0
- package/dist/modules/embed/embed.routes.js +343 -0
- package/dist/modules/embed/embed.routes.js.map +1 -0
- package/dist/modules/embed/embed.service.d.ts +169 -0
- package/dist/modules/embed/embed.service.d.ts.map +1 -0
- package/dist/modules/embed/embed.service.js +627 -0
- package/dist/modules/embed/embed.service.js.map +1 -0
- package/dist/modules/embed/embed.tools.d.ts +51 -0
- package/dist/modules/embed/embed.tools.d.ts.map +1 -0
- package/dist/modules/embed/embed.tools.js +170 -0
- package/dist/modules/embed/embed.tools.js.map +1 -0
- package/dist/modules/embed/mcp-app.d.ts +64 -0
- package/dist/modules/embed/mcp-app.d.ts.map +1 -0
- package/dist/modules/embed/mcp-app.js +147 -0
- package/dist/modules/embed/mcp-app.js.map +1 -0
- package/dist/modules/embed/mcp-app.routes.d.ts +21 -0
- package/dist/modules/embed/mcp-app.routes.d.ts.map +1 -0
- package/dist/modules/embed/mcp-app.routes.js +36 -0
- package/dist/modules/embed/mcp-app.routes.js.map +1 -0
- package/dist/modules/github-app/github-app.connection.d.ts +15 -0
- package/dist/modules/github-app/github-app.connection.d.ts.map +1 -1
- package/dist/modules/github-app/github-app.connection.js +34 -3
- package/dist/modules/github-app/github-app.connection.js.map +1 -1
- package/dist/modules/kb-fs/locking-filesystem.d.ts +65 -0
- package/dist/modules/kb-fs/locking-filesystem.d.ts.map +1 -1
- package/dist/modules/kb-fs/locking-filesystem.js +300 -0
- package/dist/modules/kb-fs/locking-filesystem.js.map +1 -1
- package/dist/modules/kb-sync/kb-sync.interface.d.ts +5 -0
- package/dist/modules/kb-sync/kb-sync.interface.d.ts.map +1 -1
- package/dist/modules/kb-sync/kb-sync.service.d.ts.map +1 -1
- package/dist/modules/kb-sync/kb-sync.service.js +6 -0
- package/dist/modules/kb-sync/kb-sync.service.js.map +1 -1
- package/dist/modules/mcp/mcp-auth.middleware.d.ts.map +1 -1
- package/dist/modules/mcp/mcp-auth.middleware.js +2 -1
- package/dist/modules/mcp/mcp-auth.middleware.js.map +1 -1
- package/dist/modules/mcp/mcp.service.d.ts +57 -3
- package/dist/modules/mcp/mcp.service.d.ts.map +1 -1
- package/dist/modules/mcp/mcp.service.js +177 -24
- package/dist/modules/mcp/mcp.service.js.map +1 -1
- package/dist/modules/mcp/oauth/bevel-oauth-provider.d.ts +24 -0
- package/dist/modules/mcp/oauth/bevel-oauth-provider.d.ts.map +1 -1
- package/dist/modules/mcp/oauth/bevel-oauth-provider.js +32 -4
- package/dist/modules/mcp/oauth/bevel-oauth-provider.js.map +1 -1
- package/dist/modules/mcp/tool-schema-guard.d.ts +105 -0
- package/dist/modules/mcp/tool-schema-guard.d.ts.map +1 -0
- package/dist/modules/mcp/tool-schema-guard.js +171 -0
- package/dist/modules/mcp/tool-schema-guard.js.map +1 -0
- package/dist/modules/onboarding/onboarding.contract.d.ts +80 -0
- package/dist/modules/onboarding/onboarding.contract.d.ts.map +1 -0
- package/dist/modules/onboarding/onboarding.contract.js +7 -0
- package/dist/modules/onboarding/onboarding.contract.js.map +1 -0
- package/dist/modules/onboarding/onboarding.routes.d.ts +18 -0
- package/dist/modules/onboarding/onboarding.routes.d.ts.map +1 -0
- package/dist/modules/onboarding/onboarding.routes.js +81 -0
- package/dist/modules/onboarding/onboarding.routes.js.map +1 -0
- package/dist/modules/onboarding/starter-pack.service.d.ts +213 -0
- package/dist/modules/onboarding/starter-pack.service.d.ts.map +1 -0
- package/dist/modules/onboarding/starter-pack.service.js +492 -0
- package/dist/modules/onboarding/starter-pack.service.js.map +1 -0
- package/dist/modules/onboarding/starter-packs.d.ts +78 -0
- package/dist/modules/onboarding/starter-packs.d.ts.map +1 -0
- package/dist/modules/onboarding/starter-packs.js +156 -0
- package/dist/modules/onboarding/starter-packs.js.map +1 -0
- package/dist/modules/plugins/index.d.ts +1 -1
- package/dist/modules/plugins/index.d.ts.map +1 -1
- package/dist/modules/plugins/index.js +1 -1
- package/dist/modules/plugins/index.js.map +1 -1
- package/dist/modules/plugins/plugin-provision.service.d.ts +19 -0
- package/dist/modules/plugins/plugin-provision.service.d.ts.map +1 -1
- package/dist/modules/plugins/plugin-provision.service.js +18 -1
- package/dist/modules/plugins/plugin-provision.service.js.map +1 -1
- package/dist/modules/plugins/plugins.routes.d.ts +0 -36
- package/dist/modules/plugins/plugins.routes.d.ts.map +1 -1
- package/dist/modules/plugins/plugins.routes.js +31 -0
- package/dist/modules/plugins/plugins.routes.js.map +1 -1
- package/dist/modules/plugins/plugins.tools.d.ts +36 -2
- package/dist/modules/plugins/plugins.tools.d.ts.map +1 -1
- package/dist/modules/plugins/plugins.tools.js +71 -14
- package/dist/modules/plugins/plugins.tools.js.map +1 -1
- package/dist/modules/settings/deployment-settings.service.d.ts +34 -7
- package/dist/modules/settings/deployment-settings.service.d.ts.map +1 -1
- package/dist/modules/settings/deployment-settings.service.js +148 -54
- package/dist/modules/settings/deployment-settings.service.js.map +1 -1
- package/dist/modules/settings/repository-source.d.ts +1 -0
- package/dist/modules/settings/repository-source.d.ts.map +1 -1
- package/dist/modules/settings/repository-source.js +3 -3
- package/dist/modules/settings/repository-source.js.map +1 -1
- package/dist/modules/settings/setup.routes.d.ts.map +1 -1
- package/dist/modules/settings/setup.routes.js +3 -6
- package/dist/modules/settings/setup.routes.js.map +1 -1
- package/dist/modules/skills/skills.tools.d.ts.map +1 -1
- package/dist/modules/skills/skills.tools.js +103 -43
- package/dist/modules/skills/skills.tools.js.map +1 -1
- package/dist/modules/tool-auth/connection-key-rejection.d.ts +10 -1
- package/dist/modules/tool-auth/connection-key-rejection.d.ts.map +1 -1
- package/dist/modules/tool-auth/connection-key-rejection.js +11 -3
- package/dist/modules/tool-auth/connection-key-rejection.js.map +1 -1
- package/dist/modules/tool-auth/external-api-key.interface.d.ts +8 -0
- package/dist/modules/tool-auth/external-api-key.interface.d.ts.map +1 -1
- package/dist/modules/tool-auth/external-api-key.service.d.ts +24 -2
- package/dist/modules/tool-auth/external-api-key.service.d.ts.map +1 -1
- package/dist/modules/tool-auth/external-api-key.service.js +59 -2
- package/dist/modules/tool-auth/external-api-key.service.js.map +1 -1
- package/dist/modules/tool-auth/tool-auth.middleware.d.ts.map +1 -1
- package/dist/modules/tool-auth/tool-auth.middleware.js +9 -4
- package/dist/modules/tool-auth/tool-auth.middleware.js.map +1 -1
- package/dist/modules/tool-helpers/index.d.ts +28 -1
- package/dist/modules/tool-helpers/index.d.ts.map +1 -1
- package/dist/modules/tool-helpers/index.js +28 -1
- package/dist/modules/tool-helpers/index.js.map +1 -1
- package/dist/modules/tool-helpers/route-argument-check.d.ts +10 -0
- package/dist/modules/tool-helpers/route-argument-check.d.ts.map +1 -0
- package/dist/modules/tool-helpers/route-argument-check.js +132 -0
- package/dist/modules/tool-helpers/route-argument-check.js.map +1 -0
- package/dist/modules/tool-helpers/route-tool-schemas.d.ts +89 -0
- package/dist/modules/tool-helpers/route-tool-schemas.d.ts.map +1 -0
- package/dist/modules/tool-helpers/route-tool-schemas.js +113 -0
- package/dist/modules/tool-helpers/route-tool-schemas.js.map +1 -0
- package/dist/modules/tool-helpers/tool-def.d.ts +71 -0
- package/dist/modules/tool-helpers/tool-def.d.ts.map +1 -1
- package/dist/modules/tool-helpers/tool-def.js +70 -6
- package/dist/modules/tool-helpers/tool-def.js.map +1 -1
- package/dist/modules/tool-helpers/tool-handler.d.ts +46 -6
- package/dist/modules/tool-helpers/tool-handler.d.ts.map +1 -1
- package/dist/modules/tool-helpers/tool-handler.js +157 -8
- package/dist/modules/tool-helpers/tool-handler.js.map +1 -1
- package/dist/modules/tool-helpers/tool.contract.d.ts +9 -0
- package/dist/modules/tool-helpers/tool.contract.d.ts.map +1 -1
- package/dist/modules/tool-helpers/tool.contract.js.map +1 -1
- package/dist/modules/tool-manuals/tool-manuals.contract.d.ts +23 -4
- package/dist/modules/tool-manuals/tool-manuals.contract.d.ts.map +1 -1
- package/dist/modules/tool-manuals/tool-manuals.contract.js.map +1 -1
- package/dist/modules/tool-manuals/tool-manuals.service.d.ts +4 -0
- package/dist/modules/tool-manuals/tool-manuals.service.d.ts.map +1 -1
- package/dist/modules/tool-manuals/tool-manuals.service.js +14 -0
- package/dist/modules/tool-manuals/tool-manuals.service.js.map +1 -1
- package/dist/modules/tool-manuals/tool-manuals.tools.d.ts +7 -0
- package/dist/modules/tool-manuals/tool-manuals.tools.d.ts.map +1 -1
- package/dist/modules/tool-manuals/tool-manuals.tools.js +77 -38
- package/dist/modules/tool-manuals/tool-manuals.tools.js.map +1 -1
- package/dist/modules/tool-registry/description-length.d.ts +20 -16
- package/dist/modules/tool-registry/description-length.d.ts.map +1 -1
- package/dist/modules/tool-registry/description-length.js +50 -30
- package/dist/modules/tool-registry/description-length.js.map +1 -1
- package/dist/modules/tool-registry/guide-first.d.ts +27 -0
- package/dist/modules/tool-registry/guide-first.d.ts.map +1 -0
- package/dist/modules/tool-registry/guide-first.js +40 -0
- package/dist/modules/tool-registry/guide-first.js.map +1 -0
- package/dist/modules/tool-registry/tool-registry.d.ts +6 -0
- package/dist/modules/tool-registry/tool-registry.d.ts.map +1 -1
- package/dist/modules/tool-registry/tool-registry.js +9 -2
- package/dist/modules/tool-registry/tool-registry.js.map +1 -1
- package/dist/modules/workflow/agent-tools/change-request-read-shape.d.ts +449 -0
- package/dist/modules/workflow/agent-tools/change-request-read-shape.d.ts.map +1 -0
- package/dist/modules/workflow/agent-tools/change-request-read-shape.js +481 -0
- package/dist/modules/workflow/agent-tools/change-request-read-shape.js.map +1 -0
- package/dist/modules/workflow/agent-tools/change-request-read.tools.d.ts +126 -0
- package/dist/modules/workflow/agent-tools/change-request-read.tools.d.ts.map +1 -0
- package/dist/modules/workflow/agent-tools/change-request-read.tools.js +611 -0
- package/dist/modules/workflow/agent-tools/change-request-read.tools.js.map +1 -0
- package/dist/modules/workflow/agent-tools/change-request-summary.d.ts +12 -1
- package/dist/modules/workflow/agent-tools/change-request-summary.d.ts.map +1 -1
- package/dist/modules/workflow/agent-tools/change-request-summary.js +5 -1
- package/dist/modules/workflow/agent-tools/change-request-summary.js.map +1 -1
- package/dist/modules/workflow/agent-tools/workflow.tools.d.ts +8 -1
- package/dist/modules/workflow/agent-tools/workflow.tools.d.ts.map +1 -1
- package/dist/modules/workflow/agent-tools/workflow.tools.js +133 -37
- package/dist/modules/workflow/agent-tools/workflow.tools.js.map +1 -1
- package/dist/modules/workflow/file-lock.service.d.ts +29 -5
- package/dist/modules/workflow/file-lock.service.d.ts.map +1 -1
- package/dist/modules/workflow/file-lock.service.js +62 -5
- package/dist/modules/workflow/file-lock.service.js.map +1 -1
- package/dist/modules/workflow/git/git.service.d.ts +282 -14
- package/dist/modules/workflow/git/git.service.d.ts.map +1 -1
- package/dist/modules/workflow/git/git.service.js +627 -126
- package/dist/modules/workflow/git/git.service.js.map +1 -1
- package/dist/modules/workflow/git/merge-commit.d.ts +73 -0
- package/dist/modules/workflow/git/merge-commit.d.ts.map +1 -0
- package/dist/modules/workflow/git/merge-commit.js +89 -0
- package/dist/modules/workflow/git/merge-commit.js.map +1 -0
- package/dist/modules/workflow/git/node-git-runner.d.ts +15 -0
- package/dist/modules/workflow/git/node-git-runner.d.ts.map +1 -1
- package/dist/modules/workflow/git/node-git-runner.js +78 -6
- package/dist/modules/workflow/git/node-git-runner.js.map +1 -1
- package/dist/modules/workflow/git/pull-request.service.d.ts +94 -1
- package/dist/modules/workflow/git/pull-request.service.d.ts.map +1 -1
- package/dist/modules/workflow/git/pull-request.service.js +332 -37
- package/dist/modules/workflow/git/pull-request.service.js.map +1 -1
- package/dist/modules/workflow/pending-commits.service.d.ts +15 -0
- package/dist/modules/workflow/pending-commits.service.d.ts.map +1 -1
- package/dist/modules/workflow/pending-commits.service.js +25 -1
- package/dist/modules/workflow/pending-commits.service.js.map +1 -1
- package/dist/modules/workflow/pending-commits.worker.d.ts +6 -1
- package/dist/modules/workflow/pending-commits.worker.d.ts.map +1 -1
- package/dist/modules/workflow/pending-commits.worker.js +23 -6
- package/dist/modules/workflow/pending-commits.worker.js.map +1 -1
- package/dist/modules/workflow/recovery-bot.js +1 -1
- package/dist/modules/workflow/recovery-bot.js.map +1 -1
- package/dist/modules/workflow/review-workflow/review-workflow.service.d.ts +35 -0
- package/dist/modules/workflow/review-workflow/review-workflow.service.d.ts.map +1 -1
- package/dist/modules/workflow/review-workflow/review-workflow.service.js +180 -13
- package/dist/modules/workflow/review-workflow/review-workflow.service.js.map +1 -1
- package/dist/modules/workflow/sanitize-error.d.ts +16 -0
- package/dist/modules/workflow/sanitize-error.d.ts.map +1 -1
- package/dist/modules/workflow/sanitize-error.js +45 -0
- package/dist/modules/workflow/sanitize-error.js.map +1 -1
- package/dist/modules/workflow/workflow.routes.d.ts +6 -2
- package/dist/modules/workflow/workflow.routes.d.ts.map +1 -1
- package/dist/modules/workflow/workflow.routes.js +7 -2
- package/dist/modules/workflow/workflow.routes.js.map +1 -1
- package/dist/modules/workflow/workflow.service.d.ts +187 -15
- package/dist/modules/workflow/workflow.service.d.ts.map +1 -1
- package/dist/modules/workflow/workflow.service.js +808 -106
- package/dist/modules/workflow/workflow.service.js.map +1 -1
- package/dist/modules/workspace/agent-download.builder.d.ts +105 -0
- package/dist/modules/workspace/agent-download.builder.d.ts.map +1 -0
- package/dist/modules/workspace/agent-download.builder.js +238 -0
- package/dist/modules/workspace/agent-download.builder.js.map +1 -0
- package/dist/modules/workspace/agent-download.routes.d.ts +77 -0
- package/dist/modules/workspace/agent-download.routes.d.ts.map +1 -0
- package/dist/modules/workspace/agent-download.routes.js +156 -0
- package/dist/modules/workspace/agent-download.routes.js.map +1 -0
- package/dist/modules/workspace/agent-download.store.d.ts +225 -0
- package/dist/modules/workspace/agent-download.store.d.ts.map +1 -0
- package/dist/modules/workspace/agent-download.store.js +423 -0
- package/dist/modules/workspace/agent-download.store.js.map +1 -0
- package/dist/modules/workspace/agent-upload.store.d.ts +10 -1
- package/dist/modules/workspace/agent-upload.store.d.ts.map +1 -1
- package/dist/modules/workspace/agent-upload.store.js +16 -6
- package/dist/modules/workspace/agent-upload.store.js.map +1 -1
- package/dist/modules/workspace/file-readers/raw-mime.d.ts +6 -0
- package/dist/modules/workspace/file-readers/raw-mime.d.ts.map +1 -0
- package/dist/modules/workspace/file-readers/raw-mime.js +38 -0
- package/dist/modules/workspace/file-readers/raw-mime.js.map +1 -0
- package/dist/modules/workspace/file-readers/text-reader.d.ts +5 -5
- package/dist/modules/workspace/file-readers/text-reader.d.ts.map +1 -1
- package/dist/modules/workspace/file-readers/text-reader.js +5 -8
- package/dist/modules/workspace/file-readers/text-reader.js.map +1 -1
- package/dist/modules/workspace/first-run.d.ts +111 -0
- package/dist/modules/workspace/first-run.d.ts.map +1 -0
- package/dist/modules/workspace/first-run.js +181 -0
- package/dist/modules/workspace/first-run.js.map +1 -0
- package/dist/modules/workspace/move-links.d.ts +92 -0
- package/dist/modules/workspace/move-links.d.ts.map +1 -0
- package/dist/modules/workspace/move-links.js +173 -0
- package/dist/modules/workspace/move-links.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 +24 -39
- package/dist/modules/workspace/startup/steps/seed-tree.js.map +1 -1
- package/dist/modules/workspace/startup/steps/template-files.step.d.ts +58 -52
- package/dist/modules/workspace/startup/steps/template-files.step.d.ts.map +1 -1
- package/dist/modules/workspace/startup/steps/template-files.step.js +209 -223
- package/dist/modules/workspace/startup/steps/template-files.step.js.map +1 -1
- package/dist/modules/workspace/startup/steps/template-source.d.ts +5 -3
- package/dist/modules/workspace/startup/steps/template-source.d.ts.map +1 -1
- package/dist/modules/workspace/startup/steps/template-source.js +5 -3
- package/dist/modules/workspace/startup/steps/template-source.js.map +1 -1
- package/dist/modules/workspace/workspace.routes.d.ts.map +1 -1
- package/dist/modules/workspace/workspace.routes.js +63 -22
- package/dist/modules/workspace/workspace.routes.js.map +1 -1
- package/dist/modules/workspace/workspace.service.d.ts +75 -2
- package/dist/modules/workspace/workspace.service.d.ts.map +1 -1
- package/dist/modules/workspace/workspace.service.js +197 -24
- package/dist/modules/workspace/workspace.service.js.map +1 -1
- package/dist/modules/workspace/workspace.tools.d.ts +68 -1
- package/dist/modules/workspace/workspace.tools.d.ts.map +1 -1
- package/dist/modules/workspace/workspace.tools.js +642 -71
- package/dist/modules/workspace/workspace.tools.js.map +1 -1
- package/dist/modules/write-access/write-access.d.ts.map +1 -1
- package/dist/modules/write-access/write-access.js +9 -0
- package/dist/modules/write-access/write-access.js.map +1 -1
- package/dist/shared/domain-errors.d.ts +84 -1
- package/dist/shared/domain-errors.d.ts.map +1 -1
- package/dist/shared/domain-errors.js +100 -12
- package/dist/shared/domain-errors.js.map +1 -1
- package/dist/shared/git.contract.d.ts +10 -1
- package/dist/shared/git.contract.d.ts.map +1 -1
- package/dist/shared/git.contract.js.map +1 -1
- package/dist/shared/hidden-tools.d.ts +44 -0
- package/dist/shared/hidden-tools.d.ts.map +1 -0
- package/dist/shared/hidden-tools.js +13 -0
- package/dist/shared/hidden-tools.js.map +1 -0
- package/dist/shared/utf8-text.d.ts +15 -0
- package/dist/shared/utf8-text.d.ts.map +1 -0
- package/dist/shared/utf8-text.js +20 -0
- package/dist/shared/utf8-text.js.map +1 -0
- package/dist/tenancy/static-tenant-source.d.ts +2 -1
- package/dist/tenancy/static-tenant-source.d.ts.map +1 -1
- package/dist/tenancy/static-tenant-source.js +9 -2
- package/dist/tenancy/static-tenant-source.js.map +1 -1
- package/kb-template/.bevelignore +0 -5
- package/mcp-app/page.html +349 -0
- package/migrations/0017_atlassian_account_links.sql +37 -0
- package/migrations/meta/0017_snapshot.json +2396 -0
- package/migrations/meta/_journal.json +8 -1
- package/package.json +6 -3
- package/src/__tests__/kb-layout-config.test.ts +10 -100
- package/src/__tests__/packaged-assets-ship.test.ts +66 -0
- package/src/assets.ts +38 -2
- package/src/core/__tests__/catalog-revision.test.ts +40 -0
- package/src/core/__tests__/gated-boot.test.ts +1 -0
- package/src/core/__tests__/lifecycle.test.ts +14 -0
- package/src/core/catalog-revision.ts +15 -2
- package/src/core/core-ports.ts +11 -0
- package/src/core/create-core-server.ts +78 -3
- package/src/core/create-core-services.ts +168 -10
- package/src/core/lifecycle.ts +28 -2
- package/src/core-config.ts +24 -1
- package/src/index.ts +2 -2
- package/src/modules/access/__tests__/access-control.atref-batch.test.ts +58 -0
- package/src/modules/access/__tests__/access-control.platform-restore.test.ts +8 -7
- package/src/modules/access/__tests__/access-control.service.test.ts +19 -0
- package/src/modules/access/__tests__/access-personal-plugin.test.ts +1 -18
- package/src/modules/access/access-control.interface.ts +28 -0
- package/src/modules/access/access-control.service.ts +37 -0
- package/src/modules/access/directory-sync-bot.ts +1 -1
- package/src/modules/agent-guide/__tests__/agent-guide.test.ts +386 -0
- package/src/modules/agent-guide/__tests__/agent-guide.tools.test.ts +207 -0
- package/src/modules/agent-guide/agent-guide.tools.ts +122 -0
- package/src/modules/agent-guide/agent-guide.ts +300 -0
- package/src/modules/agent-guide/index.ts +22 -0
- package/src/modules/agent-instructions/__tests__/shared-file-rules.test.ts +39 -118
- package/src/modules/agent-instructions/agent-instructions.routes.ts +3 -2
- package/src/modules/agent-instructions/compose.ts +26 -8
- package/src/modules/agent-instructions/index.ts +0 -3
- package/src/modules/agent-instructions/shared-file-rules.ts +50 -112
- package/src/modules/audit/__tests__/agent-audit.service.test.ts +64 -4
- package/src/modules/audit/__tests__/agent-connection.routes.test.ts +102 -0
- package/src/modules/audit/agent-audit.service.ts +58 -3
- package/src/modules/audit/agent-connection.routes.ts +56 -0
- package/src/modules/audit/audit.contract.ts +26 -0
- package/src/modules/audit/index.ts +1 -0
- package/src/modules/auth/__tests__/account.routes.test.ts +5 -3
- package/src/modules/auth/account.routes.ts +10 -3
- package/src/modules/auth/auth.middleware.ts +1 -1
- package/src/modules/code-mode/__tests__/code-mode.tool.test.ts +18 -2
- package/src/modules/code-mode/code-mode.tool.ts +13 -2
- package/src/modules/database/core-schema.ts +33 -3
- package/src/modules/embed/__tests__/account-links.migration.test.ts +98 -0
- package/src/modules/embed/__tests__/embed-link.test.ts +122 -0
- package/src/modules/embed/__tests__/embed.routes.test.ts +362 -0
- package/src/modules/embed/__tests__/embed.service.lock.test.ts +193 -0
- package/src/modules/embed/__tests__/embed.service.test.ts +805 -0
- package/src/modules/embed/__tests__/embed.tools.test.ts +304 -0
- package/src/modules/embed/__tests__/mcp-app.test.ts +207 -0
- package/src/modules/embed/account-link.service.ts +76 -0
- package/src/modules/embed/embed-link.ts +180 -0
- package/src/modules/embed/embed.errors.ts +43 -0
- package/src/modules/embed/embed.interface.ts +203 -0
- package/src/modules/embed/embed.routes.ts +363 -0
- package/src/modules/embed/embed.service.ts +705 -0
- package/src/modules/embed/embed.tools.ts +206 -0
- package/src/modules/embed/mcp-app.routes.ts +41 -0
- package/src/modules/embed/mcp-app.ts +173 -0
- package/src/modules/github-app/__tests__/github-app.test.ts +76 -0
- package/src/modules/github-app/github-app.connection.ts +38 -4
- package/src/modules/kb-fs/__tests__/move-with-edits.test.ts +304 -0
- package/src/modules/kb-fs/locking-filesystem.ts +287 -0
- package/src/modules/kb-sync/__tests__/kb-sync.service.test.ts +35 -0
- package/src/modules/kb-sync/kb-sync.interface.ts +2 -0
- package/src/modules/kb-sync/kb-sync.service.ts +6 -0
- package/src/modules/mcp/__tests__/bevel-oauth-provider.test.ts +68 -11
- package/src/modules/mcp/__tests__/fake-downstream-mcp-server.ts +14 -3
- package/src/modules/mcp/__tests__/mcp-proxy-helpers.test.ts +37 -2
- package/src/modules/mcp/__tests__/mcp.app.e2e.test.ts +200 -0
- package/src/modules/mcp/__tests__/mcp.e2e.test.ts +252 -0
- package/src/modules/mcp/__tests__/mcp.service.test.ts +42 -24
- package/src/modules/mcp/__tests__/tool-call-interface.e2e.test.ts +499 -0
- package/src/modules/mcp/__tests__/tool-schema-guard.test.ts +266 -0
- package/src/modules/mcp/mcp-auth.middleware.ts +2 -1
- package/src/modules/mcp/mcp.service.ts +218 -31
- package/src/modules/mcp/oauth/bevel-oauth-provider.ts +42 -4
- package/src/modules/mcp/tool-schema-guard.ts +196 -0
- package/src/modules/onboarding/__tests__/onboarding.routes.test.ts +163 -0
- package/src/modules/onboarding/__tests__/starter-pack.service.test.ts +635 -0
- package/src/modules/onboarding/__tests__/starter-packs.test.ts +169 -0
- package/src/modules/onboarding/onboarding.contract.ts +85 -0
- package/src/modules/onboarding/onboarding.routes.ts +85 -0
- package/src/modules/onboarding/starter-pack.service.ts +580 -0
- package/src/modules/onboarding/starter-packs.ts +182 -0
- package/src/modules/plugins/__tests__/plugins.tools.test.ts +177 -4
- package/src/modules/plugins/index.ts +1 -0
- package/src/modules/plugins/plugin-provision.service.ts +22 -1
- package/src/modules/plugins/plugins.routes.ts +29 -0
- package/src/modules/plugins/plugins.tools.ts +75 -15
- package/src/modules/settings/__tests__/deployment-settings.service.test.ts +176 -56
- package/src/modules/settings/__tests__/repository-source.test.ts +9 -0
- package/src/modules/settings/deployment-settings.service.ts +144 -55
- package/src/modules/settings/repository-source.ts +4 -4
- package/src/modules/settings/setup.routes.ts +3 -6
- package/src/modules/skills/__tests__/skills.tools.description.test.ts +95 -0
- package/src/modules/skills/skills.tools.ts +112 -46
- package/src/modules/tool-auth/__tests__/connection-key-rejection.test.ts +32 -0
- package/src/modules/tool-auth/__tests__/external-api-key.service.test.ts +65 -0
- package/src/modules/tool-auth/connection-key-rejection.ts +13 -3
- package/src/modules/tool-auth/external-api-key.interface.ts +9 -0
- package/src/modules/tool-auth/external-api-key.service.ts +61 -4
- package/src/modules/tool-auth/tool-auth.middleware.ts +13 -4
- package/src/modules/tool-helpers/__tests__/branch-declaration.test.ts +502 -0
- package/src/modules/tool-helpers/__tests__/route-argument-check.test.ts +337 -0
- package/src/modules/tool-helpers/index.ts +34 -1
- package/src/modules/tool-helpers/route-argument-check.ts +149 -0
- package/src/modules/tool-helpers/route-tool-schemas.ts +171 -0
- package/src/modules/tool-helpers/tool-def.ts +132 -7
- package/src/modules/tool-helpers/tool-handler.ts +196 -8
- package/src/modules/tool-helpers/tool.contract.ts +9 -0
- package/src/modules/tool-manuals/__tests__/tool-manuals.detail.route.test.ts +57 -0
- package/src/modules/tool-manuals/__tests__/tool-manuals.tools.test.ts +73 -4
- package/src/modules/tool-manuals/tool-manuals.contract.ts +24 -4
- package/src/modules/tool-manuals/tool-manuals.service.ts +17 -0
- package/src/modules/tool-manuals/tool-manuals.tools.ts +86 -38
- package/src/modules/tool-registry/__tests__/own-tool-schemas.test.ts +173 -0
- package/src/modules/tool-registry/__tests__/tool-description-length.test.ts +81 -66
- package/src/modules/tool-registry/description-length.ts +57 -32
- package/src/modules/tool-registry/guide-first.ts +41 -0
- package/src/modules/tool-registry/tool-registry.ts +9 -2
- package/src/modules/workflow/__tests__/apply-failure.test.ts +6 -1
- package/src/modules/workflow/__tests__/file-lock.service.deletion-gate.test.ts +104 -0
- package/src/modules/workflow/__tests__/pending-commits.repository-replaced.test.ts +66 -0
- package/src/modules/workflow/__tests__/pending-commits.worker.test.ts +31 -2
- package/src/modules/workflow/__tests__/preserve-roles-yaml.test.ts +110 -6
- package/src/modules/workflow/__tests__/sanitize-error.test.ts +89 -1
- package/src/modules/workflow/__tests__/workflow.routes.push-refused.test.ts +202 -0
- package/src/modules/workflow/__tests__/workflow.service.branch-in-use.test.ts +6 -6
- package/src/modules/workflow/__tests__/workflow.service.delete-branch.test.ts +1026 -0
- package/src/modules/workflow/__tests__/workflow.service.facade.test.ts +468 -10
- package/src/modules/workflow/__tests__/workflow.service.folder-proposals.test.ts +4 -0
- package/src/modules/workflow/__tests__/workflow.service.releaseLock.test.ts +5 -4
- package/src/modules/workflow/__tests__/workflow.service.update-from-target.test.ts +54 -2
- package/src/modules/workflow/agent-tools/__tests__/change-request-read-shape.test.ts +705 -0
- package/src/modules/workflow/agent-tools/__tests__/change-request-read.tools.test.ts +1554 -0
- package/src/modules/workflow/agent-tools/__tests__/workflow.tools.test.ts +150 -4
- package/src/modules/workflow/agent-tools/change-request-read-shape.ts +712 -0
- package/src/modules/workflow/agent-tools/change-request-read.tools.ts +759 -0
- package/src/modules/workflow/agent-tools/change-request-summary.ts +5 -1
- package/src/modules/workflow/agent-tools/workflow.tools.ts +137 -35
- package/src/modules/workflow/file-lock.service.ts +67 -5
- package/src/modules/workflow/git/__tests__/git.service.appliedChange.test.ts +285 -0
- package/src/modules/workflow/git/__tests__/git.service.changedFilesForPr.test.ts +124 -0
- package/src/modules/workflow/git/__tests__/git.service.deleteBranch.test.ts +164 -0
- package/src/modules/workflow/git/__tests__/git.service.mergeChangeRequest.test.ts +334 -0
- package/src/modules/workflow/git/__tests__/git.service.pull.test.ts +48 -1
- package/src/modules/workflow/git/__tests__/node-git-runner.test.ts +231 -2
- package/src/modules/workflow/git/__tests__/pull-request.service.list-fetch.test.ts +72 -2
- package/src/modules/workflow/git/__tests__/pull-request.service.placeholder.test.ts +24 -2
- package/src/modules/workflow/git/__tests__/pull-request.service.test.ts +620 -1
- package/src/modules/workflow/git/git.service.ts +714 -133
- package/src/modules/workflow/git/merge-commit.ts +88 -0
- package/src/modules/workflow/git/node-git-runner.ts +82 -7
- package/src/modules/workflow/git/pull-request.service.ts +380 -54
- package/src/modules/workflow/pending-commits.service.ts +29 -1
- package/src/modules/workflow/pending-commits.worker.ts +24 -6
- package/src/modules/workflow/recovery-bot.ts +1 -1
- package/src/modules/workflow/review-workflow/__tests__/approval-states.test.ts +7 -1
- package/src/modules/workflow/review-workflow/__tests__/merge-records-own-commit.test.ts +407 -0
- package/src/modules/workflow/review-workflow/review-workflow.service.ts +191 -12
- package/src/modules/workflow/sanitize-error.ts +50 -0
- package/src/modules/workflow/workflow.routes.ts +7 -2
- package/src/modules/workflow/workflow.service.ts +886 -112
- package/src/modules/workspace/__tests__/agent-downloads.test.ts +823 -0
- package/src/modules/workspace/__tests__/escape-sequences.routes.test.ts +4 -3
- package/src/modules/workspace/__tests__/first-run.test.ts +234 -0
- package/src/modules/workspace/__tests__/git-internals.security.test.ts +5 -2
- package/src/modules/workspace/__tests__/md-links.test.ts +353 -0
- package/src/modules/workspace/__tests__/move-links.test.ts +261 -0
- package/src/modules/workspace/__tests__/rest-tool-arguments.test.ts +219 -0
- package/src/modules/workspace/__tests__/workspace-paths-inside-the-repo.test.ts +3 -0
- package/src/modules/workspace/__tests__/workspace.routes.delete.test.ts +102 -0
- package/src/modules/workspace/__tests__/workspace.routes.download.test.ts +52 -5
- package/src/modules/workspace/__tests__/workspace.routes.folder-zip-access.test.ts +190 -0
- package/src/modules/workspace/__tests__/workspace.routes.move-platform-files.test.ts +21 -10
- package/src/modules/workspace/__tests__/workspace.service.forget-clone-races.test.ts +8 -4
- package/src/modules/workspace/__tests__/workspace.service.test.ts +50 -8
- package/src/modules/workspace/__tests__/workspace.tools.agents-file.test.ts +33 -55
- package/src/modules/workspace/__tests__/workspace.tools.branch-errors.test.ts +44 -1
- package/src/modules/workspace/__tests__/workspace.tools.test.ts +664 -34
- package/src/modules/workspace/agent-download.builder.ts +308 -0
- package/src/modules/workspace/agent-download.routes.ts +183 -0
- package/src/modules/workspace/agent-download.store.ts +533 -0
- package/src/modules/workspace/agent-upload.store.ts +23 -7
- package/src/modules/workspace/file-readers/raw-mime.ts +40 -0
- package/src/modules/workspace/file-readers/text-reader.ts +5 -8
- package/src/modules/workspace/first-run.ts +222 -0
- package/src/modules/workspace/move-links.ts +237 -0
- package/src/modules/workspace/startup/steps/__tests__/steps.test.ts +191 -489
- package/src/modules/workspace/startup/steps/seed-tree.ts +23 -38
- package/src/modules/workspace/startup/steps/template-files.step.ts +217 -249
- package/src/modules/workspace/startup/steps/template-source.ts +5 -3
- package/src/modules/workspace/workspace.routes.ts +64 -22
- package/src/modules/workspace/workspace.service.ts +202 -25
- package/src/modules/workspace/workspace.tools.ts +707 -68
- package/src/modules/write-access/__tests__/write-access.test.ts +13 -0
- package/src/modules/write-access/write-access.ts +9 -0
- package/src/shared/domain-errors.ts +123 -11
- package/src/shared/git.contract.ts +8 -1
- package/src/shared/hidden-tools.ts +45 -0
- package/src/shared/utf8-text.ts +21 -0
- package/src/tenancy/__tests__/static-tenant-source.test.ts +1 -0
- package/src/tenancy/static-tenant-source.ts +10 -2
- package/starter-packs/README.md +103 -0
- package/starter-packs/engineering/KnowledgeBase/About us.md +9 -0
- package/starter-packs/engineering/KnowledgeBase/Architecture.md +9 -0
- package/starter-packs/engineering/KnowledgeBase/Glossary.md +9 -0
- package/starter-packs/engineering/KnowledgeBase/How we ship.md +9 -0
- package/starter-packs/engineering/Plugins/engineering-starter/LICENSE +21 -0
- package/starter-packs/engineering/Plugins/engineering-starter/SOURCE.md +20 -0
- package/starter-packs/engineering/Plugins/engineering-starter/access.md +19 -0
- package/starter-packs/engineering/Plugins/engineering-starter/plugin.json +10 -0
- package/starter-packs/engineering/Plugins/engineering-starter/skills/office-hours/SKILL.md +171 -0
- package/starter-packs/engineering/Plugins/engineering-starter/skills/office-hours/sections/phase-2a-startup-diagnostic.md +157 -0
- package/starter-packs/engineering/Plugins/engineering-starter/skills/office-hours/sections/phase-2b-builder-brainstorm.md +39 -0
- package/starter-packs/engineering/Plugins/engineering-starter/skills/plan-ceo-review/SKILL.md +144 -0
- package/starter-packs/engineering/Plugins/engineering-starter/skills/plan-eng-review/SKILL.md +144 -0
- package/starter-packs/engineering/Plugins/engineering-starter/skills/review/SKILL.md +80 -0
- package/starter-packs/engineering/Plugins/engineering-starter/skills/review/checklist.md +184 -0
- package/starter-packs/engineering/pack.yaml +11 -0
- package/starter-packs/general/KnowledgeBase/About us.md +9 -0
- package/starter-packs/general/KnowledgeBase/Glossary.md +9 -0
- package/starter-packs/general/KnowledgeBase/How we work.md +9 -0
- package/starter-packs/general/Plugins/general-starter/LICENSE +202 -0
- package/starter-packs/general/Plugins/general-starter/SOURCE.md +10 -0
- package/starter-packs/general/Plugins/general-starter/access.md +19 -0
- package/starter-packs/general/Plugins/general-starter/plugin.json +10 -0
- package/starter-packs/general/Plugins/general-starter/skills/memory-management/SKILL.md +332 -0
- package/starter-packs/general/Plugins/general-starter/skills/productivity-start/SKILL.md +163 -0
- package/starter-packs/general/Plugins/general-starter/skills/productivity-update/SKILL.md +167 -0
- package/starter-packs/general/Plugins/general-starter/skills/task-management/SKILL.md +83 -0
- package/starter-packs/general/pack.yaml +10 -0
- package/starter-packs/operations/KnowledgeBase/Glossary.md +9 -0
- package/starter-packs/operations/KnowledgeBase/How we work.md +9 -0
- package/starter-packs/operations/KnowledgeBase/Processes.md +9 -0
- package/starter-packs/operations/KnowledgeBase/Tools we use.md +9 -0
- package/starter-packs/operations/KnowledgeBase/Vendors.md +9 -0
- package/starter-packs/operations/Plugins/operations-starter/LICENSE +202 -0
- package/starter-packs/operations/Plugins/operations-starter/SOURCE.md +10 -0
- package/starter-packs/operations/Plugins/operations-starter/access.md +19 -0
- package/starter-packs/operations/Plugins/operations-starter/plugin.json +10 -0
- package/starter-packs/operations/Plugins/operations-starter/skills/capacity-plan/SKILL.md +111 -0
- package/starter-packs/operations/Plugins/operations-starter/skills/change-request/SKILL.md +119 -0
- package/starter-packs/operations/Plugins/operations-starter/skills/compliance-tracking/SKILL.md +44 -0
- package/starter-packs/operations/Plugins/operations-starter/skills/process-doc/SKILL.md +80 -0
- package/starter-packs/operations/Plugins/operations-starter/skills/process-optimization/SKILL.md +39 -0
- package/starter-packs/operations/Plugins/operations-starter/skills/risk-assessment/SKILL.md +40 -0
- package/starter-packs/operations/Plugins/operations-starter/skills/runbook/SKILL.md +85 -0
- package/starter-packs/operations/Plugins/operations-starter/skills/status-report/SKILL.md +75 -0
- package/starter-packs/operations/Plugins/operations-starter/skills/vendor-review/SKILL.md +102 -0
- package/starter-packs/operations/pack.yaml +12 -0
- package/starter-packs/sales/KnowledgeBase/About us.md +9 -0
- package/starter-packs/sales/KnowledgeBase/Customers.md +9 -0
- package/starter-packs/sales/KnowledgeBase/Glossary.md +9 -0
- package/starter-packs/sales/KnowledgeBase/How we sell.md +9 -0
- package/starter-packs/sales/KnowledgeBase/Objections.md +9 -0
- package/starter-packs/sales/KnowledgeBase/Pricing.md +9 -0
- package/starter-packs/sales/Plugins/sales-starter/LICENSE +202 -0
- package/starter-packs/sales/Plugins/sales-starter/SOURCE.md +10 -0
- package/starter-packs/sales/Plugins/sales-starter/access.md +19 -0
- package/starter-packs/sales/Plugins/sales-starter/plugin.json +10 -0
- package/starter-packs/sales/Plugins/sales-starter/skills/account-context/SKILL.md +94 -0
- package/starter-packs/sales/Plugins/sales-starter/skills/account-plan/SKILL.md +95 -0
- package/starter-packs/sales/Plugins/sales-starter/skills/account-research/SKILL.md +99 -0
- package/starter-packs/sales/Plugins/sales-starter/skills/account-tiering/SKILL.md +101 -0
- package/starter-packs/sales/Plugins/sales-starter/skills/call-prep/SKILL.md +111 -0
- package/starter-packs/sales/Plugins/sales-starter/skills/call-summary/SKILL.md +111 -0
- package/starter-packs/sales/Plugins/sales-starter/skills/close-plan/SKILL.md +106 -0
- package/starter-packs/sales/Plugins/sales-starter/skills/competitive-intelligence/SKILL.md +102 -0
- package/starter-packs/sales/Plugins/sales-starter/skills/create-an-asset/SKILL.md +105 -0
- package/starter-packs/sales/Plugins/sales-starter/skills/crm-hygiene-check/SKILL.md +103 -0
- package/starter-packs/sales/Plugins/sales-starter/skills/customer-health/SKILL.md +108 -0
- package/starter-packs/sales/Plugins/sales-starter/skills/customer-voice/SKILL.md +94 -0
- package/starter-packs/sales/Plugins/sales-starter/skills/daily-briefing/SKILL.md +61 -0
- package/starter-packs/sales/Plugins/sales-starter/skills/deal-advance-gap/SKILL.md +102 -0
- package/starter-packs/sales/Plugins/sales-starter/skills/deal-review/SKILL.md +105 -0
- package/starter-packs/sales/Plugins/sales-starter/skills/deal-signals/SKILL.md +96 -0
- package/starter-packs/sales/Plugins/sales-starter/skills/deal-slip-scenario/SKILL.md +104 -0
- package/starter-packs/sales/Plugins/sales-starter/skills/draft-outreach/SKILL.md +116 -0
- package/starter-packs/sales/Plugins/sales-starter/skills/end-of-day/SKILL.md +114 -0
- package/starter-packs/sales/Plugins/sales-starter/skills/expansion-whitespace/SKILL.md +97 -0
- package/starter-packs/sales/Plugins/sales-starter/skills/forecast/SKILL.md +100 -0
- package/starter-packs/sales/Plugins/sales-starter/skills/handle-objection/SKILL.md +93 -0
- package/starter-packs/sales/Plugins/sales-starter/skills/inbox-sweep/SKILL.md +114 -0
- package/starter-packs/sales/Plugins/sales-starter/skills/lead-triage/SKILL.md +106 -0
- package/starter-packs/sales/Plugins/sales-starter/skills/log-activity/SKILL.md +111 -0
- package/starter-packs/sales/Plugins/sales-starter/skills/pipeline-review/SKILL.md +97 -0
- package/starter-packs/sales/Plugins/sales-starter/skills/renewal-radar/SKILL.md +95 -0
- package/starter-packs/sales/Plugins/sales-starter/skills/rep-context/SKILL.md +90 -0
- package/starter-packs/sales/Plugins/sales-starter/skills/route-lead/SKILL.md +111 -0
- package/starter-packs/sales/Plugins/sales-starter/skills/sales-setup/SKILL.md +200 -0
- package/starter-packs/sales/Plugins/sales-starter/skills/schedule-meeting/SKILL.md +99 -0
- package/starter-packs/sales/Plugins/sales-starter/skills/stakeholder-map/SKILL.md +107 -0
- package/starter-packs/sales/Plugins/sales-starter/skills/team-pipeline/SKILL.md +105 -0
- package/starter-packs/sales/Plugins/sales-starter/skills/update-opportunity/SKILL.md +112 -0
- package/starter-packs/sales/Plugins/sales-starter/skills/weekly-wrap/SKILL.md +90 -0
- package/starter-packs/sales/Plugins/sales-starter/skills/win-loss-review/SKILL.md +88 -0
- package/starter-packs/sales/pack.yaml +13 -0
- package/starter-packs/support/KnowledgeBase/Escalations.md +9 -0
- package/starter-packs/support/KnowledgeBase/Glossary.md +9 -0
- package/starter-packs/support/KnowledgeBase/Known issues.md +9 -0
- package/starter-packs/support/KnowledgeBase/Products.md +9 -0
- package/starter-packs/support/KnowledgeBase/Tone of voice.md +9 -0
- package/starter-packs/support/Plugins/support-starter/LICENSE +202 -0
- package/starter-packs/support/Plugins/support-starter/SOURCE.md +10 -0
- package/starter-packs/support/Plugins/support-starter/access.md +19 -0
- package/starter-packs/support/Plugins/support-starter/plugin.json +10 -0
- package/starter-packs/support/Plugins/support-starter/skills/customer-escalation/SKILL.md +246 -0
- package/starter-packs/support/Plugins/support-starter/skills/customer-research/SKILL.md +250 -0
- package/starter-packs/support/Plugins/support-starter/skills/draft-response/SKILL.md +416 -0
- package/starter-packs/support/Plugins/support-starter/skills/kb-article/SKILL.md +350 -0
- package/starter-packs/support/Plugins/support-starter/skills/ticket-triage/SKILL.md +273 -0
- package/starter-packs/support/pack.yaml +12 -0
- package/kb-template/AGENTS.md +0 -730
|
@@ -25,6 +25,8 @@ const syncLog = logger('sync');
|
|
|
25
25
|
const lockLog = logger('lock');
|
|
26
26
|
const crLog = logger('cr');
|
|
27
27
|
const mergeLog = logger('merge');
|
|
28
|
+
/** Who the leftover cleanup deletes as. Its deletions skip the authorship check, and the log names them `system`. */
|
|
29
|
+
const SYSTEM_ACTOR = { id: 'system', email: 'system', name: 'system' };
|
|
28
30
|
import { promises as fs } from 'node:fs';
|
|
29
31
|
import { execFile } from 'node:child_process';
|
|
30
32
|
import { promisify } from 'node:util';
|
|
@@ -34,11 +36,12 @@ import { changeRequests } from '../database/schema.js';
|
|
|
34
36
|
import { redactSecret } from '../../shared/redact-secret.js';
|
|
35
37
|
import { workspaceIdForBranch, branchForWorkspaceId } from '../../shared/workspace-id.js';
|
|
36
38
|
import { canonicalFileIdentity } from '../../shared/canonical-file-identity.js';
|
|
37
|
-
import { sanitizeError } from './sanitize-error.js';
|
|
39
|
+
import { describeSyncFailure, sanitizeError } from './sanitize-error.js';
|
|
38
40
|
import { WorkflowHooks } from './workflow-hooks.js';
|
|
39
41
|
import { WorkspaceMutex } from '../kb-fs/mutex.js';
|
|
42
|
+
import { assertValidBranchName } from '../kb-fs/branch-name.js';
|
|
40
43
|
import { canonicalEmail, hashEmail } from '../../shared/email-identity.js';
|
|
41
|
-
import { ChangeRequestConflictsError, DuplicateChangeRequestError, OpenChangeRequestBlocksMergeError, RolesYamlPreservationError, PullRebaseConflictError, PushNeedsAgentResolutionError, RemoteBranchGoneError, WorkflowDomainError, WorkflowValidationError, } from '../../shared/domain-errors.js';
|
|
44
|
+
import { BranchAuthorshipError, BranchDeleteBlockedError, BranchNotFoundError, ChangeRequestConflictsError, DuplicateChangeRequestError, OpenChangeRequestBlocksMergeError, ProtectedBranchError, RolesYamlPreservationError, PullRebaseConflictError, PushNeedsAgentResolutionError, RemoteBranchGoneError, WorkflowDomainError, WorkflowValidationError, } from '../../shared/domain-errors.js';
|
|
42
45
|
import { RECOVERY_BOT_EMAIL, RECOVERY_BOT_NAME } from './recovery-bot.js';
|
|
43
46
|
import { AccessDeniedError } from '../access-model/access-errors.js';
|
|
44
47
|
import { printable } from '../../shared/printable.js';
|
|
@@ -391,9 +394,19 @@ export class WorkflowService {
|
|
|
391
394
|
// request has to be withdrawn or declined first. `systemCleanup` does
|
|
392
395
|
// its own open-request check under this same lock, and the legacy
|
|
393
396
|
// `onlyIfNoRemote` prune only fires when origin no longer has the ref.
|
|
397
|
+
// A request that proposes nothing any more is closed first, and then
|
|
398
|
+
// blocks nothing. Only for a caller the deletion would go ahead for: a
|
|
399
|
+
// refused deletion changes nothing, a closed request included — so one
|
|
400
|
+
// that fails after the close reopens what it closed.
|
|
401
|
+
let closed = [];
|
|
394
402
|
if (!opts?.systemCleanup && !opts?.onlyIfNoRemote) {
|
|
395
|
-
|
|
403
|
+
let open = await this.openChangeRequestOn(name);
|
|
404
|
+
if (open !== null && !this.kb.isProtectedBranch(name) && (await this.mayDelete(workspaceId, name, user))) {
|
|
405
|
+
closed = await this.closeEmptyRequestsOn(name, user.id, { workspaceId, fresh: false });
|
|
406
|
+
open = await this.openChangeRequestOn(name);
|
|
407
|
+
}
|
|
396
408
|
if (open !== null) {
|
|
409
|
+
await this.reopenAfterFailedDelete(closed, name, user.id);
|
|
397
410
|
// Two different situations, two different actors. A request FROM
|
|
398
411
|
// this branch is the deleter's own to withdraw; a request INTO it
|
|
399
412
|
// belongs to someone else, and telling the branch owner to
|
|
@@ -406,12 +419,24 @@ export class WorkflowService {
|
|
|
406
419
|
`after that the branch can be deleted.`);
|
|
407
420
|
}
|
|
408
421
|
}
|
|
409
|
-
|
|
422
|
+
try {
|
|
423
|
+
await this.deleteBranchUnlocked(workspaceId, name, user, opts);
|
|
424
|
+
}
|
|
425
|
+
catch (err) {
|
|
426
|
+
await this.reopenAfterFailedDelete(closed, name, user.id);
|
|
427
|
+
throw err;
|
|
428
|
+
}
|
|
410
429
|
});
|
|
411
430
|
}
|
|
412
431
|
/** The deletion itself — callers must hold the branch-lifecycle lock. */
|
|
413
432
|
async deleteBranchUnlocked(workspaceId, name, user, opts) {
|
|
414
|
-
await this.git.deleteBranch(workspaceId, name, user, opts);
|
|
433
|
+
const { lastCommit } = await this.git.deleteBranch(workspaceId, name, user, opts);
|
|
434
|
+
// One line per deletion, whoever asked: the commit is what a restore
|
|
435
|
+
// starts from, and for a deletion made in the app or by the cleanup this
|
|
436
|
+
// line is the only trace there is.
|
|
437
|
+
// By id, never by address: log lines carry no email (the release's own promise).
|
|
438
|
+
const actor = opts?.systemCleanup ? 'system' : user.id;
|
|
439
|
+
log.info(`branch deleted by ${actor}: "${name}" at ${lastCommit ?? '(no commit)'}`);
|
|
415
440
|
// Retire the deleted branch's own workspace clone, best-effort. A stale
|
|
416
441
|
// clone left on disk would be silently REUSED if the branch name is ever
|
|
417
442
|
// recreated (the workspace bootstrap short-circuits on an existing
|
|
@@ -425,6 +450,305 @@ export class WorkflowService {
|
|
|
425
450
|
catch (err) {
|
|
426
451
|
log.warn(`could not retire workspace clone of deleted branch "${name}":`, { err });
|
|
427
452
|
}
|
|
453
|
+
return { lastCommit };
|
|
454
|
+
}
|
|
455
|
+
/**
|
|
456
|
+
* Every open change request naming `branch`, at either end — the preview's
|
|
457
|
+
* list. `openChangeRequestOn` answers the first one, which is all a refusal
|
|
458
|
+
* needs.
|
|
459
|
+
*/
|
|
460
|
+
async openChangeRequestsOn(branch) {
|
|
461
|
+
const rows = await this.db
|
|
462
|
+
.select({ number: changeRequests.number, sourceBranch: changeRequests.sourceBranch })
|
|
463
|
+
.from(changeRequests)
|
|
464
|
+
.where(and(or(eq(changeRequests.sourceBranch, branch), eq(changeRequests.targetBranch, branch)), eq(changeRequests.state, 'open')));
|
|
465
|
+
return rows.map((row) => ({
|
|
466
|
+
number: row.number,
|
|
467
|
+
end: row.sourceBranch === branch ? 'source' : 'target',
|
|
468
|
+
}));
|
|
469
|
+
}
|
|
470
|
+
/**
|
|
471
|
+
* Whether open change request `summary` proposes nothing at this moment:
|
|
472
|
+
* its changes, recomputed now, are empty, and no save to its source branch
|
|
473
|
+
* is still landing (a save under a held lock or queued for the commit
|
|
474
|
+
* worker is on disk but not yet in the diff). Anything that cannot be
|
|
475
|
+
* determined is a no — a request is never closed on a guess.
|
|
476
|
+
*
|
|
477
|
+
* `workspaceId` is the clone to compute in (any clone will do: the diff is
|
|
478
|
+
* between origin's refs); absent, the source branch's own, as the app's
|
|
479
|
+
* lazy close always did. `fresh` says that clone was just fetched, so the
|
|
480
|
+
* refs are read as they are instead of fetched again per request.
|
|
481
|
+
*/
|
|
482
|
+
async proposesNothing(summary, opts = {}) {
|
|
483
|
+
try {
|
|
484
|
+
const wsId = opts.workspaceId ?? (await this.workspaceService.getOrCreateForBranch(summary.branch)).id;
|
|
485
|
+
const paths = await this.git.changedPathsForPr(wsId, summary.base, summary.branch, opts.fresh ? { fetch: false } : {});
|
|
486
|
+
if (paths.length > 0)
|
|
487
|
+
return false;
|
|
488
|
+
}
|
|
489
|
+
catch (err) {
|
|
490
|
+
crLog.warn(`empty-check for change request #${summary.number} failed — leaving it open:`, { err });
|
|
491
|
+
return false;
|
|
492
|
+
}
|
|
493
|
+
return !(await this.savesInFlight(workspaceIdForBranch(summary.branch)));
|
|
494
|
+
}
|
|
495
|
+
/**
|
|
496
|
+
* Close open change request `number` if it proposes nothing right now
|
|
497
|
+
* ({@link proposesNothing}), recorded as today's empty-close: closed state
|
|
498
|
+
* and closing time, guarded on `state = 'open'` so a concurrent merge or
|
|
499
|
+
* withdrawal wins. Writes the log line every such close owes. Does NOT
|
|
500
|
+
* retire the source branch — the caller decides that, and a caller holding
|
|
501
|
+
* that branch's lifecycle lock must not. Answers the request's target when
|
|
502
|
+
* this call closed it, else null.
|
|
503
|
+
*/
|
|
504
|
+
async closeIfEmpty(number, opts) {
|
|
505
|
+
const summary = await this.prs.getPr(number);
|
|
506
|
+
if (!summary || summary.state !== 'open')
|
|
507
|
+
return null;
|
|
508
|
+
if (!(await this.proposesNothing(summary, opts)))
|
|
509
|
+
return null;
|
|
510
|
+
const now = new Date();
|
|
511
|
+
const updated = await this.db
|
|
512
|
+
.update(changeRequests)
|
|
513
|
+
.set({ state: 'closed', closedAt: now, updatedAt: now })
|
|
514
|
+
.where(and(eq(changeRequests.number, number), eq(changeRequests.state, 'open')))
|
|
515
|
+
.returning({ id: changeRequests.id });
|
|
516
|
+
if (updated.length === 0)
|
|
517
|
+
return null;
|
|
518
|
+
this.prs.invalidateDetailCache(number);
|
|
519
|
+
this.events?.emit({ kind: 'change-request-rejected', number });
|
|
520
|
+
crLog.info(`closed change request #${number} ("${summary.branch}" into "${summary.base}") by ${opts.actor}: ` +
|
|
521
|
+
`it proposes nothing (${opts.reason})`);
|
|
522
|
+
return { base: summary.base };
|
|
523
|
+
}
|
|
524
|
+
/**
|
|
525
|
+
* The deletion's close-empty move: every open request from or into
|
|
526
|
+
* `branch` that proposes nothing is closed. Best effort per request — one
|
|
527
|
+
* that cannot be judged or closed stays open, and the open-request check
|
|
528
|
+
* after this refuses on it. Never retires a branch: the caller holds
|
|
529
|
+
* `branch`'s lifecycle lock, and the deletion itself removes it. Answers
|
|
530
|
+
* the numbers it closed, so a deletion that then fails can reopen them
|
|
531
|
+
* ({@link reopenAfterFailedDelete}).
|
|
532
|
+
*/
|
|
533
|
+
async closeEmptyRequestsOn(branch, actor, opts) {
|
|
534
|
+
const closed = [];
|
|
535
|
+
for (const cr of await this.openChangeRequestsOn(branch)) {
|
|
536
|
+
try {
|
|
537
|
+
if (await this.closeIfEmpty(cr.number, { ...opts, actor, reason: `empty when "${branch}" was deleted` })) {
|
|
538
|
+
closed.push(cr.number);
|
|
539
|
+
}
|
|
540
|
+
}
|
|
541
|
+
catch (err) {
|
|
542
|
+
crLog.warn(`could not close empty change request #${cr.number} before deleting "${branch}":`, { err });
|
|
543
|
+
}
|
|
544
|
+
}
|
|
545
|
+
return closed;
|
|
546
|
+
}
|
|
547
|
+
/**
|
|
548
|
+
* Undo {@link closeEmptyRequestsOn} for a deletion that failed after it: a
|
|
549
|
+
* refused deletion changes nothing, and the requests it closed are open
|
|
550
|
+
* again. Still under the branch's lifecycle lock, so nothing can have
|
|
551
|
+
* merged, withdrawn or declined them in between; guarded on `closed` all
|
|
552
|
+
* the same. Best effort: a request that cannot be reopened is logged.
|
|
553
|
+
*/
|
|
554
|
+
async reopenAfterFailedDelete(numbers, branch, actor) {
|
|
555
|
+
if (numbers.length === 0)
|
|
556
|
+
return;
|
|
557
|
+
try {
|
|
558
|
+
const reopened = await this.db
|
|
559
|
+
.update(changeRequests)
|
|
560
|
+
.set({ state: 'open', closedAt: null, updatedAt: new Date() })
|
|
561
|
+
.where(and(inArray(changeRequests.number, numbers), eq(changeRequests.state, 'closed')))
|
|
562
|
+
.returning({ number: changeRequests.number });
|
|
563
|
+
for (const { number } of reopened) {
|
|
564
|
+
this.prs.invalidateDetailCache(number);
|
|
565
|
+
crLog.info(`reopened change request #${number} for ${actor}: deleting "${branch}" failed after it was closed`);
|
|
566
|
+
// The close told every tab the request was gone; announce it back,
|
|
567
|
+
// as an open does, so none keeps leaving it out.
|
|
568
|
+
const summary = await this.prs.getPr(number).catch(() => null);
|
|
569
|
+
if (summary) {
|
|
570
|
+
this.events?.emit({
|
|
571
|
+
kind: 'change-request-opened',
|
|
572
|
+
number,
|
|
573
|
+
source: summary.branch,
|
|
574
|
+
target: summary.base,
|
|
575
|
+
authorIdHash: summary.authorId ?? null,
|
|
576
|
+
title: summary.title,
|
|
577
|
+
});
|
|
578
|
+
}
|
|
579
|
+
}
|
|
580
|
+
}
|
|
581
|
+
catch (err) {
|
|
582
|
+
crLog.warn(`could not reopen change requests ${numbers.map((n) => `#${n}`).join(', ')} after deleting "${branch}" failed:`, { err });
|
|
583
|
+
}
|
|
584
|
+
}
|
|
585
|
+
/** The author-or-Admin rule, as a yes/no; a rule that cannot be read is a no. */
|
|
586
|
+
async mayDelete(workspaceId, name, user) {
|
|
587
|
+
try {
|
|
588
|
+
return await this.git.mayDeleteBranch(workspaceId, name, user);
|
|
589
|
+
}
|
|
590
|
+
catch {
|
|
591
|
+
return false;
|
|
592
|
+
}
|
|
593
|
+
}
|
|
594
|
+
/**
|
|
595
|
+
* The refusal for an open request that still proposes something: names it,
|
|
596
|
+
* links to it, and says who can act — its author withdraws it, an Admin can
|
|
597
|
+
* decline it.
|
|
598
|
+
*/
|
|
599
|
+
openRequestRefusal(branch, cr, visible = true) {
|
|
600
|
+
// One the caller may not see is not named, linked or placed at an end:
|
|
601
|
+
// `get_change_request` answers it as not found, and so does this.
|
|
602
|
+
if (!visible) {
|
|
603
|
+
return new BranchDeleteBlockedError('open-change-request', branch, `An open change request you cannot see still proposes changes from or into "${branch}". ` +
|
|
604
|
+
`An Admin can decline it in the app; then the branch can be deleted.`);
|
|
605
|
+
}
|
|
606
|
+
return new BranchDeleteBlockedError('open-change-request', branch, `Change request #${cr.number} (${cr.url}) is open ${cr.end === 'source' ? 'from' : 'into'} "${branch}" and still ` +
|
|
607
|
+
`proposes changes. Its author can withdraw it, or an Admin can decline it, in the app; then the branch can be deleted.`, { number: cr.number, url: cr.url });
|
|
608
|
+
}
|
|
609
|
+
/**
|
|
610
|
+
* The default branch's workspace, freshly fetched — where an agent's
|
|
611
|
+
* deletion and the leftover cleanup both decide and act. Never the
|
|
612
|
+
* branch's own checkout (which git refuses to delete from), and always
|
|
613
|
+
* after a strict, forced fetch: the refs are about to authorize a
|
|
614
|
+
* deletion, and a fetch that failed proves nothing about them.
|
|
615
|
+
*/
|
|
616
|
+
async freshDefaultWorkspace() {
|
|
617
|
+
const ws = await this.workspaceService.getOrCreateForBranch(this.kb.defaultBranch);
|
|
618
|
+
await this.workspaceService.ensureRemotesFetched(ws.id, { strict: true, force: true });
|
|
619
|
+
return ws.id;
|
|
620
|
+
}
|
|
621
|
+
async deleteBranchChecked(user, name, opts = {}) {
|
|
622
|
+
assertValidBranchName(name);
|
|
623
|
+
// The caller's view of a request, as `get_change_request` gives it: one
|
|
624
|
+
// they may not see is never named, linked or judged in an answer. Absent,
|
|
625
|
+
// every request is visible. A verdict that fails is a no.
|
|
626
|
+
const maySee = async (number) => opts.maySee ? opts.maySee(number).catch(() => false) : true;
|
|
627
|
+
// Under the lock `deleteBranch`, `createBranch` and `openChangeRequest`
|
|
628
|
+
// take, so nothing this answer depends on — a request being opened, the
|
|
629
|
+
// branch being recreated — can change between the checks and the delete.
|
|
630
|
+
// A preview takes it too: it is short, and it then answers a settled state.
|
|
631
|
+
return this.branchLifecycle.run(`branch:${name}`, async () => {
|
|
632
|
+
const isProtected = this.kb.isProtectedBranch(name);
|
|
633
|
+
if (isProtected && !opts.dryRun)
|
|
634
|
+
throw new ProtectedBranchError(name, 'deleting a protected branch');
|
|
635
|
+
let wsId;
|
|
636
|
+
try {
|
|
637
|
+
wsId = await this.freshDefaultWorkspace();
|
|
638
|
+
}
|
|
639
|
+
catch (err) {
|
|
640
|
+
log.warn(`delete_branch "${name}": could not fetch the shared repository — deleting nothing:`, { err });
|
|
641
|
+
throw new BranchDeleteBlockedError('state-unconfirmed', name, `Could not reach the shared repository to confirm the current state of "${name}", so nothing was deleted. Try again shortly.`);
|
|
642
|
+
}
|
|
643
|
+
const state = await this.git.branchState(wsId, name, this.kb.defaultBranch);
|
|
644
|
+
if (!state.exists) {
|
|
645
|
+
if (!opts.dryRun)
|
|
646
|
+
throw new BranchNotFoundError(name);
|
|
647
|
+
return {
|
|
648
|
+
kind: 'preview',
|
|
649
|
+
branch: name,
|
|
650
|
+
exists: false,
|
|
651
|
+
canDelete: false,
|
|
652
|
+
refusals: [new BranchNotFoundError(name).message],
|
|
653
|
+
unmergedCommits: 0,
|
|
654
|
+
openChangeRequests: [],
|
|
655
|
+
lastCommit: null,
|
|
656
|
+
};
|
|
657
|
+
}
|
|
658
|
+
// Every refusal, in the order a deletion meets them. The preview lists
|
|
659
|
+
// them all; a deletion throws the first.
|
|
660
|
+
const refusals = [];
|
|
661
|
+
if (isProtected)
|
|
662
|
+
refusals.push(new ProtectedBranchError(name, 'deleting a protected branch'));
|
|
663
|
+
if (!(await this.git.mayDeleteBranch(wsId, name, user)))
|
|
664
|
+
refusals.push(new BranchAuthorshipError(name));
|
|
665
|
+
// Every open request at either end, each judged now: one that proposes
|
|
666
|
+
// nothing is closed by the deletion and blocks nothing; one that still
|
|
667
|
+
// proposes something — or whose changes cannot be determined — blocks.
|
|
668
|
+
// A request the caller may not see is not part of a preview at all —
|
|
669
|
+
// not listed, not judged, not weighed in `canDelete` — so the preview
|
|
670
|
+
// reads the same whether one exists, and whether it is empty. A
|
|
671
|
+
// deletion still closes it or is refused on it like any other, without
|
|
672
|
+
// naming it: the rules above are the deletion's, not the caller's view.
|
|
673
|
+
const open = [];
|
|
674
|
+
for (const cr of await this.openChangeRequestsOn(name)) {
|
|
675
|
+
const visible = await maySee(cr.number);
|
|
676
|
+
if (!visible && opts.dryRun)
|
|
677
|
+
continue;
|
|
678
|
+
const summary = await this.prs.getPr(cr.number).catch(() => null);
|
|
679
|
+
const empty = summary ? await this.proposesNothing(summary, { workspaceId: wsId, fresh: true }) : false;
|
|
680
|
+
open.push({
|
|
681
|
+
...cr,
|
|
682
|
+
url: summary?.url ?? `/change-requests/${cr.number}`,
|
|
683
|
+
proposesNothing: empty,
|
|
684
|
+
visible,
|
|
685
|
+
});
|
|
686
|
+
}
|
|
687
|
+
const blocking = open.find((cr) => !cr.proposesNothing);
|
|
688
|
+
if (blocking)
|
|
689
|
+
refusals.push(this.openRequestRefusal(name, blocking, blocking.visible));
|
|
690
|
+
// Not coverable by `discardUnmerged`: these are a person's saves on
|
|
691
|
+
// their way into the branch, not work already on it.
|
|
692
|
+
if (await this.savesInFlight(workspaceIdForBranch(name))) {
|
|
693
|
+
refusals.push(this.savesLandingRefusal(name));
|
|
694
|
+
}
|
|
695
|
+
if (state.unmergedCommits > 0 && !opts.discardUnmerged) {
|
|
696
|
+
const n = state.unmergedCommits;
|
|
697
|
+
refusals.push(new BranchDeleteBlockedError('unmerged-commits', name, `"${name}" holds ${n} commit${n === 1 ? '' : 's'} that ${n === 1 ? 'is' : 'are'} not on ${this.kb.defaultBranch}; ` +
|
|
698
|
+
`deleting it would lose ${n === 1 ? 'it' : 'them'}. Pass discardUnmerged: true to delete it anyway.`, { unmergedCommits: n }));
|
|
699
|
+
}
|
|
700
|
+
if (opts.dryRun) {
|
|
701
|
+
return {
|
|
702
|
+
kind: 'preview',
|
|
703
|
+
branch: name,
|
|
704
|
+
exists: true,
|
|
705
|
+
canDelete: refusals.length === 0,
|
|
706
|
+
refusals: refusals.map((r) => r.message),
|
|
707
|
+
unmergedCommits: state.unmergedCommits,
|
|
708
|
+
openChangeRequests: open.map(({ number, end, url, proposesNothing }) => ({ number, end, url, proposesNothing })),
|
|
709
|
+
lastCommit: state.lastCommit,
|
|
710
|
+
};
|
|
711
|
+
}
|
|
712
|
+
// A refused deletion changes nothing: no request is closed either.
|
|
713
|
+
if (refusals[0])
|
|
714
|
+
throw refusals[0];
|
|
715
|
+
// The saves check above is a snapshot; a save could take its file lock
|
|
716
|
+
// since. With no lock grantable on the branch, it is made again and
|
|
717
|
+
// nothing can start landing between it and the deletion.
|
|
718
|
+
const { lastCommit } = await this.fileLocks.whileNoneAcquired(name, async () => {
|
|
719
|
+
if (await this.savesInFlight(workspaceIdForBranch(name)))
|
|
720
|
+
throw this.savesLandingRefusal(name);
|
|
721
|
+
// Every open request proposes nothing: close them, each re-judged at
|
|
722
|
+
// the moment of closing — the last step before the deletion, so no
|
|
723
|
+
// later check can refuse after it. Nothing here takes this branch's
|
|
724
|
+
// lifecycle lock, so a request whose source IS this branch cannot make
|
|
725
|
+
// the deletion wait on itself. Should the deletion still fail (the
|
|
726
|
+
// leased push below), the requests closed here are reopened.
|
|
727
|
+
const closed = await this.closeEmptyRequestsOn(name, user.id, { workspaceId: wsId, fresh: true });
|
|
728
|
+
try {
|
|
729
|
+
const still = (await this.openChangeRequestsOn(name))[0];
|
|
730
|
+
if (still) {
|
|
731
|
+
const summary = await this.prs.getPr(still.number).catch(() => null);
|
|
732
|
+
throw this.openRequestRefusal(name, { ...still, url: summary?.url ?? `/change-requests/${still.number}` }, await maySee(still.number));
|
|
733
|
+
}
|
|
734
|
+
// Leased on the tip the checks above were made against, so a commit
|
|
735
|
+
// pushed since is refused by the host instead of deleted unseen.
|
|
736
|
+
return await this.deleteBranchUnlocked(wsId, name, user, {
|
|
737
|
+
...(state.lastCommit ? { expectTip: state.lastCommit } : {}),
|
|
738
|
+
});
|
|
739
|
+
}
|
|
740
|
+
catch (err) {
|
|
741
|
+
await this.reopenAfterFailedDelete(closed, name, user.id);
|
|
742
|
+
throw err;
|
|
743
|
+
}
|
|
744
|
+
});
|
|
745
|
+
return {
|
|
746
|
+
kind: 'deleted',
|
|
747
|
+
branch: name,
|
|
748
|
+
lastCommit: lastCommit ?? state.lastCommit ?? '',
|
|
749
|
+
discardedCommits: state.unmergedCommits,
|
|
750
|
+
};
|
|
751
|
+
});
|
|
428
752
|
}
|
|
429
753
|
// No `switchBranch` here on purpose. Under the per-branch workspace model,
|
|
430
754
|
// switching branches is a frontend navigation that triggers a workspace
|
|
@@ -440,7 +764,7 @@ export class WorkflowService {
|
|
|
440
764
|
// `discardWorkingChanges` removed: under save=share there's nothing dirty
|
|
441
765
|
// to discard.
|
|
442
766
|
shareCurrentBranch(workspaceId, user) {
|
|
443
|
-
return this.
|
|
767
|
+
return this.pushWithRecovery(workspaceId, branchForWorkspaceId(workspaceId), '(share)', user);
|
|
444
768
|
}
|
|
445
769
|
refreshRemotes(workspaceId) {
|
|
446
770
|
return this.git.fetch(workspaceId);
|
|
@@ -584,7 +908,7 @@ export class WorkflowService {
|
|
|
584
908
|
}
|
|
585
909
|
const message = sanitizeError(err);
|
|
586
910
|
syncLog.warn(`pull failed for branch "${branch}": ${message}`);
|
|
587
|
-
this.noteGitSyncFailed(gitId, branch, err);
|
|
911
|
+
this.noteGitSyncFailed(gitId, branch, err, undefined, 'pull');
|
|
588
912
|
return { branch, outcome: 'error', error: message };
|
|
589
913
|
}
|
|
590
914
|
// A clean pull is proof origin is reachable and this clone rebases onto
|
|
@@ -905,7 +1229,9 @@ export class WorkflowService {
|
|
|
905
1229
|
});
|
|
906
1230
|
}
|
|
907
1231
|
else {
|
|
908
|
-
lockLog.info(
|
|
1232
|
+
lockLog.info(
|
|
1233
|
+
// The holder by id only: log lines carry no names.
|
|
1234
|
+
`ACQUIRE ws=${workspaceId} branch=${branch} path=${targetPath} user=${user.id} → contended, held by ${result.lock.holderUserId}`);
|
|
909
1235
|
}
|
|
910
1236
|
return result;
|
|
911
1237
|
}
|
|
@@ -969,7 +1295,8 @@ export class WorkflowService {
|
|
|
969
1295
|
let recoveryError = null;
|
|
970
1296
|
if (looksLikeNonFastForward) {
|
|
971
1297
|
try {
|
|
972
|
-
|
|
1298
|
+
// Same as `pushWithRecovery`: keep a stranded merge a merge.
|
|
1299
|
+
await this.pullWorkspace(workspaceId, { preserveMerges: true });
|
|
973
1300
|
await this.git.push(workspaceId, user);
|
|
974
1301
|
recovered = true;
|
|
975
1302
|
this.noteGitSyncOk(workspaceId, branch);
|
|
@@ -1049,7 +1376,7 @@ export class WorkflowService {
|
|
|
1049
1376
|
lockLog.info(`RELEASE start ws=${workspaceId} branch=${branch} path=${targetPath} user=${user.id}`);
|
|
1050
1377
|
const lock = await this.fileLocks.get(workspaceId, branch, targetPath);
|
|
1051
1378
|
if (!lock || lock.holderUserId !== user.id) {
|
|
1052
|
-
lockLog.warn(`RELEASE refused ws=${workspaceId} branch=${branch} path=${targetPath} user=${user.id} → ${lock ? `held by ${lock.
|
|
1379
|
+
lockLog.warn(`RELEASE refused ws=${workspaceId} branch=${branch} path=${targetPath} user=${user.id} → ${lock ? `held by ${lock.holderUserId}` : 'no lock row'}`);
|
|
1053
1380
|
throw new WorkflowValidationError(`Cannot release lock on "${targetPath}": not held by you (or no longer exists).`, { kind: 'lock-not-held', branch, path: targetPath });
|
|
1054
1381
|
}
|
|
1055
1382
|
// Releasing a coordination hold must NEVER enqueue a commit: the hold was
|
|
@@ -1192,7 +1519,13 @@ export class WorkflowService {
|
|
|
1192
1519
|
* clip would make the banner disagree with them. Callers that already
|
|
1193
1520
|
* hold the sentence pass it; otherwise it is derived below.
|
|
1194
1521
|
*/
|
|
1195
|
-
explicitConflict
|
|
1522
|
+
explicitConflict,
|
|
1523
|
+
/**
|
|
1524
|
+
* Which side of the sync failed. A 403 or "Repository not found" on a
|
|
1525
|
+
* pull means the credentials cannot READ the repository, and telling the
|
|
1526
|
+
* operator to grant push access would send them to the wrong setting.
|
|
1527
|
+
*/
|
|
1528
|
+
operation = 'push') {
|
|
1196
1529
|
// A rebase conflict is a conflict whichever path ran into it. The remote
|
|
1197
1530
|
// sync names its files on purpose; the push-recovery paths (autosave, the
|
|
1198
1531
|
// lock release, the queued retries) raise the same banner with the raw
|
|
@@ -1213,48 +1546,34 @@ export class WorkflowService {
|
|
|
1213
1546
|
kind: 'git-sync-failed',
|
|
1214
1547
|
workspaceId: id,
|
|
1215
1548
|
branch,
|
|
1216
|
-
//
|
|
1217
|
-
//
|
|
1218
|
-
|
|
1549
|
+
// Never the git output itself — not even sanitised: stderr quotes the
|
|
1550
|
+
// host's own error text and URLs, and the banner is user-facing. A
|
|
1551
|
+
// fixed sentence says what kind of failure it was; the raw text is in
|
|
1552
|
+
// the server log, where the banner points whoever can act on it.
|
|
1553
|
+
reason: conflict ? conflict.message : describeSyncFailure(err, operation),
|
|
1219
1554
|
...(conflict ? { conflictedPaths: conflict.paths } : {}),
|
|
1220
1555
|
});
|
|
1221
1556
|
}
|
|
1222
|
-
/**
|
|
1223
|
-
* A bare push wrapped in the sync tracker: a failure raises the banner, a
|
|
1224
|
-
* success clears it. For the workflow paths that push directly — sharing a
|
|
1225
|
-
* branch, publishing a CR's source, update-from-base, the decline revert,
|
|
1226
|
-
* the roles.yaml restore — rather than through `pushWithRecovery`'s
|
|
1227
|
-
* cooperative ladder. Behaviour is otherwise unchanged: the error still
|
|
1228
|
-
* propagates to the caller exactly as the bare push's did. Without this,
|
|
1229
|
-
* those pushes could fail invisibly (no banner) and, worse, a successful
|
|
1230
|
-
* share could not CLEAR a banner an earlier save had raised.
|
|
1231
|
-
*
|
|
1232
|
-
* The branch is derived from the workspace id (they are the same string,
|
|
1233
|
-
* URL-encoding aside) so call sites cannot pass a mismatched pair.
|
|
1234
|
-
*/
|
|
1235
|
-
async trackedPush(workspaceId, user, opts) {
|
|
1236
|
-
const branch = branchForWorkspaceId(workspaceId);
|
|
1237
|
-
try {
|
|
1238
|
-
// Preserve the exact call shape of the bare pushes this replaces — an
|
|
1239
|
-
// explicit `undefined` third argument is a different call signature to
|
|
1240
|
-
// every spy that asserts on it.
|
|
1241
|
-
await (opts ? this.git.push(workspaceId, user, opts) : this.git.push(workspaceId, user));
|
|
1242
|
-
this.noteGitSyncOk(workspaceId, branch);
|
|
1243
|
-
}
|
|
1244
|
-
catch (err) {
|
|
1245
|
-
this.noteGitSyncFailed(workspaceId, branch, err);
|
|
1246
|
-
throw err;
|
|
1247
|
-
}
|
|
1248
|
-
}
|
|
1249
1557
|
/**
|
|
1250
1558
|
* Shared implementation of "push, and on non-fast-forward try a
|
|
1251
1559
|
* cooperative pull-rebase + retry, then hand off to the agent if that
|
|
1252
1560
|
* still fails." The full rationale lives at the call site in
|
|
1253
1561
|
* `releaseLock`; this method is the de-duplicated body.
|
|
1562
|
+
*
|
|
1563
|
+
* EVERY workflow push goes through here — saves, sharing a branch, opening
|
|
1564
|
+
* a request, update-from-target, the decline revert, the roles.yaml
|
|
1565
|
+
* restore — so a push the host refuses answers one way everywhere: the
|
|
1566
|
+
* commit stays local, the sync banner goes up, and the caller gets the
|
|
1567
|
+
* 409 saved-locally sentence instead of a raw git error (which a route
|
|
1568
|
+
* would turn into a 500). A later successful push of the branch carries
|
|
1569
|
+
* the commits and clears the banner.
|
|
1254
1570
|
*/
|
|
1255
1571
|
async pushWithRecovery(workspaceId, branch, targetPath, user, opts) {
|
|
1572
|
+
// No explicit `undefined` third argument when there are no options: the
|
|
1573
|
+
// call shape stays the bare `push(workspaceId, user)` every caller made.
|
|
1574
|
+
const push = () => opts ? this.git.push(workspaceId, user, opts) : this.git.push(workspaceId, user);
|
|
1256
1575
|
try {
|
|
1257
|
-
await
|
|
1576
|
+
await push();
|
|
1258
1577
|
this.noteGitSyncOk(workspaceId, branch);
|
|
1259
1578
|
}
|
|
1260
1579
|
catch (firstPushErr) {
|
|
@@ -1263,10 +1582,25 @@ export class WorkflowService {
|
|
|
1263
1582
|
let recovered = false;
|
|
1264
1583
|
let recoveryDetail = '(cooperative path not attempted)';
|
|
1265
1584
|
let recoveryError = null;
|
|
1585
|
+
// The retry push's own rejection, when the pull got that far: the
|
|
1586
|
+
// divergence is cured by then, so it — not the first rejection — says
|
|
1587
|
+
// whether the host is refusing.
|
|
1588
|
+
let retryDetail = null;
|
|
1266
1589
|
if (looksLikeNonFastForward) {
|
|
1267
1590
|
try {
|
|
1268
|
-
|
|
1269
|
-
|
|
1591
|
+
// `preserveMerges` on every push, not only the ones that make a
|
|
1592
|
+
// merge: a merge stranded by a refused open or update rides along
|
|
1593
|
+
// with whatever push of the branch comes next — a save, a share —
|
|
1594
|
+
// and a plain rebase would flatten it into cherry-picks. With no
|
|
1595
|
+
// merge among the local commits the two spellings replay the same.
|
|
1596
|
+
await this.pullWorkspace(workspaceId, { preserveMerges: true });
|
|
1597
|
+
try {
|
|
1598
|
+
await push();
|
|
1599
|
+
}
|
|
1600
|
+
catch (retryErr) {
|
|
1601
|
+
retryDetail = retryErr instanceof Error ? retryErr.message : String(retryErr);
|
|
1602
|
+
throw retryErr;
|
|
1603
|
+
}
|
|
1270
1604
|
recovered = true;
|
|
1271
1605
|
this.noteGitSyncOk(workspaceId, branch);
|
|
1272
1606
|
log.info(`non-fast-forward push recovered via pull --rebase for workspace=${workspaceId} branch=${branch} path=${targetPath}`);
|
|
@@ -1285,9 +1619,17 @@ export class WorkflowService {
|
|
|
1285
1619
|
// When recovery RAN, its error is the current state of the world (the
|
|
1286
1620
|
// first rejection may be a stale non-fast-forward the pull already
|
|
1287
1621
|
// cured); when it was skipped, the first error is all there is.
|
|
1288
|
-
|
|
1622
|
+
// A recovery that failed before its retry push failed in the PULL.
|
|
1623
|
+
const failedInPull = recoveryError !== null && retryDetail === null;
|
|
1624
|
+
this.noteGitSyncFailed(workspaceId, branch, recoveryError ?? firstPushErr, undefined, failedInPull ? 'pull' : 'push');
|
|
1289
1625
|
log.warn(`push failed for workspace=${workspaceId} user=${user.id}; throwing PushNeedsAgentResolutionError so the frontend can hand off to the agent:`, { detail: firstDetail });
|
|
1290
|
-
throw new PushNeedsAgentResolutionError(branch, targetPath, firstDetail, recoveryDetail
|
|
1626
|
+
throw new PushNeedsAgentResolutionError(branch, targetPath, firstDetail, recoveryDetail,
|
|
1627
|
+
// `rejected` above also matches the host's own `[remote rejected]`
|
|
1628
|
+
// (an outage, a hook): worth the cooperative try, but only a real
|
|
1629
|
+
// divergence is described as one.
|
|
1630
|
+
/non-fast-forward|fetch first|updates were rejected|! \[rejected\]/i.test(retryDetail ?? firstDetail)
|
|
1631
|
+
? 'diverged'
|
|
1632
|
+
: 'refused');
|
|
1291
1633
|
}
|
|
1292
1634
|
}
|
|
1293
1635
|
}
|
|
@@ -1428,6 +1770,9 @@ export class WorkflowService {
|
|
|
1428
1770
|
listChangeRequests(opts) {
|
|
1429
1771
|
return this.prs.listOpenPrs(opts);
|
|
1430
1772
|
}
|
|
1773
|
+
listChangeRequestsByState(states, opts) {
|
|
1774
|
+
return this.prs.listPrsByState(states, opts);
|
|
1775
|
+
}
|
|
1431
1776
|
listChangeRequestsAuthoredBy(emailOrLogin, opts) {
|
|
1432
1777
|
return this.prs.listPrsAuthoredBy(emailOrLogin, opts);
|
|
1433
1778
|
}
|
|
@@ -1524,10 +1869,21 @@ export class WorkflowService {
|
|
|
1524
1869
|
if (mergeOutcome.kind === 'conflicts') {
|
|
1525
1870
|
throw new ChangeRequestConflictsError(input.sourceBranch, input.targetBranch, mergeOutcome.paths);
|
|
1526
1871
|
}
|
|
1527
|
-
// Push the source
|
|
1528
|
-
//
|
|
1529
|
-
//
|
|
1530
|
-
|
|
1872
|
+
// Push the source *after* the auto-merge so the remote sees the merge
|
|
1873
|
+
// commit too. A push the host refuses does not undo the open: the merge
|
|
1874
|
+
// is committed locally, the request is a DB row, and the next push of
|
|
1875
|
+
// the branch carries the commits — so the row is still created below and
|
|
1876
|
+
// the refusal (409, the saved-locally sentence, the sync banner) is
|
|
1877
|
+
// answered once it exists.
|
|
1878
|
+
let pushRefused = null;
|
|
1879
|
+
try {
|
|
1880
|
+
await this.pushWithRecovery(workspaceId, branchForWorkspaceId(workspaceId), '(opening a change request)', user);
|
|
1881
|
+
}
|
|
1882
|
+
catch (err) {
|
|
1883
|
+
if (!(err instanceof PushNeedsAgentResolutionError))
|
|
1884
|
+
throw err;
|
|
1885
|
+
pushRefused = err;
|
|
1886
|
+
}
|
|
1531
1887
|
const workspacePath = await this.workspaceService.getWorkspacePath(workspaceId);
|
|
1532
1888
|
const cwd = path.join(workspacePath, this.kbDirName);
|
|
1533
1889
|
// Touched paths between target and source — basis for the
|
|
@@ -1589,6 +1945,21 @@ export class WorkflowService {
|
|
|
1589
1945
|
throw new WorkflowValidationError(`Failed to open change request: ${redactTokens(rawMsg, this.git.credentials?.token())}`, { kind: 'open-change-request-failed' });
|
|
1590
1946
|
}
|
|
1591
1947
|
this.prs.invalidateDetailCache(prNumber);
|
|
1948
|
+
const opened = {
|
|
1949
|
+
kind: 'change-request-opened',
|
|
1950
|
+
number: prNumber,
|
|
1951
|
+
source: input.sourceBranch,
|
|
1952
|
+
target: input.targetBranch,
|
|
1953
|
+
authorIdHash: hashEmail(user.email),
|
|
1954
|
+
title: input.title,
|
|
1955
|
+
};
|
|
1956
|
+
if (pushRefused) {
|
|
1957
|
+
// The request exists; its detail reads the published refs, which the
|
|
1958
|
+
// refused push never moved — answering the refusal is what the caller
|
|
1959
|
+
// needs, not a detail read that may fail on a branch the host lacks.
|
|
1960
|
+
this.events?.emit(opened);
|
|
1961
|
+
throw pushRefused;
|
|
1962
|
+
}
|
|
1592
1963
|
const detail = await this.prs.getPrDetail(prNumber, {
|
|
1593
1964
|
fresh: true,
|
|
1594
1965
|
workspaceId,
|
|
@@ -1597,14 +1968,7 @@ export class WorkflowService {
|
|
|
1597
1968
|
if (!detail) {
|
|
1598
1969
|
throw new WorkflowValidationError(`Created change request #${prNumber} but could not fetch its detail`);
|
|
1599
1970
|
}
|
|
1600
|
-
this.events?.emit(
|
|
1601
|
-
kind: 'change-request-opened',
|
|
1602
|
-
number: prNumber,
|
|
1603
|
-
source: input.sourceBranch,
|
|
1604
|
-
target: input.targetBranch,
|
|
1605
|
-
authorIdHash: hashEmail(user.email),
|
|
1606
|
-
title: input.title,
|
|
1607
|
-
});
|
|
1971
|
+
this.events?.emit(opened);
|
|
1608
1972
|
return detail;
|
|
1609
1973
|
});
|
|
1610
1974
|
}
|
|
@@ -1716,7 +2080,7 @@ export class WorkflowService {
|
|
|
1716
2080
|
// do. Asking git what is unpushed retries it instead.
|
|
1717
2081
|
//
|
|
1718
2082
|
// A failed probe falls back to the old gate rather than to "push
|
|
1719
|
-
// anyway":
|
|
2083
|
+
// anyway": a push on a clone with nothing to push is a wasted
|
|
1720
2084
|
// round trip on every single update.
|
|
1721
2085
|
const hasUnpushed = await this.git
|
|
1722
2086
|
.hasUnpushedCommits(workspaceId)
|
|
@@ -1725,7 +2089,10 @@ export class WorkflowService {
|
|
|
1725
2089
|
return !outcome.alreadyUpToDate;
|
|
1726
2090
|
});
|
|
1727
2091
|
if (hasUnpushed) {
|
|
1728
|
-
|
|
2092
|
+
// A refusal answers 409 with the saved-locally sentence and leaves the
|
|
2093
|
+
// merge applied in the clone; the probe above makes the next update
|
|
2094
|
+
// (or any save on the branch) push it.
|
|
2095
|
+
await this.pushWithRecovery(workspaceId, branchForWorkspaceId(workspaceId), `(update from ${detail.base})`, user);
|
|
1729
2096
|
}
|
|
1730
2097
|
// Where the branch ended up, as PUBLISHED — two rev-parses, not a second
|
|
1731
2098
|
// detail read. Everything below needs only the sha, and the detail that
|
|
@@ -1901,8 +2268,10 @@ export class WorkflowService {
|
|
|
1901
2268
|
// Best-effort freshen of the source checkout: the diff below reads origin
|
|
1902
2269
|
// refs, but the restore commits from the working tree — a stale tree
|
|
1903
2270
|
// would push non-fast-forward and fail loudly anyway; this just makes
|
|
1904
|
-
// that rare.
|
|
1905
|
-
|
|
2271
|
+
// that rare. `preserveMerges`: the branch may carry a merge a refused open
|
|
2272
|
+
// or update stranded locally, and a plain rebase would flatten it before
|
|
2273
|
+
// the revert's push publishes the branch.
|
|
2274
|
+
await this.pullWorkspace(ws.id, { preserveMerges: true }).catch(() => undefined);
|
|
1906
2275
|
// The verb acts on the request as it is NOW — never a cached file list.
|
|
1907
2276
|
const paths = await this.git.changedPathsForPr(ws.id, baseBranch, headBranch);
|
|
1908
2277
|
if (!paths.includes(repoRelPath)) {
|
|
@@ -1922,6 +2291,10 @@ export class WorkflowService {
|
|
|
1922
2291
|
// save must not race the restore between write and commit.
|
|
1923
2292
|
const held = [];
|
|
1924
2293
|
let revertError = null;
|
|
2294
|
+
// A push the host refuses leaves the revert committed on the branch
|
|
2295
|
+
// locally; it is answered (409, the saved-locally sentence) only after
|
|
2296
|
+
// the locks are released and the request's caches reflect the new head.
|
|
2297
|
+
let pushRefused = null;
|
|
1925
2298
|
try {
|
|
1926
2299
|
for (const p of reverted) {
|
|
1927
2300
|
const lockPath = `${this.kbDirName}/${p}`;
|
|
@@ -1937,7 +2310,14 @@ export class WorkflowService {
|
|
|
1937
2310
|
await this.git.restorePathFromRef(ws.id, mergeBase, p);
|
|
1938
2311
|
await this.git.commitFile(ws.id, user, p, `Revert ${p} (declined in change request #${number})`, true);
|
|
1939
2312
|
}
|
|
1940
|
-
|
|
2313
|
+
try {
|
|
2314
|
+
await this.pushWithRecovery(ws.id, headBranch, repoRelPath, user);
|
|
2315
|
+
}
|
|
2316
|
+
catch (err) {
|
|
2317
|
+
if (!(err instanceof PushNeedsAgentResolutionError))
|
|
2318
|
+
throw err;
|
|
2319
|
+
pushRefused = err;
|
|
2320
|
+
}
|
|
1941
2321
|
}
|
|
1942
2322
|
catch (err) {
|
|
1943
2323
|
revertError = { reason: err };
|
|
@@ -1953,9 +2333,15 @@ export class WorkflowService {
|
|
|
1953
2333
|
const failedRelease = released.find((r) => r.status === 'rejected');
|
|
1954
2334
|
if (failedRelease)
|
|
1955
2335
|
throw failedRelease.reason;
|
|
2336
|
+
this.prs.invalidateDetailCache(number);
|
|
2337
|
+
// A refused push moved nothing the request is judged on. A recorded
|
|
2338
|
+
// apply failure describes the published head, which is still where it
|
|
2339
|
+
// was, so it stays; and the file list reads the published refs, so
|
|
2340
|
+
// closing an "empty" request is decided once the push lands.
|
|
2341
|
+
if (pushRefused)
|
|
2342
|
+
throw pushRefused;
|
|
1956
2343
|
// The source head moved: the refusal described a revision that is gone.
|
|
1957
2344
|
await this.clearApplyFailure(number, { recordedBefore: headMovedAfter });
|
|
1958
|
-
this.prs.invalidateDetailCache(number);
|
|
1959
2345
|
const remaining = await this.git.changedPathsForPr(ws.id, baseBranch, headBranch);
|
|
1960
2346
|
if (remaining.length > 0) {
|
|
1961
2347
|
// Answered like the file list: the placeholder is not a remaining file.
|
|
@@ -2073,7 +2459,9 @@ export class WorkflowService {
|
|
|
2073
2459
|
continue;
|
|
2074
2460
|
const ws = await this.workspaceService.getOrCreateForBranch(summary.branch);
|
|
2075
2461
|
try {
|
|
2076
|
-
|
|
2462
|
+
// `preserveMerges`: a merge a refused open or update stranded on the
|
|
2463
|
+
// request's branch must survive until the branch's next push.
|
|
2464
|
+
await this.pullWorkspace(ws.id, { preserveMerges: true });
|
|
2077
2465
|
}
|
|
2078
2466
|
catch (err) {
|
|
2079
2467
|
log.warn(`pull failed for #${summary.number} before removing folder ${printable(folder)}: ${printable(sanitizeError(err))}`);
|
|
@@ -2336,37 +2724,10 @@ export class WorkflowService {
|
|
|
2336
2724
|
* Returns true when THIS call closed it.
|
|
2337
2725
|
*/
|
|
2338
2726
|
async closeEmptyChangeRequest(number, user) {
|
|
2339
|
-
const
|
|
2340
|
-
if (!
|
|
2341
|
-
return false;
|
|
2342
|
-
let paths;
|
|
2343
|
-
let wsId;
|
|
2344
|
-
try {
|
|
2345
|
-
const ws = await this.workspaceService.getOrCreateForBranch(summary.branch);
|
|
2346
|
-
wsId = ws.id;
|
|
2347
|
-
paths = await this.git.changedPathsForPr(ws.id, summary.base, summary.branch);
|
|
2348
|
-
}
|
|
2349
|
-
catch (err) {
|
|
2350
|
-
crLog.warn(`empty-check for change request #${number} failed — leaving it open:`, { err });
|
|
2727
|
+
const closed = await this.closeIfEmpty(number, { reason: 'found empty', actor: user.email });
|
|
2728
|
+
if (!closed)
|
|
2351
2729
|
return false;
|
|
2352
|
-
|
|
2353
|
-
if (paths.length > 0)
|
|
2354
|
-
return false;
|
|
2355
|
-
if (await this.savesInFlight(wsId))
|
|
2356
|
-
return false;
|
|
2357
|
-
// Guard on `state = 'open'` so a concurrent merge or withdraw wins the
|
|
2358
|
-
// race and this becomes a no-op.
|
|
2359
|
-
const now = new Date();
|
|
2360
|
-
const updated = await this.db
|
|
2361
|
-
.update(changeRequests)
|
|
2362
|
-
.set({ state: 'closed', closedAt: now, updatedAt: now })
|
|
2363
|
-
.where(and(eq(changeRequests.number, number), eq(changeRequests.state, 'open')))
|
|
2364
|
-
.returning({ id: changeRequests.id });
|
|
2365
|
-
if (updated.length === 0)
|
|
2366
|
-
return false;
|
|
2367
|
-
this.prs.invalidateDetailCache(number);
|
|
2368
|
-
this.events?.emit({ kind: 'change-request-rejected', number });
|
|
2369
|
-
await this.retireMergedSourceBranch(number, summary.base, user);
|
|
2730
|
+
await this.retireMergedSourceBranch(number, closed.base, user);
|
|
2370
2731
|
return true;
|
|
2371
2732
|
}
|
|
2372
2733
|
/**
|
|
@@ -2375,6 +2736,9 @@ export class WorkflowService {
|
|
|
2375
2736
|
* (their file is still only on disk). An answer that cannot be read is a
|
|
2376
2737
|
* yes — the caller is about to delete the checkout.
|
|
2377
2738
|
*/
|
|
2739
|
+
savesLandingRefusal(name) {
|
|
2740
|
+
return new BranchDeleteBlockedError('saves-landing', name, `"${name}" still has saves waiting to be committed or a file held for editing. Try again once they have landed.`);
|
|
2741
|
+
}
|
|
2378
2742
|
async savesInFlight(wsId) {
|
|
2379
2743
|
try {
|
|
2380
2744
|
return (await this.fileLocks.hasAnyActive(wsId)) || (await this.pendingCommits.hasAnyForWorkspace(wsId));
|
|
@@ -2608,10 +2972,245 @@ export class WorkflowService {
|
|
|
2608
2972
|
}
|
|
2609
2973
|
})
|
|
2610
2974
|
.catch((err) => crLog.warn('on-demand deleted-branch sweep failed:', { err }))
|
|
2975
|
+
// Every sweep is followed by the tidy-up, which never throws.
|
|
2976
|
+
.then(() => this.tidyAfterSweep())
|
|
2977
|
+
.then(() => undefined)
|
|
2611
2978
|
.finally(() => {
|
|
2612
2979
|
this.sweepKick = null;
|
|
2613
2980
|
});
|
|
2614
2981
|
}
|
|
2982
|
+
tidyRound = null;
|
|
2983
|
+
tidyStopped = false;
|
|
2984
|
+
/**
|
|
2985
|
+
* Stop the tidy-up when the graph stops: no round starts after this, and a
|
|
2986
|
+
* round under way stops at its next request or branch. Then
|
|
2987
|
+
* {@link drainTidy} waits for it, so it cannot fetch or delete once the
|
|
2988
|
+
* pool is ended or a replacement graph owns the same checkout.
|
|
2989
|
+
*/
|
|
2990
|
+
stopTidying() {
|
|
2991
|
+
this.tidyStopped = true;
|
|
2992
|
+
}
|
|
2993
|
+
/** Settles once no tidy-up (nor the sweep that leads into one) is running. */
|
|
2994
|
+
async drainTidy() {
|
|
2995
|
+
await Promise.allSettled([this.sweepKick, this.tidyRound]);
|
|
2996
|
+
}
|
|
2997
|
+
/**
|
|
2998
|
+
* What follows every sweep for deleted branches — at startup, on a remote
|
|
2999
|
+
* sync, after a merge and on demand: first the open requests that propose
|
|
3000
|
+
* nothing are closed and their source branch removed
|
|
3001
|
+
* ({@link closeEmptyOpenChangeRequests}), then the branches merged requests
|
|
3002
|
+
* left behind ({@link retireLeftoverMergedBranches}, behind its own
|
|
3003
|
+
* setting). In that order, because closing an empty request is what can
|
|
3004
|
+
* free a branch for the second. Never throws; coalesced.
|
|
3005
|
+
*/
|
|
3006
|
+
tidyAfterSweep() {
|
|
3007
|
+
if (this.tidyRound)
|
|
3008
|
+
return this.tidyRound;
|
|
3009
|
+
if (this.tidyStopped)
|
|
3010
|
+
return Promise.resolve({ closedEmpty: 0, removedLeftovers: 0 });
|
|
3011
|
+
this.tidyRound = (async () => {
|
|
3012
|
+
const closedEmpty = await this.closeEmptyOpenChangeRequests().catch((err) => {
|
|
3013
|
+
crLog.warn('background close of empty change requests failed:', { err });
|
|
3014
|
+
return 0;
|
|
3015
|
+
});
|
|
3016
|
+
const removedLeftovers = await this.retireLeftoverMergedBranches();
|
|
3017
|
+
return { closedEmpty, removedLeftovers };
|
|
3018
|
+
})().finally(() => {
|
|
3019
|
+
this.tidyRound = null;
|
|
3020
|
+
});
|
|
3021
|
+
return this.tidyRound;
|
|
3022
|
+
}
|
|
3023
|
+
/**
|
|
3024
|
+
* Close every open change request that proposes nothing, and remove its
|
|
3025
|
+
* source branch — what opening such a request's page in the app does
|
|
3026
|
+
* (`closeEmptyChangeRequest`), done without anyone opening it. Each is
|
|
3027
|
+
* judged from the default branch's workspace against origin's refs, after
|
|
3028
|
+
* ONE strict fetch, so no request's branch is cloned for the question; a
|
|
3029
|
+
* round whose fetch fails closes nothing. A request whose changes cannot be
|
|
3030
|
+
* determined, or with a save still landing on its branch, stays open.
|
|
3031
|
+
* Returns how many it closed.
|
|
3032
|
+
*
|
|
3033
|
+
* The source branch is removed under the same guards as any other
|
|
3034
|
+
* deletion: under its lifecycle lock, only with no request open from or
|
|
3035
|
+
* into it, with no save landing and no lock grantable on it at that moment,
|
|
3036
|
+
* and leased on the tip the request was judged empty at — a commit pushed
|
|
3037
|
+
* since is refused by the host instead of deleted unseen.
|
|
3038
|
+
*/
|
|
3039
|
+
async closeEmptyOpenChangeRequests() {
|
|
3040
|
+
const open = await this.db
|
|
3041
|
+
.select({ number: changeRequests.number, sourceBranch: changeRequests.sourceBranch })
|
|
3042
|
+
.from(changeRequests)
|
|
3043
|
+
.where(eq(changeRequests.state, 'open'));
|
|
3044
|
+
if (open.length === 0)
|
|
3045
|
+
return 0;
|
|
3046
|
+
let wsId;
|
|
3047
|
+
try {
|
|
3048
|
+
wsId = await this.freshDefaultWorkspace();
|
|
3049
|
+
}
|
|
3050
|
+
catch (err) {
|
|
3051
|
+
crLog.warn('background close of empty change requests skipped — could not fetch the shared repository:', { err });
|
|
3052
|
+
return 0;
|
|
3053
|
+
}
|
|
3054
|
+
let closed = 0;
|
|
3055
|
+
for (const { number, sourceBranch } of open) {
|
|
3056
|
+
if (this.tidyStopped)
|
|
3057
|
+
break;
|
|
3058
|
+
try {
|
|
3059
|
+
// Read before the request is judged, so the lease below covers
|
|
3060
|
+
// anything that reached the branch after the judgement.
|
|
3061
|
+
const tip = this.kb.isProtectedBranch(sourceBranch)
|
|
3062
|
+
? null
|
|
3063
|
+
: (await this.git.branchState(wsId, sourceBranch, this.kb.defaultBranch)).lastCommit;
|
|
3064
|
+
const done = await this.closeIfEmpty(number, {
|
|
3065
|
+
workspaceId: wsId,
|
|
3066
|
+
fresh: true,
|
|
3067
|
+
actor: 'system',
|
|
3068
|
+
reason: 'empty in the background sweep',
|
|
3069
|
+
});
|
|
3070
|
+
if (!done)
|
|
3071
|
+
continue;
|
|
3072
|
+
closed++;
|
|
3073
|
+
if (tip)
|
|
3074
|
+
await this.retireEmptySourceBranch(wsId, sourceBranch, tip);
|
|
3075
|
+
}
|
|
3076
|
+
catch (err) {
|
|
3077
|
+
crLog.warn(`background close of change request #${number} failed — leaving it open:`, { err });
|
|
3078
|
+
}
|
|
3079
|
+
}
|
|
3080
|
+
return closed;
|
|
3081
|
+
}
|
|
3082
|
+
/**
|
|
3083
|
+
* Remove the source branch of a request the background sweep just closed
|
|
3084
|
+
* as empty, from the default branch's freshly fetched workspace `wsId`.
|
|
3085
|
+
* Skips quietly when another request is open from or into it or a save is
|
|
3086
|
+
* landing on it; the delete is leased on `tip`. Never throws.
|
|
3087
|
+
*/
|
|
3088
|
+
async retireEmptySourceBranch(wsId, branch, tip) {
|
|
3089
|
+
try {
|
|
3090
|
+
await this.branchLifecycle.run(`branch:${branch}`, async () => {
|
|
3091
|
+
if ((await this.openChangeRequestOn(branch)) !== null)
|
|
3092
|
+
return;
|
|
3093
|
+
await this.fileLocks.whileNoneAcquired(branch, async () => {
|
|
3094
|
+
if (await this.savesInFlight(workspaceIdForBranch(branch))) {
|
|
3095
|
+
crLog.info(`kept "${branch}" after closing its empty change request: a save is still landing on it`);
|
|
3096
|
+
return;
|
|
3097
|
+
}
|
|
3098
|
+
await this.deleteBranchUnlocked(wsId, branch, SYSTEM_ACTOR, { systemCleanup: true, expectTip: tip });
|
|
3099
|
+
});
|
|
3100
|
+
});
|
|
3101
|
+
}
|
|
3102
|
+
catch (err) {
|
|
3103
|
+
crLog.warn(`could not remove "${branch}" after closing its empty change request (non-fatal):`, { err });
|
|
3104
|
+
}
|
|
3105
|
+
}
|
|
3106
|
+
/**
|
|
3107
|
+
* Whether the leftover cleanup below runs — the `retireMergedBranches`
|
|
3108
|
+
* deployment setting, read at every round so the Deployment page applies
|
|
3109
|
+
* without a restart. Set by the composition root; on by default.
|
|
3110
|
+
*/
|
|
3111
|
+
leftoverCleanupEnabled = () => true;
|
|
3112
|
+
leftoverRound = null;
|
|
3113
|
+
/**
|
|
3114
|
+
* Remove the branches merged change requests left behind: sources of a
|
|
3115
|
+
* merge from before the post-merge retirement existed (core-backend 0.5.0),
|
|
3116
|
+
* and ones whose retirement failed. What `retireMergedSourceBranch` does
|
|
3117
|
+
* after a merge, applied to every merge on record.
|
|
3118
|
+
*
|
|
3119
|
+
* A branch goes only when ALL hold: it is not protected; it was the source
|
|
3120
|
+
* of a merged request; it has no commit that is not on the default branch;
|
|
3121
|
+
* no request is open from or into it; its tip is contained in the merge
|
|
3122
|
+
* commit recorded for one of those requests — a branch recreated under the
|
|
3123
|
+
* same name after the merge is somebody's new draft, and a request with no
|
|
3124
|
+
* recorded merge commit proves nothing; and its checkout has no saves
|
|
3125
|
+
* landing and no file held. Each is decided and deleted under the branch's
|
|
3126
|
+
* lifecycle lock, like the post-merge retirement.
|
|
3127
|
+
*
|
|
3128
|
+
* Fails SAFE, like the deleted-branch sweep it runs beside: a round whose
|
|
3129
|
+
* fetch fails removes nothing, and a branch whose state cannot be read is
|
|
3130
|
+
* left. The switch covers this round only; the post-merge retirement is not
|
|
3131
|
+
* asked. Never throws. Coalesced, because boot, the remote sync and the
|
|
3132
|
+
* on-demand sweep can all start one. Returns how many it removed.
|
|
3133
|
+
*/
|
|
3134
|
+
retireLeftoverMergedBranches() {
|
|
3135
|
+
if (this.leftoverRound)
|
|
3136
|
+
return this.leftoverRound;
|
|
3137
|
+
this.leftoverRound = this.retireLeftoverMergedBranchesOnce()
|
|
3138
|
+
.catch((err) => {
|
|
3139
|
+
log.warn('leftover branch cleanup failed:', { err });
|
|
3140
|
+
return 0;
|
|
3141
|
+
})
|
|
3142
|
+
.finally(() => {
|
|
3143
|
+
this.leftoverRound = null;
|
|
3144
|
+
});
|
|
3145
|
+
return this.leftoverRound;
|
|
3146
|
+
}
|
|
3147
|
+
async retireLeftoverMergedBranchesOnce() {
|
|
3148
|
+
if (!this.leftoverCleanupEnabled())
|
|
3149
|
+
return 0;
|
|
3150
|
+
const merged = await this.db
|
|
3151
|
+
.select({ sourceBranch: changeRequests.sourceBranch, mergedSha: changeRequests.mergedSha })
|
|
3152
|
+
.from(changeRequests)
|
|
3153
|
+
.where(eq(changeRequests.state, 'merged'));
|
|
3154
|
+
const mergeCommits = new Map();
|
|
3155
|
+
for (const row of merged) {
|
|
3156
|
+
if (this.kb.isProtectedBranch(row.sourceBranch))
|
|
3157
|
+
continue;
|
|
3158
|
+
const shas = mergeCommits.get(row.sourceBranch) ?? [];
|
|
3159
|
+
if (row.mergedSha)
|
|
3160
|
+
shas.push(row.mergedSha);
|
|
3161
|
+
mergeCommits.set(row.sourceBranch, shas);
|
|
3162
|
+
}
|
|
3163
|
+
if (mergeCommits.size === 0)
|
|
3164
|
+
return 0;
|
|
3165
|
+
let wsId;
|
|
3166
|
+
try {
|
|
3167
|
+
wsId = await this.freshDefaultWorkspace();
|
|
3168
|
+
}
|
|
3169
|
+
catch (err) {
|
|
3170
|
+
log.warn('leftover branch cleanup skipped — could not fetch the shared repository:', { err });
|
|
3171
|
+
return 0;
|
|
3172
|
+
}
|
|
3173
|
+
let removed = 0;
|
|
3174
|
+
for (const [branch, shas] of mergeCommits) {
|
|
3175
|
+
if (this.tidyStopped)
|
|
3176
|
+
break;
|
|
3177
|
+
if (shas.length === 0)
|
|
3178
|
+
continue;
|
|
3179
|
+
try {
|
|
3180
|
+
const done = await this.branchLifecycle.run(`branch:${branch}`, async () => {
|
|
3181
|
+
const state = await this.git.branchState(wsId, branch, this.kb.defaultBranch);
|
|
3182
|
+
if (!state.exists || state.lastCommit === null || state.unmergedCommits > 0)
|
|
3183
|
+
return false;
|
|
3184
|
+
if ((await this.openChangeRequestOn(branch)) !== null)
|
|
3185
|
+
return false;
|
|
3186
|
+
let contained = false;
|
|
3187
|
+
for (const sha of shas) {
|
|
3188
|
+
if (await this.git.isAncestor(wsId, state.lastCommit, sha).catch(() => false)) {
|
|
3189
|
+
contained = true;
|
|
3190
|
+
break;
|
|
3191
|
+
}
|
|
3192
|
+
}
|
|
3193
|
+
if (!contained)
|
|
3194
|
+
return false;
|
|
3195
|
+
const tip = state.lastCommit;
|
|
3196
|
+
return this.fileLocks.whileNoneAcquired(branch, async () => {
|
|
3197
|
+
if (await this.savesInFlight(workspaceIdForBranch(branch)))
|
|
3198
|
+
return false;
|
|
3199
|
+
await this.deleteBranchUnlocked(wsId, branch, SYSTEM_ACTOR, { systemCleanup: true, expectTip: tip });
|
|
3200
|
+
return true;
|
|
3201
|
+
});
|
|
3202
|
+
});
|
|
3203
|
+
if (done)
|
|
3204
|
+
removed++;
|
|
3205
|
+
}
|
|
3206
|
+
catch (err) {
|
|
3207
|
+
log.warn(`leftover branch cleanup could not remove "${branch}":`, { err });
|
|
3208
|
+
}
|
|
3209
|
+
}
|
|
3210
|
+
if (removed > 0)
|
|
3211
|
+
log.info(`removed ${removed} branch${removed === 1 ? '' : 'es'} left over from merged change requests`);
|
|
3212
|
+
return removed;
|
|
3213
|
+
}
|
|
2615
3214
|
/**
|
|
2616
3215
|
* Reject (close-without-merging) a change request. Per PLAN.md spec, the
|
|
2617
3216
|
* authorized set is:
|
|
@@ -2741,6 +3340,9 @@ export class WorkflowService {
|
|
|
2741
3340
|
// AFTER the event: the applying UI is waiting on `change-request-merged`,
|
|
2742
3341
|
// and branch retirement is git IO it must never wait behind.
|
|
2743
3342
|
await this.retireMergedSourceBranch(number, baseBranch, user);
|
|
3343
|
+
// A merge can empty OTHER open requests into the same target; the sweep's
|
|
3344
|
+
// tidy-up closes those. Detached and coalesced, like every kick.
|
|
3345
|
+
this.kickDeletedBranchSweep();
|
|
2744
3346
|
return { kind: 'merged', result };
|
|
2745
3347
|
}
|
|
2746
3348
|
/**
|
|
@@ -2808,10 +3410,82 @@ export class WorkflowService {
|
|
|
2808
3410
|
this.events?.emit({ kind: 'fs-tree-changed', workspaceId: targetWorkspaceId, branch: targetBranch });
|
|
2809
3411
|
}
|
|
2810
3412
|
}
|
|
3413
|
+
/**
|
|
3414
|
+
* How long `mergeBranch` waits for the source's queued commits, and how
|
|
3415
|
+
* often it asks. The worker polls every half second and a commit takes a few
|
|
3416
|
+
* seconds; twenty seconds of waiting is an outage, not latency.
|
|
3417
|
+
*
|
|
3418
|
+
* Twenty, not thirty, because of who is waiting on THIS call: an agent over
|
|
3419
|
+
* `/api/mcp` reaches the tool through `@utcp/http`, whose request timeout is
|
|
3420
|
+
* a fixed 30 seconds (its call-template schema drops any `timeout` field).
|
|
3421
|
+
* The wait plus the merge after it (fetch, merge, push) must fit inside
|
|
3422
|
+
* that, or the agent hears "timeout of 30000ms exceeded" instead of the
|
|
3423
|
+
* `pending-commits` answer this wait exists to give. Mutable for tests only.
|
|
3424
|
+
*/
|
|
3425
|
+
mergeWaitForPendingCommits = { timeoutMs: 20_000, pollMs: 250 };
|
|
3426
|
+
/**
|
|
3427
|
+
* Wait until no commit is queued on `branch`, or the bound runs out. A
|
|
3428
|
+
* commit escalated to a person ends the wait at once: nothing will land it.
|
|
3429
|
+
*
|
|
3430
|
+
* Polls the queue rather than draining it: the worker's own per-workspace
|
|
3431
|
+
* serialization stays the only writer to the clone, and the row is deleted
|
|
3432
|
+
* only after its push, so "no row" means the remote holds the commit.
|
|
3433
|
+
*/
|
|
3434
|
+
async awaitPendingCommits(branch) {
|
|
3435
|
+
const { timeoutMs, pollMs } = this.mergeWaitForPendingCommits;
|
|
3436
|
+
const deadline = Date.now() + timeoutMs;
|
|
3437
|
+
for (;;) {
|
|
3438
|
+
const { queued, stuck, needsAttention } = await this.pendingCommits.queuedOnBranch(branch);
|
|
3439
|
+
if (needsAttention !== null)
|
|
3440
|
+
return { ok: false, pending: queued + stuck, needsAttention };
|
|
3441
|
+
if (queued === 0)
|
|
3442
|
+
return { ok: true };
|
|
3443
|
+
if (Date.now() >= deadline)
|
|
3444
|
+
return { ok: false, pending: queued };
|
|
3445
|
+
await new Promise((resolve) => setTimeout(resolve, Math.min(pollMs, Math.max(0, deadline - Date.now()))));
|
|
3446
|
+
}
|
|
3447
|
+
}
|
|
3448
|
+
/** The one question `mergeBranch` refuses on: is a request from `source` into `target` open? */
|
|
3449
|
+
async openRequestBlockingMerge(sourceBranch, targetBranch) {
|
|
3450
|
+
const [open] = await this.db
|
|
3451
|
+
.select({ number: changeRequests.number })
|
|
3452
|
+
.from(changeRequests)
|
|
3453
|
+
.where(and(eq(changeRequests.sourceBranch, sourceBranch), eq(changeRequests.targetBranch, targetBranch), eq(changeRequests.state, 'open')))
|
|
3454
|
+
.limit(1);
|
|
3455
|
+
return open ? open.number : null;
|
|
3456
|
+
}
|
|
2811
3457
|
async mergeBranch(user, sourceBranch, targetBranch) {
|
|
2812
3458
|
if (sourceBranch === targetBranch) {
|
|
2813
3459
|
throw new WorkflowValidationError(`\`source\` and \`target\` are both "${sourceBranch}" — name two different branches.`);
|
|
2814
3460
|
}
|
|
3461
|
+
// Asked once BEFORE waiting, so a merge the open request forbids is refused
|
|
3462
|
+
// at once rather than after the source's commits have landed. Not the
|
|
3463
|
+
// answer that counts — that one is asked again under the locks below.
|
|
3464
|
+
const openBefore = await this.openRequestBlockingMerge(sourceBranch, targetBranch);
|
|
3465
|
+
if (openBefore !== null)
|
|
3466
|
+
throw new OpenChangeRequestBlocksMergeError(sourceBranch, targetBranch, openBefore);
|
|
3467
|
+
// Writes are committed asynchronously: a write tool answers once the bytes
|
|
3468
|
+
// are on disk and the pending-commits worker commits and pushes them a few
|
|
3469
|
+
// seconds later. The merge below merges what the REMOTE holds of the
|
|
3470
|
+
// source, so issued in those seconds it would find nothing new and land
|
|
3471
|
+
// nothing. Wait for them first — OUTSIDE the lifecycle locks, which guard
|
|
3472
|
+
// creating, deleting and opening requests on these branches, and must not
|
|
3473
|
+
// be held for seconds of waiting on a queue.
|
|
3474
|
+
const settled = await this.awaitPendingCommits(sourceBranch);
|
|
3475
|
+
if (!settled.ok) {
|
|
3476
|
+
const message = settled.needsAttention
|
|
3477
|
+
? `A commit queued on "${sourceBranch}" failed and needs a person: ${settled.needsAttention} ` +
|
|
3478
|
+
`Nothing was merged. Tell the user; merge again once it has been resolved.`
|
|
3479
|
+
: `${settled.pending} write(s) on "${sourceBranch}" are still being committed. ` +
|
|
3480
|
+
`Nothing was merged. Retry the merge shortly.`;
|
|
3481
|
+
return {
|
|
3482
|
+
kind: 'pending-commits',
|
|
3483
|
+
branch: sourceBranch,
|
|
3484
|
+
pending: settled.pending,
|
|
3485
|
+
...(settled.needsAttention ? { needsAttention: settled.needsAttention } : {}),
|
|
3486
|
+
message,
|
|
3487
|
+
};
|
|
3488
|
+
}
|
|
2815
3489
|
// Under the lifecycle lock of BOTH branches, the same protocol
|
|
2816
3490
|
// `openChangeRequest` follows — and for the same window. That method holds
|
|
2817
3491
|
// these keys across its whole check-then-insert (an auto-merge and a push,
|
|
@@ -2825,13 +3499,9 @@ export class WorkflowService {
|
|
|
2825
3499
|
// A person merges a change request. Only the request's OWN direction is
|
|
2826
3500
|
// blocked: the target merged into the source is how a draft under review
|
|
2827
3501
|
// stays current, and that must keep working while the request is open.
|
|
2828
|
-
const
|
|
2829
|
-
|
|
2830
|
-
|
|
2831
|
-
.where(and(eq(changeRequests.sourceBranch, sourceBranch), eq(changeRequests.targetBranch, targetBranch), eq(changeRequests.state, 'open')))
|
|
2832
|
-
.limit(1);
|
|
2833
|
-
if (open)
|
|
2834
|
-
throw new OpenChangeRequestBlocksMergeError(sourceBranch, targetBranch, open.number);
|
|
3502
|
+
const open = await this.openRequestBlockingMerge(sourceBranch, targetBranch);
|
|
3503
|
+
if (open !== null)
|
|
3504
|
+
throw new OpenChangeRequestBlocksMergeError(sourceBranch, targetBranch, open);
|
|
2835
3505
|
// The merge runs in the target's own workspace, like a change request's.
|
|
2836
3506
|
const targetWorkspaceId = (await this.workspaceService.getOrCreateForBranch(targetBranch)).id;
|
|
2837
3507
|
if (!(await this.git.remoteBranchExists(targetWorkspaceId, sourceBranch))) {
|
|
@@ -2891,6 +3561,13 @@ export class WorkflowService {
|
|
|
2891
3561
|
return { kind: 'conflicts-need-resolution', conflictedPaths: result.paths };
|
|
2892
3562
|
}
|
|
2893
3563
|
await this.pullMergeTarget(targetBranch, user);
|
|
3564
|
+
// No merge commit: the target already held everything on the source, and
|
|
3565
|
+
// `sha` is its unchanged tip. Reported as such — a `merged` that applied
|
|
3566
|
+
// nothing is what sent agents on as if their write had landed.
|
|
3567
|
+
if (result.mergeCommit === null)
|
|
3568
|
+
return { kind: 'nothing-to-merge', sha: result.sha };
|
|
3569
|
+
// What landed on the target may have emptied open requests into it.
|
|
3570
|
+
this.kickDeletedBranchSweep();
|
|
2894
3571
|
return { kind: 'merged', sha: result.sha };
|
|
2895
3572
|
});
|
|
2896
3573
|
}
|
|
@@ -3140,8 +3817,9 @@ export class WorkflowService {
|
|
|
3140
3817
|
// sweep those unrelated edits into our "preserve roles.yaml" commit under the
|
|
3141
3818
|
// wrong author/message. Feature branch → the protected-branch gate doesn't
|
|
3142
3819
|
// fire, so this commits cleanly. Then publish so the merge sees the
|
|
3143
|
-
// neutralised source. A
|
|
3144
|
-
// throws
|
|
3820
|
+
// neutralised source. A push that still fails after the cooperative
|
|
3821
|
+
// pull-rebase throws PushNeedsAgentResolutionError → the merge aborts,
|
|
3822
|
+
// fail-closed, with the restore kept locally for the next push.
|
|
3145
3823
|
const committed = await this.git.commitFile(ws.id, user, ROLES_YAML, 'Preserve official roles.yaml (not editable via change request)', true);
|
|
3146
3824
|
// We are past the `baseRoles === headRoles` check, so origin/<head> and
|
|
3147
3825
|
// origin/<base> DO differ — a restore commit is REQUIRED to protect the
|
|
@@ -3150,10 +3828,27 @@ export class WorkflowService {
|
|
|
3150
3828
|
// with origin/<head>), then origin/<head> STILL carries the divergent
|
|
3151
3829
|
// roles.yaml and the merge would land it. Treat that as a hard failure —
|
|
3152
3830
|
// returning false here would fail OPEN. Fail closed instead.
|
|
3153
|
-
|
|
3831
|
+
//
|
|
3832
|
+
// One shape of "nothing to commit" is not out of sync: a restore an
|
|
3833
|
+
// earlier merge attempt committed, whose push the host refused. The
|
|
3834
|
+
// clone's HEAD already carries the base version — that is why there
|
|
3835
|
+
// was nothing new to commit — and only the push is owed. Without this,
|
|
3836
|
+
// the retry the refusal promises could never succeed: every attempt
|
|
3837
|
+
// would find the file already restored and stop here, and the merge
|
|
3838
|
+
// would stay refused until some unrelated save pushed the branch.
|
|
3839
|
+
//
|
|
3840
|
+
// The question is NOT "does the clone hold unpushed commits" — any
|
|
3841
|
+
// queued save answers yes. It is whether HEAD already holds all of
|
|
3842
|
+
// origin/<head>, so that the push fast-forwards origin to a head whose
|
|
3843
|
+
// roles.yaml is the base version. A clone BEHIND origin would have
|
|
3844
|
+
// origin's divergent copy rebased into HEAD by the cooperative pull on
|
|
3845
|
+
// the way to the push, and the merge would land it: that clone is the
|
|
3846
|
+
// out-of-sync one this guard refuses. A probe that cannot answer
|
|
3847
|
+
// throws, and the guard fails closed with it.
|
|
3848
|
+
if (!committed && !(await this.git.headContainsOrigin(ws.id))) {
|
|
3154
3849
|
throw new Error('roles.yaml restore produced no commit while origin still diverges from base — source workspace out of sync; refusing to merge');
|
|
3155
3850
|
}
|
|
3156
|
-
await this.
|
|
3851
|
+
await this.pushWithRecovery(ws.id, headBranch, ROLES_YAML, user);
|
|
3157
3852
|
this.accessControl.invalidate(ws.id);
|
|
3158
3853
|
return true;
|
|
3159
3854
|
}
|
|
@@ -3164,6 +3859,13 @@ export class WorkflowService {
|
|
|
3164
3859
|
}
|
|
3165
3860
|
}
|
|
3166
3861
|
catch (err) {
|
|
3862
|
+
// A refused push: the restore is committed on the source locally and the
|
|
3863
|
+
// merge does not run (origin still carries the divergent roles.yaml).
|
|
3864
|
+
// Answer it like every other refused push — 409, the saved-locally
|
|
3865
|
+
// sentence, the banner already raised — so the merge can be retried
|
|
3866
|
+
// once the host accepts the push.
|
|
3867
|
+
if (err instanceof PushNeedsAgentResolutionError)
|
|
3868
|
+
throw err;
|
|
3167
3869
|
const detail = err instanceof Error ? err.message : String(err);
|
|
3168
3870
|
// Log the raw git/push detail server-side; the thrown error keeps it OFF
|
|
3169
3871
|
// the client-facing 502 message (see RolesYamlPreservationError).
|