@bridge_gpt/mcp-server 0.2.62 → 0.2.66
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 +66 -15
- package/build/agent-launchers/claude-executor-adapter.js +145 -20
- package/build/agent-launchers/executor-adapter.js +81 -2
- package/build/agents.generated.js +10 -1
- package/build/bounded-wait.js +32 -1
- package/build/bridge-config.js +104 -0
- package/build/codex-bridge-profile.js +10 -1
- package/build/commands.generated.js +7 -6
- package/build/conductor/bridge-api-client.js +565 -32
- package/build/conductor/bring-up-facts.js +3 -1
- package/build/conductor/doctor.js +23 -14
- package/build/conductor/epic-child-continuation.js +351 -0
- package/build/conductor/epic-implementer-cli.js +154 -17
- package/build/conductor/git-hooks.js +529 -118
- package/build/conductor/guided-repair.js +323 -0
- package/build/conductor/hook-runtime-qualification.js +340 -0
- package/build/conductor/readiness-cli.js +30 -9
- package/build/conductor/readiness-sections.js +5 -4
- package/build/conductor/readiness.js +45 -12
- package/build/conductor/recovery-cli.js +859 -27
- package/build/conductor/recovery-operations.js +574 -5
- package/build/conductor/repair-contract.js +69 -3
- package/build/conductor-bundle-artifacts.js +1 -0
- package/build/credential-store.js +52 -13
- package/build/diagnostic-text.js +36 -0
- package/build/docs.generated.js +3 -2
- package/build/doctor.js +147 -21
- package/build/drive-epic-key.js +12 -0
- package/build/drive-epic.js +1537 -125
- package/build/epic-coherence-input-cli.js +137 -0
- package/build/epic-coherence-input.js +920 -0
- package/build/epic-coherence-input.test-fixtures.js +175 -0
- package/build/epic-implementer-bin.js +41 -33
- package/build/epic-implementer-bundle-cli.js +14 -1
- package/build/epic-launch-diagnostic.js +31 -0
- package/build/epic-launch-profile.js +58 -19
- package/build/epic-run-status.js +5 -0
- package/build/epic-shared/bridge-client.js +46 -4
- package/build/executor/cli.js +39 -0
- package/build/executor/completion-contract.js +423 -0
- package/build/executor/completion-supervision.js +291 -0
- package/build/executor/correction-dispatch.js +60 -0
- package/build/executor/credentials.js +3 -1
- package/build/executor/deps.js +65 -1
- package/build/executor/env.js +12 -0
- package/build/executor/heartbeat.js +13 -3
- package/build/executor/http-client.js +91 -4
- package/build/executor/integration-job.js +6 -1
- package/build/executor/job-errors.js +61 -4
- package/build/executor/job-log-registry.js +340 -73
- package/build/executor/job-runner.js +1550 -55
- package/build/executor/job-types.js +9 -6
- package/build/executor/preflight.js +189 -1
- package/build/executor/process-containment.js +756 -0
- package/build/executor/process.js +235 -5
- package/build/executor/prompt-spec.js +366 -1
- package/build/executor/provider-checks.js +69 -0
- package/build/executor/rebase-finalization.js +351 -0
- package/build/executor/reconcile-overlap-job.js +77 -84
- package/build/executor/required-verification.js +313 -0
- package/build/executor/required-work-ledger.js +1121 -0
- package/build/executor/results.js +142 -0
- package/build/executor/resume-pre-spawn.js +87 -4
- package/build/executor/runner.js +168 -0
- package/build/executor/stale-artifacts.js +5 -4
- package/build/executor/test-declaration.js +20 -0
- package/build/executor/types.js +13 -0
- package/build/executor/watch-cli.js +35 -8
- package/build/executor/worker-command.js +70 -34
- package/build/executor/worker-finalization.js +168 -26
- package/build/executor/worker-guard-hook.js +75 -14
- package/build/executor/worker-log.js +137 -70
- package/build/executor/worktree-gc.js +30 -6
- package/build/executor/worktree.js +13 -0
- package/build/executor-launch.js +658 -0
- package/build/index.js +594 -1152
- package/build/init.js +107 -4
- package/build/install-bridge-conductor.js +231 -41
- package/build/install-bridge.js +284 -20
- package/build/install-doctor.js +172 -30
- package/build/install-reexec.js +69 -7
- package/build/macos-terminal-permission-guidance.js +16 -0
- package/build/merge-pull-request.js +1 -1
- package/build/model-thresholds-tool.js +185 -0
- package/build/pipelines.generated.js +9 -8
- package/build/plan-discovery.js +137 -0
- package/build/plan-epic-implementer-eligibility.js +2 -2
- package/build/plane/cli.js +37 -3
- package/build/plane/member-logs.js +3 -0
- package/build/plane/member-roster.js +7 -1
- package/build/plane/shutdown.js +59 -1
- package/build/plane/status.js +4 -0
- package/build/plane/supervisor.js +146 -0
- package/build/plane/worker-exit-code.js +2 -0
- package/build/project-root.js +99 -0
- package/build/readme.generated.js +1 -1
- package/build/review-tickets.js +156 -13
- package/build/scripts/executor-protocol-contract-driver.js +13 -3
- package/build/setup-epic.js +208 -125
- package/build/sfcc/setup-status.js +2 -0
- package/build/sfcc/tool-wrapper.js +2 -0
- package/build/sfcc/tools-configured.js +96 -0
- package/build/start-tickets.js +163 -58
- package/build/upgrade-cli.js +109 -24
- package/build/version.generated.js +3 -3
- package/build/windows-npm-entry.js +103 -0
- package/build/worker-guard-hook-bin.js +5 -4
- package/build/worktree-core.js +6 -5
- package/docs/CONDUCTOR.md +116 -2
- package/docs/install/mcp-tool-integrations.md +1 -0
- package/docs/install/sfcc-integration.md +12 -7
- package/package.json +2 -1
- package/pipelines/implement-ticket.json +1 -1
|
@@ -0,0 +1,351 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Focused merge-stage conflict repair: context validation and the supervised
|
|
3
|
+
* post-push wait (BAPI-1311).
|
|
4
|
+
*
|
|
5
|
+
* Before BAPI-1311 a `merging`-gate `rebase` re-ran the whole `/implement-ticket`
|
|
6
|
+
* recipe, and its worker also owned the post-push CI wait. A check that was still
|
|
7
|
+
* running when that wait ended turned a completed repair into a failed attempt,
|
|
8
|
+
* which the gate then answered with ANOTHER full repair.
|
|
9
|
+
*
|
|
10
|
+
* This module holds the two executor halves of the replacement:
|
|
11
|
+
*
|
|
12
|
+
* 1. {@link validateFocusedRebaseContext} — the narrow boundary validator for the
|
|
13
|
+
* server's versioned `payload.focused_rebase` context. It runs at claimed-job
|
|
14
|
+
* entry, before any worktree effect. A malformed context is REFUSED by name; it
|
|
15
|
+
* is never reinterpreted as a legacy rebase and never downgraded to the full
|
|
16
|
+
* recipe.
|
|
17
|
+
* 2. {@link waitForFocusedRebase} — after the worker exited cleanly and
|
|
18
|
+
* finalization PROVED its push landed, wait for the evidence the server's
|
|
19
|
+
* completion verifier will read: the same open pull request, at the exact
|
|
20
|
+
* pushed head, on the expected head branch and base, mergeable, with every
|
|
21
|
+
* effective required check green at that head. Every read and sleep is bounded
|
|
22
|
+
* by the claim's remaining absolute deadline and abort signal.
|
|
23
|
+
*
|
|
24
|
+
* The wait READS; it never acts. It launches no worker, enqueues nothing, resets
|
|
25
|
+
* nothing, and pushes nothing. `ready` is permission to submit `/complete` once,
|
|
26
|
+
* not a gate verdict: the server re-reads the provider and may still refuse (R14
|
|
27
|
+
* rule 3).
|
|
28
|
+
*/
|
|
29
|
+
import { ExecutorNamedError, FocusedRebaseContext } from "./job-errors.js";
|
|
30
|
+
import { allRequiredChecksGreen, anyRequiredCheckFailed, ProviderChecksUnobservable, } from "./provider-checks.js";
|
|
31
|
+
import { FOCUSED_REBASE_CAPABILITY, FOCUSED_REBASE_CONTEXT_VERSION, } from "./types.js";
|
|
32
|
+
/** The job type the focused context is valid on. */
|
|
33
|
+
export const FOCUSED_REBASE_JOB_TYPE = "rebase";
|
|
34
|
+
/** The payload key the server writes the context under. */
|
|
35
|
+
export const FOCUSED_REBASE_PAYLOAD_KEY = "focused_rebase";
|
|
36
|
+
/**
|
|
37
|
+
* Bounds shared with the server (`focused_rebase_dispatch.py`). Over-bound input
|
|
38
|
+
* is refused, never truncated: a truncated identity is a different identity.
|
|
39
|
+
*/
|
|
40
|
+
export const FOCUSED_REBASE_IDENTITY_MAX_CHARS = 255;
|
|
41
|
+
export const FOCUSED_REBASE_MAX_REQUIRED_CHECKS = 64;
|
|
42
|
+
export const FOCUSED_REBASE_CHECK_NAME_MAX_CHARS = 200;
|
|
43
|
+
/**
|
|
44
|
+
* Time reserved at the end of the job for the one terminal report, mirroring the
|
|
45
|
+
* overlap lane's `CI_WAIT_MARGIN_MS` precedent. Clamped to half the remaining
|
|
46
|
+
* time, so a repair that finishes close to its deadline still gets one bounded
|
|
47
|
+
* look at the evidence instead of none.
|
|
48
|
+
*/
|
|
49
|
+
export const FOCUSED_REBASE_REPORT_MARGIN_MS = 90_000;
|
|
50
|
+
/** Poll cadence while evidence is pending. */
|
|
51
|
+
export const FOCUSED_REBASE_POLL_INTERVAL_MS = 30_000;
|
|
52
|
+
/** Cap on any single provider read, before the deadline clamp. */
|
|
53
|
+
export const FOCUSED_REBASE_READ_MAX_MS = 60_000;
|
|
54
|
+
/** Does this claimed job carry a focused context at all? (Presence, not validity.) */
|
|
55
|
+
export function hasFocusedRebaseContext(job) {
|
|
56
|
+
const payload = job.payload;
|
|
57
|
+
if (!payload || typeof payload !== "object")
|
|
58
|
+
return false;
|
|
59
|
+
const raw = payload[FOCUSED_REBASE_PAYLOAD_KEY];
|
|
60
|
+
return raw !== undefined && raw !== null;
|
|
61
|
+
}
|
|
62
|
+
/** The named refusal the runner reports for an invalid focused context. */
|
|
63
|
+
export function focusedRebaseContextError(job, reason) {
|
|
64
|
+
return new ExecutorNamedError(FocusedRebaseContext, `rebase job ${job.id} refused before worker effects (reason=${reason})`);
|
|
65
|
+
}
|
|
66
|
+
function identity(value) {
|
|
67
|
+
if (typeof value !== "string")
|
|
68
|
+
return null;
|
|
69
|
+
const text = value.trim();
|
|
70
|
+
if (text.length === 0 || text.length > FOCUSED_REBASE_IDENTITY_MAX_CHARS)
|
|
71
|
+
return null;
|
|
72
|
+
if (/[\r\n\0]/.test(text))
|
|
73
|
+
return null;
|
|
74
|
+
return text;
|
|
75
|
+
}
|
|
76
|
+
function positiveInt(value) {
|
|
77
|
+
return typeof value === "number" && Number.isInteger(value) && value > 0 ? value : null;
|
|
78
|
+
}
|
|
79
|
+
function checkList(value) {
|
|
80
|
+
if (!Array.isArray(value) || value.length > FOCUSED_REBASE_MAX_REQUIRED_CHECKS)
|
|
81
|
+
return null;
|
|
82
|
+
const out = [];
|
|
83
|
+
for (const item of value) {
|
|
84
|
+
if (typeof item !== "string" ||
|
|
85
|
+
item.trim().length === 0 ||
|
|
86
|
+
item.length > FOCUSED_REBASE_CHECK_NAME_MAX_CHARS ||
|
|
87
|
+
/[\n\0]/.test(item)) {
|
|
88
|
+
return null;
|
|
89
|
+
}
|
|
90
|
+
out.push(item);
|
|
91
|
+
}
|
|
92
|
+
return out;
|
|
93
|
+
}
|
|
94
|
+
const CHECK_SOURCES = ["policy", "plan", "none"];
|
|
95
|
+
/**
|
|
96
|
+
* Validate `payload.focused_rebase` against the CLAIMED job, before any effect.
|
|
97
|
+
*
|
|
98
|
+
* Every identity the context binds must equal what the claim itself carries:
|
|
99
|
+
* run, ticket, repository, and branch against the claim's top-level fields; the
|
|
100
|
+
* attempt, pull request, base, and required checks against the payload fields
|
|
101
|
+
* the server wrote beside the context (the completion verifier reads the payload
|
|
102
|
+
* copies, so a disagreement would make the executor wait on one set while the
|
|
103
|
+
* server verifies another). The claim must also REQUIRE the focused capability,
|
|
104
|
+
* which is what kept an executor unable to honor the context from claiming it.
|
|
105
|
+
*
|
|
106
|
+
* Nothing is coerced and nothing falls back: a conflicting value is a refusal,
|
|
107
|
+
* not a choice between the two.
|
|
108
|
+
*/
|
|
109
|
+
export function validateFocusedRebaseContext(job) {
|
|
110
|
+
const fail = (reason) => ({ ok: false, reason });
|
|
111
|
+
if (job.job_type !== FOCUSED_REBASE_JOB_TYPE)
|
|
112
|
+
return fail("not_rebase_job");
|
|
113
|
+
const payload = job.payload && typeof job.payload === "object" ? job.payload : {};
|
|
114
|
+
const raw = payload[FOCUSED_REBASE_PAYLOAD_KEY];
|
|
115
|
+
if (!raw || typeof raw !== "object" || Array.isArray(raw))
|
|
116
|
+
return fail("context_not_object");
|
|
117
|
+
const ctx = raw;
|
|
118
|
+
if (ctx.version !== FOCUSED_REBASE_CONTEXT_VERSION)
|
|
119
|
+
return fail("version_unsupported");
|
|
120
|
+
const required = Array.isArray(job.required_capabilities) ? job.required_capabilities : [];
|
|
121
|
+
if (!required.includes(FOCUSED_REBASE_CAPABILITY))
|
|
122
|
+
return fail("capability_not_required");
|
|
123
|
+
const prompt = payload.prompt;
|
|
124
|
+
if (typeof prompt !== "string" || prompt.trim().length === 0)
|
|
125
|
+
return fail("prompt_missing");
|
|
126
|
+
const epicRunId = identity(ctx.epic_run_id);
|
|
127
|
+
const dedupKey = identity(ctx.dedup_key);
|
|
128
|
+
const ticketKey = identity(ctx.ticket_key);
|
|
129
|
+
const repoName = identity(ctx.repo_name);
|
|
130
|
+
const workBranch = identity(ctx.work_branch);
|
|
131
|
+
const attempt = positiveInt(ctx.attempt);
|
|
132
|
+
if (epicRunId === null ||
|
|
133
|
+
dedupKey === null ||
|
|
134
|
+
ticketKey === null ||
|
|
135
|
+
repoName === null ||
|
|
136
|
+
workBranch === null ||
|
|
137
|
+
attempt === null) {
|
|
138
|
+
return fail("identity_malformed");
|
|
139
|
+
}
|
|
140
|
+
if (epicRunId !== (typeof job.epic_run_id === "string" ? job.epic_run_id.trim() : null)) {
|
|
141
|
+
return fail("run_mismatch");
|
|
142
|
+
}
|
|
143
|
+
if (ticketKey !== (typeof job.ticket_key === "string" ? job.ticket_key.trim() : null)) {
|
|
144
|
+
return fail("ticket_mismatch");
|
|
145
|
+
}
|
|
146
|
+
if (repoName !== job.repo_name.trim())
|
|
147
|
+
return fail("repository_mismatch");
|
|
148
|
+
if (workBranch !== (typeof job.expected_branch === "string" ? job.expected_branch.trim() : null)) {
|
|
149
|
+
return fail("branch_mismatch");
|
|
150
|
+
}
|
|
151
|
+
if (!dedupKey.includes(ticketKey) || attempt !== payload.attempt) {
|
|
152
|
+
return fail("job_identity_mismatch");
|
|
153
|
+
}
|
|
154
|
+
const prNumber = positiveInt(ctx.pr_number);
|
|
155
|
+
if (prNumber === null)
|
|
156
|
+
return fail("pr_malformed");
|
|
157
|
+
if (prNumber !== payload.pr_number)
|
|
158
|
+
return fail("pr_mismatch");
|
|
159
|
+
let baseBranch = null;
|
|
160
|
+
if (ctx.base_branch !== null) {
|
|
161
|
+
baseBranch = identity(ctx.base_branch);
|
|
162
|
+
if (baseBranch === null)
|
|
163
|
+
return fail("identity_malformed");
|
|
164
|
+
}
|
|
165
|
+
const payloadBase = typeof payload.base_branch === "string" ? payload.base_branch.trim() : null;
|
|
166
|
+
if (baseBranch !== payloadBase)
|
|
167
|
+
return fail("base_mismatch");
|
|
168
|
+
const checks = checkList(ctx.required_checks);
|
|
169
|
+
const source = ctx.required_checks_source;
|
|
170
|
+
if (checks === null ||
|
|
171
|
+
typeof source !== "string" ||
|
|
172
|
+
!CHECK_SOURCES.includes(source) ||
|
|
173
|
+
(source === "none" && checks.length > 0)) {
|
|
174
|
+
return fail("required_checks_malformed");
|
|
175
|
+
}
|
|
176
|
+
const payloadChecks = payload.required_checks;
|
|
177
|
+
if (!Array.isArray(payloadChecks) ||
|
|
178
|
+
payloadChecks.length !== checks.length ||
|
|
179
|
+
!checks.every((name, i) => payloadChecks[i] === name)) {
|
|
180
|
+
return fail("required_checks_mismatch");
|
|
181
|
+
}
|
|
182
|
+
return {
|
|
183
|
+
ok: true,
|
|
184
|
+
context: {
|
|
185
|
+
version: FOCUSED_REBASE_CONTEXT_VERSION,
|
|
186
|
+
epicRunId,
|
|
187
|
+
dedupKey,
|
|
188
|
+
attempt,
|
|
189
|
+
ticketKey,
|
|
190
|
+
repoName,
|
|
191
|
+
workBranch,
|
|
192
|
+
prNumber,
|
|
193
|
+
baseBranch,
|
|
194
|
+
requiredChecks: checks,
|
|
195
|
+
requiredChecksSource: source,
|
|
196
|
+
prompt,
|
|
197
|
+
},
|
|
198
|
+
};
|
|
199
|
+
}
|
|
200
|
+
/** A promise that rejects when `signal` aborts or `ms` elapses, whichever first. */
|
|
201
|
+
function boundedRace(work, ms, signal) {
|
|
202
|
+
if (signal.aborted)
|
|
203
|
+
return Promise.resolve("aborted");
|
|
204
|
+
return new Promise((resolve) => {
|
|
205
|
+
let settled = false;
|
|
206
|
+
const finish = (value) => {
|
|
207
|
+
if (settled)
|
|
208
|
+
return;
|
|
209
|
+
settled = true;
|
|
210
|
+
clearTimeout(timer);
|
|
211
|
+
signal.removeEventListener("abort", onAbort);
|
|
212
|
+
resolve(value);
|
|
213
|
+
};
|
|
214
|
+
const onAbort = () => finish("aborted");
|
|
215
|
+
const timer = setTimeout(() => finish("timeout"), Math.max(0, ms));
|
|
216
|
+
signal.addEventListener("abort", onAbort, { once: true });
|
|
217
|
+
work.then((value) => finish(value), (err) => {
|
|
218
|
+
if (err instanceof ProviderChecksUnobservable) {
|
|
219
|
+
finish(err);
|
|
220
|
+
return;
|
|
221
|
+
}
|
|
222
|
+
finish("timeout");
|
|
223
|
+
});
|
|
224
|
+
});
|
|
225
|
+
}
|
|
226
|
+
/**
|
|
227
|
+
* Wait for the focused repair's current-head evidence under the original claim
|
|
228
|
+
* deadline. Never throws for a provider problem; every read failure is PENDING.
|
|
229
|
+
*
|
|
230
|
+
* `ready` requires, in one iteration: the SAME open pull request (by number), at
|
|
231
|
+
* the exact pushed head, on the bound work branch, targeting the expected base,
|
|
232
|
+
* reading `MERGEABLE`, and every effective required check green AT THAT HEAD. An
|
|
233
|
+
* explicitly empty server-derived check set is satisfied vacuously, but only once
|
|
234
|
+
* every pull-request condition holds.
|
|
235
|
+
*
|
|
236
|
+
* Proven failures stop at once: a required check concluded red, the pull request
|
|
237
|
+
* closed or reads `CONFLICTING`, or its number, head, head branch, or base moved.
|
|
238
|
+
* Running or missing checks, `UNKNOWN` mergeability, and unavailable reads stay
|
|
239
|
+
* pending while time remains. When the deadline (less the report margin) arrives
|
|
240
|
+
* first, the outcome is an UNVERIFIED stop naming what was still missing — never
|
|
241
|
+
* a red-CI claim.
|
|
242
|
+
*/
|
|
243
|
+
export async function waitForFocusedRebase(input) {
|
|
244
|
+
const started = input.now();
|
|
245
|
+
const elapsed = () => Math.max(0, input.now() - started);
|
|
246
|
+
const remainingAtStart = Math.max(0, input.deadlineMs - started);
|
|
247
|
+
const margin = Math.min(input.reportMarginMs ?? FOCUSED_REBASE_REPORT_MARGIN_MS, Math.floor(remainingAtStart / 2));
|
|
248
|
+
const waitUntil = input.deadlineMs - margin;
|
|
249
|
+
const budget = () => Math.max(0, waitUntil - input.now());
|
|
250
|
+
const readMax = input.readMaxMs ?? FOCUSED_REBASE_READ_MAX_MS;
|
|
251
|
+
const interval = input.pollIntervalMs ?? FOCUSED_REBASE_POLL_INTERVAL_MS;
|
|
252
|
+
const pushed = input.pushedHeadSha.trim().toLowerCase();
|
|
253
|
+
const ctx = input.context;
|
|
254
|
+
const ids = `job=${input.jobId} ticket=${ctx.ticketKey} pr=#${ctx.prNumber} branch=${ctx.workBranch} ` +
|
|
255
|
+
`head=${pushed.slice(0, 12)} base=${input.expectedBaseBranch}`;
|
|
256
|
+
const secs = () => `${Math.round(elapsed() / 1000)}s`;
|
|
257
|
+
let lastPending = null;
|
|
258
|
+
const observe = async () => {
|
|
259
|
+
const prRead = await boundedRace(input.readPullRequest(Math.min(readMax, budget())), Math.min(readMax, budget()), input.signal);
|
|
260
|
+
if (prRead === "aborted" || prRead === "timeout") {
|
|
261
|
+
return prRead === "aborted" ? "aborted" : { kind: "pending", reason: "pr_unavailable" };
|
|
262
|
+
}
|
|
263
|
+
if (prRead instanceof ProviderChecksUnobservable)
|
|
264
|
+
return { kind: "pending", reason: "pr_unavailable" };
|
|
265
|
+
const pr = prRead;
|
|
266
|
+
if (!pr.available)
|
|
267
|
+
return { kind: "pending", reason: "pr_unavailable" };
|
|
268
|
+
if (pr.number !== ctx.prNumber)
|
|
269
|
+
return { kind: "stopped", reason: "pr_identity_changed" };
|
|
270
|
+
if (pr.state === null)
|
|
271
|
+
return { kind: "pending", reason: "pr_unavailable" };
|
|
272
|
+
if (pr.state !== "OPEN")
|
|
273
|
+
return { kind: "stopped", reason: "pr_closed" };
|
|
274
|
+
if (pr.headRefOid !== pushed)
|
|
275
|
+
return { kind: "stopped", reason: "head_changed" };
|
|
276
|
+
if (pr.headRefName !== ctx.workBranch)
|
|
277
|
+
return { kind: "stopped", reason: "head_branch_changed" };
|
|
278
|
+
if (pr.baseRefName !== input.expectedBaseBranch)
|
|
279
|
+
return { kind: "stopped", reason: "base_changed" };
|
|
280
|
+
if (pr.mergeable === "conflicting")
|
|
281
|
+
return { kind: "stopped", reason: "pr_not_mergeable" };
|
|
282
|
+
if (pr.mergeable !== "mergeable")
|
|
283
|
+
return { kind: "pending", reason: "mergeability_computing" };
|
|
284
|
+
if (ctx.requiredChecks.length === 0)
|
|
285
|
+
return { kind: "ready", ciVacuous: true };
|
|
286
|
+
if (budget() <= 0)
|
|
287
|
+
return "timeout";
|
|
288
|
+
const poll = await boundedRace(input.pollChecks(pushed), Math.min(readMax, budget()), input.signal);
|
|
289
|
+
if (poll === "aborted")
|
|
290
|
+
return "aborted";
|
|
291
|
+
if (poll instanceof ProviderChecksUnobservable)
|
|
292
|
+
return { kind: "unobservable" };
|
|
293
|
+
if (poll === "timeout" || poll === undefined || poll === null) {
|
|
294
|
+
return { kind: "pending", reason: "checks_unavailable" };
|
|
295
|
+
}
|
|
296
|
+
if (anyRequiredCheckFailed(poll, ctx.requiredChecks))
|
|
297
|
+
return { kind: "stopped", reason: "required_check_failed" };
|
|
298
|
+
if (allRequiredChecksGreen(poll, ctx.requiredChecks))
|
|
299
|
+
return { kind: "ready", ciVacuous: false };
|
|
300
|
+
return { kind: "pending", reason: "checks_pending" };
|
|
301
|
+
};
|
|
302
|
+
const unverifiedReason = () => {
|
|
303
|
+
if (lastPending === "checks_pending")
|
|
304
|
+
return "checks_still_pending";
|
|
305
|
+
if (lastPending === "mergeability_computing")
|
|
306
|
+
return "mergeability_still_computing";
|
|
307
|
+
return "evidence_unavailable";
|
|
308
|
+
};
|
|
309
|
+
for (;;) {
|
|
310
|
+
if (input.signal.aborted)
|
|
311
|
+
return { kind: "cancelled", elapsedMs: elapsed() };
|
|
312
|
+
if (budget() <= 0) {
|
|
313
|
+
const reason = unverifiedReason();
|
|
314
|
+
input.errorLog(`[executor] Stopped: ${reason === "evidence_unavailable" ? "evidence unavailable" : reason} at the job ` +
|
|
315
|
+
`deadline — pushed work preserved; the repair is UNVERIFIED, not failed CI; ${ids}; elapsed=${secs()}`);
|
|
316
|
+
return { kind: "stopped", proven: false, reason, elapsedMs: elapsed() };
|
|
317
|
+
}
|
|
318
|
+
const reading = await observe();
|
|
319
|
+
if (reading === "aborted")
|
|
320
|
+
return { kind: "cancelled", elapsedMs: elapsed() };
|
|
321
|
+
if (reading === "timeout")
|
|
322
|
+
continue;
|
|
323
|
+
if (reading.kind === "ready") {
|
|
324
|
+
input.errorLog(`[executor] Ready: focused rebase evidence current — pushed work at the pull request head is mergeable ` +
|
|
325
|
+
`and ${reading.ciVacuous ? "no required checks are configured (vacuous CI)" : "every required check is green"}; ` +
|
|
326
|
+
`submitting completion once; ${ids}; elapsed=${secs()}`);
|
|
327
|
+
return { kind: "ready", ciVacuous: reading.ciVacuous, elapsedMs: elapsed() };
|
|
328
|
+
}
|
|
329
|
+
if (reading.kind === "stopped") {
|
|
330
|
+
input.errorLog(`[executor] Stopped: ${reading.reason} — pushed work preserved, no further repair launched; ${ids}; elapsed=${secs()}`);
|
|
331
|
+
return { kind: "stopped", proven: true, reason: reading.reason, elapsedMs: elapsed() };
|
|
332
|
+
}
|
|
333
|
+
if (reading.kind === "unobservable") {
|
|
334
|
+
input.errorLog(`[executor] Stopped: checks_unobservable — this executor has no Bridge API access to read required ` +
|
|
335
|
+
`checks; pushed work preserved; the repair is UNVERIFIED; ${ids}; elapsed=${secs()}`);
|
|
336
|
+
return { kind: "stopped", proven: false, reason: "checks_unobservable", elapsedMs: elapsed() };
|
|
337
|
+
}
|
|
338
|
+
// Pending: log once per distinct reason so a long wait stays bounded in stderr.
|
|
339
|
+
if (reading.reason !== lastPending) {
|
|
340
|
+
input.errorLog(`[executor] Waiting for mergeability/required checks (${reading.reason}) — pushed work preserved; the ` +
|
|
341
|
+
`executor supervises this wait and will not launch another worker; ${ids}; elapsed=${secs()}`);
|
|
342
|
+
}
|
|
343
|
+
lastPending = reading.reason;
|
|
344
|
+
const pause = Math.min(interval, budget());
|
|
345
|
+
if (pause > 0) {
|
|
346
|
+
const slept = await boundedRace(input.sleep(pause), pause + 1_000, input.signal);
|
|
347
|
+
if (slept === "aborted")
|
|
348
|
+
return { kind: "cancelled", elapsedMs: elapsed() };
|
|
349
|
+
}
|
|
350
|
+
}
|
|
351
|
+
}
|
|
@@ -39,13 +39,16 @@
|
|
|
39
39
|
* refuses the push is reported as `ReconcileOverlapPushUnauthorized`.
|
|
40
40
|
*/
|
|
41
41
|
import path from "node:path";
|
|
42
|
-
import { pollCiChecksForCommit, resolveConductorBridgeApiAccess, } from "../conductor/bridge-api-client.js";
|
|
43
42
|
import { normalizeRepoRelativePath } from "../conductor/file-scope-guard.js";
|
|
44
|
-
|
|
43
|
+
// BAPI-1311: the read-only poller and predicates moved to `provider-checks.ts` so
|
|
44
|
+
// the focused rebase wait shares them; re-exported here for existing importers.
|
|
45
|
+
import { allRequiredChecksGreen, anyRequiredCheckFailed, lazyProviderCheckPoller, ProviderChecksUnobservable, } from "./provider-checks.js";
|
|
46
|
+
export { lazyProviderCheckPoller, ProviderChecksUnobservable };
|
|
45
47
|
import { buildExecutorBaseWorkerEnv } from "./env.js";
|
|
46
48
|
import { ReconcileOverlapBranchMismatch, ReconcileOverlapConflictsUnresolved, ReconcileOverlapMergeFailed, ReconcileOverlapNothingToMerge, ReconcileOverlapPayload as ReconcileOverlapPayloadInvalid, ReconcileOverlapPushRejected, ReconcileOverlapPushUnauthorized, ReconcileOverlapTopologyRefused, ReconcileOverlapVerificationRed, ReconcileOverlapWorkerRewroteHistory, } from "./job-errors.js";
|
|
47
49
|
import { classifyMergeTree } from "./merge-tree-classifier.js";
|
|
48
50
|
import { RECONCILE_OVERLAP_PAYLOAD_SCHEMA_VERSION, } from "./reconcile-overlap-types.js";
|
|
51
|
+
import { runRequiredVerificationCommands } from "./required-verification.js";
|
|
49
52
|
import { validateWorkerFinalization } from "./worker-finalization.js";
|
|
50
53
|
export const RECONCILE_OVERLAP_JOB_TYPE = "reconcile_overlap";
|
|
51
54
|
/** Result-envelope bounds, mirrored from the server's result model. */
|
|
@@ -88,14 +91,6 @@ const WRAPPER_STAGE_EXCLUDES = [
|
|
|
88
91
|
":(exclude).claude/settings.local.json",
|
|
89
92
|
];
|
|
90
93
|
const SHA_RE = /^(?:[0-9a-f]{40}|[0-9a-f]{64})$/;
|
|
91
|
-
const FAILED_CHECK_CONCLUSIONS = new Set([
|
|
92
|
-
"failure",
|
|
93
|
-
"cancelled",
|
|
94
|
-
"timed_out",
|
|
95
|
-
"action_required",
|
|
96
|
-
"startup_failure",
|
|
97
|
-
"stale",
|
|
98
|
-
]);
|
|
99
94
|
const PUSH_AUTH_FAILURE_RE = /authentication failed|could not read username|permission denied|terminal prompts disabled|invalid username or password|returned error: 403|requested url returned error: 401/i;
|
|
100
95
|
const PUSH_LEASE_FAILURE_RE = /stale info|\[rejected\]|non-fast-forward|fetch first/i;
|
|
101
96
|
class PayloadError extends Error {
|
|
@@ -259,28 +254,6 @@ export function validateReconcileOverlapPayload(job) {
|
|
|
259
254
|
/** Thrown between steps when the supervising phase has been aborted. */
|
|
260
255
|
export class ReconcileOverlapAborted extends Error {
|
|
261
256
|
}
|
|
262
|
-
/** Thrown by a provider-check poller that has no Bridge API access at all. */
|
|
263
|
-
export class ProviderChecksUnobservable extends Error {
|
|
264
|
-
}
|
|
265
|
-
/**
|
|
266
|
-
* A provider-check poller that resolves Bridge API access once, on first use —
|
|
267
|
-
* the same read-only CI poll the `merge` job waits on. Without access it throws
|
|
268
|
-
* {@link ProviderChecksUnobservable}, and the wait ends immediately rather than
|
|
269
|
-
* sleeping out the job budget; the server's completion contract then reads the
|
|
270
|
-
* checks itself.
|
|
271
|
-
*/
|
|
272
|
-
export function lazyProviderCheckPoller(accessDeps) {
|
|
273
|
-
let access;
|
|
274
|
-
return async (sha) => {
|
|
275
|
-
if (access === undefined) {
|
|
276
|
-
const resolved = await resolveConductorBridgeApiAccess(accessDeps);
|
|
277
|
-
access = resolved.ok ? resolved.access : null;
|
|
278
|
-
}
|
|
279
|
-
if (access === null)
|
|
280
|
-
throw new ProviderChecksUnobservable("bridge api access unavailable");
|
|
281
|
-
return pollCiChecksForCommit(access, sha);
|
|
282
|
-
};
|
|
283
|
-
}
|
|
284
257
|
function bounded(text, max) {
|
|
285
258
|
if (text.length <= max)
|
|
286
259
|
return text;
|
|
@@ -290,6 +263,18 @@ function bounded(text, max) {
|
|
|
290
263
|
function tail(text) {
|
|
291
264
|
return text.length <= COMMAND_OUTPUT_TAIL_CHARS ? text : "…" + text.slice(-COMMAND_OUTPUT_TAIL_CHARS);
|
|
292
265
|
}
|
|
266
|
+
/**
|
|
267
|
+
* The overlap result contract's conclusion for each shared-core outcome. The
|
|
268
|
+
* vocabulary is this lane's own and predates BAPI-1302; it does not change.
|
|
269
|
+
*/
|
|
270
|
+
const OVERLAP_CONCLUSIONS = {
|
|
271
|
+
exited_zero: "success",
|
|
272
|
+
exited_nonzero: "failure",
|
|
273
|
+
timed_out: "timed_out",
|
|
274
|
+
launch_error: "error",
|
|
275
|
+
cancelled: "error",
|
|
276
|
+
not_run: "not_run",
|
|
277
|
+
};
|
|
293
278
|
/** The deduplicated union of both tickets' commands, child first. */
|
|
294
279
|
export function verificationCommandUnion(child, peer) {
|
|
295
280
|
const seen = new Set();
|
|
@@ -329,24 +314,6 @@ export function buildReconcileEnvelope(payload, facts, extra = {}) {
|
|
|
329
314
|
}
|
|
330
315
|
return envelope;
|
|
331
316
|
}
|
|
332
|
-
function anyRequiredCheckFailed(pollResponse, required) {
|
|
333
|
-
if (pollResponse === null || typeof pollResponse !== "object")
|
|
334
|
-
return false;
|
|
335
|
-
let obj = pollResponse;
|
|
336
|
-
const detail = obj.detail;
|
|
337
|
-
if (detail && typeof detail === "object" && Array.isArray(detail.checks)) {
|
|
338
|
-
obj = detail;
|
|
339
|
-
}
|
|
340
|
-
const checks = Array.isArray(obj.checks) ? obj.checks : [];
|
|
341
|
-
const wanted = new Set(required);
|
|
342
|
-
return checks.some((c) => {
|
|
343
|
-
if (!c || typeof c !== "object")
|
|
344
|
-
return false;
|
|
345
|
-
const name = typeof c.name === "string" ? c.name : "";
|
|
346
|
-
const conclusion = typeof c.conclusion === "string" ? c.conclusion.toLowerCase() : "";
|
|
347
|
-
return wanted.has(name) && FAILED_CHECK_CONCLUSIONS.has(conclusion);
|
|
348
|
-
});
|
|
349
|
-
}
|
|
350
317
|
// ---------------------------------------------------------------------------
|
|
351
318
|
// The driver
|
|
352
319
|
// ---------------------------------------------------------------------------
|
|
@@ -469,43 +436,69 @@ export function createReconcileOverlapDriver(job, payload, seams) {
|
|
|
469
436
|
}
|
|
470
437
|
return lines.join("\n");
|
|
471
438
|
};
|
|
472
|
-
/**
|
|
439
|
+
/**
|
|
440
|
+
* Run the union of local verification commands. Never a provider check.
|
|
441
|
+
*
|
|
442
|
+
* BAPI-1302: the subprocess work goes through the shared list-based core in
|
|
443
|
+
* `required-verification.ts`. Everything that is overlap POLICY stays here:
|
|
444
|
+
* the child/peer union and its order, the `CI_WAIT_MARGIN_MS` reservation,
|
|
445
|
+
* running every command even after one fails or times out, the conclusion
|
|
446
|
+
* vocabulary its result contract reports, and the worker environment passed
|
|
447
|
+
* explicitly on every check. The launch seam keeps this lane's historical
|
|
448
|
+
* `runCommand` behavior, including its elapsed-time timeout reading.
|
|
449
|
+
*/
|
|
473
450
|
const runVerification = async (ctx) => {
|
|
474
|
-
|
|
475
|
-
|
|
476
|
-
|
|
477
|
-
|
|
478
|
-
const
|
|
479
|
-
|
|
480
|
-
|
|
481
|
-
|
|
482
|
-
|
|
451
|
+
if (ctx.signal.aborted)
|
|
452
|
+
throw new ReconcileOverlapAborted("reconciliation phase aborted");
|
|
453
|
+
const launch = async (request) => {
|
|
454
|
+
const [file, ...args] = request.argv;
|
|
455
|
+
const started = seams.now();
|
|
456
|
+
try {
|
|
457
|
+
const result = await seams.runCommand(file, args, {
|
|
458
|
+
cwd: request.cwd,
|
|
459
|
+
timeoutMs: request.timeoutMs,
|
|
460
|
+
// The SAME secret-free environment a worker gets, including the
|
|
461
|
+
// sentinel database target: a verification command can no more reach
|
|
462
|
+
// the operator's real database than a worker can.
|
|
463
|
+
env: request.env,
|
|
464
|
+
});
|
|
465
|
+
const output = `${result.stdout}\n${result.stderr}`;
|
|
466
|
+
if (result.exitCode === 0)
|
|
467
|
+
return { outcome: { kind: "exited", exitCode: 0 }, output };
|
|
468
|
+
return seams.now() - started >= request.timeoutMs - 50
|
|
469
|
+
? { outcome: { kind: "timed_out" }, output }
|
|
470
|
+
: { outcome: { kind: "exited", exitCode: result.exitCode }, output };
|
|
483
471
|
}
|
|
484
|
-
|
|
485
|
-
|
|
486
|
-
try {
|
|
487
|
-
const [file, ...args] = command.argv;
|
|
488
|
-
const result = await seams.runCommand(file, args, {
|
|
489
|
-
cwd: ctx.worktreePath,
|
|
490
|
-
timeoutMs: budget,
|
|
491
|
-
// The SAME secret-free environment a worker gets, including the
|
|
492
|
-
// sentinel database target: a verification command can no more reach
|
|
493
|
-
// the operator's real database than a worker can.
|
|
494
|
-
env: workerEnv,
|
|
495
|
-
});
|
|
496
|
-
output = `${result.stdout}\n${result.stderr}`;
|
|
497
|
-
if (result.exitCode === 0)
|
|
498
|
-
conclusion = "success";
|
|
499
|
-
else
|
|
500
|
-
conclusion = seams.now() - started >= budget - 50 ? "timed_out" : "failure";
|
|
501
|
-
}
|
|
502
|
-
catch {
|
|
503
|
-
conclusion = "error";
|
|
504
|
-
}
|
|
472
|
+
catch {
|
|
473
|
+
return { outcome: { kind: "launch_error" } };
|
|
505
474
|
}
|
|
506
|
-
|
|
507
|
-
|
|
508
|
-
|
|
475
|
+
};
|
|
476
|
+
const { facts: runFacts } = await runRequiredVerificationCommands({
|
|
477
|
+
commands: commands.map((command, position) => ({
|
|
478
|
+
identity: `overlap-${position}`,
|
|
479
|
+
position,
|
|
480
|
+
name: command.name,
|
|
481
|
+
argv: command.argv,
|
|
482
|
+
timeoutSeconds: command.timeoutSeconds,
|
|
483
|
+
})),
|
|
484
|
+
worktreePath: ctx.worktreePath,
|
|
485
|
+
env: workerEnv,
|
|
486
|
+
launch,
|
|
487
|
+
now: seams.now,
|
|
488
|
+
// The overlap lane keeps its CI margin: checks may use only what is left
|
|
489
|
+
// after reserving the post-push CI wait.
|
|
490
|
+
deadlineAtMs: seams.now() + ctx.remainingMs() - CI_WAIT_MARGIN_MS,
|
|
491
|
+
signal: ctx.signal,
|
|
492
|
+
onInterruption: "continue",
|
|
493
|
+
});
|
|
494
|
+
if (ctx.signal.aborted)
|
|
495
|
+
throw new ReconcileOverlapAborted("reconciliation phase aborted");
|
|
496
|
+
const runs = [];
|
|
497
|
+
for (const fact of runFacts) {
|
|
498
|
+
const conclusion = OVERLAP_CONCLUSIONS[fact.outcome];
|
|
499
|
+
const observation = { name: fact.name, conclusion };
|
|
500
|
+
facts.checks.set(fact.name, conclusion);
|
|
501
|
+
runs.push({ observation, green: conclusion === "success", output: fact.output });
|
|
509
502
|
}
|
|
510
503
|
return runs;
|
|
511
504
|
};
|