@bridge_gpt/mcp-server 0.2.62 → 0.2.66

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 (113) hide show
  1. package/README.md +66 -15
  2. package/build/agent-launchers/claude-executor-adapter.js +145 -20
  3. package/build/agent-launchers/executor-adapter.js +81 -2
  4. package/build/agents.generated.js +10 -1
  5. package/build/bounded-wait.js +32 -1
  6. package/build/bridge-config.js +104 -0
  7. package/build/codex-bridge-profile.js +10 -1
  8. package/build/commands.generated.js +7 -6
  9. package/build/conductor/bridge-api-client.js +565 -32
  10. package/build/conductor/bring-up-facts.js +3 -1
  11. package/build/conductor/doctor.js +23 -14
  12. package/build/conductor/epic-child-continuation.js +351 -0
  13. package/build/conductor/epic-implementer-cli.js +154 -17
  14. package/build/conductor/git-hooks.js +529 -118
  15. package/build/conductor/guided-repair.js +323 -0
  16. package/build/conductor/hook-runtime-qualification.js +340 -0
  17. package/build/conductor/readiness-cli.js +30 -9
  18. package/build/conductor/readiness-sections.js +5 -4
  19. package/build/conductor/readiness.js +45 -12
  20. package/build/conductor/recovery-cli.js +859 -27
  21. package/build/conductor/recovery-operations.js +574 -5
  22. package/build/conductor/repair-contract.js +69 -3
  23. package/build/conductor-bundle-artifacts.js +1 -0
  24. package/build/credential-store.js +52 -13
  25. package/build/diagnostic-text.js +36 -0
  26. package/build/docs.generated.js +3 -2
  27. package/build/doctor.js +147 -21
  28. package/build/drive-epic-key.js +12 -0
  29. package/build/drive-epic.js +1537 -125
  30. package/build/epic-coherence-input-cli.js +137 -0
  31. package/build/epic-coherence-input.js +920 -0
  32. package/build/epic-coherence-input.test-fixtures.js +175 -0
  33. package/build/epic-implementer-bin.js +41 -33
  34. package/build/epic-implementer-bundle-cli.js +14 -1
  35. package/build/epic-launch-diagnostic.js +31 -0
  36. package/build/epic-launch-profile.js +58 -19
  37. package/build/epic-run-status.js +5 -0
  38. package/build/epic-shared/bridge-client.js +46 -4
  39. package/build/executor/cli.js +39 -0
  40. package/build/executor/completion-contract.js +423 -0
  41. package/build/executor/completion-supervision.js +291 -0
  42. package/build/executor/correction-dispatch.js +60 -0
  43. package/build/executor/credentials.js +3 -1
  44. package/build/executor/deps.js +65 -1
  45. package/build/executor/env.js +12 -0
  46. package/build/executor/heartbeat.js +13 -3
  47. package/build/executor/http-client.js +91 -4
  48. package/build/executor/integration-job.js +6 -1
  49. package/build/executor/job-errors.js +61 -4
  50. package/build/executor/job-log-registry.js +340 -73
  51. package/build/executor/job-runner.js +1550 -55
  52. package/build/executor/job-types.js +9 -6
  53. package/build/executor/preflight.js +189 -1
  54. package/build/executor/process-containment.js +756 -0
  55. package/build/executor/process.js +235 -5
  56. package/build/executor/prompt-spec.js +366 -1
  57. package/build/executor/provider-checks.js +69 -0
  58. package/build/executor/rebase-finalization.js +351 -0
  59. package/build/executor/reconcile-overlap-job.js +77 -84
  60. package/build/executor/required-verification.js +313 -0
  61. package/build/executor/required-work-ledger.js +1121 -0
  62. package/build/executor/results.js +142 -0
  63. package/build/executor/resume-pre-spawn.js +87 -4
  64. package/build/executor/runner.js +168 -0
  65. package/build/executor/stale-artifacts.js +5 -4
  66. package/build/executor/test-declaration.js +20 -0
  67. package/build/executor/types.js +13 -0
  68. package/build/executor/watch-cli.js +35 -8
  69. package/build/executor/worker-command.js +70 -34
  70. package/build/executor/worker-finalization.js +168 -26
  71. package/build/executor/worker-guard-hook.js +75 -14
  72. package/build/executor/worker-log.js +137 -70
  73. package/build/executor/worktree-gc.js +30 -6
  74. package/build/executor/worktree.js +13 -0
  75. package/build/executor-launch.js +658 -0
  76. package/build/index.js +594 -1152
  77. package/build/init.js +107 -4
  78. package/build/install-bridge-conductor.js +231 -41
  79. package/build/install-bridge.js +284 -20
  80. package/build/install-doctor.js +172 -30
  81. package/build/install-reexec.js +69 -7
  82. package/build/macos-terminal-permission-guidance.js +16 -0
  83. package/build/merge-pull-request.js +1 -1
  84. package/build/model-thresholds-tool.js +185 -0
  85. package/build/pipelines.generated.js +9 -8
  86. package/build/plan-discovery.js +137 -0
  87. package/build/plan-epic-implementer-eligibility.js +2 -2
  88. package/build/plane/cli.js +37 -3
  89. package/build/plane/member-logs.js +3 -0
  90. package/build/plane/member-roster.js +7 -1
  91. package/build/plane/shutdown.js +59 -1
  92. package/build/plane/status.js +4 -0
  93. package/build/plane/supervisor.js +146 -0
  94. package/build/plane/worker-exit-code.js +2 -0
  95. package/build/project-root.js +99 -0
  96. package/build/readme.generated.js +1 -1
  97. package/build/review-tickets.js +156 -13
  98. package/build/scripts/executor-protocol-contract-driver.js +13 -3
  99. package/build/setup-epic.js +208 -125
  100. package/build/sfcc/setup-status.js +2 -0
  101. package/build/sfcc/tool-wrapper.js +2 -0
  102. package/build/sfcc/tools-configured.js +96 -0
  103. package/build/start-tickets.js +163 -58
  104. package/build/upgrade-cli.js +109 -24
  105. package/build/version.generated.js +3 -3
  106. package/build/windows-npm-entry.js +103 -0
  107. package/build/worker-guard-hook-bin.js +5 -4
  108. package/build/worktree-core.js +6 -5
  109. package/docs/CONDUCTOR.md +116 -2
  110. package/docs/install/mcp-tool-integrations.md +1 -0
  111. package/docs/install/sfcc-integration.md +12 -7
  112. package/package.json +2 -1
  113. package/pipelines/implement-ticket.json +1 -1
