@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
package/build/doctor.js CHANGED
@@ -22,6 +22,8 @@ import { createDefaultStartTicketsDeps } from "./start-tickets.js";
22
22
  import { VERSION, BUILD_COMMIT } from "./version.generated.js";
23
23
  import { collectInstallStatusChecks, formatInstallStatusReport, formatInstallStatusFallbackReport, resolveInstallDoctorTarget, } from "./install-doctor.js";
24
24
  import { codexHome, inspectBridgeCodexProfile } from "./codex-bridge-profile.js";
25
+ import { resolveRoutingHost } from "./model-routing-host.js";
26
+ import { modelRoutingPreferencesPath, readModelRoutingPreference } from "./model-routing-preferences.js";
25
27
  import { inspectExecutorAgentAdapter, inspectionForFailedResolution, } from "./agent-launchers/executor-adapter-inspection.js";
26
28
  import { resolveExecutorAgentAdapter } from "./agent-launchers/executor-adapter-registry.js";
27
29
  import { parseDefaultOnEnvFlag } from "./env-flags.js";
@@ -29,15 +31,13 @@ import { createBridgeApiUrls } from "./bridge-api-urls.js";
29
31
  import { probeToolSurface } from "./tool-surface-gating.js";
30
32
  import { resolveBapiCredentials } from "./credential-store.js";
31
33
  import { resolveConductorBridgeApiAccess } from "./conductor/bridge-api-client.js";
32
- import { getLiveRepositoryConductors, getParseDispatcherHealth, } from "./implement-epic/bridge-client.js";
33
- import { resolveConductEpicStateDirectory } from "./implement-epic/checkpoint-store.js";
34
- import { isConductEpicLockOwnerAlive, parseConductEpicLock, } from "./implement-epic/lock.js";
34
+ import { getLiveRepositoryConductors, getParseDispatcherHealth, } from "./epic-shared/bridge-client.js";
35
35
  import { resolveRequiredStartTicketsRepoName } from "./start-tickets-repo.js";
36
36
  import { collectWorkerContainmentDiagnostic, formatWorkerContainmentDiagnosticReport, } from "./worker-containment-diagnostic.js";
37
37
  import { BRIDGE_PACKAGE_NAME, describeLauncherReason, DUPLICATE_REGISTRATION_GUIDANCE, inspectLauncherConfigs, parseLauncherPin as parseSharedLauncherPin, } from "./launcher-config-inspection.js";
38
38
  import { MCP_SERVER_NAME, MCP_PACKAGE_NAME, } from "./mcp-identity.js";
39
39
  import { DEFAULT_AGENT_NAME, resolveAgentSpec, isAgentName, formatValidAgentNames, } from "./agent-registry.js";
40
- import { getDoctorPrereqDescriptors, probePrerequisite, resolveWorktrunkBinary, } from "./start-tickets-prereqs.js";
40
+ import { getDoctorPrereqDescriptors, probePrerequisite, } from "./start-tickets-prereqs.js";
41
41
  import { resolveProfiles } from "./mcp-profile.js";
42
42
  import { executorLaunchdDirForHome, executorSystemdDirForHome, executorIdFromLaunchdFilename, executorIdFromSystemdFilename, inspectExecutorServiceArtifact, EXECUTOR_SERVICE_BASE_URL_ENV, } from "./executor/index.js";
43
43
  import { DEFAULT_BAPI_BASE_URL } from "./executor/credentials.js";
@@ -1311,175 +1311,6 @@ export function formatPlaneCredentialDiagnosticReport(diagnostic) {
1311
1311
  lines.push(" no .bridge/plane/ directory, persists nothing, and starts no plane member.");
1312
1312
  return lines.join("\n");
1313
1313
  }
