@bridge_gpt/mcp-server 0.2.41 → 0.2.43
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 +330 -191
- package/build/agent-capabilities/cli.js +2 -1
- package/build/agent-launchers/claude-executor-adapter.js +17 -4
- package/build/agents.generated.js +2 -2
- package/build/claude-review-workflow.js +510 -45
- package/build/claude-user-config-doctor.js +42 -11
- package/build/cli-release.js +2 -1
- package/build/commands.generated.js +6 -5
- package/build/conduct-epic/bridge-client.js +354 -113
- package/build/conduct-epic/checkpoint-store.js +17 -0
- package/build/conduct-epic/cli.js +947 -99
- package/build/conduct-epic/cut-protocol.js +327 -0
- package/build/conduct-epic/spawn.js +14 -2
- package/build/conductor/bridge-api-client.js +148 -1
- package/build/conductor/cli.js +109 -1
- package/build/conductor/doctor.js +101 -16
- package/build/conductor/epic-reconcile.js +72 -19
- package/build/conductor/epic-runtime.js +15 -3
- package/build/conductor/errors.js +47 -0
- package/build/conductor/git-hooks.js +205 -11
- package/build/conductor/install-doctor.js +230 -1
- package/build/conductor/local-merge.js +130 -28
- package/build/conductor/recovery-cli.js +313 -0
- package/build/conductor/recovery-operations.js +219 -0
- package/build/conductor/tools.js +32 -3
- package/build/conductor/worker-ledger-cli.js +27 -1
- package/build/conductor-bin.js +20 -16
- package/build/credentials-cli.js +3 -2
- package/build/docs.generated.js +2 -1
- package/build/doctor.js +120 -44
- package/build/drive-epic.js +375 -0
- package/build/executor/cli.js +48 -1
- package/build/executor/env.js +21 -0
- package/build/executor/http-client.js +71 -3
- package/build/executor/index-scope.js +39 -0
- package/build/executor/job-errors.js +9 -0
- package/build/executor/job-log-registry.js +69 -0
- package/build/executor/job-runner.js +198 -29
- package/build/executor/live-worker-registry.js +83 -0
- package/build/executor/observation.js +259 -6
- package/build/executor/platform.js +147 -3
- package/build/executor/process.js +58 -14
- package/build/executor/runner.js +454 -48
- package/build/executor/test-clock.js +3 -2
- package/build/executor/worker-finalization.js +233 -56
- package/build/executor/worktree.js +8 -1
- package/build/index-scope-contract.js +96 -0
- package/build/index.js +2277 -270
- package/build/init.js +83 -22
- package/build/install-bridge-conductor.js +323 -14
- package/build/install-bridge.js +225 -47
- package/build/install-doctor.js +23 -9
- package/build/install-reexec.js +2 -1
- package/build/launcher-config-inspection.js +83 -22
- package/build/mcp-host-config.js +331 -67
- package/build/mcp-host-targets.js +45 -21
- package/build/mcp-identity.js +92 -0
- package/build/mcp-install-state.js +94 -1
- package/build/mcp-invoke.js +2 -1
- package/build/mcp-provisioning.js +45 -12
- package/build/mcp-registration-doctor.js +35 -13
- package/build/mcp-server-invocation.js +4 -2
- package/build/merge-pull-request.js +208 -9
- package/build/pipelines.generated.js +305 -15
- package/build/plane/cli.js +73 -7
- package/build/plane/defaults.js +18 -5
- package/build/plane/manifest.js +90 -0
- package/build/plane/preflight.js +100 -10
- package/build/plane/shutdown.js +71 -3
- package/build/plane/test-fakes.js +9 -1
- package/build/readme.generated.js +1 -1
- package/build/regression-check.js +3 -2
- package/build/review-tickets.js +8 -7
- package/build/run-unit-tests-launcher.js +149 -6
- package/build/schedule-run.js +3 -2
- package/build/setup-epic.js +531 -82
- package/build/sfcc/tool-wrapper.js +15 -0
- package/build/start-tickets-prereqs.js +11 -6
- package/build/start-tickets.js +91 -85
- package/build/update-check.js +3 -2
- package/build/upgrade-advice.js +2 -1
- package/build/upgrade-cli.js +50 -18
- package/build/version.generated.js +2 -1
- package/build/worktree-core.js +31 -17
- package/docs/CONDUCTOR.md +22 -0
- package/docs/install/mcp-tool-integrations.md +19 -3
- package/package.json +2 -2
- package/pipelines/greenfield-setup.json +286 -0
|
@@ -34,8 +34,29 @@
|
|
|
34
34
|
* the terminal-disposition classifier (`terminal_disposition.py`) can route it as
|
|
35
35
|
* a resumable, deterministic cause instead of laundering it into a generic crash
|
|
36
36
|
* that gets retried as a transient upstream failure.
|
|
37
|
+
*
|
|
38
|
+
* BAPI-862: a matching pushed head is NECESSARY but not SUFFICIENT. The BAPI-837
|
|
39
|
+
* run's finding F4 had BAPI-850's implement job push three commits, report plain
|
|
40
|
+
* success, and never open a pull request — every check above passed, so the gap
|
|
41
|
+
* only surfaced hours later as an implement gate polling `pr_not_attached`
|
|
42
|
+
* forever. Finalization therefore also requires an authoritative pull request for
|
|
43
|
+
* the branch: visible, carrying a usable URL, carrying a NON-BLANK description
|
|
44
|
+
* (BAPI-824 folded in — the VCS API never applies `.github/PULL_REQUEST_TEMPLATE.md`,
|
|
45
|
+
* so an omitted body opens a PR with no description at all), and — when the
|
|
46
|
+
* provider reports it — sitting at the same pushed head. Anything short of that
|
|
47
|
+
* is `WorkerFinalizationPrNotAttached`, whose guidance names attachment and never
|
|
48
|
+
* suggests removing or re-writing the branch: the commits ARE the completed
|
|
49
|
+
* implementation, and both of the stale-worktree guard's historical remedies
|
|
50
|
+
* would have destroyed them.
|
|
51
|
+
*
|
|
52
|
+
* This check is deliberately fail-CLOSED, unlike the BAPI-586 base check it now
|
|
53
|
+
* shares a lookup with. "The PR could not be verified" and "there is no PR" are
|
|
54
|
+
* the same thing from the contract's point of view: neither is evidence that the
|
|
55
|
+
* work was published, and laundering either into success is precisely the defect
|
|
56
|
+
* this ticket removes. The reconciler stays the decision-maker — the executor
|
|
57
|
+
* only reports the observation (R14 rule 3).
|
|
37
58
|
*/
|
|
38
|
-
import { secretFreeErrorMessage, WorkerFinalizationMissingRemoteBranchAndPr, WorkerFinalizationPrBaseMismatch, WorkerFinalizationSavedButUnfinalized, } from "./job-errors.js";
|
|
59
|
+
import { secretFreeErrorMessage, WorkerFinalizationMissingRemoteBranchAndPr, WorkerFinalizationPrBaseMismatch, WorkerFinalizationPrNotAttached, WorkerFinalizationSavedButUnfinalized, } from "./job-errors.js";
|
|
39
60
|
import { isImplementationStyleJobType } from "./job-types.js";
|
|
40
61
|
/** Bounded settling re-check defaults for the authoritative origin-tip lookup. */
|
|
41
62
|
const DEFAULT_ORIGIN_FINALIZATION_ATTEMPTS = 3;
|
|
@@ -115,51 +136,73 @@ async function resolveOriginBranchShaForFinalization(runCommand, worktreePath, b
|
|
|
115
136
|
remoteSha = await resolveOriginBranchSha(runCommand, worktreePath, branch);
|
|
116
137
|
if (remoteSha !== null) {
|
|
117
138
|
// Existence-only success when HEAD is unknown; exact-match success otherwise.
|
|
118
|
-
if (!trimmedHeadSha || remoteSha === trimmedHeadSha)
|
|
119
|
-
return remoteSha;
|
|
139
|
+
if (!trimmedHeadSha || remoteSha === trimmedHeadSha) {
|
|
140
|
+
return { sha: remoteSha, attemptsUsed: attempt + 1 };
|
|
141
|
+
}
|
|
120
142
|
}
|
|
121
143
|
if (attempt < total - 1)
|
|
122
144
|
await sleep(retryDelayMs);
|
|
123
145
|
}
|
|
124
|
-
|
|
146
|
+
// `attemptsUsed` is reported even on failure so the caller can tell a
|
|
147
|
+
// first-try answer from one that needed the settling window (BAPI-862).
|
|
148
|
+
return { sha: remoteSha, attemptsUsed: total };
|
|
149
|
+
}
|
|
150
|
+
/** The bounded, public projection finalization asks the provider for. */
|
|
151
|
+
const PR_VIEW_JSON_FIELDS = "number,url,baseRefName,body,headRefOid";
|
|
152
|
+
/** Trim a string field to a non-empty value, or null. Never throws on odd JSON. */
|
|
153
|
+
function nonBlankString(value) {
|
|
154
|
+
if (typeof value !== "string")
|
|
155
|
+
return null;
|
|
156
|
+
const trimmed = value.trim();
|
|
157
|
+
return trimmed.length > 0 ? trimmed : null;
|
|
125
158
|
}
|
|
159
|
+
/** The "no PR observed" reading, used for every unavailability. */
|
|
160
|
+
const PR_LOOKUP_ABSENT = {
|
|
161
|
+
found: false,
|
|
162
|
+
baseRef: null,
|
|
163
|
+
url: null,
|
|
164
|
+
body: null,
|
|
165
|
+
headRefOid: null,
|
|
166
|
+
};
|
|
126
167
|
/**
|
|
127
|
-
*
|
|
128
|
-
*
|
|
129
|
-
*
|
|
130
|
-
*
|
|
131
|
-
* When a PR IS visible, `baseRef` is the trimmed `baseRefName`, or null if that
|
|
132
|
-
* field is absent/non-string (which the caller treats as a fail-closed mismatch —
|
|
133
|
-
* missing base evidence is never a match). Never copies raw stderr.
|
|
168
|
+
* Resolve the pull request for `branch` via a single `gh pr view`. Parses ONLY
|
|
169
|
+
* the bounded public fields in {@link PR_VIEW_JSON_FIELDS}; every failure mode
|
|
170
|
+
* collapses to {@link PR_LOOKUP_ABSENT} without copying raw stderr, an
|
|
171
|
+
* authenticated remote, or the command line into anything the caller can render.
|
|
134
172
|
*/
|
|
135
|
-
async function
|
|
173
|
+
async function resolvePrForFinalization(runCommand, worktreePath, branch) {
|
|
136
174
|
const args = ["pr", "view"];
|
|
137
175
|
if (branch)
|
|
138
176
|
args.push(branch);
|
|
139
|
-
args.push("--json",
|
|
177
|
+
args.push("--json", PR_VIEW_JSON_FIELDS);
|
|
140
178
|
let result;
|
|
141
179
|
try {
|
|
142
180
|
result = await runCommand("gh", args, { cwd: worktreePath });
|
|
143
181
|
}
|
|
144
182
|
catch {
|
|
145
|
-
return
|
|
183
|
+
return PR_LOOKUP_ABSENT;
|
|
146
184
|
}
|
|
147
185
|
if (result.exitCode !== 0)
|
|
148
|
-
return
|
|
186
|
+
return PR_LOOKUP_ABSENT;
|
|
149
187
|
let parsed;
|
|
150
188
|
try {
|
|
151
189
|
parsed = JSON.parse(result.stdout);
|
|
152
190
|
}
|
|
153
191
|
catch {
|
|
154
|
-
return
|
|
192
|
+
return PR_LOOKUP_ABSENT;
|
|
155
193
|
}
|
|
156
194
|
if (!parsed || typeof parsed !== "object")
|
|
157
|
-
return
|
|
195
|
+
return PR_LOOKUP_ABSENT;
|
|
158
196
|
const obj = parsed;
|
|
159
|
-
const
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
197
|
+
const rawHead = nonBlankString(obj.headRefOid);
|
|
198
|
+
return {
|
|
199
|
+
found: true,
|
|
200
|
+
prNumber: typeof obj.number === "number" ? obj.number : undefined,
|
|
201
|
+
baseRef: nonBlankString(obj.baseRefName),
|
|
202
|
+
url: nonBlankString(obj.url),
|
|
203
|
+
body: nonBlankString(obj.body),
|
|
204
|
+
headRefOid: rawHead === null ? null : rawHead.toLowerCase(),
|
|
205
|
+
};
|
|
163
206
|
}
|
|
164
207
|
/**
|
|
165
208
|
* BAPI-586: bounded, secret-free failure for a PR that targets a branch other
|
|
@@ -183,6 +226,102 @@ function prBaseMismatchFailure(job, prNumber, expectedBase, actualBase) {
|
|
|
183
226
|
classification: "crashed",
|
|
184
227
|
};
|
|
185
228
|
}
|
|
229
|
+
/**
|
|
230
|
+
* BAPI-586's base check over one PR observation, or `null` when it does not fire.
|
|
231
|
+
*
|
|
232
|
+
* Extracted (BAPI-862) so it can run against a REFRESHED observation as well as
|
|
233
|
+
* the first one. Semantics are unchanged: no expected base, or no PR visible,
|
|
234
|
+
* means no verdict at all (fail-open on unavailability), while a visible PR with
|
|
235
|
+
* absent or malformed base data is a fail-CLOSED mismatch.
|
|
236
|
+
*/
|
|
237
|
+
function evaluatePrBase(job, pr, expectedBase) {
|
|
238
|
+
if (!expectedBase || !pr.found)
|
|
239
|
+
return null;
|
|
240
|
+
if (pr.baseRef === null) {
|
|
241
|
+
return prBaseMismatchFailure(job, pr.prNumber, expectedBase, "(unresolved)");
|
|
242
|
+
}
|
|
243
|
+
if (pr.baseRef !== expectedBase) {
|
|
244
|
+
return prBaseMismatchFailure(job, pr.prNumber, expectedBase, pr.baseRef);
|
|
245
|
+
}
|
|
246
|
+
return null;
|
|
247
|
+
}
|
|
248
|
+
/**
|
|
249
|
+
* Evaluate one PR observation against the implement-completion contract.
|
|
250
|
+
*
|
|
251
|
+
* Reads STRUCTURED fields only — never a rendered message, never worker prose.
|
|
252
|
+
* `workerHeadSha` is the already-normalized worker HEAD that the authoritative
|
|
253
|
+
* origin tip has ALREADY been proven to match, so a `headRefOid` disagreement
|
|
254
|
+
* means the provider has not yet reflected the push, not that the push is in
|
|
255
|
+
* doubt.
|
|
256
|
+
*/
|
|
257
|
+
function evaluatePrContract(pr, workerHeadSha) {
|
|
258
|
+
if (!pr.found) {
|
|
259
|
+
return {
|
|
260
|
+
ok: false,
|
|
261
|
+
detail: "no pull request could be observed for that branch",
|
|
262
|
+
retryable: true,
|
|
263
|
+
};
|
|
264
|
+
}
|
|
265
|
+
if (pr.url === null) {
|
|
266
|
+
return {
|
|
267
|
+
ok: false,
|
|
268
|
+
detail: "its pull request reports no usable URL",
|
|
269
|
+
retryable: false,
|
|
270
|
+
};
|
|
271
|
+
}
|
|
272
|
+
if (pr.body === null) {
|
|
273
|
+
// BAPI-824, folded in here. The VCS API never applies the repository's
|
|
274
|
+
// PULL_REQUEST_TEMPLATE.md, so a pull request opened without a composed body
|
|
275
|
+
// has NO description at all. The server-side `create_pull_request` contract
|
|
276
|
+
// already rejects a blank body with a 422; this second layer catches a pull
|
|
277
|
+
// request opened some other way — by hand, by an older client, or by a future
|
|
278
|
+
// recovery path — before it can satisfy the success contract.
|
|
279
|
+
return {
|
|
280
|
+
ok: false,
|
|
281
|
+
detail: "its pull request has an empty description",
|
|
282
|
+
retryable: false,
|
|
283
|
+
};
|
|
284
|
+
}
|
|
285
|
+
if (workerHeadSha && pr.headRefOid !== null && pr.headRefOid !== workerHeadSha) {
|
|
286
|
+
return {
|
|
287
|
+
ok: false,
|
|
288
|
+
detail: `its pull request is still at ${pr.headRefOid.slice(0, 12)} and does not yet carry that commit`,
|
|
289
|
+
retryable: true,
|
|
290
|
+
};
|
|
291
|
+
}
|
|
292
|
+
return { ok: true };
|
|
293
|
+
}
|
|
294
|
+
/**
|
|
295
|
+
* BAPI-862: the pushed-but-unattached failure.
|
|
296
|
+
*
|
|
297
|
+
* The message is built ONLY from public identifiers — ticket key, job id, branch,
|
|
298
|
+
* truncated SHAs, the run base — plus a fixed detail phrase, so no provider
|
|
299
|
+
* output, URL, or credential can ride along. Two properties are load-bearing
|
|
300
|
+
* rather than stylistic:
|
|
301
|
+
*
|
|
302
|
+
* 1. It names `pr_not_attached`, the reason code the implement gate already uses,
|
|
303
|
+
* so the executor failure and the reconciler's observation are searchable as
|
|
304
|
+
* one state instead of two vocabularies for the same thing.
|
|
305
|
+
* 2. It recommends attachment and says plainly that removing or re-writing the
|
|
306
|
+
* branch destroys completed work. In the F4 incident the pushed branch WAS the
|
|
307
|
+
* finished implementation, and both remedies the stale-worktree guard offered
|
|
308
|
+
* would have thrown it away.
|
|
309
|
+
*/
|
|
310
|
+
function prNotAttachedFailure(job, branch, workerHeadSha, expectedBase, detail) {
|
|
311
|
+
const label = job.ticket_key ? `${job.ticket_key} (job ${job.id})` : `job ${job.id}`;
|
|
312
|
+
const headLabel = workerHeadSha ? ` at ${workerHeadSha.slice(0, 12)}` : "";
|
|
313
|
+
const baseLabel = expectedBase ? ` targeting '${expectedBase}'` : "";
|
|
314
|
+
return {
|
|
315
|
+
error_kind: WorkerFinalizationPrNotAttached,
|
|
316
|
+
error_message: `${label} exited cleanly and its HEAD${headLabel} is durable on origin branch '${branch}', ` +
|
|
317
|
+
`but ${detail}. This is the pr_not_attached state: the implementation is COMPLETE and ` +
|
|
318
|
+
`published, only its pull request is missing. Recovery: open or attach a pull request for ` +
|
|
319
|
+
`'${branch}'${baseLabel} with a non-empty description. Removing or re-writing that branch ` +
|
|
320
|
+
`would destroy finished work — the commits on it are the implementation.`,
|
|
321
|
+
classification: "crashed",
|
|
322
|
+
last_commit_sha: workerHeadSha || undefined,
|
|
323
|
+
};
|
|
324
|
+
}
|
|
186
325
|
/**
|
|
187
326
|
* `detail` describes a state with NO evidence any work reached origin (branch
|
|
188
327
|
* unresolved, branch absent from origin, or the origin lookup itself failed).
|
|
@@ -224,47 +363,26 @@ function savedButUnfinalizedFailure(job, branch, originSha, workerHeadSha) {
|
|
|
224
363
|
}
|
|
225
364
|
/**
|
|
226
365
|
* Validate that an implementation-style job's clean exit actually published its
|
|
227
|
-
* work to origin. Non-implementation-style jobs
|
|
228
|
-
*
|
|
229
|
-
* (BAPI-762) — the authoritative origin-tip-vs-HEAD
|
|
230
|
-
* so an unpushed local commit is detected whether
|
|
366
|
+
* work to origin AND left an attached pull request. Non-implementation-style jobs
|
|
367
|
+
* (verdict jobs) bypass both checks entirely. A `pr_url` on the completion result
|
|
368
|
+
* is NOT proof of a push (BAPI-762) — the authoritative origin-tip-vs-HEAD
|
|
369
|
+
* comparison below always runs, so an unpushed local commit is detected whether
|
|
370
|
+
* or not the worker claims a pull request; and symmetrically, a pushed head is
|
|
371
|
+
* not proof of a pull request (BAPI-862), so the attachment contract runs after
|
|
372
|
+
* it. Both halves read only durable provider/git facts the executor observed
|
|
373
|
+
* itself.
|
|
231
374
|
*/
|
|
232
375
|
export async function validateWorkerFinalization(input) {
|
|
233
376
|
const { job, branch, worktreePath, result, runCommand, headSha } = input;
|
|
234
377
|
if (!isImplementationStyleJobType(job.job_type)) {
|
|
235
378
|
return { ok: true };
|
|
236
379
|
}
|
|
237
|
-
// BAPI-586: a wrong-base PR (opened against a dependency's feature branch or the
|
|
238
|
-
// repo default instead of the run base) never fires `claude-review.yml` and
|
|
239
|
-
// strands the ticket at `code_review`. When the run base is known, query the
|
|
240
|
-
// PR's `baseRefName` and fail loud on any mismatch BEFORE accepting the clean
|
|
241
|
-
// exit — even when a `pr_url` is present (a PR existing is not proof of a
|
|
242
|
-
// correct base). Fail-open when no PR is visible (gh unavailable / no PR):
|
|
243
|
-
// reconciliation is the backstop. A PR with absent/malformed base data is a
|
|
244
|
-
// fail-CLOSED mismatch — missing base evidence is never treated as a match.
|
|
245
|
-
const expectedBase = typeof input.expectedBaseBranch === "string" ? input.expectedBaseBranch.trim() : "";
|
|
246
|
-
if (expectedBase) {
|
|
247
|
-
const branchForPr = typeof branch === "string" ? branch.trim() : "";
|
|
248
|
-
const prBase = await resolvePrBaseRef(runCommand, worktreePath, branchForPr);
|
|
249
|
-
if (prBase.found) {
|
|
250
|
-
if (prBase.baseRef === null) {
|
|
251
|
-
return {
|
|
252
|
-
ok: false,
|
|
253
|
-
failure: prBaseMismatchFailure(job, prBase.prNumber, expectedBase, "(unresolved)"),
|
|
254
|
-
};
|
|
255
|
-
}
|
|
256
|
-
if (prBase.baseRef !== expectedBase) {
|
|
257
|
-
return {
|
|
258
|
-
ok: false,
|
|
259
|
-
failure: prBaseMismatchFailure(job, prBase.prNumber, expectedBase, prBase.baseRef),
|
|
260
|
-
};
|
|
261
|
-
}
|
|
262
|
-
}
|
|
263
|
-
}
|
|
264
380
|
const hasPrUrl = Boolean(extractPrUrl(result));
|
|
265
381
|
// BAPI-762: `branch`/`headSha` are normalized once here and reused through the
|
|
266
382
|
// rest of the check — including by the diagnostics below — instead of being
|
|
267
|
-
// re-derived at each failure site.
|
|
383
|
+
// re-derived at each failure site. This purely local resolution runs FIRST
|
|
384
|
+
// (BAPI-862) so an unresolvable branch is reported without issuing a single
|
|
385
|
+
// provider or git query against the wrong subject.
|
|
268
386
|
const trimmedBranch = typeof branch === "string" ? branch.trim() : "";
|
|
269
387
|
if (!trimmedBranch) {
|
|
270
388
|
return {
|
|
@@ -272,6 +390,27 @@ export async function validateWorkerFinalization(input) {
|
|
|
272
390
|
failure: missingBranchAndPrFailure(job, "no expected branch to verify on origin", hasPrUrl),
|
|
273
391
|
};
|
|
274
392
|
}
|
|
393
|
+
// BAPI-862 folded the two PR-reading guards onto ONE `gh pr view`: the base
|
|
394
|
+
// check below and the attachment contract at the end of this function read the
|
|
395
|
+
// same observation. The lookup runs for every implementation-style job now —
|
|
396
|
+
// not only when a run base is known — because the attachment contract is
|
|
397
|
+
// unconditional. Their dispositions still differ on purpose (see `PrLookup`),
|
|
398
|
+
// and the ORDER is unchanged: a wrong-base PR is still reported before any
|
|
399
|
+
// origin lookup, exactly as BAPI-586 specified.
|
|
400
|
+
let pr = await resolvePrForFinalization(runCommand, worktreePath, trimmedBranch);
|
|
401
|
+
// BAPI-586: a wrong-base PR (opened against a dependency's feature branch or the
|
|
402
|
+
// repo default instead of the run base) never fires `claude-review.yml` and
|
|
403
|
+
// strands the ticket at `code_review`. When the run base is known, fail loud on
|
|
404
|
+
// any mismatch BEFORE the origin lookup below — even when a `pr_url` is present
|
|
405
|
+
// (a PR existing is not proof of a correct base). Still fail-OPEN when no PR is
|
|
406
|
+
// visible: reconciliation is the backstop for that, and BAPI-862's attachment
|
|
407
|
+
// contract at the end of this function is what now refuses to call it a success.
|
|
408
|
+
// A PR with absent/malformed base data stays a fail-CLOSED mismatch — missing
|
|
409
|
+
// base evidence is never treated as a match.
|
|
410
|
+
const expectedBase = typeof input.expectedBaseBranch === "string" ? input.expectedBaseBranch.trim() : "";
|
|
411
|
+
const baseVerdict = evaluatePrBase(job, pr, expectedBase);
|
|
412
|
+
if (baseVerdict)
|
|
413
|
+
return { ok: false, failure: baseVerdict };
|
|
275
414
|
// Recovery/resume jobs fetch `origin/<branch>` before the worker even starts,
|
|
276
415
|
// so the ref merely existing is not proof this session's commit landed. When
|
|
277
416
|
// the worker's own HEAD is known, require the authoritative origin tip to match
|
|
@@ -286,9 +425,9 @@ export async function validateWorkerFinalization(input) {
|
|
|
286
425
|
const attempts = normalizeAttempts(input.originResolveAttempts);
|
|
287
426
|
const retryDelayMs = normalizeRetryDelay(input.originResolveRetryDelayMs);
|
|
288
427
|
const sleep = input.sleep ?? sleepMs;
|
|
289
|
-
let
|
|
428
|
+
let origin;
|
|
290
429
|
try {
|
|
291
|
-
|
|
430
|
+
origin = await resolveOriginBranchShaForFinalization(runCommand, worktreePath, trimmedBranch, trimmedHeadSha, attempts, retryDelayMs, sleep);
|
|
292
431
|
}
|
|
293
432
|
catch (err) {
|
|
294
433
|
return {
|
|
@@ -296,6 +435,7 @@ export async function validateWorkerFinalization(input) {
|
|
|
296
435
|
failure: missingBranchAndPrFailure(job, `branch '${trimmedBranch}' could not be verified on origin: ${secretFreeErrorMessage(err)}`, hasPrUrl),
|
|
297
436
|
};
|
|
298
437
|
}
|
|
438
|
+
const remoteSha = origin.sha;
|
|
299
439
|
if (remoteSha === null) {
|
|
300
440
|
return {
|
|
301
441
|
ok: false,
|
|
@@ -314,5 +454,42 @@ export async function validateWorkerFinalization(input) {
|
|
|
314
454
|
failure: savedButUnfinalizedFailure(job, trimmedBranch, remoteSha, trimmedHeadSha),
|
|
315
455
|
};
|
|
316
456
|
}
|
|
317
|
-
|
|
457
|
+
// BAPI-862: the work is provably ON ORIGIN. That was the whole of the success
|
|
458
|
+
// contract until finding F4 showed it was not enough — an implement job can
|
|
459
|
+
// satisfy everything above and still leave nothing for the reconciler to bind
|
|
460
|
+
// to. The last obligation is an authoritative, usable pull request.
|
|
461
|
+
//
|
|
462
|
+
// Placed LAST on purpose: an unpushed or mismatched head is a different and
|
|
463
|
+
// more urgent failure, and reporting "no pull request" for work that never
|
|
464
|
+
// reached origin would send an operator to the wrong remedy. So every
|
|
465
|
+
// pre-existing failure keeps precedence and this one only fires once the push
|
|
466
|
+
// is proven.
|
|
467
|
+
// The snapshot above was taken BEFORE the origin lookup, because BAPI-586
|
|
468
|
+
// requires a wrong-base PR to be reported ahead of any git call. When the
|
|
469
|
+
// origin lookup then needed the settling window, that snapshot predates the
|
|
470
|
+
// very push we just confirmed and is stale by construction — so refresh it
|
|
471
|
+
// once, for free, rather than making the contract's own retry budget spend its
|
|
472
|
+
// first cycle recovering from known staleness. The refreshed observation is
|
|
473
|
+
// re-checked against the base too: if the stale snapshot saw no PR at all, the
|
|
474
|
+
// base check fail-opened on it, and the PR that has since appeared must not
|
|
475
|
+
// reach success without that check.
|
|
476
|
+
if (origin.attemptsUsed > 1) {
|
|
477
|
+
pr = await resolvePrForFinalization(runCommand, worktreePath, trimmedBranch);
|
|
478
|
+
const refreshedBaseVerdict = evaluatePrBase(job, pr, expectedBase);
|
|
479
|
+
if (refreshedBaseVerdict)
|
|
480
|
+
return { ok: false, failure: refreshedBaseVerdict };
|
|
481
|
+
}
|
|
482
|
+
let contract = evaluatePrContract(pr, trimmedHeadSha);
|
|
483
|
+
for (let attempt = 1; attempt < attempts && !contract.ok && contract.retryable; attempt++) {
|
|
484
|
+
await sleep(retryDelayMs);
|
|
485
|
+
pr = await resolvePrForFinalization(runCommand, worktreePath, trimmedBranch);
|
|
486
|
+
contract = evaluatePrContract(pr, trimmedHeadSha);
|
|
487
|
+
}
|
|
488
|
+
if (!contract.ok) {
|
|
489
|
+
return {
|
|
490
|
+
ok: false,
|
|
491
|
+
failure: prNotAttachedFailure(job, trimmedBranch, trimmedHeadSha, expectedBase, contract.detail),
|
|
492
|
+
};
|
|
493
|
+
}
|
|
494
|
+
return { ok: true, prUrl: pr.url ?? undefined, prNumber: pr.prNumber };
|
|
318
495
|
}
|
|
@@ -183,7 +183,14 @@ export async function ensureExecutorWorktree(job, options, deps, policy = {}) {
|
|
|
183
183
|
};
|
|
184
184
|
}
|
|
185
185
|
const baseSha = resolvedBase.base_sha;
|
|
186
|
-
const row = await createWorktreeForTicket(toWorktreeCoreDeps(deps), key, { [key]: branch }, options.worktrunkBinary, baseSha, guardStaleWorktree, {
|
|
186
|
+
const row = await createWorktreeForTicket(toWorktreeCoreDeps(deps), key, { [key]: branch }, options.worktrunkBinary, baseSha, guardStaleWorktree, {
|
|
187
|
+
alignExistingBranchTo: baseSha,
|
|
188
|
+
verifyHeadMatches: baseSha,
|
|
189
|
+
// BAPI-862: the guard is ON for this fresh-dispatch path, and this is the
|
|
190
|
+
// path finding F4 hit — a re-dispatched implement whose branch already
|
|
191
|
+
// carried the previous, successful attempt's pushed commits.
|
|
192
|
+
staleBranchClassification: policy.staleBranchClassification,
|
|
193
|
+
});
|
|
187
194
|
if (row.status === "created" && typeof row.path === "string") {
|
|
188
195
|
// BAPI-731: `baseSha` is already the pinned immutable commit this fresh
|
|
189
196
|
// worktree was cut from and verified against (`verifyHeadMatches`), so it is
|
|
@@ -0,0 +1,96 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The opaque index-scope declaration carried across a process boundary (BAPI-844).
|
|
3
|
+
*
|
|
4
|
+
* An index scope is the per-epic shadow index research is routed to. A pilot
|
|
5
|
+
* ticket has no `epic_ticket_status` / `epic_runs` row, so server-side membership
|
|
6
|
+
* discovery proves "non-epic" and routes to the canonical index — which is why the
|
|
7
|
+
* scope has to be DECLARED by the worker rather than discovered for it.
|
|
8
|
+
*
|
|
9
|
+
* This module owns the three names that declaration travels under, and nothing
|
|
10
|
+
* else:
|
|
11
|
+
*
|
|
12
|
+
* - {@link INDEX_SCOPE_ENV_VAR} — the worker environment variable,
|
|
13
|
+
* - {@link INDEX_SCOPE_HEADER} — the out-of-band Bridge API request header,
|
|
14
|
+
* - {@link validateOptionalIndexScope} — the shared shape check.
|
|
15
|
+
*
|
|
16
|
+
* TOKEN-OPAQUE BY CONSTRUCTION. The validator checks type, length, and shape and
|
|
17
|
+
* derives NOTHING from the value: not a repository, not a branch, not an epic run,
|
|
18
|
+
* not a shadow repository name. Those derivations are server-side, because the
|
|
19
|
+
* scope id is a server-minted identity (R14) and a worker that could compute a
|
|
20
|
+
* shadow name from it would be authorizing itself.
|
|
21
|
+
*
|
|
22
|
+
* VALIDATION IS NOT AUTHORIZATION. A well-formed declaration only means the value
|
|
23
|
+
* is shaped like a scope id. Whether the declaring worker may USE that scope is
|
|
24
|
+
* decided server-side, against the authenticated repository — see
|
|
25
|
+
* `api/agent_flows/code_writer/library/research_repo_resolver.py`. Nothing here
|
|
26
|
+
* may ever be read as a permission check.
|
|
27
|
+
*/
|
|
28
|
+
/**
|
|
29
|
+
* The environment variable a scoped worker carries.
|
|
30
|
+
*
|
|
31
|
+
* Deliberately NOT in `executor/env.ts`'s `ALLOWED_ENV_KEYS`, and explicitly on
|
|
32
|
+
* its `EXPLICIT_DENY_KEYS`: the value must arrive from an explicit, validated
|
|
33
|
+
* spawn option, never inherited from the executor's (or an operator's) ambient
|
|
34
|
+
* environment. This mirrors `BAPI_BASE_BRANCH` exactly — an ambient value that
|
|
35
|
+
* could override a run-scoped decision is the failure mode both keys avoid.
|
|
36
|
+
*/
|
|
37
|
+
export const INDEX_SCOPE_ENV_VAR = "BAPI_INDEX_SCOPE";
|
|
38
|
+
/**
|
|
39
|
+
* The Bridge API request header a scoped MCP worker attaches.
|
|
40
|
+
*
|
|
41
|
+
* Out-of-band on purpose. Every request body keeps the CANONICAL `repo_name`,
|
|
42
|
+
* because repository identity is what authorization, configuration, billing, and
|
|
43
|
+
* Jira side effects are keyed by; only research RETRIEVAL consumes this header.
|
|
44
|
+
*/
|
|
45
|
+
export const INDEX_SCOPE_HEADER = "X-Bapi-Index-Scope";
|
|
46
|
+
/**
|
|
47
|
+
* The server-minted scope-id shape: exactly 32 lowercase hexadecimal characters.
|
|
48
|
+
*
|
|
49
|
+
* Mirrors the server's own `_SCOPE_ID_RE` (`api/library/db/epic_shadow_index.py`),
|
|
50
|
+
* which is what `mint_scope_id()` (a `uuid4().hex`) produces. Anchored at both
|
|
51
|
+
* ends, so a longer string containing a valid token is rejected rather than
|
|
52
|
+
* partially matched.
|
|
53
|
+
*/
|
|
54
|
+
const INDEX_SCOPE_PATTERN = /^[0-9a-f]{32}$/;
|
|
55
|
+
/**
|
|
56
|
+
* The ONE configuration-error message every malformed declaration produces.
|
|
57
|
+
*
|
|
58
|
+
* Fixed and value-free by design: an error that echoed the submitted value would
|
|
59
|
+
* push an opaque routing token into terminal output, worker logs, conductor
|
|
60
|
+
* events, and error reports — all places this token must never appear. Every
|
|
61
|
+
* rejection is therefore indistinguishable in its text, which is deliberate.
|
|
62
|
+
*/
|
|
63
|
+
export const INDEX_SCOPE_CONFIGURATION_ERROR = `${INDEX_SCOPE_ENV_VAR} is not a valid index-scope declaration. ` +
|
|
64
|
+
"Expected a server-minted scope identity; the value was not logged.";
|
|
65
|
+
/** Thrown for a malformed declaration. Its message is always the fixed text above. */
|
|
66
|
+
export class IndexScopeConfigurationError extends Error {
|
|
67
|
+
constructor() {
|
|
68
|
+
super(INDEX_SCOPE_CONFIGURATION_ERROR);
|
|
69
|
+
this.name = "IndexScopeConfigurationError";
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
/**
|
|
73
|
+
* Validate an OPTIONAL scope declaration.
|
|
74
|
+
*
|
|
75
|
+
* Returns `undefined` for an absent declaration (`undefined`, `null`, or an
|
|
76
|
+
* empty/blank string) — absence is the ordinary unscoped case and must stay
|
|
77
|
+
* indistinguishable from today's behavior. Returns a well-formed value BYTE-FOR-BYTE
|
|
78
|
+
* unchanged: no trimming, no case folding, no normalization, because a value the
|
|
79
|
+
* server minted is the only value the server will recognize.
|
|
80
|
+
*
|
|
81
|
+
* Throws {@link IndexScopeConfigurationError} for anything else, including a
|
|
82
|
+
* non-string. Failing loudly is the point: silently dropping a malformed
|
|
83
|
+
* declaration would route a scoped worker's research at the canonical index while
|
|
84
|
+
* every observable signal still said "scoped".
|
|
85
|
+
*/
|
|
86
|
+
export function validateOptionalIndexScope(value) {
|
|
87
|
+
if (value === undefined || value === null)
|
|
88
|
+
return undefined;
|
|
89
|
+
if (typeof value !== "string")
|
|
90
|
+
throw new IndexScopeConfigurationError();
|
|
91
|
+
if (value.trim().length === 0)
|
|
92
|
+
return undefined;
|
|
93
|
+
if (!INDEX_SCOPE_PATTERN.test(value))
|
|
94
|
+
throw new IndexScopeConfigurationError();
|
|
95
|
+
return value;
|
|
96
|
+
}
|