package/build/init.js CHANGED
@@ -10,6 +10,7 @@
10
10
  * effects and depends only on leaf modules.
11
11
  */
12
12
  import { writeFile, mkdir, readFile, stat } from "fs/promises";
13
+ import { execFile } from "child_process";
13
14
  import path from "path";
14
15
  import os from "os";
15
16
  import { COMMANDS } from "./commands.generated.js";
@@ -19,7 +20,7 @@ import { AGENTS } from "./agents.generated.js";
19
20
  import { DOCS } from "./docs.generated.js";
20
21
  import { VERSION, LAUNCHER_ARGS } from "./version.generated.js";
21
22
  import { reconstructAgentMarkdown, translateAgentToCopilot, translateCommandToCopilot, } from "./agent-utils.js";
22
- import { validateRepoName, resolveRepoNameForProjectRoot } from "./bridge-config.js";
23
+ import { BRIDGE_CONFIG_TEAMMATE_NOTICE_LINES, validateBridgeConfigRepoNameRepresentability, validateRepoName, resolveRepoNameForProjectRoot, } from "./bridge-config.js";
23
24
  import { ensureGitignored as ensureGitignoredShared } from "./git-ignore-utils.js";
24
25
  import { resolveBapiCredentials } from "./credential-store.js";
25
26
  import { describeLauncherReason, DUPLICATE_REGISTRATION_GUIDANCE, inspectLauncherConfigs, isPrerequisiteFailure, } from "./launcher-config-inspection.js";
