@bridge_gpt/mcp-server 0.2.59 → 0.2.62

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 (78) hide show
  1. package/README.md +181 -51
  2. package/build/agent-guidance.generated.js +1 -1
  3. package/build/agent-notes.js +7 -1
  4. package/build/commands.generated.js +6 -6
  5. package/build/conductor/bridge-api-client.js +406 -4
  6. package/build/conductor/doctor.js +255 -53
  7. package/build/conductor/epic-runtime.js +11 -2
  8. package/build/conductor/install-doctor.js +24 -2
  9. package/build/conductor/local-merge.js +319 -26
  10. package/build/conductor/merge-execution-contract.js +86 -0
  11. package/build/conductor/merge-execution-eligibility.js +130 -0
  12. package/build/conductor/readiness-sections.js +256 -16
  13. package/build/conductor/readiness.js +203 -29
  14. package/build/conductor/recovery-operations.js +8 -2
  15. package/build/connect-bitbucket-api.js +6 -1
  16. package/build/connect-bitbucket.js +398 -88
  17. package/build/credential-store.js +172 -0
  18. package/build/docs.generated.js +2 -2
  19. package/build/doctor.js +277 -15
  20. package/build/drive-epic.js +147 -12
  21. package/build/epic-implementer-bin.js +15 -15
  22. package/build/epic-implementer-claude-hook-bin.js +1 -1
  23. package/build/epic-implementer-env.js +43 -14
  24. package/build/epic-integration-pr.js +61 -15
  25. package/build/epic-launch-profile.js +455 -0
  26. package/build/executor/ci-rerun-job.js +269 -0
  27. package/build/executor/cli.js +40 -0
  28. package/build/executor/credentials.js +28 -1
  29. package/build/executor/env.js +60 -0
  30. package/build/executor/http-client.js +45 -1
  31. package/build/executor/index.js +21 -4
  32. package/build/executor/integration-job.js +759 -0
  33. package/build/executor/job-runner.js +218 -9
  34. package/build/executor/job-types.js +12 -2
  35. package/build/executor/merge-job.js +11 -2
  36. package/build/executor/observation.js +114 -7
  37. package/build/executor/runner.js +101 -1
  38. package/build/executor/service-lifecycle.js +136 -0
  39. package/build/executor/service-unit.js +427 -9
  40. package/build/executor/types.js +48 -0
  41. package/build/index.js +479 -240
  42. package/build/install-bridge-conductor.js +599 -14
  43. package/build/install-bridge.js +448 -46
  44. package/build/mcp-install-state.js +42 -4
  45. package/build/merge-pull-request.js +103 -10
  46. package/build/merge-workflow-codeowners.js +224 -0
  47. package/build/merge-workflow-config.js +187 -0
  48. package/build/merge-workflow-drift.js +97 -0
  49. package/build/merge-workflow-install.js +638 -0
  50. package/build/merge-workflow.js +956 -0
  51. package/build/pipelines.generated.js +3 -3
  52. package/build/plane/cli.js +24 -2
  53. package/build/plane/preflight.js +68 -0
  54. package/build/plane/supervisor.js +6 -0
  55. package/build/readme.generated.js +1 -1
  56. package/build/run-unit-tests-launcher.js +5 -0
  57. package/build/setup-epic.js +263 -38
  58. package/build/version.generated.js +3 -3
  59. package/package.json +3 -3
  60. package/build/conduct-epic/bridge-client.js +0 -773
  61. package/build/conduct-epic/checkpoint-store.js +0 -542
  62. package/build/conduct-epic/cli.js +0 -3145
  63. package/build/conduct-epic/cut-protocol.js +0 -392
  64. package/build/conduct-epic/lock.js +0 -302
  65. package/build/conduct-epic/pr-state.js +0 -286
  66. package/build/conduct-epic/spawn.js +0 -113
  67. package/build/conductor/cli.js +0 -1278
  68. package/build/conductor-bundle-cli.js +0 -256
  69. package/build/direct-ticket-tools.js +0 -70
  70. package/build/executor/reconcile-overlap-governance.js +0 -129
  71. package/build/implement-epic/bridge-client.js +0 -773
  72. package/build/implement-epic/checkpoint-store.js +0 -542
  73. package/build/implement-epic/cli.js +0 -3158
  74. package/build/implement-epic/cut-protocol.js +0 -392
  75. package/build/implement-epic/lock.js +0 -302
  76. package/build/implement-epic/pr-state.js +0 -286
  77. package/build/implement-epic/spawn.js +0 -113
  78. package/build/plan-epic-conductor-eligibility.js +0 -213
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";
@@ -29,7 +34,8 @@ import { resolveExecutorAgentAdapter } from "./agent-launchers/executor-adapter-
29
34
  import { parseDefaultOnEnvFlag } from "./env-flags.js";
