@bridge_gpt/mcp-server 0.2.34 → 0.2.36

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (49) hide show
  1. package/README.md +456 -370
  2. package/build/agent-capabilities/probe-context.js +8 -1
  3. package/build/agent-capabilities/probes.js +7 -1
  4. package/build/agents.generated.js +1 -1
  5. package/build/claude-review-workflow.js +264 -0
  6. package/build/cli-release.js +53 -0
  7. package/build/commands.generated.js +4 -4
  8. package/build/conductor/bridge-api-client.js +215 -0
  9. package/build/conductor/deny-enforcement-preflight.js +1 -0
  10. package/build/conductor/done-gate.js +44 -5
  11. package/build/conductor/epic-reconcile.js +6 -0
  12. package/build/conductor/install-doctor.js +462 -0
  13. package/build/conductor-bin.js +3 -3
  14. package/build/conductor-bundle-artifacts.js +30 -9
  15. package/build/doctor.js +234 -1
  16. package/build/executor/cli.js +32 -5
  17. package/build/executor/credentials.js +45 -11
  18. package/build/executor/deps.js +14 -0
  19. package/build/executor/env.js +23 -6
  20. package/build/executor/index.js +4 -0
  21. package/build/executor/job-runner.js +119 -9
  22. package/build/executor/permissions.js +12 -2
  23. package/build/executor/preflight.js +95 -8
  24. package/build/executor/prompt-spec.js +51 -0
  25. package/build/executor/runner.js +15 -2
  26. package/build/executor/service-unit.js +876 -0
  27. package/build/executor/test-clock.js +8 -0
  28. package/build/executor/types.js +0 -17
  29. package/build/executor/worker-command.js +62 -9
  30. package/build/index.js +575 -143
  31. package/build/init.js +153 -51
  32. package/build/install-bridge-conductor.js +491 -0
  33. package/build/install-bridge.js +628 -175
  34. package/build/install-reexec.js +233 -0
  35. package/build/mcp-host-config.js +11 -1
  36. package/build/mcp-install-state.js +32 -0
  37. package/build/mcp-provisioning.js +22 -6
  38. package/build/pipelines.generated.js +14 -8
  39. package/build/readme.generated.js +1 -1
  40. package/build/run-unit-tests-launcher.js +257 -0
  41. package/build/setup-epic.js +117 -8
  42. package/build/upgrade-cli.js +1 -15
  43. package/build/version.generated.js +1 -1
  44. package/docs/CONDUCTOR.md +115 -4
  45. package/docs/install/mcp-tool-integrations.md +29 -21
  46. package/package.json +8 -5
  47. package/pipelines/implement-ticket.json +6 -1
  48. package/build/conductor/supervisor-judgment-python.js +0 -141
  49. package/build/conductor/supervisor-judgment.js +0 -215
@@ -1,7 +1,12 @@
1
1
  /**
2
2
  * install-bridge — the one-command Bridge API project bootstrap (BAPI-429).
3
3
  *
4
- * npx -y @bridge_gpt/mcp-server@latest install-bridge [--repo <name>] [--api-key <key>] [--force] [--dry-run] [--agent <name>]
4
+ * npx -y @bridge_gpt/mcp-server install [--repo <name>] [--api-key <key>] [--force] [--dry-run] [--agent <name>]
5
+ *
6
+ * The documented spelling is `install` (BAPI-714, Group E). The legacy
7
+ * `install-bridge` token remains a silent, permanent alias — every string this
8
+ * module prints names `install`, because that is the only spelling the README,
9
+ * the invite email, and the operator-provisioned command teach.
5
10
  *
6
11
  * Collapses the previously five-step manual setup into a single CLI subcommand.
7
12
  * It does the DETERMINISTIC work in the shell:
@@ -36,8 +41,9 @@
36
41
  *
37
42
  * then SPAWNS a fresh agent session (Step 5) for the CONFIGURE-ONLY agentic
38
43
  * remainder: `/install-bridge` config-field derivation → the read-after-write
39
- * concise capability report → a `/learn-repository` recommendation (BAPI-658).
40
- * It does NOT chain into running `/learn-repository` itself — that stays the
44
+ * concise capability report → whatever next step the manifest's `next_step.command`
45
+ * names, rendered only when it is non-empty (BAPI-658, BAPI-720).
46
+ * It does NOT chain into running that command itself — that stays the
41
47
  * human's next explicit invocation — and it never asks about, decides, or
42
48
  * claims anything about repository indexing: indexing is entirely automatic,
43
49
  * gated server-side by the existing readiness funnel, with no prompt on any
@@ -57,22 +63,41 @@
57
63
  * methods, differing only in how the first token is obtained: `bootstrap-invite`
58
64
  * (BAPI-606) redeems a pre-issued invite; `self-serve` (BAPI-618) mints one from an
59
65
  * email. Both then feed the SAME redemption protocol. Selection is pure and
60
- * deterministic from flags/env, except that a BARE interactive run is asked which
61
- * branch it wants (BAPI-626) without that question a first-time user cannot
62
- * discover self-serve at all. Either need-key method NAMES a new project, so the
63
- * repository prompt asks for a new project name rather than an existing
64
- * registration (see `RepoNamePromptMode`).
65
- *
66
- * ORDINARY-ENTRY INVITE RECLASSIFICATION (BAPI-661) the have-key branch's own
67
- * credential input (`--api-key`, `BAPI_API_KEY`, or its hidden prompt) is
68
- * CREDENTIAL-AGNOSTIC: it accepts a full API key OR a bootstrap-invite token,
69
- * because a user handed only an invite has no way to know it belongs in a
70
- * different flag. `classifyEnteredCredential` inspects the resolved value
71
- * immediately after input resolution (before repository-name resolution); a
72
- * `bapi_inv_…` value flips the branch's `bootstrapInviteMode`/`inviteToken` and
73
- * the run falls into the SAME bootstrap-invite protocol described below, verbatim.
74
- * Explicit `--invite`/`BAPI_INVITE` and `--email`/`BAPI_SIGNUP_EMAIL` are never
75
- * reclassified they are already unambiguous and remain authoritative.
66
+ * deterministic from flags/env, except that a BARE interactive run is asked ONE
67
+ * question (BAPI-626/708): `Welcome to Bridge - do you have a token yet?`, answered
68
+ * `1` (yes) or `2` (no). Answering `2` is the self-serve route, which a first-time
69
+ * user could not otherwise discover. Answering `1` opens a single hidden,
70
+ * CREDENTIAL-AGNOSTIC token prompt and the route is then inferred from the pasted
71
+ * value's SHAPE — the user is never asked to pre-classify their own credential,
72
+ * because the shape checks below do that with no probe request. Either need-key
73
+ * method NAMES a new project, so the repository prompt asks for a new project name
74
+ * rather than an existing registration (see `RepoNamePromptMode`).
75
+ *
76
+ * SHAPE-BASED ROUTING, AND THE INFERRED/DECLARED SPLIT (BAPI-661/687/708) — the
77
+ * have-key branch's credential input (`--api-key`, `BAPI_API_KEY`, or its hidden
78
+ * prompt) is CREDENTIAL-AGNOSTIC: it accepts a full API key OR a bootstrap-invite
79
+ * token. `classifyEnteredCredential` (prefix) and `isHighEntropyApiKeyShape`
80
+ * (positive 43-char canonical base64url test) inspect the resolved value
81
+ * immediately after input resolution, before repository-name resolution; a
82
+ * `bapi_inv_…` value flips `bootstrapInviteMode`/`inviteToken` and the run falls
83
+ * into the SAME bootstrap-invite protocol described below, verbatim.
84
+ *
85
+ * What differs is whether the user DECLARED a route:
86
+ *
87
+ * - INFERRED — the bare interactive question was answered. This is the PRIMARY
88
+ * path, not mis-paste recovery: nothing was declared, so the detected type and
89
+ * destination route are ANNOUNCED and the run proceeds. There is no
90
+ * confirmation prompt and no decline exit, because there is no stated intent
91
+ * for the value to contradict.
92
+ * - DECLARED — `--api-key`/`BAPI_API_KEY`, `--invite`/`BAPI_INVITE`, or
93
+ * `--email`/`BAPI_SIGNUP_EMAIL`. BAPI-687 behavior is unchanged, asymmetry
94
+ * included: an invite-shaped value on the key path auto-switches on a non-TTY
95
+ * (with a notice), while an API-key-shaped value on the invite path exits
96
+ * non-zero rather than silently redirecting a script.
97
+ *
98
+ * A value matching NEITHER shape is never reclassified in either direction; it
99
+ * stays on its route and fails there. Because the question is reachable only on a
100
+ * bare interactive TTY run, every non-interactive run is declared by construction.
76
101
  *
77
102
  * BOOTSTRAP-INVITE MODE (BAPI-606) — one of the exceptions to "this command
78
103
  * consumes a key, it does not create one" (the ordinary-entry reclassification
@@ -118,12 +143,17 @@ import { randomBytes as cryptoRandomBytes, createHash } from "crypto";
118
143
  import os from "os";
119
144
  import path from "path";
120
145
  import readline from "readline";
121
- import { runInit, buildBridgeApiEntry } from "./init.js";
146
+ import { runInit, buildBridgeApiEntry, resolveInitScaffoldAssets, refreshBridgeApiPackageSpec, currentBridgePackageSpec, } from "./init.js";
122
147
  import { VERSION } from "./version.generated.js";
123
148
  import { validateRepoName } from "./bridge-config.js";
124
149
  import { MCP_HOST_TARGETS, HOST_PLATFORM_ORDER, allHostTargets, agentForPlatform, isHostPlatformId, detectDefaultPlatforms, } from "./mcp-host-targets.js";
125
150
  import { provisionHostTarget, createDefaultVendorProcessDeps, } from "./mcp-host-config.js";
126
- import { writeMcpInstallState } from "./mcp-install-state.js";
151
+ import { recordInstalledProjectArtifact, writeMcpInstallState } from "./mcp-install-state.js";
152
+ import { runInstallBridgeConductorCli, } from "./install-bridge-conductor.js";
153
+ import { runConductorInstallDoctor, } from "./conductor/install-doctor.js";
154
+ import { resolveConductorBridgeApiAccess, } from "./conductor/bridge-api-client.js";
155
+ import { claudeReviewWorkflowPath, writeClaudeReviewWorkflow, } from "./claude-review-workflow.js";
156
+ import { runSetupEpicCli } from "./setup-epic.js";
127
157
  import { ensureGitignored as ensureGitignoredShared, } from "./git-ignore-utils.js";
128
158
  import { resolveStartTicketsRepoName } from "./start-tickets-repo.js";
129
159
  import { upsertBapiCredential, getPrimaryCredentialStorePath, prepareBootstrapPendingCredential, repointBootstrapPendingCredential, promoteBootstrapPendingCredential, lookupSelfServeBootstrapPendingCredential, discardBootstrapPendingCredential, getBootstrapPendingTarget, resolveBapiCredentials, } from "./credential-store.js";
@@ -136,11 +166,17 @@ import { buildGenericAgentShellCommand, getDefaultSpawnTerminalTabForPlatform, d
136
166
  /** Redaction sentinel — the API-key value is NEVER printed; this stands in. */
137
167
  export const REDACTED_API_KEY = "<REDACTED>";
