@bridge_gpt/mcp-server 0.2.46 → 0.2.49

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 (39) hide show
  1. package/README.md +101 -85
  2. package/build/agent-capabilities/default-deps.js +2 -2
  3. package/build/agent-launchers/claude.js +10 -19
  4. package/build/agent-launchers/cursor.js +4 -12
  5. package/build/agent-launchers/prompt.js +117 -0
  6. package/build/commands.generated.js +16 -22
  7. package/build/conduct-epic/bridge-client.js +73 -0
  8. package/build/conduct-epic/cli.js +152 -6
  9. package/build/conductor/cli.js +6 -7
  10. package/build/conductor/doctor.js +13 -116
  11. package/build/conductor/tools.js +18 -349
  12. package/build/conductor-bin.js +6 -30
  13. package/build/docs.generated.js +1 -1
  14. package/build/executor/deps.js +1 -0
  15. package/build/executor/service-lifecycle.js +6 -6
  16. package/build/executor/service-unit.js +13 -16
  17. package/build/index.js +214 -755
  18. package/build/init.js +15 -17
  19. package/build/install-doctor.js +1 -1
  20. package/build/learn-tool-gating.js +283 -0
  21. package/build/mcp-profile.js +13 -3
  22. package/build/mcp-server-invocation.js +14 -0
  23. package/build/pipelines.generated.js +19 -139
  24. package/build/platform-escaping.js +72 -0
  25. package/build/readme.generated.js +1 -1
  26. package/build/review-tickets.js +1 -1
  27. package/build/run-unit-tests-launcher.js +0 -1
  28. package/build/sfcc/register.js +41 -31
  29. package/build/sfcc/registration-inventory.js +44 -20
  30. package/build/start-tickets-conductor.js +2 -2
  31. package/build/start-tickets.js +8 -38
  32. package/build/version.generated.js +2 -2
  33. package/docs/CONDUCTOR.md +10 -12
  34. package/docs/install/mcp-tool-integrations.md +9 -55
  35. package/package.json +1 -1
  36. package/pipelines/idea-to-ticket.json +2 -2
  37. package/pipelines/review-ticket.json +9 -8
  38. package/pipelines/check-ci-ticket.json +0 -36
  39. package/pipelines/pr-ticket.json +0 -24
@@ -236,6 +236,7 @@ const INDEX_SCOPE_FRESHNESS_VALUES = new Set([
236
236
  "blocked",
237
237
  "failed",
238
238
  "unavailable",
239
+ "unobserved_advance",
239
240
  ]);
