@bridge_gpt/mcp-server 0.2.67 → 0.2.68

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 (114) hide show
  1. package/README.md +178 -15
  2. package/build/agent-launchers/claude-executor-adapter.js +27 -0
  3. package/build/agents.generated.js +22 -2
  4. package/build/bitbucket-workspace-remote.js +136 -0
  5. package/build/bootstrap-project-identity.js +24 -0
  6. package/build/bridge-permissions.js +829 -0
  7. package/build/claude-settings.js +18 -0
  8. package/build/cli-help.js +21 -0
  9. package/build/command-assets-doctor.js +24 -70
  10. package/build/command-provisioning.js +29 -5
  11. package/build/commands.generated.js +8 -8
  12. package/build/conductor/bridge-api-client.js +714 -0
  13. package/build/conductor/command-drift-production.js +92 -0
  14. package/build/conductor/command-drift.js +89 -0
  15. package/build/conductor/doctor.js +69 -26
  16. package/build/conductor/epic-branch-ci-inspection.js +1375 -0
  17. package/build/conductor/epic-implementer-cli.js +76 -22
  18. package/build/conductor/gate-discovery.js +890 -0
  19. package/build/conductor/install-doctor.js +15 -3
  20. package/build/conductor/json-output.js +63 -0
  21. package/build/conductor/readiness-cli.js +95 -10
  22. package/build/conductor/readiness-sections.js +369 -3
  23. package/build/conductor/readiness.js +211 -8
  24. package/build/conductor/recovery-cli.js +110 -4
  25. package/build/conductor/recovery-operations.js +99 -1
  26. package/build/connect-bitbucket-api.js +41 -6
  27. package/build/connect-bitbucket.js +106 -33
  28. package/build/credential-store.js +207 -47
  29. package/build/docs.generated.js +57 -3
  30. package/build/doctor.js +92 -1
  31. package/build/drive-epic.js +45 -8
  32. package/build/epic-coherence-input.test-fixtures.js +4 -3
  33. package/build/epic-implementer-bin.js +1374 -25
  34. package/build/epic-workarounds-path.js +39 -0
  35. package/build/executor/cli.js +30 -0
  36. package/build/executor/completion-contract.js +18 -5
  37. package/build/executor/deps.js +4 -1
  38. package/build/executor/gate-qualification-job.js +499 -0
  39. package/build/executor/guarded-fast-forward-push.js +129 -0
  40. package/build/executor/integration-job.js +122 -8
  41. package/build/executor/job-errors.js +8 -1
  42. package/build/executor/job-runner.js +589 -47
  43. package/build/executor/likely-tests.js +290 -0
  44. package/build/executor/live-worker-registry.js +1 -1
  45. package/build/executor/observation.js +31 -2
  46. package/build/executor/packaged-recipe.js +106 -0
  47. package/build/executor/permissions.js +115 -28
  48. package/build/executor/platform.js +1 -1
  49. package/build/executor/preflight.js +61 -12
  50. package/build/executor/process-containment.js +36 -12
  51. package/build/executor/process.js +15 -7
  52. package/build/executor/prompt-spec.js +33 -1
  53. package/build/executor/recently-failed-tests-file.js +71 -0
  54. package/build/executor/reconcile-overlap-job.js +41 -23
  55. package/build/executor/required-verification.js +1 -1
  56. package/build/executor/required-work-ledger.js +105 -17
  57. package/build/executor/retirement.js +69 -0
  58. package/build/executor/runner.js +114 -11
  59. package/build/executor/test-clock.js +15 -0
  60. package/build/executor/types.js +32 -0
  61. package/build/executor/viewer-tabs.js +58 -8
  62. package/build/executor/worker-command.js +23 -7
  63. package/build/executor/worker-diagnostics.js +37 -0
  64. package/build/executor/worker-finalization.js +31 -5
  65. package/build/executor/worker-guard-hook.js +179 -25
  66. package/build/executor/worker-log.js +4 -3
  67. package/build/executor/worker-verification-guidance.js +37 -0
  68. package/build/executor/worktree.js +88 -11
  69. package/build/first-build-advisory.js +97 -0
  70. package/build/generated-assets-advisory.js +14 -0
  71. package/build/generated-assets-catalog.js +66 -0
  72. package/build/generated-assets-context.js +46 -0
  73. package/build/generated-assets-dependencies.js +26 -0
  74. package/build/generated-assets-doctor.js +50 -0
  75. package/build/generated-assets-health.js +65 -0
  76. package/build/generated-assets-host.js +17 -0
  77. package/build/generated-assets-inventory.js +17 -0
  78. package/build/generated-assets-paths.js +60 -0
  79. package/build/generated-assets-publisher.js +133 -0
  80. package/build/generated-assets-remedy.js +25 -0
  81. package/build/generated-assets-repair.js +72 -0
  82. package/build/generated-assets-session.js +71 -0
  83. package/build/generated-assets-test-support.js +35 -0
  84. package/build/index.js +964 -768
  85. package/build/init.js +54 -177
  86. package/build/install-bridge-conductor-gate.js +1372 -0
  87. package/build/install-bridge-conductor.js +289 -36
  88. package/build/install-bridge.js +544 -37
  89. package/build/mcp-install-state.js +50 -4
  90. package/build/merge-workflow-install.js +3 -4
  91. package/build/pipeline-utils.js +6 -2
  92. package/build/pipelines.generated.js +22 -142
  93. package/build/plan-execution-ledger.js +2 -32
  94. package/build/plane/manifest.js +16 -1
  95. package/build/plane/preflight.js +54 -1
  96. package/build/printed-remedy-commands.js +16 -1
  97. package/build/readme.generated.js +1 -1
  98. package/build/repair-assets.js +93 -0
  99. package/build/run-unit-tests-launcher.js +130 -49
  100. package/build/secret-redaction.js +33 -0
  101. package/build/setup-epic.js +108 -3
  102. package/build/start-tickets-prereqs.js +70 -16
  103. package/build/start-tickets.js +29 -5
  104. package/build/tool-surface-gating.js +36 -0
  105. package/build/upgrade-cli.js +55 -1
  106. package/build/version.generated.js +3 -3
  107. package/build/worker-guard-hook-bin.js +3 -2
  108. package/build/worker-guard-hook-main.js +1 -1
  109. package/build/worktree-core.js +15 -2
  110. package/docs/EPIC-IMPLEMENTER-INSTALL.md +30 -12
  111. package/docs/EPIC-IMPLEMENTER.md +68 -2
  112. package/package.json +3 -3
  113. package/pipelines/greenfield-setup.json +4 -128
  114. package/pipelines/implement-ticket.json +3 -2