1314
- /**
1315
- * Collect implement-epic readiness, read-only.
1316
- *
1317
- * The credential is resolved only to establish that it CAN be, and the resolved
1318
- * access object is discarded immediately: only the repo name and a boolean
1319
- * survive into the returned diagnostic, so no later formatting change can print
1320
- * a key it never received.
1321
- */
1322
- export async function collectConductEpicDiagnostic(deps) {
1323
- const isAlive = deps.isProcessAlive ?? isConductEpicLockOwnerAlive;
1324
- let ghPresent = false;
1325
- let ghAuthenticated = false;
1326
- try {
1327
- const version = await deps.runCommand("gh", ["--version"], { cwd: deps.cwd });
1328
- ghPresent = version.exitCode === 0;
1329
- }
1330
- catch {
1331
- ghPresent = false;
1332
- }
1333
- if (ghPresent) {
1334
- try {
1335
- const auth = await deps.runCommand("gh", ["auth", "status"], { cwd: deps.cwd });
1336
- ghAuthenticated = auth.exitCode === 0;
1337
- }
1338
- catch {
1339
- ghAuthenticated = false;
1340
- }
1341
- }
1342
- const worktrunkBinary = resolveWorktrunkBinary(deps.platform, deps.env);
1343
- let worktrunkAvailable = false;
1344
- try {
1345
- const probe = await deps.runCommand(worktrunkBinary, ["--version"], { cwd: deps.cwd });
1346
- worktrunkAvailable = probe.exitCode === 0;
1347
- }
1348
- catch {
1349
- worktrunkAvailable = false;
1350
- }
1351
- const resolveAccess = deps.resolveAccess ?? resolveConductorBridgeApiAccess;
1352
- let repo = null;
1353
- let bridgeCredentialResolved = false;
1354
- try {
1355
- const result = await resolveAccess({
1356
- env: deps.env,
1357
- cwd: deps.cwd,
1358
- homedir: deps.homedir,
1359
- platform: deps.platform,
1360
- readFile: deps.readFile,
1361
- stat: deps.stat,
1362
- });
1363
- if (result.ok) {
1364
- repo = result.access.repoName;
1365
- bridgeCredentialResolved = true;
1366
- }
1367
- }
1368
- catch {
1369
- bridgeCredentialResolved = false;
1370
- }
1371
- let stateDirectory = null;
1372
- let stateDirectoryReadable = false;
1373
- const checkpoints = [];
1374
- const locks = [];
1375
- if (repo !== null) {
1376
- stateDirectory = resolveConductEpicStateDirectory(repo, {
1377
- env: deps.env,
1378
- homedir: deps.homedir,
1379
- });
1380
- let entries = [];
1381
- try {
1382
- entries = await deps.readdir(stateDirectory);
1383
- stateDirectoryReadable = true;
1384
- }
1385
- catch {
1386
- stateDirectoryReadable = false;
1387
- }
1388
- for (const entry of entries) {
1389
- if (entry.endsWith(".json"))
1390
- checkpoints.push(entry);
1391
- if (!entry.endsWith(".lock"))
1392
- continue;
1393
- const epic = entry.slice(0, -".lock".length);
1394
- let raw;
1395
- try {
1396
- raw = await deps.readFile(path.join(stateDirectory, entry));
1397
- }
1398
- catch {
1399
- locks.push({ epic, owner_pid: null, host: null, alive: null, problem: "unreadable" });
1400
- continue;
1401
- }
1402
- const owner = parseConductEpicLock(raw);
1403
- if (owner === null) {
1404
- locks.push({ epic, owner_pid: null, host: null, alive: null, problem: "malformed or unsupported version" });
1405
- continue;
1406
- }
1407
- // Liveness is only meaningful for a lock taken on THIS host.
1408
- const alive = owner.host === deps.env.HOSTNAME || owner.host === os.hostname()
1409
- ? isAlive(owner.owner_pid)
1410
- : null;
1411
- locks.push({ epic, owner_pid: owner.owner_pid, host: owner.host, alive });
1412
- }
1413
- }
1414
- return {
1415
- ghPresent,
1416
- ghAuthenticated,
1417
- worktrunkBinary,
1418
- worktrunkAvailable,
1419
- repo,
1420
- bridgeCredentialResolved,
1421
- stateDirectory,
1422
- stateDirectoryReadable,
1423
- checkpoints,
1424
- locks,
1425
- };
1426
- }
1427
- /** Render the implement-epic section. Advisory: never changes the exit code. */
1428
- export function formatConductEpicDiagnosticReport(diagnostic) {
1429
- const lines = ["", "implement-epic readiness (advisory)"];
1430
- lines.push(diagnostic.ghAuthenticated
1431
- ? " OK gh: present and authenticated."
1432
- : diagnostic.ghPresent
1433
- ? " MISSING gh: present but NOT authenticated. Run `gh auth login`."
1434
- : " MISSING gh: not found on PATH. `implement-epic init` and `status` need it.");
1435
- lines.push(diagnostic.worktrunkAvailable
1436
- ? ` OK Worktrunk: '${diagnostic.worktrunkBinary}' resolves.`
1437
- : ` MISSING Worktrunk: '${diagnostic.worktrunkBinary}' not found (honors BAPI_WORKTRUNK_BIN).`);
1438
- if (diagnostic.repo === null) {
1439
- lines.push(" MISSING Bridge identity: the repository could not be resolved. Set BAPI_REPO_NAME\n" +
1440
- " or add a valid .bridge/config at the repository root.");
1441
- }
1442
- else if (diagnostic.bridgeCredentialResolved) {
1443
- lines.push(` OK Bridge credential for ${diagnostic.repo}: resolved.`);
1444
- }
1445
- else {
1446
- lines.push(` MISSING Bridge credential for ${diagnostic.repo}: NOT resolved.`);
1447
- }
1448
- if (diagnostic.stateDirectory === null) {
1449
- lines.push(" SKIP Checkpoint directory: not resolved (no repository identity).");
1450
- }
1451
- else if (!diagnostic.stateDirectoryReadable) {
1452
- lines.push(` SKIP Checkpoint directory ${diagnostic.stateDirectory}: absent or unreadable\n` +
1453
- " (normal before the first `implement-epic init`).");
1454
- }
1455
- else {
1456
- lines.push(` OK Checkpoint directory ${diagnostic.stateDirectory}: readable, ` +
1457
- `${diagnostic.checkpoints.length} checkpoint(s).`);
1458
- }
1459
- if (diagnostic.locks.length === 0) {
1460
- lines.push(" OK Locks: none held.");
1461
- }
1462
- else {
1463
- for (const lock of diagnostic.locks) {
1464
- if (lock.problem !== undefined) {
1465
- lines.push(` WARN Lock ${lock.epic}: ${lock.problem}. Inspect it by hand.`);
1466
- }
1467
- else if (lock.alive === false) {
1468
- lines.push(` WARN Lock ${lock.epic}: owner pid ${lock.owner_pid} on ${lock.host} is gone (stale).\n` +
1469
- " The next mutating verb recovers it automatically.");
1470
- }
1471
- else if (lock.alive === null) {
1472
- lines.push(` WARN Lock ${lock.epic}: taken on host ${lock.host}; liveness is unknowable here.`);
1473
- }
1474
- else {
1475
- lines.push(` OK Lock ${lock.epic}: held by live pid ${lock.owner_pid}.`);
1476
- }
1477
- }
1478
- }
1479
- lines.push(" Read-only: this section probes commands and reads local state. It recovers no");
1480
- lines.push(" lock, rewrites no checkpoint, and never changes the exit code.");
1481
- return lines.join("\n");
1482
- }
1483
1314
  /**
1484
1315
  * The transitional local helpers BAPI-1102 retires.
1485
1316
  *
@@ -1501,14 +1332,10 @@ export const LEGACY_DRIVE_HELPER_RELATIVE_PATHS = [
1501
1332
  /**
1502
1333
  * Collect the operational advisories, read-only.
1503
1334
  *
1504
- * Local pilot conductors are identified from the SAME checkpoint-directory and
1505
- * lock inspection `implement-epic readiness` uses — `parseConductEpicLock` plus the
1506
- * injected liveness seam — never from the process table. Only a lock proven live
1507
- * on THIS host counts; malformed, unreadable, dead, and remote-host locks are
1508
- * excluded from the live set, with the indeterminate ones counted separately.
1335
+ * Concurrent conductors and parse-dispatcher health are read from two read-only
1336
+ * Bridge GETs; no local process table or lock directory is inspected.
1509
1337
  */
