@bridge_gpt/mcp-server 0.2.58 → 0.2.60
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 +231 -113
- package/build/agent-guidance.generated.js +1 -1
- package/build/agent-notes.js +7 -1
- 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 +1 -1
- package/build/commands.generated.js +7 -8
- package/build/conductor/bridge-api-client.js +202 -2
- package/build/conductor/bring-up-facts.js +1 -1
- package/build/conductor/claude-hook.js +16 -16
- package/build/conductor/doctor.js +279 -72
- package/build/conductor/epic-implementer-cli.js +38 -5
- package/build/conductor/epic-reconcile.js +1 -1
- package/build/conductor/epic-runtime.js +21 -12
- 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 +28 -6
- package/build/conductor/local-merge.js +319 -26
- package/build/conductor/merge-execution-contract.js +61 -0
- package/build/conductor/merge-execution-eligibility.js +129 -0
- 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 +166 -9
- package/build/conductor/readiness.js +97 -16
- package/build/conductor/recovery-cli.js +243 -12
- package/build/conductor/recovery-operations.js +64 -6
- 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/connect-bitbucket-api.js +6 -1
- package/build/connect-bitbucket.js +398 -88
- package/build/credential-store.js +16 -0
- package/build/docs.generated.js +2 -2
- package/build/doctor.js +199 -303
- package/build/drive-epic.js +88 -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 +184 -0
- package/build/epic-integration-pr.js +2 -2
- package/build/executor/ci-rerun-job.js +269 -0
- package/build/executor/cli.js +31 -0
- package/build/executor/credentials.js +11 -0
- package/build/executor/env.js +79 -8
- package/build/executor/heartbeat.js +1 -1
- package/build/executor/index.js +21 -4
- package/build/executor/job-errors.js +3 -3
- package/build/executor/job-runner.js +127 -1
- package/build/executor/job-types.js +12 -2
- package/build/executor/merge-job.js +46 -5
- 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/executor/service-lifecycle.js +136 -0
- package/build/executor/service-unit.js +427 -9
- package/build/executor/types.js +5 -0
- package/build/index.js +320 -342
- package/build/init.js +20 -28
- package/build/install-bridge-conductor.js +208 -15
- package/build/install-bridge.js +555 -37
- package/build/mcp-host-entry-adapter.js +6 -0
- package/build/mcp-host-targets.js +2 -0
- package/build/mcp-install-state.js +42 -4
- 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 +118 -10
- package/build/merge-workflow-codeowners.js +224 -0
- package/build/merge-workflow-config.js +133 -0
- package/build/merge-workflow-drift.js +66 -0
- package/build/merge-workflow-install.js +638 -0
- package/build/merge-workflow.js +572 -0
- package/build/model-routing-host.js +25 -0
- package/build/model-routing-preferences.js +99 -0
- package/build/pipelines.generated.js +10 -10
- package/build/{plan-epic-conductor-eligibility.js → plan-epic-implementer-eligibility.js} +18 -16
- package/build/plan-execution-ledger.js +8 -0
- package/build/plane/cli.js +24 -2
- package/build/plane/member-roster.js +5 -5
- package/build/plane/preflight.js +68 -0
- 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/run-unit-tests-launcher.js +5 -0
- package/build/setup-epic.js +171 -17
- package/build/start-tickets-conductor.js +32 -32
- package/build/start-tickets-prereqs.js +4 -9
- package/build/start-tickets.js +475 -91
- 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 +4 -4
- package/pipelines/implement-ticket.json +1 -1
- package/pipelines/plan-epic.json +2 -2
- package/build/conduct-epic/bridge-client.js +0 -773
- package/build/conduct-epic/checkpoint-store.js +0 -542
- package/build/conduct-epic/cli.js +0 -3145
- package/build/conduct-epic/cut-protocol.js +0 -392
- package/build/conduct-epic/lock.js +0 -302
- package/build/conduct-epic/pr-state.js +0 -286
- package/build/conduct-epic/spawn.js +0 -113
- package/build/conductor/cli.js +0 -1278
- package/build/conductor-bundle-cli.js +0 -256
- package/build/direct-ticket-tools.js +0 -70
- package/build/executor/reconcile-overlap-governance.js +0 -129
- package/build/implement-epic/checkpoint-store.js +0 -542
- package/build/implement-epic/cli.js +0 -3158
- package/build/implement-epic/lock.js +0 -302
- package/build/implement-epic/pr-state.js +0 -286
- package/build/implement-epic/spawn.js +0 -113
- /package/build/{implement-epic → epic-shared}/bridge-client.js +0 -0
- /package/build/{implement-epic → epic-shared}/cut-protocol.js +0 -0
package/build/install-bridge.js
CHANGED
|
@@ -162,12 +162,17 @@ import { resolveAgentNotesTargets, writeAgentNotes, } from "./agent-notes.js";
|
|
|
162
162
|
// `executor install-service` as a subprocess, which would lose the typed plan,
|
|
163
163
|
// the write outcome, and the unit path this flow has to record.
|
|
164
164
|
import { collectExecutorInstallPreflight, } from "./executor/install-preflight.js";
|
|
165
|
-
import { inspectExecutorServiceState, startExecutorService, } from "./executor/service-lifecycle.js";
|
|
166
|
-
import { executorLaunchdLabelForId, executorSystemdUnitNameForId, executorLaunchdPlistPathForId, executorSystemdUnitPathForId, resolvePackagedExecutorInvocation, writeExecutorServicePlan, } from "./executor/service-unit.js";
|
|
165
|
+
import { inspectExecutorServiceDurability, inspectExecutorServiceState, startExecutorService, } from "./executor/service-lifecycle.js";
|
|
166
|
+
import { executorLaunchdLabelForId, executorSystemdUnitNameForId, executorLaunchdPlistPathForId, executorSystemdUnitPathForId, resolvePackagedExecutorInvocation, writeExecutorServicePlan, EXECUTOR_SERVICE_UNIT_FILE_MODE, } from "./executor/service-unit.js";
|
|
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
|
-
|
|
170
|
+
// BAPI-1136: the merge-workflow generate-and-PR installer. Domain logic lives in
|
|
171
|
+
// its own module; this file supplies the parser and the process seams.
|
|
172
|
+
import { runMergeWorkflowInstall, } from "./merge-workflow-install.js";
|
|
173
|
+
import { MERGE_WORKFLOW_REMEDY_COMMAND, } from "./conductor/merge-execution-contract.js";
|
|
174
|
+
import { MERGE_WORKFLOW_API_KEY_SECRET_NAME, } from "./merge-workflow-config.js";
|
|
175
|
+
import { runConductorInstallDoctor, EPIC_IMPLEMENTER_PROFILE_TOKEN, } from "./conductor/install-doctor.js";
|
|
171
176
|
import { resolveConductorBridgeApiAccess, } from "./conductor/bridge-api-client.js";
|
|
172
177
|
import { claudeReviewWorkflowPath, writeClaudeReviewWorkflow, } from "./claude-review-workflow.js";
|
|
173
178
|
// BAPI-941: read-only workflow-lineage probe, wired into the conductor install
|
|
@@ -300,7 +305,7 @@ export const INSTALL_BRIDGE_AGENT_PROMPT = "Run /install-bridge now, with no arg
|
|
|
300
305
|
"duplicate. This silence applies ONLY to success and ONLY in the install-spawn context: a Stage 6 " +
|
|
301
306
|
"FAILURE is still reported loudly and in full, and in JOIN MODE or a direct/manual /install-bridge " +
|
|
302
307
|
"invocation Stage 6 reports its success exactly as it does today. " +
|
|
303
|
-
"The install-spawn skip set is exactly Stage 8
|
|
308
|
+
"The install-spawn skip set is exactly Stage 8 and Stage 9. The command-owned Stage 11 " +
|
|
304
309
|
"(invite teammates) is NOT in that skip set: it is independently gated and may run only when the " +
|
|
305
310
|
"caller's role is admin AND customer_type is b2b AND an interactive response is available; " +
|
|
306
311
|
"otherwise it is skipped silently. If Stage 11 mints a teammate key, show that plaintext key " +
|
|
@@ -572,7 +577,7 @@ export function getInstallBridgeUsage(baseUrl = DEFAULT_BAPI_BASE_URL) {
|
|
|
572
577
|
" (its own separate consent), then closes the local",
|
|
573
578
|
" out-of-the-box gap in two more phases, each behind",
|
|
574
579
|
" its OWN consent: tool visibility adds the",
|
|
575
|
-
" `
|
|
580
|
+
" `epic_implementer` token to existing .mcp.json /",
|
|
576
581
|
" .vscode/mcp.json / .cursor/mcp.json entries (never",
|
|
577
582
|
" Codex or Copilot, and never creating a file) and",
|
|
578
583
|
" prints the MCP-client restart advisory; local",
|
|
@@ -589,6 +594,26 @@ export function getInstallBridgeUsage(baseUrl = DEFAULT_BAPI_BASE_URL) {
|
|
|
589
594
|
" executor is actually observed. See",
|
|
590
595
|
" `install conductor --help`.",
|
|
591
596
|
"",
|
|
597
|
+
" install conductor --merge-workflow --codeowner <@org/team|@user>",
|
|
598
|
+
" Generate the Epic Implementer merge workflow",
|
|
599
|
+
" plus its CODEOWNERS fragment and repository-rule",
|
|
600
|
+
" guidance, commit them on a managed branch, push",
|
|
601
|
+
" with your own git, and open a pull request with",
|
|
602
|
+
" your own `gh` (BAPI-1136). It NEVER merges that",
|
|
603
|
+
" pull request and Bridge performs no repository",
|
|
604
|
+
" write. A complete read-only preflight runs before",
|
|
605
|
+
" the first branch, file, commit, push, or pull",
|
|
606
|
+
" request. Re-running reports one of four states —",
|
|
607
|
+
" not installed, managed version is older,",
|
|
608
|
+
" identical, or modified/unrecognized — and an",
|
|
609
|
+
" already-open installation or upgrade pull request",
|
|
610
|
+
" is reported rather than duplicated. A modified or",
|
|
611
|
+
" unrecognized workflow is never overwritten. No",
|
|
612
|
+
" credential is ever accepted, prompted for, or",
|
|
613
|
+
" written: the repository Actions secret is named",
|
|
614
|
+
" only, and you create it yourself afterwards.",
|
|
615
|
+
" `--dry-run` performs NO write of any kind.",
|
|
616
|
+
"",
|
|
592
617
|
"Environment: BAPI_BASE_URL (default https://bridgegpt-api.com) and BAPI_DOCS_DIR",
|
|
593
618
|
"(default docs/tmp) are read from the environment with the shown fallbacks.",
|
|
594
619
|
"BAPI_SIGNUP_EMAIL supplies the self-serve signup email non-interactively (it is",
|
|
@@ -960,6 +985,10 @@ export const INSTALL_BRIDGE_BITBUCKET_OFFER_CONTEXT = [
|
|
|
960
985
|
"Step 4b — optional: connect Bitbucket.",
|
|
961
986
|
" This installs the Bridge Bitbucket Forge app so you can implement and review tickets with Bridge.",
|
|
962
987
|
" It opens Bitbucket in your browser; no Bitbucket credential is shared with Bridge.",
|
|
988
|
+
// BAPI-1247: name the destination precisely. "Admin panel" sent people looking for a
|
|
989
|
+
// page that does not exist under that name — the app lives under the workspace's own
|
|
990
|
+
// settings, and saying so is the difference between a 20-second step and a hunt.
|
|
991
|
+
' You finish by pasting a printed code into Workspace settings → Apps → "Connect Bitbucket to Bridge API".',
|
|
963
992
|
` You can do this later instead: ${INSTALL_BRIDGE_CONNECT_BITBUCKET_COMMAND}`,
|
|
964
993
|
];
|
|
965
994
|
/** Default-No Bitbucket offer prompt. Mirrors {@link INSTALL_BRIDGE_GITHUB_CONNECT_PROMPT}. */
|
|
@@ -1024,10 +1053,30 @@ export async function offerBitbucketConnection(repoName, baseUrl, deps, log, ses
|
|
|
1024
1053
|
if (answer !== "y" && answer !== "yes")
|
|
1025
1054
|
return; // declining is a normal outcome
|
|
1026
1055
|
const connectDeps = createDefaultConnectBitbucketDeps();
|
|
1027
|
-
const
|
|
1028
|
-
|
|
1029
|
-
|
|
1030
|
-
|
|
1056
|
+
const result = await runBitbucketConnectionFlow(connectDeps, api, repoName, {
|
|
1057
|
+
// The install path has no workspace slug to give: a workspace is only discoverable
|
|
1058
|
+
// AFTER redemption stages candidates, which is later than this. The connection flow
|
|
1059
|
+
// falls back to its textual navigation path rather than guessing a link.
|
|
1060
|
+
workspace: undefined,
|
|
1061
|
+
// The Step 4b prompt just above IS the approval for the first mint. Asking again
|
|
1062
|
+
// inside the flow would be a second confirmation for one decision.
|
|
1063
|
+
initialMintApproved: true,
|
|
1064
|
+
});
|
|
1065
|
+
if (result.kind === "connected") {
|
|
1066
|
+
// BAPI-1171, mirroring offerGithubConnection: only a CONFIRMED success marks the
|
|
1067
|
+
// run's VCS provider as connected.
|
|
1068
|
+
session.vcsConnected = true;
|
|
1069
|
+
}
|
|
1070
|
+
else {
|
|
1071
|
+
// BAPI-1247: close Bitbucket out loud before Step 4c starts. Step 4c is about
|
|
1072
|
+
// agent notes and has nothing to do with Bitbucket, so without this line a user
|
|
1073
|
+
// who declined a renewal sees the install move on and cannot tell whether the
|
|
1074
|
+
// Bitbucket step succeeded, failed, or is still going.
|
|
1075
|
+
log(" note: Bitbucket connection was not completed. Your install is complete and will continue — " +
|
|
1076
|
+
// Single-quoted repo name, matching the connect command's own resume form, so
|
|
1077
|
+
// a name with spaces or shell metacharacters is inert when pasted.
|
|
1078
|
+
`connect Bitbucket later with npx -y ${MCP_PACKAGE_NAME}@latest connect-bitbucket --repo '${repoName}' ` +
|
|
1079
|
+
"(this issues a new code).");
|
|
1031
1080
|
}
|
|
1032
1081
|
}
|
|
1033
1082
|
catch {
|
|
@@ -1036,21 +1085,59 @@ export async function offerBitbucketConnection(repoName, baseUrl, deps, log, ses
|
|
|
1036
1085
|
`Bitbucket later with 'npx -y ${MCP_PACKAGE_NAME}@latest connect-bitbucket --repo ${repoName}'.`);
|
|
1037
1086
|
}
|
|
1038
1087
|
}
|
|
1039
|
-
/**
|
|
1040
|
-
|
|
1088
|
+
/**
|
|
1089
|
+
* The exact agent-notes consent-prompt prefix, shared with tests so the copy
|
|
1090
|
+
* cannot drift. Mirrors {@link INSTALL_BRIDGE_LAUNCH_CONSENT_PROMPT_PREFIX}: the
|
|
1091
|
+
* prefix is pinned, the platform tail varies with the selection.
|
|
1092
|
+
*/
|
|
1093
|
+
export const INSTALL_BRIDGE_AGENT_NOTES_PROMPT_PREFIX = "Write guidance so ";
|
|
1094
|
+
/**
|
|
1095
|
+
* Describe the selected notes platforms for the offer copy: a naturally joined
|
|
1096
|
+
* subject and whether it is plural (so a verb can agree). `platformLabels` are
|
|
1097
|
+
* the display names of the SELECTED platforms that resolve to a writable notes
|
|
1098
|
+
* target (Windsurf, which has no convention, never appears). An empty list —
|
|
1099
|
+
* not reached while a live offer runs, since the offer self-gates on a writable
|
|
1100
|
+
* target — falls back to a generic, singular "your coding agent".
|
|
1101
|
+
*/
|
|
1102
|
+
function describeAgentNotesPlatforms(platformLabels) {
|
|
1103
|
+
if (platformLabels.length === 0)
|
|
1104
|
+
return { subject: "your coding agent", plural: false };
|
|
1105
|
+
return { subject: joinLabelsWithAnd(platformLabels), plural: platformLabels.length > 1 };
|
|
1106
|
+
}
|
|
1107
|
+
/** Join labels for prose: "A", "A and B", or "A, B, and C". */
|
|
1108
|
+
function joinLabelsWithAnd(labels) {
|
|
1109
|
+
if (labels.length <= 1)
|
|
1110
|
+
return labels[0] ?? "";
|
|
1111
|
+
if (labels.length === 2)
|
|
1112
|
+
return `${labels[0]} and ${labels[1]}`;
|
|
1113
|
+
return `${labels.slice(0, -1).join(", ")}, and ${labels[labels.length - 1]}`;
|
|
1114
|
+
}
|
|
1115
|
+
/**
|
|
1116
|
+
* The default-No agent-notes consent prompt (BAPI-1207; payoff-led, platform-named
|
|
1117
|
+
* copy per the BAPI-1239 install-issue #4 follow-up). Leads with WHY — teaching
|
|
1118
|
+
* the agent how to use Bridge's MCP tools — and NAMES the selected platform(s)
|
|
1119
|
+
* rather than the old generic "cross-platform" phrasing.
|
|
1120
|
+
*/
|
|
1121
|
+
export function buildAgentNotesPrompt(platformLabels) {
|
|
1122
|
+
const { subject, plural } = describeAgentNotesPlatforms(platformLabels);
|
|
1123
|
+
return `${INSTALL_BRIDGE_AGENT_NOTES_PROMPT_PREFIX}${subject} know${plural ? "" : "s"} how to use the Bridge MCP? [y/N]: `;
|
|
1124
|
+
}
|
|
1041
1125
|
/**
|
|
1042
1126
|
* The offer-context lines printed immediately before the agent-notes prompt
|
|
1043
|
-
* (BAPI-1207).
|
|
1044
|
-
*
|
|
1045
|
-
*
|
|
1127
|
+
* (BAPI-1207). Leads with the payoff — teaching the named platform(s) WHEN to
|
|
1128
|
+
* reach for Bridge's MCP tools — then states the marker-scoped, non-destructive
|
|
1129
|
+
* contract as a secondary line, mirroring the GitHub/Bitbucket offers' "explain
|
|
1130
|
+
* before asking" convention.
|
|
1046
1131
|
*/
|
|
1047
|
-
export function buildAgentNotesOfferContext(targets) {
|
|
1132
|
+
export function buildAgentNotesOfferContext(targets, platformLabels) {
|
|
1133
|
+
const { subject, plural } = describeAgentNotesPlatforms(platformLabels);
|
|
1048
1134
|
return [
|
|
1049
1135
|
"",
|
|
1050
|
-
|
|
1051
|
-
`
|
|
1052
|
-
"
|
|
1053
|
-
|
|
1136
|
+
`Step 4c — optional: Bridge MCP guidance for ${subject}.`,
|
|
1137
|
+
` Write guidance so ${subject} know${plural ? "" : "s"} when to reach for Bridge's MCP tools`,
|
|
1138
|
+
" during a task — for project standards, a second opinion, a council, or a ticket review.",
|
|
1139
|
+
` It writes a Bridge-owned region to ${targets.join(" and ")}; only that marked region is`,
|
|
1140
|
+
" created or refreshed — any existing content, inside or outside it, is otherwise preserved.",
|
|
1054
1141
|
];
|
|
1055
1142
|
}
|
|
1056
1143
|
/**
|
|
@@ -1064,16 +1151,16 @@ export function buildAgentNotesOfferContext(targets) {
|
|
|
1064
1151
|
* actual write via {@link writeAgentNotes}, so a `--dry-run` (which never
|
|
1065
1152
|
* reaches this call) and the live path share exactly one write path.
|
|
1066
1153
|
*/
|
|
1067
|
-
export async function offerAgentGuidanceNotes(deps, log, targets) {
|
|
1154
|
+
export async function offerAgentGuidanceNotes(deps, log, targets, platformLabels) {
|
|
1068
1155
|
if (targets.length === 0)
|
|
1069
1156
|
return { kind: "not-applicable" };
|
|
1070
1157
|
// No prompt surface → no offer. Never assume consent on a non-interactive run.
|
|
1071
1158
|
if (!deps.isTTY || !deps.promptLine)
|
|
1072
1159
|
return { kind: "headless-skip" };
|
|
1073
1160
|
try {
|
|
1074
|
-
for (const line of buildAgentNotesOfferContext(targets))
|
|
1161
|
+
for (const line of buildAgentNotesOfferContext(targets, platformLabels))
|
|
1075
1162
|
log(line);
|
|
1076
|
-
const answer = (await deps.promptLine(
|
|
1163
|
+
const answer = (await deps.promptLine(buildAgentNotesPrompt(platformLabels))).trim().toLowerCase();
|
|
1077
1164
|
// Only an explicit yes proceeds — blank input, EOF, "n", and anything malformed
|
|
1078
1165
|
// all decline without writing.
|
|
1079
1166
|
return answer === "y" || answer === "yes" ? { kind: "consented" } : { kind: "declined" };
|
|
@@ -1123,9 +1210,9 @@ export function buildAgentNotesSummaryLine(offer, result, writeFailed, unsupport
|
|
|
1123
1210
|
* Exported so `index.ts`'s `--init` can share the exact same prompt mechanics
|
|
1124
1211
|
* as `install`'s missing-project-root confirmation (BAPI-818, R1).
|
|
1125
1212
|
*/
|
|
1126
|
-
export function promptLineViaReadline(promptText) {
|
|
1213
|
+
export function promptLineViaReadline(promptText, input = process.stdin, output = process.stderr) {
|
|
1127
1214
|
return new Promise((resolve) => {
|
|
1128
|
-
const rl = readline.createInterface({ input
|
|
1215
|
+
const rl = readline.createInterface({ input, output });
|
|
1129
1216
|
// See promptSecretViaReadline: EOF must resolve rather than deadlock the
|
|
1130
1217
|
// top-level await, and `answered` guards the synchronous `close` from
|
|
1131
1218
|
// discarding a real answer.
|
|
@@ -1374,6 +1461,160 @@ function defaultShellFreeRunner(executable, args) {
|
|
|
1374
1461
|
child.on("close", (code) => resolve({ exitCode: code ?? 1, stdout, stderr }));
|
|
1375
1462
|
});
|
|
1376
1463
|
}
|
|
1464
|
+
export function parseMergeWorkflowInstallArgs(argv) {
|
|
1465
|
+
if (!argv.some((a) => a === "--merge-workflow" || a.startsWith("--merge-workflow="))) {
|
|
1466
|
+
return { status: "not-selected" };
|
|
1467
|
+
}
|
|
1468
|
+
let codeOwner;
|
|
1469
|
+
let dryRun = false;
|
|
1470
|
+
let error = null;
|
|
1471
|
+
const setError = (message) => {
|
|
1472
|
+
if (error === null)
|
|
1473
|
+
error = message;
|
|
1474
|
+
};
|
|
1475
|
+
for (let i = 0; i < argv.length; i++) {
|
|
1476
|
+
const raw = argv[i];
|
|
1477
|
+
const eq = raw.indexOf("=");
|
|
1478
|
+
const flag = eq === -1 ? raw : raw.slice(0, eq);
|
|
1479
|
+
const inline = eq === -1 ? undefined : raw.slice(eq + 1);
|
|
1480
|
+
switch (flag) {
|
|
1481
|
+
case "--merge-workflow":
|
|
1482
|
+
if (inline !== undefined)
|
|
1483
|
+
setError("--merge-workflow takes no value");
|
|
1484
|
+
break;
|
|
1485
|
+
case "--dry-run":
|
|
1486
|
+
if (inline !== undefined)
|
|
1487
|
+
setError("--dry-run takes no value");
|
|
1488
|
+
dryRun = true;
|
|
1489
|
+
break;
|
|
1490
|
+
case "--codeowner": {
|
|
1491
|
+
const next = argv[i + 1];
|
|
1492
|
+
if (inline !== undefined) {
|
|
1493
|
+
if (inline.trim().length === 0)
|
|
1494
|
+
setError("--codeowner requires a value");
|
|
1495
|
+
else
|
|
1496
|
+
codeOwner = inline.trim();
|
|
1497
|
+
}
|
|
1498
|
+
else if (next === undefined || next.startsWith("-") || next.trim().length === 0) {
|
|
1499
|
+
setError("--codeowner requires a value");
|
|
1500
|
+
}
|
|
1501
|
+
else {
|
|
1502
|
+
codeOwner = next.trim();
|
|
1503
|
+
i++;
|
|
1504
|
+
}
|
|
1505
|
+
break;
|
|
1506
|
+
}
|
|
1507
|
+
default:
|
|
1508
|
+
setError(`Unknown option: ${flag}`);
|
|
1509
|
+
}
|
|
1510
|
+
}
|
|
1511
|
+
if (error === null && codeOwner === undefined) {
|
|
1512
|
+
error = "--codeowner <@org/team|@user> is required with --merge-workflow";
|
|
1513
|
+
}
|
|
1514
|
+
return {
|
|
1515
|
+
status: "selected",
|
|
1516
|
+
error,
|
|
1517
|
+
options: { codeOwner: codeOwner ?? "", dryRun },
|
|
1518
|
+
};
|
|
1519
|
+
}
|
|
1520
|
+
/** Usage text for the merge-workflow installer. */
|
|
1521
|
+
export function getMergeWorkflowInstallUsage() {
|
|
1522
|
+
return [
|
|
1523
|
+
`Usage: ${MERGE_WORKFLOW_REMEDY_COMMAND} --codeowner <@org/team|@user> [--dry-run]`,
|
|
1524
|
+
"",
|
|
1525
|
+
"Generate the Epic Implementer merge workflow plus its CODEOWNERS fragment and",
|
|
1526
|
+
"repository-rule guidance, commit them on a managed branch, push with your own",
|
|
1527
|
+
"git identity, and open a pull request with your own `gh`.",
|
|
1528
|
+
"",
|
|
1529
|
+
"Options:",
|
|
1530
|
+
" --merge-workflow Select this command.",
|
|
1531
|
+
" --codeowner <@owner> REQUIRED. The one @user or @org/team that should own",
|
|
1532
|
+
" review of the workflow path. Not inferred.",
|
|
1533
|
+
" --dry-run Print the plan and perform NO write: no branch, no",
|
|
1534
|
+
" file, no install-state write, no commit, no push,",
|
|
1535
|
+
" and no pull request.",
|
|
1536
|
+
"",
|
|
1537
|
+
"It never merges the pull request and never asks for a credential. The Bridge",
|
|
1538
|
+
`API key is named only (${MERGE_WORKFLOW_API_KEY_SECRET_NAME}); create that`,
|
|
1539
|
+
"repository Actions secret yourself after the pull request lands.",
|
|
1540
|
+
].join("\n");
|
|
1541
|
+
}
|
|
1542
|
+
/**
|
|
1543
|
+
* Build the installer's seams from the final merged installer deps.
|
|
1544
|
+
*
|
|
1545
|
+
* `git` and `gh` run through {@link defaultShellFreeRunner} — argument arrays,
|
|
1546
|
+
* `shell: false` — and `gh` additionally receives the non-interactive pins so a
|
|
1547
|
+
* prompt can never hang an install. The resulting environment is never printed.
|
|
1548
|
+
*/
|
|
1549
|
+
export function buildMergeWorkflowInstallDeps(deps, log, errorLog) {
|
|
1550
|
+
const toResult = (r) => ({
|
|
1551
|
+
status: r.exitCode,
|
|
1552
|
+
stdout: r.stdout,
|
|
1553
|
+
stderr: r.stderr,
|
|
1554
|
+
});
|
|
1555
|
+
return {
|
|
1556
|
+
cwd: deps.cwd,
|
|
1557
|
+
log,
|
|
1558
|
+
errorLog,
|
|
1559
|
+
readFile: deps.readFile,
|
|
1560
|
+
writeFile: deps.writeFile,
|
|
1561
|
+
mkdir: deps.mkdir,
|
|
1562
|
+
runGit: deps.mergeWorkflowRunGit ??
|
|
1563
|
+
(async (args) => toResult(await defaultShellFreeRunner("git", args))),
|
|
1564
|
+
runGh: deps.mergeWorkflowRunGh ??
|
|
1565
|
+
(async (args) => toResult(await defaultShellFreeRunnerWithEnv("gh", args, {
|
|
1566
|
+
GH_PROMPT_DISABLED: "1",
|
|
1567
|
+
GH_NO_UPDATE_NOTIFIER: "1",
|
|
1568
|
+
}))),
|
|
1569
|
+
confirmWrite: deps.isTTY && deps.promptLine
|
|
1570
|
+
? async (promptText) => {
|
|
1571
|
+
const answer = (await deps.promptLine(promptText)).trim().toLowerCase();
|
|
1572
|
+
return answer === "y" || answer === "yes";
|
|
1573
|
+
}
|
|
1574
|
+
: undefined,
|
|
1575
|
+
recordArtifact: (cwd, relPath) => recordInstalledProjectArtifact(cwd, relPath, {
|
|
1576
|
+
readFile: deps.readFile,
|
|
1577
|
+
writeFile: (p, data) => deps.writeFile(p, data),
|
|
1578
|
+
rename: deps.rename,
|
|
1579
|
+
mkdir: (p, o) => deps.mkdir(p, o),
|
|
1580
|
+
unlink: deps.unlink,
|
|
1581
|
+
}),
|
|
1582
|
+
};
|
|
1583
|
+
}
|
|
1584
|
+
/**
|
|
1585
|
+
* {@link defaultShellFreeRunner} with extra environment entries overlaid.
|
|
1586
|
+
*
|
|
1587
|
+
* Kept separate rather than widening the shared runner's signature: every other
|
|
1588
|
+
* caller wants the inherited environment exactly as it is, and an optional
|
|
1589
|
+
* parameter that only one caller passes is an invitation to pass it wrongly.
|
|
1590
|
+
*
|
|
1591
|
+
* NOTE ON THE INHERITED ENVIRONMENT (BAPI-1136). This spread is deliberate and
|
|
1592
|
+
* is NOT the ambient-identity pattern this ticket removes from the merge path.
|
|
1593
|
+
* The two are opposites: a merge must authenticate as one explicitly configured
|
|
1594
|
+
* credential, because it performs a privileged write on Bridge's behalf; this
|
|
1595
|
+
* `gh` opens a pull request AS THE CUSTOMER, using their own login, which is
|
|
1596
|
+
* exactly what makes the resulting review theirs. `conductor/local-merge.ts`'s
|
|
1597
|
+
* allowlisted child environment is where the other rule lives.
|
|
1598
|
+
*/
|
|
1599
|
+
function defaultShellFreeRunnerWithEnv(executable, args, extraEnv) {
|
|
1600
|
+
return new Promise((resolve, reject) => {
|
|
1601
|
+
const child = spawn(executable, args, {
|
|
1602
|
+
shell: false,
|
|
1603
|
+
stdio: ["ignore", "pipe", "pipe"],
|
|
1604
|
+
env: { ...process.env, ...extraEnv },
|
|
1605
|
+
});
|
|
1606
|
+
let stdout = "";
|
|
1607
|
+
let stderr = "";
|
|
1608
|
+
child.stdout?.on("data", (chunk) => {
|
|
1609
|
+
stdout += chunk.toString("utf-8");
|
|
1610
|
+
});
|
|
1611
|
+
child.stderr?.on("data", (chunk) => {
|
|
1612
|
+
stderr += chunk.toString("utf-8");
|
|
1613
|
+
});
|
|
1614
|
+
child.on("error", reject);
|
|
1615
|
+
child.on("close", (code) => resolve({ exitCode: code ?? 1, stdout, stderr }));
|
|
1616
|
+
});
|
|
1617
|
+
}
|
|
1377
1618
|
/** Presence probe: `<tool> --version` exiting 0 means the tool resolves on PATH. */
|
|
1378
1619
|
async function defaultExecutablePresenceProbe(name) {
|
|
1379
1620
|
try {
|
|
@@ -1393,7 +1634,18 @@ async function defaultExecutablePresenceProbe(name) {
|
|
|
1393
1634
|
* stays in `install-bridge-conductor.ts`; moving it into this dispatch layer
|
|
1394
1635
|
* would put policy in the place least able to test it.
|
|
1395
1636
|
*/
|
|
1396
|
-
|
|
1637
|
+
/**
|
|
1638
|
+
* Compose the executor-provisioning bundle from the FINAL merged deps.
|
|
1639
|
+
*
|
|
1640
|
+
* EXPORTED for testing (BAPI-1137). The bundle's seams are only reachable
|
|
1641
|
+
* through the nested conductor runner after two consents and a successful
|
|
1642
|
+
* start, so driving the whole CLI to assert "the durability probe got the
|
|
1643
|
+
* injected uid" turns a one-line property into an end-to-end rehearsal. Every
|
|
1644
|
+
* entry here is a thin adapter over an implementation that is tested elsewhere;
|
|
1645
|
+
* what this function decides is only WHICH seam each one is wired to, and that
|
|
1646
|
+
* is exactly what a direct test can check.
|
|
1647
|
+
*/
|
|
1648
|
+
export function buildConductorExecutorDeps(deps, baseUrl) {
|
|
1397
1649
|
const homeDir = deps.homedir();
|
|
1398
1650
|
const lookupExecutable = deps.lookupExecutable ?? defaultExecutablePresenceProbe;
|
|
1399
1651
|
const runLifecycleCommand = deps.runLifecycleCommand ?? defaultShellFreeRunner;
|
|
@@ -1416,7 +1668,14 @@ function buildConductorExecutorDeps(deps, baseUrl) {
|
|
|
1416
1668
|
writeUnit: (plan) => writeExecutorServicePlan(plan, {
|
|
1417
1669
|
stat: (filePath) => deps.stat(filePath),
|
|
1418
1670
|
mkdir: (dirPath, options) => deps.mkdir(dirPath, options),
|
|
1419
|
-
|
|
1671
|
+
// BAPI-1137: the write mode is passed through EXPLICITLY rather than
|
|
1672
|
+
// left to the process umask — a unit names the repositories and endpoint
|
|
1673
|
+
// this host serves, and how readable it ends up must not depend on the
|
|
1674
|
+
// shell that happened to run the installer.
|
|
1675
|
+
writeFile: (filePath, content, options) => deps.writeFile(filePath, content, options ?? { encoding: "utf-8", mode: EXECUTOR_SERVICE_UNIT_FILE_MODE }),
|
|
1676
|
+
// The permission seam, so an ALREADY-PRESENT unit's mode is corrected
|
|
1677
|
+
// too: `writeFile`'s mode only applies at creation.
|
|
1678
|
+
chmod: deps.chmod,
|
|
1420
1679
|
// Supplying `readFile` is what enables the `unchanged` outcome, so a
|
|
1421
1680
|
// re-run against an identical unit reports the truth instead of an
|
|
1422
1681
|
// unnecessary rewrite.
|
|
@@ -1433,6 +1692,29 @@ function buildConductorExecutorDeps(deps, baseUrl) {
|
|
|
1433
1692
|
return result.ok ? { ok: true } : { ok: false, error: result.error };
|
|
1434
1693
|
},
|
|
1435
1694
|
startService: (plan) => startExecutorService(plan, runLifecycleCommand),
|
|
1695
|
+
// BAPI-1137 — both new seams are THIN adapters over `service-lifecycle.ts`,
|
|
1696
|
+
// exactly like `startService` above. No probe is built here, no command
|
|
1697
|
+
// string is assembled here, and no policy about what a state means lives
|
|
1698
|
+
// here: this file stays a dispatch layer, and the module that owns
|
|
1699
|
+
// launchctl/systemctl/loginctl keeps owning them.
|
|
1700
|
+
inspectServiceState: (plan) => inspectExecutorServiceState({
|
|
1701
|
+
platform: plan.platform,
|
|
1702
|
+
unitPath: plan.unitPath,
|
|
1703
|
+
serviceIdentifier: plan.serviceIdentifier,
|
|
1704
|
+
// The same injected `getuid` seam the start plan uses — the launchd
|
|
1705
|
+
// domain target needs it, and reading it here rather than inside the
|
|
1706
|
+
// lifecycle module keeps that module free of ambient process state.
|
|
1707
|
+
uid: deps.getuid ? deps.getuid() : null,
|
|
1708
|
+
}, runLifecycleCommand),
|
|
1709
|
+
inspectDurability: (plan) => inspectExecutorServiceDurability({
|
|
1710
|
+
platform: plan.platform,
|
|
1711
|
+
unitPath: plan.unitPath,
|
|
1712
|
+
serviceIdentifier: plan.serviceIdentifier,
|
|
1713
|
+
// Linux lingering is a property of the USER (`loginctl show-user
|
|
1714
|
+
// <uid>`) and the macOS login domain is `gui/<uid>`, so both probes
|
|
1715
|
+
// need the numeric uid from this same seam.
|
|
1716
|
+
uid: deps.getuid ? deps.getuid() : null,
|
|
1717
|
+
}, runLifecycleCommand),
|
|
1436
1718
|
now: deps.monotonicNow ?? (() => Number(process.hrtime.bigint() / 1000000n)),
|
|
1437
1719
|
sleep: deps.sleep ?? ((ms) => new Promise((resolve) => setTimeout(resolve, ms))),
|
|
1438
1720
|
};
|
|
@@ -1454,8 +1736,8 @@ function buildConductorExecutorDeps(deps, baseUrl) {
|
|
|
1454
1736
|
*/
|
|
1455
1737
|
function buildConductorLocalDeps(deps) {
|
|
1456
1738
|
return {
|
|
1457
|
-
mergeProfileToken: () => mergeBridgeApiProfileToken(deps.cwd,
|
|
1458
|
-
inspectProfileToken: () => inspectBridgeApiProfileToken(deps.cwd,
|
|
1739
|
+
mergeProfileToken: () => mergeBridgeApiProfileToken(deps.cwd, EPIC_IMPLEMENTER_PROFILE_TOKEN),
|
|
1740
|
+
inspectProfileToken: () => inspectBridgeApiProfileToken(deps.cwd, EPIC_IMPLEMENTER_PROFILE_TOKEN, {
|
|
1459
1741
|
readFile: deps.readFile,
|
|
1460
1742
|
}),
|
|
1461
1743
|
// The repository root is this command's cwd, which is what the hook
|
|
@@ -2013,6 +2295,174 @@ export function promptMultiSelectViaReadline(promptText, options, input = proces
|
|
|
2013
2295
|
prompt();
|
|
2014
2296
|
});
|
|
2015
2297
|
}
|
|
2298
|
+
/**
|
|
2299
|
+
* Subscription display name for the three platforms whose maximize-posture
|
|
2300
|
+
* option-three wording names the actual tool, keyed by the SAME
|
|
2301
|
+
* `HostPlatformId` `resolveSelectedHostPlatforms` returns. Deliberately does
|
|
2302
|
+
* NOT project `MCP_HOST_TARGETS[id].label` — that registry label reads
|
|
2303
|
+
* "OpenAI Codex", which is not this feature's approved wording.
|
|
2304
|
+
*/
|
|
2305
|
+
const OPTIMIZATION_POSTURE_SUBSCRIPTION_NAMES = {
|
|
2306
|
+
"claude-code": "Claude Code",
|
|
2307
|
+
codex: "Codex",
|
|
2308
|
+
cursor: "Cursor",
|
|
2309
|
+
};
|
|
2310
|
+
/**
|
|
2311
|
+
* `"{name} subscription"` for the three eligible platforms, and the generic
|
|
2312
|
+
* `"coding subscription"` for every other input — Copilot, Windsurf, an
|
|
2313
|
+
* unrecognized id, or no primary platform at all (empty tool selection).
|
|
2314
|
+
*/
|
|
2315
|
+
export function optimizationPostureSubscriptionNounPhrase(primaryPlatform) {
|
|
2316
|
+
const mapped = primaryPlatform ? OPTIMIZATION_POSTURE_SUBSCRIPTION_NAMES[primaryPlatform] : undefined;
|
|
2317
|
+
return mapped ? `${mapped} subscription` : "coding subscription";
|
|
2318
|
+
}
|
|
2319
|
+
/** Option three's exact wording, platform-aware (BAPI-1221 Step 2). */
|
|
2320
|
+
function buildOptimizationPostureOptionThree(primaryPlatform) {
|
|
2321
|
+
return `Get the most out of your ${optimizationPostureSubscriptionNounPhrase(primaryPlatform)}?`;
|
|
2322
|
+
}
|
|
2323
|
+
/** The neutral single-line correction reprompted for any malformed answer. */
|
|
2324
|
+
const OPTIMIZATION_POSTURE_CORRECTION = "Enter a single number from 1 to 3.";
|
|
2325
|
+
/**
|
|
2326
|
+
* Bounds the picker's correction reprompts, mirroring this file's existing
|
|
2327
|
+
* {@link INSTALL_BRIDGE_MAX_CODE_REJECTIONS} convention. A real interactive
|
|
2328
|
+
* user always reaches a terminal answer well inside this many tries — Enter
|
|
2329
|
+
* (blank) or Ctrl+D (EOF) both resolve immediately — so this cap exists only
|
|
2330
|
+
* to stop a `promptLine` seam that NEVER produces a digit or a blank answer
|
|
2331
|
+
* from reprompting forever. Set comfortably above the widest malformed-input
|
|
2332
|
+
* sweep this picker is tested against (a comma list, a range, signed and
|
|
2333
|
+
* decimal values, `0`, `4`, blank-with-more-input, and arbitrary text).
|
|
2334
|
+
*/
|
|
2335
|
+
const OPTIMIZATION_POSTURE_MAX_CORRECTIONS = 20;
|
|
2336
|
+
/**
|
|
2337
|
+
* Render the one-time posture menu and its input instruction. Exactly three
|
|
2338
|
+
* vertically ordered numbered options; option three is platform-aware. Plain,
|
|
2339
|
+
* wrap-safe text — no color-only state, icons, or decorative framing.
|
|
2340
|
+
*/
|
|
2341
|
+
function buildOptimizationPostureMenu(primaryPlatform) {
|
|
2342
|
+
return ("How should Bridge optimize its work on this repository?\n" +
|
|
2343
|
+
" 1. Quality\n" +
|
|
2344
|
+
" 2. Speed\n" +
|
|
2345
|
+
` 3. ${buildOptimizationPostureOptionThree(primaryPlatform)}\n` +
|
|
2346
|
+
`${OPTIMIZATION_POSTURE_CORRECTION} `);
|
|
2347
|
+
}
|
|
2348
|
+
/**
|
|
2349
|
+
* The purpose-built, strict single-select optimization-posture picker
|
|
2350
|
+
* (BAPI-1221 Step 2). Renders the three-option menu exactly once and loops
|
|
2351
|
+
* ONLY the neutral correction line on a malformed answer — never
|
|
2352
|
+
* `promptMultiSelectViaReadline()`, whose comma-separated contract this
|
|
2353
|
+
* picker deliberately does not share.
|
|
2354
|
+
*
|
|
2355
|
+
* Accepts only a trimmed single digit `1`, `2`, or `3`; every other answer
|
|
2356
|
+
* (a comma list, a range, a signed or decimal value, blank text, or
|
|
2357
|
+
* unrelated text) is malformed and reprompted. EOF, an unavailable
|
|
2358
|
+
* `deps.promptLine` seam, or a genuinely blank/no-answer result all resolve
|
|
2359
|
+
* `null` rather than looping indefinitely — `promptLineViaReadline` itself
|
|
2360
|
+
* already collapses real EOF and an empty typed answer to the same `""`, so
|
|
2361
|
+
* there is no seam-level way (or need) to tell them apart here.
|
|
2362
|
+
*/
|
|
2363
|
+
export async function promptOptimizationPosture(deps, primaryPlatform) {
|
|
2364
|
+
if (!deps.isTTY || !deps.promptLine)
|
|
2365
|
+
return null;
|
|
2366
|
+
const promptLine = deps.promptLine;
|
|
2367
|
+
try {
|
|
2368
|
+
let answer = (await promptLine(buildOptimizationPostureMenu(primaryPlatform))).trim();
|
|
2369
|
+
for (let attempts = 0;; attempts++) {
|
|
2370
|
+
if (answer === "1")
|
|
2371
|
+
return "quality";
|
|
2372
|
+
if (answer === "2")
|
|
2373
|
+
return "speed";
|
|
2374
|
+
if (answer === "3")
|
|
2375
|
+
return "maximize_subscription";
|
|
2376
|
+
if (answer === "")
|
|
2377
|
+
return null;
|
|
2378
|
+
if (attempts >= OPTIMIZATION_POSTURE_MAX_CORRECTIONS)
|
|
2379
|
+
return null;
|
|
2380
|
+
answer = (await promptLine(`${OPTIMIZATION_POSTURE_CORRECTION} `)).trim();
|
|
2381
|
+
}
|
|
2382
|
+
}
|
|
2383
|
+
catch {
|
|
2384
|
+
// Prompt/stream exception at the posture-step boundary: fail open to a
|
|
2385
|
+
// no-answer outcome rather than aborting an otherwise-complete install.
|
|
2386
|
+
return null;
|
|
2387
|
+
}
|
|
2388
|
+
}
|
|
2389
|
+
/** Build the posture config-field URL, matching {@link buildPingUrl}'s normalization. */
|
|
2390
|
+
function buildOptimizationPostureUrl(baseUrl) {
|
|
2391
|
+
return `${baseUrl.replace(/\/+$/, "")}/jira/config-field/optimization_posture`;
|
|
2392
|
+
}
|
|
2393
|
+
/**
|
|
2394
|
+
* Persist the captured posture through the existing, generic configuration
|
|
2395
|
+
* endpoint (BAPI-1221 Step 4) — the SAME `PUT /jira/config-field/{field_name}`
|
|
2396
|
+
* every other config-field write uses; this ticket adds no new endpoint,
|
|
2397
|
+
* model, or migration. Fail-open by design: a missing repo identity, a
|
|
2398
|
+
* forbidden write, any other non-OK response, or a network/timeout failure
|
|
2399
|
+
* all return a coarse outcome rather than throwing, and none of them retries
|
|
2400
|
+
* or blocks the rest of installation. The response body is never read or
|
|
2401
|
+
* logged, and the API key never appears in the URL, body, or any returned
|
|
2402
|
+
* message.
|
|
2403
|
+
*/
|
|
2404
|
+
export async function persistOptimizationPosture(deps, baseUrl, apiKey, repoName, value) {
|
|
2405
|
+
if (!repoName || repoName.trim().length === 0) {
|
|
2406
|
+
return { kind: "missing-repo-identity" };
|
|
2407
|
+
}
|
|
2408
|
+
let resp;
|
|
2409
|
+
try {
|
|
2410
|
+
resp = await deps.fetch(buildOptimizationPostureUrl(baseUrl), {
|
|
2411
|
+
method: "PUT",
|
|
2412
|
+
headers: { "X-API-Key": apiKey, "Content-Type": "application/json" },
|
|
2413
|
+
body: JSON.stringify({ repo_name: repoName, value }),
|
|
2414
|
+
signal: AbortSignal.timeout(10_000),
|
|
2415
|
+
});
|
|
2416
|
+
}
|
|
2417
|
+
catch {
|
|
2418
|
+
return { kind: "request-failed" };
|
|
2419
|
+
}
|
|
2420
|
+
if (resp.ok)
|
|
2421
|
+
return { kind: "success" };
|
|
2422
|
+
if (resp.status === 403)
|
|
2423
|
+
return { kind: "forbidden" };
|
|
2424
|
+
return { kind: "http-error", status: resp.status };
|
|
2425
|
+
}
|
|
2426
|
+
/** Confirmation noun phrase used in the deferred-activation success message. */
|
|
2427
|
+
function optimizationPostureConfirmationPhrase(value, primaryPlatform) {
|
|
2428
|
+
if (value === "quality")
|
|
2429
|
+
return "quality";
|
|
2430
|
+
if (value === "speed")
|
|
2431
|
+
return "speed";
|
|
2432
|
+
return `getting the most out of your ${optimizationPostureSubscriptionNounPhrase(primaryPlatform)}`;
|
|
2433
|
+
}
|
|
2434
|
+
/** Printed immediately after a successful posture write, on its own line. */
|
|
2435
|
+
const OPTIMIZATION_POSTURE_BLOG_URL = "https://bridgegpt.ai/blog/how-we-optimize-your-setup";
|
|
2436
|
+
const OPTIMIZATION_POSTURE_REMEDIATION = "Update it later with /teach-bridge or the optimization_posture setting in the web UI.";
|
|
2437
|
+
/**
|
|
2438
|
+
* The generic, secret-free fail-open message shared by every non-success,
|
|
2439
|
+
* non-forbidden outcome — an interactive no-answer/prompt-exception (no PUT
|
|
2440
|
+
* was ever attempted) and a missing-repo-identity/HTTP-error/network failure
|
|
2441
|
+
* (a PUT was attempted and did not take). States plainly that the current
|
|
2442
|
+
* server value is unchanged; a new repository merely READS as Quality via
|
|
2443
|
+
* the BAPI-1220 server-side default, so this never claims every repository
|
|
2444
|
+
* IS now Quality.
|
|
2445
|
+
*/
|
|
2446
|
+
const OPTIMIZATION_POSTURE_UNCHANGED_MESSAGE = ` Optimization posture was not changed; new repositories use Quality by default. ${OPTIMIZATION_POSTURE_REMEDIATION}`;
|
|
2447
|
+
/**
|
|
2448
|
+
* Report the outcome of the optional posture capture/persistence step
|
|
2449
|
+
* (BAPI-1221 Step 5) — called only when an explicit interactive selection was
|
|
2450
|
+
* captured and the PUT helper actually ran.
|
|
2451
|
+
*/
|
|
2452
|
+
function reportOptimizationPostureOutcome(log, result, value, primaryPlatform) {
|
|
2453
|
+
if (result.kind === "success") {
|
|
2454
|
+
log(` Preference saved for later activation — we'll optimize for ${optimizationPostureConfirmationPhrase(value, primaryPlatform)}.`);
|
|
2455
|
+
log(` ${OPTIMIZATION_POSTURE_BLOG_URL}`);
|
|
2456
|
+
return;
|
|
2457
|
+
}
|
|
2458
|
+
if (result.kind === "forbidden") {
|
|
2459
|
+
log(` Optimization posture was not changed (an admin API key is required). ${OPTIMIZATION_POSTURE_REMEDIATION}`);
|
|
2460
|
+
return;
|
|
2461
|
+
}
|
|
2462
|
+
// missing-repo-identity, http-error, request-failed: none carries
|
|
2463
|
+
// user-actionable detail beyond "it didn't take."
|
|
2464
|
+
log(OPTIMIZATION_POSTURE_UNCHANGED_MESSAGE);
|
|
2465
|
+
}
|
|
2016
2466
|
/**
|
|
2017
2467
|
* Resolve the selected host platforms with strict precedence (BAPI-635):
|
|
2018
2468
|
* 1. explicit `--tools` (including an explicit EMPTY selection — never falls
|
|
@@ -3862,7 +4312,7 @@ export function buildDryRunPreview(ctx) {
|
|
|
3862
4312
|
...credentialStep,
|
|
3863
4313
|
...buildCommitAssetsPreviewLines(ctx.commitSafeAssets, ctx.host.kind === "explicit" ? ctx.host.configTargets : []),
|
|
3864
4314
|
...(ctx.host.kind === "explicit" ? buildLaunchStepPreview(ctx.host.launch) : buildDeferredLaunchStepPreview()),
|
|
3865
|
-
...(ctx.host.kind === "explicit" ? buildAgentNotesPreviewLines(ctx.host.notesTargets ?? []) : []),
|
|
4315
|
+
...(ctx.host.kind === "explicit" ? buildAgentNotesPreviewLines(ctx.host.notesTargets ?? [], ctx.host.notesPlatformLabels ?? []) : []),
|
|
3866
4316
|
];
|
|
3867
4317
|
}
|
|
3868
4318
|
/**
|
|
@@ -3974,14 +4424,21 @@ function buildDeferredLaunchStepPreview() {
|
|
|
3974
4424
|
* no writable target (e.g. Windsurf-only), mirroring how the GitHub offer
|
|
3975
4425
|
* lines are unconditional but this offer's applicability is not.
|
|
3976
4426
|
*/
|
|
3977
|
-
function buildAgentNotesPreviewLines(notesTargets) {
|
|
4427
|
+
function buildAgentNotesPreviewLines(notesTargets, platformLabels) {
|
|
3978
4428
|
if (notesTargets.length === 0)
|
|
3979
4429
|
return [];
|
|
4430
|
+
const { subject, plural } = describeAgentNotesPlatforms(platformLabels);
|
|
3980
4431
|
return [
|
|
3981
|
-
|
|
3982
|
-
|
|
3983
|
-
|
|
3984
|
-
"
|
|
4432
|
+
// BAPI-1247: state the sequencing explicitly. A real run reaches this point only
|
|
4433
|
+
// after Step 4b has ended one way or the other, and the live run now says so out
|
|
4434
|
+
// loud, so the preview says so too rather than leaving the order implied.
|
|
4435
|
+
"Step 4c — reached only after the Step 4b VCS connect offer has completed or been",
|
|
4436
|
+
" explicitly deferred (a declined or incomplete connect is announced first,",
|
|
4437
|
+
" with the command to finish it later, and never blocks this step).",
|
|
4438
|
+
`Step 4c — optional: Bridge MCP guidance for ${subject} (SKIPPED in --dry-run): if the`,
|
|
4439
|
+
` terminal is interactive, offer to write guidance so ${subject} know${plural ? "" : "s"} when to reach`,
|
|
4440
|
+
` for Bridge's MCP tools, in a Bridge-owned marker region in ${notesTargets.join(" and ")}; defaults`,
|
|
4441
|
+
" to No, and existing content outside that region is always preserved. No notes files are",
|
|
3985
4442
|
" written in --dry-run.",
|
|
3986
4443
|
];
|
|
3987
4444
|
}
|
|
@@ -4065,7 +4522,7 @@ function buildBootstrapDryRunPreview(ctx) {
|
|
|
4065
4522
|
...promoteStep,
|
|
4066
4523
|
...buildCommitAssetsPreviewLines(ctx.commitSafeAssets, ctx.host.kind === "explicit" ? ctx.host.configTargets : []),
|
|
4067
4524
|
...(ctx.host.kind === "explicit" ? buildLaunchStepPreview(ctx.host.launch) : buildDeferredLaunchStepPreview()),
|
|
4068
|
-
...(ctx.host.kind === "explicit" ? buildAgentNotesPreviewLines(ctx.host.notesTargets ?? []) : []),
|
|
4525
|
+
...(ctx.host.kind === "explicit" ? buildAgentNotesPreviewLines(ctx.host.notesTargets ?? [], ctx.host.notesPlatformLabels ?? []) : []),
|
|
4069
4526
|
];
|
|
4070
4527
|
}
|
|
4071
4528
|
/**
|
|
@@ -4491,6 +4948,40 @@ export async function runInstallBridgeCli(argv, overrides = {}) {
|
|
|
4491
4948
|
// `epic-implementer` name. This nested `install-bridge conductor` surface is
|
|
4492
4949
|
// untouched by that rename.
|
|
4493
4950
|
if (argv[0] === "conductor") {
|
|
4951
|
+
// ---- BAPI-1136: `install-bridge conductor --merge-workflow` ----------
|
|
4952
|
+
//
|
|
4953
|
+
// Intercepted ahead of `parseInstallBridgeConductorArgs` on purpose. That
|
|
4954
|
+
// parser REQUIRES at least one `--required-check` because an empty done gate
|
|
4955
|
+
// passes unconditionally — a rule that is right for the bootstrap phase and
|
|
4956
|
+
// meaningless for this one, which writes three files and opens a pull
|
|
4957
|
+
// request and has no done gate at all. Routing here keeps the bootstrap
|
|
4958
|
+
// parser's invariant intact instead of weakening it for a sibling command.
|
|
4959
|
+
//
|
|
4960
|
+
// It also runs BEFORE any MCP server construction or connection, and before
|
|
4961
|
+
// the base-URL resolution below, because this command makes no Bridge API
|
|
4962
|
+
// call whatsoever: it talks only to the customer's own git and `gh`.
|
|
4963
|
+
const mergeWorkflowArgs = parseMergeWorkflowInstallArgs(argv.slice(1));
|
|
4964
|
+
if (mergeWorkflowArgs.status === "selected") {
|
|
4965
|
+
if (mergeWorkflowArgs.error) {
|
|
4966
|
+
errorLog(`Error: ${mergeWorkflowArgs.error}`);
|
|
4967
|
+
errorLog("");
|
|
4968
|
+
errorLog(getMergeWorkflowInstallUsage());
|
|
4969
|
+
return 1;
|
|
4970
|
+
}
|
|
4971
|
+
try {
|
|
4972
|
+
return await runMergeWorkflowInstall(deps.cwd, mergeWorkflowArgs.options, buildMergeWorkflowInstallDeps(deps, log, errorLog));
|
|
4973
|
+
}
|
|
4974
|
+
catch {
|
|
4975
|
+
// Containment. A domain or subprocess exception escaping here would
|
|
4976
|
+
// print an unbounded message that can carry a remote URL with an
|
|
4977
|
+
// embedded token; the named reason plus a remediation sentence is the
|
|
4978
|
+
// only operator-facing surface this command has.
|
|
4979
|
+
errorLog("merge_workflow_install_failed");
|
|
4980
|
+
errorLog("The merge-workflow installer stopped unexpectedly. Re-run it; anything already " +
|
|
4981
|
+
"created was left in place.");
|
|
4982
|
+
return 1;
|
|
4983
|
+
}
|
|
4984
|
+
}
|
|
4494
4985
|
if (!baseUrlResult.ok) {
|
|
4495
4986
|
errorLog(baseUrlResult.error);
|
|
4496
4987
|
return 1;
|
|
@@ -4615,6 +5106,7 @@ export async function runInstallBridgeCli(argv, overrides = {}) {
|
|
|
4615
5106
|
let host;
|
|
4616
5107
|
if (dryRunPlatforms !== undefined) {
|
|
4617
5108
|
const dryRunTargets = hostConfigTargetsForPlatforms(dryRunPlatforms);
|
|
5109
|
+
const dryRunNotesPlan = resolveAgentNotesTargets(dryRunPlatforms);
|
|
4618
5110
|
const launchDecision = resolveInstallBridgeLaunchDecision(dryRunPlatforms, options.agentName);
|
|
4619
5111
|
let launch;
|
|
4620
5112
|
if (launchDecision.kind === "spawn") {
|
|
@@ -4641,7 +5133,8 @@ export async function runInstallBridgeCli(argv, overrides = {}) {
|
|
|
4641
5133
|
configTargets: dryRunTargets.map((t) => t.relPath),
|
|
4642
5134
|
manualEditors: manualEditorNames(await detectManualEditors(deps)),
|
|
4643
5135
|
launch,
|
|
4644
|
-
notesTargets:
|
|
5136
|
+
notesTargets: dryRunNotesPlan.targets,
|
|
5137
|
+
notesPlatformLabels: labelsForPlatforms(dryRunNotesPlan.supportedPlatforms),
|
|
4645
5138
|
};
|
|
4646
5139
|
}
|
|
4647
5140
|
else {
|
|
@@ -4948,6 +5441,18 @@ export async function runInstallBridgeCli(argv, overrides = {}) {
|
|
|
4948
5441
|
// global (Codex) or manual (Windsurf) target is provisioned by the
|
|
4949
5442
|
// registry-driven emitter after the connectivity check.
|
|
4950
5443
|
const selectedPlatforms = await resolveSelectedHostPlatforms(deps, options);
|
|
5444
|
+
// ---- BAPI-1221 (S2): capture the optimization-posture preference ----
|
|
5445
|
+
// Asked immediately after tool selection, on every otherwise-eligible
|
|
5446
|
+
// interactive install (including reinstalls and an explicit empty
|
|
5447
|
+
// selection) — a prior posture is never read first. `--dry-run` already
|
|
5448
|
+
// returns long before this point (see the NOTE below), so this gate is
|
|
5449
|
+
// belt-and-suspenders, not the sole guard. The primary platform is
|
|
5450
|
+
// `selectedPlatforms[0]` exclusively, relying on the resolver's own
|
|
5451
|
+
// `HOST_PLATFORM_ORDER` normalization even when several tools were chosen.
|
|
5452
|
+
const optimizationPostureEligible = deps.isTTY && Boolean(deps.promptLine) && !options.dryRun;
|
|
5453
|
+
const optimizationPostureSelection = optimizationPostureEligible
|
|
5454
|
+
? await promptOptimizationPosture(deps, selectedPlatforms[0])
|
|
5455
|
+
: null;
|
|
4951
5456
|
if (options.codexProfile && !selectedPlatforms.includes("codex")) {
|
|
4952
5457
|
errorLog("Error: --codex-profile requires Codex in --tools or the interactive selection.");
|
|
4953
5458
|
return 1;
|
|
@@ -5648,6 +6153,18 @@ export async function runInstallBridgeCli(argv, overrides = {}) {
|
|
|
5648
6153
|
: `Error: ${ping.message}`);
|
|
5649
6154
|
}
|
|
5650
6155
|
log(" connectivity OK");
|
|
6156
|
+
// ---- BAPI-1221 (S2): persist the captured posture, now that connectivity
|
|
6157
|
+
// is validated and `repoName`/`apiKey` are both final. Overwrites any
|
|
6158
|
+
// existing value unconditionally — the old value is never read first. A
|
|
6159
|
+
// no-answer (or a prompt/stream exception the picker already absorbed)
|
|
6160
|
+
// reports the same fail-open remediation without ever attempting a write.
|
|
6161
|
+
if (optimizationPostureSelection) {
|
|
6162
|
+
const postureResult = await persistOptimizationPosture(deps, baseUrl, apiKey, repoName, optimizationPostureSelection);
|
|
6163
|
+
reportOptimizationPostureOutcome(log, postureResult, optimizationPostureSelection, selectedPlatforms[0]);
|
|
6164
|
+
}
|
|
6165
|
+
else if (optimizationPostureEligible) {
|
|
6166
|
+
log(OPTIMIZATION_POSTURE_UNCHANGED_MESSAGE);
|
|
6167
|
+
}
|
|
5651
6168
|
const entry = buildInstallBridgeServerEntry(deps.cwd, repoName, apiKey, baseUrl, docsDir);
|
|
5652
6169
|
// BAPI-666: the secret-free variant (no BAPI_API_KEY) is written instead of the
|
|
5653
6170
|
// real entry into any git-tracked project config the user did not explicitly
|
|
@@ -5896,7 +6413,8 @@ export async function runInstallBridgeCli(argv, overrides = {}) {
|
|
|
5896
6413
|
// selected set. `writeAgentNotes` never touches `writeMcpInstallState` or any
|
|
5897
6414
|
// other persisted install-state contract — consent is re-evaluated every run.
|
|
5898
6415
|
const notesPlan = resolveAgentNotesTargets(selectedPlatforms);
|
|
5899
|
-
const
|
|
6416
|
+
const notesPlatformLabels = labelsForPlatforms(notesPlan.supportedPlatforms);
|
|
6417
|
+
const notesOffer = await offerAgentGuidanceNotes(deps, log, notesPlan.targets, notesPlatformLabels);
|
|
5900
6418
|
let notesResult;
|
|
5901
6419
|
let notesWriteFailed = false;
|
|
5902
6420
|
if (notesOffer.kind === "consented") {
|