@bridge_gpt/mcp-server 0.2.58 → 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 (127) hide show
  1. package/README.md +231 -113
  2. package/build/agent-guidance.generated.js +1 -1
  3. package/build/agent-notes.js +7 -1
  4. package/build/agent-registry.js +15 -0
  5. package/build/agent-utils.js +16 -8
  6. package/build/claude-user-config-doctor.js +1 -35
  7. package/build/codex-bridge-profile.js +1 -1
  8. package/build/commands.generated.js +7 -8
  9. package/build/conductor/bridge-api-client.js +202 -2
  10. package/build/conductor/bring-up-facts.js +1 -1
  11. package/build/conductor/claude-hook.js +16 -16
  12. package/build/conductor/doctor.js +279 -72
  13. package/build/conductor/epic-implementer-cli.js +38 -5
  14. package/build/conductor/epic-reconcile.js +1 -1
  15. package/build/conductor/epic-runtime.js +21 -12
  16. package/build/conductor/errors.js +3 -3
  17. package/build/conductor/file-scope-guard.js +4 -4
  18. package/build/conductor/git-ci-types.js +1 -1
  19. package/build/conductor/git-inspection.js +2 -2
  20. package/build/conductor/install-doctor.js +28 -6
  21. package/build/conductor/local-merge.js +319 -26
  22. package/build/conductor/merge-execution-contract.js +61 -0
  23. package/build/conductor/merge-execution-eligibility.js +129 -0
  24. package/build/conductor/merge-ledger.js +2 -2
  25. package/build/conductor/pr-ci-producer.js +6 -6
  26. package/build/conductor/pr-review-producer.js +2 -2
  27. package/build/conductor/producer-ledger.js +2 -0
  28. package/build/conductor/readiness-sections.js +166 -9
  29. package/build/conductor/readiness.js +97 -16
  30. package/build/conductor/recovery-cli.js +243 -12
  31. package/build/conductor/recovery-operations.js +64 -6
  32. package/build/conductor/repair-contract.js +38 -4
  33. package/build/conductor/spec-review-producer.js +6 -6
  34. package/build/conductor/store.js +10 -10
  35. package/build/conductor/supervisor-config.js +14 -14
  36. package/build/conductor/supervisor-ledger.js +2 -2
  37. package/build/conductor/supervisor-message-relay.js +2 -2
  38. package/build/conductor/worker-ledger-cli.js +19 -19
  39. package/build/conductor-bundle-artifacts.js +2 -2
  40. package/build/connect-bitbucket-api.js +6 -1
  41. package/build/connect-bitbucket.js +398 -88
  42. package/build/credential-store.js +16 -0
  43. package/build/docs.generated.js +2 -2
  44. package/build/doctor.js +199 -303
  45. package/build/drive-epic.js +88 -71
  46. package/build/epic-implementer-bin.js +22 -17
  47. package/build/epic-implementer-claude-hook-bin.js +4 -2
  48. package/build/epic-implementer-env.js +184 -0
  49. package/build/epic-integration-pr.js +2 -2
  50. package/build/executor/ci-rerun-job.js +269 -0
  51. package/build/executor/cli.js +31 -0
  52. package/build/executor/credentials.js +11 -0
  53. package/build/executor/env.js +79 -8
  54. package/build/executor/heartbeat.js +1 -1
  55. package/build/executor/index.js +21 -4
  56. package/build/executor/job-errors.js +3 -3
  57. package/build/executor/job-runner.js +127 -1
  58. package/build/executor/job-types.js +12 -2
  59. package/build/executor/merge-job.js +46 -5
  60. package/build/executor/observation.js +1 -1
  61. package/build/executor/preflight.js +1 -1
  62. package/build/executor/prompt-spec.js +1 -1
  63. package/build/executor/reconcile-overlap-job.js +10 -130
  64. package/build/executor/reconcile-overlap-types.js +1 -1
  65. package/build/executor/runner.js +1 -1
  66. package/build/executor/service-lifecycle.js +136 -0
  67. package/build/executor/service-unit.js +427 -9
  68. package/build/executor/types.js +5 -0
  69. package/build/index.js +320 -342
  70. package/build/init.js +20 -28
  71. package/build/install-bridge-conductor.js +208 -15
  72. package/build/install-bridge.js +555 -37
  73. package/build/mcp-host-entry-adapter.js +6 -0
  74. package/build/mcp-host-targets.js +2 -0
  75. package/build/mcp-install-state.js +42 -4
  76. package/build/mcp-invoke.js +5 -5
  77. package/build/mcp-profile.js +2 -2
  78. package/build/mcp-provisioning.js +2 -2
  79. package/build/mcp-server-invocation.js +3 -3
  80. package/build/merge-pull-request.js +118 -10
  81. package/build/merge-workflow-codeowners.js +224 -0
  82. package/build/merge-workflow-config.js +133 -0
  83. package/build/merge-workflow-drift.js +66 -0
  84. package/build/merge-workflow-install.js +638 -0
  85. package/build/merge-workflow.js +572 -0
  86. package/build/model-routing-host.js +25 -0
  87. package/build/model-routing-preferences.js +99 -0
  88. package/build/pipelines.generated.js +10 -10
  89. package/build/{plan-epic-conductor-eligibility.js → plan-epic-implementer-eligibility.js} +18 -16
  90. package/build/plan-execution-ledger.js +8 -0
  91. package/build/plane/cli.js +24 -2
  92. package/build/plane/member-roster.js +5 -5
  93. package/build/plane/preflight.js +68 -0
  94. package/build/plane/supervisor.js +1 -1
  95. package/build/plane/types.js +5 -5
  96. package/build/readiness-check.js +1 -1
  97. package/build/readme.generated.js +1 -1
  98. package/build/review-tickets.js +4 -4
  99. package/build/run-unit-tests-launcher.js +5 -0
  100. package/build/setup-epic.js +171 -17
  101. package/build/start-tickets-conductor.js +32 -32
  102. package/build/start-tickets-prereqs.js +4 -9
  103. package/build/start-tickets.js +475 -91
  104. package/build/version.generated.js +3 -3
  105. package/docs/CONDUCTOR.md +69 -18
  106. package/docs/install/mcp-tool-integrations.md +2 -1
  107. package/package.json +4 -4
  108. package/pipelines/implement-ticket.json +1 -1
  109. package/pipelines/plan-epic.json +2 -2
  110. package/build/conduct-epic/bridge-client.js +0 -773
  111. package/build/conduct-epic/checkpoint-store.js +0 -542
  112. package/build/conduct-epic/cli.js +0 -3145
  113. package/build/conduct-epic/cut-protocol.js +0 -392
  114. package/build/conduct-epic/lock.js +0 -302
  115. package/build/conduct-epic/pr-state.js +0 -286
  116. package/build/conduct-epic/spawn.js +0 -113
  117. package/build/conductor/cli.js +0 -1278
  118. package/build/conductor-bundle-cli.js +0 -256
  119. package/build/direct-ticket-tools.js +0 -70
  120. package/build/executor/reconcile-overlap-governance.js +0 -129
  121. package/build/implement-epic/checkpoint-store.js +0 -542
  122. package/build/implement-epic/cli.js +0 -3158
  123. package/build/implement-epic/lock.js +0 -302
  124. package/build/implement-epic/pr-state.js +0 -286
  125. package/build/implement-epic/spawn.js +0 -113
  126. /package/build/{implement-epic → epic-shared}/bridge-client.js +0 -0
  127. /package/build/{implement-epic → epic-shared}/cut-protocol.js +0 -0
