@revisium/orchestrator 0.2.0-alpha.3 → 0.2.0-alpha.5
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/README.md +4 -4
- package/control-plane/bootstrap.config.json +108 -550
- package/control-plane/default-playbook/catalog/pipelines.json +446 -1123
- package/control-plane/default-playbook/catalog/roles.json +1 -1
- package/control-plane/default-playbook/catalog/run-profiles.json +338 -0
- package/control-plane/default-playbook/package.json +1 -1
- package/control-plane/default-playbook/playbook.json +2 -1
- package/control-plane/default-playbook/prompts/developer.md +6 -6
- package/control-plane/default-playbook/prompts/integrator.md +1 -1
- package/dist/__generated__/client/browser.js +18 -0
- package/dist/__generated__/client/browser.js.map +1 -0
- package/dist/__generated__/client/client.js +32 -0
- package/dist/__generated__/client/client.js.map +1 -0
- package/dist/__generated__/client/commonInputTypes.js +11 -0
- package/dist/__generated__/client/commonInputTypes.js.map +1 -0
- package/dist/__generated__/client/enums.js +19 -0
- package/dist/__generated__/client/enums.js.map +1 -0
- package/dist/__generated__/client/internal/class.js +53 -0
- package/dist/__generated__/client/internal/class.js.map +1 -0
- package/dist/__generated__/client/internal/prismaNamespace.js +450 -0
- package/dist/__generated__/client/internal/prismaNamespace.js.map +1 -0
- package/dist/__generated__/client/internal/prismaNamespaceBrowser.js +421 -0
- package/dist/__generated__/client/internal/prismaNamespaceBrowser.js.map +1 -0
- package/dist/__generated__/client/models/Branch.js +2 -0
- package/dist/__generated__/client/models/Branch.js.map +1 -0
- package/dist/__generated__/client/models/CostLedgerEntry.js +2 -0
- package/dist/__generated__/client/models/CostLedgerEntry.js.map +1 -0
- package/dist/__generated__/client/models/FileBlob.js +2 -0
- package/dist/__generated__/client/models/FileBlob.js.map +1 -0
- package/dist/__generated__/client/models/InboxItem.js +2 -0
- package/dist/__generated__/client/models/InboxItem.js.map +1 -0
- package/dist/__generated__/client/models/ProjectFileUsage.js +2 -0
- package/dist/__generated__/client/models/ProjectFileUsage.js.map +1 -0
- package/dist/__generated__/client/models/Revision.js +2 -0
- package/dist/__generated__/client/models/Revision.js.map +1 -0
- package/dist/__generated__/client/models/RevoProject.js +2 -0
- package/dist/__generated__/client/models/RevoProject.js.map +1 -0
- package/dist/__generated__/client/models/Row.js +2 -0
- package/dist/__generated__/client/models/Row.js.map +1 -0
- package/dist/__generated__/client/models/RunAttempt.js +2 -0
- package/dist/__generated__/client/models/RunAttempt.js.map +1 -0
- package/dist/__generated__/client/models/RunEvent.js +2 -0
- package/dist/__generated__/client/models/RunEvent.js.map +1 -0
- package/dist/__generated__/client/models/RunOutput.js +2 -0
- package/dist/__generated__/client/models/RunOutput.js.map +1 -0
- package/dist/__generated__/client/models/RunTask.js +2 -0
- package/dist/__generated__/client/models/RunTask.js.map +1 -0
- package/dist/__generated__/client/models/Table.js +2 -0
- package/dist/__generated__/client/models/Table.js.map +1 -0
- package/dist/__generated__/client/models/TableMigration.js +2 -0
- package/dist/__generated__/client/models/TableMigration.js.map +1 -0
- package/dist/__generated__/client/models/TaskRun.js +2 -0
- package/dist/__generated__/client/models/TaskRun.js.map +1 -0
- package/dist/__generated__/client/models.js +2 -0
- package/dist/__generated__/client/models.js.map +1 -0
- package/dist/api/graphql-api/graphql-ws/pubsub.module.js +2 -1
- package/dist/api/graphql-api/graphql-ws/pubsub.module.js.map +1 -1
- package/dist/api/graphql-api/graphql-ws/subscription-bridge.service.js +110 -28
- package/dist/api/graphql-api/graphql-ws/subscription-bridge.service.js.map +1 -1
- package/dist/api/graphql-api/graphql-ws/subscription-mappers.js +3 -1
- package/dist/api/graphql-api/graphql-ws/subscription-mappers.js.map +1 -1
- package/dist/api/graphql-api/inbox/inputs/resolve-gate.input.js +9 -0
- package/dist/api/graphql-api/inbox/inputs/resolve-gate.input.js.map +1 -1
- package/dist/api/graphql-api/method/inputs/create-run-profile.input.js +54 -0
- package/dist/api/graphql-api/method/inputs/create-run-profile.input.js.map +1 -0
- package/dist/api/graphql-api/method/inputs/deprecate-run-profile.input.js +37 -0
- package/dist/api/graphql-api/method/inputs/deprecate-run-profile.input.js.map +1 -0
- package/dist/api/graphql-api/method/inputs/get-run-profile.input.js +32 -0
- package/dist/api/graphql-api/method/inputs/get-run-profile.input.js.map +1 -0
- package/dist/api/graphql-api/method/inputs/list-run-profiles.input.js +33 -0
- package/dist/api/graphql-api/method/inputs/list-run-profiles.input.js.map +1 -0
- package/dist/api/graphql-api/method/inputs/update-run-profile.input.js +59 -0
- package/dist/api/graphql-api/method/inputs/update-run-profile.input.js.map +1 -0
- package/dist/api/graphql-api/method/inputs/validate-run-profile.input.js +33 -0
- package/dist/api/graphql-api/method/inputs/validate-run-profile.input.js.map +1 -0
- package/dist/api/graphql-api/method/method.resolver.js +76 -1
- package/dist/api/graphql-api/method/method.resolver.js.map +1 -1
- package/dist/api/graphql-api/method/model/run-profile-connection.model.js +16 -0
- package/dist/api/graphql-api/method/model/run-profile-connection.model.js.map +1 -0
- package/dist/api/graphql-api/method/model/run-profile-status.model.js +6 -0
- package/dist/api/graphql-api/method/model/run-profile-status.model.js.map +1 -0
- package/dist/api/graphql-api/method/model/run-profile.model.js +79 -0
- package/dist/api/graphql-api/method/model/run-profile.model.js.map +1 -0
- package/dist/api/graphql-api/pr/inputs/pr-readiness.input.js +6 -0
- package/dist/api/graphql-api/pr/inputs/pr-readiness.input.js.map +1 -1
- package/dist/api/graphql-api/registerGraphqlEnums.js +6 -0
- package/dist/api/graphql-api/registerGraphqlEnums.js.map +1 -1
- package/dist/api/graphql-api/runs/inputs/create-run.input.js +17 -1
- package/dist/api/graphql-api/runs/inputs/create-run.input.js.map +1 -1
- package/dist/api/graphql-api/runs/inputs/simulate-route.input.js +11 -1
- package/dist/api/graphql-api/runs/inputs/simulate-route.input.js.map +1 -1
- package/dist/api/graphql-api/runs/model/run-workflow.model.js +5 -0
- package/dist/api/graphql-api/runs/model/run-workflow.model.js.map +1 -1
- package/dist/api/graphql-api/share/model/issue-action.model.js +7 -0
- package/dist/api/graphql-api/share/model/issue-action.model.js.map +1 -0
- package/dist/cli/commands/doctor-report.js +1 -13
- package/dist/cli/commands/doctor-report.js.map +1 -1
- package/dist/cli/commands/lifecycle.js +50 -65
- package/dist/cli/commands/lifecycle.js.map +1 -1
- package/dist/cli/commands/{revisium-helpers.js → process-helpers.js} +1 -24
- package/dist/cli/commands/process-helpers.js.map +1 -0
- package/dist/config.js +17 -55
- package/dist/config.js.map +1 -1
- package/dist/control-plane/bootstrap-json-validator.js +65 -0
- package/dist/control-plane/bootstrap-json-validator.js.map +1 -0
- package/dist/control-plane/bootstrap.js +25 -38
- package/dist/control-plane/bootstrap.js.map +1 -1
- package/dist/control-plane/change-notifications.js +45 -14
- package/dist/control-plane/change-notifications.js.map +1 -1
- package/dist/control-plane/data-access.js +1 -106
- package/dist/control-plane/data-access.js.map +1 -1
- package/dist/control-plane/default-playbook-policy.js +488 -94
- package/dist/control-plane/default-playbook-policy.js.map +1 -1
- package/dist/control-plane/definitions.js +4 -5
- package/dist/control-plane/definitions.js.map +1 -1
- package/dist/control-plane/engine-transport.js +383 -0
- package/dist/control-plane/engine-transport.js.map +1 -0
- package/dist/control-plane/inbox.js +15 -7
- package/dist/control-plane/inbox.js.map +1 -1
- package/dist/control-plane/index.js +0 -1
- package/dist/control-plane/index.js.map +1 -1
- package/dist/control-plane/json-fields.js +1 -1
- package/dist/control-plane/json-fields.js.map +1 -1
- package/dist/control-plane/merge-override-audit.js +2 -2
- package/dist/control-plane/merge-override-audit.js.map +1 -1
- package/dist/control-plane/pipeline-coverage-registry.js +486 -0
- package/dist/control-plane/pipeline-coverage-registry.js.map +1 -0
- package/dist/control-plane/query-types.js +2 -0
- package/dist/control-plane/query-types.js.map +1 -0
- package/dist/control-plane/run-profiles.js +175 -0
- package/dist/control-plane/run-profiles.js.map +1 -0
- package/dist/control-plane/schema-migration.js +0 -35
- package/dist/control-plane/schema-migration.js.map +1 -1
- package/dist/control-plane/seed-default-playbook.js +2 -31
- package/dist/control-plane/seed-default-playbook.js.map +1 -1
- package/dist/control-plane/tables.js +8 -0
- package/dist/control-plane/tables.js.map +1 -1
- package/dist/control-plane/transport.js +16 -0
- package/dist/control-plane/transport.js.map +1 -0
- package/dist/control-plane/versioned-meaning.js +72 -15
- package/dist/control-plane/versioned-meaning.js.map +1 -1
- package/dist/e2e/kit/agents.js +63 -8
- package/dist/e2e/kit/agents.js.map +1 -1
- package/dist/e2e/kit/assertions.js +15 -3
- package/dist/e2e/kit/assertions.js.map +1 -1
- package/dist/e2e/kit/crash.js +8 -4
- package/dist/e2e/kit/crash.js.map +1 -1
- package/dist/e2e/kit/drive.js +46 -11
- package/dist/e2e/kit/drive.js.map +1 -1
- package/dist/e2e/kit/env.js +17 -1
- package/dist/e2e/kit/env.js.map +1 -1
- package/dist/e2e/kit/fake-integrator.js +9 -22
- package/dist/e2e/kit/fake-integrator.js.map +1 -1
- package/dist/e2e/kit/gh-emulator.js +166 -26
- package/dist/e2e/kit/gh-emulator.js.map +1 -1
- package/dist/e2e/kit/harness.js +44 -10
- package/dist/e2e/kit/harness.js.map +1 -1
- package/dist/e2e/kit/index.js +2 -0
- package/dist/e2e/kit/index.js.map +1 -1
- package/dist/e2e/kit/run-profiles.js +50 -0
- package/dist/e2e/kit/run-profiles.js.map +1 -0
- package/dist/e2e/kit/scenario.js +207 -0
- package/dist/e2e/kit/scenario.js.map +1 -0
- package/dist/e2e/kit/scenarios.js +38 -45
- package/dist/e2e/kit/scenarios.js.map +1 -1
- package/dist/e2e/recovery-crash-child.js +2 -2
- package/dist/e2e/recovery-crash-child.js.map +1 -1
- package/dist/e2e/recovery-dd-crash-child.js +8 -6
- package/dist/e2e/recovery-dd-crash-child.js.map +1 -1
- package/dist/engine/dbos.service.js +8 -1
- package/dist/engine/dbos.service.js.map +1 -1
- package/dist/features/inbox/commands/impl/resolve-gate.command.js.map +1 -1
- package/dist/features/method/commands/handlers/method-command.handlers.js +64 -0
- package/dist/features/method/commands/handlers/method-command.handlers.js.map +1 -0
- package/dist/features/method/commands/impl/create-run-profile.command.js +7 -0
- package/dist/features/method/commands/impl/create-run-profile.command.js.map +1 -0
- package/dist/features/method/commands/impl/deprecate-run-profile.command.js +7 -0
- package/dist/features/method/commands/impl/deprecate-run-profile.command.js.map +1 -0
- package/dist/features/method/commands/impl/update-run-profile.command.js +7 -0
- package/dist/features/method/commands/impl/update-run-profile.command.js.map +1 -0
- package/dist/features/method/commands/index.js +6 -1
- package/dist/features/method/commands/index.js.map +1 -1
- package/dist/features/method/method-api.service.js +31 -3
- package/dist/features/method/method-api.service.js.map +1 -1
- package/dist/features/method/queries/handlers/method-query.handlers.js +58 -1
- package/dist/features/method/queries/handlers/method-query.handlers.js.map +1 -1
- package/dist/features/method/queries/impl/get-run-profile.query.js +7 -0
- package/dist/features/method/queries/impl/get-run-profile.query.js.map +1 -0
- package/dist/features/method/queries/impl/list-run-profiles.query.js +7 -0
- package/dist/features/method/queries/impl/list-run-profiles.query.js.map +1 -0
- package/dist/features/method/queries/impl/validate-run-profile.query.js +7 -0
- package/dist/features/method/queries/impl/validate-run-profile.query.js.map +1 -0
- package/dist/features/method/queries/index.js +4 -1
- package/dist/features/method/queries/index.js.map +1 -1
- package/dist/features/pr/queries/impl/get-pr-readiness.query.js.map +1 -1
- package/dist/features/runs/commands/impl/create-run.command.js.map +1 -1
- package/dist/features/runs/queries/impl/simulate-route.query.js.map +1 -1
- package/dist/features/shared/connection.js +6 -3
- package/dist/features/shared/connection.js.map +1 -1
- package/dist/host/daemon.js +32 -8
- package/dist/host/daemon.js.map +1 -1
- package/dist/host/ensure-host.js +3 -3
- package/dist/host/ensure-host.js.map +1 -1
- package/dist/host/host-runtime.js.map +1 -1
- package/dist/host/host.lifecycle.js +3 -16
- package/dist/host/host.lifecycle.js.map +1 -1
- package/dist/http/graphql-host.js +17 -8
- package/dist/http/graphql-host.js.map +1 -1
- package/dist/mcp/mcp-capabilities.js +8 -2
- package/dist/mcp/mcp-capabilities.js.map +1 -1
- package/dist/mcp/mcp-facade.service.js +53 -10
- package/dist/mcp/mcp-facade.service.js.map +1 -1
- package/dist/mcp/mcp-tool-result.js +22 -0
- package/dist/mcp/mcp-tool-result.js.map +1 -0
- package/dist/mcp/mcp-tools.js +108 -1
- package/dist/mcp/mcp-tools.js.map +1 -1
- package/dist/pipeline/await-human.js +27 -8
- package/dist/pipeline/await-human.js.map +1 -1
- package/dist/pipeline/data-driven-task.workflow.js +573 -105
- package/dist/pipeline/data-driven-task.workflow.js.map +1 -1
- package/dist/pipeline/pipeline.service.js +28 -12
- package/dist/pipeline/pipeline.service.js.map +1 -1
- package/dist/pipeline/route-contract.js +80 -39
- package/dist/pipeline/route-contract.js.map +1 -1
- package/dist/pipeline-core/condition-scopes.js +15 -0
- package/dist/pipeline-core/condition-scopes.js.map +1 -0
- package/dist/pipeline-core/index.js +2 -0
- package/dist/pipeline-core/index.js.map +1 -1
- package/dist/pipeline-core/kit/fixtures.js +81 -44
- package/dist/pipeline-core/kit/fixtures.js.map +1 -1
- package/dist/pipeline-core/materialize.js +221 -0
- package/dist/pipeline-core/materialize.js.map +1 -0
- package/dist/pipeline-core/types.js +5 -0
- package/dist/pipeline-core/types.js.map +1 -1
- package/dist/pipeline-core/validate-loops.js +2 -15
- package/dist/pipeline-core/validate-loops.js.map +1 -1
- package/dist/pipeline-core/validate-resilience.js +257 -0
- package/dist/pipeline-core/validate-resilience.js.map +1 -0
- package/dist/pipeline-core/validate.js +2 -0
- package/dist/pipeline-core/validate.js.map +1 -1
- package/dist/playbook/catalog-loader.js +40 -3
- package/dist/playbook/catalog-loader.js.map +1 -1
- package/dist/playbook/catalog-schema-validator.js +468 -0
- package/dist/playbook/catalog-schema-validator.js.map +1 -0
- package/dist/playbook/import-mapper.js +55 -3
- package/dist/playbook/import-mapper.js.map +1 -1
- package/dist/playbook/manifest.js +5 -0
- package/dist/playbook/manifest.js.map +1 -1
- package/dist/playbook/playbook-installer.js +17 -1
- package/dist/playbook/playbook-installer.js.map +1 -1
- package/dist/poller/pr-readiness-core.js +257 -93
- package/dist/poller/pr-readiness-core.js.map +1 -1
- package/dist/poller/pr-readiness.js +2 -2
- package/dist/poller/pr-readiness.js.map +1 -1
- package/dist/revisium/inbox.service.js +8 -8
- package/dist/revisium/inbox.service.js.map +1 -1
- package/dist/revisium/playbooks.service.js +311 -21
- package/dist/revisium/playbooks.service.js.map +1 -1
- package/dist/revisium/revisium.module.js +8 -8
- package/dist/revisium/revisium.module.js.map +1 -1
- package/dist/revisium/roles.service.js +3 -1
- package/dist/revisium/roles.service.js.map +1 -1
- package/dist/revisium/run.service.js +9 -8
- package/dist/revisium/run.service.js.map +1 -1
- package/dist/revisium/tokens.js +0 -1
- package/dist/revisium/tokens.js.map +1 -1
- package/dist/run/create-run.js +1 -4
- package/dist/run/create-run.js.map +1 -1
- package/dist/run/inspect-run.js +33 -29
- package/dist/run/inspect-run.js.map +1 -1
- package/dist/run/issue-ref.js +97 -2
- package/dist/run/issue-ref.js.map +1 -1
- package/dist/run/prisma-runtime-data-access.js +616 -0
- package/dist/run/prisma-runtime-data-access.js.map +1 -0
- package/dist/run/run-outputs.js +39 -14
- package/dist/run/run-outputs.js.map +1 -1
- package/dist/run/terminal-run-status.js.map +1 -1
- package/dist/runners/gh-identity.js +29 -7
- package/dist/runners/gh-identity.js.map +1 -1
- package/dist/runners/integrator.js +692 -89
- package/dist/runners/integrator.js.map +1 -1
- package/dist/runners/worktree.service.js +10 -8
- package/dist/runners/worktree.service.js.map +1 -1
- package/dist/schema/ajv-errors.js +15 -0
- package/dist/schema/ajv-errors.js.map +1 -0
- package/dist/smoke/isolation.js +7 -16
- package/dist/smoke/isolation.js.map +1 -1
- package/dist/storage/ensure-storage.js +118 -0
- package/dist/storage/ensure-storage.js.map +1 -0
- package/dist/storage/revo-database.js +39 -0
- package/dist/storage/revo-database.js.map +1 -0
- package/dist/storage/revo-prisma.service.js +31 -0
- package/dist/storage/revo-prisma.service.js.map +1 -0
- package/dist/storage/revo-storage.module.js +18 -0
- package/dist/storage/revo-storage.module.js.map +1 -0
- package/dist/task-control-plane/pr-readiness.service.js +5 -2
- package/dist/task-control-plane/pr-readiness.service.js.map +1 -1
- package/dist/task-control-plane/run-watch.service.js +3 -6
- package/dist/task-control-plane/run-watch.service.js.map +1 -1
- package/dist/task-control-plane/task-control-plane-api.service.js +518 -85
- package/dist/task-control-plane/task-control-plane-api.service.js.map +1 -1
- package/dist/testing/runtime-data-access.js +175 -0
- package/dist/testing/runtime-data-access.js.map +1 -0
- package/dist/worker/build-context.js +178 -2
- package/dist/worker/build-context.js.map +1 -1
- package/dist/worker/claude-code-runner.js +23 -6
- package/dist/worker/claude-code-runner.js.map +1 -1
- package/dist/worker/codex-runner.js +6 -1
- package/dist/worker/codex-runner.js.map +1 -1
- package/package.json +20 -7
- package/prisma/migrations/20260707080500_initial_schema/migration.sql +259 -0
- package/prisma/migrations/20260707103000_prisma_runtime_tables/migration.sql +234 -0
- package/prisma/migrations/migration_lock.toml +3 -0
- package/prisma/schema.prisma +338 -0
- package/prisma.config.ts +11 -0
- package/dist/cli/commands/revisium-helpers.js.map +0 -1
- package/dist/control-plane/client-transport.js +0 -168
- package/dist/control-plane/client-transport.js.map +0 -1
- package/dist/engine/ensure-postgres.js +0 -46
- package/dist/engine/ensure-postgres.js.map +0 -1
- package/dist/host/ensure-revisium.js +0 -135
- package/dist/host/ensure-revisium.js.map +0 -1
- package/revisium.config.json +0 -10
|
@@ -12,24 +12,38 @@ var __param = (this && this.__param) || function (paramIndex, decorator) {
|
|
|
12
12
|
};
|
|
13
13
|
import { Inject, Injectable } from '@nestjs/common';
|
|
14
14
|
import { execFileSync } from 'node:child_process';
|
|
15
|
-
import { collectPrReadiness, fetchRequiredCheckNames, } from '../poller/pr-readiness-core.js';
|
|
15
|
+
import { collectPrReadiness, fetchRequiredCheckNames, GITHUB_CHECK_ROLLUP_UNAVAILABLE, } from '../poller/pr-readiness-core.js';
|
|
16
16
|
import { RunService } from '../revisium/run.service.js';
|
|
17
|
-
import { issueRefTag } from '../run/issue-ref.js';
|
|
18
|
-
import {
|
|
17
|
+
import { hasClosingIssueReference, hasIssueRefToken, issueBodyWithClosingReference, issueRefTag, } from '../run/issue-ref.js';
|
|
18
|
+
import { redactTokens, resolvePinnedGh } from './gh-identity.js';
|
|
19
19
|
import { gitAbsPath, branchExists, countAhead } from './integrator-git.js';
|
|
20
20
|
import { resolveOwnerRepo } from './integrator-remote.js';
|
|
21
21
|
import { branchName } from './integrator-branch-naming.js';
|
|
22
22
|
export { resolveExecutable } from './integrator-git.js';
|
|
23
23
|
export { branchName };
|
|
24
24
|
export { parseOwnerRepo } from './integrator-remote.js';
|
|
25
|
-
function issueBoundTitle(title, issueRef, ownerRepo) {
|
|
25
|
+
function issueBoundTitle(title, issueRef, ownerRepo, issueAction = issueRef ? 'close' : 'none') {
|
|
26
|
+
if (!issueRef || issueAction === 'none')
|
|
27
|
+
return title;
|
|
28
|
+
if (hasIssueRefToken(title, issueRef, ownerRepo))
|
|
29
|
+
return title;
|
|
26
30
|
const tag = issueRefTag(issueRef, ownerRepo);
|
|
27
31
|
return tag ? `${tag} ${title}` : title;
|
|
28
32
|
}
|
|
29
|
-
function commitMessage(title, issueRef, ownerRepo) {
|
|
33
|
+
function commitMessage(title, issueRef, ownerRepo, issueAction = issueRef ? 'close' : 'none') {
|
|
34
|
+
if (!issueRef || issueAction === 'none')
|
|
35
|
+
return `feat: ${title}`;
|
|
30
36
|
const tag = issueRefTag(issueRef, ownerRepo);
|
|
31
37
|
return tag ? `feat: ${tag} ${title}` : `feat: ${title}`;
|
|
32
38
|
}
|
|
39
|
+
function prBody(body, issueRef, ownerRepo, issueAction) {
|
|
40
|
+
if (issueAction !== 'close')
|
|
41
|
+
return body ?? '';
|
|
42
|
+
return issueBodyWithClosingReference(body, issueRef, ownerRepo);
|
|
43
|
+
}
|
|
44
|
+
function resolvedIssueAction(issueRef, issueAction) {
|
|
45
|
+
return issueAction ?? (issueRef ? 'close' : undefined);
|
|
46
|
+
}
|
|
33
47
|
function parsePrList(raw) {
|
|
34
48
|
try {
|
|
35
49
|
return JSON.parse(raw);
|
|
@@ -38,6 +52,22 @@ function parsePrList(raw) {
|
|
|
38
52
|
throw new Error(`gh pr list returned non-JSON: ${raw.slice(0, 200)}`);
|
|
39
53
|
}
|
|
40
54
|
}
|
|
55
|
+
function prAuthorLogin(author) {
|
|
56
|
+
if (typeof author === 'string' && author.trim().length > 0)
|
|
57
|
+
return author.trim();
|
|
58
|
+
if (author && typeof author === 'object' && typeof author.login === 'string' && author.login.trim().length > 0) {
|
|
59
|
+
return author.login.trim();
|
|
60
|
+
}
|
|
61
|
+
return undefined;
|
|
62
|
+
}
|
|
63
|
+
function asRecord(value) {
|
|
64
|
+
if (value === null || typeof value !== 'object' || Array.isArray(value))
|
|
65
|
+
return undefined;
|
|
66
|
+
return value;
|
|
67
|
+
}
|
|
68
|
+
function nonBlank(value) {
|
|
69
|
+
return typeof value === 'string' && value.trim().length > 0 ? value.trim() : undefined;
|
|
70
|
+
}
|
|
41
71
|
function matchingOpenPr(ownerRepo, branch, base, execGh, jsonFields) {
|
|
42
72
|
const raw = execGh([
|
|
43
73
|
'pr',
|
|
@@ -56,7 +86,14 @@ function matchingOpenPr(ownerRepo, branch, base, execGh, jsonFields) {
|
|
|
56
86
|
const pr = matching[0];
|
|
57
87
|
if (!pr)
|
|
58
88
|
throw new Error('unexpected empty match');
|
|
59
|
-
return {
|
|
89
|
+
return {
|
|
90
|
+
prUrl: pr.url,
|
|
91
|
+
prNumber: pr.number,
|
|
92
|
+
...(pr.headRefOid ? { headSha: pr.headRefOid } : {}),
|
|
93
|
+
...(pr.title !== undefined ? { title: pr.title } : {}),
|
|
94
|
+
...(pr.body !== undefined ? { body: pr.body } : {}),
|
|
95
|
+
...(prAuthorLogin(pr.author) ? { author: prAuthorLogin(pr.author) } : {}),
|
|
96
|
+
};
|
|
60
97
|
}
|
|
61
98
|
if (matching.length > 1) {
|
|
62
99
|
const candidates = matching.map((p) => `#${p.number}`).join(', ');
|
|
@@ -67,7 +104,9 @@ function matchingOpenPr(ownerRepo, branch, base, execGh, jsonFields) {
|
|
|
67
104
|
}
|
|
68
105
|
return null;
|
|
69
106
|
}
|
|
70
|
-
function createPr(ownerRepo, branch, base, title, issueRef, execGh) {
|
|
107
|
+
function createPr(ownerRepo, branch, base, title, issueRef, issueAction, execGh) {
|
|
108
|
+
const resolvedTitle = issueBoundTitle(title, issueRef, ownerRepo, issueAction);
|
|
109
|
+
const resolvedBody = prBody(undefined, issueRef, ownerRepo, issueAction);
|
|
71
110
|
const createOut = execGh([
|
|
72
111
|
'pr',
|
|
73
112
|
'create',
|
|
@@ -79,9 +118,9 @@ function createPr(ownerRepo, branch, base, title, issueRef, execGh) {
|
|
|
79
118
|
'--head',
|
|
80
119
|
branch,
|
|
81
120
|
'--title',
|
|
82
|
-
|
|
121
|
+
resolvedTitle,
|
|
83
122
|
'--body',
|
|
84
|
-
|
|
123
|
+
resolvedBody,
|
|
85
124
|
]);
|
|
86
125
|
const createdUrl = createOut.trim();
|
|
87
126
|
const viewRaw = execGh([
|
|
@@ -104,19 +143,67 @@ function createPr(ownerRepo, branch, base, title, issueRef, execGh) {
|
|
|
104
143
|
'check if the PR was created and update the run manually',
|
|
105
144
|
};
|
|
106
145
|
}
|
|
107
|
-
return { prUrl: viewData.url, prNumber: viewData.number };
|
|
146
|
+
return { prUrl: viewData.url, prNumber: viewData.number, title: resolvedTitle, body: resolvedBody };
|
|
147
|
+
}
|
|
148
|
+
function repairPr(ownerRepo, prNumber, issueRef, issueAction, title, body, desiredTitle, desiredBody, execGh) {
|
|
149
|
+
if (!issueRef && issueAction !== 'close')
|
|
150
|
+
return;
|
|
151
|
+
const args = ['pr', 'edit', String(prNumber), '--repo', ownerRepo];
|
|
152
|
+
if ((title ?? '') !== desiredTitle)
|
|
153
|
+
args.push('--title', desiredTitle);
|
|
154
|
+
if ((body ?? '') !== desiredBody)
|
|
155
|
+
args.push('--body', desiredBody);
|
|
156
|
+
if (args.length > 5)
|
|
157
|
+
execGh(args);
|
|
108
158
|
}
|
|
109
|
-
function findOrCreatePr(ownerRepo, branch, base, title, issueRef, execGh) {
|
|
110
|
-
const existing = matchingOpenPr(ownerRepo, branch, base, execGh, 'number,url,baseRefName');
|
|
159
|
+
function findOrCreatePr(ownerRepo, branch, base, title, issueRef, issueAction, execGh) {
|
|
160
|
+
const existing = matchingOpenPr(ownerRepo, branch, base, execGh, 'number,url,baseRefName,title,body');
|
|
111
161
|
if (existing && !('needsHuman' in existing)) {
|
|
162
|
+
repairPr(ownerRepo, existing.prNumber, issueRef, issueAction, existing.title, existing.body, issueBoundTitle(existing.title || title, issueRef, ownerRepo, issueAction), prBody(existing.body, issueRef, ownerRepo, issueAction), execGh);
|
|
112
163
|
return { prUrl: existing.prUrl, prNumber: existing.prNumber };
|
|
113
164
|
}
|
|
114
165
|
if (existing)
|
|
115
166
|
return existing;
|
|
116
|
-
return createPr(ownerRepo, branch, base, title, issueRef, execGh);
|
|
167
|
+
return createPr(ownerRepo, branch, base, title, issueRef, issueAction, execGh);
|
|
117
168
|
}
|
|
118
169
|
function findExistingPrWithHead(ownerRepo, branch, base, execGh) {
|
|
119
|
-
return matchingOpenPr(ownerRepo, branch, base, execGh, 'number,url,baseRefName,headRefOid');
|
|
170
|
+
return matchingOpenPr(ownerRepo, branch, base, execGh, 'number,url,baseRefName,headRefOid,title,body,author');
|
|
171
|
+
}
|
|
172
|
+
function foreignPrProvenance(author, integratorAccount) {
|
|
173
|
+
if (!integratorAccount)
|
|
174
|
+
return {};
|
|
175
|
+
if (!author || author.toLowerCase() === integratorAccount.toLowerCase())
|
|
176
|
+
return {};
|
|
177
|
+
return { foreignPr: true, prAuthor: author, integratorAccount };
|
|
178
|
+
}
|
|
179
|
+
function repairProducedChangePr(context, existing, provenance) {
|
|
180
|
+
if (provenance.foreignPr)
|
|
181
|
+
return;
|
|
182
|
+
repairPr(context.ownerRepo, existing.prNumber, context.issueRef, context.issueAction, existing.title, existing.body, issueBoundTitle(existing.title || context.title, context.issueRef, context.ownerRepo, context.issueAction), prBody(existing.body, context.issueRef, context.ownerRepo, context.issueAction), context.execGh);
|
|
183
|
+
}
|
|
184
|
+
function existingProducedChangeOutput(context, existing, provenance, status) {
|
|
185
|
+
return {
|
|
186
|
+
prUrl: existing.prUrl,
|
|
187
|
+
branch: context.branch,
|
|
188
|
+
prNumber: existing.prNumber,
|
|
189
|
+
...(context.issueRef ? { issueRef: context.issueRef } : {}),
|
|
190
|
+
headSha: context.change.headSha,
|
|
191
|
+
status,
|
|
192
|
+
...(status === 'noop' ? { message: 'nothing to integrate — produced head already pushed and equals PR head' } : {}),
|
|
193
|
+
...provenance,
|
|
194
|
+
};
|
|
195
|
+
}
|
|
196
|
+
function reuseExistingProducedChangePr(context, existing) {
|
|
197
|
+
if (existing.headSha !== context.change.headSha)
|
|
198
|
+
return null;
|
|
199
|
+
const provenance = foreignPrProvenance(existing.author, context.integratorAccount);
|
|
200
|
+
repairProducedChangePr(context, existing, provenance);
|
|
201
|
+
return existingProducedChangeOutput(context, existing, provenance, 'noop');
|
|
202
|
+
}
|
|
203
|
+
function updateExistingProducedChangePr(context, existing) {
|
|
204
|
+
const provenance = foreignPrProvenance(existing.author, context.integratorAccount);
|
|
205
|
+
repairProducedChangePr(context, existing, provenance);
|
|
206
|
+
return existingProducedChangeOutput(context, existing, provenance, 'pushed');
|
|
120
207
|
}
|
|
121
208
|
export async function preflightLive(taskId, base, deps) {
|
|
122
209
|
const { execGit, resolveTaskCwd } = deps;
|
|
@@ -172,20 +259,13 @@ export async function preflightLive(taskId, base, deps) {
|
|
|
172
259
|
}
|
|
173
260
|
return { ok: true };
|
|
174
261
|
}
|
|
175
|
-
export function stubIntegrate(input) {
|
|
176
|
-
return {
|
|
177
|
-
prUrl: 'stub://pr/placeholder',
|
|
178
|
-
branch: `feat/${input.taskId}-stub`,
|
|
179
|
-
prNumber: 0,
|
|
180
|
-
...(input.issueRef ? { issueRef: input.issueRef } : {}),
|
|
181
|
-
};
|
|
182
|
-
}
|
|
183
262
|
export async function captureProducedChange(input, deps) {
|
|
184
263
|
const { execGit: git, resolveRunCwd } = deps;
|
|
185
264
|
const cwd = await resolveRunCwd(input.runId, input.taskId);
|
|
186
265
|
const branch = branchName(input.taskId, input.title, input.issueRef);
|
|
187
266
|
const ownerRepoResult = resolveOwnerRepo(git, cwd);
|
|
188
267
|
const ownerRepo = 'needsHuman' in ownerRepoResult ? undefined : ownerRepoResult.ownerRepo;
|
|
268
|
+
const issueAction = resolvedIssueAction(input.issueRef, input.issueAction);
|
|
189
269
|
if (branchExists(git, cwd, branch)) {
|
|
190
270
|
git(['switch', branch], cwd);
|
|
191
271
|
}
|
|
@@ -194,13 +274,14 @@ export async function captureProducedChange(input, deps) {
|
|
|
194
274
|
}
|
|
195
275
|
git(['add', '-A'], cwd);
|
|
196
276
|
if (stagedDiffPresent(git, cwd)) {
|
|
197
|
-
git(['commit', '-m', commitMessage(input.title, input.issueRef, ownerRepo)], cwd);
|
|
277
|
+
git(['commit', '-m', commitMessage(input.title, input.issueRef, ownerRepo, issueAction)], cwd);
|
|
198
278
|
}
|
|
199
279
|
const headSha = git(['rev-parse', 'HEAD'], cwd).trim();
|
|
200
280
|
return {
|
|
201
281
|
branch,
|
|
202
282
|
headSha,
|
|
203
283
|
...(input.issueRef ? { issueRef: input.issueRef } : {}),
|
|
284
|
+
...(issueAction ? { issueAction } : {}),
|
|
204
285
|
worktreePath: cwd,
|
|
205
286
|
...(input.artifactRef ? { artifactRef: input.artifactRef } : {}),
|
|
206
287
|
};
|
|
@@ -224,6 +305,7 @@ export async function integrate(input, deps) {
|
|
|
224
305
|
if ('needsHuman' in ownerRepoResult)
|
|
225
306
|
return ownerRepoResult;
|
|
226
307
|
const { ownerRepo } = ownerRepoResult;
|
|
308
|
+
const issueAction = resolvedIssueAction(input.issueRef, input.issueAction);
|
|
227
309
|
git(['fetch', 'origin', input.base], cwd);
|
|
228
310
|
if (branchExists(git, cwd, branch)) {
|
|
229
311
|
git(['switch', branch], cwd);
|
|
@@ -233,7 +315,7 @@ export async function integrate(input, deps) {
|
|
|
233
315
|
}
|
|
234
316
|
git(['add', '-A'], cwd);
|
|
235
317
|
if (stagedDiffPresent(git, cwd)) {
|
|
236
|
-
const commitMsg = commitMessage(input.title, input.issueRef, ownerRepo);
|
|
318
|
+
const commitMsg = commitMessage(input.title, input.issueRef, ownerRepo, issueAction);
|
|
237
319
|
git(['commit', '-m', commitMsg], cwd);
|
|
238
320
|
}
|
|
239
321
|
else {
|
|
@@ -256,7 +338,7 @@ export async function integrate(input, deps) {
|
|
|
256
338
|
}
|
|
257
339
|
}
|
|
258
340
|
git(['push', '-u', 'origin', branch], cwd);
|
|
259
|
-
const prResult = findOrCreatePr(ownerRepo, branch, input.base, input.title, input.issueRef, gh);
|
|
341
|
+
const prResult = findOrCreatePr(ownerRepo, branch, input.base, input.title, input.issueRef, issueAction, gh);
|
|
260
342
|
if ('needsHuman' in prResult)
|
|
261
343
|
return prResult;
|
|
262
344
|
return { prUrl: prResult.prUrl, branch, prNumber: prResult.prNumber, ...(input.issueRef ? { issueRef: input.issueRef } : {}) };
|
|
@@ -266,6 +348,7 @@ async function integrateProducedChange(input, deps, change) {
|
|
|
266
348
|
const cwd = change.worktreePath ?? await deps.resolveRunCwd(input.runId, input.taskId);
|
|
267
349
|
const branch = change.branch;
|
|
268
350
|
const issueRef = change.issueRef ?? input.issueRef;
|
|
351
|
+
const issueAction = resolvedIssueAction(issueRef, change.issueAction ?? input.issueAction);
|
|
269
352
|
const ownerRepoResult = resolveOwnerRepo(git, cwd);
|
|
270
353
|
if ('needsHuman' in ownerRepoResult)
|
|
271
354
|
return ownerRepoResult;
|
|
@@ -274,16 +357,11 @@ async function integrateProducedChange(input, deps, change) {
|
|
|
274
357
|
const existing = findExistingPrWithHead(ownerRepo, branch, input.base, gh);
|
|
275
358
|
if (existing && 'needsHuman' in existing)
|
|
276
359
|
return existing;
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
...(issueRef ? { issueRef } : {}),
|
|
283
|
-
headSha: change.headSha,
|
|
284
|
-
status: 'noop',
|
|
285
|
-
message: 'nothing to integrate — produced head already pushed and equals PR head',
|
|
286
|
-
};
|
|
360
|
+
const prContext = { ownerRepo, branch, title: input.title, integratorAccount: input.githubAccount, issueRef, issueAction, change, execGh: gh };
|
|
361
|
+
if (existing) {
|
|
362
|
+
const reused = reuseExistingProducedChangePr(prContext, existing);
|
|
363
|
+
if (reused)
|
|
364
|
+
return reused;
|
|
287
365
|
}
|
|
288
366
|
if (!existing && countAhead(git, cwd, change.headSha, input.base) === 0) {
|
|
289
367
|
return {
|
|
@@ -294,16 +372,9 @@ async function integrateProducedChange(input, deps, change) {
|
|
|
294
372
|
}
|
|
295
373
|
git(['push', 'origin', `${change.headSha}:refs/heads/${branch}`], cwd);
|
|
296
374
|
if (existing) {
|
|
297
|
-
return
|
|
298
|
-
prUrl: existing.prUrl,
|
|
299
|
-
branch,
|
|
300
|
-
prNumber: existing.prNumber,
|
|
301
|
-
...(issueRef ? { issueRef } : {}),
|
|
302
|
-
headSha: change.headSha,
|
|
303
|
-
status: 'pushed',
|
|
304
|
-
};
|
|
375
|
+
return updateExistingProducedChangePr(prContext, existing);
|
|
305
376
|
}
|
|
306
|
-
const created = createPr(ownerRepo, branch, input.base, input.title, issueRef, gh);
|
|
377
|
+
const created = createPr(ownerRepo, branch, input.base, input.title, issueRef, issueAction, gh);
|
|
307
378
|
if ('needsHuman' in created)
|
|
308
379
|
return created;
|
|
309
380
|
return {
|
|
@@ -315,6 +386,33 @@ async function integrateProducedChange(input, deps, change) {
|
|
|
315
386
|
status: 'pushed',
|
|
316
387
|
};
|
|
317
388
|
}
|
|
389
|
+
function mergeabilityForConfirmMerge(pr, overrideAccepted) {
|
|
390
|
+
if (!overrideAccepted) {
|
|
391
|
+
return pr.mergeStateStatus === 'CLEAN' ? 'clean' : 'blocked';
|
|
392
|
+
}
|
|
393
|
+
const mergeable = pr.mergeable ?? (pr.mergeStateStatus === 'CLEAN' ? 'MERGEABLE' : undefined);
|
|
394
|
+
return mergeSignal(pr.mergeStateStatus, mergeable);
|
|
395
|
+
}
|
|
396
|
+
function readyFailureIsBenign(message) {
|
|
397
|
+
return /not a draft|already ready(?: for review)?/i.test(message);
|
|
398
|
+
}
|
|
399
|
+
function markPrReadyForReview(input) {
|
|
400
|
+
try {
|
|
401
|
+
input.gh(['pr', 'ready', input.branch, '--repo', input.ownerRepo]);
|
|
402
|
+
return undefined;
|
|
403
|
+
}
|
|
404
|
+
catch (err) {
|
|
405
|
+
const raw = err instanceof Error ? err.message : String(err);
|
|
406
|
+
if (readyFailureIsBenign(raw))
|
|
407
|
+
return undefined;
|
|
408
|
+
const account = input.githubAccount ?? 'resolved-host-account';
|
|
409
|
+
return {
|
|
410
|
+
needsHuman: true,
|
|
411
|
+
lesson: `failed to mark PR #${input.prNumber ?? 'unknown'} ready for review ` +
|
|
412
|
+
`(githubAccount=${account}, repo=${input.ownerRepo}, branch=${input.branch}): ${redactTokens(raw || 'gh pr ready failed')}`,
|
|
413
|
+
};
|
|
414
|
+
}
|
|
415
|
+
}
|
|
318
416
|
export async function confirmMerge(input, deps) {
|
|
319
417
|
const { execGit: git, execGh: gh, resolveRunCwd } = deps;
|
|
320
418
|
const cwd = await resolveRunCwd(input.runId, input.taskId);
|
|
@@ -323,8 +421,9 @@ export async function confirmMerge(input, deps) {
|
|
|
323
421
|
if ('needsHuman' in ownerRepoResult)
|
|
324
422
|
return ownerRepoResult;
|
|
325
423
|
const { ownerRepo } = ownerRepoResult;
|
|
424
|
+
const issueAction = resolvedIssueAction(input.issueRef, input.issueAction);
|
|
326
425
|
const view = () => {
|
|
327
|
-
const raw = gh(['pr', 'view', branch, '--repo', ownerRepo, '--json', 'number,url,state,isDraft,mergeStateStatus']);
|
|
426
|
+
const raw = gh(['pr', 'view', branch, '--repo', ownerRepo, '--json', 'number,url,state,isDraft,mergeStateStatus,mergeable,closingIssuesReferences']);
|
|
328
427
|
try {
|
|
329
428
|
return JSON.parse(raw);
|
|
330
429
|
}
|
|
@@ -338,7 +437,9 @@ export async function confirmMerge(input, deps) {
|
|
|
338
437
|
if (pr.state !== 'OPEN') {
|
|
339
438
|
return { needsHuman: true, lesson: `PR #${pr.number} is ${pr.state} (not OPEN) and not merged — resolve manually` };
|
|
340
439
|
}
|
|
341
|
-
|
|
440
|
+
const overrideAccepted = input.mergeReadiness?.override?.accepted === true;
|
|
441
|
+
const mergeability = mergeabilityForConfirmMerge(pr, overrideAccepted);
|
|
442
|
+
if (mergeability !== 'clean') {
|
|
342
443
|
return {
|
|
343
444
|
needsHuman: true,
|
|
344
445
|
lesson: `PR #${pr.number} is not auto-mergeable (mergeStateStatus=${pr.mergeStateStatus}) — CI not green, ` +
|
|
@@ -352,8 +453,22 @@ export async function confirmMerge(input, deps) {
|
|
|
352
453
|
lesson: `PR #${pr.number} merge requires a fresh merge readiness headSha guard — re-run readiness before approving merge`,
|
|
353
454
|
};
|
|
354
455
|
}
|
|
456
|
+
if (issueAction === 'close' && input.issueRef && !hasClosingIssueReference(pr.closingIssuesReferences, input.issueRef)) {
|
|
457
|
+
return {
|
|
458
|
+
needsHuman: true,
|
|
459
|
+
lesson: `PR #${pr.number} is expected to close ${issueRefTag(input.issueRef, ownerRepo)} but GitHub closingIssuesReferences does not include it`,
|
|
460
|
+
};
|
|
461
|
+
}
|
|
355
462
|
if (pr.isDraft) {
|
|
356
|
-
|
|
463
|
+
const readyFailure = markPrReadyForReview({
|
|
464
|
+
gh,
|
|
465
|
+
branch,
|
|
466
|
+
ownerRepo,
|
|
467
|
+
prNumber: pr.number,
|
|
468
|
+
...(input.githubAccount ? { githubAccount: input.githubAccount } : {}),
|
|
469
|
+
});
|
|
470
|
+
if (readyFailure)
|
|
471
|
+
return readyFailure;
|
|
357
472
|
}
|
|
358
473
|
try {
|
|
359
474
|
gh(['pr', 'merge', branch, '--repo', ownerRepo, '--squash', '--delete-branch', '--match-head-commit', expectedHeadSha]);
|
|
@@ -375,14 +490,22 @@ function envInt(name, fallback) {
|
|
|
375
490
|
const n = Number.parseInt(process.env[name] ?? '', 10);
|
|
376
491
|
return Number.isFinite(n) && n > 0 ? n : fallback;
|
|
377
492
|
}
|
|
378
|
-
function defaultCollect(repo, branch, base, execGh, issueRef) {
|
|
379
|
-
return collectPrReadiness({ repo, headBranch: branch, baseBranch: base, issueRef, includeReviewThreads: true, includeComments: false }, execGh).then((r) => ({
|
|
493
|
+
function defaultCollect(repo, branch, base, execGh, issueRef, issueAction) {
|
|
494
|
+
return collectPrReadiness({ repo, headBranch: branch, baseBranch: base, issueRef, issueAction, includeReviewThreads: true, includeComments: false }, execGh).then((r) => ({
|
|
380
495
|
pr: { number: r.pr.number, headSha: r.pr.headSha },
|
|
381
496
|
checks: { pending: r.checks.pending, fail: r.checks.fail, list: r.checks.list },
|
|
382
|
-
reviewThreads: {
|
|
497
|
+
reviewThreads: {
|
|
498
|
+
items: r.reviewThreads.items,
|
|
499
|
+
unresolvedCount: r.reviewThreads.unresolvedCount,
|
|
500
|
+
...(r.reviewThreads.truncated ? { truncated: true } : {}),
|
|
501
|
+
},
|
|
383
502
|
readinessVerdict: r.verdict,
|
|
384
503
|
nextAction: r.nextAction,
|
|
385
504
|
evidence: r.evidence,
|
|
505
|
+
mergeStateStatus: r.pr.mergeState,
|
|
506
|
+
mergeable: r.ciSummary.mergeable,
|
|
507
|
+
draft: r.pr.draft,
|
|
508
|
+
feedback: r.feedback,
|
|
386
509
|
}));
|
|
387
510
|
}
|
|
388
511
|
const defaultSleep = (ms) => new Promise((resolve) => setTimeout(resolve, ms));
|
|
@@ -428,8 +551,364 @@ function unsettledReadinessFeedback(input, readiness, reason) {
|
|
|
428
551
|
verdict: 'recheck',
|
|
429
552
|
ciFailures: ciFailuresFrom(readiness),
|
|
430
553
|
reviewThreads,
|
|
554
|
+
...(readiness.mergeStateStatus !== undefined ? { mergeStateStatus: readiness.mergeStateStatus } : {}),
|
|
555
|
+
...(readiness.mergeable !== undefined ? { mergeable: readiness.mergeable } : {}),
|
|
556
|
+
};
|
|
557
|
+
}
|
|
558
|
+
// UNSTABLE/HAS_HOOKS: GitHub marks a PR mergeable even when non-required checks are unsettled.
|
|
559
|
+
// Required-check gating is handled upstream; this quirk is intentional for advisory-only scenarios.
|
|
560
|
+
export function mergeSignal(mergeStateStatus, mergeable) {
|
|
561
|
+
const ms = (mergeStateStatus ?? '').toUpperCase();
|
|
562
|
+
const mg = (mergeable ?? '').toUpperCase();
|
|
563
|
+
if (mg === 'CONFLICTING' || ms === 'DIRTY' || ms === 'BLOCKED' || ms === 'BEHIND')
|
|
564
|
+
return 'blocked';
|
|
565
|
+
if (mg === 'MERGEABLE' && (ms === 'CLEAN' || ms === 'UNSTABLE' || ms === 'HAS_HOOKS'))
|
|
566
|
+
return 'clean';
|
|
567
|
+
return 'unknown';
|
|
568
|
+
}
|
|
569
|
+
function mergeOverrideGate(input) {
|
|
570
|
+
const resolution = asRecord(input.gateResolution);
|
|
571
|
+
const audit = asRecord(resolution?.['mergeOverrideAudit']);
|
|
572
|
+
const note = nonBlank(resolution?.['note']);
|
|
573
|
+
if (!resolution || !note || !audit) {
|
|
574
|
+
return {
|
|
575
|
+
needsHuman: true,
|
|
576
|
+
lesson: 'override_merge requires a non-empty note and mergeOverrideAudit from the merge gate',
|
|
577
|
+
};
|
|
578
|
+
}
|
|
579
|
+
const actor = nonBlank(audit.actor) ?? nonBlank(resolution['resolvedBy']) ?? 'operator';
|
|
580
|
+
const trustedHeadSha = nonBlank(resolution['trustedGateHeadSha']);
|
|
581
|
+
return {
|
|
582
|
+
note,
|
|
583
|
+
audit,
|
|
584
|
+
actor,
|
|
585
|
+
...(trustedHeadSha ? { trustedHeadSha } : {}),
|
|
586
|
+
source: { gate: 'mergeGate', inboxId: nonBlank(resolution['inboxId']) ?? '' },
|
|
587
|
+
};
|
|
588
|
+
}
|
|
589
|
+
function prReviewThreads(readiness) {
|
|
590
|
+
return readiness.reviewThreads.items.map((thread) => ({
|
|
591
|
+
threadId: thread.id,
|
|
592
|
+
path: thread.path,
|
|
593
|
+
line: thread.line,
|
|
594
|
+
author: thread.author,
|
|
595
|
+
body: thread.body,
|
|
596
|
+
}));
|
|
597
|
+
}
|
|
598
|
+
function checkFact(severity, kind, name, result) {
|
|
599
|
+
return { severity, kind, name, summary: `${name}: ${result}`, evidence: result };
|
|
600
|
+
}
|
|
601
|
+
function emptyFactBuckets() {
|
|
602
|
+
return { hard: [], advisory: [] };
|
|
603
|
+
}
|
|
604
|
+
function pushFact(buckets, fact) {
|
|
605
|
+
buckets[fact.severity].push(fact);
|
|
606
|
+
}
|
|
607
|
+
function reviewThreadFact(thread) {
|
|
608
|
+
return {
|
|
609
|
+
severity: 'advisory',
|
|
610
|
+
kind: 'review_thread',
|
|
611
|
+
threadId: thread.id,
|
|
612
|
+
summary: thread.body,
|
|
613
|
+
evidence: [thread.path, thread.line].filter((item) => item !== undefined).join(':') || thread.url || thread.id,
|
|
614
|
+
};
|
|
615
|
+
}
|
|
616
|
+
function feedbackEvidence(item) {
|
|
617
|
+
return item.evidence ?? item.location ?? item.author ?? item.summary;
|
|
618
|
+
}
|
|
619
|
+
function classifyFeedbackFacts(readiness) {
|
|
620
|
+
const facts = emptyFactBuckets();
|
|
621
|
+
const feedback = readiness.feedback;
|
|
622
|
+
if (!feedback)
|
|
623
|
+
return facts;
|
|
624
|
+
for (const fix of feedback.developerFixes) {
|
|
625
|
+
if (fix.source === 'ci' || fix.source === 'review_thread')
|
|
626
|
+
continue;
|
|
627
|
+
const fact = {
|
|
628
|
+
severity: 'advisory',
|
|
629
|
+
kind: fix.source,
|
|
630
|
+
summary: fix.summary,
|
|
631
|
+
evidence: feedbackEvidence(fix),
|
|
632
|
+
};
|
|
633
|
+
pushFact(facts, fix.source === 'human_review' ? { ...fact, severity: 'hard' } : fact);
|
|
634
|
+
}
|
|
635
|
+
for (const question of feedback.reviewerQuestions) {
|
|
636
|
+
pushFact(facts, {
|
|
637
|
+
severity: 'hard',
|
|
638
|
+
kind: 'reviewer_question',
|
|
639
|
+
summary: question.summary,
|
|
640
|
+
evidence: feedbackEvidence(question),
|
|
641
|
+
});
|
|
642
|
+
}
|
|
643
|
+
for (const decision of feedback.humanDecisions) {
|
|
644
|
+
pushFact(facts, {
|
|
645
|
+
severity: 'hard',
|
|
646
|
+
kind: decision.source,
|
|
647
|
+
summary: decision.summary,
|
|
648
|
+
});
|
|
649
|
+
}
|
|
650
|
+
for (const wait of feedback.providerWait) {
|
|
651
|
+
pushFact(facts, {
|
|
652
|
+
severity: 'advisory',
|
|
653
|
+
kind: `provider_wait:${wait.provider}`,
|
|
654
|
+
summary: wait.evidence,
|
|
655
|
+
evidence: wait.reason,
|
|
656
|
+
});
|
|
657
|
+
}
|
|
658
|
+
return facts;
|
|
659
|
+
}
|
|
660
|
+
function checkStatusFact(check, required, status) {
|
|
661
|
+
const severity = required ? 'hard' : 'advisory';
|
|
662
|
+
const kind = `${required ? 'required' : 'non_required'}_check_${status}`;
|
|
663
|
+
return checkFact(severity, kind, check.name, check.result);
|
|
664
|
+
}
|
|
665
|
+
function pushCheckStatusFact(facts, check, names, required, status) {
|
|
666
|
+
if (!names.includes(check.name))
|
|
667
|
+
return;
|
|
668
|
+
pushFact(facts, checkStatusFact(check, required, status));
|
|
669
|
+
}
|
|
670
|
+
function factsFromRequiredChecks(readiness, required) {
|
|
671
|
+
const facts = emptyFactBuckets();
|
|
672
|
+
for (const check of readiness.checks.list) {
|
|
673
|
+
const isRequired = required.has(check.name);
|
|
674
|
+
pushCheckStatusFact(facts, check, readiness.checks.fail, isRequired, 'failed');
|
|
675
|
+
pushCheckStatusFact(facts, check, readiness.checks.pending, isRequired, 'pending');
|
|
676
|
+
}
|
|
677
|
+
if (readiness.checks.list.length === 0) {
|
|
678
|
+
pushFact(facts, {
|
|
679
|
+
severity: 'advisory',
|
|
680
|
+
kind: 'checks_none_registered',
|
|
681
|
+
summary: 'No registered checks were reported for the PR.',
|
|
682
|
+
evidence: 'checks: none registered',
|
|
683
|
+
});
|
|
684
|
+
}
|
|
685
|
+
return facts;
|
|
686
|
+
}
|
|
687
|
+
function checkRollupUnavailableFact(readiness) {
|
|
688
|
+
const unavailable = readiness.checks.pending.includes(GITHUB_CHECK_ROLLUP_UNAVAILABLE)
|
|
689
|
+
|| readiness.evidence.some((item) => item.includes(GITHUB_CHECK_ROLLUP_UNAVAILABLE));
|
|
690
|
+
if (!unavailable)
|
|
691
|
+
return undefined;
|
|
692
|
+
return {
|
|
693
|
+
severity: 'hard',
|
|
694
|
+
kind: 'check_rollup_unavailable',
|
|
695
|
+
summary: 'GitHub check rollup is unavailable; override cannot distinguish pending checks from no registered checks.',
|
|
696
|
+
evidence: GITHUB_CHECK_ROLLUP_UNAVAILABLE,
|
|
697
|
+
};
|
|
698
|
+
}
|
|
699
|
+
function reviewThreadsIncompleteFact(readiness) {
|
|
700
|
+
if (readiness.reviewThreads.truncated !== true)
|
|
701
|
+
return undefined;
|
|
702
|
+
return {
|
|
703
|
+
severity: 'hard',
|
|
704
|
+
kind: 'review_threads_incomplete',
|
|
705
|
+
summary: 'Review thread data is incomplete; override cannot safely resolve every unresolved thread.',
|
|
706
|
+
evidence: `unresolvedCount=${readiness.reviewThreads.unresolvedCount ?? readiness.reviewThreads.items.length}`,
|
|
707
|
+
};
|
|
708
|
+
}
|
|
709
|
+
function mergeOverrideOutput(input) {
|
|
710
|
+
const readiness = input.readiness;
|
|
711
|
+
const evidence = [
|
|
712
|
+
...(readiness?.evidence ?? []),
|
|
713
|
+
...(readiness ? readinessEvidence(readiness) : []),
|
|
714
|
+
...input.facts.map((fact) => `${fact.severity} ${fact.kind}: ${fact.summary}`),
|
|
715
|
+
input.reason,
|
|
716
|
+
`PR headSha=${readiness?.pr.headSha ?? input.fallbackHeadSha ?? input.gate.audit.headSha}`,
|
|
717
|
+
`overrideMerge verdict=${input.verdict}`,
|
|
718
|
+
].filter((item) => typeof item === 'string' && item.length > 0);
|
|
719
|
+
return {
|
|
720
|
+
prNumber: readiness?.pr.number ?? null,
|
|
721
|
+
headSha: readiness?.pr.headSha ?? input.fallbackHeadSha ?? input.gate.audit.headSha,
|
|
722
|
+
evidence,
|
|
723
|
+
verdict: input.verdict,
|
|
724
|
+
ciFailures: readiness ? ciFailuresFrom(readiness) : [],
|
|
725
|
+
reviewThreads: readiness ? prReviewThreads(readiness) : [],
|
|
726
|
+
...(readiness?.mergeStateStatus !== undefined ? { mergeStateStatus: readiness.mergeStateStatus } : {}),
|
|
727
|
+
...(readiness?.mergeable !== undefined ? { mergeable: readiness.mergeable } : {}),
|
|
728
|
+
override: {
|
|
729
|
+
accepted: input.accepted,
|
|
730
|
+
actor: input.gate.actor,
|
|
731
|
+
note: input.gate.note,
|
|
732
|
+
audit: {
|
|
733
|
+
reason: input.gate.audit.reason,
|
|
734
|
+
risk: input.gate.audit.risk,
|
|
735
|
+
verificationResponsibility: input.gate.audit.verificationResponsibility,
|
|
736
|
+
headSha: input.gate.audit.headSha,
|
|
737
|
+
},
|
|
738
|
+
source: input.gate.source,
|
|
739
|
+
facts: input.facts,
|
|
740
|
+
replied: input.response?.replied ?? 0,
|
|
741
|
+
resolved: input.response?.resolved ?? 0,
|
|
742
|
+
...(input.reason ? { reason: input.reason } : {}),
|
|
743
|
+
},
|
|
744
|
+
};
|
|
745
|
+
}
|
|
746
|
+
function hardMergeabilityFact(readiness) {
|
|
747
|
+
const signal = mergeSignal(readiness.mergeStateStatus, readiness.mergeable);
|
|
748
|
+
if (signal === 'clean')
|
|
749
|
+
return undefined;
|
|
750
|
+
return {
|
|
751
|
+
severity: 'hard',
|
|
752
|
+
kind: signal === 'blocked' ? 'mergeability_blocked' : 'mergeability_unknown',
|
|
753
|
+
summary: `mergeStateStatus=${readiness.mergeStateStatus ?? ''} mergeable=${readiness.mergeable ?? ''}`,
|
|
754
|
+
evidence: `mergeStateStatus=${readiness.mergeStateStatus ?? ''}; mergeable=${readiness.mergeable ?? ''}`,
|
|
431
755
|
};
|
|
432
756
|
}
|
|
757
|
+
function movedHeadFact(readiness, gate) {
|
|
758
|
+
const approvedHeadSha = gate.trustedHeadSha;
|
|
759
|
+
if (!approvedHeadSha) {
|
|
760
|
+
return {
|
|
761
|
+
severity: 'hard',
|
|
762
|
+
kind: 'trusted_gate_head_unavailable',
|
|
763
|
+
summary: 'Trusted merge gate head is unavailable.',
|
|
764
|
+
evidence: `audit=${gate.audit.headSha}; fresh=${readiness.pr.headSha}`,
|
|
765
|
+
};
|
|
766
|
+
}
|
|
767
|
+
if (readiness.pr.headSha === approvedHeadSha)
|
|
768
|
+
return undefined;
|
|
769
|
+
return {
|
|
770
|
+
severity: 'hard',
|
|
771
|
+
kind: 'head_moved',
|
|
772
|
+
summary: `approved head ${approvedHeadSha} no longer matches fresh head ${readiness.pr.headSha}`,
|
|
773
|
+
evidence: `approved=${approvedHeadSha}; audit=${gate.audit.headSha}; fresh=${readiness.pr.headSha}`,
|
|
774
|
+
};
|
|
775
|
+
}
|
|
776
|
+
function auditHeadMismatchFact(gate) {
|
|
777
|
+
if (!gate.trustedHeadSha)
|
|
778
|
+
return undefined;
|
|
779
|
+
if (gate.audit.headSha === gate.trustedHeadSha)
|
|
780
|
+
return undefined;
|
|
781
|
+
return {
|
|
782
|
+
severity: 'hard',
|
|
783
|
+
kind: 'audit_head_mismatch',
|
|
784
|
+
summary: `audit head ${gate.audit.headSha} does not match trusted merge gate head ${gate.trustedHeadSha}`,
|
|
785
|
+
evidence: `audit=${gate.audit.headSha}; approved=${gate.trustedHeadSha}`,
|
|
786
|
+
};
|
|
787
|
+
}
|
|
788
|
+
function initialMergeOverrideHardFacts(readiness, gate, unavailableRollup) {
|
|
789
|
+
return [
|
|
790
|
+
readiness.draft === true ? { severity: 'hard', kind: 'draft_pr', summary: 'PR is still draft.' } : undefined,
|
|
791
|
+
auditHeadMismatchFact(gate),
|
|
792
|
+
movedHeadFact(readiness, gate),
|
|
793
|
+
hardMergeabilityFact(readiness),
|
|
794
|
+
unavailableRollup,
|
|
795
|
+
reviewThreadsIncompleteFact(readiness),
|
|
796
|
+
].filter((fact) => fact !== undefined);
|
|
797
|
+
}
|
|
798
|
+
function appendFeedbackFacts(buckets, readiness) {
|
|
799
|
+
for (const thread of readiness.reviewThreads.items) {
|
|
800
|
+
buckets.advisory.push(reviewThreadFact(thread));
|
|
801
|
+
}
|
|
802
|
+
const feedbackFacts = classifyFeedbackFacts(readiness);
|
|
803
|
+
buckets.hard.push(...feedbackFacts.hard);
|
|
804
|
+
buckets.advisory.push(...feedbackFacts.advisory);
|
|
805
|
+
}
|
|
806
|
+
function classifyAvailableCheckFacts(readiness, ownerRepo, execGh, requiredChecks) {
|
|
807
|
+
if (readiness.checks.fail.length === 0 && readiness.checks.pending.length === 0) {
|
|
808
|
+
return readiness.checks.list.length === 0 ? factsFromRequiredChecks(readiness, new Set()) : emptyFactBuckets();
|
|
809
|
+
}
|
|
810
|
+
if (readiness.pr.number === null) {
|
|
811
|
+
return {
|
|
812
|
+
hard: [{ severity: 'hard', kind: 'pr_number_unavailable', summary: 'Cannot classify required checks without a PR number.' }],
|
|
813
|
+
advisory: [],
|
|
814
|
+
};
|
|
815
|
+
}
|
|
816
|
+
try {
|
|
817
|
+
return factsFromRequiredChecks(readiness, requiredChecks(ownerRepo, readiness.pr.number, execGh));
|
|
818
|
+
}
|
|
819
|
+
catch (err) {
|
|
820
|
+
return {
|
|
821
|
+
hard: [{
|
|
822
|
+
severity: 'hard',
|
|
823
|
+
kind: 'required_check_names_unavailable',
|
|
824
|
+
summary: 'Required check names are unavailable.',
|
|
825
|
+
evidence: err instanceof Error ? err.message : String(err),
|
|
826
|
+
}],
|
|
827
|
+
advisory: [],
|
|
828
|
+
};
|
|
829
|
+
}
|
|
830
|
+
}
|
|
831
|
+
async function classifyMergeOverrideFacts(readiness, gate, ownerRepo, execGh, requiredChecks) {
|
|
832
|
+
const unavailableRollup = checkRollupUnavailableFact(readiness);
|
|
833
|
+
const facts = {
|
|
834
|
+
hard: initialMergeOverrideHardFacts(readiness, gate, unavailableRollup),
|
|
835
|
+
advisory: [],
|
|
836
|
+
};
|
|
837
|
+
if (!unavailableRollup) {
|
|
838
|
+
const checkFacts = classifyAvailableCheckFacts(readiness, ownerRepo, execGh, requiredChecks);
|
|
839
|
+
facts.hard.push(...checkFacts.hard);
|
|
840
|
+
facts.advisory.push(...checkFacts.advisory);
|
|
841
|
+
}
|
|
842
|
+
appendFeedbackFacts(facts, readiness);
|
|
843
|
+
return facts;
|
|
844
|
+
}
|
|
845
|
+
function overrideThreadTriage(readiness, note) {
|
|
846
|
+
return {
|
|
847
|
+
items: readiness.reviewThreads.items.map((thread) => ({
|
|
848
|
+
threadId: thread.id,
|
|
849
|
+
decision: 'wontfix',
|
|
850
|
+
replyText: `merged by operator override: ${note}`,
|
|
851
|
+
})),
|
|
852
|
+
};
|
|
853
|
+
}
|
|
854
|
+
export async function overrideMerge(input, deps) {
|
|
855
|
+
const gate = mergeOverrideGate(input);
|
|
856
|
+
if ('needsHuman' in gate)
|
|
857
|
+
return gate;
|
|
858
|
+
const { execGit: git, execGh: gh, resolveRunCwd } = deps;
|
|
859
|
+
const cwd = await resolveRunCwd(input.runId, input.taskId);
|
|
860
|
+
const branch = branchName(input.taskId, input.title, input.issueRef);
|
|
861
|
+
const ownerRepoResult = resolveOwnerRepo(git, cwd);
|
|
862
|
+
if ('needsHuman' in ownerRepoResult)
|
|
863
|
+
return ownerRepoResult;
|
|
864
|
+
const { ownerRepo } = ownerRepoResult;
|
|
865
|
+
let readiness;
|
|
866
|
+
try {
|
|
867
|
+
readiness = await (deps.collect ?? defaultCollect)(ownerRepo, branch, input.base, gh, input.issueRef, input.issueAction);
|
|
868
|
+
}
|
|
869
|
+
catch (err) {
|
|
870
|
+
const fact = {
|
|
871
|
+
severity: 'hard',
|
|
872
|
+
kind: 'readiness_transport_degraded',
|
|
873
|
+
summary: 'Fresh override reverify could not read PR readiness.',
|
|
874
|
+
evidence: err instanceof Error ? err.message : String(err),
|
|
875
|
+
};
|
|
876
|
+
return mergeOverrideOutput({
|
|
877
|
+
gate,
|
|
878
|
+
verdict: 'recheck',
|
|
879
|
+
facts: [fact],
|
|
880
|
+
accepted: false,
|
|
881
|
+
reason: fact.summary,
|
|
882
|
+
});
|
|
883
|
+
}
|
|
884
|
+
if (readiness.readinessVerdict === 'merged') {
|
|
885
|
+
return mergeOverrideOutput({ gate, readiness, verdict: 'merged', facts: [], accepted: false, reason: 'PR already merged externally.' });
|
|
886
|
+
}
|
|
887
|
+
if (readiness.readinessVerdict === 'closed') {
|
|
888
|
+
const fact = { severity: 'hard', kind: 'pr_closed_externally', summary: 'PR is closed without being merged.' };
|
|
889
|
+
return mergeOverrideOutput({ gate, readiness, verdict: 'closed', facts: [fact], accepted: false, reason: fact.summary });
|
|
890
|
+
}
|
|
891
|
+
const facts = await classifyMergeOverrideFacts(readiness, gate, ownerRepo, gh, deps.requiredChecks ?? fetchRequiredCheckNames);
|
|
892
|
+
if (facts.hard.length > 0) {
|
|
893
|
+
return mergeOverrideOutput({
|
|
894
|
+
gate,
|
|
895
|
+
readiness,
|
|
896
|
+
verdict: 'recheck',
|
|
897
|
+
facts: facts.hard,
|
|
898
|
+
accepted: false,
|
|
899
|
+
reason: 'override_merge refused because hard blockers remain',
|
|
900
|
+
});
|
|
901
|
+
}
|
|
902
|
+
const response = await respondThreads(overrideThreadTriage(readiness, gate.note), { execGh: gh });
|
|
903
|
+
return mergeOverrideOutput({
|
|
904
|
+
gate,
|
|
905
|
+
readiness,
|
|
906
|
+
verdict: 'clean',
|
|
907
|
+
facts: facts.advisory,
|
|
908
|
+
accepted: true,
|
|
909
|
+
response,
|
|
910
|
+
});
|
|
911
|
+
}
|
|
433
912
|
export async function pollPr(input, deps) {
|
|
434
913
|
const { execGit: git, execGh: gh, resolveRunCwd } = deps;
|
|
435
914
|
const collect = deps.collect ?? defaultCollect;
|
|
@@ -445,10 +924,27 @@ export async function pollPr(input, deps) {
|
|
|
445
924
|
const { ownerRepo } = ownerRepoResult;
|
|
446
925
|
let readiness;
|
|
447
926
|
let lastReadiness;
|
|
927
|
+
let markedReadyForReview = false;
|
|
448
928
|
for (let i = 0; i < maxPolls; i++) {
|
|
449
|
-
readiness = await collect(ownerRepo, branch, input.base, gh, input.issueRef);
|
|
929
|
+
readiness = await collect(ownerRepo, branch, input.base, gh, input.issueRef, input.issueAction);
|
|
930
|
+
if (readiness.draft === true
|
|
931
|
+
&& readiness.readinessVerdict !== 'merged'
|
|
932
|
+
&& readiness.readinessVerdict !== 'closed'
|
|
933
|
+
&& ciFailuresFrom(readiness).length === 0) {
|
|
934
|
+
const readyFailure = markPrReadyForReview({
|
|
935
|
+
gh,
|
|
936
|
+
branch,
|
|
937
|
+
ownerRepo,
|
|
938
|
+
prNumber: readiness.pr.number,
|
|
939
|
+
...(input.githubAccount ? { githubAccount: input.githubAccount } : {}),
|
|
940
|
+
});
|
|
941
|
+
if (readyFailure)
|
|
942
|
+
return readyFailure;
|
|
943
|
+
markedReadyForReview = true;
|
|
944
|
+
readiness = await collect(ownerRepo, branch, input.base, gh, input.issueRef, input.issueAction);
|
|
945
|
+
}
|
|
450
946
|
lastReadiness = readiness;
|
|
451
|
-
if (readiness.checks.pending.length === 0
|
|
947
|
+
if (readiness.checks.pending.length === 0)
|
|
452
948
|
break;
|
|
453
949
|
readiness = undefined;
|
|
454
950
|
if (i < maxPolls - 1)
|
|
@@ -468,24 +964,80 @@ export async function pollPr(input, deps) {
|
|
|
468
964
|
return { needsHuman: true, lesson: `pollPr timed out after ${maxPolls} polls before reading PR readiness for ${branch}` };
|
|
469
965
|
}
|
|
470
966
|
let settled = readiness;
|
|
967
|
+
const makeTerminalFeedback = (verdict) => ({
|
|
968
|
+
prNumber: settled.pr.number ?? null,
|
|
969
|
+
headSha: settled.pr.headSha,
|
|
970
|
+
evidence: [...settled.evidence, ...readinessEvidence(settled), `PR headSha=${settled.pr.headSha}`, `pollPr verdict=${verdict}`],
|
|
971
|
+
...(input.issueRef ? { issueRef: input.issueRef } : {}),
|
|
972
|
+
verdict,
|
|
973
|
+
ciFailures: ciFailuresFrom(settled),
|
|
974
|
+
reviewThreads: [],
|
|
975
|
+
...(settled.mergeStateStatus !== undefined ? { mergeStateStatus: settled.mergeStateStatus } : {}),
|
|
976
|
+
...(settled.mergeable !== undefined ? { mergeable: settled.mergeable } : {}),
|
|
977
|
+
});
|
|
978
|
+
if (settled.readinessVerdict === 'merged')
|
|
979
|
+
return makeTerminalFeedback('merged');
|
|
980
|
+
if (settled.readinessVerdict === 'closed')
|
|
981
|
+
return makeTerminalFeedback('closed');
|
|
982
|
+
const standardCheckResults = new Set('ACTION_REQUIRED CANCELLED ERROR EXPECTED FAILURE IN_PROGRESS NEUTRAL PENDING QUEUED SKIPPED STALE STARTUP_FAILURE SUCCESS TIMED_OUT UNKNOWN'.split(' '));
|
|
983
|
+
const standardMergeStateStatuses = new Set('BEHIND BLOCKED CLEAN DIRTY DRAFT HAS_HOOKS UNKNOWN UNSTABLE'.split(' '));
|
|
984
|
+
const standardMergeableStates = new Set('CONFLICTING MERGEABLE UNKNOWN'.split(' '));
|
|
985
|
+
const addUnclassifiableState = (states, label, value, known) => {
|
|
986
|
+
const state = (value ?? '').trim().toUpperCase();
|
|
987
|
+
if (state !== '' && !known.has(state))
|
|
988
|
+
states.push(`${label} ${state}`);
|
|
989
|
+
};
|
|
990
|
+
const unclassifiablePollState = (snapshot) => {
|
|
991
|
+
const states = [];
|
|
992
|
+
for (const check of snapshot.checks.list) {
|
|
993
|
+
addUnclassifiableState(states, 'check result', check.result, standardCheckResults);
|
|
994
|
+
}
|
|
995
|
+
addUnclassifiableState(states, 'mergeStateStatus', snapshot.mergeStateStatus, standardMergeStateStatuses);
|
|
996
|
+
addUnclassifiableState(states, 'mergeable', snapshot.mergeable, standardMergeableStates);
|
|
997
|
+
return states;
|
|
998
|
+
};
|
|
999
|
+
const unclassifiableReadinessBlock = (snapshot, states) => ({
|
|
1000
|
+
needsHuman: true,
|
|
1001
|
+
lesson: [
|
|
1002
|
+
...snapshot.evidence,
|
|
1003
|
+
...readinessEvidence(snapshot),
|
|
1004
|
+
`pollPr unclassifiable readiness state: ${states.join(', ')}`,
|
|
1005
|
+
`PR headSha=${snapshot.pr.headSha}`,
|
|
1006
|
+
].join('; '),
|
|
1007
|
+
});
|
|
1008
|
+
const unclassifiable = unclassifiablePollState(settled);
|
|
1009
|
+
if (unclassifiable.length > 0) {
|
|
1010
|
+
return unclassifiableReadinessBlock(settled, unclassifiable);
|
|
1011
|
+
}
|
|
471
1012
|
const initialCiFailures = ciFailuresFrom(settled);
|
|
472
1013
|
if (initialCiFailures.length === 0) {
|
|
473
|
-
|
|
474
|
-
|
|
475
|
-
|
|
476
|
-
|
|
1014
|
+
if (!markedReadyForReview) {
|
|
1015
|
+
const readyFailure = markPrReadyForReview({
|
|
1016
|
+
gh,
|
|
1017
|
+
branch,
|
|
1018
|
+
ownerRepo,
|
|
1019
|
+
prNumber: settled.pr.number,
|
|
1020
|
+
...(input.githubAccount ? { githubAccount: input.githubAccount } : {}),
|
|
1021
|
+
});
|
|
1022
|
+
if (readyFailure)
|
|
1023
|
+
return readyFailure;
|
|
477
1024
|
}
|
|
478
1025
|
const reviewGracePolls = deps.reviewGracePolls ?? envInt('REVO_POLL_PR_REVIEW_GRACE_POLLS', 4);
|
|
479
1026
|
for (let i = 0; i < reviewGracePolls && settled.reviewThreads.items.length === 0; i++) {
|
|
480
1027
|
await sleep(intervalMs);
|
|
481
|
-
settled = await collect(ownerRepo, branch, input.base, gh, input.issueRef);
|
|
1028
|
+
settled = await collect(ownerRepo, branch, input.base, gh, input.issueRef, input.issueAction);
|
|
482
1029
|
}
|
|
483
1030
|
}
|
|
484
|
-
if (settled.
|
|
485
|
-
|
|
486
|
-
|
|
487
|
-
|
|
488
|
-
|
|
1031
|
+
if (settled.readinessVerdict === 'merged')
|
|
1032
|
+
return makeTerminalFeedback('merged');
|
|
1033
|
+
if (settled.readinessVerdict === 'closed')
|
|
1034
|
+
return makeTerminalFeedback('closed');
|
|
1035
|
+
const finalUnclassifiable = unclassifiablePollState(settled);
|
|
1036
|
+
if (finalUnclassifiable.length > 0) {
|
|
1037
|
+
return unclassifiableReadinessBlock(settled, finalUnclassifiable);
|
|
1038
|
+
}
|
|
1039
|
+
if (settled.checks.pending.length > 0) {
|
|
1040
|
+
return unsettledReadinessFeedback(input, settled, `pollPr found unsettled readiness after readying ${branch}: pending checks: ${settled.checks.pending.join(', ')}`);
|
|
489
1041
|
}
|
|
490
1042
|
const reviewThreads = settled.reviewThreads.items.map((t) => ({
|
|
491
1043
|
threadId: t.id,
|
|
@@ -496,27 +1048,51 @@ export async function pollPr(input, deps) {
|
|
|
496
1048
|
}));
|
|
497
1049
|
const ciFailures = ciFailuresFrom(settled);
|
|
498
1050
|
let ciVerdictFailures = ciFailures;
|
|
1051
|
+
let requiredCheckFetchFailure;
|
|
499
1052
|
if (ciFailures.length > 0 && settled.pr.number !== null) {
|
|
500
1053
|
try {
|
|
501
1054
|
const required = requiredChecks(ownerRepo, settled.pr.number, gh);
|
|
502
1055
|
if (required.size > 0)
|
|
503
1056
|
ciVerdictFailures = ciFailures.filter((f) => required.has(f.name));
|
|
504
1057
|
}
|
|
505
|
-
catch {
|
|
1058
|
+
catch (err) {
|
|
1059
|
+
requiredCheckFetchFailure = err instanceof Error ? err.message : String(err);
|
|
506
1060
|
}
|
|
507
1061
|
}
|
|
508
|
-
const
|
|
509
|
-
? 'review_changes'
|
|
510
|
-
: ciVerdictFailures.length > 0 ? 'ci_changes' : 'clean';
|
|
511
|
-
return {
|
|
1062
|
+
const makeFeedback = (verdict, extraEvidence = []) => ({
|
|
512
1063
|
prNumber: settled.pr.number ?? null,
|
|
513
1064
|
headSha: settled.pr.headSha,
|
|
514
|
-
evidence: [...settled.evidence, ...readinessEvidence(settled), `PR headSha=${settled.pr.headSha}`, `pollPr verdict=${verdict}`],
|
|
1065
|
+
evidence: [...settled.evidence, ...readinessEvidence(settled), ...extraEvidence, `PR headSha=${settled.pr.headSha}`, `pollPr verdict=${verdict}`],
|
|
515
1066
|
...(input.issueRef ? { issueRef: input.issueRef } : {}),
|
|
516
1067
|
verdict,
|
|
517
1068
|
ciFailures,
|
|
518
1069
|
reviewThreads,
|
|
519
|
-
|
|
1070
|
+
...(settled.mergeStateStatus !== undefined ? { mergeStateStatus: settled.mergeStateStatus } : {}),
|
|
1071
|
+
...(settled.mergeable !== undefined ? { mergeable: settled.mergeable } : {}),
|
|
1072
|
+
});
|
|
1073
|
+
if (reviewThreads.length > 0 || readinessRequiresReview(settled))
|
|
1074
|
+
return makeFeedback('review_changes');
|
|
1075
|
+
if (requiredCheckFetchFailure) {
|
|
1076
|
+
return makeFeedback('recheck', [`Required check names unavailable: ${requiredCheckFetchFailure}`]);
|
|
1077
|
+
}
|
|
1078
|
+
if (ciVerdictFailures.length > 0)
|
|
1079
|
+
return makeFeedback('ci_changes');
|
|
1080
|
+
const signal = mergeSignal(settled.mergeStateStatus, settled.mergeable);
|
|
1081
|
+
if (signal === 'blocked') {
|
|
1082
|
+
return {
|
|
1083
|
+
needsHuman: true,
|
|
1084
|
+
lesson: [
|
|
1085
|
+
...settled.evidence,
|
|
1086
|
+
...readinessEvidence(settled),
|
|
1087
|
+
`pollPr merge readiness blocked (mergeStateStatus=${settled.mergeStateStatus ?? ''} mergeable=${settled.mergeable ?? ''})`,
|
|
1088
|
+
`PR headSha=${settled.pr.headSha}`,
|
|
1089
|
+
].join('; '),
|
|
1090
|
+
};
|
|
1091
|
+
}
|
|
1092
|
+
if (signal === 'unknown') {
|
|
1093
|
+
return unsettledReadinessFeedback(input, settled, `pollPr merge readiness unknown (mergeable=${settled.mergeable ?? 'undefined'})`);
|
|
1094
|
+
}
|
|
1095
|
+
return makeFeedback('clean');
|
|
520
1096
|
}
|
|
521
1097
|
const TRIAGE_DECISIONS = new Set(['fix', 'wontfix', 'question']);
|
|
522
1098
|
export function asTriage(value) {
|
|
@@ -531,6 +1107,37 @@ export function asTriage(value) {
|
|
|
531
1107
|
: [];
|
|
532
1108
|
return { items };
|
|
533
1109
|
}
|
|
1110
|
+
function questionGateResolution(value) {
|
|
1111
|
+
if (value === null || typeof value !== 'object')
|
|
1112
|
+
return undefined;
|
|
1113
|
+
const outcome = value.outcome;
|
|
1114
|
+
if (outcome !== 'fix' && outcome !== 'wontfix')
|
|
1115
|
+
return undefined;
|
|
1116
|
+
const rawNote = value.note;
|
|
1117
|
+
const note = typeof rawNote === 'string' ? rawNote.trim() : '';
|
|
1118
|
+
return { decision: outcome, ...(note ? { note } : {}) };
|
|
1119
|
+
}
|
|
1120
|
+
function replyTextForQuestionResolution(decision, note) {
|
|
1121
|
+
if (decision === 'fix')
|
|
1122
|
+
return note ? `Addressed: ${note}` : 'Addressed.';
|
|
1123
|
+
return note ? `Won't fix: ${note}` : "Won't fix.";
|
|
1124
|
+
}
|
|
1125
|
+
export function triageForRespondThreads(input) {
|
|
1126
|
+
const triage = asTriage(input.triage);
|
|
1127
|
+
const resolution = questionGateResolution(input.gateResolution);
|
|
1128
|
+
if (!resolution)
|
|
1129
|
+
return triage;
|
|
1130
|
+
return {
|
|
1131
|
+
...triage,
|
|
1132
|
+
items: triage.items.map((item) => item.decision === 'question'
|
|
1133
|
+
? {
|
|
1134
|
+
...item,
|
|
1135
|
+
decision: resolution.decision,
|
|
1136
|
+
replyText: replyTextForQuestionResolution(resolution.decision, resolution.note),
|
|
1137
|
+
}
|
|
1138
|
+
: item),
|
|
1139
|
+
};
|
|
1140
|
+
}
|
|
534
1141
|
export async function respondThreads(triage, deps) {
|
|
535
1142
|
const { execGh: gh } = deps;
|
|
536
1143
|
let replied = 0;
|
|
@@ -570,27 +1177,21 @@ let IntegratorService = class IntegratorService {
|
|
|
570
1177
|
};
|
|
571
1178
|
}
|
|
572
1179
|
runIntegrate = (input) => {
|
|
573
|
-
const pinned = resolvePinnedGh();
|
|
1180
|
+
const pinned = resolvePinnedGh({ account: input.githubAccount });
|
|
574
1181
|
if ('needsHuman' in pinned) {
|
|
575
1182
|
console.warn(`[integrator] ${pinned.lesson}`);
|
|
576
1183
|
return Promise.resolve(pinned);
|
|
577
1184
|
}
|
|
578
|
-
console.log(`[integrator] gh pinned to account '${
|
|
579
|
-
return integrate(input, { ...this.deps, execGh: pinned.execGh });
|
|
580
|
-
};
|
|
581
|
-
runStub = (input) => {
|
|
582
|
-
return stubIntegrate(input);
|
|
1185
|
+
console.log(`[integrator] gh pinned to account '${pinned.account}' (GH_TOKEN, not ambient)`);
|
|
1186
|
+
return integrate({ ...input, githubAccount: pinned.account }, { ...this.deps, execGh: pinned.execGh });
|
|
583
1187
|
};
|
|
584
1188
|
runConfirmMerge = (input) => {
|
|
585
|
-
const pinned = resolvePinnedGh();
|
|
1189
|
+
const pinned = resolvePinnedGh({ account: input.githubAccount });
|
|
586
1190
|
if ('needsHuman' in pinned) {
|
|
587
1191
|
console.warn(`[confirm-merge] ${pinned.lesson}`);
|
|
588
1192
|
return Promise.resolve(pinned);
|
|
589
1193
|
}
|
|
590
|
-
return confirmMerge(input, { ...this.deps, execGh: pinned.execGh });
|
|
591
|
-
};
|
|
592
|
-
runConfirmStub = (input) => {
|
|
593
|
-
return { merged: true, prNumber: 0, prUrl: `stub://pr/${input.taskId}/merged` };
|
|
1194
|
+
return confirmMerge({ ...input, githubAccount: pinned.account }, { ...this.deps, execGh: pinned.execGh });
|
|
594
1195
|
};
|
|
595
1196
|
runPreflight = (taskId, base) => {
|
|
596
1197
|
return preflightLive(taskId, base, this.deps);
|
|
@@ -599,26 +1200,28 @@ let IntegratorService = class IntegratorService {
|
|
|
599
1200
|
return captureProducedChange(input, this.deps);
|
|
600
1201
|
};
|
|
601
1202
|
runPollPr = (input) => {
|
|
602
|
-
const pinned = resolvePinnedGh();
|
|
1203
|
+
const pinned = resolvePinnedGh({ account: input.githubAccount });
|
|
603
1204
|
if ('needsHuman' in pinned) {
|
|
604
1205
|
console.warn(`[poll-pr] ${pinned.lesson}`);
|
|
605
1206
|
return Promise.resolve(pinned);
|
|
606
1207
|
}
|
|
607
|
-
return pollPr(input, { ...this.deps, execGh: pinned.execGh });
|
|
1208
|
+
return pollPr({ ...input, githubAccount: pinned.account }, { ...this.deps, execGh: pinned.execGh });
|
|
608
1209
|
};
|
|
609
|
-
|
|
610
|
-
|
|
1210
|
+
runOverrideMerge = (input) => {
|
|
1211
|
+
const pinned = resolvePinnedGh({ account: input.githubAccount });
|
|
1212
|
+
if ('needsHuman' in pinned) {
|
|
1213
|
+
console.warn(`[override-merge] ${pinned.lesson}`);
|
|
1214
|
+
return Promise.resolve(pinned);
|
|
1215
|
+
}
|
|
1216
|
+
return overrideMerge({ ...input, githubAccount: pinned.account }, { ...this.deps, execGh: pinned.execGh });
|
|
611
1217
|
};
|
|
612
1218
|
runRespondThreads = (input) => {
|
|
613
|
-
const pinned = resolvePinnedGh();
|
|
1219
|
+
const pinned = resolvePinnedGh({ account: input.githubAccount });
|
|
614
1220
|
if ('needsHuman' in pinned) {
|
|
615
1221
|
console.warn(`[respond-threads] ${pinned.lesson}`);
|
|
616
1222
|
return Promise.resolve(pinned);
|
|
617
1223
|
}
|
|
618
|
-
return respondThreads(
|
|
619
|
-
};
|
|
620
|
-
runRespondStub = (_input) => {
|
|
621
|
-
return { replied: 0, resolved: 0 };
|
|
1224
|
+
return respondThreads(triageForRespondThreads(input), { execGh: pinned.execGh });
|
|
622
1225
|
};
|
|
623
1226
|
};
|
|
624
1227
|
IntegratorService = __decorate([
|