@@ -106,7 +106,8 @@
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 { PRINTED_INSTALL_COMMAND } from "./printed-remedy-commands.js";
109
+ import { PRINTED_INSTALL_COMMAND, PRINTED_QUALIFY_GATE_COMMAND } from "./printed-remedy-commands.js";
110
+ import { emptyConductorGateFlags, hasExplicitGateInput, parseLocalCommandFlag, parseEpicBranchCiDecisionFlag, parseMigrationGuardFlag, resolveConductorGate, runEpicBranchCiInspection, runClearConductorGate, runConductorGateQualification, runQualifyConductorGate, storeConductorGate, validateConductorGateFlags, } from "./install-bridge-conductor-gate.js";
110
111
  import { bootstrapConductorSupervisorDefaults, classifySupervisorBootstrapFailure, fetchConductorReadiness, previewConductorSupervisorBootstrap, } from "./conductor/bridge-api-client.js";
111
112
  import { CONDUCTOR_OPERATOR_RUNBOOK_POINTER, EXECUTOR_PROVISIONING_GUIDANCE, conductorInstallDoctorExitCode, conductorInstallDoctorHasFatal, formatConductorInstallDoctorReport, } from "./conductor/install-doctor.js";
112
113
  import { describeNativeLedgerAvailability, } from "./conductor/doctor.js";
@@ -131,9 +132,18 @@ export const CONDUCTOR_REVIEW_POLICY_SOURCES = [
131
132
  "verdict_protocol",
132
133
  "native_review_decision",
133
134
  "none",
135
+ "repository_policy",
134
136
  ];
135
- /** Default per-run review policy when the operator does not select one. */
136
- export const DEFAULT_CONDUCTOR_REVIEW_POLICY = "verdict_protocol";
137
+ /**
138
+ * Install-time review policy when the operator does not select one (BAPI-1406).
139
+ *
140
+ * `repository_policy` needs no Claude-review workflow, so an omitted flag
141
+ * scaffolds none. It is an install-time assumption ONLY: nothing persists it.
142
+ * {@link InstallBridgeConductorOptions.reviewPolicyExplicit} stays the sole basis
143
+ * for storing a repository default, and the server stamps the run-level default
144
+ * at first approval. AI review is an explicit `--review-policy verdict_protocol`.
145
+ */
146
+ export const DEFAULT_CONDUCTOR_REVIEW_POLICY = "repository_policy";
137
147
  /** Usage text for `install-bridge conductor`. */
