@bridge_gpt/mcp-server 0.2.54 → 0.2.55

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 (81) hide show
  1. package/README.md +38 -54
  2. package/build/agent-guidance.generated.js +3 -0
  3. package/build/agent-launchers/claude-executor-adapter.js +3 -0
  4. package/build/agent-notes.js +178 -0
  5. package/build/agent-registry.js +5 -2
  6. package/build/agent-utils.js +58 -0
  7. package/build/agents.generated.js +1 -1
  8. package/build/codex-skill-adapter.js +55 -0
  9. package/build/commands.generated.js +5 -4
  10. package/build/conductor/bridge-api-client.js +199 -6
  11. package/build/conductor/bring-up-facts.js +187 -0
  12. package/build/conductor/claude-hook.js +7 -5
  13. package/build/conductor/cli.js +28 -0
  14. package/build/conductor/doctor.js +80 -9
  15. package/build/conductor/epic-implementer-cli.js +1298 -0
  16. package/build/conductor/epic-runtime.js +1 -1
  17. package/build/conductor/errors.js +2 -2
  18. package/build/conductor/git-ci-types.js +1 -1
  19. package/build/conductor/git-hooks.js +28 -14
  20. package/build/conductor/install-doctor.js +11 -5
  21. package/build/conductor/readiness-cli.js +10 -10
  22. package/build/conductor/readiness-sections.js +58 -9
  23. package/build/conductor/readiness.js +37 -6
  24. package/build/conductor/recovery-cli.js +289 -10
  25. package/build/conductor/recovery-operations.js +125 -2
  26. package/build/conductor/repair-contract.js +58 -0
  27. package/build/conductor/store.js +2 -2
  28. package/build/conductor/supervisor-runtime.js +1 -1
  29. package/build/conductor-bin.js +2 -139
  30. package/build/conductor-claude-hook-bin.js +2 -2
  31. package/build/conductor-claude-hook-removed-stub-bin.js +31 -0
  32. package/build/conductor-removed-stub-bin.js +30 -0
  33. package/build/docs.generated.js +1 -1
  34. package/build/doctor.js +77 -17
  35. package/build/drive-epic.js +541 -115
  36. package/build/epic-implementer-bin.js +145 -0
  37. package/build/epic-implementer-bundle-cli.js +264 -0
  38. package/build/epic-implementer-claude-hook-bin.js +3 -0
  39. package/build/epic-integration-pr.js +5 -3
  40. package/build/executor/env.js +6 -0
  41. package/build/executor/interrupted-worktree.js +60 -0
  42. package/build/executor/job-errors.js +45 -0
  43. package/build/executor/job-runner.js +274 -9
  44. package/build/executor/job-types.js +25 -9
  45. package/build/executor/merge-tree-classifier.js +171 -0
  46. package/build/executor/reconcile-overlap-governance.js +129 -0
  47. package/build/executor/reconcile-overlap-job.js +989 -0
  48. package/build/executor/reconcile-overlap-types.js +14 -0
  49. package/build/executor/spawn-job-driver.js +1 -0
  50. package/build/executor/types.js +2 -0
  51. package/build/executor/worker-finalization.js +25 -2
  52. package/build/executor/worker-guard-hook.js +15 -7
  53. package/build/implement-epic/bridge-client.js +773 -0
  54. package/build/implement-epic/checkpoint-store.js +542 -0
  55. package/build/implement-epic/cli.js +3158 -0
  56. package/build/implement-epic/cut-protocol.js +392 -0
  57. package/build/implement-epic/lock.js +302 -0
  58. package/build/implement-epic/pr-state.js +286 -0
  59. package/build/implement-epic/spawn.js +113 -0
  60. package/build/index.js +586 -138
  61. package/build/init.js +72 -8
  62. package/build/install-bridge-conductor.js +5 -5
  63. package/build/install-bridge.js +403 -70
  64. package/build/mcp-host-config.js +22 -60
  65. package/build/mcp-host-entry-adapter.js +18 -0
  66. package/build/mcp-host-targets.js +1 -21
  67. package/build/merge-pull-request.js +1 -1
  68. package/build/pipelines.generated.js +7 -7
  69. package/build/plan-epic-conductor-eligibility.js +1 -1
  70. package/build/plane/cli.js +36 -5
  71. package/build/plane/preflight.js +128 -12
  72. package/build/plane/shutdown.js +4 -4
  73. package/build/readiness-check.js +3 -3
  74. package/build/readme.generated.js +1 -1
  75. package/build/run-unit-tests-launcher.js +1 -1
  76. package/build/setup-epic.js +69 -31
  77. package/build/start-tickets-conductor.js +8 -7
  78. package/build/version.generated.js +3 -3
  79. package/build/worker-guard-hook-bin.js +1 -1
  80. package/docs/CONDUCTOR.md +8 -6
  81. package/package.json +5 -3
