@bridge_gpt/mcp-server 0.2.59 → 0.2.62
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 +181 -51
- package/build/agent-guidance.generated.js +1 -1
- package/build/agent-notes.js +7 -1
- package/build/commands.generated.js +6 -6
- package/build/conductor/bridge-api-client.js +406 -4
- package/build/conductor/doctor.js +255 -53
- package/build/conductor/epic-runtime.js +11 -2
- package/build/conductor/install-doctor.js +24 -2
- package/build/conductor/local-merge.js +319 -26
- package/build/conductor/merge-execution-contract.js +86 -0
- package/build/conductor/merge-execution-eligibility.js +130 -0
- package/build/conductor/readiness-sections.js +256 -16
- package/build/conductor/readiness.js +203 -29
- package/build/conductor/recovery-operations.js +8 -2
- package/build/connect-bitbucket-api.js +6 -1
- package/build/connect-bitbucket.js +398 -88
- package/build/credential-store.js +172 -0
- package/build/docs.generated.js +2 -2
- package/build/doctor.js +277 -15
- package/build/drive-epic.js +147 -12
- package/build/epic-implementer-bin.js +15 -15
- package/build/epic-implementer-claude-hook-bin.js +1 -1
- package/build/epic-implementer-env.js +43 -14
- package/build/epic-integration-pr.js +61 -15
- package/build/epic-launch-profile.js +455 -0
- package/build/executor/ci-rerun-job.js +269 -0
- package/build/executor/cli.js +40 -0
- package/build/executor/credentials.js +28 -1
- package/build/executor/env.js +60 -0
- package/build/executor/http-client.js +45 -1
- package/build/executor/index.js +21 -4
- package/build/executor/integration-job.js +759 -0
- package/build/executor/job-runner.js +218 -9
- package/build/executor/job-types.js +12 -2
- package/build/executor/merge-job.js +11 -2
- package/build/executor/observation.js +114 -7
- package/build/executor/runner.js +101 -1
- package/build/executor/service-lifecycle.js +136 -0
- package/build/executor/service-unit.js +427 -9
- package/build/executor/types.js +48 -0
- package/build/index.js +479 -240
- package/build/install-bridge-conductor.js +599 -14
- package/build/install-bridge.js +448 -46
- package/build/mcp-install-state.js +42 -4
- package/build/merge-pull-request.js +103 -10
- package/build/merge-workflow-codeowners.js +224 -0
- package/build/merge-workflow-config.js +187 -0
- package/build/merge-workflow-drift.js +97 -0
- package/build/merge-workflow-install.js +638 -0
- package/build/merge-workflow.js +956 -0
- package/build/pipelines.generated.js +3 -3
- package/build/plane/cli.js +24 -2
- package/build/plane/preflight.js +68 -0
- package/build/plane/supervisor.js +6 -0
- package/build/readme.generated.js +1 -1
- package/build/run-unit-tests-launcher.js +5 -0
- package/build/setup-epic.js +263 -38
- package/build/version.generated.js +3 -3
- package/package.json +3 -3
- package/build/conduct-epic/bridge-client.js +0 -773
- package/build/conduct-epic/checkpoint-store.js +0 -542
- package/build/conduct-epic/cli.js +0 -3145
- package/build/conduct-epic/cut-protocol.js +0 -392
- package/build/conduct-epic/lock.js +0 -302
- package/build/conduct-epic/pr-state.js +0 -286
- package/build/conduct-epic/spawn.js +0 -113
- package/build/conductor/cli.js +0 -1278
- package/build/conductor-bundle-cli.js +0 -256
- package/build/direct-ticket-tools.js +0 -70
- package/build/executor/reconcile-overlap-governance.js +0 -129
- package/build/implement-epic/bridge-client.js +0 -773
- package/build/implement-epic/checkpoint-store.js +0 -542
- package/build/implement-epic/cli.js +0 -3158
- package/build/implement-epic/cut-protocol.js +0 -392
- package/build/implement-epic/lock.js +0 -302
- package/build/implement-epic/pr-state.js +0 -286
- package/build/implement-epic/spawn.js +0 -113
- package/build/plan-epic-conductor-eligibility.js +0 -213
|
@@ -36,7 +36,9 @@
|
|
|
36
36
|
import { MANAGED_HOOK_NAMES } from "./git-hooks.js";
|
|
37
37
|
import { MCP_PACKAGE_NAME } from "../mcp-identity.js";
|
|
38
38
|
import { CLAUDE_REVIEW_WORKFLOW_DRIFT_REMEDIATION, summarizeClaudeReviewWorkflowDrift, } from "../claude-review-workflow-drift.js";
|
|
39
|
-
import { describeNativeLedgerAvailability } from "./doctor.js";
|
|
39
|
+
import { describeNativeLedgerAvailability, GH_ACTIONS_MERGE_AUTH_REMEDIATION, GH_ACTIONS_MERGE_SKIPPED_DETAIL, GH_ACTIONS_MERGE_UNOBSERVABLE_REMEDIATION, } from "./doctor.js";
|
|
40
|
+
import { classifyMergeExecutionEligibility, MERGE_EXECUTION_UNVERIFIABLE_REMEDIATION, } from "./merge-execution-eligibility.js";
|
|
41
|
+
import { MERGE_WORKFLOW_CHILD_MERGE_COMPATIBLE_VERSIONS, MERGE_WORKFLOW_PATH, mergeWorkflowVersionSupportsChildMerge, } from "./merge-execution-contract.js";
|
|
40
42
|
import { classifyDatabaseMigration, classifySingleTransitionAuthority, classifyWorkerPresent, } from "./bring-up-facts.js";
|
|
41
43
|
/** Operator runbook pointer surfaced beside the executor gap. */
|
|
42
44
|
export const CONDUCTOR_OPERATOR_RUNBOOK_POINTER = "docs/claude/epic-implementer-v2-operator-runbook.md";
|
|
@@ -71,13 +73,95 @@ export const CONDUCTOR_HOOK_TARGET_REMEDIATION = "re-run `install conductor` and
|
|
|
71
73
|
export const CONDUCTOR_LEDGER_LOADABILITY_REMEDIATION = "`better-sqlite3` is an optionalDependency npm silently skips on a build failure — " +
|
|
72
74
|
"reinstall it for this Node runtime to restore local conductor observability.";
|
|
73
75
|
/** Remediation surfaced whenever no healthy generated service unit is detected. */
|
|
74
|
-
export const EXECUTOR_INSTALL_SERVICE_REMEDIATION = "generate a persistent unit with `install conductor --executor-id <id
|
|
76
|
+
export const EXECUTOR_INSTALL_SERVICE_REMEDIATION = "generate a persistent unit with `install conductor --executor-id <id> --repo-wide` or " +
|
|
75
77
|
"`executor install-service` (or run the executor by hand) — " +
|
|
76
78
|
`see ${CONDUCTOR_OPERATOR_RUNBOOK_POINTER}.`;
|
|
77
79
|
/** Remediation for a unit that exists but whose service is not running. */
|
|
78
80
|
export const EXECUTOR_SERVICE_START_REMEDIATION = "start the service (re-run `install conductor` and accept the start consent, or run the " +
|
|
79
81
|
`printed launchctl/systemctl command by hand) — see ${CONDUCTOR_OPERATOR_RUNBOOK_POINTER}.`;
|
|
80
82
|
// ---------------------------------------------------------------------------
|
|
83
|
+
// BAPI-1254 — the ONE required-check presentation
|
|
84
|
+
// ---------------------------------------------------------------------------
|
|
85
|
+
/**
|
|
86
|
+
* Operator-hold sentence shared by every non-verified outcome. It states what an
|
|
87
|
+
* operator must do; it does not claim this report blocks anything — activation
|
|
88
|
+
* and dispatch enforcement belong to a separate slice.
|
|
89
|
+
*/
|
|
90
|
+
const REQUIRED_CHECKS_OPERATOR_HOLD = "hold new unattended epic work until this is resolved";
|
|
91
|
+
/** Fixed, secret-free copy for each outcome. Names no check, branch, or SHA. */
|
|
92
|
+
export const REQUIRED_CHECKS_PRESENTATION_COPY = {
|
|
93
|
+
verified: {
|
|
94
|
+
detail: "Verified — every configured required check is produced by the lane workflows",
|
|
95
|
+
},
|
|
96
|
+
unknown_name: {
|
|
97
|
+
detail: "Configuration mismatch confirmed — a configured required check conflicts with the " +
|
|
98
|
+
`current lane producer contract; ${REQUIRED_CHECKS_OPERATOR_HOLD}`,
|
|
99
|
+
remediation: "repair the project-default required-check configuration so it names the check the " +
|
|
100
|
+
"lane workflow emits (see the operator runbook's patch-policy section for active runs).",
|
|
101
|
+
},
|
|
102
|
+
unverifiable: {
|
|
103
|
+
detail: "Could not verify — the configured required checks could not be matched to an " +
|
|
104
|
+
`authoritative producer; ${REQUIRED_CHECKS_OPERATOR_HOLD}`,
|
|
105
|
+
remediation: "restore required-check verification (a readable default branch with static lane " +
|
|
106
|
+
"workflows and a current Bridge API), or correct the required-check configuration.",
|
|
107
|
+
},
|
|
108
|
+
empty: {
|
|
109
|
+
detail: "Could not verify — the required-check list is EMPTY, which is unsafe: the done gate " +
|
|
110
|
+
`would pass unconditionally; ${REQUIRED_CHECKS_OPERATOR_HOLD}`,
|
|
111
|
+
remediation: "configure a non-empty required-check list naming the checks the lane workflows emit.",
|
|
112
|
+
},
|
|
113
|
+
olderServer: {
|
|
114
|
+
detail: "Could not verify — this Bridge API does not report required-check verification; " +
|
|
115
|
+
REQUIRED_CHECKS_OPERATOR_HOLD,
|
|
116
|
+
remediation: "upgrade the Bridge API deploy so it verifies required checks, or correct the " +
|
|
117
|
+
"required-check configuration; its state is unknown here, not healthy.",
|
|
118
|
+
},
|
|
119
|
+
};
|
|
120
|
+
/**
|
|
121
|
+
* Map the validated supervisor facts onto one required-check presentation.
|
|
122
|
+
*
|
|
123
|
+
* PURE, and the only place either renderer decides this row: the CLI descriptor
|
|
124
|
+
* and the install-time section both call it, so they cannot drift and neither
|
|
125
|
+
* performs a second producer verification. Only the server's `verified` verdict
|
|
126
|
+
* is satisfied. A nonempty count is never proof, an older server's absent group
|
|
127
|
+
* is `unverifiable`, and an empty list stays unsafe whatever else is reported.
|
|
128
|
+
* No remediation suggests disabling required checks.
|
|
129
|
+
*/
|
|
130
|
+
export function presentRequiredChecks(supervisor) {
|
|
131
|
+
const verdict = supervisor.required_checks_verification ?? null;
|
|
132
|
+
if (supervisor.required_checks_empty) {
|
|
133
|
+
return { outcome: "unverifiable", satisfied: false, ...REQUIRED_CHECKS_PRESENTATION_COPY.empty };
|
|
134
|
+
}
|
|
135
|
+
if (verdict === null) {
|
|
136
|
+
return {
|
|
137
|
+
outcome: "unverifiable",
|
|
138
|
+
satisfied: false,
|
|
139
|
+
...REQUIRED_CHECKS_PRESENTATION_COPY.olderServer,
|
|
140
|
+
};
|
|
141
|
+
}
|
|
142
|
+
if (verdict === "verified") {
|
|
143
|
+
const fresh = supervisor.required_checks_evidence_fresh === true ? ", fresh evidence" : "";
|
|
144
|
+
return {
|
|
145
|
+
outcome: "verified",
|
|
146
|
+
satisfied: true,
|
|
147
|
+
detail: `${REQUIRED_CHECKS_PRESENTATION_COPY.verified.detail} ` +
|
|
148
|
+
`(${supervisor.required_checks_count} required check(s) configured${fresh})`,
|
|
149
|
+
};
|
|
150
|
+
}
|
|
151
|
+
if (verdict === "unknown_name") {
|
|
152
|
+
return {
|
|
153
|
+
outcome: "unknown_name",
|
|
154
|
+
satisfied: false,
|
|
155
|
+
...REQUIRED_CHECKS_PRESENTATION_COPY.unknown_name,
|
|
156
|
+
};
|
|
157
|
+
}
|
|
158
|
+
return {
|
|
159
|
+
outcome: "unverifiable",
|
|
160
|
+
satisfied: false,
|
|
161
|
+
...REQUIRED_CHECKS_PRESENTATION_COPY.unverifiable,
|
|
162
|
+
};
|
|
163
|
+
}
|
|
164
|
+
// ---------------------------------------------------------------------------
|
|
81
165
|
// Section builders
|
|
82
166
|
// ---------------------------------------------------------------------------
|
|
83
167
|
export function supervisorSection(readiness) {
|
|
@@ -107,20 +191,20 @@ export function supervisorSection(readiness) {
|
|
|
107
191
|
// Deliberately its OWN section, not folded into the one above: "configured but
|
|
108
192
|
// unsafe" and "not configured" drive different operator actions, and an empty
|
|
109
193
|
// required-check list makes the CI gate pass unconditionally rather than fail.
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
:
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
194
|
+
//
|
|
195
|
+
// BAPI-1254: the verdict comes from the shared `presentRequiredChecks`, the
|
|
196
|
+
// same mapping the CLI descriptor uses. Only `verified` is `ok`; a confirmed
|
|
197
|
+
// mismatch and could-not-verify are both `degraded`, never `fatal`.
|
|
198
|
+
const requiredChecks = presentRequiredChecks(s);
|
|
199
|
+
sections.push({
|
|
200
|
+
id: "supervisor-required-checks",
|
|
201
|
+
label: "Supervisor required CI checks",
|
|
202
|
+
status: requiredChecks.satisfied ? "ok" : "degraded",
|
|
203
|
+
detail: requiredChecks.detail,
|
|
204
|
+
...(requiredChecks.remediation === undefined
|
|
205
|
+
? {}
|
|
206
|
+
: { remediation: requiredChecks.remediation }),
|
|
207
|
+
});
|
|
124
208
|
// BAPI-694 (R5) — its OWN section, and WARN-ONLY.
|
|
125
209
|
//
|
|
126
210
|
// `degraded`, never `fatal`: a divergence between the code_review gate's
|
|
@@ -216,6 +300,95 @@ export function githubActionsSection(readiness) {
|
|
|
216
300
|
remediation: "grant the GitHub App `actions: write`; the conductor's workflow rerun lane fails open on 403 without it.",
|
|
217
301
|
};
|
|
218
302
|
}
|
|
303
|
+
/**
|
|
304
|
+
* The "Merge execution eligibility" section (BAPI-1135).
|
|
305
|
+
*
|
|
306
|
+
* A DEDICATED section rather than extra rows on the GitHub App block, because it
|
|
307
|
+
* is a different safety diagnostic about a different principal: the App block
|
|
308
|
+
* describes the installation's dispatch permission, this one describes whether
|
|
309
|
+
* the repository's merge workflow can actually merge.
|
|
310
|
+
*
|
|
311
|
+
* Rows are rendered in DEPENDENCY order — probe/readability, presence, enabled
|
|
312
|
+
* state, exact version, App `actions: write`, workflow-token write authority,
|
|
313
|
+
* branch-policy admission — so a reader meets the fact that decided the verdict
|
|
314
|
+
* before the facts that were never reached.
|
|
315
|
+
*
|
|
316
|
+
* Three rendering rules this section holds to:
|
|
317
|
+
*
|
|
318
|
+
* * an absent older-server block and a failed probe are explicit UNKNOWN rows,
|
|
319
|
+
* never an omitted section and never a pass. "Not reported" is not "healthy";
|
|
320
|
+
* omitting the section would make an unknown look like a clean bill of health.
|
|
321
|
+
* * confirmed policy denial stays verbally distinct from unreadability. The
|
|
322
|
+
* status word (`degraded` for unknown, `fatal`-adjacent `degraded` with
|
|
323
|
+
* different copy for confirmed) is carried in TEXT, not by colour alone, and
|
|
324
|
+
* the detail line opens with `could not verify:` or `not eligible:`.
|
|
325
|
+
* * command strings, workflow paths, versions, and reason codes are rendered as
|
|
326
|
+
* backticked monospace through the established section conventions, and are
|
|
327
|
+
* allowed to wrap rather than being truncated — a truncated remedy command is
|
|
328
|
+
* not a remedy.
|
|
329
|
+
*/
|
|
330
|
+
export function mergeExecutionSections(readiness) {
|
|
331
|
+
const verdict = classifyMergeExecutionEligibility(readiness);
|
|
332
|
+
const m = readiness.merge_execution;
|
|
333
|
+
if (m === null || m === undefined) {
|
|
334
|
+
// One row, not seven. Fabricating six more unknown rows from a block that
|
|
335
|
+
// was never reported would imply the server answered six questions.
|
|
336
|
+
return [
|
|
337
|
+
{
|
|
338
|
+
id: "merge-execution-eligibility",
|
|
339
|
+
label: "Merge execution eligibility",
|
|
340
|
+
status: "degraded",
|
|
341
|
+
detail: `could not verify: ${verdict.detail}`,
|
|
342
|
+
remediation: verdict.remediation ?? MERGE_EXECUTION_UNVERIFIABLE_REMEDIATION,
|
|
343
|
+
},
|
|
344
|
+
];
|
|
345
|
+
}
|
|
346
|
+
const decisive = {
|
|
347
|
+
id: "merge-execution-eligibility",
|
|
348
|
+
label: "Merge execution eligibility",
|
|
349
|
+
status: verdict.status === "eligible" ? "ok" : "degraded",
|
|
350
|
+
detail: verdict.status === "eligible"
|
|
351
|
+
? `eligible: ${verdict.detail}`
|
|
352
|
+
: verdict.status === "could_not_verify"
|
|
353
|
+
? `could not verify: ${verdict.detail}`
|
|
354
|
+
: `not eligible: ${verdict.detail} (\`${verdict.reason}\`)`,
|
|
355
|
+
...(verdict.remediation ? { remediation: verdict.remediation } : {}),
|
|
356
|
+
};
|
|
357
|
+
// The supporting facts, in dependency order. Each is a plain readable row — no
|
|
358
|
+
// remediation of its own, because the decisive row above already names the one
|
|
359
|
+
// action, and repeating it seven times would bury it.
|
|
360
|
+
const fact = (id, label, ok, detail) => ({
|
|
361
|
+
id,
|
|
362
|
+
label,
|
|
363
|
+
status: ok ? "ok" : "degraded",
|
|
364
|
+
detail,
|
|
365
|
+
});
|
|
366
|
+
return [
|
|
367
|
+
decisive,
|
|
368
|
+
fact("merge-execution-branch-policy-readable", "Branch policy readable", m.branch_protection_probe_succeeded, m.branch_protection_probe_succeeded
|
|
369
|
+
? `read on the ${m.branch_scope.replace("_", " ")}`
|
|
370
|
+
: "could not verify: the branch policy could not be read"),
|
|
371
|
+
fact("merge-execution-workflow-present", "Merge workflow present", m.merge_workflow_probe_succeeded && m.merge_workflow_present, !m.merge_workflow_probe_succeeded
|
|
372
|
+
? "could not verify: the workflow metadata could not be read"
|
|
373
|
+
: m.merge_workflow_present
|
|
374
|
+
? `\`${MERGE_WORKFLOW_PATH}\` present`
|
|
375
|
+
: `not eligible: \`${MERGE_WORKFLOW_PATH}\` is absent`),
|
|
376
|
+
fact("merge-execution-workflow-enabled", "Merge workflow enabled", m.merge_workflow_enabled, m.merge_workflow_enabled ? "enabled" : "not eligible: disabled or unknown"),
|
|
377
|
+
fact("merge-execution-workflow-version", "Merge workflow version", mergeWorkflowVersionSupportsChildMerge(m.merge_workflow_version), `\`${m.merge_workflow_version ?? "unset"}\` (expected one of ` +
|
|
378
|
+
`\`${MERGE_WORKFLOW_CHILD_MERGE_COMPATIBLE_VERSIONS.join(", ")}\`)`),
|
|
379
|
+
fact("merge-execution-app-actions-write", "GitHub App `actions: write`", readiness.github.actions_write, readiness.github.actions_probe_succeeded
|
|
380
|
+
? `actions: \`${readiness.github.actions_permission_level}\``
|
|
381
|
+
: "could not verify: the App permission could not be checked"),
|
|
382
|
+
fact("merge-execution-workflow-token-write", "Workflow token write authority", m.merge_workflow_token_write_authority, m.merge_workflow_token_write_authority
|
|
383
|
+
? "the merge workflow's own token holds write authority"
|
|
384
|
+
: "not eligible: the merge workflow's own token does not hold write authority"),
|
|
385
|
+
fact("merge-execution-actor-admitted", "Branch policy admits the workflow actor", m.branch_protection_actions_bot_admitted, m.branch_protection_probe_succeeded
|
|
386
|
+
? m.branch_protection_actions_bot_admitted
|
|
387
|
+
? "admitted"
|
|
388
|
+
: "not eligible: the workflow actor is excluded"
|
|
389
|
+
: "could not verify: the branch policy could not be read"),
|
|
390
|
+
];
|
|
391
|
+
}
|
|
219
392
|
export function reconcilerSection(readiness) {
|
|
220
393
|
const r = readiness.reconciler;
|
|
221
394
|
if (!r.liveness_readable) {
|
|
@@ -630,6 +803,73 @@ export function conductorHookTargetsSection(legacyConductor, conductorContext) {
|
|
|
630
803
|
* sanitized failure kind, because the common cause is npm silently skipping an
|
|
631
804
|
* optionalDependency build and that is undiagnosable without the ABI.
|
|
632
805
|
*/
|
|
806
|
+
/**
|
|
807
|
+
* Project the doctor's read-only `github_actions` merge OBSERVABILITY inspection
|
|
808
|
+
* into this layer's sections (BAPI-1135).
|
|
809
|
+
*
|
|
810
|
+
* Projected here rather than formatted inside installation code, for the same
|
|
811
|
+
* reason every other doctor fact is: the install-time doctor and the advisory
|
|
812
|
+
* readiness gate must render one classification, and a second formatter in the
|
|
813
|
+
* installer is how the two come to describe the same probe differently.
|
|
814
|
+
*
|
|
815
|
+
* Two sections, never one. `gh` authentication and endpoint reachability are
|
|
816
|
+
* independently actionable, and a collapsed row would hide which of the two the
|
|
817
|
+
* operator has to fix.
|
|
818
|
+
*
|
|
819
|
+
* These describe THIS HOST's ability to observe — never whether the capability
|
|
820
|
+
* is sufficient. {@link mergeExecutionSections} owns that, from the server's own
|
|
821
|
+
* report. The wording keeps them apart deliberately: `not observable` and `could
|
|
822
|
+
* not verify` here, versus `not eligible` there.
|
|
823
|
+
*/
|
|
824
|
+
export function gitHubActionsMergeObservabilitySections(legacyConductor) {
|
|
825
|
+
const authBase = {
|
|
826
|
+
id: "gh-actions-merge-auth",
|
|
827
|
+
label: "GitHub Actions merge: local observation prerequisites",
|
|
828
|
+
};
|
|
829
|
+
const observeBase = {
|
|
830
|
+
id: "gh-actions-merge-observable",
|
|
831
|
+
label: "GitHub Actions merge: capability observability",
|
|
832
|
+
};
|
|
833
|
+
const inspection = legacyConductor?.github_actions_merge;
|
|
834
|
+
if (inspection === undefined || inspection === null) {
|
|
835
|
+
// The doctor was not collected, or predates this inspection. Both rows say
|
|
836
|
+
// so rather than being omitted — an absent row reads as "fine".
|
|
837
|
+
const detail = "could not verify — the epic-implementer doctor reported no observation";
|
|
838
|
+
return [
|
|
839
|
+
{ ...authBase, status: "degraded", detail },
|
|
840
|
+
{ ...observeBase, status: "degraded", detail },
|
|
841
|
+
];
|
|
842
|
+
}
|
|
843
|
+
return [
|
|
844
|
+
inspection.gh_authed
|
|
845
|
+
? { ...authBase, status: "ok", detail: "eligible to observe — `gh` is authenticated" }
|
|
846
|
+
: {
|
|
847
|
+
...authBase,
|
|
848
|
+
status: "degraded",
|
|
849
|
+
detail: "not observable — no authenticated `gh` identity on this host",
|
|
850
|
+
remediation: GH_ACTIONS_MERGE_AUTH_REMEDIATION,
|
|
851
|
+
},
|
|
852
|
+
!inspection.gh_authed
|
|
853
|
+
? {
|
|
854
|
+
...observeBase,
|
|
855
|
+
status: "degraded",
|
|
856
|
+
detail: GH_ACTIONS_MERGE_SKIPPED_DETAIL,
|
|
857
|
+
remediation: GH_ACTIONS_MERGE_AUTH_REMEDIATION,
|
|
858
|
+
}
|
|
859
|
+
: inspection.capability_observable
|
|
860
|
+
? {
|
|
861
|
+
...observeBase,
|
|
862
|
+
status: "ok",
|
|
863
|
+
detail: "eligible to observe — the workflow metadata endpoint answered",
|
|
864
|
+
}
|
|
865
|
+
: {
|
|
866
|
+
...observeBase,
|
|
867
|
+
status: "degraded",
|
|
868
|
+
detail: "could not verify — the workflow metadata endpoint could not be read",
|
|
869
|
+
remediation: GH_ACTIONS_MERGE_UNOBSERVABLE_REMEDIATION,
|
|
870
|
+
},
|
|
871
|
+
];
|
|
872
|
+
}
|
|
633
873
|
export function conductorLedgerLoadabilitySection(legacyConductor) {
|
|
634
874
|
const base = {
|
|
635
875
|
id: "epic-implementer-ledger-loadability",
|
|
@@ -47,19 +47,28 @@
|
|
|
47
47
|
* a local collector, and the Bridge API owner for a server-side collector that
|
|
48
48
|
* keeps timing out or answering unreadably.
|
|
49
49
|
*
|
|
50
|
-
* ##
|
|
51
|
-
*
|
|
52
|
-
*
|
|
53
|
-
*
|
|
54
|
-
*
|
|
55
|
-
*
|
|
56
|
-
*
|
|
57
|
-
*
|
|
58
|
-
*
|
|
59
|
-
*
|
|
60
|
-
*
|
|
61
|
-
*
|
|
62
|
-
*
|
|
50
|
+
* ## RETIRED deferral: local `gh` auth (AC-7, retired by BAPI-1136)
|
|
51
|
+
*
|
|
52
|
+
* This deferral named its retirement owner as epic A2, and A2 has now landed.
|
|
53
|
+
* It is recorded here rather than deleted, because the point of naming an owner
|
|
54
|
+
* is being able to check afterwards that the owner actually did the work.
|
|
55
|
+
*
|
|
56
|
+
* **What it used to be.** Detection was `conductor.gh-cli` and
|
|
57
|
+
* `conductor.gh-auth`: the merge ran under this host's own `gh` session, because
|
|
58
|
+
* the GitHub App token is `contents: read` and structurally cannot merge (see
|
|
59
|
+
* `mcp_server/src/executor/merge-job.ts`). Until A2 shipped, that ambient-session
|
|
60
|
+
* check was load-bearing.
|
|
61
|
+
*
|
|
62
|
+
* **What replaced it.** BAPI-1136 de-personalized the merge: it now authenticates
|
|
63
|
+
* with one explicitly configured, customer-held credential
|
|
64
|
+
* (`BAPI_LOCAL_MERGE_TOKEN` or `BAPI_MACHINE_MERGE_TOKEN`) and never with the
|
|
65
|
+
* ambient session. So the second check was RE-POINTED, not removed —
|
|
66
|
+
* `conductor.gh-cli` still asks whether `gh` exists, and
|
|
67
|
+
* `conductor.merge-credential` asks whether a credential is configured. Removing
|
|
68
|
+
* it would have been a silent downgrade; leaving it would have been worse, since
|
|
69
|
+
* it reported green on exactly the hosts about to fail closed. **Containment** and
|
|
70
|
+
* **escalation** are unchanged: the merge refuses with a named reason rather than
|
|
71
|
+
* merging without a credential, and the invoking operator is the first target.
|
|
63
72
|
*
|
|
64
73
|
* ## Deferred retirement: native SQLite ledger (AC-7)
|
|
65
74
|
*
|
|
@@ -87,9 +96,11 @@ import { mapConductorDoctorReportToReadinessChecks, } from "./doctor.js";
|
|
|
87
96
|
import { mapInstallStatusChecksToReadinessChecks, } from "../install-doctor.js";
|
|
88
97
|
import { mapPlanePreflightToReadinessChecks, } from "../plane/preflight.js";
|
|
89
98
|
import { runConductorInstallDoctor, } from "./install-doctor.js";
|
|
90
|
-
import { BRING_UP_SECTION_LABELS } from "./readiness-sections.js";
|
|
99
|
+
import { BRING_UP_SECTION_LABELS, presentRequiredChecks, } from "./readiness-sections.js";
|
|
91
100
|
import { classifyBringUpFacts } from "./bring-up-facts.js";
|
|
101
|
+
import { classifyMergeExecutionEligibility, MERGE_EXECUTION_INELIGIBLE_REMEDIATION, MERGE_EXECUTION_UNVERIFIABLE_REMEDIATION, } from "./merge-execution-eligibility.js";
|
|
92
102
|
import { createReadinessCheck, createReadinessCheckSafely, summarizeReadinessChecks, validateReadinessChecks, } from "../readiness-check.js";
|
|
103
|
+
import { ADMISSION_EVIDENCE_SCHEMA_VERSION, } from "./bridge-api-client.js";
|
|
93
104
|
/** This report's own schema version. See the module docstring. */
|
|
94
105
|
export const CONDUCTOR_READINESS_GATE_SCHEMA_VERSION = 1;
|
|
95
106
|
/** Fixed statement carried in every report, so no consumer can mistake it for a gate. */
|
|
@@ -110,6 +121,13 @@ export const SERVER_READINESS_DESCRIPTORS = [
|
|
|
110
121
|
{ id: "review-policy", label: "Review policy" },
|
|
111
122
|
{ id: "github-credentials", label: "GitHub App credentials" },
|
|
112
123
|
{ id: "github-actions-permission", label: "GitHub App actions permission" },
|
|
124
|
+
// BAPI-1135 — merge-execution eligibility, BESIDE the App permission row and
|
|
125
|
+
// never replacing it: that row is the installation's dispatch/rerun
|
|
126
|
+
// permission, these two are the merge workflow and the branch policy that
|
|
127
|
+
// governs it. Two rows rather than one, because a workflow that is absent and
|
|
128
|
+
// a branch policy that excludes the actor are separately actionable.
|
|
129
|
+
{ id: "merge-execution-workflow", label: "Merge workflow eligibility" },
|
|
130
|
+
{ id: "merge-execution-branch-policy", label: "Merge branch policy" },
|
|
113
131
|
// BAPI-1153 — the "Bring-up safety" group: deployment-wide facts that decide
|
|
114
132
|
// whether a plane can drive a run at all, ahead of the repository-scoped
|
|
115
133
|
// reconciler/executor rows. Labels come from the shared section layer.
|
|
@@ -153,7 +171,6 @@ const SERVER_FAILURE_COPY = {
|
|
|
153
171
|
const SERVER_REMEDIATIONS = {
|
|
154
172
|
supervisorSetup: "run `install conductor` (or `setup-epic`) for this repository to write the project-default supervisor posture.",
|
|
155
173
|
supervisorConfig: "run `install conductor` for this repository to write the project-default supervisor configuration.",
|
|
156
|
-
requiredChecks: "configure a non-empty required-check list in done_gate_config; an empty list makes the done gate pass unconditionally.",
|
|
157
174
|
mergeApproval: "set the merge-approval requirement explicitly in the supervisor configuration rather than leaving it unset.",
|
|
158
175
|
reviewPolicyMissing: "set the repository review-policy default so the merge path and the code_review gate read the same signal.",
|
|
159
176
|
reviewPolicyDivergent: "align the repository review-policy default with the done gate's review_state condition, or confirm the difference is intended.",
|
|
@@ -163,7 +180,7 @@ const SERVER_REMEDIATIONS = {
|
|
|
163
180
|
reconcilerUnreadable: "retry once the Bridge API is reachable; reconciler liveness is unknown here, not confirmed stale.",
|
|
164
181
|
reconcilerStale: "start the reconciler for this repository (the `epic-implementer epic-tick` schedule) so ticks resume.",
|
|
165
182
|
executorUnreadable: "retry once the Bridge API is reachable; no executor readiness claim is ever made from local state.",
|
|
166
|
-
executorNeverSeen: "provision and start an executor (`install conductor --executor-id <id
|
|
183
|
+
executorNeverSeen: "provision and start an executor (`install conductor --executor-id <id> --repo-wide` or `executor install-service`); readiness comes only from a live server observation.",
|
|
167
184
|
executorStale: "restart the executor service; it was observed once and has gone quiet past the server's staleness threshold.",
|
|
168
185
|
reviewWorkflowUnknown: "re-run; the default branch's review workflow could not be evaluated, which is not the same as it being absent.",
|
|
169
186
|
reviewWorkflowIncomplete: "install the claude-review workflow template on the default branch so it emits both the run id and the reviewed SHA the verdict protocol reads.",
|
|
@@ -175,6 +192,11 @@ const SERVER_REMEDIATIONS = {
|
|
|
175
192
|
unattendedConsent: "set `unattended_epic_implementer_allowed` for this repository (docs/claude/account-settings-operator-runbook.md), or run setup with --attended.",
|
|
176
193
|
unattendedNotifyDefault: "supply a --policy-file declaring notify.webhook_url, or run setup with --attended; an unattended run must have a working escalation channel, and notify.local_sink is not one.",
|
|
177
194
|
repositoryWorkflowReadiness: "run `install-bridge conductor` so the review and conductor-CI workflows are confirmed on the default branch, or run setup with --attended.",
|
|
195
|
+
// BAPI-1135. Both are IMPORTED from the shared classifier rather than written
|
|
196
|
+
// out here, so the remedy command has exactly one spelling across the refusal,
|
|
197
|
+
// the readiness row, and the doctor — see `merge-execution-contract.ts`.
|
|
198
|
+
mergeExecutionUnverifiable: MERGE_EXECUTION_UNVERIFIABLE_REMEDIATION,
|
|
199
|
+
mergeExecutionIneligible: MERGE_EXECUTION_INELIGIBLE_REMEDIATION,
|
|
178
200
|
};
|
|
179
201
|
/**
|
|
180
202
|
* Project the validated server readiness response into `server.*` checks.
|
|
@@ -230,18 +252,20 @@ export function mapServerReadinessToChecks(readiness, failure = "unreachable") {
|
|
|
230
252
|
});
|
|
231
253
|
// Counts and booleans are safe facts and stay in `detail`; they never become
|
|
232
254
|
// checks of their own, because there is nothing separate to remediate.
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
:
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
255
|
+
//
|
|
256
|
+
// BAPI-1254: a nonempty count is not proof. The shared presentation — the same
|
|
257
|
+
// one the install-time section renders — passes ONLY a server `verified`
|
|
258
|
+
// verdict; a confirmed mismatch, could-not-verify, an empty list, and an older
|
|
259
|
+
// server's silence all fail, with distinct fixed copy.
|
|
260
|
+
const requiredChecks = presentRequiredChecks(s);
|
|
261
|
+
outcomes.push({
|
|
262
|
+
id: "supervisor-required-checks",
|
|
263
|
+
status: requiredChecks.satisfied ? "pass" : "fail",
|
|
264
|
+
detail: requiredChecks.detail,
|
|
265
|
+
...(requiredChecks.remediation === undefined
|
|
266
|
+
? {}
|
|
267
|
+
: { remediation: requiredChecks.remediation }),
|
|
268
|
+
});
|
|
245
269
|
// Auto-merge OFF is a posture, not a gap: an operator may deliberately merge
|
|
246
270
|
// by hand. Reported, never remediated.
|
|
247
271
|
outcomes.push({
|
|
@@ -279,6 +303,7 @@ export function mapServerReadinessToChecks(readiness, failure = "unreachable") {
|
|
|
279
303
|
remediation: SERVER_REMEDIATIONS.githubCredentials,
|
|
280
304
|
});
|
|
281
305
|
outcomes.push(githubActionsOutcome(readiness));
|
|
306
|
+
outcomes.push(...mergeExecutionOutcomes(readiness));
|
|
282
307
|
outcomes.push(...bringUpOutcomes(readiness));
|
|
283
308
|
outcomes.push(reconcilerOutcome(readiness));
|
|
284
309
|
outcomes.push(executorOutcome(readiness));
|
|
@@ -286,7 +311,7 @@ export function mapServerReadinessToChecks(readiness, failure = "unreachable") {
|
|
|
286
311
|
outcomes.push(ciWorkflowOutcome(readiness));
|
|
287
312
|
outcomes.push(...unattendedOutcomes(readiness));
|
|
288
313
|
const byId = new Map(outcomes.map((outcome) => [outcome.id, outcome]));
|
|
289
|
-
|
|
314
|
+
const described = SERVER_READINESS_DESCRIPTORS.map(({ id, label }) => {
|
|
290
315
|
const outcome = byId.get(id);
|
|
291
316
|
if (!outcome) {
|
|
292
317
|
return createReadinessCheck({
|
|
@@ -309,6 +334,97 @@ export function mapServerReadinessToChecks(readiness, failure = "unreachable") {
|
|
|
309
334
|
: { remediation: outcome.remediation ?? SERVER_REMEDIATIONS.olderServer }),
|
|
310
335
|
});
|
|
311
336
|
});
|
|
337
|
+
// BAPI-1260 — appended ONLY when the server reported the block, so every
|
|
338
|
+
// pre-BAPI-1260 report keeps exactly the descriptor set above.
|
|
339
|
+
return [...described, ...admissionEvidenceChecks(readiness)];
|
|
340
|
+
}
|
|
341
|
+
/** Human labels for the known BAPI-1260 evidence predicates. */
|
|
342
|
+
const ADMISSION_EVIDENCE_LABELS = {
|
|
343
|
+
unattended_consent: "Launch evidence: unattended consent",
|
|
344
|
+
notify_webhook: "Launch evidence: notification destination",
|
|
345
|
+
repository_readiness: "Launch evidence: repository workflows",
|
|
346
|
+
required_checks: "Launch evidence: required CI checks",
|
|
347
|
+
index_provenance: "Launch evidence: canonical index provenance",
|
|
348
|
+
durable_actor: "Launch evidence: durable actor handshake",
|
|
349
|
+
migration_head: "Launch evidence: database migration head",
|
|
350
|
+
};
|
|
351
|
+
/** Who acts, in fixed prose. Never a value from the payload. */
|
|
352
|
+
const ADMISSION_OWNER_REMEDIATION = {
|
|
353
|
+
operator: "the operator re-runs guided setup (`install conductor --launch-profile <name>`) and re-observes.",
|
|
354
|
+
repository_admin: "a repository administrator resolves it through the account-settings runbook; setup never grants it.",
|
|
355
|
+
bridge_operations: "escalate to Bridge operations; this is not something an operator command can change.",
|
|
356
|
+
};
|
|
357
|
+
/**
|
|
358
|
+
* Project the server's typed launch evidence (BAPI-1260) into `server.*`
|
|
359
|
+
* checks, read-only.
|
|
360
|
+
*
|
|
361
|
+
* The CLIENT decides nothing here: `confirmed` renders pass, `not_applicable`
|
|
362
|
+
* renders skip, and `denied` and `unable_to_verify` both render fail with
|
|
363
|
+
* DIFFERENT detail — a known denial and an inability to verify call for
|
|
364
|
+
* different actions, and neither authorizes a launch. The server is the only
|
|
365
|
+
* admission authority; this row is what it said, not a verdict of our own.
|
|
366
|
+
*/
|
|
367
|
+
export function admissionEvidenceChecks(readiness) {
|
|
368
|
+
const evidence = readiness.admission_evidence;
|
|
369
|
+
if (evidence === undefined)
|
|
370
|
+
return [];
|
|
371
|
+
if (evidence === null || evidence.schema_version !== ADMISSION_EVIDENCE_SCHEMA_VERSION) {
|
|
372
|
+
return [
|
|
373
|
+
createReadinessCheckSafely({
|
|
374
|
+
id: "server.admission-evidence",
|
|
375
|
+
source: "server",
|
|
376
|
+
label: "Launch evidence",
|
|
377
|
+
status: "fail",
|
|
378
|
+
detail: evidence === null
|
|
379
|
+
? "the server reported no launch evidence, so no mandatory prerequisite is proven"
|
|
380
|
+
: "the server's launch-evidence schema is not one this CLI renders; treat every prerequisite as unverified",
|
|
381
|
+
remediation: "upgrade the CLI or the Bridge API deploy so both speak the same evidence schema.",
|
|
382
|
+
}),
|
|
383
|
+
];
|
|
384
|
+
}
|
|
385
|
+
return evidence.records.map((record) => admissionEvidenceCheck(record));
|
|
386
|
+
}
|
|
387
|
+
function admissionEvidenceCheck(record) {
|
|
388
|
+
const slug = record.predicate.replace(/[^a-z0-9]+/g, "-").replace(/^-+|-+$/g, "") || "unknown";
|
|
389
|
+
const label = ADMISSION_EVIDENCE_LABELS[record.predicate] ?? "Launch evidence";
|
|
390
|
+
const reason = record.reason ? ` (${record.reason})` : "";
|
|
391
|
+
const remediation = ADMISSION_OWNER_REMEDIATION[record.next_action_owner ?? "operator"] ?? ADMISSION_OWNER_REMEDIATION.operator;
|
|
392
|
+
switch (record.verdict) {
|
|
393
|
+
case "confirmed":
|
|
394
|
+
return createReadinessCheckSafely({
|
|
395
|
+
id: `server.admission-${slug}`,
|
|
396
|
+
source: "server",
|
|
397
|
+
label,
|
|
398
|
+
status: "pass",
|
|
399
|
+
detail: `confirmed by ${record.source}; fresh for ${record.max_age_seconds}s after observation`,
|
|
400
|
+
});
|
|
401
|
+
case "not_applicable":
|
|
402
|
+
return createReadinessCheckSafely({
|
|
403
|
+
id: `server.admission-${slug}`,
|
|
404
|
+
source: "server",
|
|
405
|
+
label,
|
|
406
|
+
status: "skip",
|
|
407
|
+
detail: `not applicable${reason}`,
|
|
408
|
+
});
|
|
409
|
+
case "denied":
|
|
410
|
+
return createReadinessCheckSafely({
|
|
411
|
+
id: `server.admission-${slug}`,
|
|
412
|
+
source: "server",
|
|
413
|
+
label,
|
|
414
|
+
status: "fail",
|
|
415
|
+
detail: `DENIED by ${record.source}${reason} — dependent activation, dispatch, and privileged operations are held`,
|
|
416
|
+
remediation,
|
|
417
|
+
});
|
|
418
|
+
default:
|
|
419
|
+
return createReadinessCheckSafely({
|
|
420
|
+
id: `server.admission-${slug}`,
|
|
421
|
+
source: "server",
|
|
422
|
+
label,
|
|
423
|
+
status: "fail",
|
|
424
|
+
detail: `could not be verified${reason} — unknown is not safe, so dependent work is held`,
|
|
425
|
+
remediation,
|
|
426
|
+
});
|
|
427
|
+
}
|
|
312
428
|
}
|
|
313
429
|
/**
|
|
314
430
|
* The three BAPI-1153 bring-up safety rows.
|
|
@@ -460,6 +576,64 @@ function githubActionsOutcome(readiness) {
|
|
|
460
576
|
remediation: SERVER_REMEDIATIONS.actionsMissing,
|
|
461
577
|
};
|
|
462
578
|
}
|
|
579
|
+
/**
|
|
580
|
+
* The two BAPI-1135 merge-execution rows.
|
|
581
|
+
*
|
|
582
|
+
* Both are backed SOLELY by {@link classifyMergeExecutionEligibility}, so this
|
|
583
|
+
* gate, the install doctor's section, the `drive-epic` requirement, and the
|
|
584
|
+
* `setup-epic` refusal cannot disagree about the status or the first blocker.
|
|
585
|
+
* Nothing here re-reads the server block or re-derives a fact.
|
|
586
|
+
*
|
|
587
|
+
* A `could_not_verify` verdict produces a RETRY-oriented row on both, and never
|
|
588
|
+
* claims the capability is absent. A confirmed insufficiency produces a
|
|
589
|
+
* `not eligible` row carrying the exact shared remedy command. The two are
|
|
590
|
+
* deliberately different statuses — `fail` for confirmed, `warn` for unknown —
|
|
591
|
+
* so the visible status label distinguishes them without relying on colour.
|
|
592
|
+
*
|
|
593
|
+
* Which of the two rows carries the blocker is decided by the reason: a branch
|
|
594
|
+
* policy that could not be read or an actor that is not admitted belongs to the
|
|
595
|
+
* branch-policy row; everything else belongs to the workflow row. The other row
|
|
596
|
+
* then reports that it was not reached, rather than showing a misleading pass.
|
|
597
|
+
*/
|
|
598
|
+
function mergeExecutionOutcomes(readiness) {
|
|
599
|
+
const verdict = classifyMergeExecutionEligibility(readiness);
|
|
600
|
+
const branchScoped = verdict.reason === "branch_policy_unreadable" || verdict.reason === "actor_not_admitted";
|
|
601
|
+
if (verdict.status === "eligible") {
|
|
602
|
+
return [
|
|
603
|
+
{
|
|
604
|
+
id: "merge-execution-workflow",
|
|
605
|
+
status: "pass",
|
|
606
|
+
detail: verdict.detail,
|
|
607
|
+
},
|
|
608
|
+
{
|
|
609
|
+
id: "merge-execution-branch-policy",
|
|
610
|
+
status: "pass",
|
|
611
|
+
detail: "the branch policy admits the workflow actor",
|
|
612
|
+
},
|
|
613
|
+
];
|
|
614
|
+
}
|
|
615
|
+
const status = verdict.status === "could_not_verify" ? "warn" : "fail";
|
|
616
|
+
const remediation = verdict.status === "could_not_verify"
|
|
617
|
+
? SERVER_REMEDIATIONS.mergeExecutionUnverifiable
|
|
618
|
+
: SERVER_REMEDIATIONS.mergeExecutionIneligible;
|
|
619
|
+
const blocked = {
|
|
620
|
+
id: branchScoped ? "merge-execution-branch-policy" : "merge-execution-workflow",
|
|
621
|
+
status,
|
|
622
|
+
detail: verdict.status === "could_not_verify"
|
|
623
|
+
? `could not verify: ${verdict.detail}`
|
|
624
|
+
: `not eligible: ${verdict.detail}`,
|
|
625
|
+
remediation,
|
|
626
|
+
};
|
|
627
|
+
// The row that did not decide reports exactly that. Showing it as a pass would
|
|
628
|
+
// claim evidence the classifier stopped short of gathering.
|
|
629
|
+
const notReached = {
|
|
630
|
+
id: branchScoped ? "merge-execution-workflow" : "merge-execution-branch-policy",
|
|
631
|
+
status: "warn",
|
|
632
|
+
detail: "could not verify: not evaluated — an earlier merge-eligibility fact decided first",
|
|
633
|
+
remediation,
|
|
634
|
+
};
|
|
635
|
+
return branchScoped ? [notReached, blocked] : [blocked, notReached];
|
|
636
|
+
}
|
|
463
637
|
/** Reconciler liveness, preserving the server's own `stale` conclusion. */
|
|
464
638
|
function reconcilerOutcome(readiness) {
|
|
465
639
|
const r = readiness.reconciler;
|
|
@@ -20,7 +20,7 @@
|
|
|
20
20
|
import { randomUUID } from "crypto";
|
|
21
21
|
import { adoptCurrentHeadAndUnparkTicket, completeMergeChild, ConductorBridgeApiError, dispatchRemediation, fetchEpicRunState, mergeChild, patchPolicy, resumeRun, safeDiagnosticMessage, scopeCatchUp, scopeReparse, stopEpicRun, unparkEpicTicket, updateEpicRunStatus, } from "./bridge-api-client.js";
|
|
22
22
|
import { DEFAULT_GATE_NAME } from "./git-ci-types.js";
|
|
23
|
-
import { resolveLocalMergeMethod, runApprovedLocalMerge } from "./local-merge.js";
|
|
23
|
+
import { resolveLocalMergeCredential, resolveLocalMergeMethod, runApprovedLocalMerge, } from "./local-merge.js";
|
|
24
24
|
/**
|
|
25
25
|
* Bounded total attempts for a ticket-recovery CAS retry loop. Each attempt
|
|
26
26
|
* begins with a fresh, authoritative state read — so this bounds full
|
|
@@ -312,7 +312,13 @@ export async function mergeChildRepair(access, options, deps = {}) {
|
|
|
312
312
|
// which is discarded — never the server's action key, which the server
|
|
313
313
|
// recomposes itself at completion.
|
|
314
314
|
action_key: `merge-child:${options.epicRunId}:${options.prNumber}`,
|
|
315
|
-
}, { method: resolveLocalMergeMethod(local.merge_method) },
|
|
315
|
+
}, { method: resolveLocalMergeMethod(local.merge_method) },
|
|
316
|
+
// BAPI-1136: an explicitly configured local credential, never the ambient
|
|
317
|
+
// login. `env` remains the source of the allowlisted operational base only.
|
|
318
|
+
{
|
|
319
|
+
env: deps.env ?? process.env,
|
|
320
|
+
credential: resolveLocalMergeCredential(deps.env ?? process.env, "local"),
|
|
321
|
+
});
|
|
316
322
|
succeeded = localResult.status === "succeeded";
|
|
317
323
|
alreadyMerged = succeeded && localResult.reason === "already_merged";
|
|
318
324
|
failureReason = boundedLocalMergeReason(localResult.reason);
|