@bridge_gpt/mcp-server 0.2.33 → 0.2.36
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 +456 -340
- package/build/agent-capabilities/probe-context.js +8 -1
- package/build/agent-capabilities/probes.js +7 -1
- package/build/agents.generated.js +1 -1
- package/build/claude-review-workflow.js +264 -0
- package/build/cli-release.js +53 -0
- package/build/commands.generated.js +4 -4
- package/build/conductor/bridge-api-client.js +252 -3
- package/build/conductor/deny-enforcement-preflight.js +1 -0
- package/build/conductor/done-gate.js +44 -5
- package/build/conductor/epic-reconcile.js +6 -0
- package/build/conductor/install-doctor.js +462 -0
- package/build/conductor-bin.js +3 -3
- package/build/conductor-bundle-artifacts.js +30 -9
- package/build/doctor.js +234 -1
- package/build/executor/cli.js +32 -5
- package/build/executor/credentials.js +45 -11
- package/build/executor/deps.js +14 -0
- package/build/executor/env.js +23 -6
- package/build/executor/index.js +4 -0
- package/build/executor/job-runner.js +119 -9
- package/build/executor/permissions.js +12 -2
- package/build/executor/preflight.js +95 -8
- package/build/executor/prompt-spec.js +51 -0
- package/build/executor/runner.js +15 -2
- package/build/executor/service-unit.js +876 -0
- package/build/executor/test-clock.js +8 -0
- package/build/executor/types.js +0 -17
- package/build/executor/worker-command.js +62 -9
- package/build/index.js +575 -143
- package/build/init.js +153 -51
- package/build/install-bridge-conductor.js +491 -0
- package/build/install-bridge.js +884 -176
- package/build/install-reexec.js +233 -0
- package/build/mcp-host-config.js +11 -1
- package/build/mcp-install-state.js +32 -0
- package/build/mcp-provisioning.js +22 -6
- package/build/pipelines.generated.js +14 -8
- package/build/readme.generated.js +1 -1
- package/build/run-unit-tests-launcher.js +257 -0
- package/build/setup-epic.js +117 -8
- package/build/upgrade-cli.js +1 -15
- package/build/version.generated.js +1 -1
- package/docs/CONDUCTOR.md +115 -4
- package/docs/install/mcp-tool-integrations.md +29 -21
- package/package.json +9 -6
- package/pipelines/implement-ticket.json +6 -1
- package/build/conductor/supervisor-judgment-python.js +0 -141
- package/build/conductor/supervisor-judgment.js +0 -215
|
@@ -0,0 +1,462 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Unified conductor install doctor (BAPI-679).
|
|
3
|
+
*
|
|
4
|
+
* Three read-only diagnostics already exist and each answers part of "is the
|
|
5
|
+
* conductor installed correctly": `buildConductorDoctorReport` (local ledger,
|
|
6
|
+
* hooks, schedule), `collectExecutorPreflight` (local executor tooling), and
|
|
7
|
+
* `collectInstallStatusChecks` (Bridge install status). None of them can see
|
|
8
|
+
* SERVER-side conductor state, because the CLI holds neither Postgres access nor
|
|
9
|
+
* the GitHub App token. This module composes all three with the server readiness
|
|
10
|
+
* endpoint — plus the read-only BAPI-688 executor service-unit diagnostics
|
|
11
|
+
* (`collectExecutorServiceDiagnostics`) — so one command reports every gap.
|
|
12
|
+
*
|
|
13
|
+
* COMPOSITION, NOT MERGING. Each existing builder's report object is stored
|
|
14
|
+
* verbatim in its own field and rendered by its own formatter. Nothing here
|
|
15
|
+
* mutates, rewrites, reclassifies, or drops a legacy finding — three reports plus
|
|
16
|
+
* a set of new named sections, never one flattened schema. That keeps this module
|
|
17
|
+
* additive: a change to any legacy builder cannot silently alter what the unified
|
|
18
|
+
* doctor reports about it.
|
|
19
|
+
*
|
|
20
|
+
* STRICTLY READ-ONLY. No POST, no filesystem write, no install-state write, no
|
|
21
|
+
* workflow scaffold, no schema migration, no hook installation. It is invoked
|
|
22
|
+
* from a write-capable installer, so that boundary is pinned by tests.
|
|
23
|
+
*/
|
|
24
|
+
import { readdir as fsReaddir, readFile as fsReadFile, stat as fsStat } from "node:fs/promises";
|
|
25
|
+
import os from "node:os";
|
|
26
|
+
import { buildConductorDoctorReport, formatConductorDoctorReport, } from "./doctor.js";
|
|
27
|
+
import { collectExecutorServiceDiagnostics, formatExecutorServiceDiagnosticsReport, } from "../doctor.js";
|
|
28
|
+
import { collectInstallStatusChecks, formatInstallStatusReport, } from "../install-doctor.js";
|
|
29
|
+
import { ConductorBridgeApiError, fetchConductorReadiness, } from "./bridge-api-client.js";
|
|
30
|
+
/** Operator runbook pointer surfaced beside the executor gap. */
|
|
31
|
+
export const CONDUCTOR_OPERATOR_RUNBOOK_POINTER = "docs/claude/epic-conductor-v2-operator-runbook.md";
|
|
32
|
+
/**
|
|
33
|
+
* Capability-matrix guidance for the executor leg (BAPI-688 shipped in the same
|
|
34
|
+
* build as BAPI-679): a persistent unit is *generated* by `executor
|
|
35
|
+
* install-service`, but starting/stopping it stays operator-managed, so the
|
|
36
|
+
* executor is never claimed as provisioned by a bootstrap alone.
|
|
37
|
+
*/
|
|
38
|
+
export const EXECUTOR_PROVISIONING_GUIDANCE = "generate a unit with `executor install-service`; start/stop stays operator-managed";
|
|
39
|
+
/** Remediation surfaced whenever no healthy generated service unit is detected. */
|
|
40
|
+
export const EXECUTOR_INSTALL_SERVICE_REMEDIATION = "generate a persistent unit with `executor install-service` (or run the executor by hand) — " +
|
|
41
|
+
`see ${CONDUCTOR_OPERATOR_RUNBOOK_POINTER}.`;
|
|
42
|
+
// ---------------------------------------------------------------------------
|
|
43
|
+
// Section builders
|
|
44
|
+
// ---------------------------------------------------------------------------
|
|
45
|
+
function supervisorSection(readiness) {
|
|
46
|
+
const s = readiness.supervisor;
|
|
47
|
+
const sections = [];
|
|
48
|
+
if (!s.setup_present || !s.config_present) {
|
|
49
|
+
const missing = [
|
|
50
|
+
!s.setup_present ? "setup" : null,
|
|
51
|
+
!s.config_present ? "config" : null,
|
|
52
|
+
].filter(Boolean).join(" and ");
|
|
53
|
+
sections.push({
|
|
54
|
+
id: "supervisor-configuration",
|
|
55
|
+
label: "Supervisor configuration",
|
|
56
|
+
status: "degraded",
|
|
57
|
+
detail: `project-default supervisor ${missing} absent`,
|
|
58
|
+
remediation: "bootstrap writes the safe project-default posture.",
|
|
59
|
+
});
|
|
60
|
+
}
|
|
61
|
+
else {
|
|
62
|
+
sections.push({
|
|
63
|
+
id: "supervisor-configuration",
|
|
64
|
+
label: "Supervisor configuration",
|
|
65
|
+
status: "ok",
|
|
66
|
+
detail: `setup=${s.setup_source}, config=${s.config_source}`,
|
|
67
|
+
});
|
|
68
|
+
}
|
|
69
|
+
// Deliberately its OWN section, not folded into the one above: "configured but
|
|
70
|
+
// unsafe" and "not configured" drive different operator actions, and an empty
|
|
71
|
+
// required-check list makes the CI gate pass unconditionally rather than fail.
|
|
72
|
+
sections.push(s.required_checks_empty
|
|
73
|
+
? {
|
|
74
|
+
id: "supervisor-required-checks",
|
|
75
|
+
label: "Supervisor required CI checks",
|
|
76
|
+
status: "degraded",
|
|
77
|
+
detail: "required-check list is EMPTY — the done gate would pass unconditionally",
|
|
78
|
+
remediation: "bootstrap writes a non-empty required-check list into done_gate_config.",
|
|
79
|
+
}
|
|
80
|
+
: {
|
|
81
|
+
id: "supervisor-required-checks",
|
|
82
|
+
label: "Supervisor required CI checks",
|
|
83
|
+
status: "ok",
|
|
84
|
+
detail: `${s.required_checks_count} required check(s) configured`,
|
|
85
|
+
});
|
|
86
|
+
return sections;
|
|
87
|
+
}
|
|
88
|
+
function githubCredentialsSection(readiness) {
|
|
89
|
+
const g = readiness.github;
|
|
90
|
+
if (g.credentials_complete) {
|
|
91
|
+
return {
|
|
92
|
+
id: "github-credentials",
|
|
93
|
+
label: "GitHub App credentials",
|
|
94
|
+
status: "ok",
|
|
95
|
+
detail: "owner, repository id, and installation id all resolved",
|
|
96
|
+
};
|
|
97
|
+
}
|
|
98
|
+
const missing = [
|
|
99
|
+
!g.owner_resolved ? "owner" : null,
|
|
100
|
+
!g.repo_id_resolved ? "repository id" : null,
|
|
101
|
+
!g.installation_id_resolved ? "installation id" : null,
|
|
102
|
+
].filter(Boolean).join(", ");
|
|
103
|
+
return {
|
|
104
|
+
id: "github-credentials",
|
|
105
|
+
label: "GitHub App credentials",
|
|
106
|
+
status: "degraded",
|
|
107
|
+
detail: g.credentials_readable
|
|
108
|
+
? `incomplete: ${missing} unresolved`
|
|
109
|
+
: "no GitHub credential row for this repository",
|
|
110
|
+
remediation: "connect GitHub from the Bridge setup UI (`install-bridge connect-github`).",
|
|
111
|
+
};
|
|
112
|
+
}
|
|
113
|
+
function githubActionsSection(readiness) {
|
|
114
|
+
const g = readiness.github;
|
|
115
|
+
if (g.actions_write) {
|
|
116
|
+
return {
|
|
117
|
+
id: "github-actions-permission",
|
|
118
|
+
label: "GitHub App actions permission",
|
|
119
|
+
status: "ok",
|
|
120
|
+
detail: "actions: write",
|
|
121
|
+
};
|
|
122
|
+
}
|
|
123
|
+
if (!g.actions_probe_succeeded) {
|
|
124
|
+
// "Could not be checked" is NOT the same finding as "confirmedly missing" —
|
|
125
|
+
// the first needs a retry, the second needs a permission grant.
|
|
126
|
+
return {
|
|
127
|
+
id: "github-actions-permission",
|
|
128
|
+
label: "GitHub App actions permission",
|
|
129
|
+
status: "degraded",
|
|
130
|
+
detail: "permission could not be checked",
|
|
131
|
+
remediation: "re-run once credentials resolve; the workflow rerun lane fails open on 403 until this reads `write`.",
|
|
132
|
+
};
|
|
133
|
+
}
|
|
134
|
+
return {
|
|
135
|
+
id: "github-actions-permission",
|
|
136
|
+
label: "GitHub App actions permission",
|
|
137
|
+
status: "degraded",
|
|
138
|
+
detail: `actions: ${g.actions_permission_level}`,
|
|
139
|
+
remediation: "grant the GitHub App `actions: write`; the conductor's workflow rerun lane fails open on 403 without it.",
|
|
140
|
+
};
|
|
141
|
+
}
|
|
142
|
+
function reconcilerSection(readiness) {
|
|
143
|
+
const r = readiness.reconciler;
|
|
144
|
+
if (!r.liveness_readable) {
|
|
145
|
+
return {
|
|
146
|
+
id: "reconciler-liveness",
|
|
147
|
+
label: "Reconciler tick liveness",
|
|
148
|
+
status: "degraded",
|
|
149
|
+
detail: "liveness could not be read",
|
|
150
|
+
};
|
|
151
|
+
}
|
|
152
|
+
if (!r.stale) {
|
|
153
|
+
return {
|
|
154
|
+
id: "reconciler-liveness",
|
|
155
|
+
label: "Reconciler tick liveness",
|
|
156
|
+
status: "ok",
|
|
157
|
+
detail: `last tick ${r.last_tick_age_seconds}s ago across ${r.active_run_count} active run(s)`,
|
|
158
|
+
};
|
|
159
|
+
}
|
|
160
|
+
return {
|
|
161
|
+
id: "reconciler-liveness",
|
|
162
|
+
label: "Reconciler tick liveness",
|
|
163
|
+
status: "degraded",
|
|
164
|
+
detail: r.last_tick_at === null
|
|
165
|
+
? "no reconciler tick recorded for this repository"
|
|
166
|
+
: `last tick ${r.last_tick_age_seconds}s ago (threshold ${readiness.thresholds.reconciler_stale_after_seconds}s)`,
|
|
167
|
+
remediation: "start the reconciler (`conductor epic-tick` schedule) for this repository.",
|
|
168
|
+
};
|
|
169
|
+
}
|
|
170
|
+
function workflowSection(presence, reviewPolicySource) {
|
|
171
|
+
if (presence === "present") {
|
|
172
|
+
return {
|
|
173
|
+
id: "claude-review-workflow",
|
|
174
|
+
label: "claude-review workflow",
|
|
175
|
+
status: "ok",
|
|
176
|
+
detail: ".github/workflows/claude-review.yml present",
|
|
177
|
+
};
|
|
178
|
+
}
|
|
179
|
+
// Applicability is policy-dependent: a run whose review signal is GitHub's own
|
|
180
|
+
// review decision does not need the Claude workflow at all, so its absence is
|
|
181
|
+
// not a gap. Reporting it as degraded there would train operators to ignore
|
|
182
|
+
// this section.
|
|
183
|
+
if (reviewPolicySource === "native_review_decision" || reviewPolicySource === "none") {
|
|
184
|
+
return {
|
|
185
|
+
id: "claude-review-workflow",
|
|
186
|
+
label: "claude-review workflow",
|
|
187
|
+
status: "ok",
|
|
188
|
+
detail: `not applicable for review policy '${reviewPolicySource}'`,
|
|
189
|
+
};
|
|
190
|
+
}
|
|
191
|
+
return {
|
|
192
|
+
id: "claude-review-workflow",
|
|
193
|
+
label: "claude-review workflow",
|
|
194
|
+
status: "degraded",
|
|
195
|
+
detail: presence === "unreadable"
|
|
196
|
+
? ".github/workflows/claude-review.yml could not be read"
|
|
197
|
+
: ".github/workflows/claude-review.yml absent",
|
|
198
|
+
remediation: "the selected review policy consumes the sticky verdict this workflow emits; install the template.",
|
|
199
|
+
};
|
|
200
|
+
}
|
|
201
|
+
/**
|
|
202
|
+
* Real detection of BAPI-688 generated service units, composed from
|
|
203
|
+
* `collectExecutorServiceDiagnostics`. Never `fatal` (a missing unit is a
|
|
204
|
+
* repairable gap, and blocking every install on it would be wrong) and never
|
|
205
|
+
* omitted (the flow must not read as fully provisioned by silence). A healthy
|
|
206
|
+
* unit reports `ok` for *provisioning* only — whether the service is running is
|
|
207
|
+
* the capability matrix's `executor_ready`, observed server-side.
|
|
208
|
+
*/
|
|
209
|
+
function executorProvisioningSection(diagnostics) {
|
|
210
|
+
const base = { id: "executor-provisioning", label: "Executor provisioning" };
|
|
211
|
+
if (diagnostics === null) {
|
|
212
|
+
return {
|
|
213
|
+
...base,
|
|
214
|
+
status: "degraded",
|
|
215
|
+
detail: "executor service-unit diagnostics could not be collected",
|
|
216
|
+
remediation: EXECUTOR_INSTALL_SERVICE_REMEDIATION,
|
|
217
|
+
};
|
|
218
|
+
}
|
|
219
|
+
if (diagnostics.status === "skipped") {
|
|
220
|
+
// The collector's `reason` is already sanitized and secret-free (fixed
|
|
221
|
+
// convention paths only): "no units found", unsupported platform, or the
|
|
222
|
+
// Windows manual-Task-Scheduler case.
|
|
223
|
+
return {
|
|
224
|
+
...base,
|
|
225
|
+
status: "degraded",
|
|
226
|
+
detail: diagnostics.reason,
|
|
227
|
+
remediation: EXECUTOR_INSTALL_SERVICE_REMEDIATION,
|
|
228
|
+
};
|
|
229
|
+
}
|
|
230
|
+
const unhealthy = diagnostics.units.filter((unit) => unit.warnings.length > 0 || unit.credentials.some((cred) => !cred.resolved));
|
|
231
|
+
const ids = diagnostics.units.map((unit) => unit.executorId).join(", ");
|
|
232
|
+
if (unhealthy.length === 0 && diagnostics.warnings.length === 0) {
|
|
233
|
+
return {
|
|
234
|
+
...base,
|
|
235
|
+
status: "ok",
|
|
236
|
+
detail: `${diagnostics.units.length} generated service unit(s) found (${ids}); ` +
|
|
237
|
+
"lifecycle stays operator-managed",
|
|
238
|
+
};
|
|
239
|
+
}
|
|
240
|
+
return {
|
|
241
|
+
...base,
|
|
242
|
+
status: "degraded",
|
|
243
|
+
detail: `${diagnostics.units.length} generated service unit(s) found (${ids}), but ` +
|
|
244
|
+
`${unhealthy.length} unit(s) carry warnings or unresolved credentials — see the ` +
|
|
245
|
+
"embedded executor-provisioning report",
|
|
246
|
+
remediation: EXECUTOR_INSTALL_SERVICE_REMEDIATION,
|
|
247
|
+
};
|
|
248
|
+
}
|
|
249
|
+
// ---------------------------------------------------------------------------
|
|
250
|
+
// Runner
|
|
251
|
+
// ---------------------------------------------------------------------------
|
|
252
|
+
async function inspectWorkflowPresence(readWorkflowFile) {
|
|
253
|
+
try {
|
|
254
|
+
await readWorkflowFile();
|
|
255
|
+
return "present";
|
|
256
|
+
}
|
|
257
|
+
catch (err) {
|
|
258
|
+
// ENOENT is the ordinary "absent" case; anything else is "unreadable". The
|
|
259
|
+
// error's message is never surfaced — it can echo an absolute path.
|
|
260
|
+
const code = err?.code;
|
|
261
|
+
return code === "ENOENT" ? "absent" : "unreadable";
|
|
262
|
+
}
|
|
263
|
+
}
|
|
264
|
+
/**
|
|
265
|
+
* Build the unified report. Never throws: every probe failure becomes a section.
|
|
266
|
+
*/
|
|
267
|
+
export async function runConductorInstallDoctor(deps) {
|
|
268
|
+
const sections = [];
|
|
269
|
+
// --- Legacy builders, composed verbatim ---------------------------------
|
|
270
|
+
const buildConductor = deps.buildConductorReport ?? buildConductorDoctorReport;
|
|
271
|
+
let legacyConductor = null;
|
|
272
|
+
try {
|
|
273
|
+
legacyConductor = await buildConductor(deps.conductorDoctorDeps);
|
|
274
|
+
}
|
|
275
|
+
catch {
|
|
276
|
+
sections.push({
|
|
277
|
+
id: "conductor-ledger",
|
|
278
|
+
label: "Conductor ledger doctor",
|
|
279
|
+
status: "degraded",
|
|
280
|
+
detail: "the local conductor doctor could not be built",
|
|
281
|
+
});
|
|
282
|
+
}
|
|
283
|
+
let legacyExecutorPreflight = null;
|
|
284
|
+
if (deps.collectExecutorPreflight) {
|
|
285
|
+
try {
|
|
286
|
+
legacyExecutorPreflight = await deps.collectExecutorPreflight();
|
|
287
|
+
}
|
|
288
|
+
catch {
|
|
289
|
+
sections.push({
|
|
290
|
+
id: "executor-preflight",
|
|
291
|
+
label: "Executor preflight",
|
|
292
|
+
status: "degraded",
|
|
293
|
+
detail: "the executor preflight could not be collected",
|
|
294
|
+
});
|
|
295
|
+
}
|
|
296
|
+
}
|
|
297
|
+
let legacyInstallChecks = null;
|
|
298
|
+
const collectInstall = deps.collectInstallChecks ??
|
|
299
|
+
(deps.installDoctorDeps
|
|
300
|
+
? () => collectInstallStatusChecks(deps.installDoctorDeps)
|
|
301
|
+
: null);
|
|
302
|
+
if (collectInstall) {
|
|
303
|
+
try {
|
|
304
|
+
legacyInstallChecks = await collectInstall();
|
|
305
|
+
}
|
|
306
|
+
catch {
|
|
307
|
+
sections.push({
|
|
308
|
+
id: "install-status",
|
|
309
|
+
label: "Bridge install status",
|
|
310
|
+
status: "degraded",
|
|
311
|
+
detail: "the install-status checklist could not be collected",
|
|
312
|
+
});
|
|
313
|
+
}
|
|
314
|
+
}
|
|
315
|
+
// --- Server readiness ---------------------------------------------------
|
|
316
|
+
let readiness = null;
|
|
317
|
+
if (!deps.access) {
|
|
318
|
+
// No resolvable identity/credential means every server-side section below is
|
|
319
|
+
// unknowable — the operator must fix this before any of it can be judged.
|
|
320
|
+
sections.push({
|
|
321
|
+
id: "bridge-access",
|
|
322
|
+
label: "Bridge API access",
|
|
323
|
+
status: "fatal",
|
|
324
|
+
detail: deps.accessError ?? "Bridge API repository/credential could not be resolved",
|
|
325
|
+
remediation: "run `install-bridge` (or set BAPI_API_KEY) for this repository.",
|
|
326
|
+
});
|
|
327
|
+
}
|
|
328
|
+
else {
|
|
329
|
+
try {
|
|
330
|
+
readiness = await fetchConductorReadiness(deps.access, deps.fetch);
|
|
331
|
+
}
|
|
332
|
+
catch (err) {
|
|
333
|
+
const kind = err instanceof ConductorBridgeApiError ? err.kind : "network";
|
|
334
|
+
// Authentication failure and a malformed 200 are FATAL: in both cases the
|
|
335
|
+
// report cannot be trusted, and continuing would present unknown server
|
|
336
|
+
// state as healthy. A transient transport failure is degraded — the local
|
|
337
|
+
// sections are still meaningful and a retry may succeed.
|
|
338
|
+
const fatalKind = kind === "unauthorized" || kind === "invalid-input";
|
|
339
|
+
sections.push({
|
|
340
|
+
id: "server-readiness",
|
|
341
|
+
label: "Server readiness",
|
|
342
|
+
status: fatalKind ? "fatal" : "degraded",
|
|
343
|
+
detail: kind === "unauthorized"
|
|
344
|
+
? "Bridge API rejected the credential for this repository"
|
|
345
|
+
: kind === "invalid-input"
|
|
346
|
+
? "the readiness response failed shape validation"
|
|
347
|
+
: "the readiness endpoint could not be reached",
|
|
348
|
+
remediation: fatalKind
|
|
349
|
+
? "re-check the API key and repository, then re-run."
|
|
350
|
+
: "retry; the local sections above are unaffected.",
|
|
351
|
+
});
|
|
352
|
+
}
|
|
353
|
+
}
|
|
354
|
+
if (readiness) {
|
|
355
|
+
sections.push(...supervisorSection(readiness));
|
|
356
|
+
sections.push(githubCredentialsSection(readiness));
|
|
357
|
+
sections.push(githubActionsSection(readiness));
|
|
358
|
+
}
|
|
359
|
+
const presence = await inspectWorkflowPresence(deps.readWorkflowFile);
|
|
360
|
+
sections.push(workflowSection(presence, deps.reviewPolicySource));
|
|
361
|
+
if (readiness) {
|
|
362
|
+
sections.push(reconcilerSection(readiness));
|
|
363
|
+
}
|
|
364
|
+
// --- Executor provisioning (BAPI-688): real, read-only unit detection -----
|
|
365
|
+
const collectServiceUnits = deps.collectExecutorServiceUnits ??
|
|
366
|
+
(() => collectExecutorServiceDiagnostics({
|
|
367
|
+
platform: process.platform,
|
|
368
|
+
env: process.env,
|
|
369
|
+
homedir: os.homedir,
|
|
370
|
+
readdir: (dirPath) => fsReaddir(dirPath),
|
|
371
|
+
readFile: (filePath) => fsReadFile(filePath, "utf-8"),
|
|
372
|
+
stat: (filePath) => fsStat(filePath),
|
|
373
|
+
}));
|
|
374
|
+
let executorService = null;
|
|
375
|
+
try {
|
|
376
|
+
executorService = await collectServiceUnits();
|
|
377
|
+
}
|
|
378
|
+
catch {
|
|
379
|
+
// The collector itself never throws; this guards an injected fake. The
|
|
380
|
+
// section reports the collection gap rather than losing the section.
|
|
381
|
+
}
|
|
382
|
+
sections.push(executorProvisioningSection(executorService));
|
|
383
|
+
return {
|
|
384
|
+
legacyConductor,
|
|
385
|
+
legacyExecutorPreflight,
|
|
386
|
+
legacyInstallChecks,
|
|
387
|
+
executorService,
|
|
388
|
+
readiness,
|
|
389
|
+
sections,
|
|
390
|
+
};
|
|
391
|
+
}
|
|
392
|
+
// ---------------------------------------------------------------------------
|
|
393
|
+
// Formatting + exit code
|
|
394
|
+
// ---------------------------------------------------------------------------
|
|
395
|
+
/** Status tag, matching `formatConductorDoctorReport`'s visual hierarchy. */
|
|
396
|
+
function statusTag(status) {
|
|
397
|
+
if (status === "fatal")
|
|
398
|
+
return "✗ FATAL";
|
|
399
|
+
if (status === "degraded")
|
|
400
|
+
return "! DEGRADED";
|
|
401
|
+
return "✓ OK";
|
|
402
|
+
}
|
|
403
|
+
/**
|
|
404
|
+
* Render the unified report. The three legacy reports are EMBEDDED unchanged via
|
|
405
|
+
* their own formatters — never re-rendered from a merged schema.
|
|
406
|
+
*/
|
|
407
|
+
export function formatConductorInstallDoctorReport(report) {
|
|
408
|
+
const lines = [
|
|
409
|
+
"Conductor install doctor",
|
|
410
|
+
"════════════════════════",
|
|
411
|
+
"",
|
|
412
|
+
];
|
|
413
|
+
for (const section of report.sections) {
|
|
414
|
+
lines.push(`${statusTag(section.status)} ${section.label}`);
|
|
415
|
+
lines.push(` ${section.detail}`);
|
|
416
|
+
if (section.remediation)
|
|
417
|
+
lines.push(` → ${section.remediation}`);
|
|
418
|
+
}
|
|
419
|
+
// Each legacy formatter is called defensively. They are written for the exact
|
|
420
|
+
// shape their own builder returns, so a builder that degrades (or an injected
|
|
421
|
+
// fake) can make one throw — and this renderer runs inside the installer right
|
|
422
|
+
// before its write decisions, where an unhandled throw would abort the run with
|
|
423
|
+
// a raw stack instead of the report the operator needs to act on.
|
|
424
|
+
const embed = (label, render) => {
|
|
425
|
+
lines.push("");
|
|
426
|
+
try {
|
|
427
|
+
lines.push(render());
|
|
428
|
+
}
|
|
429
|
+
catch {
|
|
430
|
+
lines.push(`${label}: report could not be rendered.`);
|
|
431
|
+
}
|
|
432
|
+
};
|
|
433
|
+
if (report.legacyInstallChecks) {
|
|
434
|
+
embed("Bridge install status", () => formatInstallStatusReport(report.legacyInstallChecks));
|
|
435
|
+
}
|
|
436
|
+
if (report.legacyConductor) {
|
|
437
|
+
embed("Conductor ledger doctor", () => formatConductorDoctorReport(report.legacyConductor));
|
|
438
|
+
}
|
|
439
|
+
if (report.legacyExecutorPreflight) {
|
|
440
|
+
const p = report.legacyExecutorPreflight;
|
|
441
|
+
lines.push("");
|
|
442
|
+
lines.push("executor preflight (local)");
|
|
443
|
+
lines.push("──────────────────────────");
|
|
444
|
+
lines.push(`ok: ${p.ok}`);
|
|
445
|
+
for (const finding of p.fatalFindings)
|
|
446
|
+
lines.push(` fatal: ${finding}`);
|
|
447
|
+
for (const warning of p.warnings)
|
|
448
|
+
lines.push(` warn: ${warning}`);
|
|
449
|
+
}
|
|
450
|
+
if (report.executorService) {
|
|
451
|
+
embed("Executor provisioning", () => formatExecutorServiceDiagnosticsReport(report.executorService));
|
|
452
|
+
}
|
|
453
|
+
return lines.join("\n");
|
|
454
|
+
}
|
|
455
|
+
/** `1` when any section is fatal; `0` for ok/degraded-only reports. */
|
|
456
|
+
export function conductorInstallDoctorExitCode(report) {
|
|
457
|
+
return report.sections.some((s) => s.status === "fatal") ? 1 : 0;
|
|
458
|
+
}
|
|
459
|
+
/** True when the report has a fatal section (the pre-write abort predicate). */
|
|
460
|
+
export function conductorInstallDoctorHasFatal(report) {
|
|
461
|
+
return report.sections.some((s) => s.status === "fatal");
|
|
462
|
+
}
|