@@ -258,9 +258,26 @@ function redactDiagnosticValues(diagnostics, secrets) {
258
258
  * values in `headers` (e.g. `X-API-Key`) are additionally redacted from the
259
259
  * preview so a body that echoes the credential can never surface it.
260
260
  */
261
- async function readSanitizedErrorDiagnostics(resp, headers = {}) {
261
+ async function readSanitizedErrorDiagnostics(resp, headers = {}, options = {}) {
262
262
  try {
263
- const diagnostics = extractSanitizedErrorDiagnostics(stripWebhookUrlsDeep(await resp.json()));
263
+ const parsed = stripWebhookUrlsDeep(await resp.json());
264
+ const diagnostics = extractSanitizedErrorDiagnostics(parsed);
265
+ if (options.readErrorField && !diagnostics.bodyPreview) {
266
+ // BAPI-1154: opt-in only. The route layer's `_http_error` envelope carries
267
+ // its server-authored explanation in `detail.error`, which the shared
268
+ // extractor deliberately does not read (pinned by its own tests). The
269
+ // repair verbs render that explanation to the operator who asked, so they
270
+ // opt in; every other caller's diagnostics are unchanged.
271
+ const detail = parsed && typeof parsed === "object" && !Array.isArray(parsed)
272
+ ? parsed["detail"]
273
+ : undefined;
274
+ const error = detail && typeof detail === "object" && !Array.isArray(detail)
275
+ ? detail["error"]
276
+ : undefined;
277
+ if (typeof error === "string" && error.trim()) {
278
+ diagnostics.bodyPreview = boundedErrorPreview(error.trim());
279
+ }
280
+ }
264
281
  const secrets = Object.entries(headers)
265
282
  .filter(([k]) => /key|authorization|token/i.test(k))
266
283
  .map(([, v]) => v);
@@ -503,8 +520,8 @@ function conductorPostHeaders(access) {
503
520
  * request body in the error. The timer is always cleared. Shared core for the
504
521
  * POST and PATCH wrappers below.
505
522
  */
506
- async function fetchConductorJsonWithMethodAndTimeout(method, url, headers, body, timeoutMs, fetchImpl) {
507
- const { body: parsed } = await fetchConductorJsonWithMethodStatusAndTimeout(method, url, headers, body, timeoutMs, fetchImpl);
523
+ async function fetchConductorJsonWithMethodAndTimeout(method, url, headers, body, timeoutMs, fetchImpl, errorOptions = {}) {
524
+ const { body: parsed } = await fetchConductorJsonWithMethodStatusAndTimeout(method, url, headers, body, timeoutMs, fetchImpl, errorOptions);
508
525
  return parsed;
509
526
  }
510
527
  /**
@@ -518,7 +535,7 @@ async function fetchConductorJsonWithMethodAndTimeout(method, url, headers, body
518
535
  * that only sees the body has to issue another request to learn which happened.
519
536
  * Success statuses only; every non-2xx path still throws exactly as before.
520
537
  */
521
- async function fetchConductorJsonWithMethodStatusAndTimeout(method, url, headers, body, timeoutMs, fetchImpl) {
538
+ async function fetchConductorJsonWithMethodStatusAndTimeout(method, url, headers, body, timeoutMs, fetchImpl, errorOptions = {}) {
522
539
  const controller = new AbortController();
523
540
  const timer = setTimeout(() => controller.abort(), timeoutMs);
524
541
  try {
@@ -530,7 +547,7 @@ async function fetchConductorJsonWithMethodStatusAndTimeout(method, url, headers
530
547
  throw new ConductorBridgeApiError(controller.signal.aborted ? "timeout" : "network");
531
548
  }
532
549
  if (!resp.ok) {
533
- const diagnostics = await readSanitizedErrorDiagnostics(resp, headers);
550
+ const diagnostics = await readSanitizedErrorDiagnostics(resp, headers, errorOptions);
534
551
  if (resp.status === 401 || resp.status === 403) {
535
552
  throw new ConductorBridgeApiError("unauthorized", resp.status, diagnostics);
536
553
  }
@@ -1165,6 +1182,155 @@ export async function adoptCurrentHeadAndUnparkTicket(access, request, fetchImpl
1165
1182
  });
1166
1183
  return postUnparkLikeRequest(url, conductorPostHeaders(access), body, fetchImpl);
1167
1184
  }
1185
+ // ---------------------------------------------------------------------------
1186
+ // BAPI-1154 — run-addressed operator repair verbs (CLI-only; no MCP tool)
1187
+ // ---------------------------------------------------------------------------
1188
+ // Each client sends only its documented body and returns the server's bounded,
1189
+ // authoritative result. None accepts or transmits a head SHA, gate identity,
1190
+ // action key, branch, provider credential, actor, row version, or idempotency
1191
+ // receipt: ownership, identity, and state are the SERVER's to decide. A named
1192
+ // refusal surfaces as a thrown `ConductorBridgeApiError` carrying the server's
1193
+ // `errorCode` (e.g. `TICKET_NOT_IN_RUN`), which the CLI renders by code.
1194
+ function parseRepairNullableString(value) {
1195
+ return typeof value === "string" ? value : null;
1196
+ }
1197
+ function parseRepairBase(parsed) {
1198
+ if (!parsed || typeof parsed !== "object" || Array.isArray(parsed)) {
1199
+ throw new ConductorBridgeApiError("server");
1200
+ }
1201
+ const p = parsed;
1202
+ if (typeof p["epic_run_id"] !== "string" ||
1203
+ typeof p["committed"] !== "boolean" ||
1204
+ typeof p["event_recorded"] !== "boolean") {
1205
+ throw new ConductorBridgeApiError("server");
1206
+ }
1207
+ return p;
1208
+ }
1209
+ function parseScopeReparseResult(parsed) {
1210
+ const p = parseRepairBase(parsed);
1211
+ if (typeof p["scope_id"] !== "string" || typeof p["parse_scheduled"] !== "boolean") {
1212
+ throw new ConductorBridgeApiError("server");
1213
+ }
1214
+ return {
1215
+ epic_run_id: p["epic_run_id"],
1216
+ scope_id: p["scope_id"],
1217
+ before: parseRepairNullableString(p["before"]),
1218
+ after: parseRepairNullableString(p["after"]),
1219
+ committed: p["committed"],
1220
+ event_recorded: p["event_recorded"],
1221
+ outcome: typeof p["outcome"] === "string" ? p["outcome"] : "",
1222
+ parse_scheduled: p["parse_scheduled"],
1223
+ };
1224
+ }
1225
+ function parseDispatchRemediationResult(parsed) {
1226
+ const p = parseRepairBase(parsed);
1227
+ if (typeof p["ticket_key"] !== "string") {
1228
+ throw new ConductorBridgeApiError("server");
1229
+ }
1230
+ const jobId = p["job_id"];
1231
+ return {
1232
+ epic_run_id: p["epic_run_id"],
1233
+ ticket_key: p["ticket_key"],
1234
+ before: parseRepairNullableString(p["before"]),
1235
+ after: parseRepairNullableString(p["after"]),
1236
+ committed: p["committed"],
1237
+ event_recorded: p["event_recorded"],
1238
+ job_type: typeof p["job_type"] === "string" ? p["job_type"] : "",
1239
+ job_id: typeof jobId === "number" && Number.isSafeInteger(jobId) ? jobId : null,
1240
+ };
1241
+ }
1242
+ function parseMergeChildRouteResponse(parsed) {
1243
+ const p = parseRepairBase(parsed);
1244
+ const prNumber = p["pr_number"];
1245
+ if (typeof prNumber !== "number" || !Number.isSafeInteger(prNumber) || prNumber <= 0) {
1246
+ throw new ConductorBridgeApiError("server");
1247
+ }
1248
+ let localExecution = null;
1249
+ const local = p["local_execution"];
1250
+ if (local && typeof local === "object" && !Array.isArray(local)) {
1251
+ const l = local;
1252
+ const sha = normalizeSha(l["expected_head_sha"]);
1253
+ if (sha === null) {
1254
+ // An approval that names no usable head cannot be executed safely.
1255
+ throw new ConductorBridgeApiError("server");
1256
+ }
1257
+ localExecution = {
1258
+ expected_head_sha: sha,
1259
+ merge_method: parseRepairNullableString(l["merge_method"]),
1260
+ };
1261
+ }
1262
+ return {
1263
+ epic_run_id: p["epic_run_id"],
1264
+ pr_number: prNumber,
1265
+ ticket_key: typeof p["ticket_key"] === "string" ? p["ticket_key"] : "",
1266
+ before: parseRepairNullableString(p["before"]),
1267
+ after: parseRepairNullableString(p["after"]),
1268
+ committed: p["committed"],
1269
+ event_recorded: p["event_recorded"],
1270
+ reason: parseRepairNullableString(p["reason"]),
1271
+ local_execution: localExecution,
1272
+ };
1273
+ }
1274
+ async function postRepairRequest(access, apiPath, body, fetchImpl) {
1275
+ return fetchConductorJsonWithMethodAndTimeout("POST", buildConductorJiraUrl(access.baseUrl, apiPath), conductorPostHeaders(access), JSON.stringify({ repo_name: access.repoName, ...body }), CONDUCTOR_FETCH_TIMEOUT_MS, fetchImpl,
1276
+ // The server's refusal explanation is the operator-facing message here.
1277
+ { readErrorField: true });
1278
+ }
1279
+ /**
1280
+ * POST `/jira/epic-runs/runs/{epic_run_id}/scope-reparse` — re-drive the run's
1281
+ * OWN index scope through the retry-reparse lifecycle. Body: `{repo_name,
1282
+ * scope_id}`. The server refuses a scope not bound to the run (`404
1283
+ * TICKET_NOT_IN_RUN`) with nothing written.
1284
+ */
1285
+ export async function scopeReparse(access, request, fetchImpl = globalThis.fetch) {
1286
+ requireNonEmptyString(request.epicRunId);
1287
+ requireNonEmptyString(request.scopeId);
1288
+ const parsed = await postRepairRequest(access, `${epicRunApiPath(request.epicRunId)}/scope-reparse`, { scope_id: request.scopeId }, fetchImpl);
1289
+ return parseScopeReparseResult(parsed);
1290
+ }
1291
+ /**
1292
+ * POST `/jira/epic-runs/runs/{epic_run_id}/tickets/{ticket_key}/dispatch-remediation`
1293
+ * — body `{repo_name, context}`. The SERVER selects `remediate` or
1294
+ * `reconcile_overlap` and admits the job through the reconciler's own sequence;
1295
+ * this client never names a job type.
1296
+ */
1297
+ export async function dispatchRemediation(access, request, fetchImpl = globalThis.fetch) {
1298
+ requireNonEmptyString(request.epicRunId);
1299
+ requireNonEmptyString(request.ticketKey);
1300
+ requireNonEmptyString(request.context);
1301
+ const parsed = await postRepairRequest(access, `${epicRunApiPath(request.epicRunId)}/tickets/${encodeURIComponent(request.ticketKey)}/dispatch-remediation`, { context: request.context }, fetchImpl);
1302
+ return parseDispatchRemediationResult(parsed);
1303
+ }
1304
+ /**
1305
+ * POST `/jira/epic-runs/runs/{epic_run_id}/merge-child` — body EXACTLY
1306
+ * `{repo_name, pr_number}`. The server composes the head, gate, and action key.
1307
+ * When the repository merges locally the response carries a `local_execution`
1308
+ * approval the caller completes with {@link completeMergeChild}.
1309
+ */
1310
+ export async function mergeChild(access, request, fetchImpl = globalThis.fetch) {
1311
+ requireNonEmptyString(request.epicRunId);
1312
+ requirePositiveSafeInteger(request.prNumber);
1313
+ const parsed = await postRepairRequest(access, `${epicRunApiPath(request.epicRunId)}/merge-child`, { pr_number: request.prNumber }, fetchImpl);
1314
+ return parseMergeChildRouteResponse(parsed);
1315
+ }
1316
+ /**
1317
+ * POST `/jira/epic-runs/runs/{epic_run_id}/merge-child/complete` — body
1318
+ * `{repo_name, pr_number, result, reason?}`. No SHA and no action key: the server
1319
+ * recomposes the merge identity and reads the merge commit from the provider.
1320
+ */
1321
+ export async function completeMergeChild(access, request, fetchImpl = globalThis.fetch) {
1322
+ requireNonEmptyString(request.epicRunId);
1323
+ requirePositiveSafeInteger(request.prNumber);
1324
+ if (!["merged", "already_merged", "failed"].includes(request.result)) {
1325
+ throw new ConductorBridgeApiError("invalid-input");
1326
+ }
1327
+ const parsed = await postRepairRequest(access, `${epicRunApiPath(request.epicRunId)}/merge-child/complete`, {
1328
+ pr_number: request.prNumber,
1329
+ result: request.result,
1330
+ ...(request.reason !== undefined ? { reason: request.reason } : {}),
1331
+ }, fetchImpl);
1332
+ return parseMergeChildRouteResponse(parsed);
1333
+ }
1168
1334
  /**
1169
1335
  * Idempotently seed an epic ticket status row via POST to the per-epic tickets
1170
1336
  * endpoint. The backend uses ON CONFLICT DO NOTHING so repeated seeding across
@@ -1770,6 +1936,8 @@ export const CONDUCTOR_REVIEW_ALIGNMENT_STATUSES = new Set([
1770
1936
  "not_configured",
1771
1937
  "invalid",
1772
1938
  ]);
1939
+ /** Upper bound of the server's capped `fresh_reconciler_count` ("two or more"). */
1940
+ export const READINESS_FRESH_RECONCILER_COUNT_CAP = 2;
1773
1941
  const READINESS_SOURCES = new Set(["epic", "project_default", "none"]);
1774
1942
  const ACTIONS_LEVELS = new Set(["write", "read", "none", "unknown"]);
1775
1943
  function readinessShapeError() {
@@ -1922,6 +2090,31 @@ export function parseConductorReadinessResponse(body) {
1922
2090
  review_workflow: parseReviewWorkflow(root),
1923
2091
  conductor_ci_workflow: parseConductorCiWorkflow(root),
1924
2092
  unattended: parseUnattended(root),
2093
+ bring_up: parseBringUp(root),
2094
+ };
2095
+ }
2096
+ /**
2097
+ * Parse the optional `bring_up` block (BAPI-1153). Same older-server rule as the
2098
+ * blocks above: absent (or null) is `null`; a PRESENT block is validated
2099
+ * strictly, every field required, and the capped count range-checked. A
2100
+ * malformed block throws the same field-free shape error as everything else.
2101
+ */
2102
+ function parseBringUp(body) {
2103
+ const raw = body.bring_up;
2104
+ if (raw === undefined || raw === null)
2105
+ return null;
2106
+ const o = requireObject(raw);
2107
+ const freshCount = requireInt(o, "fresh_reconciler_count");
2108
+ if (freshCount < 0 || freshCount > READINESS_FRESH_RECONCILER_COUNT_CAP) {
2109
+ throw readinessShapeError();
2110
+ }
2111
+ return {
2112
+ migration_readable: requireBool(o, "migration_readable"),
2113
+ database_migration_current: requireBool(o, "database_migration_current"),
2114
+ heartbeat_readable: requireBool(o, "heartbeat_readable"),
2115
+ worker_present: requireBool(o, "worker_present"),
2116
+ fresh_reconciler_count: freshCount,
2117
+ single_transition_authority: requireBool(o, "single_transition_authority"),
1925
2118
  };
1926
2119
  }
1927
2120
  /**
@@ -0,0 +1,187 @@
1
+ /** The pinned operator runbook every remedy below points into. */
2
+ export const BRING_UP_RUNBOOK = "docs/claude/epic-conductor-v2-operator-runbook.md";
3
+ /**
4
+ * The client-derived refusal vocabulary, IN REFUSAL ORDER.
5
+ *
6
+ * Kebab case, because these are computed by the client (the server reports
7
+ * facts, not refusals) and `second-reconciler-detected` is the mandated name;
8
+ * the snake-case unattended reasons mirror SERVER codes and are a separate
9
+ * vocabulary. Each fact's "unreadable" precedes its "bad", so an operator is
10
+ * never told to repair a condition that could not even be read.
11
+ */
12
+ export const BRING_UP_REFUSAL_REASONS = [
13
+ "bring-up-facts-unreported",
14
+ "database-migration-unreadable",
15
+ "database-migration-behind",
16
+ "worker-heartbeat-unreadable",
17
+ "worker-not-present",
18
+ "second-reconciler-detected",
19
+ ];
20
+ // ---------------------------------------------------------------------------
21
+ // Fixed remediations
22
+ // ---------------------------------------------------------------------------
23
+ export const BRING_UP_UNREPORTED_REMEDIATION = "upgrade the Bridge API deploy to a build that reports the bring-up safety facts; " +
24
+ "their state is unknown here, not healthy.";
25
+ export const DATABASE_MIGRATION_UNREADABLE_REMEDIATION = "retry; the migration state could not be read, which is not the same as confirmed-behind. " +
26
+ `If it persists, check the applied revision with \`alembic current\` (${BRING_UP_RUNBOOK} §3d).`;
27
+ export const DATABASE_MIGRATION_BEHIND_REMEDIATION = "confirm the applied revision with `alembic current` (not `alembic heads`, which reads the " +
28
+ "migration scripts), apply this checkout's migrations with `alembic -c alembic.ini upgrade head`, " +
29
+ `then retry (${BRING_UP_RUNBOOK} §3d).`;
30
+ export const WORKER_HEARTBEAT_UNREADABLE_REMEDIATION = "retry once the Bridge API database is reachable; the reconciler heartbeat could not be read, " +
31
+ "so worker presence is unknown, not confirmed absent.";
32
+ export const WORKER_NOT_PRESENT_REMEDIATION = "start a worker (`python worker.py`, or bring up a local plane) — a server started with " +
33
+ "DISABLE_SCHEDULER=true runs none. Create and approve only against a server that has a " +
34
+ `worker (${BRING_UP_RUNBOOK} §3d).`;
35
+ export const SECOND_RECONCILER_REMEDIATION = "find and stop the extra reconciler: check `ps aux | grep worker.py` for a reparented " +
36
+ `survivor of an earlier plane (${BRING_UP_RUNBOOK} §6), and note that a dev server started ` +
37
+ "without DISABLE_SCHEDULER=true runs a reconciler too. A reconciler stopped in the last 120 " +
38
+ "seconds still counts — heartbeat rows are not deleted on shutdown — so wait and retry.";
39
+ // ---------------------------------------------------------------------------
40
+ // Classification
41
+ // ---------------------------------------------------------------------------
42
+ const UNREPORTED_DETAIL = "not reported by this Bridge API deploy";
43
+ function unreported(id) {
44
+ return {
45
+ id,
46
+ state: "unreported",
47
+ detail: UNREPORTED_DETAIL,
48
+ remediation: BRING_UP_UNREPORTED_REMEDIATION,
49
+ refusal: "bring-up-facts-unreported",
50
+ };
51
+ }
52
+ /** Database migration head: behind refuses; ahead is the intended model. */
53
+ export function classifyDatabaseMigration(bringUp) {
54
+ const id = "database-migration-current";
55
+ if (bringUp === null)
56
+ return unreported(id);
57
+ if (!bringUp.migration_readable) {
58
+ return {
59
+ id,
60
+ state: "unreadable",
61
+ detail: "the migration state could not be read (unknown, not confirmed behind)",
62
+ remediation: DATABASE_MIGRATION_UNREADABLE_REMEDIATION,
63
+ refusal: "database-migration-unreadable",
64
+ };
65
+ }
66
+ if (!bringUp.database_migration_current) {
67
+ return {
68
+ id,
69
+ state: "bad",
70
+ detail: "the database is BEHIND the server's code migration head",
71
+ remediation: DATABASE_MIGRATION_BEHIND_REMEDIATION,
72
+ refusal: "database-migration-behind",
73
+ };
74
+ }
75
+ return { id, state: "ok", detail: "the database is at (or ahead of) the code migration head" };
76
+ }
77
+ /** Worker presence, from the durable reconciler heartbeat. */
78
+ export function classifyWorkerPresent(bringUp) {
79
+ const id = "worker-present";
80
+ if (bringUp === null)
81
+ return unreported(id);
82
+ if (!bringUp.heartbeat_readable) {
83
+ return {
84
+ id,
85
+ state: "unreadable",
86
+ detail: "the reconciler heartbeat could not be read (unknown, not confirmed absent)",
87
+ remediation: WORKER_HEARTBEAT_UNREADABLE_REMEDIATION,
88
+ refusal: "worker-heartbeat-unreadable",
89
+ };
90
+ }
91
+ if (!bringUp.worker_present) {
92
+ return {
93
+ id,
94
+ state: "bad",
95
+ detail: "0 fresh reconcilers — no worker is running against this deployment",
96
+ remediation: WORKER_NOT_PRESENT_REMEDIATION,
97
+ refusal: "worker-not-present",
98
+ };
99
+ }
100
+ return { id, state: "ok", detail: "a reconciler heartbeat is fresh" };
101
+ }
102
+ /**
103
+ * Single transition authority. Refuses only on TWO OR MORE fresh reconcilers;
104
+ * zero is the worker fact's finding and is not repeated here as a refusal.
105
+ */
106
+ export function classifySingleTransitionAuthority(bringUp) {
107
+ const id = "single-transition-authority";
108
+ if (bringUp === null)
109
+ return unreported(id);
110
+ if (!bringUp.heartbeat_readable) {
111
+ return {
112
+ id,
113
+ state: "unreadable",
114
+ detail: "the reconciler heartbeat could not be read (unknown, not confirmed)",
115
+ remediation: WORKER_HEARTBEAT_UNREADABLE_REMEDIATION,
116
+ refusal: "worker-heartbeat-unreadable",
117
+ };
118
+ }
119
+ if (bringUp.fresh_reconciler_count >= 2) {
120
+ return {
121
+ id,
122
+ state: "bad",
123
+ detail: "2+ fresh reconcilers — more than one transition authority is ticking",
124
+ remediation: SECOND_RECONCILER_REMEDIATION,
125
+ refusal: "second-reconciler-detected",
126
+ };
127
+ }
128
+ if (bringUp.fresh_reconciler_count === 0) {
129
+ return {
130
+ id,
131
+ state: "bad",
132
+ detail: "0 fresh reconcilers — no transition authority is ticking",
133
+ remediation: WORKER_NOT_PRESENT_REMEDIATION,
134
+ refusal: "worker-not-present",
135
+ };
136
+ }
137
+ return { id, state: "ok", detail: "exactly 1 fresh reconciler" };
138
+ }
139
+ /** All three verdicts, in render order. */
140
+ export function classifyBringUpFacts(bringUp) {
141
+ return [
142
+ classifyDatabaseMigration(bringUp),
143
+ classifyWorkerPresent(bringUp),
144
+ classifySingleTransitionAuthority(bringUp),
145
+ ];
146
+ }
147
+ /**
148
+ * The FIRST refusal the block produces, in {@link BRING_UP_REFUSAL_REASONS}
149
+ * order, or `null` when all three facts hold.
150
+ *
151
+ * Order comes from the vocabulary constant, never from the order the verdicts
152
+ * happened to be computed, so a given combination of failures always names the
153
+ * same reason.
154
+ */
155
+ export function firstBringUpRefusal(bringUp) {
156
+ const reasons = new Set(classifyBringUpFacts(bringUp)
157
+ .map((verdict) => verdict.refusal)
158
+ .filter((reason) => reason !== undefined));
159
+ return BRING_UP_REFUSAL_REASONS.find((reason) => reasons.has(reason)) ?? null;
160
+ }
161
+ /**
162
+ * The operator sentence for one refusal: the fact, the observed condition, and
163
+ * one concrete remedy. The caller appends its own closing sentence ("No run was
164
+ * created."), because only the caller knows what it did not do.
165
+ */
166
+ export const BRING_UP_REFUSAL_MESSAGES = {
167
+ "bring-up-facts-unreported": "the Bridge API did not report the bring-up safety facts (database migration, worker " +
168
+ "presence, single reconciler), so none of them could be confirmed. " +
169
+ capitalize(BRING_UP_UNREPORTED_REMEDIATION),
170
+ "database-migration-unreadable": "database migration: the server could not read whether its database is at the migration " +
171
+ "head, so this is unknown, not confirmed behind. " +
172
+ capitalize(DATABASE_MIGRATION_UNREADABLE_REMEDIATION),
173
+ "database-migration-behind": "database migration: the server's database is BEHIND its code's migration head, so the " +
174
+ "code would run against a schema that was never applied. " +
175
+ capitalize(DATABASE_MIGRATION_BEHIND_REMEDIATION),
176
+ "worker-heartbeat-unreadable": "worker presence: the durable reconciler heartbeat could not be read, so whether a worker " +
177
+ "is running is unknown, not confirmed absent. " +
178
+ capitalize(WORKER_HEARTBEAT_UNREADABLE_REMEDIATION),
179
+ "worker-not-present": "worker presence: no reconciler heartbeat is fresh, so nothing would drive this run. " +
180
+ capitalize(WORKER_NOT_PRESENT_REMEDIATION),
181
+ "second-reconciler-detected": "single transition authority: two or more reconcilers are ticking the shared database, " +
182
+ "and two transition authorities wedge an epic. " +
183
+ capitalize(SECOND_RECONCILER_REMEDIATION),
184
+ };
185
+ function capitalize(text) {
186
+ return text.length === 0 ? text : text[0].toUpperCase() + text.slice(1);
187
+ }
@@ -175,10 +175,12 @@ export function buildConductorEmitEventArgs(event) {
175
175
  return args;
176
176
  }
177
177
  /**
178
- * Resolve how to invoke `conductor emit-event`. Prefers `BAPI_CONDUCTOR_CLI_FILE`
179
- * (run via the current Node executable so no global install is required),
180
- * otherwise falls back to `BAPI_CONDUCTOR_BIN` or the bare `conductor` binary on
181
- * PATH. `emitArgs` are appended after the `emit-event` subcommand.
178
+ * Resolve how to invoke `epic-implementer emit-event`. Prefers
179
+ * `BAPI_CONDUCTOR_CLI_FILE` (run via the current Node executable so no global
180
+ * install is required), otherwise falls back to `BAPI_CONDUCTOR_BIN` or the
181
+ * bare `epic-implementer` binary on PATH — never the retained `conductor`
182
+ * name, which is now a fixed migration stub (S4/BAPI-1080). `emitArgs` are
183
+ * appended after the `emit-event` subcommand.
182
184
  */
183
185
  export function resolveConductorEmitCommand(env, emitArgs) {
184
186
  const cliFile = env.BAPI_CONDUCTOR_CLI_FILE;
@@ -188,7 +190,7 @@ export function resolveConductorEmitCommand(env, emitArgs) {
188
190
  args: [cliFile.trim(), "emit-event", ...emitArgs],
189
191
  };
190
192
  }
191
- const bin = nonEmpty(env.BAPI_CONDUCTOR_BIN) ? env.BAPI_CONDUCTOR_BIN.trim() : "conductor";
193
+ const bin = nonEmpty(env.BAPI_CONDUCTOR_BIN) ? env.BAPI_CONDUCTOR_BIN.trim() : "epic-implementer";
192
194
  return { command: bin, args: ["emit-event", ...emitArgs] };
193
195
  }
194
196
  const defaultConductorSpawn = (command, args, input) => {
@@ -18,6 +18,7 @@ import { ConductorValidationError, ConductorEpicTickV1FrozenError, toConductorEr
18
18
  import { emitConductorEvent, purgeConductorLedger, sendWorkerMessage, checkWorkerMessages, } from "./store.js";
19
19
  import { isDuplicateConstraintError } from "./producer-ledger.js";
20
20
  import { SEMANTIC_EVENT_TYPES } from "./taxonomy.js";
21
+ import { DRIVER_CONTEXT_MAX_BYTES } from "./repair-contract.js";
21
22
  import { installConductorGitHooks, resolveConductorHookBin } from "./git-hooks.js";
22
23
  import { runFileScopeGuardCli } from "./file-scope-guard.js";
23
24
  import { runPostCommitHookProducer, runReferenceTransactionHookProducer } from "./git-producer.js";
@@ -78,6 +79,15 @@ export function getConductorUsage() {
78
79
  " adopt-current-head-and-unpark",
79
80
  " Recover a ticket parked by a PR-head drift: adopt the",
80
81
  " CURRENT PR head and unpark in one step.",
82
+ " scope-reparse --epic-run-id <id> --scope-id <scope>",
83
+ " Re-drive the run's OWN index scope through retry-reparse",
84
+ " and schedule its parse. Refused if the scope is not the run's.",
85
+ " dispatch-remediation --epic-run-id <id> --ticket-key <key> --context-file <path>",
86
+ ` Dispatch a remediation job carrying driver context (UTF-8,`,
87
+ ` at most DRIVER_CONTEXT_MAX_BYTES = ${DRIVER_CONTEXT_MAX_BYTES} bytes; never truncated).`,
88
+ " merge-child --epic-run-id <id> --pr <number>",
89
+ " Merge the run's PR-bound child through the shared merge",
90
+ " service; the server composes the head and gate identity.",
81
91
  "",
82
92
  "supervise options:",
83
93
  " --run-id <id> Run/session identifier to supervise (required)",
@@ -241,6 +251,10 @@ const VALID_COMMANDS = new Set([
241
251
  "abandon-run",
242
252
  "unpark",
243
253
  "adopt-current-head-and-unpark",
254
+ // BAPI-1154: CLI-only run-addressed repair verbs — never registered as MCP tools.
255
+ "scope-reparse",
256
+ "dispatch-remediation",
257
+ "merge-child",
244
258
  // Private, and deliberately ABSENT from the usage text: `__hook-bin` exists as
245
259
  // the bundled-artifact regression guard for BAPI-772 (mirroring `plane
246
260
  // __entrypoint`), not as a supported operator workflow.
@@ -1237,6 +1251,20 @@ export async function runConductorCli(argv) {
1237
1251
  const { runAdoptCurrentHeadAndUnparkCommand } = await import("./recovery-cli.js");
1238
1252
  return await runAdoptCurrentHeadAndUnparkCommand(parsed.argv);
1239
1253
  }
1254
+ // BAPI-1154: the three run-addressed repair verbs — CLI-only, and lazily
1255
+ // imported exactly like the four recovery verbs above.
1256
+ case "scope-reparse": {
1257
+ const { runScopeReparseCommand } = await import("./recovery-cli.js");
1258
+ return await runScopeReparseCommand(parsed.argv);
1259
+ }
1260
+ case "dispatch-remediation": {
1261
+ const { runDispatchRemediationCommand } = await import("./recovery-cli.js");
1262
+ return await runDispatchRemediationCommand(parsed.argv);
1263
+ }
1264
+ case "merge-child": {
1265
+ const { runMergeChildCommand } = await import("./recovery-cli.js");
1266
+ return await runMergeChildCommand(parsed.argv);
1267
+ }
1240
1268
  default:
1241
1269
  console.error('Error: Unknown command. Run "conductor --help" for usage.');
1242
1270
  return 1;