138
168
  /**
139
- * Always-surfaced success-path note (BAPI-451 W3, E-3). The pinned launcher's
140
- * FIRST cold launch can resolve/download the package inline; if the MCP client's
141
- * connect deadline is very short it may time out before the handshake. Pre-warming
142
- * (below) makes this rare, but the note is surfaced unconditionally so the
143
- * fail-soft backstop is always discoverable.
169
+ * Fail-soft backstop note (BAPI-451 W3, E-3). The pinned launcher's FIRST cold
170
+ * launch can resolve/download the package inline; if the MCP client's connect
171
+ * deadline is very short it may time out before the handshake.
172
+ *
173
+ * BAPI-708 (Part B) narrowed WHERE this is surfaced. It is no longer printed after
174
+ * a SUCCESSFUL pre-warm — there, the cold install the note warns about has already
175
+ * been paid, so the remedy addressed a problem that did not occur. It is still
176
+ * emitted by the pre-warm FAILURE branch, where an incomplete warm makes the
177
+ * remedy real, and by both `--dry-run` previews, which describe the run rather
178
+ * than perform it. `mcp_server/src/doctor.ts` carries an independently-worded
179
+ * near-duplicate for the after-the-fact diagnosis case.
144
180
  */
145
181
  export const MCP_TIMEOUT_GUIDANCE = "Note: if your MCP client has a very short connect deadline, raise MCP_TIMEOUT for the first launch " +
146
182
  "(the initial npx package resolution/download can exceed a short connect timeout).";
