@bridge_gpt/mcp-server 0.2.58 → 0.2.60

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 (127) hide show
  1. package/README.md +231 -113
  2. package/build/agent-guidance.generated.js +1 -1
  3. package/build/agent-notes.js +7 -1
  4. package/build/agent-registry.js +15 -0
  5. package/build/agent-utils.js +16 -8
  6. package/build/claude-user-config-doctor.js +1 -35
  7. package/build/codex-bridge-profile.js +1 -1
  8. package/build/commands.generated.js +7 -8
  9. package/build/conductor/bridge-api-client.js +202 -2
  10. package/build/conductor/bring-up-facts.js +1 -1
  11. package/build/conductor/claude-hook.js +16 -16
  12. package/build/conductor/doctor.js +279 -72
  13. package/build/conductor/epic-implementer-cli.js +38 -5
  14. package/build/conductor/epic-reconcile.js +1 -1
  15. package/build/conductor/epic-runtime.js +21 -12
  16. package/build/conductor/errors.js +3 -3
  17. package/build/conductor/file-scope-guard.js +4 -4
  18. package/build/conductor/git-ci-types.js +1 -1
  19. package/build/conductor/git-inspection.js +2 -2
  20. package/build/conductor/install-doctor.js +28 -6
  21. package/build/conductor/local-merge.js +319 -26
  22. package/build/conductor/merge-execution-contract.js +61 -0
  23. package/build/conductor/merge-execution-eligibility.js +129 -0
  24. package/build/conductor/merge-ledger.js +2 -2
  25. package/build/conductor/pr-ci-producer.js +6 -6
  26. package/build/conductor/pr-review-producer.js +2 -2
  27. package/build/conductor/producer-ledger.js +2 -0
  28. package/build/conductor/readiness-sections.js +166 -9
  29. package/build/conductor/readiness.js +97 -16
  30. package/build/conductor/recovery-cli.js +243 -12
  31. package/build/conductor/recovery-operations.js +64 -6
  32. package/build/conductor/repair-contract.js +38 -4
  33. package/build/conductor/spec-review-producer.js +6 -6
  34. package/build/conductor/store.js +10 -10
  35. package/build/conductor/supervisor-config.js +14 -14
  36. package/build/conductor/supervisor-ledger.js +2 -2
  37. package/build/conductor/supervisor-message-relay.js +2 -2
  38. package/build/conductor/worker-ledger-cli.js +19 -19
  39. package/build/conductor-bundle-artifacts.js +2 -2
  40. package/build/connect-bitbucket-api.js +6 -1
  41. package/build/connect-bitbucket.js +398 -88
  42. package/build/credential-store.js +16 -0
  43. package/build/docs.generated.js +2 -2
  44. package/build/doctor.js +199 -303
  45. package/build/drive-epic.js +88 -71
  46. package/build/epic-implementer-bin.js +22 -17
  47. package/build/epic-implementer-claude-hook-bin.js +4 -2
  48. package/build/epic-implementer-env.js +184 -0
  49. package/build/epic-integration-pr.js +2 -2
  50. package/build/executor/ci-rerun-job.js +269 -0
  51. package/build/executor/cli.js +31 -0
  52. package/build/executor/credentials.js +11 -0
  53. package/build/executor/env.js +79 -8
  54. package/build/executor/heartbeat.js +1 -1
  55. package/build/executor/index.js +21 -4
  56. package/build/executor/job-errors.js +3 -3
  57. package/build/executor/job-runner.js +127 -1
  58. package/build/executor/job-types.js +12 -2
  59. package/build/executor/merge-job.js +46 -5
  60. package/build/executor/observation.js +1 -1
  61. package/build/executor/preflight.js +1 -1
  62. package/build/executor/prompt-spec.js +1 -1
  63. package/build/executor/reconcile-overlap-job.js +10 -130
  64. package/build/executor/reconcile-overlap-types.js +1 -1
  65. package/build/executor/runner.js +1 -1
  66. package/build/executor/service-lifecycle.js +136 -0
  67. package/build/executor/service-unit.js +427 -9
  68. package/build/executor/types.js +5 -0
  69. package/build/index.js +320 -342
  70. package/build/init.js +20 -28
  71. package/build/install-bridge-conductor.js +208 -15
  72. package/build/install-bridge.js +555 -37
  73. package/build/mcp-host-entry-adapter.js +6 -0
  74. package/build/mcp-host-targets.js +2 -0
  75. package/build/mcp-install-state.js +42 -4
  76. package/build/mcp-invoke.js +5 -5
  77. package/build/mcp-profile.js +2 -2
  78. package/build/mcp-provisioning.js +2 -2
  79. package/build/mcp-server-invocation.js +3 -3
  80. package/build/merge-pull-request.js +118 -10
  81. package/build/merge-workflow-codeowners.js +224 -0
  82. package/build/merge-workflow-config.js +133 -0
  83. package/build/merge-workflow-drift.js +66 -0
  84. package/build/merge-workflow-install.js +638 -0
  85. package/build/merge-workflow.js +572 -0
  86. package/build/model-routing-host.js +25 -0
  87. package/build/model-routing-preferences.js +99 -0
  88. package/build/pipelines.generated.js +10 -10
  89. package/build/{plan-epic-conductor-eligibility.js → plan-epic-implementer-eligibility.js} +18 -16
  90. package/build/plan-execution-ledger.js +8 -0
  91. package/build/plane/cli.js +24 -2
  92. package/build/plane/member-roster.js +5 -5
  93. package/build/plane/preflight.js +68 -0
  94. package/build/plane/supervisor.js +1 -1
  95. package/build/plane/types.js +5 -5
  96. package/build/readiness-check.js +1 -1
  97. package/build/readme.generated.js +1 -1
  98. package/build/review-tickets.js +4 -4
  99. package/build/run-unit-tests-launcher.js +5 -0
  100. package/build/setup-epic.js +171 -17
  101. package/build/start-tickets-conductor.js +32 -32
  102. package/build/start-tickets-prereqs.js +4 -9
  103. package/build/start-tickets.js +475 -91
  104. package/build/version.generated.js +3 -3
  105. package/docs/CONDUCTOR.md +69 -18
  106. package/docs/install/mcp-tool-integrations.md +2 -1
  107. package/package.json +4 -4
  108. package/pipelines/implement-ticket.json +1 -1
  109. package/pipelines/plan-epic.json +2 -2
  110. package/build/conduct-epic/bridge-client.js +0 -773
  111. package/build/conduct-epic/checkpoint-store.js +0 -542
  112. package/build/conduct-epic/cli.js +0 -3145
  113. package/build/conduct-epic/cut-protocol.js +0 -392
  114. package/build/conduct-epic/lock.js +0 -302
  115. package/build/conduct-epic/pr-state.js +0 -286
  116. package/build/conduct-epic/spawn.js +0 -113
  117. package/build/conductor/cli.js +0 -1278
  118. package/build/conductor-bundle-cli.js +0 -256
  119. package/build/direct-ticket-tools.js +0 -70
  120. package/build/executor/reconcile-overlap-governance.js +0 -129
  121. package/build/implement-epic/checkpoint-store.js +0 -542
  122. package/build/implement-epic/cli.js +0 -3158
  123. package/build/implement-epic/lock.js +0 -302
  124. package/build/implement-epic/pr-state.js +0 -286
  125. package/build/implement-epic/spawn.js +0 -113
  126. /package/build/{implement-epic → epic-shared}/bridge-client.js +0 -0
  127. /package/build/{implement-epic → epic-shared}/cut-protocol.js +0 -0
