@bridge_gpt/mcp-server 0.2.52 → 0.2.53

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.
@@ -32,637 +32,28 @@
32
32
  */
33
33
  import { readdir as fsReaddir, readFile as fsReadFile, stat as fsStat } from "node:fs/promises";
34
34
  import os from "node:os";
35
- import { buildConductorDoctorReport, describeNativeLedgerAvailability, formatConductorDoctorReport, } from "./doctor.js";
36
- import { MANAGED_HOOK_NAMES } from "./git-hooks.js";
35
+ import { buildConductorDoctorReport, formatConductorDoctorReport, } from "./doctor.js";
37
36
  import { inspectBridgeApiProfileToken, } from "../mcp-host-config.js";
38
- import { MCP_PACKAGE_NAME } from "../mcp-identity.js";
39
- // BAPI-941: the shared drift classifier — a leaf module with no git, network, or
40
- // CLI dependency, so it is safe to import from every diagnostic surface.
41
- import { CLAUDE_REVIEW_WORKFLOW_DRIFT_REMEDIATION, summarizeClaudeReviewWorkflowDrift, } from "../claude-review-workflow-drift.js";
42
37
  import { collectExecutorServiceDiagnostics, formatExecutorServiceDiagnosticsReport, } from "../doctor.js";
43
38
  import { collectInstallStatusChecks, formatInstallStatusReport, } from "../install-doctor.js";
39
+ import { conductorHookTargetsSection, conductorLedgerLoadabilitySection, conductorProfileTokenSection, CONDUCTOR_PROFILE_TOKEN, detectConductorContext, executorObservationSection, executorProvisioningSection, executorServiceStateSection, githubActionsSection, githubCredentialsSection, inspectWorkflowPresence, reconcilerSection, supervisorSection, workflowSection, } from "./readiness-sections.js";
44
40
  import { ConductorBridgeApiError, fetchConductorReadiness, } from "./bridge-api-client.js";
45
- /** Operator runbook pointer surfaced beside the executor gap. */
46
- export const CONDUCTOR_OPERATOR_RUNBOOK_POINTER = "docs/claude/epic-conductor-v2-operator-runbook.md";
47
41
  /**
48
- * Capability-matrix guidance for the executor leg.
42
+ * Run one optional collector, containing any throw.
49
43
  *
50
- * Amended by BAPI-779 (R-1): `install conductor` can now write the unit AND
51
- * start it, each behind its own consent. What did NOT change is the readiness
52
- * rule `executor_ready` still comes only from a live server observation, so
53
- * neither a written unit nor a successful local start may produce that claim.
44
+ * `failed` is reported separately from `value` because "not supplied" and
45
+ * "supplied and threw" are different facts: only the second produces a degraded
46
+ * section. Nothing here is retried and no timeout is imposed each collector
47
+ * owns its own.
54
48
  */
