@bevel-software/platform-core-backend 0.22.0 → 0.23.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/dist/core/core-ports.d.ts +11 -1
- package/dist/core/core-ports.d.ts.map +1 -1
- package/dist/core/core-ports.js.map +1 -1
- package/dist/core/create-core-server.d.ts +3 -3
- package/dist/core/create-core-server.d.ts.map +1 -1
- package/dist/core/create-core-server.js +30 -11
- package/dist/core/create-core-server.js.map +1 -1
- package/dist/core/create-core-services.d.ts +9 -2
- package/dist/core/create-core-services.d.ts.map +1 -1
- package/dist/core/create-core-services.js +112 -23
- package/dist/core/create-core-services.js.map +1 -1
- package/dist/core/lifecycle.d.ts +34 -1
- package/dist/core/lifecycle.d.ts.map +1 -1
- package/dist/core/lifecycle.js +89 -13
- package/dist/core/lifecycle.js.map +1 -1
- package/dist/core-config.d.ts +0 -12
- package/dist/core-config.d.ts.map +1 -1
- package/dist/core-config.js +11 -13
- package/dist/core-config.js.map +1 -1
- package/dist/index.d.ts +3 -2
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +2 -1
- package/dist/index.js.map +1 -1
- package/dist/modules/access/access-control.interface.d.ts +42 -12
- package/dist/modules/access/access-control.interface.d.ts.map +1 -1
- package/dist/modules/access/access-control.service.d.ts +67 -10
- package/dist/modules/access/access-control.service.d.ts.map +1 -1
- package/dist/modules/access/access-control.service.js +214 -35
- package/dist/modules/access/access-control.service.js.map +1 -1
- package/dist/modules/access/access.routes.d.ts.map +1 -1
- package/dist/modules/access/access.routes.js +13 -15
- package/dist/modules/access/access.routes.js.map +1 -1
- package/dist/modules/auth/account-admission.d.ts +58 -7
- package/dist/modules/auth/account-admission.d.ts.map +1 -1
- package/dist/modules/auth/account-admission.js +44 -1
- package/dist/modules/auth/account-admission.js.map +1 -1
- package/dist/modules/auth/account.routes.d.ts +1 -1
- package/dist/modules/auth/account.routes.d.ts.map +1 -1
- package/dist/modules/auth/account.routes.js +61 -5
- package/dist/modules/auth/account.routes.js.map +1 -1
- package/dist/modules/auth/auth.middleware.d.ts +1 -1
- package/dist/modules/auth/auth.middleware.d.ts.map +1 -1
- package/dist/modules/auth/auth.middleware.js +18 -7
- package/dist/modules/auth/auth.middleware.js.map +1 -1
- package/dist/modules/auth/auth.routes.d.ts.map +1 -1
- package/dist/modules/auth/auth.routes.js +8 -0
- package/dist/modules/auth/auth.routes.js.map +1 -1
- package/dist/modules/auth/auth.service.d.ts +70 -2
- package/dist/modules/auth/auth.service.d.ts.map +1 -1
- package/dist/modules/auth/auth.service.js +176 -7
- package/dist/modules/auth/auth.service.js.map +1 -1
- package/dist/modules/auth/oidc-auth-provider.d.ts.map +1 -1
- package/dist/modules/auth/oidc-auth-provider.js +7 -2
- package/dist/modules/auth/oidc-auth-provider.js.map +1 -1
- package/dist/modules/database/core-schema.d.ts +41 -24
- package/dist/modules/database/core-schema.d.ts.map +1 -1
- package/dist/modules/database/core-schema.js +35 -27
- package/dist/modules/database/core-schema.js.map +1 -1
- package/dist/modules/kb-fs/branch-name.d.ts.map +1 -1
- package/dist/modules/kb-fs/branch-name.js +12 -2
- package/dist/modules/kb-fs/branch-name.js.map +1 -1
- package/dist/modules/kb-fs/repo-path.d.ts +11 -16
- package/dist/modules/kb-fs/repo-path.d.ts.map +1 -1
- package/dist/modules/kb-fs/repo-path.js +79 -0
- package/dist/modules/kb-fs/repo-path.js.map +1 -1
- package/dist/modules/kb-sync/kb-sync.routes.d.ts +2 -2
- package/dist/modules/kb-sync/kb-sync.routes.d.ts.map +1 -1
- package/dist/modules/kb-sync/kb-sync.routes.js +14 -3
- package/dist/modules/kb-sync/kb-sync.routes.js.map +1 -1
- package/dist/modules/kb-sync/sync-auth.d.ts +3 -1
- package/dist/modules/kb-sync/sync-auth.d.ts.map +1 -1
- package/dist/modules/kb-sync/sync-auth.js +1 -1
- package/dist/modules/kb-sync/sync-auth.js.map +1 -1
- package/dist/modules/mcp/mcp-auth.middleware.d.ts.map +1 -1
- package/dist/modules/mcp/mcp-auth.middleware.js +21 -6
- package/dist/modules/mcp/mcp-auth.middleware.js.map +1 -1
- package/dist/modules/mcp/oauth/bevel-oauth-provider.d.ts.map +1 -1
- package/dist/modules/mcp/oauth/bevel-oauth-provider.js +3 -2
- package/dist/modules/mcp/oauth/bevel-oauth-provider.js.map +1 -1
- package/dist/modules/settings/deployment-settings.service.d.ts.map +1 -1
- package/dist/modules/settings/deployment-settings.service.js +8 -3
- package/dist/modules/settings/deployment-settings.service.js.map +1 -1
- package/dist/modules/settings/setup.routes.d.ts +52 -1
- package/dist/modules/settings/setup.routes.d.ts.map +1 -1
- package/dist/modules/settings/setup.routes.js +207 -19
- package/dist/modules/settings/setup.routes.js.map +1 -1
- package/dist/modules/skills/skills.contract.d.ts +90 -18
- package/dist/modules/skills/skills.contract.d.ts.map +1 -1
- package/dist/modules/skills/skills.contract.js +4 -1
- package/dist/modules/skills/skills.contract.js.map +1 -1
- package/dist/modules/skills/skills.service.d.ts +98 -9
- package/dist/modules/skills/skills.service.d.ts.map +1 -1
- package/dist/modules/skills/skills.service.js +239 -37
- package/dist/modules/skills/skills.service.js.map +1 -1
- package/dist/modules/skills/skills.tools.d.ts +7 -0
- package/dist/modules/skills/skills.tools.d.ts.map +1 -1
- package/dist/modules/skills/skills.tools.js +71 -7
- package/dist/modules/skills/skills.tools.js.map +1 -1
- package/dist/modules/tool-auth/external-api-key.service.d.ts.map +1 -1
- package/dist/modules/tool-auth/external-api-key.service.js +4 -2
- package/dist/modules/tool-auth/external-api-key.service.js.map +1 -1
- package/dist/modules/tool-auth/internal-token.service.d.ts +3 -3
- package/dist/modules/tool-auth/tool-auth.middleware.d.ts +16 -7
- package/dist/modules/tool-auth/tool-auth.middleware.d.ts.map +1 -1
- package/dist/modules/tool-auth/tool-auth.middleware.js +34 -12
- package/dist/modules/tool-auth/tool-auth.middleware.js.map +1 -1
- package/dist/modules/tool-helpers/tool-handler.d.ts +2 -1
- package/dist/modules/tool-helpers/tool-handler.d.ts.map +1 -1
- package/dist/modules/tool-helpers/tool-handler.js +25 -4
- package/dist/modules/tool-helpers/tool-handler.js.map +1 -1
- package/dist/modules/tool-helpers/tool.contract.d.ts +3 -2
- 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-helpers/validate-token.js +1 -1
- package/dist/modules/tool-helpers/validate-token.js.map +1 -1
- package/dist/modules/workflow/agent-tools/change-request-summary.d.ts +98 -0
- package/dist/modules/workflow/agent-tools/change-request-summary.d.ts.map +1 -0
- package/dist/modules/workflow/agent-tools/change-request-summary.js +81 -0
- package/dist/modules/workflow/agent-tools/change-request-summary.js.map +1 -0
- package/dist/modules/workflow/agent-tools/workflow.tools.d.ts.map +1 -1
- package/dist/modules/workflow/agent-tools/workflow.tools.js +75 -35
- package/dist/modules/workflow/agent-tools/workflow.tools.js.map +1 -1
- package/dist/modules/workflow/file-lock.service.d.ts +24 -0
- package/dist/modules/workflow/file-lock.service.d.ts.map +1 -1
- package/dist/modules/workflow/file-lock.service.js +30 -0
- package/dist/modules/workflow/file-lock.service.js.map +1 -1
- package/dist/modules/workflow/git/git.service.d.ts +69 -0
- package/dist/modules/workflow/git/git.service.d.ts.map +1 -1
- package/dist/modules/workflow/git/git.service.js +153 -2
- package/dist/modules/workflow/git/git.service.js.map +1 -1
- package/dist/modules/workflow/pending-commits.service.d.ts +38 -0
- package/dist/modules/workflow/pending-commits.service.d.ts.map +1 -1
- package/dist/modules/workflow/pending-commits.service.js +55 -0
- package/dist/modules/workflow/pending-commits.service.js.map +1 -1
- package/dist/modules/workflow/workflow-hooks.d.ts +54 -32
- package/dist/modules/workflow/workflow-hooks.d.ts.map +1 -1
- package/dist/modules/workflow/workflow-hooks.js +16 -1
- package/dist/modules/workflow/workflow-hooks.js.map +1 -1
- package/dist/modules/workflow/workflow.service.d.ts +60 -6
- package/dist/modules/workflow/workflow.service.d.ts.map +1 -1
- package/dist/modules/workflow/workflow.service.js +111 -4
- package/dist/modules/workflow/workflow.service.js.map +1 -1
- package/dist/modules/workspace/agent-access.gate.d.ts +94 -0
- package/dist/modules/workspace/agent-access.gate.d.ts.map +1 -0
- package/dist/modules/workspace/agent-access.gate.js +123 -0
- package/dist/modules/workspace/agent-access.gate.js.map +1 -0
- package/dist/modules/workspace/routine-write-policy.d.ts +5 -6
- package/dist/modules/workspace/routine-write-policy.d.ts.map +1 -1
- package/dist/modules/workspace/routine-write-policy.js +5 -6
- package/dist/modules/workspace/routine-write-policy.js.map +1 -1
- package/dist/modules/workspace/session-sink.d.ts +5 -5
- package/dist/modules/workspace/set-aside-clone.d.ts +46 -0
- package/dist/modules/workspace/set-aside-clone.d.ts.map +1 -0
- package/dist/modules/workspace/set-aside-clone.js +92 -0
- package/dist/modules/workspace/set-aside-clone.js.map +1 -0
- package/dist/modules/workspace/startup/kb-startup-runner.d.ts +59 -9
- package/dist/modules/workspace/startup/kb-startup-runner.d.ts.map +1 -1
- package/dist/modules/workspace/startup/kb-startup-runner.js +65 -24
- package/dist/modules/workspace/startup/kb-startup-runner.js.map +1 -1
- package/dist/modules/workspace/workspace.routes.d.ts.map +1 -1
- package/dist/modules/workspace/workspace.routes.js +92 -3
- package/dist/modules/workspace/workspace.routes.js.map +1 -1
- package/dist/modules/workspace/workspace.service.d.ts +115 -5
- package/dist/modules/workspace/workspace.service.d.ts.map +1 -1
- package/dist/modules/workspace/workspace.service.js +255 -25
- package/dist/modules/workspace/workspace.service.js.map +1 -1
- package/dist/modules/workspace/workspace.tools.d.ts +2 -2
- package/dist/modules/workspace/workspace.tools.d.ts.map +1 -1
- package/dist/modules/workspace/workspace.tools.js +309 -115
- package/dist/modules/workspace/workspace.tools.js.map +1 -1
- package/dist/modules/write-access/write-access.d.ts +60 -0
- package/dist/modules/write-access/write-access.d.ts.map +1 -0
- package/dist/modules/write-access/write-access.js +129 -0
- package/dist/modules/write-access/write-access.js.map +1 -0
- package/dist/shared/domain-errors.d.ts +25 -0
- package/dist/shared/domain-errors.d.ts.map +1 -1
- package/dist/shared/domain-errors.js +28 -0
- package/dist/shared/domain-errors.js.map +1 -1
- package/dist/shared/git.contract.d.ts +20 -0
- package/dist/shared/git.contract.d.ts.map +1 -1
- package/dist/shared/git.contract.js +26 -0
- package/dist/shared/git.contract.js.map +1 -1
- package/dist/tenancy/static-tenant-source.d.ts +0 -1
- package/dist/tenancy/static-tenant-source.d.ts.map +1 -1
- package/dist/tenancy/static-tenant-source.js +0 -2
- package/dist/tenancy/static-tenant-source.js.map +1 -1
- package/migrations/0014_change_request_closed_reason.sql +1 -0
- package/migrations/0015_account_deactivation.sql +3 -0
- package/migrations/meta/0014_snapshot.json +2265 -0
- package/migrations/meta/0015_snapshot.json +2271 -0
- package/migrations/meta/_journal.json +14 -0
- package/package.json +3 -3
- package/src/__tests__/kb-layout-config.test.ts +0 -2
- package/src/__tests__/retired-settings.test.ts +96 -0
- package/src/core/__tests__/gated-boot.test.ts +132 -0
- package/src/core/__tests__/lifecycle.test.ts +170 -1
- package/src/core/__tests__/set-aside-root-is-one-place.test.ts +63 -0
- package/src/core/core-ports.ts +11 -1
- package/src/core/create-core-server.ts +32 -14
- package/src/core/create-core-services.ts +129 -23
- package/src/core/lifecycle.ts +98 -13
- package/src/core-config.ts +13 -14
- package/src/index.ts +12 -1
- package/src/modules/access/__tests__/access-control.preview-relocation.test.ts +385 -0
- package/src/modules/access/__tests__/access-control.prospective.test.ts +94 -18
- package/src/modules/access/__tests__/access.routes.prospective.test.ts +6 -7
- package/src/modules/access/access-control.interface.ts +43 -12
- package/src/modules/access/access-control.service.ts +234 -36
- package/src/modules/access/access.routes.ts +13 -15
- package/src/modules/agent-instructions/__tests__/agent-instructions.route.test.ts +3 -1
- package/src/modules/auth/__tests__/account-deactivation.test.ts +252 -0
- package/src/modules/auth/__tests__/account.routes.test.ts +81 -3
- package/src/modules/auth/__tests__/auth.middleware.test.ts +45 -22
- package/src/modules/auth/__tests__/auth.service.test.ts +1 -1
- package/src/modules/auth/account-admission.ts +78 -9
- package/src/modules/auth/account.routes.ts +63 -7
- package/src/modules/auth/auth.middleware.ts +19 -8
- package/src/modules/auth/auth.routes.ts +8 -0
- package/src/modules/auth/auth.service.ts +179 -6
- package/src/modules/auth/oidc-auth-provider.ts +7 -2
- package/src/modules/database/core-schema.ts +35 -27
- package/src/modules/kb-fs/__tests__/branch-name.test.ts +10 -0
- package/src/modules/kb-fs/__tests__/repo-path.test.ts +123 -0
- package/src/modules/kb-fs/branch-name.ts +14 -1
- package/src/modules/kb-fs/repo-path.ts +85 -0
- package/src/modules/kb-sync/__tests__/kb-sync.routes.test.ts +26 -1
- package/src/modules/kb-sync/kb-sync.routes.ts +14 -4
- package/src/modules/kb-sync/sync-auth.ts +2 -2
- package/src/modules/mcp/__tests__/mcp-auth.middleware.test.ts +60 -3
- package/src/modules/mcp/__tests__/mcp.service.test.ts +68 -6
- package/src/modules/mcp/mcp-auth.middleware.ts +20 -6
- package/src/modules/mcp/oauth/bevel-oauth-provider.ts +3 -1
- package/src/modules/plugins/__tests__/plugins.tools.test.ts +2 -1
- package/src/modules/settings/__tests__/deployment-settings.service.test.ts +16 -0
- package/src/modules/settings/__tests__/setup.routes.git-mode.test.ts +91 -58
- package/src/modules/settings/__tests__/setup.routes.github-app.test.ts +25 -8
- package/src/modules/settings/__tests__/setup.routes.managed-phase.test.ts +14 -11
- package/src/modules/settings/__tests__/setup.routes.repository-change.test.ts +390 -0
- package/src/modules/settings/__tests__/setup.routes.test.ts +3 -0
- package/src/modules/settings/deployment-settings.service.ts +8 -3
- package/src/modules/settings/setup.routes.ts +263 -19
- package/src/modules/skills/__tests__/allowed-tools-warn.tools.test.ts +2 -1
- package/src/modules/skills/__tests__/branch-skills.tools.test.ts +218 -0
- package/src/modules/skills/__tests__/skills.service.test.ts +229 -5
- package/src/modules/skills/skills.contract.ts +91 -18
- package/src/modules/skills/skills.service.ts +278 -41
- package/src/modules/skills/skills.tools.ts +80 -8
- package/src/modules/tool-auth/__tests__/manual-auth.middleware.test.ts +14 -1
- package/src/modules/tool-auth/external-api-key.service.ts +4 -2
- package/src/modules/tool-auth/internal-token.service.ts +3 -3
- package/src/modules/tool-auth/tool-auth.middleware.ts +34 -11
- package/src/modules/tool-helpers/__tests__/agent-roles-write.test.ts +5 -3
- package/src/modules/tool-helpers/__tests__/phase4-tools.test.ts +3 -3
- package/src/modules/tool-helpers/__tests__/validate-token.test.ts +11 -1
- package/src/modules/tool-helpers/tool-handler.ts +24 -4
- package/src/modules/tool-helpers/tool.contract.ts +3 -2
- package/src/modules/tool-helpers/validate-token.ts +1 -1
- package/src/modules/workflow/__tests__/pending-commits.repository-replaced.test.ts +111 -0
- package/src/modules/workflow/__tests__/workflow.routes.history-read-gate.test.ts +25 -0
- package/src/modules/workflow/__tests__/workflow.service.repository-replaced.test.ts +249 -0
- package/src/modules/workflow/agent-tools/__tests__/workflow.tools.test.ts +264 -6
- package/src/modules/workflow/agent-tools/change-request-summary.ts +182 -0
- package/src/modules/workflow/agent-tools/workflow.tools.ts +86 -35
- package/src/modules/workflow/file-lock.service.ts +31 -0
- package/src/modules/workflow/git/__tests__/git.service.fileBytesAtCommit.test.ts +260 -0
- package/src/modules/workflow/git/git.service.ts +168 -0
- package/src/modules/workflow/pending-commits.service.ts +59 -1
- package/src/modules/workflow/workflow-hooks.ts +64 -26
- package/src/modules/workflow/workflow.service.ts +121 -4
- package/src/modules/workspace/__tests__/agent-access.gate.test.ts +208 -0
- package/src/modules/workspace/__tests__/escape-sequences.routes.test.ts +384 -0
- package/src/modules/workspace/__tests__/file-stat-access.test.ts +2 -1
- package/src/modules/workspace/__tests__/git-internals.security.test.ts +2 -1
- package/src/modules/workspace/__tests__/set-aside-clone.test.ts +52 -0
- package/src/modules/workspace/__tests__/workspace.routes.at-ref.test.ts +305 -0
- package/src/modules/workspace/__tests__/workspace.routes.download.test.ts +2 -0
- package/src/modules/workspace/__tests__/workspace.service.forget-clone-races.test.ts +147 -0
- package/src/modules/workspace/__tests__/workspace.service.replaced-repository.test.ts +326 -0
- package/src/modules/workspace/__tests__/workspace.service.test.ts +20 -9
- package/src/modules/workspace/__tests__/workspace.tools.agents-file.test.ts +2 -3
- package/src/modules/workspace/__tests__/workspace.tools.branch-errors.test.ts +2 -3
- package/src/modules/workspace/__tests__/workspace.tools.test.ts +685 -14
- package/src/modules/workspace/agent-access.gate.ts +164 -0
- package/src/modules/workspace/routine-write-policy.ts +5 -6
- package/src/modules/workspace/session-sink.ts +5 -5
- package/src/modules/workspace/set-aside-clone.ts +96 -0
- package/src/modules/workspace/startup/__tests__/kb-startup-runner.test.ts +86 -0
- package/src/modules/workspace/startup/kb-startup-runner.ts +115 -34
- package/src/modules/workspace/workspace.routes.ts +95 -3
- package/src/modules/workspace/workspace.service.ts +256 -24
- package/src/modules/workspace/workspace.tools.ts +338 -119
- package/src/modules/write-access/__tests__/write-access.test.ts +248 -0
- package/src/modules/write-access/write-access.ts +153 -0
- package/src/shared/domain-errors.ts +31 -0
- package/src/shared/git.contract.ts +27 -0
- package/src/tenancy/__tests__/static-tenant-source.test.ts +0 -1
- package/src/tenancy/static-tenant-source.ts +0 -3
- package/dist/modules/workflow/session-ontology.policy.d.ts +0 -52
- package/dist/modules/workflow/session-ontology.policy.d.ts.map +0 -1
- package/dist/modules/workflow/session-ontology.policy.js +0 -62
- package/dist/modules/workflow/session-ontology.policy.js.map +0 -1
- package/dist/modules/workflow/session-ontology.service.d.ts +0 -105
- package/dist/modules/workflow/session-ontology.service.d.ts.map +0 -1
- package/dist/modules/workflow/session-ontology.service.js +0 -147
- package/dist/modules/workflow/session-ontology.service.js.map +0 -1
- package/dist/modules/workspace/session-ontology.gate.d.ts +0 -114
- package/dist/modules/workspace/session-ontology.gate.d.ts.map +0 -1
- package/dist/modules/workspace/session-ontology.gate.js +0 -161
- package/dist/modules/workspace/session-ontology.gate.js.map +0 -1
- package/dist/shared/kb-layout.d.ts +0 -39
- package/dist/shared/kb-layout.d.ts.map +0 -1
- package/dist/shared/kb-layout.js +0 -103
- package/dist/shared/kb-layout.js.map +0 -1
- package/dist/shared/kb-layout.test.d.ts +0 -2
- package/dist/shared/kb-layout.test.d.ts.map +0 -1
- package/dist/shared/kb-layout.test.js +0 -75
- package/dist/shared/kb-layout.test.js.map +0 -1
- package/src/modules/workflow/__tests__/session-ontology.policy.test.ts +0 -62
- package/src/modules/workflow/__tests__/session-ontology.service.test.ts +0 -201
- package/src/modules/workflow/session-ontology.policy.ts +0 -70
- package/src/modules/workflow/session-ontology.service.ts +0 -183
- package/src/modules/workspace/__tests__/session-ontology.gate.test.ts +0 -239
- package/src/modules/workspace/session-ontology.gate.ts +0 -191
- package/src/shared/kb-layout.test.ts +0 -98
- package/src/shared/kb-layout.ts +0 -102
|
@@ -9,11 +9,76 @@ import { createToolContextResolver } from '../../../tool-helpers/tool-context.js
|
|
|
9
9
|
import { createToolHandlerFactory } from '../../../tool-helpers/tool-handler.js';
|
|
10
10
|
import { createManualRoutes } from '../../../tool-registry/manual.routes.js';
|
|
11
11
|
import { registerWorkflowTools } from '../workflow.tools.js';
|
|
12
|
-
import { OpenChangeRequestBlocksMergeError } from '../../../../shared/domain-errors.js';
|
|
12
|
+
import { ChangeRequestConflictsError, OpenChangeRequestBlocksMergeError } from '../../../../shared/domain-errors.js';
|
|
13
13
|
|
|
14
14
|
const WS = 'target-company-state';
|
|
15
15
|
|
|
16
|
+
/**
|
|
17
|
+
* One file of a change-request detail as the service returns it: a 15 KB
|
|
18
|
+
* addition, patch included. The reported failure was 27 of these — the detail
|
|
19
|
+
* answered ~445,000 characters and overflowed the agent's tool-result limit.
|
|
20
|
+
*/
|
|
21
|
+
function addedFile(i: number): Record<string, unknown> {
|
|
22
|
+
const path = `KnowledgeBase/Engineering/Knowledge/Hexis/Generated-Node-${i}.md`;
|
|
23
|
+
return {
|
|
24
|
+
path,
|
|
25
|
+
status: 'added',
|
|
26
|
+
additions: 400,
|
|
27
|
+
deletions: 0,
|
|
28
|
+
// 400 × 37 characters ≈ 15 KB, as reported.
|
|
29
|
+
patch: `@@ -0,0 +1,400 @@\n${'+a line of generated knowledge text\n'.repeat(400)}`,
|
|
30
|
+
isBinary: false,
|
|
31
|
+
sha: `blob-${i}`,
|
|
32
|
+
rawUrl: `https://raw.example.com/blob-${i}`,
|
|
33
|
+
};
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
/** The per-file approval state the detail carries, as the app's dialog reads it. */
|
|
37
|
+
function approvalOf(path: string): Record<string, unknown> {
|
|
38
|
+
return {
|
|
39
|
+
path,
|
|
40
|
+
eligibleApprovers: {
|
|
41
|
+
roles: ['Admin', 'Engineering'],
|
|
42
|
+
users: [{ name: 'Razvan Radulescu', email: 'razvan.radulescu@bevel.software' }],
|
|
43
|
+
},
|
|
44
|
+
approvedBy: [],
|
|
45
|
+
eligibilityResolved: true,
|
|
46
|
+
isApproved: false,
|
|
47
|
+
inMergeGate: true,
|
|
48
|
+
viewerCanApprove: true,
|
|
49
|
+
};
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
/** A full change-request detail over the given files — what `openChangeRequest` answers. */
|
|
53
|
+
function detailOf(files: Record<string, unknown>[]): Record<string, unknown> {
|
|
54
|
+
return {
|
|
55
|
+
number: 7,
|
|
56
|
+
url: 'https://bevel.example.com/change-requests/7',
|
|
57
|
+
title: 'My change',
|
|
58
|
+
state: 'open',
|
|
59
|
+
branch: 'me/draft',
|
|
60
|
+
base: 'target-company-state',
|
|
61
|
+
body: 'Body shown verbatim to reviewers.',
|
|
62
|
+
author: { login: 'bevel-bot' },
|
|
63
|
+
headSha: 'head-1',
|
|
64
|
+
baseSha: 'base-1',
|
|
65
|
+
files,
|
|
66
|
+
comments: [],
|
|
67
|
+
approvals: files.map((f) => approvalOf(f.path as string)),
|
|
68
|
+
mergeableInBevel: false,
|
|
69
|
+
mergeBlockedReasons: files.map((f) => `${f.path as string} is not approved`),
|
|
70
|
+
mergeWarnings: files.map((f) => `${f.path as string} is not approved`),
|
|
71
|
+
};
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
/** The reported case: 27 new files of 15 KB each. */
|
|
75
|
+
const BIG = () => detailOf(Array.from({ length: 27 }, (_, i) => addedFile(i)));
|
|
76
|
+
|
|
16
77
|
let calls: unknown[][] = [];
|
|
78
|
+
/** What the service answers `openChangeRequest` with; a test replaces it. */
|
|
79
|
+
let opened: Record<string, unknown> = BIG();
|
|
80
|
+
/** Set to make `openChangeRequest` throw instead. */
|
|
81
|
+
let openFails: Error | undefined;
|
|
17
82
|
const externalApiKeyService = {
|
|
18
83
|
looksLikeExternalApiKey: (t: string) => typeof t === 'string' && t.startsWith('bevel_'),
|
|
19
84
|
verifyAndLoadToken: async (t: string) =>
|
|
@@ -38,7 +103,8 @@ const workflowService = {
|
|
|
38
103
|
},
|
|
39
104
|
openChangeRequest: async (ws: string, user: { id: string }, body: unknown) => {
|
|
40
105
|
calls.push(['openChangeRequest', ws, user.id, body]);
|
|
41
|
-
|
|
106
|
+
if (openFails) throw openFails;
|
|
107
|
+
return opened;
|
|
42
108
|
},
|
|
43
109
|
createBranch: async (ws: string, name: string) => {
|
|
44
110
|
calls.push(['createBranch', ws, name]);
|
|
@@ -114,6 +180,8 @@ const post = (url: string, bearer: string, body: unknown = {}) =>
|
|
|
114
180
|
|
|
115
181
|
beforeEach(() => {
|
|
116
182
|
calls = [];
|
|
183
|
+
opened = BIG();
|
|
184
|
+
openFails = undefined;
|
|
117
185
|
});
|
|
118
186
|
afterEach(async () => {
|
|
119
187
|
if (httpServer) await new Promise<void>((r) => httpServer!.close(() => r()));
|
|
@@ -229,7 +297,7 @@ describe('registerWorkflowTools', () => {
|
|
|
229
297
|
expect(calls).toContainEqual(['listBranches', 'existing-ws']);
|
|
230
298
|
});
|
|
231
299
|
|
|
232
|
-
it('open_change_request
|
|
300
|
+
it('open_change_request acts on the SOURCE branch\'s workspace', async () => {
|
|
233
301
|
const base = await start();
|
|
234
302
|
const res = await post(`${base}/api/agent/tools/open_change_request`, writeTok(), {
|
|
235
303
|
sourceBranch: 'me/draft',
|
|
@@ -237,9 +305,6 @@ describe('registerWorkflowTools', () => {
|
|
|
237
305
|
title: 'My change',
|
|
238
306
|
});
|
|
239
307
|
expect(res.status).toBe(200);
|
|
240
|
-
expect(await res.json()).toMatchObject({
|
|
241
|
-
changeRequest: { number: 7, url: 'https://bevel.example.com/change-requests/7' },
|
|
242
|
-
});
|
|
243
308
|
// The workspace must be derived from the SOURCE branch (not a separate
|
|
244
309
|
// `branch` arg) — encodeURIComponent('me/draft'). Regression guard for the
|
|
245
310
|
// `-b undefined` clone bug when the model omitted `branch`.
|
|
@@ -251,6 +316,199 @@ describe('registerWorkflowTools', () => {
|
|
|
251
316
|
]);
|
|
252
317
|
});
|
|
253
318
|
|
|
319
|
+
/**
|
|
320
|
+
* One test per Scenario of the Specification
|
|
321
|
+
* (`hx-change-request-answer-is-small-spec`). The reported failure: opening a
|
|
322
|
+
* request for 27 files answered ~445,000 characters, because the answer was
|
|
323
|
+
* the detail the app's dialog reads, every patch included. It overflowed the
|
|
324
|
+
* tool-result limit, so the agent could not even read the link.
|
|
325
|
+
*/
|
|
326
|
+
describe('what open_change_request answers', () => {
|
|
327
|
+
const open = async (base: string, include?: string[]) => {
|
|
328
|
+
const res = await post(`${base}/api/agent/tools/open_change_request`, writeTok(), {
|
|
329
|
+
sourceBranch: 'me/draft',
|
|
330
|
+
targetBranch: 'target-company-state',
|
|
331
|
+
title: 'My change',
|
|
332
|
+
...(include ? { include } : {}),
|
|
333
|
+
});
|
|
334
|
+
expect(res.status).toBe(200);
|
|
335
|
+
return (await res.json()) as Record<string, unknown>;
|
|
336
|
+
};
|
|
337
|
+
|
|
338
|
+
it('answers a summary for 27 new files: the link first, 25 paths of 27, approvers, blockers, no patch', async () => {
|
|
339
|
+
const base = await start();
|
|
340
|
+
const body = await open(base);
|
|
341
|
+
|
|
342
|
+
// The link is the FIRST field: the one thing the agent must hand the
|
|
343
|
+
// user, so it survives a truncation of anything after it.
|
|
344
|
+
expect(Object.keys(body)[0]).toBe('url');
|
|
345
|
+
expect(body).toMatchObject({
|
|
346
|
+
url: 'https://bevel.example.com/change-requests/7',
|
|
347
|
+
number: 7,
|
|
348
|
+
title: 'My change',
|
|
349
|
+
state: 'open',
|
|
350
|
+
sourceBranch: 'me/draft',
|
|
351
|
+
targetBranch: 'target-company-state',
|
|
352
|
+
totalFiles: 27,
|
|
353
|
+
});
|
|
354
|
+
|
|
355
|
+
const files = body.files as { path: string; change: string }[];
|
|
356
|
+
expect(files).toHaveLength(25);
|
|
357
|
+
expect(new Set(files.map((f) => f.change))).toEqual(new Set(['added']));
|
|
358
|
+
expect(files[0].path).toBe('KnowledgeBase/Engineering/Knowledge/Hexis/Generated-Node-0.md');
|
|
359
|
+
|
|
360
|
+
// Who must approve, per path — what Juan found useful and must stay.
|
|
361
|
+
expect(body.approvals).toHaveLength(25);
|
|
362
|
+
expect((body.approvals as unknown[])[0]).toEqual({
|
|
363
|
+
path: 'KnowledgeBase/Engineering/Knowledge/Hexis/Generated-Node-0.md',
|
|
364
|
+
roles: ['Admin', 'Engineering'],
|
|
365
|
+
users: ['Razvan Radulescu'],
|
|
366
|
+
approved: false,
|
|
367
|
+
inMergeGate: true,
|
|
368
|
+
});
|
|
369
|
+
// What blocks the merge: every reason, for cut paths too.
|
|
370
|
+
expect(body.mergeBlockedReasons).toHaveLength(27);
|
|
371
|
+
|
|
372
|
+
// No patch, and no file content under any other name.
|
|
373
|
+
const json = JSON.stringify(body);
|
|
374
|
+
expect(json).not.toContain('patch');
|
|
375
|
+
expect(json).not.toContain('a line of generated knowledge text');
|
|
376
|
+
expect(files.every((f) => !('patch' in f))).toBe(true);
|
|
377
|
+
});
|
|
378
|
+
|
|
379
|
+
it('keeps the 27-file answer under 10,000 characters', async () => {
|
|
380
|
+
const base = await start();
|
|
381
|
+
const body = await open(base);
|
|
382
|
+
// The detail this was shaped from is the ~445,000-character one.
|
|
383
|
+
expect(JSON.stringify(opened).length).toBeGreaterThan(400_000);
|
|
384
|
+
// 8,918 characters as this fixture stands. The three per-path lists
|
|
385
|
+
// (`files`, `approvals`, `mergeBlockedReasons`) are what fills it, so a
|
|
386
|
+
// field added to any of them eats the margin ~25 times over.
|
|
387
|
+
expect(JSON.stringify(body).length).toBeLessThan(10_000);
|
|
388
|
+
});
|
|
389
|
+
|
|
390
|
+
it('`include: ["all-paths"]` lists all 27 paths, still without patches', async () => {
|
|
391
|
+
const base = await start();
|
|
392
|
+
const body = await open(base, ['all-paths']);
|
|
393
|
+
expect(body.files).toHaveLength(27);
|
|
394
|
+
expect(body.totalFiles).toBe(27);
|
|
395
|
+
expect(body.approvals).toHaveLength(27);
|
|
396
|
+
expect(JSON.stringify(body)).not.toContain('a line of generated knowledge text');
|
|
397
|
+
});
|
|
398
|
+
|
|
399
|
+
it('`include: ["patches"]` gives each listed file its patch', async () => {
|
|
400
|
+
const base = await start();
|
|
401
|
+
const body = await open(base, ['patches']);
|
|
402
|
+
const files = body.files as { path: string; patch?: string }[];
|
|
403
|
+
expect(files).toHaveLength(25);
|
|
404
|
+
expect(files.every((f) => f.patch?.startsWith('@@ -0,0 +1,400 @@'))).toBe(true);
|
|
405
|
+
// Cut to 25 still: `patches` adds the diffs, it does not lift the cut.
|
|
406
|
+
expect(body.totalFiles).toBe(27);
|
|
407
|
+
});
|
|
408
|
+
|
|
409
|
+
it('both together: every path, each with its patch', async () => {
|
|
410
|
+
const base = await start();
|
|
411
|
+
const body = await open(base, ['patches', 'all-paths']);
|
|
412
|
+
const files = body.files as { patch?: string }[];
|
|
413
|
+
expect(files).toHaveLength(27);
|
|
414
|
+
expect(files.every((f) => typeof f.patch === 'string')).toBe(true);
|
|
415
|
+
});
|
|
416
|
+
|
|
417
|
+
it('reads a modification `changed` and a rename `moved`, with its old path', async () => {
|
|
418
|
+
const base = await start();
|
|
419
|
+
opened = detailOf([
|
|
420
|
+
{ path: 'KnowledgeBase/Kept.md', status: 'modified', additions: 2, deletions: 1, patch: '@@ x', isBinary: false, sha: 'b1', rawUrl: 'r1' },
|
|
421
|
+
{ path: 'KnowledgeBase/New-Home.md', previousPath: 'KnowledgeBase/Old-Home.md', status: 'renamed', additions: 0, deletions: 0, isBinary: false, sha: 'b2', rawUrl: 'r2' },
|
|
422
|
+
]);
|
|
423
|
+
const body = await open(base);
|
|
424
|
+
expect(body.files).toEqual([
|
|
425
|
+
{ path: 'KnowledgeBase/Kept.md', change: 'changed' },
|
|
426
|
+
{ path: 'KnowledgeBase/New-Home.md', change: 'moved', previousPath: 'KnowledgeBase/Old-Home.md' },
|
|
427
|
+
]);
|
|
428
|
+
expect(body.totalFiles).toBe(2);
|
|
429
|
+
});
|
|
430
|
+
|
|
431
|
+
it('a deletion reads `deleted`', async () => {
|
|
432
|
+
const base = await start();
|
|
433
|
+
opened = detailOf([
|
|
434
|
+
{ path: 'KnowledgeBase/Gone.md', status: 'removed', additions: 0, deletions: 9, isBinary: false, sha: 'b3', rawUrl: 'r3' },
|
|
435
|
+
]);
|
|
436
|
+
expect((await open(base)).files).toEqual([{ path: 'KnowledgeBase/Gone.md', change: 'deleted' }]);
|
|
437
|
+
});
|
|
438
|
+
|
|
439
|
+
it('says the approvers are unknown rather than implying nobody must approve', async () => {
|
|
440
|
+
const base = await start();
|
|
441
|
+
opened = detailOf([
|
|
442
|
+
{ path: 'KnowledgeBase/Opaque.md', status: 'added', additions: 1, deletions: 0, isBinary: false, sha: 'b4', rawUrl: 'r4' },
|
|
443
|
+
]);
|
|
444
|
+
// No usable access config on the base: the empty approver set means "not
|
|
445
|
+
// known", and nothing may be granted on the strength of that emptiness.
|
|
446
|
+
opened.approvals = [
|
|
447
|
+
{ path: 'KnowledgeBase/Opaque.md', eligibleApprovers: { roles: [], users: [] }, approvedBy: [], eligibilityResolved: false, isApproved: false, inMergeGate: false },
|
|
448
|
+
];
|
|
449
|
+
expect((await open(base)).approvals).toEqual([
|
|
450
|
+
{ path: 'KnowledgeBase/Opaque.md', roles: [], users: [], approved: false, inMergeGate: false, approversUnknown: true },
|
|
451
|
+
]);
|
|
452
|
+
|
|
453
|
+
// A detail that carries no verdict at all reads the same way: absent is
|
|
454
|
+
// not resolved, so the emptiness still means "not known".
|
|
455
|
+
const legacy = { ...(opened.approvals as Record<string, unknown>[])[0] };
|
|
456
|
+
delete legacy.eligibilityResolved;
|
|
457
|
+
opened.approvals = [legacy];
|
|
458
|
+
expect((await open(base)).approvals).toEqual([
|
|
459
|
+
{ path: 'KnowledgeBase/Opaque.md', roles: [], users: [], approved: false, inMergeGate: false, approversUnknown: true },
|
|
460
|
+
]);
|
|
461
|
+
});
|
|
462
|
+
|
|
463
|
+
it('surfaces the `change-request-conflicts` error as before', async () => {
|
|
464
|
+
const base = await start();
|
|
465
|
+
openFails = new ChangeRequestConflictsError('me/draft', 'target-company-state', ['KnowledgeBase/Clash.md']);
|
|
466
|
+
const res = await post(`${base}/api/agent/tools/open_change_request`, writeTok(), {
|
|
467
|
+
sourceBranch: 'me/draft',
|
|
468
|
+
targetBranch: 'target-company-state',
|
|
469
|
+
title: 'My change',
|
|
470
|
+
});
|
|
471
|
+
expect(res.status).toBe(409);
|
|
472
|
+
expect(await res.json()).toMatchObject({
|
|
473
|
+
kind: 'change-request-conflicts',
|
|
474
|
+
sourceBranch: 'me/draft',
|
|
475
|
+
targetBranch: 'target-company-state',
|
|
476
|
+
conflictedPaths: ['KnowledgeBase/Clash.md'],
|
|
477
|
+
});
|
|
478
|
+
});
|
|
479
|
+
|
|
480
|
+
/**
|
|
481
|
+
* The app's dialog reads the same detail from its own route. Shaping the
|
|
482
|
+
* agent's answer must not touch it — so the detail the service handed back
|
|
483
|
+
* still carries all 27 files and all their patches after the tool answered.
|
|
484
|
+
*/
|
|
485
|
+
it('leaves the detail the app\'s dialog reads exactly as the service returned it', async () => {
|
|
486
|
+
const base = await start();
|
|
487
|
+
const before = JSON.stringify(opened);
|
|
488
|
+
await open(base);
|
|
489
|
+
expect(JSON.stringify(opened)).toBe(before);
|
|
490
|
+
expect((opened.files as unknown[])).toHaveLength(27);
|
|
491
|
+
expect((opened.files as { patch?: string }[]).every((f) => typeof f.patch === 'string')).toBe(true);
|
|
492
|
+
});
|
|
493
|
+
|
|
494
|
+
it('says in its description what the default answer holds and how to ask for more', async () => {
|
|
495
|
+
await start();
|
|
496
|
+
for (const tools of [await registryRef!.listInternal(), await registryRef!.listExternal()]) {
|
|
497
|
+
const def = tools.find((t) => t.name === 'open_change_request')!;
|
|
498
|
+
expect(def.description).toMatch(/SUMMARY/);
|
|
499
|
+
expect(def.description).toMatch(/cut to 25/);
|
|
500
|
+
expect(def.description).toMatch(/No patches and no file content/);
|
|
501
|
+
expect(def.description).toMatch(/include: \["patches"\]/);
|
|
502
|
+
expect(def.description).toMatch(/include: \["all-paths"\]/);
|
|
503
|
+
// `toolDef` wraps a tool's flat inputs under `body`.
|
|
504
|
+
const body = (def.inputs as { properties: { body: { properties: Record<string, { items?: { enum?: string[] }; description?: string }> } } })
|
|
505
|
+
.properties.body;
|
|
506
|
+
expect(body.properties.include.items?.enum).toEqual(['patches', 'all-paths']);
|
|
507
|
+
expect(body.properties.include.description).toMatch(/every changed path instead of the first 25/);
|
|
508
|
+
}
|
|
509
|
+
});
|
|
510
|
+
});
|
|
511
|
+
|
|
254
512
|
it('create_branch forks in the BASE branch\'s workspace, never the new draft\'s', async () => {
|
|
255
513
|
const base = await start();
|
|
256
514
|
const res = await post(`${base}/api/agent/tools/create_branch`, writeTok(), {
|
|
@@ -0,0 +1,182 @@
|
|
|
1
|
+
import type {
|
|
2
|
+
ChangeRequestDetail,
|
|
3
|
+
ChangeRequestState,
|
|
4
|
+
ChangedFile,
|
|
5
|
+
FileApproval,
|
|
6
|
+
PrFileStatus,
|
|
7
|
+
} from '@bevel-software/platform-shared';
|
|
8
|
+
|
|
9
|
+
/**
|
|
10
|
+
* What `open_change_request` answers, and why it is not the detail the app's
|
|
11
|
+
* dialog reads.
|
|
12
|
+
*
|
|
13
|
+
* The dialog renders every file's diff, so its detail carries every patch. An
|
|
14
|
+
* agent that opens a change request needs none of that: it needs the link to
|
|
15
|
+
* hand the user, and enough of the request to confirm it carries the files it
|
|
16
|
+
* meant to propose and to say what the request is now waiting on. Handing it
|
|
17
|
+
* the dialog's detail cost ~445,000 characters for 27 files of 15 KB — past
|
|
18
|
+
* every tool-result limit, so the one field the agent actually needed (the
|
|
19
|
+
* link) could not be read at all.
|
|
20
|
+
*
|
|
21
|
+
* So this is a purpose-made answer: the link first, the request's identity,
|
|
22
|
+
* who must approve each path, what blocks the merge verbatim from the detail,
|
|
23
|
+
* and the changed paths with their kind of change — no patch, no file content.
|
|
24
|
+
* An agent that wants more asks for it by name through `include`.
|
|
25
|
+
*/
|
|
26
|
+
|
|
27
|
+
/** What `include` accepts. */
|
|
28
|
+
export type ChangeRequestInclude = 'patches' | 'all-paths';
|
|
29
|
+
|
|
30
|
+
/** The two `include` values, for the tool's input schema and its validation. */
|
|
31
|
+
export const CHANGE_REQUEST_INCLUDES: readonly ChangeRequestInclude[] = ['patches', 'all-paths'];
|
|
32
|
+
|
|
33
|
+
/** How many paths the answer lists before it cuts; `all-paths` lifts the cut. */
|
|
34
|
+
export const CHANGE_REQUEST_PATH_LIMIT = 25;
|
|
35
|
+
|
|
36
|
+
/**
|
|
37
|
+
* The kind of change to one path, in the four words the answer speaks. Git's
|
|
38
|
+
* own statuses are finer (`modified` vs `changed`, `renamed` vs `copied`) in
|
|
39
|
+
* ways nothing downstream acts on.
|
|
40
|
+
*/
|
|
41
|
+
export type ChangeKind = 'added' | 'changed' | 'deleted' | 'moved';
|
|
42
|
+
|
|
43
|
+
/** One changed path. `previousPath` is where a `moved` one came from. */
|
|
44
|
+
export interface ChangeRequestSummaryFile {
|
|
45
|
+
path: string;
|
|
46
|
+
change: ChangeKind;
|
|
47
|
+
previousPath?: string;
|
|
48
|
+
/** Unified diff. Only with `include: ["patches"]`, and absent for a binary or oversized file. */
|
|
49
|
+
patch?: string;
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
/**
|
|
53
|
+
* Who must approve one path, at the same per-path granularity the detail
|
|
54
|
+
* carries (the access tree resolves a role per file or per folder; this says
|
|
55
|
+
* what it resolved to for this file).
|
|
56
|
+
*
|
|
57
|
+
* The detail's own entry is the dialog's: it also carries each approver's
|
|
58
|
+
* email, the viewer's own eligibility, and every approval's timestamp and
|
|
59
|
+
* staleness. Repeated over 25 files that is ~4,500 characters an agent cannot
|
|
60
|
+
* act on — it may not approve anything — and it is what would push the
|
|
61
|
+
* 27-file answer back over the limit this ticket exists to get under. So the
|
|
62
|
+
* answer keeps what "who must approve" means and nothing else.
|
|
63
|
+
*/
|
|
64
|
+
export interface ChangeRequestApprovers {
|
|
65
|
+
path: string;
|
|
66
|
+
/** Roles that confer approval here. `everyone` means any signed-in approver. */
|
|
67
|
+
roles: string[];
|
|
68
|
+
/** Approvers named directly, by display name. */
|
|
69
|
+
users: string[];
|
|
70
|
+
/** True once an eligible approver has approved at the request's current head. */
|
|
71
|
+
approved: boolean;
|
|
72
|
+
/** False when the merge gate does not bind this path — then nobody has to approve it. */
|
|
73
|
+
inMergeGate: boolean;
|
|
74
|
+
/**
|
|
75
|
+
* Present when the access tree could not be resolved for this path — which
|
|
76
|
+
* includes a detail that carries no verdict at all. Then empty
|
|
77
|
+
* `roles`/`users` means "not known", NOT "nobody has to approve".
|
|
78
|
+
*/
|
|
79
|
+
approversUnknown?: true;
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
export interface ChangeRequestSummary {
|
|
83
|
+
/** First field on purpose: it is the one an agent must hand the user. */
|
|
84
|
+
url: string;
|
|
85
|
+
urlNote?: string;
|
|
86
|
+
number: number;
|
|
87
|
+
title: string;
|
|
88
|
+
state: ChangeRequestState;
|
|
89
|
+
sourceBranch: string;
|
|
90
|
+
targetBranch: string;
|
|
91
|
+
/** Who must approve, one entry per listed path. */
|
|
92
|
+
approvals: ChangeRequestApprovers[];
|
|
93
|
+
/** What blocks the merge — verbatim from the detail. Empty when nothing does. */
|
|
94
|
+
mergeBlockedReasons: string[];
|
|
95
|
+
/** The changed paths, cut to {@link CHANGE_REQUEST_PATH_LIMIT} unless `all-paths` was asked for. */
|
|
96
|
+
files: ChangeRequestSummaryFile[];
|
|
97
|
+
/** How many files the request changes in total, cut or not. */
|
|
98
|
+
totalFiles: number;
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
/**
|
|
102
|
+
* A copy and a rename both carry `previousPath`, and the one thing a reader
|
|
103
|
+
* does with either is look at where the text came from — so both read `moved`.
|
|
104
|
+
* `unchanged` is a file the request lists without changing (git reports it on
|
|
105
|
+
* a mode-only change); `changed` is the honest word for it among these four.
|
|
106
|
+
*/
|
|
107
|
+
function changeKindOf(status: PrFileStatus | undefined): ChangeKind {
|
|
108
|
+
switch (status) {
|
|
109
|
+
case 'added':
|
|
110
|
+
return 'added';
|
|
111
|
+
case 'removed':
|
|
112
|
+
return 'deleted';
|
|
113
|
+
case 'renamed':
|
|
114
|
+
case 'copied':
|
|
115
|
+
return 'moved';
|
|
116
|
+
default:
|
|
117
|
+
return 'changed';
|
|
118
|
+
}
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
function approversOf(approval: FileApproval): ChangeRequestApprovers {
|
|
122
|
+
const eligible = approval.eligibleApprovers ?? { roles: [], users: [] };
|
|
123
|
+
return {
|
|
124
|
+
path: approval.path,
|
|
125
|
+
roles: eligible.roles ?? [],
|
|
126
|
+
users: (eligible.users ?? []).map((u) => u.name),
|
|
127
|
+
approved: approval.isApproved,
|
|
128
|
+
inMergeGate: approval.inMergeGate,
|
|
129
|
+
// Anything but an explicit `true` is unresolved — a detail built before
|
|
130
|
+
// the flag existed carries no flag at all, and reading that as "resolved"
|
|
131
|
+
// would turn an empty approver set into "nobody must approve". Same
|
|
132
|
+
// fail-closed reading as `computeViewerCanUpdate`.
|
|
133
|
+
...(approval.eligibilityResolved !== true ? { approversUnknown: true as const } : {}),
|
|
134
|
+
};
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
function summaryFileOf(file: ChangedFile, withPatch: boolean): ChangeRequestSummaryFile {
|
|
138
|
+
return {
|
|
139
|
+
path: file.path,
|
|
140
|
+
change: changeKindOf(file.status),
|
|
141
|
+
...(file.previousPath ? { previousPath: file.previousPath } : {}),
|
|
142
|
+
...(withPatch && file.patch ? { patch: file.patch } : {}),
|
|
143
|
+
};
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
/**
|
|
147
|
+
* Shape the tool's answer out of the detail the service already returns. Pure:
|
|
148
|
+
* the service and the app's routes are untouched, and the detail they serve is
|
|
149
|
+
* exactly what it was.
|
|
150
|
+
*
|
|
151
|
+
* `approvals` is narrowed to the paths the answer lists, because the detail's
|
|
152
|
+
* contract is one entry per listed file. Nothing about the merge gate is lost
|
|
153
|
+
* by that: `mergeBlockedReasons` names every missing approval, cut or not.
|
|
154
|
+
*/
|
|
155
|
+
export function summarizeChangeRequest(
|
|
156
|
+
detail: ChangeRequestDetail,
|
|
157
|
+
include: readonly ChangeRequestInclude[] = [],
|
|
158
|
+
): ChangeRequestSummary {
|
|
159
|
+
const all = detail.files ?? [];
|
|
160
|
+
const withPatch = include.includes('patches');
|
|
161
|
+
const listed = include.includes('all-paths') ? all : all.slice(0, CHANGE_REQUEST_PATH_LIMIT);
|
|
162
|
+
const listedPaths = new Set(listed.map((f) => f.path));
|
|
163
|
+
return {
|
|
164
|
+
url: detail.url,
|
|
165
|
+
...(detail.urlNote ? { urlNote: detail.urlNote } : {}),
|
|
166
|
+
number: detail.number,
|
|
167
|
+
title: detail.title,
|
|
168
|
+
state: detail.state,
|
|
169
|
+
sourceBranch: detail.branch,
|
|
170
|
+
targetBranch: detail.base,
|
|
171
|
+
approvals: (detail.approvals ?? []).filter((a) => listedPaths.has(a.path)).map(approversOf),
|
|
172
|
+
mergeBlockedReasons: detail.mergeBlockedReasons ?? [],
|
|
173
|
+
files: listed.map((f) => summaryFileOf(f, withPatch)),
|
|
174
|
+
totalFiles: all.length,
|
|
175
|
+
};
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
/** The `include` values off a tool call's arguments; anything else is ignored. */
|
|
179
|
+
export function parseInclude(raw: unknown): ChangeRequestInclude[] {
|
|
180
|
+
if (!Array.isArray(raw)) return [];
|
|
181
|
+
return CHANGE_REQUEST_INCLUDES.filter((v) => raw.includes(v));
|
|
182
|
+
}
|
|
@@ -9,6 +9,12 @@ import { assertBranchProvided } from '../../../shared/domain-errors.js';
|
|
|
9
9
|
import { workspaceIdForBranch } from '../../../shared/workspace-id.js';
|
|
10
10
|
import { assertInsideRepo, normalizePathArgs } from '../../kb-fs/repo-path.js';
|
|
11
11
|
import { RETIRED_TOOL_MESSAGES } from '@bevel-software/platform-mcp-core';
|
|
12
|
+
import {
|
|
13
|
+
CHANGE_REQUEST_INCLUDES,
|
|
14
|
+
CHANGE_REQUEST_PATH_LIMIT,
|
|
15
|
+
parseInclude,
|
|
16
|
+
summarizeChangeRequest,
|
|
17
|
+
} from './change-request-summary.js';
|
|
12
18
|
|
|
13
19
|
/** `knowledge-base/KnowledgeBase/x.md` → `KnowledgeBase/x.md`; anything else unchanged. */
|
|
14
20
|
function stripKbDir(path: string, kbDirName: string): string {
|
|
@@ -47,20 +53,31 @@ const commitSchema: JsonSchema = {
|
|
|
47
53
|
required: ['authorName', 'authorEmail', 'sha', 'subject', 'committedAt'],
|
|
48
54
|
};
|
|
49
55
|
|
|
50
|
-
const
|
|
56
|
+
const changeRequestSummaryFileSchema: JsonSchema = {
|
|
51
57
|
type: 'object',
|
|
52
58
|
properties: {
|
|
53
59
|
path: { type: 'string' },
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
60
|
+
change: { type: 'string', description: '`added` | `changed` | `deleted` | `moved`.' },
|
|
61
|
+
previousPath: { type: 'string', description: 'Where a `moved` file came from.' },
|
|
62
|
+
patch: {
|
|
63
|
+
type: 'string',
|
|
64
|
+
description: 'Unified diff. Only with `include: ["patches"]`, and absent for a binary or oversized file.',
|
|
65
|
+
},
|
|
66
|
+
},
|
|
67
|
+
required: ['path', 'change'],
|
|
68
|
+
};
|
|
69
|
+
|
|
70
|
+
const changeRequestApproversSchema: JsonSchema = {
|
|
71
|
+
type: 'object',
|
|
72
|
+
properties: {
|
|
73
|
+
path: { type: 'string' },
|
|
74
|
+
roles: { type: 'array', items: { type: 'string' }, description: 'Roles that confer approval here; `everyone` means any signed-in approver.' },
|
|
75
|
+
users: { type: 'array', items: { type: 'string' }, description: 'Approvers named directly, by display name.' },
|
|
76
|
+
approved: { type: 'boolean', description: 'True once an eligible approver has approved at the current head.' },
|
|
77
|
+
inMergeGate: { type: 'boolean', description: 'False when the merge gate does not bind this path — then nobody has to approve it.' },
|
|
78
|
+
approversUnknown: { type: 'boolean', description: 'Present when the access tree could not be resolved: empty `roles`/`users` means unknown, not nobody.' },
|
|
62
79
|
},
|
|
63
|
-
required: ['path', '
|
|
80
|
+
required: ['path', 'roles', 'users', 'approved', 'inMergeGate'],
|
|
64
81
|
};
|
|
65
82
|
|
|
66
83
|
const commentSchema: JsonSchema = {
|
|
@@ -99,24 +116,43 @@ function linkOf(cr: { number: number; url: string; urlNote?: string }): { number
|
|
|
99
116
|
return { number: cr.number, url: cr.url, ...(cr.urlNote ? { urlNote: cr.urlNote } : {}) };
|
|
100
117
|
}
|
|
101
118
|
|
|
102
|
-
|
|
119
|
+
/**
|
|
120
|
+
* What `open_change_request` answers: a summary, `url` first, with no patch and
|
|
121
|
+
* no file content. The app's dialog reads the full detail from its own route;
|
|
122
|
+
* handing that to an agent cost ~445,000 characters for 27 files of 15 KB and
|
|
123
|
+
* overflowed every tool-result limit. Built by `summarizeChangeRequest`.
|
|
124
|
+
*
|
|
125
|
+
* Property order is the answer's field order, and `url` leads it: the link is
|
|
126
|
+
* the one field the agent must hand the user, so it must survive any truncation.
|
|
127
|
+
*/
|
|
128
|
+
const changeRequestSummarySchema: JsonSchema = {
|
|
103
129
|
type: 'object',
|
|
104
|
-
description: 'Full change-request detail (aliased from the underlying pull request).',
|
|
105
130
|
properties: {
|
|
106
|
-
number: { type: 'integer' },
|
|
107
131
|
url: changeRequestUrlSchema,
|
|
108
132
|
urlNote: changeRequestUrlNoteSchema,
|
|
133
|
+
number: { type: 'integer' },
|
|
109
134
|
title: { type: 'string' },
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
135
|
+
state: { type: 'string', description: '`open` | `merged` | `closed`.' },
|
|
136
|
+
sourceBranch: { type: 'string', description: 'The draft branch carrying the changes.' },
|
|
137
|
+
targetBranch: { type: 'string', description: 'The branch the changes apply to.' },
|
|
138
|
+
approvals: {
|
|
139
|
+
type: 'array',
|
|
140
|
+
items: changeRequestApproversSchema,
|
|
141
|
+
description: 'Who must approve, one entry per listed file. A file nobody can approve (`inMergeGate: false`) blocks nothing.',
|
|
142
|
+
},
|
|
143
|
+
mergeBlockedReasons: {
|
|
144
|
+
type: 'array',
|
|
145
|
+
items: { type: 'string' },
|
|
146
|
+
description: 'What blocks the merge — every missing approval, whether its file is listed or cut. Empty when nothing blocks.',
|
|
147
|
+
},
|
|
148
|
+
files: {
|
|
149
|
+
type: 'array',
|
|
150
|
+
items: changeRequestSummaryFileSchema,
|
|
151
|
+
description: `The changed paths with their kind of change, cut to ${CHANGE_REQUEST_PATH_LIMIT}; \`include: ["all-paths"]\` lists them all.`,
|
|
152
|
+
},
|
|
153
|
+
totalFiles: { type: 'integer', description: 'How many files the request changes in total, cut or not.' },
|
|
117
154
|
},
|
|
118
|
-
required: ['
|
|
119
|
-
additionalProperties: true,
|
|
155
|
+
required: ['url', 'number', 'title', 'state', 'sourceBranch', 'targetBranch', 'approvals', 'mergeBlockedReasons', 'files', 'totalFiles'],
|
|
120
156
|
};
|
|
121
157
|
|
|
122
158
|
/**
|
|
@@ -399,6 +435,12 @@ export function registerWorkflowTools(
|
|
|
399
435
|
'Open a change request from a draft branch into a target branch. Auto-merges the latest target ' +
|
|
400
436
|
'into the source first, pushes, then creates the CR. On conflicts returns a ' +
|
|
401
437
|
'`change-request-conflicts` error with affected paths. The author marker is injected server-side. ' +
|
|
438
|
+
'Answers a SUMMARY, not the request\'s content: `url` first (hand it to the user), then `number`, ' +
|
|
439
|
+
'`title`, `state`, `sourceBranch`, `targetBranch`, `approvals` (who must approve each file), ' +
|
|
440
|
+
'`mergeBlockedReasons` (what blocks the merge), and `files` — the changed paths with their kind of ' +
|
|
441
|
+
`change (\`added\` / \`changed\` / \`deleted\` / \`moved\`), cut to ${CHANGE_REQUEST_PATH_LIMIT} of \`totalFiles\`. ` +
|
|
442
|
+
'No patches and no file content: ask for the diffs with `include: ["patches"]` and for every path ' +
|
|
443
|
+
'with `include: ["all-paths"]`. ' +
|
|
402
444
|
'An agent proposes; a person reviews and merges the request in the app — hand the user its `url`.',
|
|
403
445
|
inputs: {
|
|
404
446
|
type: 'object',
|
|
@@ -407,28 +449,37 @@ export function registerWorkflowTools(
|
|
|
407
449
|
targetBranch: { type: 'string', minLength: 1, description: `The branch to apply to (e.g. '${kb.defaultBranch}').` },
|
|
408
450
|
title: { type: 'string', minLength: 1, maxLength: 256, description: 'Short imperative title (≤256 chars).' },
|
|
409
451
|
description: { type: 'string', description: 'Optional markdown body shown verbatim to reviewers.' },
|
|
452
|
+
include: {
|
|
453
|
+
type: 'array',
|
|
454
|
+
items: { type: 'string', enum: [...CHANGE_REQUEST_INCLUDES] },
|
|
455
|
+
description:
|
|
456
|
+
'Add to the default answer, which carries neither: `patches` gives each listed file its unified diff, ' +
|
|
457
|
+
`\`all-paths\` lists every changed path instead of the first ${CHANGE_REQUEST_PATH_LIMIT}. Both may be given. ` +
|
|
458
|
+
'Patches of a large request run to hundreds of thousands of characters — ask for them only when you will read them.',
|
|
459
|
+
},
|
|
410
460
|
},
|
|
411
461
|
required: ['sourceBranch', 'targetBranch', 'title'],
|
|
412
462
|
additionalProperties: false,
|
|
413
463
|
},
|
|
414
|
-
outputs:
|
|
415
|
-
type: 'object',
|
|
416
|
-
properties: { changeRequest: changeRequestDetailSchema },
|
|
417
|
-
required: ['changeRequest'],
|
|
418
|
-
},
|
|
464
|
+
outputs: changeRequestSummarySchema,
|
|
419
465
|
write: true,
|
|
420
466
|
// The workspace this acts on is the SOURCE draft's — `sourceBranch` already
|
|
421
467
|
// names it, so skip the auto-injected (and here redundant) `branch` input
|
|
422
468
|
// rather than asking the model for both and reading the wrong one.
|
|
423
469
|
skipBranch: true,
|
|
424
|
-
|
|
425
|
-
|
|
426
|
-
|
|
427
|
-
|
|
428
|
-
|
|
429
|
-
|
|
430
|
-
|
|
431
|
-
|
|
470
|
+
// The service still returns the full detail — the app's routes serve the
|
|
471
|
+
// same call and must keep getting it. The shaping happens here, in the
|
|
472
|
+
// answer, so nothing below the tool changes.
|
|
473
|
+
handler: async (args, ctx: ToolContext) =>
|
|
474
|
+
summarizeChangeRequest(
|
|
475
|
+
await ctx.workflowService.openChangeRequest(workspaceIdForBranch(args.sourceBranch as string), ctx.user, {
|
|
476
|
+
sourceBranch: args.sourceBranch as string,
|
|
477
|
+
targetBranch: args.targetBranch as string,
|
|
478
|
+
title: args.title as string,
|
|
479
|
+
description: typeof args.description === 'string' ? args.description : undefined,
|
|
480
|
+
}),
|
|
481
|
+
parseInclude(args.include),
|
|
482
|
+
),
|
|
432
483
|
});
|
|
433
484
|
|
|
434
485
|
mount({
|