package/build/doctor.js CHANGED
@@ -10,9 +10,14 @@
10
10
  * found/missing plus the exact per-OS manual install command for each missing one.
11
11
  *
12
12
  * The audience is security-conscious/corporate, so the doctor is STRICTLY
13
- * read-only: no `--fix`, no auto-install, no npm `postinstall`, no terminal
14
- * spawning, no MCP server startup. It only ever runs read-only PATH probes
15
- * (`which`/`where`, `bash --version`, `git rev-parse`) through the injected deps.
13
+ * read-only: no `--fix`, no auto-install, no npm `postinstall`, no interactive
14
+ * terminal spawning, no MCP server startup. Every child process it starts is a
15
+ * QUERY: read-only PATH probes (`which`/`where`, `bash --version`,
16
+ * `git rev-parse`) and, on a host that has generated executor service units, the
17
+ * one read-only session probe that section issues (`loginctl show-user
18
+ * --property=Linger` / `launchctl print gui/<uid>`, BAPI-1137). Nothing here can
19
+ * reach a mutating lifecycle verb — `enable-linger`, `bootstrap`, `start` and
20
+ * their kin exist in this file only as remediation TEXT the operator runs.
16
21
  */
17
22
  import { readdir, readFile, stat } from "fs/promises";
18
23
  import { spawn } from "child_process";