240
241
  /**
241
242
  * Read the server's freshness verdict, failing CLOSED.
@@ -424,6 +425,78 @@ export async function getIndexScopeStatus(access, scopeId, fetchImpl = globalThi
424
425
  },
425
426
  };
426
427
  }
428
+ /**
429
+ * The bounded outcomes `POST /jira/index-scope/catch-up` may report (BAPI-932).
430
+ *
431
+ * Three are successes — the scope was behind and every replayed commit passed
432
+ * the gate (`repaired`); it was already current and nothing was written
433
+ * (`already_current`); it was pinned correctly but not yet indexed, so the
434
+ * existing scheduler was asked to re-drive the parse (`parse_scheduled`). The
435
+ * rest are refusals, and a refusal leaves both watermarks byte-identical.
436
+ */
437
+ export const INDEX_SCOPE_CATCH_UP_OUTCOMES = [
438
+ "repaired",
439
+ "already_current",
440
+ "parse_scheduled",
441
+ "race_lost",
442
+ "blocked",
443
+ "history_limit_exceeded",
444
+ "refused_lifecycle",
445
+ "invalid_scope",
446
+ "unavailable",
447
+ // Route-minted, before the service is reached.
448
+ "unknown_index_scope",
449
+ "epic_shadow_index_lookup_failed",
450
+ "scope_does_not_belong_to_repository",
451
+ ];
452
+ /**
453
+ * `POST /jira/index-scope/catch-up` — replay a scope's missed observations.
454
+ *
455
+ * Fails CLOSED in the same two ways the rest of this client does: a soft-envelope
456
+ * refusal becomes a `BridgeResult` failure, and an unrecognized `outcome` narrows
457
+ * to `null` rather than being passed through. A `null` outcome with `ok: true`
458
+ * cannot happen — an unrecognized success is downgraded to a failure, because
459
+ * "the server said something we do not understand" must never be read by a
460
+ * conductor as "the repair worked".
461
+ */
462
+ export async function catchUpIndexScope(access, request, fetchImpl = globalThis.fetch) {
463
+ const result = await wrap(access, () => {
464
+ const url = buildConductorJiraUrl(access.baseUrl, "/index-scope/catch-up");
465
+ return fetchConductorJsonPostWithTimeout(url, postHeaders(access), JSON.stringify({ repo_name: access.repoName, scope_id: request.scopeId }), CONDUCTOR_FETCH_TIMEOUT_MS, fetchImpl);
466
+ });
467
+ if (!result.ok)
468
+ return result;
469
+ if (!isRecord(result.value))
470
+ return { ok: false, status: null, error: GENERIC_ERROR };
471
+ const body = result.value;
472
+ const rawOutcome = body["outcome"];
473
+ const outcome = typeof rawOutcome === "string" && INDEX_SCOPE_CATCH_UP_OUTCOMES.includes(rawOutcome)
474
+ ? rawOutcome
475
+ : null;
476
+ const ok = body["ok"] === true;
477
+ if (ok && outcome === null) {
478
+ // A success we cannot name is not a success we may act on.
479
+ return { ok: false, status: null, error: GENERIC_ERROR };
480
+ }
481
+ if (!ok && outcome === null) {
482
+ // A refusal we cannot name still refuses; surface the server's own error
483
+ // token when it gave one, exactly as the soft-envelope helper would.
484
+ const refusal = softEnvelopeFailure(body);
485
+ if (refusal)
486
+ return refusal;
487
+ return { ok: false, status: null, error: GENERIC_ERROR };
488
+ }
489
+ return {
490
+ ok: true,
491
+ value: {
492
+ ok,
493
+ outcome,
494
+ reason: nullableString(body["reason"]) ?? null,
495
+ required_commit_sha: nullableString(body["required_commit_sha"]) ?? null,
496
+ parse_scheduled: body["parse_scheduled"] === true,
497
+ },
498
+ };
499
+ }
427
500
  function nullableNumber(value) {
428
501
  return typeof value === "number" && Number.isFinite(value) ? value : null;
429
502
  }
@@ -39,7 +39,7 @@ import { runGhCommand } from "../conductor/pr-discovery.js";
39
39
  import { getDefaultSpawnTerminalTabForPlatform, detectTerminal, createDefaultStartTicketsDeps, } from "../start-tickets.js";
40
40
  import { resolveWorktrunkBinary } from "../start-tickets-prereqs.js";
41
41
  import { resolveRequiredStartTicketsRepoName } from "../start-tickets-repo.js";
42
- import { bootstrapIndexScope, getConfigFieldBaseBranch, getConductorReadiness, getIndexScopeLifecycle, getIndexScopeStatus, getEffectiveSupervisorConfig, getEffectiveSupervisorSetup, getEpicRunState, getParseStatus, getPrReviewStatus, heartbeatIndexScope, pollCiChecks, putSupervisorConfigDefaults, reclaimIndexScope, recoverIndexScope, resolveCiChecks, retireIndexScope, } from "./bridge-client.js";
42
+ import { bootstrapIndexScope, getConfigFieldBaseBranch, getConductorReadiness, getIndexScopeLifecycle, catchUpIndexScope, getIndexScopeStatus, getEffectiveSupervisorConfig, getEffectiveSupervisorSetup, getEpicRunState, getParseStatus, getPrReviewStatus, heartbeatIndexScope, pollCiChecks, putSupervisorConfigDefaults, reclaimIndexScope, recoverIndexScope, resolveCiChecks, retireIndexScope, } from "./bridge-client.js";
43
43
  import { appendTicketJournal, createInitialConductEpicCheckpoint, readConductEpicCheckpoint, resolveConductEpicCheckpointPath, resolveConductEpicLockPath, writeConductEpicCheckpointAtomic, CONDUCT_EPIC_REVIEW_VERDICTLESS_CEILING, CONDUCT_EPIC_TICKET_STATUSES, } from "./checkpoint-store.js";
44
44
  import { acquireConductEpicLock, inspectConductEpicLock, isConductEpicLockOwnerAlive, } from "./lock.js";
