@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
@@ -106,10 +106,16 @@
106
106
  * The command does NOT persist a repository-level review policy: that is
107
107
  * BAPI-694's. The policy selected here is per-run and is reported as such.
108
108
  */
109
- import { bootstrapConductorSupervisorDefaults, ConductorBridgeApiError, fetchConductorReadiness, } from "./conductor/bridge-api-client.js";
109
+ import { bootstrapConductorSupervisorDefaults, classifySupervisorBootstrapFailure, fetchConductorReadiness, previewConductorSupervisorBootstrap, } from "./conductor/bridge-api-client.js";
110
110
  import { CONDUCTOR_OPERATOR_RUNBOOK_POINTER, EXECUTOR_PROVISIONING_GUIDANCE, conductorInstallDoctorExitCode, conductorInstallDoctorHasFatal, formatConductorInstallDoctorReport, } from "./conductor/install-doctor.js";
111
111
  import { describeNativeLedgerAvailability, } from "./conductor/doctor.js";
112
112
  import { CONDUCTOR_BIN_UNRESOLVED_ERROR, MANAGED_HOOK_NAMES, } from "./conductor/git-hooks.js";
113
+ import os from "node:os";
114
+ import path from "node:path";
115
+ import { readFile as readFileAsync, stat as statAsync } from "node:fs/promises";
116
+ import { bindEpicLaunchCredential, readStoredBinding, } from "./credential-store.js";
117
+ import { EPIC_LAUNCH_EXECUTION_MODES, EPIC_LAUNCH_PROFILE_NAME_PATTERN, EPIC_LAUNCH_PROFILE_VERSION, createNodeEpicLaunchStoreDeps, readEpicLaunchProfile, renderEpicLaunchCommand, resolveEpicLaunchTarget, writeEpicLaunchProfile, } from "./epic-launch-profile.js";
118
+ import { DEFAULT_BAPI_BASE_URL } from "./executor/credentials.js";
113
119
  import { MCP_PACKAGE_NAME } from "./mcp-identity.js";
114
120
  import { CLAUDE_REVIEW_WORKFLOW_RELPATH, renderClaudeReviewWorkflow, } from "./claude-review-workflow.js";
115
121
  import { formatExecutorInstallPreflight, } from "./executor/install-preflight.js";
