@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.
Files changed (78) hide show
  1. package/README.md +181 -51
  2. package/build/agent-guidance.generated.js +1 -1
  3. package/build/agent-notes.js +7 -1
  4. package/build/commands.generated.js +6 -6
  5. package/build/conductor/bridge-api-client.js +406 -4
  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 +86 -0
  11. package/build/conductor/merge-execution-eligibility.js +130 -0
  12. package/build/conductor/readiness-sections.js +256 -16
  13. package/build/conductor/readiness.js +203 -29
  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 +172 -0
  18. package/build/docs.generated.js +2 -2
  19. package/build/doctor.js +277 -15
  20. package/build/drive-epic.js +147 -12
  21. package/build/epic-implementer-bin.js +15 -15
  22. package/build/epic-implementer-claude-hook-bin.js +1 -1
  23. package/build/epic-implementer-env.js +43 -14
  24. package/build/epic-integration-pr.js +61 -15
  25. package/build/epic-launch-profile.js +455 -0
  26. package/build/executor/ci-rerun-job.js +269 -0
  27. package/build/executor/cli.js +40 -0
  28. package/build/executor/credentials.js +28 -1
  29. package/build/executor/env.js +60 -0
  30. package/build/executor/http-client.js +45 -1
  31. package/build/executor/index.js +21 -4
  32. package/build/executor/integration-job.js +759 -0
  33. package/build/executor/job-runner.js +218 -9
  34. package/build/executor/job-types.js +12 -2
  35. package/build/executor/merge-job.js +11 -2
  36. package/build/executor/observation.js +114 -7
  37. package/build/executor/runner.js +101 -1
  38. package/build/executor/service-lifecycle.js +136 -0
  39. package/build/executor/service-unit.js +427 -9
  40. package/build/executor/types.js +48 -0
  41. package/build/index.js +479 -240
  42. package/build/install-bridge-conductor.js +599 -14
  43. package/build/install-bridge.js +448 -46
  44. package/build/mcp-install-state.js +42 -4
  45. package/build/merge-pull-request.js +103 -10
  46. package/build/merge-workflow-codeowners.js +224 -0
  47. package/build/merge-workflow-config.js +187 -0
  48. package/build/merge-workflow-drift.js +97 -0
  49. package/build/merge-workflow-install.js +638 -0
  50. package/build/merge-workflow.js +956 -0
  51. package/build/pipelines.generated.js +3 -3
  52. package/build/plane/cli.js +24 -2
  53. package/build/plane/preflight.js +68 -0
  54. package/build/plane/supervisor.js +6 -0
  55. package/build/readme.generated.js +1 -1
  56. package/build/run-unit-tests-launcher.js +5 -0
  57. package/build/setup-epic.js +263 -38
  58. package/build/version.generated.js +3 -3
  59. package/package.json +3 -3
  60. package/build/conduct-epic/bridge-client.js +0 -773
  61. package/build/conduct-epic/checkpoint-store.js +0 -542
  62. package/build/conduct-epic/cli.js +0 -3145
  63. package/build/conduct-epic/cut-protocol.js +0 -392
  64. package/build/conduct-epic/lock.js +0 -302
  65. package/build/conduct-epic/pr-state.js +0 -286
  66. package/build/conduct-epic/spawn.js +0 -113
  67. package/build/conductor/cli.js +0 -1278
  68. package/build/conductor-bundle-cli.js +0 -256
  69. package/build/direct-ticket-tools.js +0 -70
  70. package/build/executor/reconcile-overlap-governance.js +0 -129
  71. package/build/implement-epic/bridge-client.js +0 -773
  72. package/build/implement-epic/checkpoint-store.js +0 -542
  73. package/build/implement-epic/cli.js +0 -3158
  74. package/build/implement-epic/cut-protocol.js +0 -392
  75. package/build/implement-epic/lock.js +0 -302
  76. package/build/implement-epic/pr-state.js +0 -286
  77. package/build/implement-epic/spawn.js +0 -113
  78. package/build/plan-epic-conductor-eligibility.js +0 -213
@@ -13,6 +13,7 @@ import os from "node:os";
13
13
  import { doctorConductorLedger } from "./store.js";
14
14
  import { detectClaudeLogin, formatClaudeLoginAdvisory } from "../claude-login.js";
15
15
  import { inspectConductorGitHooks } from "./git-hooks.js";