30
35
  import { createBridgeApiUrls } from "./bridge-api-urls.js";
31
36
  import { probeToolSurface } from "./tool-surface-gating.js";
32
- import { resolveBapiCredentials } from "./credential-store.js";
37
+ import { readStoredBinding, resolveBapiCredentials } from "./credential-store.js";
38
+ import { listEpicLaunchProfiles } from "./epic-launch-profile.js";
33
39
  import { resolveConductorBridgeApiAccess } from "./conductor/bridge-api-client.js";
34
40
  import { getLiveRepositoryConductors, getParseDispatcherHealth, } from "./epic-shared/bridge-client.js";
35
41
  import { resolveRequiredStartTicketsRepoName } from "./start-tickets-repo.js";
@@ -39,7 +45,7 @@ import { MCP_SERVER_NAME, MCP_PACKAGE_NAME, } from "./mcp-identity.js";
39
45
  import { DEFAULT_AGENT_NAME, resolveAgentSpec, isAgentName, formatValidAgentNames, } from "./agent-registry.js";
40
46
  import { getDoctorPrereqDescriptors, probePrerequisite, } from "./start-tickets-prereqs.js";
41
47
  import { resolveProfiles } from "./mcp-profile.js";
42
- import { executorLaunchdDirForHome, executorSystemdDirForHome, executorIdFromLaunchdFilename, executorIdFromSystemdFilename, inspectExecutorServiceArtifact, EXECUTOR_SERVICE_BASE_URL_ENV, } from "./executor/index.js";
48
+ import { executorLaunchdDirForHome, executorSystemdDirForHome, executorIdFromLaunchdFilename, executorIdFromSystemdFilename, inspectExecutorServiceArtifact, inspectExecutorServiceDurability, EXECUTOR_SERVICE_BASE_URL_ENV, } from "./executor/index.js";
43
49
  import { DEFAULT_BAPI_BASE_URL } from "./executor/credentials.js";
44
50
  import { detectClaudeLogin, formatClaudeLoginAdvisory } from "./claude-login.js";
45
51
  import { validateBranchName } from "./base-ref.js";
@@ -1005,6 +1011,53 @@ export function formatToolSurfaceDiagnosticReport(diag) {
1005
1011
  lines.push("to observe surface changes; no project MCP configuration change is required.");
1006
1012
  return lines.join("\n");
1007
1013
  }
