@bridge_gpt/mcp-server 0.2.60 → 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.
- package/README.md +16 -8
- package/build/commands.generated.js +3 -3
- package/build/conductor/bridge-api-client.js +353 -2
- package/build/conductor/merge-execution-contract.js +30 -5
- package/build/conductor/merge-execution-eligibility.js +10 -9
- package/build/conductor/readiness-sections.js +99 -16
- package/build/conductor/readiness.js +108 -15
- package/build/credential-store.js +156 -0
- package/build/doctor.js +94 -6
- package/build/drive-epic.js +124 -12
- package/build/epic-implementer-bin.js +13 -13
- package/build/epic-integration-pr.js +61 -15
- package/build/epic-launch-profile.js +455 -0
- package/build/executor/cli.js +9 -0
- package/build/executor/credentials.js +17 -1
- package/build/executor/http-client.js +45 -1
- package/build/executor/integration-job.js +759 -0
- package/build/executor/job-runner.js +92 -9
- package/build/executor/observation.js +114 -7
- package/build/executor/runner.js +101 -1
- package/build/executor/types.js +43 -0
- package/build/index.js +338 -114
- package/build/install-bridge-conductor.js +398 -6
- package/build/install-bridge.js +92 -16
- package/build/merge-workflow-config.js +54 -0
- package/build/merge-workflow-drift.js +35 -4
- package/build/merge-workflow.js +386 -2
- package/build/pipelines.generated.js +1 -1
- package/build/plane/supervisor.js +6 -0
- package/build/readme.generated.js +1 -1
- package/build/setup-epic.js +103 -32
- package/build/version.generated.js +3 -3
- package/package.json +1 -1
package/build/drive-epic.js
CHANGED
|
@@ -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,
|
|
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";
|
|
@@ -333,6 +335,18 @@ export function getDriveEpicUsage() {
|
|
|
333
335
|
" --review-policy <src> PER-RUN review policy source.",
|
|
334
336
|
" --replace-policy Authorize replacing a LIVE run's stored policy.",
|
|
335
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
|
+
"",
|
|
336
350
|
" -h, --help Show this help",
|
|
337
351
|
].join("\n");
|
|
338
352
|
}
|
|
@@ -359,6 +373,9 @@ export function parseDriveEpicArgs(argv) {
|
|
|
359
373
|
let policyFile;
|
|
360
374
|
let reviewPolicy;
|
|
361
375
|
let replacePolicy = false;
|
|
376
|
+
// BAPI-1260 launch selection.
|
|
377
|
+
let launchProfile;
|
|
378
|
+
let baseUrl;
|
|
362
379
|
// BAPI-1009: validated with the SHARED validator setup-epic uses, so a name
|
|
363
380
|
// drive-epic accepts is exactly a name setup-epic accepts. A second rule here
|
|
364
381
|
// would let a name pass the router and fail the delegate.
|
|
@@ -394,7 +411,9 @@ export function parseDriveEpicArgs(argv) {
|
|
|
394
411
|
arg === "--repo" ||
|
|
395
412
|
arg === "--feature-branch" ||
|
|
396
413
|
arg === "--policy-file" ||
|
|
397
|
-
arg === "--review-policy"
|
|
414
|
+
arg === "--review-policy" ||
|
|
415
|
+
arg === "--launch-profile" ||
|
|
416
|
+
arg === "--base-url") {
|
|
398
417
|
const value = argv[i + 1];
|
|
399
418
|
if (value === undefined || value.startsWith("-")) {
|
|
400
419
|
return { status: "error", message: `${arg} requires a value.` };
|
|
@@ -407,6 +426,10 @@ export function parseDriveEpicArgs(argv) {
|
|
|
407
426
|
policyFile = value;
|
|
408
427
|
else if (arg === "--review-policy")
|
|
409
428
|
reviewPolicy = value;
|
|
429
|
+
else if (arg === "--launch-profile")
|
|
430
|
+
launchProfile = value;
|
|
431
|
+
else if (arg === "--base-url")
|
|
432
|
+
baseUrl = value;
|
|
410
433
|
else if (takeFeatureBranch(value) === null) {
|
|
411
434
|
return {
|
|
412
435
|
status: "error",
|
|
@@ -416,7 +439,7 @@ export function parseDriveEpicArgs(argv) {
|
|
|
416
439
|
i++;
|
|
417
440
|
continue;
|
|
418
441
|
}
|
|
419
|
-
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)=(.*)$/);
|
|
420
443
|
if (eq) {
|
|
421
444
|
const value = eq[2];
|
|
422
445
|
if (eq[1] === "--feature-branch") {
|
|
@@ -437,6 +460,10 @@ export function parseDriveEpicArgs(argv) {
|
|
|
437
460
|
policyFile = value;
|
|
438
461
|
else if (eq[1] === "--review-policy")
|
|
439
462
|
reviewPolicy = value;
|
|
463
|
+
else if (eq[1] === "--launch-profile")
|
|
464
|
+
launchProfile = value;
|
|
465
|
+
else if (eq[1] === "--base-url")
|
|
466
|
+
baseUrl = value;
|
|
440
467
|
else
|
|
441
468
|
repo = value;
|
|
442
469
|
continue;
|
|
@@ -458,6 +485,22 @@ export function parseDriveEpicArgs(argv) {
|
|
|
458
485
|
"may be selected — remove whichever one you did not mean.",
|
|
459
486
|
};
|
|
460
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
|
+
}
|
|
461
504
|
if (positionals.length === 0) {
|
|
462
505
|
return { status: "error", message: "Missing required epic key." };
|
|
463
506
|
}
|
|
@@ -485,6 +528,8 @@ export function parseDriveEpicArgs(argv) {
|
|
|
485
528
|
...(policyFile ? { policyFile } : {}),
|
|
486
529
|
...(reviewPolicy ? { reviewPolicy } : {}),
|
|
487
530
|
...(replacePolicy ? { replacePolicy: true } : {}),
|
|
531
|
+
...(launchProfile ? { launchProfile } : {}),
|
|
532
|
+
...(baseUrl ? { baseUrl } : {}),
|
|
488
533
|
};
|
|
489
534
|
return { status: "ok", options };
|
|
490
535
|
}
|
|
@@ -537,7 +582,7 @@ export function selectConductor(readiness) {
|
|
|
537
582
|
* no quoting to get wrong.
|
|
538
583
|
*/
|
|
539
584
|
export function buildSetupEpicArgv(options) {
|
|
540
|
-
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;
|
|
541
586
|
return [
|
|
542
587
|
"--epic-key",
|
|
543
588
|
epicKey,
|
|
@@ -551,6 +596,11 @@ export function buildSetupEpicArgv(options) {
|
|
|
551
596
|
...(policyFile ? ["--policy-file", policyFile] : []),
|
|
552
597
|
...(reviewPolicy ? ["--review-policy", reviewPolicy] : []),
|
|
553
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] : []),
|
|
554
604
|
];
|
|
555
605
|
}
|
|
556
606
|
/**
|
|
@@ -561,8 +611,25 @@ export function buildSetupEpicArgv(options) {
|
|
|
561
611
|
* is only true once {@link runDriveEpicCli} has declined to delegate — so route
|
|
562
612
|
* first, render second.
|
|
563
613
|
*/
|
|
564
|
-
export function renderConductorHandoff(selection, epicKey
|
|
565
|
-
|
|
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>`;
|
|
566
633
|
return {
|
|
567
634
|
conductor: "v2",
|
|
568
635
|
invocation,
|
|
@@ -608,6 +675,16 @@ export function assertSingleConductorInvocation(text) {
|
|
|
608
675
|
throw new Error(`drive-epic emitted more than one conductor invocation (${named.join(", ")}). ` +
|
|
609
676
|
"Exactly one path may ever be presented.");
|
|
610
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
|
+
}
|
|
611
688
|
}
|
|
612
689
|
/**
|
|
613
690
|
* Conservative bound on any single advisory field. Exists so malformed or
|
|
@@ -809,14 +886,31 @@ export function resolvePlaneReconcilerInstanceId(manifest, env, hostname) {
|
|
|
809
886
|
}
|
|
810
887
|
/** Terminal `epic_runs.status` values; every other status is a live run. */
|
|
811
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
|
+
}
|
|
812
896
|
export function createDefaultDriveEpicDeps() {
|
|
813
897
|
return {
|
|
814
|
-
resolveAccess: async (repo, cwd) => {
|
|
815
|
-
|
|
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 ?? {}, {
|
|
816
902
|
...(repo ? { repoName: repo } : {}),
|
|
817
903
|
...(cwd ? { cwd } : {}),
|
|
818
904
|
});
|
|
819
|
-
return result.ok
|
|
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;
|
|
820
914
|
},
|
|
821
915
|
readReadiness: (access) => fetchConductorReadiness(access, globalThis.fetch),
|
|
822
916
|
runSetupEpic: (argv) => runSetupEpicWorkflow(argv),
|
|
@@ -887,12 +981,19 @@ export async function runDriveEpicCli(argv, overrides = {}) {
|
|
|
887
981
|
return 1;
|
|
888
982
|
}
|
|
889
983
|
const { epicKey, planFile, repo, featureBranch, intoBase } = parsed.options;
|
|
984
|
+
const launch = launchSelectionOf(parsed.options);
|
|
890
985
|
try {
|
|
891
986
|
// --- Resolve access. A missing credential is UNKNOWN, not not-ready. -----
|
|
892
|
-
|
|
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);
|
|
893
992
|
if (!accessResult.ok) {
|
|
894
993
|
return escalate(deps, epicKey, `conductor readiness could not be read: ${accessResult.error}`);
|
|
895
994
|
}
|
|
995
|
+
const launchProfile = accessResult.profile ?? null;
|
|
996
|
+
deps.adoptLaunchTarget?.(accessResult.access.baseUrl);
|
|
896
997
|
// --- Read readiness. Any failure here is UNKNOWN — with ONE exception. ---
|
|
897
998
|
let readiness;
|
|
898
999
|
try {
|
|
@@ -907,6 +1008,14 @@ export async function runDriveEpicCli(argv, overrides = {}) {
|
|
|
907
1008
|
// because there unknown is still fatal (R14).
|
|
908
1009
|
if (planFile !== undefined && err instanceof ConductorBridgeApiError && err.kind === "network") {
|
|
909
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
|
+
}
|
|
910
1019
|
if (endpoint !== null) {
|
|
911
1020
|
const probe = await deps.probePlanePort(endpoint.host, endpoint.port);
|
|
912
1021
|
if (probe.kind === "refused") {
|
|
@@ -1078,7 +1187,10 @@ export async function runDriveEpicCli(argv, overrides = {}) {
|
|
|
1078
1187
|
deps.errorLog(refusal);
|
|
1079
1188
|
return 1;
|
|
1080
1189
|
}
|
|
1081
|
-
const handoff = renderConductorHandoff(selection, epicKey
|
|
1190
|
+
const handoff = renderConductorHandoff(selection, epicKey, {
|
|
1191
|
+
profile: launchProfile,
|
|
1192
|
+
options: parsed.options,
|
|
1193
|
+
});
|
|
1082
1194
|
const text = handoff.lines.join("\n");
|
|
1083
1195
|
assertSingleConductorInvocation(text);
|
|
1084
1196
|
deps.log(text);
|
|
@@ -1287,7 +1399,7 @@ async function coldStartPlane(ctx) {
|
|
|
1287
1399
|
*/
|
|
1288
1400
|
async function attachToPlane(ctx, manifest, readiness) {
|
|
1289
1401
|
const { deps, epicKey } = ctx;
|
|
1290
|
-
const rooted = await deps.resolveAccess(ctx.options.repo, manifest.repoRoot);
|
|
1402
|
+
const rooted = await deps.resolveAccess(ctx.options.repo, manifest.repoRoot, launchSelectionOf(ctx.options));
|
|
1291
1403
|
if (!rooted.ok) {
|
|
1292
1404
|
return refuse(deps, [
|
|
1293
1405
|
`${epicKey}: cannot attach to the running plane — Bridge API credentials could not be ` +
|