16
+ import { MERGE_WORKFLOW_IDENTIFIER } from "./merge-execution-contract.js";
16
17
  import { resolveMcpShimInvocationForRuntime, resolvePackageRootFromModuleUrl, } from "../mcp-server-invocation.js";
17
18
  import { MCP_PACKAGE_NAME } from "../mcp-identity.js";
18
19
  import { resolveProfiles } from "../mcp-profile.js";
@@ -38,13 +39,24 @@ export const MCP_PROFILE_REMEDIATION = "ADD `epic_implementer` to BRIDGE_MCP_PRO
38
39
  * is blocked on epic A2 (App elevation to `contents: write`); until A2 ships,
39
40
  * `gh` is a real prerequisite and this stays a named check.
40
41
  */
41
- export const GH_CLI_REMEDIATION = "install the GitHub CLI (`gh`) and put it on PATH; conductor-driven merges run under this " +
42
- "host's own `gh` auth because the GitHub App token is `contents: read` and cannot merge.";
43
- /** Remediation for an installed but unauthenticated `gh` (AC-7; see {@link GH_CLI_REMEDIATION}). */
44
- export const GH_AUTH_REMEDIATION = "run `gh auth login` to grant the conductor merge permission; otherwise local merge emits " +
45
- "merge.failed/skip rather than merging.";
46
- /** Detail used when `gh` authentication was never tested because `gh` is absent. */
47
- export const GH_AUTH_SKIPPED_DETAIL = "not tested — `gh` is not installed, so `gh auth status` was never run";
42
+ export const GH_CLI_REMEDIATION = "install the GitHub CLI (`gh`) and put it on PATH; conductor-driven merges shell out to it " +
43
+ "because the GitHub App token is `contents: read` and cannot merge. Since BAPI-1136 `gh` " +
44
+ "authenticates with the configured merge credential, not with this host's own `gh` login.";
45
+ /**
46
+ * Remediation for a host with no configured merge credential (BAPI-1136).
47
+ *
48
+ * This REPLACED a `gh auth login` remediation, and the replacement is the whole
49
+ * point. After the merge stopped authenticating as whoever is logged into `gh`,
50
+ * that instruction sent an operator to fix something the merge no longer reads —
51
+ * the most expensive kind of wrong hint, because following it looks reasonable
52
+ * and changes nothing. `merge-pull-request.ts` says exactly that about the same
53
+ * text; this is the last place it was still live.
54
+ */
55
+ export const GH_AUTH_REMEDIATION = "set BAPI_LOCAL_MERGE_TOKEN (or BAPI_MACHINE_MERGE_TOKEN for machine-user mode) in the " +
56
+ "environment this host launches the conductor with; local merge has no ambient-login " +
57
+ "fallback and fails closed with a named reason when neither is configured.";
58
+ /** Detail used when the credential check was not reached because `gh` is absent. */
59
+ export const GH_AUTH_SKIPPED_DETAIL = "not tested — `gh` is not installed, so no merge could run regardless of credential";
48
60
  /**
49
61
  * Remediation for a native ledger whose binding does not load.
50
62
  *
@@ -422,7 +434,7 @@ export function inspectMcpProfile(env) {
422
434
  * `gh auth status` (no writes, no merge) and reports whether the host could merge
423
435
  * a PR locally if `policy_json.local_merge.enabled` were set. Never throws.
424
436
  */