1014
+ // ---------------------------------------------------------------------------
1015
+ // Executor provisioning diagnostics (BAPI-688) — STRICTLY read-only.
1016
+ //
1017
+ // Enumerates the deterministic, convention-named executor service units this
1018
+ // package generates (`executor install-service`) and reports their non-secret
1019
+ // configuration plus credential readiness. It never writes, never runs a
1020
+ // MUTATING `launchctl`/`systemctl`/`loginctl` verb, and — like every advisory
1021
+ // section — never influences the doctor exit code.
1022
+ //
1023
+ // It does spawn ONE child process, and only on a host that already has such a
1024
+ // unit: the read-only session-durability query added by BAPI-1137, through
1025
+ // `defaultReadOnlyLifecycleRunner` below. That is a real change to this
1026
+ // section's behavior and is stated here rather than left to be discovered —
1027
+ // with no injected runner it issues none, and the fact degrades to
1028
+ // `unverifiable` rather than being guessed.
1029
+ //
1030
+ // Discovery is derived SOLELY from the fixed OS directory + filename prefix, not
1031
+ // from `.bridge/install-state.json`: install state is conductor install history,
1032
+ // not an artifact registry, so a unit written by hand or by another machine's
1033
+ // operator is still found, and a stale state entry never invents one.
1034
+ // ---------------------------------------------------------------------------
1035
+ /**
1036
+ * Shell-free runner for the ONE read-only durability query this section issues
1037
+ * (BAPI-1137).
1038
+ *
1039
+ * `shell: false` plus an argument ARRAY, matching every other service-manager
1040
+ * invocation in this package: a uid or identifier is data here, never syntax.
1041
+ * The probes it runs — `loginctl show-user --property=Linger`,
1042
+ * `launchctl print gui/<uid>` — are queries; nothing in this file can reach a
1043
+ * mutating lifecycle command. Output is captured only so the lifecycle module
1044
+ * can map a recognized state token, and is never surfaced.
1045
+ */
1046
+ function defaultReadOnlyLifecycleRunner(executable, args) {
1047
+ return new Promise((resolve, reject) => {
1048
+ const child = spawn(executable, args, { shell: false, stdio: ["ignore", "pipe", "pipe"] });
1049
+ let stdout = "";
1050
+ let stderr = "";
1051
+ child.stdout?.on("data", (chunk) => {
1052
+ stdout += chunk.toString("utf-8");
1053
+ });
1054
+ child.stderr?.on("data", (chunk) => {
1055
+ stderr += chunk.toString("utf-8");
1056
+ });
1057
+ child.on("error", reject);
1058
+ child.on("close", (code) => resolve({ exitCode: code ?? 1, stdout, stderr }));
1059
+ });
1060
+ }
1008
1061
  /** Where generated units live, and how their filenames encode the executor id. */
