@bridge_gpt/mcp-server 0.2.59 → 0.2.60

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.
Files changed (71) hide show
  1. package/README.md +173 -51
  2. package/build/agent-guidance.generated.js +1 -1
  3. package/build/agent-notes.js +7 -1
  4. package/build/commands.generated.js +4 -4
  5. package/build/conductor/bridge-api-client.js +53 -2
  6. package/build/conductor/doctor.js +255 -53
  7. package/build/conductor/epic-runtime.js +11 -2
  8. package/build/conductor/install-doctor.js +24 -2
  9. package/build/conductor/local-merge.js +319 -26
  10. package/build/conductor/merge-execution-contract.js +61 -0
  11. package/build/conductor/merge-execution-eligibility.js +129 -0
  12. package/build/conductor/readiness-sections.js +159 -2
  13. package/build/conductor/readiness.js +95 -14
  14. package/build/conductor/recovery-operations.js +8 -2
  15. package/build/connect-bitbucket-api.js +6 -1
  16. package/build/connect-bitbucket.js +398 -88
  17. package/build/credential-store.js +16 -0
  18. package/build/docs.generated.js +2 -2
  19. package/build/doctor.js +183 -9
  20. package/build/drive-epic.js +23 -0
  21. package/build/epic-implementer-bin.js +10 -10
  22. package/build/epic-implementer-claude-hook-bin.js +1 -1
  23. package/build/epic-implementer-env.js +43 -14
  24. package/build/executor/ci-rerun-job.js +269 -0
  25. package/build/executor/cli.js +31 -0
  26. package/build/executor/credentials.js +11 -0
  27. package/build/executor/env.js +60 -0
  28. package/build/executor/index.js +21 -4
  29. package/build/executor/job-runner.js +126 -0
  30. package/build/executor/job-types.js +12 -2
  31. package/build/executor/merge-job.js +11 -2
  32. package/build/executor/service-lifecycle.js +136 -0
  33. package/build/executor/service-unit.js +427 -9
  34. package/build/executor/types.js +5 -0
  35. package/build/index.js +194 -179
  36. package/build/install-bridge-conductor.js +201 -8
  37. package/build/install-bridge.js +356 -30
  38. package/build/mcp-install-state.js +42 -4
  39. package/build/merge-pull-request.js +103 -10
  40. package/build/merge-workflow-codeowners.js +224 -0
  41. package/build/merge-workflow-config.js +133 -0
  42. package/build/merge-workflow-drift.js +66 -0
  43. package/build/merge-workflow-install.js +638 -0
  44. package/build/merge-workflow.js +572 -0
  45. package/build/pipelines.generated.js +2 -2
  46. package/build/plane/cli.js +24 -2
  47. package/build/plane/preflight.js +68 -0
  48. package/build/readme.generated.js +1 -1
  49. package/build/run-unit-tests-launcher.js +5 -0
  50. package/build/setup-epic.js +163 -9
  51. package/build/version.generated.js +3 -3
  52. package/package.json +3 -3
  53. package/build/conduct-epic/bridge-client.js +0 -773
  54. package/build/conduct-epic/checkpoint-store.js +0 -542
  55. package/build/conduct-epic/cli.js +0 -3145
  56. package/build/conduct-epic/cut-protocol.js +0 -392
  57. package/build/conduct-epic/lock.js +0 -302
  58. package/build/conduct-epic/pr-state.js +0 -286
  59. package/build/conduct-epic/spawn.js +0 -113
  60. package/build/conductor/cli.js +0 -1278
  61. package/build/conductor-bundle-cli.js +0 -256
  62. package/build/direct-ticket-tools.js +0 -70
  63. package/build/executor/reconcile-overlap-governance.js +0 -129
  64. package/build/implement-epic/bridge-client.js +0 -773
  65. package/build/implement-epic/checkpoint-store.js +0 -542
  66. package/build/implement-epic/cli.js +0 -3158
  67. package/build/implement-epic/cut-protocol.js +0 -392
  68. package/build/implement-epic/lock.js +0 -302
  69. package/build/implement-epic/pr-state.js +0 -286
  70. package/build/implement-epic/spawn.js +0 -113
  71. 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_EXPECTED_VERSION, MERGE_WORKFLOW_PATH, } 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,7 +73,7 @@ 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>` or " +
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. */
@@ -216,6 +218,94 @@ export function githubActionsSection(readiness) {
216
218
  remediation: "grant the GitHub App `actions: write`; the conductor's workflow rerun lane fails open on 403 without it.",
217
219
  };
218
220
  }
221
+ /**
222
+ * The "Merge execution eligibility" section (BAPI-1135).
223
+ *
224
+ * A DEDICATED section rather than extra rows on the GitHub App block, because it
225
+ * is a different safety diagnostic about a different principal: the App block
226
+ * describes the installation's dispatch permission, this one describes whether
227
+ * the repository's merge workflow can actually merge.
228
+ *
229
+ * Rows are rendered in DEPENDENCY order — probe/readability, presence, enabled
230
+ * state, exact version, App `actions: write`, workflow-token write authority,
231
+ * branch-policy admission — so a reader meets the fact that decided the verdict
232
+ * before the facts that were never reached.
233
+ *
234
+ * Three rendering rules this section holds to:
235
+ *
236
+ * * an absent older-server block and a failed probe are explicit UNKNOWN rows,
237
+ * never an omitted section and never a pass. "Not reported" is not "healthy";
238
+ * omitting the section would make an unknown look like a clean bill of health.
239
+ * * confirmed policy denial stays verbally distinct from unreadability. The
240
+ * status word (`degraded` for unknown, `fatal`-adjacent `degraded` with
241
+ * different copy for confirmed) is carried in TEXT, not by colour alone, and
242
+ * the detail line opens with `could not verify:` or `not eligible:`.
243
+ * * command strings, workflow paths, versions, and reason codes are rendered as
244
+ * backticked monospace through the established section conventions, and are
245
+ * allowed to wrap rather than being truncated — a truncated remedy command is
246
+ * not a remedy.
247
+ */
248
+ export function mergeExecutionSections(readiness) {
249
+ const verdict = classifyMergeExecutionEligibility(readiness);
250
+ const m = readiness.merge_execution;
251
+ if (m === null || m === undefined) {
252
+ // One row, not seven. Fabricating six more unknown rows from a block that
253
+ // was never reported would imply the server answered six questions.
254
+ return [
255
+ {
256
+ id: "merge-execution-eligibility",
257
+ label: "Merge execution eligibility",
258
+ status: "degraded",
259
+ detail: `could not verify: ${verdict.detail}`,
260
+ remediation: verdict.remediation ?? MERGE_EXECUTION_UNVERIFIABLE_REMEDIATION,
261
+ },
262
+ ];
263
+ }
264
+ const decisive = {
265
+ id: "merge-execution-eligibility",
266
+ label: "Merge execution eligibility",
267
+ status: verdict.status === "eligible" ? "ok" : "degraded",
268
+ detail: verdict.status === "eligible"
269
+ ? `eligible: ${verdict.detail}`
270
+ : verdict.status === "could_not_verify"
271
+ ? `could not verify: ${verdict.detail}`
272
+ : `not eligible: ${verdict.detail} (\`${verdict.reason}\`)`,
273
+ ...(verdict.remediation ? { remediation: verdict.remediation } : {}),
274
+ };
275
+ // The supporting facts, in dependency order. Each is a plain readable row — no
276
+ // remediation of its own, because the decisive row above already names the one
277
+ // action, and repeating it seven times would bury it.
278
+ const fact = (id, label, ok, detail) => ({
279
+ id,
280
+ label,
281
+ status: ok ? "ok" : "degraded",
282
+ detail,
283
+ });
284
+ return [
285
+ decisive,
286
+ fact("merge-execution-branch-policy-readable", "Branch policy readable", m.branch_protection_probe_succeeded, m.branch_protection_probe_succeeded
287
+ ? `read on the ${m.branch_scope.replace("_", " ")}`
288
+ : "could not verify: the branch policy could not be read"),
289
+ fact("merge-execution-workflow-present", "Merge workflow present", m.merge_workflow_probe_succeeded && m.merge_workflow_present, !m.merge_workflow_probe_succeeded
290
+ ? "could not verify: the workflow metadata could not be read"
291
+ : m.merge_workflow_present
292
+ ? `\`${MERGE_WORKFLOW_PATH}\` present`
293
+ : `not eligible: \`${MERGE_WORKFLOW_PATH}\` is absent`),
294
+ fact("merge-execution-workflow-enabled", "Merge workflow enabled", m.merge_workflow_enabled, m.merge_workflow_enabled ? "enabled" : "not eligible: disabled or unknown"),
295
+ fact("merge-execution-workflow-version", "Merge workflow version", m.merge_workflow_version === MERGE_WORKFLOW_EXPECTED_VERSION, `\`${m.merge_workflow_version ?? "unset"}\` (expected \`${MERGE_WORKFLOW_EXPECTED_VERSION}\`)`),
296
+ fact("merge-execution-app-actions-write", "GitHub App `actions: write`", readiness.github.actions_write, readiness.github.actions_probe_succeeded
297
+ ? `actions: \`${readiness.github.actions_permission_level}\``
298
+ : "could not verify: the App permission could not be checked"),
299
+ fact("merge-execution-workflow-token-write", "Workflow token write authority", m.merge_workflow_token_write_authority, m.merge_workflow_token_write_authority
300
+ ? "the merge workflow's own token holds write authority"
301
+ : "not eligible: the merge workflow's own token does not hold write authority"),
302
+ fact("merge-execution-actor-admitted", "Branch policy admits the workflow actor", m.branch_protection_actions_bot_admitted, m.branch_protection_probe_succeeded
303
+ ? m.branch_protection_actions_bot_admitted
304
+ ? "admitted"
305
+ : "not eligible: the workflow actor is excluded"
306
+ : "could not verify: the branch policy could not be read"),
307
+ ];
308
+ }
219
309
  export function reconcilerSection(readiness) {
220
310
  const r = readiness.reconciler;
221
311
  if (!r.liveness_readable) {
@@ -630,6 +720,73 @@ export function conductorHookTargetsSection(legacyConductor, conductorContext) {
630
720
  * sanitized failure kind, because the common cause is npm silently skipping an
631
721
  * optionalDependency build and that is undiagnosable without the ABI.
632
722
  */
723
+ /**
724
+ * Project the doctor's read-only `github_actions` merge OBSERVABILITY inspection
725
+ * into this layer's sections (BAPI-1135).
726
+ *
727
+ * Projected here rather than formatted inside installation code, for the same
728
+ * reason every other doctor fact is: the install-time doctor and the advisory
729
+ * readiness gate must render one classification, and a second formatter in the
730
+ * installer is how the two come to describe the same probe differently.
731
+ *
732
+ * Two sections, never one. `gh` authentication and endpoint reachability are
733
+ * independently actionable, and a collapsed row would hide which of the two the
734
+ * operator has to fix.
735
+ *
736
+ * These describe THIS HOST's ability to observe — never whether the capability
737
+ * is sufficient. {@link mergeExecutionSections} owns that, from the server's own
738
+ * report. The wording keeps them apart deliberately: `not observable` and `could
739
+ * not verify` here, versus `not eligible` there.
740
+ */
741
+ export function gitHubActionsMergeObservabilitySections(legacyConductor) {
742
+ const authBase = {
743
+ id: "gh-actions-merge-auth",
744
+ label: "GitHub Actions merge: local observation prerequisites",
745
+ };
746
+ const observeBase = {
747
+ id: "gh-actions-merge-observable",
748
+ label: "GitHub Actions merge: capability observability",
749
+ };
750
+ const inspection = legacyConductor?.github_actions_merge;
751
+ if (inspection === undefined || inspection === null) {
752
+ // The doctor was not collected, or predates this inspection. Both rows say
753
+ // so rather than being omitted — an absent row reads as "fine".
754
+ const detail = "could not verify — the epic-implementer doctor reported no observation";
755
+ return [
756
+ { ...authBase, status: "degraded", detail },
757
+ { ...observeBase, status: "degraded", detail },
758
+ ];
759
+ }
760
+ return [
761
+ inspection.gh_authed
762
+ ? { ...authBase, status: "ok", detail: "eligible to observe — `gh` is authenticated" }
763
+ : {
764
+ ...authBase,
765
+ status: "degraded",
766
+ detail: "not observable — no authenticated `gh` identity on this host",
767
+ remediation: GH_ACTIONS_MERGE_AUTH_REMEDIATION,
768
+ },
769
+ !inspection.gh_authed
770
+ ? {
771
+ ...observeBase,
772
+ status: "degraded",
773
+ detail: GH_ACTIONS_MERGE_SKIPPED_DETAIL,
774
+ remediation: GH_ACTIONS_MERGE_AUTH_REMEDIATION,
775
+ }
776
+ : inspection.capability_observable
777
+ ? {
778
+ ...observeBase,
779
+ status: "ok",
780
+ detail: "eligible to observe — the workflow metadata endpoint answered",
781
+ }
782
+ : {
783
+ ...observeBase,
784
+ status: "degraded",
785
+ detail: "could not verify — the workflow metadata endpoint could not be read",
786
+ remediation: GH_ACTIONS_MERGE_UNOBSERVABLE_REMEDIATION,
787
+ },
788
+ ];
789
+ }
633
790
  export function conductorLedgerLoadabilitySection(legacyConductor) {
634
791
  const base = {
635
792
  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
- * ## Deferred retirement: local `gh` (AC-7)
51
- *
52
- * **Detection** is `conductor.gh-cli` and `conductor.gh-auth`, carried as two
53
- * separate checks because installing `gh` and authenticating it are two
54
- * different operator actions. **Containment** is the existing local-merge
55
- * execution path, which refuses and emits `merge.failed`/`merge.skipped` rather
56
- * than merging without auth — a missing `gh` degrades a run, it does not corrupt
57
- * one. **Escalation** targets the invoking operator first; there is no automated
58
- * fallback, because the GitHub App token is `contents: read` and structurally
59
- * cannot merge (see `mcp_server/src/executor/merge-job.ts`). **Retirement** is
60
- * owned by epic A2, after the App is elevated to `contents: write`. Until A2
61
- * ships this check is load-bearing and no code here may remove, hide, or
62
- * downgrade it.
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
  *
@@ -89,6 +98,7 @@ import { mapPlanePreflightToReadinessChecks, } from "../plane/preflight.js";
89
98
  import { runConductorInstallDoctor, } from "./install-doctor.js";
90
99
  import { BRING_UP_SECTION_LABELS } 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";
93
103
  /** This report's own schema version. See the module docstring. */
94
104
  export const CONDUCTOR_READINESS_GATE_SCHEMA_VERSION = 1;
@@ -110,6 +120,13 @@ export const SERVER_READINESS_DESCRIPTORS = [
110
120
  { id: "review-policy", label: "Review policy" },
111
121
  { id: "github-credentials", label: "GitHub App credentials" },
112
122
  { id: "github-actions-permission", label: "GitHub App actions permission" },
123
+ // BAPI-1135 — merge-execution eligibility, BESIDE the App permission row and
124
+ // never replacing it: that row is the installation's dispatch/rerun
125
+ // permission, these two are the merge workflow and the branch policy that
126
+ // governs it. Two rows rather than one, because a workflow that is absent and
127
+ // a branch policy that excludes the actor are separately actionable.
128
+ { id: "merge-execution-workflow", label: "Merge workflow eligibility" },
129
+ { id: "merge-execution-branch-policy", label: "Merge branch policy" },
113
130
  // BAPI-1153 — the "Bring-up safety" group: deployment-wide facts that decide
114
131
  // whether a plane can drive a run at all, ahead of the repository-scoped
115
132
  // reconciler/executor rows. Labels come from the shared section layer.
@@ -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>` or `executor install-service`); readiness comes only from a live server observation.",
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.
@@ -279,6 +301,7 @@ export function mapServerReadinessToChecks(readiness, failure = "unreachable") {
279
301
  remediation: SERVER_REMEDIATIONS.githubCredentials,
280
302
  });
281
303
  outcomes.push(githubActionsOutcome(readiness));
304
+ outcomes.push(...mergeExecutionOutcomes(readiness));
282
305
  outcomes.push(...bringUpOutcomes(readiness));
283
306
  outcomes.push(reconcilerOutcome(readiness));
284
307
  outcomes.push(executorOutcome(readiness));
@@ -460,6 +483,64 @@ function githubActionsOutcome(readiness) {
460
483
  remediation: SERVER_REMEDIATIONS.actionsMissing,
461
484
  };
462
485
  }
486
+ /**
487
+ * The two BAPI-1135 merge-execution rows.
488
+ *
489
+ * Both are backed SOLELY by {@link classifyMergeExecutionEligibility}, so this
490
+ * gate, the install doctor's section, the `drive-epic` requirement, and the
491
+ * `setup-epic` refusal cannot disagree about the status or the first blocker.
492
+ * Nothing here re-reads the server block or re-derives a fact.
493
+ *
494
+ * A `could_not_verify` verdict produces a RETRY-oriented row on both, and never
495
+ * claims the capability is absent. A confirmed insufficiency produces a
496
+ * `not eligible` row carrying the exact shared remedy command. The two are
497
+ * deliberately different statuses — `fail` for confirmed, `warn` for unknown —
498
+ * so the visible status label distinguishes them without relying on colour.
499
+ *
500
+ * Which of the two rows carries the blocker is decided by the reason: a branch
501
+ * policy that could not be read or an actor that is not admitted belongs to the
502
+ * branch-policy row; everything else belongs to the workflow row. The other row
503
+ * then reports that it was not reached, rather than showing a misleading pass.
504
+ */
505
+ function mergeExecutionOutcomes(readiness) {
506
+ const verdict = classifyMergeExecutionEligibility(readiness);
507
+ const branchScoped = verdict.reason === "branch_policy_unreadable" || verdict.reason === "actor_not_admitted";
508
+ if (verdict.status === "eligible") {
509
+ return [
510
+ {
511
+ id: "merge-execution-workflow",
512
+ status: "pass",
513
+ detail: verdict.detail,
514
+ },
515
+ {
516
+ id: "merge-execution-branch-policy",
517
+ status: "pass",
518
+ detail: "the branch policy admits the workflow actor",
519
+ },
520
+ ];
521
+ }
522
+ const status = verdict.status === "could_not_verify" ? "warn" : "fail";
523
+ const remediation = verdict.status === "could_not_verify"
524
+ ? SERVER_REMEDIATIONS.mergeExecutionUnverifiable
525
+ : SERVER_REMEDIATIONS.mergeExecutionIneligible;
526
+ const blocked = {
527
+ id: branchScoped ? "merge-execution-branch-policy" : "merge-execution-workflow",
528
+ status,
529
+ detail: verdict.status === "could_not_verify"
530
+ ? `could not verify: ${verdict.detail}`
531
+ : `not eligible: ${verdict.detail}`,
532
+ remediation,
533
+ };
534
+ // The row that did not decide reports exactly that. Showing it as a pass would
535
+ // claim evidence the classifier stopped short of gathering.
536
+ const notReached = {
537
+ id: branchScoped ? "merge-execution-workflow" : "merge-execution-branch-policy",
538
+ status: "warn",
539
+ detail: "could not verify: not evaluated — an earlier merge-eligibility fact decided first",
540
+ remediation,
541
+ };
542
+ return branchScoped ? [notReached, blocked] : [blocked, notReached];
543
+ }
463
544
  /** Reconciler liveness, preserving the server's own `stale` conclusion. */
464
545
  function reconcilerOutcome(readiness) {
465
546
  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) }, { env: deps.env ?? process.env });
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);
@@ -138,7 +138,12 @@ export async function mintBitbucketConnection(deps, repoName) {
138
138
  if (!body || !state || !installUrl || typeof ttlSeconds !== "number") {
139
139
  return { ok: false, kind: "malformed" };
140
140
  }
141
- return { ok: true, value: { state, installUrl, ttlSeconds } };
141
+ // Optional and permissive ON PURPOSE, and only here: absent, null, empty, or any
142
+ // non-string collapses to null through the same nullable helper the candidate parser
143
+ // uses. Required-field strictness above is untouched — an optional field must not
144
+ // become a new way for a server change to fail a mint.
145
+ const redeemUrlTemplate = asNullableString(body.redeem_url_template);
146
+ return { ok: true, value: { state, installUrl, ttlSeconds, redeemUrlTemplate } };
142
147
  }
143
148
  /** Strictly parse candidates. A candidate missing a usable identity fails the whole list. */
144
149
  function parseCandidates(value) {