55
- export const EXECUTOR_PROVISIONING_GUIDANCE = "`install conductor` writes the unit and starts it, each behind its own consent; " +
56
- "readiness still comes only from a live server observation";
57
- /**
58
- * The `BRIDGE_MCP_PROFILE` token that makes the 8 conductor MCP tools visible.
59
- *
60
- * The tools are profile-gated, never core, and this is the only token that
61
- * un-gates them — so the doctor names it explicitly rather than describing the
62
- * gap abstractly (BAPI-775).
63
- */
64
- export const CONDUCTOR_PROFILE_TOKEN = "conductor";
65
- /** Remediation for a host MCP config that carries no Bridge entry at all. */
66
- export const CONDUCTOR_PROFILE_TOKEN_NO_ENTRY_REMEDIATION = "no supported host MCP config carries a Bridge entry — run " +
67
- `\`npx ${MCP_PACKAGE_NAME} install\` to register one, then re-run \`install conductor\`.`;
68
- /** Remediation for an entry that exists but lacks the conductor token. */
69
- export const CONDUCTOR_PROFILE_TOKEN_REMEDIATION = "re-run `install conductor` and accept the tool-visibility consent; the conductor " +
70
- "tools become visible after the MCP client is restarted.";
71
- /** Remediation surfaced whenever the managed conductor hooks are not usable. */
72
- export const CONDUCTOR_HOOK_TARGET_REMEDIATION = "re-run `install conductor` and accept the local-observability consent, or run " +
73
- "`conductor install-git-hooks` directly.";
74
- /** Remediation surfaced when the ledger's native binding does not load. */
75
- export const CONDUCTOR_LEDGER_LOADABILITY_REMEDIATION = "`better-sqlite3` is an optionalDependency npm silently skips on a build failure — " +
76
- "reinstall it for this Node runtime to restore local conductor observability.";
77
- /** Remediation surfaced whenever no healthy generated service unit is detected. */
78
- export const EXECUTOR_INSTALL_SERVICE_REMEDIATION = "generate a persistent unit with `install conductor --executor-id <id>` or " +
79
- "`executor install-service` (or run the executor by hand) — " +
80
- `see ${CONDUCTOR_OPERATOR_RUNBOOK_POINTER}.`;
81
- /** Remediation for a unit that exists but whose service is not running. */
82
- export const EXECUTOR_SERVICE_START_REMEDIATION = "start the service (re-run `install conductor` and accept the start consent, or run the " +
83
- `printed launchctl/systemctl command by hand) — see ${CONDUCTOR_OPERATOR_RUNBOOK_POINTER}.`;
84
- // ---------------------------------------------------------------------------
85
- // Section builders
86
- // ---------------------------------------------------------------------------
87
- function supervisorSection(readiness) {
88
- const s = readiness.supervisor;
89
- const sections = [];
90
- if (!s.setup_present || !s.config_present) {
91
- const missing = [
92
- !s.setup_present ? "setup" : null,
93
- !s.config_present ? "config" : null,
94
- ].filter(Boolean).join(" and ");
95
- sections.push({
96
- id: "supervisor-configuration",
97
- label: "Supervisor configuration",
98
- status: "degraded",
99
- detail: `project-default supervisor ${missing} absent`,
100
- remediation: "bootstrap writes the safe project-default posture.",
101
- });
102
- }
103
- else {
104
- sections.push({
105
- id: "supervisor-configuration",
106
- label: "Supervisor configuration",
107
- status: "ok",
108
- detail: `setup=${s.setup_source}, config=${s.config_source}`,
109
- });
110
- }
111
- // Deliberately its OWN section, not folded into the one above: "configured but
112
- // unsafe" and "not configured" drive different operator actions, and an empty
113
- // required-check list makes the CI gate pass unconditionally rather than fail.
114
- sections.push(s.required_checks_empty
115
- ? {
116
- id: "supervisor-required-checks",
117
- label: "Supervisor required CI checks",
118
- status: "degraded",
119
- detail: "required-check list is EMPTY — the done gate would pass unconditionally",
120
- remediation: "bootstrap writes a non-empty required-check list into done_gate_config.",
121
- }
122
- : {
123
- id: "supervisor-required-checks",
124
- label: "Supervisor required CI checks",
125
- status: "ok",
126
- detail: `${s.required_checks_count} required check(s) configured`,
127
- });
128
- // BAPI-694 (R5) — its OWN section, and WARN-ONLY.
129
- //
130
- // `degraded`, never `fatal`: a divergence between the code_review gate's
131
- // signal and the merge path's may be exactly what the operator intended, and
132
- // blocking an install over a legitimate posture would be worse than the
133
- // silence this replaces. It also changes no gate: both settings are reported,
134
- // neither is coerced into the other's vocabulary.
135
- //
136
- // Absent (`null`) means the server predates BAPI-694 and cannot report
137
- // alignment, so no section is emitted at all — an advisory addition must not
138
- // manufacture a finding out of a server that never answered.
139
- const alignment = s.review_policy_alignment;
140
- if (alignment !== null) {
141
- if (alignment.status === "divergent" || alignment.status === "invalid") {
142
- sections.push({
143
- id: "supervisor-review-alignment",
144
- label: "Supervisor review-policy alignment",
145
- status: "degraded",
146
- detail: alignment.explanation,
147
- remediation: alignment.status === "invalid"
148
- ? "fix the done gate's review_state condition — review configuration " +
149
- "exists but cannot be read, so the merge path refuses rather than merges."
150
- : "set the repository review-policy default and the done gate's " +
151
- "review_state condition to the same signal, or confirm the difference " +
152
- `is intended — see ${CONDUCTOR_OPERATOR_RUNBOOK_POINTER}.`,
153
- });
154
- }
155
- else {
156
- // `aligned` and `not_configured` are both non-warning states, and are
157
- // reported separately: "they agree" and "neither is set" are different
158
- // facts, and collapsing them would imply a stored default that is absent.
159
- sections.push({
160
- id: "supervisor-review-alignment",
161
- label: "Supervisor review-policy alignment",
162
- status: "ok",
163
- detail: alignment.explanation,
164
- });
165
- }
166
- }
167
- return sections;
168
- }
169
- function githubCredentialsSection(readiness) {
170
- const g = readiness.github;
171
- if (g.credentials_complete) {
172
- return {
173
- id: "github-credentials",
174
- label: "GitHub App credentials",
175
- status: "ok",
176
- detail: "owner, repository id, and installation id all resolved",
177
- };
178
- }
179
- const missing = [
180
- !g.owner_resolved ? "owner" : null,
181
- !g.repo_id_resolved ? "repository id" : null,
182
- !g.installation_id_resolved ? "installation id" : null,
183
- ].filter(Boolean).join(", ");
184
- return {
185
- id: "github-credentials",
186
- label: "GitHub App credentials",
187
- status: "degraded",
188
- detail: g.credentials_readable
189
- ? `incomplete: ${missing} unresolved`
190
- : "no GitHub credential row for this repository",
191
- remediation: "connect GitHub from the Bridge setup UI (`install-bridge connect-github`).",
192
- };
193
- }
194
- function githubActionsSection(readiness) {
195
- const g = readiness.github;
196
- if (g.actions_write) {
197
- return {
198
- id: "github-actions-permission",
199
- label: "GitHub App actions permission",
200
- status: "ok",
201
- detail: "actions: write",
202
- };
203
- }
204
- if (!g.actions_probe_succeeded) {
205
- // "Could not be checked" is NOT the same finding as "confirmedly missing" —
206
- // the first needs a retry, the second needs a permission grant.
207
- return {
208
- id: "github-actions-permission",
209
- label: "GitHub App actions permission",
210
- status: "degraded",
211
- detail: "permission could not be checked",
212
- remediation: "re-run once credentials resolve; the workflow rerun lane fails open on 403 until this reads `write`.",
213
- };
214
- }
215
- return {
216
- id: "github-actions-permission",
217
- label: "GitHub App actions permission",
218
- status: "degraded",
219
- detail: `actions: ${g.actions_permission_level}`,
220
- remediation: "grant the GitHub App `actions: write`; the conductor's workflow rerun lane fails open on 403 without it.",
221
- };
222
- }
223
- function reconcilerSection(readiness) {
224
- const r = readiness.reconciler;
225
- if (!r.liveness_readable) {
226
- return {
227
- id: "reconciler-liveness",
228
- label: "Reconciler tick liveness",
229
- status: "degraded",
230
- detail: "liveness could not be read",
231
- };
232
- }
233
- if (!r.stale) {
234
- return {
235
- id: "reconciler-liveness",
236
- label: "Reconciler tick liveness",
237
- status: "ok",
238
- detail: `last tick ${r.last_tick_age_seconds}s ago across ${r.active_run_count} active run(s)`,
239
- };
240
- }
241
- return {
242
- id: "reconciler-liveness",
243
- label: "Reconciler tick liveness",
244
- status: "degraded",
245
- detail: r.last_tick_at === null
246
- ? "no reconciler tick recorded for this repository"
247
- : `last tick ${r.last_tick_age_seconds}s ago (threshold ${readiness.thresholds.reconciler_stale_after_seconds}s)`,
248
- remediation: "start the reconciler (`conductor epic-tick` schedule) for this repository.",
249
- };
250
- }
251
- function workflowSection(presence, reviewPolicySource, driftDetail) {
252
- if (presence === "present") {
253
- return {
254
- id: "claude-review-workflow",
255
- label: "claude-review workflow",
256
- status: "ok",
257
- // BAPI-941: when the lineage comparison ran but could not be completed,
258
- // that uncertainty rides on the SAME informational detail line rather than
259
- // becoming a status of its own. The section stays `ok`: the workflow is
260
- // installed and readable, which is all this section has ever asserted.
261
- detail: driftDetail
262
- ? `.github/workflows/claude-review.yml present — ${driftDetail}`
263
- : ".github/workflows/claude-review.yml present",
264
- };
265
- }
266
- // BAPI-941: present but stale. Reported through the SAME section, status
267
- // vocabulary, and detail/remediation shape as every other degraded state — no
268
- // new health component, no bespoke styling, and no color dependence: the
269
- // detail's own wording carries the warning. The file IS installed, so this is
270
- // never `absent`, and the policy-applicability branch below does not apply
271
- // (a stale workflow only matters to a policy that consumes its verdict, and
272
- // the caller only classifies drift when it does).
273
- if (presence === "drifted") {
274
- return {
275
- id: "claude-review-workflow",
276
- label: "claude-review workflow",
277
- status: "degraded",
278
- detail: driftDetail ??
279
- ".github/workflows/claude-review.yml differs from the repository default branch",
280
- remediation: CLAUDE_REVIEW_WORKFLOW_DRIFT_REMEDIATION,
281
- };
282
- }
283
- // Applicability is policy-dependent: a run whose review signal is GitHub's own
284
- // review decision does not need the Claude workflow at all, so its absence is
285
- // not a gap. Reporting it as degraded there would train operators to ignore
286
- // this section.
287
- if (reviewPolicySource === "native_review_decision" || reviewPolicySource === "none") {
288
- return {
289
- id: "claude-review-workflow",
290
- label: "claude-review workflow",
291
- status: "ok",
292
- detail: `not applicable for review policy '${reviewPolicySource}'`,
293
- };
294
- }
295
- return {
296
- id: "claude-review-workflow",
297
- label: "claude-review workflow",
298
- status: "degraded",
299
- detail: presence === "unreadable"
300
- ? ".github/workflows/claude-review.yml could not be read"
301
- : ".github/workflows/claude-review.yml absent",
302
- remediation: "the selected review policy consumes the sticky verdict this workflow emits; install the template.",
303
- };
304
- }
305
- /**
306
- * Real detection of BAPI-688 generated service units, composed from
307
- * `collectExecutorServiceDiagnostics`. Never `fatal` (a missing unit is a
308
- * repairable gap, and blocking every install on it would be wrong) and never
309
- * omitted (the flow must not read as fully provisioned by silence).
310
- *
311
- * This section reports UNIT PRESENCE and nothing else (BAPI-779). Whether the
312
- * service manager is actually running the unit is
313
- * {@link executorServiceStateSection}, and whether the SERVER has observed an
314
- * executor is {@link executorObservationSection}. Three separate facts, three
315
- * separate sections — a healthy unit here proves only that a file exists.
316
- */
317
- function executorProvisioningSection(diagnostics) {
318
- const base = { id: "executor-provisioning", label: "Executor provisioning" };
319
- if (diagnostics === null) {
320
- return {
321
- ...base,
322
- status: "degraded",
323
- detail: "executor service-unit diagnostics could not be collected",
324
- remediation: EXECUTOR_INSTALL_SERVICE_REMEDIATION,
325
- };
326
- }
327
- if (diagnostics.status === "skipped") {
328
- // The collector's `reason` is already sanitized and secret-free (fixed
329
- // convention paths only): "no units found", unsupported platform, or the
330
- // Windows manual-Task-Scheduler case.
331
- return {
332
- ...base,
333
- status: "degraded",
334
- detail: diagnostics.reason,
335
- remediation: EXECUTOR_INSTALL_SERVICE_REMEDIATION,
336
- };
337
- }
338
- const unhealthy = diagnostics.units.filter((unit) => unit.warnings.length > 0 || unit.credentials.some((cred) => !cred.resolved));
339
- const ids = diagnostics.units.map((unit) => unit.executorId).join(", ");
340
- if (unhealthy.length === 0 && diagnostics.warnings.length === 0) {
341
- return {
342
- ...base,
343
- status: "ok",
344
- detail: `${diagnostics.units.length} generated service unit(s) found (${ids}); ` +
345
- "unit presence only — see the service-state and observation sections",
346
- };
347
- }
348
- return {
349
- ...base,
350
- status: "degraded",
351
- detail: `${diagnostics.units.length} generated service unit(s) found (${ids}), but ` +
352
- `${unhealthy.length} unit(s) carry warnings or unresolved credentials — see the ` +
353
- "embedded executor-provisioning report",
354
- remediation: EXECUTOR_INSTALL_SERVICE_REMEDIATION,
355
- };
356
- }
357
- /**
358
- * Local service-manager state for the units that were discovered (BAPI-779).
359
- *
360
- * Never `fatal`: this is a diagnostic, and an unreadable service manager is a
361
- * gap in the *report*, not a broken install. `unknown` is deliberately not
362
- * collapsed into `not-found` — "the probe could not answer" and "the service
363
- * manager has no such unit" lead to different operator actions.
364
- */
365
- function executorServiceStateSection(observations, unitsDiscovered, inspectorAvailable) {
366
- const base = { id: "executor-service-state", label: "Executor service state" };
367
- if (!unitsDiscovered) {
368
- return {
369
- ...base,
370
- status: "degraded",
371
- detail: "no generated unit to inspect, so no service state was collected",
372
- remediation: EXECUTOR_INSTALL_SERVICE_REMEDIATION,
373
- };
374
- }
375
- if (!inspectorAvailable || observations.length === 0) {
376
- return {
377
- ...base,
378
- status: "degraded",
379
- detail: "local service state could not be collected on this host",
380
- remediation: EXECUTOR_SERVICE_START_REMEDIATION,
381
- };
382
- }
383
- // Deterministic ordering by executor id so two runs of the same host render
384
- // identically regardless of enumeration order.
385
- const rendered = [...observations]
386
- .sort((a, b) => (a.executorId < b.executorId ? -1 : a.executorId > b.executorId ? 1 : 0))
387
- .map((o) => `${o.executorId}=${o.state}`)
388
- .join(", ");
389
- const allActive = observations.every((o) => o.state === "active");
390
- return allActive
391
- ? { ...base, status: "ok", detail: `service state: ${rendered}` }
392
- : {
393
- ...base,
394
- status: "degraded",
395
- detail: `service state: ${rendered}`,
396
- remediation: EXECUTOR_SERVICE_START_REMEDIATION,
397
- };
398
- }
399
- /**
400
- * The AUTHORITATIVE executor fact: what the server has observed (BAPI-779).
401
- *
402
- * This is the only section permitted to say `executor_ready`, and only for
403
- * `readiness.executor.ready === true`. The three not-ready shapes are kept
404
- * distinct because they mean different things: `null` is "never observed" (the
405
- * executor has not claimed yet), `false` is "observed but stale" (it claimed
406
- * once and went quiet), and an unreadable readiness payload is "unknown" (the
407
- * doctor cannot say). Collapsing them would let a stale executor read like a
408
- * fresh install, or vice versa.
409
- */
410
- function executorObservationSection(readiness) {
411
- const base = { id: "executor-observation", label: "Executor server observation" };
412
- if (readiness === null || !readiness.executor.liveness_readable) {
413
- return {
414
- ...base,
415
- status: "degraded",
416
- detail: "server executor observation is unknown (readiness could not be read)",
417
- remediation: "retry once the Bridge API is reachable; no readiness claim is made from local state.",
418
- };
419
- }
420
- const e = readiness.executor;
421
- const seen = e.last_seen_at === null
422
- ? "never seen"
423
- : `last seen ${e.last_seen_at} (${e.last_seen_age_seconds}s ago)`;
424
- if (e.ready === true) {
425
- return { ...base, status: "ok", detail: `executor_ready — ${seen}` };
426
- }
427
- if (e.ready === false) {
428
- return {
429
- ...base,
430
- status: "degraded",
431
- detail: `observed but stale/not ready — ${seen}`,
432
- remediation: EXECUTOR_SERVICE_START_REMEDIATION,
433
- };
434
- }
435
- return {
436
- ...base,
437
- status: "degraded",
438
- detail: `never observed by the server — ${seen}`,
439
- remediation: EXECUTOR_SERVICE_START_REMEDIATION,
440
- };
441
- }
442
- // ---------------------------------------------------------------------------
443
- // Local conductor capability sections (BAPI-775)
444
- //
445
- // Three INDEPENDENT read-only checks — host profile token, managed hook targets,
446
- // native ledger loadability. All three are non-fatal by construction: a missing
447
- // local conductor capability is a repairable gap, and letting one flip
448
- // `conductorInstallDoctorExitCode` to 1 would make every ordinary install fail
449
- // on a capability it never asked for.
450
- // ---------------------------------------------------------------------------
451
- /**
452
- * Whether this host looks like a conductor context.
453
- *
454
- * Composed from the two facts already collected: the resolved MCP-profile
455
- * inspection (`BAPI_CONDUCTOR_ENABLED` or a registered epic-tick schedule) and
456
- * the read-only host token inspection. Outside such a context a missing local
457
- * capability is ADVISORY — reporting it as a gap would train operators of
458
- * ordinary repositories to ignore this whole part of the report.
459
- */
460
- function detectConductorContext(legacyConductor, profile) {
461
- // Read defensively: an INJECTED report sentinel (and the installer's own
462
- // degraded-doctor fallback) may populate only the fields a test cares about,
463
- // and a doctor that throws on a partially-built report would abort the run it
464
- // exists to diagnose. Same posture as the defensive legacy formatters below.
465
- return (legacyConductor?.mcp_profile?.conductor_context_detected === true ||
466
- profile?.tokenPresent === true);
467
- }
468
- /**
469
- * Host MCP profile-token presence.
470
- *
471
- * Reports CONFIG IDENTIFIERS AND BOOLEANS ONLY — never an entry, an `env`
472
- * object, a command line, an API key, or an unrelated profile token — and names
473
- * only the three configs the merge path can write, so nothing here can imply
474
- * that Codex or Copilot configuration was inspected or changed.
475
- */
476
- function conductorProfileTokenSection(profile, conductorContext) {
477
- const base = { id: "conductor-profile-token", label: "MCP conductor profile token" };
478
- if (profile === null) {
479
- return {
480
- ...base,
481
- status: "degraded",
482
- detail: "the host MCP profile token could not be inspected",
483
- remediation: CONDUCTOR_PROFILE_TOKEN_REMEDIATION,
484
- };
485
- }
486
- const carrying = profile.configs
487
- .filter((c) => c.state === "token-present")
488
- .map((c) => c.path);
489
- const scope = "inspected .mcp.json, .vscode/mcp.json, .cursor/mcp.json only " +
490
- "(Codex and Copilot host configuration is not inspected)";
491
- if (carrying.length > 0) {
492
- return {
493
- ...base,
494
- status: "ok",
495
- detail: `\`${profile.token}\` present in ${carrying.join(", ")}; ${scope}`,
496
- };
497
- }
498
- // "No entry anywhere" and "an entry without the token" are different findings
499
- // with different next steps, so they never collapse into one remediation.
500
- const detail = profile.bridgeEntryPresent
501
- ? `no supported host config carries the \`${profile.token}\` token; ${scope}`
502
- : `no supported host config carries a Bridge entry; ${scope}`;
503
- const remediation = profile.bridgeEntryPresent
504
- ? CONDUCTOR_PROFILE_TOKEN_REMEDIATION
505
- : CONDUCTOR_PROFILE_TOKEN_NO_ENTRY_REMEDIATION;
506
- return conductorContext
507
- ? { ...base, status: "degraded", detail, remediation }
508
- : { ...base, status: "ok", detail: `advisory — ${detail}`, remediation };
509
- }
510
- /**
511
- * Managed conductor hook targets, DERIVED from `legacyConductor.git_hooks`.
512
- *
513
- * Derived, never re-probed: a second parse of the same hook files is a second
514
- * chance to disagree with the report embedded right below this section. Healthy
515
- * requires each managed hook to exist, carry the managed block, and point at an
516
- * embedded binary that stats as a regular file — a hook whose target vanished is
517
- * a dead hook, and reporting it as installed is the exact failure BAPI-772 made
518
- * structurally impossible at INSTALL time but which can still arise later.
519
- *
520
- * Renders hook NAME and boolean state only. The embedded absolute binary path is
521
- * read for the file check and never copied into this section.
522
- */
523
- function conductorHookTargetsSection(legacyConductor, conductorContext) {
524
- const base = { id: "conductor-hook-targets", label: "Conductor hook targets" };
525
- // Nullish, not `=== null`: an injected builder may resolve `undefined`, and a
526
- // doctor that throws on it would abort the run it exists to diagnose.
527
- if (legacyConductor === null || legacyConductor === undefined) {
528
- return {
529
- ...base,
530
- status: "degraded",
531
- detail: "hook targets could not be derived (the conductor doctor was not collected)",
532
- remediation: CONDUCTOR_HOOK_TARGET_REMEDIATION,
533
- };
534
- }
535
- // Defensive for the same reason as {@link detectConductorContext}: an injected
536
- // sentinel need not carry every legacy field.
537
- const hooks = legacyConductor.git_hooks;
538
- if (hooks === undefined) {
539
- return {
540
- ...base,
541
- status: "degraded",
542
- detail: "hook targets could not be derived (the conductor doctor reported no hook state)",
543
- remediation: CONDUCTOR_HOOK_TARGET_REMEDIATION,
544
- };
545
- }
546
- if (!hooks.is_worktree) {
547
- return {
548
- ...base,
549
- status: "ok",
550
- detail: "advisory — not a git worktree, so no managed conductor hooks apply",
551
- };
552
- }
553
- const usable = (name) => {
554
- const hook = (hooks.hooks ?? []).find((h) => h.name === name);
555
- return (hook !== undefined &&
556
- hook.exists &&
557
- hook.managed_block_present &&
558
- hook.embedded_bin_is_file);
559
- };
560
- const rendered = MANAGED_HOOK_NAMES.map((name) => `${name}=${usable(name)}`).join(", ");
561
- const allUsable = MANAGED_HOOK_NAMES.every((name) => usable(name));
562
- if (allUsable) {
563
- return { ...base, status: "ok", detail: `managed hooks usable: ${rendered}` };
564
- }
565
- const detail = `managed hooks usable: ${rendered}`;
566
- return conductorContext
567
- ? { ...base, status: "degraded", detail, remediation: CONDUCTOR_HOOK_TARGET_REMEDIATION }
568
- : {
569
- ...base,
570
- status: "ok",
571
- detail: `advisory — ${detail}`,
572
- remediation: CONDUCTOR_HOOK_TARGET_REMEDIATION,
573
- };
574
- }
575
- /**
576
- * Native ledger loadability, DERIVED from `legacyConductor.native_ledger`.
577
- *
578
- * No parallel `better-sqlite3` check: the mapping from an inspection to
579
- * available/degraded lives in one place ({@link describeNativeLedgerAvailability}),
580
- * so this section and the conductor doctor can never disagree about whether the
581
- * binding loads. A degraded result carries the module name, the ABI, and the
582
- * sanitized failure kind, because the common cause is npm silently skipping an
583
- * optionalDependency build and that is undiagnosable without the ABI.
584
- */
585
- function conductorLedgerLoadabilitySection(legacyConductor) {
586
- const base = {
587
- id: "conductor-ledger-loadability",
588
- label: "Conductor ledger loadability",
589
- };
590
- if (legacyConductor === null || legacyConductor === undefined) {
591
- return {
592
- ...base,
593
- status: "degraded",
594
- detail: "ledger loadability could not be derived (the conductor doctor was not collected)",
595
- remediation: CONDUCTOR_LEDGER_LOADABILITY_REMEDIATION,
596
- };
597
- }
598
- const inspection = legacyConductor.native_ledger;
599
- if (inspection === undefined) {
600
- return {
601
- ...base,
602
- status: "degraded",
603
- detail: "ledger loadability could not be derived (the conductor doctor reported no native-ledger state)",
604
- remediation: CONDUCTOR_LEDGER_LOADABILITY_REMEDIATION,
605
- };
606
- }
607
- const availability = describeNativeLedgerAvailability(inspection);
608
- if (availability.status === "available") {
609
- return { ...base, status: "ok", detail: "ledger available — the native binding loads" };
610
- }
611
- return {
612
- ...base,
613
- status: "degraded",
614
- detail: `ledger degraded — ${availability.module} did not load ` +
615
- `(NODE_MODULE_VERSION ${availability.nodeModulesAbi}, failure: ${availability.failureKind})`,
616
- remediation: CONDUCTOR_LEDGER_LOADABILITY_REMEDIATION,
617
- };
618
- }
619
- // ---------------------------------------------------------------------------
620
- // Runner
621
- // ---------------------------------------------------------------------------
622
- /**
623
- * BAPI-941: the workflow's local state AND, when the seam is supplied and the
624
- * file is genuinely present, its lineage relative to the default branch. The
625
- * drift detail is bounded text from the shared classifier — never git stderr and
626
- * never file content.
627
- */
628
- async function inspectWorkflowPresence(readWorkflowFile, classifyWorkflowDrift) {
629
- const local = await inspectWorkflowLocalPresence(readWorkflowFile);
630
- if (local !== "present" || !classifyWorkflowDrift)
631
- return { presence: local };
49
+ async function settle(collect) {
50
+ if (!collect)
51
+ return { value: null, failed: false };
632
52
  try {
633
- const classification = await classifyWorkflowDrift();
634
- if (classification.state === "drifted") {
635
- return {
636
- presence: "drifted",
637
- driftDetail: summarizeClaudeReviewWorkflowDrift(classification),
638
- };
639
- }
640
- if (classification.state === "unverified") {
641
- // Inconclusive, so the presence state stays `present` and non-failing. The
642
- // uncertainty is still reported, through the existing detail line rather
643
- // than by inventing a failure the probe did not establish.
644
- return {
645
- presence: "present",
646
- driftDetail: summarizeClaudeReviewWorkflowDrift(classification),
647
- };
648
- }
649
- return { presence: "present" };
53
+ return { value: await collect(), failed: false };
650
54
  }
651
55
  catch {
652
- // A thrown probe is an unavailable comparison, not a fault in the workflow.
653
- return { presence: "present" };
654
- }
655
- }
656
- async function inspectWorkflowLocalPresence(readWorkflowFile) {
657
- try {
658
- await readWorkflowFile();
659
- return "present";
660
- }
661
- catch (err) {
662
- // ENOENT is the ordinary "absent" case; anything else is "unreadable". The
663
- // error's message is never surfaced — it can echo an absolute path.
664
- const code = err?.code;
665
- return code === "ENOENT" ? "absent" : "unreadable";
56
+ return { value: null, failed: true };
666
57
  }
667
58
  }
668
59
  /**
@@ -671,50 +62,57 @@ async function inspectWorkflowLocalPresence(readWorkflowFile) {
671
62
  export async function runConductorInstallDoctor(deps) {
672
63
  const sections = [];
673
64
  // --- Legacy builders, composed verbatim ---------------------------------
65
+ //
66
+ // BAPI-1055: the three legacy collectors are INDEPENDENT of one another, so
67
+ // they are awaited together rather than in series. Each keeps its own
68
+ // containment (a thrown collector still becomes its own degraded section) and
69
+ // its own timeout — none is added, removed, or widened here — and the sections
70
+ // below are still pushed in the exact order they were pushed before, because
71
+ // report order is what an operator reads. Only the waiting is shared.
72
+ //
73
+ // The server readiness fetch deliberately stays OUT of this group: it depends
74
+ // on `deps.access` having resolved, and its failure classification (fatal vs
75
+ // degraded) drives sections the local collectors know nothing about.
674
76
  const buildConductor = deps.buildConductorReport ?? buildConductorDoctorReport;
675
- let legacyConductor = null;
676
- try {
677
- legacyConductor = await buildConductor(deps.conductorDoctorDeps);
678
- }
679
- catch {
77
+ const collectInstall = deps.collectInstallChecks ??
78
+ (deps.installDoctorDeps
79
+ ? () => collectInstallStatusChecks(deps.installDoctorDeps)
80
+ : null);
81
+ const [conductorSettled, executorPreflightSettled, installChecksSettled] = await Promise.all([
82
+ settle(() => buildConductor(deps.conductorDoctorDeps)),
83
+ settle(deps.collectExecutorPreflight ? () => deps.collectExecutorPreflight() : null),
84
+ settle(collectInstall),
85
+ ]);
86
+ const legacyConductor = conductorSettled.value ?? null;
87
+ if (conductorSettled.failed) {
680
88
  sections.push({
681
89
  id: "conductor-ledger",
682
90
  label: "Conductor ledger doctor",
683
91
  status: "degraded",
684
92
  detail: "the local conductor doctor could not be built",
93
+ remediation: "run `conductor doctor` directly to see why the local ledger report failed.",
685
94
  });
686
95
  }
687
- let legacyExecutorPreflight = null;
688
- if (deps.collectExecutorPreflight) {
689
- try {
690
- legacyExecutorPreflight = await deps.collectExecutorPreflight();
691
- }
692
- catch {
693
- sections.push({
694
- id: "executor-preflight",
695
- label: "Executor preflight",
696
- status: "degraded",
697
- detail: "the executor preflight could not be collected",
698
- });
699
- }
96
+ const legacyExecutorPreflight = executorPreflightSettled.value ?? null;
97
+ if (executorPreflightSettled.failed) {
98
+ sections.push({
99
+ id: "executor-preflight",
100
+ label: "Executor preflight",
101
+ status: "degraded",
102
+ detail: "the executor preflight could not be collected",
103
+ remediation: "re-run this command; if it persists, run `executor` directly to surface the " +
104
+ "preflight failure.",
105
+ });
700
106
  }
701
- let legacyInstallChecks = null;
702
- const collectInstall = deps.collectInstallChecks ??
703
- (deps.installDoctorDeps
704
- ? () => collectInstallStatusChecks(deps.installDoctorDeps)
705
- : null);
706
- if (collectInstall) {
707
- try {
708
- legacyInstallChecks = await collectInstall();
709
- }
710
- catch {
711
- sections.push({
712
- id: "install-status",
713
- label: "Bridge install status",
714
- status: "degraded",
715
- detail: "the install-status checklist could not be collected",
716
- });
717
- }
107
+ const legacyInstallChecks = installChecksSettled.value ?? null;
108
+ if (installChecksSettled.failed) {
109
+ sections.push({
110
+ id: "install-status",
111
+ label: "Bridge install status",
112
+ status: "degraded",
113
+ detail: "the install-status checklist could not be collected",
114
+ remediation: "run `doctor` to collect the advisory install-status checklist on its own.",
115
+ });
718
116
  }
719
117
  // --- Server readiness ---------------------------------------------------
720
118
  let readiness = null;
@@ -923,3 +321,14 @@ export function conductorInstallDoctorExitCode(report) {
923
321
  export function conductorInstallDoctorHasFatal(report) {
924
322
  return report.sections.some((s) => s.status === "fatal");
925
323
  }
324
+ // ---------------------------------------------------------------------------
325
+ // Compatibility surface (BAPI-1055)
326
+ //
327
+ // The section contract and every pure section builder now live in
328
+ // `readiness-sections.ts`, so the install-time doctor and the advisory
329
+ // `conductor readiness` gate share ONE adapter set. They are re-exported from
330
+ // here unchanged: every existing import of `ConductorInstallSection`, the
331
+ // remediation constants, or the installer reporting APIs from
332
+ // `conductor/install-doctor.js` keeps resolving exactly as before.
333
+ // ---------------------------------------------------------------------------
334
+ export { CONDUCTOR_OPERATOR_RUNBOOK_POINTER, EXECUTOR_PROVISIONING_GUIDANCE, CONDUCTOR_PROFILE_TOKEN, CONDUCTOR_PROFILE_TOKEN_NO_ENTRY_REMEDIATION, CONDUCTOR_PROFILE_TOKEN_REMEDIATION, CONDUCTOR_HOOK_TARGET_REMEDIATION, CONDUCTOR_LEDGER_LOADABILITY_REMEDIATION, EXECUTOR_INSTALL_SERVICE_REMEDIATION, EXECUTOR_SERVICE_START_REMEDIATION, } from "./readiness-sections.js";