@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.
Files changed (89) hide show
  1. package/README.md +58 -11
  2. package/build/agent-launchers/claude-executor-adapter.js +134 -3
  3. package/build/agent-launchers/executor-adapter.js +54 -2
  4. package/build/agents.generated.js +10 -1
  5. package/build/bounded-wait.js +32 -1
  6. package/build/commands.generated.js +8 -7
  7. package/build/conductor/bridge-api-client.js +678 -14
  8. package/build/conductor/bring-up-facts.js +3 -1
  9. package/build/conductor/doctor.js +23 -14
  10. package/build/conductor/epic-child-continuation.js +324 -0
  11. package/build/conductor/epic-implementer-cli.js +142 -17
  12. package/build/conductor/git-hooks.js +529 -118
  13. package/build/conductor/hook-runtime-qualification.js +340 -0
  14. package/build/conductor/merge-execution-contract.js +30 -5
  15. package/build/conductor/merge-execution-eligibility.js +10 -9
  16. package/build/conductor/readiness-sections.js +104 -20
  17. package/build/conductor/readiness.js +129 -25
  18. package/build/conductor/recovery-cli.js +354 -13
  19. package/build/conductor/recovery-operations.js +251 -5
  20. package/build/conductor/repair-contract.js +8 -0
  21. package/build/conductor-bundle-artifacts.js +1 -0
  22. package/build/credential-store.js +194 -0
  23. package/build/docs.generated.js +2 -1
  24. package/build/doctor.js +161 -7
  25. package/build/drive-epic-key.js +12 -0
  26. package/build/drive-epic.js +1318 -111
  27. package/build/epic-coherence-input-cli.js +137 -0
  28. package/build/epic-coherence-input.js +920 -0
  29. package/build/epic-coherence-input.test-fixtures.js +175 -0
  30. package/build/epic-implementer-bin.js +35 -29
  31. package/build/epic-implementer-bundle-cli.js +14 -1
  32. package/build/epic-integration-pr.js +61 -15
  33. package/build/epic-launch-profile.js +464 -0
  34. package/build/epic-run-status.js +5 -0
  35. package/build/epic-shared/bridge-client.js +46 -4
  36. package/build/executor/cli.js +48 -0
  37. package/build/executor/completion-contract.js +423 -0
  38. package/build/executor/completion-supervision.js +286 -0
  39. package/build/executor/credentials.js +19 -1
  40. package/build/executor/deps.js +65 -1
  41. package/build/executor/env.js +6 -0
  42. package/build/executor/heartbeat.js +13 -3
  43. package/build/executor/http-client.js +117 -1
  44. package/build/executor/integration-job.js +759 -0
  45. package/build/executor/job-errors.js +48 -0
  46. package/build/executor/job-runner.js +1386 -35
  47. package/build/executor/job-types.js +10 -2
  48. package/build/executor/observation.js +114 -7
  49. package/build/executor/preflight.js +99 -0
  50. package/build/executor/process-containment.js +756 -0
  51. package/build/executor/process.js +235 -5
  52. package/build/executor/prompt-spec.js +262 -1
  53. package/build/executor/provider-checks.js +69 -0
  54. package/build/executor/rebase-finalization.js +351 -0
  55. package/build/executor/reconcile-overlap-job.js +77 -84
  56. package/build/executor/required-verification.js +313 -0
  57. package/build/executor/required-work-ledger.js +1121 -0
  58. package/build/executor/results.js +142 -0
  59. package/build/executor/resume-pre-spawn.js +87 -4
  60. package/build/executor/runner.js +246 -1
  61. package/build/executor/test-declaration.js +20 -0
  62. package/build/executor/types.js +55 -0
  63. package/build/executor/worker-command.js +49 -13
  64. package/build/executor/worker-finalization.js +88 -11
  65. package/build/executor/worker-guard-hook.js +29 -4
  66. package/build/executor/worktree.js +13 -0
  67. package/build/executor-launch.js +658 -0
  68. package/build/index.js +772 -954
  69. package/build/install-bridge-conductor.js +622 -40
  70. package/build/install-bridge.js +92 -16
  71. package/build/merge-pull-request.js +1 -1
  72. package/build/merge-workflow-config.js +54 -0
  73. package/build/merge-workflow-drift.js +35 -4
  74. package/build/merge-workflow.js +386 -2
  75. package/build/pipelines.generated.js +9 -8
  76. package/build/plan-discovery.js +137 -0
  77. package/build/plan-epic-implementer-eligibility.js +2 -2
  78. package/build/plane/cli.js +37 -3
  79. package/build/plane/member-logs.js +3 -0
  80. package/build/plane/member-roster.js +7 -1
  81. package/build/plane/shutdown.js +59 -1
  82. package/build/plane/supervisor.js +148 -0
  83. package/build/readme.generated.js +1 -1
  84. package/build/scripts/executor-protocol-contract-driver.js +13 -3
  85. package/build/setup-epic.js +130 -48
  86. package/build/version.generated.js +3 -3
  87. package/build/worker-guard-hook-bin.js +2 -2
  88. package/docs/CONDUCTOR.md +116 -2
  89. 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 ONLY workflow contract version this CLI understands.
50
+ * The workflow contract version this CLI RENDERS and installs.
51
51
  *
52
- * Exact equality. A newer version is a workflow this build has never been taught
53
- * to reason about, so it is NOT eligible — see
54
- * {@link classifyMergeExecutionEligibility}.
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 = 1;
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
- * ## Exact version, always
26
+ * ## Known versions only, always
27
27
  *
28
- * Eligibility requires {@link MERGE_WORKFLOW_EXPECTED_VERSION} exactly. A NEWER
29
- * version is not eligible either: it is a workflow this build has never been
30
- * taught to reason about, and "newer is probably fine" is a guess about a merge
31
- * actuator.
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 { MERGE_WORKFLOW_EXPECTED_VERSION, MERGE_WORKFLOW_REMEDY_COMMAND, } from "./merge-execution-contract.js";
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 !== MERGE_WORKFLOW_EXPECTED_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 ${MERGE_WORKFLOW_EXPECTED_VERSION})`);
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 ${MERGE_WORKFLOW_EXPECTED_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 { MERGE_WORKFLOW_EXPECTED_VERSION, MERGE_WORKFLOW_PATH, } from "./merge-execution-contract.js";
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
- sections.push(s.required_checks_empty
113
- ? {
114
- id: "supervisor-required-checks",
115
- label: "Supervisor required CI checks",
116
- status: "degraded",
117
- detail: "required-check list is EMPTY — the done gate would pass unconditionally",
118
- remediation: "bootstrap writes a non-empty required-check list into done_gate_config.",
119
- }
120
- : {
121
- id: "supervisor-required-checks",
122
- label: "Supervisor required CI checks",
123
- status: "ok",
124
- detail: `${s.required_checks_count} required check(s) configured`,
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 === MERGE_WORKFLOW_EXPECTED_VERSION, `\`${m.merge_workflow_version ?? "unset"}\` (expected \`${MERGE_WORKFLOW_EXPECTED_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 (hook !== undefined &&
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));