@wardby/cli 0.2.1 → 0.3.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/.env.example +34 -4
- package/README.md +41 -4
- package/dist/claude-coding-worker/driver.d.ts +6 -1
- package/dist/claude-coding-worker/driver.js +27 -1
- package/dist/claude-coding-worker/main.js +2 -0
- package/dist/claude-coding-worker/tool-socket.d.ts +9 -0
- package/dist/claude-coding-worker/tool-socket.js +26 -0
- package/dist/cli-help.d.ts +1 -1
- package/dist/cli-help.js +13 -4
- package/dist/cli.d.ts +1 -1
- package/dist/cli.js +219 -29
- package/dist/coding/collect-exclude.d.ts +25 -0
- package/dist/coding/collect-exclude.js +76 -0
- package/dist/coding/profile.d.ts +67 -22
- package/dist/coding/profile.js +58 -25
- package/dist/coding/protected-path-wording.d.ts +24 -0
- package/dist/coding/protected-path-wording.js +55 -0
- package/dist/coding/protected-paths.d.ts +31 -0
- package/dist/coding/protected-paths.js +48 -0
- package/dist/coding/protocol.d.ts +75 -4
- package/dist/coding/protocol.js +107 -7
- package/dist/coding/registry/adapters.d.ts +2 -0
- package/dist/coding/registry/adapters.js +9 -0
- package/dist/coding/registry/allowlist.d.ts +9 -0
- package/dist/coding/registry/allowlist.js +28 -0
- package/dist/coding/registry/json-scan.d.ts +55 -0
- package/dist/coding/registry/json-scan.js +191 -0
- package/dist/coding/registry/lockfiles.d.ts +6 -0
- package/dist/coding/registry/lockfiles.js +89 -0
- package/dist/coding/registry/npm-lockfile.d.ts +9 -0
- package/dist/coding/registry/npm-lockfile.js +73 -0
- package/dist/coding/registry/npm-plan.d.ts +29 -0
- package/dist/coding/registry/npm-plan.js +289 -0
- package/dist/coding/registry/npm.d.ts +7 -0
- package/dist/coding/registry/npm.js +252 -0
- package/dist/coding/registry/pypi.d.ts +4 -0
- package/dist/coding/registry/pypi.js +210 -0
- package/dist/coding/registry/report.d.ts +37 -0
- package/dist/coding/registry/report.js +31 -0
- package/dist/coding/registry/token.d.ts +4 -0
- package/dist/coding/registry/token.js +7 -0
- package/dist/coding/registry/types.d.ts +286 -0
- package/dist/coding/registry/types.js +19 -0
- package/dist/coding/registry/worker-config.d.ts +14 -0
- package/dist/coding/registry/worker-config.js +31 -0
- package/dist/coding/services/builtins.d.ts +22 -0
- package/dist/coding/services/builtins.js +85 -0
- package/dist/coding/services/catalog.d.ts +443 -0
- package/dist/coding/services/catalog.js +159 -0
- package/dist/coding/services/declaration.d.ts +11 -0
- package/dist/coding/services/declaration.js +114 -0
- package/dist/coding/services/note.d.ts +8 -0
- package/dist/coding/services/note.js +15 -0
- package/dist/coding/services/resolve.d.ts +25 -0
- package/dist/coding/services/resolve.js +50 -0
- package/dist/coding/services/wording.d.ts +25 -0
- package/dist/coding/services/wording.js +70 -0
- package/dist/coding-proxy/main.js +24 -4
- package/dist/coding-worker/artifact.d.ts +6 -0
- package/dist/coding-worker/debug-trace.d.ts +37 -0
- package/dist/coding-worker/debug-trace.js +117 -0
- package/dist/coding-worker/driver.d.ts +13 -1
- package/dist/coding-worker/driver.js +102 -13
- package/dist/coding-worker/errors.js +8 -0
- package/dist/coding-worker/main.js +10 -2
- package/dist/coding-worker/sdk.d.ts +2 -2
- package/dist/coding-worker/sdk.js +7 -2
- package/dist/coding-worker/types.d.ts +3 -0
- package/dist/config/providers.d.ts +41 -0
- package/dist/config/providers.js +66 -0
- package/dist/core/budget-groups.d.ts +108 -5
- package/dist/core/budget-groups.js +125 -19
- package/dist/core/budget-wording.d.ts +22 -0
- package/dist/core/budget-wording.js +55 -0
- package/dist/core/datastores.js +18 -2
- package/dist/core/db.d.ts +5 -1
- package/dist/core/db.js +8 -2
- package/dist/core/dispatch.d.ts +33 -6
- package/dist/core/dispatch.js +260 -43
- package/dist/core/engine-native.js +27 -10
- package/dist/core/grants.d.ts +86 -0
- package/dist/core/grants.js +126 -0
- package/dist/core/host-events.d.ts +49 -0
- package/dist/core/host-events.js +257 -0
- package/dist/core/host-identity-links.d.ts +54 -0
- package/dist/core/host-identity-links.js +189 -0
- package/dist/core/host-status.d.ts +61 -0
- package/dist/core/host-status.js +211 -0
- package/dist/core/in-flight-runs.d.ts +8 -0
- package/dist/core/in-flight-runs.js +56 -0
- package/dist/core/provider-wording.d.ts +12 -0
- package/dist/core/provider-wording.js +44 -0
- package/dist/core/reconciler.d.ts +38 -2
- package/dist/core/reconciler.js +86 -2
- package/dist/core/repo-access.d.ts +99 -0
- package/dist/core/repo-access.js +136 -0
- package/dist/core/review-host-checks.d.ts +11 -0
- package/dist/core/review-host-checks.js +41 -0
- package/dist/core/review-host-tools.d.ts +47 -0
- package/dist/core/review-host-tools.js +347 -0
- package/dist/core/run-heartbeat.d.ts +27 -0
- package/dist/core/run-heartbeat.js +54 -0
- package/dist/core/runner.d.ts +30 -8
- package/dist/core/runner.js +237 -40
- package/dist/core/secrets.d.ts +11 -2
- package/dist/core/secrets.js +25 -6
- package/dist/core/subagent-memory-tools.d.ts +1 -1
- package/dist/core/subagent-memory-tools.js +16 -3
- package/dist/core/tool-admin.d.ts +81 -0
- package/dist/core/tool-admin.js +129 -0
- package/dist/core/tool-names.d.ts +42 -0
- package/dist/core/tool-names.js +64 -0
- package/dist/core/untrusted-content.d.ts +32 -0
- package/dist/core/untrusted-content.js +72 -0
- package/dist/core/webhooks.d.ts +8 -1
- package/dist/core/webhooks.js +23 -2
- package/dist/generated/prisma/browser.d.ts +100 -0
- package/dist/generated/prisma/client.d.ts +100 -0
- package/dist/generated/prisma/commonInputTypes.d.ts +30 -0
- package/dist/generated/prisma/enums.d.ts +6 -0
- package/dist/generated/prisma/enums.js +6 -1
- package/dist/generated/prisma/internal/class.d.ts +143 -0
- package/dist/generated/prisma/internal/class.js +4 -4
- package/dist/generated/prisma/internal/prismaNamespace.d.ts +1746 -577
- package/dist/generated/prisma/internal/prismaNamespace.js +179 -6
- package/dist/generated/prisma/internal/prismaNamespaceBrowser.d.ts +186 -0
- package/dist/generated/prisma/internal/prismaNamespaceBrowser.js +179 -6
- package/dist/generated/prisma/models/Agent.d.ts +294 -1
- package/dist/generated/prisma/models/AgentRepository.d.ts +1425 -0
- package/dist/generated/prisma/models/AgentRepository.js +1 -0
- package/dist/generated/prisma/models/AgentTool.d.ts +95 -1
- package/dist/generated/prisma/models/AuthUser.d.ts +56 -1
- package/dist/generated/prisma/models/CodingAgentProfile.d.ts +258 -7
- package/dist/generated/prisma/models/CodingProxySession.d.ts +123 -2
- package/dist/generated/prisma/models/CodingRun.d.ts +1276 -95
- package/dist/generated/prisma/models/CodingService.d.ts +1348 -0
- package/dist/generated/prisma/models/CodingService.js +1 -0
- package/dist/generated/prisma/models/HostEventDelivery.d.ts +946 -0
- package/dist/generated/prisma/models/HostEventDelivery.js +1 -0
- package/dist/generated/prisma/models/HostIdentity.d.ts +1232 -0
- package/dist/generated/prisma/models/HostIdentity.js +1 -0
- package/dist/generated/prisma/models/HostIdentityLinkRequest.d.ts +1473 -0
- package/dist/generated/prisma/models/HostIdentityLinkRequest.js +1 -0
- package/dist/generated/prisma/models/Principal.d.ts +455 -0
- package/dist/generated/prisma/models/RegistryAllowance.d.ts +1148 -0
- package/dist/generated/prisma/models/RegistryAllowance.js +1 -0
- package/dist/generated/prisma/models/RegistryApprovedVersion.d.ts +1219 -0
- package/dist/generated/prisma/models/RegistryApprovedVersion.js +1 -0
- package/dist/generated/prisma/models/RegistryFetch.d.ts +1428 -0
- package/dist/generated/prisma/models/RegistryFetch.js +1 -0
- package/dist/generated/prisma/models/RegistryPlanRefusal.d.ts +1294 -0
- package/dist/generated/prisma/models/RegistryPlanRefusal.js +1 -0
- package/dist/generated/prisma/models/RegistryVersionFact.d.ts +1085 -0
- package/dist/generated/prisma/models/RegistryVersionFact.js +1 -0
- package/dist/generated/prisma/models/ResourceGrant.d.ts +1437 -0
- package/dist/generated/prisma/models/ResourceGrant.js +1 -0
- package/dist/generated/prisma/models/Run.d.ts +389 -1
- package/dist/generated/prisma/models/RunHostCheck.d.ts +1239 -0
- package/dist/generated/prisma/models/RunHostCheck.js +1 -0
- package/dist/generated/prisma/models/RunHostStatus.d.ts +1315 -0
- package/dist/generated/prisma/models/RunHostStatus.js +1 -0
- package/dist/generated/prisma/models/Tool.d.ts +15 -3
- package/dist/generated/prisma/models.d.ts +13 -0
- package/dist/help/build.d.ts +1 -0
- package/dist/help/build.js +9 -0
- package/dist/help/catalog.d.ts +24 -0
- package/dist/help/catalog.js +160 -0
- package/dist/help/cli.d.ts +2 -0
- package/dist/help/cli.js +65 -0
- package/dist/help/runtime.d.ts +3 -0
- package/dist/help/runtime.js +44 -0
- package/dist/help/search.d.ts +9 -0
- package/dist/help/search.js +104 -0
- package/dist/help-index.json +660 -0
- package/dist/import/cli-args.js +3 -2
- package/dist/import/create.d.ts +5 -0
- package/dist/import/create.js +67 -13
- package/dist/import/index.js +19 -9
- package/dist/import/neutral-schema.d.ts +11 -11
- package/dist/mcp/auth/access.d.ts +70 -0
- package/dist/mcp/auth/access.js +58 -0
- package/dist/mcp/auth/grants-cli.d.ts +149 -0
- package/dist/mcp/auth/grants-cli.js +518 -0
- package/dist/mcp/auth/host-account-cli.d.ts +2 -0
- package/dist/mcp/auth/host-account-cli.js +47 -0
- package/dist/mcp/auth/ownership.d.ts +26 -52
- package/dist/mcp/auth/ownership.js +19 -14
- package/dist/mcp/auth/repo-authorization.d.ts +22 -0
- package/dist/mcp/auth/repo-authorization.js +51 -0
- package/dist/mcp/auth/resource-server.d.ts +31 -2
- package/dist/mcp/auth/resource-server.js +84 -3
- package/dist/mcp/auth/self-hosted/browser.js +2 -2
- package/dist/mcp/auth/self-hosted/cli.js +47 -13
- package/dist/mcp/auth/self-hosted/credentials.d.ts +20 -2
- package/dist/mcp/auth/self-hosted/credentials.js +62 -4
- package/dist/mcp/auth/self-hosted/session.d.ts +2 -1
- package/dist/mcp/context.d.ts +27 -1
- package/dist/mcp/errors.d.ts +25 -6
- package/dist/mcp/errors.js +98 -0
- package/dist/mcp/host-events/github-ingress.d.ts +36 -0
- package/dist/mcp/host-events/github-ingress.js +92 -0
- package/dist/mcp/host-events/github-user-callback.d.ts +18 -0
- package/dist/mcp/host-events/github-user-callback.js +50 -0
- package/dist/mcp/index.d.ts +8 -1
- package/dist/mcp/index.js +106 -16
- package/dist/mcp/server.js +27 -9
- package/dist/mcp/tools/agents.js +446 -48
- package/dist/mcp/tools/budget-groups.js +3 -3
- package/dist/mcp/tools/datastore.js +22 -15
- package/dist/mcp/tools/grants.d.ts +2 -0
- package/dist/mcp/tools/grants.js +239 -0
- package/dist/mcp/tools/help.d.ts +5 -0
- package/dist/mcp/tools/help.js +67 -0
- package/dist/mcp/tools/host-accounts.d.ts +2 -0
- package/dist/mcp/tools/host-accounts.js +114 -0
- package/dist/mcp/tools/memory.d.ts +7 -1
- package/dist/mcp/tools/memory.js +5 -5
- package/dist/mcp/tools/repositories.d.ts +2 -0
- package/dist/mcp/tools/repositories.js +181 -0
- package/dist/mcp/tools/runs.d.ts +6 -0
- package/dist/mcp/tools/runs.js +60 -6
- package/dist/mcp/tools/scheduling.js +3 -6
- package/dist/mcp/tools/secrets.js +18 -6
- package/dist/mcp/tools/services.d.ts +2 -0
- package/dist/mcp/tools/services.js +222 -0
- package/dist/mcp/tools/subagents.js +52 -16
- package/dist/mcp/tools/tools.d.ts +41 -0
- package/dist/mcp/tools/tools.js +201 -40
- package/dist/mcp/tools/trigger.js +36 -6
- package/dist/mcp/tools/webhooks.js +15 -4
- package/dist/mcp/transport/streamable-http.d.ts +10 -0
- package/dist/mcp/transport/streamable-http.js +32 -2
- package/dist/providers/auth/delegating.d.ts +19 -0
- package/dist/providers/auth/delegating.js +71 -0
- package/dist/providers/auth/index.d.ts +2 -0
- package/dist/providers/auth/index.js +11 -0
- package/dist/providers/auth/self-hosted.d.ts +2 -1
- package/dist/providers/auth/self-hosted.js +16 -4
- package/dist/providers/auth/types.d.ts +6 -0
- package/dist/providers/coding-proxy/memory-ledger.d.ts +3 -0
- package/dist/providers/coding-proxy/memory-ledger.js +21 -2
- package/dist/providers/coding-proxy/metering.d.ts +4 -0
- package/dist/providers/coding-proxy/metering.js +28 -2
- package/dist/providers/coding-proxy/prisma-ledger.d.ts +17 -0
- package/dist/providers/coding-proxy/prisma-ledger.js +50 -5
- package/dist/providers/coding-proxy/proxy.d.ts +13 -0
- package/dist/providers/coding-proxy/proxy.js +479 -18
- package/dist/providers/coding-proxy/registry/audit.d.ts +131 -0
- package/dist/providers/coding-proxy/registry/audit.js +380 -0
- package/dist/providers/coding-proxy/registry/bounded-fetch.d.ts +16 -0
- package/dist/providers/coding-proxy/registry/bounded-fetch.js +77 -0
- package/dist/providers/coding-proxy/registry/plan.d.ts +58 -0
- package/dist/providers/coding-proxy/registry/plan.js +304 -0
- package/dist/providers/coding-proxy/registry/prisma-store.d.ts +34 -0
- package/dist/providers/coding-proxy/registry/prisma-store.js +151 -0
- package/dist/providers/coding-proxy/registry/service.d.ts +213 -0
- package/dist/providers/coding-proxy/registry/service.js +1137 -0
- package/dist/providers/coding-proxy/registry/store.d.ts +127 -0
- package/dist/providers/coding-proxy/registry/store.js +70 -0
- package/dist/providers/coding-proxy/runtime.d.ts +17 -0
- package/dist/providers/coding-proxy/runtime.js +63 -0
- package/dist/providers/coding-proxy/secure-fetch.js +0 -1
- package/dist/providers/coding-proxy/server.d.ts +11 -0
- package/dist/providers/coding-proxy/server.js +163 -0
- package/dist/providers/coding-proxy/types.d.ts +15 -0
- package/dist/providers/engine/types.d.ts +10 -0
- package/dist/providers/executor/build.d.ts +2 -2
- package/dist/providers/executor/composition.d.ts +3 -0
- package/dist/providers/executor/composition.js +19 -0
- package/dist/providers/executor/container.d.ts +105 -3
- package/dist/providers/executor/container.js +296 -20
- package/dist/providers/executor/dbos.d.ts +2 -3
- package/dist/providers/executor/in-process.d.ts +2 -3
- package/dist/providers/executor/routing.d.ts +7 -0
- package/dist/providers/executor/routing.js +13 -0
- package/dist/providers/executor/types.d.ts +22 -0
- package/dist/providers/jobs/claude-tool-setup.d.ts +23 -0
- package/dist/providers/jobs/claude-tool-setup.js +50 -0
- package/dist/providers/jobs/collect-prune.d.ts +7 -0
- package/dist/providers/jobs/collect-prune.js +27 -0
- package/dist/providers/jobs/docker-isolation.d.ts +48 -3
- package/dist/providers/jobs/docker-isolation.js +213 -22
- package/dist/providers/jobs/docker-services.d.ts +35 -0
- package/dist/providers/jobs/docker-services.js +191 -0
- package/dist/providers/jobs/docker.d.ts +49 -2
- package/dist/providers/jobs/docker.js +279 -32
- package/dist/providers/jobs/fake-kubernetes-api.d.ts +1 -0
- package/dist/providers/jobs/fake-kubernetes-api.js +12 -3
- package/dist/providers/jobs/kubernetes-isolation.d.ts +17 -2
- package/dist/providers/jobs/kubernetes-isolation.js +238 -57
- package/dist/providers/jobs/kubernetes-platform.d.ts +10 -4
- package/dist/providers/jobs/kubernetes-platform.js +11 -5
- package/dist/providers/jobs/kubernetes-preflight.js +3 -0
- package/dist/providers/jobs/kubernetes.d.ts +21 -2
- package/dist/providers/jobs/kubernetes.js +112 -21
- package/dist/providers/jobs/types.d.ts +18 -0
- package/dist/providers/llm/anthropic.js +6 -2
- package/dist/providers/llm/bedrock.js +2 -1
- package/dist/providers/llm/claude-messages.d.ts +5 -1
- package/dist/providers/llm/claude-messages.js +1 -0
- package/dist/providers/llm/claude-provider.d.ts +3 -1
- package/dist/providers/llm/claude-provider.js +5 -1
- package/dist/providers/llm/index.d.ts +1 -1
- package/dist/providers/llm/index.js +1 -1
- package/dist/providers/llm/pricing-anthropic.d.ts +10 -0
- package/dist/providers/llm/pricing-anthropic.js +32 -14
- package/dist/providers/llm/pricing-bedrock-claude.d.ts +8 -0
- package/dist/providers/llm/pricing-bedrock-claude.js +9 -0
- package/dist/providers/llm/routing.d.ts +10 -1
- package/dist/providers/llm/routing.js +18 -0
- package/dist/providers/llm/types.d.ts +12 -0
- package/dist/providers/llm/types.js +8 -1
- package/dist/providers/review-host/diff-lines.d.ts +16 -0
- package/dist/providers/review-host/diff-lines.js +59 -0
- package/dist/providers/review-host/github-events.d.ts +6 -0
- package/dist/providers/review-host/github-events.js +180 -0
- package/dist/providers/review-host/github-user-auth.d.ts +38 -0
- package/dist/providers/review-host/github-user-auth.js +128 -0
- package/dist/providers/review-host/github.d.ts +57 -0
- package/dist/providers/review-host/github.js +567 -0
- package/dist/providers/review-host/index.d.ts +12 -0
- package/dist/providers/review-host/index.js +30 -0
- package/dist/providers/review-host/review-format.d.ts +19 -0
- package/dist/providers/review-host/review-format.js +59 -0
- package/dist/providers/review-host/types.d.ts +284 -0
- package/dist/providers/review-host/types.js +24 -0
- package/dist/providers/vcs/git.d.ts +16 -6
- package/dist/providers/vcs/git.js +77 -13
- package/dist/providers/vcs/github.d.ts +74 -3
- package/dist/providers/vcs/github.js +195 -15
- package/dist/providers/vcs/types.d.ts +42 -4
- package/dist/quickstart/index.js +13 -2
- package/dist/sandbox/fetch-policy.d.ts +20 -2
- package/dist/sandbox/fetch-policy.js +64 -3
- package/dist/sandbox/host-functions.d.ts +9 -1
- package/dist/sandbox/host-functions.js +12 -5
- package/dist/serve.js +1 -1
- package/dist/wardby-bin.js +6 -0
- package/docs/README.md +30 -0
- package/docs/architecture-runtime.md +90 -0
- package/docs/assets/brand/wardby-icon-512.png +0 -0
- package/docs/assets/brand/wardby-icon.svg +16 -0
- package/docs/assets/brand/wardby-mascot-profile-512.png +0 -0
- package/docs/assets/brand/wardby-mascot.png +0 -0
- package/docs/assets/brand/wardby-mascot.svg +5 -0
- package/docs/assets/wardby-workflow.svg +106 -0
- package/docs/code-review-agents.md +481 -0
- package/docs/coding-agent-setup.md +172 -0
- package/docs/coding-packages.md +455 -0
- package/docs/coding-services.md +300 -0
- package/docs/coding-worker-byo-images.md +98 -0
- package/docs/coding-worker-isolation.md +1061 -0
- package/docs/getting-started-gke.md +615 -0
- package/docs/getting-started-identity-provider.md +308 -0
- package/docs/getting-started.md +133 -0
- package/docs/observability.md +53 -0
- package/docs/release-verification.md +66 -0
- package/docs/security-deployment.md +657 -0
- package/help/code-review-agents.md +31 -0
- package/help/coding-packages.md +31 -0
- package/help/coding-services.md +71 -0
- package/help/creating-agents.md +68 -0
- package/help/deploy-gke.md +39 -0
- package/help/deployment-targets.md +39 -0
- package/help/errors/budget-group-exhausted.md +25 -0
- package/help/errors/docker-isolation-unsupported.md +26 -0
- package/help/errors/protected-path.md +45 -0
- package/help/errors/repo-access.md +27 -0
- package/help/errors/service-declaration-invalid.md +27 -0
- package/help/errors/service-declaration-unavailable.md +25 -0
- package/help/errors/service-launcher-unsupported.md +30 -0
- package/help/errors/service-not-allowed.md +27 -0
- package/help/errors/service-unknown.md +23 -0
- package/help/errors/service-unready.md +49 -0
- package/help/getting-started.md +31 -0
- package/help/github.md +30 -0
- package/help/identity-and-access.md +39 -0
- package/help/mcp.md +30 -0
- package/help/native-capabilities.md +30 -0
- package/help/observability.md +41 -0
- package/help/operating-agents.md +30 -0
- package/help/security.md +27 -0
- package/help/troubleshooting/budgets.md +26 -0
- package/help/troubleshooting/coding-workers.md +47 -0
- package/help/troubleshooting/repository-access.md +27 -0
- package/package.json +12 -3
- package/prisma/migrations/20260925010000_coding_collect_exclude/migration.sql +8 -0
- package/prisma/migrations/20260925015000_allowed_egress_default/migration.sql +8 -0
- package/prisma/migrations/20260925020000_coding_package_registry/migration.sql +55 -0
- package/prisma/migrations/20260925030000_tool_name_per_owner/migration.sql +11 -0
- package/prisma/migrations/20260926010000_run_trigger_host_event/migration.sql +7 -0
- package/prisma/migrations/20260926020000_code_review_hosts/migration.sql +53 -0
- package/prisma/migrations/20260926030000_auth_user_roles/migration.sql +9 -0
- package/prisma/migrations/20260926040000_agent_effort/migration.sql +6 -0
- package/prisma/migrations/20260926050000_registry_lockfile_plan/migration.sql +32 -0
- package/prisma/migrations/20260926050000_repo_access_authorization/migration.sql +82 -0
- package/prisma/migrations/20260926060000_registry_plan_refusal/migration.sql +19 -0
- package/prisma/migrations/20260926100000_registry_plan_refusal_published_at/migration.sql +5 -0
- package/prisma/migrations/20260926190000_run_host_status/migration.sql +18 -0
- package/prisma/migrations/20260926210000_run_host_status_at_dispatch/migration.sql +5 -0
- package/prisma/migrations/20260927010000_resource_grants/migration.sql +72 -0
- package/prisma/migrations/20260927020000_proxy_session_budget_exhausted/migration.sql +3 -0
- package/prisma/migrations/20260927030000_coding_debug_trace/migration.sql +4 -0
- package/prisma/migrations/20260927040000_proxy_session_upstream_failure/migration.sql +3 -0
- package/prisma/migrations/20260927050000_coding_run_services/migration.sql +74 -0
- package/prisma/migrations/20260928000000_coding_run_tool_image/migration.sql +5 -0
- package/prisma/schema.prisma +407 -21
|
@@ -0,0 +1,567 @@
|
|
|
1
|
+
import { normalizeGitHubRepository } from "../../coding/protocol.js";
|
|
2
|
+
import { partitionComments } from "./diff-lines.js";
|
|
3
|
+
import { findingMarker, hasFindingMarker, hasReviewMarker, parseReviewMarker, renderInlineComment, renderSummaryComment, verdictConclusion, } from "./review-format.js";
|
|
4
|
+
import { ReviewHostError, } from "./types.js";
|
|
5
|
+
const READ = { contents: "read", pull_requests: "read" };
|
|
6
|
+
const SAFE_SHA = /^[0-9a-f]{40}$/;
|
|
7
|
+
const MAX_FILE_PAGES = 3;
|
|
8
|
+
const MAX_COMMENT_PAGES = 3;
|
|
9
|
+
const MAX_LISTED_FILES = 1000;
|
|
10
|
+
const VENDORED = /(^|\/)(node_modules|dist|\.venv)\//;
|
|
11
|
+
const CHECK_TEXT_LIMIT = 65_535;
|
|
12
|
+
const COMMENT_WRITE = { issues: "write", pull_requests: "write" };
|
|
13
|
+
const REVIEW_WRITE = { pull_requests: "write", checks: "write" };
|
|
14
|
+
/** Always implicitly granted to an installation token; enough for the collaborator-permission endpoint. */
|
|
15
|
+
const METADATA_READ = { metadata: "read" };
|
|
16
|
+
const GITHUB_USER_ID = /^[1-9]\d{0,19}$/;
|
|
17
|
+
const GITHUB_LOGIN = /^[A-Za-z0-9](?:[A-Za-z0-9-]{0,38})$/;
|
|
18
|
+
/** Highest first: user.permissions booleans, which (unlike the legacy field) keep maintain and triage apart. */
|
|
19
|
+
const PERMISSION_FLAGS = [
|
|
20
|
+
["admin", "admin"],
|
|
21
|
+
["maintain", "maintain"],
|
|
22
|
+
["push", "write"],
|
|
23
|
+
["triage", "triage"],
|
|
24
|
+
["pull", "read"],
|
|
25
|
+
];
|
|
26
|
+
const LEGACY_PERMISSIONS = {
|
|
27
|
+
admin: "admin",
|
|
28
|
+
maintain: "maintain",
|
|
29
|
+
write: "write",
|
|
30
|
+
triage: "triage",
|
|
31
|
+
read: "read",
|
|
32
|
+
};
|
|
33
|
+
const CHECK_TITLES = { APPROVE: "Approved", CHANGES_REQUESTED: "Changes requested", COMMENT: "Comments" };
|
|
34
|
+
function record(value) {
|
|
35
|
+
if (!value || typeof value !== "object" || Array.isArray(value))
|
|
36
|
+
throw new ReviewHostError("host_invalid_response");
|
|
37
|
+
return value;
|
|
38
|
+
}
|
|
39
|
+
function list(value) {
|
|
40
|
+
if (!Array.isArray(value))
|
|
41
|
+
throw new ReviewHostError("host_invalid_response");
|
|
42
|
+
return value.map(record);
|
|
43
|
+
}
|
|
44
|
+
function str(value) {
|
|
45
|
+
if (typeof value !== "string")
|
|
46
|
+
throw new ReviewHostError("host_invalid_response");
|
|
47
|
+
return value;
|
|
48
|
+
}
|
|
49
|
+
function num(value) {
|
|
50
|
+
if (typeof value !== "number" || !Number.isFinite(value))
|
|
51
|
+
throw new ReviewHostError("host_invalid_response");
|
|
52
|
+
return value;
|
|
53
|
+
}
|
|
54
|
+
/** Maps GitHubAppClient's fixed error messages onto host-neutral codes; keeps the body-free message. */
|
|
55
|
+
export function toReviewHostError(err) {
|
|
56
|
+
if (err instanceof ReviewHostError)
|
|
57
|
+
return err;
|
|
58
|
+
const message = err instanceof Error ? err.message : String(err);
|
|
59
|
+
if (message === "github_app_not_installed")
|
|
60
|
+
return new ReviewHostError("host_not_installed", message);
|
|
61
|
+
if (message === "github_installation_token_scope_invalid") {
|
|
62
|
+
return new ReviewHostError("host_permission_missing", message);
|
|
63
|
+
}
|
|
64
|
+
if (message === "github_api_invalid_response")
|
|
65
|
+
return new ReviewHostError("host_invalid_response", message);
|
|
66
|
+
if (message.startsWith("github_api_error") || message === "github_api_unavailable") {
|
|
67
|
+
return new ReviewHostError("host_api_error", message);
|
|
68
|
+
}
|
|
69
|
+
return new ReviewHostError("host_api_error", "github_request_failed");
|
|
70
|
+
}
|
|
71
|
+
/** Ranks a collaborator-permission answer: user.permissions first, then the legacy permission field. */
|
|
72
|
+
function permissionLevel(answer) {
|
|
73
|
+
const user = answer.user && typeof answer.user === "object" ? answer.user : null;
|
|
74
|
+
const flags = user?.permissions && typeof user.permissions === "object" ? user.permissions : null;
|
|
75
|
+
if (flags)
|
|
76
|
+
return PERMISSION_FLAGS.find(([flag]) => flags[flag] === true)?.[1] ?? "none";
|
|
77
|
+
return typeof answer.permission === "string" ? (LEGACY_PERMISSIONS[answer.permission] ?? "none") : "none";
|
|
78
|
+
}
|
|
79
|
+
/** True only for a comment the wardby App itself wrote; anyone can type a marker into a comment. */
|
|
80
|
+
function writtenByApp(comment, appId) {
|
|
81
|
+
const app = comment.performed_via_github_app;
|
|
82
|
+
return !!app && typeof app === "object" && app.id === appId;
|
|
83
|
+
}
|
|
84
|
+
function repoPath(repository) {
|
|
85
|
+
const [owner, name] = normalizeGitHubRepository(repository).split("/");
|
|
86
|
+
return `/repos/${encodeURIComponent(owner)}/${encodeURIComponent(name)}`;
|
|
87
|
+
}
|
|
88
|
+
function assertRelativePath(path) {
|
|
89
|
+
if (!path || path.startsWith("/") || path.split("/").some((part) => part === ".." || part === "")) {
|
|
90
|
+
throw new ReviewHostError("host_api_error", "path_invalid");
|
|
91
|
+
}
|
|
92
|
+
}
|
|
93
|
+
function fileView(raw) {
|
|
94
|
+
return {
|
|
95
|
+
filename: str(raw.filename),
|
|
96
|
+
status: str(raw.status),
|
|
97
|
+
additions: num(raw.additions),
|
|
98
|
+
deletions: num(raw.deletions),
|
|
99
|
+
previousFilename: typeof raw.previous_filename === "string" ? raw.previous_filename : undefined,
|
|
100
|
+
patch: "",
|
|
101
|
+
patchTruncated: false,
|
|
102
|
+
fullPatch: typeof raw.patch === "string" ? raw.patch : undefined,
|
|
103
|
+
changes: typeof raw.changes === "number" ? raw.changes : num(raw.additions) + num(raw.deletions),
|
|
104
|
+
};
|
|
105
|
+
}
|
|
106
|
+
const THREADS_QUERY = `query($owner: String!, $name: String!, $number: Int!) {
|
|
107
|
+
repository(owner: $owner, name: $name) {
|
|
108
|
+
pullRequest(number: $number) {
|
|
109
|
+
reviewThreads(first: 100) {
|
|
110
|
+
nodes { id isResolved isOutdated path line comments(first: 1) { nodes { body author { __typename login } } } }
|
|
111
|
+
}
|
|
112
|
+
}
|
|
113
|
+
}
|
|
114
|
+
}`;
|
|
115
|
+
const RESOLVE_MUTATION = `mutation($threadId: ID!) {
|
|
116
|
+
resolveReviewThread(input: { threadId: $threadId }) { thread { id isResolved } }
|
|
117
|
+
}`;
|
|
118
|
+
/** resolveReviewThread is gated on Contents write for App tokens, although it changes no content. */
|
|
119
|
+
const RESOLVE_WRITE = { contents: "write", pull_requests: "write" };
|
|
120
|
+
const MAX_THREAD_BODY = 1000;
|
|
121
|
+
/**
|
|
122
|
+
* The agent's own unresolved threads: the first comment was written by this
|
|
123
|
+
* App (a Bot author with the App's login) and carries the agent's finding
|
|
124
|
+
* marker. Anyone can type a marker; only the App can be a Bot with its login.
|
|
125
|
+
*/
|
|
126
|
+
async function ownOpenThreads(graphql, repository, prNumber, agentMarker, slug) {
|
|
127
|
+
const [owner, name] = normalizeGitHubRepository(repository).split("/");
|
|
128
|
+
const data = await graphql(THREADS_QUERY, { owner, name, number: prNumber });
|
|
129
|
+
const threads = record(record(record(data.repository).pullRequest).reviewThreads);
|
|
130
|
+
const nodes = list(threads.nodes ?? []);
|
|
131
|
+
const marker = findingMarker(agentMarker);
|
|
132
|
+
const out = [];
|
|
133
|
+
for (const node of nodes) {
|
|
134
|
+
if (node.isResolved !== false || typeof node.id !== "string")
|
|
135
|
+
continue;
|
|
136
|
+
const first = list(record(node.comments).nodes ?? [])[0];
|
|
137
|
+
const author = first && first.author && typeof first.author === "object" ? first.author : null;
|
|
138
|
+
if (author?.__typename !== "Bot" || (author.login !== slug && author.login !== `${slug}[bot]`))
|
|
139
|
+
continue;
|
|
140
|
+
if (typeof first.body !== "string" || !hasFindingMarker(first.body, agentMarker))
|
|
141
|
+
continue;
|
|
142
|
+
const outdated = node.isOutdated === true;
|
|
143
|
+
out.push({
|
|
144
|
+
id: node.id,
|
|
145
|
+
path: str(node.path),
|
|
146
|
+
line: !outdated && typeof node.line === "number" ? node.line : null,
|
|
147
|
+
outdated,
|
|
148
|
+
body: first.body.replace(marker, "").trim().slice(0, MAX_THREAD_BODY),
|
|
149
|
+
});
|
|
150
|
+
}
|
|
151
|
+
return out;
|
|
152
|
+
}
|
|
153
|
+
export class GitHubReviewHost {
|
|
154
|
+
client;
|
|
155
|
+
provider = "github";
|
|
156
|
+
constructor(client) {
|
|
157
|
+
this.client = client;
|
|
158
|
+
}
|
|
159
|
+
/** Runs `action` with a token for exactly `permissions`, translating failures to ReviewHostError. */
|
|
160
|
+
async withToken(repository, permissions, action) {
|
|
161
|
+
try {
|
|
162
|
+
return await this.client.withScopedToken(repository, permissions, (token) => action((path, init, expected) => this.client.requestJson(path, token, init, expected), (query, variables) => this.client.graphql(token, query, variables)));
|
|
163
|
+
}
|
|
164
|
+
catch (err) {
|
|
165
|
+
throw toReviewHostError(err);
|
|
166
|
+
}
|
|
167
|
+
}
|
|
168
|
+
async repositoryPermission(repository, user) {
|
|
169
|
+
if (!GITHUB_USER_ID.test(user.id) || !GITHUB_LOGIN.test(user.login)) {
|
|
170
|
+
throw new ReviewHostError("host_api_error", "host_user_invalid");
|
|
171
|
+
}
|
|
172
|
+
const base = repoPath(repository);
|
|
173
|
+
return this.withToken(repository, METADATA_READ, async (get) => {
|
|
174
|
+
// Null = GitHub answered for no one (404) or for a different account: never trust it.
|
|
175
|
+
const ask = async (login) => {
|
|
176
|
+
const response = await get(`${base}/collaborators/${encodeURIComponent(login)}/permission`, {}, [200, 404]);
|
|
177
|
+
if (response.status === 404)
|
|
178
|
+
return null;
|
|
179
|
+
const answer = record(await response.json());
|
|
180
|
+
const answeredFor = answer.user && typeof answer.user === "object" ? answer.user.id : undefined;
|
|
181
|
+
if (!Number.isSafeInteger(answeredFor) || String(answeredFor) !== user.id)
|
|
182
|
+
return null;
|
|
183
|
+
return permissionLevel(answer);
|
|
184
|
+
};
|
|
185
|
+
const level = await ask(user.login);
|
|
186
|
+
if (level !== null)
|
|
187
|
+
return { level, login: user.login };
|
|
188
|
+
// The login may have been renamed (or recycled): resolve the account by id and retry once.
|
|
189
|
+
const response = await get(`/user/${user.id}`, {}, [200, 404]);
|
|
190
|
+
if (response.status === 404)
|
|
191
|
+
return { level: "none", login: user.login };
|
|
192
|
+
const current = record(await response.json());
|
|
193
|
+
const login = current.login;
|
|
194
|
+
if (String(current.id) !== user.id || typeof login !== "string" || !GITHUB_LOGIN.test(login)) {
|
|
195
|
+
return { level: "none", login: user.login };
|
|
196
|
+
}
|
|
197
|
+
if (login.toLowerCase() === user.login.toLowerCase())
|
|
198
|
+
return { level: "none", login };
|
|
199
|
+
return { level: (await ask(login)) ?? "none", login };
|
|
200
|
+
});
|
|
201
|
+
}
|
|
202
|
+
async pullRequestHead(repository, prNumber) {
|
|
203
|
+
return this.withToken(repository, READ, async (get) => {
|
|
204
|
+
const pr = record(await (await get(`${repoPath(repository)}/pulls/${prNumber}`)).json());
|
|
205
|
+
return this.head(repository, pr);
|
|
206
|
+
});
|
|
207
|
+
}
|
|
208
|
+
head(repository, pr) {
|
|
209
|
+
const head = record(pr.head);
|
|
210
|
+
const headRepo = head.repo && typeof head.repo === "object" ? record(head.repo) : null;
|
|
211
|
+
const headSha = str(head.sha);
|
|
212
|
+
if (!SAFE_SHA.test(headSha))
|
|
213
|
+
throw new ReviewHostError("host_invalid_response");
|
|
214
|
+
const isFork = !headRepo ||
|
|
215
|
+
typeof headRepo.full_name !== "string" ||
|
|
216
|
+
headRepo.full_name.toLowerCase() !== normalizeGitHubRepository(repository);
|
|
217
|
+
return { headSha, isFork, state: str(pr.state) };
|
|
218
|
+
}
|
|
219
|
+
async readPullRequest(repository, prNumber, opts) {
|
|
220
|
+
const base = repoPath(repository);
|
|
221
|
+
return this.withToken(repository, READ, async (get, graphql) => {
|
|
222
|
+
const pr = record(await (await get(`${base}/pulls/${prNumber}`)).json());
|
|
223
|
+
const { headSha, isFork, state } = this.head(repository, pr);
|
|
224
|
+
const prFiles = async () => {
|
|
225
|
+
const all = [];
|
|
226
|
+
for (let page = 1; page <= MAX_FILE_PAGES; page++) {
|
|
227
|
+
const batch = list(await (await get(`${base}/pulls/${prNumber}/files?per_page=100&page=${page}`)).json());
|
|
228
|
+
all.push(...batch);
|
|
229
|
+
if (batch.length < 100)
|
|
230
|
+
break;
|
|
231
|
+
}
|
|
232
|
+
return all;
|
|
233
|
+
};
|
|
234
|
+
let rawFiles = null;
|
|
235
|
+
let comparedFrom = null;
|
|
236
|
+
let baseMergedSince = false;
|
|
237
|
+
if (opts.sinceSha && SAFE_SHA.test(opts.sinceSha) && opts.sinceSha !== headSha) {
|
|
238
|
+
const compare = record(await (await get(`${base}/compare/${opts.sinceSha}...${headSha}`)).json());
|
|
239
|
+
if (compare.status === "ahead") {
|
|
240
|
+
rawFiles = list(compare.files ?? []);
|
|
241
|
+
comparedFrom = opts.sinceSha;
|
|
242
|
+
// A merge commit since the last review (typically the base branch
|
|
243
|
+
// merged in) makes the compare carry every base-branch change too.
|
|
244
|
+
// The PR's own diff is always relative to the base, so it holds
|
|
245
|
+
// only the PR's changes; keep those in files that moved since.
|
|
246
|
+
baseMergedSince = list(compare.commits ?? []).some((c) => list(c.parents ?? []).length > 1);
|
|
247
|
+
if (baseMergedSince) {
|
|
248
|
+
const changedSince = new Set(rawFiles.map((f) => f.filename));
|
|
249
|
+
rawFiles = (await prFiles()).filter((f) => changedSince.has(f.filename));
|
|
250
|
+
}
|
|
251
|
+
}
|
|
252
|
+
}
|
|
253
|
+
rawFiles ??= await prFiles();
|
|
254
|
+
let budget = opts.maxPatchChars;
|
|
255
|
+
const files = rawFiles.map((raw) => {
|
|
256
|
+
const { fullPatch, changes, ...view } = fileView(raw);
|
|
257
|
+
const patch = fullPatch ?? "";
|
|
258
|
+
const kept = patch.length <= budget ? patch : patch.slice(0, Math.max(0, budget));
|
|
259
|
+
budget -= kept.length;
|
|
260
|
+
return {
|
|
261
|
+
...view,
|
|
262
|
+
patch: kept,
|
|
263
|
+
patchTruncated: kept.length < patch.length || (fullPatch === undefined && changes > 0),
|
|
264
|
+
};
|
|
265
|
+
});
|
|
266
|
+
const { id: appId } = await this.client.appIdentity();
|
|
267
|
+
let lastReviewedSha = null;
|
|
268
|
+
for (let page = 1; page <= MAX_COMMENT_PAGES && !lastReviewedSha; page++) {
|
|
269
|
+
const comments = list(await (await get(`${base}/issues/${prNumber}/comments?per_page=100&page=${page}`)).json());
|
|
270
|
+
for (const comment of comments) {
|
|
271
|
+
if (typeof comment.body === "string" && writtenByApp(comment, appId)) {
|
|
272
|
+
lastReviewedSha ??= parseReviewMarker(comment.body, opts.agentMarker);
|
|
273
|
+
}
|
|
274
|
+
}
|
|
275
|
+
if (comments.length < 100)
|
|
276
|
+
break;
|
|
277
|
+
}
|
|
278
|
+
const { slug } = await this.client.appIdentity();
|
|
279
|
+
const openThreads = await ownOpenThreads(graphql, repository, prNumber, opts.agentMarker, slug);
|
|
280
|
+
const user = pr.user && typeof pr.user === "object" ? record(pr.user) : null;
|
|
281
|
+
return {
|
|
282
|
+
number: num(pr.number),
|
|
283
|
+
title: str(pr.title),
|
|
284
|
+
body: (typeof pr.body === "string" ? pr.body : "").slice(0, 8000),
|
|
285
|
+
author: user && typeof user.login === "string" ? user.login : null,
|
|
286
|
+
state,
|
|
287
|
+
merged: pr.merged === true,
|
|
288
|
+
draft: pr.draft === true,
|
|
289
|
+
baseRef: str(record(pr.base).ref),
|
|
290
|
+
headRef: str(record(pr.head).ref),
|
|
291
|
+
headSha,
|
|
292
|
+
isFork,
|
|
293
|
+
htmlUrl: str(pr.html_url),
|
|
294
|
+
lastReviewedSha,
|
|
295
|
+
comparedFrom,
|
|
296
|
+
baseMergedSince,
|
|
297
|
+
files,
|
|
298
|
+
openThreads,
|
|
299
|
+
};
|
|
300
|
+
});
|
|
301
|
+
}
|
|
302
|
+
async readFile(repository, path, ref, window) {
|
|
303
|
+
assertRelativePath(path);
|
|
304
|
+
const encoded = path.split("/").map(encodeURIComponent).join("/");
|
|
305
|
+
const query = ref ? `?ref=${encodeURIComponent(ref)}` : "";
|
|
306
|
+
return this.withToken(repository, { contents: "read" }, async (get) => {
|
|
307
|
+
const response = await get(`${repoPath(repository)}/contents/${encoded}${query}`, { headers: { accept: "application/vnd.github.raw+json" } }, [200, 404]);
|
|
308
|
+
if (response.status === 404)
|
|
309
|
+
return { kind: "not_found", path };
|
|
310
|
+
const text = await response.text();
|
|
311
|
+
if (text.trimStart().startsWith("[")) {
|
|
312
|
+
try {
|
|
313
|
+
const entries = JSON.parse(text);
|
|
314
|
+
if (Array.isArray(entries) &&
|
|
315
|
+
entries.every((e) => e && typeof e === "object" && "type" in e && "path" in e)) {
|
|
316
|
+
return {
|
|
317
|
+
kind: "directory",
|
|
318
|
+
path,
|
|
319
|
+
entries: entries.map((e) => `${e.type === "dir" ? "dir " : "file"} ${e.path}`),
|
|
320
|
+
};
|
|
321
|
+
}
|
|
322
|
+
}
|
|
323
|
+
catch {
|
|
324
|
+
// A file whose content happens to start with "[" — fall through.
|
|
325
|
+
}
|
|
326
|
+
}
|
|
327
|
+
const lines = text.split("\n");
|
|
328
|
+
const slice = lines.slice(window.startLine - 1, window.startLine - 1 + window.maxLines);
|
|
329
|
+
return {
|
|
330
|
+
kind: "file",
|
|
331
|
+
path,
|
|
332
|
+
ref: ref ?? "default branch",
|
|
333
|
+
totalLines: lines.length,
|
|
334
|
+
startLine: window.startLine,
|
|
335
|
+
endLine: window.startLine + slice.length - 1,
|
|
336
|
+
truncated: window.startLine - 1 + slice.length < lines.length,
|
|
337
|
+
content: slice.map((line, i) => `${window.startLine + i}: ${line}`).join("\n"),
|
|
338
|
+
};
|
|
339
|
+
});
|
|
340
|
+
}
|
|
341
|
+
async listFiles(repository, ref, pathPrefix = "") {
|
|
342
|
+
const base = repoPath(repository);
|
|
343
|
+
return this.withToken(repository, { contents: "read" }, async (get) => {
|
|
344
|
+
const resolvedRef = ref ?? str(record(await (await get(base)).json()).default_branch);
|
|
345
|
+
const tree = record(await (await get(`${base}/git/trees/${encodeURIComponent(resolvedRef)}?recursive=1`)).json());
|
|
346
|
+
const files = list(tree.tree)
|
|
347
|
+
.filter((e) => e.type === "blob" && typeof e.path === "string" && e.path.startsWith(pathPrefix))
|
|
348
|
+
.filter((e) => !VENDORED.test(str(e.path)) && !str(e.path).endsWith("package-lock.json"))
|
|
349
|
+
.map((e) => `${str(e.path)} (${num(e.size)} bytes)`);
|
|
350
|
+
return {
|
|
351
|
+
ref: resolvedRef,
|
|
352
|
+
count: files.length,
|
|
353
|
+
truncated: tree.truncated === true || files.length > MAX_LISTED_FILES,
|
|
354
|
+
files: files.slice(0, MAX_LISTED_FILES),
|
|
355
|
+
};
|
|
356
|
+
});
|
|
357
|
+
}
|
|
358
|
+
async publishReview(repository, input) {
|
|
359
|
+
const published = await this.publishOnly(repository, input);
|
|
360
|
+
if (!published.published)
|
|
361
|
+
return published;
|
|
362
|
+
const requested = [...new Set(input.resolveThreadIds ?? [])];
|
|
363
|
+
if (requested.length === 0)
|
|
364
|
+
return published;
|
|
365
|
+
return { ...published, ...(await this.resolveOwnThreads(repository, input, requested)) };
|
|
366
|
+
}
|
|
367
|
+
/**
|
|
368
|
+
* Resolves the requested threads that are still this agent's own open
|
|
369
|
+
* threads on the PR, each with its own GraphQL call; everything else is
|
|
370
|
+
* skipped. Never throws: the review is already published, so a refusal
|
|
371
|
+
* (for example an App without Contents write) only skips.
|
|
372
|
+
*/
|
|
373
|
+
async resolveOwnThreads(repository, input, requested) {
|
|
374
|
+
const resolvedThreadIds = [];
|
|
375
|
+
try {
|
|
376
|
+
const { slug } = await this.client.appIdentity();
|
|
377
|
+
await this.withToken(repository, RESOLVE_WRITE, async (_get, graphql) => {
|
|
378
|
+
const own = new Set((await ownOpenThreads(graphql, repository, input.prNumber, input.agentMarker, slug)).map((t) => t.id));
|
|
379
|
+
for (const threadId of requested) {
|
|
380
|
+
if (!own.has(threadId))
|
|
381
|
+
continue;
|
|
382
|
+
try {
|
|
383
|
+
await graphql(RESOLVE_MUTATION, { threadId });
|
|
384
|
+
resolvedThreadIds.push(threadId);
|
|
385
|
+
}
|
|
386
|
+
catch {
|
|
387
|
+
// Skipped below; the thread stays open.
|
|
388
|
+
}
|
|
389
|
+
}
|
|
390
|
+
});
|
|
391
|
+
}
|
|
392
|
+
catch {
|
|
393
|
+
// Listing or the token failed: every requested thread is skipped.
|
|
394
|
+
}
|
|
395
|
+
const resolved = new Set(resolvedThreadIds);
|
|
396
|
+
return { resolvedThreadIds, skippedThreadIds: requested.filter((id) => !resolved.has(id)) };
|
|
397
|
+
}
|
|
398
|
+
async publishOnly(repository, input) {
|
|
399
|
+
const base = repoPath(repository);
|
|
400
|
+
return this.withToken(repository, REVIEW_WRITE, async (get) => {
|
|
401
|
+
const pr = record(await (await get(`${base}/pulls/${input.prNumber}`)).json());
|
|
402
|
+
const { headSha } = this.head(repository, pr);
|
|
403
|
+
if (headSha !== input.headSha) {
|
|
404
|
+
if (input.checkId) {
|
|
405
|
+
await this.patchCheck(get, base, {
|
|
406
|
+
checkId: input.checkId,
|
|
407
|
+
conclusion: "neutral",
|
|
408
|
+
title: "Superseded by a newer push",
|
|
409
|
+
summary: `The pull request moved to ${headSha.slice(0, 7)} before this review finished.`,
|
|
410
|
+
});
|
|
411
|
+
}
|
|
412
|
+
return { published: false, reason: "stale_head", currentHeadSha: headSha };
|
|
413
|
+
}
|
|
414
|
+
const patches = new Map();
|
|
415
|
+
for (let page = 1; page <= MAX_FILE_PAGES; page++) {
|
|
416
|
+
const batch = list(await (await get(`${base}/pulls/${input.prNumber}/files?per_page=100&page=${page}`)).json());
|
|
417
|
+
for (const file of batch)
|
|
418
|
+
patches.set(str(file.filename), typeof file.patch === "string" ? file.patch : undefined);
|
|
419
|
+
if (batch.length < 100)
|
|
420
|
+
break;
|
|
421
|
+
}
|
|
422
|
+
const { inline, outside } = partitionComments(input.comments, patches);
|
|
423
|
+
let reviewUrl = null;
|
|
424
|
+
if (inline.length > 0) {
|
|
425
|
+
const review = record(await (await get(`${base}/pulls/${input.prNumber}/reviews`, {
|
|
426
|
+
method: "POST",
|
|
427
|
+
body: JSON.stringify({
|
|
428
|
+
commit_id: input.headSha,
|
|
429
|
+
event: "COMMENT",
|
|
430
|
+
body: `wardby review: ${inline.length} inline comment${inline.length === 1 ? "" : "s"} — the summary is in the conversation.`,
|
|
431
|
+
comments: inline.map((c) => ({
|
|
432
|
+
path: c.path,
|
|
433
|
+
line: c.line,
|
|
434
|
+
side: c.side,
|
|
435
|
+
body: renderInlineComment(c, input.agentMarker),
|
|
436
|
+
})),
|
|
437
|
+
}),
|
|
438
|
+
}, [200])).json());
|
|
439
|
+
reviewUrl = str(review.html_url);
|
|
440
|
+
}
|
|
441
|
+
const summaryBody = renderSummaryComment({
|
|
442
|
+
agentMarker: input.agentMarker,
|
|
443
|
+
headSha: input.headSha,
|
|
444
|
+
verdict: input.verdict,
|
|
445
|
+
summary: input.summary,
|
|
446
|
+
body: input.body,
|
|
447
|
+
outside,
|
|
448
|
+
});
|
|
449
|
+
const { id: appId } = await this.client.appIdentity();
|
|
450
|
+
const existing = await this.findSummaryComment(get, base, input.prNumber, input.agentMarker, appId);
|
|
451
|
+
const summaryResponse = existing
|
|
452
|
+
? await get(`${base}/issues/comments/${existing}`, { method: "PATCH", body: JSON.stringify({ body: summaryBody }) }, [200])
|
|
453
|
+
: await get(`${base}/issues/${input.prNumber}/comments`, { method: "POST", body: JSON.stringify({ body: summaryBody }) }, [201]);
|
|
454
|
+
const summaryCommentUrl = str(record(await summaryResponse.json()).html_url);
|
|
455
|
+
const conclusion = verdictConclusion(input.verdict);
|
|
456
|
+
const output = {
|
|
457
|
+
title: CHECK_TITLES[input.verdict],
|
|
458
|
+
summary: input.summary,
|
|
459
|
+
text: summaryBody.slice(0, CHECK_TEXT_LIMIT),
|
|
460
|
+
};
|
|
461
|
+
let checkId = input.checkId ?? null;
|
|
462
|
+
if (checkId) {
|
|
463
|
+
await this.patchCheck(get, base, { checkId, conclusion, ...output, detailsUrl: summaryCommentUrl });
|
|
464
|
+
}
|
|
465
|
+
else if (input.checkName) {
|
|
466
|
+
const created = record(await (await get(`${base}/check-runs`, {
|
|
467
|
+
method: "POST",
|
|
468
|
+
body: JSON.stringify({
|
|
469
|
+
name: input.checkName,
|
|
470
|
+
head_sha: input.headSha,
|
|
471
|
+
status: "completed",
|
|
472
|
+
conclusion,
|
|
473
|
+
details_url: summaryCommentUrl,
|
|
474
|
+
output,
|
|
475
|
+
}),
|
|
476
|
+
}, [201])).json());
|
|
477
|
+
checkId = String(num(created.id));
|
|
478
|
+
}
|
|
479
|
+
return {
|
|
480
|
+
published: true,
|
|
481
|
+
reviewUrl,
|
|
482
|
+
summaryCommentUrl,
|
|
483
|
+
checkId,
|
|
484
|
+
checkConclusion: conclusion,
|
|
485
|
+
inlineCount: inline.length,
|
|
486
|
+
outsideDiffCount: outside.length,
|
|
487
|
+
resolvedThreadIds: [],
|
|
488
|
+
skippedThreadIds: [],
|
|
489
|
+
};
|
|
490
|
+
});
|
|
491
|
+
}
|
|
492
|
+
async findSummaryComment(get, base, prNumber, agentMarker, appId) {
|
|
493
|
+
for (let page = 1; page <= MAX_COMMENT_PAGES; page++) {
|
|
494
|
+
const comments = list(await (await get(`${base}/issues/${prNumber}/comments?per_page=100&page=${page}`)).json());
|
|
495
|
+
const mine = comments.find((c) => typeof c.body === "string" && writtenByApp(c, appId) && hasReviewMarker(c.body, agentMarker));
|
|
496
|
+
if (mine)
|
|
497
|
+
return num(mine.id);
|
|
498
|
+
if (comments.length < 100)
|
|
499
|
+
break;
|
|
500
|
+
}
|
|
501
|
+
return null;
|
|
502
|
+
}
|
|
503
|
+
async patchCheck(get, base, input) {
|
|
504
|
+
await get(`${base}/check-runs/${encodeURIComponent(input.checkId)}`, {
|
|
505
|
+
method: "PATCH",
|
|
506
|
+
body: JSON.stringify({
|
|
507
|
+
status: "completed",
|
|
508
|
+
conclusion: input.conclusion,
|
|
509
|
+
...(input.detailsUrl ? { details_url: input.detailsUrl } : {}),
|
|
510
|
+
output: {
|
|
511
|
+
title: input.title,
|
|
512
|
+
summary: input.summary.slice(0, CHECK_TEXT_LIMIT),
|
|
513
|
+
...(input.text !== undefined ? { text: input.text.slice(0, CHECK_TEXT_LIMIT) } : {}),
|
|
514
|
+
},
|
|
515
|
+
}),
|
|
516
|
+
}, [200]);
|
|
517
|
+
}
|
|
518
|
+
async comment(repository, input) {
|
|
519
|
+
const base = repoPath(repository);
|
|
520
|
+
return this.withToken(repository, COMMENT_WRITE, async (get) => {
|
|
521
|
+
const response = input.replyToReviewCommentId
|
|
522
|
+
? await get(`${base}/pulls/${input.number}/comments/${encodeURIComponent(input.replyToReviewCommentId)}/replies`, { method: "POST", body: JSON.stringify({ body: input.body }) }, [201])
|
|
523
|
+
: await get(`${base}/issues/${input.number}/comments`, { method: "POST", body: JSON.stringify({ body: input.body }) }, [201]);
|
|
524
|
+
const created = record(await response.json());
|
|
525
|
+
return { url: str(created.html_url), id: String(num(created.id)) };
|
|
526
|
+
});
|
|
527
|
+
}
|
|
528
|
+
async editComment(repository, input) {
|
|
529
|
+
const base = repoPath(repository);
|
|
530
|
+
const id = encodeURIComponent(input.id);
|
|
531
|
+
const path = input.kind === "inline" ? `${base}/pulls/comments/${id}` : `${base}/issues/comments/${id}`;
|
|
532
|
+
await this.withToken(repository, COMMENT_WRITE, async (get) => {
|
|
533
|
+
await get(path, { method: "PATCH", body: JSON.stringify({ body: input.body }) }, [200]);
|
|
534
|
+
});
|
|
535
|
+
}
|
|
536
|
+
async acknowledge(repository, target) {
|
|
537
|
+
const base = repoPath(repository);
|
|
538
|
+
const id = encodeURIComponent(target.id);
|
|
539
|
+
const path = target.kind === "inline"
|
|
540
|
+
? `${base}/pulls/comments/${id}/reactions`
|
|
541
|
+
: target.kind === "subject"
|
|
542
|
+
? `${base}/issues/${id}/reactions`
|
|
543
|
+
: `${base}/issues/comments/${id}/reactions`;
|
|
544
|
+
await this.withToken(repository, COMMENT_WRITE, async (get) => {
|
|
545
|
+
await get(path, { method: "POST", body: JSON.stringify({ content: "eyes" }) }, [200, 201]);
|
|
546
|
+
});
|
|
547
|
+
}
|
|
548
|
+
async startCheck(repository, input) {
|
|
549
|
+
const base = repoPath(repository);
|
|
550
|
+
return this.withToken(repository, { checks: "write" }, async (get) => {
|
|
551
|
+
const created = record(await (await get(`${base}/check-runs`, {
|
|
552
|
+
method: "POST",
|
|
553
|
+
body: JSON.stringify({
|
|
554
|
+
name: input.name,
|
|
555
|
+
head_sha: input.headSha,
|
|
556
|
+
status: "in_progress",
|
|
557
|
+
output: { title: "Review in progress", summary: "wardby is reviewing this commit." },
|
|
558
|
+
}),
|
|
559
|
+
}, [201])).json());
|
|
560
|
+
return { checkId: String(num(created.id)) };
|
|
561
|
+
});
|
|
562
|
+
}
|
|
563
|
+
async completeCheck(repository, input) {
|
|
564
|
+
const base = repoPath(repository);
|
|
565
|
+
await this.withToken(repository, { checks: "write" }, (get) => this.patchCheck(get, base, input));
|
|
566
|
+
}
|
|
567
|
+
}
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
import type { HostUserAuthorizerRegistry, ReviewHostRegistry } from "./types.js";
|
|
2
|
+
export * from "./types.js";
|
|
3
|
+
export { GitHubReviewHost } from "./github.js";
|
|
4
|
+
export { GitHubUserAuthorizer } from "./github-user-auth.js";
|
|
5
|
+
/** One host per configured provider; empty when no GitHub App is configured (the repo_* tools are then never offered). */
|
|
6
|
+
export declare function buildReviewHosts(env?: NodeJS.ProcessEnv): ReviewHostRegistry;
|
|
7
|
+
/**
|
|
8
|
+
* One identity-linking flow per provider whose App has OAuth client
|
|
9
|
+
* credentials (GITHUB_APP_CLIENT_ID/SECRET). Empty = link_host_account is
|
|
10
|
+
* disabled; repository authorization is enforced either way.
|
|
11
|
+
*/
|
|
12
|
+
export declare function buildHostUserAuthorizers(env?: NodeJS.ProcessEnv): HostUserAuthorizerRegistry;
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
import { loadGitHubUserAuthConfig, loadGitHubVcsConfig } from "../../config/providers.js";
|
|
2
|
+
import { GitHubAppClient } from "../vcs/github.js";
|
|
3
|
+
import { GitHubReviewHost } from "./github.js";
|
|
4
|
+
import { GitHubUserAuthorizer } from "./github-user-auth.js";
|
|
5
|
+
export * from "./types.js";
|
|
6
|
+
export { GitHubReviewHost } from "./github.js";
|
|
7
|
+
export { GitHubUserAuthorizer } from "./github-user-auth.js";
|
|
8
|
+
function githubClient(env) {
|
|
9
|
+
const github = loadGitHubVcsConfig(env);
|
|
10
|
+
if (!github.appId || !github.privateKey)
|
|
11
|
+
return null;
|
|
12
|
+
return new GitHubAppClient({ appId: github.appId, privateKey: github.privateKey, apiVersion: github.apiVersion });
|
|
13
|
+
}
|
|
14
|
+
/** One host per configured provider; empty when no GitHub App is configured (the repo_* tools are then never offered). */
|
|
15
|
+
export function buildReviewHosts(env = process.env) {
|
|
16
|
+
const client = githubClient(env);
|
|
17
|
+
return client ? { github: new GitHubReviewHost(client) } : {};
|
|
18
|
+
}
|
|
19
|
+
/**
|
|
20
|
+
* One identity-linking flow per provider whose App has OAuth client
|
|
21
|
+
* credentials (GITHUB_APP_CLIENT_ID/SECRET). Empty = link_host_account is
|
|
22
|
+
* disabled; repository authorization is enforced either way.
|
|
23
|
+
*/
|
|
24
|
+
export function buildHostUserAuthorizers(env = process.env) {
|
|
25
|
+
const client = githubClient(env);
|
|
26
|
+
const { clientId, clientSecret } = loadGitHubUserAuthConfig(env);
|
|
27
|
+
if (!client || !clientId || !clientSecret)
|
|
28
|
+
return {};
|
|
29
|
+
return { github: new GitHubUserAuthorizer({ client, clientId, clientSecret }) };
|
|
30
|
+
}
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
import type { CheckConclusion, InlineComment, ReviewVerdict } from "./types.js";
|
|
2
|
+
/** Agent ids are cuids; anything else could break out of the HTML comment. */
|
|
3
|
+
export declare const SAFE_AGENT_MARKER: RegExp;
|
|
4
|
+
export declare function reviewMarker(agentMarker: string, headSha: string): string;
|
|
5
|
+
export declare function hasReviewMarker(body: string, agentMarker: string): boolean;
|
|
6
|
+
export declare function parseReviewMarker(body: string, agentMarker: string): string | null;
|
|
7
|
+
export declare function verdictConclusion(verdict: ReviewVerdict): CheckConclusion;
|
|
8
|
+
/** Tags an inline comment with the agent that wrote it, so only that agent resolves its thread. */
|
|
9
|
+
export declare function findingMarker(agentMarker: string): string;
|
|
10
|
+
export declare function hasFindingMarker(body: string, agentMarker: string): boolean;
|
|
11
|
+
export declare function renderInlineComment(comment: InlineComment, agentMarker: string): string;
|
|
12
|
+
export declare function renderSummaryComment(input: {
|
|
13
|
+
agentMarker: string;
|
|
14
|
+
headSha: string;
|
|
15
|
+
verdict: ReviewVerdict;
|
|
16
|
+
summary: string;
|
|
17
|
+
body: string;
|
|
18
|
+
outside: InlineComment[];
|
|
19
|
+
}): string;
|