@bridge_gpt/mcp-server 0.2.66 → 0.2.67

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 (100) hide show
  1. package/README.md +252 -77
  2. package/build/agent-capabilities/probes.js +15 -5
  3. package/build/agent-launchers/claude-executor-adapter.js +43 -16
  4. package/build/agent-launchers/executor-adapter.js +7 -4
  5. package/build/agents.generated.js +1 -1
  6. package/build/bounded-wait.js +67 -10
  7. package/build/commands.generated.js +11 -11
  8. package/build/conductor/bridge-api-client.js +143 -1
  9. package/build/conductor/bring-up-facts.js +1 -1
  10. package/build/conductor/doctor.js +3 -3
  11. package/build/conductor/epic-implementer-cli.js +22 -6
  12. package/build/conductor/errors.js +4 -3
  13. package/build/conductor/guided-repair.js +6 -2
  14. package/build/conductor/hook-runtime-qualification.js +1 -1
  15. package/build/conductor/merge-execution-contract.js +27 -8
  16. package/build/conductor/merge-execution-eligibility.js +3 -2
  17. package/build/conductor/pr-ci-producer.js +1 -1
  18. package/build/conductor/readiness-cli.js +18 -2
  19. package/build/conductor/readiness-sections.js +41 -6
  20. package/build/conductor/readiness.js +181 -29
  21. package/build/conductor/recovery-cli.js +200 -8
  22. package/build/conductor/recovery-operations.js +69 -3
  23. package/build/conductor/repair-contract.js +54 -3
  24. package/build/conductor/repository-workflow-remedy.js +125 -0
  25. package/build/conductor/tools.js +2 -2
  26. package/build/conductor-bundle-artifacts.js +8 -0
  27. package/build/docs.generated.js +8 -3
  28. package/build/drive-epic-key.js +6 -5
  29. package/build/drive-epic.js +85 -54
  30. package/build/epic-coherence-input.js +2 -1
  31. package/build/epic-implementer-bin.js +22 -20
  32. package/build/epic-implementer-bundle-cli.js +2 -2
  33. package/build/executor/cli.js +3 -2
  34. package/build/executor/completion-contract.js +48 -5
  35. package/build/executor/completion-supervision.js +4 -0
  36. package/build/executor/env.js +19 -0
  37. package/build/executor/heartbeat.js +1 -1
  38. package/build/executor/http-client.js +20 -0
  39. package/build/executor/job-errors.js +25 -0
  40. package/build/executor/job-runner.js +225 -41
  41. package/build/executor/observation.js +6 -1
  42. package/build/executor/preflight.js +51 -14
  43. package/build/executor/required-work-ledger.js +6 -1
  44. package/build/executor/resume-pre-spawn.js +2 -0
  45. package/build/executor/runner.js +8 -2
  46. package/build/executor/service-unit.js +7 -1
  47. package/build/executor/worker-finalization.js +82 -29
  48. package/build/executor/worker-guard-hook.js +137 -33
  49. package/build/executor/worktree.js +82 -1
  50. package/build/index.js +403 -429
  51. package/build/install-bridge-conductor.js +8 -6
  52. package/build/install-bridge.js +3 -3
  53. package/build/phase-context-git-probe.js +68 -0
  54. package/build/phase-result-artifacts.js +173 -5
  55. package/build/pipelines.generated.js +18 -24
  56. package/build/plane/alembic-head.js +12 -3
  57. package/build/plane/base-worktree-guard.js +147 -0
  58. package/build/plane/cli.js +217 -17
  59. package/build/plane/code-snapshot.js +468 -0
  60. package/build/plane/defaults.js +48 -3
  61. package/build/plane/endpoint.js +41 -0
  62. package/build/plane/manifest.js +120 -1
  63. package/build/plane/member-roster.js +94 -19
  64. package/build/plane/preflight.js +30 -2
  65. package/build/plane/process-cwd.js +67 -0
  66. package/build/plane/restart.js +542 -0
  67. package/build/plane/shutdown.js +31 -0
  68. package/build/plane/status.js +143 -8
  69. package/build/plane/supervisor.js +586 -179
  70. package/build/plane/types.js +54 -2
  71. package/build/printed-remedy-commands.js +37 -0
  72. package/build/readiness-check.js +1 -1
  73. package/build/readme.generated.js +1 -1
  74. package/build/review-generation.js +22 -0
  75. package/build/setup-epic.js +44 -19
  76. package/build/sfcc/config.js +9 -0
  77. package/build/sfcc/credentials.js +100 -38
  78. package/build/sfcc/hostname.js +67 -0
  79. package/build/sfcc/permissions.js +17 -13
  80. package/build/sfcc/register.js +1 -1
  81. package/build/sfcc/tool-wrapper.js +39 -43
  82. package/build/sfcc/write-grants.js +17 -11
  83. package/build/sfcc/write-result.js +4 -3
  84. package/build/sfcc/writes-site-preference.js +20 -8
  85. package/build/start-tickets.js +179 -31
  86. package/build/tool-error-envelope.js +61 -0
  87. package/build/version.generated.js +3 -3
  88. package/build/visual-diff.js +22 -1
  89. package/build/worker-guard-hook-bin.js +3 -6
  90. package/build/worker-guard-hook-main.js +47 -0
  91. package/build/worktree-core.js +130 -4
  92. package/docs/EPIC-IMPLEMENTER-INSTALL.md +849 -0
  93. package/docs/EPIC-IMPLEMENTER.md +662 -0
  94. package/docs/install/mcp-tool-integrations.md +24 -15
  95. package/docs/install/sfcc-integration.md +12 -4
  96. package/package.json +1 -1
  97. package/pipelines/plan-epic.json +0 -5
  98. package/pipelines/review-ticket.json +1 -1
  99. package/docs/CONDUCTOR.md +0 -605
  100. package/pipelines/idea-to-pr.json +0 -49