1009
1062
  function resolveExecutorServiceConvention(platform, homeDir) {
1010
1063
  if (platform === "darwin") {
@@ -1097,6 +1150,36 @@ export async function collectExecutorServiceDiagnostics(deps) {
1097
1150
  const pathApi = path.posix;
1098
1151
  const units = [];
1099
1152
  const warnings = [];
1153
+ // BAPI-1137 — the host-level durability prerequisite, probed ONCE per run
1154
+ // rather than once per unit: lingering and the GUI login domain are properties
1155
+ // of the host and the user, not of any particular unit. Without an injected
1156
+ // read-only runner no command is issued at all and the fact stays
1157
+ // `unverifiable`, so a caller that supplies no runner keeps a zero-probe run.
1158
+ let durability = {
1159
+ prerequisite: "unverifiable",
1160
+ remediation: null,
1161
+ };
1162
+ if (deps.runLifecycleCommand) {
1163
+ try {
1164
+ const inspection = await inspectExecutorServiceDurability({
1165
+ platform: deps.platform === "darwin" ? "darwin" : "linux",
1166
+ // The durability probe addresses the USER's session/linger state, not
1167
+ // a unit, so it needs neither a unit path nor a service identifier.
1168
+ unitPath: "",
1169
+ serviceIdentifier: "durability-probe",
1170
+ uid: deps.uid ?? null,
1171
+ }, deps.runLifecycleCommand);
1172
+ durability = {
1173
+ prerequisite: inspection.prerequisite,
1174
+ remediation: inspection.remediation,
1175
+ };
1176
+ }
1177
+ catch {
1178
+ // Sanitized: a probe that throws leaves the fact unverifiable rather than
1179
+ // surfacing exception text or inventing a state.
1180
+ durability = { prerequisite: "unverifiable", remediation: null };
1181
+ }
1182
+ }
1100
1183
  for (const name of matching) {
1101
1184
  const unitPath = pathApi.join(convention.dir, name);
1102
1185
  const executorId = convention.idFromFilename(name) ?? name;
@@ -1120,12 +1203,33 @@ export async function collectExecutorServiceDiagnostics(deps) {
1120
1203
  baseUrlClass: "missing",
1121
1204
  repos: [],
1122
1205
  credentials: [],
1123
- warnings: ["unit could not be read; its configuration cannot be verified."],
1206
+ // A unit whose bytes could not be read establishes NOTHING about what it
1207
+ // declares. `unverifiable`, never `confirmed-absent`.
1208
+ noStandingGithubWriteCredential: "unverifiable",
1209
+ warnings: [
1210
+ "unit could not be read; its configuration cannot be verified.",
1211
+ "regenerate it through durable-host mode (`install conductor`) if this persists.",
1212
+ ],
1124
1213
  });
1125
1214
  continue;
1126
1215
  }
1127
1216
  const inspection = inspectExecutorServiceArtifact(convention.kind, content);
1128
1217
  const unitWarnings = [...inspection.problems];
1218
+ // BAPI-1137 — the classification comes from the GENERATOR's shared helper,
1219
+ // applied to the unit read off disk. doctor maintains no second notion of
1220
+ // what "declares no GitHub write credential" means, so a unit this section
1221
+ // calls compliant is exactly a unit the generator would agree to write.
1222
+ const credentialDeclaration = inspection.noStandingGithubWriteCredential;
1223
+ unitWarnings.push(...inspection.credentialDeclarationProblems);
1224
+ if (credentialDeclaration === "present") {
1225
+ unitWarnings.push("this unit declares something outside the closed durable-host contract — regenerate it " +
1226
+ "through durable-host mode (`install conductor`) and remove the hand edit.");
1227
+ }
1228
+ else if (credentialDeclaration === "unverifiable") {
1229
+ unitWarnings.push("this unit could only be parsed partially, so the no-standing-GitHub-write-credential " +
1230
+ "property could not be established — regenerate it through durable-host mode " +
1231
+ "(`install conductor`).");
1232
+ }
1129
1233
  let baseUrlClass;
1130
1234
  if (!inspection.baseUrl) {
1131
1235
  baseUrlClass = "missing";
@@ -1153,10 +1257,24 @@ export async function collectExecutorServiceDiagnostics(deps) {
1153
1257
  baseUrlClass,
1154
1258
  repos: inspection.repos,
1155
1259
  credentials,
1260
+ noStandingGithubWriteCredential: credentialDeclaration,
1156
1261
  warnings: unitWarnings,
1157
1262
  });
1158
1263
  }
1159
- return { status: "collected", units, warnings };
1264
+ return { status: "collected", units, warnings, durability };
1265
+ }
1266
+ /**
1267
+ * The rendered name of the BAPI-1137 credential diagnostic.
1268
+ *
1269
+ * Named for the NARROW claim it actually makes — the unit's own declarations —
1270
+ * rather than something broader like "host holds no GitHub credential", which
1271
+ * the check cannot support and an operator would reasonably read as covering
1272
+ * their SSH keys, keychain, and `gh` login.
1273
+ */
1274
+ export const EXECUTOR_UNIT_CREDENTIAL_DIAGNOSTIC_NAME = "unit-declares-no-github-write-credential";
1275
+ /** How each classification renders, once, so the three spellings cannot drift. */
1276
+ function formatCredentialDeclarationLine(classification) {
1277
+ return ` ${EXECUTOR_UNIT_CREDENTIAL_DIAGNOSTIC_NAME}: ${classification}`;
1160
1278
  }