1510
1338
  export async function collectOperationalAdvisories(deps) {
1511
- const isAlive = deps.isProcessAlive ?? isConductEpicLockOwnerAlive;
1512
1339
  const now = deps.now ?? (() => new Date());
1513
1340
  const resolveAccess = deps.resolveAccess ?? resolveConductorBridgeApiAccess;
1514
1341
  let dispatcher = null;
@@ -1542,63 +1369,6 @@ export async function collectOperationalAdvisories(deps) {
1542
1369
  conductors.push(...live.value.conductors);
1543
1370
  }
1544
1371
  }
1545
- // Local pilot locks. Read through the same path the readiness section uses; a
1546
- // failure to read the directory is silence, not a claim that none are held.
1547
- let indeterminateLocks = 0;
1548
- if (repo !== null) {
1549
- const stateDirectory = resolveConductEpicStateDirectory(repo, {
1550
- env: deps.env,
1551
- homedir: deps.homedir,
1552
- });
1553
- let entries = [];
1554
- try {
1555
- entries = await deps.readdir(stateDirectory);
1556
- }
1557
- catch {
1558
- entries = [];
1559
- }
1560
- for (const entry of entries) {
1561
- if (!entry.endsWith(".lock"))
1562
- continue;
1563
- const epic = entry.slice(0, -".lock".length);
1564
- let raw;
1565
- try {
1566
- raw = await deps.readFile(path.join(stateDirectory, entry));
1567
- }
1568
- catch {
1569
- indeterminateLocks += 1;
1570
- continue;
1571
- }
1572
- const owner = parseConductEpicLock(raw);
1573
- if (owner === null) {
1574
- indeterminateLocks += 1;
1575
- continue;
1576
- }
1577
- const local = owner.host === deps.env.HOSTNAME || owner.host === os.hostname();
1578
- if (!local) {
1579
- indeterminateLocks += 1;
1580
- continue;
1581
- }
1582
- if (!isAlive(owner.owner_pid))
1583
- continue;
1584
- // A pilot lock and a v2 run are two DISTINCT conductor authorities. Even
1585
- // when both name the same epic they are not deduplicated — that pairing is
1586
- // precisely the overlap an operator most needs to see.
1587
- //
1588
- // The key comes from a FILENAME, so it is bounded against the epic-key shape
1589
- // before it is rendered; an unrecognized name reports `unknown` rather than
1590
- // putting arbitrary filesystem text into a diagnostic line.
1591
- conductors.push({
1592
- kind: "pilot",
1593
- epicKey: validateTicketKey(epic, "epic key").ok ? epic : null,
1594
- state: "lock held",
1595
- livenessSource: "lock",
1596
- });
1597
- }
1598
- if (conductors.length > 0 && conductorObservation !== "observed") {
1599
- conductorObservation = "observed";
1600
- }
1601
- }
1602
1372
  // BAPI-1102 — a pure path-presence check, using the SAME injected `stat` and