@@ -22,6 +27,8 @@ import { createDefaultStartTicketsDeps } from "./start-tickets.js";
22
27
  import { VERSION, BUILD_COMMIT } from "./version.generated.js";
23
28
  import { collectInstallStatusChecks, formatInstallStatusReport, formatInstallStatusFallbackReport, resolveInstallDoctorTarget, } from "./install-doctor.js";
24
29
  import { codexHome, inspectBridgeCodexProfile } from "./codex-bridge-profile.js";
30
+ import { resolveRoutingHost } from "./model-routing-host.js";
31
+ import { modelRoutingPreferencesPath, readModelRoutingPreference } from "./model-routing-preferences.js";
25
32
  import { inspectExecutorAgentAdapter, inspectionForFailedResolution, } from "./agent-launchers/executor-adapter-inspection.js";
26
33
  import { resolveExecutorAgentAdapter } from "./agent-launchers/executor-adapter-registry.js";
27
34
  import { parseDefaultOnEnvFlag } from "./env-flags.js";
@@ -29,17 +36,15 @@ import { createBridgeApiUrls } from "./bridge-api-urls.js";
29
36
  import { probeToolSurface } from "./tool-surface-gating.js";
30
37
  import { resolveBapiCredentials } from "./credential-store.js";
31
38
  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";
39
+ import { getLiveRepositoryConductors, getParseDispatcherHealth, } from "./epic-shared/bridge-client.js";
35
40
  import { resolveRequiredStartTicketsRepoName } from "./start-tickets-repo.js";
36
41
  import { collectWorkerContainmentDiagnostic, formatWorkerContainmentDiagnosticReport, } from "./worker-containment-diagnostic.js";
37
42
  import { BRIDGE_PACKAGE_NAME, describeLauncherReason, DUPLICATE_REGISTRATION_GUIDANCE, inspectLauncherConfigs, parseLauncherPin as parseSharedLauncherPin, } from "./launcher-config-inspection.js";
38
43
  import { MCP_SERVER_NAME, MCP_PACKAGE_NAME, } from "./mcp-identity.js";
39
44
  import { DEFAULT_AGENT_NAME, resolveAgentSpec, isAgentName, formatValidAgentNames, } from "./agent-registry.js";
40
- import { getDoctorPrereqDescriptors, probePrerequisite, resolveWorktrunkBinary, } from "./start-tickets-prereqs.js";
45
+ import { getDoctorPrereqDescriptors, probePrerequisite, } from "./start-tickets-prereqs.js";
41
46
  import { resolveProfiles } from "./mcp-profile.js";
42
- import { executorLaunchdDirForHome, executorSystemdDirForHome, executorIdFromLaunchdFilename, executorIdFromSystemdFilename, inspectExecutorServiceArtifact, EXECUTOR_SERVICE_BASE_URL_ENV, } from "./executor/index.js";
47
+ import { executorLaunchdDirForHome, executorSystemdDirForHome, executorIdFromLaunchdFilename, executorIdFromSystemdFilename, inspectExecutorServiceArtifact, inspectExecutorServiceDurability, EXECUTOR_SERVICE_BASE_URL_ENV, } from "./executor/index.js";
43
48
  import { DEFAULT_BAPI_BASE_URL } from "./executor/credentials.js";
44
49
  import { detectClaudeLogin, formatClaudeLoginAdvisory } from "./claude-login.js";
45
50
  import { validateBranchName } from "./base-ref.js";
@@ -1005,6 +1010,53 @@ export function formatToolSurfaceDiagnosticReport(diag) {
1005
1010
  lines.push("to observe surface changes; no project MCP configuration change is required.");
1006
1011
  return lines.join("\n");
1007
1012
  }
