@revisium/orchestrator 0.2.0-alpha.2 → 0.2.0-alpha.4
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 +3 -3
- package/control-plane/bootstrap.config.json +104 -550
- package/control-plane/default-playbook/catalog/pipelines.json +405 -1065
- package/control-plane/default-playbook/catalog/run-profiles.json +208 -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/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 +451 -0
- package/dist/__generated__/client/internal/prismaNamespace.js.map +1 -0
- package/dist/__generated__/client/internal/prismaNamespaceBrowser.js +422 -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 +46 -0
- package/dist/api/graphql-api/inbox/inputs/resolve-gate.input.js.map +1 -1
- 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 +2 -0
- package/dist/api/graphql-api/registerGraphqlEnums.js.map +1 -1
- package/dist/api/graphql-api/runs/inputs/create-run.input.js +17 -0
- package/dist/api/graphql-api/runs/inputs/create-run.input.js.map +1 -1
- package/dist/api/graphql-api/runs/inputs/execution-profile.input.js +85 -0
- package/dist/api/graphql-api/runs/inputs/execution-profile.input.js.map +1 -0
- package/dist/api/graphql-api/runs/inputs/simulate-route.input.js +11 -0
- 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/audit-validation-helpers.js +31 -0
- package/dist/control-plane/audit-validation-helpers.js.map +1 -0
- 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 +499 -78
- 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 +381 -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/manual-adoption-audit.js +3 -24
- package/dist/control-plane/manual-adoption-audit.js.map +1 -1
- package/dist/control-plane/merge-override-audit.js +44 -0
- package/dist/control-plane/merge-override-audit.js.map +1 -0
- package/dist/control-plane/pipeline-coverage-registry.js +478 -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 +89 -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 +56 -14
- package/dist/control-plane/versioned-meaning.js.map +1 -1
- package/dist/e2e/kit/agents.js +60 -6
- package/dist/e2e/kit/agents.js.map +1 -1
- package/dist/e2e/kit/assertions.js +13 -1
- package/dist/e2e/kit/assertions.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 +25 -5
- package/dist/e2e/kit/fake-integrator.js.map +1 -1
- package/dist/e2e/kit/gh-emulator.js +172 -40
- 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 +1 -0
- package/dist/e2e/kit/index.js.map +1 -1
- package/dist/e2e/kit/scenario.js +165 -0
- package/dist/e2e/kit/scenario.js.map +1 -0
- package/dist/e2e/kit/scenarios.js +12 -18
- package/dist/e2e/kit/scenarios.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/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/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 +12 -11
- package/dist/mcp/mcp-capabilities.js.map +1 -1
- package/dist/mcp/mcp-facade.service.js +45 -5
- package/dist/mcp/mcp-facade.service.js.map +1 -1
- package/dist/mcp/mcp-tools.js +59 -5
- package/dist/mcp/mcp-tools.js.map +1 -1
- package/dist/mcp/monitoring-directive.js +43 -0
- package/dist/mcp/monitoring-directive.js.map +1 -0
- package/dist/pipeline/data-driven-task.workflow.js +393 -182
- package/dist/pipeline/data-driven-task.workflow.js.map +1 -1
- package/dist/pipeline/pipeline.service.js +27 -5
- package/dist/pipeline/pipeline.service.js.map +1 -1
- package/dist/pipeline/route-contract.js +123 -3
- 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 +39 -2
- package/dist/playbook/catalog-loader.js.map +1 -1
- package/dist/playbook/catalog-schema-validator.js +438 -0
- package/dist/playbook/catalog-schema-validator.js.map +1 -0
- package/dist/playbook/import-mapper.js +41 -0
- 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 +280 -94
- 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 +116 -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 -1
- package/dist/run/create-run.js.map +1 -1
- package/dist/run/inspect-run.js +32 -28
- 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 +619 -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/integrator.js +657 -56
- 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 +117 -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 +234 -43
- 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 +22 -3
- 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 +235 -0
- package/prisma/migrations/migration_lock.toml +3 -0
- package/prisma/schema.prisma +339 -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,9 +12,9 @@ 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';
|
|
17
|
+
import { hasClosingIssueReference, hasIssueRefToken, issueBodyWithClosingReference, issueRefTag, } from '../run/issue-ref.js';
|
|
18
18
|
import { resolveGhAccount, resolvePinnedGh } from './gh-identity.js';
|
|
19
19
|
import { gitAbsPath, branchExists, countAhead } from './integrator-git.js';
|
|
20
20
|
import { resolveOwnerRepo } from './integrator-remote.js';
|
|
@@ -22,14 +22,28 @@ 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,66 @@ 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 };
|
|
108
147
|
}
|
|
109
|
-
function
|
|
110
|
-
|
|
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);
|
|
158
|
+
}
|
|
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) {
|
|
173
|
+
const integratorAccount = resolveGhAccount();
|
|
174
|
+
if (!author || author.toLowerCase() === integratorAccount.toLowerCase())
|
|
175
|
+
return {};
|
|
176
|
+
return { foreignPr: true, prAuthor: author, integratorAccount };
|
|
177
|
+
}
|
|
178
|
+
function repairProducedChangePr(context, existing, provenance) {
|
|
179
|
+
if (provenance.foreignPr)
|
|
180
|
+
return;
|
|
181
|
+
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);
|
|
182
|
+
}
|
|
183
|
+
function existingProducedChangeOutput(context, existing, provenance, status) {
|
|
184
|
+
return {
|
|
185
|
+
prUrl: existing.prUrl,
|
|
186
|
+
branch: context.branch,
|
|
187
|
+
prNumber: existing.prNumber,
|
|
188
|
+
...(context.issueRef ? { issueRef: context.issueRef } : {}),
|
|
189
|
+
headSha: context.change.headSha,
|
|
190
|
+
status,
|
|
191
|
+
...(status === 'noop' ? { message: 'nothing to integrate — produced head already pushed and equals PR head' } : {}),
|
|
192
|
+
...provenance,
|
|
193
|
+
};
|
|
194
|
+
}
|
|
195
|
+
function reuseExistingProducedChangePr(context, existing) {
|
|
196
|
+
if (existing.headSha !== context.change.headSha)
|
|
197
|
+
return null;
|
|
198
|
+
const provenance = foreignPrProvenance(existing.author);
|
|
199
|
+
repairProducedChangePr(context, existing, provenance);
|
|
200
|
+
return existingProducedChangeOutput(context, existing, provenance, 'noop');
|
|
201
|
+
}
|
|
202
|
+
function updateExistingProducedChangePr(context, existing) {
|
|
203
|
+
const provenance = foreignPrProvenance(existing.author);
|
|
204
|
+
repairProducedChangePr(context, existing, provenance);
|
|
205
|
+
return existingProducedChangeOutput(context, existing, provenance, 'pushed');
|
|
120
206
|
}
|
|
121
207
|
export async function preflightLive(taskId, base, deps) {
|
|
122
208
|
const { execGit, resolveTaskCwd } = deps;
|
|
@@ -186,6 +272,7 @@ export async function captureProducedChange(input, deps) {
|
|
|
186
272
|
const branch = branchName(input.taskId, input.title, input.issueRef);
|
|
187
273
|
const ownerRepoResult = resolveOwnerRepo(git, cwd);
|
|
188
274
|
const ownerRepo = 'needsHuman' in ownerRepoResult ? undefined : ownerRepoResult.ownerRepo;
|
|
275
|
+
const issueAction = resolvedIssueAction(input.issueRef, input.issueAction);
|
|
189
276
|
if (branchExists(git, cwd, branch)) {
|
|
190
277
|
git(['switch', branch], cwd);
|
|
191
278
|
}
|
|
@@ -194,13 +281,14 @@ export async function captureProducedChange(input, deps) {
|
|
|
194
281
|
}
|
|
195
282
|
git(['add', '-A'], cwd);
|
|
196
283
|
if (stagedDiffPresent(git, cwd)) {
|
|
197
|
-
git(['commit', '-m', commitMessage(input.title, input.issueRef, ownerRepo)], cwd);
|
|
284
|
+
git(['commit', '-m', commitMessage(input.title, input.issueRef, ownerRepo, issueAction)], cwd);
|
|
198
285
|
}
|
|
199
286
|
const headSha = git(['rev-parse', 'HEAD'], cwd).trim();
|
|
200
287
|
return {
|
|
201
288
|
branch,
|
|
202
289
|
headSha,
|
|
203
290
|
...(input.issueRef ? { issueRef: input.issueRef } : {}),
|
|
291
|
+
...(issueAction ? { issueAction } : {}),
|
|
204
292
|
worktreePath: cwd,
|
|
205
293
|
...(input.artifactRef ? { artifactRef: input.artifactRef } : {}),
|
|
206
294
|
};
|
|
@@ -224,6 +312,7 @@ export async function integrate(input, deps) {
|
|
|
224
312
|
if ('needsHuman' in ownerRepoResult)
|
|
225
313
|
return ownerRepoResult;
|
|
226
314
|
const { ownerRepo } = ownerRepoResult;
|
|
315
|
+
const issueAction = resolvedIssueAction(input.issueRef, input.issueAction);
|
|
227
316
|
git(['fetch', 'origin', input.base], cwd);
|
|
228
317
|
if (branchExists(git, cwd, branch)) {
|
|
229
318
|
git(['switch', branch], cwd);
|
|
@@ -233,7 +322,7 @@ export async function integrate(input, deps) {
|
|
|
233
322
|
}
|
|
234
323
|
git(['add', '-A'], cwd);
|
|
235
324
|
if (stagedDiffPresent(git, cwd)) {
|
|
236
|
-
const commitMsg = commitMessage(input.title, input.issueRef, ownerRepo);
|
|
325
|
+
const commitMsg = commitMessage(input.title, input.issueRef, ownerRepo, issueAction);
|
|
237
326
|
git(['commit', '-m', commitMsg], cwd);
|
|
238
327
|
}
|
|
239
328
|
else {
|
|
@@ -256,7 +345,7 @@ export async function integrate(input, deps) {
|
|
|
256
345
|
}
|
|
257
346
|
}
|
|
258
347
|
git(['push', '-u', 'origin', branch], cwd);
|
|
259
|
-
const prResult = findOrCreatePr(ownerRepo, branch, input.base, input.title, input.issueRef, gh);
|
|
348
|
+
const prResult = findOrCreatePr(ownerRepo, branch, input.base, input.title, input.issueRef, issueAction, gh);
|
|
260
349
|
if ('needsHuman' in prResult)
|
|
261
350
|
return prResult;
|
|
262
351
|
return { prUrl: prResult.prUrl, branch, prNumber: prResult.prNumber, ...(input.issueRef ? { issueRef: input.issueRef } : {}) };
|
|
@@ -266,6 +355,7 @@ async function integrateProducedChange(input, deps, change) {
|
|
|
266
355
|
const cwd = change.worktreePath ?? await deps.resolveRunCwd(input.runId, input.taskId);
|
|
267
356
|
const branch = change.branch;
|
|
268
357
|
const issueRef = change.issueRef ?? input.issueRef;
|
|
358
|
+
const issueAction = resolvedIssueAction(issueRef, change.issueAction ?? input.issueAction);
|
|
269
359
|
const ownerRepoResult = resolveOwnerRepo(git, cwd);
|
|
270
360
|
if ('needsHuman' in ownerRepoResult)
|
|
271
361
|
return ownerRepoResult;
|
|
@@ -274,16 +364,11 @@ async function integrateProducedChange(input, deps, change) {
|
|
|
274
364
|
const existing = findExistingPrWithHead(ownerRepo, branch, input.base, gh);
|
|
275
365
|
if (existing && 'needsHuman' in existing)
|
|
276
366
|
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
|
-
};
|
|
367
|
+
const prContext = { ownerRepo, branch, title: input.title, issueRef, issueAction, change, execGh: gh };
|
|
368
|
+
if (existing) {
|
|
369
|
+
const reused = reuseExistingProducedChangePr(prContext, existing);
|
|
370
|
+
if (reused)
|
|
371
|
+
return reused;
|
|
287
372
|
}
|
|
288
373
|
if (!existing && countAhead(git, cwd, change.headSha, input.base) === 0) {
|
|
289
374
|
return {
|
|
@@ -294,16 +379,9 @@ async function integrateProducedChange(input, deps, change) {
|
|
|
294
379
|
}
|
|
295
380
|
git(['push', 'origin', `${change.headSha}:refs/heads/${branch}`], cwd);
|
|
296
381
|
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
|
-
};
|
|
382
|
+
return updateExistingProducedChangePr(prContext, existing);
|
|
305
383
|
}
|
|
306
|
-
const created = createPr(ownerRepo, branch, input.base, input.title, issueRef, gh);
|
|
384
|
+
const created = createPr(ownerRepo, branch, input.base, input.title, issueRef, issueAction, gh);
|
|
307
385
|
if ('needsHuman' in created)
|
|
308
386
|
return created;
|
|
309
387
|
return {
|
|
@@ -315,6 +393,13 @@ async function integrateProducedChange(input, deps, change) {
|
|
|
315
393
|
status: 'pushed',
|
|
316
394
|
};
|
|
317
395
|
}
|
|
396
|
+
function mergeabilityForConfirmMerge(pr, overrideAccepted) {
|
|
397
|
+
if (!overrideAccepted) {
|
|
398
|
+
return pr.mergeStateStatus === 'CLEAN' ? 'clean' : 'blocked';
|
|
399
|
+
}
|
|
400
|
+
const mergeable = pr.mergeable ?? (pr.mergeStateStatus === 'CLEAN' ? 'MERGEABLE' : undefined);
|
|
401
|
+
return mergeSignal(pr.mergeStateStatus, mergeable);
|
|
402
|
+
}
|
|
318
403
|
export async function confirmMerge(input, deps) {
|
|
319
404
|
const { execGit: git, execGh: gh, resolveRunCwd } = deps;
|
|
320
405
|
const cwd = await resolveRunCwd(input.runId, input.taskId);
|
|
@@ -323,8 +408,9 @@ export async function confirmMerge(input, deps) {
|
|
|
323
408
|
if ('needsHuman' in ownerRepoResult)
|
|
324
409
|
return ownerRepoResult;
|
|
325
410
|
const { ownerRepo } = ownerRepoResult;
|
|
411
|
+
const issueAction = resolvedIssueAction(input.issueRef, input.issueAction);
|
|
326
412
|
const view = () => {
|
|
327
|
-
const raw = gh(['pr', 'view', branch, '--repo', ownerRepo, '--json', 'number,url,state,isDraft,mergeStateStatus']);
|
|
413
|
+
const raw = gh(['pr', 'view', branch, '--repo', ownerRepo, '--json', 'number,url,state,isDraft,mergeStateStatus,mergeable,closingIssuesReferences']);
|
|
328
414
|
try {
|
|
329
415
|
return JSON.parse(raw);
|
|
330
416
|
}
|
|
@@ -338,7 +424,9 @@ export async function confirmMerge(input, deps) {
|
|
|
338
424
|
if (pr.state !== 'OPEN') {
|
|
339
425
|
return { needsHuman: true, lesson: `PR #${pr.number} is ${pr.state} (not OPEN) and not merged — resolve manually` };
|
|
340
426
|
}
|
|
341
|
-
|
|
427
|
+
const overrideAccepted = input.mergeReadiness?.override?.accepted === true;
|
|
428
|
+
const mergeability = mergeabilityForConfirmMerge(pr, overrideAccepted);
|
|
429
|
+
if (mergeability !== 'clean') {
|
|
342
430
|
return {
|
|
343
431
|
needsHuman: true,
|
|
344
432
|
lesson: `PR #${pr.number} is not auto-mergeable (mergeStateStatus=${pr.mergeStateStatus}) — CI not green, ` +
|
|
@@ -352,6 +440,12 @@ export async function confirmMerge(input, deps) {
|
|
|
352
440
|
lesson: `PR #${pr.number} merge requires a fresh merge readiness headSha guard — re-run readiness before approving merge`,
|
|
353
441
|
};
|
|
354
442
|
}
|
|
443
|
+
if (issueAction === 'close' && input.issueRef && !hasClosingIssueReference(pr.closingIssuesReferences, input.issueRef)) {
|
|
444
|
+
return {
|
|
445
|
+
needsHuman: true,
|
|
446
|
+
lesson: `PR #${pr.number} is expected to close ${issueRefTag(input.issueRef, ownerRepo)} but GitHub closingIssuesReferences does not include it`,
|
|
447
|
+
};
|
|
448
|
+
}
|
|
355
449
|
if (pr.isDraft) {
|
|
356
450
|
gh(['pr', 'ready', branch, '--repo', ownerRepo]);
|
|
357
451
|
}
|
|
@@ -375,14 +469,22 @@ function envInt(name, fallback) {
|
|
|
375
469
|
const n = Number.parseInt(process.env[name] ?? '', 10);
|
|
376
470
|
return Number.isFinite(n) && n > 0 ? n : fallback;
|
|
377
471
|
}
|
|
378
|
-
function defaultCollect(repo, branch, base, execGh, issueRef) {
|
|
379
|
-
return collectPrReadiness({ repo, headBranch: branch, baseBranch: base, issueRef, includeReviewThreads: true, includeComments: false }, execGh).then((r) => ({
|
|
472
|
+
function defaultCollect(repo, branch, base, execGh, issueRef, issueAction) {
|
|
473
|
+
return collectPrReadiness({ repo, headBranch: branch, baseBranch: base, issueRef, issueAction, includeReviewThreads: true, includeComments: false }, execGh).then((r) => ({
|
|
380
474
|
pr: { number: r.pr.number, headSha: r.pr.headSha },
|
|
381
475
|
checks: { pending: r.checks.pending, fail: r.checks.fail, list: r.checks.list },
|
|
382
|
-
reviewThreads: {
|
|
476
|
+
reviewThreads: {
|
|
477
|
+
items: r.reviewThreads.items,
|
|
478
|
+
unresolvedCount: r.reviewThreads.unresolvedCount,
|
|
479
|
+
...(r.reviewThreads.truncated ? { truncated: true } : {}),
|
|
480
|
+
},
|
|
383
481
|
readinessVerdict: r.verdict,
|
|
384
482
|
nextAction: r.nextAction,
|
|
385
483
|
evidence: r.evidence,
|
|
484
|
+
mergeStateStatus: r.pr.mergeState,
|
|
485
|
+
mergeable: r.ciSummary.mergeable,
|
|
486
|
+
draft: r.pr.draft,
|
|
487
|
+
feedback: r.feedback,
|
|
386
488
|
}));
|
|
387
489
|
}
|
|
388
490
|
const defaultSleep = (ms) => new Promise((resolve) => setTimeout(resolve, ms));
|
|
@@ -428,8 +530,364 @@ function unsettledReadinessFeedback(input, readiness, reason) {
|
|
|
428
530
|
verdict: 'recheck',
|
|
429
531
|
ciFailures: ciFailuresFrom(readiness),
|
|
430
532
|
reviewThreads,
|
|
533
|
+
...(readiness.mergeStateStatus !== undefined ? { mergeStateStatus: readiness.mergeStateStatus } : {}),
|
|
534
|
+
...(readiness.mergeable !== undefined ? { mergeable: readiness.mergeable } : {}),
|
|
535
|
+
};
|
|
536
|
+
}
|
|
537
|
+
// UNSTABLE/HAS_HOOKS: GitHub marks a PR mergeable even when non-required checks are unsettled.
|
|
538
|
+
// Required-check gating is handled upstream; this quirk is intentional for advisory-only scenarios.
|
|
539
|
+
export function mergeSignal(mergeStateStatus, mergeable) {
|
|
540
|
+
const ms = (mergeStateStatus ?? '').toUpperCase();
|
|
541
|
+
const mg = (mergeable ?? '').toUpperCase();
|
|
542
|
+
if (mg === 'CONFLICTING' || ms === 'DIRTY' || ms === 'BLOCKED' || ms === 'BEHIND')
|
|
543
|
+
return 'blocked';
|
|
544
|
+
if (mg === 'MERGEABLE' && (ms === 'CLEAN' || ms === 'UNSTABLE' || ms === 'HAS_HOOKS'))
|
|
545
|
+
return 'clean';
|
|
546
|
+
return 'unknown';
|
|
547
|
+
}
|
|
548
|
+
function mergeOverrideGate(input) {
|
|
549
|
+
const resolution = asRecord(input.gateResolution);
|
|
550
|
+
const audit = asRecord(resolution?.['mergeOverrideAudit']);
|
|
551
|
+
const note = nonBlank(resolution?.['note']);
|
|
552
|
+
if (!resolution || !note || !audit) {
|
|
553
|
+
return {
|
|
554
|
+
needsHuman: true,
|
|
555
|
+
lesson: 'override_merge requires a non-empty note and mergeOverrideAudit from the merge gate',
|
|
556
|
+
};
|
|
557
|
+
}
|
|
558
|
+
const actor = nonBlank(audit.actor) ?? nonBlank(resolution['resolvedBy']) ?? 'operator';
|
|
559
|
+
const trustedHeadSha = nonBlank(resolution['trustedGateHeadSha']);
|
|
560
|
+
return {
|
|
561
|
+
note,
|
|
562
|
+
audit,
|
|
563
|
+
actor,
|
|
564
|
+
...(trustedHeadSha ? { trustedHeadSha } : {}),
|
|
565
|
+
source: { gate: 'mergeGate', inboxId: nonBlank(resolution['inboxId']) ?? '' },
|
|
566
|
+
};
|
|
567
|
+
}
|
|
568
|
+
function prReviewThreads(readiness) {
|
|
569
|
+
return readiness.reviewThreads.items.map((thread) => ({
|
|
570
|
+
threadId: thread.id,
|
|
571
|
+
path: thread.path,
|
|
572
|
+
line: thread.line,
|
|
573
|
+
author: thread.author,
|
|
574
|
+
body: thread.body,
|
|
575
|
+
}));
|
|
576
|
+
}
|
|
577
|
+
function checkFact(severity, kind, name, result) {
|
|
578
|
+
return { severity, kind, name, summary: `${name}: ${result}`, evidence: result };
|
|
579
|
+
}
|
|
580
|
+
function emptyFactBuckets() {
|
|
581
|
+
return { hard: [], advisory: [] };
|
|
582
|
+
}
|
|
583
|
+
function pushFact(buckets, fact) {
|
|
584
|
+
buckets[fact.severity].push(fact);
|
|
585
|
+
}
|
|
586
|
+
function reviewThreadFact(thread) {
|
|
587
|
+
return {
|
|
588
|
+
severity: 'advisory',
|
|
589
|
+
kind: 'review_thread',
|
|
590
|
+
threadId: thread.id,
|
|
591
|
+
summary: thread.body,
|
|
592
|
+
evidence: [thread.path, thread.line].filter((item) => item !== undefined).join(':') || thread.url || thread.id,
|
|
593
|
+
};
|
|
594
|
+
}
|
|
595
|
+
function feedbackEvidence(item) {
|
|
596
|
+
return item.evidence ?? item.location ?? item.author ?? item.summary;
|
|
597
|
+
}
|
|
598
|
+
function classifyFeedbackFacts(readiness) {
|
|
599
|
+
const facts = emptyFactBuckets();
|
|
600
|
+
const feedback = readiness.feedback;
|
|
601
|
+
if (!feedback)
|
|
602
|
+
return facts;
|
|
603
|
+
for (const fix of feedback.developerFixes) {
|
|
604
|
+
if (fix.source === 'ci' || fix.source === 'review_thread')
|
|
605
|
+
continue;
|
|
606
|
+
const fact = {
|
|
607
|
+
severity: 'advisory',
|
|
608
|
+
kind: fix.source,
|
|
609
|
+
summary: fix.summary,
|
|
610
|
+
evidence: feedbackEvidence(fix),
|
|
611
|
+
};
|
|
612
|
+
pushFact(facts, fix.source === 'human_review' ? { ...fact, severity: 'hard' } : fact);
|
|
613
|
+
}
|
|
614
|
+
for (const question of feedback.reviewerQuestions) {
|
|
615
|
+
pushFact(facts, {
|
|
616
|
+
severity: 'hard',
|
|
617
|
+
kind: 'reviewer_question',
|
|
618
|
+
summary: question.summary,
|
|
619
|
+
evidence: feedbackEvidence(question),
|
|
620
|
+
});
|
|
621
|
+
}
|
|
622
|
+
for (const decision of feedback.humanDecisions) {
|
|
623
|
+
pushFact(facts, {
|
|
624
|
+
severity: 'hard',
|
|
625
|
+
kind: decision.source,
|
|
626
|
+
summary: decision.summary,
|
|
627
|
+
});
|
|
628
|
+
}
|
|
629
|
+
for (const wait of feedback.providerWait) {
|
|
630
|
+
pushFact(facts, {
|
|
631
|
+
severity: 'advisory',
|
|
632
|
+
kind: `provider_wait:${wait.provider}`,
|
|
633
|
+
summary: wait.evidence,
|
|
634
|
+
evidence: wait.reason,
|
|
635
|
+
});
|
|
636
|
+
}
|
|
637
|
+
return facts;
|
|
638
|
+
}
|
|
639
|
+
function checkStatusFact(check, required, status) {
|
|
640
|
+
const severity = required ? 'hard' : 'advisory';
|
|
641
|
+
const kind = `${required ? 'required' : 'non_required'}_check_${status}`;
|
|
642
|
+
return checkFact(severity, kind, check.name, check.result);
|
|
643
|
+
}
|
|
644
|
+
function pushCheckStatusFact(facts, check, names, required, status) {
|
|
645
|
+
if (!names.includes(check.name))
|
|
646
|
+
return;
|
|
647
|
+
pushFact(facts, checkStatusFact(check, required, status));
|
|
648
|
+
}
|
|
649
|
+
function factsFromRequiredChecks(readiness, required) {
|
|
650
|
+
const facts = emptyFactBuckets();
|
|
651
|
+
for (const check of readiness.checks.list) {
|
|
652
|
+
const isRequired = required.has(check.name);
|
|
653
|
+
pushCheckStatusFact(facts, check, readiness.checks.fail, isRequired, 'failed');
|
|
654
|
+
pushCheckStatusFact(facts, check, readiness.checks.pending, isRequired, 'pending');
|
|
655
|
+
}
|
|
656
|
+
if (readiness.checks.list.length === 0) {
|
|
657
|
+
pushFact(facts, {
|
|
658
|
+
severity: 'advisory',
|
|
659
|
+
kind: 'checks_none_registered',
|
|
660
|
+
summary: 'No registered checks were reported for the PR.',
|
|
661
|
+
evidence: 'checks: none registered',
|
|
662
|
+
});
|
|
663
|
+
}
|
|
664
|
+
return facts;
|
|
665
|
+
}
|
|
666
|
+
function checkRollupUnavailableFact(readiness) {
|
|
667
|
+
const unavailable = readiness.checks.pending.includes(GITHUB_CHECK_ROLLUP_UNAVAILABLE)
|
|
668
|
+
|| readiness.evidence.some((item) => item.includes(GITHUB_CHECK_ROLLUP_UNAVAILABLE));
|
|
669
|
+
if (!unavailable)
|
|
670
|
+
return undefined;
|
|
671
|
+
return {
|
|
672
|
+
severity: 'hard',
|
|
673
|
+
kind: 'check_rollup_unavailable',
|
|
674
|
+
summary: 'GitHub check rollup is unavailable; override cannot distinguish pending checks from no registered checks.',
|
|
675
|
+
evidence: GITHUB_CHECK_ROLLUP_UNAVAILABLE,
|
|
676
|
+
};
|
|
677
|
+
}
|
|
678
|
+
function reviewThreadsIncompleteFact(readiness) {
|
|
679
|
+
if (readiness.reviewThreads.truncated !== true)
|
|
680
|
+
return undefined;
|
|
681
|
+
return {
|
|
682
|
+
severity: 'hard',
|
|
683
|
+
kind: 'review_threads_incomplete',
|
|
684
|
+
summary: 'Review thread data is incomplete; override cannot safely resolve every unresolved thread.',
|
|
685
|
+
evidence: `unresolvedCount=${readiness.reviewThreads.unresolvedCount ?? readiness.reviewThreads.items.length}`,
|
|
431
686
|
};
|
|
432
687
|
}
|
|
688
|
+
function mergeOverrideOutput(input) {
|
|
689
|
+
const readiness = input.readiness;
|
|
690
|
+
const evidence = [
|
|
691
|
+
...(readiness?.evidence ?? []),
|
|
692
|
+
...(readiness ? readinessEvidence(readiness) : []),
|
|
693
|
+
...input.facts.map((fact) => `${fact.severity} ${fact.kind}: ${fact.summary}`),
|
|
694
|
+
input.reason,
|
|
695
|
+
`PR headSha=${readiness?.pr.headSha ?? input.fallbackHeadSha ?? input.gate.audit.headSha}`,
|
|
696
|
+
`overrideMerge verdict=${input.verdict}`,
|
|
697
|
+
].filter((item) => typeof item === 'string' && item.length > 0);
|
|
698
|
+
return {
|
|
699
|
+
prNumber: readiness?.pr.number ?? null,
|
|
700
|
+
headSha: readiness?.pr.headSha ?? input.fallbackHeadSha ?? input.gate.audit.headSha,
|
|
701
|
+
evidence,
|
|
702
|
+
verdict: input.verdict,
|
|
703
|
+
ciFailures: readiness ? ciFailuresFrom(readiness) : [],
|
|
704
|
+
reviewThreads: readiness ? prReviewThreads(readiness) : [],
|
|
705
|
+
...(readiness?.mergeStateStatus !== undefined ? { mergeStateStatus: readiness.mergeStateStatus } : {}),
|
|
706
|
+
...(readiness?.mergeable !== undefined ? { mergeable: readiness.mergeable } : {}),
|
|
707
|
+
override: {
|
|
708
|
+
accepted: input.accepted,
|
|
709
|
+
actor: input.gate.actor,
|
|
710
|
+
note: input.gate.note,
|
|
711
|
+
audit: {
|
|
712
|
+
reason: input.gate.audit.reason,
|
|
713
|
+
risk: input.gate.audit.risk,
|
|
714
|
+
verificationResponsibility: input.gate.audit.verificationResponsibility,
|
|
715
|
+
headSha: input.gate.audit.headSha,
|
|
716
|
+
},
|
|
717
|
+
source: input.gate.source,
|
|
718
|
+
facts: input.facts,
|
|
719
|
+
replied: input.response?.replied ?? 0,
|
|
720
|
+
resolved: input.response?.resolved ?? 0,
|
|
721
|
+
...(input.reason ? { reason: input.reason } : {}),
|
|
722
|
+
},
|
|
723
|
+
};
|
|
724
|
+
}
|
|
725
|
+
function hardMergeabilityFact(readiness) {
|
|
726
|
+
const signal = mergeSignal(readiness.mergeStateStatus, readiness.mergeable);
|
|
727
|
+
if (signal === 'clean')
|
|
728
|
+
return undefined;
|
|
729
|
+
return {
|
|
730
|
+
severity: 'hard',
|
|
731
|
+
kind: signal === 'blocked' ? 'mergeability_blocked' : 'mergeability_unknown',
|
|
732
|
+
summary: `mergeStateStatus=${readiness.mergeStateStatus ?? ''} mergeable=${readiness.mergeable ?? ''}`,
|
|
733
|
+
evidence: `mergeStateStatus=${readiness.mergeStateStatus ?? ''}; mergeable=${readiness.mergeable ?? ''}`,
|
|
734
|
+
};
|
|
735
|
+
}
|
|
736
|
+
function movedHeadFact(readiness, gate) {
|
|
737
|
+
const approvedHeadSha = gate.trustedHeadSha;
|
|
738
|
+
if (!approvedHeadSha) {
|
|
739
|
+
return {
|
|
740
|
+
severity: 'hard',
|
|
741
|
+
kind: 'trusted_gate_head_unavailable',
|
|
742
|
+
summary: 'Trusted merge gate head is unavailable.',
|
|
743
|
+
evidence: `audit=${gate.audit.headSha}; fresh=${readiness.pr.headSha}`,
|
|
744
|
+
};
|
|
745
|
+
}
|
|
746
|
+
if (readiness.pr.headSha === approvedHeadSha)
|
|
747
|
+
return undefined;
|
|
748
|
+
return {
|
|
749
|
+
severity: 'hard',
|
|
750
|
+
kind: 'head_moved',
|
|
751
|
+
summary: `approved head ${approvedHeadSha} no longer matches fresh head ${readiness.pr.headSha}`,
|
|
752
|
+
evidence: `approved=${approvedHeadSha}; audit=${gate.audit.headSha}; fresh=${readiness.pr.headSha}`,
|
|
753
|
+
};
|
|
754
|
+
}
|
|
755
|
+
function auditHeadMismatchFact(gate) {
|
|
756
|
+
if (!gate.trustedHeadSha)
|
|
757
|
+
return undefined;
|
|
758
|
+
if (gate.audit.headSha === gate.trustedHeadSha)
|
|
759
|
+
return undefined;
|
|
760
|
+
return {
|
|
761
|
+
severity: 'hard',
|
|
762
|
+
kind: 'audit_head_mismatch',
|
|
763
|
+
summary: `audit head ${gate.audit.headSha} does not match trusted merge gate head ${gate.trustedHeadSha}`,
|
|
764
|
+
evidence: `audit=${gate.audit.headSha}; approved=${gate.trustedHeadSha}`,
|
|
765
|
+
};
|
|
766
|
+
}
|
|
767
|
+
function initialMergeOverrideHardFacts(readiness, gate, unavailableRollup) {
|
|
768
|
+
return [
|
|
769
|
+
readiness.draft === true ? { severity: 'hard', kind: 'draft_pr', summary: 'PR is still draft.' } : undefined,
|
|
770
|
+
auditHeadMismatchFact(gate),
|
|
771
|
+
movedHeadFact(readiness, gate),
|
|
772
|
+
hardMergeabilityFact(readiness),
|
|
773
|
+
unavailableRollup,
|
|
774
|
+
reviewThreadsIncompleteFact(readiness),
|
|
775
|
+
].filter((fact) => fact !== undefined);
|
|
776
|
+
}
|
|
777
|
+
function appendFeedbackFacts(buckets, readiness) {
|
|
778
|
+
for (const thread of readiness.reviewThreads.items) {
|
|
779
|
+
buckets.advisory.push(reviewThreadFact(thread));
|
|
780
|
+
}
|
|
781
|
+
const feedbackFacts = classifyFeedbackFacts(readiness);
|
|
782
|
+
buckets.hard.push(...feedbackFacts.hard);
|
|
783
|
+
buckets.advisory.push(...feedbackFacts.advisory);
|
|
784
|
+
}
|
|
785
|
+
function classifyAvailableCheckFacts(readiness, ownerRepo, execGh, requiredChecks) {
|
|
786
|
+
if (readiness.checks.fail.length === 0 && readiness.checks.pending.length === 0) {
|
|
787
|
+
return readiness.checks.list.length === 0 ? factsFromRequiredChecks(readiness, new Set()) : emptyFactBuckets();
|
|
788
|
+
}
|
|
789
|
+
if (readiness.pr.number === null) {
|
|
790
|
+
return {
|
|
791
|
+
hard: [{ severity: 'hard', kind: 'pr_number_unavailable', summary: 'Cannot classify required checks without a PR number.' }],
|
|
792
|
+
advisory: [],
|
|
793
|
+
};
|
|
794
|
+
}
|
|
795
|
+
try {
|
|
796
|
+
return factsFromRequiredChecks(readiness, requiredChecks(ownerRepo, readiness.pr.number, execGh));
|
|
797
|
+
}
|
|
798
|
+
catch (err) {
|
|
799
|
+
return {
|
|
800
|
+
hard: [{
|
|
801
|
+
severity: 'hard',
|
|
802
|
+
kind: 'required_check_names_unavailable',
|
|
803
|
+
summary: 'Required check names are unavailable.',
|
|
804
|
+
evidence: err instanceof Error ? err.message : String(err),
|
|
805
|
+
}],
|
|
806
|
+
advisory: [],
|
|
807
|
+
};
|
|
808
|
+
}
|
|
809
|
+
}
|
|
810
|
+
async function classifyMergeOverrideFacts(readiness, gate, ownerRepo, execGh, requiredChecks) {
|
|
811
|
+
const unavailableRollup = checkRollupUnavailableFact(readiness);
|
|
812
|
+
const facts = {
|
|
813
|
+
hard: initialMergeOverrideHardFacts(readiness, gate, unavailableRollup),
|
|
814
|
+
advisory: [],
|
|
815
|
+
};
|
|
816
|
+
if (!unavailableRollup) {
|
|
817
|
+
const checkFacts = classifyAvailableCheckFacts(readiness, ownerRepo, execGh, requiredChecks);
|
|
818
|
+
facts.hard.push(...checkFacts.hard);
|
|
819
|
+
facts.advisory.push(...checkFacts.advisory);
|
|
820
|
+
}
|
|
821
|
+
appendFeedbackFacts(facts, readiness);
|
|
822
|
+
return facts;
|
|
823
|
+
}
|
|
824
|
+
function overrideThreadTriage(readiness, note) {
|
|
825
|
+
return {
|
|
826
|
+
items: readiness.reviewThreads.items.map((thread) => ({
|
|
827
|
+
threadId: thread.id,
|
|
828
|
+
decision: 'wontfix',
|
|
829
|
+
replyText: `merged by operator override: ${note}`,
|
|
830
|
+
})),
|
|
831
|
+
};
|
|
832
|
+
}
|
|
833
|
+
export async function overrideMerge(input, deps) {
|
|
834
|
+
const gate = mergeOverrideGate(input);
|
|
835
|
+
if ('needsHuman' in gate)
|
|
836
|
+
return gate;
|
|
837
|
+
const { execGit: git, execGh: gh, resolveRunCwd } = deps;
|
|
838
|
+
const cwd = await resolveRunCwd(input.runId, input.taskId);
|
|
839
|
+
const branch = branchName(input.taskId, input.title, input.issueRef);
|
|
840
|
+
const ownerRepoResult = resolveOwnerRepo(git, cwd);
|
|
841
|
+
if ('needsHuman' in ownerRepoResult)
|
|
842
|
+
return ownerRepoResult;
|
|
843
|
+
const { ownerRepo } = ownerRepoResult;
|
|
844
|
+
let readiness;
|
|
845
|
+
try {
|
|
846
|
+
readiness = await (deps.collect ?? defaultCollect)(ownerRepo, branch, input.base, gh, input.issueRef, input.issueAction);
|
|
847
|
+
}
|
|
848
|
+
catch (err) {
|
|
849
|
+
const fact = {
|
|
850
|
+
severity: 'hard',
|
|
851
|
+
kind: 'readiness_transport_degraded',
|
|
852
|
+
summary: 'Fresh override reverify could not read PR readiness.',
|
|
853
|
+
evidence: err instanceof Error ? err.message : String(err),
|
|
854
|
+
};
|
|
855
|
+
return mergeOverrideOutput({
|
|
856
|
+
gate,
|
|
857
|
+
verdict: 'recheck',
|
|
858
|
+
facts: [fact],
|
|
859
|
+
accepted: false,
|
|
860
|
+
reason: fact.summary,
|
|
861
|
+
});
|
|
862
|
+
}
|
|
863
|
+
if (readiness.readinessVerdict === 'merged') {
|
|
864
|
+
return mergeOverrideOutput({ gate, readiness, verdict: 'merged', facts: [], accepted: false, reason: 'PR already merged externally.' });
|
|
865
|
+
}
|
|
866
|
+
if (readiness.readinessVerdict === 'closed') {
|
|
867
|
+
const fact = { severity: 'hard', kind: 'pr_closed_externally', summary: 'PR is closed without being merged.' };
|
|
868
|
+
return mergeOverrideOutput({ gate, readiness, verdict: 'closed', facts: [fact], accepted: false, reason: fact.summary });
|
|
869
|
+
}
|
|
870
|
+
const facts = await classifyMergeOverrideFacts(readiness, gate, ownerRepo, gh, deps.requiredChecks ?? fetchRequiredCheckNames);
|
|
871
|
+
if (facts.hard.length > 0) {
|
|
872
|
+
return mergeOverrideOutput({
|
|
873
|
+
gate,
|
|
874
|
+
readiness,
|
|
875
|
+
verdict: 'recheck',
|
|
876
|
+
facts: facts.hard,
|
|
877
|
+
accepted: false,
|
|
878
|
+
reason: 'override_merge refused because hard blockers remain',
|
|
879
|
+
});
|
|
880
|
+
}
|
|
881
|
+
const response = await respondThreads(overrideThreadTriage(readiness, gate.note), { execGh: gh });
|
|
882
|
+
return mergeOverrideOutput({
|
|
883
|
+
gate,
|
|
884
|
+
readiness,
|
|
885
|
+
verdict: 'clean',
|
|
886
|
+
facts: facts.advisory,
|
|
887
|
+
accepted: true,
|
|
888
|
+
response,
|
|
889
|
+
});
|
|
890
|
+
}
|
|
433
891
|
export async function pollPr(input, deps) {
|
|
434
892
|
const { execGit: git, execGh: gh, resolveRunCwd } = deps;
|
|
435
893
|
const collect = deps.collect ?? defaultCollect;
|
|
@@ -446,9 +904,9 @@ export async function pollPr(input, deps) {
|
|
|
446
904
|
let readiness;
|
|
447
905
|
let lastReadiness;
|
|
448
906
|
for (let i = 0; i < maxPolls; i++) {
|
|
449
|
-
readiness = await collect(ownerRepo, branch, input.base, gh, input.issueRef);
|
|
907
|
+
readiness = await collect(ownerRepo, branch, input.base, gh, input.issueRef, input.issueAction);
|
|
450
908
|
lastReadiness = readiness;
|
|
451
|
-
if (readiness.checks.pending.length === 0
|
|
909
|
+
if (readiness.checks.pending.length === 0)
|
|
452
910
|
break;
|
|
453
911
|
readiness = undefined;
|
|
454
912
|
if (i < maxPolls - 1)
|
|
@@ -468,6 +926,51 @@ export async function pollPr(input, deps) {
|
|
|
468
926
|
return { needsHuman: true, lesson: `pollPr timed out after ${maxPolls} polls before reading PR readiness for ${branch}` };
|
|
469
927
|
}
|
|
470
928
|
let settled = readiness;
|
|
929
|
+
const makeTerminalFeedback = (verdict) => ({
|
|
930
|
+
prNumber: settled.pr.number ?? null,
|
|
931
|
+
headSha: settled.pr.headSha,
|
|
932
|
+
evidence: [...settled.evidence, ...readinessEvidence(settled), `PR headSha=${settled.pr.headSha}`, `pollPr verdict=${verdict}`],
|
|
933
|
+
...(input.issueRef ? { issueRef: input.issueRef } : {}),
|
|
934
|
+
verdict,
|
|
935
|
+
ciFailures: ciFailuresFrom(settled),
|
|
936
|
+
reviewThreads: [],
|
|
937
|
+
...(settled.mergeStateStatus !== undefined ? { mergeStateStatus: settled.mergeStateStatus } : {}),
|
|
938
|
+
...(settled.mergeable !== undefined ? { mergeable: settled.mergeable } : {}),
|
|
939
|
+
});
|
|
940
|
+
if (settled.readinessVerdict === 'merged')
|
|
941
|
+
return makeTerminalFeedback('merged');
|
|
942
|
+
if (settled.readinessVerdict === 'closed')
|
|
943
|
+
return makeTerminalFeedback('closed');
|
|
944
|
+
const standardCheckResults = new Set('ACTION_REQUIRED CANCELLED ERROR EXPECTED FAILURE IN_PROGRESS NEUTRAL PENDING QUEUED SKIPPED STALE STARTUP_FAILURE SUCCESS TIMED_OUT UNKNOWN'.split(' '));
|
|
945
|
+
const standardMergeStateStatuses = new Set('BEHIND BLOCKED CLEAN DIRTY DRAFT HAS_HOOKS UNKNOWN UNSTABLE'.split(' '));
|
|
946
|
+
const standardMergeableStates = new Set('CONFLICTING MERGEABLE UNKNOWN'.split(' '));
|
|
947
|
+
const addUnclassifiableState = (states, label, value, known) => {
|
|
948
|
+
const state = (value ?? '').trim().toUpperCase();
|
|
949
|
+
if (state !== '' && !known.has(state))
|
|
950
|
+
states.push(`${label} ${state}`);
|
|
951
|
+
};
|
|
952
|
+
const unclassifiablePollState = (snapshot) => {
|
|
953
|
+
const states = [];
|
|
954
|
+
for (const check of snapshot.checks.list) {
|
|
955
|
+
addUnclassifiableState(states, 'check result', check.result, standardCheckResults);
|
|
956
|
+
}
|
|
957
|
+
addUnclassifiableState(states, 'mergeStateStatus', snapshot.mergeStateStatus, standardMergeStateStatuses);
|
|
958
|
+
addUnclassifiableState(states, 'mergeable', snapshot.mergeable, standardMergeableStates);
|
|
959
|
+
return states;
|
|
960
|
+
};
|
|
961
|
+
const unclassifiableReadinessBlock = (snapshot, states) => ({
|
|
962
|
+
needsHuman: true,
|
|
963
|
+
lesson: [
|
|
964
|
+
...snapshot.evidence,
|
|
965
|
+
...readinessEvidence(snapshot),
|
|
966
|
+
`pollPr unclassifiable readiness state: ${states.join(', ')}`,
|
|
967
|
+
`PR headSha=${snapshot.pr.headSha}`,
|
|
968
|
+
].join('; '),
|
|
969
|
+
});
|
|
970
|
+
const unclassifiable = unclassifiablePollState(settled);
|
|
971
|
+
if (unclassifiable.length > 0) {
|
|
972
|
+
return unclassifiableReadinessBlock(settled, unclassifiable);
|
|
973
|
+
}
|
|
471
974
|
const initialCiFailures = ciFailuresFrom(settled);
|
|
472
975
|
if (initialCiFailures.length === 0) {
|
|
473
976
|
try {
|
|
@@ -478,14 +981,19 @@ export async function pollPr(input, deps) {
|
|
|
478
981
|
const reviewGracePolls = deps.reviewGracePolls ?? envInt('REVO_POLL_PR_REVIEW_GRACE_POLLS', 4);
|
|
479
982
|
for (let i = 0; i < reviewGracePolls && settled.reviewThreads.items.length === 0; i++) {
|
|
480
983
|
await sleep(intervalMs);
|
|
481
|
-
settled = await collect(ownerRepo, branch, input.base, gh, input.issueRef);
|
|
984
|
+
settled = await collect(ownerRepo, branch, input.base, gh, input.issueRef, input.issueAction);
|
|
482
985
|
}
|
|
483
986
|
}
|
|
484
|
-
if (settled.
|
|
485
|
-
|
|
486
|
-
|
|
487
|
-
|
|
488
|
-
|
|
987
|
+
if (settled.readinessVerdict === 'merged')
|
|
988
|
+
return makeTerminalFeedback('merged');
|
|
989
|
+
if (settled.readinessVerdict === 'closed')
|
|
990
|
+
return makeTerminalFeedback('closed');
|
|
991
|
+
const finalUnclassifiable = unclassifiablePollState(settled);
|
|
992
|
+
if (finalUnclassifiable.length > 0) {
|
|
993
|
+
return unclassifiableReadinessBlock(settled, finalUnclassifiable);
|
|
994
|
+
}
|
|
995
|
+
if (settled.checks.pending.length > 0) {
|
|
996
|
+
return unsettledReadinessFeedback(input, settled, `pollPr found unsettled readiness after readying ${branch}: pending checks: ${settled.checks.pending.join(', ')}`);
|
|
489
997
|
}
|
|
490
998
|
const reviewThreads = settled.reviewThreads.items.map((t) => ({
|
|
491
999
|
threadId: t.id,
|
|
@@ -496,27 +1004,51 @@ export async function pollPr(input, deps) {
|
|
|
496
1004
|
}));
|
|
497
1005
|
const ciFailures = ciFailuresFrom(settled);
|
|
498
1006
|
let ciVerdictFailures = ciFailures;
|
|
1007
|
+
let requiredCheckFetchFailure;
|
|
499
1008
|
if (ciFailures.length > 0 && settled.pr.number !== null) {
|
|
500
1009
|
try {
|
|
501
1010
|
const required = requiredChecks(ownerRepo, settled.pr.number, gh);
|
|
502
1011
|
if (required.size > 0)
|
|
503
1012
|
ciVerdictFailures = ciFailures.filter((f) => required.has(f.name));
|
|
504
1013
|
}
|
|
505
|
-
catch {
|
|
1014
|
+
catch (err) {
|
|
1015
|
+
requiredCheckFetchFailure = err instanceof Error ? err.message : String(err);
|
|
506
1016
|
}
|
|
507
1017
|
}
|
|
508
|
-
const
|
|
509
|
-
? 'review_changes'
|
|
510
|
-
: ciVerdictFailures.length > 0 ? 'ci_changes' : 'clean';
|
|
511
|
-
return {
|
|
1018
|
+
const makeFeedback = (verdict, extraEvidence = []) => ({
|
|
512
1019
|
prNumber: settled.pr.number ?? null,
|
|
513
1020
|
headSha: settled.pr.headSha,
|
|
514
|
-
evidence: [...settled.evidence, ...readinessEvidence(settled), `PR headSha=${settled.pr.headSha}`, `pollPr verdict=${verdict}`],
|
|
1021
|
+
evidence: [...settled.evidence, ...readinessEvidence(settled), ...extraEvidence, `PR headSha=${settled.pr.headSha}`, `pollPr verdict=${verdict}`],
|
|
515
1022
|
...(input.issueRef ? { issueRef: input.issueRef } : {}),
|
|
516
1023
|
verdict,
|
|
517
1024
|
ciFailures,
|
|
518
1025
|
reviewThreads,
|
|
519
|
-
|
|
1026
|
+
...(settled.mergeStateStatus !== undefined ? { mergeStateStatus: settled.mergeStateStatus } : {}),
|
|
1027
|
+
...(settled.mergeable !== undefined ? { mergeable: settled.mergeable } : {}),
|
|
1028
|
+
});
|
|
1029
|
+
if (reviewThreads.length > 0 || readinessRequiresReview(settled))
|
|
1030
|
+
return makeFeedback('review_changes');
|
|
1031
|
+
if (requiredCheckFetchFailure) {
|
|
1032
|
+
return makeFeedback('recheck', [`Required check names unavailable: ${requiredCheckFetchFailure}`]);
|
|
1033
|
+
}
|
|
1034
|
+
if (ciVerdictFailures.length > 0)
|
|
1035
|
+
return makeFeedback('ci_changes');
|
|
1036
|
+
const signal = mergeSignal(settled.mergeStateStatus, settled.mergeable);
|
|
1037
|
+
if (signal === 'blocked') {
|
|
1038
|
+
return {
|
|
1039
|
+
needsHuman: true,
|
|
1040
|
+
lesson: [
|
|
1041
|
+
...settled.evidence,
|
|
1042
|
+
...readinessEvidence(settled),
|
|
1043
|
+
`pollPr merge readiness blocked (mergeStateStatus=${settled.mergeStateStatus ?? ''} mergeable=${settled.mergeable ?? ''})`,
|
|
1044
|
+
`PR headSha=${settled.pr.headSha}`,
|
|
1045
|
+
].join('; '),
|
|
1046
|
+
};
|
|
1047
|
+
}
|
|
1048
|
+
if (signal === 'unknown') {
|
|
1049
|
+
return unsettledReadinessFeedback(input, settled, `pollPr merge readiness unknown (mergeable=${settled.mergeable ?? 'undefined'})`);
|
|
1050
|
+
}
|
|
1051
|
+
return makeFeedback('clean');
|
|
520
1052
|
}
|
|
521
1053
|
const TRIAGE_DECISIONS = new Set(['fix', 'wontfix', 'question']);
|
|
522
1054
|
export function asTriage(value) {
|
|
@@ -531,6 +1063,37 @@ export function asTriage(value) {
|
|
|
531
1063
|
: [];
|
|
532
1064
|
return { items };
|
|
533
1065
|
}
|
|
1066
|
+
function questionGateResolution(value) {
|
|
1067
|
+
if (value === null || typeof value !== 'object')
|
|
1068
|
+
return undefined;
|
|
1069
|
+
const outcome = value.outcome;
|
|
1070
|
+
if (outcome !== 'fix' && outcome !== 'wontfix')
|
|
1071
|
+
return undefined;
|
|
1072
|
+
const rawNote = value.note;
|
|
1073
|
+
const note = typeof rawNote === 'string' ? rawNote.trim() : '';
|
|
1074
|
+
return { decision: outcome, ...(note ? { note } : {}) };
|
|
1075
|
+
}
|
|
1076
|
+
function replyTextForQuestionResolution(decision, note) {
|
|
1077
|
+
if (decision === 'fix')
|
|
1078
|
+
return note ? `Addressed: ${note}` : 'Addressed.';
|
|
1079
|
+
return note ? `Won't fix: ${note}` : "Won't fix.";
|
|
1080
|
+
}
|
|
1081
|
+
export function triageForRespondThreads(input) {
|
|
1082
|
+
const triage = asTriage(input.triage);
|
|
1083
|
+
const resolution = questionGateResolution(input.gateResolution);
|
|
1084
|
+
if (!resolution)
|
|
1085
|
+
return triage;
|
|
1086
|
+
return {
|
|
1087
|
+
...triage,
|
|
1088
|
+
items: triage.items.map((item) => item.decision === 'question'
|
|
1089
|
+
? {
|
|
1090
|
+
...item,
|
|
1091
|
+
decision: resolution.decision,
|
|
1092
|
+
replyText: replyTextForQuestionResolution(resolution.decision, resolution.note),
|
|
1093
|
+
}
|
|
1094
|
+
: item),
|
|
1095
|
+
};
|
|
1096
|
+
}
|
|
534
1097
|
export async function respondThreads(triage, deps) {
|
|
535
1098
|
const { execGh: gh } = deps;
|
|
536
1099
|
let replied = 0;
|
|
@@ -609,13 +1172,51 @@ let IntegratorService = class IntegratorService {
|
|
|
609
1172
|
runPollStub = (_input) => {
|
|
610
1173
|
return { prNumber: null, headSha: 'stub', evidence: ['stub pollPr readiness: clean'], verdict: 'clean', ciFailures: [], reviewThreads: [] };
|
|
611
1174
|
};
|
|
1175
|
+
runOverrideMerge = (input) => {
|
|
1176
|
+
const pinned = resolvePinnedGh();
|
|
1177
|
+
if ('needsHuman' in pinned) {
|
|
1178
|
+
console.warn(`[override-merge] ${pinned.lesson}`);
|
|
1179
|
+
return Promise.resolve(pinned);
|
|
1180
|
+
}
|
|
1181
|
+
return overrideMerge(input, { ...this.deps, execGh: pinned.execGh });
|
|
1182
|
+
};
|
|
1183
|
+
runOverrideStub = (input) => {
|
|
1184
|
+
const gate = mergeOverrideGate(input);
|
|
1185
|
+
if ('needsHuman' in gate) {
|
|
1186
|
+
return {
|
|
1187
|
+
prNumber: null,
|
|
1188
|
+
headSha: 'stub',
|
|
1189
|
+
evidence: [gate.lesson, 'overrideMerge verdict=recheck'],
|
|
1190
|
+
verdict: 'recheck',
|
|
1191
|
+
ciFailures: [],
|
|
1192
|
+
reviewThreads: [],
|
|
1193
|
+
override: {
|
|
1194
|
+
accepted: false,
|
|
1195
|
+
actor: 'stub',
|
|
1196
|
+
note: '',
|
|
1197
|
+
source: { gate: 'mergeGate', inboxId: '' },
|
|
1198
|
+
facts: [{ severity: 'hard', kind: 'override_input_missing', summary: gate.lesson }],
|
|
1199
|
+
replied: 0,
|
|
1200
|
+
resolved: 0,
|
|
1201
|
+
reason: gate.lesson,
|
|
1202
|
+
},
|
|
1203
|
+
};
|
|
1204
|
+
}
|
|
1205
|
+
return mergeOverrideOutput({
|
|
1206
|
+
gate,
|
|
1207
|
+
verdict: 'clean',
|
|
1208
|
+
facts: [],
|
|
1209
|
+
accepted: true,
|
|
1210
|
+
fallbackHeadSha: gate.audit.headSha || 'stub',
|
|
1211
|
+
});
|
|
1212
|
+
};
|
|
612
1213
|
runRespondThreads = (input) => {
|
|
613
1214
|
const pinned = resolvePinnedGh();
|
|
614
1215
|
if ('needsHuman' in pinned) {
|
|
615
1216
|
console.warn(`[respond-threads] ${pinned.lesson}`);
|
|
616
1217
|
return Promise.resolve(pinned);
|
|
617
1218
|
}
|
|
618
|
-
return respondThreads(
|
|
1219
|
+
return respondThreads(triageForRespondThreads(input), { execGh: pinned.execGh });
|
|
619
1220
|
};
|
|
620
1221
|
runRespondStub = (_input) => {
|
|
621
1222
|
return { replied: 0, resolved: 0 };
|