@bridge_gpt/mcp-server 0.2.58 → 0.2.59

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 (86) hide show
  1. package/README.md +105 -109
  2. package/build/agent-registry.js +15 -0
  3. package/build/agent-utils.js +16 -8
  4. package/build/claude-user-config-doctor.js +1 -35
  5. package/build/codex-bridge-profile.js +1 -1
  6. package/build/commands.generated.js +5 -6
  7. package/build/conductor/bridge-api-client.js +149 -0
  8. package/build/conductor/bring-up-facts.js +1 -1
  9. package/build/conductor/claude-hook.js +16 -16
  10. package/build/conductor/doctor.js +24 -19
  11. package/build/conductor/epic-implementer-cli.js +38 -5
  12. package/build/conductor/epic-reconcile.js +1 -1
  13. package/build/conductor/epic-runtime.js +10 -10
  14. package/build/conductor/errors.js +3 -3
  15. package/build/conductor/file-scope-guard.js +4 -4
  16. package/build/conductor/git-ci-types.js +1 -1
  17. package/build/conductor/git-inspection.js +2 -2
  18. package/build/conductor/install-doctor.js +6 -6
  19. package/build/conductor/merge-ledger.js +2 -2
  20. package/build/conductor/pr-ci-producer.js +6 -6
  21. package/build/conductor/pr-review-producer.js +2 -2
  22. package/build/conductor/producer-ledger.js +2 -0
  23. package/build/conductor/readiness-sections.js +7 -7
  24. package/build/conductor/readiness.js +2 -2
  25. package/build/conductor/recovery-cli.js +243 -12
  26. package/build/conductor/recovery-operations.js +56 -4
  27. package/build/conductor/repair-contract.js +38 -4
  28. package/build/conductor/spec-review-producer.js +6 -6
  29. package/build/conductor/store.js +10 -10
  30. package/build/conductor/supervisor-config.js +14 -14
  31. package/build/conductor/supervisor-ledger.js +2 -2
  32. package/build/conductor/supervisor-message-relay.js +2 -2
  33. package/build/conductor/worker-ledger-cli.js +19 -19
  34. package/build/conductor-bundle-artifacts.js +2 -2
  35. package/build/doctor.js +16 -294
  36. package/build/drive-epic.js +65 -71
  37. package/build/epic-implementer-bin.js +22 -17
  38. package/build/epic-implementer-claude-hook-bin.js +4 -2
  39. package/build/epic-implementer-env.js +155 -0
  40. package/build/epic-integration-pr.js +2 -2
  41. package/build/epic-shared/bridge-client.js +773 -0
  42. package/build/epic-shared/cut-protocol.js +392 -0
  43. package/build/executor/env.js +19 -8
  44. package/build/executor/heartbeat.js +1 -1
  45. package/build/executor/job-errors.js +3 -3
  46. package/build/executor/job-runner.js +1 -1
  47. package/build/executor/merge-job.js +35 -3
  48. package/build/executor/observation.js +1 -1
  49. package/build/executor/preflight.js +1 -1
  50. package/build/executor/prompt-spec.js +1 -1
  51. package/build/executor/reconcile-overlap-job.js +10 -130
  52. package/build/executor/reconcile-overlap-types.js +1 -1
  53. package/build/executor/runner.js +1 -1
  54. package/build/implement-epic/cli.js +1 -1
  55. package/build/index.js +181 -218
  56. package/build/init.js +20 -28
  57. package/build/install-bridge-conductor.js +7 -7
  58. package/build/install-bridge.js +199 -7
  59. package/build/mcp-host-entry-adapter.js +6 -0
  60. package/build/mcp-host-targets.js +2 -0
  61. package/build/mcp-invoke.js +5 -5
  62. package/build/mcp-profile.js +2 -2
  63. package/build/mcp-provisioning.js +2 -2
  64. package/build/mcp-server-invocation.js +3 -3
  65. package/build/merge-pull-request.js +15 -0
  66. package/build/model-routing-host.js +25 -0
  67. package/build/model-routing-preferences.js +99 -0
  68. package/build/pipelines.generated.js +9 -9
  69. package/build/plan-epic-implementer-eligibility.js +215 -0
  70. package/build/plan-execution-ledger.js +8 -0
  71. package/build/plane/member-roster.js +5 -5
  72. package/build/plane/supervisor.js +1 -1
  73. package/build/plane/types.js +5 -5
  74. package/build/readiness-check.js +1 -1
  75. package/build/readme.generated.js +1 -1
  76. package/build/review-tickets.js +4 -4
  77. package/build/setup-epic.js +8 -8
  78. package/build/start-tickets-conductor.js +32 -32
  79. package/build/start-tickets-prereqs.js +4 -9
  80. package/build/start-tickets.js +475 -91
  81. package/build/version.generated.js +3 -3
  82. package/docs/CONDUCTOR.md +69 -18
  83. package/docs/install/mcp-tool-integrations.md +2 -1
  84. package/package.json +2 -2
  85. package/pipelines/implement-ticket.json +1 -1
  86. package/pipelines/plan-epic.json +2 -2