138
148
  export function getInstallBridgeConductorUsage() {
139
149
  return [
@@ -145,14 +155,53 @@ export function getInstallBridgeConductorUsage() {
145
155
  "",
146
156
  "Options:",
147
157
  " --repo <name> Repository name (default: BAPI_REPO_NAME / .bridge/config)",
148
- " --required-check <name> Required CI check for the done gate (repeatable, REQUIRED)",
158
+ " --required-check <name> A provider check for the gate (repeatable). Explicit",
159
+ " owner input: stored as a confirmed provider gate and",
160
+ " projected into the done gate, as before.",
149
161
  " --required-checks a,b Comma-separated form of --required-check",
162
+ "",
163
+ "Repository gate (BAPI-1391) — what must pass before a child merges. A CONFIRMED",
164
+ "gate is required: provider checks, local commands, both (mixed), or none.",
165
+ "Without gate flags, a confirmed gate already stored (including an older",
166
+ "--required-check setup) is used unchanged; otherwise discovery reads this",
167
+ "working tree and the provider's real check history and PROPOSES a gate, which",
168
+ "is stored only after you confirm or edit it at a prompt. Discovery saves",
169
+ "nothing. Explicit gate flags are owner input and are stored as confirmed.",
170
+ " --provider-check <name> An exact provider-reported check name (repeatable).",
171
+ " Admission refuses one that has never reported.",
172
+ " --local-command <cmd> A local command the executor runs before each merge",
173
+ " (repeatable): a discovered candidate id such as",
174
+ " npm:test, or <name>=<plain words> such as",
175
+ " unit=pytest -q (no shell syntax; run without a shell).",
176
+ " --migration-guard <ref> provider:<check> or local:<command>: the member that",
177
+ " guards Alembic migrations.",
178
+ " --gate-none No gate: unattended runs need approval for every",
179
+ " child merge. Exclusive with the member flags.",
180
+ " --customized-command <n> A packaged command this repository customizes, stored",
181
+ " in the confirmed gate (repeatable), e.g. implement-ticket.",
182
+ " --confirm-gate Confirm explicit gate flags even when they REMOVE",
183
+ " members from the stored gate (otherwise you are asked).",
184
+ " --clear-gate Clear the confirmed gate (its own consent), then exit.",
185
+ " --qualify-gate Qualify a confirmed local or mixed gate: queue the",
186
+ " qualification job and drain it with a short-lived",
187
+ " repository-wide executor limited to that job type.",
188
+ " Alone, it does only that; with gate flags it runs",
189
+ " after they are stored, without the offer prompt.",
190
+ " Every gate write first checks that the server advertises gate_profile.v1;",
191
+ " against an older server it prints \"Bridge API update required\" and exits 1.",
192
+ "",
150
193
  ` --review-policy <source> One of: ${CONDUCTOR_REVIEW_POLICY_SOURCES.join(", ")}`,
151
- ` (default: ${DEFAULT_CONDUCTOR_REVIEW_POLICY}). Supplying it`,
152
- " EXPLICITLY also persists it as the supervisor",
153
- " project default, stamped into a new run that",
154
- " carries no review policy of its own. Omitting it",
155
- " stores no default at all.",
194
+ " Omitted, install assumes repository_policy: the",
195
+ " prospective default for a new feature-branch run at",
196
+ " first approval when neither the run nor the",
197
+ " repository sets a policy. It scaffolds no Claude",
198
+ " review workflow and stores no default at all.",
199
+ " AI review is an explicit opt-in: pass",
200
+ " --review-policy verdict_protocol to scaffold the",
201
+ " workflow. Supplying any value EXPLICITLY also",
202
+ " persists it as the supervisor project default,",
203
+ " stamped into a new run that carries no review",
204
+ " policy of its own.",
156
205
  " --epic-key <KEY> Epic key for the setup-epic verification phase",
157
206
  " --plan-file <path> Plan sidecar for the setup-epic verification phase",
158
207
  " --executor-id <id> Stable executor id. Supplying it OPTS IN to the",
@@ -196,6 +245,11 @@ export function getInstallBridgeConductorUsage() {
196
245
  " start-consent prompt.",
197
246
  " Still asks the base-URL confirmation below first —",
198
247
  " its doctor readiness GET carries the repo API key.",
248
+ " --epic-branch-ci-decision <include|decline>",
249
+ " (BAPI-1405, GitHub only) Record whether child merges",
250
+ " into an epic branch wait for CI on that PR. Without",
251
+ " it you are asked; a non-interactive run records",
252
+ " nothing. Never consent to change an approval rule.",
199
253
  " -h, --help Show this help",
200
254
  "",
201
255
  "The resolved BAPI_BASE_URL is displayed and confirmed BEFORE any network call",
@@ -283,10 +337,12 @@ export function parseInstallBridgeConductorArgs(argv) {
283
337
  let secretName;
284
338
  let branches;
285
339
  const requiredChecks = [];
340
+ const gate = emptyConductorGateFlags();
286
341
  let launchProfile;
287
342
  let baseUrl;
288
343
  let executionMode;
289
344
  let policyFile;
345
+ let epicBranchCiDecision;
290
346
  const takeValue = (arg, inline, next) => {
291
347
  if (inline !== undefined) {
292
348
  if (inline.trim().length === 0)
@@ -324,6 +380,21 @@ export function parseInstallBridgeConductorArgs(argv) {
324
380
  return { status: "error", message: "--repo-wide takes no value" };
325
381
  repoWide = true;
326
382
  break;
383
+ case "--gate-none":
384
+ case "--confirm-gate":
385
+ case "--clear-gate":
386
+ case "--qualify-gate":
387
+ if (inline !== undefined)
388
+ return { status: "error", message: `${flag} takes no value` };
389
+ if (flag === "--gate-none")
390
+ gate.gateNone = true;
391
+ else if (flag === "--confirm-gate")
392
+ gate.confirmGate = true;
393
+ else if (flag === "--clear-gate")
394
+ gate.clearGate = true;
395
+ else
396
+ gate.qualifyGate = true;
397
+ break;
327
398
  case "--repo":
328
399
  case "--required-check":
329
400
  case "--required-checks":
@@ -338,7 +409,12 @@ export function parseInstallBridgeConductorArgs(argv) {
338
409
  case "--launch-profile":
339
410
  case "--base-url":
340
411
  case "--execution-mode":
341
- case "--policy-file": {
412
+ case "--policy-file":
413
+ case "--provider-check":
414
+ case "--local-command":
415
+ case "--migration-guard":
416
+ case "--customized-command":
417
+ case "--epic-branch-ci-decision": {
342
418
  const taken = takeValue(flag, inline, argv[i + 1]);
343
419
  if (!taken.ok)
344
420
  return { status: "error", message: taken.error };
@@ -347,8 +423,35 @@ export function parseInstallBridgeConductorArgs(argv) {
347
423
  const value = taken.value;
348
424
  if (flag === "--repo")
349
425
  repo = value;
350
- else if (flag === "--required-check")
426
+ else if (flag === "--required-check" || flag === "--provider-check")
351
427
  requiredChecks.push(value);
428
+ else if (flag === "--local-command") {
429
+ const local = parseLocalCommandFlag(value);
430
+ if (!local.ok)
431
+ return { status: "error", message: local.error };
432
+ gate.localCommands.push(local.value);
433
+ }
434
+ else if (flag === "--migration-guard") {
435
+ if (gate.migrationGuard !== undefined) {
436
+ return { status: "error", message: "--migration-guard may be given once" };
437
+ }
438
+ const guard = parseMigrationGuardFlag(value);
439
+ if (!guard.ok)
440
+ return { status: "error", message: guard.error };
441
+ gate.migrationGuard = guard.value;
442
+ }
443
+ else if (flag === "--epic-branch-ci-decision") {
444
+ if (epicBranchCiDecision !== undefined) {
445
+ return { status: "error", message: "--epic-branch-ci-decision may be given once" };
446
+ }
447
+ const decision = parseEpicBranchCiDecisionFlag(value);
448
+ if (!decision.ok)
449
+ return { status: "error", message: decision.error };
450
+ epicBranchCiDecision = decision.value;
451
+ }
452
+ else if (flag === "--customized-command") {
453
+ gate.customizedCommands = [...(gate.customizedCommands ?? []), value];
454
+ }
352
455
  else if (flag === "--required-checks")
353
456
  requiredChecks.push(...splitList(value));
354
457
  else if (flag === "--epic-key")
@@ -407,9 +510,12 @@ export function parseInstallBridgeConductorArgs(argv) {
407
510
  return { status: "error", message: `Unknown option: ${flag}` };
408
511
  }
409
512
  }
410
- // Normalize + validate the required-check list at parse time so a bad list can
411
- // never reach the bootstrap request. An EMPTY done gate is not a weaker gate —
412
- // it passes unconditionally — so it is rejected here, not merely defaulted.
513
+ // Normalize + validate the provider-check list at parse time so a bad list can
514
+ // never reach a request. BAPI-1391 replaced "at least one --required-check" with
515
+ // "a confirmed gate": a stored gate, a confirmed proposal, or explicit gate
516
+ // flags — and a confirmed local-only or `none` gate is valid. An empty CI list
517
+ // is still never written as a passing gate: the server accepts one only for a
518
+ // stored local or none gate, and the run stops without a confirmed gate.
413
519
  const normalizedChecks = [];
414
520
  const seen = new Set();
415
521
  for (const check of requiredChecks) {
@@ -419,13 +525,10 @@ export function parseInstallBridgeConductorArgs(argv) {
419
525
  seen.add(check);
420
526
  normalizedChecks.push(check);
421
527
  }
422
- if (normalizedChecks.length === 0) {
423
- return {
424
- status: "error",
425
- message: "At least one --required-check is required. An empty done gate passes unconditionally, " +
426
- "so the bootstrap refuses to write one.",
427
- };
428
- }
528
+ gate.providerChecks = normalizedChecks;
529
+ const gateError = validateConductorGateFlags(gate, { replaceDoneGate });
530
+ if (gateError !== null)
531
+ return { status: "error", message: gateError };
429
532
  // BAPI-1137 — the claim scope is validated HERE, at parse time, for the same
430
533
  // reason the required-check list is: a unit that cannot start is worse than a
431
534
  // refused invocation, and discovering it costs a reboot to notice.
@@ -487,6 +590,7 @@ export function parseInstallBridgeConductorArgs(argv) {
487
590
  repo,
488
591
  dryRun,
489
592
  requiredChecks: normalizedChecks,
593
+ gate,
490
594
  reviewPolicy,
491
595
  reviewPolicyExplicit,
492
596
  epicKey,
@@ -505,6 +609,7 @@ export function parseInstallBridgeConductorArgs(argv) {
505
609
  ? { executionMode: executionMode ?? (executorId !== undefined ? "durable" : "local") }
506
610
  : {}),
507
611
  ...(policyFile ? { policyFile } : {}),
612
+ ...(epicBranchCiDecision ? { epicBranchCiDecision } : {}),
508
613
  },
509
614
  };
510
615
  }
@@ -690,12 +795,21 @@ async function confirm(deps, promptText) {
690
795
  }
691
796
  function renderCapabilityMatrix(deps, readiness) {
692
797
  const apiReady = readiness !== null;
798
+ // BAPI-1391 — a CONFIRMED local-only or `none` gate has no CI checks by
799
+ // design: its gate is the profile (executor-run commands, or approval for
800
+ // every merge), so an empty CI list is not "unconfigured" there. Read from
801
+ // the server's own confirmed-profile state; an older server reports no
802
+ // `gate_profile` block and keeps the original rule exactly.
803
+ const gateProfile = readiness?.gate_profile ?? null;
804
+ const confirmedCiFreeGate = gateProfile !== null &&
805
+ gateProfile.confirmed === true &&
806
+ (gateProfile.kind === "local" || gateProfile.kind === "none");
693
807
  // Derived from READINESS, never from the bootstrap's HTTP status: a 200 proves
694
808
  // the write was accepted, not that the resulting posture is safe.
695
809
  const configurationReady = readiness !== null &&
696
810
  readiness.supervisor.setup_present &&
697
811
  readiness.supervisor.config_present &&
698
- !readiness.supervisor.required_checks_empty;
812
+ (!readiness.supervisor.required_checks_empty || confirmedCiFreeGate);
699
813
  // `ready === null` (never provisioned) and `false` (stale) are both "not ready"
700
814
  // here, but only a recent server-observed claim/heartbeat counts as ready — a
701
815
  // configured command or a local process does not.
@@ -706,6 +820,14 @@ function renderCapabilityMatrix(deps, readiness) {
706
820
  deps.log(` api_ready: ${apiReady}`);
707
821
  deps.log(` configuration_ready: ${configurationReady}`);
708
822
  deps.log(` executor_ready: ${executorReady}`);
823
+ if (gateProfile !== null) {
824
+ // Names nothing: the kind, provenance, revision, and qualification state only.
825
+ deps.log(` repository gate: ${gateProfile.confirmed ? (gateProfile.kind ?? "unknown") : "not confirmed"}` +
826
+ (gateProfile.confirmed ? ` (${gateProfile.source}, revision ${gateProfile.revision}` +
827
+ (gateProfile.qualification_state && gateProfile.qualification_state !== "not_required"
828
+ ? `, local gate ${gateProfile.qualification_state})`
829
+ : ")") : ""));
830
+ }
709
831
  deps.log(` executor provisioning: ${EXECUTOR_PROVISIONING_GUIDANCE}`);
710
832
  deps.log(` runbook: ${CONDUCTOR_OPERATOR_RUNBOOK_POINTER}`);
711
833
  return { apiReady, configurationReady, executorReady };
@@ -1505,6 +1627,27 @@ export async function runInstallBridgeConductorCli(argv, deps) {
1505
1627
  const accessResult = await deps.resolveAccess(options.repo, baseUrl);
1506
1628
  const access = accessResult.ok ? accessResult.access : null;
1507
1629
  const accessError = accessResult.ok ? undefined : accessResult.error;
1630
+ // ---- BAPI-1391: the two standalone gate actions -----------------------
1631
+ // `--clear-gate`, and `--qualify-gate` without gate flags, act on the stored
1632
+ // gate and stop: they never bootstrap, scaffold, or provision anything else.
1633
+ const gateIo = {
1634
+ log: deps.log,
1635
+ errorLog: deps.errorLog,
1636
+ promptLine: deps.promptLine,
1637
+ fetchJson: deps.fetchJson,
1638
+ postJson: deps.postJson,
1639
+ };
1640
+ const qualifyOnly = options.gate.qualifyGate && !hasExplicitGateInput(options.gate);
1641
+ if (options.gate.clearGate || qualifyOnly) {
1642
+ if (!access) {
1643
+ deps.errorLog(`Bridge API access could not be resolved: ${accessError ?? "unknown"}`);
1644
+ deps.errorLog("Nothing was written.");
1645
+ return 1;
1646
+ }
1647
+ return options.gate.clearGate
1648
+ ? runClearConductorGate(gateIo, access, { dryRun: options.dryRun })
1649
+ : runQualifyConductorGate(gateIo, access, deps.gate, { dryRun: options.dryRun, baseUrl });
1650
+ }
1508
1651
  // ---- Phase 1: unified doctor, BEFORE every write -----------------------
1509
1652
  deps.log("Phase 1/8 — running the unified epic-implementer doctor (read-only)…");
1510
1653
  const firstReport = await deps.runDoctor({
@@ -1527,8 +1670,10 @@ export async function runInstallBridgeConductorCli(argv, deps) {
1527
1670
  "default and stamped into a new run that sets no review policy of its own. " +
1528
1671
  "An explicit per-run policy always wins, and changing this later never " +
1529
1672
  "alters an already-approved run."
1530
- : " Applied to runs created with this selection. No repository default is " +
1531
- "persisted — pass --review-policy explicitly to store one.");
1673
+ : " Install assumption only: no repository default is persisted and no " +
1674
+ "Claude review workflow is scaffolded. A new feature-branch run receives " +
1675
+ "repository_policy at first approval when nothing else sets one — pass " +
1676
+ "--review-policy verdict_protocol to opt in to AI review.");
1532
1677
  // ---- Executor preflight: AFTER the URL consent, BEFORE any mutation ----
1533
1678
  // Placed here so an unsupported host or a missing `wt`/`git`/`claude` refuses
1534
1679
  // before the bootstrap POST, the workflow write, or a single filesystem
@@ -1577,15 +1722,52 @@ export async function runInstallBridgeConductorCli(argv, deps) {
1577
1722
  deps.log(" executor provisioning skipped — pass --executor-id <id> to write and start a " +
1578
1723
  "service unit for this host.");
1579
1724
  }
1725
+ // ---- BAPI-1391: the repository gate, decided BEFORE any write ----------
1726
+ // Capability probe first (a read, after the base-URL consent), then the stored
1727
+ // gate, explicit gate flags, or a discovery proposal the owner confirms. Under
1728
+ // --dry-run it only shows the decision: no PUT, no enqueue, no gate prompt.
1729
+ if (!access) {
1730
+ deps.errorLog("Bridge API access could not be resolved; aborting before any write.");
1731
+ return 1;
1732
+ }
1733
+ const gatePhase = await resolveConductorGate(gateIo, access, options.gate, deps.gate, {
1734
+ dryRun: options.dryRun,
1735
+ });
1736
+ if (gatePhase.kind === "exit")
1737
+ return gatePhase.code;
1738
+ const gatePlan = gatePhase.kind === "plan" ? gatePhase.plan : null;
1739
+ // The bootstrap's CI list is the gate's provider leg. A local-only or `none`
1740
+ // gate sends an empty list, which the server accepts only for a stored gate
1741
+ // of that kind.
1742
+ const bootstrapChecks = gatePlan?.desired.providerChecks ?? options.requiredChecks;
1580
1743
  // ---- Dry run: preview and RETURN before every write/prompt -------------
1581
1744
  if (options.dryRun) {
1582
1745
  deps.log("");
1583
1746
  deps.log("Dry run — no writes will be performed.");
1747
+ if (gatePlan === null) {
1748
+ deps.log(" Repository gate: none confirmed — a real run asks you to confirm one before any write.");
1749
+ }
1750
+ else if (gatePlan.write !== null) {
1751
+ // Field NAMES and counts only, as below.
1752
+ deps.log(` Would PUT /jira/epic-runs/supervisor-setup/gate-profile/ (confirm, expected revision ` +
1753
+ `${gatePlan.write.expected_revision}) with field NAMES: kind, ` +
1754
+ `provider_checks (${gatePlan.desired.providerChecks.length}), ` +
1755
+ `local_commands (${gatePlan.desired.localCommands.length}), migration_guard` +
1756
+ (gatePlan.write.customized_commands !== undefined ? ", customized_commands" : ""));
1757
+ }
1758
+ else {
1759
+ deps.log(" Repository gate: already confirmed as shown — no gate write.");
1760
+ }
1761
+ if (gatePlan !== null && gatePlan.desired.localCommands.length > 0) {
1762
+ deps.log(" Gate qualification: would be offered after the gate is stored — no job was queued.");
1763
+ }
1764
+ deps.log(" Epic-branch CI: a real run inspects workflow triggers and epic-branch rules (GitHub only), " +
1765
+ "then asks include or decline — nothing was inspected or recorded.");
1584
1766
  deps.log(" Would PREVIEW, then POST /jira/epic-runs/supervisor-bootstrap with field NAMES:");
1585
1767
  // Field NAMES and a COUNT only — never the check values, which are
1586
1768
  // configuration content, and never the API key.
1587
1769
  deps.log(" - done_gate_config.required_checks " +
1588
- `(${options.requiredChecks.length} check(s))`);
1770
+ `(${bootstrapChecks.length} check(s))`);
1589
1771
  if (options.reviewPolicyExplicit)
1590
1772
  deps.log(" - review_policy");
1591
1773
  if (options.replaceDoneGate)
@@ -1636,10 +1818,10 @@ export async function runInstallBridgeConductorCli(argv, deps) {
1636
1818
  deps.log("complete-with-manual-steps (dry run)");
1637
1819
  return 0;
1638
1820
  }
1639
- if (!access) {
1640
- // Unreachable in practice (a null access produces a fatal doctor section
1641
- // above), but the type must be narrowed before the preview.
1642
- deps.errorLog("Bridge API access could not be resolved; aborting before the write.");
1821
+ if (gatePlan === null) {
1822
+ // Unreachable outside a dry run: the gate phase either decided a gate or
1823
+ // returned. Narrowed here so the bootstrap below always has one.
1824
+ deps.errorLog("No confirmed repository gate; aborting before any write.");
1643
1825
  return 1;
1644
1826
  }
1645
1827
  // ---- Phase 2: bootstrap — preview, consent, exact approval, apply -------
@@ -1650,7 +1832,7 @@ export async function runInstallBridgeConductorCli(argv, deps) {
1650
1832
  deps.log("");
1651
1833
  deps.log("Phase 2/8 — bootstrapping supervisor project defaults…");
1652
1834
  const bootstrapRequest = {
1653
- required_checks: options.requiredChecks,
1835
+ required_checks: bootstrapChecks,
1654
1836
  // BAPI-694 — the narrow ReviewPolicy object, sent ONLY on an explicit
1655
1837
  // selection. `review_source` is deliberately NOT sent: on an existing
1656
1838
  // repository an omitted field keeps its stored value, and a new repository
@@ -1661,6 +1843,32 @@ export async function runInstallBridgeConductorCli(argv, deps) {
1661
1843
  merge_approval_required: "true",
1662
1844
  ...(options.replaceDoneGate ? { replace_done_gate: true } : {}),
1663
1845
  };
1846
+ // BAPI-1391 — the gate is stored FIRST, under its own combined consent, in
1847
+ // two cases:
1848
+ // * a gate with no provider checks (local-only or `none`): the server
1849
+ // accepts an empty CI list only for a stored gate of that kind;
1850
+ // * any change to an already STORED gate: the bootstrap apply would
1851
+ // otherwise run BAPI-1390's profile sync first, which merges the additive
1852
+ // CI list into the stored profile under a new revision — so a later PUT
1853
+ // would be refused as stale while the gate had silently been widened.
1854
+ // Stored first, the CI checks already equal the gate, the bootstrap adds
1855
+ // nothing, and the sync has nothing to change.
1856
+ // A legacy or unconfirmed repository keeps the original order (preview,
1857
+ // consent, apply) and is stored after the apply; the sync never touches it.
1858
+ const gateFirst = gatePlan.write !== null &&
1859
+ (bootstrapChecks.length === 0 || gatePlan.current.source === "stored");
1860
+ let storedGate = gatePlan.write === null ? gatePlan.current : null;
1861
+ if (gateFirst) {
1862
+ deps.log("");
1863
+ const gateOk = await confirm(deps, "Store this repository gate and write the safe project-default supervisor posture now? [y/N]: ");
1864
+ if (!gateOk) {
1865
+ deps.log("Aborted — no server write was attempted.");
1866
+ return 0;
1867
+ }
1868
+ storedGate = await storeConductorGate(gateIo, access, gatePlan);
1869
+ if (storedGate === null)
1870
+ return 1;
1871
+ }
1664
1872
  let preview;
1665
1873
  try {
1666
1874
  preview = await previewConductorSupervisorBootstrap(access, bootstrapRequest, deps.postJson);
@@ -1673,11 +1881,14 @@ export async function runInstallBridgeConductorCli(argv, deps) {
1673
1881
  // ---- Consent #2: the bootstrap write itself ----------------------------
1674
1882
  // Deliberately separate from the base-URL consent above (which was given
1675
1883
  // before any network call). Confirming WHERE to write is not consent TO write.
1676
- deps.log("");
1677
- const bootstrapOk = await confirm(deps, "Write the safe project-default supervisor posture now? [y/N]: ");
1678
- if (!bootstrapOk) {
1679
- deps.log("Aborted — no server write was attempted.");
1680
- return 0;
1884
+ // A gate stored first above was stored under its own combined consent.
1885
+ if (!gateFirst) {
1886
+ deps.log("");
1887
+ const bootstrapOk = await confirm(deps, "Write the safe project-default supervisor posture now? [y/N]: ");
1888
+ if (!bootstrapOk) {
1889
+ deps.log("Aborted — no server write was attempted.");
1890
+ return 0;
1891
+ }
1681
1892
  }
1682
1893
  // ---- Consent #3: the EXACT gate change, only when the preview needs it ---
1683
1894
  // Never inferred from consent #2: agreeing to install is not agreeing to
@@ -1709,6 +1920,49 @@ export async function runInstallBridgeConductorCli(argv, deps) {
1709
1920
  return 1;
1710
1921
  }
1711
1922
  renderBootstrapOutcome(deps, bootstrapResponse);
1923
+ // ---- BAPI-1391: store the confirmed gate, then offer its qualification --
1924
+ // Stored after the apply so the bootstrap's additive CI write cannot widen
1925
+ // the gate's exact provider checks: the gate PUT rewrites that projection to
1926
+ // exactly the confirmed checks, under the server's revision compare-and-swap.
1927
+ if (storedGate === null) {
1928
+ storedGate = await storeConductorGate(gateIo, access, gatePlan);
1929
+ if (storedGate === null)
1930
+ return 1;
1931
+ }
1932
+ const manualActions = [];
1933
+ if (gatePlan.desired.localCommands.length > 0) {
1934
+ const qualified = storedGate.qualification_state === "qualified";
1935
+ let qualify = options.gate.qualifyGate;
1936
+ if (!qualify && !qualified) {
1937
+ deps.log("");
1938
+ deps.log(" The local gate must pass once under the executor before an unattended run is admitted.");
1939
+ qualify = await confirm(deps, "Qualify gate now? [y/N]: ");
1940
+ }
1941
+ if (qualify) {
1942
+ // Non-fatal by design: a failed qualification warns and the install goes on.
1943
+ await runConductorGateQualification(gateIo, access, deps.gate, {
1944
+ revision: storedGate.revision,
1945
+ baseUrl,
1946
+ });
1947
+ }
1948
+ else if (!qualified) {
1949
+ manualActions.push(`gate qualification: run ${PRINTED_QUALIFY_GATE_COMMAND} to qualify the local gate; ` +
1950
+ "until then admission refuses it (local_command_unqualified).");
1951
+ }
1952
+ else {
1953
+ deps.log(` local gate already qualified at revision ${storedGate.revision}.`);
1954
+ }
1955
+ }
1956
+ // ---- BAPI-1405: epic-branch CI inspection and the explicit decision ----
1957
+ // Runs after the gate is resolved and stored, inside the gate phase. It only
1958
+ // reports and proposes; it never confirms a gate or authorizes a merge. A
1959
+ // failed read or decision write warns and the install continues.
1960
+ const epicBranchCi = await runEpicBranchCiInspection(gateIo, access, deps.gate, {
1961
+ decisionFlag: options.epicBranchCiDecision ?? null,
1962
+ dryRun: false,
1963
+ });
1964
+ if (epicBranchCi.manualAction !== null)
1965
+ manualActions.push(epicBranchCi.manualAction);
1712
1966
  // ---- Phase 3: optional workflow scaffold -------------------------------
1713
1967
  deps.log("");
1714
1968
  deps.log("Phase 3/8 — claude-review workflow…");
@@ -1772,7 +2026,6 @@ export async function runInstallBridgeConductorCli(argv, deps) {
1772
2026
  // Placed after the workflow scaffold and BEFORE setup-epic verification,
1773
2027
  // executor provisioning, and the capability report, so a run that stops at a
1774
2028
  // later phase has still closed the local gap it consented to close.
1775
- const manualActions = [];
1776
2029
  deps.log("");
1777
2030
  deps.log("Phase 4/8 — conductor tool visibility…");
1778
2031
  if (local) {