@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.
- package/README.md +231 -113
- package/build/agent-guidance.generated.js +1 -1
- package/build/agent-notes.js +7 -1
- package/build/agent-registry.js +15 -0
- package/build/agent-utils.js +16 -8
- package/build/claude-user-config-doctor.js +1 -35
- package/build/codex-bridge-profile.js +1 -1
- package/build/commands.generated.js +7 -8
- package/build/conductor/bridge-api-client.js +202 -2
- package/build/conductor/bring-up-facts.js +1 -1
- package/build/conductor/claude-hook.js +16 -16
- package/build/conductor/doctor.js +279 -72
- package/build/conductor/epic-implementer-cli.js +38 -5
- package/build/conductor/epic-reconcile.js +1 -1
- package/build/conductor/epic-runtime.js +21 -12
- package/build/conductor/errors.js +3 -3
- package/build/conductor/file-scope-guard.js +4 -4
- package/build/conductor/git-ci-types.js +1 -1
- package/build/conductor/git-inspection.js +2 -2
- package/build/conductor/install-doctor.js +28 -6
- package/build/conductor/local-merge.js +319 -26
- package/build/conductor/merge-execution-contract.js +61 -0
- package/build/conductor/merge-execution-eligibility.js +129 -0
- package/build/conductor/merge-ledger.js +2 -2
- package/build/conductor/pr-ci-producer.js +6 -6
- package/build/conductor/pr-review-producer.js +2 -2
- package/build/conductor/producer-ledger.js +2 -0
- package/build/conductor/readiness-sections.js +166 -9
- package/build/conductor/readiness.js +97 -16
- package/build/conductor/recovery-cli.js +243 -12
- package/build/conductor/recovery-operations.js +64 -6
- package/build/conductor/repair-contract.js +38 -4
- package/build/conductor/spec-review-producer.js +6 -6
- package/build/conductor/store.js +10 -10
- package/build/conductor/supervisor-config.js +14 -14
- package/build/conductor/supervisor-ledger.js +2 -2
- package/build/conductor/supervisor-message-relay.js +2 -2
- package/build/conductor/worker-ledger-cli.js +19 -19
- package/build/conductor-bundle-artifacts.js +2 -2
- package/build/connect-bitbucket-api.js +6 -1
- package/build/connect-bitbucket.js +398 -88
- package/build/credential-store.js +16 -0
- package/build/docs.generated.js +2 -2
- package/build/doctor.js +199 -303
- package/build/drive-epic.js +88 -71
- package/build/epic-implementer-bin.js +22 -17
- package/build/epic-implementer-claude-hook-bin.js +4 -2
- package/build/epic-implementer-env.js +184 -0
- package/build/epic-integration-pr.js +2 -2
- package/build/executor/ci-rerun-job.js +269 -0
- package/build/executor/cli.js +31 -0
- package/build/executor/credentials.js +11 -0
- package/build/executor/env.js +79 -8
- package/build/executor/heartbeat.js +1 -1
- package/build/executor/index.js +21 -4
- package/build/executor/job-errors.js +3 -3
- package/build/executor/job-runner.js +127 -1
- package/build/executor/job-types.js +12 -2
- package/build/executor/merge-job.js +46 -5
- package/build/executor/observation.js +1 -1
- package/build/executor/preflight.js +1 -1
- package/build/executor/prompt-spec.js +1 -1
- package/build/executor/reconcile-overlap-job.js +10 -130
- package/build/executor/reconcile-overlap-types.js +1 -1
- package/build/executor/runner.js +1 -1
- package/build/executor/service-lifecycle.js +136 -0
- package/build/executor/service-unit.js +427 -9
- package/build/executor/types.js +5 -0
- package/build/index.js +320 -342
- package/build/init.js +20 -28
- package/build/install-bridge-conductor.js +208 -15
- package/build/install-bridge.js +555 -37
- package/build/mcp-host-entry-adapter.js +6 -0
- package/build/mcp-host-targets.js +2 -0
- package/build/mcp-install-state.js +42 -4
- package/build/mcp-invoke.js +5 -5
- package/build/mcp-profile.js +2 -2
- package/build/mcp-provisioning.js +2 -2
- package/build/mcp-server-invocation.js +3 -3
- package/build/merge-pull-request.js +118 -10
- package/build/merge-workflow-codeowners.js +224 -0
- package/build/merge-workflow-config.js +133 -0
- package/build/merge-workflow-drift.js +66 -0
- package/build/merge-workflow-install.js +638 -0
- package/build/merge-workflow.js +572 -0
- package/build/model-routing-host.js +25 -0
- package/build/model-routing-preferences.js +99 -0
- package/build/pipelines.generated.js +10 -10
- package/build/{plan-epic-conductor-eligibility.js → plan-epic-implementer-eligibility.js} +18 -16
- package/build/plan-execution-ledger.js +8 -0
- package/build/plane/cli.js +24 -2
- package/build/plane/member-roster.js +5 -5
- package/build/plane/preflight.js +68 -0
- package/build/plane/supervisor.js +1 -1
- package/build/plane/types.js +5 -5
- package/build/readiness-check.js +1 -1
- package/build/readme.generated.js +1 -1
- package/build/review-tickets.js +4 -4
- package/build/run-unit-tests-launcher.js +5 -0
- package/build/setup-epic.js +171 -17
- package/build/start-tickets-conductor.js +32 -32
- package/build/start-tickets-prereqs.js +4 -9
- package/build/start-tickets.js +475 -91
- package/build/version.generated.js +3 -3
- package/docs/CONDUCTOR.md +69 -18
- package/docs/install/mcp-tool-integrations.md +2 -1
- package/package.json +4 -4
- package/pipelines/implement-ticket.json +1 -1
- package/pipelines/plan-epic.json +2 -2
- package/build/conduct-epic/bridge-client.js +0 -773
- package/build/conduct-epic/checkpoint-store.js +0 -542
- package/build/conduct-epic/cli.js +0 -3145
- package/build/conduct-epic/cut-protocol.js +0 -392
- package/build/conduct-epic/lock.js +0 -302
- package/build/conduct-epic/pr-state.js +0 -286
- package/build/conduct-epic/spawn.js +0 -113
- package/build/conductor/cli.js +0 -1278
- package/build/conductor-bundle-cli.js +0 -256
- package/build/direct-ticket-tools.js +0 -70
- package/build/executor/reconcile-overlap-governance.js +0 -129
- package/build/implement-epic/checkpoint-store.js +0 -542
- package/build/implement-epic/cli.js +0 -3158
- package/build/implement-epic/lock.js +0 -302
- package/build/implement-epic/pr-state.js +0 -286
- package/build/implement-epic/spawn.js +0 -113
- /package/build/{implement-epic → epic-shared}/bridge-client.js +0 -0
- /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
|
|
14
|
-
* spawning, no MCP server startup.
|
|
15
|
-
* (`which`/`where`, `bash --version`,
|
|
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 "./
|
|
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,
|
|
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
|
-
|
|
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
|
|
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
|
-
*
|
|
1505
|
-
*
|
|
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.
|
|
1676
|
-
"
|
|
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
|
|
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
|
|
1977
|
-
//
|
|
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.
|