@@ -1331,6 +1331,155 @@ export async function completeMergeChild(access, request, fetchImpl = globalThis
1331
1331
  }, fetchImpl);
1332
1332
  return parseMergeChildRouteResponse(parsed);
1333
1333
  }
1334
+ // ---------------------------------------------------------------------------
1335
+ // BAPI-1176 — scope-catch-up, patch-policy, resume-run (CLI-only; no MCP tool)
1336
+ // ---------------------------------------------------------------------------
1337
+ // The same request path as the BAPI-1154 clients above, with STRICT parsers: a
1338
+ // response carrying any field outside its documented set is refused, so a server
1339
+ // that ever returned a generation, a control record, or a policy value would fail
1340
+ // closed here rather than reach operator output. No parser error echoes the body.
1341
+ /** The fields every repair response carries. */
1342
+ const REPAIR_BASE_KEYS = [
1343
+ "epic_run_id",
1344
+ "before",
1345
+ "after",
1346
+ "committed",
1347
+ "event_recorded",
1348
+ ];
1349
+ /** A bounded, closed-vocabulary token (a catch-up outcome such as `parse_scheduled`). */
1350
+ const REPAIR_TOKEN_RE = /^[a-z0-9_.:-]{1,120}$/;
1351
+ /** A `patch-policy` digest display token: `policy:` and twelve lowercase hex digits. */
1352
+ const POLICY_DIGEST_TOKEN_RE = /^policy:[0-9a-f]{12}$/;
1353
+ /** A dotted policy-path NAME. Never a value. */
1354
+ const POLICY_PATH_RE = /^[A-Za-z0-9_-]{1,100}(?:\.[A-Za-z0-9_-]{1,100}){0,15}$/;
1355
+ /** `resume-run`'s closed run-control state vocabulary. */
1356
+ const RESUME_RUN_STATES = ["paused", "quarantined", "none"];
1357
+ function requireOnlyRepairKeys(p, extraKeys) {
1358
+ for (const key of Object.keys(p)) {
1359
+ if (!REPAIR_BASE_KEYS.includes(key) && !extraKeys.includes(key)) {
1360
+ throw new ConductorBridgeApiError("server");
1361
+ }
1362
+ }
1363
+ }
1364
+ function parseStrictNullableString(value) {
1365
+ if (value === null || value === undefined)
1366
+ return null;
1367
+ if (typeof value !== "string") {
1368
+ throw new ConductorBridgeApiError("server");
1369
+ }
1370
+ return value;
1371
+ }
1372
+ function parseScopeCatchUpResult(parsed) {
1373
+ const p = parseRepairBase(parsed);
1374
+ requireOnlyRepairKeys(p, ["scope_id", "outcome", "parse_scheduled"]);
1375
+ const scopeId = p["scope_id"];
1376
+ const outcome = p["outcome"];
1377
+ const parseScheduled = p["parse_scheduled"];
1378
+ if (typeof scopeId !== "string" ||
1379
+ typeof outcome !== "string" ||
1380
+ !REPAIR_TOKEN_RE.test(outcome) ||
1381
+ typeof parseScheduled !== "boolean") {
1382
+ throw new ConductorBridgeApiError("server");
1383
+ }
1384
+ return {
1385
+ epic_run_id: p["epic_run_id"],
1386
+ scope_id: scopeId,
1387
+ before: parseStrictNullableString(p["before"]),
1388
+ after: parseStrictNullableString(p["after"]),
1389
+ committed: p["committed"],
1390
+ event_recorded: p["event_recorded"],
1391
+ outcome,
1392
+ parse_scheduled: parseScheduled,
1393
+ };
1394
+ }
1395
+ function parsePatchPolicyResult(parsed) {
1396
+ const p = parseRepairBase(parsed);
1397
+ requireOnlyRepairKeys(p, ["policy_paths"]);
1398
+ const before = p["before"];
1399
+ const after = p["after"];
1400
+ const paths = p["policy_paths"];
1401
+ if (typeof before !== "string" ||
1402
+ !POLICY_DIGEST_TOKEN_RE.test(before) ||
1403
+ typeof after !== "string" ||
1404
+ !POLICY_DIGEST_TOKEN_RE.test(after) ||
1405
+ !Array.isArray(paths) ||
1406
+ !paths.every((path) => typeof path === "string" && POLICY_PATH_RE.test(path))) {
1407
+ throw new ConductorBridgeApiError("server");
1408
+ }
1409
+ return {
1410
+ epic_run_id: p["epic_run_id"],
1411
+ before,
1412
+ after,
1413
+ committed: p["committed"],
1414
+ event_recorded: p["event_recorded"],
1415
+ policy_paths: [...paths],
1416
+ };
1417
+ }
1418
+ function parseResumeRunResult(parsed) {
1419
+ const p = parseRepairBase(parsed);
1420
+ requireOnlyRepairKeys(p, []);
1421
+ const before = p["before"];
1422
+ const after = p["after"];
1423
+ if (typeof before !== "string" ||
1424
+ !RESUME_RUN_STATES.includes(before) ||
1425
+ typeof after !== "string" ||
1426
+ !RESUME_RUN_STATES.includes(after)) {
1427
+ throw new ConductorBridgeApiError("server");
1428
+ }
1429
+ return {
1430
+ epic_run_id: p["epic_run_id"],
1431
+ before,
1432
+ after,
1433
+ committed: p["committed"],
1434
+ event_recorded: p["event_recorded"],
1435
+ };
1436
+ }
1437
+ /**
1438
+ * POST `/jira/epic-runs/runs/{epic_run_id}/scope-catch-up` — replay merges the
1439
+ * run's OWN index scope did not observe, through the unchanged guarded advance
1440
+ * gate. Body EXACTLY `{repo_name, scope_id}`: no force flag and no caller-supplied
1441
+ * commit. The server refuses a scope not bound to the run (`404
1442
+ * TICKET_NOT_IN_RUN`) and a blocked advance (`409 SCOPE_CATCH_UP_REFUSED`).
1443
+ */
1444
+ export async function scopeCatchUp(access, request, fetchImpl = globalThis.fetch) {
1445
+ requireNonEmptyString(request.epicRunId);
1446
+ requireNonEmptyString(request.scopeId);
1447
+ const parsed = await postRepairRequest(access, `${epicRunApiPath(request.epicRunId)}/scope-catch-up`, { scope_id: request.scopeId }, fetchImpl);
1448
+ return parseScopeCatchUpResult(parsed);
1449
+ }
1450
+ /**
1451
+ * POST `/jira/epic-runs/runs/{epic_run_id}/patch-policy` — body EXACTLY
1452
+ * `{repo_name, merge_patch}`. The SERVER reads the stored policy, applies the RFC
1453
+ * 7396 merge patch, validates the result, and writes it under a snapshot guard.
1454
+ * This is deliberately never `replaceEpicRunPolicy`: the client never reads,
1455
+ * holds, or sends a whole policy, so a server-retained value it cannot see (the
1456
+ * webhook URL) can never be dropped by a read-modify-write.
1457
+ */
1458
+ export async function patchPolicy(access, request, fetchImpl = globalThis.fetch) {
1459
+ requireNonEmptyString(request.epicRunId);
1460
+ const patch = request.mergePatch;
1461
+ if (!patch ||
1462
+ typeof patch !== "object" ||
1463
+ Array.isArray(patch) ||
1464
+ Object.keys(patch).length === 0) {
1465
+ throw new ConductorBridgeApiError("invalid-input");
1466
+ }
1467
+ const parsed = await postRepairRequest(access, `${epicRunApiPath(request.epicRunId)}/patch-policy`, { merge_patch: patch }, fetchImpl);
1468
+ return parsePatchPolicyResult(parsed);
1469
+ }
1470
+ /**
1471
+ * POST `/jira/epic-runs/runs/{epic_run_id}/resume-run` — body EXACTLY
1472
+ * `{repo_name, clear_quarantine}`. No generation and no reason: the server
1473
+ * re-reads the control record, re-evaluates activation, and owns the CAS retry.
1474
+ */
1475
+ export async function resumeRun(access, request, fetchImpl = globalThis.fetch) {
1476
+ requireNonEmptyString(request.epicRunId);
1477
+ if (typeof request.clearQuarantine !== "boolean") {
1478
+ throw new ConductorBridgeApiError("invalid-input");
1479
+ }
1480
+ const parsed = await postRepairRequest(access, `${epicRunApiPath(request.epicRunId)}/resume-run`, { clear_quarantine: request.clearQuarantine }, fetchImpl);
1481
+ return parseResumeRunResult(parsed);
1482
+ }
1334
1483
  /**
1335
1484
  * Idempotently seed an epic ticket status row via POST to the per-epic tickets
1336
1485
  * endpoint. The backend uses ON CONFLICT DO NOTHING so repeated seeding across
@@ -1,5 +1,5 @@
1
1
  /** The pinned operator runbook every remedy below points into. */
