@bridge_gpt/mcp-server 0.2.60 → 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 +58 -11
- 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 +8 -7
- package/build/conductor/bridge-api-client.js +678 -14
- 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/merge-execution-contract.js +30 -5
- package/build/conductor/merge-execution-eligibility.js +10 -9
- package/build/conductor/readiness-sections.js +104 -20
- package/build/conductor/readiness.js +129 -25
- 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 +194 -0
- package/build/docs.generated.js +2 -1
- package/build/doctor.js +161 -7
- package/build/drive-epic-key.js +12 -0
- package/build/drive-epic.js +1318 -111
- 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-integration-pr.js +61 -15
- package/build/epic-launch-profile.js +464 -0
- package/build/epic-run-status.js +5 -0
- package/build/epic-shared/bridge-client.js +46 -4
- package/build/executor/cli.js +48 -0
- package/build/executor/completion-contract.js +423 -0
- package/build/executor/completion-supervision.js +286 -0
- package/build/executor/credentials.js +19 -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 +117 -1
- package/build/executor/integration-job.js +759 -0
- package/build/executor/job-errors.js +48 -0
- package/build/executor/job-runner.js +1386 -35
- package/build/executor/job-types.js +10 -2
- package/build/executor/observation.js +114 -7
- 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 +246 -1
- package/build/executor/test-declaration.js +20 -0
- package/build/executor/types.js +55 -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 +772 -954
- package/build/install-bridge-conductor.js +622 -40
- package/build/install-bridge.js +92 -16
- package/build/merge-pull-request.js +1 -1
- package/build/merge-workflow-config.js +54 -0
- package/build/merge-workflow-drift.js +35 -4
- package/build/merge-workflow.js +386 -2
- package/build/pipelines.generated.js +9 -8
- 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 +148 -0
- package/build/readme.generated.js +1 -1
- package/build/scripts/executor-protocol-contract-driver.js +13 -3
- package/build/setup-epic.js +130 -48
- 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
|
+
}
|
|
@@ -47,13 +47,38 @@ export const MERGE_WORKFLOW_IDENTIFIER = "epic-implementer-merge.yml";
|
|
|
47
47
|
/** The top-level workflow key carrying the machine-readable contract version. */
|
|
48
48
|
export const MERGE_WORKFLOW_VERSION_MARKER_KEY = "x-bridge-merge-contract-version";
|
|
49
49
|
/**
|
|
50
|
-
* The
|
|
50
|
+
* The workflow contract version this CLI RENDERS and installs.
|
|
51
51
|
*
|
|
52
|
-
*
|
|
53
|
-
*
|
|
54
|
-
*
|
|
52
|
+
* BAPI-1259 moved it from `1` to `2` (the integration-action job and the
|
|
53
|
+
* `operation` dispatch input). Whether an INSTALLED version is usable is
|
|
54
|
+
* answered by the two closed sets below — never by "at least". A newer version
|
|
55
|
+
* is a workflow this build has never been taught to reason about, so it is NOT
|
|
56
|
+
* eligible — see {@link classifyMergeExecutionEligibility}.
|
|
55
57
|
*/
|
|
56
|
-
export const MERGE_WORKFLOW_EXPECTED_VERSION =
|
|
58
|
+
export const MERGE_WORKFLOW_EXPECTED_VERSION = 2;
|
|
59
|
+
/**
|
|
60
|
+
* Installed contract versions a CHILD merge may be dispatched to. Mirrors
|
|
61
|
+
* `MERGE_WORKFLOW_CHILD_MERGE_COMPATIBLE_VERSIONS` in the Python contract.
|
|
62
|
+
* Version 1's child-merge job is the behavior version 2 keeps unchanged.
|
|
63
|
+
*/
|
|
64
|
+
export const MERGE_WORKFLOW_CHILD_MERGE_COMPATIBLE_VERSIONS = [1, 2];
|
|
65
|
+
/**
|
|
66
|
+
* Installed contract versions an INTEGRATION action may be dispatched to.
|
|
67
|
+
* Mirrors `INTEGRATION_ACTION_WORKFLOW_VERSIONS` in the Python contract.
|
|
68
|
+
*/
|
|
69
|
+
export const INTEGRATION_ACTION_WORKFLOW_VERSIONS = [2];
|
|
70
|
+
/** True when an installed marker may receive a child-merge dispatch. */
|
|
71
|
+
export function mergeWorkflowVersionSupportsChildMerge(version) {
|
|
72
|
+
return (typeof version === "number" &&
|
|
73
|
+
Number.isInteger(version) &&
|
|
74
|
+
MERGE_WORKFLOW_CHILD_MERGE_COMPATIBLE_VERSIONS.includes(version));
|
|
75
|
+
}
|
|
76
|
+
/** True when an installed marker may receive an integration-action dispatch. */
|
|
77
|
+
export function mergeWorkflowVersionSupportsIntegrationActions(version) {
|
|
78
|
+
return (typeof version === "number" &&
|
|
79
|
+
Number.isInteger(version) &&
|
|
80
|
+
INTEGRATION_ACTION_WORKFLOW_VERSIONS.includes(version));
|
|
81
|
+
}
|
|
57
82
|
/**
|
|
58
83
|
* The single command that generates the merge workflow and opens its pull
|
|
59
84
|
* request. Every refusal, remediation, and readiness line imports this constant.
|
|
@@ -23,14 +23,15 @@
|
|
|
23
23
|
* credentials or connectivity. Collapsing them sends half of all operators to do
|
|
24
24
|
* work that changes nothing. `could_not_verify` is never treated as eligible.
|
|
25
25
|
*
|
|
26
|
-
* ##
|
|
26
|
+
* ## Known versions only, always
|
|
27
27
|
*
|
|
28
|
-
* Eligibility requires
|
|
29
|
-
*
|
|
30
|
-
*
|
|
31
|
-
*
|
|
28
|
+
* Eligibility requires membership in the closed set
|
|
29
|
+
* {@link MERGE_WORKFLOW_CHILD_MERGE_COMPATIBLE_VERSIONS} (BAPI-1259: version 1
|
|
30
|
+
* and version 2 both carry the unchanged child-merge job). A NEWER version is
|
|
31
|
+
* not eligible: it is a workflow this build has never been taught to reason
|
|
32
|
+
* about, and "newer is probably fine" is a guess about a merge actuator.
|
|
32
33
|
*/
|
|
33
|
-
import {
|
|
34
|
+
import { MERGE_WORKFLOW_CHILD_MERGE_COMPATIBLE_VERSIONS, MERGE_WORKFLOW_REMEDY_COMMAND, mergeWorkflowVersionSupportsChildMerge, } from "./merge-execution-contract.js";
|
|
34
35
|
/**
|
|
35
36
|
* Remediation for a verdict that could not be established.
|
|
36
37
|
*
|
|
@@ -107,10 +108,10 @@ export function classifyMergeExecutionEligibility(readiness) {
|
|
|
107
108
|
if (!m.merge_workflow_enabled) {
|
|
108
109
|
return ineligible("workflow_disabled", "the merge workflow is present but disabled");
|
|
109
110
|
}
|
|
110
|
-
if (m.merge_workflow_version
|
|
111
|
+
if (!mergeWorkflowVersionSupportsChildMerge(m.merge_workflow_version)) {
|
|
111
112
|
// A newer version lands here too — see the module docstring.
|
|
112
113
|
return ineligible("workflow_version_mismatch", `merge workflow version ${m.merge_workflow_version ?? "unset"} ` +
|
|
113
|
-
`(expected ${
|
|
114
|
+
`(expected one of ${MERGE_WORKFLOW_CHILD_MERGE_COMPATIBLE_VERSIONS.join(", ")})`);
|
|
114
115
|
}
|
|
115
116
|
if (!gh.actions_write) {
|
|
116
117
|
return ineligible("app_actions_write_denied", `the GitHub App holds actions: ${gh.actions_permission_level}`);
|
|
@@ -123,7 +124,7 @@ export function classifyMergeExecutionEligibility(readiness) {
|
|
|
123
124
|
}
|
|
124
125
|
return {
|
|
125
126
|
status: "eligible",
|
|
126
|
-
detail: `merge workflow present, enabled, version ${
|
|
127
|
+
detail: `merge workflow present, enabled, version ${m.merge_workflow_version}; ` +
|
|
127
128
|
"App actions: write, workflow token write authority, workflow actor admitted",
|
|
128
129
|
};
|
|
129
130
|
}
|
|
@@ -38,7 +38,7 @@ import { MCP_PACKAGE_NAME } from "../mcp-identity.js";
|
|
|
38
38
|
import { CLAUDE_REVIEW_WORKFLOW_DRIFT_REMEDIATION, summarizeClaudeReviewWorkflowDrift, } from "../claude-review-workflow-drift.js";
|
|
39
39
|
import { describeNativeLedgerAvailability, GH_ACTIONS_MERGE_AUTH_REMEDIATION, GH_ACTIONS_MERGE_SKIPPED_DETAIL, GH_ACTIONS_MERGE_UNOBSERVABLE_REMEDIATION, } from "./doctor.js";
|
|
40
40
|
import { classifyMergeExecutionEligibility, MERGE_EXECUTION_UNVERIFIABLE_REMEDIATION, } from "./merge-execution-eligibility.js";
|
|
41
|
-
import {
|
|
41
|
+
import { MERGE_WORKFLOW_CHILD_MERGE_COMPATIBLE_VERSIONS, MERGE_WORKFLOW_PATH, mergeWorkflowVersionSupportsChildMerge, } from "./merge-execution-contract.js";
|
|
42
42
|
import { classifyDatabaseMigration, classifySingleTransitionAuthority, classifyWorkerPresent, } from "./bring-up-facts.js";
|
|
43
43
|
/** Operator runbook pointer surfaced beside the executor gap. */
|
|
44
44
|
export const CONDUCTOR_OPERATOR_RUNBOOK_POINTER = "docs/claude/epic-implementer-v2-operator-runbook.md";
|
|
@@ -80,6 +80,88 @@ export const EXECUTOR_INSTALL_SERVICE_REMEDIATION = "generate a persistent unit
|
|
|
80
80
|
export const EXECUTOR_SERVICE_START_REMEDIATION = "start the service (re-run `install conductor` and accept the start consent, or run the " +
|
|
81
81
|
`printed launchctl/systemctl command by hand) — see ${CONDUCTOR_OPERATOR_RUNBOOK_POINTER}.`;
|
|
82
82
|
// ---------------------------------------------------------------------------
|
|
83
|
+
// BAPI-1254 — the ONE required-check presentation
|
|
84
|
+
// ---------------------------------------------------------------------------
|
|
85
|
+
/**
|
|
86
|
+
* Operator-hold sentence shared by every non-verified outcome. It states what an
|
|
87
|
+
* operator must do; it does not claim this report blocks anything — activation
|
|
88
|
+
* and dispatch enforcement belong to a separate slice.
|
|
89
|
+
*/
|
|
90
|
+
const REQUIRED_CHECKS_OPERATOR_HOLD = "hold new unattended epic work until this is resolved";
|
|
91
|
+
/** Fixed, secret-free copy for each outcome. Names no check, branch, or SHA. */
|
|
92
|
+
export const REQUIRED_CHECKS_PRESENTATION_COPY = {
|
|
93
|
+
verified: {
|
|
94
|
+
detail: "Verified — every configured required check is produced by the lane workflows",
|
|
95
|
+
},
|
|
96
|
+
unknown_name: {
|
|
97
|
+
detail: "Configuration mismatch confirmed — a configured required check conflicts with the " +
|
|
98
|
+
`current lane producer contract; ${REQUIRED_CHECKS_OPERATOR_HOLD}`,
|
|
99
|
+
remediation: "repair the project-default required-check configuration so it names the check the " +
|
|
100
|
+
"lane workflow emits (see the operator runbook's patch-policy section for active runs).",
|
|
101
|
+
},
|
|
102
|
+
unverifiable: {
|
|
103
|
+
detail: "Could not verify — the configured required checks could not be matched to an " +
|
|
104
|
+
`authoritative producer; ${REQUIRED_CHECKS_OPERATOR_HOLD}`,
|
|
105
|
+
remediation: "restore required-check verification (a readable default branch with static lane " +
|
|
106
|
+
"workflows and a current Bridge API), or correct the required-check configuration.",
|
|
107
|
+
},
|
|
108
|
+
empty: {
|
|
109
|
+
detail: "Could not verify — the required-check list is EMPTY, which is unsafe: the done gate " +
|
|
110
|
+
`would pass unconditionally; ${REQUIRED_CHECKS_OPERATOR_HOLD}`,
|
|
111
|
+
remediation: "configure a non-empty required-check list naming the checks the lane workflows emit.",
|
|
112
|
+
},
|
|
113
|
+
olderServer: {
|
|
114
|
+
detail: "Could not verify — this Bridge API does not report required-check verification; " +
|
|
115
|
+
REQUIRED_CHECKS_OPERATOR_HOLD,
|
|
116
|
+
remediation: "upgrade the Bridge API deploy so it verifies required checks, or correct the " +
|
|
117
|
+
"required-check configuration; its state is unknown here, not healthy.",
|
|
118
|
+
},
|
|
119
|
+
};
|
|
120
|
+
/**
|
|
121
|
+
* Map the validated supervisor facts onto one required-check presentation.
|
|
122
|
+
*
|
|
123
|
+
* PURE, and the only place either renderer decides this row: the CLI descriptor
|
|
124
|
+
* and the install-time section both call it, so they cannot drift and neither
|
|
125
|
+
* performs a second producer verification. Only the server's `verified` verdict
|
|
126
|
+
* is satisfied. A nonempty count is never proof, an older server's absent group
|
|
127
|
+
* is `unverifiable`, and an empty list stays unsafe whatever else is reported.
|
|
128
|
+
* No remediation suggests disabling required checks.
|
|
129
|
+
*/
|
|
130
|
+
export function presentRequiredChecks(supervisor) {
|
|
131
|
+
const verdict = supervisor.required_checks_verification ?? null;
|
|
132
|
+
if (supervisor.required_checks_empty) {
|
|
133
|
+
return { outcome: "unverifiable", satisfied: false, ...REQUIRED_CHECKS_PRESENTATION_COPY.empty };
|
|
134
|
+
}
|
|
135
|
+
if (verdict === null) {
|
|
136
|
+
return {
|
|
137
|
+
outcome: "unverifiable",
|
|
138
|
+
satisfied: false,
|
|
139
|
+
...REQUIRED_CHECKS_PRESENTATION_COPY.olderServer,
|
|
140
|
+
};
|
|
141
|
+
}
|
|
142
|
+
if (verdict === "verified") {
|
|
143
|
+
const fresh = supervisor.required_checks_evidence_fresh === true ? ", fresh evidence" : "";
|
|
144
|
+
return {
|
|
145
|
+
outcome: "verified",
|
|
146
|
+
satisfied: true,
|
|
147
|
+
detail: `${REQUIRED_CHECKS_PRESENTATION_COPY.verified.detail} ` +
|
|
148
|
+
`(${supervisor.required_checks_count} required check(s) configured${fresh})`,
|
|
149
|
+
};
|
|
150
|
+
}
|
|
151
|
+
if (verdict === "unknown_name") {
|
|
152
|
+
return {
|
|
153
|
+
outcome: "unknown_name",
|
|
154
|
+
satisfied: false,
|
|
155
|
+
...REQUIRED_CHECKS_PRESENTATION_COPY.unknown_name,
|
|
156
|
+
};
|
|
157
|
+
}
|
|
158
|
+
return {
|
|
159
|
+
outcome: "unverifiable",
|
|
160
|
+
satisfied: false,
|
|
161
|
+
...REQUIRED_CHECKS_PRESENTATION_COPY.unverifiable,
|
|
162
|
+
};
|
|
163
|
+
}
|
|
164
|
+
// ---------------------------------------------------------------------------
|
|
83
165
|
// Section builders
|
|
84
166
|
// ---------------------------------------------------------------------------
|
|
85
167
|
export function supervisorSection(readiness) {
|
|
@@ -109,20 +191,20 @@ export function supervisorSection(readiness) {
|
|
|
109
191
|
// Deliberately its OWN section, not folded into the one above: "configured but
|
|
110
192
|
// unsafe" and "not configured" drive different operator actions, and an empty
|
|
111
193
|
// required-check list makes the CI gate pass unconditionally rather than fail.
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
:
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
194
|
+
//
|
|
195
|
+
// BAPI-1254: the verdict comes from the shared `presentRequiredChecks`, the
|
|
196
|
+
// same mapping the CLI descriptor uses. Only `verified` is `ok`; a confirmed
|
|
197
|
+
// mismatch and could-not-verify are both `degraded`, never `fatal`.
|
|
198
|
+
const requiredChecks = presentRequiredChecks(s);
|
|
199
|
+
sections.push({
|
|
200
|
+
id: "supervisor-required-checks",
|
|
201
|
+
label: "Supervisor required CI checks",
|
|
202
|
+
status: requiredChecks.satisfied ? "ok" : "degraded",
|
|
203
|
+
detail: requiredChecks.detail,
|
|
204
|
+
...(requiredChecks.remediation === undefined
|
|
205
|
+
? {}
|
|
206
|
+
: { remediation: requiredChecks.remediation }),
|
|
207
|
+
});
|
|
126
208
|
// BAPI-694 (R5) — its OWN section, and WARN-ONLY.
|
|
127
209
|
//
|
|
128
210
|
// `degraded`, never `fatal`: a divergence between the code_review gate's
|
|
@@ -292,7 +374,8 @@ export function mergeExecutionSections(readiness) {
|
|
|
292
374
|
? `\`${MERGE_WORKFLOW_PATH}\` present`
|
|
293
375
|
: `not eligible: \`${MERGE_WORKFLOW_PATH}\` is absent`),
|
|
294
376
|
fact("merge-execution-workflow-enabled", "Merge workflow enabled", m.merge_workflow_enabled, m.merge_workflow_enabled ? "enabled" : "not eligible: disabled or unknown"),
|
|
295
|
-
fact("merge-execution-workflow-version", "Merge workflow version", m.merge_workflow_version
|
|
377
|
+
fact("merge-execution-workflow-version", "Merge workflow version", mergeWorkflowVersionSupportsChildMerge(m.merge_workflow_version), `\`${m.merge_workflow_version ?? "unset"}\` (expected one of ` +
|
|
378
|
+
`\`${MERGE_WORKFLOW_CHILD_MERGE_COMPATIBLE_VERSIONS.join(", ")}\`)`),
|
|
296
379
|
fact("merge-execution-app-actions-write", "GitHub App `actions: write`", readiness.github.actions_write, readiness.github.actions_probe_succeeded
|
|
297
380
|
? `actions: \`${readiness.github.actions_permission_level}\``
|
|
298
381
|
: "could not verify: the App permission could not be checked"),
|
|
@@ -688,12 +771,13 @@ export function conductorHookTargetsSection(legacyConductor, conductorContext) {
|
|
|
688
771
|
detail: "advisory — not a git worktree, so no managed conductor hooks apply",
|
|
689
772
|
};
|
|
690
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.
|
|
691
778
|
const usable = (name) => {
|
|
692
779
|
const hook = (hooks.hooks ?? []).find((h) => h.name === name);
|
|
693
|
-
return
|
|
694
|
-
hook.exists &&
|
|
695
|
-
hook.managed_block_present &&
|
|
696
|
-
hook.embedded_bin_is_file);
|
|
780
|
+
return hook !== undefined && hook.usable === true;
|
|
697
781
|
};
|
|
698
782
|
const rendered = MANAGED_HOOK_NAMES.map((name) => `${name}=${usable(name)}`).join(", ");
|
|
699
783
|
const allUsable = MANAGED_HOOK_NAMES.every((name) => usable(name));
|