@@ -20,7 +20,7 @@ import { parseContinuationStatusResult, parseContinueParkedChildResult, } from "
20
20
  import { resolveDeclaredRunBaseBranch } from "./run-branch.js";
21
21
  import { safeEpicLaunchTargetUrl } from "../epic-launch-diagnostic.js";
22
22
  import { validateHttpBaseUrl } from "../base-url.js";
23
- import { isCiResumeRefusalCode } from "./repair-contract.js";
23
+ import { RERUN_CI_CHECK_STATUSES, RERUN_CI_NOT_RUNNABLE_REASONS, RERUN_CI_OUTCOMES, isCiResumeRefusalCode, } from "./repair-contract.js";
24
24
  /** Default Bridge API base URL when `BAPI_BASE_URL` is unset. */
25
25
  export const CONDUCTOR_DEFAULT_BASE_URL = "https://bridgegpt-api.com";
26
26
  /** Default per-request timeout for conductor Bridge API calls. */
@@ -1532,6 +1532,117 @@ export async function dispatchRemediation(access, request, fetchImpl = globalThi
1532
1532
  const parsed = await postRepairRequest(access, `${epicRunApiPath(request.epicRunId)}/tickets/${encodeURIComponent(request.ticketKey)}/dispatch-remediation`, withGuard({ context: request.context }, request.guard), fetchImpl);
1533
1533
  return parseDispatchRemediationResult(parsed);
1534
1534
  }