1013
+ // ---------------------------------------------------------------------------
1014
+ // Executor provisioning diagnostics (BAPI-688) — STRICTLY read-only.
1015
+ //
1016
+ // Enumerates the deterministic, convention-named executor service units this
1017
+ // package generates (`executor install-service`) and reports their non-secret
1018
+ // configuration plus credential readiness. It never writes, never runs a
1019
+ // MUTATING `launchctl`/`systemctl`/`loginctl` verb, and — like every advisory
1020
+ // section — never influences the doctor exit code.
1021
+ //
1022
+ // It does spawn ONE child process, and only on a host that already has such a
1023
+ // unit: the read-only session-durability query added by BAPI-1137, through
1024
+ // `defaultReadOnlyLifecycleRunner` below. That is a real change to this
1025
+ // section's behavior and is stated here rather than left to be discovered —
1026
+ // with no injected runner it issues none, and the fact degrades to
1027
+ // `unverifiable` rather than being guessed.
1028
+ //
1029
+ // Discovery is derived SOLELY from the fixed OS directory + filename prefix, not
1030
+ // from `.bridge/install-state.json`: install state is conductor install history,
1031
+ // not an artifact registry, so a unit written by hand or by another machine's
1032
+ // operator is still found, and a stale state entry never invents one.
1033
+ // ---------------------------------------------------------------------------
1034
+ /**
1035
+ * Shell-free runner for the ONE read-only durability query this section issues
1036
+ * (BAPI-1137).
1037
+ *
1038
+ * `shell: false` plus an argument ARRAY, matching every other service-manager
1039
+ * invocation in this package: a uid or identifier is data here, never syntax.
1040
+ * The probes it runs — `loginctl show-user --property=Linger`,
1041
+ * `launchctl print gui/<uid>` — are queries; nothing in this file can reach a
1042
+ * mutating lifecycle command. Output is captured only so the lifecycle module
1043
+ * can map a recognized state token, and is never surfaced.
1044
+ */
1045
+ function defaultReadOnlyLifecycleRunner(executable, args) {
1046
+ return new Promise((resolve, reject) => {
1047
+ const child = spawn(executable, args, { shell: false, stdio: ["ignore", "pipe", "pipe"] });
1048
+ let stdout = "";
1049
+ let stderr = "";
1050
+ child.stdout?.on("data", (chunk) => {
1051
+ stdout += chunk.toString("utf-8");
1052
+ });
1053
+ child.stderr?.on("data", (chunk) => {
1054
+ stderr += chunk.toString("utf-8");
1055
+ });
1056
+ child.on("error", reject);
1057
+ child.on("close", (code) => resolve({ exitCode: code ?? 1, stdout, stderr }));
1058
+ });
1059
+ }
1008
1060
  /** Where generated units live, and how their filenames encode the executor id. */
