@bridge_gpt/mcp-server 0.2.59 → 0.2.62

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 (78) hide show
  1. package/README.md +181 -51
  2. package/build/agent-guidance.generated.js +1 -1
  3. package/build/agent-notes.js +7 -1
  4. package/build/commands.generated.js +6 -6
  5. package/build/conductor/bridge-api-client.js +406 -4
  6. package/build/conductor/doctor.js +255 -53
  7. package/build/conductor/epic-runtime.js +11 -2
  8. package/build/conductor/install-doctor.js +24 -2
  9. package/build/conductor/local-merge.js +319 -26
  10. package/build/conductor/merge-execution-contract.js +86 -0
  11. package/build/conductor/merge-execution-eligibility.js +130 -0
  12. package/build/conductor/readiness-sections.js +256 -16
  13. package/build/conductor/readiness.js +203 -29
  14. package/build/conductor/recovery-operations.js +8 -2
  15. package/build/connect-bitbucket-api.js +6 -1
  16. package/build/connect-bitbucket.js +398 -88
  17. package/build/credential-store.js +172 -0
  18. package/build/docs.generated.js +2 -2
  19. package/build/doctor.js +277 -15
  20. package/build/drive-epic.js +147 -12
  21. package/build/epic-implementer-bin.js +15 -15
  22. package/build/epic-implementer-claude-hook-bin.js +1 -1
  23. package/build/epic-implementer-env.js +43 -14
  24. package/build/epic-integration-pr.js +61 -15
  25. package/build/epic-launch-profile.js +455 -0
  26. package/build/executor/ci-rerun-job.js +269 -0
  27. package/build/executor/cli.js +40 -0
  28. package/build/executor/credentials.js +28 -1
  29. package/build/executor/env.js +60 -0
  30. package/build/executor/http-client.js +45 -1
  31. package/build/executor/index.js +21 -4
  32. package/build/executor/integration-job.js +759 -0
  33. package/build/executor/job-runner.js +218 -9
  34. package/build/executor/job-types.js +12 -2
  35. package/build/executor/merge-job.js +11 -2
  36. package/build/executor/observation.js +114 -7
  37. package/build/executor/runner.js +101 -1
  38. package/build/executor/service-lifecycle.js +136 -0
  39. package/build/executor/service-unit.js +427 -9
  40. package/build/executor/types.js +48 -0
  41. package/build/index.js +479 -240
  42. package/build/install-bridge-conductor.js +599 -14
  43. package/build/install-bridge.js +448 -46
  44. package/build/mcp-install-state.js +42 -4
  45. package/build/merge-pull-request.js +103 -10
  46. package/build/merge-workflow-codeowners.js +224 -0
  47. package/build/merge-workflow-config.js +187 -0
  48. package/build/merge-workflow-drift.js +97 -0
  49. package/build/merge-workflow-install.js +638 -0
  50. package/build/merge-workflow.js +956 -0
  51. package/build/pipelines.generated.js +3 -3
  52. package/build/plane/cli.js +24 -2
  53. package/build/plane/preflight.js +68 -0
  54. package/build/plane/supervisor.js +6 -0
  55. package/build/readme.generated.js +1 -1
  56. package/build/run-unit-tests-launcher.js +5 -0
  57. package/build/setup-epic.js +263 -38
  58. package/build/version.generated.js +3 -3
  59. package/package.json +3 -3
  60. package/build/conduct-epic/bridge-client.js +0 -773
  61. package/build/conduct-epic/checkpoint-store.js +0 -542
  62. package/build/conduct-epic/cli.js +0 -3145
  63. package/build/conduct-epic/cut-protocol.js +0 -392
  64. package/build/conduct-epic/lock.js +0 -302
  65. package/build/conduct-epic/pr-state.js +0 -286
  66. package/build/conduct-epic/spawn.js +0 -113
  67. package/build/conductor/cli.js +0 -1278
  68. package/build/conductor-bundle-cli.js +0 -256
  69. package/build/direct-ticket-tools.js +0 -70
  70. package/build/executor/reconcile-overlap-governance.js +0 -129
  71. package/build/implement-epic/bridge-client.js +0 -773
  72. package/build/implement-epic/checkpoint-store.js +0 -542
  73. package/build/implement-epic/cli.js +0 -3158
  74. package/build/implement-epic/cut-protocol.js +0 -392
  75. package/build/implement-epic/lock.js +0 -302
  76. package/build/implement-epic/pr-state.js +0 -286
  77. package/build/implement-epic/spawn.js +0 -113
  78. package/build/plan-epic-conductor-eligibility.js +0 -213