1603
1373
  // `homedir` seams every other advisory here uses, so it stays deterministic
1604
1374
  // under test and reaches no real filesystem in one. A `stat` that throws is
@@ -1620,7 +1390,6 @@ export async function collectOperationalAdvisories(deps) {
1620
1390
  dispatcher,
1621
1391
  conductorObservation,
1622
1392
  conductors,
1623
- indeterminateLocks,
1624
1393
  legacyDriveHelpers,
1625
1394
  };
1626
1395
  }
@@ -1658,10 +1427,6 @@ export function formatOperationalAdvisoryReport(diagnostic) {
1658
1427
  lines.push(" A distinct-epic overlap is informational and may be legitimate: separately\n" +
1659
1428
  " scoped epics do not share transition authority.");
1660
1429
  }
1661
- if (diagnostic.indeterminateLocks > 0) {
1662
- lines.push(` ${diagnostic.indeterminateLocks} local lock(s) could not be judged (remote host,\n` +
1663
- " malformed, or unreadable) and are excluded from the live count.");
1664
- }
1665
1430
  // BAPI-1102. A WARN, never a failure, and deliberately separate from every
1666
1431
  // readiness signal above: a stale helper says nothing about whether this
1667
1432
  // repository's conductor works, so classifying it as a readiness problem would
@@ -1677,7 +1442,7 @@ export function formatOperationalAdvisoryReport(diagnostic) {
1677
1442
  " doctor performed a PATH-PRESENCE check only: it did not run, read, or\n" +
1678
1443
  " modify either file, and it cannot delete them for you.");
1679
1444
  }
1680
- lines.push(" Read-only: this section issues two Bridge GETs and reads local lock state. It");
1445
+ lines.push(" Read-only: this section issues two Bridge GETs and reads no local state. It");
1681
1446
  lines.push(" runs NO command probe, repairs no scheduler, and never changes the exit code.");
1682
1447
  return lines.join("\n");
1683
1448
  }
@@ -1752,6 +1517,13 @@ export async function runDoctorCli(argv, overrides = {}) {
1752
1517
  ? "Codex Bridge approval profile is an older unmodified managed version; the next spawned worker refreshes it before launch."
1753
1518
  : `Codex Bridge approval profile ${profile.status}; spawned workers use existing Codex approvals. Run install --tools=codex --codex-profile=on to opt in.`);
1754
1519
  }
