@revisium/orchestrator 0.1.0-alpha.9 → 0.2.0-alpha.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +210 -146
- package/control-plane/bootstrap.config.json +43 -1
- package/control-plane/default-playbook/catalog/pipelines.json +1157 -10
- package/control-plane/default-playbook/catalog/roles.json +54 -0
- package/control-plane/default-playbook/package.json +1 -1
- package/control-plane/default-playbook/prompts/developer.md +1 -0
- package/control-plane/default-playbook/prompts/triager.md +3 -0
- package/dist/api/graphql-api/graphql-ws/pubsub.module.js +0 -1
- package/dist/api/graphql-api/graphql-ws/pubsub.module.js.map +1 -1
- package/dist/api/graphql-api/graphql-ws/run-progress-subscription-poller.service.js +0 -1
- package/dist/api/graphql-api/graphql-ws/run-progress-subscription-poller.service.js.map +1 -1
- package/dist/api/graphql-api/graphql-ws/subscription-bridge.service.js +0 -1
- 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 -0
- package/dist/api/graphql-api/graphql-ws/subscription-mappers.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/pr/model/pr-readiness.model.js +26 -0
- package/dist/api/graphql-api/pr/model/pr-readiness.model.js.map +1 -1
- package/dist/api/graphql-api/runs/inputs/create-run.input.js +6 -0
- package/dist/api/graphql-api/runs/inputs/create-run.input.js.map +1 -1
- package/dist/api/graphql-api/runs/model/run.model.js +6 -0
- package/dist/api/graphql-api/runs/model/run.model.js.map +1 -1
- package/dist/api/graphql-api/share/model/issue-ref.model.js +53 -0
- package/dist/api/graphql-api/share/model/issue-ref.model.js.map +1 -0
- package/dist/cli/commands/doctor-report.js +0 -18
- package/dist/cli/commands/doctor-report.js.map +1 -1
- package/dist/cli/commands/lifecycle.js +3 -71
- package/dist/cli/commands/lifecycle.js.map +1 -1
- package/dist/cli/commands/mcp.js +0 -14
- package/dist/cli/commands/mcp.js.map +1 -1
- package/dist/cli/commands/process-tree.js +4 -26
- package/dist/cli/commands/process-tree.js.map +1 -1
- package/dist/cli/commands/revisium-helpers.js +0 -6
- package/dist/cli/commands/revisium-helpers.js.map +1 -1
- package/dist/cli/commands/rogue-reaper.js +0 -34
- package/dist/cli/commands/rogue-reaper.js.map +1 -1
- package/dist/cli/index.js +0 -4
- package/dist/cli/index.js.map +1 -1
- package/dist/cli/program.js +0 -5
- package/dist/cli/program.js.map +1 -1
- package/dist/config.js +0 -25
- package/dist/config.js.map +1 -1
- package/dist/control-plane/bootstrap.js +12 -27
- package/dist/control-plane/bootstrap.js.map +1 -1
- package/dist/control-plane/change-notifications.js +0 -1
- package/dist/control-plane/change-notifications.js.map +1 -1
- package/dist/control-plane/client-transport.js +1 -12
- package/dist/control-plane/client-transport.js.map +1 -1
- package/dist/control-plane/default-playbook-policy.js +593 -0
- package/dist/control-plane/default-playbook-policy.js.map +1 -0
- package/dist/control-plane/definitions.js +8 -17
- package/dist/control-plane/definitions.js.map +1 -1
- package/dist/control-plane/inbox.js +2 -120
- package/dist/control-plane/inbox.js.map +1 -1
- package/dist/control-plane/resolve-cwd.js +0 -61
- package/dist/control-plane/resolve-cwd.js.map +1 -1
- package/dist/control-plane/schema-migration.js +0 -9
- package/dist/control-plane/schema-migration.js.map +1 -1
- package/dist/control-plane/seed-default-playbook.js +50 -46
- package/dist/control-plane/seed-default-playbook.js.map +1 -1
- package/dist/control-plane/steps.js +0 -15
- package/dist/control-plane/steps.js.map +1 -1
- package/dist/e2e/kit/agents.js +15 -21
- package/dist/e2e/kit/agents.js.map +1 -1
- package/dist/e2e/kit/assertions.js +12 -8
- package/dist/e2e/kit/assertions.js.map +1 -1
- package/dist/e2e/kit/fake-integrator.js +11 -2
- package/dist/e2e/kit/fake-integrator.js.map +1 -1
- package/dist/e2e/kit/git-target-repo.js +1 -1
- package/dist/engine/dbos.service.js +12 -179
- package/dist/engine/dbos.service.js.map +1 -1
- package/dist/engine/ensure-postgres.js +0 -48
- package/dist/engine/ensure-postgres.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/handlers/runs-query.handlers.js +1 -0
- package/dist/features/runs/queries/handlers/runs-query.handlers.js.map +1 -1
- package/dist/host/daemon.js +2 -21
- package/dist/host/daemon.js.map +1 -1
- package/dist/host/dbos-identity.js +0 -31
- package/dist/host/dbos-identity.js.map +1 -1
- package/dist/host/ensure-host.js +1 -37
- package/dist/host/ensure-host.js.map +1 -1
- package/dist/host/ensure-revisium.js +1 -135
- package/dist/host/ensure-revisium.js.map +1 -1
- package/dist/host/host-runtime.js +0 -28
- package/dist/host/host-runtime.js.map +1 -1
- package/dist/host/host.lifecycle.js +0 -27
- package/dist/host/host.lifecycle.js.map +1 -1
- package/dist/host/queue-ownership.js +0 -42
- package/dist/host/queue-ownership.js.map +1 -1
- package/dist/host/queue-poller-census.js +1 -28
- package/dist/host/queue-poller-census.js.map +1 -1
- package/dist/http/graphql-host.module.js +0 -3
- package/dist/http/graphql-host.module.js.map +1 -1
- package/dist/mcp/mcp-capabilities.js +23 -5
- package/dist/mcp/mcp-capabilities.js.map +1 -1
- package/dist/mcp/mcp-facade.service.js +335 -22
- package/dist/mcp/mcp-facade.service.js.map +1 -1
- package/dist/mcp/mcp-http.service.js +0 -18
- package/dist/mcp/mcp-http.service.js.map +1 -1
- package/dist/mcp/mcp-tools.js +41 -36
- package/dist/mcp/mcp-tools.js.map +1 -1
- package/dist/observability/activity-signal.js +103 -0
- package/dist/observability/activity-signal.js.map +1 -0
- package/dist/observability/agent-activity-reporter.js +0 -4
- package/dist/observability/agent-activity-reporter.js.map +1 -1
- package/dist/observability/index.js +1 -0
- package/dist/observability/index.js.map +1 -1
- package/dist/pipeline/await-human.js +0 -40
- package/dist/pipeline/await-human.js.map +1 -1
- package/dist/pipeline/data-driven-task.workflow.js +563 -251
- package/dist/pipeline/data-driven-task.workflow.js.map +1 -1
- package/dist/pipeline/data-driven-template.js +0 -25
- package/dist/pipeline/data-driven-template.js.map +1 -1
- package/dist/pipeline/pipeline.module.js +0 -12
- package/dist/pipeline/pipeline.module.js.map +1 -1
- package/dist/pipeline/pipeline.service.js +194 -189
- package/dist/pipeline/pipeline.service.js.map +1 -1
- package/dist/pipeline/route-contract.js +4 -3
- package/dist/pipeline/route-contract.js.map +1 -1
- package/dist/pipeline-core/index.js +1 -13
- package/dist/pipeline-core/index.js.map +1 -1
- package/dist/pipeline-core/interpret.js +21 -94
- package/dist/pipeline-core/interpret.js.map +1 -1
- package/dist/pipeline-core/kit/assertions.js +0 -19
- package/dist/pipeline-core/kit/assertions.js.map +1 -1
- package/dist/pipeline-core/kit/builders.js +1 -21
- package/dist/pipeline-core/kit/builders.js.map +1 -1
- package/dist/pipeline-core/kit/drive.js +1 -31
- package/dist/pipeline-core/kit/drive.js.map +1 -1
- package/dist/pipeline-core/kit/fixtures.js +78 -101
- package/dist/pipeline-core/kit/fixtures.js.map +1 -1
- package/dist/pipeline-core/kit/index.js +0 -8
- package/dist/pipeline-core/kit/index.js.map +1 -1
- package/dist/pipeline-core/types.js +13 -52
- package/dist/pipeline-core/types.js.map +1 -1
- package/dist/pipeline-core/validate-capability.js +0 -3
- package/dist/pipeline-core/validate-capability.js.map +1 -1
- package/dist/pipeline-core/validate-conflicts.js +0 -9
- package/dist/pipeline-core/validate-conflicts.js.map +1 -1
- package/dist/pipeline-core/validate-dataflow.js +0 -17
- package/dist/pipeline-core/validate-dataflow.js.map +1 -1
- package/dist/pipeline-core/validate-diff.js +3 -36
- package/dist/pipeline-core/validate-diff.js.map +1 -1
- package/dist/pipeline-core/validate-edges.js +0 -6
- package/dist/pipeline-core/validate-edges.js.map +1 -1
- package/dist/pipeline-core/validate-graph.js +0 -11
- package/dist/pipeline-core/validate-graph.js.map +1 -1
- package/dist/pipeline-core/validate-loops.js +37 -44
- package/dist/pipeline-core/validate-loops.js.map +1 -1
- package/dist/pipeline-core/validate-parallel.js +9 -15
- package/dist/pipeline-core/validate-parallel.js.map +1 -1
- package/dist/pipeline-core/validate-sink.js +0 -7
- package/dist/pipeline-core/validate-sink.js.map +1 -1
- package/dist/pipeline-core/validate-topology.js +1 -20
- package/dist/pipeline-core/validate-topology.js.map +1 -1
- package/dist/pipeline-core/validate-verdict.js +52 -7
- package/dist/pipeline-core/validate-verdict.js.map +1 -1
- package/dist/pipeline-core/validate.js +5 -29
- package/dist/pipeline-core/validate.js.map +1 -1
- package/dist/playbook/catalog-loader.js +8 -3
- package/dist/playbook/catalog-loader.js.map +1 -1
- package/dist/playbook/import-mapper.js +4 -5
- package/dist/playbook/import-mapper.js.map +1 -1
- package/dist/poller/pr-readiness-core.js +226 -29
- package/dist/poller/pr-readiness-core.js.map +1 -1
- package/dist/revisium/inbox.service.js +0 -24
- package/dist/revisium/inbox.service.js.map +1 -1
- package/dist/revisium/playbooks.service.js +15 -2
- package/dist/revisium/playbooks.service.js.map +1 -1
- package/dist/revisium/revisium.module.js +0 -12
- package/dist/revisium/revisium.module.js.map +1 -1
- package/dist/revisium/roles.service.js +0 -6
- package/dist/revisium/roles.service.js.map +1 -1
- package/dist/revisium/run.service.js +2 -75
- package/dist/revisium/run.service.js.map +1 -1
- package/dist/revisium/tokens.js +0 -4
- package/dist/revisium/tokens.js.map +1 -1
- package/dist/run/append-event.js +6 -37
- package/dist/run/append-event.js.map +1 -1
- package/dist/run/block-run.js.map +1 -1
- package/dist/run/cancel-run.js +0 -33
- package/dist/run/cancel-run.js.map +1 -1
- package/dist/run/complete-run.js.map +1 -1
- package/dist/run/create-run.js +5 -8
- package/dist/run/create-run.js.map +1 -1
- package/dist/run/fail-run.js +0 -3
- package/dist/run/fail-run.js.map +1 -1
- package/dist/run/inspect-run.js +10 -29
- package/dist/run/inspect-run.js.map +1 -1
- package/dist/run/issue-ref.js +88 -0
- package/dist/run/issue-ref.js.map +1 -0
- package/dist/run/run-outputs.js +0 -19
- package/dist/run/run-outputs.js.map +1 -1
- package/dist/run/terminal-run-status.js +0 -12
- package/dist/run/terminal-run-status.js.map +1 -1
- package/dist/runners/claude-code.service.js +2 -13
- package/dist/runners/claude-code.service.js.map +1 -1
- package/dist/runners/codex.service.js +2 -11
- package/dist/runners/codex.service.js.map +1 -1
- package/dist/runners/gh-identity.js +0 -52
- package/dist/runners/gh-identity.js.map +1 -1
- package/dist/runners/integrator-branch-naming.js +16 -19
- package/dist/runners/integrator-branch-naming.js.map +1 -1
- package/dist/runners/integrator-git.js +0 -25
- package/dist/runners/integrator-git.js.map +1 -1
- package/dist/runners/integrator-remote.js +0 -2
- package/dist/runners/integrator-remote.js.map +1 -1
- package/dist/runners/integrator-types.js +0 -7
- package/dist/runners/integrator-types.js.map +1 -1
- package/dist/runners/integrator.js +231 -230
- package/dist/runners/integrator.js.map +1 -1
- package/dist/runners/runner.module.js +0 -6
- package/dist/runners/runner.module.js.map +1 -1
- package/dist/runners/tokens.js +0 -4
- package/dist/runners/tokens.js.map +1 -1
- package/dist/runners/worktree.service.js +3 -19
- package/dist/runners/worktree.service.js.map +1 -1
- package/dist/task-control-plane/pr-readiness.service.js +15 -13
- package/dist/task-control-plane/pr-readiness.service.js.map +1 -1
- package/dist/task-control-plane/run-watch.service.js +170 -63
- package/dist/task-control-plane/run-watch.service.js.map +1 -1
- package/dist/task-control-plane/task-control-plane-api.service.js +285 -40
- package/dist/task-control-plane/task-control-plane-api.service.js.map +1 -1
- package/dist/worker/artifact-store.js +21 -3
- package/dist/worker/artifact-store.js.map +1 -1
- package/dist/worker/build-context.js +10 -10
- package/dist/worker/build-context.js.map +1 -1
- package/dist/worker/claude-code-runner.js +76 -48
- package/dist/worker/claude-code-runner.js.map +1 -1
- package/dist/worker/codex-runner.js +128 -35
- package/dist/worker/codex-runner.js.map +1 -1
- package/dist/worker/git-worktree-manager.js +20 -67
- package/dist/worker/git-worktree-manager.js.map +1 -1
- package/dist/worker/process-executor.js +144 -27
- package/dist/worker/process-executor.js.map +1 -1
- package/dist/worker/result-envelope.js +33 -49
- package/dist/worker/result-envelope.js.map +1 -1
- package/dist/worker/runner-common.js +29 -0
- package/dist/worker/runner-common.js.map +1 -1
- package/dist/worker/runner-dispatch.js +0 -3
- package/dist/worker/runner-dispatch.js.map +1 -1
- package/dist/worker/runner.js +16 -1
- package/dist/worker/runner.js.map +1 -1
- package/dist/worker/stub-runner.js +0 -7
- package/dist/worker/stub-runner.js.map +1 -1
- package/package.json +2 -2
|
@@ -10,35 +10,35 @@ var __metadata = (this && this.__metadata) || function (k, v) {
|
|
|
10
10
|
var __param = (this && this.__param) || function (paramIndex, decorator) {
|
|
11
11
|
return function (target, key) { decorator(target, key, paramIndex); }
|
|
12
12
|
};
|
|
13
|
-
/**
|
|
14
|
-
* integrator.ts — deterministic, replay-safe code integrator.
|
|
15
|
-
*
|
|
16
|
-
* DBOS-SEALED: zero @dbos-inc imports. Registration happens in PipelineService ctor.
|
|
17
|
-
*
|
|
18
|
-
* Exposes:
|
|
19
|
-
* - integrate(input, deps) — REAL integrator (live only); git/gh side effects; resumable.
|
|
20
|
-
* - stubIntegrate(input) — STUB (script only); ZERO external effects; pure + deterministic.
|
|
21
|
-
* - preflightLive(taskId, base, deps) — LIVE PREFLIGHT; clean check + base freshness; one-shot.
|
|
22
|
-
* - IntegratorService — @Injectable wrapper with bound arrow properties.
|
|
23
|
-
*
|
|
24
|
-
* The pure primitives (resolveExecutable, branchName, parseOwnerRepo) and shared types live in
|
|
25
|
-
* sibling modules (integrator-git / -branch-naming / -remote / -types) and are re-exported here so
|
|
26
|
-
* the public import path ('./integrator.js') is unchanged.
|
|
27
|
-
*/
|
|
28
13
|
import { Inject, Injectable } from '@nestjs/common';
|
|
29
14
|
import { execFileSync } from 'node:child_process';
|
|
30
|
-
import { collectPrReadiness } from '../poller/pr-readiness-core.js';
|
|
15
|
+
import { collectPrReadiness, fetchRequiredCheckNames, } from '../poller/pr-readiness-core.js';
|
|
31
16
|
import { RunService } from '../revisium/run.service.js';
|
|
17
|
+
import { issueRefTag } from '../run/issue-ref.js';
|
|
32
18
|
import { resolveGhAccount, resolvePinnedGh } from './gh-identity.js';
|
|
33
19
|
import { gitAbsPath, branchExists, countAhead } from './integrator-git.js';
|
|
34
20
|
import { resolveOwnerRepo } from './integrator-remote.js';
|
|
35
21
|
import { branchName } from './integrator-branch-naming.js';
|
|
36
|
-
// Public-API re-exports — the implementations now live in focused sibling modules, but callers
|
|
37
|
-
// (worktree.service, git-worktree-manager, the test kit) keep importing from './integrator.js'.
|
|
38
22
|
export { resolveExecutable } from './integrator-git.js';
|
|
39
23
|
export { branchName };
|
|
40
24
|
export { parseOwnerRepo } from './integrator-remote.js';
|
|
41
|
-
function
|
|
25
|
+
function issueBoundTitle(title, issueRef, ownerRepo) {
|
|
26
|
+
const tag = issueRefTag(issueRef, ownerRepo);
|
|
27
|
+
return tag ? `${tag} ${title}` : title;
|
|
28
|
+
}
|
|
29
|
+
function commitMessage(title, issueRef, ownerRepo) {
|
|
30
|
+
const tag = issueRefTag(issueRef, ownerRepo);
|
|
31
|
+
return tag ? `feat: ${tag} ${title}` : `feat: ${title}`;
|
|
32
|
+
}
|
|
33
|
+
function parsePrList(raw) {
|
|
34
|
+
try {
|
|
35
|
+
return JSON.parse(raw);
|
|
36
|
+
}
|
|
37
|
+
catch {
|
|
38
|
+
throw new Error(`gh pr list returned non-JSON: ${raw.slice(0, 200)}`);
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
function matchingOpenPr(ownerRepo, branch, base, execGh, jsonFields) {
|
|
42
42
|
const raw = execGh([
|
|
43
43
|
'pr',
|
|
44
44
|
'list',
|
|
@@ -49,21 +49,14 @@ function findOrCreatePr(ownerRepo, branch, base, title, execGh) {
|
|
|
49
49
|
'--state',
|
|
50
50
|
'open',
|
|
51
51
|
'--json',
|
|
52
|
-
|
|
52
|
+
jsonFields,
|
|
53
53
|
]);
|
|
54
|
-
|
|
55
|
-
try {
|
|
56
|
-
entries = JSON.parse(raw);
|
|
57
|
-
}
|
|
58
|
-
catch {
|
|
59
|
-
throw new Error(`gh pr list returned non-JSON: ${raw.slice(0, 200)}`);
|
|
60
|
-
}
|
|
61
|
-
const matching = entries.filter((p) => p.baseRefName === base);
|
|
54
|
+
const matching = parsePrList(raw).filter((p) => p.baseRefName === base);
|
|
62
55
|
if (matching.length === 1) {
|
|
63
56
|
const pr = matching[0];
|
|
64
57
|
if (!pr)
|
|
65
58
|
throw new Error('unexpected empty match');
|
|
66
|
-
return { prUrl: pr.url, prNumber: pr.number };
|
|
59
|
+
return { prUrl: pr.url, prNumber: pr.number, ...(pr.headRefOid ? { headSha: pr.headRefOid } : {}) };
|
|
67
60
|
}
|
|
68
61
|
if (matching.length > 1) {
|
|
69
62
|
const candidates = matching.map((p) => `#${p.number}`).join(', ');
|
|
@@ -72,7 +65,9 @@ function findOrCreatePr(ownerRepo, branch, base, title, execGh) {
|
|
|
72
65
|
lesson: `Ambiguous: ${matching.length} open PRs for branch "${branch}" targeting "${base}" in ${ownerRepo} — candidates ${candidates} — manual review needed`,
|
|
73
66
|
};
|
|
74
67
|
}
|
|
75
|
-
|
|
68
|
+
return null;
|
|
69
|
+
}
|
|
70
|
+
function createPr(ownerRepo, branch, base, title, issueRef, execGh) {
|
|
76
71
|
const createOut = execGh([
|
|
77
72
|
'pr',
|
|
78
73
|
'create',
|
|
@@ -84,13 +79,11 @@ function findOrCreatePr(ownerRepo, branch, base, title, execGh) {
|
|
|
84
79
|
'--head',
|
|
85
80
|
branch,
|
|
86
81
|
'--title',
|
|
87
|
-
title,
|
|
82
|
+
issueBoundTitle(title, issueRef, ownerRepo),
|
|
88
83
|
'--body',
|
|
89
84
|
'',
|
|
90
85
|
]);
|
|
91
|
-
// gh pr create prints the PR url on stdout
|
|
92
86
|
const createdUrl = createOut.trim();
|
|
93
|
-
// Fetch back number from the URL
|
|
94
87
|
const viewRaw = execGh([
|
|
95
88
|
'pr',
|
|
96
89
|
'view',
|
|
@@ -105,8 +98,6 @@ function findOrCreatePr(ownerRepo, branch, base, title, execGh) {
|
|
|
105
98
|
viewData = JSON.parse(viewRaw);
|
|
106
99
|
}
|
|
107
100
|
catch {
|
|
108
|
-
// gh pr view returned non-JSON after create — cannot determine real PR url/number.
|
|
109
|
-
// A live integrator must NEVER emit a stub:// url; surface as needsHuman for human review.
|
|
110
101
|
return {
|
|
111
102
|
needsHuman: true,
|
|
112
103
|
lesson: `gh pr view returned non-JSON after create (url=${createdUrl || 'empty'}); ` +
|
|
@@ -115,23 +106,21 @@ function findOrCreatePr(ownerRepo, branch, base, title, execGh) {
|
|
|
115
106
|
}
|
|
116
107
|
return { prUrl: viewData.url, prNumber: viewData.number };
|
|
117
108
|
}
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
*/
|
|
109
|
+
function findOrCreatePr(ownerRepo, branch, base, title, issueRef, execGh) {
|
|
110
|
+
const existing = matchingOpenPr(ownerRepo, branch, base, execGh, 'number,url,baseRefName');
|
|
111
|
+
if (existing && !('needsHuman' in existing)) {
|
|
112
|
+
return { prUrl: existing.prUrl, prNumber: existing.prNumber };
|
|
113
|
+
}
|
|
114
|
+
if (existing)
|
|
115
|
+
return existing;
|
|
116
|
+
return createPr(ownerRepo, branch, base, title, issueRef, execGh);
|
|
117
|
+
}
|
|
118
|
+
function findExistingPrWithHead(ownerRepo, branch, base, execGh) {
|
|
119
|
+
return matchingOpenPr(ownerRepo, branch, base, execGh, 'number,url,baseRefName,headRefOid');
|
|
120
|
+
}
|
|
131
121
|
export async function preflightLive(taskId, base, deps) {
|
|
132
122
|
const { execGit, resolveTaskCwd } = deps;
|
|
133
123
|
const cwd = await resolveTaskCwd(taskId);
|
|
134
|
-
// 1. Fetch (only mutation; idempotent; ignore errors — fetch failure handled as mismatch)
|
|
135
124
|
try {
|
|
136
125
|
execGit(['fetch', 'origin', base], cwd);
|
|
137
126
|
}
|
|
@@ -141,7 +130,6 @@ export async function preflightLive(taskId, base, deps) {
|
|
|
141
130
|
lesson: `live preflight: git fetch origin ${base} failed — base branch may not exist on remote: ${String(err)}`,
|
|
142
131
|
};
|
|
143
132
|
}
|
|
144
|
-
// 2. Clean check
|
|
145
133
|
const porcelain = execGit(['status', '--porcelain'], cwd).trim();
|
|
146
134
|
if (porcelain !== '') {
|
|
147
135
|
const lineCount = porcelain.split('\n').length;
|
|
@@ -150,9 +138,6 @@ export async function preflightLive(taskId, base, deps) {
|
|
|
150
138
|
lesson: `target repo ${cwd} is not clean (${lineCount} uncommitted change${lineCount === 1 ? '' : 's'}); commit/stash and retry --live`,
|
|
151
139
|
};
|
|
152
140
|
}
|
|
153
|
-
// 3. Base freshness:
|
|
154
|
-
// - base branch itself must exactly match origin/<base>
|
|
155
|
-
// - feature branches are valid when origin/<base> is an ancestor of HEAD
|
|
156
141
|
let headBranch;
|
|
157
142
|
let headSha;
|
|
158
143
|
let originSha;
|
|
@@ -164,150 +149,176 @@ export async function preflightLive(taskId, base, deps) {
|
|
|
164
149
|
catch (err) {
|
|
165
150
|
return {
|
|
166
151
|
needsHuman: true,
|
|
167
|
-
lesson: `live preflight: cannot
|
|
152
|
+
lesson: `live preflight: cannot resolve origin/${base} after fetch — ${String(err)}`,
|
|
168
153
|
};
|
|
169
154
|
}
|
|
170
155
|
if (headBranch === base && headSha !== originSha) {
|
|
171
|
-
// The clean base branch isn't at origin/<base>. SELF-HEAL instead of dead-ending: a sibling run
|
|
172
|
-
// merging advances origin/<base>, and the old hard block forced a manual `git pull` + re-run for
|
|
173
|
-
// every other in-flight run (slice 142 / dogfood). The tree is already clean (step 2), so if the
|
|
174
|
-
// base is simply BEHIND (HEAD is an ancestor of origin/<base>) we fast-forward it — exactly what
|
|
175
|
-
// `git pull --ff-only` does, and harmless for worktree-isolated runs (they re-cut from origin anyway).
|
|
176
|
-
// Only a genuinely DIVERGED base (local commits absent from the remote) still blocks for a human.
|
|
177
156
|
let behind = false;
|
|
178
157
|
try {
|
|
179
158
|
execGit(['merge-base', '--is-ancestor', 'HEAD', `origin/${base}`], cwd);
|
|
180
159
|
behind = true;
|
|
181
160
|
}
|
|
182
161
|
catch {
|
|
183
|
-
behind = false;
|
|
162
|
+
behind = false;
|
|
184
163
|
}
|
|
185
164
|
if (!behind) {
|
|
186
165
|
return {
|
|
187
166
|
needsHuman: true,
|
|
188
|
-
lesson: `target repo base branch ${base} has
|
|
167
|
+
lesson: `target repo base branch ${base} has local-only or diverged commits relative to origin/${base} ` +
|
|
189
168
|
`(HEAD=${headSha.slice(0, 8)} has local commits absent from origin/${base}@${originSha.slice(0, 8)}); ` +
|
|
190
169
|
`reconcile manually, then retry --live`,
|
|
191
170
|
};
|
|
192
171
|
}
|
|
193
|
-
try {
|
|
194
|
-
execGit(['merge', '--ff-only', `origin/${base}`], cwd); // clean + ancestor → cannot conflict
|
|
195
|
-
}
|
|
196
|
-
catch (err) {
|
|
197
|
-
return {
|
|
198
|
-
needsHuman: true,
|
|
199
|
-
lesson: `live preflight: fast-forward of ${base} to origin/${base} failed — ${String(err)}; pull ${base} manually and retry --live`,
|
|
200
|
-
};
|
|
201
|
-
}
|
|
202
|
-
}
|
|
203
|
-
if (headBranch !== base) {
|
|
204
|
-
try {
|
|
205
|
-
execGit(['merge-base', '--is-ancestor', `origin/${base}`, 'HEAD'], cwd);
|
|
206
|
-
}
|
|
207
|
-
catch {
|
|
208
|
-
return {
|
|
209
|
-
needsHuman: true,
|
|
210
|
-
lesson: `target repo branch is not based on fresh origin/${base} ` +
|
|
211
|
-
`(HEAD=${headBranch}@${headSha.slice(0, 8)}, expected origin/${base}@${originSha.slice(0, 8)} as ancestor); ` +
|
|
212
|
-
`rebase or merge origin/${base}, then retry --live`,
|
|
213
|
-
};
|
|
214
|
-
}
|
|
215
172
|
}
|
|
216
173
|
return { ok: true };
|
|
217
174
|
}
|
|
218
|
-
// ─── STUB integrator (script only) ───────────────────────────────────────────
|
|
219
|
-
/**
|
|
220
|
-
* stubIntegrate — PURE, zero side effects; returns a placeholder result.
|
|
221
|
-
* Used by `script` mode (default + --stub). Makes NO execGit/execGh calls.
|
|
222
|
-
*/
|
|
223
175
|
export function stubIntegrate(input) {
|
|
224
176
|
return {
|
|
225
177
|
prUrl: 'stub://pr/placeholder',
|
|
226
178
|
branch: `feat/${input.taskId}-stub`,
|
|
227
179
|
prNumber: 0,
|
|
180
|
+
...(input.issueRef ? { issueRef: input.issueRef } : {}),
|
|
181
|
+
};
|
|
182
|
+
}
|
|
183
|
+
export async function captureProducedChange(input, deps) {
|
|
184
|
+
const { execGit: git, resolveRunCwd } = deps;
|
|
185
|
+
const cwd = await resolveRunCwd(input.runId, input.taskId);
|
|
186
|
+
const branch = branchName(input.taskId, input.title, input.issueRef);
|
|
187
|
+
const ownerRepoResult = resolveOwnerRepo(git, cwd);
|
|
188
|
+
const ownerRepo = 'needsHuman' in ownerRepoResult ? undefined : ownerRepoResult.ownerRepo;
|
|
189
|
+
if (branchExists(git, cwd, branch)) {
|
|
190
|
+
git(['switch', branch], cwd);
|
|
191
|
+
}
|
|
192
|
+
else {
|
|
193
|
+
git(['switch', '-c', branch], cwd);
|
|
194
|
+
}
|
|
195
|
+
git(['add', '-A'], cwd);
|
|
196
|
+
if (stagedDiffPresent(git, cwd)) {
|
|
197
|
+
git(['commit', '-m', commitMessage(input.title, input.issueRef, ownerRepo)], cwd);
|
|
198
|
+
}
|
|
199
|
+
const headSha = git(['rev-parse', 'HEAD'], cwd).trim();
|
|
200
|
+
return {
|
|
201
|
+
branch,
|
|
202
|
+
headSha,
|
|
203
|
+
...(input.issueRef ? { issueRef: input.issueRef } : {}),
|
|
204
|
+
worktreePath: cwd,
|
|
205
|
+
...(input.artifactRef ? { artifactRef: input.artifactRef } : {}),
|
|
228
206
|
};
|
|
229
207
|
}
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
208
|
+
function stagedDiffPresent(git, cwd) {
|
|
209
|
+
try {
|
|
210
|
+
git(['diff', '--cached', '--quiet'], cwd);
|
|
211
|
+
return false;
|
|
212
|
+
}
|
|
213
|
+
catch {
|
|
214
|
+
return true;
|
|
215
|
+
}
|
|
216
|
+
}
|
|
236
217
|
export async function integrate(input, deps) {
|
|
218
|
+
if (input.change)
|
|
219
|
+
return integrateProducedChange(input, deps, input.change);
|
|
237
220
|
const { execGit: git, execGh: gh, resolveRunCwd } = deps;
|
|
238
|
-
// Resolve the run's ISOLATED worktree (plan 0017) — it is already checked out on `branch`, so the
|
|
239
|
-
// branchExists→switch path below is a no-op and the dirty-tree `switch -c origin/<base>` (which
|
|
240
|
-
// failed when the base checkout carried prior-run changes) is never taken.
|
|
241
221
|
const cwd = await resolveRunCwd(input.runId, input.taskId);
|
|
242
|
-
const branch = branchName(input.taskId, input.title);
|
|
243
|
-
// 1. Derive owner/repo
|
|
222
|
+
const branch = branchName(input.taskId, input.title, input.issueRef);
|
|
244
223
|
const ownerRepoResult = resolveOwnerRepo(git, cwd);
|
|
245
224
|
if ('needsHuman' in ownerRepoResult)
|
|
246
225
|
return ownerRepoResult;
|
|
247
226
|
const { ownerRepo } = ownerRepoResult;
|
|
248
|
-
// 2. Fetch base
|
|
249
227
|
git(['fetch', 'origin', input.base], cwd);
|
|
250
|
-
// 3. Branch create-if-absent; never clobber a prior commit
|
|
251
228
|
if (branchExists(git, cwd, branch)) {
|
|
252
229
|
git(['switch', branch], cwd);
|
|
253
230
|
}
|
|
254
231
|
else {
|
|
255
232
|
git(['switch', '-c', branch, `origin/${input.base}`], cwd);
|
|
256
233
|
}
|
|
257
|
-
// 4. Stage all changes (safe: clean-repo precondition from preflight)
|
|
258
234
|
git(['add', '-A'], cwd);
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
try {
|
|
262
|
-
git(['diff', '--cached', '--quiet'], cwd);
|
|
263
|
-
hasStagedDiff = false; // exit 0 → nothing staged
|
|
264
|
-
}
|
|
265
|
-
catch {
|
|
266
|
-
hasStagedDiff = true; // exit 1 → staged diff present
|
|
267
|
-
}
|
|
268
|
-
if (hasStagedDiff) {
|
|
269
|
-
// Commit — NO Co-Authored-By, NO summary footer (MEMORY)
|
|
270
|
-
const commitMsg = `feat: ${input.title}`;
|
|
235
|
+
if (stagedDiffPresent(git, cwd)) {
|
|
236
|
+
const commitMsg = commitMessage(input.title, input.issueRef, ownerRepo);
|
|
271
237
|
git(['commit', '-m', commitMsg], cwd);
|
|
272
238
|
}
|
|
273
239
|
else {
|
|
274
|
-
// No staged diff — check if branch is ahead of origin/<base>
|
|
275
240
|
const ahead = countAhead(git, cwd, branch, input.base);
|
|
276
241
|
if (ahead === 0) {
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
242
|
+
let lesson = 'nothing to integrate — no staged changes and branch is not ahead of origin/' + input.base;
|
|
243
|
+
try {
|
|
244
|
+
const baseCwd = await deps.resolveTaskCwd(input.taskId);
|
|
245
|
+
const basePorcelain = git(['status', '--porcelain'], baseCwd).trim();
|
|
246
|
+
if (basePorcelain !== '') {
|
|
247
|
+
lesson =
|
|
248
|
+
`developer produced changes but the run's worktree is empty — they appear to have been ` +
|
|
249
|
+
`written OUTSIDE the worktree (the base checkout ${baseCwd} is dirty); see slice 143. ` +
|
|
250
|
+
`Re-run; the agent must write under its cwd / $REVO_WORKTREE_PATH.`;
|
|
251
|
+
}
|
|
252
|
+
}
|
|
253
|
+
catch {
|
|
254
|
+
}
|
|
255
|
+
return { needsHuman: true, lesson };
|
|
282
256
|
}
|
|
283
|
-
// Ahead but no staged diff → commit happened on a prior attempt; fall through to push
|
|
284
257
|
}
|
|
285
|
-
// 6. Push (idempotent — no force)
|
|
286
258
|
git(['push', '-u', 'origin', branch], cwd);
|
|
287
|
-
|
|
288
|
-
const prResult = findOrCreatePr(ownerRepo, branch, input.base, input.title, gh);
|
|
259
|
+
const prResult = findOrCreatePr(ownerRepo, branch, input.base, input.title, input.issueRef, gh);
|
|
289
260
|
if ('needsHuman' in prResult)
|
|
290
261
|
return prResult;
|
|
291
|
-
return { prUrl: prResult.prUrl, branch, prNumber: prResult.prNumber };
|
|
262
|
+
return { prUrl: prResult.prUrl, branch, prNumber: prResult.prNumber, ...(input.issueRef ? { issueRef: input.issueRef } : {}) };
|
|
263
|
+
}
|
|
264
|
+
async function integrateProducedChange(input, deps, change) {
|
|
265
|
+
const { execGit: git, execGh: gh } = deps;
|
|
266
|
+
const cwd = change.worktreePath ?? await deps.resolveRunCwd(input.runId, input.taskId);
|
|
267
|
+
const branch = change.branch;
|
|
268
|
+
const issueRef = change.issueRef ?? input.issueRef;
|
|
269
|
+
const ownerRepoResult = resolveOwnerRepo(git, cwd);
|
|
270
|
+
if ('needsHuman' in ownerRepoResult)
|
|
271
|
+
return ownerRepoResult;
|
|
272
|
+
const { ownerRepo } = ownerRepoResult;
|
|
273
|
+
git(['fetch', 'origin', input.base], cwd);
|
|
274
|
+
const existing = findExistingPrWithHead(ownerRepo, branch, input.base, gh);
|
|
275
|
+
if (existing && 'needsHuman' in existing)
|
|
276
|
+
return existing;
|
|
277
|
+
if (existing?.headSha === change.headSha) {
|
|
278
|
+
return {
|
|
279
|
+
prUrl: existing.prUrl,
|
|
280
|
+
branch,
|
|
281
|
+
prNumber: existing.prNumber,
|
|
282
|
+
...(issueRef ? { issueRef } : {}),
|
|
283
|
+
headSha: change.headSha,
|
|
284
|
+
status: 'noop',
|
|
285
|
+
message: 'nothing to integrate — produced head already pushed and equals PR head',
|
|
286
|
+
};
|
|
287
|
+
}
|
|
288
|
+
if (!existing && countAhead(git, cwd, change.headSha, input.base) === 0) {
|
|
289
|
+
return {
|
|
290
|
+
needsHuman: true,
|
|
291
|
+
lesson: `nothing to integrate — produced head ${change.headSha.slice(0, 8)} is not ahead of ` +
|
|
292
|
+
`origin/${input.base} and no open PR exists`,
|
|
293
|
+
};
|
|
294
|
+
}
|
|
295
|
+
git(['push', 'origin', `${change.headSha}:refs/heads/${branch}`], cwd);
|
|
296
|
+
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
|
+
};
|
|
305
|
+
}
|
|
306
|
+
const created = createPr(ownerRepo, branch, input.base, input.title, issueRef, gh);
|
|
307
|
+
if ('needsHuman' in created)
|
|
308
|
+
return created;
|
|
309
|
+
return {
|
|
310
|
+
prUrl: created.prUrl,
|
|
311
|
+
branch,
|
|
312
|
+
prNumber: created.prNumber,
|
|
313
|
+
...(issueRef ? { issueRef } : {}),
|
|
314
|
+
headSha: change.headSha,
|
|
315
|
+
status: 'pushed',
|
|
316
|
+
};
|
|
292
317
|
}
|
|
293
|
-
/**
|
|
294
|
-
* confirmMerge — REAL (live only). Ensures the run's PR is actually merged before the run reaches its
|
|
295
|
-
* success terminal (so the worktree, released on `succeeded`, is cleaned only once the branch is in the
|
|
296
|
-
* base — truly disposable). Idempotent + replay-safe:
|
|
297
|
-
*
|
|
298
|
-
* 1. `gh pr view` the run's branch. Already `merged` (a human merged it) → succeed.
|
|
299
|
-
* 2. Not merged + OPEN + `mergeStateStatus === CLEAN` (CI green, no conflicts) → `gh pr merge --squash
|
|
300
|
-
* --delete-branch`, then re-view to CONFIRM merged → succeed; otherwise block.
|
|
301
|
-
* 3. Not mergeable (not OPEN, or not CLEAN — red CI / conflicts / blocked) → block (needsHuman), which
|
|
302
|
-
* routes to a `blocked` terminal and KEEPS the worktree for rework.
|
|
303
|
-
*
|
|
304
|
-
* Merge method is `--squash` by default; node-level parameterization (method/policy) is a deferred
|
|
305
|
-
* follow-up (run-time node params).
|
|
306
|
-
*/
|
|
307
318
|
export async function confirmMerge(input, deps) {
|
|
308
319
|
const { execGit: git, execGh: gh, resolveRunCwd } = deps;
|
|
309
320
|
const cwd = await resolveRunCwd(input.runId, input.taskId);
|
|
310
|
-
const branch = branchName(input.taskId, input.title);
|
|
321
|
+
const branch = branchName(input.taskId, input.title, input.issueRef);
|
|
311
322
|
const ownerRepoResult = resolveOwnerRepo(git, cwd);
|
|
312
323
|
if ('needsHuman' in ownerRepoResult)
|
|
313
324
|
return ownerRepoResult;
|
|
@@ -323,12 +334,10 @@ export async function confirmMerge(input, deps) {
|
|
|
323
334
|
};
|
|
324
335
|
const pr = view();
|
|
325
336
|
if (pr.state === 'MERGED')
|
|
326
|
-
return { merged: true, prNumber: pr.number, prUrl: pr.url };
|
|
337
|
+
return { merged: true, prNumber: pr.number, prUrl: pr.url, ...(input.issueRef ? { issueRef: input.issueRef } : {}) };
|
|
327
338
|
if (pr.state !== 'OPEN') {
|
|
328
339
|
return { needsHuman: true, lesson: `PR #${pr.number} is ${pr.state} (not OPEN) and not merged — resolve manually` };
|
|
329
340
|
}
|
|
330
|
-
// Only auto-merge a CLEAN PR (CI green, no conflicts). Other states (BLOCKED/DIRTY/UNSTABLE/BEHIND…)
|
|
331
|
-
// require a human — block and keep the worktree for rework.
|
|
332
341
|
if (pr.mergeStateStatus !== 'CLEAN') {
|
|
333
342
|
return {
|
|
334
343
|
needsHuman: true,
|
|
@@ -336,60 +345,80 @@ export async function confirmMerge(input, deps) {
|
|
|
336
345
|
`conflicts, or required reviews pending; merge it manually (or fix + re-run) then cleanup`,
|
|
337
346
|
};
|
|
338
347
|
}
|
|
339
|
-
|
|
340
|
-
|
|
348
|
+
const expectedHeadSha = input.mergeReadiness?.headSha.trim();
|
|
349
|
+
if (!expectedHeadSha) {
|
|
350
|
+
return {
|
|
351
|
+
needsHuman: true,
|
|
352
|
+
lesson: `PR #${pr.number} merge requires a fresh merge readiness headSha guard — re-run readiness before approving merge`,
|
|
353
|
+
};
|
|
354
|
+
}
|
|
341
355
|
if (pr.isDraft) {
|
|
342
356
|
gh(['pr', 'ready', branch, '--repo', ownerRepo]);
|
|
343
357
|
}
|
|
344
|
-
|
|
345
|
-
|
|
358
|
+
try {
|
|
359
|
+
gh(['pr', 'merge', branch, '--repo', ownerRepo, '--squash', '--delete-branch', '--match-head-commit', expectedHeadSha]);
|
|
360
|
+
}
|
|
361
|
+
catch (err) {
|
|
362
|
+
const message = err instanceof Error ? err.message : String(err);
|
|
363
|
+
return {
|
|
364
|
+
needsHuman: true,
|
|
365
|
+
lesson: `PR #${pr.number} merge was blocked by the GitHub head guard ` +
|
|
366
|
+
`(expected ${expectedHeadSha}): ${message || 'merge command failed'}`,
|
|
367
|
+
};
|
|
368
|
+
}
|
|
346
369
|
const after = view();
|
|
347
370
|
if (after.state === 'MERGED')
|
|
348
|
-
return { merged: true, prNumber: after.number, prUrl: after.url };
|
|
371
|
+
return { merged: true, prNumber: after.number, prUrl: after.url, ...(input.issueRef ? { issueRef: input.issueRef } : {}) };
|
|
349
372
|
return { needsHuman: true, lesson: `PR #${after.number} merge did not take effect (state=${after.state}) — verify manually` };
|
|
350
373
|
}
|
|
351
|
-
/** Default polling bounds — pr-readiness parity (20 polls × 30s), overridable via env so the e2e suite
|
|
352
|
-
* doesn't real-sleep; also injectable (deps.maxPolls/pollIntervalMs) so unit tests run instantly. */
|
|
353
374
|
function envInt(name, fallback) {
|
|
354
375
|
const n = Number.parseInt(process.env[name] ?? '', 10);
|
|
355
376
|
return Number.isFinite(n) && n > 0 ? n : fallback;
|
|
356
377
|
}
|
|
357
|
-
function defaultCollect(repo, branch, base, execGh) {
|
|
358
|
-
|
|
359
|
-
// feed, so suppress the `api repos/.../{reviews,comments}` calls (`includeComments: false`). Those
|
|
360
|
-
// extra calls are pure overhead here and the review-thread query already carries the actionable
|
|
361
|
-
// feedback the loop acts on.
|
|
362
|
-
return collectPrReadiness({ repo, headBranch: branch, baseBranch: base, includeReviewThreads: true, includeComments: false }, execGh).then((r) => ({
|
|
378
|
+
function defaultCollect(repo, branch, base, execGh, issueRef) {
|
|
379
|
+
return collectPrReadiness({ repo, headBranch: branch, baseBranch: base, issueRef, includeReviewThreads: true, includeComments: false }, execGh).then((r) => ({
|
|
363
380
|
pr: { number: r.pr.number, headSha: r.pr.headSha },
|
|
364
381
|
checks: { pending: r.checks.pending, fail: r.checks.fail, list: r.checks.list },
|
|
365
382
|
reviewThreads: { items: r.reviewThreads.items },
|
|
383
|
+
readinessVerdict: r.verdict,
|
|
384
|
+
nextAction: r.nextAction,
|
|
385
|
+
evidence: r.evidence,
|
|
366
386
|
}));
|
|
367
387
|
}
|
|
368
388
|
const defaultSleep = (ms) => new Promise((resolve) => setTimeout(resolve, ms));
|
|
369
|
-
|
|
370
|
-
|
|
371
|
-
|
|
372
|
-
|
|
373
|
-
|
|
374
|
-
|
|
389
|
+
function ciFailuresFrom(readiness) {
|
|
390
|
+
return readiness.checks.list
|
|
391
|
+
.filter((c) => readiness.checks.fail.includes(c.name))
|
|
392
|
+
.map((c) => ({ name: c.name, conclusion: c.result }));
|
|
393
|
+
}
|
|
394
|
+
function readinessRequiresReview(readiness) {
|
|
395
|
+
return readiness.readinessVerdict === 'needs_human'
|
|
396
|
+
|| readiness.nextAction === 'human_decision'
|
|
397
|
+
|| readiness.nextAction === 'reviewer_triage'
|
|
398
|
+
|| (readiness.nextAction === 'developer_fix' && readiness.checks.fail.length === 0);
|
|
399
|
+
}
|
|
400
|
+
function readinessEvidence(readiness) {
|
|
401
|
+
return [
|
|
402
|
+
readiness.readinessVerdict ? `readiness verdict=${readiness.readinessVerdict}` : undefined,
|
|
403
|
+
readiness.nextAction ? `readiness nextAction=${readiness.nextAction}` : undefined,
|
|
404
|
+
].filter((item) => item !== undefined);
|
|
405
|
+
}
|
|
375
406
|
export async function pollPr(input, deps) {
|
|
376
407
|
const { execGit: git, execGh: gh, resolveRunCwd } = deps;
|
|
377
408
|
const collect = deps.collect ?? defaultCollect;
|
|
378
409
|
const sleep = deps.sleep ?? defaultSleep;
|
|
379
|
-
|
|
380
|
-
// of import order; prod default stays 20 × 30s.
|
|
410
|
+
const requiredChecks = deps.requiredChecks ?? fetchRequiredCheckNames;
|
|
381
411
|
const maxPolls = deps.maxPolls ?? envInt('REVO_POLL_PR_MAX_POLLS', 20);
|
|
382
412
|
const intervalMs = deps.pollIntervalMs ?? envInt('REVO_POLL_PR_INTERVAL_MS', 30_000);
|
|
383
413
|
const cwd = await resolveRunCwd(input.runId, input.taskId);
|
|
384
|
-
const branch = branchName(input.taskId, input.title);
|
|
414
|
+
const branch = branchName(input.taskId, input.title, input.issueRef);
|
|
385
415
|
const ownerRepoResult = resolveOwnerRepo(git, cwd);
|
|
386
416
|
if ('needsHuman' in ownerRepoResult)
|
|
387
417
|
return ownerRepoResult;
|
|
388
418
|
const { ownerRepo } = ownerRepoResult;
|
|
389
419
|
let readiness;
|
|
390
420
|
for (let i = 0; i < maxPolls; i++) {
|
|
391
|
-
readiness = await collect(ownerRepo, branch, input.base, gh);
|
|
392
|
-
// Terminal once nothing is pending and at least one check has registered (matches pr-readiness).
|
|
421
|
+
readiness = await collect(ownerRepo, branch, input.base, gh, input.issueRef);
|
|
393
422
|
if (readiness.checks.pending.length === 0 && readiness.checks.list.length > 0)
|
|
394
423
|
break;
|
|
395
424
|
readiness = undefined;
|
|
@@ -402,32 +431,30 @@ export async function pollPr(input, deps) {
|
|
|
402
431
|
lesson: `pollPr timed out after ${maxPolls} polls — CI checks still pending or none registered for ${branch}`,
|
|
403
432
|
};
|
|
404
433
|
}
|
|
405
|
-
// `settled` is the CI-terminal readiness (defined past the guard); the review-grace loop may refresh it.
|
|
406
434
|
let settled = readiness;
|
|
407
|
-
|
|
408
|
-
|
|
409
|
-
.filter((c) => settled.checks.fail.includes(c.name))
|
|
410
|
-
.map((c) => ({ name: c.name, conclusion: c.result }));
|
|
411
|
-
// CI green → flip the PR draft→ready so CodeRabbit / human reviewers actually engage. They SKIP
|
|
412
|
-
// draft PRs, which silently bypassed the entire review loop (the PR was readied only at confirmMerge,
|
|
413
|
-
// i.e. after the merge gate) — slice 142. Then wait a BOUNDED grace for review threads to surface
|
|
414
|
-
// before declaring clean, so we don't merge-gate a millisecond before CodeRabbit posts. We never
|
|
415
|
-
// BLOCK on a review arriving: the human merge gate is the backstop, so an absent / rate-limited
|
|
416
|
-
// reviewer falls through to it rather than deadlocking the run. CI-red stays draft (don't request
|
|
417
|
-
// review of broken code) and routes to ci_changes for a fix first.
|
|
418
|
-
if (ciFailures.length === 0) {
|
|
435
|
+
const initialCiFailures = ciFailuresFrom(settled);
|
|
436
|
+
if (initialCiFailures.length === 0) {
|
|
419
437
|
try {
|
|
420
438
|
gh(['pr', 'ready', branch, '--repo', ownerRepo]);
|
|
421
439
|
}
|
|
422
440
|
catch {
|
|
423
|
-
// already ready / nothing to ready — idempotent best-effort
|
|
424
441
|
}
|
|
425
442
|
const reviewGracePolls = deps.reviewGracePolls ?? envInt('REVO_POLL_PR_REVIEW_GRACE_POLLS', 4);
|
|
426
443
|
for (let i = 0; i < reviewGracePolls && settled.reviewThreads.items.length === 0; i++) {
|
|
427
444
|
await sleep(intervalMs);
|
|
428
|
-
settled = await collect(ownerRepo, branch, input.base, gh);
|
|
445
|
+
settled = await collect(ownerRepo, branch, input.base, gh, input.issueRef);
|
|
429
446
|
}
|
|
430
447
|
}
|
|
448
|
+
if (settled.checks.pending.length > 0 || settled.checks.list.length === 0) {
|
|
449
|
+
const detail = settled.checks.pending.length > 0
|
|
450
|
+
? `pending checks: ${settled.checks.pending.join(', ')}`
|
|
451
|
+
: 'no checks registered';
|
|
452
|
+
const evidence = settled.evidence.length > 0 ? ` Evidence: ${settled.evidence.join(' | ')}` : '';
|
|
453
|
+
return {
|
|
454
|
+
needsHuman: true,
|
|
455
|
+
lesson: `pollPr found unsettled readiness after readying ${branch} - ${detail}.${evidence}`,
|
|
456
|
+
};
|
|
457
|
+
}
|
|
431
458
|
const reviewThreads = settled.reviewThreads.items.map((t) => ({
|
|
432
459
|
threadId: t.id,
|
|
433
460
|
path: t.path,
|
|
@@ -435,19 +462,31 @@ export async function pollPr(input, deps) {
|
|
|
435
462
|
author: t.author,
|
|
436
463
|
body: t.body,
|
|
437
464
|
}));
|
|
438
|
-
|
|
439
|
-
|
|
465
|
+
const ciFailures = ciFailuresFrom(settled);
|
|
466
|
+
let ciVerdictFailures = ciFailures;
|
|
467
|
+
if (ciFailures.length > 0 && settled.pr.number !== null) {
|
|
468
|
+
try {
|
|
469
|
+
const required = requiredChecks(ownerRepo, settled.pr.number, gh);
|
|
470
|
+
if (required.size > 0)
|
|
471
|
+
ciVerdictFailures = ciFailures.filter((f) => required.has(f.name));
|
|
472
|
+
}
|
|
473
|
+
catch {
|
|
474
|
+
}
|
|
475
|
+
}
|
|
476
|
+
const verdict = reviewThreads.length > 0 || readinessRequiresReview(settled)
|
|
477
|
+
? 'review_changes'
|
|
478
|
+
: ciVerdictFailures.length > 0 ? 'ci_changes' : 'clean';
|
|
440
479
|
return {
|
|
441
480
|
prNumber: settled.pr.number ?? null,
|
|
442
481
|
headSha: settled.pr.headSha,
|
|
482
|
+
evidence: [...settled.evidence, ...readinessEvidence(settled), `PR headSha=${settled.pr.headSha}`, `pollPr verdict=${verdict}`],
|
|
483
|
+
...(input.issueRef ? { issueRef: input.issueRef } : {}),
|
|
443
484
|
verdict,
|
|
444
485
|
ciFailures,
|
|
445
486
|
reviewThreads,
|
|
446
487
|
};
|
|
447
488
|
}
|
|
448
489
|
const TRIAGE_DECISIONS = new Set(['fix', 'wontfix', 'question']);
|
|
449
|
-
/** Coerce the analyst's (LLM-produced, untrusted-shape) triage output to a Triage, dropping malformed
|
|
450
|
-
* items so respondThreads never throws on a null/garbled entry (CodeRabbit). */
|
|
451
490
|
export function asTriage(value) {
|
|
452
491
|
if (value === null || typeof value !== 'object')
|
|
453
492
|
return { items: [] };
|
|
@@ -460,20 +499,13 @@ export function asTriage(value) {
|
|
|
460
499
|
: [];
|
|
461
500
|
return { items };
|
|
462
501
|
}
|
|
463
|
-
/**
|
|
464
|
-
* respondThreads — REAL (live only). For each triaged thread we acted on (decision fix OR wontfix —
|
|
465
|
-
* plan 0018 decision #2), reply in the thread then resolve it, via the gh-pinned GraphQL API
|
|
466
|
-
* (`addPullRequestReviewThreadReply` then `resolveReviewThread`). Question items are skipped (they go to
|
|
467
|
-
* the question gate, not auto-resolved). Idempotent: resolving an already-resolved thread is a no-op for
|
|
468
|
-
* the loop (a reopened/new comment is caught by the next pollPr).
|
|
469
|
-
*/
|
|
470
502
|
export async function respondThreads(triage, deps) {
|
|
471
503
|
const { execGh: gh } = deps;
|
|
472
504
|
let replied = 0;
|
|
473
505
|
let resolved = 0;
|
|
474
506
|
for (const item of triage.items) {
|
|
475
507
|
if (item.decision !== 'fix' && item.decision !== 'wontfix')
|
|
476
|
-
continue;
|
|
508
|
+
continue;
|
|
477
509
|
const body = item.replyText ?? (item.decision === 'wontfix' ? 'Acknowledged; not changing.' : 'Addressed.');
|
|
478
510
|
gh([
|
|
479
511
|
'api', 'graphql',
|
|
@@ -491,37 +523,20 @@ export async function respondThreads(triage, deps) {
|
|
|
491
523
|
}
|
|
492
524
|
return { replied, resolved };
|
|
493
525
|
}
|
|
494
|
-
// ─── IntegratorService ─────────────────────────────────────────────────────────
|
|
495
|
-
/** Default execGit implementation wrapping execFileSync with a resolved absolute path. */
|
|
496
526
|
function defaultExecGit(args, cwd) {
|
|
497
527
|
return execFileSync(gitAbsPath(), args, { encoding: 'utf8', cwd, timeout: 60_000 });
|
|
498
528
|
}
|
|
499
|
-
/**
|
|
500
|
-
* IntegratorService — @Injectable wrapper over integrate / stubIntegrate / preflightLive.
|
|
501
|
-
* Exposes bound arrow properties so they survive being passed to registerStep unbound.
|
|
502
|
-
* DBOS-SEALED: zero @dbos-inc imports; PipelineService registers these as steps.
|
|
503
|
-
*/
|
|
504
529
|
let IntegratorService = class IntegratorService {
|
|
505
530
|
runService;
|
|
506
531
|
deps;
|
|
507
532
|
constructor(runService) {
|
|
508
533
|
this.runService = runService;
|
|
509
|
-
// execGh is resolved per-run inside runIntegrate (fail-loud on an unresolved pinned identity),
|
|
510
|
-
// so it is NOT built here — only the git + cwd deps are stable at construction.
|
|
511
534
|
this.deps = {
|
|
512
535
|
execGit: defaultExecGit,
|
|
513
536
|
resolveTaskCwd: this.runService.makeResolveTaskCwd(),
|
|
514
537
|
resolveRunCwd: this.runService.makeResolveRunCwd(),
|
|
515
538
|
};
|
|
516
539
|
}
|
|
517
|
-
/**
|
|
518
|
-
* Real integrator — live path. Arrow property: safe to pass unbound to registerStep.
|
|
519
|
-
*
|
|
520
|
-
* 0008 #1 (fail-loud, 2026-06-12 dogfood): resolve the PINNED gh identity first. If its token
|
|
521
|
-
* cannot be resolved we REFUSE to fall back to the ambient gh account (which would open the PR
|
|
522
|
-
* as the wrong user) and block as needsHuman instead. Only on success do we run the integrator
|
|
523
|
-
* with the pinned execGh.
|
|
524
|
-
*/
|
|
525
540
|
runIntegrate = (input) => {
|
|
526
541
|
const pinned = resolvePinnedGh();
|
|
527
542
|
if ('needsHuman' in pinned) {
|
|
@@ -531,14 +546,9 @@ let IntegratorService = class IntegratorService {
|
|
|
531
546
|
console.log(`[integrator] gh pinned to account '${resolveGhAccount()}' (GH_TOKEN, not ambient)`);
|
|
532
547
|
return integrate(input, { ...this.deps, execGh: pinned.execGh });
|
|
533
548
|
};
|
|
534
|
-
/** Stub integrator — script path; zero external effects. */
|
|
535
549
|
runStub = (input) => {
|
|
536
550
|
return stubIntegrate(input);
|
|
537
551
|
};
|
|
538
|
-
/**
|
|
539
|
-
* Real confirm-merge — live path. Same fail-loud pinned-gh handling as runIntegrate: refuse the
|
|
540
|
-
* ambient account, block if the pinned identity cannot be resolved.
|
|
541
|
-
*/
|
|
542
552
|
runConfirmMerge = (input) => {
|
|
543
553
|
const pinned = resolvePinnedGh();
|
|
544
554
|
if ('needsHuman' in pinned) {
|
|
@@ -547,18 +557,15 @@ let IntegratorService = class IntegratorService {
|
|
|
547
557
|
}
|
|
548
558
|
return confirmMerge(input, { ...this.deps, execGh: pinned.execGh });
|
|
549
559
|
};
|
|
550
|
-
/** Stub confirm-merge — script path; zero external effects (treats the stub PR as merged). */
|
|
551
560
|
runConfirmStub = (input) => {
|
|
552
561
|
return { merged: true, prNumber: 0, prUrl: `stub://pr/${input.taskId}/merged` };
|
|
553
562
|
};
|
|
554
|
-
/** Live preflight — clean check + base invariant. Arrow property for safe unbound registration. */
|
|
555
563
|
runPreflight = (taskId, base) => {
|
|
556
564
|
return preflightLive(taskId, base, this.deps);
|
|
557
565
|
};
|
|
558
|
-
|
|
559
|
-
|
|
560
|
-
|
|
561
|
-
*/
|
|
566
|
+
runCaptureProducedChange = (input) => {
|
|
567
|
+
return captureProducedChange(input, this.deps);
|
|
568
|
+
};
|
|
562
569
|
runPollPr = (input) => {
|
|
563
570
|
const pinned = resolvePinnedGh();
|
|
564
571
|
if ('needsHuman' in pinned) {
|
|
@@ -567,14 +574,9 @@ let IntegratorService = class IntegratorService {
|
|
|
567
574
|
}
|
|
568
575
|
return pollPr(input, { ...this.deps, execGh: pinned.execGh });
|
|
569
576
|
};
|
|
570
|
-
/** Stub pollPr — script path; zero external effects (reports a clean PR so the loop converges to merge). */
|
|
571
577
|
runPollStub = (_input) => {
|
|
572
|
-
return { prNumber: null, headSha: 'stub', verdict: 'clean', ciFailures: [], reviewThreads: [] };
|
|
578
|
+
return { prNumber: null, headSha: 'stub', evidence: ['stub pollPr readiness: clean'], verdict: 'clean', ciFailures: [], reviewThreads: [] };
|
|
573
579
|
};
|
|
574
|
-
/**
|
|
575
|
-
* Real respondThreads — live path. Same fail-loud pinned-gh handling: refuse the ambient account.
|
|
576
|
-
* The consumed `triage` rides in IntegratorInput.triage (hydrated by the adapter from run_outputs).
|
|
577
|
-
*/
|
|
578
580
|
runRespondThreads = (input) => {
|
|
579
581
|
const pinned = resolvePinnedGh();
|
|
580
582
|
if ('needsHuman' in pinned) {
|
|
@@ -583,7 +585,6 @@ let IntegratorService = class IntegratorService {
|
|
|
583
585
|
}
|
|
584
586
|
return respondThreads(asTriage(input.triage), { execGh: pinned.execGh });
|
|
585
587
|
};
|
|
586
|
-
/** Stub respondThreads — script path; zero external effects (no threads to reply/resolve). */
|
|
587
588
|
runRespondStub = (_input) => {
|
|
588
589
|
return { replied: 0, resolved: 0 };
|
|
589
590
|
};
|