@bridge_gpt/mcp-server 0.2.67 → 0.2.68
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 +178 -15
- package/build/agent-launchers/claude-executor-adapter.js +27 -0
- package/build/agents.generated.js +22 -2
- package/build/bitbucket-workspace-remote.js +136 -0
- package/build/bootstrap-project-identity.js +24 -0
- package/build/bridge-permissions.js +829 -0
- package/build/claude-settings.js +18 -0
- package/build/cli-help.js +21 -0
- package/build/command-assets-doctor.js +24 -70
- package/build/command-provisioning.js +29 -5
- package/build/commands.generated.js +8 -8
- package/build/conductor/bridge-api-client.js +714 -0
- package/build/conductor/command-drift-production.js +92 -0
- package/build/conductor/command-drift.js +89 -0
- package/build/conductor/doctor.js +69 -26
- package/build/conductor/epic-branch-ci-inspection.js +1375 -0
- package/build/conductor/epic-implementer-cli.js +76 -22
- package/build/conductor/gate-discovery.js +890 -0
- package/build/conductor/install-doctor.js +15 -3
- package/build/conductor/json-output.js +63 -0
- package/build/conductor/readiness-cli.js +95 -10
- package/build/conductor/readiness-sections.js +369 -3
- package/build/conductor/readiness.js +211 -8
- package/build/conductor/recovery-cli.js +110 -4
- package/build/conductor/recovery-operations.js +99 -1
- package/build/connect-bitbucket-api.js +41 -6
- package/build/connect-bitbucket.js +106 -33
- package/build/credential-store.js +207 -47
- package/build/docs.generated.js +57 -3
- package/build/doctor.js +92 -1
- package/build/drive-epic.js +45 -8
- package/build/epic-coherence-input.test-fixtures.js +4 -3
- package/build/epic-implementer-bin.js +1374 -25
- package/build/epic-workarounds-path.js +39 -0
- package/build/executor/cli.js +30 -0
- package/build/executor/completion-contract.js +18 -5
- package/build/executor/deps.js +4 -1
- package/build/executor/gate-qualification-job.js +499 -0
- package/build/executor/guarded-fast-forward-push.js +129 -0
- package/build/executor/integration-job.js +122 -8
- package/build/executor/job-errors.js +8 -1
- package/build/executor/job-runner.js +589 -47
- package/build/executor/likely-tests.js +290 -0
- package/build/executor/live-worker-registry.js +1 -1
- package/build/executor/observation.js +31 -2
- package/build/executor/packaged-recipe.js +106 -0
- package/build/executor/permissions.js +115 -28
- package/build/executor/platform.js +1 -1
- package/build/executor/preflight.js +61 -12
- package/build/executor/process-containment.js +36 -12
- package/build/executor/process.js +15 -7
- package/build/executor/prompt-spec.js +33 -1
- package/build/executor/recently-failed-tests-file.js +71 -0
- package/build/executor/reconcile-overlap-job.js +41 -23
- package/build/executor/required-verification.js +1 -1
- package/build/executor/required-work-ledger.js +105 -17
- package/build/executor/retirement.js +69 -0
- package/build/executor/runner.js +114 -11
- package/build/executor/test-clock.js +15 -0
- package/build/executor/types.js +32 -0
- package/build/executor/viewer-tabs.js +58 -8
- package/build/executor/worker-command.js +23 -7
- package/build/executor/worker-diagnostics.js +37 -0
- package/build/executor/worker-finalization.js +31 -5
- package/build/executor/worker-guard-hook.js +179 -25
- package/build/executor/worker-log.js +4 -3
- package/build/executor/worker-verification-guidance.js +37 -0
- package/build/executor/worktree.js +88 -11
- package/build/first-build-advisory.js +97 -0
- package/build/generated-assets-advisory.js +14 -0
- package/build/generated-assets-catalog.js +66 -0
- package/build/generated-assets-context.js +46 -0
- package/build/generated-assets-dependencies.js +26 -0
- package/build/generated-assets-doctor.js +50 -0
- package/build/generated-assets-health.js +65 -0
- package/build/generated-assets-host.js +17 -0
- package/build/generated-assets-inventory.js +17 -0
- package/build/generated-assets-paths.js +60 -0
- package/build/generated-assets-publisher.js +133 -0
- package/build/generated-assets-remedy.js +25 -0
- package/build/generated-assets-repair.js +72 -0
- package/build/generated-assets-session.js +71 -0
- package/build/generated-assets-test-support.js +35 -0
- package/build/index.js +964 -768
- package/build/init.js +54 -177
- package/build/install-bridge-conductor-gate.js +1372 -0
- package/build/install-bridge-conductor.js +289 -36
- package/build/install-bridge.js +544 -37
- package/build/mcp-install-state.js +50 -4
- package/build/merge-workflow-install.js +3 -4
- package/build/pipeline-utils.js +6 -2
- package/build/pipelines.generated.js +22 -142
- package/build/plan-execution-ledger.js +2 -32
- package/build/plane/manifest.js +16 -1
- package/build/plane/preflight.js +54 -1
- package/build/printed-remedy-commands.js +16 -1
- package/build/readme.generated.js +1 -1
- package/build/repair-assets.js +93 -0
- package/build/run-unit-tests-launcher.js +130 -49
- package/build/secret-redaction.js +33 -0
- package/build/setup-epic.js +108 -3
- package/build/start-tickets-prereqs.js +70 -16
- package/build/start-tickets.js +29 -5
- package/build/tool-surface-gating.js +36 -0
- package/build/upgrade-cli.js +55 -1
- package/build/version.generated.js +3 -3
- package/build/worker-guard-hook-bin.js +3 -2
- package/build/worker-guard-hook-main.js +1 -1
- package/build/worktree-core.js +15 -2
- package/docs/EPIC-IMPLEMENTER-INSTALL.md +30 -12
- package/docs/EPIC-IMPLEMENTER.md +68 -2
- package/package.json +3 -3
- package/pipelines/greenfield-setup.json +4 -128
- package/pipelines/implement-ticket.json +3 -2
|
@@ -0,0 +1,129 @@
|
|
|
1
|
+
export const PUSH_TIMEOUT_MS = 180_000;
|
|
2
|
+
export 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;
|
|
3
|
+
const PUSH_LEASE_FAILURE_RE = /stale info|\[rejected\]|non-fast-forward|fetch first/i;
|
|
4
|
+
const SHA = /^(?:[0-9a-f]{40}|[0-9a-f]{64})$/i;
|
|
5
|
+
export function isSafePublicationBranch(branch) {
|
|
6
|
+
return branch.length > 0 && !branch.startsWith("-") && !branch.startsWith("/") &&
|
|
7
|
+
!branch.endsWith("/") && !branch.endsWith(".") && branch !== "@" &&
|
|
8
|
+
!/[\x00-\x20\x7f~^:?*\[\\+]/.test(branch) && !branch.includes("..") &&
|
|
9
|
+
!branch.includes("@{") && !branch.includes("//") &&
|
|
10
|
+
branch.split("/").every(part => !part.startsWith(".") && !part.endsWith(".lock"));
|
|
11
|
+
}
|
|
12
|
+
/** No command text, remote URL, or environment escapes this closed outcome. */
|
|
13
|
+
export async function guardedFastForwardPush(input) {
|
|
14
|
+
const refuse = (reason) => ({ status: "refused", reason });
|
|
15
|
+
if (!isSafePublicationBranch(input.branch) || !SHA.test(input.expectedOriginSha) ||
|
|
16
|
+
input.trustedExcludedShas?.some(sha => !SHA.test(sha)) ||
|
|
17
|
+
(input.candidateSha !== undefined && (!SHA.test(input.candidateSha) || input.candidateSha === input.expectedOriginSha)))
|
|
18
|
+
return refuse("invalid_input");
|
|
19
|
+
let timedOut = false;
|
|
20
|
+
const git = async (args) => {
|
|
21
|
+
const timeoutMs = Math.min(PUSH_TIMEOUT_MS, input.remainingMs());
|
|
22
|
+
if (timeoutMs <= 0 || (!input.mayMutate() || input.signal?.aborted)) {
|
|
23
|
+
timedOut = true;
|
|
24
|
+
return { exitCode: 124, stdout: "", stderr: "" };
|
|
25
|
+
}
|
|
26
|
+
return input.runCommand("git", args, { cwd: input.worktreePath, timeoutMs, signal: input.signal,
|
|
27
|
+
env: { ...input.env, GIT_TERMINAL_PROMPT: "0" } });
|
|
28
|
+
};
|
|
29
|
+
try {
|
|
30
|
+
if ((await git(["check-ref-format", "--branch", input.branch])).exitCode !== 0)
|
|
31
|
+
return refuse("invalid_input");
|
|
32
|
+
const branch = await git(["symbolic-ref", "--quiet", "--short", "HEAD"]);
|
|
33
|
+
if (branch.exitCode !== 0 || branch.stdout.trim() !== input.branch)
|
|
34
|
+
return refuse("branch_mismatch");
|
|
35
|
+
const head = await git(["rev-parse", "HEAD"]);
|
|
36
|
+
const candidate = head.stdout.trim().toLowerCase();
|
|
37
|
+
if (head.exitCode !== 0 || !SHA.test(candidate))
|
|
38
|
+
return refuse("ancestry_unknown");
|
|
39
|
+
if (candidate === input.expectedOriginSha.toLowerCase() ||
|
|
40
|
+
(input.candidateSha !== undefined && candidate !== input.candidateSha.toLowerCase()))
|
|
41
|
+
return refuse("invalid_input");
|
|
42
|
+
const ancestor = await git(["merge-base", "--is-ancestor", input.expectedOriginSha, "HEAD"]);
|
|
43
|
+
if (ancestor.exitCode === 1)
|
|
44
|
+
return refuse("not_fast_forward");
|
|
45
|
+
if (ancestor.exitCode !== 0)
|
|
46
|
+
return refuse("ancestry_unknown");
|
|
47
|
+
for (const sha of input.trustedExcludedShas ?? []) {
|
|
48
|
+
if ((await git(["cat-file", "-e", `${sha}^{commit}`])).exitCode !== 0 ||
|
|
49
|
+
(await git(["merge-base", "--is-ancestor", sha, "HEAD"])).exitCode !== 0)
|
|
50
|
+
return refuse("range_unreadable");
|
|
51
|
+
}
|
|
52
|
+
const range = await git(["rev-list", "HEAD", `^${input.expectedOriginSha}`, ...(input.trustedExcludedShas ?? []).map(sha => `^${sha}`)]);
|
|
53
|
+
const commits = range.stdout.trim().split(/\s+/);
|
|
54
|
+
if (range.exitCode !== 0 || commits.length === 0 || commits.some(sha => !SHA.test(sha)))
|
|
55
|
+
return refuse("range_unreadable");
|
|
56
|
+
const workflow = (paths) => paths.split("\0").some(path => path.startsWith(".github/workflows/"));
|
|
57
|
+
for (const commit of commits) {
|
|
58
|
+
const parentsResult = await git(["rev-list", "--parents", "-n", "1", commit]);
|
|
59
|
+
const ids = parentsResult.stdout.trim().split(/\s+/);
|
|
60
|
+
if (parentsResult.exitCode !== 0 || ids[0] !== commit || ids.some(sha => !SHA.test(sha)))
|
|
61
|
+
return refuse("range_unreadable");
|
|
62
|
+
const parents = ids.slice(1);
|
|
63
|
+
let paths;
|
|
64
|
+
if (parents.length > 1) {
|
|
65
|
+
if (parents.length !== 2)
|
|
66
|
+
return refuse("range_unreadable");
|
|
67
|
+
// Compare the candidate resolution to Git's automatic merge, rather than
|
|
68
|
+
// attributing already-published excluded parent changes to the worker.
|
|
69
|
+
const merge = await git(["merge-tree", "--write-tree", "-z", "--name-only", "--messages", parents[0], parents[1]]);
|
|
70
|
+
const tokens = merge.stdout.split("\0");
|
|
71
|
+
const tree = tokens[0]?.trim() ?? "";
|
|
72
|
+
if (![0, 1].includes(merge.exitCode) || !SHA.test(tree) || !merge.stdout.endsWith("\0"))
|
|
73
|
+
return refuse("range_unreadable");
|
|
74
|
+
const endPaths = tokens.indexOf("", 1);
|
|
75
|
+
if (endPaths < 0)
|
|
76
|
+
return refuse("range_unreadable");
|
|
77
|
+
const conflicts = tokens.slice(1, endPaths);
|
|
78
|
+
if (merge.exitCode === 1 && conflicts.length === 0)
|
|
79
|
+
return refuse("range_unreadable");
|
|
80
|
+
if (conflicts.some(path => path.startsWith(".github/workflows/")))
|
|
81
|
+
return refuse("workflow_files_in_range");
|
|
82
|
+
for (let at = endPaths + 1; at < tokens.length - 1;) {
|
|
83
|
+
if (tokens[at] === "")
|
|
84
|
+
break;
|
|
85
|
+
const count = Number(tokens[at]);
|
|
86
|
+
if (!Number.isInteger(count) || count < 1 || at + count + 2 >= tokens.length - 1)
|
|
87
|
+
return refuse("range_unreadable");
|
|
88
|
+
at += count + 3;
|
|
89
|
+
}
|
|
90
|
+
paths = await git(["diff-tree", "-r", "--no-commit-id", "--name-only", "--no-renames", "-z", tree, commit]);
|
|
91
|
+
}
|
|
92
|
+
else {
|
|
93
|
+
paths = await git(["diff-tree", "--root", "-r", "--no-commit-id", "--name-only", "--no-renames", "-z", commit]);
|
|
94
|
+
}
|
|
95
|
+
if (paths.exitCode !== 0)
|
|
96
|
+
return refuse("range_unreadable");
|
|
97
|
+
if (workflow(paths.stdout))
|
|
98
|
+
return refuse("workflow_files_in_range");
|
|
99
|
+
}
|
|
100
|
+
// Re-check HEAD and ownership immediately before mutation.
|
|
101
|
+
const latest = await git(["rev-parse", "HEAD"]);
|
|
102
|
+
if (latest.exitCode !== 0 || latest.stdout.trim().toLowerCase() !== candidate)
|
|
103
|
+
return refuse("ancestry_unknown");
|
|
104
|
+
if ((!input.mayMutate() || input.signal?.aborted) || input.remainingMs() <= 0)
|
|
105
|
+
return refuse("timeout");
|
|
106
|
+
const destination = `refs/heads/${input.branch}`;
|
|
107
|
+
const push = await git(["push", "--no-verify", "--porcelain",
|
|
108
|
+
`--force-with-lease=${destination}:${input.expectedOriginSha}`, "origin", `HEAD:${destination}`]);
|
|
109
|
+
const text = `${push.stdout}\n${push.stderr}`;
|
|
110
|
+
if (timedOut || push.exitCode === 124 || /timed out|timeout/i.test(text))
|
|
111
|
+
return refuse("timeout");
|
|
112
|
+
if (PUSH_AUTH_FAILURE_RE.test(text))
|
|
113
|
+
return refuse("auth_failed");
|
|
114
|
+
if (PUSH_LEASE_FAILURE_RE.test(text))
|
|
115
|
+
return refuse("lease_rejected");
|
|
116
|
+
if (push.exitCode !== 0)
|
|
117
|
+
return refuse("remote_rejected");
|
|
118
|
+
const update = push.stdout.split("\n").some(line => {
|
|
119
|
+
const parts = line.split("\t");
|
|
120
|
+
return parts[0] === " " && parts[1] === `HEAD:${destination}`;
|
|
121
|
+
});
|
|
122
|
+
if (!update)
|
|
123
|
+
return refuse("lease_rejected");
|
|
124
|
+
return { status: "published", from_sha: input.expectedOriginSha.toLowerCase(), to_sha: candidate };
|
|
125
|
+
}
|
|
126
|
+
catch {
|
|
127
|
+
return refuse(timedOut || input.remainingMs() <= 0 ? "timeout" : "remote_rejected");
|
|
128
|
+
}
|
|
129
|
+
}
|
|
@@ -14,7 +14,12 @@
|
|
|
14
14
|
* epic branch through the provider's merge API. No checkout, no force.
|
|
15
15
|
* * `mark_ready` — finds the ONE open pull request from the epic branch to the
|
|
16
16
|
* base (creating it only after conclusive absence), confirms its head is the
|
|
17
|
-
* expected epic head, and takes it out of draft.
|
|
17
|
+
* expected epic head, and takes it out of draft. BAPI-1399: first it lists the
|
|
18
|
+
* workaround folder `.bridge/epic-workarounds/` (the canonical path, imported
|
|
19
|
+
* from `../epic-workarounds-path.ts`) at the expected epic head. While the folder is
|
|
20
|
+
* present it only finds or creates the draft and reports `already_satisfied`
|
|
21
|
+
* with `detail_code: "workarounds_present"`, so a stale coordinator
|
|
22
|
+
* observation can never take the pull request out of draft.
|
|
18
23
|
* * `correct` — runs ONE worker through the executor's existing locked spawn
|
|
19
24
|
* path under a driver that prepares the epic branch, verifies the worker's
|
|
20
25
|
* edits, makes the one commit, and performs the one push. The worker has no
|
|
@@ -46,7 +51,8 @@
|
|
|
46
51
|
* and this module chooses no next ticket and no gate.
|
|
47
52
|
*/
|
|
48
53
|
import { buildLocalMergeChildEnv, resolveLocalMergeCredential, sanitizeLocalMergeOutput, } from "../conductor/local-merge.js";
|
|
49
|
-
import { createProductionEpicIntegrationGhRunner, ensureEpicIntegrationPullRequest, } from "../epic-integration-pr.js";
|
|
54
|
+
import { EPIC_INTEGRATION_PR_READ_TIMEOUT_MS, createProductionEpicIntegrationGhRunner, ensureEpicIntegrationPullRequest, } from "../epic-integration-pr.js";
|
|
55
|
+
import { EPIC_WORKAROUNDS_DIR_PATH } from "../epic-workarounds-path.js";
|
|
50
56
|
import { INTEGRATION_ACTION_OPERATIONS, INTEGRATION_ACTION_OUTCOMES, INTEGRATION_ACTION_PROTOCOL_VERSION, } from "./types.js";
|
|
51
57
|
/** The route prefix, mirrored from `api/routes/__init__.py`. */
|
|
52
58
|
export const INTEGRATION_ACTION_ROUTE_PREFIX = "/epic-integration-actions";
|
|
@@ -386,20 +392,128 @@ function mapPullRequestOutcome(outcome) {
|
|
|
386
392
|
return { outcome: "indeterminate", detail_code: safeDetail(outcome.reason) };
|
|
387
393
|
}
|
|
388
394
|
}
|
|
389
|
-
|
|
395
|
+
// ---------------------------------------------------------------------------
|
|
396
|
+
// BAPI-1399: the workaround folder re-check
|
|
397
|
+
// ---------------------------------------------------------------------------
|
|
398
|
+
/** The `detail_code` a held `mark_ready` reports on its `already_satisfied` outcome. */
|
|
399
|
+
export const WORKAROUNDS_PRESENT_DETAIL_CODE = "workarounds_present";
|
|
400
|
+
/**
|
|
401
|
+
* Classify one contents-API answer for `.bridge/epic-workarounds/`. Pure, and
|
|
402
|
+
* the twin of `classify_workaround_listing` in
|
|
403
|
+
* `api/library/github/github_webhooks_lib.py`; both suites read the shared
|
|
404
|
+
* parity vectors. `absent` needs a 404 AND proof that the repository and the
|
|
405
|
+
* epic head were readable with the same credential; anything the reader cannot
|
|
406
|
+
* establish — an unproven 404, any other status, no status, an empty array, an
|
|
407
|
+
* object, or entries that are not directory records — is `failed`.
|
|
408
|
+
*/
|
|
409
|
+
export function classifyWorkaroundListing(status, body, accessProven) {
|
|
410
|
+
if (status === 404)
|
|
411
|
+
return accessProven === true ? "absent" : "failed";
|
|
412
|
+
if (status !== 200)
|
|
413
|
+
return "failed";
|
|
414
|
+
if (!Array.isArray(body) || body.length === 0)
|
|
415
|
+
return "failed";
|
|
416
|
+
for (const entry of body) {
|
|
417
|
+
if (typeof entry !== "object" || entry === null || Array.isArray(entry))
|
|
418
|
+
return "failed";
|
|
419
|
+
const name = entry.name;
|
|
420
|
+
if (typeof name !== "string" || name.length === 0)
|
|
421
|
+
return "failed";
|
|
422
|
+
}
|
|
423
|
+
return "present";
|
|
424
|
+
}
|
|
425
|
+
/**
|
|
426
|
+
* List the workaround folder at the expected epic head through the restricted
|
|
427
|
+
* credentialed `gh` environment, with a bounded timeout. `accessProven` is true
|
|
428
|
+
* only when the caller read the epic head with this same credential in this
|
|
429
|
+
* attempt. Never throws; a failure is `failed`, never absence.
|
|
430
|
+
*/
|
|
431
|
+
async function listEpicWorkarounds(deps, credential, env, repository, expectedEpicHeadSha, accessProven) {
|
|
432
|
+
if (!PROVIDER_REPOSITORY_RE.test(repository) || !SHA_RE.test(expectedEpicHeadSha))
|
|
433
|
+
return "failed";
|
|
434
|
+
const r = await runCredentialed(deps, credential, env, "gh", [
|
|
435
|
+
"api", "--include",
|
|
436
|
+
`repos/${repository}/contents/${EPIC_WORKAROUNDS_DIR_PATH}?ref=${expectedEpicHeadSha}`,
|
|
437
|
+
], { timeoutMs: READ_TIMEOUT_MS });
|
|
438
|
+
const status = parseGhHttpStatus(r.stdout, r.stderr);
|
|
439
|
+
if (status === 200 && r.exitCode !== 0)
|
|
440
|
+
return "failed";
|
|
441
|
+
return classifyWorkaroundListing(status, status === 200 ? parseIncludedBody(r.stdout) : null, accessProven);
|
|
442
|
+
}
|
|
443
|
+
/**
|
|
444
|
+
* Read one pull request's draft flag, branches, and head, bounded. The draft-only
|
|
445
|
+
* path of {@link ensureEpicIntegrationPullRequest} does not bind the head, so the
|
|
446
|
+
* held path proves the binding itself before it reports a hold. `null` when the
|
|
447
|
+
* read fails or the answer is not well formed.
|
|
448
|
+
*/
|
|
449
|
+
async function readPullRequestBinding(gh, prNumber, repository, cwd) {
|
|
450
|
+
try {
|
|
451
|
+
const r = await gh([
|
|
452
|
+
"pr", "view", String(prNumber), "--repo", repository,
|
|
453
|
+
"--json", "isDraft,headRefName,baseRefName,headRefOid",
|
|
454
|
+
], { cwd, timeoutMs: EPIC_INTEGRATION_PR_READ_TIMEOUT_MS });
|
|
455
|
+
if (r.exitCode !== 0)
|
|
456
|
+
return null;
|
|
457
|
+
const parsed = JSON.parse(r.stdout);
|
|
458
|
+
if (typeof parsed !== "object" || parsed === null)
|
|
459
|
+
return null;
|
|
460
|
+
const { isDraft, headRefName, baseRefName, headRefOid } = parsed;
|
|
461
|
+
if (typeof isDraft !== "boolean" || typeof headRefName !== "string" || typeof baseRefName !== "string") {
|
|
462
|
+
return null;
|
|
463
|
+
}
|
|
464
|
+
const sha = typeof headRefOid === "string" ? headRefOid.toLowerCase() : "";
|
|
465
|
+
return { isDraft, headRefName, baseRefName, headSha: SHA_RE.test(sha) ? sha : null };
|
|
466
|
+
}
|
|
467
|
+
catch {
|
|
468
|
+
return null;
|
|
469
|
+
}
|
|
470
|
+
}
|
|
471
|
+
async function performMarkReady(action, deps, credential, env, seams) {
|
|
390
472
|
const createGh = seams.createGhRunner ?? ((e) => createProductionEpicIntegrationGhRunner(undefined, e));
|
|
391
|
-
const
|
|
473
|
+
const gh = createGh(env);
|
|
474
|
+
const ensure = (requestReady) => ensureEpicIntegrationPullRequest({
|
|
392
475
|
epicKey: action.epic_key,
|
|
393
476
|
epicBranch: action.epic_branch,
|
|
394
477
|
baseBranch: action.base_branch,
|
|
395
478
|
command: "executor integration",
|
|
396
|
-
gh
|
|
479
|
+
gh,
|
|
397
480
|
cwd: deps.cwd,
|
|
398
|
-
requestReady
|
|
481
|
+
requestReady,
|
|
399
482
|
expectedHeadSha: action.expected_epic_head_sha,
|
|
400
483
|
repo: action.provider_repository,
|
|
401
484
|
});
|
|
402
|
-
|
|
485
|
+
// The caller read the live epic head with this same credential immediately
|
|
486
|
+
// before this call, so a 404 here can only mean the folder is absent.
|
|
487
|
+
const listing = await listEpicWorkarounds(deps, credential, env, action.provider_repository, action.expected_epic_head_sha, true);
|
|
488
|
+
if (listing === "failed") {
|
|
489
|
+
// Unreadable is never absence: report indeterminate, which spends one
|
|
490
|
+
// reconciliation probe exactly as any other unreadable observation does.
|
|
491
|
+
return { outcome: "indeterminate", detail_code: "probe_inconclusive" };
|
|
492
|
+
}
|
|
493
|
+
if (listing === "absent")
|
|
494
|
+
return mapPullRequestOutcome(await ensure(true));
|
|
495
|
+
// Present: find or create the draft and leave it a draft. `not_requested` is
|
|
496
|
+
// never passed through `mapPullRequestOutcome`, which would read it as failed.
|
|
497
|
+
const outcome = await ensure(false);
|
|
498
|
+
if (outcome.kind !== "already_open" && outcome.kind !== "created") {
|
|
499
|
+
return mapPullRequestOutcome(outcome);
|
|
500
|
+
}
|
|
501
|
+
if (outcome.prNumber === null)
|
|
502
|
+
return { outcome: "indeterminate", detail_code: "probe_inconclusive" };
|
|
503
|
+
const pr = outcome.prNumber;
|
|
504
|
+
const binding = await readPullRequestBinding(gh, pr, action.provider_repository, deps.cwd);
|
|
505
|
+
if (binding === null)
|
|
506
|
+
return { outcome: "indeterminate", pr_number: pr, detail_code: "probe_inconclusive" };
|
|
507
|
+
if (binding.headRefName !== action.epic_branch || binding.baseRefName !== action.base_branch) {
|
|
508
|
+
return { outcome: "pr_ambiguous", pr_number: pr, detail_code: "probe_ambiguous" };
|
|
509
|
+
}
|
|
510
|
+
if (binding.headSha !== action.expected_epic_head_sha) {
|
|
511
|
+
return { outcome: "head_drift", pr_number: pr, detail_code: "pr_head_moved" };
|
|
512
|
+
}
|
|
513
|
+
// A non-draft PR is never claimed as held: it is already ready, as today.
|
|
514
|
+
if (!binding.isDraft)
|
|
515
|
+
return { outcome: "already_satisfied", pr_number: pr };
|
|
516
|
+
return { outcome: "already_satisfied", pr_number: pr, detail_code: WORKAROUNDS_PRESENT_DETAIL_CODE };
|
|
403
517
|
}
|
|
404
518
|
/**
|
|
405
519
|
* Perform one claimed integration action and report its facts.
|
|
@@ -469,7 +583,7 @@ export async function runIntegrationAction(action, client, deps, seams = {}) {
|
|
|
469
583
|
report = await performSyncBase(action, deps, credential, env, reads);
|
|
470
584
|
}
|
|
471
585
|
else if (action.operation === "mark_ready") {
|
|
472
|
-
report = await performMarkReady(action, deps, env, seams);
|
|
586
|
+
report = await performMarkReady(action, deps, credential, env, seams);
|
|
473
587
|
}
|
|
474
588
|
else if (seams.runCorrection === undefined) {
|
|
475
589
|
report = { outcome: "failed", detail_code: "correction_unsupported" };
|
|
@@ -39,7 +39,7 @@
|
|
|
39
39
|
* origin, but not this worker's latest commit. Distinct from
|
|
40
40
|
* `WorkerFinalizationMissingRemoteBranchAndPr` (which covers "no evidence any
|
|
41
41
|
* work reached origin") because this case has positive proof of recoverable
|
|
42
|
-
* state: an operator can
|
|
42
|
+
* state: an operator can preserve the newer local worktree commit rather than
|
|
43
43
|
* re-running from scratch; see `worker-finalization.ts`.
|
|
44
44
|
* - `WorkerFinalizationOriginUnverifiable` — an implementation-style job
|
|
45
45
|
* (BAPI-1357) exited cleanly, but the origin branch lookup could not be
|
|
@@ -164,6 +164,13 @@ export const WorkerFinalizationOriginUnverifiable = "WorkerFinalizationOriginUnv
|
|
|
164
164
|
* no event text, command, path, or output.
|
|
165
165
|
*/
|
|
166
166
|
export const WorkerEndedWithPendingBackgroundWork = "WorkerEndedWithPendingBackgroundWork";
|
|
167
|
+
/**
|
|
168
|
+
* BAPI-1392: a recognized Bridge command's packaged recipe could not be delivered
|
|
169
|
+
* to the worker. Refused before spawn rather than silently running the committed
|
|
170
|
+
* copy, which may be stale. The message carries a fixed `reason=<code>` token and
|
|
171
|
+
* a command name only — never prompt text, arguments, or recipe bytes.
|
|
172
|
+
*/
|
|
173
|
+
export const PackagedRecipeUndeliverable = "ContractError.PackagedRecipeUndeliverable";
|
|
167
174
|
export const WorkerConfigIsolation = "ContractError.WorkerConfigIsolation";
|
|
168
175
|
export const PreSpawnVerification = "ContractError.PreSpawnVerification";
|
|
169
176
|
export const BridgeCredentialUnavailable = "ContractError.BridgeCredentialUnavailable";
|