@@ -159,8 +195,12 @@ export function buildPrewarmCommandPreview() {
159
195
  /**
160
196
  * The natural-language prompt handed to the spawned agent session. It is
161
197
  * CONFIGURE-ONLY: it derives and applies configuration, presents the concise
162
- * capability report, and closes with a `/learn-repository` recommendation
163
- * (BAPI-658). It never chains straight into running /learn-repository itself —
198
+ * capability report the availability-framed 'Available now' / 'Needs setup'
199
+ * pair the server authors (BAPI-719), rendered verbatim with zero local
200
+ * curation — and closes with the server-provided `next_step.command`
201
+ * when the manifest returned a non-empty one — and with nothing at all when it
202
+ * did not (BAPI-658, BAPI-720). The eligibility decision is the server's, not
203
+ * this prompt's. It never chains straight into running that command itself —
164
204
  * that remains the human's next explicit invocation — and it never asks about,
165
205
  * decides, or claims anything about repository indexing: indexing is entirely
166
206
  * automatic, gated by the existing readiness funnel
@@ -171,13 +211,32 @@ export const INSTALL_BRIDGE_AGENT_PROMPT = "Execute the /install-bridge command
171
211
  "The command chooses its own mode from the install manifest's `configured` flag — do NOT force a " +
172
212
  "mode: when `configured` is false it runs its fresh-configuration flow, and when `configured` is " +
173
213
  "true it runs JOIN MODE. In JOIN MODE the command must NOT derive, approve, apply, or offer any " +
174
- "configuration and applies zero fields for any caller; a joining member's only closing interaction " +
175
- "is the welcome plus the concise capability report (no /learn-repository prompt for a member). " +
214
+ "configuration and applies zero fields for any caller; a joining member's closing interaction " +
215
+ "is the welcome plus the concise capability report. " +
176
216
  "Complete the command's concise capability report: the single 'What Bridge " +
177
- "can help with' section, rendered exactly as the server's concise_tool_capabilities field gives it " +
178
- "(Regularly useful, then Occasionally useful, each tool's display_name only, plus each tier's " +
179
- "server-computed '+N more' where non-zero). Do not render the obsolete five-section report, and do " +
180
- "not locally filter, count, or fall back to the complete tool_capabilities catalog. " +
217
+ "can help with' section, rendered exactly as the server's concise_tool_capabilities field gives it. " +
218
+ "That field is exactly two availability sections in server order 'Available now' first, then " +
219
+ "'Needs setup' and each item in them carries display_name, description, how_to_use, and (in the " +
220
+ "'Needs setup' section only) availability_text. Render each item's display_name, its description, " +
221
+ "and its how_to_use verbatim when that value is a non-empty string; render availability_text " +
222
+ "verbatim as the setup guidance for a 'Needs setup' item, and show NO availability caveat, " +
223
+ "asterisk, or footnote on an 'Available now' item. An item's `kind` ('tool' or 'workflow') is " +
224
+ "contract metadata only — render both kinds identically, with no label, icon, or separate list, " +
225
+ "and never branch on it. Keep an empty section visible with a short neutral line rather than " +
226
+ "dropping it, and after both sections close with a pointer to the Bridge MCP server README at " +
227
+ "https://www.npmjs.com/package/@bridge_gpt/mcp-server for the complete tool documentation, " +
228
+ "including when either or both sections are empty. Do not render the obsolete five-section report, " +
229
+ "and do not locally filter, count, sort, regroup, infer availability, fabricate an item, or fall " +
230
+ "back to the complete tool_capabilities catalog or the workflows collection. If " +
231
+ "concise_tool_capabilities is missing or malformed, take the command's single fallback line and " +
232
+ "render nothing else in its place. " +
233
+ "In the install-spawn context Stage 6 must STILL call persist_routing_credential, but must NOT " +
234
+ "print or summarize its SUCCESSFUL result anywhere — no 'Stored routing credential for …' line, " +
235
+ "and no restatement of that target or path in any later summary or Return section — because the " +
236
+ "install-bridge CLI has already reported where the credential landed and a second sentence is a " +
237
+ "duplicate. This silence applies ONLY to success and ONLY in the install-spawn context: a Stage 6 " +
238
+ "FAILURE is still reported loudly and in full, and in JOIN MODE or a direct/manual /install-bridge " +
239
+ "invocation Stage 6 reports its success exactly as it does today. " +
181
240
  "The install-spawn skip set is exactly Stage 8, Stage 9, and Stage 10. The command-owned Stage 11 " +
182
241
  "(invite teammates) is NOT in that skip set: it is independently gated and may run only when the " +
183
242
  "caller's role is admin AND customer_type is b2b AND an interactive response is available; " +
@@ -186,11 +245,19 @@ export const INSTALL_BRIDGE_AGENT_PROMPT = "Execute the /install-bridge command
186
245
  "Do NOT ask any indexing question, call parse_repository, mention /parse-repository, or claim " +
187
246
  "indexing has started or is pending — indexing is decided entirely by the server-side readiness " +
188
247
  "funnel with no visibility from this session, so say nothing about it at all. " +
189
- "After the capability report on the fresh-configuration path, recommend /learn-repository as the " +
190
- "next step: explain briefly that it " +
191
- "learns repository-specific architecture, review, testing, correctness, and validation-manual " +
192
- "configuration by researching the actual codebase. Do NOT run /learn-repository yourself only " +
193
- "recommend it; running it is the human's next explicit invocation. " +
248
+ "Close with the server's own next step, which you must read at runtime rather than assume. Inspect " +
249
+ "the `next_step` object on the manifest response the flow already used: on the fresh-configuration " +
250
+ "path that is the Stage 7 post-apply read-after-write manifest, and in JOIN MODE it is the Stage 2 " +
251
+ "manifest (JOIN MODE performs no read-after-write). Look at `next_step.command`: when it is a " +
252
+ "NON-EMPTY string, recommend exactly that command, verbatim, in inline code do not rename it or " +
253
+ "substitute a command you happen to know. When it is an empty string, make NO next-command " +
254
+ "recommendation at all and say nothing in its place: no heading, no placeholder, no 'nothing to " +
255
+ "do' line. The server decides eligibility (repository state AND whether this caller can run the " +
256
+ "command), so do NOT apply a role rule or a learned-state rule of your own. Do NOT run the " +
257
+ "returned command yourself — only recommend it; running it is the human's next explicit " +
258
+ "invocation. For supporting context around a recommended command, use the manifest's own " +
259
+ "next_step.post_install_indexing and next_step.post_install_scheduler text rather than inventing " +
260
+ "an explanation for it. " +
194
261
  "Never request, echo, or transport any credential — only ever direct the human to that " +
195
262
  "integration's own configure_in pointer, verbatim. The pointer is per-integration and is NOT " +
196
263
  "always the setup UI: GitHub's is a terminal command (connect-github), while Jira, SFCC, and " +
@@ -227,16 +294,18 @@ export function getInstallBridgeUsage(baseUrl = DEFAULT_BAPI_BASE_URL) {
227
294
  const setupUrl = buildInstallBridgeSetupUrl(baseUrl);
228
295
  return [
229
296
  "Usage:",
230
- " npx -y @bridge_gpt/mcp-server@latest install-bridge [flags]",
297
+ " npx -y @bridge_gpt/mcp-server install [flags]",
231
298
  "",
232
299
  "One-command Bridge API project bootstrap. Scaffolds the project, writes the",
233
300
  "per-host MCP config with your credentials, verifies connectivity, persists the",
234
301
  "routing credential, then — on a TTY, only after a Y/N consent prompt — opens a",
235
302
  "fresh session in your selected tool. If the project is not yet configured it",
236
- "derives the remaining config, presents a concise capability report, and",
237
- "recommends /learn-repository. If the project is already configured it instead",
303
+ "derives the remaining config, presents a concise capability report, and shows",
304
+ "the next step the server returned when it returns one.",
305
+ "If the project is already configured it instead",
238
306
  "joins you to it without proposing or applying any changes and just shows the",
239
- "concise capability report (a b2b admin is additionally offered a teammate-invite",
307
+ "concise capability report plus that same conditional next step (a b2b admin is",
308
+ "additionally offered a teammate-invite",
240
309
  "step). So not every run applies config fields. Indexing is never asked about —",
241
310
  "it starts automatically once the repository reaches full parse readiness.",
242
311
  "",
@@ -247,28 +316,37 @@ export function getInstallBridgeUsage(baseUrl = DEFAULT_BAPI_BASE_URL) {
247
316
  "resolves your key from the credential store at runtime. A config file that",
248
317
  "cannot be parsed is left untouched, with manual-merge instructions printed.",
249
318
  "",
250
- "Run it bare — `install-bridge` with no flags — in a terminal and it asks",
251
- `\`${INSTALL_BRIDGE_KEY_SELECTOR_PROMPT}\` first, with three numbered choices:`,
319
+ "Run it bare — `install` with no flags — in a terminal and it asks",
320
+ `\`${INSTALL_BRIDGE_KEY_SELECTOR_PROMPT}\` first, with two numbered choices:`,
252
321
  ...INSTALL_BRIDGE_ONBOARDING_CHOICES.map((choice) => ` ${choice}`),
253
- "There is NO default — pressing Enter selects nothing; you must type 1, 2, or 3",
254
- "(one blank or invalid answer re-prompts once, then exits with guidance). Option 1",
255
- "is the bootstrap-invite flow, option 2 prompts for an email and creates a",
256
- "brand-new Bridge workspace for you (the self-serve flow), and option 3 is the",
257
- "existing-project API-key flow below. The project-CREATING routes lead",
258
- "deliberately. That question is asked ONLY for a bare interactive run: passing",
259
- "ANY flag, setting BAPI_API_KEY, or running without an interactive terminal keeps",
260
- "the existing deterministic behavior and no prompt.",
322
+ "There is NO default — pressing Enter selects nothing; you must type 1 or 2 (one",
323
+ "blank or invalid answer re-prompts once, then exits with guidance). Answering 1",
324
+ "opens a single hidden token prompt: paste whatever you were given and its SHAPE",
325
+ "picks the route a bootstrap invite (bapi_inv_…) is redeemed to CREATE a new",
326
+ "project and mint your first admin key, while a Bridge API key connects to the",
327
+ "project that already exists. You are never asked which kind of token you hold.",
328
+ "Answering 2 prompts for an email and creates a brand-new Bridge workspace for",
329
+ "you (the self-serve flow). That question is asked ONLY for a bare interactive",
330
+ "run: passing ANY flag, setting BAPI_API_KEY, or running without an interactive",
331
+ "terminal keeps the existing deterministic behavior and no prompt.",
332
+ "",
333
+ "On that bare run the route is INFERRED, so it is only announced — the detected",
334
+ "credential type and where it leads are printed and the run proceeds. There is no",
335
+ "confirmation question and no way to decline, because you never declared a route",
336
+ "for the value to contradict.",
261
337
  "",
262
- "Entered the wrong kind of credential? Both directions are detected by SHAPE",
263
- "alone no probe request is sent and neither switches routes in silence:",
264
- " - a bootstrap invite (bapi_inv_…) entered as an API key is announced, then",
338
+ "When you DO declare a route with a flag or env var, the confirmations stay, and",
339
+ "the two directions are deliberately asymmetric. Both are detected by SHAPE alone",
340
+ " no probe request is sent BEFORE any project-name prompt, credential write,",
341
+ "or network call:",
342
+ " - a bootstrap invite (bapi_inv_…) passed as an API key is announced, then",
265
343
  " confirmed on a terminal before it is redeemed; a non-interactive run keeps",
266
344
  " today's automatic switch but prints the notice.",
267
- " - an API-key-shaped value entered as an invite is announced and may switch in",
345
+ " - an API-key-shaped value passed as an invite is announced and may switch in",
268
346
  " place only after you confirm on a terminal — non-interactive runs exit with",
269
- " guidance instead, so a script is never silently redirected. The check runs",
270
- " BEFORE any project-name prompt, credential write, or network call.",
271
- "A value that is neither shape is left on the route you chose.",
347
+ " guidance instead, so a script is never silently redirected.",
348
+ "A value that is neither shape is left on the route you chose, and is not",
349
+ "announced.",
272
350
  "",
273
351
  "Inputs (the only two irreducible ones):",
274
352
  " --api-key <key> Bridge API key OR bootstrap invite. Falls back to the",
@@ -280,7 +358,10 @@ export function getInstallBridgeUsage(baseUrl = DEFAULT_BAPI_BASE_URL) {
280
358
  " CREATE a new project and its first admin key — the same as",
281
359
  " --invite — so it skips repository lookup entirely (--email",
282
360
  " and --invite remain the explicit, preferred entry points for",
283
- " a new project). NEVER printed or logged.",
361
+ " a new project). Passing the flag or setting the env var",
362
+ " DECLARES the key route, so that switch is confirmed on a",
363
+ " terminal; the bare run's hidden token prompt infers instead",
364
+ " and only announces. NEVER printed or logged.",
284
365
  " --repo <name> Repository name. --repo and BAPI_REPO_NAME still take",
285
366
  " priority and short-circuit before any network call. When",
286
367
  " neither is set, a compatible server resolves the unique",
@@ -294,8 +375,8 @@ export function getInstallBridgeUsage(baseUrl = DEFAULT_BAPI_BASE_URL) {
294
375
  " --repo is required when stdin is non-interactive). In the",
295
376
  " existing-project flow it MUST match the server-side repo",
296
377
  " registration (it keys the credential store as bapi:<repo>).",
297
- " In either new-project flow (--email, --invite, or chooser",
298
- " option 1 or 2 above) it instead NAMES the project this run",
378
+ " In either new-project flow (--email, --invite, answering 2",
379
+ " above, or a token that turns out to be an invite) it NAMES",
299
380
  " creates, so you are asked to name a new project — with a",
300
381
  " suggested default — rather than match an existing one; the",
301
382
  " name must be globally unique.",
@@ -306,13 +387,18 @@ export function getInstallBridgeUsage(baseUrl = DEFAULT_BAPI_BASE_URL) {
306
387
  " It requests a fresh workspace for that email, then creates",
307
388
  " the project and mints your own admin API key in one command.",
308
389
  " Falls back to the BAPI_SIGNUP_EMAIL env var, then a visible",
309
- " interactive prompt — which is also what chooser option 2 on a",
390
+ " interactive prompt — which is also what answering 2 on a",
310
391
  " bare run reaches. The email is NOT a",
311
392
  " secret (it is shown as you type), but it is never printed to",
312
393
  " a log. Mutually",
313
- " exclusive with --api-key and --invite. No email verification",
314
- " is performed and no message is sent to the address — it only",
315
- " labels the new workspace.",
394
+ " exclusive with --api-key and --invite. The address labels the",
395
+ " new workspace AND is used for transactional invite/setup",
396
+ " delivery, so a setup message may be sent to it. Delivery is",
397
+ " asynchronous and best-effort: it is queued after signup rather",
398
+ " than awaited, so do not wait on the message to continue — this",
399
+ " command already prints everything you need. No email",
400
+ " verification is performed; the address is not confirmed before",
401
+ " the workspace is created.",
316
402
  "",
317
403
  " RESUMABLE: a self-serve run that fails mid-protocol saves its",
318
404
  " signup state under bootstrap-pending:<repo> in the credential",
@@ -328,7 +414,7 @@ export function getInstallBridgeUsage(baseUrl = DEFAULT_BAPI_BASE_URL) {
328
414
  " Mutually exclusive with --api-key and --email (in this mode",
329
415
  " the key is created, not consumed).",
330
416
  "",
331
- " Run it WITHOUT a value — `install-bridge --invite` — and the",
417
+ " Run it WITHOUT a value — `install --invite` — and the",
332
418
  " token is read from an interactive prompt with echo",
333
419
  " suppressed, then sent only in the request body. This is the",
334
420
  " default and the recommended path: 'a copy/paste one-liner'",
@@ -385,6 +471,35 @@ export function getInstallBridgeUsage(baseUrl = DEFAULT_BAPI_BASE_URL) {
385
471
  " --agent cursor-agent still force-spawns it.",
386
472
  " -h, --help Show this help.",
387
473
  "",
474
+ "Subcommands:",
475
+ " install conductor Bootstrap Epic Conductor v2 for this repository",
476
+ " (BAPI-679). Runs a unified READ-ONLY doctor that",
477
+ " composes the conductor, executor-preflight,",
478
+ " install-status, and executor service-unit",
479
+ " diagnostics with server-side",
480
+ " readiness (supervisor defaults, empty required-",
481
+ " check detection, GitHub App credentials and the",
482
+ " `actions` permission, reconciler tick liveness).",
483
+ " The resolved BAPI_BASE_URL is confirmed BEFORE",
484
+ " any network call — the doctor's readiness GET",
485
+ " carries the repo API key, and the URL silently",
486
+ " defaults to production when unset. A fatal",
487
+ " finding aborts BEFORE any write. A separate",
488
+ " consent immediately precedes the bootstrap POST,",
489
+ " which writes the safe project-default supervisor",
490
+ " posture in one transaction; it optionally",
491
+ " scaffolds a parameterized claude-review workflow",
492
+ " (its own separate consent), prints the capability",
493
+ " matrix, and re-runs the doctor. `--dry-run`",
494
+ " performs NO write of any kind and no write-consent",
495
+ " prompt (the base-URL confirmation still applies).",
496
+ " Executor service units are generated by",
497
+ " `executor install-service`; starting one stays",
498
+ " operator-managed, so a run reports",
499
+ " `complete-with-manual-steps` until a live",
500
+ " executor is actually observed. See",
501
+ " `install conductor --help`.",
502
+ "",
388
503
  "Environment: BAPI_BASE_URL (default https://bridgegpt-api.com) and BAPI_DOCS_DIR",
389
504
  "(default docs/tmp) are read from the environment with the shown fallbacks.",
390
505
  "BAPI_SIGNUP_EMAIL supplies the self-serve signup email non-interactively (it is",
@@ -519,7 +634,7 @@ export function parseInstallBridgeArgs(argv) {
519
634
  }
520
635
  return {
521
636
  status: "error",
522
- message: `Unexpected positional argument: '${arg}'. install-bridge does not accept positional arguments.`,
637
+ message: `Unexpected positional argument: '${arg}'. install does not accept positional arguments.`,
523
638
  };
524
639
  }
525
640
  // Mutually exclusive by construction: bootstrap-invite mode CREATES the key, so
@@ -651,7 +766,7 @@ export const INSTALL_BRIDGE_CONNECT_GITHUB_COMMAND = "npx -y @bridge_gpt/mcp-ser
651
766
  export const INSTALL_BRIDGE_GITHUB_OFFER_CONTEXT = [
652
767
  "",
653
768
  "Step 4b — optional: connect GitHub.",
654
- " This installs the Bridge GitHub App so pull requests and code review work. It opens",
769
+ " This installs the Bridge GitHub App so you can implement and review tickets with Bridge. It opens",
655
770
  " github.com in your browser; no GitHub credential is shared with Bridge.",
656
771
  ` You can do this later instead: ${INSTALL_BRIDGE_CONNECT_GITHUB_COMMAND}`,
657
772
  ];
@@ -854,6 +969,55 @@ export function createDefaultInstallBridgeDeps() {
854
969
  if (process.env.BAPI_INSTALL_DEBUG)
855
970
  console.error(m);
856
971
  },
972
+ // ---- Nested `install-bridge conductor` seams (BAPI-679) ----
973
+ conductorRunDoctor: (params) => runConductorInstallDoctor({
974
+ access: params.access,
975
+ accessError: params.accessError,
976
+ fetch: params.fetch,
977
+ reviewPolicySource: params.reviewPolicySource,
978
+ readWorkflowFile: () => params.readFile(claudeReviewWorkflowPath(params.cwd)),
979
+ installDoctorDeps: {
980
+ env: params.env,
981
+ cwd: params.cwd,
982
+ platform: process.platform,
983
+ homedir: os.homedir,
984
+ readFile: params.readFile,
985
+ stat: (p) => stat(p),
986
+ fetch: (...args) => fetch(...args),
987
+ },
988
+ }),
989
+ conductorResolveAccess: async (repoName, baseUrl, mergedDeps) => {
990
+ const result = await resolveConductorBridgeApiAccess({
991
+ env: { ...mergedDeps.env, BAPI_BASE_URL: baseUrl },
992
+ cwd: mergedDeps.cwd,
993
+ homedir: mergedDeps.homedir,
994
+ platform: mergedDeps.platform,
995
+ readFile: mergedDeps.readFile,
996
+ stat: (p) => mergedDeps.stat(p),
997
+ repoName,
998
+ });
999
+ // Never surface the resolver's secret-bearing internals — it already
1000
+ // guarantees a secret-free `error`, which is forwarded verbatim.
1001
+ return result.ok ? { ok: true, access: result.access } : { ok: false, error: result.error };
1002
+ },
1003
+ conductorWriteWorkflow: (cwd, content, options, mergedDeps) => writeClaudeReviewWorkflow(cwd, content, {
1004
+ readFile: mergedDeps.readFile,
1005
+ mkdir: (p, o) => mergedDeps.mkdir(p, o),
1006
+ writeFile: (p, data) => mergedDeps.writeFile(p, data),
1007
+ }, options),
1008
+ conductorRecordArtifact: async (cwd, relPath, mergedDeps) => {
1009
+ const result = await recordInstalledProjectArtifact(cwd, relPath, {
1010
+ readFile: mergedDeps.readFile,
1011
+ writeFile: (p, data) => mergedDeps.writeFile(p, data),
1012
+ rename: mergedDeps.rename,
1013
+ mkdir: (p, o) => mergedDeps.mkdir(p, o),
1014
+ unlink: mergedDeps.unlink,
1015
+ });
1016
+ return result.ok ? { ok: true } : { ok: false, error: result.error };
1017
+ },
1018
+ conductorRunSetupEpicDryRun: async (argv) => ({
1019
+ exitCode: await runSetupEpicCli(argv),
1020
+ }),
857
1021
  };
858
1022
  }
859
1023
  /**
@@ -881,7 +1045,11 @@ export async function resolveApiKey(options, deps) {
881
1045
  return { ok: true, value: fromEnv.trim(), source: "env" };
882
1046
  }
883
1047
  if (deps.isTTY && deps.promptSecret) {
884
- const entered = (await deps.promptSecret("Bridge API key or invite (input hidden): ")).trim();
1048
+ // BAPI-708 (A-3): the prompt no longer asks the user to pre-classify their own
1049
+ // credential ("Bridge API key or invite"). Classification is done from SHAPE
1050
+ // downstream with no probe request, so the only thing worth asking for is the
1051
+ // token itself.
1052
+ const entered = (await deps.promptSecret("Paste your Bridge token (input hidden): ")).trim();
885
1053
  if (entered.length > 0) {
886
1054
  return { ok: true, value: entered, source: "prompt" };
887
1055
  }
@@ -890,12 +1058,12 @@ export async function resolveApiKey(options, deps) {
890
1058
  // and it named no route usable by someone who has no key at all.
891
1059
  return {
892
1060
  ok: false,
893
- error: `No Bridge API key or invite entered. ${INSTALL_BRIDGE_ENTRY_ROUTES_ADVICE}`,
1061
+ error: `No Bridge token entered. ${INSTALL_BRIDGE_ENTRY_ROUTES_ADVICE}`,
894
1062
  };
895
1063
  }
896
1064
  return {
897
1065
  ok: false,
898
- error: "A Bridge API key or invite is required (no interactive terminal is available to prompt " +
1066
+ error: "A Bridge token is required (no interactive terminal is available to prompt " +
899
1067
  `for it). ${INSTALL_BRIDGE_ENTRY_ROUTES_ADVICE}`,
900
1068
  };
901
1069
  }
@@ -935,7 +1103,7 @@ export async function resolveInviteToken(options, deps) {
935
1103
  error: "A bootstrap invite token is required. Pass --invite <token> or set the BAPI_INVITE " +
936
1104
  "environment variable (no interactive terminal is available to prompt for it). Note that " +
937
1105
  "both forms expose the token to your shell history and process list — prefer running " +
938
- "'install-bridge --invite' interactively.",
1106
+ "'install --invite' interactively.",
939
1107
  };
940
1108
  }
941
1109
  /**
@@ -1002,40 +1170,38 @@ export function resolveInstallBridgeOnboardingBranch(options, env) {
1002
1170
  return { kind: "have-key" };
1003
1171
  }
1004
1172
  /**
1005
- * The bare-TTY onboarding chooser (BAPI-667), replacing the BAPI-626/661 Y/n
1006
- * question. The old prompt defaulted a bare Enter to "yes" and dropped the newest
1007
- * users — the ones with no key, no invite, and no account — into a hidden API-key
1008
- * prompt they could not answer, then exited. The self-serve path built for exactly
1009
- * that user was never named. The chooser names all three routes and has NO default,
1010
- * so Enter can no longer route anyone into a prompt they cannot satisfy.
1173
+ * The bare-TTY onboarding question (BAPI-708, Part A), replacing the BAPI-667/687
1174
+ * three-option chooser.
1175
+ *
1176
+ * The three-option form asked the user to make a distinction the CODE can already
1177
+ * make for itself: whether the string in their clipboard is an invite or an API
1178
+ * key. `classifyEnteredCredential` + `isHighEntropyApiKeyShape` decide that from
1179
+ * SHAPE alone, with no probe request — so the only thing a first-time user
1180
+ * genuinely knows, and the only thing worth asking, is whether they have a token
1181
+ * at all. Hence a yes/no question, with the route inferred afterwards.
1011
1182
  *
1012
- * Interaction contract mirrors the BAPI-663 tool picker: render the heading and the
1013
- * numbered options once, accept ONE strict line.
1183
+ * Interaction contract is unchanged from BAPI-667/687 and remains load-bearing:
1184
+ * render the heading and the numbered options exactly once, accept ONE strict
1185
+ * line, no default (a bare Enter selects nothing), bounded to a single re-prompt
1186
+ * after one hint, and EOF resolves as an empty answer so it can never hang.
1014
1187
  */
1015
- export const INSTALL_BRIDGE_KEY_SELECTOR_PROMPT = "How would you like to connect to Bridge API?";
1188
+ export const INSTALL_BRIDGE_KEY_SELECTOR_PROMPT = "Welcome to Bridge - do you have a token yet?";
1016
1189
  /**
1017
- * The chooser's numbered options, in render order. Index + 1 is the accepted token.
1190
+ * The question's numbered options, in render order. Index + 1 is the accepted
1191
+ * token, exactly as before — only the option SET narrowed from three to two.
1018
1192
  *
1019
- * BAPI-687 (A-1) put the two PROJECT-CREATING routes first. The previous order led
1020
- * with the existing-project key path, labelled only "I have a Bridge API key" — which
1021
- * reads as the default for anyone holding any credential, so a user with a valid
1022
- * invite picked it and dead-ended at a repo-not-registered 403 with no route to the
1023
- * path that would have created their project. Position was doing the opposite of the
1024
- * intended routing.
1025
- *
1026
- * Option 3's label carries its own scope ("for an existing project") rather than
1027
- * relying on position (A-3): a stale screenshot or a habituated user must still be
1028
- * able to read the right choice off the label itself.
1193
+ * "Token" is deliberately credential-agnostic: an invite and an API key are both
1194
+ * tokens as far as this question is concerned, and answering "Yes" leads to ONE
1195
+ * hidden prompt whose value selects the route by shape.
1029
1196
  */
1030
1197
  export const INSTALL_BRIDGE_ONBOARDING_CHOICES = [
1031
- "1. I have an invite token",
1032
- "2. I'm new — set me up with just my email",
1033
- "3. I have an API key for an existing project",
1198
+ "1. Yes, I have received a token",
1199
+ "2. No, I need one",
1034
1200
  ];
1035
1201
  /** The single-line answer prompt drawn after the options (no bracketed default). */
1036
- export const INSTALL_BRIDGE_ONBOARDING_CHOICE_PROMPT = "Enter 1, 2, or 3: ";
1202
+ export const INSTALL_BRIDGE_ONBOARDING_CHOICE_PROMPT = "Enter 1 or 2: ";
1037
1203
  /** The one hint line printed after a blank/invalid answer, before the single re-prompt. */
1038
- export const INSTALL_BRIDGE_ONBOARDING_CHOICE_HINT = "Enter 1, 2, or 3.";
1204
+ export const INSTALL_BRIDGE_ONBOARDING_CHOICE_HINT = "Enter 1 or 2.";
1039
1205
  /**
1040
1206
  * The one place the "no usable credential" copy lives (BAPI-667, U1-3).
1041
1207
  *
@@ -1044,10 +1210,10 @@ export const INSTALL_BRIDGE_ONBOARDING_CHOICE_HINT = "Enter 1, 2, or 3.";
1044
1210
  * advised "try the hidden prompt again" — false, because the process has already
1045
1211
  * exited by the time the user reads it. All three now name the SAME three routes.
1046
1212
  */
1047
- export const INSTALL_BRIDGE_ENTRY_ROUTES_ADVICE = "Re-run install-bridge and choose an option: pass --api-key <key> if you have a Bridge API " +
1048
- "key (or set BAPI_API_KEY), --invite if you were sent an invite token, or --email <addr> " +
1049
- "to sign up with just an email on a bare interactive run, re-run and choose option 2 to " +
1050
- "sign up with just an email.";
1213
+ export const INSTALL_BRIDGE_ENTRY_ROUTES_ADVICE = "Re-run install: on a bare interactive run answer 1 if you already have a token " +
1214
+ "(an invite or an API key either one is accepted at the hidden prompt) or 2 to sign up " +
1215
+ "with just an email. Non-interactively, pass --api-key <key> (or set BAPI_API_KEY) for an " +
1216
+ "existing project, --invite if you were sent an invite token, or --email <addr> to sign up.";
1051
1217
  /**
1052
1218
  * Interactive wrapper around {@link resolveInstallBridgeOnboardingBranch}.
1053
1219
  *
@@ -1076,21 +1242,32 @@ export const INSTALL_BRIDGE_ENTRY_ROUTES_ADVICE = "Re-run install-bridge and cho
1076
1242
  * bounded to ONE re-prompt — an unanswerable prompt that loops is the same
1077
1243
  * dead-end in a different costume.
1078
1244
  *
1079
- * BAPI-687 reordered the options so the project-CREATING routes lead (1 = invite,
1080
- * 2 = self-serve email, 3 = existing-project API key). Only the labels and the
1081
- * dispatch moved: the heading, the `Enter 1, 2, or 3: ` prompt, the accepted-token
1082
- * set, the no-default rule, EOF handling, and every flag/env/non-TTY bypass are
1083
- * byte-for-byte unchanged.
1245
+ * BAPI-708 (Part A) narrowed those three options to a token yes/no question. The
1246
+ * chooser was asking the user to pre-classify their own credential, which the
1247
+ * shape checks downstream already do without a probe request. Every interaction
1248
+ * property above survives verbatim heading + options rendered once, one strict
1249
+ * line, no default, one hint, one re-prompt, EOF-as-empty — and so does every
1250
+ * bypass. Only the option SET and the dispatch changed.
1251
+ *
1252
+ * PROVENANCE. This function is the SOLE authority on `credentialIntent`, and that
1253
+ * is deliberate: the route-switch arms downstream must not re-derive provenance
1254
+ * from `argv`, because a second derivation is exactly the drift BAPI-708 exists to
1255
+ * fix. `"inferred"` is returned ONLY when the question was actually answered — a
1256
+ * state reachable on a bare interactive TTY run and nowhere else. Every other exit
1257
+ * is `"declared"`, so 100% of non-interactive runs stay on declared paths and the
1258
+ * documented directional asymmetry (invite-on-key auto-switches on a non-TTY;
1259
+ * key-on-invite exits non-zero) is preserved BY CONSTRUCTION. Do not "unify" the
1260
+ * two.
1084
1261
  */
1085
1262
  export async function resolveInstallBridgeOnboardingBranchForRun(options, deps, argv) {
1086
1263
  const branch = resolveInstallBridgeOnboardingBranch(options, deps.env);
1087
1264
  // An explicit need-key intent is already unambiguous — never re-ask it.
1088
1265
  if (branch.kind === "need-key")
1089
- return { ok: true, branch };
1266
+ return { ok: true, branch, credentialIntent: "declared" };
1090
1267
  const hasEnvApiKey = (deps.env.BAPI_API_KEY ?? "").trim().length > 0;
1091
1268
  const isBareInvocation = argv.length === 0;
1092
1269
  if (!deps.isTTY || !deps.promptLine || !isBareInvocation || hasEnvApiKey) {
1093
- return { ok: true, branch };
1270
+ return { ok: true, branch, credentialIntent: "declared" };
1094
1271
  }
1095
1272
  const promptLine = deps.promptLine;
1096
1273
  try {
@@ -1105,18 +1282,20 @@ export async function resolveInstallBridgeOnboardingBranchForRun(options, deps,
1105
1282
  // it can never hang or silently select a branch.
1106
1283
  for (let attempt = 0; attempt < 2; attempt += 1) {
1107
1284
  const answer = (await promptLine(INSTALL_BRIDGE_ONBOARDING_CHOICE_PROMPT)).trim();
1108
- // BAPI-687 (A-2): the dispatch moved WITH the labels the accepted tokens are
1109
- // still exactly 1/2/3 (no letters, no aliases), so the one-strict-line input
1110
- // contract BAPI-667 established is unchanged. Either branch stays
1111
- // credential-agnostic: a value pasted onto the wrong path is detected and
1112
- // re-routed downstream rather than silently accepted (B-2 / B-3).
1113
- if (answer === "1") {
1114
- return { ok: true, branch: { kind: "need-key", method: "bootstrap-invite" } };
1285
+ // BAPI-708 (A-1): the accepted tokens are now exactly 1/2 (no letters, no
1286
+ // aliases), preserving the one-strict-line contract. "Yes" routes to the
1287
+ // have-key branch's credential-agnostic prompt, whose value is classified by
1288
+ // SHAPE — so a user holding an invite still lands in the redemption protocol
1289
+ // without ever having been asked which kind of token they hold.
1290
+ if (answer === "1")
1291
+ return { ok: true, branch: { kind: "have-key" }, credentialIntent: "inferred" };
1292
+ if (answer === "2") {
1293
+ return {
1294
+ ok: true,
1295
+ branch: { kind: "need-key", method: "self-serve" },
1296
+ credentialIntent: "inferred",
1297
+ };
1115
1298
  }
1116
- if (answer === "2")
1117
- return { ok: true, branch: { kind: "need-key", method: "self-serve" } };
1118
- if (answer === "3")
1119
- return { ok: true, branch: { kind: "have-key" } };
1120
1299
  if (attempt === 0)
1121
1300
  deps.log(INSTALL_BRIDGE_ONBOARDING_CHOICE_HINT);
1122
1301
  }
@@ -1465,13 +1644,21 @@ export function buildManualInstallBridgeContinuation(kind, toolLabels) {
1465
1644
  if (kind === "empty-selection") {
1466
1645
  return [
1467
1646
  "No AI coding tools were configured, so nothing was set up for this project.",
1468
- "Re-run install-bridge and select at least one tool to configure it.",
1647
+ "Re-run install and select at least one tool to configure it.",
1469
1648
  ].join("\n");
1470
1649
  }
1471
1650
  const toolPhrase = formatToolLabelPhrase(toolLabels);
1651
+ // BAPI-708 (B-c): the reload clause lives here so the requirement survives on
1652
+ // every NO-SPAWN terminal path (declined consent, non-TTY suppression, no
1653
+ // launchable tool, chooser failure, failed spawn). A written project MCP config
1654
+ // is not a live connection until the host reloads and its trust prompt is
1655
+ // approved — the single most common place a fresh install looks broken. The
1656
+ // spawn path states the same requirement in its own handoff.
1472
1657
  return [
1473
1658
  `To finish configuring this project, open it in ${toolPhrase} that has the`,
1474
1659
  "Bridge MCP server configured, start a new session, and run /install-bridge.",
1660
+ "Restart or reload the tool first if it is already running — the newly written",
1661
+ "MCP server is not live until it reloads and you approve it if prompted.",
1475
1662
  "Until the project is configured, your Bridge MCP tools stay limited.",
1476
1663
  ].join("\n");
1477
1664
  }
@@ -1795,6 +1982,22 @@ async function requestTrackedConfigConsent(deps, relPath) {
1795
1982
  const normalized = (answer ?? "").trim().toLowerCase();
1796
1983
  return normalized === "y" || normalized === "yes";
1797
1984
  }
1985
+ /**
1986
+ * Overlay a freshly built Bridge entry onto whatever is already registered,
1987
+ * preserving the EXISTING launcher args composition (BAPI-714, Group C).
1988
+ *
1989
+ * Only the package-spec token is refreshed on an existing entry, so a legacy bare
1990
+ * launcher stays bare and an already-`serve`-bearing launcher keeps `serve`
1991
+ * exactly once. When there is no existing entry — or its args are a shape we
1992
+ * cannot recognize — the new template args (which DO include `serve`) are used
1993
+ * verbatim. `command` and `env` always come from the freshly built entry: this
1994
+ * helper governs launcher-token migration only, never credentials.
1995
+ */
1996
+ export function preserveExistingLauncherArgs(existing, entry) {
1997
+ const existingArgs = asRecord(existing)?.args;
1998
+ const preserved = refreshBridgeApiPackageSpec(existingArgs, currentBridgePackageSpec());
1999
+ return preserved ? { ...entry, args: preserved } : entry;
2000
+ }
1798
2001
  /**
1799
2002
  * Write the `bridge-api` entry into each host config via read-merge-write,
1800
2003
  * preserving unrelated servers and top-level keys (BAPI-666). Per target:
@@ -1855,7 +2058,13 @@ async function writeHostConfigs(deps, targets, entries, trackedState, ctx) {
1855
2058
  // never initialized before its tracked-state consent is resolved.
1856
2059
  const config = read.state === "parsed" ? read.config : {};
1857
2060
  const topLevel = asRecord(config[target.topLevelKey]) ?? {};
1858
- topLevel["bridge-api"] = entry;
2061
+ // BAPI-714 (Group C): a PRE-EXISTING Bridge entry keeps its own launcher args
2062
+ // composition — only the package-spec token is refreshed — so the installer
2063
+ // never migrates a legacy bare launcher to `serve`. A newly created entry gets
2064
+ // the full template args, `serve` included. The rule is identical for the
2065
+ // real-key and secret-free variants: credential safety must not decide whether
2066
+ // a launcher is migrated.
2067
+ topLevel["bridge-api"] = preserveExistingLauncherArgs(topLevel["bridge-api"], entry);
1859
2068
  config[target.topLevelKey] = topLevel;
1860
2069
  await deps.mkdir(path.dirname(fullPath), { recursive: true });
1861
2070
  await deps.writeFile(fullPath, JSON.stringify(config, null, 2) + "\n", {
@@ -2294,7 +2503,7 @@ export const INSTALL_BRIDGE_INVITE_ON_KEY_PATH_PROMPT = "Redeem it as an invite
2294
2503
  /** Non-TTY form: the switch still happens automatically, but it is announced (B-2). */
2295
2504
  export const INSTALL_BRIDGE_INVITE_ON_KEY_PATH_AUTO_NOTICE = "Redeeming it as a bootstrap invite (non-interactive — no confirmation is possible here).";
2296
2505
  /** Printed on decline. Names the other route rather than leaving the user stranded. */
2297
- export const INSTALL_BRIDGE_INVITE_ON_KEY_PATH_DECLINED = "Error: cancelled — nothing was created. Re-run install-bridge with an API key for an " +
2506
+ export const INSTALL_BRIDGE_INVITE_ON_KEY_PATH_DECLINED = "Error: cancelled — nothing was created. Re-run install with an API key for an " +
2298
2507
  "existing project (--api-key <key>, or BAPI_API_KEY), or re-run and confirm to redeem the " +
2299
2508
  "invite and create a new project.";
2300
2509
  /**
@@ -2320,7 +2529,7 @@ export const INSTALL_BRIDGE_KEY_ON_INVITE_PATH_PROMPT = "Use it to connect to yo
2320
2529
  * direction would silently redirect a script away from the project-creating run it
2321
2530
  * asked for. A script gets a non-zero exit and the exact flag it should have passed.
2322
2531
  */
2323
- export const INSTALL_BRIDGE_KEY_ON_INVITE_PATH_DECLINED = "Error: cancelled — nothing was created and no invite was spent. Re-run install-bridge with " +
2532
+ export const INSTALL_BRIDGE_KEY_ON_INVITE_PATH_DECLINED = "Error: cancelled — nothing was created and no invite was spent. Re-run install with " +
2324
2533
  "--api-key <key> (or BAPI_API_KEY) to connect to your existing project, or supply a bootstrap " +
2325
2534
  "invite token (bapi_inv_…) to create a new one.";
2326
2535
  /**
@@ -2430,7 +2639,7 @@ export const BOOTSTRAP_INVITE_LOST_SECRET_MESSAGE = [
2430
2639
  "",
2431
2640
  "Ask your Bridge API operator to recover it for you: they revoke the orphaned key",
2432
2641
  "(DELETE /setup/keys/{id}) and issue a replacement key for the EXISTING project",
2433
- "(POST /setup/keys), then send you that key. Run install-bridge with --api-key <that key>.",
2642
+ "(POST /setup/keys), then send you that key. Run install with --api-key <that key>.",
2434
2643
  ].join("\n");
2435
2644
  /**
2436
2645
  * The 401 message for a run that REUSED an existing pending secret — the replay
@@ -2459,8 +2668,8 @@ export const BOOTSTRAP_SELF_SERVE_EXPIRED_MESSAGE = "The Bridge API rejected the
2459
2668
  */
2460
2669
  export function buildBootstrapRetryAdvice(selfServeSignupMode) {
2461
2670
  return selfServeSignupMode
2462
- ? "Re-run install-bridge and choose the email option — your previous attempt will resume."
2463
- : "Re-run install-bridge with the same bootstrap invite — the redemption will replay and " +
2671
+ ? "Re-run install and choose the email option — your previous attempt will resume."
2672
+ : "Re-run install with the same bootstrap invite — the redemption will replay and " +
2464
2673
  "return the same key.";
2465
2674
  }
2466
2675
  /**
@@ -2509,6 +2718,69 @@ export async function resolveCredentialConflictConsent(prepared, ctx) {
2509
2718
  ctx.grantConsent();
2510
2719
  return ctx.retry();
2511
2720
  }
2721
+ /**
2722
+ * The secret-free install-state file. Gitignored before it is written, so it is
2723
+ * named here once and fed to BOTH the gitignore call and the commit-notice
2724
+ * exclusion set — a single definition site keeps the two from drifting apart.
2725
+ */
2726
+ export const INSTALL_BRIDGE_INSTALL_STATE_PATH = ".bridge/install-state.json";
2727
+ /** Heading printed above the commit-your-assets list. Shared with the tests. */
2728
+ export const INSTALL_BRIDGE_COMMIT_ASSETS_HEADING = "Commit these generated files so your team inherits the same Bridge setup:";
2729
+ /**
2730
+ * Format the commit-your-assets notice (BAPI-708, Part C).
2731
+ *
2732
+ * THIS IS A CORRECTNESS BOUNDARY, NOT COPY. Several files this install writes are
2733
+ * deliberately gitignored because they can carry a real `BAPI_API_KEY` — the
2734
+ * per-host MCP configs above all — and `.bridge/install-state.json` is gitignored
2735
+ * too. A generic "commit your Bridge assets" is exactly how a live key reaches a
2736
+ * public remote. So the notice is built by SUBTRACTION as well as selection: every
2737
+ * path this run handed to `ensureGitignoredShared` is removed from the candidate
2738
+ * list, which means the safe set cannot silently acquire a gitignored target if
2739
+ * the host-target registry grows a new entry later.
2740
+ *
2741
+ * SECRET DISCIPLINE: parameters are project-relative scaffold paths only. The
2742
+ * credential-store path is never passed in — it lives outside the repository and
2743
+ * holds the key, so it is excluded by never being a candidate at all, not by
2744
+ * filtering.
2745
+ *
2746
+ * Returns `""` when nothing survives, so a caller can skip an empty notice.
2747
+ */
2748
+ export function buildCommitGeneratedAssetsNotice(safePaths, gitignoredPaths) {
2749
+ // POSIX-normalized, trailing slash preserved (it marks a directory), so
2750
+ // "./a/b" and "a//b" compare equal to "a/b" on both sides of the exclusion.
2751
+ const normalize = (value) => {
2752
+ const trailingSlash = /\/+$/.test(value.trim());
2753
+ const collapsed = value.trim().replace(/\\/g, "/").replace(/\/+/g, "/").replace(/^\.\//, "").replace(/\/+$/, "");
2754
+ return trailingSlash ? `${collapsed}/` : collapsed;
2755
+ };
2756
+ const excluded = new Set();
2757
+ for (const raw of gitignoredPaths) {
2758
+ const normalized = normalize(raw);
2759
+ if (normalized.length === 0)
2760
+ continue;
2761
+ // Match a gitignored file whether the candidate names it with or without a
2762
+ // trailing slash, so a registry entry can never slip through on formatting.
2763
+ excluded.add(normalized);
2764
+ excluded.add(normalized.replace(/\/+$/, ""));
2765
+ excluded.add(`${normalized.replace(/\/+$/, "")}/`);
2766
+ }
2767
+ const seen = new Set();
2768
+ const lines = [];
2769
+ for (const raw of safePaths) {
2770
+ const normalized = normalize(raw);
2771
+ if (normalized.length === 0)
2772
+ continue;
2773
+ if (excluded.has(normalized))
2774
+ continue;
2775
+ if (seen.has(normalized))
2776
+ continue;
2777
+ seen.add(normalized);
2778
+ lines.push(` ${normalized}`);
2779
+ }
2780
+ if (lines.length === 0)
2781
+ return "";
2782
+ return [INSTALL_BRIDGE_COMMIT_ASSETS_HEADING, ...lines].join("\n");
2783
+ }
2512
2784
  /**
2513
2785
  * Render the --dry-run preview lines. Every secret is ALWAYS redacted — the
2514
2786
  * spawnCommand, config preview, and (in bootstrap-invite mode) the exchange body
@@ -2525,8 +2797,8 @@ export function buildDryRunPreview(plan) {
2525
2797
  // truthful for every outcome — but ONLY `resolved` may claim the name came back
2526
2798
  // from the server (BAPI-687, C-4).
2527
2799
  plan.serverRepoResolutionStatus !== undefined
2528
- ? "install-bridge --dry-run (one read-only repository-resolution GET may already have occurred; no writes, no state-changing requests, no spawns)"
2529
- : "install-bridge --dry-run (no writes, no network, no spawns)",
2800
+ ? "install --dry-run (one read-only repository-resolution GET may already have occurred; no writes, no state-changing requests, no spawns)"
2801
+ : "install --dry-run (no writes, no network, no spawns)",
2530
2802
  `Repo name: ${plan.repoName}${plan.serverRepoResolutionStatus === "resolved"
2531
2803
  ? " (resolved server-side from your API key)"
2532
2804
  : ""}`,
@@ -2546,9 +2818,25 @@ export function buildDryRunPreview(plan) {
2546
2818
  `Step 3b — pre-warm the version-pinned launcher bucket (fail-open, env sanitized — BAPI_API_KEY removed): ${plan.prewarmCommand}`,
2547
2819
  MCP_TIMEOUT_GUIDANCE,
2548
2820
  `Step 4 — persist routing credential: target ${plan.credentialTarget} at ${plan.credentialStorePath}`,
2821
+ ...buildCommitAssetsPreviewLines(plan),
2549
2822
  ...buildLaunchStepPreview(plan),
2550
2823
  ];
2551
2824
  }
2825
+ /**
2826
+ * The commit-assets notice as preview lines (BAPI-708, Part C). A `--dry-run`
2827
+ * returns before the live emission point, and this module's convention is that a
2828
+ * preview never advertises different behavior — so the same notice, built from the
2829
+ * same resolved manifest, appears here too. The gitignored exclusion set is the
2830
+ * plan's own `configTargets` plus the install-state file, exactly as the live path
2831
+ * computes it.
2832
+ */
2833
+ function buildCommitAssetsPreviewLines(plan) {
2834
+ const notice = buildCommitGeneratedAssetsNotice(plan.commitSafeAssets ?? [], [
2835
+ ...plan.configTargets,
2836
+ INSTALL_BRIDGE_INSTALL_STATE_PATH,
2837
+ ]);
2838
+ return notice.length > 0 ? ["", notice] : [];
2839
+ }
2552
2840
  /**
2553
2841
  * One-line description of the planned launch agent for the dry-run header (BAPI-657).
2554
2842
  * Never claims an unconditional Claude spawn: it reflects the discriminated launch
@@ -2589,8 +2877,10 @@ function buildLaunchStepPreview(plan) {
2589
2877
  "Step 4b — optional GitHub connect (SKIPPED in --dry-run): read GitHub's configured state",
2590
2878
  " via the install manifest and, only when it is unconfigured and the terminal is",
2591
2879
  ` interactive, offer '${INSTALL_BRIDGE_GITHUB_CONNECT_PROMPT.replace(/:\s*$/, "")}' before the agent session starts.`,
2592
- " It installs the Bridge GitHub App so pull requests and code review work, opens",
2593
- " github.com in your browser, and shares no GitHub credential with Bridge; it",
2880
+ // BAPI-708: the purpose sentence is kept on ONE line in both copies so the
2881
+ // no-drift test can compare the whole phrase rather than a wrapped fragment.
2882
+ " It installs the Bridge GitHub App so you can implement and review tickets with Bridge.",
2883
+ " It opens github.com in your browser, and shares no GitHub credential with Bridge; it",
2594
2884
  ` defaults to No and can be run later instead: ${INSTALL_BRIDGE_CONNECT_GITHUB_COMMAND}`,
2595
2885
  ];
2596
2886
  if (plan.launch.kind === "spawn") {
@@ -2634,8 +2924,8 @@ function buildBootstrapDryRunPreview(plan) {
2634
2924
  // created, no mint request is sent, no email leaves the machine. The email and
2635
2925
  // any synthetic token are deliberately absent from this output.
2636
2926
  const header = plan.selfServeSignup
2637
- ? "install-bridge --email --dry-run (no writes, no network, no spawns, no account created, no secret generated)"
2638
- : "install-bridge --invite --dry-run (no writes, no network, no spawns, no secret generated)";
2927
+ ? "install --email --dry-run (no writes, no network, no spawns, no account created, no secret generated)"
2928
+ : "install --invite --dry-run (no writes, no network, no spawns, no secret generated)";
2639
2929
  const repoLine = plan.selfServeSignup
2640
2930
  ? `Repo name: ${plan.repoName} (created by the self-serve exchange; globally unique)`
2641
2931
  : `Repo name: ${plan.repoName} (created by the exchange; globally unique)`;
@@ -2675,6 +2965,7 @@ function buildBootstrapDryRunPreview(plan) {
2675
2965
  `Step 3b — pre-warm the version-pinned launcher bucket (fail-open, env sanitized — BAPI_API_KEY / BAPI_INVITE removed): ${plan.prewarmCommand}`,
2676
2966
  MCP_TIMEOUT_GUIDANCE,
2677
2967
  `Step 4 — promote ${pendingTarget} → ${plan.credentialTarget} at ${plan.credentialStorePath} (only after the exchange succeeds)`,
2968
+ ...buildCommitAssetsPreviewLines(plan),
2678
2969
  ...buildLaunchStepPreview(plan),
2679
2970
  ];
2680
2971
  }
@@ -2724,6 +3015,28 @@ export function manualEditorNames(editors) {
2724
3015
  export function formatNeedKeyCredentialStoreLine(repoName, credentialStorePath) {
2725
3016
  return `Your API key is stored at ${credentialStorePath} under "bapi:${repoName}" — copy BAPI_API_KEY from there.`;
2726
3017
  }
3018
+ /**
3019
+ * Where THIS run's routing credential landed (BAPI-708, B-b) — the single Step 4
3020
+ * result line, printed on the have-key and need-key arms alike.
3021
+ *
3022
+ * It replaces both `stored routing credential for … at …` lines, and it is also
3023
+ * what makes Part B-a honest: `.mcp.json` is an MCP SERVER REGISTRATION, not a
3024
+ * credentials file, so the one place a credential is reported must name the
3025
+ * credential store instead.
3026
+ *
3027
+ * Distinct from {@link formatNeedKeyCredentialStoreLine}, which stays for the
3028
+ * MANUAL-EDITOR context — its "copy BAPI_API_KEY from there" instruction only
3029
+ * makes sense when the user has a global config to paste into.
3030
+ *
3031
+ * SECRET DISCIPLINE (hard constraint, same as its sibling): this formatter accepts
3032
+ * NO key material. Its parameters are a repo name and a filesystem path, both
3033
+ * printable; it must never be handed `apiKey`, `inviteToken`, `keySecret`, a signup
3034
+ * email, or a credential record. The arity is pinned by test precisely so a third
3035
+ * parameter cannot be added without that test failing.
3036
+ */
3037
+ export function formatProjectCredentialStoreLine(repoName, credentialStorePath) {
3038
+ return `We wrote your project credentials to: ${credentialStorePath} (target "bapi:${repoName}")`;
3039
+ }
2727
3040
  /**
2728
3041
  * Build the manual-instruction block for the DETECTED global-config editors only
2729
3042
  * (key always redacted). Returns `null` when neither Windsurf nor Codex is
@@ -2868,7 +3181,7 @@ function readInstallBridgeDebugValue(read) {
2868
3181
  */
2869
3182
  export function buildInstallBridgeFailureLines(error, context) {
2870
3183
  const lines = [
2871
- `Error: install-bridge failed at: ${context.step}`,
3184
+ `Error: install failed at: ${context.step}`,
2872
3185
  ` cause: ${classifyInstallBridgeFailure(error)}`,
2873
3186
  ` ${INSTALL_BRIDGE_DOCTOR_POINTER}`,
2874
3187
  ];
@@ -2935,6 +3248,14 @@ export function resolveInstallBridgeBaseUrl(env) {
2935
3248
  * Every printed Step 1–5 label, defined once. `runInstallBridgeCli` uses each
2936
3249
  * constant BOTH when logging the step AND when recording the current step, so a
2937
3250
  * failure can never be attributed to a label the user did not just see.
3251
+ *
3252
+ * BAPI-708 (Part D) removed the separate `promoteCredential` entry: promotion is
3253
+ * not a step the user does anything about, and printing it as its own numbered
3254
+ * step told them the bootstrap two-phase commit exists without giving them a way
3255
+ * to act on it. Both credential arms now log `persistCredential`. Attribution is
3256
+ * NOT lost — the promotion-failure fatal is highly specific about what failed and
3257
+ * what state the user is left in. The step COUNT is unchanged at 5; nothing is
3258
+ * renumbered.
2938
3259
  */
2939
3260
  export const INSTALL_BRIDGE_STEP_LABELS = {
2940
3261
  scaffold: "Step 1/5 — scaffolding project (commands, agents, pipelines, config placeholders)…",
@@ -2942,7 +3263,6 @@ export const INSTALL_BRIDGE_STEP_LABELS = {
2942
3263
  inviteRedeem: "Step 2/5 — redeeming the bootstrap invite…",
2943
3264
  verifyConnectivity: "Step 2/5 — verifying connectivity…",
2944
3265
  writeHostConfigs: "Step 3/5 — writing per-host MCP config…",
2945
- promoteCredential: "Step 4/5 — promoting the bootstrap credential…",
2946
3266
  persistCredential: "Step 4/5 — persisting routing credential…",
2947
3267
  };
2948
3268
  /**
@@ -2974,7 +3294,7 @@ export const INSTALL_BRIDGE_NO_GIT_WARNING = "This doesn't look like a project r
2974
3294
  /** Default-No confirmation shown only on a TTY when `.git` is absent. */
2975
3295
  export const INSTALL_BRIDGE_NO_GIT_PROMPT = `${INSTALL_BRIDGE_NO_GIT_WARNING} — continue? [y/N]: `;
2976
3296
  /** Printed on decline. Paired with exit code 0 — a cancel is not an error. */
2977
- export const INSTALL_BRIDGE_NO_GIT_ABORT = "Aborted — run install-bridge from your project root, or re-run and confirm to continue.";
3297
+ export const INSTALL_BRIDGE_NO_GIT_ABORT = "Aborted — run install from your project root, or re-run and confirm to continue.";
2978
3298
  /** The non-TTY form: same finding, stated as a warning, then the run continues. */
2979
3299
  export const INSTALL_BRIDGE_NO_GIT_NONINTERACTIVE_WARNING = `Warning: ${INSTALL_BRIDGE_NO_GIT_WARNING} — continuing (non-interactive).`;
2980
3300
  /**
@@ -3040,6 +3360,63 @@ export async function runInstallBridgeCli(argv, overrides = {}) {
3040
3360
  // A rejected value cannot be interpolated into guidance, so usage falls back to
3041
3361
  // the documented default — the run is about to fail on it anyway.
3042
3362
  const usageBaseUrl = baseUrlResult.ok ? baseUrlResult.value : DEFAULT_BAPI_BASE_URL;
3363
+ // ---- BAPI-679: nested `install-bridge conductor` -----------------------
3364
+ // Intercepted HERE, before `parseInstallBridgeArgs`, because that parser
3365
+ // rejects ALL positional arguments — a bare `conductor` token would otherwise
3366
+ // be an argument error. Deliberately after the deps merge and the base-URL
3367
+ // resolution above, so the nested command inherits the same final merged deps
3368
+ // (including a test's `fetch` override) and the same normalized base URL.
3369
+ // Note this is `install-bridge conductor`; the bare top-level `conductor`
3370
+ // subcommand still routes to `runConductorCli` (the local event ledger) and is
3371
+ // untouched.
3372
+ if (argv[0] === "conductor") {
3373
+ if (!baseUrlResult.ok) {
3374
+ errorLog(baseUrlResult.error);
3375
+ return 1;
3376
+ }
3377
+ const conductorBaseUrl = baseUrlResult.value;
3378
+ const conductorDeps = {
3379
+ env: deps.env,
3380
+ cwd: deps.cwd,
3381
+ log: deps.log,
3382
+ errorLog: deps.errorLog,
3383
+ promptLine: deps.promptLine,
3384
+ resolveBaseUrl: () => ({ ok: true, value: conductorBaseUrl }),
3385
+ resolveAccess: (repoName, resolvedBaseUrl) => (deps.conductorResolveAccess ??
3386
+ (async () => ({ ok: false, error: "conductor access seam unavailable" })))(repoName, resolvedBaseUrl, deps),
3387
+ fetchJson: deps.fetch,
3388
+ postJson: deps.fetch,
3389
+ runDoctor: (params) => (deps.conductorRunDoctor ??
3390
+ (async () => ({
3391
+ legacyConductor: null,
3392
+ legacyExecutorPreflight: null,
3393
+ legacyInstallChecks: null,
3394
+ executorService: null,
3395
+ readiness: null,
3396
+ sections: [
3397
+ {
3398
+ id: "conductor-doctor",
3399
+ label: "Conductor install doctor",
3400
+ status: "fatal",
3401
+ detail: "the unified doctor seam is unavailable",
3402
+ },
3403
+ ],
3404
+ })))({
3405
+ ...params,
3406
+ fetch: deps.fetch,
3407
+ cwd: deps.cwd,
3408
+ env: deps.env,
3409
+ readFile: deps.readFile,
3410
+ }),
3411
+ writeWorkflow: (cwd, content, options) => (deps.conductorWriteWorkflow ??
3412
+ (async () => ({ ok: false, error: "workflow seam unavailable" })))(cwd, content, options, deps),
3413
+ recordArtifact: (cwd, relPath) => (deps.conductorRecordArtifact ??
3414
+ (async () => ({ ok: false, error: "install-state seam unavailable" })))(cwd, relPath, deps),
3415
+ runSetupEpicDryRun: (setupArgv) => (deps.conductorRunSetupEpicDryRun ??
3416
+ (async () => ({ exitCode: 1 })))(setupArgv),
3417
+ };
3418
+ return runInstallBridgeConductorCli(argv.slice(1), conductorDeps);
3419
+ }
3043
3420
  const parsed = parseInstallBridgeArgs(argv);
3044
3421
  if (parsed.status === "help") {
3045
3422
  log(getInstallBridgeUsage(usageBaseUrl));
@@ -3109,6 +3486,10 @@ export async function runInstallBridgeCli(argv, overrides = {}) {
3109
3486
  return 1;
3110
3487
  }
3111
3488
  const branch = branchResult.branch;
3489
+ // BAPI-708 (A-5): provenance comes from the branch resolver and NOWHERE else. It
3490
+ // is deliberately NOT re-derived from `argv`/`env` here — the resolver owns that
3491
+ // logic, and a second derivation is exactly the drift this ticket removes.
3492
+ const credentialIntent = branchResult.credentialIntent;
3112
3493
  // Mutable (BAPI-661): an ordinary have-key credential that classifies as a
3113
3494
  // bootstrap invite (see the have-key arm below) flips `bootstrapInviteMode` to
3114
3495
  // true AFTER branch selection, before repository-name resolution — everything
@@ -3170,13 +3551,23 @@ export async function runInstallBridgeCli(argv, overrides = {}) {
3170
3551
  if (classifyEnteredCredential(inviteToken) !== "invite" &&
3171
3552
  isHighEntropyApiKeyShape(inviteToken)) {
3172
3553
  log(INSTALL_BRIDGE_KEY_ON_INVITE_PATH_NOTICE);
3173
- const switchToKeyPath = await requestCredentialRouteSwitchConfirmation(deps, INSTALL_BRIDGE_KEY_ON_INVITE_PATH_PROMPT);
3174
- if (!switchToKeyPath) {
3175
- // Decline, an unreadable prompt, and a non-TTY run all land here nothing
3176
- // has been prompted for, written, or sent, so the exit costs the user only
3177
- // the re-run named in the guidance.
3178
- errorLog(INSTALL_BRIDGE_KEY_ON_INVITE_PATH_DECLINED);
3179
- return 1;
3554
+ // BAPI-708 (A-4): DO NOT UNIFY THESE TWO BRANCHES. On an INFERRED route the
3555
+ // announcement above is purely informational — the user never declared the
3556
+ // invite path, so there is no stated intent for the value to contradict and a
3557
+ // confirmation would ask them to ratify a decision they never made. On a
3558
+ // DECLARED route the confirmation is byte-for-byte BAPI-687 behavior,
3559
+ // including the intentional non-TTY asymmetry: an API-key-shaped value on the
3560
+ // declared invite path exits non-zero rather than silently redirecting a
3561
+ // script away from the project-creating run it asked for.
3562
+ if (credentialIntent === "declared") {
3563
+ const switchToKeyPath = await requestCredentialRouteSwitchConfirmation(deps, INSTALL_BRIDGE_KEY_ON_INVITE_PATH_PROMPT);
3564
+ if (!switchToKeyPath) {
3565
+ // Decline, an unreadable prompt, and a non-TTY run all land here — nothing
3566
+ // has been prompted for, written, or sent, so the exit costs the user only
3567
+ // the re-run named in the guidance.
3568
+ errorLog(INSTALL_BRIDGE_KEY_ON_INVITE_PATH_DECLINED);
3569
+ return 1;
3570
+ }
3180
3571
  }
3181
3572
  apiKey = inviteToken;
3182
3573
  inviteToken = "";
@@ -3200,20 +3591,26 @@ export async function runInstallBridgeCli(argv, overrides = {}) {
3200
3591
  // bootstrap-invite protocol verbatim.
3201
3592
  if (classifyEnteredCredential(keyResult.value) === "invite") {
3202
3593
  // BAPI-687 (B-2): the switch survives, the SILENCE does not. The user is told
3203
- // which credential type was detected and what it will do, and on a TTY is
3204
- // asked to confirm before the branch changes. A non-TTY run keeps today's
3205
- // automatic switch — a script must not gain a prompt — but still prints the
3206
- // notice, so the transcript explains itself.
3594
+ // which credential type was detected and what it will do.
3207
3595
  log(INSTALL_BRIDGE_INVITE_ON_KEY_PATH_NOTICE);
3208
- if (deps.isTTY && deps.promptLine) {
3209
- const switchToInvite = await requestCredentialRouteSwitchConfirmation(deps, INSTALL_BRIDGE_INVITE_ON_KEY_PATH_PROMPT);
3210
- if (!switchToInvite) {
3211
- errorLog(INSTALL_BRIDGE_INVITE_ON_KEY_PATH_DECLINED);
3212
- return 1;
3596
+ // BAPI-708 (A-4): DO NOT UNIFY THESE TWO BRANCHES — see the matching comment
3597
+ // on the invite-path arm above. INFERRED means the user answered "yes, I have
3598
+ // a token" and pasted it; they never claimed it was an API key, so the notice
3599
+ // is an announcement and the run simply proceeds. DECLARED keeps BAPI-687
3600
+ // verbatim, asymmetry included: a TTY gets the default-No confirmation, while
3601
+ // a non-TTY still auto-switches (a script that worked yesterday must not gain
3602
+ // a prompt) and prints the notice so the transcript explains itself.
3603
+ if (credentialIntent === "declared") {
3604
+ if (deps.isTTY && deps.promptLine) {
3605
+ const switchToInvite = await requestCredentialRouteSwitchConfirmation(deps, INSTALL_BRIDGE_INVITE_ON_KEY_PATH_PROMPT);
3606
+ if (!switchToInvite) {
3607
+ errorLog(INSTALL_BRIDGE_INVITE_ON_KEY_PATH_DECLINED);
3608
+ return 1;
3609
+ }
3610
+ }
3611
+ else {
3612
+ log(INSTALL_BRIDGE_INVITE_ON_KEY_PATH_AUTO_NOTICE);
3213
3613
  }
3214
- }
3215
- else {
3216
- log(INSTALL_BRIDGE_INVITE_ON_KEY_PATH_AUTO_NOTICE);
3217
3614
  }
3218
3615
  inviteToken = keyResult.value.trim();
3219
3616
  apiKey = "";
@@ -3224,6 +3621,16 @@ export async function runInstallBridgeCli(argv, overrides = {}) {
3224
3621
  apiKeySource = undefined;
3225
3622
  }
3226
3623
  else {
3624
+ // BAPI-708 (A-4): on the INFERRED path the route decision is announced even
3625
+ // when nothing switches, so a bare run always says which credential type it
3626
+ // detected and where that leads. Reuses the existing notice because it names
3627
+ // only the credential TYPE and the destination ROUTE — never the value.
3628
+ // Guarded on the POSITIVE shape test: a value matching neither shape is not
3629
+ // reclassified AND not announced; it stays on this route and fails here
3630
+ // exactly as it does today. Declared paths announce nothing new.
3631
+ if (credentialIntent === "inferred" && isHighEntropyApiKeyShape(keyResult.value.trim())) {
3632
+ log(INSTALL_BRIDGE_KEY_ON_INVITE_PATH_NOTICE);
3633
+ }
3227
3634
  apiKey = keyResult.value;
3228
3635
  apiKeySource = keyResult.source;
3229
3636
  }
@@ -3343,12 +3750,24 @@ export async function runInstallBridgeCli(argv, overrides = {}) {
3343
3750
  else {
3344
3751
  planLaunch = { kind: "manual", reason: launchDecision.reason };
3345
3752
  }
3753
+ // BAPI-708 (Part C): resolved from THIS invocation's cwd + env, so the notice
3754
+ // names only what this run actually scaffolds — `.cursor/commands/` only when
3755
+ // Cursor was detected, `.github/agents/` only when VS Code was, and the pipelines
3756
+ // / instructions entries under whatever `BAPI_PIPELINES_DIR` resolved to. It is
3757
+ // deliberately independent of which MCP HOSTS were selected: a scaffolded command
3758
+ // directory is committed whether or not its editor got an MCP registration.
3759
+ const scaffoldAssets = await resolveInitScaffoldAssets({
3760
+ cwd: deps.cwd,
3761
+ env: deps.env,
3762
+ stat: deps.stat,
3763
+ });
3346
3764
  const plan = {
3347
3765
  repoName,
3348
3766
  baseUrl,
3349
3767
  docsDir,
3350
3768
  launch: planLaunch,
3351
3769
  configTargets: targets.map((t) => t.relPath),
3770
+ commitSafeAssets: scaffoldAssets.commitSafePaths,
3352
3771
  manualEditors: manualEditorNames(manualEditors),
3353
3772
  credentialTarget: `bapi:${repoName}`,
3354
3773
  credentialStorePath,
@@ -3585,7 +4004,11 @@ export async function runInstallBridgeCli(argv, overrides = {}) {
3585
4004
  }
3586
4005
  keySecret = prep.keySecret;
3587
4006
  reusedPendingSecret = prep.reused;
3588
- log(` saved the pending credential for ${prep.target} (fsynced before the exchange)`);
4007
+ // BAPI-708 (Part D): the pending write is NOT narrated on the happy path.
4008
+ // The target name and the fsync ordering are implementation detail the user
4009
+ // cannot act on; they remain fully described in --dry-run and in the failure
4010
+ // messages, which name the pending record and what it means for a retry. The
4011
+ // MECHANISM above is untouched — see the LOAD-BEARING ORDERING block below.
3589
4012
  return true;
3590
4013
  };
3591
4014
  // ---- Self-serve signup (BAPI-618): mint the invite token from the email ----
@@ -3670,7 +4093,8 @@ export async function runInstallBridgeCli(argv, overrides = {}) {
3670
4093
  }
3671
4094
  keySecret = prepared.keySecret;
3672
4095
  reusedPendingSecret = prepared.reused;
3673
- log(` saved the pending credential for ${prepared.target} (fsynced before the exchange)`);
4096
+ // BAPI-708 (Part D): happy-path narration removed; the mechanism above is
4097
+ // unchanged. See the matching comment on the self-serve arm.
3674
4098
  }
3675
4099
  // The exchange REPLACES the pre-flight ping: in invite mode there is no key to
3676
4100
  // ping with — this call is what mints it.
@@ -3727,7 +4151,7 @@ export async function runInstallBridgeCli(argv, overrides = {}) {
3727
4151
  !selfServeRefreshed) {
3728
4152
  if (!deps.isTTY || !deps.promptLine) {
3729
4153
  return fatal(BOOTSTRAP_SELF_SERVE_EXPIRED_MESSAGE, `The saved attempt at ${getBootstrapPendingTarget(repoName)} in ${credentialStorePath} ` +
3730
- "has been left untouched. Re-run install-bridge on an interactive terminal to " +
4154
+ "has been left untouched. Re-run install on an interactive terminal to " +
3731
4155
  "discard it and start a fresh signup.");
3732
4156
  }
3733
4157
  errorLog(BOOTSTRAP_SELF_SERVE_EXPIRED_MESSAGE);
@@ -3860,8 +4284,14 @@ export async function runInstallBridgeCli(argv, overrides = {}) {
3860
4284
  trackedState.set(target.relPath, await isTrackedProjectConfig(deps, target.relPath));
3861
4285
  }
3862
4286
  const writeResult = await writeHostConfigs(deps, targets, { real: entry, secretFree: secretFreeEntry }, trackedState, { repoName, credentialStorePath });
4287
+ // BAPI-708 (B-a): these files are the MCP SERVER REGISTRATION (BAPI_REPO_NAME,
4288
+ // BAPI_BASE_URL, BAPI_DOCS_DIR, BAPI_PROJECT_ROOT, and the version-pinned npx
4289
+ // launcher) — calling them a credentials file was simply inaccurate. Where the
4290
+ // credential actually landed is reported once, at Step 4.
3863
4291
  for (const { relPath, mode } of writeResult.written) {
3864
- log(mode === "secret-free" ? ` wrote ${relPath} (secret-free — key resolved at runtime)` : ` wrote ${relPath}`);
4292
+ log(mode === "secret-free"
4293
+ ? ` registered the Bridge MCP server in ${relPath} (secret-free — key resolved at runtime)`
4294
+ : ` registered the Bridge MCP server in ${relPath}`);
3865
4295
  }
3866
4296
  for (const { relPath } of writeResult.skipped) {
3867
4297
  log(` skipped ${relPath} — existing config could not be parsed safely; left untouched`);
@@ -3897,16 +4327,16 @@ export async function runInstallBridgeCli(argv, overrides = {}) {
3897
4327
  log(" Note: Claude Code uses the project .mcp.json while GitHub Copilot CLI uses only its " +
3898
4328
  "global ~/.copilot/mcp-config.json — the two are configured separately.");
3899
4329
  }
3900
- // BAPI-635 (Step 7): Claude trust reminder a written project MCP config is
3901
- // not a live connection until approved in Claude Code's trust dialog.
3902
- if (selectedPlatforms.includes("claude-code")) {
3903
- log(" Claude Code: the project MCP server is pending approval in Claude Code's trust dialog; " +
3904
- "restart or reload an already-running session for it to take effect.");
3905
- }
4330
+ // BAPI-708 (B-c): the Claude trust reminder that used to print here was
4331
+ // RELOCATED, not dropped the reload requirement is genuinely actionable (the
4332
+ // server does not work until the host reloads), but at Step 3 it duplicated the
4333
+ // spawn handoff. It now survives on EVERY terminal path: the spawn handoff
4334
+ // already carries it, and `buildManualInstallBridgeContinuation("configured")`
4335
+ // carries it for the declined, non-TTY, and failed-spawn paths.
3906
4336
  // BAPI-635 (Step 8 + Step 3): persist the secret-free install state, ignoring
3907
4337
  // it before the write. Project-local paths only; global paths are never in it.
3908
4338
  try {
3909
- await ensureGitignoredShared(deps.cwd, ".bridge/install-state.json", gitignoreDeps);
4339
+ await ensureGitignoredShared(deps.cwd, INSTALL_BRIDGE_INSTALL_STATE_PATH, gitignoreDeps);
3910
4340
  // writeMcpInstallState catches its own I/O errors and returns { ok: false }
3911
4341
  // (it does NOT throw), so the failure warning must inspect the return value —
3912
4342
  // a try/catch alone would silently swallow a real persistence failure.
@@ -3939,7 +4369,11 @@ export async function runInstallBridgeCli(argv, overrides = {}) {
3939
4369
  // resolve. Promotion removes the pending record and writes the credential in one
3940
4370
  // durable replacement; on failure the pending record survives so a re-run replays
3941
4371
  // the same redemption. The agent session is NOT spawned until this succeeds.
3942
- currentStep = INSTALL_BRIDGE_STEP_LABELS.promoteCredential;
4372
+ // BAPI-708 (Part D): the SHARED Step 4 label — identical on both arms, so a
4373
+ // successful run prints exactly one Step 4 line and never narrates promotion
4374
+ // as a step of its own. Still assigned before the log, preserving the
4375
+ // log == record invariant the failure attribution depends on.
4376
+ currentStep = INSTALL_BRIDGE_STEP_LABELS.persistCredential;
3943
4377
  log(currentStep);
3944
4378
  const promoted = await deps.promoteBootstrapPending({ repoName, inviteFingerprint, allowOverwriteExistingCredential: overwriteConsent }, credentialWriteDeps);
3945
4379
  if (!promoted.ok) {
@@ -3949,7 +4383,7 @@ export async function runInstallBridgeCli(argv, overrides = {}) {
3949
4383
  `(${promoted.kind}). ${promoted.error} Your key is still saved locally as a pending ` +
3950
4384
  `record. ${buildBootstrapRetryAdvice(selfServeSignupMode)}`);
3951
4385
  }
3952
- log(` stored routing credential for ${promoted.target} at ${promoted.path}`);
4386
+ log(formatProjectCredentialStoreLine(repoName, promoted.path));
3953
4387
  }
3954
4388
  else {
3955
4389
  // ---- persist routing credential (non-blocking / fail-open) ----
@@ -3958,13 +4392,13 @@ export async function runInstallBridgeCli(argv, overrides = {}) {
3958
4392
  try {
3959
4393
  const result = await deps.upsertCredential(repoName, apiKey, credentialWriteDeps);
3960
4394
  if (result.ok) {
3961
- log(` stored routing credential for ${result.target} at ${result.path}`);
4395
+ log(formatProjectCredentialStoreLine(repoName, result.path));
3962
4396
  }
3963
4397
  else {
3964
4398
  log(` warning: could not persist the routing credential (${result.kind}). ` +
3965
4399
  `start-tickets model routing may not resolve the key for bapi:${repoName} ` +
3966
4400
  "and will fail open to the premium/Opus tier (the most expensive) — " +
3967
- "set BAPI_API_KEY in the shell or re-run install-bridge, then verify with " +
4401
+ "set BAPI_API_KEY in the shell or re-run install, then verify with " +
3968
4402
  "'npx -y @bridge_gpt/mcp-server doctor'.");
3969
4403
  }
3970
4404
  }
@@ -3977,6 +4411,21 @@ export async function runInstallBridgeCli(argv, overrides = {}) {
3977
4411
  "'npx -y @bridge_gpt/mcp-server doctor'.");
3978
4412
  }
3979
4413
  }
4414
+ // ---- commit-your-assets notice (BAPI-708, Part C) ----
4415
+ // Emitted AFTER the credential step and BEFORE the GitHub offer and the Step 5
4416
+ // consent/spawn block, so every terminal path — spawn, decline, non-TTY, and
4417
+ // spawn-failure alike — receives it exactly once. The exclusion set is computed
4418
+ // from what this run actually gitignored (the selected project host targets and
4419
+ // the install-state file), not a hand-copied literal, so a future host-registry
4420
+ // entry is excluded automatically. `credentialStorePath` is never a candidate.
4421
+ const commitAssetsNotice = buildCommitGeneratedAssetsNotice(scaffoldAssets.commitSafePaths, [
4422
+ ...targets.map((t) => t.relPath),
4423
+ INSTALL_BRIDGE_INSTALL_STATE_PATH,
4424
+ ]);
4425
+ if (commitAssetsNotice.length > 0) {
4426
+ log("");
4427
+ log(commitAssetsNotice);
4428
+ }
3980
4429
  // ---- optional GitHub connect offer (BAPI-631) ----
3981
4430
  // Placed AFTER the credential is durable (the flow needs a resolvable key) and BEFORE
3982
4431
  // the agent spawn, for two reasons: the spawned session's capability report should
@@ -3992,8 +4441,11 @@ export async function runInstallBridgeCli(argv, overrides = {}) {
3992
4441
  if (prewarmPromise) {
3993
4442
  const prewarm = await prewarmPromise;
3994
4443
  if (prewarm.ok) {
3995
- log(" launcher bucket warmed (the first MCP launch will not pay a cold install).");
3996
- log(` ${MCP_TIMEOUT_GUIDANCE}`);
4444
+ // BAPI-708 (Part B): a SUCCESSFUL pre-warm is silent. Both lines described
4445
+ // an internal optimization that worked and a remedy for a problem that did
4446
+ // not occur. The failure branch below — where the remedy is real — keeps
4447
+ // the MCP_TIMEOUT guidance verbatim, and both dry-run previews still carry
4448
+ // it, so it stays discoverable exactly where it is actionable.
3997
4449
  }
3998
4450
  else {
3999
4451
  // Escalate to a stronger, still secret-free warning when pre-warm didn't
@@ -4035,8 +4487,9 @@ export async function runInstallBridgeCli(argv, overrides = {}) {
4035
4487
  // raw agent binary name — computed once so all three sentences agree.
4036
4488
  const handoffToolLabel = toolLabelForLaunchAgent(finalAgentName);
4037
4489
  log("");
4038
- log(`install-bridge setup steps complete. A fresh ${handoffToolLabel} session is now applying ` +
4039
- "configuration, presenting the concise capability report, and recommending /learn-repository.");
4490
+ log(`install setup steps complete. A fresh ${handoffToolLabel} session is now applying ` +
4491
+ "configuration, presenting the concise capability report, and showing any next step the " +
4492
+ "server provides.");
4040
4493
  // The trust dialog is the single most common place a spawned session stalls:
4041
4494
  // the tab opens, nothing is approved, and the install looks hung.
4042
4495
  log(`In the new tab: approve the workspace and the 'bridge-api' MCP server if ${handoffToolLabel} ` +