1161
1279
  /**
1162
1280
  * Render the advisory "Executor provisioning" section (pure formatting — no I/O).
@@ -1179,6 +1297,7 @@ export function formatExecutorServiceDiagnosticsReport(diagnostics) {
1179
1297
  lines.push(` executor id: ${unit.executorId}`);
1180
1298
  lines.push(` ${EXECUTOR_SERVICE_BASE_URL_ENV}: ${unit.baseUrl ?? "(absent)"} [${unit.baseUrlClass}]`);
1181
1299
  lines.push(` repos: ${unit.repos.length > 0 ? unit.repos.join(", ") : "(none parsed)"}`);
1300
+ lines.push(formatCredentialDeclarationLine(unit.noStandingGithubWriteCredential));
1182
1301
  for (const cred of unit.credentials) {
1183
1302
  lines.push(cred.resolved
1184
1303
  ? ` credential ${cred.target}: resolved (source: ${cred.source})`
@@ -1188,11 +1307,52 @@ export function formatExecutorServiceDiagnosticsReport(diagnostics) {
1188
1307
  lines.push(`WARN ${unit.path}: ${warning}`);
1189
1308
  }
1190
1309
  }
1310
+ // The host-level durability prerequisite, rendered SEPARATELY from every unit
1311
+ // above: it is a property of this host and user, and attaching it to a unit
1312
+ // would imply the two were one fact.
1313
+ lines.push("");
1314
+ if (diagnostics.durability.prerequisite === "configured") {
1315
+ lines.push("found durable session: configured — services keep running without an active login.");
1316
+ }
1317
+ else if (diagnostics.durability.prerequisite === "session-bound") {
1318
+ // Deliberately NOT a "found" line. The prerequisite this platform can check
1319
+ // is satisfied, but what it establishes is a login session, not logout
1320
+ // durability, and saying otherwise would contradict launchd.
1321
+ lines.push("WARN durable session: session-bound — a login session exists, but it is NOT logout-durable.");
1322
+ if (diagnostics.durability.remediation) {
1323
+ lines.push(` operator action: ${diagnostics.durability.remediation}`);
1324
+ }
1325
+ 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.");
1326
+ }
1327
+ else if (diagnostics.durability.prerequisite === "not-configured") {
1328
+ lines.push("WARN durable session: NOT durably configured for this host.");
1329
+ if (diagnostics.durability.remediation) {
1330
+ lines.push(` remediation (operator-run): ${diagnostics.durability.remediation}`);
1331
+ }
1332
+ 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.");
1333
+ }
1334
+ else if (diagnostics.durability.prerequisite === "not-applicable") {
1335
+ lines.push("SKIP durable session: this platform has no session/linger prerequisite.");
1336
+ }
1337
+ else {
1338
+ 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.");
1339
+ }
1340
+ lines.push("");
1341
+ lines.push("Unit-content compliance and service-manager state are INDEPENDENT facts: the manager may");
1342
+ lines.push("still be running a previously loaded definition rather than the bytes currently on disk, so a");
1343
+ lines.push("compliant unit file is not evidence about the running process, and neither is evidence that");
1344
+ lines.push("Bridge has observed an executor claim.");
1345
+ lines.push("");
1346
+ lines.push(`The ${EXECUTOR_UNIT_CREDENTIAL_DIAGNOSTIC_NAME} fact covers the unit's OWN declarations —`);
1347
+ lines.push("its environment keys and its arguments. Host-wide SSH keys and agents, login keychains, `gh`");
1348
+ lines.push("login state, git credential helpers, and independently managed Claude worker authentication");
1349
+ lines.push("are outside it and are never inspected or asserted here.");
1191
1350
  lines.push("");
1192
1351
  lines.push("Generated units carry only the base URL, the generating shell's PATH, repositories, and", "executor id — credentials resolve at");
1193
1352
  lines.push("launch time from BAPI_API_KEY or the user-scoped bapi:<repo> store, so no key value is stored on");
1194
1353
  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.");
1354
+ lines.push("is advisory — including the credential-declaration and durable-session facts, which are excluded");
1355
+ lines.push("from required-prerequisite computation — and never changes the doctor exit code.");
1196
1356
  return lines.join("\n");
1197
1357
  }
1198
1358
  export async function collectPlaneCredentialDiagnostic(deps) {
@@ -1370,6 +1530,12 @@ export async function collectOperationalAdvisories(deps) {
1370
1530
  }
1371
1531
  }
1372
1532
  // BAPI-1102 — a pure path-presence check, using the SAME injected `stat` and
1533
+ // (BAPI-1137) still ONLY a path-presence check: the helpers' CONTENTS are
1534
+ // never read, they are never executed, and neither file is ever deleted. The
1535
+ // guidance below names durable-host mode as their replacement; removing them
1536
+ // stays an operator step, because they live outside this repository and
1537
+ // product code that deleted files in `~/.local/bin` would be a far larger
1538
+ // authority than a read-only diagnostic should hold.
1373
1539
  // `homedir` seams every other advisory here uses, so it stays deterministic
1374
1540
  // under test and reaches no real filesystem in one. A `stat` that throws is
1375
1541
  // "absent"; it is never retried, and a permission error is not reported as a
@@ -1437,8 +1603,10 @@ export function formatOperationalAdvisoryReport(diagnostic) {
1437
1603
  for (const helper of diagnostic.legacyDriveHelpers) {
1438
1604
  lines.push(` ${helper}`);
1439
1605
  }
1440
- lines.push(" These predate the scoped two-phase bring-up and are retired. Confirm\n" +
1441
- " `drive-epic <EPIC> --plan-file <dag>` works for you, then remove them.\n" +
1606
+ lines.push(" These predate the scoped two-phase bring-up and are retired. The supported\n" +
1607
+ " replacement is DURABLE-HOST MODE: run `install conductor` to plan, write, and\n" +
1608
+ " start the executor as a persistent service, then confirm\n" +
1609
+ " `drive-epic <EPIC> --plan-file <dag>` works for you, and remove them.\n" +
1442
1610
  " doctor performed a PATH-PRESENCE check only: it did not run, read, or\n" +
1443
1611
  " modify either file, and it cannot delete them for you.");
1444
1612
  }
@@ -1447,12 +1615,78 @@ export function formatOperationalAdvisoryReport(diagnostic) {
1447
1615
  return lines.join("\n");
1448
1616
  }
1449
1617
  /**
1450
- * CLI entry for the read-only `doctor` subcommand. Returns a process exit code.
1451
- * Help returns 0; parser errors return 1; otherwise it prints the report and
1452
- * returns 0 when every required prerequisite is present, non-zero when any is
1453
- * missing or the platform is unsupported. Never installs, mutates, spawns tabs,
1454
- * or starts the MCP server.
1618
+ * Read-only inventory of this host's epic-launch profiles and whether each
1619
+ * one's credential binding matches its target. It reports; it never binds or
1620
+ * migrates anything.
1455
1621
  */