@@ -55,7 +55,9 @@
55
55
  */
56
56
  import { readFile } from "node:fs/promises";
57
57
  import { dirname, join } from "node:path";
58
- import { ConductorBridgeApiError, resolveConductorBridgeApiAccess, fetchConductorReadiness, fetchEpicRunState, safeDiagnosticMessage, } from "./conductor/bridge-api-client.js";
58
+ import { ConductorBridgeApiError, resolveEpicLaunchBridgeApiAccess, fetchConductorReadiness, fetchEpicRunState, safeDiagnosticMessage, } from "./conductor/bridge-api-client.js";
59
+ import { EPIC_LAUNCH_PROFILE_NAME_PATTERN, renderEpicLaunchCommand, shellQuote, } from "./epic-launch-profile.js";
60
+ import { validateHttpBaseUrl, describeBaseUrlRejection } from "./base-url.js";
59
61
  import { BRING_UP_REFUSAL_MESSAGES, BRING_UP_RUNBOOK, firstBringUpRefusal, } from "./conductor/bring-up-facts.js";
60
62
  import { runSetupEpicWorkflow, } from "./setup-epic.js";
61
63
  import { formatPlaneDiagnostic, launchPlaneControlPlane, requestPlaneScopedLanes, runDefaultPlanePreflight, runPlaneCli, } from "./plane/cli.js";
@@ -161,6 +163,29 @@ export const V2_READINESS_REQUIREMENTS = [
161
163
  remedy: "Run `install-bridge conductor` to install them, or pass --attended.",
162
164
  satisfied: (r) => r.unattended?.repository_readiness_confirmed === true,
163
165
  },
166
+ // BAPI-1135 — merge-execution eligibility is deliberately NOT a requirement here.
167
+ //
168
+ // It was one, and it made `drive-epic` refuse EVERY repository the moment it
169
+ // shipped. The requirement could only be keyed on repository facts, because
170
+ // this layer keeps the composed run policy opaque to itself — and no
171
+ // repository carries the merge workflow until epic child 4 lands it. So both
172
+ // states resolved the same way: an upgraded server reported
173
+ // `merge_workflow_present: false` (`not_eligible`), an older one reported no
174
+ // block at all (`could_not_verify`), and either made `selectConductor` return
175
+ // `not_ready` and `classifyV2Readiness` return `operator_blocked` — for
176
+ // `--attended`, `--into-base` and `local`-mode runs alike, with a remedy
177
+ // naming a flag that does not exist yet.
178
+ //
179
+ // That is exactly the failure `selectConductor`'s own docstring warns about,
180
+ // and it contradicts the compatibility guarantee the rest of this slice keeps:
181
+ // nothing changes for a run that does not declare `github_actions`.
182
+ //
183
+ // Eligibility is still enforced, at the three layers that can actually see the
184
+ // declared mode: `setup-epic`'s pre-create refusal (which reads the composed
185
+ // policy), the approval route's server-side gate, and — authoritatively — the
186
+ // before-merge preflight. Readiness still REPORTS the facts as diagnostic
187
+ // rows, which inform without blocking. Reinstating a requirement here belongs
188
+ // with child 4, once a repository can actually satisfy it.
164
189
  ];