1009
1061
  function resolveExecutorServiceConvention(platform, homeDir) {
1010
1062
  if (platform === "darwin") {
@@ -1097,6 +1149,36 @@ export async function collectExecutorServiceDiagnostics(deps) {
1097
1149
  const pathApi = path.posix;
1098
1150
  const units = [];
1099
1151
  const warnings = [];
1152
+ // BAPI-1137 — the host-level durability prerequisite, probed ONCE per run
1153
+ // rather than once per unit: lingering and the GUI login domain are properties
1154
+ // of the host and the user, not of any particular unit. Without an injected
1155
+ // read-only runner no command is issued at all and the fact stays
1156
+ // `unverifiable`, so a caller that supplies no runner keeps a zero-probe run.
1157
+ let durability = {
1158
+ prerequisite: "unverifiable",
1159
+ remediation: null,
1160
+ };
1161
+ if (deps.runLifecycleCommand) {
1162
+ try {
1163
+ const inspection = await inspectExecutorServiceDurability({
1164
+ platform: deps.platform === "darwin" ? "darwin" : "linux",
1165
+ // The durability probe addresses the USER's session/linger state, not
1166
+ // a unit, so it needs neither a unit path nor a service identifier.
1167
+ unitPath: "",
1168
+ serviceIdentifier: "durability-probe",
1169
+ uid: deps.uid ?? null,
1170
+ }, deps.runLifecycleCommand);
1171
+ durability = {
1172
+ prerequisite: inspection.prerequisite,
1173
+ remediation: inspection.remediation,
1174
+ };
1175
+ }
1176
+ catch {
1177
+ // Sanitized: a probe that throws leaves the fact unverifiable rather than
1178
+ // surfacing exception text or inventing a state.
1179
+ durability = { prerequisite: "unverifiable", remediation: null };
1180
+ }
1181
+ }
1100
1182
  for (const name of matching) {
1101
1183
  const unitPath = pathApi.join(convention.dir, name);
1102
1184
  const executorId = convention.idFromFilename(name) ?? name;
@@ -1120,12 +1202,33 @@ export async function collectExecutorServiceDiagnostics(deps) {
1120
1202
  baseUrlClass: "missing",
1121
1203
  repos: [],
1122
1204
  credentials: [],
1123
- warnings: ["unit could not be read; its configuration cannot be verified."],
1205
+ // A unit whose bytes could not be read establishes NOTHING about what it
1206
+ // declares. `unverifiable`, never `confirmed-absent`.
1207
+ noStandingGithubWriteCredential: "unverifiable",
1208
+ warnings: [
1209
+ "unit could not be read; its configuration cannot be verified.",
1210
+ "regenerate it through durable-host mode (`install conductor`) if this persists.",
1211
+ ],
1124
1212
  });
1125
1213
  continue;
1126
1214
  }
1127
1215
  const inspection = inspectExecutorServiceArtifact(convention.kind, content);
1128
1216
  const unitWarnings = [...inspection.problems];
1217
+ // BAPI-1137 — the classification comes from the GENERATOR's shared helper,
1218
+ // applied to the unit read off disk. doctor maintains no second notion of
1219
+ // what "declares no GitHub write credential" means, so a unit this section
1220
+ // calls compliant is exactly a unit the generator would agree to write.
1221
+ const credentialDeclaration = inspection.noStandingGithubWriteCredential;
1222
+ unitWarnings.push(...inspection.credentialDeclarationProblems);
1223
+ if (credentialDeclaration === "present") {
1224
+ unitWarnings.push("this unit declares something outside the closed durable-host contract — regenerate it " +
1225
+ "through durable-host mode (`install conductor`) and remove the hand edit.");
1226
+ }
1227
+ else if (credentialDeclaration === "unverifiable") {
1228
+ unitWarnings.push("this unit could only be parsed partially, so the no-standing-GitHub-write-credential " +
1229
+ "property could not be established — regenerate it through durable-host mode " +
1230
+ "(`install conductor`).");
1231
+ }
1129
1232
  let baseUrlClass;
1130
1233
  if (!inspection.baseUrl) {
1131
1234
  baseUrlClass = "missing";
@@ -1153,10 +1256,24 @@ export async function collectExecutorServiceDiagnostics(deps) {
1153
1256
  baseUrlClass,
1154
1257
  repos: inspection.repos,
1155
1258
  credentials,
1259
+ noStandingGithubWriteCredential: credentialDeclaration,
1156
1260
  warnings: unitWarnings,
1157
1261
  });
1158
1262
  }
1159
- return { status: "collected", units, warnings };
1263
+ return { status: "collected", units, warnings, durability };
1264
+ }
1265
+ /**
1266
+ * The rendered name of the BAPI-1137 credential diagnostic.
1267
+ *
1268
+ * Named for the NARROW claim it actually makes — the unit's own declarations —
1269
+ * rather than something broader like "host holds no GitHub credential", which
1270
+ * the check cannot support and an operator would reasonably read as covering
1271
+ * their SSH keys, keychain, and `gh` login.
1272
+ */
1273
+ export const EXECUTOR_UNIT_CREDENTIAL_DIAGNOSTIC_NAME = "unit-declares-no-github-write-credential";
1274
+ /** How each classification renders, once, so the three spellings cannot drift. */
1275
+ function formatCredentialDeclarationLine(classification) {
1276
+ return ` ${EXECUTOR_UNIT_CREDENTIAL_DIAGNOSTIC_NAME}: ${classification}`;
1160
1277
  }