@@ -109,7 +109,7 @@ export function buildGateMetEventInput(binding, evaluation, runId = null, worker
109
109
  if (!evaluation.met || !evaluation.gateEventData)
110
110
  return null;
111
111
  return {
112
- source: "conductor",
112
+ source: "epic-implementer",
113
113
  type: "gate.met",
114
114
  subject: binding.subject,
115
115
  run_id: runId,
@@ -142,8 +142,8 @@ async function observeWithResolved(binding, access, gateConfig, deps, expectedBa
142
142
  const now = deps.now ?? (() => new Date().toISOString());
143
143
  // Resolve run_id/worker_id for attribution. Env takes precedence; fall back
144
144
  // to the injected resolution seam when the env identity is absent.
145
- let run_id = deps.env?.BAPI_CONDUCTOR_RUN_ID?.trim() || null;
146
- const worker_id = deps.env?.BAPI_CONDUCTOR_WORKER_ID?.trim() || null;
145
+ let run_id = deps.env?.BAPI_EPIC_IMPLEMENTER_RUN_ID?.trim() || null;
146
+ const worker_id = deps.env?.BAPI_EPIC_IMPLEMENTER_WORKER_ID?.trim() || null;
147
147
  if (run_id === null && deps.resolveRunId) {
148
148
  try {
149
149
  run_id = await deps.resolveRunId(access, binding) ?? null;
@@ -432,8 +432,8 @@ export async function observePrCiFromPollResponse(commitRef, pollResponse, deps
432
432
  }
433
433
  // Env-stamp identity; fall back to the resolution seam when env is absent.
434
434
  // Access resolution for the seam is attempted inline within the gate block.
435
- let run_id = deps.env?.BAPI_CONDUCTOR_RUN_ID?.trim() || null;
436
- const worker_id = deps.env?.BAPI_CONDUCTOR_WORKER_ID?.trim() || null;
435
+ let run_id = deps.env?.BAPI_EPIC_IMPLEMENTER_RUN_ID?.trim() || null;
436
+ const worker_id = deps.env?.BAPI_EPIC_IMPLEMENTER_WORKER_ID?.trim() || null;
437
437
  const result = {
438
438
  binding,
439
439
  pr_opened_emitted: false,
@@ -541,7 +541,7 @@ function extractTicketKeyFromRef(headRef) {
541
541
  * any step fails — the producer never crashes on a resolution failure.
542
542
  *
543
543
  * Intended to be injected as `PrCiProducerDeps.resolveRunId` at call sites
544
- * that lack `BAPI_CONDUCTOR_RUN_ID` in the environment (git-hook / CI-poll
544
+ * that lack `BAPI_EPIC_IMPLEMENTER_RUN_ID` in the environment (git-hook / CI-poll
545
545
  * contexts). A production `fetchImpl` is optional; defaults to the global fetch.
546
546
  */
547
547
  export async function resolveDispatchRunIdForBinding(access, binding, fetchImpl = fetch) {
@@ -58,8 +58,8 @@ export function buildReviewObservationEventInput(binding, snapshot, eventType, r
58
58
  export async function observeReviewWithResolved(binding, access, gateConfig, deps = {}) {
59
59
  const fetchStatus = deps.fetchReviewStatus ?? fetchPrReviewStatus;
60
60
  const emitIfNew = deps.emitIfNew ?? emitConductorEventIfNew;
61
- const run_id = deps.env?.BAPI_CONDUCTOR_RUN_ID?.trim() || null;
62
- const worker_id = deps.env?.BAPI_CONDUCTOR_WORKER_ID?.trim() || null;
61
+ const run_id = deps.env?.BAPI_EPIC_IMPLEMENTER_RUN_ID?.trim() || null;
62
+ const worker_id = deps.env?.BAPI_EPIC_IMPLEMENTER_WORKER_ID?.trim() || null;
63
63
  const result = {
64
64
  snapshot: null,
65
65
  review_passed_emitted: false,
@@ -33,6 +33,8 @@ export function makeProducerDedupeKey(dimensions) {
33
33
  * the `events.id` UNIQUE constraint instead of appending a duplicate.
34
34
  */
35
35
  export function makeStableProducerEventId(dedupeKey) {
36
+ // Upgrade seam: preserve the legacy namespace so retries spanning releases
37
+ // collide with the event ID already written before the wire-name migration.
36
38
  const h = createHash("sha256").update(`conductor-producer:${dedupeKey}`).digest("hex");
37
39
  return `${h.slice(0, 8)}-${h.slice(8, 12)}-${h.slice(12, 16)}-${h.slice(16, 20)}-${h.slice(20, 32)}`;
38
40
  }
@@ -36,10 +36,12 @@
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
- export const CONDUCTOR_OPERATOR_RUNBOOK_POINTER = "docs/claude/epic-conductor-v2-operator-runbook.md";
44
+ export const CONDUCTOR_OPERATOR_RUNBOOK_POINTER = "docs/claude/epic-implementer-v2-operator-runbook.md";
43
45
  /**
44
46
  * Capability-matrix guidance for the executor leg.
45
47
  *
@@ -51,13 +53,13 @@ export const CONDUCTOR_OPERATOR_RUNBOOK_POINTER = "docs/claude/epic-conductor-v2
51
53
  export const EXECUTOR_PROVISIONING_GUIDANCE = "`install conductor` writes the unit and starts it, each behind its own consent; " +
52
54
  "readiness still comes only from a live server observation";
53
55
  /**
54
- * The `BRIDGE_MCP_PROFILE` token that makes the 8 conductor MCP tools visible.
56
+ * The `BRIDGE_MCP_PROFILE` token that makes the 8 Epic Implementer MCP tools visible.
55
57
  *
56
58
  * The tools are profile-gated, never core, and this is the only token that
57
59
  * un-gates them — so the doctor names it explicitly rather than describing the
58
60
  * gap abstractly (BAPI-775).
59
61
  */
60
- export const CONDUCTOR_PROFILE_TOKEN = "conductor";
62
+ export const EPIC_IMPLEMENTER_PROFILE_TOKEN = "epic_implementer";
61
63
  /** Remediation for a host MCP config that carries no Bridge entry at all. */
62
64
  export const CONDUCTOR_PROFILE_TOKEN_NO_ENTRY_REMEDIATION = "no supported host MCP config carries a Bridge entry — run " +
63
65
  `\`npx ${MCP_PACKAGE_NAME} install\` to register one, then re-run \`install conductor\`.`;
@@ -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) {
@@ -500,7 +590,7 @@ export function executorObservationSection(readiness) {
500
590
  * Whether this host looks like a conductor context.
501
591
  *
502
592
  * Composed from the two facts already collected: the resolved MCP-profile
503
- * inspection (`BAPI_CONDUCTOR_ENABLED` or a registered epic-tick schedule) and
593
+ * inspection (`BAPI_EPIC_IMPLEMENTER_ENABLED` or a registered epic-tick schedule) and
504
594
  * the read-only host token inspection. Outside such a context a missing local
505
595
  * capability is ADVISORY — reporting it as a gap would train operators of
506
596
  * ordinary repositories to ignore this whole part of the report.
@@ -522,7 +612,7 @@ export function detectConductorContext(legacyConductor, profile) {
522
612
  * that Codex or Copilot configuration was inspected or changed.
523
613
  */
524
614
  export function conductorProfileTokenSection(profile, conductorContext) {
525
- const base = { id: "conductor-profile-token", label: "MCP conductor profile token" };
615
+ const base = { id: "epic-implementer-profile-token", label: "Epic Implementer MCP profile token" };
526
616
  if (profile === null) {
527
617
  return {
528
618
  ...base,
@@ -630,10 +720,77 @@ 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
- id: "conductor-ledger-loadability",
636
- label: "Conductor ledger loadability",
792
+ id: "epic-implementer-ledger-loadability",
793
+ label: "Epic Implementer ledger loadability",
637
794
  };
638
795
  if (legacyConductor === null || legacyConductor === undefined) {
639
796
  return {
@@ -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;
@@ -626,8 +707,8 @@ const INSTALL_SECTION_IDS_OWNED_ELSEWHERE = new Set([
626
707
  "server-readiness",
627
708
  "bridge-access",
628
709
  // Local conductor facts — owned by `mapConductorDoctorReportToReadinessChecks`.
629
- "conductor-ledger",
630
- "conductor-ledger-loadability",
710
+ "epic-implementer-ledger",
711
+ "epic-implementer-ledger-loadability",
631
712
  // Legacy embedded reports — owned by their own adapters.
632
713
  "install-status",
633
714
  ]);