@wardby/cli 0.5.3 → 0.6.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 +33 -0
- package/README.md +25 -14
- package/dist/claude-coding-worker/context.d.ts +22 -0
- package/dist/claude-coding-worker/context.js +92 -0
- package/dist/claude-coding-worker/driver.d.ts +7 -1
- package/dist/claude-coding-worker/driver.js +38 -5
- package/dist/claude-coding-worker/sdk.js +13 -4
- package/dist/cli-help.d.ts +1 -1
- package/dist/cli-help.js +5 -2
- package/dist/cli-native-mode.d.ts +9 -0
- package/dist/cli-native-mode.js +20 -0
- package/dist/cli.d.ts +3 -1
- package/dist/cli.js +141 -5
- package/dist/coding/claude-context.d.ts +45 -0
- package/dist/coding/claude-context.js +434 -0
- package/dist/coding/profile.d.ts +12 -0
- package/dist/coding/profile.js +6 -0
- package/dist/coding/protocol.d.ts +104 -0
- package/dist/coding/protocol.js +87 -3
- package/dist/coding-worker/artifact.d.ts +8 -0
- package/dist/coding-worker/artifact.js +2 -2
- package/dist/coding-worker/driver.js +2 -0
- package/dist/coding-worker/sdk.d.ts +6 -0
- package/dist/coding-worker/sdk.js +1 -0
- package/dist/coding-worker/skills.d.ts +23 -0
- package/dist/coding-worker/skills.js +117 -0
- package/dist/coding-worker/types.d.ts +2 -0
- package/dist/config/github-allowed-accounts.d.ts +15 -0
- package/dist/config/github-allowed-accounts.js +36 -0
- package/dist/config/low-power.d.ts +12 -0
- package/dist/config/low-power.js +18 -0
- package/dist/config/mobile.d.ts +12 -0
- package/dist/config/mobile.js +18 -0
- package/dist/config/providers.d.ts +53 -0
- package/dist/config/providers.js +92 -6
- package/dist/core/db.d.ts +16 -6
- package/dist/core/db.js +12 -10
- package/dist/core/dispatch.d.ts +11 -0
- package/dist/core/dispatch.js +36 -1
- package/dist/core/engine-native.js +9 -5
- package/dist/core/host-events.d.ts +49 -3
- package/dist/core/host-events.js +289 -15
- package/dist/core/issue-bridge.js +42 -2
- package/dist/core/issue-events.js +12 -1
- package/dist/core/issue-status.js +14 -3
- package/dist/core/merge-order-check.d.ts +129 -0
- package/dist/core/merge-order-check.js +336 -0
- package/dist/core/notification-dispatcher.d.ts +70 -0
- package/dist/core/notification-dispatcher.js +368 -0
- package/dist/core/notification-templates.d.ts +25 -0
- package/dist/core/notification-templates.js +105 -0
- package/dist/core/notifications.d.ts +10 -0
- package/dist/core/notifications.js +38 -0
- package/dist/core/pg-auth.d.ts +22 -0
- package/dist/core/pg-auth.js +152 -0
- package/dist/core/power-composition.d.ts +16 -0
- package/dist/core/power-composition.js +37 -0
- package/dist/core/power-idle.d.ts +14 -0
- package/dist/core/power-idle.js +30 -0
- package/dist/core/power.d.ts +71 -0
- package/dist/core/power.js +202 -0
- package/dist/core/reconciler.d.ts +4 -2
- package/dist/core/reconciler.js +29 -5
- package/dist/core/related-pull-requests.d.ts +48 -4
- package/dist/core/related-pull-requests.js +113 -11
- package/dist/core/repo-access.d.ts +15 -0
- package/dist/core/repo-access.js +66 -0
- package/dist/core/review-fix.d.ts +37 -0
- package/dist/core/review-fix.js +207 -6
- package/dist/core/review-host-tools.d.ts +20 -0
- package/dist/core/review-host-tools.js +58 -2
- package/dist/core/runner.d.ts +336 -3
- package/dist/core/runner.js +894 -317
- package/dist/core/secret-broker-config.d.ts +103 -0
- package/dist/core/secret-broker-config.js +112 -0
- package/dist/core/secrets.d.ts +39 -4
- package/dist/core/secrets.js +148 -40
- package/dist/core/workflow-event-recorder.d.ts +14 -0
- package/dist/core/workflow-event-recorder.js +120 -0
- package/dist/core/workflow-events.d.ts +82 -0
- package/dist/core/workflow-events.js +44 -0
- package/dist/core/workflow-run-events.d.ts +16 -0
- package/dist/core/workflow-run-events.js +40 -0
- package/dist/generated/prisma/browser.d.ts +58 -0
- package/dist/generated/prisma/client.d.ts +58 -0
- package/dist/generated/prisma/commonInputTypes.d.ts +150 -0
- package/dist/generated/prisma/enums.d.ts +27 -0
- package/dist/generated/prisma/enums.js +23 -0
- package/dist/generated/prisma/internal/class.d.ts +88 -0
- package/dist/generated/prisma/internal/class.js +4 -4
- package/dist/generated/prisma/internal/prismaNamespace.d.ts +772 -1
- package/dist/generated/prisma/internal/prismaNamespace.js +135 -4
- package/dist/generated/prisma/internal/prismaNamespaceBrowser.d.ts +139 -0
- package/dist/generated/prisma/internal/prismaNamespaceBrowser.js +135 -4
- package/dist/generated/prisma/models/Agent.d.ts +360 -1
- package/dist/generated/prisma/models/CodingAgentProfile.d.ts +73 -1
- package/dist/generated/prisma/models/CodingRun.d.ts +205 -1
- package/dist/generated/prisma/models/DeferredReview.d.ts +78 -1
- package/dist/generated/prisma/models/NativeGatewayCall.d.ts +1675 -0
- package/dist/generated/prisma/models/NativeGatewayCall.js +1 -0
- package/dist/generated/prisma/models/NativeGatewaySession.d.ts +1517 -0
- package/dist/generated/prisma/models/NativeGatewaySession.js +1 -0
- package/dist/generated/prisma/models/NativeWarmWorker.d.ts +1296 -0
- package/dist/generated/prisma/models/NativeWarmWorker.js +1 -0
- package/dist/generated/prisma/models/NotificationChannel.d.ts +1543 -0
- package/dist/generated/prisma/models/NotificationChannel.js +1 -0
- package/dist/generated/prisma/models/NotificationDelivery.d.ts +1502 -0
- package/dist/generated/prisma/models/NotificationDelivery.js +1 -0
- package/dist/generated/prisma/models/NotificationThread.d.ts +1090 -0
- package/dist/generated/prisma/models/NotificationThread.js +1 -0
- package/dist/generated/prisma/models/Run.d.ts +826 -1
- package/dist/generated/prisma/models/RunHostCheck.d.ts +36 -1
- package/dist/generated/prisma/models/Secret.d.ts +37 -1
- package/dist/generated/prisma/models/SecretBrokerChange.d.ts +1101 -0
- package/dist/generated/prisma/models/SecretBrokerChange.js +1 -0
- package/dist/generated/prisma/models/WorkflowEvent.d.ts +1422 -0
- package/dist/generated/prisma/models/WorkflowEvent.js +1 -0
- package/dist/generated/prisma/models.d.ts +8 -0
- package/dist/help/build.js +4 -2
- package/dist/help/catalog.d.ts +1 -1
- package/dist/help/catalog.js +31 -6
- package/dist/help-index.json +1363 -176
- package/dist/import/create.js +17 -0
- package/dist/import/neutral-schema.d.ts +6 -2
- package/dist/import/neutral-schema.js +2 -0
- package/dist/mcp/auth/access.d.ts +1 -0
- package/dist/mcp/auth/repo-authorization.js +14 -0
- package/dist/mcp/auth/resource-server.d.ts +2 -1
- package/dist/mcp/auth/resource-server.js +7 -2
- package/dist/mcp/auth/self-hosted/browser.d.ts +1 -1
- package/dist/mcp/auth/self-hosted/browser.js +19 -6
- package/dist/mcp/context.d.ts +6 -0
- package/dist/mcp/host-events/github-ingress.d.ts +23 -1
- package/dist/mcp/host-events/github-ingress.js +68 -0
- package/dist/mcp/index.d.ts +13 -0
- package/dist/mcp/index.js +59 -10
- package/dist/mcp/tools/agents.js +82 -11
- package/dist/mcp/tools/notification-channels.d.ts +2 -0
- package/dist/mcp/tools/notification-channels.js +293 -0
- package/dist/mcp/tools/power.d.ts +2 -0
- package/dist/mcp/tools/power.js +77 -0
- package/dist/mcp/tools/runs.d.ts +0 -6
- package/dist/mcp/tools/runs.js +7 -2
- package/dist/mcp/tools/secret-broker-compat.d.ts +12 -0
- package/dist/mcp/tools/secret-broker-compat.js +23 -0
- package/dist/mcp/tools/secret-elicitation-form.d.ts +22 -2
- package/dist/mcp/tools/secret-elicitation-form.js +200 -10
- package/dist/mcp/tools/secret-elicitation-server.js +8 -1
- package/dist/mcp/tools/secret-elicitation.d.ts +38 -3
- package/dist/mcp/tools/secret-elicitation.js +110 -12
- package/dist/mcp/tools/secrets.js +184 -7
- package/dist/mcp/tools/tools.d.ts +1 -1
- package/dist/mcp/tools/tools.js +22 -2
- package/dist/native-worker/composition.d.ts +23 -0
- package/dist/native-worker/composition.js +77 -0
- package/dist/native-worker/docker-isolation.d.ts +65 -0
- package/dist/native-worker/docker-isolation.js +183 -0
- package/dist/native-worker/docker-launcher.d.ts +74 -0
- package/dist/native-worker/docker-launcher.js +237 -0
- package/dist/native-worker/gateway-server.d.ts +39 -0
- package/dist/native-worker/gateway-server.js +315 -0
- package/dist/native-worker/gateway.d.ts +44 -0
- package/dist/native-worker/gateway.js +197 -0
- package/dist/native-worker/http-server.d.ts +22 -0
- package/dist/native-worker/http-server.js +109 -0
- package/dist/native-worker/http-transport.d.ts +17 -0
- package/dist/native-worker/http-transport.js +88 -0
- package/dist/native-worker/kubernetes-isolation.d.ts +90 -0
- package/dist/native-worker/kubernetes-isolation.js +218 -0
- package/dist/native-worker/kubernetes-launcher.d.ts +102 -0
- package/dist/native-worker/kubernetes-launcher.js +335 -0
- package/dist/native-worker/launch.d.ts +45 -0
- package/dist/native-worker/launch.js +44 -0
- package/dist/native-worker/ledger.d.ts +109 -0
- package/dist/native-worker/ledger.js +192 -0
- package/dist/native-worker/loopback.d.ts +12 -0
- package/dist/native-worker/loopback.js +49 -0
- package/dist/native-worker/main.d.ts +7 -0
- package/dist/native-worker/main.js +74 -0
- package/dist/native-worker/protocol.d.ts +744 -0
- package/dist/native-worker/protocol.js +200 -0
- package/dist/native-worker/sandbox-executor.d.ts +68 -0
- package/dist/native-worker/sandbox-executor.js +210 -0
- package/dist/native-worker/stdio.d.ts +29 -0
- package/dist/native-worker/stdio.js +188 -0
- package/dist/native-worker/warm-delivery.d.ts +16 -0
- package/dist/native-worker/warm-delivery.js +33 -0
- package/dist/native-worker/warm-pool-ledger.d.ts +39 -0
- package/dist/native-worker/warm-pool-ledger.js +73 -0
- package/dist/native-worker/warm-pool.d.ts +107 -0
- package/dist/native-worker/warm-pool.js +283 -0
- package/dist/native-worker/worker.d.ts +17 -0
- package/dist/native-worker/worker.js +115 -0
- package/dist/providers/auth/authorization-server.d.ts +3 -0
- package/dist/providers/auth/index.js +2 -0
- package/dist/providers/auth/self-hosted.d.ts +13 -1
- package/dist/providers/auth/self-hosted.js +54 -4
- package/dist/providers/chat/fake.d.ts +46 -0
- package/dist/providers/chat/fake.js +39 -0
- package/dist/providers/chat/index.d.ts +4 -0
- package/dist/providers/chat/index.js +8 -0
- package/dist/providers/chat/slack.d.ts +37 -0
- package/dist/providers/chat/slack.js +105 -0
- package/dist/providers/chat/types.d.ts +42 -0
- package/dist/providers/chat/types.js +19 -0
- package/dist/providers/engine/types.d.ts +19 -0
- package/dist/providers/engine/types.js +22 -0
- package/dist/providers/executor/composition.d.ts +24 -1
- package/dist/providers/executor/composition.js +39 -6
- package/dist/providers/executor/container.d.ts +24 -1
- package/dist/providers/executor/container.js +101 -11
- package/dist/providers/executor/dbos.d.ts +42 -0
- package/dist/providers/executor/dbos.js +118 -12
- package/dist/providers/executor/deferred.d.ts +56 -0
- package/dist/providers/executor/deferred.js +119 -0
- package/dist/providers/executor/routing.d.ts +37 -10
- package/dist/providers/executor/routing.js +73 -32
- package/dist/providers/executor/types.d.ts +12 -0
- package/dist/providers/jobs/docker.js +3 -3
- package/dist/providers/jobs/fake-kubernetes-api.d.ts +21 -2
- package/dist/providers/jobs/fake-kubernetes-api.js +42 -1
- package/dist/providers/jobs/kubernetes-api.d.ts +17 -1
- package/dist/providers/jobs/kubernetes-api.js +7 -0
- package/dist/providers/jobs/kubernetes-client.d.ts +10 -1
- package/dist/providers/jobs/kubernetes-client.js +52 -3
- package/dist/providers/jobs/kubernetes-dry-run-fixture.d.ts +6 -1
- package/dist/providers/jobs/kubernetes-dry-run-fixture.js +6 -3
- package/dist/providers/jobs/kubernetes-isolation.d.ts +3 -1
- package/dist/providers/jobs/kubernetes-isolation.js +6 -0
- package/dist/providers/jobs/kubernetes-platform.d.ts +4 -1
- package/dist/providers/jobs/kubernetes-platform.js +38 -1
- package/dist/providers/jobs/kubernetes-preflight.d.ts +2 -1
- package/dist/providers/jobs/kubernetes-preflight.js +4 -2
- package/dist/providers/jobs/kubernetes.d.ts +36 -1
- package/dist/providers/jobs/kubernetes.js +74 -4
- package/dist/providers/jobs/metadata-probes.d.ts +11 -0
- package/dist/providers/jobs/metadata-probes.js +13 -0
- package/dist/providers/jobs/types.d.ts +9 -0
- package/dist/providers/review-host/github.d.ts +11 -1
- package/dist/providers/review-host/github.js +50 -0
- package/dist/providers/review-host/types.d.ts +23 -0
- package/dist/providers/vcs/github.d.ts +32 -6
- package/dist/providers/vcs/github.js +74 -16
- package/dist/quickstart/coding-seed.d.ts +10 -3
- package/dist/quickstart/coding-seed.js +74 -3
- package/dist/quickstart/images.d.ts +2 -0
- package/dist/quickstart/images.js +7 -1
- package/dist/quickstart/reviewer-prompt.d.ts +1 -1
- package/dist/quickstart/reviewer-prompt.js +1 -1
- package/dist/quickstart-images.json +1 -1
- package/dist/sandbox/brokered-fetch.d.ts +23 -0
- package/dist/sandbox/brokered-fetch.js +37 -0
- package/dist/sandbox/host-functions.d.ts +34 -0
- package/dist/sandbox/host-functions.js +144 -80
- package/dist/sandbox/prelude.js +7 -1
- package/dist/sandbox/run-in-sandbox.d.ts +19 -0
- package/dist/sandbox/run-in-sandbox.js +28 -10
- package/dist/sandbox/safe-fetch.d.ts +2 -0
- package/dist/sandbox/safe-fetch.js +3 -1
- package/dist/sandbox/secret-broker-sigv4.d.ts +2 -0
- package/dist/sandbox/secret-broker-sigv4.js +49 -0
- package/dist/sandbox/secret-broker.d.ts +34 -0
- package/dist/sandbox/secret-broker.js +162 -0
- package/dist/sandbox/user-tool.d.ts +14 -0
- package/dist/sandbox/user-tool.js +32 -0
- package/dist/serve.js +18 -0
- package/dist/viewer/api-schema.d.ts +210 -14
- package/dist/viewer/api-schema.js +22 -0
- package/dist/viewer/build-schemas.js +4 -1
- package/dist/viewer/event-bus.d.ts +6 -0
- package/dist/viewer/event-bus.js +16 -7
- package/dist/viewer/graph.d.ts +5 -2
- package/dist/viewer/graph.js +7 -2
- package/dist/viewer/http.d.ts +0 -1
- package/dist/viewer/http.js +2 -2
- package/dist/viewer/infra.js +25 -2
- package/dist/viewer/limits.d.ts +4 -0
- package/dist/viewer/limits.js +4 -0
- package/dist/viewer/openapi.d.ts +15 -0
- package/dist/viewer/openapi.js +172 -0
- package/docs/README.md +7 -0
- package/docs/agent-recipes.md +57 -8
- package/docs/architecture-runtime.md +16 -1
- package/docs/code-review-agents.md +139 -5
- package/docs/coding-agent-setup.md +113 -5
- package/docs/coding-packages.md +4 -2
- package/docs/coding-services.md +3 -1
- package/docs/coding-worker-byo-images.md +198 -6
- package/docs/coding-worker-isolation.md +68 -14
- package/docs/getting-started-gke.md +225 -9
- package/docs/getting-started-identity-provider.md +21 -10
- package/docs/getting-started.md +5 -4
- package/docs/jira-agents.md +5 -1
- package/docs/native-sandbox.md +635 -0
- package/docs/release-verification.md +9 -0
- package/docs/security-deployment.md +113 -12
- package/docs/slack-notifications.md +252 -0
- package/docs/tool-secrets.md +252 -0
- package/docs/viewer-api.md +46 -5
- package/docs/viewer-api.openapi.yml +1404 -0
- package/help/admin-viewer.md +6 -2
- package/help/agent-recipes.md +27 -0
- package/help/architecture-agent.md +1 -1
- package/help/brokered-secrets.md +79 -0
- package/help/build-claude-code-image.md +131 -0
- package/help/build-worker-image.md +3 -6
- package/help/code-review-agents.md +68 -6
- package/help/deploy-gke.md +8 -1
- package/help/deployment-targets.md +25 -14
- package/help/errors/coding-provider-not-configured.md +2 -2
- package/help/errors/database-auth-invalid.md +28 -0
- package/help/errors/github-account-not-allowed.md +25 -0
- package/help/errors/low-power-disabled.md +29 -0
- package/help/errors/low-power-scale-forbidden.md +35 -0
- package/help/errors/low-power-wake-timeout.md +30 -0
- package/help/errors/mobile-disabled.md +31 -0
- package/help/errors/native-sandbox-capacity.md +22 -0
- package/help/errors/native-sandbox-deadline-exceeded.md +19 -0
- package/help/errors/native-sandbox-docker-failed.md +20 -0
- package/help/errors/native-sandbox-gateway-unavailable.md +21 -0
- package/help/errors/native-sandbox-image-not-pinned.md +19 -0
- package/help/errors/native-sandbox-isolation-mismatch.md +20 -0
- package/help/errors/native-sandbox-network-unenforced.md +22 -0
- package/help/errors/native-sandbox-requires-catalog.md +31 -0
- package/help/errors/native-sandbox-unavailable.md +19 -0
- package/help/errors/native-sandbox-warm-delivery-failed.md +21 -0
- package/help/errors/native-sandbox-worker-exited.md +20 -0
- package/help/errors/native-sandbox-worker-lost.md +20 -0
- package/help/errors/native-sandbox-worker-unready.md +22 -0
- package/help/errors/secret-broker-body-invalid.md +26 -0
- package/help/errors/secret-broker-changed.md +20 -0
- package/help/errors/secret-broker-config-invalid.md +30 -0
- package/help/errors/secret-broker-conflict.md +27 -0
- package/help/errors/secret-broker-destination-denied.md +28 -0
- package/help/errors/secret-broker-value-invalid.md +28 -0
- package/help/errors/secret-brokered.md +24 -0
- package/help/errors/secret-not-brokered.md +25 -0
- package/help/errors/slack-auth-failed.md +48 -0
- package/help/errors/slack-channel-unreachable.md +39 -0
- package/help/errors/slack-not-configured.md +33 -0
- package/help/getting-started.md +4 -5
- package/help/github.md +18 -1
- package/help/identity-and-access.md +5 -1
- package/help/local-repositories.md +2 -3
- package/help/low-power-mode.md +125 -0
- package/help/merge-order-check.md +117 -0
- package/help/mobile-app.md +54 -0
- package/help/native-sandbox.md +136 -0
- package/help/observability.md +8 -0
- package/help/related-pull-requests.md +70 -9
- package/help/repo-instructions-and-skills.md +91 -0
- package/help/review-fix-rounds.md +10 -0
- package/help/slack-notifications.md +62 -0
- package/help/troubleshooting/coding-workers.md +21 -0
- package/package.json +11 -4
- package/prisma/migrations/20261009000003_native_execution_mode/migration.sql +12 -0
- package/prisma/migrations/20261009000004_native_gateway_ledger/migration.sql +65 -0
- package/prisma/migrations/20261009000005_native_gateway_call_child_agent/migration.sql +5 -0
- package/prisma/migrations/20261009000006_run_deferred_start_stop/migration.sql +14 -0
- package/prisma/migrations/20261009000007_native_gateway_network_ready/migration.sql +5 -0
- package/prisma/migrations/20261010000001_workflow_notifications/migration.sql +99 -0
- package/prisma/migrations/20261011000001_native_gateway_session_status_index/migration.sql +2 -0
- package/prisma/migrations/20261012000001_native_warm_worker/migration.sql +32 -0
- package/prisma/migrations/20261013000001_coding_run_merge_order/migration.sql +2 -0
- package/prisma/migrations/20261013000001_secret_broker/migration.sql +17 -0
- package/prisma/migrations/20261013000002_deferred_review_request/migration.sql +4 -0
- package/prisma/migrations/20261014000000_repo_skills/migration.sql +15 -0
- package/prisma/migrations/20261015000001_coding_run_merge_order_index/migration.sql +2 -0
- package/prisma/migrations/20261015000002_no_change_rereview/migration.sql +2 -0
- package/prisma/schema.prisma +362 -91
package/dist/core/host-events.js
CHANGED
|
@@ -1,9 +1,18 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Host-neutral routing of code-review host events (GitHub App webhooks today)
|
|
3
|
+
* to the native agents linked to that repository. Adapters normalise their
|
|
4
|
+
* payloads into HostEvent (providers/review-host/github-events.ts); this
|
|
5
|
+
* module decides which agents run. See
|
|
6
|
+
* docs/private/2026-09-25-code-review-host-design.md §6.3.
|
|
7
|
+
*/
|
|
8
|
+
import { Prisma } from "#prisma";
|
|
1
9
|
import { linkedPullRequestAttribution, openingRunAttribution, RESPONSE_PATH_SNAPSHOT_BUDGET } from "./attribution.js";
|
|
2
10
|
import { checkContinuation, dispatchRun } from "./dispatch.js";
|
|
3
11
|
import { mentionStatusRow, postMentionStatus } from "./host-status.js";
|
|
4
12
|
import { handlePullRequestClosed } from "./issue-bridge.js";
|
|
5
13
|
import { logger } from "./logger.js";
|
|
6
|
-
import {
|
|
14
|
+
import { syncMergeOrderChecksForPullRequest } from "./merge-order-check.js";
|
|
15
|
+
import { MAX_SIBLING_HINTS, openSiblings, runTreeRoot, SIBLING_GUIDANCE, } from "./related-pull-requests.js";
|
|
7
16
|
import { requiredLevel } from "./repo-access.js";
|
|
8
17
|
import { composeTaskOverride } from "./untrusted-content.js";
|
|
9
18
|
import { DEFAULT_KNOWLEDGE_BUNDLE_PATH, parseConcept, RESERVED_BUNDLE_FILES, } from "../knowledge/concept.js";
|
|
@@ -338,6 +347,11 @@ export function unknownPriorRunBody(priorRunId) {
|
|
|
338
347
|
"deployment has no record of that run opening this pull request (another wardby deployment may have opened it). " +
|
|
339
348
|
"Ask the deployment that opened it, or make the change on this branch by hand.");
|
|
340
349
|
}
|
|
350
|
+
/** A review run's task: the head to review, plus any extra context its trigger adds. */
|
|
351
|
+
export function reviewTaskText(repository, prNumber, headSha, context) {
|
|
352
|
+
const task = `Review pull request #${prNumber} in ${repository} (head ${headSha}).`;
|
|
353
|
+
return context ? composeTaskOverride(`${task}\n\n${context.task}`, context.untrustedContext) : task;
|
|
354
|
+
}
|
|
341
355
|
/** Run states that already cover a commit: a review of it is under way or done. */
|
|
342
356
|
const COVERING_REVIEW_STATUSES = ["pending", "running", "succeeded"];
|
|
343
357
|
/**
|
|
@@ -360,9 +374,16 @@ async function alreadyReviewed(deps, repository, prNumber, headSha, agentId) {
|
|
|
360
374
|
});
|
|
361
375
|
return prior !== null;
|
|
362
376
|
}
|
|
363
|
-
|
|
377
|
+
/** Starts one review run per target on a pull request head; returns the started run ids. */
|
|
378
|
+
export async function startReviews(deps, host, repository, prNumber, headSha, targets,
|
|
364
379
|
/** True for automatic triggers; an explicit re-run or `@wardby review` always runs. */
|
|
365
|
-
skipReviewedCommits = false
|
|
380
|
+
skipReviewedCommits = false,
|
|
381
|
+
/**
|
|
382
|
+
* Extra context for this review: `task` (written by wardby, trusted) is appended to the
|
|
383
|
+
* review task; `untrustedContext` (anything a model or a person wrote) travels separately,
|
|
384
|
+
* delivered as data.
|
|
385
|
+
*/
|
|
386
|
+
context) {
|
|
366
387
|
const runIds = [];
|
|
367
388
|
// One lookup per PR, not per reviewer, and none at all when every reviewer
|
|
368
389
|
// skips this commit: taken at the first actual dispatch.
|
|
@@ -404,7 +425,7 @@ skipReviewedCommits = false) {
|
|
|
404
425
|
selfDefects: { db: deps.db, issueTrackers: deps.issueTrackers },
|
|
405
426
|
agentId: target.agentId,
|
|
406
427
|
trigger: "host_event",
|
|
407
|
-
taskOverride:
|
|
428
|
+
taskOverride: reviewTaskText(repository, prNumber, headSha, context),
|
|
408
429
|
attribution: await linkedAttribution(),
|
|
409
430
|
afterPersist: checkId
|
|
410
431
|
? async (tx, run) => {
|
|
@@ -590,6 +611,7 @@ async function deferUntilCi(deps, host, key, links) {
|
|
|
590
611
|
prNumber: key.prNumber,
|
|
591
612
|
headSha: key.headSha,
|
|
592
613
|
agentId: { in: links.map((l) => l.agentId) },
|
|
614
|
+
reason: "ci",
|
|
593
615
|
},
|
|
594
616
|
select: { id: true, agentId: true, checkName: true },
|
|
595
617
|
});
|
|
@@ -606,8 +628,8 @@ async function deferUntilCi(deps, host, key, links) {
|
|
|
606
628
|
* Starts deferred reviews for one PR head. The head must still be that sha,
|
|
607
629
|
* open, and not a fork; otherwise every row is dropped (a newer push records
|
|
608
630
|
* its own). Each row is claimed by deleting it, so a concurrent ci_completed
|
|
609
|
-
* delivery or sweep starts each review only once. On `ci`, only
|
|
610
|
-
* still authorized are claimed (the rest wait for the sweep); on `sweep` (the
|
|
631
|
+
* delivery or sweep starts each review only once. On `ci` and `request`, only
|
|
632
|
+
* reviewers still authorized are claimed (the rest wait for the sweep); on `sweep` (the
|
|
611
633
|
* last attempt) every row is claimed and unauthorized or unlinked reviewers
|
|
612
634
|
* are dropped.
|
|
613
635
|
*/
|
|
@@ -623,7 +645,7 @@ async function startDeferred(deps, host, key, rows, reviewerLinks, mode, reason)
|
|
|
623
645
|
const allowedIds = new Set(allowed.map((l) => l.agentId));
|
|
624
646
|
const targets = [];
|
|
625
647
|
for (const row of rows) {
|
|
626
|
-
if (mode
|
|
648
|
+
if (mode !== "sweep" && !allowedIds.has(row.agentId))
|
|
627
649
|
continue;
|
|
628
650
|
const claimed = await deps.db.deferredReview.deleteMany({ where: { id: row.id } });
|
|
629
651
|
if (claimed.count !== 1)
|
|
@@ -647,14 +669,16 @@ async function startDeferred(deps, host, key, rows, reviewerLinks, mode, reason)
|
|
|
647
669
|
}
|
|
648
670
|
/**
|
|
649
671
|
* ci_completed: starts the reviews deferred on this head (see deferUntilCi),
|
|
650
|
-
* unless CI there is still pending (a later completion decides).
|
|
672
|
+
* unless CI there is still pending (a later completion decides). Rows still
|
|
673
|
+
* waiting for their delegating run (reason "request") are not CI's to start:
|
|
674
|
+
* startDeferredForRequest hands them over once that run is terminal. Never
|
|
651
675
|
* throws; a failure is logged and the rows stay for the reconciler sweep.
|
|
652
676
|
*/
|
|
653
677
|
async function startDeferredAfterCi(deps, host, event, prNumber, reviewerLinks) {
|
|
654
678
|
try {
|
|
655
679
|
const key = { provider: event.provider, repository: event.repository, prNumber, headSha: event.headSha };
|
|
656
680
|
const rows = await deps.db.deferredReview.findMany({
|
|
657
|
-
where: key,
|
|
681
|
+
where: { ...key, reason: "ci" },
|
|
658
682
|
select: { id: true, agentId: true, checkName: true },
|
|
659
683
|
});
|
|
660
684
|
if (rows.length === 0)
|
|
@@ -676,9 +700,21 @@ async function startDeferredAfterCi(deps, host, event, prNumber, reviewerLinks)
|
|
|
676
700
|
* DEFERRED_REVIEW_MAX_AGE_MS, and starts (regardless of CI) those waiting
|
|
677
701
|
* longer than DEFERRED_REVIEW_MAX_WAIT_MS, at most DEFERRED_REVIEW_BATCH per
|
|
678
702
|
* pass, oldest first. Covers CI that never reported, ran very long, or whose
|
|
679
|
-
* completion webhook was missed.
|
|
703
|
+
* completion webhook was missed. Rows waiting for a delegating run (reason
|
|
704
|
+
* "request") are released the same way once they are that old, but only when
|
|
705
|
+
* that run is terminal (or gone): a lead the reconciler marked lost never
|
|
706
|
+
* reaches its finalizer. Never throws; returns the runs started.
|
|
680
707
|
*/
|
|
681
708
|
export async function startDeferredReviews(deps, now = new Date()) {
|
|
709
|
+
const providers = Object.keys(deps.hosts);
|
|
710
|
+
const runIds = await sweepCiRows(deps, providers, now);
|
|
711
|
+
// Outside the CI half's try: a failure there never keeps request rows from being released.
|
|
712
|
+
if (providers.length > 0)
|
|
713
|
+
runIds.push(...(await sweepRequestRows(deps, providers, now)));
|
|
714
|
+
return runIds;
|
|
715
|
+
}
|
|
716
|
+
/** The sweep's half for expiry and CI rows: drops rows past the max age, then starts CI rows past the max wait. */
|
|
717
|
+
async function sweepCiRows(deps, providers, now) {
|
|
682
718
|
const runIds = [];
|
|
683
719
|
try {
|
|
684
720
|
const expired = await deps.db.deferredReview.deleteMany({
|
|
@@ -686,12 +722,12 @@ export async function startDeferredReviews(deps, now = new Date()) {
|
|
|
686
722
|
});
|
|
687
723
|
if (expired.count > 0)
|
|
688
724
|
log.info({ count: expired.count }, "dropped deferred reviews older than 24 h");
|
|
689
|
-
const providers = Object.keys(deps.hosts);
|
|
690
725
|
if (providers.length === 0)
|
|
691
726
|
return runIds;
|
|
692
727
|
const due = await deps.db.deferredReview.findMany({
|
|
693
728
|
where: {
|
|
694
729
|
provider: { in: providers },
|
|
730
|
+
reason: "ci",
|
|
695
731
|
createdAt: { lte: new Date(now.getTime() - DEFERRED_REVIEW_MAX_WAIT_MS) },
|
|
696
732
|
},
|
|
697
733
|
select: {
|
|
@@ -732,6 +768,214 @@ export async function startDeferredReviews(deps, now = new Date()) {
|
|
|
732
768
|
}
|
|
733
769
|
return runIds;
|
|
734
770
|
}
|
|
771
|
+
/** The sweep's half for request rows: releases those older than the max wait whose delegating run is terminal or gone. */
|
|
772
|
+
async function sweepRequestRows(deps, providers, now) {
|
|
773
|
+
const runIds = [];
|
|
774
|
+
try {
|
|
775
|
+
// Joined in the query, so leads still running never fill the batch ahead of finished ones.
|
|
776
|
+
const leads = await deps.db.$queryRaw `
|
|
777
|
+
SELECT DISTINCT d."leadRunId"
|
|
778
|
+
FROM "DeferredReview" d
|
|
779
|
+
LEFT JOIN "Run" r ON r."id" = d."leadRunId"
|
|
780
|
+
WHERE d."reason" = 'request'
|
|
781
|
+
AND d."leadRunId" IS NOT NULL
|
|
782
|
+
AND d."provider" IN (${Prisma.join(providers)})
|
|
783
|
+
AND d."createdAt" <= ${new Date(now.getTime() - DEFERRED_REVIEW_MAX_WAIT_MS)}
|
|
784
|
+
AND (r."id" IS NULL OR r."status"::text NOT IN ('pending', 'running'))
|
|
785
|
+
LIMIT ${DEFERRED_REVIEW_BATCH}`;
|
|
786
|
+
for (const { leadRunId } of leads)
|
|
787
|
+
runIds.push(...(await releaseRequestRows(deps, leadRunId, "sweep")));
|
|
788
|
+
}
|
|
789
|
+
catch (err) {
|
|
790
|
+
log.warn({ err }, "deferred review sweep failed for reviews waiting on a delegating run");
|
|
791
|
+
}
|
|
792
|
+
return runIds;
|
|
793
|
+
}
|
|
794
|
+
/**
|
|
795
|
+
* The run whose finish a review of this pull request waits for: the root of
|
|
796
|
+
* the run tree when the PR's opening coding run was delegated (it has a
|
|
797
|
+
* parent run) and that root is not terminal yet; otherwise null (a
|
|
798
|
+
* non-delegated coding PR, a finished request, or an unknown marker). A lead's
|
|
799
|
+
* delegate_to_* calls block until each child is terminal, so a terminal root
|
|
800
|
+
* means every sibling coding run of the request has finished and opened its
|
|
801
|
+
* pull request. A continuation's marker resolves to its root coding run, then
|
|
802
|
+
* up that run's parents. `repository`, when given, must be the coding run's.
|
|
803
|
+
*/
|
|
804
|
+
export async function requestLeadRunId(db, prMarkerRunId, repository) {
|
|
805
|
+
const root = await runTreeRoot(db, prMarkerRunId, repository);
|
|
806
|
+
if (!root || root.isOpener || !RUN_NOT_FINISHED.has(root.status))
|
|
807
|
+
return null;
|
|
808
|
+
return root.id;
|
|
809
|
+
}
|
|
810
|
+
/** Run statuses that mean the delegating run has not finished. */
|
|
811
|
+
const RUN_NOT_FINISHED = new Set(["pending", "running"]);
|
|
812
|
+
/**
|
|
813
|
+
* pr_updated for a pull request a delegated coding run opened, while the
|
|
814
|
+
* request's lead run is still running: records one DeferredReview row per
|
|
815
|
+
* reviewer (reason "request", with or without waitForCi) and starts nothing;
|
|
816
|
+
* the lead's finalizer (startDeferredForRequest) or the sweep starts them.
|
|
817
|
+
* Returns null, to review as without it, for any other pull request, or when
|
|
818
|
+
* the lead cannot be resolved or the deferral cannot be recorded.
|
|
819
|
+
*/
|
|
820
|
+
async function deferUntilRequest(deps, host, key, links,
|
|
821
|
+
/** The pull request's origin, read at most once per event (shared with the merge order check). */
|
|
822
|
+
readOrigin) {
|
|
823
|
+
if (links.length === 0 || !readOrigin)
|
|
824
|
+
return null;
|
|
825
|
+
let leadRunId;
|
|
826
|
+
try {
|
|
827
|
+
const origin = await readOrigin();
|
|
828
|
+
if (!origin.markerRunId)
|
|
829
|
+
return null;
|
|
830
|
+
leadRunId = await requestLeadRunId(deps.db, origin.markerRunId, key.repository);
|
|
831
|
+
}
|
|
832
|
+
catch (err) {
|
|
833
|
+
log.warn({ err, repository: key.repository, prNumber: key.prNumber }, "could not read the pull request's delegating run; reviewing as usual");
|
|
834
|
+
return null;
|
|
835
|
+
}
|
|
836
|
+
if (!leadRunId)
|
|
837
|
+
return null;
|
|
838
|
+
const head = { provider: key.provider, repository: key.repository, prNumber: key.prNumber, headSha: key.headSha };
|
|
839
|
+
try {
|
|
840
|
+
// Idempotent per head and reviewer, like deferUntilCi.
|
|
841
|
+
await deps.db.deferredReview.createMany({
|
|
842
|
+
data: links.map((l) => ({ ...head, agentId: l.agentId, checkName: l.checkName, reason: "request", leadRunId })),
|
|
843
|
+
skipDuplicates: true,
|
|
844
|
+
});
|
|
845
|
+
// A row this head already had for CI waits for the request too: released only when both are done.
|
|
846
|
+
await deps.db.deferredReview.updateMany({
|
|
847
|
+
where: { ...head, agentId: { in: links.map((l) => l.agentId) }, reason: "ci" },
|
|
848
|
+
data: { reason: "request", leadRunId },
|
|
849
|
+
});
|
|
850
|
+
}
|
|
851
|
+
catch (err) {
|
|
852
|
+
log.warn({ err, repository: key.repository, prNumber: key.prNumber }, "could not record the deferred review; reviewing as usual");
|
|
853
|
+
return null;
|
|
854
|
+
}
|
|
855
|
+
log.info({ ...head, leadRunId, agentIds: links.map((l) => l.agentId) }, "review deferred until the delegating run finishes");
|
|
856
|
+
// The lead may have finished between the lookup and the insert, its
|
|
857
|
+
// finalizer finding no row yet: check once more (rows are claimed by
|
|
858
|
+
// delete, so a racing finalizer starts each review once). A failure leaves
|
|
859
|
+
// the rows for the sweep.
|
|
860
|
+
const runIds = [];
|
|
861
|
+
try {
|
|
862
|
+
const lead = await deps.db.run.findUnique({ where: { id: leadRunId }, select: { status: true } });
|
|
863
|
+
if (!lead || !RUN_NOT_FINISHED.has(lead.status)) {
|
|
864
|
+
runIds.push(...(await releaseRequestRows(deps, leadRunId, "request")));
|
|
865
|
+
}
|
|
866
|
+
}
|
|
867
|
+
catch (err) {
|
|
868
|
+
log.warn({ err, repository: key.repository, prNumber: key.prNumber, leadRunId }, "could not re-check the delegating run after deferring; the sweep starts the review");
|
|
869
|
+
}
|
|
870
|
+
return { runIds };
|
|
871
|
+
}
|
|
872
|
+
/**
|
|
873
|
+
* The delegating run `leadRunId` is terminal (it succeeded, failed, or was
|
|
874
|
+
* cancelled): releases every review that waited for it. A reviewer linked
|
|
875
|
+
* with waitForCi whose head's CI is still pending is handed to CI (the row
|
|
876
|
+
* becomes reason "ci", its max wait counted from now); every other row is
|
|
877
|
+
* started (claimed by delete, so concurrent releases start each review once).
|
|
878
|
+
* Called from the lead's finalizer, after the related pull requests sections
|
|
879
|
+
* were rewritten. Never throws.
|
|
880
|
+
*/
|
|
881
|
+
export async function startDeferredForRequest(deps, leadRunId) {
|
|
882
|
+
try {
|
|
883
|
+
await releaseRequestRows(deps, leadRunId, "request");
|
|
884
|
+
}
|
|
885
|
+
catch (err) {
|
|
886
|
+
log.warn({ err, leadRunId }, "could not start the reviews waiting for the delegating run");
|
|
887
|
+
}
|
|
888
|
+
}
|
|
889
|
+
async function releaseRequestRows(deps, leadRunId, mode) {
|
|
890
|
+
const rows = await deps.db.deferredReview.findMany({
|
|
891
|
+
where: { leadRunId, reason: "request" },
|
|
892
|
+
select: {
|
|
893
|
+
id: true,
|
|
894
|
+
provider: true,
|
|
895
|
+
repository: true,
|
|
896
|
+
prNumber: true,
|
|
897
|
+
headSha: true,
|
|
898
|
+
agentId: true,
|
|
899
|
+
checkName: true,
|
|
900
|
+
},
|
|
901
|
+
orderBy: { createdAt: "asc" },
|
|
902
|
+
});
|
|
903
|
+
const groups = new Map();
|
|
904
|
+
for (const row of rows) {
|
|
905
|
+
const id = JSON.stringify([row.provider, row.repository, row.prNumber, row.headSha]);
|
|
906
|
+
groups.set(id, [...(groups.get(id) ?? []), row]);
|
|
907
|
+
}
|
|
908
|
+
const runIds = [];
|
|
909
|
+
for (const group of groups.values()) {
|
|
910
|
+
const first = group[0];
|
|
911
|
+
const provider = first.provider;
|
|
912
|
+
const key = { provider, repository: first.repository, prNumber: first.prNumber, headSha: first.headSha };
|
|
913
|
+
const host = deps.hosts[provider];
|
|
914
|
+
if (!host)
|
|
915
|
+
continue;
|
|
916
|
+
try {
|
|
917
|
+
const reviewerLinks = (await loadLinks(deps, provider, key.repository)).filter(isReviewerLink);
|
|
918
|
+
runIds.push(...(await releaseRequestHead(deps, host, key, group, reviewerLinks, mode)));
|
|
919
|
+
}
|
|
920
|
+
catch (err) {
|
|
921
|
+
log.warn({ err, repository: key.repository, prNumber: key.prNumber, leadRunId }, "could not start a review waiting for the delegating run");
|
|
922
|
+
}
|
|
923
|
+
}
|
|
924
|
+
return runIds;
|
|
925
|
+
}
|
|
926
|
+
/** One head's request rows: hands waitForCi rows with CI still pending to CI, starts the rest. */
|
|
927
|
+
async function releaseRequestHead(deps, host, key, rows, reviewerLinks, mode) {
|
|
928
|
+
const waitsForCi = new Set(reviewerLinks.filter((l) => l.waitForCi).map((l) => l.agentId));
|
|
929
|
+
let ciRows = rows.filter((r) => waitsForCi.has(r.agentId));
|
|
930
|
+
if (ciRows.length > 0 && host.readCi) {
|
|
931
|
+
let state;
|
|
932
|
+
try {
|
|
933
|
+
state = (await host.readCi(key.repository, key.headSha)).state;
|
|
934
|
+
}
|
|
935
|
+
catch (err) {
|
|
936
|
+
log.warn({ err, repository: key.repository, prNumber: key.prNumber }, "could not read CI; reviewing now");
|
|
937
|
+
}
|
|
938
|
+
if (state === undefined || !CI_NOT_FINISHED_ON_PUSH.has(state))
|
|
939
|
+
ciRows = [];
|
|
940
|
+
}
|
|
941
|
+
else
|
|
942
|
+
ciRows = [];
|
|
943
|
+
const startNow = rows.filter((r) => !ciRows.includes(r));
|
|
944
|
+
const runIds = startNow.length > 0
|
|
945
|
+
? await startDeferred(deps, host, key, startNow, reviewerLinks, mode, "the delegating run finished")
|
|
946
|
+
: [];
|
|
947
|
+
if (ciRows.length === 0 || !host.readCi)
|
|
948
|
+
return runIds;
|
|
949
|
+
const ids = ciRows.map((r) => r.id);
|
|
950
|
+
// Only a row still waiting for the request flips: a concurrent release that claimed it already started it.
|
|
951
|
+
await deps.db.deferredReview.updateMany({
|
|
952
|
+
where: { id: { in: ids }, reason: "request" },
|
|
953
|
+
data: { reason: "ci", createdAt: new Date() },
|
|
954
|
+
});
|
|
955
|
+
log.info({
|
|
956
|
+
repository: key.repository,
|
|
957
|
+
prNumber: key.prNumber,
|
|
958
|
+
headSha: key.headSha,
|
|
959
|
+
agentIds: ciRows.map((r) => r.agentId),
|
|
960
|
+
}, "the delegating run finished; review deferred until CI finishes");
|
|
961
|
+
// As in deferUntilCi: CI may have finished before the flip, its ci_completed skipping the request row.
|
|
962
|
+
try {
|
|
963
|
+
const again = (await host.readCi(key.repository, key.headSha)).state;
|
|
964
|
+
if (!CI_NOT_FINISHED_ON_PUSH.has(again)) {
|
|
965
|
+
const handed = await deps.db.deferredReview.findMany({
|
|
966
|
+
where: { id: { in: ids }, reason: "ci" },
|
|
967
|
+
select: { id: true, agentId: true, checkName: true },
|
|
968
|
+
});
|
|
969
|
+
if (handed.length > 0) {
|
|
970
|
+
runIds.push(...(await startDeferred(deps, host, key, handed, reviewerLinks, "ci", "CI finished")));
|
|
971
|
+
}
|
|
972
|
+
}
|
|
973
|
+
}
|
|
974
|
+
catch (err) {
|
|
975
|
+
log.warn({ err, repository: key.repository, prNumber: key.prNumber }, "could not re-read CI after the delegating run finished; the sweep starts the review");
|
|
976
|
+
}
|
|
977
|
+
return runIds;
|
|
978
|
+
}
|
|
735
979
|
export async function routeHostEvent(event, deps) {
|
|
736
980
|
const none = { runIds: [], followUps: [] };
|
|
737
981
|
if (event.kind === "pr_closed") {
|
|
@@ -742,7 +986,18 @@ export async function routeHostEvent(event, deps) {
|
|
|
742
986
|
number: event.prNumber,
|
|
743
987
|
merged: event.merged,
|
|
744
988
|
});
|
|
745
|
-
|
|
989
|
+
// The merge order checks of the sets this PR is a step of, after the response. Never throws.
|
|
990
|
+
if (!deps.hosts[event.provider]?.upsertNamedCheck)
|
|
991
|
+
return none;
|
|
992
|
+
const pr = { repository: event.repository, number: event.prNumber };
|
|
993
|
+
return {
|
|
994
|
+
runIds: [],
|
|
995
|
+
followUps: [
|
|
996
|
+
() => syncMergeOrderChecksForPullRequest(deps, pr, {
|
|
997
|
+
closed: { ...pr, merged: event.merged },
|
|
998
|
+
}),
|
|
999
|
+
],
|
|
1000
|
+
};
|
|
746
1001
|
}
|
|
747
1002
|
const host = deps.hosts[event.provider];
|
|
748
1003
|
if (!host)
|
|
@@ -816,11 +1071,30 @@ export async function routeHostEvent(event, deps) {
|
|
|
816
1071
|
return { runIds, followUps: [] };
|
|
817
1072
|
}
|
|
818
1073
|
case "pr_updated": {
|
|
819
|
-
if (event.isFork
|
|
1074
|
+
if (event.isFork)
|
|
820
1075
|
return none;
|
|
1076
|
+
// Read at most once per event, by the request deferral and the merge order check alike.
|
|
1077
|
+
let origin;
|
|
1078
|
+
const readOrigin = host.pullRequestOrigin
|
|
1079
|
+
? () => (origin ??= host.pullRequestOrigin(event.repository, event.prNumber))
|
|
1080
|
+
: undefined;
|
|
1081
|
+
// A new head needs the merge order check re-posted (check runs are per commit), on every
|
|
1082
|
+
// path below, a review held for the delegating run included. After the response; never throws.
|
|
1083
|
+
const followUps = host.upsertNamedCheck && readOrigin
|
|
1084
|
+
? [
|
|
1085
|
+
() => syncMergeOrderChecksForPullRequest(deps, { repository: event.repository, number: event.prNumber }, { origin: { repository: event.repository, number: event.prNumber, read: readOrigin } }),
|
|
1086
|
+
]
|
|
1087
|
+
: [];
|
|
1088
|
+
if (reviewerLinks.length === 0)
|
|
1089
|
+
return { runIds: [], followUps };
|
|
821
1090
|
const allowed = await authorizedLinks(deps, event, reviewerLinks);
|
|
822
1091
|
if (allowed.length === 0)
|
|
823
|
-
return
|
|
1092
|
+
return { runIds: [], followUps };
|
|
1093
|
+
// A PR a delegated coding run opened is reviewed once the whole request
|
|
1094
|
+
// finished, so its sibling PRs exist and the related section is current (#259).
|
|
1095
|
+
const request = await deferUntilRequest(deps, host, event, allowed, readOrigin);
|
|
1096
|
+
if (request)
|
|
1097
|
+
return { runIds: request.runIds, followUps };
|
|
824
1098
|
// Reviewers linked with waitForCi review this head only once its CI finished.
|
|
825
1099
|
const deferral = await deferUntilCi(deps, host, event, allowed.filter((l) => l.waitForCi));
|
|
826
1100
|
const startNow = new Set(deferral.startNow);
|
|
@@ -829,7 +1103,7 @@ export async function routeHostEvent(event, deps) {
|
|
|
829
1103
|
if (reviewers.length > 0) {
|
|
830
1104
|
runIds.push(...(await startReviews(deps, host, event.repository, event.prNumber, event.headSha, reviewers, true)));
|
|
831
1105
|
}
|
|
832
|
-
return { runIds, followUps
|
|
1106
|
+
return { runIds, followUps };
|
|
833
1107
|
}
|
|
834
1108
|
case "ci_completed": {
|
|
835
1109
|
if (reviewerLinks.length === 0)
|
|
@@ -11,6 +11,7 @@
|
|
|
11
11
|
import { Prisma } from "#prisma";
|
|
12
12
|
import { projectOf, } from "../providers/issue-tracker/types.js";
|
|
13
13
|
import { logger } from "./logger.js";
|
|
14
|
+
import { dedupeKeys, emitWorkflowEvent } from "./workflow-events.js";
|
|
14
15
|
const log = logger.child({ module: "issue-bridge" });
|
|
15
16
|
export const pullRequestGlobalId = (pr) => `wardby:pr:${pr.codeProvider}:${pr.repository}#${pr.number}`;
|
|
16
17
|
/**
|
|
@@ -77,10 +78,13 @@ export async function recordPullRequests(db, tracker, input) {
|
|
|
77
78
|
const notes = [];
|
|
78
79
|
try {
|
|
79
80
|
let newlyOpened = false;
|
|
81
|
+
const opened = [];
|
|
80
82
|
for (const pr of input.pullRequests) {
|
|
81
83
|
const state = await ensurePair(db, input, pr);
|
|
82
|
-
if (state === "created" && pr.outcome === "pull_request_opened")
|
|
84
|
+
if (state === "created" && pr.outcome === "pull_request_opened") {
|
|
83
85
|
newlyOpened = true;
|
|
86
|
+
opened.push(pr);
|
|
87
|
+
}
|
|
84
88
|
if (state !== "created" && state !== "open")
|
|
85
89
|
continue;
|
|
86
90
|
try {
|
|
@@ -95,15 +99,28 @@ export async function recordPullRequests(db, tracker, input) {
|
|
|
95
99
|
}
|
|
96
100
|
}
|
|
97
101
|
const target = input.onPullRequestOpened;
|
|
102
|
+
let movedTo = null;
|
|
98
103
|
if (target && newlyOpened) {
|
|
99
104
|
try {
|
|
100
|
-
await tracker.transitionTo(input.issueKey, target);
|
|
105
|
+
const moved = await tracker.transitionTo(input.issueKey, target);
|
|
106
|
+
movedTo = moved.toStatus || target;
|
|
101
107
|
}
|
|
102
108
|
catch (err) {
|
|
103
109
|
log.warn({ err, issueKey: input.issueKey, target }, "could not move the issue after a pull request opened");
|
|
104
110
|
notes.push(`Could not move ${input.issueKey} to "${target}"; it may need a manual move.`);
|
|
105
111
|
}
|
|
106
112
|
}
|
|
113
|
+
// Only pairs this call newly recorded, so a retried completion does not notify twice. Never throws.
|
|
114
|
+
for (const pr of opened) {
|
|
115
|
+
await emitWorkflowEvent({
|
|
116
|
+
dedupeKey: dedupeKeys.prOpened(pr.codeProvider, pr.repository, pr.number),
|
|
117
|
+
runId: pr.openedByRunId,
|
|
118
|
+
agentId: input.agentId,
|
|
119
|
+
workItem: { provider: input.issueProvider, key: input.issueKey },
|
|
120
|
+
pullRequest: { codeProvider: pr.codeProvider, repository: pr.repository, number: pr.number },
|
|
121
|
+
payload: { kind: "pr_opened", prLabel: `${pr.repository}#${pr.number}`, prUrl: pr.url, movedTo },
|
|
122
|
+
});
|
|
123
|
+
}
|
|
107
124
|
}
|
|
108
125
|
catch (err) {
|
|
109
126
|
log.warn({ err, issueKey: input.issueKey }, "could not bridge the pull requests to the issue");
|
|
@@ -128,6 +145,23 @@ export async function handlePullRequestClosed(db, trackers, pr) {
|
|
|
128
145
|
// Not caught: nothing is claimed yet, and a throw rolls back the delivery so it is redelivered.
|
|
129
146
|
const rows = await db.issuePullRequest.findMany({ where: { ...where, state: "open" } });
|
|
130
147
|
for (const row of rows) {
|
|
148
|
+
// Exactly once per claimed row, from the finally below, even when a later step throws: the row is no
|
|
149
|
+
// longer open, so nothing would retry it. Never throws.
|
|
150
|
+
const emitClosed = (movedTo) => emitWorkflowEvent({
|
|
151
|
+
dedupeKey: dedupeKeys.prClosed(pr.codeProvider, pr.repository, pr.number),
|
|
152
|
+
agentId: row.agentId,
|
|
153
|
+
workItem: { provider: row.issueProvider, key: row.issueKey },
|
|
154
|
+
pullRequest: { codeProvider: pr.codeProvider, repository: pr.repository, number: pr.number },
|
|
155
|
+
payload: {
|
|
156
|
+
kind: "pr_closed",
|
|
157
|
+
prLabel: `${row.repository}#${row.number}`,
|
|
158
|
+
prUrl: row.url,
|
|
159
|
+
merged: pr.merged,
|
|
160
|
+
movedTo,
|
|
161
|
+
},
|
|
162
|
+
});
|
|
163
|
+
let claimedRow = false;
|
|
164
|
+
let movedTo = null;
|
|
131
165
|
try {
|
|
132
166
|
const claimed = await db.issuePullRequest.updateMany({
|
|
133
167
|
where: { id: row.id, state: "open" },
|
|
@@ -135,6 +169,7 @@ export async function handlePullRequestClosed(db, trackers, pr) {
|
|
|
135
169
|
});
|
|
136
170
|
if (claimed.count === 0)
|
|
137
171
|
continue;
|
|
172
|
+
claimedRow = true;
|
|
138
173
|
const tracker = trackers[row.issueProvider];
|
|
139
174
|
if (!tracker)
|
|
140
175
|
continue;
|
|
@@ -166,6 +201,7 @@ export async function handlePullRequestClosed(db, trackers, pr) {
|
|
|
166
201
|
if (target) {
|
|
167
202
|
try {
|
|
168
203
|
const moved = await tracker.transitionTo(row.issueKey, target);
|
|
204
|
+
movedTo = moved.toStatus;
|
|
169
205
|
markdown += ` Moved to ${moved.toStatus}.`;
|
|
170
206
|
}
|
|
171
207
|
catch (err) {
|
|
@@ -185,5 +221,9 @@ export async function handlePullRequestClosed(db, trackers, pr) {
|
|
|
185
221
|
catch (err) {
|
|
186
222
|
log.warn({ err, issueKey: row.issueKey, ...where }, "could not bridge the closed pull request to the issue");
|
|
187
223
|
}
|
|
224
|
+
finally {
|
|
225
|
+
if (claimedRow)
|
|
226
|
+
await emitClosed(movedTo);
|
|
227
|
+
}
|
|
188
228
|
}
|
|
189
229
|
}
|
|
@@ -4,6 +4,7 @@ import { issueStatusRow, postIssueWorkingStatus } from "./issue-status.js";
|
|
|
4
4
|
import { logger } from "./logger.js";
|
|
5
5
|
import { openSiblingsForIssue, SIBLING_GUIDANCE } from "./related-pull-requests.js";
|
|
6
6
|
import { composeTaskOverride } from "./untrusted-content.js";
|
|
7
|
+
import { dedupeKeys, emitWorkflowEvent } from "./workflow-events.js";
|
|
7
8
|
const log = logger.child({ module: "issue-events" });
|
|
8
9
|
const MAX_TASK_BODY = 8000;
|
|
9
10
|
const RUN_ID_RE = /^[A-Za-z0-9][A-Za-z0-9_-]{0,127}$/;
|
|
@@ -93,7 +94,7 @@ export async function routeIssueEvent(event, deps) {
|
|
|
93
94
|
return result;
|
|
94
95
|
const links = (await deps.db.agentIssueProject.findMany({
|
|
95
96
|
where: { provider: event.provider, projectKey: event.projectKey },
|
|
96
|
-
include: { agent: { select: { ownerId: true, kind: true } } },
|
|
97
|
+
include: { agent: { select: { ownerId: true, kind: true, name: true } } },
|
|
97
98
|
}));
|
|
98
99
|
const bot = await tracker.botAccountId();
|
|
99
100
|
// One snapshot per event, taken lazily on the first dispatch so an event no link matches costs nothing.
|
|
@@ -139,6 +140,16 @@ export async function routeIssueEvent(event, deps) {
|
|
|
139
140
|
const runId = dispatched.run.id;
|
|
140
141
|
result.runIds.push(runId);
|
|
141
142
|
result.followUps.push(() => postIssueWorkingStatus(deps.db, deps.trackers, runId));
|
|
143
|
+
// describeKind shows only admin-configured values, so the trigger is safe to show in chat.
|
|
144
|
+
const trigger = matched.map((k) => describeKind(event, k, link)).join("; ");
|
|
145
|
+
const agentName = link.agent.name;
|
|
146
|
+
result.followUps.push(() => emitWorkflowEvent({
|
|
147
|
+
dedupeKey: dedupeKeys.issuePickedUp(runId),
|
|
148
|
+
runId,
|
|
149
|
+
agentId: link.agentId,
|
|
150
|
+
workItem: { provider: event.provider, key: event.issueKey },
|
|
151
|
+
payload: { kind: "issue_picked_up", agentName, trigger },
|
|
152
|
+
}));
|
|
142
153
|
log.info({ issueKey: event.issueKey, agentId: link.agentId, runId, kinds: matched }, "issue run dispatched");
|
|
143
154
|
}
|
|
144
155
|
catch (err) {
|
|
@@ -22,12 +22,23 @@ export function toJiraMarkdown(text) {
|
|
|
22
22
|
export function issueStatusRow(event, runId, visibilityRole) {
|
|
23
23
|
return { runId, provider: "jira", issueKey: event.issueKey, visibilityRole };
|
|
24
24
|
}
|
|
25
|
+
/**
|
|
26
|
+
* Cents from $1 up ($12.34, $1234.50); below $1, two significant digits with at
|
|
27
|
+
* least two decimals so fractions of a cent stay readable ($0.50, $0.12, $0.0035).
|
|
28
|
+
* Never exponent form.
|
|
29
|
+
*/
|
|
30
|
+
const usd = (amount) => {
|
|
31
|
+
const abs = Math.abs(amount);
|
|
32
|
+
const decimals = abs === 0 || abs >= 1 ? 2 : Math.max(2, 1 - Math.floor(Math.log10(abs)));
|
|
33
|
+
// Trailing zeros past the cents are noise ($0.010, $0.100 from rounding up).
|
|
34
|
+
return `$${amount.toFixed(Math.min(decimals, 20)).replace(/(\.\d\d\d*?)0+$/, "$1")}`;
|
|
35
|
+
};
|
|
25
36
|
export function formatSpendLine(input) {
|
|
26
|
-
const parts = [`Agent spend:
|
|
37
|
+
const parts = [`Agent spend: ${usd(input.treeUsd)} this run`];
|
|
27
38
|
if (input.issueUsd !== null)
|
|
28
|
-
parts.push(
|
|
39
|
+
parts.push(`${usd(input.issueUsd)} on this issue so far`);
|
|
29
40
|
if (input.models.length > 0)
|
|
30
|
-
parts.push(input.models.map((m) => `${m.model}
|
|
41
|
+
parts.push(input.models.map((m) => `${m.model} ${usd(m.costUsd)}`).join(", "));
|
|
31
42
|
return parts.join(" · ");
|
|
32
43
|
}
|
|
33
44
|
/** The whole run tree under `runId` (every depth), the issue's total across all its runs, and the tree's models. */
|