1161
1278
  /**
1162
1279
  * Render the advisory "Executor provisioning" section (pure formatting — no I/O).
@@ -1179,6 +1296,7 @@ export function formatExecutorServiceDiagnosticsReport(diagnostics) {
1179
1296
  lines.push(` executor id: ${unit.executorId}`);
1180
1297
  lines.push(` ${EXECUTOR_SERVICE_BASE_URL_ENV}: ${unit.baseUrl ?? "(absent)"} [${unit.baseUrlClass}]`);
1181
1298
  lines.push(` repos: ${unit.repos.length > 0 ? unit.repos.join(", ") : "(none parsed)"}`);
1299
+ lines.push(formatCredentialDeclarationLine(unit.noStandingGithubWriteCredential));
1182
1300
  for (const cred of unit.credentials) {
1183
1301
  lines.push(cred.resolved
1184
1302
  ? ` credential ${cred.target}: resolved (source: ${cred.source})`
@@ -1188,11 +1306,52 @@ export function formatExecutorServiceDiagnosticsReport(diagnostics) {
1188
1306
  lines.push(`WARN ${unit.path}: ${warning}`);
1189
1307
  }
1190
1308
  }
1309
+ // The host-level durability prerequisite, rendered SEPARATELY from every unit
1310
+ // above: it is a property of this host and user, and attaching it to a unit
1311
+ // would imply the two were one fact.
1312
+ lines.push("");
1313
+ if (diagnostics.durability.prerequisite === "configured") {
1314
+ lines.push("found durable session: configured — services keep running without an active login.");
1315
+ }
1316
+ else if (diagnostics.durability.prerequisite === "session-bound") {
1317
+ // Deliberately NOT a "found" line. The prerequisite this platform can check
1318
+ // is satisfied, but what it establishes is a login session, not logout
1319
+ // durability, and saying otherwise would contradict launchd.
1320
+ lines.push("WARN durable session: session-bound — a login session exists, but it is NOT logout-durable.");
1321
+ if (diagnostics.durability.remediation) {
1322
+ lines.push(` operator action: ${diagnostics.durability.remediation}`);
1323
+ }
1324
+ lines.push(" A LaunchAgent stops with the login it runs under; closing the lid or logging out", " stops the executor. doctor performed a READ-ONLY query and changed nothing.");
1325
+ }
1326
+ else if (diagnostics.durability.prerequisite === "not-configured") {
1327
+ lines.push("WARN durable session: NOT durably configured for this host.");
1328
+ if (diagnostics.durability.remediation) {
1329
+ lines.push(` remediation (operator-run): ${diagnostics.durability.remediation}`);
1330
+ }
1331
+ lines.push(" doctor performed a READ-ONLY query only: it did not run `loginctl enable-linger`,", " change any session setting, or alter the exit code.");
1332
+ }
1333
+ else if (diagnostics.durability.prerequisite === "not-applicable") {
1334
+ lines.push("SKIP durable session: this platform has no session/linger prerequisite.");
1335
+ }
1336
+ else {
1337
+ lines.push("SKIP durable session: unverifiable — the read-only probe produced no recognized answer.", " That is not evidence that this host is, or is not, durably configured.");
1338
+ }
1339
+ lines.push("");
1340
+ lines.push("Unit-content compliance and service-manager state are INDEPENDENT facts: the manager may");
1341
+ lines.push("still be running a previously loaded definition rather than the bytes currently on disk, so a");
1342
+ lines.push("compliant unit file is not evidence about the running process, and neither is evidence that");
1343
+ lines.push("Bridge has observed an executor claim.");
1344
+ lines.push("");
1345
+ lines.push(`The ${EXECUTOR_UNIT_CREDENTIAL_DIAGNOSTIC_NAME} fact covers the unit's OWN declarations —`);
1346
+ lines.push("its environment keys and its arguments. Host-wide SSH keys and agents, login keychains, `gh`");
1347
+ lines.push("login state, git credential helpers, and independently managed Claude worker authentication");
1348
+ lines.push("are outside it and are never inspected or asserted here.");
1191
1349
  lines.push("");
1192
1350
  lines.push("Generated units carry only the base URL, the generating shell's PATH, repositories, and", "executor id — credentials resolve at");
1193
1351
  lines.push("launch time from BAPI_API_KEY or the user-scoped bapi:<repo> store, so no key value is stored on");
1194
1352
  lines.push("disk or printed here. Starting, stopping, and enabling a unit stay operator-managed. This section");
1195
- lines.push("is advisory and never changes the doctor exit code.");
1353
+ lines.push("is advisory — including the credential-declaration and durable-session facts, which are excluded");
1354
+ lines.push("from required-prerequisite computation — and never changes the doctor exit code.");
1196
1355
  return lines.join("\n");
1197
1356
  }
1198
1357
  export async function collectPlaneCredentialDiagnostic(deps) {
@@ -1311,175 +1470,6 @@ export function formatPlaneCredentialDiagnosticReport(diagnostic) {
1311
1470
  lines.push(" no .bridge/plane/ directory, persists nothing, and starts no plane member.");
1312
1471
  return lines.join("\n");
1313
1472
  }
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
1473
  /**
1484
1474
  * The transitional local helpers BAPI-1102 retires.
1485
1475
  *
@@ -1501,14 +1491,10 @@ export const LEGACY_DRIVE_HELPER_RELATIVE_PATHS = [
1501
1491
  /**
1502
1492
  * Collect the operational advisories, read-only.
1503
1493
  *
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.
1494
+ * Concurrent conductors and parse-dispatcher health are read from two read-only
1495
+ * Bridge GETs; no local process table or lock directory is inspected.
1509
1496
  */