165
190
  /**
166
191
  * The readiness facts a `plane up` can actually repair (BAPI-1054).
@@ -310,6 +335,18 @@ export function getDriveEpicUsage() {
310
335
  " --review-policy <src> PER-RUN review policy source.",
311
336
  " --replace-policy Authorize replacing a LIVE run's stored policy.",
312
337
  "",
338
+ "Launch target (BAPI-1260) — decided BEFORE any authenticated request:",
339
+ " --launch-profile <name> Select the host-local launch profile guided setup",
340
+ " approved (`install conductor --launch-profile <name>`):",
341
+ " target, repository, mode, runtime, and policy and",
342
+ " credential references. The command setup prints",
343
+ " already selects it and runs in a fresh shell.",
344
+ " --base-url <url> Explicit Bridge API target. With a profile it must",
345
+ " agree with it — an override never rebinds a profile.",
346
+ "Without either, BAPI_BASE_URL is used; with none of the three, drive-epic",
347
+ "refuses. Nothing defaults to production — select it deliberately. A stored",
348
+ "credential is sent only to the endpoint setup approved it for.",
349
+ "",
313
350
  " -h, --help Show this help",
314
351
  ].join("\n");
315
352
  }
@@ -336,6 +373,9 @@ export function parseDriveEpicArgs(argv) {
336
373
  let policyFile;
337
374
  let reviewPolicy;
338
375
  let replacePolicy = false;
376
+ // BAPI-1260 launch selection.
377
+ let launchProfile;
378
+ let baseUrl;
339
379
  // BAPI-1009: validated with the SHARED validator setup-epic uses, so a name
340
380
  // drive-epic accepts is exactly a name setup-epic accepts. A second rule here
341
381
  // would let a name pass the router and fail the delegate.
@@ -371,7 +411,9 @@ export function parseDriveEpicArgs(argv) {
371
411
  arg === "--repo" ||
372
412
  arg === "--feature-branch" ||
373
413
  arg === "--policy-file" ||
374
- arg === "--review-policy") {
414
+ arg === "--review-policy" ||
415
+ arg === "--launch-profile" ||
416
+ arg === "--base-url") {
375
417
  const value = argv[i + 1];
376
418
  if (value === undefined || value.startsWith("-")) {
377
419
  return { status: "error", message: `${arg} requires a value.` };
@@ -384,6 +426,10 @@ export function parseDriveEpicArgs(argv) {
384
426
  policyFile = value;
385
427
  else if (arg === "--review-policy")
386
428
  reviewPolicy = value;
429
+ else if (arg === "--launch-profile")
430
+ launchProfile = value;
431
+ else if (arg === "--base-url")
432
+ baseUrl = value;
387
433
  else if (takeFeatureBranch(value) === null) {
388
434
  return {
389
435
  status: "error",
@@ -393,7 +439,7 @@ export function parseDriveEpicArgs(argv) {
393
439
  i++;
394
440
  continue;
395
441
  }
396
- const eq = arg.match(/^(--plan-file|--repo|--feature-branch|--policy-file|--review-policy)=(.*)$/);
442
+ const eq = arg.match(/^(--plan-file|--repo|--feature-branch|--policy-file|--review-policy|--launch-profile|--base-url)=(.*)$/);
397
443
  if (eq) {
398
444
  const value = eq[2];
399
445
  if (eq[1] === "--feature-branch") {
@@ -414,6 +460,10 @@ export function parseDriveEpicArgs(argv) {
414
460
  policyFile = value;
415
461
  else if (eq[1] === "--review-policy")
416
462
  reviewPolicy = value;
463
+ else if (eq[1] === "--launch-profile")
464
+ launchProfile = value;
465
+ else if (eq[1] === "--base-url")
466
+ baseUrl = value;
417
467
  else
418
468
  repo = value;
419
469
  continue;
@@ -435,6 +485,22 @@ export function parseDriveEpicArgs(argv) {
435
485
  "may be selected — remove whichever one you did not mean.",
436
486
  };
437
487
  }
488
+ // BAPI-1260 — the launch selection is validated for SHAPE here, before any
489
+ // side effect. Whether it agrees with the profile and the environment is
490
+ // decided by the target resolver, still before any authenticated request.
491
+ if (launchProfile !== undefined && !EPIC_LAUNCH_PROFILE_NAME_PATTERN.test(launchProfile)) {
492
+ return {
493
+ status: "error",
494
+ message: "--launch-profile must match [a-z0-9][a-z0-9._-]{0,62}.",
495
+ };
496
+ }
497
+ if (baseUrl !== undefined) {
498
+ const validated = validateHttpBaseUrl(baseUrl);
499
+ if (!validated.ok) {
500
+ return { status: "error", message: describeBaseUrlRejection("--base-url", validated.reason) };
501
+ }
502
+ baseUrl = validated.baseUrl;
503
+ }
438
504
  if (positionals.length === 0) {
439
505
  return { status: "error", message: "Missing required epic key." };
440
506
  }
@@ -462,6 +528,8 @@ export function parseDriveEpicArgs(argv) {
462
528
  ...(policyFile ? { policyFile } : {}),
463
529
  ...(reviewPolicy ? { reviewPolicy } : {}),
464
530
  ...(replacePolicy ? { replacePolicy: true } : {}),
531
+ ...(launchProfile ? { launchProfile } : {}),
532
+ ...(baseUrl ? { baseUrl } : {}),
465
533
  };
466
534
  return { status: "ok", options };
467
535
  }
@@ -514,7 +582,7 @@ export function selectConductor(readiness) {
514
582
  * no quoting to get wrong.
515
583
  */
