@bridge_gpt/mcp-server 0.2.57 → 0.2.59
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 +112 -113
- package/build/agent-registry.js +15 -0
- package/build/agent-utils.js +16 -8
- package/build/claude-user-config-doctor.js +1 -35
- package/build/codex-bridge-profile.js +5 -1
- package/build/codex-launch-scope.js +13 -2
- package/build/commands.generated.js +5 -6
- package/build/conductor/bridge-api-client.js +149 -0
- package/build/conductor/bring-up-facts.js +1 -1
- package/build/conductor/claude-hook.js +16 -16
- package/build/conductor/doctor.js +24 -19
- package/build/conductor/epic-implementer-cli.js +38 -5
- package/build/conductor/epic-reconcile.js +1 -1
- package/build/conductor/epic-runtime.js +10 -10
- package/build/conductor/errors.js +3 -3
- package/build/conductor/file-scope-guard.js +4 -4
- package/build/conductor/git-ci-types.js +1 -1
- package/build/conductor/git-inspection.js +2 -2
- package/build/conductor/install-doctor.js +6 -6
- package/build/conductor/merge-ledger.js +2 -2
- package/build/conductor/pr-ci-producer.js +6 -6
- package/build/conductor/pr-review-producer.js +2 -2
- package/build/conductor/producer-ledger.js +2 -0
- package/build/conductor/readiness-sections.js +7 -7
- package/build/conductor/readiness.js +2 -2
- package/build/conductor/recovery-cli.js +243 -12
- package/build/conductor/recovery-operations.js +56 -4
- package/build/conductor/repair-contract.js +38 -4
- package/build/conductor/spec-review-producer.js +6 -6
- package/build/conductor/store.js +10 -10
- package/build/conductor/supervisor-config.js +14 -14
- package/build/conductor/supervisor-ledger.js +2 -2
- package/build/conductor/supervisor-message-relay.js +2 -2
- package/build/conductor/worker-ledger-cli.js +19 -19
- package/build/conductor-bundle-artifacts.js +2 -2
- package/build/cursor-agent-progress.js +72 -0
- package/build/doctor.js +20 -296
- package/build/drive-epic.js +65 -71
- package/build/epic-implementer-bin.js +22 -17
- package/build/epic-implementer-claude-hook-bin.js +4 -2
- package/build/epic-implementer-env.js +155 -0
- package/build/epic-integration-pr.js +2 -2
- package/build/epic-shared/bridge-client.js +773 -0
- package/build/epic-shared/cut-protocol.js +392 -0
- package/build/executor/env.js +19 -8
- package/build/executor/heartbeat.js +1 -1
- package/build/executor/job-errors.js +3 -3
- package/build/executor/job-runner.js +1 -1
- package/build/executor/merge-job.js +35 -3
- package/build/executor/observation.js +1 -1
- package/build/executor/preflight.js +1 -1
- package/build/executor/prompt-spec.js +1 -1
- package/build/executor/reconcile-overlap-job.js +10 -130
- package/build/executor/reconcile-overlap-types.js +1 -1
- package/build/executor/runner.js +1 -1
- package/build/implement-epic/cli.js +1 -1
- package/build/index.js +187 -224
- package/build/init.js +20 -28
- package/build/install-bridge-conductor.js +7 -7
- package/build/install-bridge.js +203 -10
- package/build/install-doctor.js +1 -1
- package/build/mcp-host-entry-adapter.js +6 -0
- package/build/mcp-host-targets.js +2 -0
- package/build/mcp-invoke.js +5 -5
- package/build/mcp-profile.js +2 -2
- package/build/mcp-provisioning.js +2 -2
- package/build/mcp-server-invocation.js +3 -3
- package/build/merge-pull-request.js +15 -0
- package/build/model-routing-host.js +25 -0
- package/build/model-routing-preferences.js +99 -0
- package/build/pipelines.generated.js +11 -11
- package/build/plan-epic-implementer-eligibility.js +215 -0
- package/build/plan-execution-ledger.js +8 -0
- package/build/plane/member-roster.js +5 -5
- package/build/plane/supervisor.js +1 -1
- package/build/plane/types.js +5 -5
- package/build/readiness-check.js +1 -1
- package/build/readme.generated.js +1 -1
- package/build/review-tickets.js +4 -4
- package/build/setup-epic.js +8 -8
- package/build/start-tickets-conductor.js +32 -32
- package/build/start-tickets-prereqs.js +4 -9
- package/build/start-tickets.js +494 -98
- package/build/version.generated.js +3 -3
- package/docs/CONDUCTOR.md +69 -18
- package/docs/install/mcp-tool-integrations.md +2 -1
- package/package.json +2 -2
- package/pipelines/implement-ticket.json +2 -2
- package/pipelines/plan-epic.json +2 -2
- package/pipelines/review-ticket.json +1 -1
package/build/init.js
CHANGED
|
@@ -818,41 +818,33 @@ export async function runInit(cwd, options = {}) {
|
|
|
818
818
|
// ---- Phase 4a: Scaffold GitHub Copilot command artifacts ----
|
|
819
819
|
// VS Code Local agent discovers prompt files; Agent Host discovers project
|
|
820
820
|
// skills. Both artifacts are rendered from COMMANDS above, never a second
|
|
821
|
-
// Copilot-specific command catalog.
|
|
821
|
+
// Copilot-specific command catalog. Each representation reports its own
|
|
822
|
+
// counts, so a written prompt never implies its skill exists (or vice versa).
|
|
822
823
|
if (ideDetection.vscode) {
|
|
823
824
|
const promptsDir = path.join(cwd, ".github", "prompts");
|
|
824
825
|
const skillsDir = path.join(cwd, ".github", "skills");
|
|
825
|
-
const copilotWritten = new Set();
|
|
826
|
-
const copilotSkipped = new Set();
|
|
827
|
-
const copilotOverwritten = new Set();
|
|
828
826
|
const artifacts = commandEntries.map(([filename, content]) => ({
|
|
829
|
-
filename,
|
|
830
827
|
commandName: filename.replace(/\.md$/, ""),
|
|
831
828
|
...translateCommandToCopilot(filename, content),
|
|
832
829
|
}));
|
|
833
|
-
const
|
|
834
|
-
|
|
835
|
-
|
|
836
|
-
|
|
837
|
-
|
|
838
|
-
|
|
839
|
-
|
|
840
|
-
|
|
841
|
-
|
|
842
|
-
|
|
843
|
-
|
|
844
|
-
|
|
845
|
-
|
|
846
|
-
|
|
847
|
-
|
|
848
|
-
|
|
849
|
-
|
|
850
|
-
|
|
851
|
-
if (copilotOverwritten.size > 0)
|
|
852
|
-
console.log(` Overwritten (content changed): ${copilotOverwritten.size}`);
|
|
853
|
-
if (copilotSkipped.size > 0)
|
|
854
|
-
console.log(` Skipped (unchanged): ${copilotSkipped.size}`);
|
|
855
|
-
console.log(" .github/prompts/ (VS Code Local agent) and .github/skills/ (Agent Host)");
|
|
830
|
+
const promptOutcomes = await Promise.all(artifacts.map(({ commandName, prompt }) => scaffoldFile(path.join(promptsDir, `${commandName}.prompt.md`), prompt, promptsDir)));
|
|
831
|
+
const skillOutcomes = await Promise.all(artifacts.map(({ commandName, skill }) => scaffoldFile(path.join(skillsDir, commandName, "SKILL.md"), skill, path.join(skillsDir, commandName))));
|
|
832
|
+
const copilotReports = [
|
|
833
|
+
["VS Code Local-agent prompt files", ".github/prompts/", promptOutcomes],
|
|
834
|
+
["Agent Host skills", ".github/skills/", skillOutcomes],
|
|
835
|
+
];
|
|
836
|
+
for (const [label, dir, outcomes] of copilotReports) {
|
|
837
|
+
const written = outcomes.filter((outcome) => outcome !== "skipped").length;
|
|
838
|
+
const overwritten = outcomes.filter((outcome) => outcome === "overwritten").length;
|
|
839
|
+
const skipped = outcomes.filter((outcome) => outcome === "skipped").length;
|
|
840
|
+
console.log(`\n${label}: scaffolded ${outcomes.length} into ${dir}`);
|
|
841
|
+
if (written > 0)
|
|
842
|
+
console.log(` Written: ${written}`);
|
|
843
|
+
if (overwritten > 0)
|
|
844
|
+
console.log(` Overwritten (content changed): ${overwritten}`);
|
|
845
|
+
if (skipped > 0)
|
|
846
|
+
console.log(` Skipped (unchanged): ${skipped}`);
|
|
847
|
+
}
|
|
856
848
|
}
|
|
857
849
|
// ---- Phase 4b: Scaffold shipped documentation assets ----
|
|
858
850
|
// Every doc asset the /install-bridge capability report points a human at,
|
|
@@ -136,7 +136,7 @@ export function getInstallBridgeConductorUsage() {
|
|
|
136
136
|
"production when unset). The bootstrap POST then asks its own, separate write",
|
|
137
137
|
"consent. Without a TTY there is no consent, so the command aborts.",
|
|
138
138
|
"",
|
|
139
|
-
"Tool visibility (its own consent): adds the `
|
|
139
|
+
"Tool visibility (its own consent): adds the `epic_implementer` token to the",
|
|
140
140
|
"BRIDGE_MCP_PROFILE of the Bridge entry in host MCP configurations that",
|
|
141
141
|
"ALREADY have one. Automatic merging covers .mcp.json, .vscode/mcp.json, and",
|
|
142
142
|
".cursor/mcp.json only — never Codex or Copilot, which are neither inspected",
|
|
@@ -556,10 +556,10 @@ async function runExecutorProvisioningPhase(deps, executor, plans, access) {
|
|
|
556
556
|
* said nothing would reasonably conclude the tools were already visible.
|
|
557
557
|
*/
|
|
558
558
|
export function formatConductorRestartAdvisory(mergedProfile) {
|
|
559
|
-
return (`⚠️
|
|
559
|
+
return (`⚠️ Epic Implementer profile updated: BRIDGE_MCP_PROFILE is now set to \`${mergedProfile}\` ` +
|
|
560
560
|
"in your local MCP config file(s). The running MCP server captured its active groups " +
|
|
561
561
|
"at startup, so this activates on the next MCP server launch — restart your MCP " +
|
|
562
|
-
"client to gain access to the
|
|
562
|
+
"client to gain access to the Epic Implementer tools.");
|
|
563
563
|
}
|
|
564
564
|
/** Remediation printed when no supported host config carries a Bridge entry. */
|
|
565
565
|
export const CONDUCTOR_PROFILE_EMPTY_MERGE_REMEDIATION = `run \`npx ${MCP_PACKAGE_NAME} install\` to create and register the host MCP entry, ` +
|
|
@@ -570,14 +570,14 @@ export const CONDUCTOR_HOOKS_MANUAL_REMEDIATION = "re-run `install conductor` an
|
|
|
570
570
|
/**
|
|
571
571
|
* Phase: tool visibility.
|
|
572
572
|
*
|
|
573
|
-
* Adds the `
|
|
573
|
+
* Adds the `epic_implementer` token to EXISTING supported host MCP entries behind its
|
|
574
574
|
* own consent, then prints the restart advisory. It never creates a host config
|
|
575
575
|
* — the merge helper's never-create rule is what keeps a declined or
|
|
576
576
|
* unconfigured repository from acquiring files it did not ask for.
|
|
577
577
|
*/
|
|
578
578
|
async function runConductorToolVisibilityPhase(deps, local) {
|
|
579
579
|
const consented = await confirm(deps, "Update existing supported host MCP configurations (.mcp.json, .vscode/mcp.json, " +
|
|
580
|
-
".cursor/mcp.json) to add the `
|
|
580
|
+
".cursor/mcp.json) to add the `epic_implementer` profile? [y/N]: ");
|
|
581
581
|
if (!consented) {
|
|
582
582
|
deps.log(" declined — no host MCP configuration and no install state were written.");
|
|
583
583
|
return {
|
|
@@ -620,7 +620,7 @@ async function runConductorToolVisibilityPhase(deps, local) {
|
|
|
620
620
|
}
|
|
621
621
|
deps.log(carrying.length > 0
|
|
622
622
|
? ` updated: ${carrying.join(", ")}`
|
|
623
|
-
: " the
|
|
623
|
+
: " the Epic Implementer profile was merged into your supported host MCP config(s).");
|
|
624
624
|
// Coverage honesty: the merge path writes these three files and no others.
|
|
625
625
|
deps.log(" Codex and Copilot host configuration was NOT inspected or changed.");
|
|
626
626
|
deps.log(` ${formatConductorRestartAdvisory(mergedProfile)}`);
|
|
@@ -1027,7 +1027,7 @@ export async function runInstallBridgeConductorCli(argv, deps) {
|
|
|
1027
1027
|
else {
|
|
1028
1028
|
deps.log(" skipped — this caller supplied no local-provisioning seams.");
|
|
1029
1029
|
manualActions.push("tool visibility: run the packaged `install conductor` command to add the " +
|
|
1030
|
-
"`
|
|
1030
|
+
"`epic_implementer` profile to your host MCP configuration.");
|
|
1031
1031
|
}
|
|
1032
1032
|
deps.log("");
|
|
1033
1033
|
deps.log("Phase 5/8 — conductor local observability…");
|
package/build/install-bridge.js
CHANGED
|
@@ -167,7 +167,7 @@ import { executorLaunchdLabelForId, executorSystemdUnitNameForId, executorLaunch
|
|
|
167
167
|
import { collectConductorNativeLedgerSafe, } from "./conductor/doctor.js";
|
|
168
168
|
import { installConductorGitHooks, } from "./conductor/git-hooks.js";
|
|
169
169
|
import { runInstallBridgeConductorCli, } from "./install-bridge-conductor.js";
|
|
170
|
-
import { runConductorInstallDoctor,
|
|
170
|
+
import { runConductorInstallDoctor, EPIC_IMPLEMENTER_PROFILE_TOKEN, } from "./conductor/install-doctor.js";
|
|
171
171
|
import { resolveConductorBridgeApiAccess, } from "./conductor/bridge-api-client.js";
|
|
172
172
|
import { claudeReviewWorkflowPath, writeClaudeReviewWorkflow, } from "./claude-review-workflow.js";
|
|
173
173
|
// BAPI-941: read-only workflow-lineage probe, wired into the conductor install
|
|
@@ -300,7 +300,7 @@ export const INSTALL_BRIDGE_AGENT_PROMPT = "Run /install-bridge now, with no arg
|
|
|
300
300
|
"duplicate. This silence applies ONLY to success and ONLY in the install-spawn context: a Stage 6 " +
|
|
301
301
|
"FAILURE is still reported loudly and in full, and in JOIN MODE or a direct/manual /install-bridge " +
|
|
302
302
|
"invocation Stage 6 reports its success exactly as it does today. " +
|
|
303
|
-
"The install-spawn skip set is exactly Stage 8
|
|
303
|
+
"The install-spawn skip set is exactly Stage 8 and Stage 9. The command-owned Stage 11 " +
|
|
304
304
|
"(invite teammates) is NOT in that skip set: it is independently gated and may run only when the " +
|
|
305
305
|
"caller's role is admin AND customer_type is b2b AND an interactive response is available; " +
|
|
306
306
|
"otherwise it is skipped silently. If Stage 11 mints a teammate key, show that plaintext key " +
|
|
@@ -520,7 +520,8 @@ export function getInstallBridgeUsage(baseUrl = DEFAULT_BAPI_BASE_URL) {
|
|
|
520
520
|
" launches nothing.",
|
|
521
521
|
" --codex-profile on|off Opt in to Bridge workflow MCP approvals for",
|
|
522
522
|
" Codex workers, or explicitly decline. With a",
|
|
523
|
-
" Codex TTY selection, the installer asks
|
|
523
|
+
" Codex TTY selection, the installer asks",
|
|
524
|
+
" with Yes as the default (press Enter).",
|
|
524
525
|
" --force Overwrite an existing real BAPI_API_KEY in a",
|
|
525
526
|
" host config (or in the credential store) without",
|
|
526
527
|
" prompting. It does NOT bypass the git-tracked",
|
|
@@ -571,7 +572,7 @@ export function getInstallBridgeUsage(baseUrl = DEFAULT_BAPI_BASE_URL) {
|
|
|
571
572
|
" (its own separate consent), then closes the local",
|
|
572
573
|
" out-of-the-box gap in two more phases, each behind",
|
|
573
574
|
" its OWN consent: tool visibility adds the",
|
|
574
|
-
" `
|
|
575
|
+
" `epic_implementer` token to existing .mcp.json /",
|
|
575
576
|
" .vscode/mcp.json / .cursor/mcp.json entries (never",
|
|
576
577
|
" Codex or Copilot, and never creating a file) and",
|
|
577
578
|
" prints the MCP-client restart advisory; local",
|
|
@@ -1122,9 +1123,9 @@ export function buildAgentNotesSummaryLine(offer, result, writeFailed, unsupport
|
|
|
1122
1123
|
* Exported so `index.ts`'s `--init` can share the exact same prompt mechanics
|
|
1123
1124
|
* as `install`'s missing-project-root confirmation (BAPI-818, R1).
|
|
1124
1125
|
*/
|
|
1125
|
-
export function promptLineViaReadline(promptText) {
|
|
1126
|
+
export function promptLineViaReadline(promptText, input = process.stdin, output = process.stderr) {
|
|
1126
1127
|
return new Promise((resolve) => {
|
|
1127
|
-
const rl = readline.createInterface({ input
|
|
1128
|
+
const rl = readline.createInterface({ input, output });
|
|
1128
1129
|
// See promptSecretViaReadline: EOF must resolve rather than deadlock the
|
|
1129
1130
|
// top-level await, and `answered` guards the synchronous `close` from
|
|
1130
1131
|
// discarding a real answer.
|
|
@@ -1453,8 +1454,8 @@ function buildConductorExecutorDeps(deps, baseUrl) {
|
|
|
1453
1454
|
*/
|
|
1454
1455
|
function buildConductorLocalDeps(deps) {
|
|
1455
1456
|
return {
|
|
1456
|
-
mergeProfileToken: () => mergeBridgeApiProfileToken(deps.cwd,
|
|
1457
|
-
inspectProfileToken: () => inspectBridgeApiProfileToken(deps.cwd,
|
|
1457
|
+
mergeProfileToken: () => mergeBridgeApiProfileToken(deps.cwd, EPIC_IMPLEMENTER_PROFILE_TOKEN),
|
|
1458
|
+
inspectProfileToken: () => inspectBridgeApiProfileToken(deps.cwd, EPIC_IMPLEMENTER_PROFILE_TOKEN, {
|
|
1458
1459
|
readFile: deps.readFile,
|
|
1459
1460
|
}),
|
|
1460
1461
|
// The repository root is this command's cwd, which is what the hook
|
|
@@ -2012,6 +2013,174 @@ export function promptMultiSelectViaReadline(promptText, options, input = proces
|
|
|
2012
2013
|
prompt();
|
|
2013
2014
|
});
|
|
2014
2015
|
}
|
|
2016
|
+
/**
|
|
2017
|
+
* Subscription display name for the three platforms whose maximize-posture
|
|
2018
|
+
* option-three wording names the actual tool, keyed by the SAME
|
|
2019
|
+
* `HostPlatformId` `resolveSelectedHostPlatforms` returns. Deliberately does
|
|
2020
|
+
* NOT project `MCP_HOST_TARGETS[id].label` — that registry label reads
|
|
2021
|
+
* "OpenAI Codex", which is not this feature's approved wording.
|
|
2022
|
+
*/
|
|
2023
|
+
const OPTIMIZATION_POSTURE_SUBSCRIPTION_NAMES = {
|
|
2024
|
+
"claude-code": "Claude Code",
|
|
2025
|
+
codex: "Codex",
|
|
2026
|
+
cursor: "Cursor",
|
|
2027
|
+
};
|
|
2028
|
+
/**
|
|
2029
|
+
* `"{name} subscription"` for the three eligible platforms, and the generic
|
|
2030
|
+
* `"coding subscription"` for every other input — Copilot, Windsurf, an
|
|
2031
|
+
* unrecognized id, or no primary platform at all (empty tool selection).
|
|
2032
|
+
*/
|
|
2033
|
+
export function optimizationPostureSubscriptionNounPhrase(primaryPlatform) {
|
|
2034
|
+
const mapped = primaryPlatform ? OPTIMIZATION_POSTURE_SUBSCRIPTION_NAMES[primaryPlatform] : undefined;
|
|
2035
|
+
return mapped ? `${mapped} subscription` : "coding subscription";
|
|
2036
|
+
}
|
|
2037
|
+
/** Option three's exact wording, platform-aware (BAPI-1221 Step 2). */
|
|
2038
|
+
function buildOptimizationPostureOptionThree(primaryPlatform) {
|
|
2039
|
+
return `Get the most out of your ${optimizationPostureSubscriptionNounPhrase(primaryPlatform)}?`;
|
|
2040
|
+
}
|
|
2041
|
+
/** The neutral single-line correction reprompted for any malformed answer. */
|
|
2042
|
+
const OPTIMIZATION_POSTURE_CORRECTION = "Enter a single number from 1 to 3.";
|
|
2043
|
+
/**
|
|
2044
|
+
* Bounds the picker's correction reprompts, mirroring this file's existing
|
|
2045
|
+
* {@link INSTALL_BRIDGE_MAX_CODE_REJECTIONS} convention. A real interactive
|
|
2046
|
+
* user always reaches a terminal answer well inside this many tries — Enter
|
|
2047
|
+
* (blank) or Ctrl+D (EOF) both resolve immediately — so this cap exists only
|
|
2048
|
+
* to stop a `promptLine` seam that NEVER produces a digit or a blank answer
|
|
2049
|
+
* from reprompting forever. Set comfortably above the widest malformed-input
|
|
2050
|
+
* sweep this picker is tested against (a comma list, a range, signed and
|
|
2051
|
+
* decimal values, `0`, `4`, blank-with-more-input, and arbitrary text).
|
|
2052
|
+
*/
|
|
2053
|
+
const OPTIMIZATION_POSTURE_MAX_CORRECTIONS = 20;
|
|
2054
|
+
/**
|
|
2055
|
+
* Render the one-time posture menu and its input instruction. Exactly three
|
|
2056
|
+
* vertically ordered numbered options; option three is platform-aware. Plain,
|
|
2057
|
+
* wrap-safe text — no color-only state, icons, or decorative framing.
|
|
2058
|
+
*/
|
|
2059
|
+
function buildOptimizationPostureMenu(primaryPlatform) {
|
|
2060
|
+
return ("How should Bridge optimize its work on this repository?\n" +
|
|
2061
|
+
" 1. Quality\n" +
|
|
2062
|
+
" 2. Speed\n" +
|
|
2063
|
+
` 3. ${buildOptimizationPostureOptionThree(primaryPlatform)}\n` +
|
|
2064
|
+
`${OPTIMIZATION_POSTURE_CORRECTION} `);
|
|
2065
|
+
}
|
|
2066
|
+
/**
|
|
2067
|
+
* The purpose-built, strict single-select optimization-posture picker
|
|
2068
|
+
* (BAPI-1221 Step 2). Renders the three-option menu exactly once and loops
|
|
2069
|
+
* ONLY the neutral correction line on a malformed answer — never
|
|
2070
|
+
* `promptMultiSelectViaReadline()`, whose comma-separated contract this
|
|
2071
|
+
* picker deliberately does not share.
|
|
2072
|
+
*
|
|
2073
|
+
* Accepts only a trimmed single digit `1`, `2`, or `3`; every other answer
|
|
2074
|
+
* (a comma list, a range, a signed or decimal value, blank text, or
|
|
2075
|
+
* unrelated text) is malformed and reprompted. EOF, an unavailable
|
|
2076
|
+
* `deps.promptLine` seam, or a genuinely blank/no-answer result all resolve
|
|
2077
|
+
* `null` rather than looping indefinitely — `promptLineViaReadline` itself
|
|
2078
|
+
* already collapses real EOF and an empty typed answer to the same `""`, so
|
|
2079
|
+
* there is no seam-level way (or need) to tell them apart here.
|
|
2080
|
+
*/
|
|
2081
|
+
export async function promptOptimizationPosture(deps, primaryPlatform) {
|
|
2082
|
+
if (!deps.isTTY || !deps.promptLine)
|
|
2083
|
+
return null;
|
|
2084
|
+
const promptLine = deps.promptLine;
|
|
2085
|
+
try {
|
|
2086
|
+
let answer = (await promptLine(buildOptimizationPostureMenu(primaryPlatform))).trim();
|
|
2087
|
+
for (let attempts = 0;; attempts++) {
|
|
2088
|
+
if (answer === "1")
|
|
2089
|
+
return "quality";
|
|
2090
|
+
if (answer === "2")
|
|
2091
|
+
return "speed";
|
|
2092
|
+
if (answer === "3")
|
|
2093
|
+
return "maximize_subscription";
|
|
2094
|
+
if (answer === "")
|
|
2095
|
+
return null;
|
|
2096
|
+
if (attempts >= OPTIMIZATION_POSTURE_MAX_CORRECTIONS)
|
|
2097
|
+
return null;
|
|
2098
|
+
answer = (await promptLine(`${OPTIMIZATION_POSTURE_CORRECTION} `)).trim();
|
|
2099
|
+
}
|
|
2100
|
+
}
|
|
2101
|
+
catch {
|
|
2102
|
+
// Prompt/stream exception at the posture-step boundary: fail open to a
|
|
2103
|
+
// no-answer outcome rather than aborting an otherwise-complete install.
|
|
2104
|
+
return null;
|
|
2105
|
+
}
|
|
2106
|
+
}
|
|
2107
|
+
/** Build the posture config-field URL, matching {@link buildPingUrl}'s normalization. */
|
|
2108
|
+
function buildOptimizationPostureUrl(baseUrl) {
|
|
2109
|
+
return `${baseUrl.replace(/\/+$/, "")}/jira/config-field/optimization_posture`;
|
|
2110
|
+
}
|
|
2111
|
+
/**
|
|
2112
|
+
* Persist the captured posture through the existing, generic configuration
|
|
2113
|
+
* endpoint (BAPI-1221 Step 4) — the SAME `PUT /jira/config-field/{field_name}`
|
|
2114
|
+
* every other config-field write uses; this ticket adds no new endpoint,
|
|
2115
|
+
* model, or migration. Fail-open by design: a missing repo identity, a
|
|
2116
|
+
* forbidden write, any other non-OK response, or a network/timeout failure
|
|
2117
|
+
* all return a coarse outcome rather than throwing, and none of them retries
|
|
2118
|
+
* or blocks the rest of installation. The response body is never read or
|
|
2119
|
+
* logged, and the API key never appears in the URL, body, or any returned
|
|
2120
|
+
* message.
|
|
2121
|
+
*/
|
|
2122
|
+
export async function persistOptimizationPosture(deps, baseUrl, apiKey, repoName, value) {
|
|
2123
|
+
if (!repoName || repoName.trim().length === 0) {
|
|
2124
|
+
return { kind: "missing-repo-identity" };
|
|
2125
|
+
}
|
|
2126
|
+
let resp;
|
|
2127
|
+
try {
|
|
2128
|
+
resp = await deps.fetch(buildOptimizationPostureUrl(baseUrl), {
|
|
2129
|
+
method: "PUT",
|
|
2130
|
+
headers: { "X-API-Key": apiKey, "Content-Type": "application/json" },
|
|
2131
|
+
body: JSON.stringify({ repo_name: repoName, value }),
|
|
2132
|
+
signal: AbortSignal.timeout(10_000),
|
|
2133
|
+
});
|
|
2134
|
+
}
|
|
2135
|
+
catch {
|
|
2136
|
+
return { kind: "request-failed" };
|
|
2137
|
+
}
|
|
2138
|
+
if (resp.ok)
|
|
2139
|
+
return { kind: "success" };
|
|
2140
|
+
if (resp.status === 403)
|
|
2141
|
+
return { kind: "forbidden" };
|
|
2142
|
+
return { kind: "http-error", status: resp.status };
|
|
2143
|
+
}
|
|
2144
|
+
/** Confirmation noun phrase used in the deferred-activation success message. */
|
|
2145
|
+
function optimizationPostureConfirmationPhrase(value, primaryPlatform) {
|
|
2146
|
+
if (value === "quality")
|
|
2147
|
+
return "quality";
|
|
2148
|
+
if (value === "speed")
|
|
2149
|
+
return "speed";
|
|
2150
|
+
return `getting the most out of your ${optimizationPostureSubscriptionNounPhrase(primaryPlatform)}`;
|
|
2151
|
+
}
|
|
2152
|
+
/** Printed immediately after a successful posture write, on its own line. */
|
|
2153
|
+
const OPTIMIZATION_POSTURE_BLOG_URL = "https://bridgegpt.ai/blog/how-we-optimize-your-setup";
|
|
2154
|
+
const OPTIMIZATION_POSTURE_REMEDIATION = "Update it later with /teach-bridge or the optimization_posture setting in the web UI.";
|
|
2155
|
+
/**
|
|
2156
|
+
* The generic, secret-free fail-open message shared by every non-success,
|
|
2157
|
+
* non-forbidden outcome — an interactive no-answer/prompt-exception (no PUT
|
|
2158
|
+
* was ever attempted) and a missing-repo-identity/HTTP-error/network failure
|
|
2159
|
+
* (a PUT was attempted and did not take). States plainly that the current
|
|
2160
|
+
* server value is unchanged; a new repository merely READS as Quality via
|
|
2161
|
+
* the BAPI-1220 server-side default, so this never claims every repository
|
|
2162
|
+
* IS now Quality.
|
|
2163
|
+
*/
|
|
2164
|
+
const OPTIMIZATION_POSTURE_UNCHANGED_MESSAGE = ` Optimization posture was not changed; new repositories use Quality by default. ${OPTIMIZATION_POSTURE_REMEDIATION}`;
|
|
2165
|
+
/**
|
|
2166
|
+
* Report the outcome of the optional posture capture/persistence step
|
|
2167
|
+
* (BAPI-1221 Step 5) — called only when an explicit interactive selection was
|
|
2168
|
+
* captured and the PUT helper actually ran.
|
|
2169
|
+
*/
|
|
2170
|
+
function reportOptimizationPostureOutcome(log, result, value, primaryPlatform) {
|
|
2171
|
+
if (result.kind === "success") {
|
|
2172
|
+
log(` Preference saved for later activation — we'll optimize for ${optimizationPostureConfirmationPhrase(value, primaryPlatform)}.`);
|
|
2173
|
+
log(` ${OPTIMIZATION_POSTURE_BLOG_URL}`);
|
|
2174
|
+
return;
|
|
2175
|
+
}
|
|
2176
|
+
if (result.kind === "forbidden") {
|
|
2177
|
+
log(` Optimization posture was not changed (an admin API key is required). ${OPTIMIZATION_POSTURE_REMEDIATION}`);
|
|
2178
|
+
return;
|
|
2179
|
+
}
|
|
2180
|
+
// missing-repo-identity, http-error, request-failed: none carries
|
|
2181
|
+
// user-actionable detail beyond "it didn't take."
|
|
2182
|
+
log(OPTIMIZATION_POSTURE_UNCHANGED_MESSAGE);
|
|
2183
|
+
}
|
|
2015
2184
|
/**
|
|
2016
2185
|
* Resolve the selected host platforms with strict precedence (BAPI-635):
|
|
2017
2186
|
* 1. explicit `--tools` (including an explicit EMPTY selection — never falls
|
|
@@ -4947,14 +5116,26 @@ export async function runInstallBridgeCli(argv, overrides = {}) {
|
|
|
4947
5116
|
// global (Codex) or manual (Windsurf) target is provisioned by the
|
|
4948
5117
|
// registry-driven emitter after the connectivity check.
|
|
4949
5118
|
const selectedPlatforms = await resolveSelectedHostPlatforms(deps, options);
|
|
5119
|
+
// ---- BAPI-1221 (S2): capture the optimization-posture preference ----
|
|
5120
|
+
// Asked immediately after tool selection, on every otherwise-eligible
|
|
5121
|
+
// interactive install (including reinstalls and an explicit empty
|
|
5122
|
+
// selection) — a prior posture is never read first. `--dry-run` already
|
|
5123
|
+
// returns long before this point (see the NOTE below), so this gate is
|
|
5124
|
+
// belt-and-suspenders, not the sole guard. The primary platform is
|
|
5125
|
+
// `selectedPlatforms[0]` exclusively, relying on the resolver's own
|
|
5126
|
+
// `HOST_PLATFORM_ORDER` normalization even when several tools were chosen.
|
|
5127
|
+
const optimizationPostureEligible = deps.isTTY && Boolean(deps.promptLine) && !options.dryRun;
|
|
5128
|
+
const optimizationPostureSelection = optimizationPostureEligible
|
|
5129
|
+
? await promptOptimizationPosture(deps, selectedPlatforms[0])
|
|
5130
|
+
: null;
|
|
4950
5131
|
if (options.codexProfile && !selectedPlatforms.includes("codex")) {
|
|
4951
5132
|
errorLog("Error: --codex-profile requires Codex in --tools or the interactive selection.");
|
|
4952
5133
|
return 1;
|
|
4953
5134
|
}
|
|
4954
5135
|
let codexProfileChoice = options.codexProfile;
|
|
4955
5136
|
if (selectedPlatforms.includes("codex") && !options.dryRun && !codexProfileChoice && deps.isTTY && deps.promptLine) {
|
|
4956
|
-
const answer = (await deps.promptLine("Install the opt-in Bridge Codex approval profile for spawned workers? (
|
|
4957
|
-
codexProfileChoice = answer === "y" || answer === "yes" ? "on" : "off";
|
|
5137
|
+
const answer = (await deps.promptLine("Install the opt-in Bridge Codex approval profile for spawned workers? (Y/n) ")).trim().toLowerCase();
|
|
5138
|
+
codexProfileChoice = answer === "" || answer === "y" || answer === "yes" ? "on" : "off";
|
|
4958
5139
|
}
|
|
4959
5140
|
const targets = hostConfigTargetsForPlatforms(selectedPlatforms);
|
|
4960
5141
|
// Read-only detection (safe in dry-run) of global-config editors we can't write.
|
|
@@ -5647,6 +5828,18 @@ export async function runInstallBridgeCli(argv, overrides = {}) {
|
|
|
5647
5828
|
: `Error: ${ping.message}`);
|
|
5648
5829
|
}
|
|
5649
5830
|
log(" connectivity OK");
|
|
5831
|
+
// ---- BAPI-1221 (S2): persist the captured posture, now that connectivity
|
|
5832
|
+
// is validated and `repoName`/`apiKey` are both final. Overwrites any
|
|
5833
|
+
// existing value unconditionally — the old value is never read first. A
|
|
5834
|
+
// no-answer (or a prompt/stream exception the picker already absorbed)
|
|
5835
|
+
// reports the same fail-open remediation without ever attempting a write.
|
|
5836
|
+
if (optimizationPostureSelection) {
|
|
5837
|
+
const postureResult = await persistOptimizationPosture(deps, baseUrl, apiKey, repoName, optimizationPostureSelection);
|
|
5838
|
+
reportOptimizationPostureOutcome(log, postureResult, optimizationPostureSelection, selectedPlatforms[0]);
|
|
5839
|
+
}
|
|
5840
|
+
else if (optimizationPostureEligible) {
|
|
5841
|
+
log(OPTIMIZATION_POSTURE_UNCHANGED_MESSAGE);
|
|
5842
|
+
}
|
|
5650
5843
|
const entry = buildInstallBridgeServerEntry(deps.cwd, repoName, apiKey, baseUrl, docsDir);
|
|
5651
5844
|
// BAPI-666: the secret-free variant (no BAPI_API_KEY) is written instead of the
|
|
5652
5845
|
// real entry into any git-tracked project config the user did not explicitly
|
package/build/install-doctor.js
CHANGED
|
@@ -104,7 +104,7 @@ export async function resolveInstallDoctorTarget(deps) {
|
|
|
104
104
|
break;
|
|
105
105
|
}
|
|
106
106
|
}
|
|
107
|
-
return { repoName, repoSource, baseUrl: baseUrl ?? DEFAULT_BASE_URL };
|
|
107
|
+
return { repoName, repoSource, baseUrl: (baseUrl ?? DEFAULT_BASE_URL).replace(/\/+$/, "") };
|
|
108
108
|
}
|
|
109
109
|
/** Classify a thrown probe failure into the closed category vocabulary. */
|
|
110
110
|
function classifyProbeFailure(error) {
|
|
@@ -10,6 +10,12 @@ export function adaptBridgeEntryForHostTarget(entry, target) {
|
|
|
10
10
|
args: [...entry.args],
|
|
11
11
|
env: { ...entry.env },
|
|
12
12
|
};
|
|
13
|
+
// The marker belongs to the installed physical registration, never the
|
|
14
|
+
// portable logical entry or committed .bridge/config. Replacing an entry
|
|
15
|
+
// refreshes it; removing the registration removes it automatically.
|
|
16
|
+
delete physical.env.BAPI_MCP_HOST_FAMILY;
|
|
17
|
+
if (target.routingHostFamily)
|
|
18
|
+
physical.env.BAPI_MCP_HOST_FAMILY = target.routingHostFamily;
|
|
13
19
|
if (adapter.transportType !== undefined)
|
|
14
20
|
physical.type = adapter.transportType;
|
|
15
21
|
if (adapter.extraEntryKeys)
|
|
@@ -67,6 +67,7 @@ export function hostAdapterForTarget(target) {
|
|
|
67
67
|
export const MCP_HOST_TARGETS = {
|
|
68
68
|
"claude-code": {
|
|
69
69
|
id: "claude-code",
|
|
70
|
+
routingHostFamily: "claude_code",
|
|
70
71
|
label: "Claude Code",
|
|
71
72
|
scope: "project",
|
|
72
73
|
relPath: ".mcp.json",
|
|
@@ -106,6 +107,7 @@ export const MCP_HOST_TARGETS = {
|
|
|
106
107
|
},
|
|
107
108
|
codex: {
|
|
108
109
|
id: "codex",
|
|
110
|
+
routingHostFamily: "codex",
|
|
109
111
|
label: "OpenAI Codex",
|
|
110
112
|
scope: "global",
|
|
111
113
|
absPathResolver: (homedir) => joinPath(homedir, ".codex/config.toml"),
|
package/build/mcp-invoke.js
CHANGED
|
@@ -306,21 +306,21 @@ export async function resolveBapiInvocation(projectRoot, deps) {
|
|
|
306
306
|
BAPI_PROJECT_ROOT: projectRoot,
|
|
307
307
|
BAPI_REPO_NAME: repoName,
|
|
308
308
|
};
|
|
309
|
-
// BAPI-527: `
|
|
309
|
+
// BAPI-527: `EPIC_IMPLEMENTER_NODE_PATH` is NON-SECRET operational metadata (the
|
|
310
310
|
// captured conductor Node path used by the worker-ledger CLI subprocess).
|
|
311
311
|
// `buildChildEnv` already inherits the full parent env, so this re-affirms the
|
|
312
312
|
// pass-through explicitly and ONLY when present — it is a non-secret allowlist
|
|
313
313
|
// entry, never a credential. It is emphatically NOT the MCP server's re-exec
|
|
314
314
|
// command: the spawned command stays `deps.execPath` + `[scriptPath]` below.
|
|
315
|
-
const conductorNodePath = deps.env.
|
|
315
|
+
const conductorNodePath = deps.env.EPIC_IMPLEMENTER_NODE_PATH;
|
|
316
316
|
if (typeof conductorNodePath === "string" && conductorNodePath.trim().length > 0) {
|
|
317
|
-
overlay.
|
|
317
|
+
overlay.EPIC_IMPLEMENTER_NODE_PATH = conductorNodePath;
|
|
318
318
|
}
|
|
319
319
|
// BAPI-727: overlay the endpoint ONLY when `--base-url` was supplied, mirroring
|
|
320
|
-
// the conditional `
|
|
320
|
+
// the conditional `EPIC_IMPLEMENTER_NODE_PATH` overlay directly above. The absent case
|
|
321
321
|
// must add, delete, normalize, and override nothing — `buildChildEnv` already
|
|
322
322
|
// inherits the parent's `BAPI_BASE_URL`, and the server keeps its own production
|
|
323
|
-
// default for callers that were never given a URL. Like `
|
|
323
|
+
// default for callers that were never given a URL. Like `EPIC_IMPLEMENTER_NODE_PATH`,
|
|
324
324
|
// this is non-secret routing metadata, not a credential.
|
|
325
325
|
if (typeof deps.baseUrl === "string" && deps.baseUrl.length > 0) {
|
|
326
326
|
overlay.BAPI_BASE_URL = deps.baseUrl;
|
package/build/mcp-profile.js
CHANGED
|
@@ -15,7 +15,7 @@
|
|
|
15
15
|
*/
|
|
16
16
|
const VALID_GROUPS = new Set([
|
|
17
17
|
"core",
|
|
18
|
-
"
|
|
18
|
+
"epic_implementer",
|
|
19
19
|
"estimation",
|
|
20
20
|
"pipeline-authoring",
|
|
21
21
|
"sfcc",
|
|
@@ -49,7 +49,7 @@ export function resolveProfiles(raw) {
|
|
|
49
49
|
// rather than an implicit widening of an existing user's surface.
|
|
50
50
|
return new Set([
|
|
51
51
|
"core",
|
|
52
|
-
"
|
|
52
|
+
"epic_implementer",
|
|
53
53
|
"estimation",
|
|
54
54
|
"pipeline-authoring",
|
|
55
55
|
"sfcc",
|
|
@@ -67,7 +67,7 @@ export function serverNameForMcpTarget(target) {
|
|
|
67
67
|
* node executable is a single substitutable field), and the npm-channel fallback
|
|
68
68
|
* as `{ command: "npx", args: ["-y", "<spec>", "mcp-invoke", …] }`.
|
|
69
69
|
*
|
|
70
|
-
* BAPI-527: `.mcp.json` remains intentionally ENV-FREE. `
|
|
70
|
+
* BAPI-527: `.mcp.json` remains intentionally ENV-FREE. `EPIC_IMPLEMENTER_NODE_PATH`
|
|
71
71
|
* (the captured conductor Node path for the worker-ledger CLI subprocess) is NOT
|
|
72
72
|
* written here — it is inherited at runtime from the worker SHELL env that
|
|
73
73
|
* `start-tickets` injects at the spawn boundary, never stored as a registration
|
|
@@ -77,7 +77,7 @@ export function serverNameForMcpTarget(target) {
|
|
|
77
77
|
*
|
|
78
78
|
* BAPI-724: the executor caller's `invocation` is resolved BEFORE spawn from its
|
|
79
79
|
* OWN running process (`resolveMcpShimInvocationForRuntime`, absolute-build-path
|
|
80
|
-
* form preferred) and never depends on a worker-injected `
|
|
80
|
+
* form preferred) and never depends on a worker-injected `EPIC_IMPLEMENTER_NODE_PATH`
|
|
81
81
|
* or any other worker-shell env — the executor worker environment carries no
|
|
82
82
|
* such key at all (see `executor/env.ts`).
|
|
83
83
|
*
|
|
@@ -17,7 +17,7 @@
|
|
|
17
17
|
* server file is invoked (`node <absPath>/index.js` vs `npx @…`). It does NOT
|
|
18
18
|
* change WHICH Node binary runs that file — the `nodeExecutable` field is kept as
|
|
19
19
|
* a single substitutable value (never string-concatenated into the command) so
|
|
20
|
-
* the later Ticket 3 (P2) work can point it at `
|
|
20
|
+
* the later Ticket 3 (P2) work can point it at `EPIC_IMPLEMENTER_NODE_PATH` without
|
|
21
21
|
* re-plumbing argv construction.
|
|
22
22
|
*/
|
|
23
23
|
import path from "node:path";
|
|
@@ -48,7 +48,7 @@ export function isTransientNpxEntryPath(entryPath) {
|
|
|
48
48
|
* Build the concrete `{ command, args }` launch pair for a target + worktree from
|
|
49
49
|
* a structured invocation. Pure — no filesystem or process access. The
|
|
50
50
|
* `nodeExecutable` is returned as the standalone `command`, never concatenated
|
|
51
|
-
* with the server entry path, so `
|
|
51
|
+
* with the server entry path, so `EPIC_IMPLEMENTER_NODE_PATH` can substitute it later.
|
|
52
52
|
*/
|
|
53
53
|
export function buildMcpShimCommand(invocation, target, absoluteWorktreePath) {
|
|
54
54
|
if (invocation.form === "absolute-build-path") {
|
|
@@ -141,7 +141,7 @@ function basenameAnySep(filePath) {
|
|
|
141
141
|
* NOTE (scope boundary): this ticket changes WHICH server file is invoked, not
|
|
142
142
|
* WHICH Node runtime runs it. `nodeExecutable` defaults to `"node"` (NOT
|
|
143
143
|
* `process.execPath`) to preserve today's worker runtime selection; Ticket 3 (P2)
|
|
144
|
-
* can later inject `
|
|
144
|
+
* can later inject `EPIC_IMPLEMENTER_NODE_PATH` into this single field.
|
|
145
145
|
*/
|
|
146
146
|
export function resolveMcpShimInvocationForRuntime(deps) {
|
|
147
147
|
const nodeExecutable = deps.nodeExecutable ?? "node";
|
|
@@ -102,6 +102,19 @@ const CI_NOT_GREEN_REASON = "ci_not_green";
|
|
|
102
102
|
* moves an unreadable, unsupported, or malformed review configuration.
|
|
103
103
|
*/
|
|
104
104
|
const REVIEW_NOT_APPROVED_REASON = "review_not_approved";
|
|
105
|
+
/**
|
|
106
|
+
* BAPI-1234 — the server's migration-graph merge guard. Both are named here so
|
|
107
|
+
* the mapping is explicit rather than an accident of the default branch, and
|
|
108
|
+
* both are `needs_human`: repeating the call against the same base and head
|
|
109
|
+
* reproduces the same refusal. `migration_graph_invalid` means a human must
|
|
110
|
+
* renumber the PR's own migration; `migration_graph_unreadable` means the graph
|
|
111
|
+
* could not be proven either way. The exact reason is returned unchanged so the
|
|
112
|
+
* caller can tell them apart; no server-supplied text is added.
|
|
113
|
+
*/
|
|
114
|
+
const MIGRATION_GRAPH_REFUSAL_REASONS = new Set([
|
|
115
|
+
"migration_graph_invalid",
|
|
116
|
+
"migration_graph_unreadable",
|
|
117
|
+
]);
|
|
105
118
|
// ---------------------------------------------------------------------------
|
|
106
119
|
// Envelope construction
|
|
107
120
|
// ---------------------------------------------------------------------------
|
|
@@ -560,6 +573,8 @@ function retryHintForFailure(reason, ciSummary) {
|
|
|
560
573
|
}
|
|
561
574
|
if (reason === REVIEW_NOT_APPROVED_REASON)
|
|
562
575
|
return "retry_later";
|
|
576
|
+
if (MIGRATION_GRAPH_REFUSAL_REASONS.has(reason))
|
|
577
|
+
return "needs_human";
|
|
563
578
|
return "needs_human";
|
|
564
579
|
}
|
|
565
580
|
/**
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
const CLIENT_NAMES = {
|
|
2
|
+
// `claude-code` is observed in the handshake captured in learn-tool-gating.ts.
|
|
3
|
+
"claude-code": "claude_code",
|
|
4
|
+
"codex": "codex",
|
|
5
|
+
};
|
|
6
|
+
export function resolveRoutingHost(clientName, marker) {
|
|
7
|
+
const fromClient = typeof clientName === "string" && Object.hasOwn(CLIENT_NAMES, clientName)
|
|
8
|
+
? CLIENT_NAMES[clientName] : null;
|
|
9
|
+
const fromMarker = marker === "codex" || marker === "claude_code" ? marker : null;
|
|
10
|
+
if (marker != null && !fromMarker)
|
|
11
|
+
return { family: fromClient, source: fromClient ? "initialized_client" : "neutral", warning: "invalid_marker" };
|
|
12
|
+
if (fromClient && fromMarker && fromClient !== fromMarker)
|
|
13
|
+
return {
|
|
14
|
+
family: null, source: "neutral", warning: "marker_conflict",
|
|
15
|
+
};
|
|
16
|
+
if (fromClient)
|
|
17
|
+
return { family: fromClient, source: "initialized_client" };
|
|
18
|
+
if (fromMarker)
|
|
19
|
+
return { family: fromMarker, source: "installer_marker" };
|
|
20
|
+
return { family: null, source: "neutral" };
|
|
21
|
+
}
|
|
22
|
+
export function resolveInitializedRoutingHost(server, marker = process.env.BAPI_MCP_HOST_FAMILY) {
|
|
23
|
+
// Called from a registered handler, after MCP initialization.
|
|
24
|
+
return resolveRoutingHost(server.server.getClientVersion?.()?.name, marker);
|
|
25
|
+
}
|