1510
1497
  export async function collectOperationalAdvisories(deps) {
1511
- const isAlive = deps.isProcessAlive ?? isConductEpicLockOwnerAlive;
1512
1498
  const now = deps.now ?? (() => new Date());
1513
1499
  const resolveAccess = deps.resolveAccess ?? resolveConductorBridgeApiAccess;
1514
1500
  let dispatcher = null;
@@ -1542,64 +1528,13 @@ export async function collectOperationalAdvisories(deps) {
1542
1528
  conductors.push(...live.value.conductors);
1543
1529
  }
1544
1530
  }
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
1531
  // BAPI-1102 — a pure path-presence check, using the SAME injected `stat` and
1532
+ // (BAPI-1137) still ONLY a path-presence check: the helpers' CONTENTS are
1533
+ // never read, they are never executed, and neither file is ever deleted. The
1534
+ // guidance below names durable-host mode as their replacement; removing them
1535
+ // stays an operator step, because they live outside this repository and
1536
+ // product code that deleted files in `~/.local/bin` would be a far larger
1537
+ // authority than a read-only diagnostic should hold.
1603
1538
  // `homedir` seams every other advisory here uses, so it stays deterministic
1604
1539
  // under test and reaches no real filesystem in one. A `stat` that throws is
1605
1540
  // "absent"; it is never retried, and a permission error is not reported as a
@@ -1620,7 +1555,6 @@ export async function collectOperationalAdvisories(deps) {
1620
1555
  dispatcher,
1621
1556
  conductorObservation,
1622
1557
  conductors,
1623
- indeterminateLocks,
1624
1558
  legacyDriveHelpers,
1625
1559
  };
1626
1560
  }
@@ -1658,10 +1592,6 @@ export function formatOperationalAdvisoryReport(diagnostic) {
1658
1592
  lines.push(" A distinct-epic overlap is informational and may be legitimate: separately\n" +
1659
1593
  " scoped epics do not share transition authority.");
1660
1594
  }
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
1595
  // BAPI-1102. A WARN, never a failure, and deliberately separate from every
1666
1596
  // readiness signal above: a stale helper says nothing about whether this
1667
1597
  // repository's conductor works, so classifying it as a readiness problem would
@@ -1672,12 +1602,14 @@ export function formatOperationalAdvisoryReport(diagnostic) {
1672
1602
  for (const helper of diagnostic.legacyDriveHelpers) {
1673
1603
  lines.push(` ${helper}`);
1674
1604
  }
1675
- lines.push(" These predate the scoped two-phase bring-up and are retired. Confirm\n" +
1676
- " `drive-epic <EPIC> --plan-file <dag>` works for you, then remove them.\n" +
1605
+ lines.push(" These predate the scoped two-phase bring-up and are retired. The supported\n" +
1606
+ " replacement is DURABLE-HOST MODE: run `install conductor` to plan, write, and\n" +
1607
+ " start the executor as a persistent service, then confirm\n" +
1608
+ " `drive-epic <EPIC> --plan-file <dag>` works for you, and remove them.\n" +
1677
1609
  " doctor performed a PATH-PRESENCE check only: it did not run, read, or\n" +
1678
1610
  " modify either file, and it cannot delete them for you.");
1679
1611
  }
1680
- lines.push(" Read-only: this section issues two Bridge GETs and reads local lock state. It");
1612
+ lines.push(" Read-only: this section issues two Bridge GETs and reads no local state. It");
1681
1613
  lines.push(" runs NO command probe, repairs no scheduler, and never changes the exit code.");
1682
1614
  return lines.join("\n");
1683
1615
  }
