@bridge_gpt/mcp-server 0.2.41 → 0.2.42
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 +10 -10
- package/build/agent-capabilities/cli.js +2 -1
- package/build/agent-launchers/claude-executor-adapter.js +17 -4
- package/build/claude-user-config-doctor.js +42 -11
- package/build/cli-release.js +2 -1
- package/build/commands.generated.js +4 -4
- package/build/conduct-epic/bridge-client.js +354 -113
- package/build/conduct-epic/checkpoint-store.js +17 -0
- package/build/conduct-epic/cli.js +752 -99
- package/build/conduct-epic/cut-protocol.js +327 -0
- package/build/conduct-epic/spawn.js +14 -2
- package/build/conductor/bridge-api-client.js +27 -1
- package/build/conductor/cli.js +46 -1
- package/build/conductor/doctor.js +101 -16
- package/build/conductor/epic-reconcile.js +72 -19
- package/build/conductor/epic-runtime.js +15 -3
- package/build/conductor/errors.js +47 -0
- package/build/conductor/git-hooks.js +205 -11
- package/build/conductor/install-doctor.js +230 -1
- package/build/conductor/local-merge.js +130 -28
- package/build/conductor/tools.js +32 -3
- package/build/conductor/worker-ledger-cli.js +27 -1
- package/build/conductor-bin.js +15 -15
- package/build/credentials-cli.js +3 -2
- package/build/doctor.js +107 -41
- package/build/executor/cli.js +48 -1
- package/build/executor/env.js +21 -0
- package/build/executor/index-scope.js +39 -0
- package/build/executor/job-log-registry.js +69 -0
- package/build/executor/job-runner.js +148 -26
- package/build/executor/live-worker-registry.js +83 -0
- package/build/executor/observation.js +167 -6
- package/build/executor/platform.js +147 -3
- package/build/executor/process.js +58 -14
- package/build/executor/runner.js +235 -48
- package/build/executor/test-clock.js +3 -2
- package/build/index-scope-contract.js +96 -0
- package/build/index.js +153 -204
- package/build/init.js +83 -22
- package/build/install-bridge-conductor.js +323 -14
- package/build/install-bridge.js +202 -38
- package/build/install-doctor.js +23 -9
- package/build/install-reexec.js +2 -1
- package/build/launcher-config-inspection.js +83 -22
- package/build/mcp-host-config.js +331 -67
- package/build/mcp-host-targets.js +45 -21
- package/build/mcp-identity.js +92 -0
- package/build/mcp-install-state.js +94 -1
- package/build/mcp-invoke.js +2 -1
- package/build/mcp-provisioning.js +45 -12
- package/build/mcp-registration-doctor.js +35 -13
- package/build/mcp-server-invocation.js +4 -2
- package/build/merge-pull-request.js +208 -9
- package/build/pipelines.generated.js +3 -3
- package/build/plane/defaults.js +4 -1
- package/build/plane/preflight.js +81 -10
- package/build/plane/test-fakes.js +9 -1
- package/build/readme.generated.js +1 -1
- package/build/regression-check.js +3 -2
- package/build/review-tickets.js +8 -7
- package/build/run-unit-tests-launcher.js +74 -1
- package/build/schedule-run.js +3 -2
- package/build/setup-epic.js +453 -78
- package/build/sfcc/tool-wrapper.js +15 -0
- package/build/start-tickets-prereqs.js +11 -6
- package/build/start-tickets.js +91 -85
- package/build/update-check.js +3 -2
- package/build/upgrade-advice.js +2 -1
- package/build/upgrade-cli.js +50 -18
- package/build/version.generated.js +1 -1
- package/docs/CONDUCTOR.md +22 -0
- package/docs/install/mcp-tool-integrations.md +19 -3
- package/package.json +2 -2
|
@@ -9,8 +9,8 @@
|
|
|
9
9
|
* unsafe to merge is left untouched with a warning. Missing hooks are a degraded
|
|
10
10
|
* OPTIONAL capability — never a fatal error.
|
|
11
11
|
*/
|
|
12
|
-
import { chmodSync, existsSync, mkdirSync, readFileSync, statSync, writeFileSync, } from "node:fs";
|
|
13
|
-
import { dirname, isAbsolute, resolve } from "node:path";
|
|
12
|
+
import { chmodSync, existsSync, mkdirSync, readFileSync, realpathSync, statSync, writeFileSync, } from "node:fs";
|
|
13
|
+
import { basename, dirname, isAbsolute, join, resolve } from "node:path";
|
|
14
14
|
import { fileURLToPath } from "node:url";
|
|
15
15
|
import { runGitCommand } from "./git-inspection.js";
|
|
16
16
|
/** Markers delimiting the conductor-managed block inside a hook file. */
|
|
@@ -36,10 +36,120 @@ export function resolveGitHooksDirectory(deps = {}) {
|
|
|
36
36
|
const absoluteCommonDir = isAbsolute(commonDir) ? commonDir : resolve(cwd, commonDir);
|
|
37
37
|
return { is_worktree: true, hooks_dir: resolve(absoluteCommonDir, "hooks") };
|
|
38
38
|
}
|
|
39
|
-
/**
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
39
|
+
/** The file name every managed hook body must invoke. */
|
|
40
|
+
export const CONDUCTOR_BIN_FILENAME = "conductor-bin.js";
|
|
41
|
+
/**
|
|
42
|
+
* The sanitized failure reason. Names the layout categories that were searched
|
|
43
|
+
* so the operator can act, and nothing else — no probed path, no caught error
|
|
44
|
+
* text, no credential-bearing string can reach it.
|
|
45
|
+
*/
|
|
46
|
+
export const CONDUCTOR_HOOK_BIN_UNRESOLVED_REASON = `no usable ${CONDUCTOR_BIN_FILENAME} was found for the executing artifact ` +
|
|
47
|
+
"(searched: the sibling bundle layout, the parent TypeScript-emit layout, and " +
|
|
48
|
+
`the invocation target when it is named ${CONDUCTOR_BIN_FILENAME}). ` +
|
|
49
|
+
"Reinstall or rebuild the package so a conductor binary ships beside the executing module.";
|
|
50
|
+
/**
|
|
51
|
+
* Absolute, canonical path of the `conductor-bin.js` belonging to the artifact
|
|
52
|
+
* that is currently executing (BAPI-772, the BAPI-768 pattern applied to hooks).
|
|
53
|
+
*
|
|
54
|
+
* ## Why this cannot be path arithmetic
|
|
55
|
+
*
|
|
56
|
+
* The old implementation hard-coded ONE layout — `dirname(import.meta.url)/../
|
|
57
|
+
* conductor-bin.js`, the tsc emit — and returned it unverified. In the shipped
|
|
58
|
+
* esbuild bundle this module IS `build/conductor-bin.js`, so the directory hop
|
|
59
|
+
* landed on `<pkg>/conductor-bin.js`, a path that has never existed. Every hook
|
|
60
|
+
* installed from the published package embedded that dead path, and because hook
|
|
61
|
+
* bodies discard output and end in `|| true`, each commit ENOENTed in silence
|
|
62
|
+
* while `conductor doctor` still called the hooks healthy.
|
|
63
|
+
*
|
|
64
|
+
* So this resolver PROBES. Candidates are drawn only from the executing module's
|
|
65
|
+
* own URL and Node's actual invocation target — never `process.cwd()` or an
|
|
66
|
+
* assumed package root, which could point a hook at a *different build* than the
|
|
67
|
+
* one the operator installed from. Every candidate is canonicalized through
|
|
68
|
+
* `realpath` (which also collapses `..` segments and rejects a dangling symlink)
|
|
69
|
+
* and must stat as a regular file before it can be returned.
|
|
70
|
+
*/
|
|
71
|
+
export function resolveConductorHookBin(overrides = {}) {
|
|
72
|
+
const deps = {
|
|
73
|
+
moduleUrl: overrides.moduleUrl ?? import.meta.url,
|
|
74
|
+
argv1: overrides.argv1 ?? process.argv[1],
|
|
75
|
+
realpath: overrides.realpath ?? defaultRealpath,
|
|
76
|
+
isFile: overrides.isFile ?? defaultIsFile,
|
|
77
|
+
};
|
|
78
|
+
const moduleFile = moduleFilePath(deps.moduleUrl);
|
|
79
|
+
const candidates = [];
|
|
80
|
+
if (moduleFile !== null) {
|
|
81
|
+
const dir = dirname(moduleFile);
|
|
82
|
+
// Bundled layout: the bin sits beside the executing module (and in the
|
|
83
|
+
// single-file bundle it IS the executing module).
|
|
84
|
+
candidates.push(join(dir, CONDUCTOR_BIN_FILENAME));
|
|
85
|
+
// tsc layout: `build/conductor/git-hooks.js` → `build/conductor-bin.js`.
|
|
86
|
+
candidates.push(join(dir, "..", CONDUCTOR_BIN_FILENAME));
|
|
87
|
+
}
|
|
88
|
+
// Last resort, and only ever a validated one: a packaged install launches
|
|
89
|
+
// through a `node_modules/.bin` symlink. Accepted only when the invocation
|
|
90
|
+
// target is exactly the conductor bin — `index.js` or `conductor-bin.js.bak`
|
|
91
|
+
// must never become a hook target.
|
|
92
|
+
if (typeof deps.argv1 === "string" && deps.argv1.length > 0) {
|
|
93
|
+
if (basename(deps.argv1) === CONDUCTOR_BIN_FILENAME)
|
|
94
|
+
candidates.push(deps.argv1);
|
|
95
|
+
}
|
|
96
|
+
const seen = new Set();
|
|
97
|
+
for (const candidate of candidates) {
|
|
98
|
+
// Canonicalize BEFORE probing so duplicates that differ only by symlink or
|
|
99
|
+
// `..` segment collapse, and a written hook can only ever embed a canonical
|
|
100
|
+
// path to an existing file.
|
|
101
|
+
//
|
|
102
|
+
// The whole loop is exception-contained: this resolver's contract is that it
|
|
103
|
+
// returns a discriminated result and NEVER throws, and that has to hold for
|
|
104
|
+
// an injected boundary too — otherwise a filesystem error (whose message can
|
|
105
|
+
// carry a home-dir path or credential) escapes as a raw exception and defeats
|
|
106
|
+
// the sanitized `reason` this function exists to produce.
|
|
107
|
+
let canonical;
|
|
108
|
+
try {
|
|
109
|
+
canonical = deps.realpath(candidate);
|
|
110
|
+
}
|
|
111
|
+
catch {
|
|
112
|
+
continue;
|
|
113
|
+
}
|
|
114
|
+
if (canonical === null)
|
|
115
|
+
continue;
|
|
116
|
+
if (seen.has(canonical))
|
|
117
|
+
continue;
|
|
118
|
+
seen.add(canonical);
|
|
119
|
+
try {
|
|
120
|
+
if (deps.isFile(canonical))
|
|
121
|
+
return { ok: true, path: canonical };
|
|
122
|
+
}
|
|
123
|
+
catch {
|
|
124
|
+
continue;
|
|
125
|
+
}
|
|
126
|
+
}
|
|
127
|
+
return { ok: false, reason: CONDUCTOR_HOOK_BIN_UNRESOLVED_REASON };
|
|
128
|
+
}
|
|
129
|
+
/** `file://` URL → filesystem path, or `null` when the URL is unusable. */
|
|
130
|
+
function moduleFilePath(moduleUrl) {
|
|
131
|
+
try {
|
|
132
|
+
return fileURLToPath(moduleUrl);
|
|
133
|
+
}
|
|
134
|
+
catch {
|
|
135
|
+
return null;
|
|
136
|
+
}
|
|
137
|
+
}
|
|
138
|
+
function defaultRealpath(filePath) {
|
|
139
|
+
try {
|
|
140
|
+
return realpathSync(filePath);
|
|
141
|
+
}
|
|
142
|
+
catch {
|
|
143
|
+
return null;
|
|
144
|
+
}
|
|
145
|
+
}
|
|
146
|
+
function defaultIsFile(filePath) {
|
|
147
|
+
try {
|
|
148
|
+
return statSync(filePath).isFile();
|
|
149
|
+
}
|
|
150
|
+
catch {
|
|
151
|
+
return false;
|
|
152
|
+
}
|
|
43
153
|
}
|
|
44
154
|
function wrapManagedBlock(body) {
|
|
45
155
|
return [BRIDGE_CONDUCTOR_HOOK_START, body, BRIDGE_CONDUCTOR_HOOK_END].join("\n");
|
|
@@ -107,27 +217,56 @@ export function mergeManagedHookSnippet(existing, managedBlock) {
|
|
|
107
217
|
const content = `${existing}${separator}${managedBlock}\n`;
|
|
108
218
|
return { ok: true, content, action: "appended" };
|
|
109
219
|
}
|
|
220
|
+
/** Stable discriminator for an install that never touched the filesystem. */
|
|
221
|
+
export const CONDUCTOR_BIN_UNRESOLVED_ERROR = "CONDUCTOR_BIN_UNRESOLVED";
|
|
110
222
|
function snippetForHook(name, conductorBin) {
|
|
111
223
|
return name === "post-commit"
|
|
112
224
|
? buildPostCommitHookSnippet(conductorBin)
|
|
113
225
|
: buildReferenceTransactionHookSnippet(conductorBin);
|
|
114
226
|
}
|
|
227
|
+
/**
|
|
228
|
+
* Re-validate a resolution before it can reach a hook body. A success produced
|
|
229
|
+
* by an injected {@link GitHooksDeps.resolveBin} seam goes through the same
|
|
230
|
+
* realpath + regular-file rules the production resolver applies, so the seam can
|
|
231
|
+
* substitute a fixture binary but can never bypass validation.
|
|
232
|
+
*/
|
|
233
|
+
function validateResolvedBin(resolution) {
|
|
234
|
+
if (!resolution.ok)
|
|
235
|
+
return resolution;
|
|
236
|
+
const canonical = defaultRealpath(resolution.path);
|
|
237
|
+
if (canonical === null || !defaultIsFile(canonical)) {
|
|
238
|
+
return { ok: false, reason: CONDUCTOR_HOOK_BIN_UNRESOLVED_REASON };
|
|
239
|
+
}
|
|
240
|
+
return { ok: true, path: canonical };
|
|
241
|
+
}
|
|
115
242
|
/**
|
|
116
243
|
* Install or update the managed `post-commit` and `reference-transaction` hooks.
|
|
117
244
|
* Creates the hooks directory when needed and marks hook files executable on
|
|
118
245
|
* POSIX. A non-worktree directory is reported as a degraded optional capability
|
|
119
246
|
* (warning), never a fatal error.
|
|
247
|
+
*
|
|
248
|
+
* BAPI-772: the conductor binary is resolved and validated BEFORE any filesystem
|
|
249
|
+
* mutation. When it cannot be resolved this returns the failure variant and
|
|
250
|
+
* touches nothing — a dead hook is now structurally unwritable rather than
|
|
251
|
+
* silently installed. The generated runtime bodies are unchanged (detached,
|
|
252
|
+
* output discarded, `|| true`): only installation-time resolution is fail-loud.
|
|
120
253
|
*/
|
|
121
254
|
export function installConductorGitHooks(deps = {}) {
|
|
122
255
|
const platform = deps.platform ?? process.platform;
|
|
123
|
-
const
|
|
256
|
+
const resolution = validateResolvedBin(deps.resolveBin ? deps.resolveBin() : resolveConductorHookBin());
|
|
257
|
+
if (!resolution.ok) {
|
|
258
|
+
return { ok: false, error: CONDUCTOR_BIN_UNRESOLVED_ERROR, reason: resolution.reason };
|
|
259
|
+
}
|
|
260
|
+
const conductorBin = resolution.path;
|
|
124
261
|
const { is_worktree, hooks_dir } = resolveGitHooksDirectory(deps);
|
|
125
262
|
if (!is_worktree || hooks_dir === null) {
|
|
126
263
|
return {
|
|
264
|
+
ok: true,
|
|
127
265
|
is_worktree: false,
|
|
128
266
|
hooks_dir: null,
|
|
129
267
|
installed: [],
|
|
130
268
|
warnings: ["not a git worktree; git hooks were not installed (degraded optional capability)"],
|
|
269
|
+
conductor_bin: conductorBin,
|
|
131
270
|
};
|
|
132
271
|
}
|
|
133
272
|
const warnings = [];
|
|
@@ -166,7 +305,35 @@ export function installConductorGitHooks(deps = {}) {
|
|
|
166
305
|
}
|
|
167
306
|
installed.push({ name, path: hookPath, action: merge.action });
|
|
168
307
|
}
|
|
169
|
-
return { is_worktree: true, hooks_dir, installed, warnings };
|
|
308
|
+
return { ok: true, is_worktree: true, hooks_dir, installed, warnings, conductor_bin: conductorBin };
|
|
309
|
+
}
|
|
310
|
+
/**
|
|
311
|
+
* Extract the JSON-quoted binary argument from the managed block's generated
|
|
312
|
+
* `node "<path>" git-hook ...` command (BAPI-772).
|
|
313
|
+
*
|
|
314
|
+
* Parsing is deliberately narrow rather than a general shell tokenization: only
|
|
315
|
+
* the exact command shape this module writes is recognized, and the quoted
|
|
316
|
+
* argument is read back with `JSON.parse` — the same encoding
|
|
317
|
+
* {@link buildPostCommitHookSnippet} used to write it, so a path containing
|
|
318
|
+
* spaces or quotes round-trips exactly. A hook that was hand-edited into an
|
|
319
|
+
* unrecognized shape yields `null` rather than a throw or a guess.
|
|
320
|
+
*/
|
|
321
|
+
export function extractManagedHookBinPath(content) {
|
|
322
|
+
const startIdx = content.indexOf(BRIDGE_CONDUCTOR_HOOK_START);
|
|
323
|
+
const endIdx = content.indexOf(BRIDGE_CONDUCTOR_HOOK_END);
|
|
324
|
+
if (startIdx === -1 || endIdx === -1 || endIdx <= startIdx)
|
|
325
|
+
return null;
|
|
326
|
+
const block = content.slice(startIdx, endIdx);
|
|
327
|
+
const match = /\bnode\s+("(?:[^"\\]|\\.)*")\s+git-hook\b/.exec(block);
|
|
328
|
+
if (match === null)
|
|
329
|
+
return null;
|
|
330
|
+
try {
|
|
331
|
+
const parsed = JSON.parse(match[1]);
|
|
332
|
+
return typeof parsed === "string" && parsed.length > 0 ? parsed : null;
|
|
333
|
+
}
|
|
334
|
+
catch {
|
|
335
|
+
return null;
|
|
336
|
+
}
|
|
170
337
|
}
|
|
171
338
|
/**
|
|
172
339
|
* Inspect the managed git hooks WITHOUT modifying anything. Reports worktree
|
|
@@ -193,6 +360,8 @@ export function inspectConductorGitHooks(deps = {}) {
|
|
|
193
360
|
const exists = existsSync(hookPath);
|
|
194
361
|
let executable = false;
|
|
195
362
|
let managedPresent = false;
|
|
363
|
+
let embeddedBinPath = null;
|
|
364
|
+
let embeddedBinIsFile = false;
|
|
196
365
|
if (exists) {
|
|
197
366
|
try {
|
|
198
367
|
const mode = statSync(hookPath).mode;
|
|
@@ -202,7 +371,24 @@ export function inspectConductorGitHooks(deps = {}) {
|
|
|
202
371
|
/* unreadable stat — leave executable false */
|
|
203
372
|
}
|
|
204
373
|
try {
|
|
205
|
-
|
|
374
|
+
const content = readFileSync(hookPath, "utf-8");
|
|
375
|
+
managedPresent = content.includes(BRIDGE_CONDUCTOR_HOOK_START);
|
|
376
|
+
if (managedPresent) {
|
|
377
|
+
// BAPI-772: a managed block is not enough — the block written by an
|
|
378
|
+
// older build can point at a binary that never existed. Stat the
|
|
379
|
+
// embedded target READ-ONLY so a dead hook is visible instead of
|
|
380
|
+
// reported healthy. Nothing here rewrites or canonicalizes the hook.
|
|
381
|
+
embeddedBinPath = extractManagedHookBinPath(content);
|
|
382
|
+
embeddedBinIsFile = embeddedBinPath !== null && defaultIsFile(embeddedBinPath);
|
|
383
|
+
if (embeddedBinPath === null) {
|
|
384
|
+
warnings.push(`${name} hook has a managed block with no recognizable conductor command; ` +
|
|
385
|
+
"reinstall with `conductor install-git-hooks`");
|
|
386
|
+
}
|
|
387
|
+
else if (!embeddedBinIsFile) {
|
|
388
|
+
warnings.push(`${name} hook points at a missing or invalid conductor binary (${embeddedBinPath}); ` +
|
|
389
|
+
"the hook is dead — reinstall with `conductor install-git-hooks`");
|
|
390
|
+
}
|
|
391
|
+
}
|
|
206
392
|
}
|
|
207
393
|
catch {
|
|
208
394
|
/* unreadable — leave managedPresent false */
|
|
@@ -211,8 +397,16 @@ export function inspectConductorGitHooks(deps = {}) {
|
|
|
211
397
|
else {
|
|
212
398
|
warnings.push(`${name} hook not installed (degraded optional capability)`);
|
|
213
399
|
}
|
|
214
|
-
hooks.push({
|
|
400
|
+
hooks.push({
|
|
401
|
+
name,
|
|
402
|
+
path: hookPath,
|
|
403
|
+
exists,
|
|
404
|
+
executable,
|
|
405
|
+
managed_block_present: managedPresent,
|
|
406
|
+
embedded_bin_path: embeddedBinPath,
|
|
407
|
+
embedded_bin_is_file: embeddedBinIsFile,
|
|
408
|
+
});
|
|
215
409
|
}
|
|
216
|
-
const degraded = hooks.some((h) => !h.exists || !h.managed_block_present);
|
|
410
|
+
const degraded = hooks.some((h) => !h.exists || !h.managed_block_present || !h.embedded_bin_is_file);
|
|
217
411
|
return { is_worktree: true, hooks_dir, hooks, warnings, degraded };
|
|
218
412
|
}
|
|
@@ -17,13 +17,25 @@
|
|
|
17
17
|
* additive: a change to any legacy builder cannot silently alter what the unified
|
|
18
18
|
* doctor reports about it.
|
|
19
19
|
*
|
|
20
|
+
* BAPI-775 adds three more named sections for the LOCAL conductor capabilities
|
|
21
|
+
* `install conductor` can now provision: host MCP profile-token presence,
|
|
22
|
+
* managed hook-target usability, and native ledger loadability. All three are
|
|
23
|
+
* DERIVED — from the shared read-only host-config inspector and from the legacy
|
|
24
|
+
* conductor report already composed above — so none of them re-probes anything
|
|
25
|
+
* or can drift from the report embedded beneath it. All three are non-fatal, so
|
|
26
|
+
* a missing local capability can never change
|
|
27
|
+
* {@link conductorInstallDoctorExitCode}.
|
|
28
|
+
*
|
|
20
29
|
* STRICTLY READ-ONLY. No POST, no filesystem write, no install-state write, no
|
|
21
30
|
* workflow scaffold, no schema migration, no hook installation. It is invoked
|
|
22
31
|
* from a write-capable installer, so that boundary is pinned by tests.
|
|
23
32
|
*/
|
|
24
33
|
import { readdir as fsReaddir, readFile as fsReadFile, stat as fsStat } from "node:fs/promises";
|
|
25
34
|
import os from "node:os";
|
|
26
|
-
import { buildConductorDoctorReport, formatConductorDoctorReport, } from "./doctor.js";
|
|
35
|
+
import { buildConductorDoctorReport, describeNativeLedgerAvailability, formatConductorDoctorReport, } from "./doctor.js";
|
|
36
|
+
import { MANAGED_HOOK_NAMES } from "./git-hooks.js";
|
|
37
|
+
import { inspectBridgeApiProfileToken, } from "../mcp-host-config.js";
|
|
38
|
+
import { MCP_PACKAGE_NAME } from "../mcp-identity.js";
|
|
27
39
|
import { collectExecutorServiceDiagnostics, formatExecutorServiceDiagnosticsReport, } from "../doctor.js";
|
|
28
40
|
import { collectInstallStatusChecks, formatInstallStatusReport, } from "../install-doctor.js";
|
|
29
41
|
import { ConductorBridgeApiError, fetchConductorReadiness, } from "./bridge-api-client.js";
|
|
@@ -39,6 +51,26 @@ export const CONDUCTOR_OPERATOR_RUNBOOK_POINTER = "docs/claude/epic-conductor-v2
|
|
|
39
51
|
*/
|
|
40
52
|
export const EXECUTOR_PROVISIONING_GUIDANCE = "`install conductor` writes the unit and starts it, each behind its own consent; " +
|
|
41
53
|
"readiness still comes only from a live server observation";
|
|
54
|
+
/**
|
|
55
|
+
* The `BRIDGE_MCP_PROFILE` token that makes the 8 conductor MCP tools visible.
|
|
56
|
+
*
|
|
57
|
+
* The tools are profile-gated, never core, and this is the only token that
|
|
58
|
+
* un-gates them — so the doctor names it explicitly rather than describing the
|
|
59
|
+
* gap abstractly (BAPI-775).
|
|
60
|
+
*/
|
|
61
|
+
export const CONDUCTOR_PROFILE_TOKEN = "conductor";
|
|
62
|
+
/** Remediation for a host MCP config that carries no Bridge entry at all. */
|
|
63
|
+
export const CONDUCTOR_PROFILE_TOKEN_NO_ENTRY_REMEDIATION = "no supported host MCP config carries a Bridge entry — run " +
|
|
64
|
+
`\`npx ${MCP_PACKAGE_NAME} install\` to register one, then re-run \`install conductor\`.`;
|
|
65
|
+
/** Remediation for an entry that exists but lacks the conductor token. */
|
|
66
|
+
export const CONDUCTOR_PROFILE_TOKEN_REMEDIATION = "re-run `install conductor` and accept the tool-visibility consent; the conductor " +
|
|
67
|
+
"tools become visible after the MCP client is restarted.";
|
|
68
|
+
/** Remediation surfaced whenever the managed conductor hooks are not usable. */
|
|
69
|
+
export const CONDUCTOR_HOOK_TARGET_REMEDIATION = "re-run `install conductor` and accept the local-observability consent, or run " +
|
|
70
|
+
"`conductor install-git-hooks` directly.";
|
|
71
|
+
/** Remediation surfaced when the ledger's native binding does not load. */
|
|
72
|
+
export const CONDUCTOR_LEDGER_LOADABILITY_REMEDIATION = "`better-sqlite3` is an optionalDependency npm silently skips on a build failure — " +
|
|
73
|
+
"reinstall it for this Node runtime to restore local conductor observability.";
|
|
42
74
|
/** Remediation surfaced whenever no healthy generated service unit is detected. */
|
|
43
75
|
export const EXECUTOR_INSTALL_SERVICE_REMEDIATION = "generate a persistent unit with `install conductor --executor-id <id>` or " +
|
|
44
76
|
"`executor install-service` (or run the executor by hand) — " +
|
|
@@ -382,6 +414,183 @@ function executorObservationSection(readiness) {
|
|
|
382
414
|
};
|
|
383
415
|
}
|
|
384
416
|
// ---------------------------------------------------------------------------
|
|
417
|
+
// Local conductor capability sections (BAPI-775)
|
|
418
|
+
//
|
|
419
|
+
// Three INDEPENDENT read-only checks — host profile token, managed hook targets,
|
|
420
|
+
// native ledger loadability. All three are non-fatal by construction: a missing
|
|
421
|
+
// local conductor capability is a repairable gap, and letting one flip
|
|
422
|
+
// `conductorInstallDoctorExitCode` to 1 would make every ordinary install fail
|
|
423
|
+
// on a capability it never asked for.
|
|
424
|
+
// ---------------------------------------------------------------------------
|
|
425
|
+
/**
|
|
426
|
+
* Whether this host looks like a conductor context.
|
|
427
|
+
*
|
|
428
|
+
* Composed from the two facts already collected: the resolved MCP-profile
|
|
429
|
+
* inspection (`BAPI_CONDUCTOR_ENABLED` or a registered epic-tick schedule) and
|
|
430
|
+
* the read-only host token inspection. Outside such a context a missing local
|
|
431
|
+
* capability is ADVISORY — reporting it as a gap would train operators of
|
|
432
|
+
* ordinary repositories to ignore this whole part of the report.
|
|
433
|
+
*/
|
|
434
|
+
function detectConductorContext(legacyConductor, profile) {
|
|
435
|
+
// Read defensively: an INJECTED report sentinel (and the installer's own
|
|
436
|
+
// degraded-doctor fallback) may populate only the fields a test cares about,
|
|
437
|
+
// and a doctor that throws on a partially-built report would abort the run it
|
|
438
|
+
// exists to diagnose. Same posture as the defensive legacy formatters below.
|
|
439
|
+
return (legacyConductor?.mcp_profile?.conductor_context_detected === true ||
|
|
440
|
+
profile?.tokenPresent === true);
|
|
441
|
+
}
|
|
442
|
+
/**
|
|
443
|
+
* Host MCP profile-token presence.
|
|
444
|
+
*
|
|
445
|
+
* Reports CONFIG IDENTIFIERS AND BOOLEANS ONLY — never an entry, an `env`
|
|
446
|
+
* object, a command line, an API key, or an unrelated profile token — and names
|
|
447
|
+
* only the three configs the merge path can write, so nothing here can imply
|
|
448
|
+
* that Codex or Copilot configuration was inspected or changed.
|
|
449
|
+
*/
|
|
450
|
+
function conductorProfileTokenSection(profile, conductorContext) {
|
|
451
|
+
const base = { id: "conductor-profile-token", label: "MCP conductor profile token" };
|
|
452
|
+
if (profile === null) {
|
|
453
|
+
return {
|
|
454
|
+
...base,
|
|
455
|
+
status: "degraded",
|
|
456
|
+
detail: "the host MCP profile token could not be inspected",
|
|
457
|
+
remediation: CONDUCTOR_PROFILE_TOKEN_REMEDIATION,
|
|
458
|
+
};
|
|
459
|
+
}
|
|
460
|
+
const carrying = profile.configs
|
|
461
|
+
.filter((c) => c.state === "token-present")
|
|
462
|
+
.map((c) => c.path);
|
|
463
|
+
const scope = "inspected .mcp.json, .vscode/mcp.json, .cursor/mcp.json only " +
|
|
464
|
+
"(Codex and Copilot host configuration is not inspected)";
|
|
465
|
+
if (carrying.length > 0) {
|
|
466
|
+
return {
|
|
467
|
+
...base,
|
|
468
|
+
status: "ok",
|
|
469
|
+
detail: `\`${profile.token}\` present in ${carrying.join(", ")}; ${scope}`,
|
|
470
|
+
};
|
|
471
|
+
}
|
|
472
|
+
// "No entry anywhere" and "an entry without the token" are different findings
|
|
473
|
+
// with different next steps, so they never collapse into one remediation.
|
|
474
|
+
const detail = profile.bridgeEntryPresent
|
|
475
|
+
? `no supported host config carries the \`${profile.token}\` token; ${scope}`
|
|
476
|
+
: `no supported host config carries a Bridge entry; ${scope}`;
|
|
477
|
+
const remediation = profile.bridgeEntryPresent
|
|
478
|
+
? CONDUCTOR_PROFILE_TOKEN_REMEDIATION
|
|
479
|
+
: CONDUCTOR_PROFILE_TOKEN_NO_ENTRY_REMEDIATION;
|
|
480
|
+
return conductorContext
|
|
481
|
+
? { ...base, status: "degraded", detail, remediation }
|
|
482
|
+
: { ...base, status: "ok", detail: `advisory — ${detail}`, remediation };
|
|
483
|
+
}
|
|
484
|
+
/**
|
|
485
|
+
* Managed conductor hook targets, DERIVED from `legacyConductor.git_hooks`.
|
|
486
|
+
*
|
|
487
|
+
* Derived, never re-probed: a second parse of the same hook files is a second
|
|
488
|
+
* chance to disagree with the report embedded right below this section. Healthy
|
|
489
|
+
* requires each managed hook to exist, carry the managed block, and point at an
|
|
490
|
+
* embedded binary that stats as a regular file — a hook whose target vanished is
|
|
491
|
+
* a dead hook, and reporting it as installed is the exact failure BAPI-772 made
|
|
492
|
+
* structurally impossible at INSTALL time but which can still arise later.
|
|
493
|
+
*
|
|
494
|
+
* Renders hook NAME and boolean state only. The embedded absolute binary path is
|
|
495
|
+
* read for the file check and never copied into this section.
|
|
496
|
+
*/
|
|
497
|
+
function conductorHookTargetsSection(legacyConductor, conductorContext) {
|
|
498
|
+
const base = { id: "conductor-hook-targets", label: "Conductor hook targets" };
|
|
499
|
+
// Nullish, not `=== null`: an injected builder may resolve `undefined`, and a
|
|
500
|
+
// doctor that throws on it would abort the run it exists to diagnose.
|
|
501
|
+
if (legacyConductor === null || legacyConductor === undefined) {
|
|
502
|
+
return {
|
|
503
|
+
...base,
|
|
504
|
+
status: "degraded",
|
|
505
|
+
detail: "hook targets could not be derived (the conductor doctor was not collected)",
|
|
506
|
+
remediation: CONDUCTOR_HOOK_TARGET_REMEDIATION,
|
|
507
|
+
};
|
|
508
|
+
}
|
|
509
|
+
// Defensive for the same reason as {@link detectConductorContext}: an injected
|
|
510
|
+
// sentinel need not carry every legacy field.
|
|
511
|
+
const hooks = legacyConductor.git_hooks;
|
|
512
|
+
if (hooks === undefined) {
|
|
513
|
+
return {
|
|
514
|
+
...base,
|
|
515
|
+
status: "degraded",
|
|
516
|
+
detail: "hook targets could not be derived (the conductor doctor reported no hook state)",
|
|
517
|
+
remediation: CONDUCTOR_HOOK_TARGET_REMEDIATION,
|
|
518
|
+
};
|
|
519
|
+
}
|
|
520
|
+
if (!hooks.is_worktree) {
|
|
521
|
+
return {
|
|
522
|
+
...base,
|
|
523
|
+
status: "ok",
|
|
524
|
+
detail: "advisory — not a git worktree, so no managed conductor hooks apply",
|
|
525
|
+
};
|
|
526
|
+
}
|
|
527
|
+
const usable = (name) => {
|
|
528
|
+
const hook = (hooks.hooks ?? []).find((h) => h.name === name);
|
|
529
|
+
return (hook !== undefined &&
|
|
530
|
+
hook.exists &&
|
|
531
|
+
hook.managed_block_present &&
|
|
532
|
+
hook.embedded_bin_is_file);
|
|
533
|
+
};
|
|
534
|
+
const rendered = MANAGED_HOOK_NAMES.map((name) => `${name}=${usable(name)}`).join(", ");
|
|
535
|
+
const allUsable = MANAGED_HOOK_NAMES.every((name) => usable(name));
|
|
536
|
+
if (allUsable) {
|
|
537
|
+
return { ...base, status: "ok", detail: `managed hooks usable: ${rendered}` };
|
|
538
|
+
}
|
|
539
|
+
const detail = `managed hooks usable: ${rendered}`;
|
|
540
|
+
return conductorContext
|
|
541
|
+
? { ...base, status: "degraded", detail, remediation: CONDUCTOR_HOOK_TARGET_REMEDIATION }
|
|
542
|
+
: {
|
|
543
|
+
...base,
|
|
544
|
+
status: "ok",
|
|
545
|
+
detail: `advisory — ${detail}`,
|
|
546
|
+
remediation: CONDUCTOR_HOOK_TARGET_REMEDIATION,
|
|
547
|
+
};
|
|
548
|
+
}
|
|
549
|
+
/**
|
|
550
|
+
* Native ledger loadability, DERIVED from `legacyConductor.native_ledger`.
|
|
551
|
+
*
|
|
552
|
+
* No parallel `better-sqlite3` check: the mapping from an inspection to
|
|
553
|
+
* available/degraded lives in one place ({@link describeNativeLedgerAvailability}),
|
|
554
|
+
* so this section and the conductor doctor can never disagree about whether the
|
|
555
|
+
* binding loads. A degraded result carries the module name, the ABI, and the
|
|
556
|
+
* sanitized failure kind, because the common cause is npm silently skipping an
|
|
557
|
+
* optionalDependency build and that is undiagnosable without the ABI.
|
|
558
|
+
*/
|
|
559
|
+
function conductorLedgerLoadabilitySection(legacyConductor) {
|
|
560
|
+
const base = {
|
|
561
|
+
id: "conductor-ledger-loadability",
|
|
562
|
+
label: "Conductor ledger loadability",
|
|
563
|
+
};
|
|
564
|
+
if (legacyConductor === null || legacyConductor === undefined) {
|
|
565
|
+
return {
|
|
566
|
+
...base,
|
|
567
|
+
status: "degraded",
|
|
568
|
+
detail: "ledger loadability could not be derived (the conductor doctor was not collected)",
|
|
569
|
+
remediation: CONDUCTOR_LEDGER_LOADABILITY_REMEDIATION,
|
|
570
|
+
};
|
|
571
|
+
}
|
|
572
|
+
const inspection = legacyConductor.native_ledger;
|
|
573
|
+
if (inspection === undefined) {
|
|
574
|
+
return {
|
|
575
|
+
...base,
|
|
576
|
+
status: "degraded",
|
|
577
|
+
detail: "ledger loadability could not be derived (the conductor doctor reported no native-ledger state)",
|
|
578
|
+
remediation: CONDUCTOR_LEDGER_LOADABILITY_REMEDIATION,
|
|
579
|
+
};
|
|
580
|
+
}
|
|
581
|
+
const availability = describeNativeLedgerAvailability(inspection);
|
|
582
|
+
if (availability.status === "available") {
|
|
583
|
+
return { ...base, status: "ok", detail: "ledger available — the native binding loads" };
|
|
584
|
+
}
|
|
585
|
+
return {
|
|
586
|
+
...base,
|
|
587
|
+
status: "degraded",
|
|
588
|
+
detail: `ledger degraded — ${availability.module} did not load ` +
|
|
589
|
+
`(NODE_MODULE_VERSION ${availability.nodeModulesAbi}, failure: ${availability.failureKind})`,
|
|
590
|
+
remediation: CONDUCTOR_LEDGER_LOADABILITY_REMEDIATION,
|
|
591
|
+
};
|
|
592
|
+
}
|
|
593
|
+
// ---------------------------------------------------------------------------
|
|
385
594
|
// Runner
|
|
386
595
|
// ---------------------------------------------------------------------------
|
|
387
596
|
async function inspectWorkflowPresence(readWorkflowFile) {
|
|
@@ -539,6 +748,26 @@ export async function runConductorInstallDoctor(deps) {
|
|
|
539
748
|
// --- Server observation: the authoritative executor fact, last so it reads
|
|
540
749
|
// as the conclusion of the three-part executor story.
|
|
541
750
|
sections.push(executorObservationSection(readiness));
|
|
751
|
+
// --- Local conductor capabilities (BAPI-775): three independent read-only
|
|
752
|
+
// checks, appended after the executor story so they read as their own group.
|
|
753
|
+
// Every one of them is non-fatal, so none can change the doctor's exit code.
|
|
754
|
+
const inspectProfile = deps.inspectProfileToken ??
|
|
755
|
+
((projectRoot, token) => inspectBridgeApiProfileToken(projectRoot, token, {
|
|
756
|
+
readFile: (filePath) => fsReadFile(filePath, "utf-8"),
|
|
757
|
+
}));
|
|
758
|
+
let profileToken = null;
|
|
759
|
+
try {
|
|
760
|
+
profileToken = await inspectProfile(deps.projectRoot ?? process.cwd(), CONDUCTOR_PROFILE_TOKEN);
|
|
761
|
+
}
|
|
762
|
+
catch {
|
|
763
|
+
// The shared inspector never throws for the ordinary absent/malformed
|
|
764
|
+
// cases; this guards an injected fake. A null inspection reports the
|
|
765
|
+
// collection gap rather than losing the section.
|
|
766
|
+
}
|
|
767
|
+
const conductorContext = detectConductorContext(legacyConductor, profileToken);
|
|
768
|
+
sections.push(conductorProfileTokenSection(profileToken, conductorContext));
|
|
769
|
+
sections.push(conductorHookTargetsSection(legacyConductor, conductorContext));
|
|
770
|
+
sections.push(conductorLedgerLoadabilitySection(legacyConductor));
|
|
542
771
|
return {
|
|
543
772
|
legacyConductor,
|
|
544
773
|
legacyExecutorPreflight,
|