@bridge_gpt/mcp-server 0.2.38 → 0.2.41
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 +189 -14
- package/build/agent-capabilities/probe-context.js +2 -1
- package/build/agent-launchers/claude-executor-adapter.js +392 -0
- package/build/agent-launchers/executor-adapter-inspection.js +163 -0
- package/build/agent-launchers/executor-adapter-registry.js +90 -0
- package/build/agent-launchers/executor-adapter.js +136 -0
- package/build/agent-registry.js +28 -0
- package/build/agents.generated.js +1 -1
- package/build/claude-login.js +85 -0
- package/build/claude-user-config-doctor.js +59 -33
- package/build/commands.generated.js +12 -11
- package/build/conduct-epic/bridge-client.js +345 -0
- package/build/conduct-epic/checkpoint-store.js +479 -0
- package/build/conduct-epic/cli.js +1765 -0
- package/build/conduct-epic/lock.js +302 -0
- package/build/conduct-epic/pr-state.js +286 -0
- package/build/conduct-epic/spawn.js +101 -0
- package/build/conductor/bridge-api-client.js +37 -2
- package/build/conductor/doctor.js +11 -1
- package/build/conductor/install-doctor.js +184 -10
- package/build/conductor-bin.js +7 -7
- package/build/credential-store.js +10 -4
- package/build/credentials-cli.js +34 -19
- package/build/docs.generated.js +1 -1
- package/build/doctor.js +579 -88
- package/build/executor/agent-identity.js +32 -0
- package/build/executor/cli.js +50 -39
- package/build/executor/deps.js +15 -1
- package/build/executor/env.js +56 -45
- package/build/executor/index.js +9 -1
- package/build/executor/install-preflight.js +138 -0
- package/build/executor/job-errors.js +200 -0
- package/build/executor/job-runner.js +619 -268
- package/build/executor/observation.js +165 -0
- package/build/executor/permissions.js +163 -36
- package/build/executor/platform.js +54 -0
- package/build/executor/preflight.js +175 -67
- package/build/executor/process.js +39 -7
- package/build/executor/runner.js +19 -0
- package/build/executor/service-lifecycle.js +269 -0
- package/build/executor/service-unit.js +121 -12
- package/build/executor/stale-artifacts.js +70 -0
- package/build/executor/test-clock.js +188 -24
- package/build/executor/worker-command.js +22 -58
- package/build/executor/worker-log.js +82 -0
- package/build/executor/worktree-lock.js +264 -0
- package/build/index.js +527 -357
- package/build/install-bridge-conductor.js +376 -38
- package/build/install-bridge.js +414 -114
- package/build/install-doctor.js +13 -0
- package/build/install-reexec.js +5 -3
- package/build/mcp-install-state.js +130 -0
- package/build/mcp-profile.js +11 -2
- package/build/mcp-provisioning.js +15 -0
- package/build/merge-pull-request.js +562 -0
- package/build/phase-result-artifacts.js +450 -0
- package/build/pipeline-orchestrator.js +4 -0
- package/build/pipeline-utils.js +16 -0
- package/build/pipelines.generated.js +7 -7
- package/build/plane/preflight.js +18 -14
- package/build/plane/supervisor.js +8 -1
- package/build/project-root.js +34 -0
- package/build/readme.generated.js +1 -1
- package/build/run-unit-tests-launcher.js +36 -9
- package/build/setup-epic.js +57 -4
- package/build/sfcc/permissions.js +25 -6
- package/build/sfcc/reads-site-preference.js +6 -0
- package/build/sfcc/register.js +61 -23
- package/build/sfcc/registration-inventory.js +89 -0
- package/build/sfcc/setup-status.js +18 -34
- package/build/sfcc/tool-wrapper.js +294 -17
- package/build/sfcc/write-grants.js +33 -1
- package/build/sfcc/write-guard.js +41 -12
- package/build/sfcc/writes-custom-object-def.js +6 -2
- package/build/sfcc/writes-site-preference.js +6 -1
- package/build/sfcc/writes-system-object.js +11 -2
- package/build/sfcc/writes.js +13 -8
- package/build/start-tickets-prereqs.js +25 -15
- package/build/start-tickets.js +123 -21
- package/build/version.generated.js +1 -1
- package/build/worktree-core.js +9 -3
- package/docs/install/mcp-tool-integrations.md +54 -9
- package/docs/install/sfcc-integration.md +71 -24
- package/package.json +3 -3
- package/build/executor/worker-config-isolation.js +0 -287
package/build/doctor.js
CHANGED
|
@@ -21,18 +21,24 @@ import path from "path";
|
|
|
21
21
|
import { createDefaultStartTicketsDeps } from "./start-tickets.js";
|
|
22
22
|
import { VERSION } from "./version.generated.js";
|
|
23
23
|
import { collectInstallStatusChecks, formatInstallStatusReport, formatInstallStatusFallbackReport, resolveInstallDoctorTarget, } from "./install-doctor.js";
|
|
24
|
+
import { inspectExecutorAgentAdapter, inspectionForFailedResolution, } from "./agent-launchers/executor-adapter-inspection.js";
|
|
25
|
+
import { resolveExecutorAgentAdapter } from "./agent-launchers/executor-adapter-registry.js";
|
|
24
26
|
import { parseDefaultOnEnvFlag } from "./env-flags.js";
|
|
25
27
|
import { createBridgeApiUrls } from "./bridge-api-urls.js";
|
|
26
28
|
import { probeToolSurface } from "./tool-surface-gating.js";
|
|
27
29
|
import { resolveBapiCredentials } from "./credential-store.js";
|
|
30
|
+
import { resolveConductorBridgeApiAccess } from "./conductor/bridge-api-client.js";
|
|
31
|
+
import { getIndexBranch } from "./conduct-epic/bridge-client.js";
|
|
32
|
+
import { resolveConductEpicStateDirectory } from "./conduct-epic/checkpoint-store.js";
|
|
33
|
+
import { isConductEpicLockOwnerAlive, parseConductEpicLock, } from "./conduct-epic/lock.js";
|
|
28
34
|
import { resolveRequiredStartTicketsRepoName } from "./start-tickets-repo.js";
|
|
29
35
|
import { BRIDGE_PACKAGE_NAME, describeLauncherReason, inspectLauncherConfigs, parseLauncherPin as parseSharedLauncherPin, } from "./launcher-config-inspection.js";
|
|
30
36
|
import { DEFAULT_AGENT_NAME, resolveAgentSpec, isAgentName, formatValidAgentNames, } from "./agent-registry.js";
|
|
31
|
-
import { getDoctorPrereqDescriptors, probePrerequisite, } from "./start-tickets-prereqs.js";
|
|
37
|
+
import { getDoctorPrereqDescriptors, probePrerequisite, resolveWorktrunkBinary, } from "./start-tickets-prereqs.js";
|
|
32
38
|
import { resolveProfiles } from "./mcp-profile.js";
|
|
33
39
|
import { executorLaunchdDirForHome, executorSystemdDirForHome, executorIdFromLaunchdFilename, executorIdFromSystemdFilename, inspectExecutorServiceArtifact, EXECUTOR_SERVICE_BASE_URL_ENV, } from "./executor/index.js";
|
|
34
40
|
import { DEFAULT_BAPI_BASE_URL } from "./executor/credentials.js";
|
|
35
|
-
import {
|
|
41
|
+
import { detectClaudeLogin, formatClaudeLoginAdvisory } from "./claude-login.js";
|
|
36
42
|
/**
|
|
37
43
|
* The report/usage title (BAPI-669, U9b). `doctor` diagnoses the whole Bridge
|
|
38
44
|
* install — install status, prerequisites, launcher cache, and tool surface — so it
|
|
@@ -75,14 +81,28 @@ export function getDoctorUsage() {
|
|
|
75
81
|
"migrate a credential, use /install-bridge or the `credentials` subcommand —",
|
|
76
82
|
"doctor stays strictly read-only.",
|
|
77
83
|
"",
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
"
|
|
85
|
-
"
|
|
84
|
+
// BAPI-815/R4: the old text here promised a 'Worker config isolation",
|
|
85
|
+
// (conductor executor)' section naming a CLAUDE_CONFIG_DIR strategy. BAPI-790
|
|
86
|
+
// replaced per-job config-directory isolation with --strict-mcp-config argv
|
|
87
|
+
// scoping, and BAPI-791 deleted that doctor section together with the whole
|
|
88
|
+
// worker-auth strategy vocabulary. The section it described exists in no
|
|
89
|
+
// doctor output, so the claim is removed rather than reworded.
|
|
90
|
+
"Worker containment is no longer reported here. A conductor worker is scoped",
|
|
91
|
+
"at spawn time with `--strict-mcp-config --mcp-config <file>`, and the",
|
|
92
|
+
"executor asserts that scope per spawn from the worker's own system/init",
|
|
93
|
+
"event — failing the job with ContractError.McpSurfaceMismatch on a confirmed",
|
|
94
|
+
"leak. That per-spawn assertion, not a doctor health line, is the containment",
|
|
95
|
+
"proof; see docs/claude/worker-config-isolation-drill.md.",
|
|
96
|
+
"",
|
|
97
|
+
"It also includes an advisory 'Local CLI launcher' section (BAPI-815): whether",
|
|
98
|
+
"the BAPI_MCP_CLI override is set, and whether the launcher it names can be",
|
|
99
|
+
"executed. When the override is unset, NO launcher probe runs at all. When it",
|
|
100
|
+
"is set, doctor runs that launcher exactly once with a single appended",
|
|
101
|
+
"--version argument through an array-based spawn — never a shell — reads only",
|
|
102
|
+
"its exit status, and discards its stdout and stderr so local output cannot",
|
|
103
|
+
"enter this report. A value containing shell operators or an unbalanced quote",
|
|
104
|
+
"is rejected without running anything. The whole section is advisory: a",
|
|
105
|
+
"missing, malformed, or unrunnable override never changes the exit code.",
|
|
86
106
|
"",
|
|
87
107
|
"It also includes an advisory 'MCP tool surface' section (BAPI-641): what",
|
|
88
108
|
"dynamic capability gating would advertise for this repo. It performs at most",
|
|
@@ -262,6 +282,84 @@ export function formatDoctorReport(platform, agent, collection) {
|
|
|
262
282
|
formatDoctorPrereqSection(platform, collection),
|
|
263
283
|
].join("\n");
|
|
264
284
|
}
|
|
285
|
+
/**
|
|
286
|
+
* Inspect the selected agent's executor adapter.
|
|
287
|
+
*
|
|
288
|
+
* Probes the executable version (cheap, read-only) but NOT deny enforcement:
|
|
289
|
+
* that probe spawns a real headless agent, and `conductor doctor` already owns
|
|
290
|
+
* that diagnostic behind its own `--no-deny-probe` switch. Duplicating it here
|
|
291
|
+
* would make a plain `doctor` run spawn an agent the operator did not ask for.
|
|
292
|
+
*/
|
|
293
|
+
export async function collectExecutorAdapterDiagnostic(agentName, deps) {
|
|
294
|
+
const resolve = deps.resolveAdapter ?? ((id) => resolveExecutorAgentAdapter(id));
|
|
295
|
+
const resolution = resolve(agentName);
|
|
296
|
+
if (!resolution.supported) {
|
|
297
|
+
// An unsupported agent is REPORTED as unsupported, never substituted with
|
|
298
|
+
// Claude's metadata. Showing Claude's adapter under another agent's name
|
|
299
|
+
// would tell an operator their agent is ready when nothing implements it.
|
|
300
|
+
return inspectionForFailedResolution(agentName, resolution);
|
|
301
|
+
}
|
|
302
|
+
const result = await inspectExecutorAgentAdapter(resolution.adapter, { platform: deps.platform, env: deps.env, runCommand: deps.runCommand }, { probeExecutable: true });
|
|
303
|
+
return result.inspection;
|
|
304
|
+
}
|
|
305
|
+
/** Render the adapter section. Pure formatting — no probing. */
|
|
306
|
+
export function formatExecutorAdapterDiagnosticReport(inspection) {
|
|
307
|
+
const lines = [];
|
|
308
|
+
lines.push("");
|
|
309
|
+
lines.push("Executor agent adapter (conductor worker spawning)");
|
|
310
|
+
lines.push("─────────────────────────────────────────────────");
|
|
311
|
+
lines.push(`agent: ${inspection.identity.agentId}`);
|
|
312
|
+
lines.push(`adapter: ${inspection.identity.adapterId}`);
|
|
313
|
+
lines.push(`adapter version: ${inspection.identity.adapterVersion}`);
|
|
314
|
+
lines.push(`strategy: ${inspection.identity.strategyId}`);
|
|
315
|
+
if (inspection.executable) {
|
|
316
|
+
const exe = inspection.executable;
|
|
317
|
+
const version = exe.version.length > 0 ? exe.version : "n/a";
|
|
318
|
+
lines.push(`executable: ${exe.command}`);
|
|
319
|
+
lines.push(`runtime version: ${version}`);
|
|
320
|
+
lines.push(`fingerprint: ${exe.fingerprint ?? "n/a"}`);
|
|
321
|
+
if (exe.failure)
|
|
322
|
+
lines.push(`executable probe: FAILED (${exe.failure})`);
|
|
323
|
+
}
|
|
324
|
+
if (inspection.platform) {
|
|
325
|
+
lines.push(inspection.platform.supported
|
|
326
|
+
? `platform: ${inspection.platform.platform} [SUPPORTED]`
|
|
327
|
+
: `platform: ${inspection.platform.platform} [UNSUPPORTED] ${inspection.platform.message}`);
|
|
328
|
+
}
|
|
329
|
+
if (inspection.capabilities.length > 0) {
|
|
330
|
+
lines.push("capabilities:");
|
|
331
|
+
for (const capability of inspection.capabilities) {
|
|
332
|
+
const status = capability.supported ? "OK " : "MISSING ";
|
|
333
|
+
const strategy = capability.strategyId ? ` (${capability.strategyId})` : "";
|
|
334
|
+
const reason = capability.reason ? ` — ${capability.reason}` : "";
|
|
335
|
+
lines.push(` ${status} ${capability.capability}${strategy}${reason}`);
|
|
336
|
+
}
|
|
337
|
+
}
|
|
338
|
+
lines.push(`lifecycle: ${inspection.lifecycleKind ?? "NOT DECLARED"}`);
|
|
339
|
+
if (inspection.auth) {
|
|
340
|
+
// Names, ownership, and billing class only — never a value.
|
|
341
|
+
const managed = inspection.auth.managedCarriers;
|
|
342
|
+
lines.push(managed.length === 0
|
|
343
|
+
? "bridge-managed creds: none (workers use the operator's own login)"
|
|
344
|
+
: `bridge-managed creds: ${managed
|
|
345
|
+
.map((c) => `${c.carrierId} [${c.billingClass}] env=${c.secretEnvNames.join(",")}`)
|
|
346
|
+
.join("; ")}`);
|
|
347
|
+
lines.push(inspection.auth.passthroughs.length === 0
|
|
348
|
+
? "operator passthroughs: none"
|
|
349
|
+
: `operator passthroughs: ${inspection.auth.passthroughs
|
|
350
|
+
.map((p) => `${p.envName} [${p.billingClass}, ${p.rule}]`)
|
|
351
|
+
.join("; ")}`);
|
|
352
|
+
}
|
|
353
|
+
if (!inspection.ok) {
|
|
354
|
+
lines.push("");
|
|
355
|
+
lines.push("This agent CANNOT spawn conductor workers. A missing or unsupported capability is " +
|
|
356
|
+
"refused outright — no other agent's behavior is substituted.");
|
|
357
|
+
}
|
|
358
|
+
for (const warning of inspection.warnings) {
|
|
359
|
+
lines.push(` - ${warning}`);
|
|
360
|
+
}
|
|
361
|
+
return lines.join("\n");
|
|
362
|
+
}
|
|
265
363
|
/**
|
|
266
364
|
* Extract the `@bridge_gpt/mcp-server[@version]` launcher spec from an args array.
|
|
267
365
|
*
|
|
@@ -418,6 +516,159 @@ export function formatLauncherCacheReport(inspections) {
|
|
|
418
516
|
}
|
|
419
517
|
return lines.join("\n");
|
|
420
518
|
}
|
|
519
|
+
// ---------------------------------------------------------------------------
|
|
520
|
+
// Local CLI launcher override (BAPI-815/R5) — advisory, strictly read-only.
|
|
521
|
+
//
|
|
522
|
+
// `BAPI_MCP_CLI` lets a packaged command shell out to a LOCAL build instead of
|
|
523
|
+
// `npx -y @bridge_gpt/mcp-server`, so an executor or CLI change can be verified
|
|
524
|
+
// before an npm publish. That is useful precisely because it is invisible: the
|
|
525
|
+
// override lives in one environment variable, and `ping` cannot tell a published
|
|
526
|
+
// 0.2.x from a local build at the same version. This section makes it visible.
|
|
527
|
+
//
|
|
528
|
+
// The probe is deliberately narrow. It runs the configured launcher ONCE with a
|
|
529
|
+
// single appended `--version` argument, through the injected array-based command
|
|
530
|
+
// runner — never a shell, never `eval`, never a concatenated command string. The
|
|
531
|
+
// probe's stdout and stderr are discarded after its exit status is read, so
|
|
532
|
+
// arbitrary local output cannot enter the doctor report. It is ADVISORY: a
|
|
533
|
+
// missing, malformed, or unrunnable override never changes the exit code.
|
|
534
|
+
// ---------------------------------------------------------------------------
|
|
535
|
+
/** The environment variable carrying the local launcher override. */
|
|
536
|
+
export const LOCAL_CLI_OVERRIDE_ENV = "BAPI_MCP_CLI";
|
|
537
|
+
/**
|
|
538
|
+
* Longest configured value echoed back into the report. The contract forbids
|
|
539
|
+
* putting a credential in the launcher, but a bound is cheap insurance against
|
|
540
|
+
* a report that dumps an arbitrarily long environment value either way.
|
|
541
|
+
*/
|
|
542
|
+
const LOCAL_CLI_OVERRIDE_DISPLAY_LIMIT = 200;
|
|
543
|
+
/**
|
|
544
|
+
* Characters that would turn the value into a shell command LINE rather than a
|
|
545
|
+
* command plus arguments. This runner never uses a shell, so these could not be
|
|
546
|
+
* interpreted anyway — but a value containing them means the operator EXPECTED
|
|
547
|
+
* shell semantics, and silently running only the first word would be a worse
|
|
548
|
+
* answer than refusing. Parentheses and braces are deliberately absent: they
|
|
549
|
+
* appear in ordinary macOS and Windows paths and are inert without a shell.
|
|
550
|
+
*/
|
|
551
|
+
const SHELL_OPERATOR_PATTERN = /[;&|<>$`\n\r]/;
|
|
552
|
+
/**
|
|
553
|
+
* Split the override into an executable and its fixed arguments.
|
|
554
|
+
*
|
|
555
|
+
* A deliberately small, non-shell tokenizer: whitespace separates tokens, and
|
|
556
|
+
* single or double quotes group a token so a launcher path containing spaces
|
|
557
|
+
* (`node "/Applications/My Build/index.js"`) survives intact. It performs no
|
|
558
|
+
* expansion of any kind — no globbing, no variable substitution, no command
|
|
559
|
+
* substitution — because the result is passed to an array-based spawn where
|
|
560
|
+
* none of that would be interpreted.
|
|
561
|
+
*/
|
|
562
|
+
export function parseLocalCliLauncher(raw) {
|
|
563
|
+
if (SHELL_OPERATOR_PATTERN.test(raw)) {
|
|
564
|
+
return { ok: false, reason: "contains shell operators, which are not supported" };
|
|
565
|
+
}
|
|
566
|
+
const tokens = [];
|
|
567
|
+
let current = "";
|
|
568
|
+
let quote = null;
|
|
569
|
+
let started = false;
|
|
570
|
+
for (const ch of raw) {
|
|
571
|
+
if (quote) {
|
|
572
|
+
if (ch === quote)
|
|
573
|
+
quote = null;
|
|
574
|
+
else
|
|
575
|
+
current += ch;
|
|
576
|
+
continue;
|
|
577
|
+
}
|
|
578
|
+
if (ch === '"' || ch === "'") {
|
|
579
|
+
quote = ch;
|
|
580
|
+
started = true;
|
|
581
|
+
continue;
|
|
582
|
+
}
|
|
583
|
+
if (ch === " " || ch === "\t") {
|
|
584
|
+
if (started) {
|
|
585
|
+
tokens.push(current);
|
|
586
|
+
current = "";
|
|
587
|
+
started = false;
|
|
588
|
+
}
|
|
589
|
+
continue;
|
|
590
|
+
}
|
|
591
|
+
current += ch;
|
|
592
|
+
started = true;
|
|
593
|
+
}
|
|
594
|
+
if (quote)
|
|
595
|
+
return { ok: false, reason: "has an unbalanced quote" };
|
|
596
|
+
if (started)
|
|
597
|
+
tokens.push(current);
|
|
598
|
+
const [executable, ...args] = tokens;
|
|
599
|
+
if (!executable)
|
|
600
|
+
return { ok: false, reason: "is empty after parsing" };
|
|
601
|
+
return { ok: true, executable, args };
|
|
602
|
+
}
|
|
603
|
+
/** Truncate a configured value for display without ever dropping it silently. */
|
|
604
|
+
function boundConfiguredValue(raw) {
|
|
605
|
+
if (raw.length <= LOCAL_CLI_OVERRIDE_DISPLAY_LIMIT)
|
|
606
|
+
return raw;
|
|
607
|
+
return `${raw.slice(0, LOCAL_CLI_OVERRIDE_DISPLAY_LIMIT)}… (truncated)`;
|
|
608
|
+
}
|
|
609
|
+
/**
|
|
610
|
+
* Collect the advisory local-launcher diagnostic. Read-only and bounded: at most
|
|
611
|
+
* ONE `--version` invocation, and only when an override is actually configured.
|
|
612
|
+
*
|
|
613
|
+
* The "no override ⇒ no probe" rule is load-bearing beyond tidiness — doctor
|
|
614
|
+
* pins that an unsupported platform issues ZERO command probes, and a section
|
|
615
|
+
* that probed unconditionally would break that guarantee.
|
|
616
|
+
*/
|
|
617
|
+
export async function collectLocalCliOverrideDiagnostic(deps) {
|
|
618
|
+
const raw = deps.env[LOCAL_CLI_OVERRIDE_ENV];
|
|
619
|
+
if (typeof raw !== "string" || raw.trim().length === 0) {
|
|
620
|
+
return { state: "not-configured" };
|
|
621
|
+
}
|
|
622
|
+
const configured = boundConfiguredValue(raw.trim());
|
|
623
|
+
const parsed = parseLocalCliLauncher(raw.trim());
|
|
624
|
+
if (!parsed.ok) {
|
|
625
|
+
// Rejected WITHOUT running anything: an unsafe or malformed value must never
|
|
626
|
+
// reach the command runner, not even to discover that it fails.
|
|
627
|
+
return { state: "invalid", configured, reason: parsed.reason };
|
|
628
|
+
}
|
|
629
|
+
try {
|
|
630
|
+
const probe = await deps.runCommand(parsed.executable, [...parsed.args, "--version"]);
|
|
631
|
+
// Read the status, then drop the output. Nothing the local launcher printed
|
|
632
|
+
// is allowed into the report.
|
|
633
|
+
return probe.exitCode === 0
|
|
634
|
+
? { state: "executable", configured }
|
|
635
|
+
: { state: "not-executable", configured };
|
|
636
|
+
}
|
|
637
|
+
catch {
|
|
638
|
+
// A spawn failure (missing file, not executable) is the same advisory
|
|
639
|
+
// outcome as a non-zero exit. The exception itself never escapes.
|
|
640
|
+
return { state: "not-executable", configured };
|
|
641
|
+
}
|
|
642
|
+
}
|
|
643
|
+
/** Render the local-launcher override section (pure formatting). */
|
|
644
|
+
export function formatLocalCliOverrideDiagnosticReport(diagnostic) {
|
|
645
|
+
const lines = ["", `Local CLI launcher (${LOCAL_CLI_OVERRIDE_ENV})`, ""];
|
|
646
|
+
switch (diagnostic.state) {
|
|
647
|
+
case "not-configured":
|
|
648
|
+
lines.push("NOT CONFIGURED packaged launcher in use (npx -y @bridge_gpt/mcp-server)");
|
|
649
|
+
lines.push(" No local override is set, so no launcher probe was run.");
|
|
650
|
+
break;
|
|
651
|
+
case "executable":
|
|
652
|
+
lines.push(`CONFIGURED ${diagnostic.configured}`);
|
|
653
|
+
lines.push(" The launcher ran successfully with --version.");
|
|
654
|
+
lines.push(" A stale local build is exactly as misleading as a stale npm publish —");
|
|
655
|
+
lines.push(" rebuild with `cd mcp_server && npm run build` before relying on it.");
|
|
656
|
+
break;
|
|
657
|
+
case "not-executable":
|
|
658
|
+
lines.push(`CONFIGURED ${diagnostic.configured}`);
|
|
659
|
+
lines.push(" The launcher could not be executed (--version did not succeed).");
|
|
660
|
+
lines.push(" Check that the path exists and has been built.");
|
|
661
|
+
break;
|
|
662
|
+
case "invalid":
|
|
663
|
+
lines.push(`CONFIGURED ${diagnostic.configured}`);
|
|
664
|
+
lines.push(` The value could not be used: it ${diagnostic.reason}.`);
|
|
665
|
+
lines.push(" No launcher was run. Expected form: node /absolute/path/to/index.js");
|
|
666
|
+
break;
|
|
667
|
+
}
|
|
668
|
+
lines.push(" Advisory only: this finding never blocks packaged CLI use and never changes");
|
|
669
|
+
lines.push(" the doctor exit code.");
|
|
670
|
+
return lines.join("\n");
|
|
671
|
+
}
|
|
421
672
|
/**
|
|
422
673
|
* Collect the read-only tool-surface diagnostic. Evaluates the kill switch first
|
|
423
674
|
* and returns without any network request when disabled. For an enabled flag and
|
|
@@ -523,57 +774,6 @@ export function formatToolSurfaceDiagnosticReport(diag) {
|
|
|
523
774
|
lines.push("to observe surface changes; no project MCP configuration change is required.");
|
|
524
775
|
return lines.join("\n");
|
|
525
776
|
}
|
|
526
|
-
/**
|
|
527
|
-
* Adapt the executor's read-only capability evaluator for doctor.
|
|
528
|
-
*
|
|
529
|
-
* READ-ONLY BY CONSTRUCTION: it forwards only `platform` and `env`, and
|
|
530
|
-
* `evaluateWorkerConfigIsolationCapability` accepts no mutating dependency at
|
|
531
|
-
* all — no directory-creation, permission-setting, file-writing, or removal
|
|
532
|
-
* operation is reachable from it even in principle. No directory is seeded, no
|
|
533
|
-
* credential value is read, and nothing is persisted. That is what preserves
|
|
534
|
-
* doctor's read-only contract while still answering AC-4's health question.
|
|
535
|
-
*
|
|
536
|
-
* NOTE: this module is pinned by `tests/pytest/mcp_server/test_doctor_static_contract.py`,
|
|
537
|
-
* which asserts the literal token `write` + `File` appears nowhere in this file —
|
|
538
|
-
* so describe write operations in prose here rather than naming that API.
|
|
539
|
-
*/
|
|
540
|
-
export function collectWorkerConfigIsolationDiagnostic(input) {
|
|
541
|
-
const capability = evaluateWorkerConfigIsolationCapability({
|
|
542
|
-
platform: input.platform,
|
|
543
|
-
env: input.env,
|
|
544
|
-
});
|
|
545
|
-
if (capability.supported) {
|
|
546
|
-
return {
|
|
547
|
-
status: "healthy",
|
|
548
|
-
platform: capability.platform,
|
|
549
|
-
strategy: capability.strategy,
|
|
550
|
-
required: input.executorConfigured,
|
|
551
|
-
};
|
|
552
|
-
}
|
|
553
|
-
return {
|
|
554
|
-
status: "unhealthy",
|
|
555
|
-
platform: capability.platform,
|
|
556
|
-
remediation: formatWorkerConfigIsolationReason(capability.platform, capability.reason),
|
|
557
|
-
required: input.executorConfigured,
|
|
558
|
-
};
|
|
559
|
-
}
|
|
560
|
-
/** Render the isolation section: fixed labels plus already-bounded reason text. */
|
|
561
|
-
export function formatWorkerConfigIsolationDiagnosticReport(diagnostic) {
|
|
562
|
-
const lines = ["", "Worker config isolation (conductor executor)"];
|
|
563
|
-
if (diagnostic.status === "healthy") {
|
|
564
|
-
lines.push(` HEALTHY ${diagnostic.platform}: isolation can be established (strategy: ${diagnostic.strategy})`);
|
|
565
|
-
lines.push(" Workers run with an executor-owned CLAUDE_CONFIG_DIR, so operator-scoped");
|
|
566
|
-
lines.push(" MCP servers are unreachable from a worker.");
|
|
567
|
-
return lines.join("\n");
|
|
568
|
-
}
|
|
569
|
-
// An executor host cannot dispatch workers safely without isolation, so the
|
|
570
|
-
// label distinguishes a blocking condition from an advisory one.
|
|
571
|
-
lines.push(` ${diagnostic.required ? "UNHEALTHY" : "SKIP "} ${diagnostic.remediation ?? ""}`);
|
|
572
|
-
if (!diagnostic.required) {
|
|
573
|
-
lines.push(" Advisory only: no executor service is configured on this host.");
|
|
574
|
-
}
|
|
575
|
-
return lines.join("\n");
|
|
576
|
-
}
|
|
577
777
|
/** Where generated units live, and how their filenames encode the executor id. */
|
|
578
778
|
function resolveExecutorServiceConvention(platform, homeDir) {
|
|
579
779
|
if (platform === "darwin") {
|
|
@@ -765,8 +965,12 @@ export function formatExecutorServiceDiagnosticsReport(diagnostics) {
|
|
|
765
965
|
return lines.join("\n");
|
|
766
966
|
}
|
|
767
967
|
export async function collectPlaneCredentialDiagnostic(deps) {
|
|
768
|
-
|
|
769
|
-
|
|
968
|
+
// BAPI-791: worker authentication is no longer a plane-readiness question this
|
|
969
|
+
// section can answer — a worker authenticates through the executor host's own
|
|
970
|
+
// interactive `claude login`, reported once elsewhere (see
|
|
971
|
+
// `formatClaudeLoginAdvisory`), not per-repo here. This section is now scoped
|
|
972
|
+
// to exactly what `plane up` still refuses on for THIS repository: a resolvable
|
|
973
|
+
// Bridge credential.
|
|
770
974
|
const resolveRepo = deps.resolveRepoName ?? resolveRequiredStartTicketsRepoName;
|
|
771
975
|
let repo = null;
|
|
772
976
|
try {
|
|
@@ -778,7 +982,7 @@ export async function collectPlaneCredentialDiagnostic(deps) {
|
|
|
778
982
|
/* sanitized: identity resolution failure is reported as an absent repo */
|
|
779
983
|
}
|
|
780
984
|
if (repo === null) {
|
|
781
|
-
return {
|
|
985
|
+
return { repo: null, target: null, bridgeCredentialResolved: false };
|
|
782
986
|
}
|
|
783
987
|
// Resolve, then keep ONLY a label. The key never reaches the diagnostic.
|
|
784
988
|
const status = await probeRepoCredential(repo, { platform: deps.platform, env: deps.env, homedir: deps.homedir, readdir: async () => [], readFile: deps.readFile, stat: deps.stat, resolveCredentials: deps.resolveCredentials }, {
|
|
@@ -792,7 +996,6 @@ export async function collectPlaneCredentialDiagnostic(deps) {
|
|
|
792
996
|
},
|
|
793
997
|
});
|
|
794
998
|
return {
|
|
795
|
-
anthropicKeyPresent,
|
|
796
999
|
repo,
|
|
797
1000
|
target: status.target,
|
|
798
1001
|
bridgeCredentialResolved: status.resolved,
|
|
@@ -803,10 +1006,6 @@ export async function collectPlaneCredentialDiagnostic(deps) {
|
|
|
803
1006
|
/** Render the plane readiness section. Advisory: never changes the exit code. */
|
|
804
1007
|
export function formatPlaneCredentialDiagnosticReport(diagnostic) {
|
|
805
1008
|
const lines = ["", "Conductor plane credentials (`plane up` — advisory)"];
|
|
806
|
-
lines.push(diagnostic.anthropicKeyPresent
|
|
807
|
-
? " OK ANTHROPIC_API_KEY is set in this shell."
|
|
808
|
-
: " MISSING ANTHROPIC_API_KEY is not set in this shell. Isolated workers have no OAuth\n" +
|
|
809
|
-
" session and refuse to spawn without it, so `plane up` will refuse.");
|
|
810
1009
|
if (diagnostic.repo === null) {
|
|
811
1010
|
lines.push(" MISSING the Bridge repository identity could not be resolved. Set BAPI_REPO_NAME or\n" +
|
|
812
1011
|
" add a valid .bridge/config at the repository root.");
|
|
@@ -823,6 +1022,204 @@ export function formatPlaneCredentialDiagnosticReport(diagnostic) {
|
|
|
823
1022
|
lines.push(" no .bridge/plane/ directory, persists nothing, and starts no plane member.");
|
|
824
1023
|
return lines.join("\n");
|
|
825
1024
|
}
|
|
1025
|
+
/**
|
|
1026
|
+
* Collect conduct-epic readiness, read-only.
|
|
1027
|
+
*
|
|
1028
|
+
* The credential is resolved only to establish that it CAN be, and the resolved
|
|
1029
|
+
* access object is discarded immediately: only the repo name and a boolean
|
|
1030
|
+
* survive into the returned diagnostic, so no later formatting change can print
|
|
1031
|
+
* a key it never received.
|
|
1032
|
+
*/
|
|
1033
|
+
export async function collectConductEpicDiagnostic(deps) {
|
|
1034
|
+
const isAlive = deps.isProcessAlive ?? isConductEpicLockOwnerAlive;
|
|
1035
|
+
let ghPresent = false;
|
|
1036
|
+
let ghAuthenticated = false;
|
|
1037
|
+
try {
|
|
1038
|
+
const version = await deps.runCommand("gh", ["--version"], { cwd: deps.cwd });
|
|
1039
|
+
ghPresent = version.exitCode === 0;
|
|
1040
|
+
}
|
|
1041
|
+
catch {
|
|
1042
|
+
ghPresent = false;
|
|
1043
|
+
}
|
|
1044
|
+
if (ghPresent) {
|
|
1045
|
+
try {
|
|
1046
|
+
const auth = await deps.runCommand("gh", ["auth", "status"], { cwd: deps.cwd });
|
|
1047
|
+
ghAuthenticated = auth.exitCode === 0;
|
|
1048
|
+
}
|
|
1049
|
+
catch {
|
|
1050
|
+
ghAuthenticated = false;
|
|
1051
|
+
}
|
|
1052
|
+
}
|
|
1053
|
+
const worktrunkBinary = resolveWorktrunkBinary(deps.platform, deps.env);
|
|
1054
|
+
let worktrunkAvailable = false;
|
|
1055
|
+
try {
|
|
1056
|
+
const probe = await deps.runCommand(worktrunkBinary, ["--version"], { cwd: deps.cwd });
|
|
1057
|
+
worktrunkAvailable = probe.exitCode === 0;
|
|
1058
|
+
}
|
|
1059
|
+
catch {
|
|
1060
|
+
worktrunkAvailable = false;
|
|
1061
|
+
}
|
|
1062
|
+
const resolveAccess = deps.resolveAccess ?? resolveConductorBridgeApiAccess;
|
|
1063
|
+
let repo = null;
|
|
1064
|
+
let bridgeCredentialResolved = false;
|
|
1065
|
+
let accessForIndexProbe = null;
|
|
1066
|
+
try {
|
|
1067
|
+
const result = await resolveAccess({
|
|
1068
|
+
env: deps.env,
|
|
1069
|
+
cwd: deps.cwd,
|
|
1070
|
+
homedir: deps.homedir,
|
|
1071
|
+
platform: deps.platform,
|
|
1072
|
+
readFile: deps.readFile,
|
|
1073
|
+
stat: deps.stat,
|
|
1074
|
+
});
|
|
1075
|
+
if (result.ok) {
|
|
1076
|
+
repo = result.access.repoName;
|
|
1077
|
+
bridgeCredentialResolved = true;
|
|
1078
|
+
accessForIndexProbe = result;
|
|
1079
|
+
}
|
|
1080
|
+
}
|
|
1081
|
+
catch {
|
|
1082
|
+
bridgeCredentialResolved = false;
|
|
1083
|
+
}
|
|
1084
|
+
let stateDirectory = null;
|
|
1085
|
+
let stateDirectoryReadable = false;
|
|
1086
|
+
const checkpoints = [];
|
|
1087
|
+
const locks = [];
|
|
1088
|
+
if (repo !== null) {
|
|
1089
|
+
stateDirectory = resolveConductEpicStateDirectory(repo, {
|
|
1090
|
+
env: deps.env,
|
|
1091
|
+
homedir: deps.homedir,
|
|
1092
|
+
});
|
|
1093
|
+
let entries = [];
|
|
1094
|
+
try {
|
|
1095
|
+
entries = await deps.readdir(stateDirectory);
|
|
1096
|
+
stateDirectoryReadable = true;
|
|
1097
|
+
}
|
|
1098
|
+
catch {
|
|
1099
|
+
stateDirectoryReadable = false;
|
|
1100
|
+
}
|
|
1101
|
+
for (const entry of entries) {
|
|
1102
|
+
if (entry.endsWith(".json"))
|
|
1103
|
+
checkpoints.push(entry);
|
|
1104
|
+
if (!entry.endsWith(".lock"))
|
|
1105
|
+
continue;
|
|
1106
|
+
const epic = entry.slice(0, -".lock".length);
|
|
1107
|
+
let raw;
|
|
1108
|
+
try {
|
|
1109
|
+
raw = await deps.readFile(path.join(stateDirectory, entry));
|
|
1110
|
+
}
|
|
1111
|
+
catch {
|
|
1112
|
+
locks.push({ epic, owner_pid: null, host: null, alive: null, problem: "unreadable" });
|
|
1113
|
+
continue;
|
|
1114
|
+
}
|
|
1115
|
+
const owner = parseConductEpicLock(raw);
|
|
1116
|
+
if (owner === null) {
|
|
1117
|
+
locks.push({ epic, owner_pid: null, host: null, alive: null, problem: "malformed or unsupported version" });
|
|
1118
|
+
continue;
|
|
1119
|
+
}
|
|
1120
|
+
// Liveness is only meaningful for a lock taken on THIS host.
|
|
1121
|
+
const alive = owner.host === deps.env.HOSTNAME || owner.host === os.hostname()
|
|
1122
|
+
? isAlive(owner.owner_pid)
|
|
1123
|
+
: null;
|
|
1124
|
+
locks.push({ epic, owner_pid: owner.owner_pid, host: owner.host, alive });
|
|
1125
|
+
}
|
|
1126
|
+
}
|
|
1127
|
+
let indexOverrideBranch = null;
|
|
1128
|
+
let indexOverrideChecked = false;
|
|
1129
|
+
if (accessForIndexProbe !== null && accessForIndexProbe.ok) {
|
|
1130
|
+
const probe = deps.getIndexBranch ?? getIndexBranch;
|
|
1131
|
+
try {
|
|
1132
|
+
const result = await probe(accessForIndexProbe.access);
|
|
1133
|
+
if (result.ok) {
|
|
1134
|
+
indexOverrideChecked = true;
|
|
1135
|
+
indexOverrideBranch = result.value.override?.override_branch ?? null;
|
|
1136
|
+
}
|
|
1137
|
+
}
|
|
1138
|
+
catch {
|
|
1139
|
+
indexOverrideChecked = false;
|
|
1140
|
+
}
|
|
1141
|
+
}
|
|
1142
|
+
return {
|
|
1143
|
+
ghPresent,
|
|
1144
|
+
ghAuthenticated,
|
|
1145
|
+
worktrunkBinary,
|
|
1146
|
+
worktrunkAvailable,
|
|
1147
|
+
repo,
|
|
1148
|
+
bridgeCredentialResolved,
|
|
1149
|
+
stateDirectory,
|
|
1150
|
+
stateDirectoryReadable,
|
|
1151
|
+
checkpoints,
|
|
1152
|
+
locks,
|
|
1153
|
+
indexOverrideBranch,
|
|
1154
|
+
indexOverrideChecked,
|
|
1155
|
+
};
|
|
1156
|
+
}
|
|
1157
|
+
/** Render the conduct-epic section. Advisory: never changes the exit code. */
|
|
1158
|
+
export function formatConductEpicDiagnosticReport(diagnostic) {
|
|
1159
|
+
const lines = ["", "conduct-epic readiness (advisory)"];
|
|
1160
|
+
lines.push(diagnostic.ghAuthenticated
|
|
1161
|
+
? " OK gh: present and authenticated."
|
|
1162
|
+
: diagnostic.ghPresent
|
|
1163
|
+
? " MISSING gh: present but NOT authenticated. Run `gh auth login`."
|
|
1164
|
+
: " MISSING gh: not found on PATH. `conduct-epic init` and `status` need it.");
|
|
1165
|
+
lines.push(diagnostic.worktrunkAvailable
|
|
1166
|
+
? ` OK Worktrunk: '${diagnostic.worktrunkBinary}' resolves.`
|
|
1167
|
+
: ` MISSING Worktrunk: '${diagnostic.worktrunkBinary}' not found (honors BAPI_WORKTRUNK_BIN).`);
|
|
1168
|
+
if (diagnostic.repo === null) {
|
|
1169
|
+
lines.push(" MISSING Bridge identity: the repository could not be resolved. Set BAPI_REPO_NAME\n" +
|
|
1170
|
+
" or add a valid .bridge/config at the repository root.");
|
|
1171
|
+
}
|
|
1172
|
+
else if (diagnostic.bridgeCredentialResolved) {
|
|
1173
|
+
lines.push(` OK Bridge credential for ${diagnostic.repo}: resolved.`);
|
|
1174
|
+
}
|
|
1175
|
+
else {
|
|
1176
|
+
lines.push(` MISSING Bridge credential for ${diagnostic.repo}: NOT resolved.`);
|
|
1177
|
+
}
|
|
1178
|
+
if (diagnostic.stateDirectory === null) {
|
|
1179
|
+
lines.push(" SKIP Checkpoint directory: not resolved (no repository identity).");
|
|
1180
|
+
}
|
|
1181
|
+
else if (!diagnostic.stateDirectoryReadable) {
|
|
1182
|
+
lines.push(` SKIP Checkpoint directory ${diagnostic.stateDirectory}: absent or unreadable\n` +
|
|
1183
|
+
" (normal before the first `conduct-epic init`).");
|
|
1184
|
+
}
|
|
1185
|
+
else {
|
|
1186
|
+
lines.push(` OK Checkpoint directory ${diagnostic.stateDirectory}: readable, ` +
|
|
1187
|
+
`${diagnostic.checkpoints.length} checkpoint(s).`);
|
|
1188
|
+
}
|
|
1189
|
+
if (diagnostic.locks.length === 0) {
|
|
1190
|
+
lines.push(" OK Locks: none held.");
|
|
1191
|
+
}
|
|
1192
|
+
else {
|
|
1193
|
+
for (const lock of diagnostic.locks) {
|
|
1194
|
+
if (lock.problem !== undefined) {
|
|
1195
|
+
lines.push(` WARN Lock ${lock.epic}: ${lock.problem}. Inspect it by hand.`);
|
|
1196
|
+
}
|
|
1197
|
+
else if (lock.alive === false) {
|
|
1198
|
+
lines.push(` WARN Lock ${lock.epic}: owner pid ${lock.owner_pid} on ${lock.host} is gone (stale).\n` +
|
|
1199
|
+
" The next mutating verb recovers it automatically.");
|
|
1200
|
+
}
|
|
1201
|
+
else if (lock.alive === null) {
|
|
1202
|
+
lines.push(` WARN Lock ${lock.epic}: taken on host ${lock.host}; liveness is unknowable here.`);
|
|
1203
|
+
}
|
|
1204
|
+
else {
|
|
1205
|
+
lines.push(` OK Lock ${lock.epic}: held by live pid ${lock.owner_pid}.`);
|
|
1206
|
+
}
|
|
1207
|
+
}
|
|
1208
|
+
}
|
|
1209
|
+
if (!diagnostic.indexOverrideChecked) {
|
|
1210
|
+
lines.push(" SKIP Indexed-branch override: not checked.");
|
|
1211
|
+
}
|
|
1212
|
+
else if (diagnostic.indexOverrideBranch === null) {
|
|
1213
|
+
lines.push(" OK Indexed-branch override: none active.");
|
|
1214
|
+
}
|
|
1215
|
+
else {
|
|
1216
|
+
lines.push(` WARN Indexed-branch override: active on '${diagnostic.indexOverrideBranch}'.\n` +
|
|
1217
|
+
" If no epic is running, this is stale — run `conduct-epic finish <EPIC>`.");
|
|
1218
|
+
}
|
|
1219
|
+
lines.push(" Read-only: this section probes commands, reads local state, and issues one");
|
|
1220
|
+
lines.push(" GET. It recovers no lock, rewrites no checkpoint, and never changes the exit code.");
|
|
1221
|
+
return lines.join("\n");
|
|
1222
|
+
}
|
|
826
1223
|
/**
|
|
827
1224
|
* CLI entry for the read-only `doctor` subcommand. Returns a process exit code.
|
|
828
1225
|
* Help returns 0; parser errors return 1; otherwise it prints the report and
|
|
@@ -886,6 +1283,37 @@ export async function runDoctorCli(argv, overrides = {}) {
|
|
|
886
1283
|
}
|
|
887
1284
|
}
|
|
888
1285
|
log(formatDoctorPrereqSection(deps.platform, collection));
|
|
1286
|
+
// Executor-adapter section (BAPI-781). Advisory with respect to the EXIT CODE
|
|
1287
|
+
// — an unusable adapter is reported loudly but does not change doctor's
|
|
1288
|
+
// pass/fail, which stays driven by the start-tickets prerequisites this
|
|
1289
|
+
// command has always gated on. Read-only: one version probe, no writes, no
|
|
1290
|
+
// credential resolution. Any unexpected throw is swallowed so a diagnostic
|
|
1291
|
+
// cannot take down the report it is part of.
|
|
1292
|
+
if (overrides.executorAdapter !== false) {
|
|
1293
|
+
try {
|
|
1294
|
+
const adapterDeps = {
|
|
1295
|
+
platform: overrides.executorAdapter?.platform ?? deps.platform,
|
|
1296
|
+
env: overrides.executorAdapter?.env ?? deps.env,
|
|
1297
|
+
runCommand: overrides.executorAdapter?.runCommand ??
|
|
1298
|
+
(async (command, args) => {
|
|
1299
|
+
const probe = await deps.runCommand(command, args);
|
|
1300
|
+
return {
|
|
1301
|
+
stdout: probe.stdout ?? "",
|
|
1302
|
+
stderr: probe.stderr ?? "",
|
|
1303
|
+
exitCode: probe.exitCode ?? 1,
|
|
1304
|
+
};
|
|
1305
|
+
}),
|
|
1306
|
+
...(overrides.executorAdapter?.resolveAdapter
|
|
1307
|
+
? { resolveAdapter: overrides.executorAdapter.resolveAdapter }
|
|
1308
|
+
: {}),
|
|
1309
|
+
};
|
|
1310
|
+
const inspection = await collectExecutorAdapterDiagnostic(agent.name, adapterDeps);
|
|
1311
|
+
log(formatExecutorAdapterDiagnosticReport(inspection));
|
|
1312
|
+
}
|
|
1313
|
+
catch {
|
|
1314
|
+
/* adapter diagnostics are advisory; never block the doctor report */
|
|
1315
|
+
}
|
|
1316
|
+
}
|
|
889
1317
|
// Strictly read-only launcher-cache diagnostics (BAPI-451). Best-effort: a probe
|
|
890
1318
|
// failure never changes the doctor exit code (cold-start readiness is advisory,
|
|
891
1319
|
// not a hard prerequisite). The exit code remains driven by required prereqs.
|
|
@@ -901,6 +1329,27 @@ export async function runDoctorCli(argv, overrides = {}) {
|
|
|
901
1329
|
catch {
|
|
902
1330
|
/* launcher-cache diagnostics are advisory; never block the doctor report */
|
|
903
1331
|
}
|
|
1332
|
+
// Advisory local-launcher override section (BAPI-815/R5), placed beside the
|
|
1333
|
+
// launcher-cache diagnostics because both answer "which build will actually
|
|
1334
|
+
// run?". Strictly read-only: at most ONE `--version` probe, and only when an
|
|
1335
|
+
// override is configured — an unset override runs no command at all, which is
|
|
1336
|
+
// what keeps the "unsupported platform issues zero probes" guarantee intact.
|
|
1337
|
+
// Advisory with respect to the EXIT CODE: an unusable override is reported
|
|
1338
|
+
// loudly and changes nothing about doctor's pass/fail.
|
|
1339
|
+
if (overrides.localCliOverride !== false) {
|
|
1340
|
+
try {
|
|
1341
|
+
const localCliDeps = {
|
|
1342
|
+
env: overrides.localCliOverride?.env ?? deps.env,
|
|
1343
|
+
runCommand: overrides.localCliOverride?.runCommand ??
|
|
1344
|
+
(async (command, args) => deps.runCommand(command, args)),
|
|
1345
|
+
};
|
|
1346
|
+
const diagnostic = await collectLocalCliOverrideDiagnostic(localCliDeps);
|
|
1347
|
+
log(formatLocalCliOverrideDiagnosticReport(diagnostic));
|
|
1348
|
+
}
|
|
1349
|
+
catch {
|
|
1350
|
+
/* local-launcher diagnostics are advisory; never block the doctor report */
|
|
1351
|
+
}
|
|
1352
|
+
}
|
|
904
1353
|
// Advisory MCP tool-surface capability section (BAPI-641). Strictly read-only:
|
|
905
1354
|
// one 500 ms GET (or none, under the kill switch). Any timeout, malformed
|
|
906
1355
|
// response, or unexpected throw degrades to a "fail-open to full surface" line
|
|
@@ -937,10 +1386,6 @@ export async function runDoctorCli(argv, overrides = {}) {
|
|
|
937
1386
|
// any key. It is collected independently of `DoctorCollectionResult` and, like
|
|
938
1387
|
// the sections above, never affects the exit code — missing units, malformed
|
|
939
1388
|
// units, and unresolved credentials are all advisory.
|
|
940
|
-
// BAPI-731: set only when isolation is unhealthy AND this host runs an
|
|
941
|
-
// executor, so an executor-only requirement never fails an ordinary MCP user's
|
|
942
|
-
// doctor run.
|
|
943
|
-
let isolationFailed = false;
|
|
944
1389
|
if (overrides.executorService !== false) {
|
|
945
1390
|
try {
|
|
946
1391
|
const injectedFs = deps;
|
|
@@ -957,19 +1402,19 @@ export async function runDoctorCli(argv, overrides = {}) {
|
|
|
957
1402
|
};
|
|
958
1403
|
const executorDiagnostics = await collectExecutorServiceDiagnostics(executorDeps);
|
|
959
1404
|
log(formatExecutorServiceDiagnosticsReport(executorDiagnostics));
|
|
960
|
-
// BAPI-
|
|
961
|
-
//
|
|
962
|
-
//
|
|
963
|
-
//
|
|
964
|
-
|
|
965
|
-
|
|
966
|
-
|
|
967
|
-
|
|
1405
|
+
// BAPI-791: the ONE shared, low-information Claude login advisory for this
|
|
1406
|
+
// doctor report, rendered right after the executor-service section since
|
|
1407
|
+
// that is what it is a prerequisite for. Replaces the retired worker config
|
|
1408
|
+
// isolation health check and Anthropic OAuth lifecycle section — a worker
|
|
1409
|
+
// is no longer isolated into its own configuration directory (BAPI-790) and
|
|
1410
|
+
// no Anthropic credential is stored or diagnosed by Bridge any more
|
|
1411
|
+
// (BAPI-791); this line reports only whether the host's own
|
|
1412
|
+
// `~/.claude.json` carries a login marker. It never blocks the doctor run.
|
|
1413
|
+
const loginResult = await detectClaudeLogin({
|
|
1414
|
+
homedir: executorDeps.homedir,
|
|
1415
|
+
readFile: executorDeps.readFile,
|
|
968
1416
|
});
|
|
969
|
-
log(
|
|
970
|
-
if (isolationDiagnostic.status === "unhealthy" && isolationDiagnostic.required) {
|
|
971
|
-
isolationFailed = true;
|
|
972
|
-
}
|
|
1417
|
+
log(`\n${formatClaudeLoginAdvisory(loginResult)}`);
|
|
973
1418
|
}
|
|
974
1419
|
catch {
|
|
975
1420
|
// Any unexpected failure still renders a sanitized advisory SKIP line.
|
|
@@ -1004,16 +1449,62 @@ export async function runDoctorCli(argv, overrides = {}) {
|
|
|
1004
1449
|
// Any unexpected failure still renders a sanitized advisory line rather
|
|
1005
1450
|
// than dropping the section or leaking exception text.
|
|
1006
1451
|
log(formatPlaneCredentialDiagnosticReport({
|
|
1007
|
-
anthropicKeyPresent: false,
|
|
1008
1452
|
repo: null,
|
|
1009
1453
|
target: null,
|
|
1010
1454
|
bridgeCredentialResolved: false,
|
|
1011
1455
|
}));
|
|
1012
1456
|
}
|
|
1013
1457
|
}
|
|
1458
|
+
// Advisory conduct-epic section (BAPI-803). Read-only, and — like every
|
|
1459
|
+
// section above — it cannot change the exit code: `conduct-epic init` enforces
|
|
1460
|
+
// these same invariants at the point where they actually block, and someone who
|
|
1461
|
+
// never conducts an epic should not see a failing doctor because of it.
|
|
1462
|
+
//
|
|
1463
|
+
// Skipped entirely on an unsupported platform. This section is the only
|
|
1464
|
+
// advisory one that runs COMMAND probes (`gh`, Worktrunk), and doctor's
|
|
1465
|
+
// standing contract is that an unsupported platform probes nothing at all —
|
|
1466
|
+
// reporting on tooling for a `conduct-epic` that cannot run there would buy
|
|
1467
|
+
// nothing and break that contract.
|
|
1468
|
+
if (overrides.conductEpic !== false && collection.ok) {
|
|
1469
|
+
try {
|
|
1470
|
+
const injectedFs = deps;
|
|
1471
|
+
const conductDeps = {
|
|
1472
|
+
env: overrides.conductEpic?.env ?? deps.env,
|
|
1473
|
+
cwd: overrides.conductEpic?.cwd ?? deps.cwd,
|
|
1474
|
+
platform: overrides.conductEpic?.platform ?? deps.platform,
|
|
1475
|
+
homedir: overrides.conductEpic?.homedir ?? injectedFs.homedir ?? os.homedir,
|
|
1476
|
+
readFile: overrides.conductEpic?.readFile ??
|
|
1477
|
+
injectedFs.readFile ??
|
|
1478
|
+
((p) => readFile(p, "utf-8")),
|
|
1479
|
+
stat: overrides.conductEpic?.stat ?? injectedFs.stat ?? ((p) => stat(p)),
|
|
1480
|
+
readdir: overrides.conductEpic?.readdir ?? ((p) => readdir(p)),
|
|
1481
|
+
runCommand: overrides.conductEpic?.runCommand ?? deps.runCommand,
|
|
1482
|
+
resolveAccess: overrides.conductEpic?.resolveAccess,
|
|
1483
|
+
getIndexBranch: overrides.conductEpic?.getIndexBranch,
|
|
1484
|
+
isProcessAlive: overrides.conductEpic?.isProcessAlive,
|
|
1485
|
+
};
|
|
1486
|
+
log(formatConductEpicDiagnosticReport(await collectConductEpicDiagnostic(conductDeps)));
|
|
1487
|
+
}
|
|
1488
|
+
catch {
|
|
1489
|
+
// Any unexpected failure still renders a sanitized advisory section rather
|
|
1490
|
+
// than dropping it or leaking exception text.
|
|
1491
|
+
log(formatConductEpicDiagnosticReport({
|
|
1492
|
+
ghPresent: false,
|
|
1493
|
+
ghAuthenticated: false,
|
|
1494
|
+
worktrunkBinary: resolveWorktrunkBinary(deps.platform, deps.env),
|
|
1495
|
+
worktrunkAvailable: false,
|
|
1496
|
+
repo: null,
|
|
1497
|
+
bridgeCredentialResolved: false,
|
|
1498
|
+
stateDirectory: null,
|
|
1499
|
+
stateDirectoryReadable: false,
|
|
1500
|
+
checkpoints: [],
|
|
1501
|
+
locks: [],
|
|
1502
|
+
indexOverrideBranch: null,
|
|
1503
|
+
indexOverrideChecked: false,
|
|
1504
|
+
}));
|
|
1505
|
+
}
|
|
1506
|
+
}
|
|
1014
1507
|
if (!collection.ok)
|
|
1015
1508
|
return 1;
|
|
1016
|
-
if (isolationFailed)
|
|
1017
|
-
return 1;
|
|
1018
1509
|
return collection.results.some((r) => !r.found) ? 1 : 0;
|
|
1019
1510
|
}
|