@bridge_gpt/mcp-server 0.2.62 → 0.2.65
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 +47 -8
- package/build/agent-launchers/claude-executor-adapter.js +134 -3
- package/build/agent-launchers/executor-adapter.js +54 -2
- package/build/agents.generated.js +10 -1
- package/build/bounded-wait.js +32 -1
- package/build/commands.generated.js +6 -5
- package/build/conductor/bridge-api-client.js +335 -22
- package/build/conductor/bring-up-facts.js +3 -1
- package/build/conductor/doctor.js +23 -14
- package/build/conductor/epic-child-continuation.js +324 -0
- package/build/conductor/epic-implementer-cli.js +142 -17
- package/build/conductor/git-hooks.js +529 -118
- package/build/conductor/hook-runtime-qualification.js +340 -0
- package/build/conductor/readiness-sections.js +5 -4
- package/build/conductor/readiness.js +22 -11
- package/build/conductor/recovery-cli.js +354 -13
- package/build/conductor/recovery-operations.js +251 -5
- package/build/conductor/repair-contract.js +8 -0
- package/build/conductor-bundle-artifacts.js +1 -0
- package/build/credential-store.js +50 -12
- package/build/docs.generated.js +2 -1
- package/build/doctor.js +77 -11
- package/build/drive-epic-key.js +12 -0
- package/build/drive-epic.js +1211 -116
- package/build/epic-coherence-input-cli.js +137 -0
- package/build/epic-coherence-input.js +920 -0
- package/build/epic-coherence-input.test-fixtures.js +175 -0
- package/build/epic-implementer-bin.js +35 -29
- package/build/epic-implementer-bundle-cli.js +14 -1
- package/build/epic-launch-profile.js +14 -5
- package/build/epic-run-status.js +5 -0
- package/build/epic-shared/bridge-client.js +46 -4
- package/build/executor/cli.js +39 -0
- package/build/executor/completion-contract.js +423 -0
- package/build/executor/completion-supervision.js +286 -0
- package/build/executor/credentials.js +3 -1
- package/build/executor/deps.js +65 -1
- package/build/executor/env.js +6 -0
- package/build/executor/heartbeat.js +13 -3
- package/build/executor/http-client.js +76 -4
- package/build/executor/job-errors.js +48 -0
- package/build/executor/job-runner.js +1294 -26
- package/build/executor/job-types.js +10 -2
- package/build/executor/preflight.js +99 -0
- package/build/executor/process-containment.js +756 -0
- package/build/executor/process.js +235 -5
- package/build/executor/prompt-spec.js +262 -1
- package/build/executor/provider-checks.js +69 -0
- package/build/executor/rebase-finalization.js +351 -0
- package/build/executor/reconcile-overlap-job.js +77 -84
- package/build/executor/required-verification.js +313 -0
- package/build/executor/required-work-ledger.js +1121 -0
- package/build/executor/results.js +142 -0
- package/build/executor/resume-pre-spawn.js +87 -4
- package/build/executor/runner.js +145 -0
- package/build/executor/test-declaration.js +20 -0
- package/build/executor/types.js +12 -0
- package/build/executor/worker-command.js +49 -13
- package/build/executor/worker-finalization.js +88 -11
- package/build/executor/worker-guard-hook.js +29 -4
- package/build/executor/worktree.js +13 -0
- package/build/executor-launch.js +658 -0
- package/build/index.js +537 -943
- package/build/install-bridge-conductor.js +231 -41
- package/build/merge-pull-request.js +1 -1
- package/build/pipelines.generated.js +8 -7
- package/build/plan-discovery.js +137 -0
- package/build/plan-epic-implementer-eligibility.js +2 -2
- package/build/plane/cli.js +37 -3
- package/build/plane/member-logs.js +3 -0
- package/build/plane/member-roster.js +7 -1
- package/build/plane/shutdown.js +59 -1
- package/build/plane/supervisor.js +142 -0
- package/build/readme.generated.js +1 -1
- package/build/scripts/executor-protocol-contract-driver.js +13 -3
- package/build/setup-epic.js +28 -17
- package/build/version.generated.js +3 -3
- package/build/worker-guard-hook-bin.js +2 -2
- package/docs/CONDUCTOR.md +116 -2
- package/package.json +2 -1
|
@@ -0,0 +1,340 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Read-only qualification of the runtime a managed conductor Git hook launches
|
|
3
|
+
* (BAPI-1290).
|
|
4
|
+
*
|
|
5
|
+
* BAPI-772 made the hook installer refuse a binary that does not EXIST. That is
|
|
6
|
+
* not enough for a hook's lifetime: a hook embeds one absolute path and runs it on
|
|
7
|
+
* every commit for as long as the hook file lives. When that path sits inside the
|
|
8
|
+
* `npx` cache (`~/.npm/_npx/<hash>/…`), ordinary cache cleanup deletes it and every
|
|
9
|
+
* later commit silently delivers nothing — the hook body is detached, discards its
|
|
10
|
+
* output, and ends in `|| true` by design.
|
|
11
|
+
*
|
|
12
|
+
* This module answers one question for both installation and diagnostics, so the
|
|
13
|
+
* two can never disagree: "is this entry a stable, package-owned invocation that
|
|
14
|
+
* the hook's `node` can actually run, including the native ledger binding?" The
|
|
15
|
+
* answer is evidence-based:
|
|
16
|
+
*
|
|
17
|
+
* 1. the canonical entry is a regular `build/epic-implementer-bin.js`;
|
|
18
|
+
* 2. it is not inside a prunable `_npx` cache directory;
|
|
19
|
+
* 3. it lives in the supported persistent npm layout
|
|
20
|
+
* (`<prefix>/node_modules/@bridge_gpt/mcp-server/build/…`);
|
|
21
|
+
* 4. the adjacent `package.json` names this package, maps the
|
|
22
|
+
* `epic-implementer` bin to that entry, and carries a compatible version;
|
|
23
|
+
* 5. the platform is a qualified one (macOS or Linux);
|
|
24
|
+
* 6. a bounded subprocess — `node` from PATH, exactly what the hook runs —
|
|
25
|
+
* executes the entry's private runtime probe and reports a qualified Node
|
|
26
|
+
* line and a loadable `better-sqlite3`.
|
|
27
|
+
*
|
|
28
|
+
* Anything missing, malformed, or inconclusive is UNAVAILABLE, never healthy. The
|
|
29
|
+
* failure carries only a fixed classification and a fixed action sentence: no
|
|
30
|
+
* probed path, no subprocess stderr, no caught exception text.
|
|
31
|
+
*
|
|
32
|
+
* Deliberately NOT here: running npm, downloading or installing anything, opening
|
|
33
|
+
* or migrating the ledger, or starting a service. The one subprocess is the
|
|
34
|
+
* bounded runtime probe, which only imports the native binding.
|
|
35
|
+
*/
|
|
36
|
+
import { spawnSync } from "node:child_process";
|
|
37
|
+
import { readFileSync, realpathSync, statSync } from "node:fs";
|
|
38
|
+
import { basename, dirname, join } from "node:path";
|
|
39
|
+
import { MCP_PACKAGE_NAME } from "../mcp-identity.js";
|
|
40
|
+
import { isTransientNpxEntryPath } from "../mcp-server-invocation.js";
|
|
41
|
+
/** The bin name the package maps to the hook entry. */
|
|
42
|
+
export const HOOK_RUNTIME_BIN_NAME = "epic-implementer";
|
|
43
|
+
/** The entry file every managed hook invokes. */
|
|
44
|
+
export const HOOK_RUNTIME_ENTRY_FILENAME = "epic-implementer-bin.js";
|
|
45
|
+
/** The private CLI action the bounded runtime probe runs. Never advertised. */
|
|
46
|
+
export const HOOK_RUNTIME_PROBE_ACTION = "__hook-runtime-probe";
|
|
47
|
+
/** Marker the probe prints so a stray line on stdout can never be mistaken for evidence. */
|
|
48
|
+
export const HOOK_RUNTIME_PROBE_MARKER = "bapi-hook-runtime-probe/1";
|
|
49
|
+
/**
|
|
50
|
+
* Platforms whose hook shell grammar and persistent npm layout are qualified.
|
|
51
|
+
* Windows Git hooks run under a different shell/runtime and are unqualified.
|
|
52
|
+
*/
|
|
53
|
+
export const HOOK_RUNTIME_QUALIFIED_PLATFORMS = ["darwin", "linux"];
|
|
54
|
+
/**
|
|
55
|
+
* The lowest Node major line accepted for durable hooks. Only even (LTS) majors
|
|
56
|
+
* at or above it qualify; odd "current" lines are unqualified because they have a
|
|
57
|
+
* short support window and a native ABI that changes under the operator.
|
|
58
|
+
*/
|
|
59
|
+
export const HOOK_RUNTIME_MIN_NODE_MAJOR = 20;
|
|
60
|
+
/**
|
|
61
|
+
* Compatible package versions: the `0.x` line from `0.2.0` onward. Every such
|
|
62
|
+
* release ships the renamed `epic-implementer-bin.js` entry and the
|
|
63
|
+
* `git-hook post-commit` / `git-hook reference-transaction` grammar the managed
|
|
64
|
+
* hook invokes, so an in-place upgrade inside this range keeps the hook valid
|
|
65
|
+
* without rewriting it. A `1.x` release must be requalified before it is added.
|
|
66
|
+
*/
|
|
67
|
+
export const HOOK_RUNTIME_COMPATIBLE_RANGE = ">=0.2.0 <1.0.0";
|
|
68
|
+
/** Bounded wall-clock for the runtime probe subprocess. */
|
|
69
|
+
export const HOOK_RUNTIME_PROBE_TIMEOUT_MS = 15_000;
|
|
70
|
+
/** Upper bound on probe output read back; anything larger is malformed. */
|
|
71
|
+
const HOOK_RUNTIME_PROBE_MAX_BUFFER = 16 * 1024;
|
|
72
|
+
/** Every classification, for exhaustive rendering and tests. */
|
|
73
|
+
export const HOOK_RUNTIME_CLASSIFICATIONS = [
|
|
74
|
+
"ephemeral-cache",
|
|
75
|
+
"missing",
|
|
76
|
+
"legacy-name",
|
|
77
|
+
"unqualified-runtime",
|
|
78
|
+
"node-unavailable",
|
|
79
|
+
"native-unavailable",
|
|
80
|
+
"unknown",
|
|
81
|
+
];
|
|
82
|
+
/** Fixed, path-free operator action for each classification. */
|
|
83
|
+
export const HOOK_RUNTIME_ACTIONS = {
|
|
84
|
+
"ephemeral-cache": "the hook runtime is inside the npx cache, which cleanup can delete. Install the CLI " +
|
|
85
|
+
`persistently (\`npm i -g ${MCP_PACKAGE_NAME}\`) and re-run \`epic-implementer install-git-hooks\`.`,
|
|
86
|
+
missing: "the hook runtime entry is not present. Reinstall the CLI persistently " +
|
|
87
|
+
`(\`npm i -g ${MCP_PACKAGE_NAME}\`) and re-run \`epic-implementer install-git-hooks\`.`,
|
|
88
|
+
"legacy-name": "the hook invokes the retired conductor-bin.js entry. Re-run `epic-implementer install-git-hooks` " +
|
|
89
|
+
"from a persistent install.",
|
|
90
|
+
"unqualified-runtime": "the hook runtime is not a qualified persistent installation (supported: a persistent npm " +
|
|
91
|
+
`install of ${MCP_PACKAGE_NAME} ${HOOK_RUNTIME_COMPATIBLE_RANGE} on macOS or Linux with an even ` +
|
|
92
|
+
`Node LTS line >= ${HOOK_RUNTIME_MIN_NODE_MAJOR}). See mcp_server/docs/CONDUCTOR.md.`,
|
|
93
|
+
"node-unavailable": "`node` could not be launched from PATH, so the hook cannot run. Put a qualified Node on PATH " +
|
|
94
|
+
"and re-run `epic-implementer doctor`.",
|
|
95
|
+
"native-unavailable": "better-sqlite3 does not load for the hook's Node, so no event can be recorded. Reinstall the " +
|
|
96
|
+
`CLI (\`npm i -g ${MCP_PACKAGE_NAME}\`) with the Node on PATH and re-run \`epic-implementer doctor\`.`,
|
|
97
|
+
unknown: "the hook runtime could not be verified (a persistent install that predates the runtime probe " +
|
|
98
|
+
`also lands here). Upgrade it with \`npm i -g ${MCP_PACKAGE_NAME}@latest\`, then re-run ` +
|
|
99
|
+
"`epic-implementer doctor`.",
|
|
100
|
+
};
|
|
101
|
+
/** One sentence naming a classification and its action. Path-free by construction. */
|
|
102
|
+
export function describeHookRuntimeClassification(classification) {
|
|
103
|
+
return `${classification}: ${HOOK_RUNTIME_ACTIONS[classification]}`;
|
|
104
|
+
}
|
|
105
|
+
function unavailable(classification) {
|
|
106
|
+
return { usable: false, classification, reason: describeHookRuntimeClassification(classification) };
|
|
107
|
+
}
|
|
108
|
+
function defaultRealpath(filePath) {
|
|
109
|
+
try {
|
|
110
|
+
return realpathSync(filePath);
|
|
111
|
+
}
|
|
112
|
+
catch {
|
|
113
|
+
return null;
|
|
114
|
+
}
|
|
115
|
+
}
|
|
116
|
+
function defaultIsFile(filePath) {
|
|
117
|
+
try {
|
|
118
|
+
return statSync(filePath).isFile();
|
|
119
|
+
}
|
|
120
|
+
catch {
|
|
121
|
+
return false;
|
|
122
|
+
}
|
|
123
|
+
}
|
|
124
|
+
function defaultReadText(filePath) {
|
|
125
|
+
try {
|
|
126
|
+
return readFileSync(filePath, "utf-8");
|
|
127
|
+
}
|
|
128
|
+
catch {
|
|
129
|
+
return null;
|
|
130
|
+
}
|
|
131
|
+
}
|
|
132
|
+
/**
|
|
133
|
+
* Run `node <stableEntry> __hook-runtime-probe` with a short timeout and a small
|
|
134
|
+
* output cap. `node` is resolved from PATH on purpose: that is the exact runtime
|
|
135
|
+
* the hook body invokes, so the probe measures what a commit will actually run.
|
|
136
|
+
* stderr is discarded unread.
|
|
137
|
+
*/
|
|
138
|
+
export function runHookRuntimeProbe(stableEntry) {
|
|
139
|
+
const result = spawnSync("node", [stableEntry, HOOK_RUNTIME_PROBE_ACTION], {
|
|
140
|
+
encoding: "utf-8",
|
|
141
|
+
timeout: HOOK_RUNTIME_PROBE_TIMEOUT_MS,
|
|
142
|
+
maxBuffer: HOOK_RUNTIME_PROBE_MAX_BUFFER,
|
|
143
|
+
stdio: ["ignore", "pipe", "ignore"],
|
|
144
|
+
windowsHide: true,
|
|
145
|
+
});
|
|
146
|
+
if (result.error) {
|
|
147
|
+
const code = result.error.code ?? null;
|
|
148
|
+
if (code === "ETIMEDOUT")
|
|
149
|
+
return { kind: "timeout" };
|
|
150
|
+
return { kind: "spawn-failed", code };
|
|
151
|
+
}
|
|
152
|
+
if (result.signal !== null)
|
|
153
|
+
return { kind: "timeout" };
|
|
154
|
+
if (result.status !== 0)
|
|
155
|
+
return { kind: "exit-nonzero" };
|
|
156
|
+
return { kind: "ok", stdout: result.stdout ?? "" };
|
|
157
|
+
}
|
|
158
|
+
/** Parse `major.minor.patch` (pre-release suffix ignored), or `null`. */
|
|
159
|
+
export function parseSemver(version) {
|
|
160
|
+
const match = /^v?(\d+)\.(\d+)\.(\d+)(?:[-+].*)?$/.exec(version.trim());
|
|
161
|
+
if (match === null)
|
|
162
|
+
return null;
|
|
163
|
+
return [Number(match[1]), Number(match[2]), Number(match[3])];
|
|
164
|
+
}
|
|
165
|
+
/** True when `version` is inside {@link HOOK_RUNTIME_COMPATIBLE_RANGE}. */
|
|
166
|
+
export function isCompatibleHookPackageVersion(version) {
|
|
167
|
+
const parsed = parseSemver(version);
|
|
168
|
+
if (parsed === null)
|
|
169
|
+
return false;
|
|
170
|
+
const [major, minor] = parsed;
|
|
171
|
+
return major === 0 && minor >= 2;
|
|
172
|
+
}
|
|
173
|
+
/** True when `nodeVersion` is an even LTS major at or above the minimum. */
|
|
174
|
+
export function isQualifiedHookNodeVersion(nodeVersion) {
|
|
175
|
+
const parsed = parseSemver(nodeVersion);
|
|
176
|
+
if (parsed === null)
|
|
177
|
+
return false;
|
|
178
|
+
const [major] = parsed;
|
|
179
|
+
return major >= HOOK_RUNTIME_MIN_NODE_MAJOR && major % 2 === 0;
|
|
180
|
+
}
|
|
181
|
+
/**
|
|
182
|
+
* Locate the persistent package root for a canonical entry: the entry must be
|
|
183
|
+
* `<root>/build/epic-implementer-bin.js` and `<root>` must be
|
|
184
|
+
* `…/node_modules/@bridge_gpt/mcp-server`. Returns `null` for any other layout.
|
|
185
|
+
*/
|
|
186
|
+
export function persistentPackageRootOf(canonicalEntry) {
|
|
187
|
+
if (basename(canonicalEntry) !== HOOK_RUNTIME_ENTRY_FILENAME)
|
|
188
|
+
return null;
|
|
189
|
+
const buildDir = dirname(canonicalEntry);
|
|
190
|
+
if (basename(buildDir) !== "build")
|
|
191
|
+
return null;
|
|
192
|
+
const packageRoot = dirname(buildDir);
|
|
193
|
+
const segments = packageRoot.split(/[\\/]/).filter((s) => s.length > 0);
|
|
194
|
+
const [scope, name] = MCP_PACKAGE_NAME.split("/");
|
|
195
|
+
const n = segments.length;
|
|
196
|
+
if (n < 3)
|
|
197
|
+
return null;
|
|
198
|
+
if (segments[n - 1] !== name || segments[n - 2] !== scope || segments[n - 3] !== "node_modules") {
|
|
199
|
+
return null;
|
|
200
|
+
}
|
|
201
|
+
// A `.bin` shim is never a package root, and neither is anything nested in one.
|
|
202
|
+
if (segments.includes(".bin"))
|
|
203
|
+
return null;
|
|
204
|
+
return packageRoot;
|
|
205
|
+
}
|
|
206
|
+
/** Parse and validate the probe's single JSON line, or `null` when malformed. */
|
|
207
|
+
export function parseHookRuntimeProbeOutput(stdout) {
|
|
208
|
+
const lines = stdout.split(/\r?\n/).filter((l) => l.trim().length > 0);
|
|
209
|
+
if (lines.length !== 1)
|
|
210
|
+
return null;
|
|
211
|
+
let parsed;
|
|
212
|
+
try {
|
|
213
|
+
parsed = JSON.parse(lines[0]);
|
|
214
|
+
}
|
|
215
|
+
catch {
|
|
216
|
+
return null;
|
|
217
|
+
}
|
|
218
|
+
if (parsed === null || typeof parsed !== "object")
|
|
219
|
+
return null;
|
|
220
|
+
const p = parsed;
|
|
221
|
+
if (p.probe !== HOOK_RUNTIME_PROBE_MARKER)
|
|
222
|
+
return null;
|
|
223
|
+
const str = (v) => typeof v === "string" && v.length > 0 && v.length <= 64;
|
|
224
|
+
if (!str(p.node_version) || !str(p.node_modules_abi) || !str(p.native_module) || !str(p.failure_kind)) {
|
|
225
|
+
return null;
|
|
226
|
+
}
|
|
227
|
+
if (typeof p.native_loads !== "boolean")
|
|
228
|
+
return null;
|
|
229
|
+
const nativeVersion = p.native_module_version === null || p.native_module_version === undefined
|
|
230
|
+
? null
|
|
231
|
+
: str(p.native_module_version) ? p.native_module_version : null;
|
|
232
|
+
const packageVersion = str(p.package_version) ? p.package_version : null;
|
|
233
|
+
return {
|
|
234
|
+
node_version: p.node_version,
|
|
235
|
+
node_modules_abi: p.node_modules_abi,
|
|
236
|
+
native_module: p.native_module,
|
|
237
|
+
native_module_version: nativeVersion,
|
|
238
|
+
native_loads: p.native_loads,
|
|
239
|
+
failure_kind: p.failure_kind,
|
|
240
|
+
package_version: packageVersion,
|
|
241
|
+
};
|
|
242
|
+
}
|
|
243
|
+
/**
|
|
244
|
+
* Qualify a candidate hook entry. Read-only and never throws: every boundary
|
|
245
|
+
* failure maps to a fixed classification.
|
|
246
|
+
*/
|
|
247
|
+
export function qualifyHookRuntime(candidateEntry, deps = {}) {
|
|
248
|
+
try {
|
|
249
|
+
return qualifyHookRuntimeUnsafe(candidateEntry, deps);
|
|
250
|
+
}
|
|
251
|
+
catch {
|
|
252
|
+
return unavailable("unknown");
|
|
253
|
+
}
|
|
254
|
+
}
|
|
255
|
+
function qualifyHookRuntimeUnsafe(candidateEntry, deps) {
|
|
256
|
+
const realpath = deps.realpath ?? defaultRealpath;
|
|
257
|
+
const isFile = deps.isFile ?? defaultIsFile;
|
|
258
|
+
const readText = deps.readText ?? defaultReadText;
|
|
259
|
+
const platform = deps.platform ?? process.platform;
|
|
260
|
+
const runProbe = deps.runProbe ?? runHookRuntimeProbe;
|
|
261
|
+
if (typeof candidateEntry !== "string" || candidateEntry.length === 0)
|
|
262
|
+
return unavailable("missing");
|
|
263
|
+
if (basename(candidateEntry) === "conductor-bin.js")
|
|
264
|
+
return unavailable("legacy-name");
|
|
265
|
+
if (isTransientNpxEntryPath(candidateEntry))
|
|
266
|
+
return unavailable("ephemeral-cache");
|
|
267
|
+
const canonical = realpath(candidateEntry);
|
|
268
|
+
if (canonical === null || !isFile(canonical))
|
|
269
|
+
return unavailable("missing");
|
|
270
|
+
// A symlink into the cache is still the cache.
|
|
271
|
+
if (isTransientNpxEntryPath(canonical))
|
|
272
|
+
return unavailable("ephemeral-cache");
|
|
273
|
+
if (basename(canonical) !== HOOK_RUNTIME_ENTRY_FILENAME)
|
|
274
|
+
return unavailable("unqualified-runtime");
|
|
275
|
+
const packageRoot = persistentPackageRootOf(canonical);
|
|
276
|
+
if (packageRoot === null)
|
|
277
|
+
return unavailable("unqualified-runtime");
|
|
278
|
+
const pkgText = readText(join(packageRoot, "package.json"));
|
|
279
|
+
if (pkgText === null)
|
|
280
|
+
return unavailable("missing");
|
|
281
|
+
let pkg;
|
|
282
|
+
try {
|
|
283
|
+
pkg = JSON.parse(pkgText);
|
|
284
|
+
}
|
|
285
|
+
catch {
|
|
286
|
+
return unavailable("unqualified-runtime");
|
|
287
|
+
}
|
|
288
|
+
if (pkg.name !== MCP_PACKAGE_NAME)
|
|
289
|
+
return unavailable("unqualified-runtime");
|
|
290
|
+
if (typeof pkg.version !== "string" || !isCompatibleHookPackageVersion(pkg.version)) {
|
|
291
|
+
return unavailable("unqualified-runtime");
|
|
292
|
+
}
|
|
293
|
+
const binMap = pkg.bin !== null && typeof pkg.bin === "object" ? pkg.bin : null;
|
|
294
|
+
const mapped = binMap?.[HOOK_RUNTIME_BIN_NAME];
|
|
295
|
+
if (typeof mapped !== "string")
|
|
296
|
+
return unavailable("unqualified-runtime");
|
|
297
|
+
const normalizedMapped = mapped.replace(/^\.\//, "").replace(/\\/g, "/");
|
|
298
|
+
if (normalizedMapped !== `build/${HOOK_RUNTIME_ENTRY_FILENAME}`)
|
|
299
|
+
return unavailable("unqualified-runtime");
|
|
300
|
+
if (!HOOK_RUNTIME_QUALIFIED_PLATFORMS.includes(platform))
|
|
301
|
+
return unavailable("unqualified-runtime");
|
|
302
|
+
// The stable launch path is the package-owned entry under the persistent
|
|
303
|
+
// prefix. An in-place `npm i -g` upgrade replaces the files beneath it, so a
|
|
304
|
+
// hook that embeds this path keeps working across supported upgrades.
|
|
305
|
+
const stableEntry = join(packageRoot, "build", HOOK_RUNTIME_ENTRY_FILENAME);
|
|
306
|
+
const outcome = runProbe(stableEntry);
|
|
307
|
+
if (outcome.kind === "spawn-failed") {
|
|
308
|
+
return unavailable(outcome.code === "ENOENT" ? "node-unavailable" : "unknown");
|
|
309
|
+
}
|
|
310
|
+
if (outcome.kind !== "ok")
|
|
311
|
+
return unavailable("unknown");
|
|
312
|
+
const evidence = parseHookRuntimeProbeOutput(outcome.stdout);
|
|
313
|
+
if (evidence === null)
|
|
314
|
+
return unavailable("unknown");
|
|
315
|
+
if (!evidence.native_loads)
|
|
316
|
+
return unavailable("native-unavailable");
|
|
317
|
+
if (!isQualifiedHookNodeVersion(evidence.node_version))
|
|
318
|
+
return unavailable("unqualified-runtime");
|
|
319
|
+
// The probe must be answering for the package we just read, not some other
|
|
320
|
+
// build the entry happens to import.
|
|
321
|
+
if (evidence.package_version !== null && evidence.package_version !== pkg.version) {
|
|
322
|
+
return unavailable("unknown");
|
|
323
|
+
}
|
|
324
|
+
return {
|
|
325
|
+
usable: true,
|
|
326
|
+
stable_entry: stableEntry,
|
|
327
|
+
canonical_entry: canonical,
|
|
328
|
+
package_name: MCP_PACKAGE_NAME,
|
|
329
|
+
package_version: pkg.version,
|
|
330
|
+
platform,
|
|
331
|
+
runtime: {
|
|
332
|
+
node_version: evidence.node_version,
|
|
333
|
+
node_modules_abi: evidence.node_modules_abi,
|
|
334
|
+
native_module: evidence.native_module,
|
|
335
|
+
native_module_version: evidence.native_module_version,
|
|
336
|
+
native_loads: evidence.native_loads,
|
|
337
|
+
failure_kind: evidence.failure_kind,
|
|
338
|
+
},
|
|
339
|
+
};
|
|
340
|
+
}
|
|
@@ -771,12 +771,13 @@ export function conductorHookTargetsSection(legacyConductor, conductorContext) {
|
|
|
771
771
|
detail: "advisory — not a git worktree, so no managed conductor hooks apply",
|
|
772
772
|
};
|
|
773
773
|
}
|
|
774
|
+
// BAPI-1290: the shared per-hook `usable` verdict — a recognized command, an
|
|
775
|
+
// executable hook, and a qualified persistent runtime whose Node and native
|
|
776
|
+
// binding load. A file that merely exists is not delivery, and a hook without
|
|
777
|
+
// that verdict (an older or partial report) is unknown, never usable.
|
|
774
778
|
const usable = (name) => {
|
|
775
779
|
const hook = (hooks.hooks ?? []).find((h) => h.name === name);
|
|
776
|
-
return
|
|
777
|
-
hook.exists &&
|
|
778
|
-
hook.managed_block_present &&
|
|
779
|
-
hook.embedded_bin_is_file);
|
|
780
|
+
return hook !== undefined && hook.usable === true;
|
|
780
781
|
};
|
|
781
782
|
const rendered = MANAGED_HOOK_NAMES.map((name) => `${name}=${usable(name)}`).join(", ");
|
|
782
783
|
const allUsable = MANAGED_HOOK_NAMES.every((name) => usable(name));
|
|
@@ -141,7 +141,7 @@ export const SERVER_READINESS_DESCRIPTORS = [
|
|
|
141
141
|
{ id: "executor-readiness", label: "Executor server observation" },
|
|
142
142
|
{ id: "review-workflow", label: "Review workflow protocol" },
|
|
143
143
|
{ id: "conductor-ci-workflow", label: "Conductor CI migration guard" },
|
|
144
|
-
// BAPI-1102 — the two
|
|
144
|
+
// BAPI-1102 — the two unattended repository prerequisites plus the aggregate the
|
|
145
145
|
// third unattended refusal reads. Noun phrases, like every row above: these
|
|
146
146
|
// render as list items under a source heading, not as sentences.
|
|
147
147
|
{ id: "unattended-consent", label: "Repository unattended consent" },
|
|
@@ -189,8 +189,10 @@ const SERVER_REMEDIATIONS = {
|
|
|
189
189
|
olderServer: "upgrade the Bridge API deploy so it reports this fact; its state is unknown here, not healthy.",
|
|
190
190
|
// BAPI-1102. Each names the ONE action that clears it, and none of them
|
|
191
191
|
// mentions a webhook URL, a destination, or any configuration value.
|
|
192
|
-
|
|
193
|
-
|
|
192
|
+
// BAPI-1273: consent defaults ON; the repository owner switches it on the
|
|
193
|
+
// admin panel's Setup page. No MCP or CLI surface ever writes it.
|
|
194
|
+
unattendedConsent: "turn on 'Allow unattended Epic Implementer runs' on the repository's Setup page in the Bridge admin panel, or run setup with --attended. If it is already on, contact Bridge support.",
|
|
195
|
+
unattendedNotifyDefault: "supply a --policy-file declaring notify.webhook_url, or add an email address to the account so escalation alerts can be emailed, or run setup with --attended; an unattended run must have a working escalation channel, and notify.local_sink is not one.",
|
|
194
196
|
repositoryWorkflowReadiness: "run `install-bridge conductor` so the review and conductor-CI workflows are confirmed on the default branch, or run setup with --attended.",
|
|
195
197
|
// BAPI-1135. Both are IMPORTED from the shared classifier rather than written
|
|
196
198
|
// out here, so the remedy command has exactly one spelling across the refusal,
|
|
@@ -351,7 +353,7 @@ const ADMISSION_EVIDENCE_LABELS = {
|
|
|
351
353
|
/** Who acts, in fixed prose. Never a value from the payload. */
|
|
352
354
|
const ADMISSION_OWNER_REMEDIATION = {
|
|
353
355
|
operator: "the operator re-runs guided setup (`install conductor --launch-profile <name>`) and re-observes.",
|
|
354
|
-
repository_admin: "a repository administrator resolves it
|
|
356
|
+
repository_admin: "a repository administrator resolves it (unattended consent is the 'Allow unattended Epic Implementer runs' toggle on the repository's Setup page in the Bridge admin panel); setup never grants it.",
|
|
355
357
|
bridge_operations: "escalate to Bridge operations; this is not something an operator command can change.",
|
|
356
358
|
};
|
|
357
359
|
/**
|
|
@@ -485,7 +487,7 @@ function unattendedOutcomes(readiness) {
|
|
|
485
487
|
: {
|
|
486
488
|
id: "unattended-consent",
|
|
487
489
|
status: "warn",
|
|
488
|
-
detail: "
|
|
490
|
+
detail: "unattended runs are off for this repository — an unattended run will be refused before it is created",
|
|
489
491
|
remediation: SERVER_REMEDIATIONS.unattendedConsent,
|
|
490
492
|
},
|
|
491
493
|
u.notify_webhook_default_declared
|
|
@@ -498,12 +500,21 @@ function unattendedOutcomes(readiness) {
|
|
|
498
500
|
? "a notify webhook default is declared and verified"
|
|
499
501
|
: "a notify webhook default is declared (verified at approval, not here)",
|
|
500
502
|
}
|
|
501
|
-
:
|
|
502
|
-
|
|
503
|
-
|
|
504
|
-
|
|
505
|
-
|
|
506
|
-
|
|
503
|
+
: u.escalation_email_available === true
|
|
504
|
+
? {
|
|
505
|
+
// BAPI-1274 — with no webhook declared, escalation alerts go to the
|
|
506
|
+
// account's email address. Presence only: the address is never
|
|
507
|
+
// reported by the server and is never rendered here.
|
|
508
|
+
id: "unattended-notify-default",
|
|
509
|
+
status: "pass",
|
|
510
|
+
detail: "No webhook declared; escalation alerts will be emailed to the account's email address",
|
|
511
|
+
}
|
|
512
|
+
: {
|
|
513
|
+
id: "unattended-notify-default",
|
|
514
|
+
status: "warn",
|
|
515
|
+
detail: "no repository notify webhook default is declared — an unattended run needs an escalation channel, and no column supplies a repository default today, so the run must declare notify.webhook_url itself or the account needs an email address for escalation alerts",
|
|
516
|
+
remediation: SERVER_REMEDIATIONS.unattendedNotifyDefault,
|
|
517
|
+
},
|
|
507
518
|
u.repository_readiness_confirmed
|
|
508
519
|
? {
|
|
509
520
|
id: "repository-workflow-readiness",
|