425
- export function inspectLocalMerge(runCommand) {
437
+ export function inspectLocalMerge(runCommand, env = process.env) {
426
438
  const run = runCommand ??
427
439
  ((cmd, args) => {
428
440
  try {
@@ -447,48 +459,51 @@ export function inspectLocalMerge(runCommand) {
447
459
  }
448
460
  });
449
461
  let gh_available = false;
450
- let gh_authed = false;
451
462
  try {
452
463
  gh_available = run("gh", ["--version"]).status === 0;
453
464
  }
454
465
  catch {
455
466
  gh_available = false;
456
467
  }
457
- if (gh_available) {
458
- try {
459
- gh_authed = run("gh", ["auth", "status"]).status === 0;
460
- }
461
- catch {
462
- gh_authed = false;
463
- }
464
- }
468
+ // BAPI-1136. This used to run `gh auth status` and call the result
469
+ // `gh_authed`. That probe measured the ambient session, which the merge stopped
470
+ // consulting — so it reported PASS on a host with a logged-in `gh` and no
471
+ // configured credential (every merge then failing
472
+ // `local_merge_credential_missing`), and WARN on a host with the credential set
473
+ // and no ambient login (where merges work). Both directions wrong.
474
+ //
475
+ // The replacement asks the question the merge actually asks, and answers it
476
+ // read-only from the environment — no subprocess, no network, and no credential
477
+ // VALUE anywhere in the result. Either mode's key satisfies it: which one a run
478
+ // uses is the server-resolved typed mode, which a local doctor cannot know.
479
+ const merge_credential_configured = ["BAPI_LOCAL_MERGE_TOKEN", "BAPI_MACHINE_MERGE_TOKEN"].some((key) => typeof env[key] === "string" && env[key].trim().length > 0);
465
480
  const warnings = [];
466
481
  if (!gh_available) {
467
482
  warnings.push("`gh` is not installed or not on PATH. Local merge (policy_json.local_merge.enabled) " +
468
483
  "cannot run; install the GitHub CLI to enable conductor-driven merges.");
469
484
  }
470
- else if (!gh_authed) {
471
- warnings.push("`gh` is installed but not authenticated (`gh auth status` failed). Run `gh auth login` " +
472
- "to grant the conductor merge permission; otherwise local merge will emit merge.failed/skip.");
473
- }
474
- // Capability gap only — local merge is opt-in, so an unauthed host is not a hard
475
- // failure unless the operator enabled it. Reported as degraded for visibility.
476
- const degraded = !gh_available || !gh_authed;
477
- // TWO outcomes, not one (BAPI-1055): installing `gh` and running `gh auth
478
- // login` are independently actionable, and `degraded` below is deliberately
479
- // left as the SAME `!gh_available || !gh_authed` conjunction it has always
480
- // been. When `gh` is absent the auth outcome is `skip`, never `fail` — the
481
- // authentication was never tested, and claiming it failed would send the
482
- // operator to `gh auth login` on a host with no `gh`.
485
+ else if (!merge_credential_configured) {
486
+ warnings.push("`gh` is installed but no merge credential is configured. Set BAPI_LOCAL_MERGE_TOKEN " +
487
+ "(or BAPI_MACHINE_MERGE_TOKEN for machine-user mode); local merge has no ambient-login " +
488
+ "fallback and fails closed with a named reason.");
489
+ }
490
+ // Capability gap only — local merge is opt-in, so an unconfigured host is not a
491
+ // hard failure unless the operator enabled it. Reported as degraded for visibility.
492
+ const degraded = !gh_available || !merge_credential_configured;
493
+ // TWO outcomes, not one (BAPI-1055): installing `gh` and configuring the merge
494
+ // credential are independently actionable, and `degraded` stays the same
495
+ // `!gh_available || !<second check>` conjunction it has always been. When `gh`
496
+ // is absent the second outcome is `skip`, never `fail` — nothing could merge on
497
+ // that host regardless of credential, so reporting a credential failure would
498
+ // name the wrong fix.
483
499
  //
484
- // AC-7 — these two checks are LOAD-BEARING and must not be retired here.
485
- // Conductor merges run under this machine's own `gh` auth because the GitHub
486
- // App token is `contents: read` and structurally cannot merge; see
487
- // `mcp_server/src/executor/merge-job.ts` and the `contents: read` constraint
488
- // documented in `mcp_server/src/merge-pull-request.ts`. Retirement is owned by
489
- // epic A2, after the App is elevated to `contents: write`. Nothing in this
490
- // change removes, hides, or downgrades either check, and no merge behaviour is
491
- // altered by reporting them.
500
+ // AC-7 RETIRED HERE (BAPI-1136). The note this replaces said these checks were
501
+ // load-bearing because "conductor merges run under this machine's own `gh`
502
+ // auth", and named epic A2 as the owner of their retirement. This IS that A2
503
+ // child: the merge now authenticates with one explicitly configured,
504
+ // customer-held credential and never with the ambient session. So the check is
505
+ // not removed — it is re-pointed at the thing that now decides, which is the
506
+ // only way it stays load-bearing rather than becoming decorative.
492
507
  const outcomes = [
493
508
  gh_available
494
509
  ? {
@@ -506,28 +521,155 @@ export function inspectLocalMerge(runCommand) {
506
521
  },
507
522
  !gh_available
508
523
  ? {
509
- id: "gh-auth",
510
- label: "GitHub CLI authentication",
524
+ id: "merge-credential",
525
+ label: "Configured merge credential",
511
526
  status: "skip",
512
527
  detail: GH_AUTH_SKIPPED_DETAIL,
513
528
  remediation: GH_CLI_REMEDIATION,
514
529
  }
515
- : gh_authed
530
+ : merge_credential_configured
516
531
  ? {
517
- id: "gh-auth",
518
- label: "GitHub CLI authentication",
532
+ id: "merge-credential",
533
+ label: "Configured merge credential",
519
534
  status: "pass",
520
- detail: "`gh auth status` reports an authenticated account",
535
+ detail: "BAPI_LOCAL_MERGE_TOKEN or BAPI_MACHINE_MERGE_TOKEN is set",
521
536
  }
522
537
  : {
523
- id: "gh-auth",
524
- label: "GitHub CLI authentication",
538
+ id: "merge-credential",
539
+ label: "Configured merge credential",
525
540
  status: "warn",
526
- detail: "`gh` is installed but `gh auth status` failed",
541
+ detail: "`gh` is installed but neither BAPI_LOCAL_MERGE_TOKEN nor " +
542
+ "BAPI_MACHINE_MERGE_TOKEN is set, so a local merge would fail closed",
527
543
  remediation: GH_AUTH_REMEDIATION,
528
544
  },
529
545
  ];
530
- return { gh_available, gh_authed, degraded, warnings, outcomes };
546
+ return { gh_available, merge_credential_configured, degraded, warnings, outcomes };
547
+ }
548
+ /** Remediation when this host has no `gh` identity to observe with. */
549
+ export const GH_ACTIONS_MERGE_AUTH_REMEDIATION = "run `gh auth login` so this host has a GitHub identity; without one it cannot " +
550
+ "observe the merge capability. This does not affect whether the capability exists.";
551
+ /** Remediation when the capability could not be observed from this host. */
552
+ export const GH_ACTIONS_MERGE_UNOBSERVABLE_REMEDIATION = "check network connectivity to api.github.com and `gh auth status`; the merge " +
553
+ "capability could NOT BE OBSERVED from this host, which is not the same as it " +
554
+ "being absent. The server's conductor readiness report is authoritative.";
555
+ /** Detail for the observability outcome when the auth prerequisite never held. */
556
+ export const GH_ACTIONS_MERGE_SKIPPED_DETAIL = "not attempted — this host has no authenticated `gh` identity";
557
+ /**
558
+ * Read-only probe of whether this host can observe the `github_actions` merge
559
+ * capability. Never throws.
560
+ *
561
+ * Two bounded, read-only commands and nothing else: `gh auth status`, then a
562
+ * status-only `gh api` GET of the canonical workflow metadata endpoint. The
563
+ * injected runner returns a STATUS ONLY — it is deliberately not widened to
564
+ * capture stdout, because parsing that output would mean re-deriving workflow
565
+ * state the server already reports, from a different identity, with no way to
566
+ * reconcile a disagreement.
567
+ *
568
+ * Every failure mode — a timeout, a non-zero status, a missing `gh`, a thrown
569
+ * dependency error — degrades to `false` with a retry-oriented warning. None
570
+ * raises a backend platform alert: an unobservable capability is a LOCAL
571
+ * condition, and paging on it would page for every laptop behind a proxy.
572
+ *
573
+ * Emits only status classifications and fixed remediation text. No process
574
+ * output, auth detail, repository URL, or command credential appears anywhere in
575
+ * the result.
576
+ */
577
+ export function inspectGitHubActionsMerge(runCommand) {
578
+ const run = runCommand ??
579
+ ((cmd, args) => {
580
+ try {
581
+ // Same structure and bound as `inspectLocalMerge`: a static ESM import,
582
+ // a 10s timeout so a network stall cannot hold the doctor past it, and
583
+ // GH_PROMPT_DISABLED so an interactive prompt cannot either.
584
+ const r = spawnSync(cmd, args, {
585
+ encoding: "utf8",
586
+ timeout: 10_000,
587
+ env: { ...process.env, GH_PROMPT_DISABLED: "1" },
588
+ });
589
+ return { status: r.status };
590
+ }
591
+ catch {
592
+ return { status: null };
593
+ }
594
+ });
595
+ let gh_authed = false;
596
+ try {
597
+ gh_authed = run("gh", ["auth", "status"]).status === 0;
598
+ }
599
+ catch {
600
+ gh_authed = false;
601
+ }
602
+ let capability_observable = false;
603
+ if (gh_authed) {
604
+ try {
605
+ // `--silent` keeps the body out of the child's stdout entirely: this probe
606
+ // wants the STATUS, and a body it never requests is a body it cannot leak.
607
+ capability_observable =
608
+ run("gh", [
609
+ "api",
610
+ "--silent",
611
+ `repos/{owner}/{repo}/actions/workflows/${MERGE_WORKFLOW_IDENTIFIER}`,
612
+ ]).status === 0;
613
+ }
614
+ catch {
615
+ capability_observable = false;
616
+ }
617
+ }
618
+ const warnings = [];
619
+ if (!gh_authed) {
620
+ warnings.push(GH_ACTIONS_MERGE_AUTH_REMEDIATION);
621
+ }
622
+ else if (!capability_observable) {
623
+ warnings.push(GH_ACTIONS_MERGE_UNOBSERVABLE_REMEDIATION);
624
+ }
625
+ const outcomes = [
626
+ gh_authed
627
+ ? {
628
+ id: "gh-actions-merge-auth",
629
+ label: "GitHub Actions merge: local observation prerequisites",
630
+ status: "pass",
631
+ detail: "eligible to observe — `gh auth status` reports an authenticated account",
632
+ }
633
+ : {
634
+ id: "gh-actions-merge-auth",
635
+ label: "GitHub Actions merge: local observation prerequisites",
636
+ status: "warn",
637
+ detail: "not observable — no authenticated `gh` identity on this host",
638
+ remediation: GH_ACTIONS_MERGE_AUTH_REMEDIATION,
639
+ },
640
+ !gh_authed
641
+ ? {
642
+ // `skip`, never `fail`: observability was never tested, and claiming it
643
+ // failed would send the operator to debug connectivity on a host that
644
+ // simply has no identity yet.
645
+ id: "gh-actions-merge-observable",
646
+ label: "GitHub Actions merge: capability observability",
647
+ status: "skip",
648
+ detail: GH_ACTIONS_MERGE_SKIPPED_DETAIL,
649
+ remediation: GH_ACTIONS_MERGE_AUTH_REMEDIATION,
650
+ }
651
+ : capability_observable
652
+ ? {
653
+ id: "gh-actions-merge-observable",
654
+ label: "GitHub Actions merge: capability observability",
655
+ status: "pass",
656
+ detail: "eligible to observe — the workflow metadata endpoint answered",
657
+ }
658
+ : {
659
+ id: "gh-actions-merge-observable",
660
+ label: "GitHub Actions merge: capability observability",
661
+ status: "warn",
662
+ detail: "could not verify — the workflow metadata endpoint could not be read",
663
+ remediation: GH_ACTIONS_MERGE_UNOBSERVABLE_REMEDIATION,
664
+ },
665
+ ];
666
+ return {
667
+ gh_authed,
668
+ capability_observable,
669
+ degraded: !gh_authed || !capability_observable,
670
+ warnings,
671
+ outcomes,
672
+ };
531
673
  }
532
674
  /**
533
675
  * Build the combined read-only doctor report. Composes the existing ledger
@@ -553,12 +695,43 @@ export async function buildConductorDoctorReport(deps = {}) {
553
695
  git_hooks: inspectHooks(deps.hooksDeps),
554
696
  mcp_profile,
555
697
  local_merge: inspectLocalMerge(deps.runCommand),
698
+ github_actions_merge: inspectGitHubActionsMergeSafe(deps),
556
699
  native_ledger,
557
700
  deny_enforcement,
558
701
  claude_login,
559
702
  repair_credentials: await inspectRepairVerbCredentials(deps.env ?? process.env, deps.resolveRepairAccess),
560
703
  };
561
704
  }
705
+ /**
706
+ * Run the GitHub Actions merge inspector, containing any throw as degraded data.
707
+ *
708
+ * The production inspector is already exception-safe; this is defense in depth
709
+ * for an INJECTED fake, so a test double that throws degrades the section rather
710
+ * than failing the whole doctor report.
711
+ */
712
+ function inspectGitHubActionsMergeSafe(deps) {
713
+ const inspect = deps.inspectGitHubActionsMerge ?? inspectGitHubActionsMerge;
714
+ try {
715
+ return inspect(deps.runCommand);
716
+ }
717
+ catch {
718
+ return {
719
+ gh_authed: false,
720
+ capability_observable: false,
721
+ degraded: true,
722
+ warnings: [GH_ACTIONS_MERGE_UNOBSERVABLE_REMEDIATION],
723
+ outcomes: [
724
+ {
725
+ id: "gh-actions-merge-observable",
726
+ label: "GitHub Actions merge: capability observability",
727
+ status: "warn",
728
+ detail: "could not verify — the local inspection could not be completed",
729
+ remediation: GH_ACTIONS_MERGE_UNOBSERVABLE_REMEDIATION,
730
+ },
731
+ ],
732
+ };
733
+ }
734
+ }
562
735
  /**
563
736
  * Run the deny-enforcement inspector and convert any thrown exception into degraded
564
737
  * report data — a deny-probe failure must never throw out of doctor. The underlying
@@ -598,7 +771,7 @@ async function inspectDenyEnforcementSafe(inspect) {
598
771
  * hierarchy across sections.
599
772
  */
600
773
  export function formatConductorDoctorReport(report) {
601
- const { ledger, git_hooks, mcp_profile, local_merge, native_ledger, deny_enforcement, claude_login } = report;
774
+ const { ledger, git_hooks, mcp_profile, local_merge, github_actions_merge, native_ledger, deny_enforcement, claude_login } = report;
602
775
  const lines = [
603
776
  "Conductor ledger doctor",
604
777
  "───────────────────────",
@@ -666,13 +839,38 @@ export function formatConductorDoctorReport(report) {
666
839
  lines.push("Local merge capability (optional, opt-in)");
667
840
  lines.push("─────────────────────────────────────────");
668
841
  const ghTag = local_merge.gh_available
669
- ? local_merge.gh_authed
842
+ ? local_merge.merge_credential_configured
670
843
  ? "[OK]"
671
- : "[WARNING] not authenticated"
844
+ : "[WARNING] no merge credential configured"
672
845
  : "[WARNING] not installed";
673
846
  lines.push(`gh available: ${local_merge.gh_available} ${ghTag}`);
674
- lines.push(`gh authenticated: ${local_merge.gh_authed}`);
847
+ lines.push(`merge credential set: ${local_merge.merge_credential_configured}`);
675
848
  lines.push(`degraded: ${local_merge.degraded}`);
849
+ // BAPI-1135 — the two observations render INDEPENDENTLY, each leading with its
850
+ // own decisive word, because they have two different fixes. Collapsing them
851
+ // into one line would hide which of the two an operator has to act on.
852
+ //
853
+ // The vocabulary is deliberate: `not observable` is a statement about THIS
854
+ // HOST, and `could not verify` is a statement about a read that failed.
855
+ // Neither ever says the capability is absent — only the server's readiness
856
+ // report may say that.
857
+ if (github_actions_merge) {
858
+ lines.push("");
859
+ lines.push("GitHub Actions merge (read-only observability)");
860
+ lines.push(`gh authenticated: ${github_actions_merge.gh_authed} ${github_actions_merge.gh_authed ? "(eligible to observe)" : "(not observable)"}`);
861
+ lines.push(`capability observable: ${github_actions_merge.capability_observable} ${github_actions_merge.gh_authed
862
+ ? github_actions_merge.capability_observable
863
+ ? "(eligible to observe)"
864
+ : "(could not verify)"
865
+ : "(not attempted)"}`);
866
+ lines.push(`degraded: ${github_actions_merge.degraded}`);
867
+ if (github_actions_merge.warnings.length > 0) {
868
+ lines.push("warnings:");
869
+ for (const w of github_actions_merge.warnings)
870
+ lines.push(` - ${w}`);
871
+ }
872
+ lines.push("");
873
+ }
676
874
  if (local_merge.warnings.length > 0) {
677
875
  lines.push("local merge warnings:");
678
876
  for (const w of local_merge.warnings)
@@ -753,7 +951,7 @@ export const CONDUCTOR_READINESS_DESCRIPTORS = [
753
951
  { id: "git-hooks", label: "Conductor git hooks" },
754
952
  { id: "mcp-profile", label: "Epic Implementer MCP profile" },
755
953
  { id: "gh-cli", label: "GitHub CLI availability" },
756
- { id: "gh-auth", label: "GitHub CLI authentication" },
954
+ { id: "merge-credential", label: "Configured merge credential" },
757
955
  { id: "native-ledger", label: "Native ledger binding" },
758
956
  { id: "mcp-registration-form", label: "Worker MCP registration form" },
759
957
  { id: "node-engine", label: "Expected Node engine" },
@@ -921,6 +1119,10 @@ export function mapConductorDoctorReportToReadinessChecks(report) {
921
1119
  record(outcome);
922
1120
  for (const outcome of outcomesOf(report?.local_merge))
923
1121
  record(outcome);
1122
+ // BAPI-1135 — both observability outcomes flow through the same readiness-check
1123
+ // mapping as every other doctor outcome, so no surface formats them twice.
1124
+ for (const outcome of outcomesOf(report?.github_actions_merge))
1125
+ record(outcome);
924
1126
  for (const outcome of outcomesOf(report?.native_ledger))
925
1127
  record(outcome);
926
1128
  record(denyEnforcementOutcome(report));
@@ -23,7 +23,7 @@ import { processGateMetMerge } from "./supervisor-merge.js";
23
23
  import { observePrCiOnce } from "./pr-ci-producer.js";
24
24
  import { runGhCommand } from "./pr-discovery.js";
25
25
  import { isPrMergeConflict, parseGhPrMergeabilityFields, } from "./github-mergeability.js";
26
- import { makeLocalMergeExecutor, resolveLocalMergeMethod } from "./local-merge.js";
26
+ import { makeLocalMergeExecutor, resolveLocalMergeCredential, resolveLocalMergeMethod, } from "./local-merge.js";
27
27
  import { getHeadSha, getPrNumber } from "./event-accessors.js";
28
28
  import { emitConductorEventIfNew } from "./producer-ledger.js";
29
29
  import { rebuildObservedState, extractWorkerLiveness, } from "./epic-state.js";
@@ -1320,7 +1320,16 @@ export async function runEpicTick(options, deps = {}) {
1320
1320
  merge: makeLocalMergeExecutor({
1321
1321
  method: resolveLocalMergeMethod(localCfg.method),
1322
1322
  approvalRequired: localCfg.approval_required === true,
1323
- }, { env: process.env }),
1323
+ },
1324
+ // BAPI-1136: `env` now supplies only the allowlisted
1325
+ // operational base; the identity is the explicitly configured
1326
+ // local credential, and an unconfigured one fails closed with
1327
+ // `local_merge_credential_missing` rather than borrowing the
1328
+ // ambient `gh` login this path used to rely on.
1329
+ {
1330
+ env: process.env,
1331
+ credential: resolveLocalMergeCredential(process.env, "local"),
1332
+ }),
1324
1333
  // BAPI-525 Change A: durable attribution for a worker_id-less gate.met.
1325
1334
  resolveWorkerIdForGateEvent,
1326
1335
  });
@@ -36,7 +36,7 @@ import { buildConductorDoctorReport, formatConductorDoctorReport, } from "./doct
36
36
  import { inspectBridgeApiProfileToken, } from "../mcp-host-config.js";
37
37
  import { collectExecutorServiceDiagnostics, formatExecutorServiceDiagnosticsReport, } from "../doctor.js";
38
38
  import { collectInstallStatusChecks, formatInstallStatusReport, } from "../install-doctor.js";
39
- import { bringUpSafetySections, conductorHookTargetsSection, conductorLedgerLoadabilitySection, conductorProfileTokenSection, EPIC_IMPLEMENTER_PROFILE_TOKEN, detectConductorContext, executorObservationSection, executorProvisioningSection, executorServiceStateSection, githubActionsSection, githubCredentialsSection, inspectWorkflowPresence, reconcilerSection, supervisorSection, workflowSection, } from "./readiness-sections.js";
39
+ import { bringUpSafetySections, conductorHookTargetsSection, conductorLedgerLoadabilitySection, gitHubActionsMergeObservabilitySections, conductorProfileTokenSection, EPIC_IMPLEMENTER_PROFILE_TOKEN, detectConductorContext, executorObservationSection, executorProvisioningSection, executorServiceStateSection, githubActionsSection, mergeExecutionSections, githubCredentialsSection, inspectWorkflowPresence, reconcilerSection, supervisorSection, workflowSection, } from "./readiness-sections.js";
40
40
  import { ConductorBridgeApiError, fetchConductorReadiness, } from "./bridge-api-client.js";
41
41
  /**
42
42
  * Run one optional collector, containing any throw.
@@ -157,6 +157,13 @@ export async function runConductorInstallDoctor(deps) {
157
157
  sections.push(...supervisorSection(readiness));
158
158
  sections.push(githubCredentialsSection(readiness));
159
159
  sections.push(githubActionsSection(readiness));
160
+ // BAPI-1135 — the shared "Merge execution eligibility" section, immediately
161
+ // after the App permission row it is most often confused with, so the two
162
+ // write facts are read side by side. Built by `readiness-sections.ts` rather
163
+ // than formatted here, so this installer and the advisory gate render one
164
+ // classification; the installer's `settle`-based degraded isolation is
165
+ // unchanged, and no row here is `fatal`.
166
+ sections.push(...mergeExecutionSections(readiness));
160
167
  }
161
168
  const workflow = await inspectWorkflowPresence(deps.readWorkflowFile, deps.classifyWorkflowDrift);
162
169
  sections.push(workflowSection(workflow.presence, deps.reviewPolicySource, workflow.driftDetail));
@@ -230,6 +237,13 @@ export async function runConductorInstallDoctor(deps) {
230
237
  sections.push(conductorProfileTokenSection(profileToken, conductorContext));
231
238
  sections.push(conductorHookTargetsSection(legacyConductor, conductorContext));
232
239
  sections.push(conductorLedgerLoadabilitySection(legacyConductor));
240
+ // BAPI-1135 — this HOST's ability to observe the merge capability, beside the
241
+ // other locally-derived doctor rows. Distinct from the server-fact
242
+ // "Merge execution eligibility" section pushed above: that one says whether
243
+ // the repository can merge, these two say whether this machine can look.
244
+ // `settle`-based degraded isolation is unchanged — a doctor that failed to
245
+ // collect yields two explicit unknown rows, never an omitted section.
246
+ sections.push(...gitHubActionsMergeObservabilitySections(legacyConductor));
233
247
  return {
234
248
  legacyConductor,
235
249
  legacyExecutorPreflight,
@@ -335,6 +349,14 @@ export function conductorInstallDoctorHasFatal(report) {
335
349
  // remediation constants, or the installer reporting APIs from
336
350
  // `conductor/install-doctor.js` keeps resolving exactly as before.
337
351
  // ---------------------------------------------------------------------------
338
- export { BRING_UP_SECTION_LABELS, bringUpSafetySections, databaseMigrationCurrentSection, singleTransitionAuthoritySection, workerPresentSection, CONDUCTOR_OPERATOR_RUNBOOK_POINTER, EXECUTOR_PROVISIONING_GUIDANCE, EPIC_IMPLEMENTER_PROFILE_TOKEN, CONDUCTOR_PROFILE_TOKEN_NO_ENTRY_REMEDIATION, CONDUCTOR_PROFILE_TOKEN_REMEDIATION, CONDUCTOR_HOOK_TARGET_REMEDIATION, CONDUCTOR_LEDGER_LOADABILITY_REMEDIATION, EXECUTOR_INSTALL_SERVICE_REMEDIATION, EXECUTOR_SERVICE_START_REMEDIATION, } from "./readiness-sections.js";
352
+ export { BRING_UP_SECTION_LABELS, bringUpSafetySections,
353
+ // BAPI-1135 — re-exported on the compatibility surface like every other
354
+ // section builder, so an existing importer of this module sees the new
355
+ // section without learning a second import path.
356
+ mergeExecutionSections, databaseMigrationCurrentSection, singleTransitionAuthoritySection, workerPresentSection, CONDUCTOR_OPERATOR_RUNBOOK_POINTER, EXECUTOR_PROVISIONING_GUIDANCE, EPIC_IMPLEMENTER_PROFILE_TOKEN, CONDUCTOR_PROFILE_TOKEN_NO_ENTRY_REMEDIATION, CONDUCTOR_PROFILE_TOKEN_REMEDIATION, CONDUCTOR_HOOK_TARGET_REMEDIATION, CONDUCTOR_LEDGER_LOADABILITY_REMEDIATION, EXECUTOR_INSTALL_SERVICE_REMEDIATION, EXECUTOR_SERVICE_START_REMEDIATION,
357
+ // BAPI-1135 — both new section builders join the compatibility surface like
358
+ // every other one, so an existing importer of this module does not learn a
359
+ // second import path for them.
360
+ gitHubActionsMergeObservabilitySections, } from "./readiness-sections.js";
339
361
  // BAPI-1153 — the bring-up remediations the three sections above render.
340
362
  export { BRING_UP_RUNBOOK, BRING_UP_UNREPORTED_REMEDIATION, DATABASE_MIGRATION_BEHIND_REMEDIATION, DATABASE_MIGRATION_UNREADABLE_REMEDIATION, SECOND_RECONCILER_REMEDIATION, WORKER_HEARTBEAT_UNREADABLE_REMEDIATION, WORKER_NOT_PRESENT_REMEDIATION, } from "./bring-up-facts.js";