1520
+ // Read-only personal routing diagnostics. The installer marker is a hint;
1521
+ // an initialized MCP client identity takes precedence during real requests.
1522
+ const routingHost = resolveRoutingHost(undefined, deps.env.BAPI_MCP_HOST_FAMILY);
1523
+ log(`Model routing host marker: ${routingHost.warning ?? routingHost.family ?? "neutral"}.`);
1524
+ const preferencePath = modelRoutingPreferencesPath(deps.homedir?.() ?? os.homedir());
1525
+ const preference = await readModelRoutingPreference(deps.env.BAPI_REPO_NAME ?? "_doctor_unscoped_", "review_ticket", preferencePath);
1526
+ log(`Personal model routing preference: ${preference.warning ?? preference.source}.`);
1755
1527
  log(formatDoctorPrereqSection(deps.platform, collection));
1756
1528
  // Executor-adapter section (BAPI-781). Advisory with respect to the EXIT CODE
1757
1529
  // — an unusable adapter is reported loudly but does not change doctor's
@@ -1973,55 +1745,8 @@ export async function runDoctorCli(argv, overrides = {}) {
1973
1745
  }));
1974
1746
  }
1975
1747
  }
1976
- // Advisory implement-epic section (BAPI-803). Read-only, and — like every
1977
- // section above — it cannot change the exit code: `implement-epic init` enforces
1978
- // these same invariants at the point where they actually block, and someone who
1979
- // never conducts an epic should not see a failing doctor because of it.
1980
- //
1981
- // Skipped entirely on an unsupported platform. This section is the only
1982
- // advisory one that runs COMMAND probes (`gh`, Worktrunk), and doctor's
1983
- // standing contract is that an unsupported platform probes nothing at all —
1984
- // reporting on tooling for a `implement-epic` that cannot run there would buy
1985
- // nothing and break that contract.
1986
- if (overrides.conductEpic !== false && collection.ok) {
1987
- try {
1988
- const injectedFs = deps;
1989
- const conductDeps = {
1990
- env: overrides.conductEpic?.env ?? deps.env,
1991
- cwd: overrides.conductEpic?.cwd ?? deps.cwd,
1992
- platform: overrides.conductEpic?.platform ?? deps.platform,
1993
- homedir: overrides.conductEpic?.homedir ?? injectedFs.homedir ?? os.homedir,
1994
- readFile: overrides.conductEpic?.readFile ??
1995
- injectedFs.readFile ??
1996
- ((p) => readFile(p, "utf-8")),
1997
- stat: overrides.conductEpic?.stat ?? injectedFs.stat ?? ((p) => stat(p)),
1998
- readdir: overrides.conductEpic?.readdir ?? ((p) => readdir(p)),
1999
- runCommand: overrides.conductEpic?.runCommand ?? deps.runCommand,
2000
- resolveAccess: overrides.conductEpic?.resolveAccess,
2001
- isProcessAlive: overrides.conductEpic?.isProcessAlive,
2002
- };
2003
- log(formatConductEpicDiagnosticReport(await collectConductEpicDiagnostic(conductDeps)));
2004
- }
2005
- catch {
2006
- // Any unexpected failure still renders a sanitized advisory section rather
2007
- // than dropping it or leaking exception text.
2008
- log(formatConductEpicDiagnosticReport({
2009
- ghPresent: false,
2010
- ghAuthenticated: false,
2011
- worktrunkBinary: resolveWorktrunkBinary(deps.platform, deps.env),
2012
- worktrunkAvailable: false,
2013
- repo: null,
2014
- bridgeCredentialResolved: false,
2015
- stateDirectory: null,
2016
- stateDirectoryReadable: false,
2017
- checkpoints: [],
2018
- locks: [],
2019
- }));
2020
- }
2021
- }
2022
- // Advisory operational-advisories section (BAPI-963), placed immediately after
2023
- // `implement-epic readiness` because both are read by the same operator at the
2024
- // same moment — "can I conduct?" then "what else is already running?".
1748
+ // Advisory operational-advisories section (BAPI-963). Read by the operator
1749
+ // asking "what is already running?" right after the readiness sections above.
2025
1750
  //
2026
1751
  // Gated on `collection.ok` for the same reason the section above is: an
2027
1752
  // unsupported platform reports nothing. That gate is belt-and-braces here,