1535
+ // ---------------------------------------------------------------------------
1536
+ // BAPI-1388 — `rerun-ci`
1537
+ // ---------------------------------------------------------------------------
1538
+ /** The synthetic code an old server's missing route is reported under. */
1539
+ export const RERUN_CI_ROUTE_UNAVAILABLE_CODE = "RERUN_CI_ROUTE_UNAVAILABLE";
1540
+ /** The fixed operator text for a server that has no `rerun-ci` route. */
1541
+ export const RERUN_CI_ROUTE_UNAVAILABLE_MESSAGE = "rerun-ci is not available on this server; upgrade the Bridge API server before using it";
1542
+ const RERUN_CI_HEAD_RE = /^(?:[0-9a-f]{40}|[0-9a-f]{64})$/;
1543
+ function parseRerunCiCheck(value) {
1544
+ if (!value || typeof value !== "object" || Array.isArray(value)) {
1545
+ throw new ConductorBridgeApiError("server");
1546
+ }
1547
+ const c = value;
1548
+ const status = c["status"];
1549
+ if (typeof status !== "string" ||
1550
+ !RERUN_CI_CHECK_STATUSES.includes(status)) {
1551
+ throw new ConductorBridgeApiError("server");
1552
+ }
1553
+ const reason = c["reason"];
1554
+ if (reason !== undefined &&
1555
+ reason !== null &&
1556
+ (typeof reason !== "string" ||
1557
+ !RERUN_CI_NOT_RUNNABLE_REASONS.includes(reason))) {
1558
+ throw new ConductorBridgeApiError("server");
1559
+ }
1560
+ const optionalPositive = (field) => {
1561
+ const v = c[field];
1562
+ if (v === undefined || v === null)
1563
+ return null;
1564
+ if (typeof v !== "number" || !Number.isSafeInteger(v) || v <= 0) {
1565
+ throw new ConductorBridgeApiError("server");
1566
+ }
1567
+ return v;
1568
+ };
1569
+ const optionalText = (field) => {
1570
+ const v = c[field];
1571
+ if (v === undefined || v === null)
1572
+ return null;
1573
+ if (typeof v !== "string")
1574
+ throw new ConductorBridgeApiError("server");
1575
+ return v;
1576
+ };
1577
+ return {
1578
+ name: optionalText("name"),
1579
+ workflow_identity: optionalText("workflow_identity"),
1580
+ run_id: optionalPositive("run_id"),
1581
+ status: status,
1582
+ reason: (reason ?? null),
1583
+ job_id: optionalPositive("job_id"),
1584
+ };
1585
+ }
1586
+ /**
1587
+ * Strictly parse a `rerun-ci` success body. Anything short of the contract — an
1588
+ * unknown outcome or per-check status, a malformed head, a non-list `checks` —
1589
+ * throws, so a malformed answer is never read as a success.
1590
+ */
1591
+ export function parseRerunCiResult(parsed) {
1592
+ const p = parseRepairBase(parsed);
1593
+ const outcome = p["outcome"];
1594
+ const checks = p["checks"];
1595
+ const head = p["head_sha"];
1596
+ if (typeof p["ticket_key"] !== "string" ||
1597
+ typeof outcome !== "string" ||
1598
+ !RERUN_CI_OUTCOMES.includes(outcome) ||
1599
+ !Array.isArray(checks) ||
1600
+ (head !== undefined && head !== null && (typeof head !== "string" || !RERUN_CI_HEAD_RE.test(head)))) {
1601
+ throw new ConductorBridgeApiError("server");
1602
+ }
1603
+ return {
1604
+ epic_run_id: p["epic_run_id"],
1605
+ ticket_key: p["ticket_key"],
1606
+ before: parseRepairNullableString(p["before"]),
1607
+ after: parseRepairNullableString(p["after"]),
1608
+ committed: p["committed"],
1609
+ event_recorded: p["event_recorded"],
1610
+ outcome: outcome,
1611
+ head_sha: typeof head === "string" ? head : null,
1612
+ checks: checks.map(parseRerunCiCheck),
1613
+ };
1614
+ }
1615
+ /**
1616
+ * POST `/jira/epic-runs/runs/{epic_run_id}/tickets/{ticket_key}/rerun-ci` — body
1617
+ * `{repo_name, guard}` and nothing else: no context, check list, or run id, because
1618
+ * the SERVER decides what is re-runnable from its own fresh, head-bound CI read.
1619
+ *
1620
+ * A generic `{"detail":"Not Found"}` 404 or a 405 means the server predates the
1621
+ * route; it is reported under {@link RERUN_CI_ROUTE_UNAVAILABLE_CODE} so no caller
1622
+ * can read it as a refusal of THIS ticket or as a success (following
1623
+ * {@link classifySupervisorBootstrapFailure}). A structured 404 — notably
1624
+ * `TICKET_NOT_IN_RUN` — keeps its own code and stays a refusal.
1625
+ */
1626
+ export async function rerunCi(access, request, fetchImpl = globalThis.fetch) {
1627
+ requireNonEmptyString(request.epicRunId);
1628
+ requireNonEmptyString(request.ticketKey);
1629
+ let parsed;
1630
+ try {
1631
+ parsed = await postRepairRequest(access, `${epicRunApiPath(request.epicRunId)}/tickets/${encodeURIComponent(request.ticketKey)}/rerun-ci`, { guard: request.guard }, fetchImpl);
1632
+ }
1633
+ catch (err) {
1634
+ if (err instanceof ConductorBridgeApiError &&
1635
+ !err.errorCode &&
1636
+ (err.status === 404 || err.status === 405)) {
1637
+ throw new ConductorBridgeApiError("http", err.status, {
1638
+ errorCode: RERUN_CI_ROUTE_UNAVAILABLE_CODE,
1639
+ bodyPreview: RERUN_CI_ROUTE_UNAVAILABLE_MESSAGE,
1640
+ });
1641
+ }
1642
+ throw err;
1643
+ }
1644
+ return parseRerunCiResult(parsed);
1645
+ }
1535
1646
  /**
1536
1647
  * POST `/jira/epic-runs/runs/{epic_run_id}/merge-child` — body EXACTLY
1537
1648
  * `{repo_name, pr_number}`. The server composes the head, gate, and action key.
@@ -2469,6 +2580,20 @@ function requireBool(o, key) {
2469
2580
  throw readinessShapeError();
2470
2581
  return v;
2471
2582
  }
2583
+ /**
2584
+ * An optional boolean field as a spreadable fragment: `{}` when the key is absent,
2585
+ * `{ [key]: value }` when it is a boolean, and a shape error for anything else —
2586
+ * including an explicit `null`. Spreading keeps an absent field ABSENT rather than
2587
+ * present-as-`undefined`.
2588
+ */
2589
+ function optionalBoolField(o, key) {
2590
+ if (!(key in o) || o[key] === undefined)
2591
+ return {};
2592
+ const v = o[key];
2593
+ if (typeof v !== "boolean")
2594
+ throw readinessShapeError();
2595
+ return { [key]: v };
2596
+ }
2472
2597
  function requireInt(o, key) {
2473
2598
  const v = o[key];
2474
2599
  if (typeof v !== "number" || !Number.isInteger(v))
@@ -2617,6 +2742,15 @@ function parseRequiredChecksVerification(sup, requiredChecksCount) {
2617
2742
  required_checks_evidence_fresh: fresh,
2618
2743
  };
2619
2744
  }
2745
+ /** Optional boolean: absent/null from an older server, a non-boolean fails closed. */
2746
+ function parseOptionalBool(o, key) {
2747
+ const v = o[key];
2748
+ if (v === undefined || v === null)
2749
+ return null;
2750
+ if (typeof v !== "boolean")
2751
+ throw readinessShapeError();
2752
+ return v;
2753
+ }
2620
2754
  /** Optional positive-integer threshold: absent/null from an older server. */