45
45
  import { discoverConductEpicPrState, discoverTicketWorktree, parseGitWorktreePorcelain, } from "./pr-state.js";
@@ -56,7 +56,7 @@ import { createExecFileRunCommand, firstOutputLine as firstLine, lsRemoteSha, no
56
56
  export { normalizeCommitSha };
57
57
  /** Epic and ticket keys accepted by every verb. */
58
58
  export const CONDUCT_EPIC_KEY_PATTERN = /^[A-Z]+-[0-9]+$/;
59
- /** The five verb families. `checkpoint set` is two tokens, one verb. */
59
+ /** The verb families. `checkpoint set` is two tokens, one verb. */
60
60
  export const CONDUCT_EPIC_VERBS = [
61
61
  "init",
62
62
  "status",
@@ -66,6 +66,7 @@ export const CONDUCT_EPIC_VERBS = [
66
66
  "recover",
67
67
  "retire",
68
68
  "reclaim",
69
+ "catch-up",
69
70
  ];
70
71
  /** Per-ticket fields `checkpoint set` may assign. */
71
72
  const TICKET_FIELDS = [
@@ -190,6 +191,16 @@ export function getConductEpicUsage() {
190
191
  " readable for post-mortem for the whole retention window. Idempotent.",
191
192
  " `finish` does this for you; this verb is for retiring without finishing.",
192
193
  "",
194
+ " catch-up <EPIC> [--scope <id>] [--checkpoint-path <p>] [--json]",
195
+ " GUARDED CATCH-UP: replay merges the server never observed, so a scope",
196
+ " stranded behind its epic branch can be repaired without waiting for the",
197
+ " next ticket to merge. Every replayed commit goes through the SAME AC-10",
198
+ " gate a merge webhook would have used — this replays verified merge",
199
+ " evidence, it does NOT force the branch into the index, and there is no",
200
+ " --force, --override, or skip-guard flag on purpose. A refusal leaves both",
201
+ " commit watermarks untouched and schedules no parse. Running it twice",
202
+ " leaves the same state as running it once.",
203
+ "",
193
204
  " reclaim <EPIC> [--scope <id>] [--override-retention] [--checkpoint-path <p>] [--json]",
194
205
  " Ask the server to schedule the scope's teardown: three Pinecone",
195
206
  " namespaces, six parse-table slices, three config rows, and a retained",
@@ -224,6 +235,11 @@ const VERB_FLAGS = {
224
235
  recover: ["--scope", "--checkpoint-path", "--json"],
225
236
  retire: ["--scope", "--checkpoint-path", "--json"],
226
237
  reclaim: ["--scope", "--override-retention", "--checkpoint-path", "--json"],
238
+ // BAPI-932. NOTE the flags that are deliberately absent: there is no --force,
239
+ // no --override, and no --skip-guards, and adding one would defeat the point of
240
+ // the verb. Catch-up replays commits the SERVER reads through the unchanged
241
+ // AC-10 gate; it cannot make that gate say yes.
242
+ "catch-up": ["--scope", "--checkpoint-path", "--json"],
227
243
  };
228
244
  /**
229
245
  * Parse and fully validate argv BEFORE any I/O.
@@ -255,7 +271,8 @@ export function parseConductEpicArgs(argv) {
255
271
  argv[0] === "spawn" ||
256
272
  argv[0] === "recover" ||
257
273
  argv[0] === "retire" ||
258
- argv[0] === "reclaim") {
274
+ argv[0] === "reclaim" ||
275
+ argv[0] === "catch-up") {
259
276
  verb = argv[0];
260
277
  rest = argv.slice(1);
261
278
  }
@@ -1762,6 +1779,13 @@ function renderScopeFreshnessLines(scope) {
1762
1779
  blocked: "Index refresh is BLOCKED — this advance will not be indexed.",
1763
1780
  failed: "Index generation FAILED for this scope.",
1764
1781
  unavailable: "Index freshness is unavailable — treat as not fresh.",
1782
+ unobserved_advance: "The epic branch has advanced BEYOND what this index scope observed.",
1783
+ };
1784
+ // The action line sits immediately under the headline so the output reads
1785
+ // status -> action -> evidence. `unobserved_advance` is the one state that is
1786
+ // actionable rather than merely informative: it is repairable in place.
1787
+ const action = {
1788
+ unobserved_advance: " Attempting guarded catch-up — replaying the missed merges through the same gate.",
1765
1789
  };
1766
1790
  const refusal = {
1767
1791
  advance_blocked_base_merge: "the base branch was merged forward into the epic branch, which would move the branch's pinned cut point",
@@ -1771,10 +1795,14 @@ function renderScopeFreshnessLines(scope) {
1771
1795
  };
1772
1796
  const lines = [
1773
1797
  headline[scope.freshness_status] ?? "Index freshness is unknown — treat as not fresh.",
1774
- ` lifecycle: ${scope.lifecycle_state ?? "unknown"}`,
1775
- ` Required commit: ${scope.required_commit_sha ?? "none"}`,
1776
- ` Indexed commit: ${scope.indexed_commit_sha ?? "none"}`,
1777
1798
  ];
1799
+ const actionLine = action[scope.freshness_status];
1800
+ if (actionLine !== undefined)
1801
+ lines.push(actionLine);
1802
+ lines.push(` lifecycle: ${scope.lifecycle_state ?? "unknown"}`, ` Required commit: ${scope.required_commit_sha ?? "none"}`, ` Indexed commit: ${scope.indexed_commit_sha ?? "none"}`);
1803
+ if (scope.freshness_status === "unobserved_advance") {
1804
+ lines.push(" Both watermarks agree — at a commit the branch has left behind, which is", " why this is not `fresh`. Run `conduct-epic catch-up <EPIC>` to repair it.");
1805
+ }
1778
1806
  if (scope.blocked_reason !== null) {
1779
1807
  lines.push(` Reason: ${scope.blocked_reason} — ${refusal[scope.blocked_reason] ?? "the server refused this branch advance"}`);
1780
1808
  lines.push(" A human must resolve the branch before the epic can continue.");
@@ -2529,6 +2557,122 @@ export async function runConductEpicRecover(deps, options) {
2529
2557
  ` lease expires: ${recovered.value.lease_expires_at ?? "unknown"}`,
2530
2558
  ]);
2531
2559
  }
2560
+ /**
2561
+ * `conduct-epic catch-up` — replay a scope's MISSED merge observations.
2562
+ *
2563
+ * The verb that rescues an ALREADY-stranded epic. Row 5 of `conduct-epic` calls
2564
+ * the same endpoint automatically when `status` reports `unobserved_advance`, so
2565
+ * the common case self-heals on a tick; this verb exists for the epic that is
2566
+ * stranded right now and should not have to wait for one.
2567
+ *
2568
+ * It is a guarded catch-up, and the wording here is deliberate everywhere it
2569
+ * appears: it replays verified merge evidence through the server's own AC-10
2570
+ * gate. It cannot force anything, and no flag will ever let it.
2571
+ */
2572
+ export async function runConductEpicCatchUp(deps, options) {
2573
+ const accessProbe = await resolveAccess(deps);
2574
+ if (!accessProbe.ok)
2575
+ return emitFailure(deps, options.json, [accessProbe.error]);
2576
+ const checkpointPath = resolveCheckpointPath(deps, await resolveRepoNameForPath(deps), options.epicKey, options.checkpointPath);
2577
+ const target = await resolveLifecycleScope(deps, options, checkpointPath);
2578
+ if (!target.ok)
2579
+ return emitFailure(deps, options.json, [target.reason]);
2580
+ const result = await catchUpIndexScope(accessProbe.access, { scopeId: target.scopeId }, deps.fetchImpl);
2581
+ if (!result.ok) {
2582
+ return emitFailure(deps, options.json, [
2583
+ `The guarded catch-up could not run: ${result.error}`,
2584
+ ]);
2585
+ }
2586
+ const value = result.value;
2587
+ const payload = {
2588
+ ok: value.ok,
2589
+ epic_key: options.epicKey,
2590
+ scope_id: target.scopeId,
2591
+ outcome: value.outcome,
2592
+ reason: value.reason,
2593
+ required_commit_sha: value.required_commit_sha,
2594
+ parse_scheduled: value.parse_scheduled,
2595
+ };
2596
+ const lines = renderCatchUpLines(value, target.scopeId);
2597
+ return value.ok
2598
+ ? emitSuccess(deps, options.json, payload, lines)
2599
+ : emitFailure(deps, options.json, lines, payload);
2600
+ }
2601
+ /**
2602
+ * Render one guarded catch-up outcome as distinct, bounded operator lines.
2603
+ *
2604
+ * Every outcome gets its own sentence rather than collapsing into "it did not
2605
+ * work": the three successes call for different next steps (nothing, poll, poll),
2606
+ * and the refusals call for genuinely different human action — a controlled AC-10
2607
+ * block needs the BRANCH fixed, a history-limit refusal needs a fresh scope, and
2608
+ * an `unavailable` needs the provider to come back.
2609
+ */
2610
+ function renderCatchUpLines(value, scopeId) {
2611
+ const pin = ` Required commit: ${value.required_commit_sha ?? "unknown"}`;
2612
+ switch (value.outcome) {
2613
+ case "repaired":
2614
+ return [
2615
+ `Guarded catch-up repaired index scope ${scopeId}.`,
2616
+ " Every replayed merge passed the AC-10 gate.",
2617
+ pin,
2618
+ value.parse_scheduled
2619
+ ? " A re-parse is scheduled. Poll `conduct-epic status` until freshness reads `fresh`."
2620
+ : " No re-parse was scheduled; poll `conduct-epic status` for the scope's own state.",
2621
+ ];
2622
+ case "already_current":
2623
+ return [
2624
+ `Index scope ${scopeId} is already current. Nothing was written.`,
2625
+ pin,
2626
+ ];
2627
+ case "parse_scheduled":
2628
+ return [
2629
+ `Index scope ${scopeId} was already pinned at its branch head; no advance was needed.`,
2630
+ value.parse_scheduled
2631
+ ? " A re-parse is scheduled. Poll `conduct-epic status` until freshness reads `fresh`."
2632
+ : ` No re-parse was scheduled (${value.reason ?? "the scope is not schedulable right now"}).`,
2633
+ pin,
2634
+ ];
2635
+ case "race_lost":
2636
+ return [
2637
+ `Another observation already owns this scope's target; the catch-up did nothing.`,
2638
+ " This is the safe outcome of a race, not a failure. Poll `conduct-epic status`.",
2639
+ pin,
2640
+ ];
2641
+ case "blocked":
2642
+ return [
2643
+ `Guarded catch-up REFUSED to advance index scope ${scopeId}.`,
2644
+ ` Reason: ${value.reason ?? "the server refused this branch advance"}`,
2645
+ " Both commit watermarks are unchanged and no parse was scheduled.",
2646
+ " A human must resolve the branch itself; there is no override.",
2647
+ pin,
2648
+ ];
2649
+ case "history_limit_exceeded":
2650
+ return [
2651
+ `Index scope ${scopeId} is too far behind its epic branch to replay.`,
2652
+ " Refusing rather than replaying unbounded history. Cut a fresh scope.",
2653
+ pin,
2654
+ ];
2655
+ case "refused_lifecycle":
2656
+ return [
2657
+ `Index scope ${scopeId} cannot take a catch-up in its current state.`,
2658
+ ` Reason: ${value.reason ?? "unknown"}`,
2659
+ pin,
2660
+ ];
2661
+ case "unavailable":
2662
+ return [
2663
+ `The guarded catch-up could not read the epic branch for scope ${scopeId}.`,
2664
+ ` Reason: ${value.reason ?? "unknown"}`,
2665
+ " An unread branch is never a current one. Retry once the provider recovers.",
2666
+ pin,
2667
+ ];
2668
+ default:
2669
+ return [
2670
+ `The guarded catch-up did not repair index scope ${scopeId}.`,
2671
+ ` Outcome: ${value.outcome ?? "unknown"} Reason: ${value.reason ?? "unknown"}`,
2672
+ pin,
2673
+ ];
2674
+ }
2675
+ }
2532
2676
  /** `conduct-epic retire` — start the retention clock; delete nothing. */
2533
2677
  export async function runConductEpicRetire(deps, options) {
2534
2678
  const accessProbe = await resolveAccess(deps);
@@ -2677,5 +2821,7 @@ export async function runConductEpicCli(argv, overrides = {}) {
2677
2821
  return runConductEpicRetire(deps, options);
2678
2822
  case "reclaim":
2679
2823
  return runConductEpicReclaim(deps, options);
2824
+ case "catch-up":
2825
+ return runConductEpicCatchUp(deps, options);
2680
2826
  }
2681
2827
  }
@@ -614,11 +614,11 @@ export async function runCheckMessagesCommand(argv) {
614
614
  const DIAGNOSTIC_BOOL_FLAGS = new Set(["--json", "--help"]);
615
615
  const DOCTOR_BOOL_FLAGS = new Set([...DIAGNOSTIC_BOOL_FLAGS, "--no-deny-probe"]);
616
616
  /**
617
- * Run the strictly read-only `doctor` command. Combines ledger health, git hook
618
- * health, and epic-tick schedule enablement status. `--json` emits the full
619
- * report with `epic_tick` alongside `git_hooks` at the top level.
620
- * `--no-deny-probe` skips the deny-enforcement preflight (no headless agent
621
- * spawn); the report then carries an explicit skipped state, never enforced.
617
+ * Run the strictly read-only `doctor` command. Combines ledger health and git
618
+ * hook health. `--json` emits the full report with `git_hooks` alongside the
619
+ * ledger fields at the top level. `--no-deny-probe` skips the deny-enforcement
620
+ * preflight (no headless agent spawn); the report then carries an explicit
621
+ * skipped state, never enforced.
622
622
  */
623
623
  export async function runDoctorCommand(argv, deps = {}) {
624
624
  const { bools } = tokenizeFlags(argv, new Set(), DOCTOR_BOOL_FLAGS);
@@ -626,11 +626,10 @@ export async function runDoctorCommand(argv, deps = {}) {
626
626
  console.log(getConductorUsage());
627
627
  return 0;
628
628
  }
629
- // scheduleDeps omitted: buildConductorDoctorReport lazily loads schedule-run.
630
629
  // A caller may inject deps (e.g. a fake deny inspector in tests) to stay hermetic.
631
630
  const report = await buildConductorDoctorReport(bools.has("--no-deny-probe") ? { ...deps, skipDenyProbe: true } : deps);
632
631
  if (bools.has("--json")) {
633
- console.log(JSON.stringify({ ...report.ledger, git_hooks: report.git_hooks, epic_tick: report.epic_tick, mcp_profile: report.mcp_profile, native_ledger: report.native_ledger, deny_enforcement: report.deny_enforcement }));
632
+ console.log(JSON.stringify({ ...report.ledger, git_hooks: report.git_hooks, mcp_profile: report.mcp_profile, native_ledger: report.native_ledger, deny_enforcement: report.deny_enforcement }));
634
633
  return 0;
635
634
  }
636
635
  console.log(formatConductorDoctorReport(report));
@@ -1,11 +1,10 @@
1
1
  /**
2
2
  * Combined, strictly read-only conductor doctor report (BAPI-395, BAPI-418).
3
3
  *
4
- * Extends the existing SQLite ledger health report with local git hook health
5
- * and Epic Supervisor schedule enablement status. Missing hooks and non-worktree
6
- * directories are reported as a degraded OPTIONAL capability never a fatal
7
- * failure. This module performs NO writes: no hook installation, no schema
8
- * migration, no event emission, and no scheduler unit creation.
4
+ * Extends the existing SQLite ledger health report with local git hook health.
5
+ * Missing hooks and non-worktree directories are reported as a degraded
6
+ * OPTIONAL capability never a fatal failure. This module performs NO writes:
7
+ * no hook installation, no schema migration, and no event emission.
9
8
  */
10
9
  import { spawnSync } from "node:child_process";
11
10
  import { existsSync, readFileSync } from "node:fs";
@@ -179,84 +178,6 @@ export async function collectConductorNativeLedgerSafe(deps = {}) {
179
178
  };
180
179
  }
181
180
  }
182
- /**
183
- * Inspect the local schedule metadata store for a registered epic-tick schedule.
184
- * Strictly read-only: composes orchestrateScheduleList (read-only list path) and
185
- * never creates, updates, or deletes any unit. A missing or erroring schedule is
186
- * mapped to a degraded state, never a thrown exception.
187
- *
188
- * Schedule-run is loaded lazily via dynamic import to avoid eagerly pulling its
189
- * node:fs/promises dependency into the module graph when doctor.ts is imported.
190
- */
191
- export async function inspectEpicTickSchedule(deps, orchestrateListOverride) {
192
- try {
193
- // Lazily import schedule-run to avoid pulling node:fs/promises into the graph
194
- // when doctor.ts is first imported (which would break node:fs mocks in tests).
195
- const schedRun = orchestrateListOverride
196
- ? null
197
- : await import("../schedule-run.js");
198
- const doList = orchestrateListOverride
199
- ? orchestrateListOverride
200
- : (d) => schedRun.orchestrateScheduleList({ json: false }, d);
201
- const resolvedDeps = orchestrateListOverride
202
- ? deps
203
- : (deps ?? schedRun.createDefaultScheduleRunDeps());
204
- const commandLabel = schedRun
205
- ? schedRun.scheduleCommandLabel
206
- : (m) => m.command ?? (m.idea_file ? "full-automation" : "(unknown)");
207
- const runStatus = schedRun
208
- ? schedRun.latestRunStatus
209
- : (m) => {
210
- const h = m.run_history;
211
- return h && h.length > 0 ? h[h.length - 1].status : "";
212
- };
213
- const report = await doList(resolvedDeps);
214
- const entry = report.entries.find((e) => commandLabel(e.metadata) === "epic-tick");
215
- if (!entry) {
216
- // The healthy state. Epic Conductor v2 reconciles server-side; there is
217
- // nothing for an operator to schedule locally.
218
- return {
219
- registered: false,
220
- backend: null,
221
- next_fire_iso: null,
222
- latest_run_status: null,
223
- degraded: false,
224
- warnings: [],
225
- };
226
- }
227
- // A registered epic-tick schedule is a dead timer: the v1 command throws
228
- // EPIC_TICK_V1_FROZEN on every fire (see conductor/errors.ts). Anyone who has
229
- // one registered followed the old advice and is now firing a no-op on a timer.
230
- const m = entry.metadata;
231
- const latest = runStatus(m);
232
- return {
233
- registered: true,
234
- backend: m.backend ?? null,
235
- next_fire_iso: m.run_at_iso ?? null,
236
- latest_run_status: latest || null,
237
- degraded: true,
238
- warnings: [
239
- "An epic-tick schedule is registered, but the v1 `conductor epic-tick` " +
240
- "path is frozen (EPIC_TICK_V1_FROZEN) — it advances nothing. Cancel it: " +
241
- `\`npx -y ${MCP_PACKAGE_NAME} schedule-run cancel --id ` +
242
- `${entry.metadata.id ?? "<id>"}\`. ` +
243
- "Epic Conductor v2 reconciles server-side; run jobs locally with " +
244
- `\`npx -y ${MCP_PACKAGE_NAME} executor --repo <name>\`.`,
245
- ],
246
- };
247
- }
248
- catch (err) {
249
- const msg = err instanceof Error ? err.message : String(err);
250
- return {
251
- registered: false,
252
- backend: null,
253
- next_fire_iso: null,
254
- latest_run_status: null,
255
- degraded: true,
256
- warnings: [`Failed to inspect epic-tick schedule: ${msg}`],
257
- };
258
- }
259
- }
260
181
  /**
261
182
  * Inspect the BRIDGE_MCP_PROFILE environment variable and warn when a
262
183
  * conductor/epic context is detected but the resolved groups omit `conductor`.
@@ -270,14 +191,12 @@ export async function inspectEpicTickSchedule(deps, orchestrateListOverride) {
270
191
  * operator to REPLACE the variable — which would have destroyed the worker's
271
192
  * other groups. The remediation now says ADD.
272
193
  */
273
- export function inspectMcpProfile(env, epicTick) {
194
+ export function inspectMcpProfile(env) {
274
195
  const raw = env.BRIDGE_MCP_PROFILE;
275
196
  const raw_profile = raw === undefined ? MCP_PROFILE_UNSET : raw;
276
197
  const groups = resolveProfiles(raw);
277
198
  const active_groups = Array.from(groups);
278
- const conductor_context_detected = env.BAPI_CONDUCTOR_ENABLED === "1" ||
279
- env.BAPI_CONDUCTOR_ENABLED === "true" ||
280
- epicTick.registered;
199
+ const conductor_context_detected = env.BAPI_CONDUCTOR_ENABLED === "1" || env.BAPI_CONDUCTOR_ENABLED === "true";
281
200
  const degraded = conductor_context_detected && !groups.has("conductor");
282
201
  const warnings = [];
283
202
  if (degraded) {
@@ -350,13 +269,12 @@ export function inspectLocalMerge(runCommand) {
350
269
  }
351
270
  /**
352
271
  * Build the combined read-only doctor report. Composes the existing ledger
353
- * doctor, git hook inspection, and the epic-tick schedule enablement check.
354
- * Never mutates the ledger, the hooks, the schema, or the OS scheduler.
272
+ * doctor and the git hook inspection. Never mutates the ledger, the hooks, or
273
+ * the schema.
355
274
  */
356
275
  export async function buildConductorDoctorReport(deps = {}) {
357
276
  const inspectHooks = deps.inspectHooks ?? inspectConductorGitHooks;
358
- const epicTick = await inspectEpicTickSchedule(deps.scheduleDeps, deps.orchestrateList);
359
- const mcp_profile = inspectMcpProfile(deps.env ?? process.env, epicTick);
277
+ const mcp_profile = inspectMcpProfile(deps.env ?? process.env);
360
278
  // Resolve the ledger once so the native-load probe reflects the same report —
361
279
  // through the SHARED collector (BAPI-775), so the installer's read-only probe
362
280
  // and this report cannot diverge in their interpretation of the binding.
@@ -371,7 +289,6 @@ export async function buildConductorDoctorReport(deps = {}) {
371
289
  return {
372
290
  ledger,
373
291
  git_hooks: inspectHooks(deps.hooksDeps),
374
- epic_tick: epicTick,
375
292
  mcp_profile,
376
293
  local_merge: inspectLocalMerge(deps.runCommand),
377
294
  native_ledger,
@@ -409,12 +326,12 @@ async function inspectDenyEnforcementSafe(inspect) {
409
326
  }
410
327
  }
411
328
  /**
412
- * Render the combined doctor report as human-readable text: ledger health,
413
- * git hooks section, and an Epic Supervisor Schedule section using semantic
414
- * status tags consistent with the git hooks section's visual hierarchy.
329
+ * Render the combined doctor report as human-readable text: ledger health and
330
+ * a git hooks section, using semantic status tags for a consistent visual
331
+ * hierarchy across sections.
415
332
  */
416
333
  export function formatConductorDoctorReport(report) {
417
- const { ledger, git_hooks, epic_tick, mcp_profile, local_merge, native_ledger, deny_enforcement, claude_login } = report;
334
+ const { ledger, git_hooks, mcp_profile, local_merge, native_ledger, deny_enforcement, claude_login } = report;
418
335
  const lines = [
419
336
  "Conductor ledger doctor",
420
337
  "───────────────────────",
@@ -458,26 +375,6 @@ export function formatConductorDoctorReport(report) {
458
375
  lines.push(` - ${w}`);
459
376
  }
460
377
  lines.push("");
461
- lines.push("Epic Supervisor Schedule (v1 epic-tick — frozen)");
462
- lines.push("───────────────────────────────────────────────");
463
- // Inverted on purpose: no schedule is the healthy state. Epic Conductor v2
464
- // reconciles server-side, so a registered epic-tick unit is a dead timer.
465
- const registeredTag = epic_tick.registered
466
- ? "[WARNING] registered — dead timer, remove it"
467
- : "[SUCCESS] none registered (v1 is frozen)";
468
- lines.push(`epic-tick schedule: ${registeredTag}`);
469
- if (epic_tick.registered) {
470
- lines.push(`backend: ${epic_tick.backend ?? "n/a"}`);
471
- lines.push(`next fire: ${epic_tick.next_fire_iso ?? "n/a"}`);
472
- lines.push(`latest run status: ${epic_tick.latest_run_status ?? "n/a"}`);
473
- }
474
- lines.push(`degraded: ${epic_tick.degraded}`);
475
- if (epic_tick.warnings.length > 0) {
476
- lines.push("epic-tick warnings:");
477
- for (const w of epic_tick.warnings)
478
- lines.push(` - ${w}`);
479
- }
480
- lines.push("");
481
378
  lines.push("Epic Conductor v2 reconciles epics server-side — nothing to schedule");
482
379
  lines.push("locally. To execute claimed jobs on this machine, run:");
483
380
  lines.push(` npx -y ${MCP_PACKAGE_NAME} executor --repo <name>`);