@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.
Files changed (80) hide show
  1. package/README.md +47 -8
  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 +6 -5
  7. package/build/conductor/bridge-api-client.js +335 -22
  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/readiness-sections.js +5 -4
  15. package/build/conductor/readiness.js +22 -11
  16. package/build/conductor/recovery-cli.js +354 -13
  17. package/build/conductor/recovery-operations.js +251 -5
  18. package/build/conductor/repair-contract.js +8 -0
  19. package/build/conductor-bundle-artifacts.js +1 -0
  20. package/build/credential-store.js +50 -12
  21. package/build/docs.generated.js +2 -1
  22. package/build/doctor.js +77 -11
  23. package/build/drive-epic-key.js +12 -0
  24. package/build/drive-epic.js +1211 -116
  25. package/build/epic-coherence-input-cli.js +137 -0
  26. package/build/epic-coherence-input.js +920 -0
  27. package/build/epic-coherence-input.test-fixtures.js +175 -0
  28. package/build/epic-implementer-bin.js +35 -29
  29. package/build/epic-implementer-bundle-cli.js +14 -1
  30. package/build/epic-launch-profile.js +14 -5
  31. package/build/epic-run-status.js +5 -0
  32. package/build/epic-shared/bridge-client.js +46 -4
  33. package/build/executor/cli.js +39 -0
  34. package/build/executor/completion-contract.js +423 -0
  35. package/build/executor/completion-supervision.js +286 -0
  36. package/build/executor/credentials.js +3 -1
  37. package/build/executor/deps.js +65 -1
  38. package/build/executor/env.js +6 -0
  39. package/build/executor/heartbeat.js +13 -3
  40. package/build/executor/http-client.js +76 -4
  41. package/build/executor/job-errors.js +48 -0
  42. package/build/executor/job-runner.js +1294 -26
  43. package/build/executor/job-types.js +10 -2
  44. package/build/executor/preflight.js +99 -0
  45. package/build/executor/process-containment.js +756 -0
  46. package/build/executor/process.js +235 -5
  47. package/build/executor/prompt-spec.js +262 -1
  48. package/build/executor/provider-checks.js +69 -0
  49. package/build/executor/rebase-finalization.js +351 -0
  50. package/build/executor/reconcile-overlap-job.js +77 -84
  51. package/build/executor/required-verification.js +313 -0
  52. package/build/executor/required-work-ledger.js +1121 -0
  53. package/build/executor/results.js +142 -0
  54. package/build/executor/resume-pre-spawn.js +87 -4
  55. package/build/executor/runner.js +145 -0
  56. package/build/executor/test-declaration.js +20 -0
  57. package/build/executor/types.js +12 -0
  58. package/build/executor/worker-command.js +49 -13
  59. package/build/executor/worker-finalization.js +88 -11
  60. package/build/executor/worker-guard-hook.js +29 -4
  61. package/build/executor/worktree.js +13 -0
  62. package/build/executor-launch.js +658 -0
  63. package/build/index.js +537 -943
  64. package/build/install-bridge-conductor.js +231 -41
  65. package/build/merge-pull-request.js +1 -1
  66. package/build/pipelines.generated.js +8 -7
  67. package/build/plan-discovery.js +137 -0
  68. package/build/plan-epic-implementer-eligibility.js +2 -2
  69. package/build/plane/cli.js +37 -3
  70. package/build/plane/member-logs.js +3 -0
  71. package/build/plane/member-roster.js +7 -1
  72. package/build/plane/shutdown.js +59 -1
  73. package/build/plane/supervisor.js +142 -0
  74. package/build/readme.generated.js +1 -1
  75. package/build/scripts/executor-protocol-contract-driver.js +13 -3
  76. package/build/setup-epic.js +28 -17
  77. package/build/version.generated.js +3 -3
  78. package/build/worker-guard-hook-bin.js +2 -2
  79. package/docs/CONDUCTOR.md +116 -2
  80. 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 (hook !== undefined &&
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 ONE-TIME unattended prerequisites plus the aggregate the
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
- unattendedConsent: "set `unattended_epic_implementer_allowed` for this repository (docs/claude/account-settings-operator-runbook.md), or run setup with --attended.",
193
- unattendedNotifyDefault: "supply a --policy-file declaring notify.webhook_url, or run setup with --attended; an unattended run must have a working escalation channel, and notify.local_sink is not one.",
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 through the account-settings runbook; setup never grants 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: "unattended_epic_implementer_allowed is NOT set — an unattended run will be refused before it is created",
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
- id: "unattended-notify-default",
503
- status: "warn",
504
- 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",
505
- remediation: SERVER_REMEDIATIONS.unattendedNotifyDefault,
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",