@@ -1752,6 +1684,13 @@ export async function runDoctorCli(argv, overrides = {}) {
1752
1684
  ? "Codex Bridge approval profile is an older unmodified managed version; the next spawned worker refreshes it before launch."
1753
1685
  : `Codex Bridge approval profile ${profile.status}; spawned workers use existing Codex approvals. Run install --tools=codex --codex-profile=on to opt in.`);
1754
1686
  }
1687
+ // Read-only personal routing diagnostics. The installer marker is a hint;
1688
+ // an initialized MCP client identity takes precedence during real requests.
1689
+ const routingHost = resolveRoutingHost(undefined, deps.env.BAPI_MCP_HOST_FAMILY);
1690
+ log(`Model routing host marker: ${routingHost.warning ?? routingHost.family ?? "neutral"}.`);
1691
+ const preferencePath = modelRoutingPreferencesPath(deps.homedir?.() ?? os.homedir());
1692
+ const preference = await readModelRoutingPreference(deps.env.BAPI_REPO_NAME ?? "_doctor_unscoped_", "review_ticket", preferencePath);
1693
+ log(`Personal model routing preference: ${preference.warning ?? preference.source}.`);
1755
1694
  log(formatDoctorPrereqSection(deps.platform, collection));
1756
1695
  // Executor-adapter section (BAPI-781). Advisory with respect to the EXIT CODE
1757
1696
  // — an unusable adapter is reported loudly but does not change doctor's
@@ -1916,6 +1855,13 @@ export async function runDoctorCli(argv, overrides = {}) {
1916
1855
  ((p) => readFile(p, "utf-8")),
1917
1856
  stat: overrides.executorService?.stat ?? injectedFs.stat ?? ((p) => stat(p)),
1918
1857
  resolveCredentials: overrides.executorService?.resolveCredentials,
1858
+ // BAPI-1137 — the durability prerequisite needs a uid and a read-only
1859
+ // runner. `process.getuid` is absent on win32, where a null uid simply
1860
+ // leaves the fact unverifiable rather than interpolating `undefined`
1861
+ // into a domain target.
1862
+ uid: overrides.executorService?.uid ??
1863
+ (typeof process.getuid === "function" ? process.getuid() : null),
1864
+ runLifecycleCommand: overrides.executorService?.runLifecycleCommand ?? defaultReadOnlyLifecycleRunner,
1919
1865
  };
1920
1866
  const executorDiagnostics = await collectExecutorServiceDiagnostics(executorDeps);
1921
1867
  log(formatExecutorServiceDiagnosticsReport(executorDiagnostics));
@@ -1973,55 +1919,8 @@ export async function runDoctorCli(argv, overrides = {}) {
1973
1919
  }));
1974
1920
  }
1975
1921
  }
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?".
1922
+ // Advisory operational-advisories section (BAPI-963). Read by the operator
1923
+ // asking "what is already running?" right after the readiness sections above.
2025
1924
  //
2026
1925
  // Gated on `collection.ok` for the same reason the section above is: an
2027
1926
  // unsupported platform reports nothing. That gate is belt-and-braces here,
@@ -2044,9 +1943,7 @@ export async function runDoctorCli(argv, overrides = {}) {
2044
1943
  injectedFs.readFile ??
2045
1944
  ((p) => readFile(p, "utf-8")),
2046
1945
  stat: overrides.operationalAdvisories?.stat ?? injectedFs.stat ?? ((p) => stat(p)),
2047
- readdir: overrides.operationalAdvisories?.readdir ?? ((p) => readdir(p)),
2048
1946
  resolveAccess: overrides.operationalAdvisories?.resolveAccess,
2049
- isProcessAlive: overrides.operationalAdvisories?.isProcessAlive,
2050
1947
  fetchImpl: overrides.operationalAdvisories?.fetchImpl,
2051
1948
  now: overrides.operationalAdvisories?.now,
2052
1949
  };
@@ -2059,7 +1956,6 @@ export async function runDoctorCli(argv, overrides = {}) {
2059
1956
  dispatcher: null,
2060
1957
  conductorObservation: "unavailable",
2061
1958
  conductors: [],
2062
- indeterminateLocks: 0,
2063
1959
  // Empty, not omitted: a collection that failed has not established
2064
1960
  // that a legacy helper is absent, and a fallback that CLAIMED to have
2065
1961
  // checked would be the one kind of wrong answer worse than silence.