@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
@@ -13,12 +13,13 @@ 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";
19
20
  import { createReadinessCheck, createReadinessCheckSafely, } from "../readiness-check.js";
20
21
  /**
21
- * Remediation for a conductor context whose resolved MCP groups omit `conductor`.
22
+ * Remediation for a conductor context whose resolved MCP groups omit `epic_implementer`.
22
23
  *
23
24
  * Fixed prose: it never interpolates the raw `BRIDGE_MCP_PROFILE` value. The
24
25
  * legacy warning does interpolate it (unchanged), but a consolidated report is a
@@ -26,7 +27,7 @@ import { createReadinessCheck, createReadinessCheckSafely, } from "../readiness-
26
27
  * gate needs to echo to name the fix. ADD, never replace — replacing the value
27
28
  * would drop the worker's other groups.
28
29
  */
29
- export const MCP_PROFILE_REMEDIATION = "ADD `conductor` to BRIDGE_MCP_PROFILE (comma-separated) — do NOT replace the existing value, " +
30
+ export const MCP_PROFILE_REMEDIATION = "ADD `epic_implementer` to BRIDGE_MCP_PROFILE (comma-separated) — do NOT replace the existing value, " +
30
31
  "which would drop the worker's other tool groups. Without it the conductor/event/supervisor " +
31
32
  "tools are not registered.";
32
33
  /**
@@ -38,13 +39,24 @@ export const MCP_PROFILE_REMEDIATION = "ADD `conductor` to BRIDGE_MCP_PROFILE (c
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
  *
@@ -70,9 +82,10 @@ export const MCP_PROFILE_UNSET = "(unset)";
70
82
  /**
71
83
  * The CLI-only verbs that call the Bridge API from a shell-spawned process and
72
84
  * therefore resolve credentials themselves (CLAUDE.md "Shell-Spawned Credential
73
- * Resolution"): the four BAPI-872 recovery verbs and the three BAPI-1154 repair
74
- * verbs. `.mcp.json` env is invisible to them — only `BAPI_API_KEY` in the parent
75
- * env and the user-scoped credential store count.
85
+ * Resolution"): the four BAPI-872 recovery verbs and the six run-addressed repair
86
+ * verbs (BAPI-1154's three and BAPI-1176's three). `.mcp.json` env is invisible to
87
+ * them — only `BAPI_API_KEY` in the parent env and the user-scoped credential
88
+ * store count.
76
89
  */
