@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
@@ -106,7 +106,7 @@
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";
@@ -167,13 +167,20 @@ export function getInstallBridgeConductorUsage() {
167
167
  " --secret-name <NAME> claude-review workflow: OAuth token secret NAME",
168
168
  " --branches a,b claude-review workflow: PR branch triggers",
169
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.",
170
175
  " --launch-profile <name> (BAPI-1260) Opt in to the reusable epic launch: after",
171
176
  " every mandatory prerequisite is verified, persist a",
172
177
  " host-local profile and print the ONE drive-epic",
173
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.",
174
181
  " --base-url <url> Explicit Bridge API target for --launch-profile.",
175
- " Required there (or BAPI_BASE_URL): nothing defaults",
176
- " to production; select it deliberately.",
182
+ " Required there (or BAPI_BASE_URL): guided setup",
183
+ " approves one named server and has no default.",
177
184
  " --execution-mode <mode> local | durable (default: durable with",
178
185
  " --executor-id, otherwise local).",
179
186
  " --policy-file <path> Run policy the profile references; an unattended",
@@ -262,6 +269,7 @@ export function parseInstallBridgeConductorArgs(argv) {
262
269
  let repo;
263
270
  let dryRun = false;
264
271
  let skipWorkflow = false;
272
+ let replaceDoneGate = false;
265
273
  let reviewPolicy = DEFAULT_CONDUCTOR_REVIEW_POLICY;
266
274
  let reviewPolicyExplicit = false;
267
275
  let epicKey;
@@ -304,6 +312,11 @@ export function parseInstallBridgeConductorArgs(argv) {
304
312
  return { status: "error", message: "--skip-workflow takes no value" };
305
313
  skipWorkflow = true;
306
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;
307
320
  case "--repo-wide":
308
321
  if (inline !== undefined)
309
322
  return { status: "error", message: "--repo-wide takes no value" };
@@ -480,6 +493,7 @@ export function parseInstallBridgeConductorArgs(argv) {
480
493
  secretName,
481
494
  branches,
482
495
  skipWorkflow,
496
+ ...(replaceDoneGate ? { replaceDoneGate: true } : {}),
483
497
  executorId,
484
498
  ...(epicRunIds.length > 0 ? { epicRunIds } : {}),
485
499
  ...(repoWide ? { repoWide: true } : {}),
@@ -582,6 +596,89 @@ export async function observeExecutorReadiness(access, fetchJson, deps, options
582
596
  // Runner
583
597
  // ---------------------------------------------------------------------------
584
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
+ }
585
682
  async function confirm(deps, promptText) {
586
683
  // A missing prompt seam means no TTY. Silence is NOT consent.
587
684
  if (!deps.promptLine)
@@ -847,6 +944,13 @@ export const CONDUCTOR_PROFILE_EMPTY_MERGE_REMEDIATION = `run \`npx ${MCP_PACKAG
847
944
  /** Remediation printed whenever the managed conductor hooks were not installed. */
848
945
  export const CONDUCTOR_HOOKS_MANUAL_REMEDIATION = "re-run `install conductor` and accept the local-observability consent, or run " +
849
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).";
850
954
  /**
851
955
  * Phase: tool visibility.
852
956
  *
@@ -947,11 +1051,33 @@ async function runConductorLocalObservabilityPhase(deps, local) {
947
1051
  // `reason` is already a sanitized sentence. This one IS a command failure —
948
1052
  // a hook that cannot resolve its binary is a dead hook, and the whole point
949
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.
950
1058
  deps.errorLog(result.error === CONDUCTOR_BIN_UNRESOLVED_ERROR
951
- ? ` hook installation refused — ${result.reason}`
952
- : " hook installation refused.");
953
- manualActions.push(`local observability: ${CONDUCTOR_HOOKS_MANUAL_REMEDIATION}`);
954
- 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
+ }
955
1081
  }
956
1082
  else if (result !== null && result.ok) {
957
1083
  if (!result.is_worktree) {
@@ -969,6 +1095,9 @@ async function runConductorLocalObservabilityPhase(deps, local) {
969
1095
  ? ` ${hook.name}: ${hook.action}`
970
1096
  : ` ${hook.name}: ${hook.action} — warning: ${hook.warning}`);
971
1097
  }
1098
+ if (result.partial) {
1099
+ deps.log(" the hook update is PARTIAL — one managed hook was published and the other was not.");
1100
+ }
972
1101
  // Sanitized: the installer's own warnings are secret-free sentences, and
973
1102
  // hook CONTENT, env, and caught errors are never rendered.
974
1103
  for (const warning of result.warnings)
@@ -979,11 +1108,21 @@ async function runConductorLocalObservabilityPhase(deps, local) {
979
1108
  // provisioned observability, and the missing reference-transaction hook
980
1109
  // is precisely the one that stops half the events from ever being
981
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.
982
1116
  const usable = (name) => {
983
1117
  const hook = result.installed.find((h) => h.name === name);
984
- 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);
985
1122
  };
986
- 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;
987
1126
  if (complete) {
988
1127
  const recorded = await local.recordCapability("conductor_local_observability");
989
1128
  if (!recorded.ok) {
@@ -1084,7 +1223,7 @@ function isHostLocalHostname(hostname) {
1084
1223
  * failure there holds activation. A launcher-local listener can never pass
1085
1224
  * here for a remote target: the server cannot reach this machine's loopback.
1086
1225
  */
1087
- function assessNotification(policy, attended, targetBaseUrl) {
1226
+ function assessNotification(policy, attended, targetBaseUrl, readiness) {
1088
1227
  if (attended) {
1089
1228
  return { status: "not-applicable", detail: "the referenced policy is attended; no escalation webhook is required." };
1090
1229
  }
@@ -1093,9 +1232,20 @@ function assessNotification(policy, attended, targetBaseUrl) {
1093
1232
  : null;
1094
1233
  const url = typeof notify?.webhook_url === "string" ? notify.webhook_url.trim() : "";
1095
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
+ }
1096
1245
  return {
1097
1246
  status: "missing",
1098
- detail: "an unattended launch needs notify.webhook_url in its --policy-file" +
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" +
1099
1249
  (notify && "local_sink" in notify ? " (notify.local_sink is not an escalation channel)." : "."),
1100
1250
  };
1101
1251
  }
@@ -1180,12 +1330,13 @@ async function runLaunchProfilePhase(deps, options, inputs) {
1180
1330
  else {
1181
1331
  deps.log(` consent: ${consent === false ? "NOT granted" : "could not be verified"}`);
1182
1332
  gaps.push(consent === false
1183
- ? "unattended consent is not granted for this repository; an administrator sets it " +
1184
- "(docs/claude/account-settings-operator-runbook.md) — this command never writes it."
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."
1185
1336
  : "unattended consent could not be verified at the selected target (older server or unreadable report).");
1186
1337
  }
1187
1338
  // (4) Notification, from the server's network perspective.
1188
- const notification = assessNotification(policy, attended, inputs.baseUrl);
1339
+ const notification = assessNotification(policy, attended, inputs.baseUrl, inputs.readiness);
1189
1340
  deps.log(` notify: ${notification.status} — ${notification.detail}`);
1190
1341
  if (notification.status === "missing")
1191
1342
  gaps.push(notification.detail);
@@ -1428,16 +1579,19 @@ export async function runInstallBridgeConductorCli(argv, deps) {
1428
1579
  if (options.dryRun) {
1429
1580
  deps.log("");
1430
1581
  deps.log("Dry run — no writes will be performed.");
1431
- 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:");
1432
1583
  // Field NAMES and a COUNT only — never the check values, which are
1433
1584
  // configuration content, and never the API key.
1434
1585
  deps.log(" - done_gate_config.required_checks " +
1435
1586
  `(${options.requiredChecks.length} check(s))`);
1436
- deps.log(" - review_source");
1437
1587
  if (options.reviewPolicyExplicit)
1438
1588
  deps.log(" - review_policy");
1589
+ if (options.replaceDoneGate)
1590
+ deps.log(" - replace_done_gate");
1439
1591
  deps.log(" - auto_merge_enabled");
1440
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.");
1441
1595
  deps.log(` Would target base URL: ${baseUrl}`);
1442
1596
  deps.log(options.skipWorkflow || options.reviewPolicy !== "verdict_protocol"
1443
1597
  ? " Workflow scaffold: skipped (not applicable for this policy)"
@@ -1480,6 +1634,40 @@ export async function runInstallBridgeConductorCli(argv, deps) {
1480
1634
  deps.log("complete-with-manual-steps (dry run)");
1481
1635
  return 0;
1482
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);
1483
1671
  // ---- Consent #2: the bootstrap write itself ----------------------------
1484
1672
  // Deliberately separate from the base-URL consent above (which was given
1485
1673
  // before any network call). Confirming WHERE to write is not consent TO write.
@@ -1489,36 +1677,36 @@ export async function runInstallBridgeConductorCli(argv, deps) {
1489
1677
  deps.log("Aborted — no server write was attempted.");
1490
1678
  return 0;
1491
1679
  }
1492
- if (!access) {
1493
- // Unreachable in practice (a null access produces a fatal doctor section
1494
- // above), but the type must be narrowed before the POST.
1495
- deps.errorLog("Bridge API access could not be resolved; aborting before the write.");
1496
- 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
+ };
1497
1696
  }
1498
- // ---- Phase 2: bootstrap ------------------------------------------------
1499
- deps.log("");
1500
- deps.log("Phase 2/8 — bootstrapping supervisor project defaults…");
1501
1697
  let bootstrapResponse;
1502
1698
  try {
1503
- bootstrapResponse = await bootstrapConductorSupervisorDefaults(access, {
1504
- required_checks: options.requiredChecks,
1505
- review_source: "verdict_protocol",
1506
- // BAPI-694 — the narrow ReviewPolicy object, sent ONLY on an explicit
1507
- // selection. `review_source` above is deliberately left alone: it is the
1508
- // WIDER done-gate vocabulary and the two are not interchangeable, so
1509
- // neither value is coerced into the other.
1510
- ...(options.reviewPolicyExplicit
1511
- ? { review_policy: { source: options.reviewPolicy } }
1512
- : {}),
1513
- merge_approval_required: "true",
1514
- }, deps.postJson);
1699
+ bootstrapResponse = await bootstrapConductorSupervisorDefaults(access, applyRequest, deps.postJson);
1515
1700
  }
1516
1701
  catch (err) {
1517
- const kind = err instanceof ConductorBridgeApiError ? err.kind : "network";
1518
- deps.errorLog(`Bootstrap failed (${kind}). No workflow or verification step was run.`);
1702
+ reportBootstrapFailure(deps, "apply", classifySupervisorBootstrapFailure(err));
1703
+ return 1;
1704
+ }
1705
+ if (bootstrapResponse.setup_outcome === null || bootstrapResponse.config_outcome === null) {
1706
+ reportBootstrapFailure(deps, "apply", "unsupported-protocol");
1519
1707
  return 1;
1520
1708
  }
1521
- deps.log(` wrote supervisor setup + config (fields: ${bootstrapResponse.audited_field_names.join(", ")})`);
1709
+ renderBootstrapOutcome(deps, bootstrapResponse);
1522
1710
  // ---- Phase 3: optional workflow scaffold -------------------------------
1523
1711
  deps.log("");
1524
1712
  deps.log("Phase 3/8 — claude-review workflow…");
@@ -1609,8 +1797,10 @@ export async function runInstallBridgeConductorCli(argv, deps) {
1609
1797
  manualActions.push("local observability: run the packaged `install conductor` command, or " +
1610
1798
  "`epic-implementer install-git-hooks`, to install the managed conductor Git hooks.");
1611
1799
  }
1612
- // The fail-loud hook-binary refusal exits 1 — but only AFTER the phase's
1613
- // 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.
1614
1804
  if (observabilityFatal)
1615
1805
  return 1;
1616
1806
  // ---- Phase 6: setup-epic verification (conditional) --------------------
@@ -83,7 +83,7 @@ import { resolveLocalMergeMethod, runApprovedLocalMerge, resolveLocalMergeCreden
83
83
  * The exact remediation for a `dry_run`. Fixed text, because a caller that has to
84
84
  * infer WHICH setting to flip from a reason code guesses wrong.
85
85
  */
86
- const DRY_RUN_HINT = "set auto_merge_enabled=true on the project default via PUT /jira/epic-runs/supervisor-config/defaults/";
86
+ const DRY_RUN_HINT = "auto_merge_enabled is off on the project default; turning it on grants merge authority, so the supervisor-config PUT refuses it (BAPI-1288) — an operator must enable it through the account-settings runbook";
87
87
  /**
88
88
  * Guidance for a post-dispatch transport failure. The action key makes the request
89
89
  * idempotent, so repeating it verbatim is safe and is the ONLY safe move: the merge