@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
|
@@ -1418,6 +1418,12 @@ export async function transitionJiraStatus(access, ticketNumber, targetStatus =
|
|
|
1418
1418
|
throw err;
|
|
1419
1419
|
}
|
|
1420
1420
|
}
|
|
1421
|
+
export const CONDUCTOR_REVIEW_ALIGNMENT_STATUSES = new Set([
|
|
1422
|
+
"aligned",
|
|
1423
|
+
"divergent",
|
|
1424
|
+
"not_configured",
|
|
1425
|
+
"invalid",
|
|
1426
|
+
]);
|
|
1421
1427
|
const READINESS_SOURCES = new Set(["epic", "project_default", "none"]);
|
|
1422
1428
|
const ACTIONS_LEVELS = new Set(["write", "read", "none", "unknown"]);
|
|
1423
1429
|
function readinessShapeError() {
|
|
@@ -1469,6 +1475,31 @@ function requireNullableBool(o, key) {
|
|
|
1469
1475
|
throw readinessShapeError();
|
|
1470
1476
|
return v;
|
|
1471
1477
|
}
|
|
1478
|
+
/**
|
|
1479
|
+
* Parse the optional alignment block (BAPI-694).
|
|
1480
|
+
*
|
|
1481
|
+
* ABSENT — the key is missing or null — yields `null`, which is how a CLI newer
|
|
1482
|
+
* than the server degrades: alignment is advisory, and hard-failing a readiness
|
|
1483
|
+
* read over a field the server has never heard of would break every operator on
|
|
1484
|
+
* an older deploy. PRESENT-but-malformed still throws, because a body that
|
|
1485
|
+
* claims to carry an alignment verdict and cannot be read is exactly the case
|
|
1486
|
+
* the fail-closed parser exists for.
|
|
1487
|
+
*/
|
|
1488
|
+
function parseReviewPolicyAlignment(o) {
|
|
1489
|
+
const raw = o.review_policy_alignment;
|
|
1490
|
+
if (raw === undefined || raw === null)
|
|
1491
|
+
return null;
|
|
1492
|
+
const a = requireObject(raw);
|
|
1493
|
+
const explanation = a.explanation;
|
|
1494
|
+
if (typeof explanation !== "string")
|
|
1495
|
+
throw readinessShapeError();
|
|
1496
|
+
return {
|
|
1497
|
+
status: requireEnum(a, "status", CONDUCTOR_REVIEW_ALIGNMENT_STATUSES),
|
|
1498
|
+
repo_review_signal: requireNullableString(a, "repo_review_signal"),
|
|
1499
|
+
done_gate_review_signal: requireNullableString(a, "done_gate_review_signal"),
|
|
1500
|
+
explanation,
|
|
1501
|
+
};
|
|
1502
|
+
}
|
|
1472
1503
|
function requireEnum(o, key, allowed) {
|
|
1473
1504
|
const v = o[key];
|
|
1474
1505
|
if (typeof v !== "string" || !allowed.has(v))
|
|
@@ -1509,6 +1540,9 @@ export function parseConductorReadinessResponse(body) {
|
|
|
1509
1540
|
required_checks_empty: requireBool(sup, "required_checks_empty"),
|
|
1510
1541
|
auto_merge_enabled: requireBool(sup, "auto_merge_enabled"),
|
|
1511
1542
|
merge_approval_required_set: requireBool(sup, "merge_approval_required_set"),
|
|
1543
|
+
review_policy_alignment: parseReviewPolicyAlignment(sup),
|
|
1544
|
+
// Absent on a pre-BAPI-694 server, where no repository default can exist.
|
|
1545
|
+
review_policy_present: sup.review_policy_present === true,
|
|
1512
1546
|
},
|
|
1513
1547
|
github: {
|
|
1514
1548
|
credentials_readable: requireBool(gh, "credentials_readable"),
|
|
@@ -1582,8 +1616,9 @@ export function parseConductorSupervisorBootstrapResponse(body) {
|
|
|
1582
1616
|
/**
|
|
1583
1617
|
* POST `/jira/epic-runs/supervisor-bootstrap?repo_name=<repo>`.
|
|
1584
1618
|
*
|
|
1585
|
-
* The ONLY write this client performs.
|
|
1586
|
-
*
|
|
1619
|
+
* The ONLY write this client performs. Since BAPI-694 it may also carry the
|
|
1620
|
+
* optional repository-default `review_policy`; omitting that field stores no
|
|
1621
|
+
* default rather than a neutral one.
|
|
1587
1622
|
*/
|
|
1588
1623
|
export async function bootstrapConductorSupervisorDefaults(access, request, fetchImpl) {
|
|
1589
1624
|
const url = buildConductorJiraUrl(access.baseUrl, `${EPIC_RUNS_API_PREFIX}/supervisor-bootstrap`, {
|
|
@@ -9,7 +9,10 @@
|
|
|
9
9
|
*/
|
|
10
10
|
import { spawnSync } from "node:child_process";
|
|
11
11
|
import { existsSync, readFileSync } from "node:fs";
|
|
12
|
+
import { readFile as readFileAsync } from "node:fs/promises";
|
|
13
|
+
import os from "node:os";
|
|
12
14
|
import { doctorConductorLedger } from "./store.js";
|
|
15
|
+
import { detectClaudeLogin, formatClaudeLoginAdvisory } from "../claude-login.js";
|
|
13
16
|
import { inspectConductorGitHooks } from "./git-hooks.js";
|
|
14
17
|
import { resolveMcpShimInvocationForRuntime, resolvePackageRootFromModuleUrl, } from "../mcp-server-invocation.js";
|
|
15
18
|
/**
|
|
@@ -284,6 +287,10 @@ export async function buildConductorDoctorReport(deps = {}) {
|
|
|
284
287
|
const deny_enforcement = deps.skipDenyProbe
|
|
285
288
|
? skippedDenyEnforcementResult()
|
|
286
289
|
: await inspectDenyEnforcementSafe(deps.inspectDenyEnforcement);
|
|
290
|
+
const claude_login = await detectClaudeLogin({
|
|
291
|
+
homedir: deps.claudeLoginDeps?.homedir ?? os.homedir,
|
|
292
|
+
readFile: deps.claudeLoginDeps?.readFile ?? ((p) => readFileAsync(p, "utf-8")),
|
|
293
|
+
});
|
|
287
294
|
return {
|
|
288
295
|
ledger,
|
|
289
296
|
git_hooks: inspectHooks(deps.hooksDeps),
|
|
@@ -292,6 +299,7 @@ export async function buildConductorDoctorReport(deps = {}) {
|
|
|
292
299
|
local_merge: inspectLocalMerge(deps.runCommand),
|
|
293
300
|
native_ledger,
|
|
294
301
|
deny_enforcement,
|
|
302
|
+
claude_login,
|
|
295
303
|
};
|
|
296
304
|
}
|
|
297
305
|
/**
|
|
@@ -329,7 +337,7 @@ async function inspectDenyEnforcementSafe(inspect) {
|
|
|
329
337
|
* status tags consistent with the git hooks section's visual hierarchy.
|
|
330
338
|
*/
|
|
331
339
|
export function formatConductorDoctorReport(report) {
|
|
332
|
-
const { ledger, git_hooks, epic_tick, mcp_profile, local_merge, native_ledger, deny_enforcement } = report;
|
|
340
|
+
const { ledger, git_hooks, epic_tick, mcp_profile, local_merge, native_ledger, deny_enforcement, claude_login } = report;
|
|
333
341
|
const lines = [
|
|
334
342
|
"Conductor ledger doctor",
|
|
335
343
|
"───────────────────────",
|
|
@@ -462,5 +470,7 @@ export function formatConductorDoctorReport(report) {
|
|
|
462
470
|
for (const w of deny_enforcement.warnings)
|
|
463
471
|
lines.push(` - ${w}`);
|
|
464
472
|
}
|
|
473
|
+
lines.push("");
|
|
474
|
+
lines.push(formatClaudeLoginAdvisory(claude_login));
|
|
465
475
|
return lines.join("\n");
|
|
466
476
|
}
|
|
@@ -30,15 +30,22 @@ import { ConductorBridgeApiError, fetchConductorReadiness, } from "./bridge-api-
|
|
|
30
30
|
/** Operator runbook pointer surfaced beside the executor gap. */
|
|
31
31
|
export const CONDUCTOR_OPERATOR_RUNBOOK_POINTER = "docs/claude/epic-conductor-v2-operator-runbook.md";
|
|
32
32
|
/**
|
|
33
|
-
* Capability-matrix guidance for the executor leg
|
|
34
|
-
*
|
|
35
|
-
*
|
|
36
|
-
*
|
|
33
|
+
* Capability-matrix guidance for the executor leg.
|
|
34
|
+
*
|
|
35
|
+
* Amended by BAPI-779 (R-1): `install conductor` can now write the unit AND
|
|
36
|
+
* start it, each behind its own consent. What did NOT change is the readiness
|
|
37
|
+
* rule — `executor_ready` still comes only from a live server observation, so
|
|
38
|
+
* neither a written unit nor a successful local start may produce that claim.
|
|
37
39
|
*/
|
|
38
|
-
export const EXECUTOR_PROVISIONING_GUIDANCE = "
|
|
40
|
+
export const EXECUTOR_PROVISIONING_GUIDANCE = "`install conductor` writes the unit and starts it, each behind its own consent; " +
|
|
41
|
+
"readiness still comes only from a live server observation";
|
|
39
42
|
/** Remediation surfaced whenever no healthy generated service unit is detected. */
|
|
40
|
-
export const EXECUTOR_INSTALL_SERVICE_REMEDIATION = "generate a persistent unit with `
|
|
43
|
+
export const EXECUTOR_INSTALL_SERVICE_REMEDIATION = "generate a persistent unit with `install conductor --executor-id <id>` or " +
|
|
44
|
+
"`executor install-service` (or run the executor by hand) — " +
|
|
41
45
|
`see ${CONDUCTOR_OPERATOR_RUNBOOK_POINTER}.`;
|
|
46
|
+
/** Remediation for a unit that exists but whose service is not running. */
|
|
47
|
+
export const EXECUTOR_SERVICE_START_REMEDIATION = "start the service (re-run `install conductor` and accept the start consent, or run the " +
|
|
48
|
+
`printed launchctl/systemctl command by hand) — see ${CONDUCTOR_OPERATOR_RUNBOOK_POINTER}.`;
|
|
42
49
|
// ---------------------------------------------------------------------------
|
|
43
50
|
// Section builders
|
|
44
51
|
// ---------------------------------------------------------------------------
|
|
@@ -83,6 +90,45 @@ function supervisorSection(readiness) {
|
|
|
83
90
|
status: "ok",
|
|
84
91
|
detail: `${s.required_checks_count} required check(s) configured`,
|
|
85
92
|
});
|
|
93
|
+
// BAPI-694 (R5) — its OWN section, and WARN-ONLY.
|
|
94
|
+
//
|
|
95
|
+
// `degraded`, never `fatal`: a divergence between the code_review gate's
|
|
96
|
+
// signal and the merge path's may be exactly what the operator intended, and
|
|
97
|
+
// blocking an install over a legitimate posture would be worse than the
|
|
98
|
+
// silence this replaces. It also changes no gate: both settings are reported,
|
|
99
|
+
// neither is coerced into the other's vocabulary.
|
|
100
|
+
//
|
|
101
|
+
// Absent (`null`) means the server predates BAPI-694 and cannot report
|
|
102
|
+
// alignment, so no section is emitted at all — an advisory addition must not
|
|
103
|
+
// manufacture a finding out of a server that never answered.
|
|
104
|
+
const alignment = s.review_policy_alignment;
|
|
105
|
+
if (alignment !== null) {
|
|
106
|
+
if (alignment.status === "divergent" || alignment.status === "invalid") {
|
|
107
|
+
sections.push({
|
|
108
|
+
id: "supervisor-review-alignment",
|
|
109
|
+
label: "Supervisor review-policy alignment",
|
|
110
|
+
status: "degraded",
|
|
111
|
+
detail: alignment.explanation,
|
|
112
|
+
remediation: alignment.status === "invalid"
|
|
113
|
+
? "fix the done gate's review_state condition — review configuration " +
|
|
114
|
+
"exists but cannot be read, so the merge path refuses rather than merges."
|
|
115
|
+
: "set the repository review-policy default and the done gate's " +
|
|
116
|
+
"review_state condition to the same signal, or confirm the difference " +
|
|
117
|
+
`is intended — see ${CONDUCTOR_OPERATOR_RUNBOOK_POINTER}.`,
|
|
118
|
+
});
|
|
119
|
+
}
|
|
120
|
+
else {
|
|
121
|
+
// `aligned` and `not_configured` are both non-warning states, and are
|
|
122
|
+
// reported separately: "they agree" and "neither is set" are different
|
|
123
|
+
// facts, and collapsing them would imply a stored default that is absent.
|
|
124
|
+
sections.push({
|
|
125
|
+
id: "supervisor-review-alignment",
|
|
126
|
+
label: "Supervisor review-policy alignment",
|
|
127
|
+
status: "ok",
|
|
128
|
+
detail: alignment.explanation,
|
|
129
|
+
});
|
|
130
|
+
}
|
|
131
|
+
}
|
|
86
132
|
return sections;
|
|
87
133
|
}
|
|
88
134
|
function githubCredentialsSection(readiness) {
|
|
@@ -202,9 +248,13 @@ function workflowSection(presence, reviewPolicySource) {
|
|
|
202
248
|
* Real detection of BAPI-688 generated service units, composed from
|
|
203
249
|
* `collectExecutorServiceDiagnostics`. Never `fatal` (a missing unit is a
|
|
204
250
|
* 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).
|
|
206
|
-
*
|
|
207
|
-
*
|
|
251
|
+
* omitted (the flow must not read as fully provisioned by silence).
|
|
252
|
+
*
|
|
253
|
+
* This section reports UNIT PRESENCE and nothing else (BAPI-779). Whether the
|
|
254
|
+
* service manager is actually running the unit is
|
|
255
|
+
* {@link executorServiceStateSection}, and whether the SERVER has observed an
|
|
256
|
+
* executor is {@link executorObservationSection}. Three separate facts, three
|
|
257
|
+
* separate sections — a healthy unit here proves only that a file exists.
|
|
208
258
|
*/
|
|
209
259
|
function executorProvisioningSection(diagnostics) {
|
|
210
260
|
const base = { id: "executor-provisioning", label: "Executor provisioning" };
|
|
@@ -234,7 +284,7 @@ function executorProvisioningSection(diagnostics) {
|
|
|
234
284
|
...base,
|
|
235
285
|
status: "ok",
|
|
236
286
|
detail: `${diagnostics.units.length} generated service unit(s) found (${ids}); ` +
|
|
237
|
-
"
|
|
287
|
+
"unit presence only — see the service-state and observation sections",
|
|
238
288
|
};
|
|
239
289
|
}
|
|
240
290
|
return {
|
|
@@ -246,6 +296,91 @@ function executorProvisioningSection(diagnostics) {
|
|
|
246
296
|
remediation: EXECUTOR_INSTALL_SERVICE_REMEDIATION,
|
|
247
297
|
};
|
|
248
298
|
}
|
|
299
|
+
/**
|
|
300
|
+
* Local service-manager state for the units that were discovered (BAPI-779).
|
|
301
|
+
*
|
|
302
|
+
* Never `fatal`: this is a diagnostic, and an unreadable service manager is a
|
|
303
|
+
* gap in the *report*, not a broken install. `unknown` is deliberately not
|
|
304
|
+
* collapsed into `not-found` — "the probe could not answer" and "the service
|
|
305
|
+
* manager has no such unit" lead to different operator actions.
|
|
306
|
+
*/
|
|
307
|
+
function executorServiceStateSection(observations, unitsDiscovered, inspectorAvailable) {
|
|
308
|
+
const base = { id: "executor-service-state", label: "Executor service state" };
|
|
309
|
+
if (!unitsDiscovered) {
|
|
310
|
+
return {
|
|
311
|
+
...base,
|
|
312
|
+
status: "degraded",
|
|
313
|
+
detail: "no generated unit to inspect, so no service state was collected",
|
|
314
|
+
remediation: EXECUTOR_INSTALL_SERVICE_REMEDIATION,
|
|
315
|
+
};
|
|
316
|
+
}
|
|
317
|
+
if (!inspectorAvailable || observations.length === 0) {
|
|
318
|
+
return {
|
|
319
|
+
...base,
|
|
320
|
+
status: "degraded",
|
|
321
|
+
detail: "local service state could not be collected on this host",
|
|
322
|
+
remediation: EXECUTOR_SERVICE_START_REMEDIATION,
|
|
323
|
+
};
|
|
324
|
+
}
|
|
325
|
+
// Deterministic ordering by executor id so two runs of the same host render
|
|
326
|
+
// identically regardless of enumeration order.
|
|
327
|
+
const rendered = [...observations]
|
|
328
|
+
.sort((a, b) => (a.executorId < b.executorId ? -1 : a.executorId > b.executorId ? 1 : 0))
|
|
329
|
+
.map((o) => `${o.executorId}=${o.state}`)
|
|
330
|
+
.join(", ");
|
|
331
|
+
const allActive = observations.every((o) => o.state === "active");
|
|
332
|
+
return allActive
|
|
333
|
+
? { ...base, status: "ok", detail: `service state: ${rendered}` }
|
|
334
|
+
: {
|
|
335
|
+
...base,
|
|
336
|
+
status: "degraded",
|
|
337
|
+
detail: `service state: ${rendered}`,
|
|
338
|
+
remediation: EXECUTOR_SERVICE_START_REMEDIATION,
|
|
339
|
+
};
|
|
340
|
+
}
|
|
341
|
+
/**
|
|
342
|
+
* The AUTHORITATIVE executor fact: what the server has observed (BAPI-779).
|
|
343
|
+
*
|
|
344
|
+
* This is the only section permitted to say `executor_ready`, and only for
|
|
345
|
+
* `readiness.executor.ready === true`. The three not-ready shapes are kept
|
|
346
|
+
* distinct because they mean different things: `null` is "never observed" (the
|
|
347
|
+
* executor has not claimed yet), `false` is "observed but stale" (it claimed
|
|
348
|
+
* once and went quiet), and an unreadable readiness payload is "unknown" (the
|
|
349
|
+
* doctor cannot say). Collapsing them would let a stale executor read like a
|
|
350
|
+
* fresh install, or vice versa.
|
|
351
|
+
*/
|
|
352
|
+
function executorObservationSection(readiness) {
|
|
353
|
+
const base = { id: "executor-observation", label: "Executor server observation" };
|
|
354
|
+
if (readiness === null || !readiness.executor.liveness_readable) {
|
|
355
|
+
return {
|
|
356
|
+
...base,
|
|
357
|
+
status: "degraded",
|
|
358
|
+
detail: "server executor observation is unknown (readiness could not be read)",
|
|
359
|
+
remediation: "retry once the Bridge API is reachable; no readiness claim is made from local state.",
|
|
360
|
+
};
|
|
361
|
+
}
|
|
362
|
+
const e = readiness.executor;
|
|
363
|
+
const seen = e.last_seen_at === null
|
|
364
|
+
? "never seen"
|
|
365
|
+
: `last seen ${e.last_seen_at} (${e.last_seen_age_seconds}s ago)`;
|
|
366
|
+
if (e.ready === true) {
|
|
367
|
+
return { ...base, status: "ok", detail: `executor_ready — ${seen}` };
|
|
368
|
+
}
|
|
369
|
+
if (e.ready === false) {
|
|
370
|
+
return {
|
|
371
|
+
...base,
|
|
372
|
+
status: "degraded",
|
|
373
|
+
detail: `observed but stale/not ready — ${seen}`,
|
|
374
|
+
remediation: EXECUTOR_SERVICE_START_REMEDIATION,
|
|
375
|
+
};
|
|
376
|
+
}
|
|
377
|
+
return {
|
|
378
|
+
...base,
|
|
379
|
+
status: "degraded",
|
|
380
|
+
detail: `never observed by the server — ${seen}`,
|
|
381
|
+
remediation: EXECUTOR_SERVICE_START_REMEDIATION,
|
|
382
|
+
};
|
|
383
|
+
}
|
|
249
384
|
// ---------------------------------------------------------------------------
|
|
250
385
|
// Runner
|
|
251
386
|
// ---------------------------------------------------------------------------
|
|
@@ -380,11 +515,36 @@ export async function runConductorInstallDoctor(deps) {
|
|
|
380
515
|
// section reports the collection gap rather than losing the section.
|
|
381
516
|
}
|
|
382
517
|
sections.push(executorProvisioningSection(executorService));
|
|
518
|
+
// --- Local service state (BAPI-779): read-only, and ONLY for units that were
|
|
519
|
+
// actually discovered above. Probing a service manager for a unit this host
|
|
520
|
+
// never generated would be a speculative query with nothing to report.
|
|
521
|
+
const discoveredUnits = executorService !== null && executorService.status !== "skipped"
|
|
522
|
+
? executorService.units
|
|
523
|
+
: [];
|
|
524
|
+
const executorServiceStates = [];
|
|
525
|
+
if (deps.inspectExecutorServiceState && discoveredUnits.length > 0) {
|
|
526
|
+
for (const unit of discoveredUnits) {
|
|
527
|
+
try {
|
|
528
|
+
const state = await deps.inspectExecutorServiceState({ executorId: unit.executorId });
|
|
529
|
+
executorServiceStates.push({ executorId: unit.executorId, state });
|
|
530
|
+
}
|
|
531
|
+
catch {
|
|
532
|
+
// An inspector that throws yields `unknown` for that unit rather than
|
|
533
|
+
// dropping it — a silently missing unit reads as "nothing to see".
|
|
534
|
+
executorServiceStates.push({ executorId: unit.executorId, state: "unknown" });
|
|
535
|
+
}
|
|
536
|
+
}
|
|
537
|
+
}
|
|
538
|
+
sections.push(executorServiceStateSection(executorServiceStates, discoveredUnits.length > 0, Boolean(deps.inspectExecutorServiceState)));
|
|
539
|
+
// --- Server observation: the authoritative executor fact, last so it reads
|
|
540
|
+
// as the conclusion of the three-part executor story.
|
|
541
|
+
sections.push(executorObservationSection(readiness));
|
|
383
542
|
return {
|
|
384
543
|
legacyConductor,
|
|
385
544
|
legacyExecutorPreflight,
|
|
386
545
|
legacyInstallChecks,
|
|
387
546
|
executorService,
|
|
547
|
+
executorServiceStates,
|
|
388
548
|
readiness,
|
|
389
549
|
sections,
|
|
390
550
|
};
|
|
@@ -450,6 +610,20 @@ export function formatConductorInstallDoctorReport(report) {
|
|
|
450
610
|
if (report.executorService) {
|
|
451
611
|
embed("Executor provisioning", () => formatExecutorServiceDiagnosticsReport(report.executorService));
|
|
452
612
|
}
|
|
613
|
+
// Per-unit local service state (BAPI-779), rendered as its OWN block rather
|
|
614
|
+
// than folded into the provisioning report above — the whole point of the
|
|
615
|
+
// three-dimension split is that a reader can see "unit written" and "service
|
|
616
|
+
// not running" as two separate lines.
|
|
617
|
+
const states = report.executorServiceStates ?? [];
|
|
618
|
+
if (states.length > 0) {
|
|
619
|
+
lines.push("");
|
|
620
|
+
lines.push("executor service state (local)");
|
|
621
|
+
lines.push("──────────────────────────────");
|
|
622
|
+
for (const observation of [...states].sort((a, b) => a.executorId < b.executorId ? -1 : a.executorId > b.executorId ? 1 : 0)) {
|
|
623
|
+
lines.push(` ${observation.executorId}: ${observation.state}`);
|
|
624
|
+
}
|
|
625
|
+
lines.push(" local state is NOT readiness — only a live server observation produces executor_ready.");
|
|
626
|
+
}
|
|
453
627
|
return lines.join("\n");
|
|
454
628
|
}
|
|
455
629
|
/** `1` when any section is fatal; `0` for ok/degraded-only reports. */
|