@@ -2044,9 +1769,7 @@ export async function runDoctorCli(argv, overrides = {}) {
2044
1769
  injectedFs.readFile ??
2045
1770
  ((p) => readFile(p, "utf-8")),
2046
1771
  stat: overrides.operationalAdvisories?.stat ?? injectedFs.stat ?? ((p) => stat(p)),
2047
- readdir: overrides.operationalAdvisories?.readdir ?? ((p) => readdir(p)),
2048
1772
  resolveAccess: overrides.operationalAdvisories?.resolveAccess,
2049
- isProcessAlive: overrides.operationalAdvisories?.isProcessAlive,
2050
1773
  fetchImpl: overrides.operationalAdvisories?.fetchImpl,
2051
1774
  now: overrides.operationalAdvisories?.now,
2052
1775
  };
@@ -2059,7 +1782,6 @@ export async function runDoctorCli(argv, overrides = {}) {
2059
1782
  dispatcher: null,
2060
1783
  conductorObservation: "unavailable",
2061
1784
  conductors: [],
2062
- indeterminateLocks: 0,
2063
1785
  // Empty, not omitted: a collection that failed has not established
2064
1786
  // that a legacy helper is absent, and a fallback that CLAIMED to have
2065
1787
  // checked would be the one kind of wrong answer worse than silence.
@@ -1,32 +1,29 @@
1
1
  /**
2
2
  * `drive-epic` — the one universal conductor entry point (BAPI-887).
3
3
  *
4
- * Bridge has two conductors and a standing rule that they must never operate on
5
- * the same epic: two transition authorities on one epic wedge it permanently.
6
- *
7
- * - **v2 engine** — bootstrapped by `setup-epic`, which creates a durable
8
- * `epic_run` row, stores a plan DAG, and approves it.
9
- * - **LLM-conductor pilot** — `/loop 5m /conduct-epic <EPIC>`, which creates no
10
- * run row and keeps its whole memory in a local checkpoint plus GitHub.
4
+ * Bridge has one conductor — the **v2 engine**, bootstrapped by `setup-epic`,
5
+ * which creates a durable `epic_run` row, stores a plan DAG, and approves it. The
6
+ * standing rule is that exactly one transition authority ever operates on an
7
+ * epic: two authorities on one epic wedge it permanently.
11
8
  *
12
9
  * Asking a prompt to pick correctly every time is not a control. This subcommand
13
10
  * makes the choice STRUCTURAL: it reads server-side conductor readiness and
14
- * routes to exactly one path, so no prompt names either underlying conductor and
15
- * no prompt can present both. Every ticket-authoring surface hands off to
16
- * `drive-epic` and to nothing else.
11
+ * routes to exactly one path, so no prompt names the underlying conductor. Every
12
+ * ticket-authoring surface hands off to `drive-epic` and to nothing else.
17
13
  *
18
14
  * ## The output invariant
19
15
  *
20
16
  * Every branch — including every error path — emits **at most one** conductor
21
- * invocation. An escalation carries NO alternative conductor command: "v2 failed,
22
- * try the pilot instead" is precisely the dual-authority hazard this subcommand
23
- * exists to remove. {@link assertSingleConductorInvocation} enforces this on the
24
- * way out rather than trusting each branch to have got it right.
17
+ * invocation. A not-ready result or an escalation carries NO alternative
18
+ * conductor command: "v2 failed, try something else instead" is precisely the
19
+ * dual-authority hazard this subcommand exists to remove.
20
+ * {@link assertSingleConductorInvocation} enforces this on the way out rather than
21
+ * trusting each branch to have got it right.
25
22
  *
26
23
  * ## Three-state routing, fail-closed
27
24
  *
28
25
  * - readiness green -> delegate to the `setup-epic` (v2) flow
29
- * - readiness read, not green -> print the pilot instruction
26
+ * - readiness read, not green -> report v2 not-ready, offer no alternative
30
27
  * - readiness UNKNOWN -> escalate to the operator, print no conductor
31
28
  *
32
29
  * Unknown is never treated as not-ready, and selection never happens by
@@ -72,17 +69,16 @@ import { validateBranchName } from "./base-ref.js";
72
69
  // BAPI-806: mcp-identity.ts is the SOLE source of the package-name literal, so
73
70
  // the printed invocation interpolates it rather than repeating it.
74
71
  import { MCP_PACKAGE_NAME } from "./mcp-identity.js";
75
- /** Jira epic key shape. Matches `implement-epic`'s positional rule exactly. */
72
+ /** Jira epic key shape. Matches the epic-key positional rule exactly. */
76
73
  const EPIC_KEY_PATTERN = /^[A-Z]+-[0-9]+$/;
77
- /** The two conductor paths. Derived types below come from this constant. */
78
- export const DRIVE_EPIC_CONDUCTORS = ["v2", "pilot"];
74
+ /** The conductor paths. Derived types below come from this constant. */
75
+ export const DRIVE_EPIC_CONDUCTORS = ["v2"];
79
76
  /**
80
- * THE consolidation-day edit point.
77
+ * The preferred conductor.
81
78
  *
82
- * When readiness is green, v2 is preferred and the pilot is the fallback for
83
- * projects that cannot run it. This is expressed as one named constant rather
84
- * than as branching scattered through the routing logic precisely so that
85
- * eliminating a conductor is a one-line change plus one deleted branch.
79
+ * v2 is the only conductor: when readiness is green it drives the epic, and when
80
+ * it is not the command reports v2 not-ready with no alternative. Expressed as
81
+ * one named constant rather than branching scattered through the routing logic.
86
82
  */
87
83
  export const DRIVE_EPIC_PREFERRED_CONDUCTOR = "v2";
88
84
  /**
@@ -482,10 +478,10 @@ export function parseDriveEpicArgs(argv) {
482
478
  * v2 engine drive an epic at all?", and the answer does not depend on whether
483
479
  * the repository has consented to UNATTENDED operation: an attended v2 run on a
484
480
  * non-consenting repository is a perfectly ordinary, fully supported run.
485
- * Including them would route every such repository to the pilot conductor the
486
- * moment this ticket deployed — a silent, repository-wide change of conductor for
487
- * a reason that has nothing to do with the choice, and the exact opposite of
488
- * AC-3's "nothing changes for an existing run".
481
+ * Including them would report v2 not-ready for every such repository the moment
482
+ * this ticket deployed — a silent, repository-wide refusal for a reason that has
483
+ * nothing to do with readiness, and the exact opposite of AC-3's "nothing
484
+ * changes for an existing run".
489
485
  *
490
486
  * They still gate the COMPOSED bring-up, through
491
487
  * {@link classifyV2Readiness}, which is where an unattended run is actually
@@ -501,8 +497,7 @@ export function selectConductor(readiness) {
501
497
  };
502
498
  }
503
499
  return {
504
- kind: "selected",
505
- conductor: "pilot",
500
+ kind: "not_ready",
506
501
  reason: `the engine path still needs ${missing.map((m) => m.describe).join("; ")}`,
507
502
  };
508
503
  }
@@ -536,51 +531,35 @@ export function buildSetupEpicArgv(options) {
536
531
  ];
537
532
  }
538
533
  /**
539
- * Render the selected path and only the selected path.
540
- *
541
- * Precondition: the caller has already ruled out delegation. The `v2` branch
542
- * reads as "green, but no plan file was supplied", which is only true once
543
- * {@link runDriveEpicCli} has declined to delegate — so route first, render
544
- * second.
534
+ * Render the selected v2 path and only that path.
545
535
  *
546
- * CONSOLIDATION DAY: delete the branch of the conductor being eliminated and
547
- * point {@link DRIVE_EPIC_PREFERRED_CONDUCTOR} at the survivor. Nothing outside
548
- * this file moves.
536
+ * Precondition: the caller has already ruled out delegation and a not-ready
537
+ * result. The `v2` branch reads as "green, but no plan file was supplied", which
538
+ * is only true once {@link runDriveEpicCli} has declined to delegate — so route
539
+ * first, render second.
549
540
  */
550
541
  export function renderConductorHandoff(selection, epicKey) {
551
- if (selection.conductor === "v2") {
552
- const invocation = `npx -y ${MCP_PACKAGE_NAME} drive-epic ${epicKey} --plan-file <path>`;
553
- return {
554
- conductor: "v2",
555
- invocation,
556
- lines: [
557
- `${epicKey}: ${selection.reason}, but no plan file was supplied.`,
558
- "",
559
- "The engine path needs a plan DAG, and nothing derives one from an epic key:",
560
- "the dependency edges and per-ticket touched_files it carries do not exist",
561
- "anywhere else. Produce one, then re-run this command with it.",
562
- "",
563
- ` 1. Run /plan-epic ${epicKey} in an interactive session. It writes the`,
564
- " sidecar to {docs_dir}/epic-plans/{epic_slug}/epic-plan.dag.json.",
565
- " If the epic's tickets already exist, run emit-epic-implementer-bundle",
566
- " finalize instead — it resolves the placeholder keys in an existing",
567
- " sidecar and attaches touched_files.",
568
- "",
569
- ` 2. ${invocation}`,
570
- "",
571
- `That second command bootstraps the run for you; ${SETUP_EPIC_SUBCOMMAND} is not`,
572
- "something you invoke directly.",
573
- ],
574
- };
575
- }
576
- const invocation = `/loop 5m /conduct-epic ${epicKey}`;
542
+ const invocation = `npx -y ${MCP_PACKAGE_NAME} drive-epic ${epicKey} --plan-file <path>`;
577
543
  return {
578
- conductor: "pilot",
544
+ conductor: "v2",
579
545
  invocation,
580
546
  lines: [
581
- `${epicKey}: ${selection.reason}. Drive it from an interactive session with:`,
547
+ `${epicKey}: ${selection.reason}, but no plan file was supplied.`,
548
+ "",
549
+ "The engine path needs a plan DAG, and nothing derives one from an epic key:",
550
+ "the dependency edges and per-ticket touched_files it carries do not exist",
551
+ "anywhere else. Produce one, then re-run this command with it.",
552
+ "",
553
+ ` 1. Run /plan-epic ${epicKey} in an interactive session. It writes the`,
554
+ " sidecar to {docs_dir}/epic-plans/{epic_slug}/epic-plan.dag.json.",
555
+ " If the epic's tickets already exist, run emit-epic-implementer-bundle",
556
+ " finalize instead — it resolves the placeholder keys in an existing",
557
+ " sidecar and attaches touched_files.",
582
558
  "",
583
- ` ${invocation}`,
559
+ ` 2. ${invocation}`,
560
+ "",
561
+ `That second command bootstraps the run for you; ${SETUP_EPIC_SUBCOMMAND} is not`,
562
+ "something you invoke directly.",
584
563
  ],
585
564
  };
586
565
  }
@@ -589,7 +568,7 @@ export function renderConductorHandoff(selection, epicKey) {
589
568
  * output guard and by its test — one list, so a new spelling cannot be added to
590
569
  * the renderer without the guard seeing it.
591
570
  */
592
- export const CONDUCTOR_INVOCATION_TOKENS = ["setup-epic", "conduct-epic"];
571
+ export const CONDUCTOR_INVOCATION_TOKENS = ["setup-epic"];
593
572
  /** The v2 token, named once so prose and the guard cannot drift apart. */
594
573
  const SETUP_EPIC_SUBCOMMAND = CONDUCTOR_INVOCATION_TOKENS[0];
595
574
  /**
@@ -935,9 +914,9 @@ export async function runDriveEpicCli(argv, overrides = {}) {
935
914
  //
936
915
  // BAPI-1054: a supplied plan file is what makes this a BRING-UP decision
937
916
  // rather than a routing one. Everything below runs before `selectConductor`'s
938
- // pilot fallback can be rendered, because a report that is non-green only on
939
- // the runtime facts is not a "use the other conductor" situation — it is a
940
- // plane that has not been started yet.
917
+ // not-ready result can be rendered, because a report that is non-green only on
918
+ // the runtime facts is not a "v2 cannot run" situation — it is a plane that
919
+ // has not been started yet.
941
920
  if (planFile) {
942
921
  // BAPI-1102 — the explicit `--attended` opt-out makes the unattended
943
922
  // prerequisites inapplicable. Both inputs are FLAG checks, not policy
@@ -1061,6 +1040,21 @@ export async function runDriveEpicCli(argv, overrides = {}) {
1061
1040
  awaitPlaneReady: controlPlane.awaitPlaneReady,
1062
1041
  });
1063
1042
  }
1043
+ // No plan file, and readiness is not green: v2 is not ready and there is no
1044
+ // second conductor to offer. Report it as a not-ready refusal on stderr with
1045
+ // exactly one — actually zero — conductor invocation, and a non-zero exit.
1046
+ if (selection.kind === "not_ready") {
1047
+ const refusal = [
1048
+ `${epicKey}: ${selection.reason}.`,
1049
+ "",
1050
+ "The v2 engine is not ready to drive this epic, and there is no alternative",
1051
+ "conductor to fall back to. Resolve conductor readiness (`install-bridge",
1052
+ "conductor`, then `doctor`) and run drive-epic again.",
1053
+ ].join("\n");
1054
+ assertSingleConductorInvocation(refusal);
1055
+ deps.errorLog(refusal);
1056
+ return 1;
1057
+ }
1064
1058
  const handoff = renderConductorHandoff(selection, epicKey);
1065
1059
  const text = handoff.lines.join("\n");
1066
1060
  assertSingleConductorInvocation(text);