2
- export const BRING_UP_RUNBOOK = "docs/claude/epic-conductor-v2-operator-runbook.md";
2
+ export const BRING_UP_RUNBOOK = "docs/claude/epic-implementer-v2-operator-runbook.md";
3
3
  /**
4
4
  * The client-derived refusal vocabulary, IN REFUSAL ORDER.
5
5
  *
@@ -87,10 +87,10 @@ export function mapClaudeHookEventToSemanticType(eventName) {
87
87
  }
88
88
  /** Conductor identity env required before any worker event may be emitted. */
89
89
  const REQUIRED_IDENTITY_ENV = [
90
- "BAPI_CONDUCTOR_RUN_ID",
91
- "BAPI_CONDUCTOR_WORKER_ID",
92
- "BAPI_CONDUCTOR_TICKET_KEY",
93
- "BAPI_CONDUCTOR_WORKTREE_PATH",
90
+ "BAPI_EPIC_IMPLEMENTER_RUN_ID",
91
+ "BAPI_EPIC_IMPLEMENTER_WORKER_ID",
92
+ "BAPI_EPIC_IMPLEMENTER_TICKET_KEY",
93
+ "BAPI_EPIC_IMPLEMENTER_WORKTREE_PATH",
94
94
  ];
95
95
  function nonEmpty(value) {
96
96
  return typeof value === "string" && value.trim().length > 0;
@@ -100,7 +100,7 @@ function nonEmpty(value) {
100
100
  * the process environment, or `null` when the event is unmapped or the
101
101
  * conductor identity is incomplete.
102
102
  *
103
- * Identity gating: requires `BAPI_CONDUCTOR_ENABLED=1` and all of
103
+ * Identity gating: requires `BAPI_EPIC_IMPLEMENTER_ENABLED=1` and all of
104
104
  * {@link REQUIRED_IDENTITY_ENV}. Any missing value returns `null` WITHOUT
105
105
  * emitting — this prevents accidental emissions from unrelated worktrees that
106
106
  * happen to have the hook on PATH.
@@ -116,7 +116,7 @@ export function buildClaudeHookConductorEvent(payload, env) {
116
116
  if (type === null) {
117
117
  return null;
118
118
  }
119
- if (env.BAPI_CONDUCTOR_ENABLED !== "1") {
119
+ if (env.BAPI_EPIC_IMPLEMENTER_ENABLED !== "1") {
120
120
  return null;
121
121
  }
122
122
  for (const key of REQUIRED_IDENTITY_ENV) {
@@ -124,14 +124,14 @@ export function buildClaudeHookConductorEvent(payload, env) {
124
124
  return null;
125
125
  }
126
126
  }
127
- const ticketKey = env.BAPI_CONDUCTOR_TICKET_KEY.trim();
128
- const worktreePath = env.BAPI_CONDUCTOR_WORKTREE_PATH.trim();
127
+ const ticketKey = env.BAPI_EPIC_IMPLEMENTER_TICKET_KEY.trim();
128
+ const worktreePath = env.BAPI_EPIC_IMPLEMENTER_WORKTREE_PATH.trim();
129
129
  const details = {
130
130
  ticket_key: ticketKey,
131
131
  worktree_path: worktreePath,
132
132
  };
133
- if (nonEmpty(env.BAPI_CONDUCTOR_REPO_NAME)) {
134
- details.repo = env.BAPI_CONDUCTOR_REPO_NAME.trim();
133
+ if (nonEmpty(env.BAPI_EPIC_IMPLEMENTER_REPO_NAME)) {
134
+ details.repo = env.BAPI_EPIC_IMPLEMENTER_REPO_NAME.trim();
135
135
  }
136
136
  // The native payload lives ONLY under data.raw. Wrap non-object payloads so the
137
137
  // raw contract is always an object, never echoing native fields elsewhere.
@@ -142,8 +142,8 @@ export function buildClaudeHookConductorEvent(payload, env) {
142
142
  source: "claude-code",
143
143
  type,
144
144
  subject: ticketKey,
145
- run_id: env.BAPI_CONDUCTOR_RUN_ID.trim(),
146
- worker_id: env.BAPI_CONDUCTOR_WORKER_ID.trim(),
145
+ run_id: env.BAPI_EPIC_IMPLEMENTER_RUN_ID.trim(),
146
+ worker_id: env.BAPI_EPIC_IMPLEMENTER_WORKER_ID.trim(),
147
147
  producer: "claude-code-hook",
148
148
  observed_via: "claude-code-hook",
149
149
  data: {
@@ -176,21 +176,21 @@ export function buildConductorEmitEventArgs(event) {
176
176
  }
177
177
  /**
178
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
179
+ * `BAPI_EPIC_IMPLEMENTER_CLI_FILE` (run via the current Node executable so no global
180
+ * install is required), otherwise falls back to `BAPI_EPIC_IMPLEMENTER_BIN` or the
181
181
  * bare `epic-implementer` binary on PATH — never the retained `conductor`
182
182
  * name, which is now a fixed migration stub (S4/BAPI-1080). `emitArgs` are
183
183
  * appended after the `emit-event` subcommand.
184
184
  */
185
185
  export function resolveConductorEmitCommand(env, emitArgs) {
186
- const cliFile = env.BAPI_CONDUCTOR_CLI_FILE;
186
+ const cliFile = env.BAPI_EPIC_IMPLEMENTER_CLI_FILE;
187
187
  if (nonEmpty(cliFile)) {
188
188
  return {
189
189
  command: process.execPath,
190
190
  args: [cliFile.trim(), "emit-event", ...emitArgs],
191
191
  };
192
192
  }
193
- const bin = nonEmpty(env.BAPI_CONDUCTOR_BIN) ? env.BAPI_CONDUCTOR_BIN.trim() : "epic-implementer";
193
+ const bin = nonEmpty(env.BAPI_EPIC_IMPLEMENTER_BIN) ? env.BAPI_EPIC_IMPLEMENTER_BIN.trim() : "epic-implementer";
194
194
  return { command: bin, args: ["emit-event", ...emitArgs] };
195
195
  }
196
196
  const defaultConductorSpawn = (command, args, input) => {
@@ -18,7 +18,7 @@ import { MCP_PACKAGE_NAME } from "../mcp-identity.js";
18
18
  import { resolveProfiles } from "../mcp-profile.js";
19
19
  import { createReadinessCheck, createReadinessCheckSafely, } from "../readiness-check.js";
20
20
  /**
21
- * Remediation for a conductor context whose resolved MCP groups omit `conductor`.
21
+ * Remediation for a conductor context whose resolved MCP groups omit `epic_implementer`.
22
22
  *
23
23
  * Fixed prose: it never interpolates the raw `BRIDGE_MCP_PROFILE` value. The
24
24
  * legacy warning does interpolate it (unchanged), but a consolidated report is a
@@ -26,7 +26,7 @@ import { createReadinessCheck, createReadinessCheckSafely, } from "../readiness-
26
26
  * gate needs to echo to name the fix. ADD, never replace — replacing the value
27
27
  * would drop the worker's other groups.
28
28
  */
29
- export const MCP_PROFILE_REMEDIATION = "ADD `conductor` to BRIDGE_MCP_PROFILE (comma-separated) — do NOT replace the existing value, " +
29
+ export const MCP_PROFILE_REMEDIATION = "ADD `epic_implementer` to BRIDGE_MCP_PROFILE (comma-separated) — do NOT replace the existing value, " +
30
30
  "which would drop the worker's other tool groups. Without it the conductor/event/supervisor " +
31
31
  "tools are not registered.";
32
32
  /**
@@ -70,9 +70,10 @@ export const MCP_PROFILE_UNSET = "(unset)";
70
70
  /**
71
71
  * The CLI-only verbs that call the Bridge API from a shell-spawned process and
72
72
  * 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.
73
+ * Resolution"): the four BAPI-872 recovery verbs and the six run-addressed repair
74
+ * verbs (BAPI-1154's three and BAPI-1176's three). `.mcp.json` env is invisible to
75
+ * them — only `BAPI_API_KEY` in the parent env and the user-scoped credential
76
+ * store count.
76
77
  */
77
78
  export const CREDENTIALED_RECOVERY_COMMANDS = [
78
79
  "stop-run",
@@ -82,6 +83,9 @@ export const CREDENTIALED_RECOVERY_COMMANDS = [
82
83
  "scope-reparse",
83
84
  "dispatch-remediation",
84
85
  "merge-child",
86
+ "scope-catch-up",
87
+ "patch-policy",
88
+ "resume-run",
85
89
  ];
86
90
  /** Remediation for recovery/repair verbs that cannot resolve Bridge credentials. */
87
91
  export const REPAIR_CREDENTIAL_REMEDIATION = "export BAPI_API_KEY in the shell that runs `conductor`, or store a credential for this " +
@@ -364,13 +368,13 @@ export async function collectConductorNativeLedgerSafe(deps = {}) {
364
368
  }
365
369
  /**
366
370
  * Inspect the BRIDGE_MCP_PROFILE environment variable and warn when a
367
- * conductor/epic context is detected but the resolved groups omit `conductor`.
371
+ * conductor/epic context is detected but the resolved groups omit `epic_implementer`.
368
372
  * Strictly read-only, never throws.
369
373
  *
370
374
  * BAPI-772: health is judged from {@link resolveProfiles} — the SAME resolver the
371
375
  * 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`
376
+ * `"epic_implementer"`. The old equality check called a worker running
377
+ * `BRIDGE_MCP_PROFILE=epic_implementer,sfcc` (exactly what `buildConductorWorkerEnv`
374
378
  * produces on SFCC projects) or `full` degraded, and its remediation told the
375
379
  * operator to REPLACE the variable — which would have destroyed the worker's
376
380
  * other groups. The remediation now says ADD.
@@ -380,15 +384,15 @@ export function inspectMcpProfile(env) {
380
384
  const raw_profile = raw === undefined ? MCP_PROFILE_UNSET : raw;
381
385
  const groups = resolveProfiles(raw);
382
386
  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");
387
+ const conductor_context_detected = env.BAPI_EPIC_IMPLEMENTER_ENABLED === "1" || env.BAPI_EPIC_IMPLEMENTER_ENABLED === "true";
388
+ const retired_profile = raw?.split(",").some((token) => token.trim().toLowerCase() === "conductor") ?? false;
389
+ const degraded = retired_profile || (conductor_context_detected && !groups.has("epic_implementer"));
385
390
  const warnings = [];
386
391
  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.`);
392
+ warnings.push(`Epic Implementer MCP profile is inactive (active groups: ${active_groups.join(", ")}). ` +
393
+ (retired_profile ? "The retired conductor profile token is ignored. " : "") +
394
+ "ADD `epic_implementer` to BRIDGE_MCP_PROFILE (comma-separated); keep other valid groups. " +
395
+ "Without it the Epic Implementer tools are not registered.");
392
396
  }
393
397
  // Fixed state labels and the CLOSED active-group vocabulary only (BAPI-1055):
394
398
  // `resolveProfiles` yields nothing but recognized group names, so this detail
@@ -397,14 +401,14 @@ export function inspectMcpProfile(env) {
397
401
  degraded
398
402
  ? {
399
403
  id: "mcp-profile",
400
- label: "MCP conductor profile",
404
+ label: "Epic Implementer MCP profile",
401
405
  status: "warn",
402
- detail: `a conductor context is active but the resolved groups omit \`conductor\` (active: ${active_groups.join(", ")})`,
406
+ detail: `the resolved groups omit \`epic_implementer\` (active: ${active_groups.join(", ")})`,
403
407
  remediation: MCP_PROFILE_REMEDIATION,
404
408
  }
405
409
  : {
406
410
  id: "mcp-profile",
407
- label: "MCP conductor profile",
411
+ label: "Epic Implementer MCP profile",
408
412
  status: "pass",
409
413
  detail: conductor_context_detected
410
414
  ? `conductor context active; resolved groups: ${active_groups.join(", ")}`
@@ -648,6 +652,7 @@ export function formatConductorDoctorReport(report) {
648
652
  // value of `full` is healthy precisely because it resolves to a set containing
649
653
  // `conductor`.
650
654
  const profileTag = mcp_profile.degraded ? "[WARNING] degraded" : "[OK]";
655
+ lines.push("Epic Implementer MCP profile");
651
656
  lines.push(`raw profile: ${mcp_profile.raw_profile}`);
652
657
  lines.push(`active groups: ${mcp_profile.active_groups.join(", ")} ${profileTag}`);
653
658
  lines.push(`conductor context: ${mcp_profile.conductor_context_detected}`);
@@ -746,7 +751,7 @@ export function formatConductorDoctorReport(report) {
746
751
  export const CONDUCTOR_READINESS_DESCRIPTORS = [
747
752
  { id: "ledger", label: "Conductor event ledger" },
748
753
  { id: "git-hooks", label: "Conductor git hooks" },
749
- { id: "mcp-profile", label: "MCP conductor profile" },
754
+ { id: "mcp-profile", label: "Epic Implementer MCP profile" },
750
755
  { id: "gh-cli", label: "GitHub CLI availability" },
751
756
  { id: "gh-auth", label: "GitHub CLI authentication" },
752
757
  { id: "native-ledger", label: "Native ledger binding" },
@@ -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
@@ -54,13 +54,13 @@ const ACTIVE_WORKER_STATUSES = new Set(["dispatched", "running"]);
54
54
  * Source + producer tags for the durable `parse.triggered` marker that the
55
55
  * parse-after-merge wait emits from the epic-tick process.
56
56
  */
57
- const PARSE_WAIT_EVENT_SOURCE = "conductor-supervisor";
57
+ const PARSE_WAIT_EVENT_SOURCE = "epic-implementer-supervisor";
58
58
  const PARSE_WAIT_EVENT_PRODUCER = "epic-parse-wait";
59
59
  /**
60
60
  * BAPI-494: source + producer tags for the durable `merge.conflict` marker the
61
61
  * done-gate pass emits when a `ready_for_review` ticket's PR is un-mergeable.
62
62
  */
63
- const MERGE_CONFLICT_EVENT_SOURCE = "conductor-supervisor";
63
+ const MERGE_CONFLICT_EVENT_SOURCE = "epic-implementer-supervisor";
64
64
  const MERGE_CONFLICT_EVENT_PRODUCER = "epic-mergeability";
65
65
  function defaultLeaseOwner() {
66
66
  return `epic-tick-${process.pid}`;
@@ -403,7 +403,7 @@ export async function runConductorDoneGatePass(ticketStatuses, deps) {
403
403
  }
404
404
  // Stamp the ticket's run_id and (when available) worker_id onto the emitted
405
405
  // gate.met via a per-ticket env override (observePrCiOnce reads
406
- // BAPI_CONDUCTOR_RUN_ID/_WORKER_ID from env, env taking precedence). run_id
406
+ // BAPI_EPIC_IMPLEMENTER_RUN_ID/_WORKER_ID from env, env taking precedence). run_id
407
407
  // correlates the fold to the ticket. worker_id is attribution ONLY: BAPI-525
408
408
  // Change A removed the worker_id requirement from merge-identity extraction, so
409
409
  // a missing worker_id no longer blocks the merge — the backstop stamps real
@@ -456,8 +456,8 @@ export async function runConductorDoneGatePass(ticketStatuses, deps) {
456
456
  }
457
457
  const perTicketEnv = {
458
458
  ...deps.env,
459
- ...(runId ? { BAPI_CONDUCTOR_RUN_ID: runId } : {}),
460
- ...(workerId ? { BAPI_CONDUCTOR_WORKER_ID: workerId } : {}),
459
+ ...(runId ? { BAPI_EPIC_IMPLEMENTER_RUN_ID: runId } : {}),
460
+ ...(workerId ? { BAPI_EPIC_IMPLEMENTER_WORKER_ID: workerId } : {}),
461
461
  };
462
462
  try {
463
463
  const observeResult = await deps.observePrCi({
@@ -550,7 +550,7 @@ export async function runEpicTick(options, deps = {}) {
550
550
  // dispatch state. Without this guard, `correlateRunId` writes a `run_spawned` row
551
551
  // under the REAL dispatch_key, and the next real tick's `claimDispatchKey` returns
552
552
  // `already-spawned`, silently skipping the real worker spawn.
553
- const isDryRun = env.BAPI_CONDUCTOR_DISPATCH_DRY_RUN === "1";
553
+ const isDryRun = env.BAPI_EPIC_IMPLEMENTER_DISPATCH_DRY_RUN === "1";
554
554
  const startMs = nowFn();
555
555
  // ---------------------------------------------------------------------------
556
556
  // Step 0: Resolve Bridge API access (offline / fail-closed guard)
@@ -1736,12 +1736,12 @@ export async function buildProductionEpicRuntimeDeps(epicKey) {
1736
1736
  // The dispatch kind comes from the plan node's automation (start-tickets or
1737
1737
  // review-tickets); default to start-tickets when unspecified.
1738
1738
  const kind = automationMap.get(tk) ?? "start-tickets";
1739
- // Operator dry-run: when BAPI_CONDUCTOR_DISPATCH_DRY_RUN=1, dispatch resolves
1739
+ // Operator dry-run: when BAPI_EPIC_IMPLEMENTER_DISPATCH_DRY_RUN=1, dispatch resolves
1740
1740
  // the spawn command + model routing but opens NO terminal, creates NO worktree,
1741
1741
  // and opens NO PR (start-tickets/review-tickets dryRun). The full loop dispatch
1742
1742
  // path (ready-set → claim key → dispatch → correlate run_id) still exercises;
1743
1743
  // dry-run rows carry no runId, so a synthetic one is substituted for correlation.
1744
- const dispatchDryRun = process.env.BAPI_CONDUCTOR_DISPATCH_DRY_RUN === "1";
1744
+ const dispatchDryRun = process.env.BAPI_EPIC_IMPLEMENTER_DISPATCH_DRY_RUN === "1";
1745
1745
  // BAPI-507 (N-2): thread the ticket's declared file scope into the
1746
1746
  // IMPLEMENTATION dispatch identity only. A `review-tickets` node does not open
1747
1747
  // an implementation PR, so it gets no file-scope env (Step 8.5).
@@ -1850,11 +1850,11 @@ export async function buildProductionEpicRuntimeDeps(epicKey) {
1850
1850
  if (cachedPlanVersion === 0) {
1851
1851
  throw new Error(`dispatchReviewSeam called before fetchPlan for epic ${ek} ticket ${tk}; cachedPlanVersion is 0`);
1852
1852
  }
1853
- const dispatchDryRun = process.env.BAPI_CONDUCTOR_DISPATCH_DRY_RUN === "1";
1853
+ const dispatchDryRun = process.env.BAPI_EPIC_IMPLEMENTER_DISPATCH_DRY_RUN === "1";
1854
1854
  // Deliberately omit `dispatch_key` from the identity: the reconcile pass owns
1855
1855
  // the idempotent :review key claim (recordEpicDispatch with reviewRole) and the
1856
1856
  // run_spawned correlation. orchestrateReviewTickets still mints the run_id and
1857
- // injects BAPI_CONDUCTOR_RUN_ID into the review agent's environment, which is
1857
+ // injects BAPI_EPIC_IMPLEMENTER_RUN_ID into the review agent's environment, which is
1858
1858
  // exactly the correlation channel the Python review orchestrator reads to stamp
1859
1859
  // the spec_review.* verdict event with this review run's id (Phase 2).
1860
1860
  const identity = {
@@ -81,7 +81,7 @@ export class ConductorEpicTickV1FrozenError extends Error {
81
81
  *
82
82
  * Worker-facing ledger operations (`check_messages`, `wait_for_done_gate` event
83
83
  * emission) MUST execute through `conductor-bin.js` under the captured conductor
84
- * Node (`CONDUCTOR_NODE_PATH`) so the worker Node never loads the `better-sqlite3`
84
+ * Node (`EPIC_IMPLEMENTER_NODE_PATH`) so the worker Node never loads the `better-sqlite3`
85
85
  * native binary. When that runtime is missing or invalid, this error makes the
86
86
  * failure LOUD and structured rather than silently falling back to the worker
87
87
  * Node. The carried `reason`/`envVar` are the ONLY diagnostic surface — both are
@@ -240,8 +240,8 @@ export function toConductorErrorEnvelope(error) {
240
240
  message: "Conductor persistence is unavailable (the optional better-sqlite3 native module is not loaded). Conductor coordination features are disabled; core tools are unaffected.",
241
241
  };
242
242
  }
243
- // Worker-ledger subprocess runtime failure (BAPI-527): CONDUCTOR_NODE_PATH /
244
- // BAPI_CONDUCTOR_CLI_FILE is missing or invalid so the worker cannot reach the
243
+ // Worker-ledger subprocess runtime failure (BAPI-527): EPIC_IMPLEMENTER_NODE_PATH /
244
+ // BAPI_EPIC_IMPLEMENTER_CLI_FILE is missing or invalid so the worker cannot reach the
245
245
  // local ledger through the captured conductor Node. Surfaced as a retryable-
246
246
  // shaped 503 with a fixed, secret-free message plus a small non-secret details
247
247
  // bag (env_var + stable reason). NOTHING path/secret-bearing is echoed. This is
@@ -2,7 +2,7 @@
2
2
  * BAPI-507 (N-2): warn-only worker file-scope guard.
3
3
  *
4
4
  * Compares a branch's changed files against the planner-declared touched-file
5
- * set (delivered to the worker via `BAPI_CONDUCTOR_DECLARED_TOUCHED_FILES_JSON`)
5
+ * set (delivered to the worker via `BAPI_EPIC_IMPLEMENTER_DECLARED_TOUCHED_FILES_JSON`)
6
6
  * and warns when the diff strays outside that set. It is strictly warn-only /
7
7
  * fail-open: it NEVER blocks PR creation, mutates the branch, or exits non-zero.
8
8
  * A worker that implements a sibling's files (the N-2 over-reach observed in the
@@ -21,9 +21,9 @@
21
21
  */
22
22
  import { spawnSync } from "node:child_process";
23
23
  /** Env var carrying the compact JSON array of declared repo-relative paths. */
24
- export const DECLARED_TOUCHED_FILES_ENV = "BAPI_CONDUCTOR_DECLARED_TOUCHED_FILES_JSON";
24
+ export const DECLARED_TOUCHED_FILES_ENV = "BAPI_EPIC_IMPLEMENTER_DECLARED_TOUCHED_FILES_JSON";
25
25
  /** Env var carrying the dispatched ticket key (used only to label warnings). */
26
- export const FILE_SCOPE_GUARD_TICKET_KEY_ENV = "BAPI_CONDUCTOR_TICKET_KEY";
26
+ export const FILE_SCOPE_GUARD_TICKET_KEY_ENV = "BAPI_EPIC_IMPLEMENTER_TICKET_KEY";
27
27
  /** Base ref the branch diff is computed against. */
28
28
  export const FILE_SCOPE_GUARD_BASE_REF = "origin/main";
29
29
  /**
@@ -73,7 +73,7 @@ export function normalizeDeclaredTouchedFiles(list) {
73
73
  return Array.from(out).sort();
74
74
  }
75
75
  /**
76
- * Parse `BAPI_CONDUCTOR_DECLARED_TOUCHED_FILES_JSON` into a normalized
76
+ * Parse `BAPI_EPIC_IMPLEMENTER_DECLARED_TOUCHED_FILES_JSON` into a normalized
77
77
  * declaration. Returns an "unspecified" result on a missing, blank, invalid-JSON,
78
78
  * non-array, or empty (after normalization) value — the guard no-ops in every
79
79
  * such case.
@@ -155,7 +155,7 @@ export function stableJsonHash(value) {
155
155
  * The two literals are exported as named constants so no consumer spells them
156
156
  * inline — a typo in one consumer would otherwise silently disagree with the
157
157
  * parser about which disposition is configured. The Python mirror lives in
158
- * `api/library/epic_conductor/epic_implementer_readiness.py` and must carry the same
158
+ * `api/library/epic_implementer/epic_implementer_readiness.py` and must carry the same
159
159
  * closed set.
160
160
  */
161
161
  export const VERDICTLESS_DISPOSITION_PARK = "park";
@@ -72,7 +72,7 @@ export function sanitizeGitRemoteUrl(url) {
72
72
  /**
73
73
  * Resolve the local worktree context: worktree root, git common dir, current
74
74
  * branch, HEAD SHA, sanitized origin remote, and repo identity. Repo name prefers
75
- * `BAPI_CONDUCTOR_REPO_NAME`, then `BAPI_REPO_NAME`, then the git root basename.
75
+ * `BAPI_EPIC_IMPLEMENTER_REPO_NAME`, then `BAPI_REPO_NAME`, then the git root basename.
76
76
  * Never throws — missing git simply yields a degraded context.
77
77
  */
78
78
  export function getGitWorktreeContext(options = {}) {
@@ -86,7 +86,7 @@ export function getGitWorktreeContext(options = {}) {
86
86
  const branch = branchRaw === null || branchRaw === "HEAD" ? null : branchRaw;
87
87
  const headSha = normalizeSha(firstLine(runGitCommand(["rev-parse", "HEAD"], { cwd })) ?? "");
88
88
  const remoteOrigin = sanitizeGitRemoteUrl(firstLine(runGitCommand(["config", "--get", "remote.origin.url"], { cwd })) ?? "");
89
- const repo = normalizeRepoName(env.BAPI_CONDUCTOR_REPO_NAME) ??
89
+ const repo = normalizeRepoName(env.BAPI_EPIC_IMPLEMENTER_REPO_NAME) ??
90
90
  normalizeRepoName(env.BAPI_REPO_NAME) ??
91
91
  normalizeRepoName(basename(worktreePath)) ??
92
92
  "unknown";