1622
+ export async function collectEpicLaunchProfileDiagnostic(deps) {
1623
+ const listing = await listEpicLaunchProfiles(deps);
1624
+ if (!listing.ok)
1625
+ return { store: "unreadable", message: listing.message };
1626
+ if (!listing.present)
1627
+ return { store: "absent" };
1628
+ const host = deps.hostname();
1629
+ const profiles = [];
1630
+ for (const entry of listing.entries) {
1631
+ if (!entry.validation.ok) {
1632
+ profiles.push({ name: entry.name, usable: false, problem: entry.validation.kind });
1633
+ continue;
1634
+ }
1635
+ const p = entry.validation.profile;
1636
+ const bound = await readStoredBinding(p.credentialRef.target, {
1637
+ env: deps.env,
1638
+ homedir: deps.homedir,
1639
+ platform: process.platform,
1640
+ readFile: deps.readFile,
1641
+ // Presence probe via read: the binding reader treats any failure as unbound.
1642
+ stat: async (filePath) => {
1643
+ await deps.readFile(filePath);
1644
+ return { mode: 0o600 };
1645
+ },
1646
+ });
1647
+ const hostMatches = p.host === host;
1648
+ const credentialBinding = bound === null ? "unbound" : bound === p.baseUrl ? "matches" : "different";
1649
+ profiles.push({
1650
+ name: p.name,
1651
+ usable: hostMatches && credentialBinding === "matches",
1652
+ target: p.baseUrl,
1653
+ repo: p.repoName,
1654
+ mode: p.executionMode,
1655
+ hostMatches,
1656
+ credentialBinding,
1657
+ });
1658
+ }
1659
+ return { store: "present", profiles };
1660
+ }
1661
+ /** Render the section, or `null` when this host has no profile store. */
1662
+ export function formatEpicLaunchProfileDiagnosticReport(diagnostic) {
1663
+ if (diagnostic.store === "absent")
1664
+ return null;
1665
+ const lines = ["", "Epic launch profiles (read-only)", ""];
1666
+ if (diagnostic.store === "unreadable") {
1667
+ lines.push(` [warn] ${diagnostic.message}`);
1668
+ return lines.join("\n");
1669
+ }
1670
+ if (diagnostic.profiles.length === 0)
1671
+ lines.push(" (none)");
1672
+ for (const p of diagnostic.profiles) {
1673
+ if (p.problem !== undefined) {
1674
+ lines.push(` [fail] ${p.name}: ${p.problem} — re-run \`install conductor --launch-profile ${p.name}\`.`);
1675
+ continue;
1676
+ }
1677
+ const tag = p.usable ? "[ok] " : "[fail]";
1678
+ lines.push(` ${tag} ${p.name}: ${p.target} repo=${p.repo} mode=${p.mode}`);
1679
+ if (p.hostMatches === false)
1680
+ lines.push(" approved on a different host — profiles are host-local.");
1681
+ if (p.credentialBinding === "unbound") {
1682
+ lines.push(" stored credential not yet approved for this target — re-run guided setup to bind it.");
1683
+ }
1684
+ else if (p.credentialBinding === "different") {
1685
+ lines.push(" stored credential is approved for a DIFFERENT target — a launch will refuse to send it.");
1686
+ }
1687
+ }
1688
+ return lines.join("\n");
1689
+ }
1456
1690
  export async function runDoctorCli(argv, overrides = {}) {
1457
1691
  const log = overrides.log ?? ((m) => console.log(m));
1458
1692
  const errorLog = overrides.errorLog ?? ((m) => console.error(m));
@@ -1688,6 +1922,13 @@ export async function runDoctorCli(argv, overrides = {}) {
1688
1922
  ((p) => readFile(p, "utf-8")),
1689
1923
  stat: overrides.executorService?.stat ?? injectedFs.stat ?? ((p) => stat(p)),
1690
1924
  resolveCredentials: overrides.executorService?.resolveCredentials,
1925
+ // BAPI-1137 — the durability prerequisite needs a uid and a read-only
1926
+ // runner. `process.getuid` is absent on win32, where a null uid simply
1927
+ // leaves the fact unverifiable rather than interpolating `undefined`
1928
+ // into a domain target.
1929
+ uid: overrides.executorService?.uid ??
1930
+ (typeof process.getuid === "function" ? process.getuid() : null),
1931
+ runLifecycleCommand: overrides.executorService?.runLifecycleCommand ?? defaultReadOnlyLifecycleRunner,
1691
1932
  };
1692
1933
  const executorDiagnostics = await collectExecutorServiceDiagnostics(executorDeps);
1693
1934
  log(formatExecutorServiceDiagnosticsReport(executorDiagnostics));
@@ -1745,6 +1986,27 @@ export async function runDoctorCli(argv, overrides = {}) {
1745
1986
  }));