@@ -133,6 +134,8 @@ export function buildBridgeConfigManifest(repoName) {
133
134
  "# Inherited by every git worktree; start-tickets worktree provisioning reads it",
134
135
  "# to decide which Bridge API MCP registrations to write. Do not add credentials,",
135
136
  "# connection URLs, or machine-specific paths here — those are resolved at runtime.",
137
+ "#",
138
+ ...BRIDGE_CONFIG_TEAMMATE_NOTICE_LINES,
136
139
  "",
137
140
  `repo_name = "${repoName}"`,
138
141
  "",
@@ -389,6 +392,65 @@ export class InitPreflightError extends Error {
389
392
  this.failures = failures;
390
393
  }
391
394
  }
395
+ /**
396
+ * Classify `git ls-files --error-unmatch -- <relPath>` from its exit code: 0 is
397
+ * tracked, 1 is untracked, anything else is undetermined. Shared by `runInit`
398
+ * and install so both reach the same verdict from the same probe.
399
+ */
400
+ export function classifyTrackedProbeExitCode(exitCode) {
401
+ if (exitCode === 0)
402
+ return "tracked";
403
+ if (exitCode === 1)
404
+ return "untracked";
405
+ return "undetermined";
406
+ }
407
+ /**
408
+ * Default tracked-config probe for `runInit`: runs git with an ARGUMENT LIST,
409
+ * never a shell string, in `cwd`. Any failure is `undetermined`, which callers
410
+ * treat as untracked — the probe fails OPEN, so a missing git or a non-repo
411
+ * folder still gets today's `BAPI_PROJECT_ROOT = cwd` rewrite.
412
+ */
413
+ export function defaultIsTrackedConfig(cwd) {
414
+ return (relPath) => new Promise((resolve) => {
415
+ try {
416
+ execFile("git", ["ls-files", "--error-unmatch", "--", relPath], { cwd, windowsHide: true, timeout: 10_000 }, (error) => {
417
+ if (!error)
418
+ return resolve("tracked");
419
+ const code = error.code;
420
+ resolve(classifyTrackedProbeExitCode(typeof code === "number" ? code : null));
421
+ });
422
+ }
423
+ catch {
424
+ resolve("undetermined");
425
+ }
426
+ });
427
+ }
428
+ /**
429
+ * Decide the `BAPI_PROJECT_ROOT` an EXISTING Bridge entry should carry (BAPI-1318).
430
+ * A git-tracked config keeps its existing non-blank value exactly: the runtime
431
+ * check in `project-root.ts` makes a committed path harmless for teammates, and
432
+ * rewriting it would put this machine's path into everyone's diff. Untracked and
433
+ * undetermined configs, and blank or absent values, get `cwd` as before.
434
+ *
435
+ * `warnReplaced` is true only when the probe was undetermined AND a different,
436
+ * non-blank value is being replaced — the one case where a tracked path may have
437
+ * been rewritten without the protection applying.
438
+ */
439
+ export function decideExistingProjectRoot(existing, cwd, tracked) {
440
+ const current = typeof existing === "string" ? existing : "";
441
+ const blank = current.trim().length === 0;
442
+ if (tracked === "tracked" && !blank)
443
+ return { value: current, warnReplaced: false };
444
+ return {
445
+ value: cwd,
446
+ warnReplaced: tracked === "undetermined" && !blank && current !== cwd,
447
+ };
448
+ }
449
+ /** The one secret-safe warning for an undetermined probe that replaced a root. */
450
+ export function formatUndeterminedRootReplacedWarning(relPath) {
451
+ return (` Warning: could not tell whether ${relPath} is tracked by git, so its BAPI_PROJECT_ROOT ` +
452
+ "was rewritten to this folder. Review its diff before committing.");
453
+ }
392
454
  /**
393
455
  * Resolve the non-secret identity a NEW Bridge entry should carry.
394
456
  *
@@ -518,6 +580,20 @@ export async function runInit(cwd, options = {}) {
518
580
  const targetSpec = `${MCP_PACKAGE_NAME}@${targetVersion}`;
519
581
  const say = options.stdout ?? ((m) => console.log(m));
520
582
  const warn = options.stderr ?? ((m) => console.error(m));
583
+ const trackedProbe = options.isTrackedConfig ?? defaultIsTrackedConfig(cwd);
584
+ const probeTracked = async (relPath) => {
585
+ try {
586
+ const verdict = await trackedProbe(relPath);
587
+ if (verdict === true)
588
+ return "tracked";
589
+ if (verdict === false)
590
+ return "untracked";
591
+ return verdict === "tracked" || verdict === "untracked" ? verdict : "undetermined";
592
+ }
593
+ catch {
594
+ return "undetermined";
595
+ }
596
+ };
521
597
  // ---- Phase 1: IDE Detection ----
522
598
  // BAPI-708: detection lives in `detectInitIdes` so the commit-safe asset
523
599
  // manifest resolves the SAME conditional targets this scaffolder writes.
@@ -701,7 +777,17 @@ export async function runInit(cwd, options = {}) {
701
777
  entry.args = refreshed;
702
778
  if (!entry.env)
703
779
  entry.env = {};
704
- entry.env.BAPI_PROJECT_ROOT = cwd;
780
+ // BAPI-1318: a git-tracked config keeps its committed BAPI_PROJECT_ROOT.
781
+ // The probe runs only when there is a non-blank value to preserve.
782
+ const existingRoot = entry.env.BAPI_PROJECT_ROOT;
783
+ let trackedVerdict = "untracked";
784
+ if (typeof existingRoot === "string" && existingRoot.trim().length > 0 && existingRoot !== cwd) {
785
+ trackedVerdict = await probeTracked(target.path);
786
+ }
787
+ const rootDecision = decideExistingProjectRoot(existingRoot, cwd, trackedVerdict);
788
+ entry.env.BAPI_PROJECT_ROOT = rootDecision.value;
789
+ if (rootDecision.warnReplaced)
790
+ warn(formatUndeterminedRootReplacedWarning(target.path));
705
791
  await io.writeFile(fullPath, JSON.stringify(parsed, null, 2) + "\n", "utf-8");
706
792
  const kind = found.action === "already-current"
707
793
  ? "already-current"
@@ -1094,9 +1180,24 @@ says to serialize structured output.
1094
1180
  // the credential landed and which generated assets are safe to commit. Removed
1095
1181
  // outright rather than behind an output-mode flag — parameterizing would change
1096
1182
  // the injected `deps.runInit` seam signature for no user-visible gain.
1183
+ // BAPI-1318: a caller that already resolved the project name (install) is
1184
+ // authoritative, so the manifest agrees with the MCP entry. `--init` and
1185
+ // `upgrade` pass no creation context and keep the folder-name default.
1186
+ let bridgeConfigCreated = false;
1187
+ let bridgeConfigRepoName;
1097
1188
  if (!bridgeConfigExists) {
1098
- await mkdir(path.dirname(bridgeConfigPath), { recursive: true });
1099
- await writeFile(bridgeConfigPath, buildBridgeConfigManifest(chooseScaffoldRepoName(cwd)), "utf-8");
1189
+ const scaffoldName = options.creationContext?.repoName ?? chooseScaffoldRepoName(cwd);
1190
+ const representable = validateBridgeConfigRepoNameRepresentability(scaffoldName);
1191
+ if (representable.ok) {
1192
+ await mkdir(path.dirname(bridgeConfigPath), { recursive: true });
1193
+ await writeFile(bridgeConfigPath, buildBridgeConfigManifest(representable.value), "utf-8");
1194
+ bridgeConfigCreated = true;
1195
+ bridgeConfigRepoName = representable.value;
1196
+ }
1197
+ else {
1198
+ warn(" Warning: .bridge/config was not created because this project name cannot be written " +
1199
+ `there (${representable.error}). Worktrees and shell tools will fall back to the git folder name.`);
1200
+ }
1100
1201
  }
1101
1202
  // BAPI-728: authoritative, secret-free outcome. `ok` is false whenever any
1102
1203
  // entry authenticates as nobody, so no caller can report full success over a
@@ -1106,5 +1207,7 @@ says to serialize structured output.
1106
1207
  configActions: structuredActions,
1107
1208
  readinessWarnings,
1108
1209
  targetVersion,
1210
+ bridgeConfigCreated,
1211
+ ...(bridgeConfigRepoName !== undefined ? { bridgeConfigRepoName } : {}),
1109
1212
  };
1110
1213
  }
@@ -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) --------------------