2621
2755
  function parseOptionalPositiveInt(o, key) {
2622
2756
  const v = o[key];
@@ -2666,6 +2800,9 @@ export function parseConductorReadinessResponse(body) {
2666
2800
  // Absent on a pre-BAPI-694 server, where no repository default can exist.
2667
2801
  review_policy_present: sup.review_policy_present === true,
2668
2802
  ...parseRequiredChecksVerification(sup, requiredChecksCount),
2803
+ required_checks_retired_gate_configured: parseOptionalBool(sup, "required_checks_retired_gate_configured"),
2804
+ required_checks_ci_workflow_absent: parseOptionalBool(sup, "required_checks_ci_workflow_absent"),
2805
+ required_checks_review_workflow_absent: parseOptionalBool(sup, "required_checks_review_workflow_absent"),
2669
2806
  },
2670
2807
  github: {
2671
2808
  credentials_readable: requireBool(gh, "credentials_readable"),
@@ -2891,6 +3028,11 @@ function parseConductorCiWorkflow(body) {
2891
3028
  probe_succeeded: requireBool(o, "probe_succeeded"),
2892
3029
  workflow_present: requireBool(o, "workflow_present"),
2893
3030
  migration_guard_present: requireBool(o, "migration_guard_present"),
3031
+ // BAPI-1359: OPTIONAL so a newer CLI still reads an older server. An absent
3032
+ // key stays `undefined` — never materialized as a default — and a present key
3033
+ // must be a real boolean (`null`, strings, numbers, and objects all reject).
3034
+ ...optionalBoolField(o, "migration_guard_required"),
3035
+ ...optionalBoolField(o, "migration_chain_probe_succeeded"),
2894
3036
  };
2895
3037
  }
2896
3038
  /**
@@ -1,5 +1,5 @@
1
1
  /** The pinned operator runbook every remedy below points into. */
2
- export const BRING_UP_RUNBOOK = "docs/claude/epic-implementer-v2-operator-runbook.md";
2
+ export const BRING_UP_RUNBOOK = "docs/epic-implementer/operator-runbook.md";
3
3
  /**
4
4
  * The client-derived refusal vocabulary, IN REFUSAL ORDER.
5
5
  *
@@ -822,9 +822,9 @@ export function formatConductorDoctorReport(report) {
822
822
  lines.push(` - ${w}`);
823
823
  }
824
824
  lines.push("");
825
- lines.push("Epic Conductor v2 reconciles epics server-side — nothing to schedule");
826
- lines.push("locally. To execute claimed jobs on this machine, run:");
827
- lines.push(` npx -y ${MCP_PACKAGE_NAME} executor --repo <name>`);
825
+ lines.push("The Epic Implementer reconciles epics server-side — nothing to schedule");
826
+ lines.push("locally. To execute a run's claimed jobs on this machine, run:");
827
+ lines.push(` npx -y ${MCP_PACKAGE_NAME} executor --repo <name> --epic-run-id <run-id>`);
828
828
  lines.push("");
829
829
  lines.push("MCP Profile (optional, local)");
830
830
  lines.push("─────────────────────────────");
@@ -57,18 +57,21 @@ export function getConductorUsage() {
57
57
  return [
58
58
  "Usage: epic-implementer <command> [options]",
59
59
  "",
60
- "Local append-only event ledger for multi-agent coordination.",
61
- "Talks ONLY to the local SQLite store (~/.config/bridge/events.db); no Bridge API calls.",
60
+ "Local append-only event ledger for multi-agent coordination, plus run-control",
61
+ "commands. The ledger commands talk ONLY to the local SQLite store",
62
+ "(~/.config/bridge/events.db); the run-control commands (stop-run through",
63
+ "rerun-ci), epic-status, and readiness call the Bridge API.",
62
64
  "",
63
65
  "Commands:",
64
66
  " emit-event Append one semantic event to the ledger",
65
67
  " supervise --run-id <id> Run the foreground, run-scoped supervisor loop",
66
- " epic-tick Run one stateless reconciliation pass for an Epic",
68
+ " epic-tick RETIRED (v1): always exits 1 with EPIC_TICK_V1_FROZEN; the",
69
+ " server-side reconciler replaced it",
67
70
  " epic-status Print read-only health summary of an Epic Run",
68
71
  " send-message Enqueue ONE typed supervisor->worker relay message (idempotent)",
69
72
  " check-messages Read + ACK pending relay messages for a worker (no redelivery)",
70
73
  " doctor Read-only health/diagnostics report (ledger + git hooks)",
71
- " readiness Read-only ADVISORY report of every conductor prerequisite",
74
+ " readiness Read-only ADVISORY report of every Epic Implementer prerequisite",
72
75
  " (install + epic-implementer doctor + plane preflight + server),",
73
76
  " each with pass/warn/fail/skip and one named remediation.",
74
77
  " Remediations are NOT run automatically; always exits 0.",
@@ -115,6 +118,10 @@ export function getConductorUsage() {
115
118
  " resume-ci --epic-run-id <id> --ticket-key <key> [--json]",
116
119
  " Resume a child parked on a qualified CI stop once CI is green: CI",
117
120
  " returns to evaluation with NO new wait, job, or repair allowance.",
121
+ " rerun-ci --epic-run-id <id> --ticket-key <key> --binding <token> [--json]",
122
+ " Ask CI to re-run a ticket's FAILED checks at its bound head (guided: copy",
123
+ " the command explain-run rendered). Charges NO allowance and never re-runs",
124
+ " claude-review. A re-run is not a verdict: after CI is green, use resume-ci.",
118
125
  "",
119
126
  "supervise options:",
120
127
  " --run-id <id> Run/session identifier to supervise (required)",
@@ -196,11 +203,11 @@ export function getConductorUsage() {
196
203
  " epic-implementer scope-catch-up --epic-run-id <id> --scope-id <scope>",
197
204
  " # a merge landed on epic/<KEY> that the scope never observed (unobserved_advance)",
198
205
  " epic-implementer patch-policy --epic-run-id <id> --patch-file required-checks.json",
199
- " # required_checks_empty_no_mode; the file holds {\"required_checks\":[\"conductor-ci / gate\"]}",
206
+ " # required_checks_empty_no_mode; the file holds {\"required_checks\":[\"epic-implementer-ci / gate\"]}",
200
207
  " epic-implementer resume-run --epic-run-id <id>",
201
208
  " # a cap_wall_clock pause whose cause is now cleared",
202
209
  "",
203
- "epic-tick options:",
210
+ "epic-tick options (retired; parsed by no live path):",
204
211
  " --epic-key <KEY> Epic key to supervise (required, non-empty)",
205
212
  " --scheduled-at <epoch> Epoch-seconds timestamp when this tick was scheduled (optional)",
206
213
  " --lease-ttl-seconds <n> Lease TTL in seconds (optional, default 120)",
@@ -259,6 +266,8 @@ export function getConductorUsage() {
259
266
  " epic-implementer continuation-receipt --epic-run-id 3f9c2b7e-... --ticket-key BAPI-852",
260
267
  " # CI finished green after the child parked on an expired CI wait:",
261
268
  " epic-implementer resume-ci --epic-run-id 3f9c2b7e-... --ticket-key BAPI-852",
269
+ " # CI failed on infrastructure: re-run it (never claude-review); then resume-ci once green:",
270
+ " epic-implementer rerun-ci --epic-run-id 3f9c2b7e-... --ticket-key BAPI-852 --binding <token>",
262
271
  ].join("\n");
263
272
  }
264
273
  /**
@@ -312,6 +321,8 @@ const VALID_COMMANDS = new Set([
312
321
  "continuation-receipt",
313
322
  // BAPI-1326: CLI-only allowance-preserving CI resume — never an MCP tool.
314
323
  "resume-ci",
324
+ // BAPI-1388: CLI-only guided provider re-run of failed CI workflow runs — never an MCP tool.
325
+ "rerun-ci",
315
326
  // Private, and deliberately ABSENT from the usage text: `__hook-bin` exists as
316
327
  // the bundled-artifact regression guard for BAPI-772 (mirroring `plane
317
328
  // __entrypoint`), not as a supported operator workflow.
@@ -1451,6 +1462,11 @@ export async function runEpicImplementerCli(argv) {
1451
1462
  const { runResumeCiCommand } = await import("./recovery-cli.js");
1452
1463
  return await runResumeCiCommand(parsed.argv);
1453
1464
  }
1465
+ case "rerun-ci": {
1466
+ // BAPI-1388: delegates only to the guided, pinned rerun-ci operation.
1467
+ const { runRerunCiCommand } = await import("./recovery-cli.js");
1468
+ return await runRerunCiCommand(parsed.argv);
1469
+ }
1454
1470
  default:
1455
1471
  console.error('Error: Unknown command. Run "epic-implementer --help" for usage.');
1456
1472
  return 1;
@@ -70,9 +70,10 @@ export class ConductorStoreError extends Error {
70
70
  */
71
71
  export class ConductorEpicTickV1FrozenError extends Error {
72
72
  constructor() {
73
- super("The v1 `epic-implementer epic-tick` path is frozen (EPIC_TICK_V1_FROZEN). Epic " +
74
- "Conductor v2 replaces the local epic tick with a server-side reconciler " +
75
- "plus the local executor — run `mcp-server executor` instead.");
73
+ super("The v1 `epic-implementer epic-tick` path is frozen (EPIC_TICK_V1_FROZEN). The " +
74
+ "Epic Implementer replaced the local epic tick with a server-side reconciler " +
75
+ "plus the local executor — run `mcp-server executor --repo <name> " +
76
+ "--epic-run-id <run-id>` instead.");
76
77
  this.name = "ConductorEpicTickV1FrozenError";
77
78
  }
78
79
  }
@@ -32,6 +32,9 @@ export const GUIDED_REPAIR_VERBS = [
32
32
  "scope-reparse",
33
33
  "scope-catch-up",
34
34
  "dispatch-remediation",
35
+ // BAPI-1388 — ticket-addressed, pinned to the same version-and-head subject as
36
+ // `dispatch-remediation`, and consuming the binding ExplainRun renders.
37
+ "rerun-ci",
35
38
  ];
36
39
  /** A malformed or mismatched binding. The message never echoes the token. */
37
40
  export class GuidedBindingError extends Error {
@@ -57,6 +60,7 @@ const ALLOWED_KEYS = {
57
60
  "scope-reparse": ["scope_id", "lifecycle_state", "required_commit_sha"],
58
61
  "scope-catch-up": ["scope_id", "lifecycle_state", "required_commit_sha"],
59
62
  "dispatch-remediation": ["ticket_key", "expected_row_version", "expected_head_sha"],
63
+ "rerun-ci": ["ticket_key", "expected_row_version", "expected_head_sha"],
60
64
  };
61
65
  function invalid() {
62
66
  return new GuidedBindingError("--binding is not a valid guided-repair binding; nothing was sent. Copy the " +
@@ -148,7 +152,7 @@ export function decodeGuidedBinding(token) {
148
152
  throw invalid();
149
153
  }
150
154
  }
151
- else if (guidedVerb === "dispatch-remediation") {
155
+ else if (guidedVerb === "dispatch-remediation" || guidedVerb === "rerun-ci") {
152
156
  binding.ticketKey = boundedString(raw.ticket_key, 64);
153
157
  binding.expectedRowVersion = nonNegativeInt(raw.expected_row_version);
154
158
  binding.expectedHeadSha = optionalSha(raw.expected_head_sha);
@@ -202,7 +206,7 @@ export function guardForBinding(binding, requestId) {
202
206
  guard.block_reason = binding.blockReason;
203
207
  return guard;
204
208
  }
205
- if (binding.verb === "dispatch-remediation") {
209
+ if (binding.verb === "dispatch-remediation" || binding.verb === "rerun-ci") {
206
210
  return {
207
211
  request_id: requestId,
208
212
  epic_run_id: binding.epicRunId,
@@ -89,7 +89,7 @@ export const HOOK_RUNTIME_ACTIONS = {
89
89
  "from a persistent install.",
90
90
  "unqualified-runtime": "the hook runtime is not a qualified persistent installation (supported: a persistent npm " +
91
91
  `install of ${MCP_PACKAGE_NAME} ${HOOK_RUNTIME_COMPATIBLE_RANGE} on macOS or Linux with an even ` +
92
- `Node LTS line >= ${HOOK_RUNTIME_MIN_NODE_MAJOR}). See mcp_server/docs/CONDUCTOR.md.`,
92
+ `Node LTS line >= ${HOOK_RUNTIME_MIN_NODE_MAJOR}). See mcp_server/docs/EPIC-IMPLEMENTER.md.`,
93
93
  "node-unavailable": "`node` could not be launched from PATH, so the hook cannot run. Put a qualified Node on PATH " +
94
94
  "and re-run `epic-implementer doctor`.",
95
95
  "native-unavailable": "better-sqlite3 does not load for the hook's Node, so no event can be recorded. Reinstall the " +
@@ -23,14 +23,13 @@
23
23
  * inspected a local checkout to decide merge eligibility would be answering a
24
24
  * different question from the one the server's before-merge preflight answers.
25
25
  *
26
- * ## Deferred remedy
26
+ * ## Remedy command
27
27
  *
28
- * {@link MERGE_WORKFLOW_REMEDY_COMMAND} names the generate-and-PR command epic
29
- * child 4 ships. Until child 4 lands, a refusal may name a command that is not
30
- * yet an executable entry point on the operator's machine. That residual risk,
31
- * its three containments, and who owns escalation are recorded once, in the
28
+ * {@link MERGE_WORKFLOW_REMEDY_COMMAND} is the complete generate-and-PR command that
29
+ * BAPI-1136 shipped (BAPI-1376 made it a full launcher command). Its history and containments are recorded once, in the
32
30
  * Python module's docstring; this mirror does not restate them.
33
31
  */
32
+ import { MCP_PACKAGE_NAME } from "../mcp-identity.js";
34
33
  /** The one `RunPolicy.merge_execution_mode` value this contract governs. */
35
34
  export const MERGE_EXECUTION_MODE_GITHUB_ACTIONS = "github_actions";
36
35
  /**
@@ -80,7 +79,27 @@ export function mergeWorkflowVersionSupportsIntegrationActions(version) {
80
79
  INTEGRATION_ACTION_WORKFLOW_VERSIONS.includes(version));
81
80
  }
82
81
  /**
83
- * The single command that generates the merge workflow and opens its pull
84
- * request. Every refusal, remediation, and readiness line imports this constant.
82
+ * The dispatched subcommand that generates the merge workflow and opens its pull
83
+ * request. This is the part the CLI's own dispatcher (`index.ts`) matches on
84
+ * (`install`), not the launcher that starts it. The Python mirror derives its
85
+ * command head from this value.
85
86
  */
86
- export const MERGE_WORKFLOW_REMEDY_COMMAND = "install-bridge conductor --merge-workflow";
87
+ export const MERGE_WORKFLOW_SUBCOMMAND = "install conductor --merge-workflow";
88
+ /**
89
+ * The complete, copy-ready command that generates the merge workflow and opens
90
+ * its pull request. Every refusal, remediation, and readiness line imports this
91
+ * constant.
92
+ *
93
+ * The package name comes from `mcp-identity.ts`, the sole source of that
94
+ * literal (BAPI-806). That module is itself dependency-free, so this import can
95
+ * never close a cycle. The `<@org/team|@user>` placeholder is deliberately NOT a valid owner — the
96
+ * installer refuses it — so an operator must replace it before running.
97
+ */
98
+ export const MERGE_WORKFLOW_REMEDY_COMMAND = `npx -y ${MCP_PACKAGE_NAME} ${MERGE_WORKFLOW_SUBCOMMAND} --codeowner <@org/team|@user>`;
99
+ /**
100
+ * One reusable instruction to print beside {@link MERGE_WORKFLOW_REMEDY_COMMAND}.
101
+ * Bridge cannot infer who should review the workflow file, so the placeholder
102
+ * must be replaced by hand.
103
+ */
104
+ export const MERGE_WORKFLOW_CODEOWNER_INSTRUCTION = "Bridge cannot infer the code owner. Before running, replace <@org/team|@user> " +
105
+ "with the one @user or @org/team who should review the workflow file.";
@@ -31,7 +31,7 @@
31
31
  * not eligible: it is a workflow this build has never been taught to reason
32
32
  * about, and "newer is probably fine" is a guess about a merge actuator.
33
33
  */
34
- import { MERGE_WORKFLOW_CHILD_MERGE_COMPATIBLE_VERSIONS, MERGE_WORKFLOW_REMEDY_COMMAND, mergeWorkflowVersionSupportsChildMerge, } from "./merge-execution-contract.js";
34
+ import { MERGE_WORKFLOW_CHILD_MERGE_COMPATIBLE_VERSIONS, MERGE_WORKFLOW_CODEOWNER_INSTRUCTION, MERGE_WORKFLOW_REMEDY_COMMAND, mergeWorkflowVersionSupportsChildMerge, } from "./merge-execution-contract.js";
35
35
  /**
36
36
  * Remediation for a verdict that could not be established.
37
37
  *
@@ -51,7 +51,8 @@ export const MERGE_EXECUTION_UNVERIFIABLE_REMEDIATION = "re-run once the Bridge
51
51
  * second way.
52
52
  */
53
53
  export const MERGE_EXECUTION_INELIGIBLE_REMEDIATION = `run \`${MERGE_WORKFLOW_REMEDY_COMMAND}\` to generate the merge workflow and open its ` +
54
- "pull request, then re-run once it is merged to the default branch.";
54
+ `pull request. ${MERGE_WORKFLOW_CODEOWNER_INSTRUCTION} ` +
55
+ "Then re-run once it is merged to the default branch.";
55
56
  function unverifiable(reason, detail) {
56
57
  return {
57
58
  status: "could_not_verify",
@@ -531,7 +531,7 @@ export async function observePrCiFromPollResponse(commitRef, pollResponse, deps
531
531
  function extractTicketKeyFromRef(headRef) {
532
532
  if (!headRef)
533
533
  return null;
534
- const match = /([A-Z][A-Z0-9]+-\d+)/i.exec(headRef);
534
+ const match = /([A-Z][A-Z0-9_]{0,19}-\d+)/i.exec(headRef);
535
535
  return match ? match[1].toUpperCase() : null;
536
536
  }
537
537
  /**
@@ -39,6 +39,8 @@ import { readinessEndpointEnvironments, resolveConductorBridgeApiAccess, resolve
39
39
  import { validateHttpBaseUrl } from "../base-url.js";
40
40
  import { claudeReviewWorkflowPath } from "../claude-review-workflow.js";
41
41
  import { runDefaultPlanePreflight } from "../plane/cli.js";
42
+ import { planeNotApplicableForTarget } from "../plane/endpoint.js";
43
+ import { resolvePlaneServerEndpoint } from "../plane/member-roster.js";
42
44
  /** Fixed notice emitted to stderr when a source could not be collected. */
43
45
  export const READINESS_COLLECTION_NOTICE = "one or more readiness sources could not be collected; the affected prerequisites are reported as failures.";
44
46
  /** Usage text for `epic-implementer readiness`. */
@@ -101,10 +103,18 @@ async function collectProductionReport(repoRoot, env, baseUrl) {
101
103
  const accessResult = baseUrl === undefined
102
104
  ? await resolveConductorBridgeApiAccess({ env: reportEnv, cwd: repoRoot })
103
105
  : await resolveEpicLaunchBridgeApiAccess({ baseUrl }, { env: resolverEnv, cwd: repoRoot });
106
+ // BAPI-1360: the plane checks apply only when the selected access target names
107
+ // the local plane. Resolved from the SAME env the plane preflight reads, through
108
+ // the shared leaf predicate `drive-epic` uses, and never guessed: a target that
109
+ // is remote, or that cannot be resolved at all, is an explicit skip.
110
+ const planeNotApplicable = planeNotApplicableForTarget(resolvePlaneServerEndpoint(reportEnv), accessResult.ok ? accessResult.access.baseUrl : null);
104
111
  if (baseUrl !== undefined && !accessResult.ok) {
105
112
  // A refused override is never permission for a downstream doctor collector
106
113
  // to resolve credentials afresh from the general CLI environment.
107
- return collectConductorReadinessGate({ repoName: null });
114
+ return collectConductorReadinessGate({
115
+ repoName: null,
116
+ ...(planeNotApplicable !== null ? { planeNotApplicable } : {}),
117
+ });
108
118
  }
109
119
  const access = accessResult.ok ? accessResult.access : null;
110
120
  return collectConductorReadinessGate({
@@ -134,7 +144,13 @@ async function collectProductionReport(repoRoot, env, baseUrl) {
134
144
  // The deny-enforcement probe is never spawned from this command; the check
135
145
  // is reported as an explicit skip naming `epic-implementer doctor` instead.
136
146
  conductorDoctorDeps: { env: reportEnv, skipDenyProbe: true },
137
- runPlanePreflight: () => runDefaultPlanePreflight(repoRoot, reportEnv),
147
+ // Provided ONLY for a target that names the local plane; otherwise the rows
148
+ // are skipped without any probe.
149
+ ...(planeNotApplicable === null
150
+ ? {
151
+ runPlanePreflight: () => runDefaultPlanePreflight(repoRoot, reportEnv),
152
+ }
153
+ : { planeNotApplicable }),
138
154
  });
139
155
  }
140
156
  /**
@@ -33,6 +33,7 @@
33
33
  * The one exception is {@link inspectWorkflowPresence}, which awaits the
34
34
  * READ-ONLY reader seams its caller injects and never opens a path of its own.
35
35
  */
36
+ import { PRINTED_CONNECT_GITHUB_COMMAND } from "../printed-remedy-commands.js";
36
37
  import { MANAGED_HOOK_NAMES } from "./git-hooks.js";
37
38
  import { MCP_PACKAGE_NAME } from "../mcp-identity.js";
38
39
  import { CLAUDE_REVIEW_WORKFLOW_DRIFT_REMEDIATION, summarizeClaudeReviewWorkflowDrift, } from "../claude-review-workflow-drift.js";
@@ -41,7 +42,7 @@ import { classifyMergeExecutionEligibility, MERGE_EXECUTION_UNVERIFIABLE_REMEDIA
41
42
  import { MERGE_WORKFLOW_CHILD_MERGE_COMPATIBLE_VERSIONS, MERGE_WORKFLOW_PATH, mergeWorkflowVersionSupportsChildMerge, } from "./merge-execution-contract.js";
42
43
  import { classifyDatabaseMigration, classifySingleTransitionAuthority, classifyWorkerPresent, } from "./bring-up-facts.js";
43
44
  /** Operator runbook pointer surfaced beside the executor gap. */
44
- export const CONDUCTOR_OPERATOR_RUNBOOK_POINTER = "docs/claude/epic-implementer-v2-operator-runbook.md";
45
+ export const CONDUCTOR_OPERATOR_RUNBOOK_POINTER = "docs/epic-implementer/operator-runbook.md";
45
46
  /**
46
47
  * Capability-matrix guidance for the executor leg.
47
48
  *
@@ -105,6 +106,22 @@ export const REQUIRED_CHECKS_PRESENTATION_COPY = {
105
106
  remediation: "restore required-check verification (a readable default branch with static lane " +
106
107
  "workflows and a current Bridge API), or correct the required-check configuration.",
107
108
  },
109
+ // BAPI-1360 — file- and check-specific copy for the two proven, repository-admin
110
+ // causes of `unverifiable`. Fixed strings over the two hard-coded workflow paths
111
+ // and the two gate names; nothing from the payload is interpolated.
112
+ retiredGate: {
113
+ detail: "Could not verify — a configured required check is the retired gate " +
114
+ `conductor-ci / gate; ${REQUIRED_CHECKS_OPERATOR_HOLD}`,
115
+ remediation: "a repository administrator replaces conductor-ci / gate with epic-implementer-ci / gate in the " +
116
+ "plan's required_checks, the supervisor done-gate checks, and branch protection.",
117
+ },
118
+ laneAbsent: {
119
+ detail: "Could not verify — a lane workflow file is missing from the default branch, so a configured " +
120
+ `required check has no producer; ${REQUIRED_CHECKS_OPERATOR_HOLD}`,
121
+ remediation: "a repository administrator adds the missing lane workflow file(s) — " +
122
+ ".github/workflows/epic-implementer-ci.yml and/or .github/workflows/claude-review.yml — " +
123
+ "to the default branch so the configured required checks (such as epic-implementer-ci / gate) have a producer.",
124
+ },
108
125
  empty: {
109
126
  detail: "Could not verify — the required-check list is EMPTY, which is unsafe: the done gate " +
110
127
  `would pass unconditionally; ${REQUIRED_CHECKS_OPERATOR_HOLD}`,
@@ -155,6 +172,24 @@ export function presentRequiredChecks(supervisor) {
155
172
  ...REQUIRED_CHECKS_PRESENTATION_COPY.unknown_name,
156
173
  };
157
174
  }
175
+ // BAPI-1360 — optional bounded facts refine the generic unverifiable copy. An
176
+ // old server reports neither, and keeps the wording above. Retired gate ranks
177
+ // first, then a proven-absent lane; anything else stays generic.
178
+ if (supervisor.required_checks_retired_gate_configured === true) {
179
+ return {
180
+ outcome: "unverifiable",
181
+ satisfied: false,
182
+ ...REQUIRED_CHECKS_PRESENTATION_COPY.retiredGate,
183
+ };
184
+ }
185
+ if (supervisor.required_checks_ci_workflow_absent === true ||
186
+ supervisor.required_checks_review_workflow_absent === true) {
187
+ return {
188
+ outcome: "unverifiable",
189
+ satisfied: false,
190
+ ...REQUIRED_CHECKS_PRESENTATION_COPY.laneAbsent,
191
+ };
192
+ }
158
193
  return {
159
194
  outcome: "unverifiable",
160
195
  satisfied: false,
@@ -268,7 +303,7 @@ export function githubCredentialsSection(readiness) {
268
303
  detail: g.credentials_readable
269
304
  ? `incomplete: ${missing} unresolved`
270
305
  : "no GitHub credential row for this repository",
271
- remediation: "connect GitHub from the Bridge setup UI (`install-bridge connect-github`).",
306
+ remediation: `connect GitHub from the Bridge setup UI or run \`${PRINTED_CONNECT_GITHUB_COMMAND}\`.`,
272
307
  };
273
308
  }
274
309
  export function githubActionsSection(readiness) {
@@ -396,7 +431,7 @@ export function reconcilerSection(readiness) {
396
431
  // remediation at all — so a report could name a gap with no next step.
397
432
  return {
398
433
  id: "reconciler-liveness",
399
- label: "Reconciler tick liveness",
434
+ label: "Active-run progress",
400
435
  status: "degraded",
401
436
  detail: "liveness could not be read",
402
437
  remediation: "retry once the Bridge API is reachable; reconciler liveness is unknown here, " +
@@ -406,19 +441,19 @@ export function reconcilerSection(readiness) {
406
441
  if (!r.stale) {
407
442
  return {
408
443
  id: "reconciler-liveness",
409
- label: "Reconciler tick liveness",
444
+ label: "Active-run progress",
410
445
  status: "ok",
411
446
  detail: `last tick ${r.last_tick_age_seconds}s ago across ${r.active_run_count} active run(s)`,
412
447
  };
413
448
  }
414
449
  return {
415
450
  id: "reconciler-liveness",
416
- label: "Reconciler tick liveness",
451
+ label: "Active-run progress",
417
452
  status: "degraded",
418
453
  detail: r.last_tick_at === null
419
454
  ? "no reconciler tick recorded for this repository"
420
455
  : `last tick ${r.last_tick_age_seconds}s ago (threshold ${readiness.thresholds.reconciler_stale_after_seconds}s)`,
421
- remediation: "start the reconciler (`epic-implementer epic-tick` schedule) for this repository.",
456
+ remediation: "check the worker first (`server.worker-present`), then inspect the stalled run with `epic-implementer epic-status`.",
422
457
  };
423
458
  }
424
459
  // ---------------------------------------------------------------------------