1746
1987
  }
1747
1988
  }
1989
+ // Advisory epic-launch profile section (BAPI-1260). Read-only: it never binds,
1990
+ // migrates, or rewrites a profile or a credential — setup does that, with
1991
+ // consent. Never affects the exit code.
1992
+ if (overrides.launchProfiles !== false) {
1993
+ try {
1994
+ const injectedFs = deps;
1995
+ const report = formatEpicLaunchProfileDiagnosticReport(await collectEpicLaunchProfileDiagnostic({
1996
+ env: overrides.launchProfiles?.env ?? deps.env,
1997
+ homedir: overrides.launchProfiles?.homedir ?? injectedFs.homedir ?? os.homedir,
1998
+ readFile: overrides.launchProfiles?.readFile ??
1999
+ injectedFs.readFile ??
2000
+ ((p) => readFile(p, "utf-8")),
2001
+ hostname: overrides.launchProfiles?.hostname ?? os.hostname,
2002
+ }));
2003
+ if (report !== null)
2004
+ log(report);
2005
+ }
2006
+ catch {
2007
+ /* advisory only: an unexpected failure drops the section, never the doctor */
2008
+ }
2009
+ }
1748
2010
  // Advisory operational-advisories section (BAPI-963). Read by the operator
1749
2011
  // asking "what is already running?" right after the readiness sections above.
1750
2012
  //