77
90
  export const CREDENTIALED_RECOVERY_COMMANDS = [
78
91
  "stop-run",
@@ -82,6 +95,9 @@ export const CREDENTIALED_RECOVERY_COMMANDS = [
82
95
  "scope-reparse",
83
96
  "dispatch-remediation",
84
97
  "merge-child",
98
+ "scope-catch-up",
99
+ "patch-policy",
100
+ "resume-run",
85
101
  ];
86
102
  /** Remediation for recovery/repair verbs that cannot resolve Bridge credentials. */
87
103
  export const REPAIR_CREDENTIAL_REMEDIATION = "export BAPI_API_KEY in the shell that runs `conductor`, or store a credential for this " +
@@ -364,13 +380,13 @@ export async function collectConductorNativeLedgerSafe(deps = {}) {
364
380
  }
365
381
  /**
366
382
  * Inspect the BRIDGE_MCP_PROFILE environment variable and warn when a
367
- * conductor/epic context is detected but the resolved groups omit `conductor`.
383
+ * conductor/epic context is detected but the resolved groups omit `epic_implementer`.
368
384
  * Strictly read-only, never throws.
369
385
  *
370
386
  * BAPI-772: health is judged from {@link resolveProfiles} — the SAME resolver the
371
387
  * MCP server registers tools with — instead of comparing the raw string to
372
- * `"conductor"`. The old equality check called a worker running
373
- * `BRIDGE_MCP_PROFILE=conductor,sfcc` (exactly what `buildConductorWorkerEnv`
388
+ * `"epic_implementer"`. The old equality check called a worker running
389
+ * `BRIDGE_MCP_PROFILE=epic_implementer,sfcc` (exactly what `buildConductorWorkerEnv`
374
390
  * produces on SFCC projects) or `full` degraded, and its remediation told the
375
391
  * operator to REPLACE the variable — which would have destroyed the worker's
376
392
  * other groups. The remediation now says ADD.
@@ -380,15 +396,15 @@ export function inspectMcpProfile(env) {
380
396
  const raw_profile = raw === undefined ? MCP_PROFILE_UNSET : raw;
381
397
  const groups = resolveProfiles(raw);
382
398
  const active_groups = Array.from(groups);
383
- const conductor_context_detected = env.BAPI_CONDUCTOR_ENABLED === "1" || env.BAPI_CONDUCTOR_ENABLED === "true";
384
- const degraded = conductor_context_detected && !groups.has("conductor");
399
+ const conductor_context_detected = env.BAPI_EPIC_IMPLEMENTER_ENABLED === "1" || env.BAPI_EPIC_IMPLEMENTER_ENABLED === "true";
400
+ const retired_profile = raw?.split(",").some((token) => token.trim().toLowerCase() === "conductor") ?? false;
401
+ const degraded = retired_profile || (conductor_context_detected && !groups.has("epic_implementer"));
385
402
  const warnings = [];
386
403
  if (degraded) {
387
- warnings.push(`BRIDGE_MCP_PROFILE is ${raw_profile} (active groups: ${active_groups.join(", ")}) ` +
388
- `but a conductor context is active. ADD \`conductor\` to BRIDGE_MCP_PROFILE ` +
389
- `(e.g. \`${raw_profile === MCP_PROFILE_UNSET ? "conductor" : `${raw_profile},conductor`}\`) — ` +
390
- `do NOT replace the value, which would drop the worker's other groups. ` +
391
- `Without it the conductor/event/supervisor tools are not registered.`);
404
+ warnings.push(`Epic Implementer MCP profile is inactive (active groups: ${active_groups.join(", ")}). ` +
405
+ (retired_profile ? "The retired conductor profile token is ignored. " : "") +
406
+ "ADD `epic_implementer` to BRIDGE_MCP_PROFILE (comma-separated); keep other valid groups. " +
407
+ "Without it the Epic Implementer tools are not registered.");
392
408
  }
393
409
  // Fixed state labels and the CLOSED active-group vocabulary only (BAPI-1055):
394
410
  // `resolveProfiles` yields nothing but recognized group names, so this detail
@@ -397,14 +413,14 @@ export function inspectMcpProfile(env) {
397
413
  degraded
398
414
  ? {
399
415
  id: "mcp-profile",
400
- label: "MCP conductor profile",
416
+ label: "Epic Implementer MCP profile",
401
417
  status: "warn",
402
- detail: `a conductor context is active but the resolved groups omit \`conductor\` (active: ${active_groups.join(", ")})`,
418
+ detail: `the resolved groups omit \`epic_implementer\` (active: ${active_groups.join(", ")})`,
403
419
  remediation: MCP_PROFILE_REMEDIATION,
404
420
  }
405
421
  : {
406
422
  id: "mcp-profile",
407
- label: "MCP conductor profile",
423
+ label: "Epic Implementer MCP profile",
408
424
  status: "pass",
409
425
  detail: conductor_context_detected
410
426
  ? `conductor context active; resolved groups: ${active_groups.join(", ")}`
@@ -418,7 +434,7 @@ export function inspectMcpProfile(env) {
418
434
  * `gh auth status` (no writes, no merge) and reports whether the host could merge
419
435
  * a PR locally if `policy_json.local_merge.enabled` were set. Never throws.
420
436
  */
421
- export function inspectLocalMerge(runCommand) {
437
+ export function inspectLocalMerge(runCommand, env = process.env) {
422
438
  const run = runCommand ??
423
439
  ((cmd, args) => {
424
440
  try {
@@ -443,48 +459,51 @@ export function inspectLocalMerge(runCommand) {
443
459
  }
444
460
  });
445
461
  let gh_available = false;
446
- let gh_authed = false;
447
462
  try {
448
463
  gh_available = run("gh", ["--version"]).status === 0;
449
464
  }
450
465
  catch {
451
466
  gh_available = false;
452
467
  }
453
- if (gh_available) {
454
- try {
455
- gh_authed = run("gh", ["auth", "status"]).status === 0;
456
- }
457
- catch {
458
- gh_authed = false;
459
- }
460
- }
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);
461
480
  const warnings = [];
462
481
  if (!gh_available) {
463
482
  warnings.push("`gh` is not installed or not on PATH. Local merge (policy_json.local_merge.enabled) " +
464
483
  "cannot run; install the GitHub CLI to enable conductor-driven merges.");
465
484
  }
466
- else if (!gh_authed) {
467
- warnings.push("`gh` is installed but not authenticated (`gh auth status` failed). Run `gh auth login` " +
468
- "to grant the conductor merge permission; otherwise local merge will emit merge.failed/skip.");
469
- }
470
- // Capability gap only — local merge is opt-in, so an unauthed host is not a hard
471
- // failure unless the operator enabled it. Reported as degraded for visibility.
472
- const degraded = !gh_available || !gh_authed;
473
- // TWO outcomes, not one (BAPI-1055): installing `gh` and running `gh auth
474
- // login` are independently actionable, and `degraded` below is deliberately
475
- // left as the SAME `!gh_available || !gh_authed` conjunction it has always
476
- // been. When `gh` is absent the auth outcome is `skip`, never `fail` — the
477
- // authentication was never tested, and claiming it failed would send the
478
- // 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.
479
499
  //
480
- // AC-7 — these two checks are LOAD-BEARING and must not be retired here.
481
- // Conductor merges run under this machine's own `gh` auth because the GitHub
482
- // App token is `contents: read` and structurally cannot merge; see
483
- // `mcp_server/src/executor/merge-job.ts` and the `contents: read` constraint
484
- // documented in `mcp_server/src/merge-pull-request.ts`. Retirement is owned by
485
- // epic A2, after the App is elevated to `contents: write`. Nothing in this
486
- // change removes, hides, or downgrades either check, and no merge behaviour is
487
- // 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.
488
507
  const outcomes = [
489
508
  gh_available
490
509
  ? {
@@ -502,28 +521,155 @@ export function inspectLocalMerge(runCommand) {
502
521
  },
503
522
  !gh_available
504
523
  ? {
505
- id: "gh-auth",
506
- label: "GitHub CLI authentication",
524
+ id: "merge-credential",
525
+ label: "Configured merge credential",
507
526
  status: "skip",
508
527
  detail: GH_AUTH_SKIPPED_DETAIL,
509
528
  remediation: GH_CLI_REMEDIATION,
510
529
  }
511
- : gh_authed
530
+ : merge_credential_configured
512
531
  ? {
513
- id: "gh-auth",
514
- label: "GitHub CLI authentication",
532
+ id: "merge-credential",
533
+ label: "Configured merge credential",
515
534
  status: "pass",
516
- detail: "`gh auth status` reports an authenticated account",
535
+ detail: "BAPI_LOCAL_MERGE_TOKEN or BAPI_MACHINE_MERGE_TOKEN is set",
517
536
  }
518
537
  : {
519
- id: "gh-auth",
520
- label: "GitHub CLI authentication",
538
+ id: "merge-credential",
539
+ label: "Configured merge credential",
521
540
  status: "warn",
522
- 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",
523
543
  remediation: GH_AUTH_REMEDIATION,
524
544
  },
525
545
  ];
526
- 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
+ };
527
673
  }
528
674
  /**
529
675
  * Build the combined read-only doctor report. Composes the existing ledger
@@ -549,12 +695,43 @@ export async function buildConductorDoctorReport(deps = {}) {
549
695
  git_hooks: inspectHooks(deps.hooksDeps),
550
696
  mcp_profile,
551
697
  local_merge: inspectLocalMerge(deps.runCommand),
698
+ github_actions_merge: inspectGitHubActionsMergeSafe(deps),
552
699
  native_ledger,
553
700
  deny_enforcement,
554
701
  claude_login,
555
702
  repair_credentials: await inspectRepairVerbCredentials(deps.env ?? process.env, deps.resolveRepairAccess),
556
703
  };
557
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
+ }
558
735
  /**
559
736
  * Run the deny-enforcement inspector and convert any thrown exception into degraded
560
737
  * report data — a deny-probe failure must never throw out of doctor. The underlying
@@ -594,7 +771,7 @@ async function inspectDenyEnforcementSafe(inspect) {
594
771
  * hierarchy across sections.
595
772
  */
596
773
  export function formatConductorDoctorReport(report) {
597
- 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;
598
775
  const lines = [
599
776
  "Conductor ledger doctor",
600
777
  "───────────────────────",
@@ -648,6 +825,7 @@ export function formatConductorDoctorReport(report) {
648
825
  // value of `full` is healthy precisely because it resolves to a set containing
649
826
  // `conductor`.
650
827
  const profileTag = mcp_profile.degraded ? "[WARNING] degraded" : "[OK]";
828
+ lines.push("Epic Implementer MCP profile");
651
829
  lines.push(`raw profile: ${mcp_profile.raw_profile}`);
652
830
  lines.push(`active groups: ${mcp_profile.active_groups.join(", ")} ${profileTag}`);
653
831
  lines.push(`conductor context: ${mcp_profile.conductor_context_detected}`);
@@ -661,13 +839,38 @@ export function formatConductorDoctorReport(report) {
661
839
  lines.push("Local merge capability (optional, opt-in)");
662
840
  lines.push("─────────────────────────────────────────");
663
841
  const ghTag = local_merge.gh_available
664
- ? local_merge.gh_authed
842
+ ? local_merge.merge_credential_configured
665
843
  ? "[OK]"
666
- : "[WARNING] not authenticated"
844
+ : "[WARNING] no merge credential configured"
667
845
  : "[WARNING] not installed";
668
846
  lines.push(`gh available: ${local_merge.gh_available} ${ghTag}`);
669
- lines.push(`gh authenticated: ${local_merge.gh_authed}`);
847
+ lines.push(`merge credential set: ${local_merge.merge_credential_configured}`);
670
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
+ }
671
874
  if (local_merge.warnings.length > 0) {
672
875
  lines.push("local merge warnings:");
673
876
  for (const w of local_merge.warnings)
@@ -746,9 +949,9 @@ export function formatConductorDoctorReport(report) {
746
949
  export const CONDUCTOR_READINESS_DESCRIPTORS = [
747
950
  { id: "ledger", label: "Conductor event ledger" },
748
951
  { id: "git-hooks", label: "Conductor git hooks" },
749
- { id: "mcp-profile", label: "MCP conductor profile" },
952
+ { id: "mcp-profile", label: "Epic Implementer MCP profile" },
750
953
  { id: "gh-cli", label: "GitHub CLI availability" },
751
- { id: "gh-auth", label: "GitHub CLI authentication" },
954
+ { id: "merge-credential", label: "Configured merge credential" },
752
955
  { id: "native-ledger", label: "Native ledger binding" },
753
956
  { id: "mcp-registration-form", label: "Worker MCP registration form" },
754
957
  { id: "node-engine", label: "Expected Node engine" },
@@ -916,6 +1119,10 @@ export function mapConductorDoctorReportToReadinessChecks(report) {
916
1119
  record(outcome);
917
1120
  for (const outcome of outcomesOf(report?.local_merge))
918
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);
919
1126
  for (const outcome of outcomesOf(report?.native_ledger))
920
1127
  record(outcome);
921
1128
  record(denyEnforcementOutcome(report));
@@ -24,7 +24,7 @@ import { ConductorValidationError, ConductorEpicTickV1FrozenError, toConductorEr
24
24
  import { emitConductorEvent, purgeConductorLedger, sendWorkerMessage, checkWorkerMessages, } from "./store.js";
25
25
  import { isDuplicateConstraintError } from "./producer-ledger.js";
26
26
  import { SEMANTIC_EVENT_TYPES } from "./taxonomy.js";
27
- import { DRIVER_CONTEXT_MAX_BYTES } from "./repair-contract.js";
27
+ import { DRIVER_CONTEXT_MAX_BYTES, POLICY_PATCH_MAX_BYTES } from "./repair-contract.js";
28
28
  import { installConductorGitHooks, resolveConductorHookBin } from "./git-hooks.js";
29
29
  import { runFileScopeGuardCli } from "./file-scope-guard.js";
30
30
  import { runPostCommitHookProducer, runReferenceTransactionHookProducer } from "./git-producer.js";
@@ -94,6 +94,15 @@ export function getConductorUsage() {
94
94
  " merge-child --epic-run-id <id> --pr <number>",
95
95
  " Merge the run's PR-bound child through the shared merge",
96
96
  " service; the server composes the head and gate identity.",
97
+ " scope-catch-up --epic-run-id <id> --scope-id <scope> [--json]",
98
+ " Replay merges the run's OWN scope did not observe through the",
99
+ " guarded advance gate (no force). Refused if the scope is not the run's.",
100
+ " patch-policy --epic-run-id <id> --patch-file <path> [--json]",
101
+ " Merge-patch (RFC 7396) the run's policy on the server. Protected",
102
+ ` paths refused; patch at most POLICY_PATCH_MAX_BYTES = ${POLICY_PATCH_MAX_BYTES} bytes.`,
103
+ " resume-run --epic-run-id <id> [--clear-quarantine] [--json]",
104
+ " Resume a paused run (a quarantined one only with --clear-quarantine);",
105
+ " activation is re-checked server-side. Active/no-control is a no-op.",
97
106
  "",
98
107
  "supervise options:",
99
108
  " --run-id <id> Run/session identifier to supervise (required)",
@@ -168,10 +177,16 @@ export function getConductorUsage() {
168
177
  " --raw-json '{\"branch\":\"feature/x\",\"sha\":\"abc123\"}'",
169
178
  " epic-implementer emit-event --type ci.failed --source ci \\",
170
179
  " --payload-ref 'file:///tmp/ci-log.txt'",
171
- " epic-implementer emit-event --type merge.succeeded --source conductor-merge \\",
180
+ " epic-implementer emit-event --type merge.succeeded --source epic-implementer-merge \\",
172
181
  " --worker-id w1 --data-json '{\"summary\":\"auto-merged\",\"status\":\"succeeded\"}'",
173
182
  " epic-implementer doctor --json",
174
183
  " epic-implementer purge",
184
+ " epic-implementer scope-catch-up --epic-run-id <id> --scope-id <scope>",
185
+ " # a merge landed on epic/<KEY> that the scope never observed (unobserved_advance)",
186
+ " epic-implementer patch-policy --epic-run-id <id> --patch-file required-checks.json",
187
+ " # required_checks_empty_no_mode; the file holds {\"required_checks\":[\"conductor-ci / gate\"]}",
188
+ " epic-implementer resume-run --epic-run-id <id>",
189
+ " # a cap_wall_clock pause whose cause is now cleared",
175
190
  "",
176
191
  "epic-tick options:",
177
192
  " --epic-key <KEY> Epic key to supervise (required, non-empty)",
@@ -263,6 +278,10 @@ const VALID_COMMANDS = new Set([
263
278
  "scope-reparse",
264
279
  "dispatch-remediation",
265
280
  "merge-child",
281
+ // BAPI-1176: CLI-only run-addressed repair verbs — never registered as MCP tools.
282
+ "scope-catch-up",
283
+ "patch-policy",
284
+ "resume-run",
266
285
  // Private, and deliberately ABSENT from the usage text: `__hook-bin` exists as
267
286
  // the bundled-artifact regression guard for BAPI-772 (mirroring `plane
268
287
  // __entrypoint`), not as a supported operator workflow.
@@ -833,7 +852,7 @@ export function parseEpicTickArgs(argv) {
833
852
  /**
834
853
  * Default the PreToolUse hook ON for epic-dispatched workers so the supervisor
835
854
  * gets worker-liveness (`tool.intent`) signals without the operator having to
836
- * export `BAPI_CONDUCTOR_ENABLE_PRE_TOOL_USE=1` (BAPI-441/A1 finding).
855
+ * export `BAPI_EPIC_IMPLEMENTER_ENABLE_PRE_TOOL_USE=1` (BAPI-441/A1 finding).
837
856
  *
838
857
  * Both the child-env copy and the hook registration key off this SAME
839
858
  * parent-process env flag, read at dispatch time, so setting it once at the
@@ -843,8 +862,8 @@ export function parseEpicTickArgs(argv) {
843
862
  * `process.env`) and returns it.
844
863
  */
845
864
  export function applyEpicTickPreToolUseDefault(env = process.env) {
846
- if (env.BAPI_CONDUCTOR_ENABLE_PRE_TOOL_USE === undefined) {
847
- env.BAPI_CONDUCTOR_ENABLE_PRE_TOOL_USE = "1";
865
+ if (env.BAPI_EPIC_IMPLEMENTER_ENABLE_PRE_TOOL_USE === undefined) {
866
+ env.BAPI_EPIC_IMPLEMENTER_ENABLE_PRE_TOOL_USE = "1";
848
867
  }
849
868
  return env;
850
869
  }
@@ -1281,6 +1300,20 @@ export async function runEpicImplementerCli(argv) {
1281
1300
  const { runMergeChildCommand } = await import("./recovery-cli.js");
1282
1301
  return await runMergeChildCommand(parsed.argv);
1283
1302
  }
1303
+ // BAPI-1176: three more run-addressed repair verbs — CLI-only, lazily
1304
+ // imported like the rest, and dispatched before any MCP construction.
1305
+ case "scope-catch-up": {
1306
+ const { runScopeCatchUpCommand } = await import("./recovery-cli.js");
1307
+ return await runScopeCatchUpCommand(parsed.argv);
1308
+ }
1309
+ case "patch-policy": {
1310
+ const { runPatchPolicyCommand } = await import("./recovery-cli.js");
1311
+ return await runPatchPolicyCommand(parsed.argv);
1312
+ }
1313
+ case "resume-run": {
1314
+ const { runResumeRunCommand } = await import("./recovery-cli.js");
1315
+ return await runResumeRunCommand(parsed.argv);
1316
+ }
1284
1317
  default:
1285
1318
  console.error('Error: Unknown command. Run "epic-implementer --help" for usage.');
1286
1319
  return 1;
@@ -55,7 +55,7 @@ export function safeDiagnosticMessage(err, fallback) {
55
55
  *
56
56
  * Deliberately diagnostic. The `epic_ticket_status` vocabulary — `parsing`,
57
57
  * `needs_human` and the rest — is owned by the SERVER-side reconciler
58
- * (`api/library/epic_conductor/reconciler.py`), which is the single transition
58
+ * (`api/library/epic_implementer/reconciler.py`), which is the single transition
59
59
  * authority for it and already parks this exact condition through its ordinary
60
60
  * `_park_needs_human` commit point under the same `shadow_stale_deadline` reason.
61
61
  * This pass runs the legacy supervisor's own, narrower status vocabulary, and