@@ -161,6 +167,25 @@ export function getInstallBridgeConductorUsage() {
161
167
  " --secret-name <NAME> claude-review workflow: OAuth token secret NAME",
162
168
  " --branches a,b claude-review workflow: PR branch triggers",
163
169
  " --skip-workflow Never offer the claude-review workflow",
170
+ " --replace-done-gate Replace an EXISTING done gate with the CI-only gate",
171
+ " built from --required-check. Without it, a repeat",
172
+ " install keeps the stored gate and only adds missing",
173
+ " checks. Any requirement a replacement would remove",
174
+ " is previewed and needs its own exact approval.",
175
+ " --launch-profile <name> (BAPI-1260) Opt in to the reusable epic launch: after",
176
+ " every mandatory prerequisite is verified, persist a",
177
+ " host-local profile and print the ONE drive-epic",
178
+ " command that selects it (runs in a fresh shell).",
179
+ " Optional for production — drive-epic targets it with",
180
+ " no profile. Needed for local or durable-host targets.",
181
+ " --base-url <url> Explicit Bridge API target for --launch-profile.",
182
+ " Required there (or BAPI_BASE_URL): guided setup",
183
+ " approves one named server and has no default.",
184
+ " --execution-mode <mode> local | durable (default: durable with",
185
+ " --executor-id, otherwise local).",
186
+ " --policy-file <path> Run policy the profile references; an unattended",
187
+ " run needs notify.webhook_url reachable from the",
188
+ " API server (notify.local_sink is not enough).",
164
189
  " --dry-run Diagnose and preview only. Performs NO writes:",
165
190
  " no POST, no file write, no host MCP config write,",
166
191
  " no Git hook write, no install-state or capability",
@@ -244,6 +269,7 @@ export function parseInstallBridgeConductorArgs(argv) {
244
269
  let repo;
245
270
  let dryRun = false;
246
271
  let skipWorkflow = false;
272
+ let replaceDoneGate = false;
247
273
  let reviewPolicy = DEFAULT_CONDUCTOR_REVIEW_POLICY;
248
274
  let reviewPolicyExplicit = false;
249
275
  let epicKey;
@@ -255,6 +281,10 @@ export function parseInstallBridgeConductorArgs(argv) {
255
281
  let secretName;
256
282
  let branches;
257
283
  const requiredChecks = [];
284
+ let launchProfile;
285
+ let baseUrl;
286
+ let executionMode;
287
+ let policyFile;
258
288
  const takeValue = (arg, inline, next) => {
259
289
  if (inline !== undefined) {
260
290
  if (inline.trim().length === 0)
@@ -282,6 +312,11 @@ export function parseInstallBridgeConductorArgs(argv) {
282
312
  return { status: "error", message: "--skip-workflow takes no value" };
283
313
  skipWorkflow = true;
284
314
  break;
315
+ case "--replace-done-gate":
316
+ if (inline !== undefined)
317
+ return { status: "error", message: "--replace-done-gate takes no value" };
318
+ replaceDoneGate = true;
319
+ break;
285
320
  case "--repo-wide":
286
321
  if (inline !== undefined)
287
322
  return { status: "error", message: "--repo-wide takes no value" };
@@ -297,7 +332,11 @@ export function parseInstallBridgeConductorArgs(argv) {
297
332
  case "--epic-run-id":
298
333
  case "--allowed-bot":
299
334
  case "--secret-name":
300
- case "--branches": {
335
+ case "--branches":
336
+ case "--launch-profile":
337
+ case "--base-url":
338
+ case "--execution-mode":
339
+ case "--policy-file": {
301
340
  const taken = takeValue(flag, inline, argv[i + 1]);
302
341
  if (!taken.ok)
303
342
  return { status: "error", message: taken.error };
@@ -322,6 +361,28 @@ export function parseInstallBridgeConductorArgs(argv) {
322
361
  allowedBot = value;
323
362
  else if (flag === "--secret-name")
324
363
  secretName = value;
364
+ else if (flag === "--launch-profile") {
365
+ if (!EPIC_LAUNCH_PROFILE_NAME_PATTERN.test(value)) {
366
+ return {
367
+ status: "error",
368
+ message: "--launch-profile must match [a-z0-9][a-z0-9._-]{0,62}.",
369
+ };
370
+ }
371
+ launchProfile = value;
372
+ }
373
+ else if (flag === "--base-url")
374
+ baseUrl = value;
375
+ else if (flag === "--policy-file")
376
+ policyFile = value;
377
+ else if (flag === "--execution-mode") {
378
+ if (!EPIC_LAUNCH_EXECUTION_MODES.includes(value)) {
379
+ return {
380
+ status: "error",
381
+ message: `Invalid --execution-mode '${value}'. Expected one of: ${EPIC_LAUNCH_EXECUTION_MODES.join(", ")}`,
382
+ };
383
+ }
384
+ executionMode = value;
385
+ }
325
386
  else if (flag === "--branches") {
326
387
  const list = splitList(value);
327
388
  if (list.length === 0)
@@ -404,6 +465,20 @@ export function parseInstallBridgeConductorArgs(argv) {
404
465
  "--executor-id.",
405
466
  };
406
467
  }
468
+ // BAPI-1260 — the launch-profile inputs only mean something together.
469
+ if (launchProfile === undefined && (baseUrl !== undefined || executionMode !== undefined)) {
470
+ return {
471
+ status: "error",
472
+ message: "--base-url and --execution-mode configure an epic launch profile, so they require --launch-profile.",
473
+ };
474
+ }
475
+ if (executionMode === "durable" && executorId === undefined) {
476
+ return {
477
+ status: "error",
478
+ message: "--execution-mode durable requires --executor-id: a durable launch is served by a provisioned " +
479
+ "executor service, and this command proves its server-observed handshake before approving it.",
480
+ };
481
+ }
407
482
  return {
408
483
  status: "ok",
409
484
  options: {
@@ -418,12 +493,38 @@ export function parseInstallBridgeConductorArgs(argv) {
418
493
  secretName,
419
494
  branches,
420
495
  skipWorkflow,
496
+ ...(replaceDoneGate ? { replaceDoneGate: true } : {}),
421
497
  executorId,
422
498
  ...(epicRunIds.length > 0 ? { epicRunIds } : {}),
423
499
  ...(repoWide ? { repoWide: true } : {}),
500
+ ...(launchProfile ? { launchProfile } : {}),
501
+ ...(baseUrl ? { baseUrl } : {}),
502
+ ...(launchProfile
503
+ ? { executionMode: executionMode ?? (executorId !== undefined ? "durable" : "local") }
504
+ : {}),
505
+ ...(policyFile ? { policyFile } : {}),
424
506
  },
425
507
  };
426
508
  }
509
+ /** Real host-local implementations of the launch-profile seams. */
510
+ export function createDefaultLaunchProfileDeps(env) {
511
+ const store = createNodeEpicLaunchStoreDeps(env);
512
+ return {
513
+ hostname: () => os.hostname(),
514
+ nowIso: () => new Date().toISOString(),
515
+ readProfile: (name) => readEpicLaunchProfile(name, store),
516
+ writeProfile: (profile) => writeEpicLaunchProfile(profile, store),
517
+ readCredentialBinding: (repoName) => readStoredBinding(`bapi:${repoName}`, {
518
+ env,
519
+ homedir: store.homedir,
520
+ platform: store.platform,
521
+ readFile: store.readFile,
522
+ stat: (p) => statAsync(p),
523
+ }),
524
+ bindCredential: (repoName, baseUrl) => bindEpicLaunchCredential(repoName, baseUrl, store),
525
+ readPolicyFile: (absolutePath) => readFileAsync(absolutePath, "utf-8"),
526
+ };
527
+ }
427
528
  // ---------------------------------------------------------------------------
428
529
  // Bounded readiness observation (BAPI-779)
429
530
  // ---------------------------------------------------------------------------
@@ -495,6 +596,89 @@ export async function observeExecutorReadiness(access, fetchJson, deps, options
495
596
  // Runner
496
597
  // ---------------------------------------------------------------------------
497
598
  const CONSENT_YES = new Set(["y", "yes"]);
599
+ /** Fixed, value-free operator text for each bootstrap failure (BAPI-1288). */
600
+ const BOOTSTRAP_FAILURE_TEXT = Object.freeze({
601
+ "unsupported-protocol": "the Bridge API server does not support the gate-change preview protocol " +
602
+ "(it predates BAPI-1288) or answered a response this installer cannot verify. " +
603
+ "Upgrade the server before re-running; this installer never falls back to the " +
604
+ "older bootstrap that replaced configured gates.",
605
+ "approval-required": "the server requires an approved preview for this change. Re-run to preview it again.",
606
+ "stale-preview": "the project defaults changed since the preview, or the approval expired. " +
607
+ "Re-run to preview the current state again.",
608
+ "unknown-stored-state": "a stored project-default row cannot be read with certainty, so no change to it " +
609
+ "can be evaluated. See the account-settings operator runbook " +
610
+ "(unknown_stored_state) before re-running.",
611
+ "non-relaxable": "the change would turn auto-merge on or drop the human merge-approval requirement, " +
612
+ "which no gate-change approval can authorize.",
613
+ "invalid-proposal": "the server rejected the proposed configuration as unreadable.",
614
+ "state-unavailable": "the server could not read or lock the project defaults right now. Retry shortly.",
615
+ unauthorized: "the API key was rejected, or it is not an admin key for this repository.",
616
+ network: "the Bridge API could not be reached.",
617
+ timeout: "the Bridge API did not answer in time.",
618
+ server: "the Bridge API reported an internal error.",
619
+ rejected: "the Bridge API rejected the request.",
620
+ });
621
+ function reportBootstrapFailure(deps, stage, kind) {
622
+ const label = stage === "preview" ? "Bootstrap preview failed" : "Bootstrap failed";
623
+ deps.errorLog(`${label} (${kind}): ${BOOTSTRAP_FAILURE_TEXT[kind]}`);
624
+ deps.errorLog(stage === "preview"
625
+ ? "No server write was attempted. No workflow or verification step was run."
626
+ : "Every configured requirement is unchanged. No workflow or verification step was run.");
627
+ }
628
+ /** Obligation tokens rendered as short fixed phrases — never server free text. */
629
+ const OBLIGATION_LABELS = Object.freeze({
630
+ ci_check_green: "required CI check green",
631
+ sticky_verdict_approved: "claude-review sticky verdict approved",
632
+ native_review_approved: "native GitHub review approved",
633
+ approval_minimum: "minimum approval count",
634
+ verdictless_review_parks: "verdictless review parks for a human",
635
+ });
636
+ function formatObligations(tokens) {
637
+ return tokens.length === 0 ? "none" : tokens.map((t) => OBLIGATION_LABELS[t] ?? t).join(", ");
638
+ }
639
+ function renderGateChangeEffect(deps, effect) {
640
+ deps.log(` - ${effect.row} ${effect.requirement}: ${effect.change}`);
641
+ deps.log(` ${effect.description}`);
642
+ deps.log(` removes: ${formatObligations(effect.removed_obligations)}`);
643
+ if (effect.replacement_obligations.length > 0) {
644
+ deps.log(` replaced by: ${formatObligations(effect.replacement_obligations)}`);
645
+ }
646
+ if (effect.removed_ci_check_count > 0) {
647
+ const refs = effect.removed_ci_check_refs.join(", ");
648
+ deps.log(` CI checks no longer required: ${effect.removed_ci_check_count}` +
649
+ (refs ? ` (refs: ${refs})` : ""));
650
+ }
651
+ if (effect.stored_min_approvals !== null || effect.proposed_min_approvals !== null) {
652
+ deps.log(` approval minimum: ${effect.stored_min_approvals ?? "none"} → ` +
653
+ `${effect.proposed_min_approvals ?? "none"}`);
654
+ }
655
+ }
656
+ function renderBootstrapPreview(deps, preview) {
657
+ deps.log(" Server preview (nothing written yet):");
658
+ deps.log(` supervisor setup: ${preview.setup_outcome}`);
659
+ deps.log(` supervisor config: ${preview.config_outcome}`);
660
+ if (preview.changed_field_names.length > 0) {
661
+ deps.log(` fields that would change: ${preview.changed_field_names.join(", ")}`);
662
+ }
663
+ else {
664
+ deps.log(" nothing would change — the configured project defaults are already in place.");
665
+ }
666
+ if (preview.approval_required) {
667
+ deps.log("");
668
+ deps.log(" This change REMOVES or WEAKENS configured requirements:");
669
+ for (const effect of preview.effects)
670
+ renderGateChangeEffect(deps, effect);
671
+ deps.log(" CI check refs are opaque; compare them with GET /jira/epic-runs/supervisor-setup/defaults/.");
672
+ }
673
+ }
674
+ function renderBootstrapOutcome(deps, response) {
675
+ const describe = (outcome) => outcome === "preserved" ? "preserved (nothing written)" : String(outcome);
676
+ deps.log(` supervisor setup: ${describe(response.setup_outcome)}`);
677
+ deps.log(` supervisor config: ${describe(response.config_outcome)}`);
678
+ if (response.audited_field_names.length > 0) {
679
+ deps.log(` committed fields: ${response.audited_field_names.join(", ")}`);
680
+ }
681
+ }
498
682
  async function confirm(deps, promptText) {
499
683
  // A missing prompt seam means no TTY. Silence is NOT consent.
500
684
  if (!deps.promptLine)
@@ -760,6 +944,13 @@ export const CONDUCTOR_PROFILE_EMPTY_MERGE_REMEDIATION = `run \`npx ${MCP_PACKAG
760
944
  /** Remediation printed whenever the managed conductor hooks were not installed. */
761
945
  export const CONDUCTOR_HOOKS_MANUAL_REMEDIATION = "re-run `install conductor` and accept the local-observability consent, or run " +
762
946
  "`epic-implementer install-git-hooks` directly.";
947
+ /**
948
+ * Remediation for a non-fatal hook refusal (BAPI-1290): the hooks need a
949
+ * persistent install to embed, and the repair is to create one and reinstall.
950
+ */
951
+ export const CONDUCTOR_HOOKS_DURABLE_REMEDIATION = `install the CLI persistently (\`npm i -g ${MCP_PACKAGE_NAME}\`), then run ` +
952
+ "`epic-implementer install-git-hooks` (or re-run `install conductor` and accept the " +
953
+ "local-observability consent).";
763
954
  /**
764
955
  * Phase: tool visibility.
765
956
  *
@@ -860,11 +1051,33 @@ async function runConductorLocalObservabilityPhase(deps, local) {
860
1051
  // `reason` is already a sanitized sentence. This one IS a command failure —
861
1052
  // a hook that cannot resolve its binary is a dead hook, and the whole point
862
1053
  // of that ticket is that it no longer gets written silently.
1054
+ // BAPI-1290: the refusal names its fixed classification (e.g.
1055
+ // `ephemeral-cache` for an npx-cache runtime) or its per-hook planning
1056
+ // reasons. It is an OPTIONAL-hook installation refusal — distinct from a
1057
+ // setup-permission stop and from engine admission, which it never gates.
863
1058
  deps.errorLog(result.error === CONDUCTOR_BIN_UNRESOLVED_ERROR
864
- ? ` hook installation refused — ${result.reason}`
865
- : " hook installation refused.");
866
- manualActions.push(`local observability: ${CONDUCTOR_HOOKS_MANUAL_REMEDIATION}`);
867
- fatal = true;
1059
+ ? ` hook installation refused (${result.classification}) — ${result.reason}`
1060
+ : ` hook installation refused — ${result.reason}`);
1061
+ deps.errorLog(" existing hooks were left unchanged; optional local hook delivery is unavailable " +
1062
+ "(engine admission is unaffected).");
1063
+ // Only BAPI-772's broken-package case stays fatal: the executing artifact
1064
+ // ships no hook binary at all. Every other refusal — an npx-cache runtime
1065
+ // (the documented `npx … install conductor` path), an unqualified or
1066
+ // unloadable runtime, an unsafe existing hook — is a manual action, and
1067
+ // the installer continues to setup-epic verification and executor
1068
+ // provisioning. Optional hooks must never stop the phases that do gate
1069
+ // a run.
1070
+ // A refusal without a classification (an older or injected result shape)
1071
+ // is read as `missing` — fail-closed to BAPI-772's fatal contract.
1072
+ const brokenPackage = result.error === CONDUCTOR_BIN_UNRESOLVED_ERROR &&
1073
+ (result.classification ?? "missing") === "missing";
1074
+ if (brokenPackage) {
1075
+ manualActions.push(`local observability: ${CONDUCTOR_HOOKS_MANUAL_REMEDIATION}`);
1076
+ fatal = true;
1077
+ }
1078
+ else {
1079
+ manualActions.push(`local observability: ${CONDUCTOR_HOOKS_DURABLE_REMEDIATION}`);
1080
+ }
868
1081
  }
869
1082
  else if (result !== null && result.ok) {
870
1083
  if (!result.is_worktree) {
@@ -882,6 +1095,9 @@ async function runConductorLocalObservabilityPhase(deps, local) {
882
1095
  ? ` ${hook.name}: ${hook.action}`
883
1096
  : ` ${hook.name}: ${hook.action} — warning: ${hook.warning}`);
884
1097
  }
1098
+ if (result.partial) {
1099
+ deps.log(" the hook update is PARTIAL — one managed hook was published and the other was not.");
1100
+ }
885
1101
  // Sanitized: the installer's own warnings are secret-free sentences, and
886
1102
  // hook CONTENT, env, and caught errors are never rendered.
887
1103
  for (const warning of result.warnings)
@@ -892,11 +1108,21 @@ async function runConductorLocalObservabilityPhase(deps, local) {
892
1108
  // provisioned observability, and the missing reference-transaction hook
893
1109
  // is precisely the one that stops half the events from ever being
894
1110
  // emitted.
1111
+ //
1112
+ // BAPI-1290: each hook's `usable` is the installer's shared verdict —
1113
+ // published over a matching snapshot, executable, and backed by the
1114
+ // qualified persistent runtime — and the installer's own `complete`
1115
+ // flag must agree. Anything less is not recorded.
895
1116
  const usable = (name) => {
896
1117
  const hook = result.installed.find((h) => h.name === name);
897
- return hook !== undefined && hook.action !== "skipped" && hook.warning === undefined;
1118
+ return (hook !== undefined &&
1119
+ hook.usable === true &&
1120
+ hook.action !== "skipped" &&
1121
+ hook.warning === undefined);
898
1122
  };
899
- const complete = MANAGED_HOOK_NAMES.every((name) => usable(name)) && result.warnings.length === 0;
1123
+ const complete = result.complete === true &&
1124
+ MANAGED_HOOK_NAMES.every((name) => usable(name)) &&
1125
+ result.warnings.length === 0;
900
1126
  if (complete) {
901
1127
  const recorded = await local.recordCapability("conductor_local_observability");
902
1128
  if (!recorded.ok) {
@@ -948,6 +1174,277 @@ async function runConductorLocalObservabilityPhase(deps, local) {
948
1174
  /**
949
1175
  * Run the nested conductor installer. Returns the process exit code.
950
1176
  */
1177
+ // ---------------------------------------------------------------------------
1178
+ // Epic launch profile phase (BAPI-1260)
1179
+ // ---------------------------------------------------------------------------
1180
+ /** The packaged CLI's own npm spec, used when no absolute build path resolves. */
1181
+ const LAUNCH_NPM_CHANNEL_SPEC = `${MCP_PACKAGE_NAME}@latest`;
1182
+ /**
1183
+ * The stable invocation a profile records. A durable profile requires the
1184
+ * absolute packaged path (the same resolver the service unit uses, which already
1185
+ * refuses npx caches); a local one may fall back to the published package on
1186
+ * a named channel. Never a transient `_npx` cache path.
1187
+ */
1188
+ function resolveLaunchRuntime(executor, mode) {
1189
+ const invocation = executor?.resolveInvocation();
1190
+ if (invocation?.ok) {
1191
+ const entryPath = invocation.invocation.prefixArgs[0];
1192
+ if (typeof entryPath === "string" && entryPath.length > 0) {
1193
+ return {
1194
+ ok: true,
1195
+ runtime: { kind: "packaged", executable: invocation.invocation.executable, entryPath },
1196
+ };
1197
+ }
1198
+ }
1199
+ if (mode === "durable") {
1200
+ return {
1201
+ ok: false,
1202
+ error: invocation && !invocation.ok
1203
+ ? invocation.error
1204
+ : "no absolute packaged CLI path could be resolved for a durable launch.",
1205
+ };
1206
+ }
1207
+ return { ok: true, runtime: { kind: "npm-channel", spec: LAUNCH_NPM_CHANNEL_SPEC } };
1208
+ }
1209
+ /** Loopback / link-local / private hostnames the API server cannot reach from elsewhere. */
1210
+ function isHostLocalHostname(hostname) {
1211
+ const h = hostname.replace(/^\[|\]$/g, "").toLowerCase();
1212
+ return (h === "localhost" ||
1213
+ h.endsWith(".localhost") ||
1214
+ h === "::1" ||
1215
+ /^127\./.test(h) ||
1216
+ /^0\.0\.0\.0$/.test(h) ||
1217
+ /^169\.254\./.test(h));
1218
+ }
1219
+ /**
1220
+ * Assess the policy's notification destination from the API SERVER's network
1221
+ * perspective. Presence and reachability SHAPE only — the authoritative
1222
+ * delivery check is the server's own `verify_notify_webhook` at approval, and a
1223
+ * failure there holds activation. A launcher-local listener can never pass
1224
+ * here for a remote target: the server cannot reach this machine's loopback.
1225
+ */
1226
+ function assessNotification(policy, attended, targetBaseUrl, readiness) {
1227
+ if (attended) {
1228
+ return { status: "not-applicable", detail: "the referenced policy is attended; no escalation webhook is required." };
1229
+ }
1230
+ const notify = policy && typeof policy.notify === "object" && policy.notify !== null
1231
+ ? policy.notify
1232
+ : null;
1233
+ const url = typeof notify?.webhook_url === "string" ? notify.webhook_url.trim() : "";
1234
+ if (url === "") {
1235
+ // BAPI-1274 — with no webhook declared, the account's email address is the
1236
+ // escalation channel when the server reports one is present (never the
1237
+ // address itself). A declared webhook stays authoritative and is assessed
1238
+ // below exactly as before; `notify.local_sink` still satisfies nothing.
1239
+ if (readiness?.unattended?.escalation_email_available === true) {
1240
+ return {
1241
+ status: "ok",
1242
+ detail: "No webhook declared; escalation alerts will be emailed to the account's email address",
1243
+ };
1244
+ }
1245
+ return {
1246
+ status: "missing",
1247
+ detail: "an unattended launch needs notify.webhook_url in its --policy-file, or an email " +
1248
+ "address on the account so escalation alerts can be emailed" +
1249
+ (notify && "local_sink" in notify ? " (notify.local_sink is not an escalation channel)." : "."),
1250
+ };
1251
+ }
1252
+ let webhook;
1253
+ let target;
1254
+ try {
1255
+ webhook = new URL(url);
1256
+ target = new URL(targetBaseUrl);
1257
+ }
1258
+ catch {
1259
+ return { status: "missing", detail: "notify.webhook_url is not an absolute URL." };
1260
+ }
1261
+ if (webhook.protocol !== "https:" && webhook.protocol !== "http:") {
1262
+ return { status: "missing", detail: "notify.webhook_url must be an http(s) URL." };
1263
+ }
1264
+ if (isHostLocalHostname(webhook.hostname) && !isHostLocalHostname(target.hostname)) {
1265
+ return {
1266
+ status: "missing",
1267
+ detail: "notify.webhook_url points at this machine's loopback, which the API server at the selected " +
1268
+ "target cannot reach. Use an API-reachable, durable notification service — a terminal-held " +
1269
+ "local listener cannot establish server reachability.",
1270
+ };
1271
+ }
1272
+ return {
1273
+ status: "ok",
1274
+ detail: "notify.webhook_url is declared and addressable from the selected API server; the server " +
1275
+ "delivers a verification to it at approval, and a failure there holds activation.",
1276
+ };
1277
+ }
1278
+ /**
1279
+ * Verify every mandatory launch prerequisite at the selected target, then —
1280
+ * and only then — persist the profile and print the one command that selects
1281
+ * it. Any unproved prerequisite yields `incomplete-setup` with the reason, and
1282
+ * no profile is written, so a profile's existence means setup was sufficient.
1283
+ *
1284
+ * Consent is READ from the server (`unattended.conductor_allowed`), never
1285
+ * written: ordinary configuration writes cannot self-grant unattended
1286
+ * authority, and nothing in this phase tries to.
1287
+ */
1288
+ async function runLaunchProfilePhase(deps, options, inputs) {
1289
+ const name = options.launchProfile;
1290
+ const mode = options.executionMode ?? "local";
1291
+ const lp = deps.launchProfiles ?? createDefaultLaunchProfileDeps(deps.env);
1292
+ const repoName = inputs.access.repoName;
1293
+ const gaps = [];
1294
+ deps.log("");
1295
+ deps.log(`Epic launch profile '${name}' — verification…`);
1296
+ deps.log(` target: ${inputs.baseUrl}`);
1297
+ deps.log(` repository: ${repoName}`);
1298
+ deps.log(` mode: ${mode}`);
1299
+ // (1) Access at THIS target — only a server answer proves the key works there.
1300
+ if (inputs.readiness === null) {
1301
+ gaps.push("the selected target's conductor readiness could not be read with this repository's credential.");
1302
+ }
1303
+ // (2) The policy reference and what it implies.
1304
+ let policy = null;
1305
+ let policyPath = null;
1306
+ if (options.policyFile !== undefined) {
1307
+ policyPath = path.resolve(deps.cwd, options.policyFile);
1308
+ try {
1309
+ const parsed = JSON.parse(await lp.readPolicyFile(policyPath));
1310
+ if (typeof parsed === "object" && parsed !== null && !Array.isArray(parsed)) {
1311
+ policy = parsed;
1312
+ }
1313
+ else {
1314
+ gaps.push("the --policy-file does not hold a JSON object.");
1315
+ }
1316
+ }
1317
+ catch {
1318
+ gaps.push("the --policy-file could not be read as JSON.");
1319
+ }
1320
+ }
1321
+ const attended = policy?.posture === "attended";
1322
+ // (3) Consent — the server's administrative authority, read not written.
1323
+ const consent = inputs.readiness?.unattended?.conductor_allowed;
1324
+ if (attended) {
1325
+ deps.log(" consent: not applicable (attended policy)");
1326
+ }
1327
+ else if (consent === true) {
1328
+ deps.log(" consent: confirmed by the server (unattended_epic_implementer_allowed)");
1329
+ }
1330
+ else {
1331
+ deps.log(` consent: ${consent === false ? "NOT granted" : "could not be verified"}`);
1332
+ gaps.push(consent === false
1333
+ ? "unattended consent is off for this repository; turn on 'Allow unattended Epic " +
1334
+ "Implementer runs' on the repository's Setup page in the Bridge admin panel " +
1335
+ "(if it is already on, contact Bridge support) — this command never writes it."
1336
+ : "unattended consent could not be verified at the selected target (older server or unreadable report).");
1337
+ }
1338
+ // (4) Notification, from the server's network perspective.
1339
+ const notification = assessNotification(policy, attended, inputs.baseUrl, inputs.readiness);
1340
+ deps.log(` notify: ${notification.status} — ${notification.detail}`);
1341
+ if (notification.status === "missing")
1342
+ gaps.push(notification.detail);
1343
+ // (5) The actor that consumes the work.
1344
+ if (mode === "durable") {
1345
+ deps.log(` actor: ${inputs.actorObserved ? "server-observed executor handshake" : "NOT observed"}`);
1346
+ if (!inputs.actorObserved) {
1347
+ gaps.push("no executor was observed by the server for this repository; a written unit, a started " +
1348
+ "service, or a local process is not proof. Provision it with --executor-id and let the " +
1349
+ "bounded observation complete.");
1350
+ }
1351
+ }
1352
+ else {
1353
+ deps.log(" actor: local — drive-epic starts this repository's plane and scoped lanes itself");
1354
+ }
1355
+ // (6) A stable runtime invocation.
1356
+ if (!inputs.runtime.ok) {
1357
+ gaps.push(`the packaged CLI invocation is not stable: ${inputs.runtime.error}`);
1358
+ }
1359
+ else {
1360
+ deps.log(` runtime: ${inputs.runtime.runtime.kind === "packaged" ? "packaged absolute path" : inputs.runtime.runtime.spec}`);
1361
+ }
1362
+ // (7) The existing profile: a changed binding is re-approved explicitly.
1363
+ const existing = await lp.readProfile(name);
1364
+ if (existing.ok) {
1365
+ const prior = existing.profile;
1366
+ const changed = prior.baseUrl !== inputs.baseUrl || prior.repoName !== repoName || prior.executionMode !== mode;
1367
+ if (changed) {
1368
+ deps.log("");
1369
+ deps.log(` profile '${name}' exists with a DIFFERENT binding:`);
1370
+ deps.log(` current: ${prior.baseUrl} repo=${prior.repoName} mode=${prior.executionMode}`);
1371
+ deps.log(` proposed: ${inputs.baseUrl} repo=${repoName} mode=${mode}`);
1372
+ const rebind = await confirm(deps, `Re-bind profile '${name}' to the proposed target? [y/N]: `);
1373
+ if (!rebind)
1374
+ gaps.push(`re-binding profile '${name}' was declined; it was left unchanged.`);
1375
+ }
1376
+ }
1377
+ else if (existing.kind !== "profile-missing") {
1378
+ // Unreadable or from a newer/older schema: reported, and replaced only
1379
+ // through the same consented write below.
1380
+ deps.log(` existing profile: ${existing.message}`);
1381
+ }
1382
+ // (8) The credential binding — the explicit one-time upgrade path.
1383
+ if (gaps.length === 0) {
1384
+ const bound = await lp.readCredentialBinding(repoName);
1385
+ if (bound === inputs.baseUrl) {
1386
+ deps.log(` credential: bapi:${repoName} is already approved for this target`);
1387
+ }
1388
+ else {
1389
+ deps.log(bound === null
1390
+ ? ` credential: bapi:${repoName} is not yet approved for any endpoint (legacy repo-only binding)`
1391
+ : ` credential: bapi:${repoName} is approved for a DIFFERENT endpoint`);
1392
+ const approve = await confirm(deps, `Approve the stored credential bapi:${repoName} for ${inputs.baseUrl} only? [y/N]: `);
1393
+ if (!approve) {
1394
+ gaps.push("the credential binding was not approved, so a launch could not use the stored key.");
1395
+ }
1396
+ else {
1397
+ const result = await lp.bindCredential(repoName, inputs.baseUrl);
1398
+ if (!result.ok)
1399
+ gaps.push(`the credential binding could not be written: ${result.error}`);
1400
+ else
1401
+ deps.log(` credential: ${result.action} — approved for this target only`);
1402
+ }
1403
+ }
1404
+ }
1405
+ if (gaps.length > 0 || !inputs.runtime.ok) {
1406
+ deps.log("");
1407
+ deps.log(` incomplete — profile '${name}' was NOT written:`);
1408
+ for (const gap of gaps)
1409
+ deps.log(` → ${gap}`);
1410
+ deps.log(` re-run: npx -y ${MCP_PACKAGE_NAME} install conductor --launch-profile ${name} ` +
1411
+ `--base-url ${inputs.baseUrl} --required-check <name> ...`);
1412
+ return { complete: false };
1413
+ }
1414
+ const write = await confirm(deps, `Write epic launch profile '${name}' (references only, no secrets)? [y/N]: `);
1415
+ if (!write) {
1416
+ deps.log(" declined — no profile was written.");
1417
+ return { complete: false };
1418
+ }
1419
+ const profile = {
1420
+ version: EPIC_LAUNCH_PROFILE_VERSION,
1421
+ name,
1422
+ baseUrl: inputs.baseUrl,
1423
+ repoName,
1424
+ executionMode: mode,
1425
+ host: lp.hostname(),
1426
+ runtime: inputs.runtime.runtime,
1427
+ policyRef: policyPath !== null ? { kind: "file", path: policyPath } : null,
1428
+ credentialRef: { store: "bridge-credentials", target: `bapi:${repoName}` },
1429
+ approvedAt: lp.nowIso(),
1430
+ };
1431
+ const written = await lp.writeProfile(profile);
1432
+ if (!written.ok) {
1433
+ deps.errorLog(` profile write failed: ${written.message}`);
1434
+ return { complete: false };
1435
+ }
1436
+ deps.log(` ${written.action} ${written.path}`);
1437
+ deps.log("");
1438
+ deps.log("Launch command (fresh shell; no exports needed):");
1439
+ deps.log(` ${renderEpicLaunchCommand(profile, {
1440
+ epicKey: options.epicKey ?? "<EPIC>",
1441
+ planFile: options.planFile ? path.resolve(deps.cwd, options.planFile) : "<path>",
1442
+ })
1443
+ // Placeholders are for the operator to replace, not literal arguments.
1444
+ .replace("'<EPIC>'", "<EPIC>")
1445
+ .replace("'<path>'", "<path>")}`);
1446
+ return { complete: true };
1447
+ }
951
1448
  export async function runInstallBridgeConductorCli(argv, deps) {
952
1449
  const parsed = parseInstallBridgeConductorArgs(argv);
953
1450
  if (parsed.status === "help") {
@@ -962,12 +1459,31 @@ export async function runInstallBridgeConductorCli(argv, deps) {
962
1459
  }
963
1460
  const options = parsed.options;
964
1461
  // ---- Base URL: normalized ONCE, confirmed BEFORE any network call ------
965
- const baseUrlResult = deps.resolveBaseUrl(deps.env);
966
- if (!baseUrlResult.ok) {
967
- deps.errorLog(baseUrlResult.error);
968
- return 1;
1462
+ // BAPI-1260: an epic launch profile has NO production default. Its target is
1463
+ // --base-url or BAPI_BASE_URL (they must agree), decided here before any
1464
+ // request; the hosted default below applies to the legacy flow only.
1465
+ let baseUrl;
1466
+ if (options.launchProfile !== undefined) {
1467
+ const target = resolveEpicLaunchTarget({
1468
+ flagBaseUrl: options.baseUrl,
1469
+ profile: null,
1470
+ env: deps.env,
1471
+ });
1472
+ if (!target.ok) {
1473
+ deps.errorLog(target.message);
1474
+ deps.errorLog("Nothing was sent anywhere and nothing was written.");
1475
+ return 1;
1476
+ }
1477
+ baseUrl = target.baseUrl;
1478
+ }
1479
+ else {
1480
+ const baseUrlResult = deps.resolveBaseUrl(deps.env);
1481
+ if (!baseUrlResult.ok) {
1482
+ deps.errorLog(baseUrlResult.error);
1483
+ return 1;
1484
+ }
1485
+ baseUrl = baseUrlResult.value;
969
1486
  }
970
- const baseUrl = baseUrlResult.value;
971
1487
  // ---- Consent #1: the resolved base URL ---------------------------------
972
1488
  // This consent gates the FIRST network request of any kind — including the
973
1489
  // read-only doctor readiness GET (which carries this repository's API key)
@@ -975,6 +1491,10 @@ export async function runInstallBridgeConductorCli(argv, deps) {
975
1491
  // BAPI_BASE_URL is unset, so the operator must see and confirm the target
976
1492
  // before the key is sent anywhere. No TTY means no consent means abort.
977
1493
  deps.log(`Bridge API base URL: ${baseUrl}`);
1494
+ if (options.launchProfile !== undefined) {
1495
+ deps.log(` explicit target for epic launch profile '${options.launchProfile}'` +
1496
+ (baseUrl === DEFAULT_BAPI_BASE_URL ? " — this is the PRODUCTION Bridge API, selected deliberately." : "."));
1497
+ }
978
1498
  const baseUrlOk = await confirm(deps, "Bootstrap against this URL? [y/N]: ");
979
1499
  if (!baseUrlOk) {
980
1500
  deps.log("Aborted — no network call (readiness GET or bootstrap POST) was attempted.");
@@ -1059,16 +1579,19 @@ export async function runInstallBridgeConductorCli(argv, deps) {
1059
1579
  if (options.dryRun) {
1060
1580
  deps.log("");
1061
1581
  deps.log("Dry run — no writes will be performed.");
1062
- deps.log(" Would POST /jira/epic-runs/supervisor-bootstrap with field NAMES:");
1582
+ deps.log(" Would PREVIEW, then POST /jira/epic-runs/supervisor-bootstrap with field NAMES:");
1063
1583
  // Field NAMES and a COUNT only — never the check values, which are
1064
1584
  // configuration content, and never the API key.
1065
1585
  deps.log(" - done_gate_config.required_checks " +
1066
1586
  `(${options.requiredChecks.length} check(s))`);
1067
- deps.log(" - review_source");
1068
1587
  if (options.reviewPolicyExplicit)
1069
1588
  deps.log(" - review_policy");
1589
+ if (options.replaceDoneGate)
1590
+ deps.log(" - replace_done_gate");
1070
1591
  deps.log(" - auto_merge_enabled");
1071
1592
  deps.log(" - merge_approval_required");
1593
+ deps.log(" Existing project defaults would be KEPT: missing checks are added, nothing is " +
1594
+ "removed without a previewed, separately approved change.");
1072
1595
  deps.log(` Would target base URL: ${baseUrl}`);
1073
1596
  deps.log(options.skipWorkflow || options.reviewPolicy !== "verdict_protocol"
1074
1597
  ? " Workflow scaffold: skipped (not applicable for this policy)"
@@ -1101,11 +1624,50 @@ export async function runInstallBridgeConductorCli(argv, deps) {
1101
1624
  else if (executor) {
1102
1625
  deps.log(" Executor provisioning: skipped (no --executor-id supplied)");
1103
1626
  }
1627
+ if (options.launchProfile !== undefined) {
1628
+ deps.log(` Epic launch profile '${options.launchProfile}': would verify target access, credential ` +
1629
+ "binding, consent, notification, and actor, and persist the profile only if all hold " +
1630
+ "(each write behind its own consent) — nothing was read from or written to the profile store.");
1631
+ }
1104
1632
  renderCapabilityMatrix(deps, firstReport.readiness);
1105
1633
  deps.log("");
1106
1634
  deps.log("complete-with-manual-steps (dry run)");
1107
1635
  return 0;
1108
1636
  }
1637
+ if (!access) {
1638
+ // Unreachable in practice (a null access produces a fatal doctor section
1639
+ // above), but the type must be narrowed before the preview.
1640
+ deps.errorLog("Bridge API access could not be resolved; aborting before the write.");
1641
+ return 1;
1642
+ }
1643
+ // ---- Phase 2: bootstrap — preview, consent, exact approval, apply -------
1644
+ // BAPI-1288. The server decides what this bootstrap would do to the stored
1645
+ // project defaults; this command only shows that decision and asks. It never
1646
+ // evaluates gate strength itself, and it never falls back to the old
1647
+ // replacing bootstrap when the server cannot preview.
1648
+ deps.log("");
1649
+ deps.log("Phase 2/8 — bootstrapping supervisor project defaults…");
1650
+ const bootstrapRequest = {
1651
+ required_checks: options.requiredChecks,
1652
+ // BAPI-694 — the narrow ReviewPolicy object, sent ONLY on an explicit
1653
+ // selection. `review_source` is deliberately NOT sent: on an existing
1654
+ // repository an omitted field keeps its stored value, and a new repository
1655
+ // receives the server's `verdict_protocol` default either way.
1656
+ ...(options.reviewPolicyExplicit
1657
+ ? { review_policy: { source: options.reviewPolicy } }
1658
+ : {}),
1659
+ merge_approval_required: "true",
1660
+ ...(options.replaceDoneGate ? { replace_done_gate: true } : {}),
1661
+ };
1662
+ let preview;
1663
+ try {
1664
+ preview = await previewConductorSupervisorBootstrap(access, bootstrapRequest, deps.postJson);
1665
+ }
1666
+ catch (err) {
1667
+ reportBootstrapFailure(deps, "preview", classifySupervisorBootstrapFailure(err));
1668
+ return 1;
1669
+ }
1670
+ renderBootstrapPreview(deps, preview);
1109
1671
  // ---- Consent #2: the bootstrap write itself ----------------------------
1110
1672
  // Deliberately separate from the base-URL consent above (which was given
1111
1673
  // before any network call). Confirming WHERE to write is not consent TO write.
@@ -1115,36 +1677,36 @@ export async function runInstallBridgeConductorCli(argv, deps) {
1115
1677
  deps.log("Aborted — no server write was attempted.");
1116
1678
  return 0;
1117
1679
  }
1118
- if (!access) {
1119
- // Unreachable in practice (a null access produces a fatal doctor section
1120
- // above), but the type must be narrowed before the POST.
1121
- deps.errorLog("Bridge API access could not be resolved; aborting before the write.");
1122
- return 1;
1680
+ // ---- Consent #3: the EXACT gate change, only when the preview needs it ---
1681
+ // Never inferred from consent #2: agreeing to install is not agreeing to
1682
+ // remove a configured requirement. A missing prompt seam is a refusal.
1683
+ let applyRequest = bootstrapRequest;
1684
+ if (preview.approval_required) {
1685
+ deps.log("");
1686
+ const gateChangeOk = await confirm(deps, "Approve EXACTLY the gate changes listed above? [y/N]: ");
1687
+ if (!gateChangeOk || !preview.approval_token) {
1688
+ deps.errorLog("Gate change not approved — no server write was attempted, and every configured " +
1689
+ "requirement is unchanged.");
1690
+ return 1;
1691
+ }
1692
+ applyRequest = {
1693
+ ...bootstrapRequest,
1694
+ approval: { approve: true, approval_token: preview.approval_token },
1695
+ };
1123
1696
  }
1124
- // ---- Phase 2: bootstrap ------------------------------------------------
1125
- deps.log("");
1126
- deps.log("Phase 2/8 — bootstrapping supervisor project defaults…");
1127
1697
  let bootstrapResponse;
1128
1698
  try {
1129
- bootstrapResponse = await bootstrapConductorSupervisorDefaults(access, {
1130
- required_checks: options.requiredChecks,
1131
- review_source: "verdict_protocol",
1132
- // BAPI-694 — the narrow ReviewPolicy object, sent ONLY on an explicit
1133
- // selection. `review_source` above is deliberately left alone: it is the
1134
- // WIDER done-gate vocabulary and the two are not interchangeable, so
1135
- // neither value is coerced into the other.
1136
- ...(options.reviewPolicyExplicit
1137
- ? { review_policy: { source: options.reviewPolicy } }
1138
- : {}),
1139
- merge_approval_required: "true",
1140
- }, deps.postJson);
1699
+ bootstrapResponse = await bootstrapConductorSupervisorDefaults(access, applyRequest, deps.postJson);
1141
1700
  }
1142
1701
  catch (err) {
1143
- const kind = err instanceof ConductorBridgeApiError ? err.kind : "network";
1144
- deps.errorLog(`Bootstrap failed (${kind}). No workflow or verification step was run.`);
1702
+ reportBootstrapFailure(deps, "apply", classifySupervisorBootstrapFailure(err));
1145
1703
  return 1;
1146
1704
  }
1147
- deps.log(` wrote supervisor setup + config (fields: ${bootstrapResponse.audited_field_names.join(", ")})`);
1705
+ if (bootstrapResponse.setup_outcome === null || bootstrapResponse.config_outcome === null) {
1706
+ reportBootstrapFailure(deps, "apply", "unsupported-protocol");
1707
+ return 1;
1708
+ }
1709
+ renderBootstrapOutcome(deps, bootstrapResponse);
1148
1710
  // ---- Phase 3: optional workflow scaffold -------------------------------
1149
1711
  deps.log("");
1150
1712
  deps.log("Phase 3/8 — claude-review workflow…");
@@ -1235,8 +1797,10 @@ export async function runInstallBridgeConductorCli(argv, deps) {
1235
1797
  manualActions.push("local observability: run the packaged `install conductor` command, or " +
1236
1798
  "`epic-implementer install-git-hooks`, to install the managed conductor Git hooks.");
1237
1799
  }
1238
- // The fail-loud hook-binary refusal exits 1 — but only AFTER the phase's
1239
- // read-only ledger report has been printed, which the phase guarantees.
1800
+ // The fail-loud hook-binary refusal (a package that ships no hook binary)
1801
+ // exits 1 — but only AFTER the phase's read-only ledger report has been
1802
+ // printed, which the phase guarantees. BAPI-1290: qualification refusals are
1803
+ // manual actions, not fatal, so they reach the phases below.
1240
1804
  if (observabilityFatal)
1241
1805
  return 1;
1242
1806
  // ---- Phase 6: setup-epic verification (conditional) --------------------
@@ -1320,6 +1884,24 @@ export async function runInstallBridgeConductorCli(argv, deps) {
1320
1884
  for (const action of manualActions)
1321
1885
  deps.log(` → ${action}`);
1322
1886
  }
1887
+ // ---- BAPI-1260: the reusable epic launch profile ------------------------
1888
+ // Last, because it can only be approved on facts every earlier phase
1889
+ // produced: server-observed readiness at THIS target, the executor handshake
1890
+ // for a durable host, and the operator's consents.
1891
+ if (options.launchProfile !== undefined) {
1892
+ const launch = await runLaunchProfilePhase(deps, options, {
1893
+ access,
1894
+ baseUrl,
1895
+ readiness: postReadiness,
1896
+ actorObserved: observedReadiness !== null || postReadiness?.executor.ready === true,
1897
+ runtime: resolveLaunchRuntime(executor, options.executionMode ?? "local"),
1898
+ });
1899
+ deps.log("");
1900
+ if (!launch.complete) {
1901
+ deps.log("incomplete-setup");
1902
+ return 1;
1903
+ }
1904
+ }
1323
1905
  deps.log("");
1324
1906
  // The executor leg can only be true when the server observed a recent claim,
1325
1907
  // so a bootstrap alone can never print `complete`. The two local phases above