@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.
- package/README.md +42 -12
- package/build/commands.generated.js +6 -4
- package/build/conductor/bridge-api-client.js +61 -0
- package/build/conductor/cli.js +23 -0
- package/build/conductor/doctor.js +428 -5
- package/build/conductor/install-doctor.js +65 -656
- package/build/conductor/readiness-cli.js +152 -0
- package/build/conductor/readiness-sections.js +666 -0
- package/build/conductor/readiness.js +710 -0
- package/build/conductor/tools.js +56 -3
- package/build/conductor-bin.js +21 -17
- package/build/index.js +4651 -4580
- package/build/install-doctor.js +154 -2
- package/build/pipelines.generated.js +3 -3
- package/build/plane/alembic-head.js +40 -11
- package/build/plane/build-freshness.js +22 -11
- package/build/plane/preflight.js +363 -48
- package/build/plane/types.js +36 -0
- package/build/readiness-check.js +412 -0
- package/build/readme.generated.js +1 -1
- package/build/version.generated.js +3 -3
- package/package.json +2 -2
- package/pipelines/{full-automation.json → idea-to-pr.json} +1 -1
|
@@ -769,6 +769,34 @@ export async function fetchEpicRunState(access, epicKey, fetchImpl = globalThis.
|
|
|
769
769
|
const parsed = await fetchConductorJsonWithTimeout(url, conductorGetHeaders(access), CONDUCTOR_FETCH_TIMEOUT_MS, fetchImpl);
|
|
770
770
|
return parsed;
|
|
771
771
|
}
|
|
772
|
+
/**
|
|
773
|
+
* Fetch the read-only ExplainRun / ExplainTicket projection (BAPI-1028).
|
|
774
|
+
*
|
|
775
|
+
* One function, two scopes, because they are two selectors over ONE projection:
|
|
776
|
+
* without `ticketKey` it reads the whole run, with it the single ticket. Both
|
|
777
|
+
* responses carry the same `schema_version` and the ticket response's `ticket`
|
|
778
|
+
* is byte-identical to that ticket's entry in the run response.
|
|
779
|
+
*
|
|
780
|
+
* Read-only end to end: the server route composes persisted rows, writes
|
|
781
|
+
* nothing, enqueues nothing, and calls no provider. Non-2xx responses travel
|
|
782
|
+
* through the shared timeout/sanitization helper as a `ConductorBridgeApiError`,
|
|
783
|
+
* so no response body ever reaches a caller.
|
|
784
|
+
*/
|
|
785
|
+
export async function fetchExplainRun(access, epicRunId, ticketKey, fetchImpl = globalThis.fetch) {
|
|
786
|
+
requireNonEmptyString(epicRunId);
|
|
787
|
+
let apiPath = `${epicRunApiPath(epicRunId)}/explain`;
|
|
788
|
+
if (ticketKey !== undefined) {
|
|
789
|
+
requireNonEmptyString(ticketKey);
|
|
790
|
+
// Encoded as ONE path segment: a ticket key carrying a slash would
|
|
791
|
+
// otherwise silently reshape the URL into a different endpoint.
|
|
792
|
+
requireNoSlashPathSegment(ticketKey);
|
|
793
|
+
apiPath = `${epicRunApiPath(epicRunId)}/tickets/${encodeURIComponent(ticketKey)}/explain`;
|
|
794
|
+
}
|
|
795
|
+
const url = buildConductorJiraUrl(access.baseUrl, apiPath, {
|
|
796
|
+
repo_name: access.repoName,
|
|
797
|
+
});
|
|
798
|
+
return fetchConductorJsonWithTimeout(url, conductorGetHeaders(access), CONDUCTOR_FETCH_TIMEOUT_MS, fetchImpl);
|
|
799
|
+
}
|
|
772
800
|
// ---------------------------------------------------------------------------
|
|
773
801
|
// Executor wind-down: read-only completion-state projection (BAPI-1010)
|
|
774
802
|
// ---------------------------------------------------------------------------
|
|
@@ -1886,6 +1914,39 @@ export function parseConductorReadinessResponse(body) {
|
|
|
1886
1914
|
reconciler_stale_after_seconds: requireInt(thr, "reconciler_stale_after_seconds"),
|
|
1887
1915
|
executor_stale_after_seconds: requireInt(thr, "executor_stale_after_seconds"),
|
|
1888
1916
|
},
|
|
1917
|
+
review_workflow: parseReviewWorkflow(root),
|
|
1918
|
+
conductor_ci_workflow: parseConductorCiWorkflow(root),
|
|
1919
|
+
};
|
|
1920
|
+
}
|
|
1921
|
+
/**
|
|
1922
|
+
* Parse the optional `review_workflow` block.
|
|
1923
|
+
*
|
|
1924
|
+
* Absent (or null) yields `null` — an older server, not a finding. A PRESENT
|
|
1925
|
+
* block is validated strictly and a malformed one throws the same shape error
|
|
1926
|
+
* as every other field, because a malformed body is untrusted input.
|
|
1927
|
+
*/
|
|
1928
|
+
function parseReviewWorkflow(body) {
|
|
1929
|
+
const raw = body.review_workflow;
|
|
1930
|
+
if (raw === undefined || raw === null)
|
|
1931
|
+
return null;
|
|
1932
|
+
const o = requireObject(raw);
|
|
1933
|
+
return {
|
|
1934
|
+
probe_succeeded: requireBool(o, "probe_succeeded"),
|
|
1935
|
+
workflow_present: requireBool(o, "workflow_present"),
|
|
1936
|
+
emits_run_id: requireBool(o, "emits_run_id"),
|
|
1937
|
+
emits_reviewed_sha: requireBool(o, "emits_reviewed_sha"),
|
|
1938
|
+
};
|
|
1939
|
+
}
|
|
1940
|
+
/** Parse the optional `conductor_ci_workflow` block. Same rule as above. */
|
|
1941
|
+
function parseConductorCiWorkflow(body) {
|
|
1942
|
+
const raw = body.conductor_ci_workflow;
|
|
1943
|
+
if (raw === undefined || raw === null)
|
|
1944
|
+
return null;
|
|
1945
|
+
const o = requireObject(raw);
|
|
1946
|
+
return {
|
|
1947
|
+
probe_succeeded: requireBool(o, "probe_succeeded"),
|
|
1948
|
+
workflow_present: requireBool(o, "workflow_present"),
|
|
1949
|
+
migration_guard_present: requireBool(o, "migration_guard_present"),
|
|
1889
1950
|
};
|
|
1890
1951
|
}
|
|
1891
1952
|
/**
|
package/build/conductor/cli.js
CHANGED
|
@@ -58,6 +58,10 @@ export function getConductorUsage() {
|
|
|
58
58
|
" send-message Enqueue ONE typed supervisor->worker relay message (idempotent)",
|
|
59
59
|
" check-messages Read + ACK pending relay messages for a worker (no redelivery)",
|
|
60
60
|
" doctor Read-only health/diagnostics report (ledger + git hooks)",
|
|
61
|
+
" readiness Read-only ADVISORY report of every conductor prerequisite",
|
|
62
|
+
" (install + conductor doctor + plane preflight + server),",
|
|
63
|
+
" each with pass/warn/fail/skip and one named remediation.",
|
|
64
|
+
" Remediations are NOT run automatically; always exits 0.",
|
|
61
65
|
" purge Delete ALL ledger rows (events, messages, supervisor_projection)",
|
|
62
66
|
" install-git-hooks Install local, opportunistic, non-blocking git hooks",
|
|
63
67
|
" git-hook post-commit Run the post-commit producer (invoked by the installed hook)",
|
|
@@ -126,6 +130,15 @@ export function getConductorUsage() {
|
|
|
126
130
|
" --json Print compact JSON result",
|
|
127
131
|
" Note: returned messages are ACKNOWLEDGED by this call and are not redelivered.",
|
|
128
132
|
"",
|
|
133
|
+
"readiness options:",
|
|
134
|
+
" --json Print the versioned structured report as JSON",
|
|
135
|
+
" --no-deny-probe Accepted for parity with `doctor`; inert here — `readiness`",
|
|
136
|
+
" never spawns the deny-enforcement probe, and reports that",
|
|
137
|
+
" check as an explicit skip naming `conductor doctor`",
|
|
138
|
+
" --help Print the readiness usage message",
|
|
139
|
+
" Note: advisory only. It blocks nothing and changes no exit code; `drive-epic`",
|
|
140
|
+
" remains the sole route-selection gate and server-side admission the sole refusal.",
|
|
141
|
+
"",
|
|
129
142
|
"doctor / purge options:",
|
|
130
143
|
" --json Print machine-readable JSON",
|
|
131
144
|
" --no-deny-probe (doctor only) Skip the deny-enforcement preflight — no headless",
|
|
@@ -216,6 +229,9 @@ const VALID_COMMANDS = new Set([
|
|
|
216
229
|
"send-message",
|
|
217
230
|
"check-messages",
|
|
218
231
|
"doctor",
|
|
232
|
+
// BAPI-1055: the consolidated ADVISORY readiness gate. Read-only, always
|
|
233
|
+
// exits 0, and registers no MCP tool — see `readiness-cli.ts`.
|
|
234
|
+
"readiness",
|
|
219
235
|
"purge",
|
|
220
236
|
"install-git-hooks",
|
|
221
237
|
"git-hook",
|
|
@@ -1183,6 +1199,13 @@ export async function runConductorCli(argv) {
|
|
|
1183
1199
|
return await runCheckMessagesCommand(parsed.argv);
|
|
1184
1200
|
case "doctor":
|
|
1185
1201
|
return await runDoctorCommand(parsed.argv);
|
|
1202
|
+
case "readiness": {
|
|
1203
|
+
// Lazily imported, like the recovery verbs: the readiness gate pulls in
|
|
1204
|
+
// the Bridge HTTP client and the plane preflight graph, and a local-only
|
|
1205
|
+
// command (doctor, emit-event) must not pay for that at load time.
|
|
1206
|
+
const { runConductorReadinessCommand } = await import("./readiness-cli.js");
|
|
1207
|
+
return await runConductorReadinessCommand(parsed.argv);
|
|
1208
|
+
}
|
|
1186
1209
|
case "purge":
|
|
1187
1210
|
return await runPurgeCommand(parsed.argv);
|
|
1188
1211
|
case "install-git-hooks":
|
|
@@ -16,6 +16,55 @@ import { inspectConductorGitHooks } from "./git-hooks.js";
|
|
|
16
16
|
import { resolveMcpShimInvocationForRuntime, resolvePackageRootFromModuleUrl, } from "../mcp-server-invocation.js";
|
|
17
17
|
import { MCP_PACKAGE_NAME } from "../mcp-identity.js";
|
|
18
18
|
import { resolveProfiles } from "../mcp-profile.js";
|
|
19
|
+
import { createReadinessCheck, createReadinessCheckSafely, } from "../readiness-check.js";
|
|
20
|
+
/**
|
|
21
|
+
* Remediation for a conductor context whose resolved MCP groups omit `conductor`.
|
|
22
|
+
*
|
|
23
|
+
* Fixed prose: it never interpolates the raw `BRIDGE_MCP_PROFILE` value. The
|
|
24
|
+
* legacy warning does interpolate it (unchanged), but a consolidated report is a
|
|
25
|
+
* wider surface and an operator-set environment variable is not a value this
|
|
26
|
+
* gate needs to echo to name the fix. ADD, never replace — replacing the value
|
|
27
|
+
* would drop the worker's other groups.
|
|
28
|
+
*/
|
|
29
|
+
export const MCP_PROFILE_REMEDIATION = "ADD `conductor` to BRIDGE_MCP_PROFILE (comma-separated) — do NOT replace the existing value, " +
|
|
30
|
+
"which would drop the worker's other tool groups. Without it the conductor/event/supervisor " +
|
|
31
|
+
"tools are not registered.";
|
|
32
|
+
/**
|
|
33
|
+
* Remediation for a host with no `gh` on PATH.
|
|
34
|
+
*
|
|
35
|
+
* LOAD-BEARING, not legacy (AC-7). Conductor merges run under the machine's own
|
|
36
|
+
* `gh` auth because the GitHub App token is `contents: read` and structurally
|
|
37
|
+
* cannot merge — see `mcp_server/src/executor/merge-job.ts`. Retiring this check
|
|
38
|
+
* is blocked on epic A2 (App elevation to `contents: write`); until A2 ships,
|
|
39
|
+
* `gh` is a real prerequisite and this stays a named check.
|
|
40
|
+
*/
|
|
41
|
+
export const GH_CLI_REMEDIATION = "install the GitHub CLI (`gh`) and put it on PATH; conductor-driven merges run under this " +
|
|
42
|
+
"host's own `gh` auth because the GitHub App token is `contents: read` and cannot merge.";
|
|
43
|
+
/** Remediation for an installed but unauthenticated `gh` (AC-7; see {@link GH_CLI_REMEDIATION}). */
|
|
44
|
+
export const GH_AUTH_REMEDIATION = "run `gh auth login` to grant the conductor merge permission; otherwise local merge emits " +
|
|
45
|
+
"merge.failed/skip rather than merging.";
|
|
46
|
+
/** Detail used when `gh` authentication was never tested because `gh` is absent. */
|
|
47
|
+
export const GH_AUTH_SKIPPED_DETAIL = "not tested — `gh` is not installed, so `gh auth status` was never run";
|
|
48
|
+
/**
|
|
49
|
+
* Remediation for a native ledger whose binding does not load.
|
|
50
|
+
*
|
|
51
|
+
* DIAGNOSTIC ONLY (BAPI-526): this never installs, rebuilds, or resolves a
|
|
52
|
+
* package path, and the remediation names the operator action rather than
|
|
53
|
+
* performing it. Retiring this check is blocked on epic B3 (parallel-ledger
|
|
54
|
+
* paring), so it stays a named check until B3 ships (AC-7).
|
|
55
|
+
*/
|
|
56
|
+
export const NATIVE_LEDGER_REMEDIATION = "`better-sqlite3` is an optionalDependency npm silently skips on a build failure — reinstall it " +
|
|
57
|
+
"for this Node runtime to restore local conductor observability. Diagnostic only: nothing is " +
|
|
58
|
+
"installed or rebuilt for you.";
|
|
59
|
+
/** Remediation for a worker MCP registration that fell back to the npm channel. */
|
|
60
|
+
export const MCP_REGISTRATION_FORM_REMEDIATION = "build this package so an on-disk entry resolves (`npm run build` in mcp_server), or accept that " +
|
|
61
|
+
"worker MCP registration depends on the published package being reachable.";
|
|
62
|
+
/** Remediation for an unreadable `engines.node`. */
|
|
63
|
+
export const NODE_ENGINE_REMEDIATION = "reinstall the package so its package.json is readable; until then the expected Node version is " +
|
|
64
|
+
"unknown and a Node/ABI skew cannot be diagnosed.";
|
|
65
|
+
/** Remediation for a deny-enforcement result the readiness gate could not establish. */
|
|
66
|
+
export const DENY_ENFORCEMENT_SKIP_REMEDIATION = "run `conductor doctor` (without --no-deny-probe) to execute the owning deny-enforcement " +
|
|
67
|
+
"preflight; `conductor readiness` never spawns that probe itself.";
|
|
19
68
|
/** Explicit display value for an unset `BRIDGE_MCP_PROFILE`. */
|
|
20
69
|
export const MCP_PROFILE_UNSET = "(unset)";
|
|
21
70
|
/**
|
|
@@ -105,6 +154,58 @@ export async function inspectNativeLedger(nativeLoad, deps = {}) {
|
|
|
105
154
|
warnings.push("Could not read package.json engines.node; expected Node version is unknown.");
|
|
106
155
|
}
|
|
107
156
|
const degraded = nativeLoad.degraded || invocation.form !== "absolute-build-path" || engines_node === null;
|
|
157
|
+
// THREE outcomes over the SAME inputs (BAPI-1055), because the aggregate
|
|
158
|
+
// `degraded` above folds three independent facts together and each has its own
|
|
159
|
+
// fix. `degraded` itself is unchanged — this adds structure beside it.
|
|
160
|
+
//
|
|
161
|
+
// PATH-FREE by construction: no branch interpolates `mcp_shim_node`, a server
|
|
162
|
+
// entry path, a package root, or a native error message. Only the module name,
|
|
163
|
+
// the ABI, the sanitized failure kind, and the closed registration form.
|
|
164
|
+
const outcomes = [
|
|
165
|
+
binding_loads
|
|
166
|
+
? {
|
|
167
|
+
id: "native-ledger",
|
|
168
|
+
label: "Native ledger binding",
|
|
169
|
+
status: "pass",
|
|
170
|
+
detail: `${nativeLoad.module} loads (NODE_MODULE_VERSION ${nativeLoad.node_modules_abi})`,
|
|
171
|
+
}
|
|
172
|
+
: {
|
|
173
|
+
id: "native-ledger",
|
|
174
|
+
label: "Native ledger binding",
|
|
175
|
+
status: "warn",
|
|
176
|
+
detail: `${nativeLoad.module} did not load ` +
|
|
177
|
+
`(NODE_MODULE_VERSION ${nativeLoad.node_modules_abi}, failure: ${failure_kind})`,
|
|
178
|
+
remediation: NATIVE_LEDGER_REMEDIATION,
|
|
179
|
+
},
|
|
180
|
+
invocation.form === "absolute-build-path"
|
|
181
|
+
? {
|
|
182
|
+
id: "mcp-registration-form",
|
|
183
|
+
label: "Worker MCP registration form",
|
|
184
|
+
status: "pass",
|
|
185
|
+
detail: "resolves an on-disk build entry",
|
|
186
|
+
}
|
|
187
|
+
: {
|
|
188
|
+
id: "mcp-registration-form",
|
|
189
|
+
label: "Worker MCP registration form",
|
|
190
|
+
status: "warn",
|
|
191
|
+
detail: `registration form is \`${invocation.form}\` — the npm-channel fallback`,
|
|
192
|
+
remediation: MCP_REGISTRATION_FORM_REMEDIATION,
|
|
193
|
+
},
|
|
194
|
+
engines_node === null
|
|
195
|
+
? {
|
|
196
|
+
id: "node-engine",
|
|
197
|
+
label: "Expected Node engine",
|
|
198
|
+
status: "warn",
|
|
199
|
+
detail: "package.json engines.node could not be read",
|
|
200
|
+
remediation: NODE_ENGINE_REMEDIATION,
|
|
201
|
+
}
|
|
202
|
+
: {
|
|
203
|
+
id: "node-engine",
|
|
204
|
+
label: "Expected Node engine",
|
|
205
|
+
status: "pass",
|
|
206
|
+
detail: `expected ${engines_node}; running ${process.version}`,
|
|
207
|
+
},
|
|
208
|
+
];
|
|
108
209
|
return {
|
|
109
210
|
cli_node_version: process.version,
|
|
110
211
|
engines_node,
|
|
@@ -116,6 +217,7 @@ export async function inspectNativeLedger(nativeLoad, deps = {}) {
|
|
|
116
217
|
failure_kind,
|
|
117
218
|
degraded,
|
|
118
219
|
warnings,
|
|
220
|
+
outcomes,
|
|
119
221
|
};
|
|
120
222
|
}
|
|
121
223
|
/**
|
|
@@ -175,6 +277,31 @@ export async function collectConductorNativeLedgerSafe(deps = {}) {
|
|
|
175
277
|
failure_kind: NATIVE_LEDGER_UNCOLLECTED_FAILURE_KIND,
|
|
176
278
|
degraded: true,
|
|
177
279
|
warnings: ["the native ledger inspection could not be collected on this host"],
|
|
280
|
+
// The collection failure is its own finding, not a load failure: an
|
|
281
|
+
// operator who cannot collect the inspection needs to retry it, not
|
|
282
|
+
// reinstall the binding. Reported at the same granularity as the real
|
|
283
|
+
// inspector so a consolidated report never loses these three checks.
|
|
284
|
+
outcomes: [
|
|
285
|
+
{
|
|
286
|
+
id: "native-ledger",
|
|
287
|
+
label: "Native ledger binding",
|
|
288
|
+
status: "warn",
|
|
289
|
+
detail: "the native ledger inspection could not be collected on this host",
|
|
290
|
+
remediation: NATIVE_LEDGER_REMEDIATION,
|
|
291
|
+
},
|
|
292
|
+
{
|
|
293
|
+
id: "mcp-registration-form",
|
|
294
|
+
label: "Worker MCP registration form",
|
|
295
|
+
status: "skip",
|
|
296
|
+
detail: "not determined — the native ledger inspection could not be collected",
|
|
297
|
+
},
|
|
298
|
+
{
|
|
299
|
+
id: "node-engine",
|
|
300
|
+
label: "Expected Node engine",
|
|
301
|
+
status: "skip",
|
|
302
|
+
detail: "not determined — the native ledger inspection could not be collected",
|
|
303
|
+
},
|
|
304
|
+
],
|
|
178
305
|
};
|
|
179
306
|
}
|
|
180
307
|
}
|
|
@@ -206,7 +333,28 @@ export function inspectMcpProfile(env) {
|
|
|
206
333
|
`do NOT replace the value, which would drop the worker's other groups. ` +
|
|
207
334
|
`Without it the conductor/event/supervisor tools are not registered.`);
|
|
208
335
|
}
|
|
209
|
-
|
|
336
|
+
// Fixed state labels and the CLOSED active-group vocabulary only (BAPI-1055):
|
|
337
|
+
// `resolveProfiles` yields nothing but recognized group names, so this detail
|
|
338
|
+
// cannot echo an arbitrary operator-set string the way `raw_profile` can.
|
|
339
|
+
const outcomes = [
|
|
340
|
+
degraded
|
|
341
|
+
? {
|
|
342
|
+
id: "mcp-profile",
|
|
343
|
+
label: "MCP conductor profile",
|
|
344
|
+
status: "warn",
|
|
345
|
+
detail: `a conductor context is active but the resolved groups omit \`conductor\` (active: ${active_groups.join(", ")})`,
|
|
346
|
+
remediation: MCP_PROFILE_REMEDIATION,
|
|
347
|
+
}
|
|
348
|
+
: {
|
|
349
|
+
id: "mcp-profile",
|
|
350
|
+
label: "MCP conductor profile",
|
|
351
|
+
status: "pass",
|
|
352
|
+
detail: conductor_context_detected
|
|
353
|
+
? `conductor context active; resolved groups: ${active_groups.join(", ")}`
|
|
354
|
+
: `no conductor context detected; resolved groups: ${active_groups.join(", ")}`,
|
|
355
|
+
},
|
|
356
|
+
];
|
|
357
|
+
return { raw_profile, active_groups, conductor_context_detected, degraded, warnings, outcomes };
|
|
210
358
|
}
|
|
211
359
|
/**
|
|
212
360
|
* Read-only probe of the local-merge (F4) capability. Runs `gh --version` and
|
|
@@ -265,7 +413,60 @@ export function inspectLocalMerge(runCommand) {
|
|
|
265
413
|
// Capability gap only — local merge is opt-in, so an unauthed host is not a hard
|
|
266
414
|
// failure unless the operator enabled it. Reported as degraded for visibility.
|
|
267
415
|
const degraded = !gh_available || !gh_authed;
|
|
268
|
-
|
|
416
|
+
// TWO outcomes, not one (BAPI-1055): installing `gh` and running `gh auth
|
|
417
|
+
// login` are independently actionable, and `degraded` below is deliberately
|
|
418
|
+
// left as the SAME `!gh_available || !gh_authed` conjunction it has always
|
|
419
|
+
// been. When `gh` is absent the auth outcome is `skip`, never `fail` — the
|
|
420
|
+
// authentication was never tested, and claiming it failed would send the
|
|
421
|
+
// operator to `gh auth login` on a host with no `gh`.
|
|
422
|
+
//
|
|
423
|
+
// AC-7 — these two checks are LOAD-BEARING and must not be retired here.
|
|
424
|
+
// Conductor merges run under this machine's own `gh` auth because the GitHub
|
|
425
|
+
// App token is `contents: read` and structurally cannot merge; see
|
|
426
|
+
// `mcp_server/src/executor/merge-job.ts` and the `contents: read` constraint
|
|
427
|
+
// documented in `mcp_server/src/merge-pull-request.ts`. Retirement is owned by
|
|
428
|
+
// epic A2, after the App is elevated to `contents: write`. Nothing in this
|
|
429
|
+
// change removes, hides, or downgrades either check, and no merge behaviour is
|
|
430
|
+
// altered by reporting them.
|
|
431
|
+
const outcomes = [
|
|
432
|
+
gh_available
|
|
433
|
+
? {
|
|
434
|
+
id: "gh-cli",
|
|
435
|
+
label: "GitHub CLI availability",
|
|
436
|
+
status: "pass",
|
|
437
|
+
detail: "`gh --version` succeeded",
|
|
438
|
+
}
|
|
439
|
+
: {
|
|
440
|
+
id: "gh-cli",
|
|
441
|
+
label: "GitHub CLI availability",
|
|
442
|
+
status: "warn",
|
|
443
|
+
detail: "`gh` is not installed or not on PATH",
|
|
444
|
+
remediation: GH_CLI_REMEDIATION,
|
|
445
|
+
},
|
|
446
|
+
!gh_available
|
|
447
|
+
? {
|
|
448
|
+
id: "gh-auth",
|
|
449
|
+
label: "GitHub CLI authentication",
|
|
450
|
+
status: "skip",
|
|
451
|
+
detail: GH_AUTH_SKIPPED_DETAIL,
|
|
452
|
+
remediation: GH_CLI_REMEDIATION,
|
|
453
|
+
}
|
|
454
|
+
: gh_authed
|
|
455
|
+
? {
|
|
456
|
+
id: "gh-auth",
|
|
457
|
+
label: "GitHub CLI authentication",
|
|
458
|
+
status: "pass",
|
|
459
|
+
detail: "`gh auth status` reports an authenticated account",
|
|
460
|
+
}
|
|
461
|
+
: {
|
|
462
|
+
id: "gh-auth",
|
|
463
|
+
label: "GitHub CLI authentication",
|
|
464
|
+
status: "warn",
|
|
465
|
+
detail: "`gh` is installed but `gh auth status` failed",
|
|
466
|
+
remediation: GH_AUTH_REMEDIATION,
|
|
467
|
+
},
|
|
468
|
+
];
|
|
469
|
+
return { gh_available, gh_authed, degraded, warnings, outcomes };
|
|
269
470
|
}
|
|
270
471
|
/**
|
|
271
472
|
* Build the combined read-only doctor report. Composes the existing ledger
|
|
@@ -312,14 +513,18 @@ async function inspectDenyEnforcementSafe(inspect) {
|
|
|
312
513
|
}
|
|
313
514
|
return await run();
|
|
314
515
|
}
|
|
315
|
-
catch
|
|
316
|
-
|
|
516
|
+
catch {
|
|
517
|
+
// BAPI-1055: the thrown message is NOT interpolated. A spawn failure here
|
|
518
|
+
// routinely carries an absolute executable path or a temp-directory path,
|
|
519
|
+
// and this warning now flows into the consolidated readiness report as well
|
|
520
|
+
// as the doctor's own output. The actionable fact — enforcement could not be
|
|
521
|
+
// established, so claiming must refuse — is fixed prose.
|
|
317
522
|
return {
|
|
318
523
|
enforced: false,
|
|
319
524
|
layer: "none",
|
|
320
525
|
degraded: true,
|
|
321
526
|
warnings: [
|
|
322
|
-
|
|
527
|
+
"Deny-layer enforcement inspection failed: the probe could not be run.",
|
|
323
528
|
"The executor claim loop MUST refuse to claim jobs until the deny layer is enforced.",
|
|
324
529
|
],
|
|
325
530
|
};
|
|
@@ -456,3 +661,221 @@ export function formatConductorDoctorReport(report) {
|
|
|
456
661
|
lines.push(formatClaudeLoginAdvisory(claude_login));
|
|
457
662
|
return lines.join("\n");
|
|
458
663
|
}
|
|
664
|
+
// ---------------------------------------------------------------------------
|
|
665
|
+
// Canonical readiness projection (BAPI-1055)
|
|
666
|
+
// ---------------------------------------------------------------------------
|
|
667
|
+
/**
|
|
668
|
+
* The complete conductor-doctor prerequisite set, in render order.
|
|
669
|
+
*
|
|
670
|
+
* Like the install-side descriptor set, this is STABLE rather than derived from
|
|
671
|
+
* whatever the report happened to contain: a report that failed to build, or an
|
|
672
|
+
* injected sentinel carrying only the fields a test cares about, must still
|
|
673
|
+
* yield every prerequisite — as an explicit unknown, never as an absence.
|
|
674
|
+
*/
|
|
675
|
+
export const CONDUCTOR_READINESS_DESCRIPTORS = [
|
|
676
|
+
{ id: "ledger", label: "Conductor event ledger" },
|
|
677
|
+
{ id: "git-hooks", label: "Conductor git hooks" },
|
|
678
|
+
{ id: "mcp-profile", label: "MCP conductor profile" },
|
|
679
|
+
{ id: "gh-cli", label: "GitHub CLI availability" },
|
|
680
|
+
{ id: "gh-auth", label: "GitHub CLI authentication" },
|
|
681
|
+
{ id: "native-ledger", label: "Native ledger binding" },
|
|
682
|
+
{ id: "mcp-registration-form", label: "Worker MCP registration form" },
|
|
683
|
+
{ id: "node-engine", label: "Expected Node engine" },
|
|
684
|
+
{ id: "deny-enforcement", label: "Deny-layer enforcement" },
|
|
685
|
+
{ id: "claude-login", label: "Claude login" },
|
|
686
|
+
];
|
|
687
|
+
/** Remediation for a degraded local event ledger. */
|
|
688
|
+
export const CONDUCTOR_LEDGER_REMEDIATION = "run `conductor doctor` for the ledger detail, then re-create the local ledger directory with " +
|
|
689
|
+
"owner-only permissions; the ledger is local observability and never blocks a run.";
|
|
690
|
+
/** Remediation for degraded or missing managed git hooks. */
|
|
691
|
+
export const CONDUCTOR_GIT_HOOKS_REMEDIATION = "run `conductor install-git-hooks` to install the managed post-commit and reference-transaction " +
|
|
692
|
+
"hooks; they are local, opportunistic, and bypassable.";
|
|
693
|
+
/** Remediation for a Claude login this host cannot confirm. */
|
|
694
|
+
export const CONDUCTOR_CLAUDE_LOGIN_REMEDIATION = "run `claude login` on the host that will run conductor workers; Bridge stores no Anthropic " +
|
|
695
|
+
"credential and can only advise here.";
|
|
696
|
+
/** Fixed detail for a prerequisite whose owning inspection was not collected. */
|
|
697
|
+
const CONDUCTOR_UNCOLLECTED_DETAIL = "not collected — the conductor doctor did not report this inspection";
|
|
698
|
+
/** Fixed remediation for a prerequisite whose owning inspection was not collected. */
|
|
699
|
+
const CONDUCTOR_UNCOLLECTED_REMEDIATION = "run `conductor doctor` directly to collect this inspection; its state is unknown, not healthy.";
|
|
700
|
+
/** Ordered outcomes an inspection contributed, or `[]` when it reported none. */
|
|
701
|
+
function outcomesOf(inspection) {
|
|
702
|
+
// Read defensively: an injected sentinel (and the installer's degraded-doctor
|
|
703
|
+
// fallback) may populate only the fields a test cares about, and an adapter
|
|
704
|
+
// that threw on a partially-built report would lose the whole locus.
|
|
705
|
+
return Array.isArray(inspection?.outcomes) ? inspection?.outcomes : [];
|
|
706
|
+
}
|
|
707
|
+
/** Derive the ledger outcome from the ledger doctor result's own `degraded` flag. */
|
|
708
|
+
function ledgerOutcome(report) {
|
|
709
|
+
const ledger = report?.ledger;
|
|
710
|
+
if (ledger === undefined)
|
|
711
|
+
return null;
|
|
712
|
+
// Fixed facts only: `DoctorResult.path` and `directory_path` are absolute and
|
|
713
|
+
// are deliberately never projected.
|
|
714
|
+
return ledger.degraded
|
|
715
|
+
? {
|
|
716
|
+
id: "ledger",
|
|
717
|
+
label: "Conductor event ledger",
|
|
718
|
+
status: "warn",
|
|
719
|
+
detail: `local ledger degraded (schema present: ${ledger.schema_present}, permissions ok: ${ledger.directory_permissions_ok && ledger.database_permissions_ok})`,
|
|
720
|
+
remediation: CONDUCTOR_LEDGER_REMEDIATION,
|
|
721
|
+
}
|
|
722
|
+
: {
|
|
723
|
+
id: "ledger",
|
|
724
|
+
label: "Conductor event ledger",
|
|
725
|
+
status: "pass",
|
|
726
|
+
detail: `local ledger healthy (schema version ${ledger.user_version ?? "unknown"})`,
|
|
727
|
+
};
|
|
728
|
+
}
|
|
729
|
+
/** Derive the git-hooks outcome from the hook inspection's own `degraded` flag. */
|
|
730
|
+
function gitHooksOutcome(report) {
|
|
731
|
+
const hooks = report?.git_hooks;
|
|
732
|
+
if (hooks === undefined)
|
|
733
|
+
return null;
|
|
734
|
+
if (!hooks.is_worktree) {
|
|
735
|
+
return {
|
|
736
|
+
id: "git-hooks",
|
|
737
|
+
label: "Conductor git hooks",
|
|
738
|
+
status: "skip",
|
|
739
|
+
detail: "not a git worktree, so no managed conductor hooks apply",
|
|
740
|
+
};
|
|
741
|
+
}
|
|
742
|
+
// Hook NAMES and booleans only — `hooks_dir` is absolute and never projected.
|
|
743
|
+
const rendered = hooks.hooks
|
|
744
|
+
.map((hook) => `${hook.name}=${hook.exists && hook.managed_block_present}`)
|
|
745
|
+
.join(", ");
|
|
746
|
+
return hooks.degraded
|
|
747
|
+
? {
|
|
748
|
+
id: "git-hooks",
|
|
749
|
+
label: "Conductor git hooks",
|
|
750
|
+
status: "warn",
|
|
751
|
+
detail: `managed hooks installed: ${rendered}`,
|
|
752
|
+
remediation: CONDUCTOR_GIT_HOOKS_REMEDIATION,
|
|
753
|
+
}
|
|
754
|
+
: {
|
|
755
|
+
id: "git-hooks",
|
|
756
|
+
label: "Conductor git hooks",
|
|
757
|
+
status: "pass",
|
|
758
|
+
detail: `managed hooks installed: ${rendered}`,
|
|
759
|
+
};
|
|
760
|
+
}
|
|
761
|
+
/**
|
|
762
|
+
* Derive the deny-enforcement outcome.
|
|
763
|
+
*
|
|
764
|
+
* `conductor readiness` NEVER runs the owning probe: `runDenyEnforcementPreflight`
|
|
765
|
+
* spawns real headless agent runs costing minutes, and an advisory report is not
|
|
766
|
+
* a reason to pay that. When the report carries an explicitly skipped result the
|
|
767
|
+
* outcome is `skip` with a remediation naming the command that DOES run it —
|
|
768
|
+
* never `pass`, which would claim enforcement that was never verified.
|
|
769
|
+
*/
|
|
770
|
+
function denyEnforcementOutcome(report) {
|
|
771
|
+
const deny = report?.deny_enforcement;
|
|
772
|
+
if (deny === undefined)
|
|
773
|
+
return null;
|
|
774
|
+
if (deny.layer === "none" && !deny.enforced && deny.status === "skip") {
|
|
775
|
+
return {
|
|
776
|
+
id: "deny-enforcement",
|
|
777
|
+
label: "Deny-layer enforcement",
|
|
778
|
+
status: "skip",
|
|
779
|
+
detail: "not probed — enforcement is UNVERIFIED, which is not the same as unenforced",
|
|
780
|
+
remediation: DENY_ENFORCEMENT_SKIP_REMEDIATION,
|
|
781
|
+
};
|
|
782
|
+
}
|
|
783
|
+
// Fixed categories only: the underlying `detail`/`warnings` can carry probe
|
|
784
|
+
// text, so neither is projected here.
|
|
785
|
+
return deny.enforced && !deny.degraded
|
|
786
|
+
? {
|
|
787
|
+
id: "deny-enforcement",
|
|
788
|
+
label: "Deny-layer enforcement",
|
|
789
|
+
status: "pass",
|
|
790
|
+
detail: `enforced by the ${deny.layer} layer`,
|
|
791
|
+
}
|
|
792
|
+
: {
|
|
793
|
+
id: "deny-enforcement",
|
|
794
|
+
label: "Deny-layer enforcement",
|
|
795
|
+
status: "fail",
|
|
796
|
+
detail: deny.enforced
|
|
797
|
+
? `enforced by the ${deny.layer} layer, but degraded`
|
|
798
|
+
: "the denied call was not rejected by any layer",
|
|
799
|
+
remediation: "restore the settings-deny layer before running a conductor worker; the executor claim " +
|
|
800
|
+
"loop refuses to claim jobs while enforcement is missing.",
|
|
801
|
+
};
|
|
802
|
+
}
|
|
803
|
+
/** Derive the Claude-login outcome from the advisory detection result. */
|
|
804
|
+
function claudeLoginOutcome(report) {
|
|
805
|
+
const login = report?.claude_login;
|
|
806
|
+
if (login === undefined)
|
|
807
|
+
return null;
|
|
808
|
+
return login.detected
|
|
809
|
+
? {
|
|
810
|
+
id: "claude-login",
|
|
811
|
+
label: "Claude login",
|
|
812
|
+
status: "pass",
|
|
813
|
+
detail: "a local Claude login marker was detected",
|
|
814
|
+
}
|
|
815
|
+
: {
|
|
816
|
+
id: "claude-login",
|
|
817
|
+
label: "Claude login",
|
|
818
|
+
status: "warn",
|
|
819
|
+
detail: "no local Claude login marker was detected",
|
|
820
|
+
remediation: CONDUCTOR_CLAUDE_LOGIN_REMEDIATION,
|
|
821
|
+
};
|
|
822
|
+
}
|
|
823
|
+
/**
|
|
824
|
+
* Project a conductor-doctor report into canonical readiness checks.
|
|
825
|
+
*
|
|
826
|
+
* PURE: no probe, no I/O, and no re-derivation of any verdict. Every outcome is
|
|
827
|
+
* read from the structured fields the owning inspections now populate, never
|
|
828
|
+
* parsed out of their free-text `warnings` — a warning is prose an author may
|
|
829
|
+
* reword, and a gate that read it would change meaning on a copy edit.
|
|
830
|
+
*
|
|
831
|
+
* A null report yields the full descriptor set as failures rather than nothing:
|
|
832
|
+
* "the conductor doctor could not be built" must not read as "there is nothing
|
|
833
|
+
* to report here".
|
|
834
|
+
*/
|
|
835
|
+
export function mapConductorDoctorReportToReadinessChecks(report) {
|
|
836
|
+
const collected = new Map();
|
|
837
|
+
const record = (outcome) => {
|
|
838
|
+
// First writer wins, so an inspection cannot overwrite another's finding.
|
|
839
|
+
if (outcome && !collected.has(outcome.id))
|
|
840
|
+
collected.set(outcome.id, outcome);
|
|
841
|
+
};
|
|
842
|
+
record(ledgerOutcome(report));
|
|
843
|
+
record(gitHooksOutcome(report));
|
|
844
|
+
for (const outcome of outcomesOf(report?.mcp_profile))
|
|
845
|
+
record(outcome);
|
|
846
|
+
for (const outcome of outcomesOf(report?.local_merge))
|
|
847
|
+
record(outcome);
|
|
848
|
+
for (const outcome of outcomesOf(report?.native_ledger))
|
|
849
|
+
record(outcome);
|
|
850
|
+
record(denyEnforcementOutcome(report));
|
|
851
|
+
record(claudeLoginOutcome(report));
|
|
852
|
+
return CONDUCTOR_READINESS_DESCRIPTORS.map(({ id, label }) => {
|
|
853
|
+
const outcome = collected.get(id);
|
|
854
|
+
if (!outcome) {
|
|
855
|
+
// Unknown, and unknown is never healthy: a prerequisite whose owning
|
|
856
|
+
// inspection did not report is a failure with a restoring remediation.
|
|
857
|
+
return createReadinessCheck({
|
|
858
|
+
id: `conductor.${id}`,
|
|
859
|
+
source: "conductor",
|
|
860
|
+
label,
|
|
861
|
+
status: "fail",
|
|
862
|
+
detail: CONDUCTOR_UNCOLLECTED_DETAIL,
|
|
863
|
+
remediation: CONDUCTOR_UNCOLLECTED_REMEDIATION,
|
|
864
|
+
});
|
|
865
|
+
}
|
|
866
|
+
return createReadinessCheckSafely({
|
|
867
|
+
id: `conductor.${id}`,
|
|
868
|
+
source: "conductor",
|
|
869
|
+
label: outcome.label || label,
|
|
870
|
+
status: outcome.status,
|
|
871
|
+
...(outcome.detail ? { detail: outcome.detail } : {}),
|
|
872
|
+
// Same fallback as the plane adapter: a real failure must never be
|
|
873
|
+
// downgraded into a generic "unreadable check" for want of a fix line.
|
|
874
|
+
...(outcome.status === "fail"
|
|
875
|
+
? { remediation: outcome.remediation ?? CONDUCTOR_UNCOLLECTED_REMEDIATION }
|
|
876
|
+
: outcome.status !== "pass" && outcome.remediation
|
|
877
|
+
? { remediation: outcome.remediation }
|
|
878
|
+
: {}),
|
|
879
|
+
});
|
|
880
|
+
});
|
|
881
|
+
}
|