516
584
  export function buildSetupEpicArgv(options) {
517
- const { epicKey, planFile, repo, featureBranch, intoBase, attended, policyFile, reviewPolicy, replacePolicy, } = options;
585
+ const { epicKey, planFile, repo, featureBranch, intoBase, attended, policyFile, reviewPolicy, replacePolicy, launchProfile, baseUrl, } = options;
518
586
  return [
519
587
  "--epic-key",
520
588
  epicKey,
@@ -528,6 +596,11 @@ export function buildSetupEpicArgv(options) {
528
596
  ...(policyFile ? ["--policy-file", policyFile] : []),
529
597
  ...(reviewPolicy ? ["--review-policy", reviewPolicy] : []),
530
598
  ...(replacePolicy ? ["--replace-policy"] : []),
599
+ // BAPI-1260 — the SAME launch selection, so setup-epic resolves the same
600
+ // target and bound credential drive-epic just did. Appended only when
601
+ // supplied, so every pre-existing argv stays byte-identical.
602
+ ...(launchProfile ? ["--launch-profile", launchProfile] : []),
603
+ ...(baseUrl ? ["--base-url", baseUrl] : []),
531
604
  ];
532
605
  }
533
606
  /**
@@ -538,8 +611,25 @@ export function buildSetupEpicArgv(options) {
538
611
  * is only true once {@link runDriveEpicCli} has declined to delegate — so route
539
612
  * first, render second.
540
613
  */
541
- export function renderConductorHandoff(selection, epicKey) {
542
- const invocation = `npx -y ${MCP_PACKAGE_NAME} drive-epic ${epicKey} --plan-file <path>`;
614
+ export function renderConductorHandoff(selection, epicKey,
615
+ /**
616
+ * BAPI-1260 — the selected launch profile and the options this invocation
617
+ * carried. With a profile, the re-run command selects it and carries every
618
+ * non-secret flag it needs, shell-quoted, so it runs unchanged in a fresh
619
+ * shell. The plan file is the one thing that cannot be filled in yet, and it
620
+ * is the only placeholder left.
621
+ */
622
+ launch = { profile: null }) {
623
+ const invocation = launch.profile !== null
624
+ ? renderEpicLaunchCommand(launch.profile, {
625
+ epicKey,
626
+ planFile: "<path>",
627
+ ...(launch.options?.policyFile ? { policyFile: launch.options.policyFile } : {}),
628
+ ...(launch.options?.attended ? { attended: true } : {}),
629
+ ...(launch.options?.intoBase ? { intoBase: true } : {}),
630
+ ...(launch.options?.featureBranch ? { featureBranch: launch.options.featureBranch } : {}),
631
+ }).replace(`${shellQuote("<path>")}`, "<path>")
632
+ : `npx -y ${MCP_PACKAGE_NAME} drive-epic ${epicKey} --plan-file <path>`;
543
633
  return {
544
634
  conductor: "v2",
545
635
  invocation,
@@ -585,6 +675,16 @@ export function assertSingleConductorInvocation(text) {
585
675
  throw new Error(`drive-epic emitted more than one conductor invocation (${named.join(", ")}). ` +
586
676
  "Exactly one path may ever be presented.");
587
677
  }
678
+ // BAPI-1260 — the entry point itself, too. A rendered handoff now carries a
679
+ // complete, runnable `drive-epic <KEY> ...` command; two DIFFERENT runnable
680
+ // commands (say, one per profile or target) would hand the reader a choice of
681
+ // authority just as surely as naming two conductors. The same command
682
+ // repeated is still one invocation.
683
+ const runnable = new Set((text.match(/\bdrive-epic\s+[A-Z][A-Z0-9]*-[0-9]+[^\n]*/g) ?? []).map((line) => line.trim()));
684
+ if (runnable.size > 1) {
685
+ throw new Error(`drive-epic emitted ${runnable.size} different runnable launch commands. ` +
686
+ "Exactly one path may ever be presented.");
687
+ }
588
688
  }
589
689
  /**
590
690
  * Conservative bound on any single advisory field. Exists so malformed or
@@ -786,14 +886,31 @@ export function resolvePlaneReconcilerInstanceId(manifest, env, hostname) {
786
886
  }
787
887
  /** Terminal `epic_runs.status` values; every other status is a live run. */
788
888
  const TERMINAL_EPIC_RUN_STATUSES = ["abandoned", "done"];
889
+ /** The BAPI-1260 launch selection carried by parsed options. */
890
+ function launchSelectionOf(options) {
891
+ return {
892
+ ...(options.launchProfile ? { profileName: options.launchProfile } : {}),
893
+ ...(options.baseUrl ? { baseUrl: options.baseUrl } : {}),
894
+ };
895
+ }
789
896
  export function createDefaultDriveEpicDeps() {
790
897
  return {
791
- resolveAccess: async (repo, cwd) => {
792
- const result = await resolveConductorBridgeApiAccess({
898
+ resolveAccess: async (repo, cwd, launch) => {
899
+ // BAPI-1260: the target-first launch contract. The generic resolver's
900
+ // production default does not apply to an epic launch.
901
+ const result = await resolveEpicLaunchBridgeApiAccess(launch ?? {}, {
793
902
  ...(repo ? { repoName: repo } : {}),
794
903
  ...(cwd ? { cwd } : {}),
795
904
  });
796
- return result.ok ? { ok: true, access: result.access } : { ok: false, error: result.error };
905
+ return result.ok
906
+ ? { ok: true, access: result.access, profile: result.profile }
907
+ : { ok: false, error: result.error };
908
+ },
909
+ // Non-secret, and only ever a value the resolver just agreed with the
910
+ // environment on (a conflicting BAPI_BASE_URL was refused above), so the
911
+ // processes this command starts see the same target it resolved.
912
+ adoptLaunchTarget: (baseUrl) => {
913
+ process.env.BAPI_BASE_URL = baseUrl;
797
914
  },
798
915
  readReadiness: (access) => fetchConductorReadiness(access, globalThis.fetch),
799
916
  runSetupEpic: (argv) => runSetupEpicWorkflow(argv),
@@ -864,12 +981,19 @@ export async function runDriveEpicCli(argv, overrides = {}) {
864
981
  return 1;
865
982
  }
866
983
  const { epicKey, planFile, repo, featureBranch, intoBase } = parsed.options;
984
+ const launch = launchSelectionOf(parsed.options);
867
985
  try {
868
986
  // --- Resolve access. A missing credential is UNKNOWN, not not-ready. -----
869
- const accessResult = await deps.resolveAccess(repo);
987
+ // BAPI-1260: the TARGET is decided first — from --launch-profile,
988
+ // --base-url, and BAPI_BASE_URL — and only then is a credential bound to
989
+ // that target read. A missing or conflicting target refuses here, before
990
+ // any authenticated request; nothing defaults to production.
991
+ const accessResult = await deps.resolveAccess(repo, undefined, launch);
870
992
  if (!accessResult.ok) {
871
993
  return escalate(deps, epicKey, `conductor readiness could not be read: ${accessResult.error}`);
872
994
  }
995
+ const launchProfile = accessResult.profile ?? null;
996
+ deps.adoptLaunchTarget?.(accessResult.access.baseUrl);
873
997
  // --- Read readiness. Any failure here is UNKNOWN — with ONE exception. ---
874
998
  let readiness;
875
999
  try {
@@ -884,6 +1008,14 @@ export async function runDriveEpicCli(argv, overrides = {}) {
884
1008
  // because there unknown is still fatal (R14).
885
1009
  if (planFile !== undefined && err instanceof ConductorBridgeApiError && err.kind === "network") {
886
1010
  const endpoint = planeEndpointForBaseUrl(deps, accessResult.access.baseUrl);
1011
+ // BAPI-1260 — only a SELECTED LOCAL plane is ever started here. A
1012
+ // profile approved for durable execution names an authority this
1013
+ // machine does not own, so a refused port is that authority being
1014
+ // down — never permission to launch a local replacement.
1015
+ if (endpoint !== null && launchProfile !== null && launchProfile.executionMode !== "local") {
1016
+ return escalate(deps, epicKey, `launch profile '${launchProfile.name}' selects durable execution, and its control plane is ` +
1017
+ "not answering; no local replacement was started");
1018
+ }
887
1019
  if (endpoint !== null) {
888
1020
  const probe = await deps.probePlanePort(endpoint.host, endpoint.port);
889
1021
  if (probe.kind === "refused") {
@@ -1055,7 +1187,10 @@ export async function runDriveEpicCli(argv, overrides = {}) {
1055
1187
  deps.errorLog(refusal);
1056
1188
  return 1;
1057
1189
  }
1058
- const handoff = renderConductorHandoff(selection, epicKey);
1190
+ const handoff = renderConductorHandoff(selection, epicKey, {
1191
+ profile: launchProfile,
1192
+ options: parsed.options,
1193
+ });
1059
1194
  const text = handoff.lines.join("\n");
1060
1195
  assertSingleConductorInvocation(text);
1061
1196
  deps.log(text);
@@ -1264,7 +1399,7 @@ async function coldStartPlane(ctx) {
1264
1399
  */
1265
1400
  async function attachToPlane(ctx, manifest, readiness) {
1266
1401
  const { deps, epicKey } = ctx;
1267
- const rooted = await deps.resolveAccess(ctx.options.repo, manifest.repoRoot);
1402
+ const rooted = await deps.resolveAccess(ctx.options.repo, manifest.repoRoot, launchSelectionOf(ctx.options));
1268
1403
  if (!rooted.ok) {
1269
1404
  return refuse(deps, [
1270
1405
  `${epicKey}: cannot attach to the running plane — Bridge API credentials could not be ` +