@bridge_gpt/mcp-server 0.2.62 → 0.2.66

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 (113) hide show
  1. package/README.md +66 -15
  2. package/build/agent-launchers/claude-executor-adapter.js +145 -20
  3. package/build/agent-launchers/executor-adapter.js +81 -2
  4. package/build/agents.generated.js +10 -1
  5. package/build/bounded-wait.js +32 -1
  6. package/build/bridge-config.js +104 -0
  7. package/build/codex-bridge-profile.js +10 -1
  8. package/build/commands.generated.js +7 -6
  9. package/build/conductor/bridge-api-client.js +565 -32
  10. package/build/conductor/bring-up-facts.js +3 -1
  11. package/build/conductor/doctor.js +23 -14
  12. package/build/conductor/epic-child-continuation.js +351 -0
  13. package/build/conductor/epic-implementer-cli.js +154 -17
  14. package/build/conductor/git-hooks.js +529 -118
  15. package/build/conductor/guided-repair.js +323 -0
  16. package/build/conductor/hook-runtime-qualification.js +340 -0
  17. package/build/conductor/readiness-cli.js +30 -9
  18. package/build/conductor/readiness-sections.js +5 -4
  19. package/build/conductor/readiness.js +45 -12
  20. package/build/conductor/recovery-cli.js +859 -27
  21. package/build/conductor/recovery-operations.js +574 -5
  22. package/build/conductor/repair-contract.js +69 -3
  23. package/build/conductor-bundle-artifacts.js +1 -0
  24. package/build/credential-store.js +52 -13
  25. package/build/diagnostic-text.js +36 -0
  26. package/build/docs.generated.js +3 -2
  27. package/build/doctor.js +147 -21
  28. package/build/drive-epic-key.js +12 -0
  29. package/build/drive-epic.js +1537 -125
  30. package/build/epic-coherence-input-cli.js +137 -0
  31. package/build/epic-coherence-input.js +920 -0
  32. package/build/epic-coherence-input.test-fixtures.js +175 -0
  33. package/build/epic-implementer-bin.js +41 -33
  34. package/build/epic-implementer-bundle-cli.js +14 -1
  35. package/build/epic-launch-diagnostic.js +31 -0
  36. package/build/epic-launch-profile.js +58 -19
  37. package/build/epic-run-status.js +5 -0
  38. package/build/epic-shared/bridge-client.js +46 -4
  39. package/build/executor/cli.js +39 -0
  40. package/build/executor/completion-contract.js +423 -0
  41. package/build/executor/completion-supervision.js +291 -0
  42. package/build/executor/correction-dispatch.js +60 -0
  43. package/build/executor/credentials.js +3 -1
  44. package/build/executor/deps.js +65 -1
  45. package/build/executor/env.js +12 -0
  46. package/build/executor/heartbeat.js +13 -3
  47. package/build/executor/http-client.js +91 -4
  48. package/build/executor/integration-job.js +6 -1
  49. package/build/executor/job-errors.js +61 -4
  50. package/build/executor/job-log-registry.js +340 -73
  51. package/build/executor/job-runner.js +1550 -55
  52. package/build/executor/job-types.js +9 -6
  53. package/build/executor/preflight.js +189 -1
  54. package/build/executor/process-containment.js +756 -0
  55. package/build/executor/process.js +235 -5
  56. package/build/executor/prompt-spec.js +366 -1
  57. package/build/executor/provider-checks.js +69 -0
  58. package/build/executor/rebase-finalization.js +351 -0
  59. package/build/executor/reconcile-overlap-job.js +77 -84
  60. package/build/executor/required-verification.js +313 -0
  61. package/build/executor/required-work-ledger.js +1121 -0
  62. package/build/executor/results.js +142 -0
  63. package/build/executor/resume-pre-spawn.js +87 -4
  64. package/build/executor/runner.js +168 -0
  65. package/build/executor/stale-artifacts.js +5 -4
  66. package/build/executor/test-declaration.js +20 -0
  67. package/build/executor/types.js +13 -0
  68. package/build/executor/watch-cli.js +35 -8
  69. package/build/executor/worker-command.js +70 -34
  70. package/build/executor/worker-finalization.js +168 -26
  71. package/build/executor/worker-guard-hook.js +75 -14
  72. package/build/executor/worker-log.js +137 -70
  73. package/build/executor/worktree-gc.js +30 -6
  74. package/build/executor/worktree.js +13 -0
  75. package/build/executor-launch.js +658 -0
  76. package/build/index.js +594 -1152
  77. package/build/init.js +107 -4
  78. package/build/install-bridge-conductor.js +231 -41
  79. package/build/install-bridge.js +284 -20
  80. package/build/install-doctor.js +172 -30
  81. package/build/install-reexec.js +69 -7
  82. package/build/macos-terminal-permission-guidance.js +16 -0
  83. package/build/merge-pull-request.js +1 -1
  84. package/build/model-thresholds-tool.js +185 -0
  85. package/build/pipelines.generated.js +9 -8
  86. package/build/plan-discovery.js +137 -0
  87. package/build/plan-epic-implementer-eligibility.js +2 -2
  88. package/build/plane/cli.js +37 -3
  89. package/build/plane/member-logs.js +3 -0
  90. package/build/plane/member-roster.js +7 -1
  91. package/build/plane/shutdown.js +59 -1
  92. package/build/plane/status.js +4 -0
  93. package/build/plane/supervisor.js +146 -0
  94. package/build/plane/worker-exit-code.js +2 -0
  95. package/build/project-root.js +99 -0
  96. package/build/readme.generated.js +1 -1
  97. package/build/review-tickets.js +156 -13
  98. package/build/scripts/executor-protocol-contract-driver.js +13 -3
  99. package/build/setup-epic.js +208 -125
  100. package/build/sfcc/setup-status.js +2 -0
  101. package/build/sfcc/tool-wrapper.js +2 -0
  102. package/build/sfcc/tools-configured.js +96 -0
  103. package/build/start-tickets.js +163 -58
  104. package/build/upgrade-cli.js +109 -24
  105. package/build/version.generated.js +3 -3
  106. package/build/windows-npm-entry.js +103 -0
  107. package/build/worker-guard-hook-bin.js +5 -4
  108. package/build/worktree-core.js +6 -5
  109. package/docs/CONDUCTOR.md +116 -2
  110. package/docs/install/mcp-tool-integrations.md +1 -0
  111. package/docs/install/sfcc-integration.md +12 -7
  112. package/package.json +2 -1
  113. package/pipelines/implement-ticket.json +1 -1
@@ -54,6 +54,7 @@
54
54
  * unit-testable on Linux CI without spawning real commands or terminals.
55
55
  */
56
56
  import { execFile } from "child_process";
57
+ import { macosTerminalPermissionGuidance } from "./macos-terminal-permission-guidance.js";
57
58
  import { readFile, writeFile, mkdir, mkdtemp, stat, lstat, symlink, unlink, copyFile, readdir, rm, } from "fs/promises";
58
59
  import os from "node:os";
59
60
  import { createHash } from "node:crypto";
@@ -136,7 +137,7 @@ export function getStartTicketsUsage() {
136
137
  " Cursor with --auto launches headlessly after a bounded readiness check; without --auto it stays interactive for the review gate.",
137
138
  " --workflow implement|review-and-implement Command each spawned worktree runs (default: implement). review-and-implement runs review-ticket then, after a per-ticket halt gate, hands off to a fresh implement-ticket session; --auto applies to both phases.",
138
139
  " --tier cheap|basic|premium Strict manual model-routing override: bypasses the per-ticket difficulty/tier lookup and applies this tier to every ticket exactly as supplied, with no per-session posture adjustment. Claude/Cursor use registry aliases and repository overrides; Codex uses the authenticated Bridge catalog. It is NOT a raw --model alias and never carries an API key or credential. A malformed value requests premium routing when available. Mutually exclusive with --auto-tier.",
139
- " --auto-tier cheap|basic|premium Internal/advanced machine-carried raw backend tier for chained automation handoffs (e.g. /review-and-implement): bypasses the per-ticket lookup like --tier, but remains subject to per-session optimization-posture adjustment (may be downshifted under maximize_subscription). Not the strict-override mechanism — use --tier for a human override. Mutually exclusive with --tier.",
140
+ " --auto-tier cheap|basic|premium Internal/advanced machine-carried raw backend tier for chained automation handoffs (e.g. /review-and-implement): bypasses the per-ticket lookup like --tier, but remains subject to per-session optimization-posture adjustment (may be downshifted under maximize_subscription, unless the server reports configured thresholds). Not the strict-override mechanism — use --tier for a human override. Mutually exclusive with --tier.",
140
141
  " --rounds 1|2 Review round count forwarded to the review phase; review-only, valid only with --workflow review-and-implement",
141
142
  " --terminal terminal|iterm Override the macOS terminal app (default: auto-detect via $TERM_PROGRAM); honored on macOS only",
142
143
  " --dry-run Print intended actions; creates no worktrees and opens no tabs, but DOES resolve model routing read-only (may compute+cache a ticket's difficulty) to preview the --model each tab would use",
@@ -2749,14 +2750,32 @@ export async function fetchCodexModelCatalog(access) {
2749
2750
  return invalid();
2750
2751
  return { ok: true, catalog: { models: models, revision } };
2751
2752
  }
2753
+ /**
2754
+ * True only when stored thresholds decided the tier (`configured` or
2755
+ * `anomaly`). The merged resolver maps every `anomaly` tier to `premium` as a
2756
+ * fail-safe, so a threshold-decided tier is final and must never be downshifted.
2757
+ *
2758
+ * A `source: "fallback"` response is never threshold-decided: the server could
2759
+ * not obtain a difficulty and assumed premium, even though it still reports the
2760
+ * stored thresholds' mode. Treating it as decided would give review tabs a
2761
+ * premium fallback and skip the maximize downshift for a guess.
2762
+ */
2763
+ export function isThresholdDecidedTierResponse(value) {
2764
+ if (value.source === "fallback")
2765
+ return false;
2766
+ return value.thresholdMode === "configured" || value.thresholdMode === "anomaly";
2767
+ }
2752
2768
  /**
2753
2769
  * Fetch the coarse tier for one ticket with a bounded timeout. Never throws to
2754
2770
  * the batch orchestrator: failures/timeouts return a structured warning plus a
2755
2771
  * classified diagnostic that preserves the ticket key in metadata.
2756
2772
  */
2757
- export async function fetchTicketModelTierForStartTickets(access, ticket) {
2773
+ export async function fetchTicketModelTierForStartTickets(access, ticket, fetchOptions = {}) {
2758
2774
  try {
2759
- const url = buildStartTicketsJiraUrl(access.baseUrl, `/tickets/${encodeURIComponent(ticket)}/model-tier`, { repo_name: access.repoName });
2775
+ const params = { repo_name: access.repoName };
2776
+ if (fetchOptions.consumer)
2777
+ params.consumer = fetchOptions.consumer;
2778
+ const url = buildStartTicketsJiraUrl(access.baseUrl, `/tickets/${encodeURIComponent(ticket)}/model-tier`, params);
2760
2779
  const body = await fetchJsonWithTimeout(url, startTicketsGetHeaders(access), TICKET_MODEL_TIER_FETCH_TIMEOUT_MS);
2761
2780
  const obj = (body ?? {});
2762
2781
  const tier = isModelTier(obj.tier) ? obj.tier : null;
@@ -2764,7 +2783,17 @@ export async function fetchTicketModelTierForStartTickets(access, ticket) {
2764
2783
  const source = obj.source === "cached" || obj.source === "computed" || obj.source === "fallback"
2765
2784
  ? obj.source
2766
2785
  : "fallback";
2767
- return { ok: true, value: { difficulty, tier, source } };
2786
+ const value = { difficulty, tier, source };
2787
+ // BAPI-1343: additive fields are attached only when the server sent them,
2788
+ // so an older server's response parses to exactly the pre-feature shape.
2789
+ if (obj.threshold_mode !== undefined && obj.threshold_mode !== null) {
2790
+ value.thresholdMode =
2791
+ obj.threshold_mode === "configured" || obj.threshold_mode === "anomaly" ? obj.threshold_mode : "legacy";
2792
+ }
2793
+ if (obj.consumer === "review" || obj.consumer === "implementation") {
2794
+ value.consumer = obj.consumer;
2795
+ }
2796
+ return { ok: true, value };
2768
2797
  }
2769
2798
  catch (err) {
2770
2799
  return {
@@ -2779,15 +2808,23 @@ export async function fetchTicketModelTierForStartTickets(access, ticket) {
2779
2808
  };
2780
2809
  }
2781
2810
  }
2811
+ /**
2812
+ * Fetch tiers for ticket keys with bounded parallelism (capped at 3). One
2813
+ * ticket's failure never aborts the others; results are keyed by ticket. The
2814
+ * shared bounded fetch behind both `start-tickets` and `/review-tickets`.
2815
+ */
2816
+ export async function fetchTicketModelTiersForKeys(access, keys, maxParallel, fetchOptions = {}) {
2817
+ const limit = Math.min(maxParallel, 3);
2818
+ const results = await runWithConcurrency(keys, limit, (key) => fetchTicketModelTierForStartTickets(access, key, fetchOptions).then((res) => [key, res]));
2819
+ return new Map(results);
2820
+ }
2782
2821
  /**
2783
2822
  * Fetch tiers for the still-spawnable rows with bounded parallelism (capped at
2784
2823
  * 3). One ticket's failure never aborts the others; results are keyed by ticket.
2785
2824
  */
2786
- export async function fetchTicketModelTiersForRows(access, rows, maxParallel, isEligible = (r) => r.status === "created" && !!r.path) {
2825
+ export async function fetchTicketModelTiersForRows(access, rows, maxParallel, isEligible = (r) => r.status === "created" && !!r.path, fetchOptions = {}) {
2787
2826
  const eligible = rows.filter(isEligible);
2788
- const limit = Math.min(maxParallel, 3);
2789
- const results = await runWithConcurrency(eligible, limit, (row) => fetchTicketModelTierForStartTickets(access, row.key).then((res) => [row.key, res]));
2790
- return new Map(results);
2827
+ return fetchTicketModelTiersForKeys(access, eligible.map((row) => row.key), maxParallel, fetchOptions);
2791
2828
  }
2792
2829
  /**
2793
2830
  * Parse a model-list command's stdout into a set of advertised model aliases.
@@ -2874,7 +2911,15 @@ export function applyDefaultModelRoutingMetadata(row, reason, diagnostic) {
2874
2911
  ...(diagnostic ? { modelRoutingDiagnostic: diagnostic } : {}),
2875
2912
  };
2876
2913
  }
2877
- function resolveRoutingAlias(agent, tier, overrides, catalog) {
2914
+ /**
2915
+ * Map a coarse tier to the agent's alias: the Codex authenticated catalog for
2916
+ * `codex`, otherwise the agent registry plus per-repo
2917
+ * `difficulty_model_tier_overrides`. Exported (BAPI-1343) so `/review-tickets`
2918
+ * maps review-threshold tiers exactly as `start-tickets` does, without copying
2919
+ * the registry or catalog rules. The result still needs
2920
+ * {@link validateResolvedModelAliasForAgent} before it reaches a shell.
2921
+ */
2922
+ export function resolveRoutingAlias(agent, tier, overrides, catalog) {
2878
2923
  return agent.name === "codex"
2879
2924
  ? catalog?.models[tier] ?? null
2880
2925
  : resolveModelAlias(agent, tier, overrides);
@@ -2951,8 +2996,12 @@ const isDryRunRoutingEligible = (r) => r.status === "dry-run";
2951
2996
  * routing (posture `not_evaluated_manual_override`) and for `quality`/`speed`;
2952
2997
  * the exact transition token — including the `cheap` floor — under
2953
2998
  * `maximize_subscription`. Total and exhaustive over {@link ModelTier}.
2999
+ * BAPI-1343: `none` whenever a `threshold` marker is supplied, because a
3000
+ * threshold-decided tier is final and is never downshifted.
2954
3001
  */
2955
- function resolveModelRoutingDecisionAdjustment(posture, baseline) {
3002
+ function resolveModelRoutingDecisionAdjustment(posture, baseline, threshold) {
3003
+ if (threshold)
3004
+ return "none";
2956
3005
  if (posture !== "maximize_subscription")
2957
3006
  return "none";
2958
3007
  switch (baseline) {
@@ -2973,7 +3022,24 @@ function resolveModelRoutingDecisionAdjustment(posture, baseline) {
2973
3022
  export function formatModelRoutingDecisionDiagnosticLine(input) {
2974
3023
  return (`[model-routing] source=${input.source} baseline=${input.baseline} ` +
2975
3024
  `posture=${input.posture} adjustment=${input.adjustment} effective=${input.effective} ` +
2976
- `alias=${input.alias ?? "unresolved"}`);
3025
+ `alias=${input.alias ?? "unresolved"}` +
3026
+ (input.threshold ? ` threshold=${input.threshold}` : ""));
3027
+ }
3028
+ /**
3029
+ * BAPI-1343 R-6: the threshold marker for a successfully fetched `/model-tier`
3030
+ * response when — and only when — the maximize downshift must be skipped:
3031
+ * posture is `maximize_subscription` AND the response is threshold-decided
3032
+ * (`configured` or `anomaly`). Every other case returns `undefined`, so the
3033
+ * caller keeps today's exact downshift and text.
3034
+ */
3035
+ function thresholdSkipMarker(posture, tierResult) {
3036
+ if (posture !== "maximize_subscription")
3037
+ return undefined;
3038
+ if (!tierResult || !tierResult.ok)
3039
+ return undefined;
3040
+ if (!isThresholdDecidedTierResponse(tierResult.value))
3041
+ return undefined;
3042
+ return tierResult.value.thresholdMode;
2977
3043
  }
2978
3044
  /**
2979
3045
  * Best-effort emitter: calls the supplied stderr sink once and swallows any
@@ -3160,36 +3226,63 @@ errorLog = (message) => console.error(message)) {
3160
3226
  // chained automation handoff (e.g. `/review-and-implement`'s `--auto-tier`).
3161
3227
  // Routed through the CURRENT session's posture EXACTLY ONCE — unlike the
3162
3228
  // manual override above, this path IS subject to `downshiftModelTier()`
3163
- // under `maximize_subscription` — while skipping ONLY the redundant
3164
- // per-ticket `fetchTicketModelTiersForRows()` lookup. The alias is
3165
- // resolved+validated ONCE (identical for every row), matching the premium
3166
- // fallback / injected-tier hoisting pattern above.
3167
- const effectiveTier = posture === "maximize_subscription" ? downshiftModelTier(autoTier) : autoTier;
3168
- const adjustment = resolveModelRoutingDecisionAdjustment(posture, autoTier);
3169
- const lineageSuffix = posture === "maximize_subscription"
3170
- ? `; auto-tier:${autoTier}->maximize:${effectiveTier}`
3171
- : "";
3172
- const alias = resolveRoutingAlias(agent, effectiveTier, overrides, catalog);
3173
- let resolvedAlias = null;
3174
- let aliasWarning = null;
3175
- if (!alias) {
3176
- aliasWarning = `model routing: no valid alias for tier=${effectiveTier}; using agent default`;
3177
- }
3178
- else {
3179
- const validation = await validateResolvedModelAliasForAgent(deps, agent, alias);
3180
- if (validation.ok) {
3181
- resolvedAlias = validation.alias;
3229
+ // under `maximize_subscription` — while never re-deriving the tier from a
3230
+ // per-ticket lookup. The alias is resolved+validated at most ONCE per
3231
+ // distinct effective tier, matching the premium fallback / injected-tier
3232
+ // hoisting pattern above.
3233
+ //
3234
+ // BAPI-1343 R-6: under `maximize_subscription` ONLY, each eligible row's
3235
+ // `/model-tier` is fetched once (the shared bounded fetch, no `consumer`
3236
+ // parameter) purely to read `threshold_mode`. A threshold-decided
3237
+ // (`configured`/`anomaly`) response keeps the carried tier — stored
3238
+ // thresholds made it final. A fetch failure, `legacy`, or anything else
3239
+ // downshifts exactly as before. `quality`/`speed` never downshift, so they
3240
+ // make no extra request.
3241
+ const thresholdTierMap = posture === "maximize_subscription"
3242
+ ? await fetchTicketModelTiersForRows(access, eligible, options.maxParallel, isEligible)
3243
+ : null;
3244
+ const aliasByEffectiveTier = new Map();
3245
+ const resolveAutoTierAlias = async (effectiveTier) => {
3246
+ const cached = aliasByEffectiveTier.get(effectiveTier);
3247
+ if (cached)
3248
+ return cached;
3249
+ const alias = resolveRoutingAlias(agent, effectiveTier, overrides, catalog);
3250
+ let resolved;
3251
+ if (!alias) {
3252
+ resolved = {
3253
+ resolvedAlias: null,
3254
+ aliasWarning: `model routing: no valid alias for tier=${effectiveTier}; using agent default`,
3255
+ };
3182
3256
  }
3183
3257
  else {
3184
- aliasWarning = validation.warning;
3258
+ const validation = await validateResolvedModelAliasForAgent(deps, agent, alias);
3259
+ resolved = validation.ok
3260
+ ? { resolvedAlias: validation.alias, aliasWarning: null }
3261
+ : { resolvedAlias: null, aliasWarning: validation.warning };
3185
3262
  }
3186
- }
3187
- const reason = resolvedAlias
3188
- ? `auto-tier=${autoTier} model=${resolvedAlias} (posture=${posture})${lineageSuffix}`
3189
- : `${aliasWarning}${lineageSuffix}`;
3190
- out = rows.map((row) => {
3191
- if (!isEligible(row))
3192
- return row;
3263
+ aliasByEffectiveTier.set(effectiveTier, resolved);
3264
+ return resolved;
3265
+ };
3266
+ const autoOut = [];
3267
+ for (const row of rows) {
3268
+ if (!isEligible(row)) {
3269
+ autoOut.push(row);
3270
+ continue;
3271
+ }
3272
+ const threshold = thresholdSkipMarker(posture, thresholdTierMap?.get(row.key));
3273
+ const effectiveTier = posture === "maximize_subscription" && !threshold
3274
+ ? downshiftModelTier(autoTier)
3275
+ : autoTier;
3276
+ const adjustment = resolveModelRoutingDecisionAdjustment(posture, autoTier, threshold);
3277
+ const lineageSuffix = threshold
3278
+ ? `; threshold:${threshold}`
3279
+ : posture === "maximize_subscription"
3280
+ ? `; auto-tier:${autoTier}->maximize:${effectiveTier}`
3281
+ : "";
3282
+ const { resolvedAlias, aliasWarning } = await resolveAutoTierAlias(effectiveTier);
3283
+ const reason = resolvedAlias
3284
+ ? `auto-tier=${autoTier} model=${resolvedAlias} (posture=${posture})${lineageSuffix}`
3285
+ : `${aliasWarning}${lineageSuffix}`;
3193
3286
  const baseRow = {
3194
3287
  ...row,
3195
3288
  difficulty: null,
@@ -3201,14 +3294,10 @@ errorLog = (message) => console.error(message)) {
3201
3294
  ? { modelRoutingDiagnostic: codexEntitlementDiagnostic(access.repoName) }
3202
3295
  : {}),
3203
3296
  };
3204
- return aliasWarning ? appendSummaryRowWarning(baseRow, aliasWarning) : baseRow;
3205
- });
3206
- // BAPI-1236: one `automatic_snapshot` diagnostic per eligible row — the
3207
- // carried baseline, the newly resolved posture, the exact adjustment token,
3208
- // the calculated effective tier, and the validated alias (or `unresolved`).
3209
- for (const row of rows) {
3210
- if (!isEligible(row))
3211
- continue;
3297
+ autoOut.push(aliasWarning ? appendSummaryRowWarning(baseRow, aliasWarning) : baseRow);
3298
+ // BAPI-1236: one `automatic_snapshot` diagnostic per eligible row — the
3299
+ // carried baseline, the newly resolved posture, the exact adjustment token,
3300
+ // the calculated effective tier, and the validated alias (or `unresolved`).
3212
3301
  emitModelRoutingDecisionDiagnostic(errorLog, {
3213
3302
  source: "automatic_snapshot",
3214
3303
  baseline: autoTier,
@@ -3216,8 +3305,10 @@ errorLog = (message) => console.error(message)) {
3216
3305
  adjustment,
3217
3306
  effective: effectiveTier,
3218
3307
  alias: resolvedAlias,
3308
+ ...(threshold ? { threshold } : {}),
3219
3309
  });
3220
3310
  }
3311
+ out = autoOut;
3221
3312
  }
3222
3313
  else {
3223
3314
  // Ordinary per-ticket difficulty-lookup path.
@@ -3298,12 +3389,24 @@ errorLog = (message) => console.error(message)) {
3298
3389
  // alias table. Copilot never reaches this loop at all: it has no
3299
3390
  // `AGENT_REGISTRY` entry and `isRunnableHostTarget()` rejects hosts
3300
3391
  // without a launch agent before an `AgentSpec` is ever resolved.
3392
+ //
3393
+ // BAPI-1343 R-6: the response is already in hand — when it is
3394
+ // threshold-decided (`configured`/`anomaly`), stored thresholds made the
3395
+ // tier final, so the maximize downshift is skipped and the lineage and
3396
+ // diagnostic carry a distinct `threshold` marker instead. The harness-
3397
+ // local premium fallback above is untouched.
3301
3398
  const difficultyTier = tier;
3302
- const effectiveTier = posture === "maximize_subscription" ? downshiftModelTier(difficultyTier) : difficultyTier;
3303
- const adjustment = resolveModelRoutingDecisionAdjustment(posture, difficultyTier);
3304
- const lineageSuffix = posture === "maximize_subscription"
3305
- ? `; difficulty:${difficultyTier}->maximize:${effectiveTier}`
3306
- : "";
3399
+ const threshold = thresholdSkipMarker(posture, tierResult);
3400
+ const effectiveTier = posture === "maximize_subscription" && !threshold
3401
+ ? downshiftModelTier(difficultyTier)
3402
+ : difficultyTier;
3403
+ const adjustment = resolveModelRoutingDecisionAdjustment(posture, difficultyTier, threshold);
3404
+ const lineageSuffix = threshold
3405
+ ? `; threshold:${threshold}`
3406
+ : posture === "maximize_subscription"
3407
+ ? `; difficulty:${difficultyTier}->maximize:${effectiveTier}`
3408
+ : "";
3409
+ const thresholdField = threshold ? { threshold } : {};
3307
3410
  // BAPI-1236: stamp `difficulty_lookup` rather than copying the backend
3308
3411
  // cache/computation source (`cached`/`computed`/`fallback`) into the
3309
3412
  // summary's own routing-provenance field — that backend source is
@@ -3325,6 +3428,7 @@ errorLog = (message) => console.error(message)) {
3325
3428
  adjustment,
3326
3429
  effective: effectiveTier,
3327
3430
  alias: null,
3431
+ ...thresholdField,
3328
3432
  });
3329
3433
  continue;
3330
3434
  }
@@ -3338,6 +3442,7 @@ errorLog = (message) => console.error(message)) {
3338
3442
  adjustment,
3339
3443
  effective: effectiveTier,
3340
3444
  alias: null,
3445
+ ...thresholdField,
3341
3446
  });
3342
3447
  continue;
3343
3448
  }
@@ -3356,6 +3461,7 @@ errorLog = (message) => console.error(message)) {
3356
3461
  adjustment,
3357
3462
  effective: effectiveTier,
3358
3463
  alias: validation.alias,
3464
+ ...thresholdField,
3359
3465
  });
3360
3466
  }
3361
3467
  out = loopOut;
@@ -3835,14 +3941,13 @@ export async function orchestrateStartTickets(deps, options, overrides = {}) {
3835
3941
  const rows = await spawnTabsFn(deps, scoped, terminal, platformConfig.config.buildAgentShellCommand);
3836
3942
  return { ok: true, rows };
3837
3943
  }
3838
- /** Platform-specific guidance printed when one or more tabs fail to spawn. */
3839
- function spawnFailureHintForPlatform(platform) {
3944
+ /**
3945
+ * Platform-specific guidance printed when one or more tabs fail to spawn.
3946
+ * Exported for the BAPI-1319 byte-identity pin on the darwin sentence.
3947
+ */
3948
+ export function spawnFailureHintForPlatform(platform) {
3840
3949
  if (platform === "darwin") {
3841
- return ("One or more tabs failed to spawn. If osascript reported a permission error, grant the " +
3842
- "required macOS permissions under System Settings -> Privacy & Security, then re-run: " +
3843
- "Automation (to let the terminal app be controlled) and, for the Terminal.app path, " +
3844
- "Accessibility (the new-tab step sends Cmd-T via System Events keystrokes, which Automation " +
3845
- "alone does not cover).");
3950
+ return "One or more tabs failed to spawn. " + macosTerminalPermissionGuidance();
3846
3951
  }
3847
3952
  if (platform === "win32") {
3848
3953
  return ("One or more tabs failed to spawn. Ensure Windows Terminal (wt.exe) or PowerShell is " +
@@ -42,26 +42,85 @@ import { fetchLatestVersion } from "./cli-release.js";
42
42
  import { CANONICAL_UPGRADE_COMMAND } from "./upgrade-advice.js";
43
43
  import { describeLauncherReason, inspectLauncherConfigs, isApplicableStandardLauncher, isPrerequisiteFailure, LAUNCHER_CONFIG_TARGETS, } from "./launcher-config-inspection.js";
44
44
  import { MCP_PACKAGE_NAME } from "./mcp-identity.js";
45
+ import { defaultNpmEntryFileExists, resolveNpmEntry } from "./windows-npm-entry.js";
45
46
  /** The sentinel this CLI passes to the child it re-execs from `@latest`. */
46
47
  const INTERNAL_REEXEC_FLAG = "--internal-reexec";
47
- // ---------------------------------------------------------------------------
48
- // Default dependency construction
49
- // ---------------------------------------------------------------------------
50
- function defaultReexec(cwd, oldVersion) {
51
- const npxCmd = process.platform === "win32" ? "npx.cmd" : "npx";
48
+ /**
49
+ * The fixed, secret-free message printed when, on Windows, the latest CLI could
50
+ * not be STARTED. Upgrade stops here rather than continuing with the local copy.
51
+ */
52
+ export const UPGRADE_REEXEC_START_FAILURE_MESSAGE = "Bridge API upgrade failed: could not start the latest version of the CLI. " +
53
+ `Nothing was changed. Run ${CANONICAL_UPGRADE_COMMAND} to run the latest copy directly.`;
54
+ export function defaultReexec(cwd, oldVersion, options = {}) {
55
+ const platform = options.platform ?? process.platform;
56
+ const doSpawn = options.spawn ?? spawn;
57
+ const errorLog = options.errorLog ?? ((message) => console.error(message));
58
+ const reexecArgs = [
59
+ "-y",
60
+ `${MCP_PACKAGE_NAME}@latest`,
61
+ "upgrade",
62
+ INTERNAL_REEXEC_FLAG,
63
+ "--old-version",
64
+ oldVersion,
65
+ ];
66
+ if (platform !== "win32") {
67
+ return new Promise((resolve) => {
68
+ const child = doSpawn("npx", reexecArgs, { stdio: "inherit", cwd });
69
+ child.on("close", (code) => resolve(code ?? 0));
70
+ child.on("error", (err) => {
71
+ errorLog(`Bridge API upgrade failed: could not re-exec npx: ${err.message}`);
72
+ resolve(1);
73
+ });
74
+ });
75
+ }
76
+ // WINDOWS (BAPI-1319): run npm's `npx-cli.js` through Node, never `npx.cmd`
77
+ // (EINVAL without a shell since CVE-2024-27980). A launch that fails before the
78
+ // child starts STOPS the upgrade with a fixed message — it never continues with
79
+ // the local copy, and a sync throw can no longer escape as an unhandled rejection.
80
+ const resolved = resolveNpmEntry("npx", reexecArgs, {
81
+ platform,
82
+ env: options.env ?? process.env,
83
+ execPath: options.execPath ?? process.execPath,
84
+ fileExists: options.fileExists ?? defaultNpmEntryFileExists,
85
+ });
86
+ if (resolved.status !== "resolved") {
87
+ errorLog(UPGRADE_REEXEC_START_FAILURE_MESSAGE);
88
+ return Promise.resolve(1);
89
+ }
52
90
  return new Promise((resolve) => {
53
- const child = spawn(npxCmd, [
54
- "-y",
55
- `${MCP_PACKAGE_NAME}@latest`,
56
- "upgrade",
57
- INTERNAL_REEXEC_FLAG,
58
- "--old-version",
59
- oldVersion,
60
- ], { stdio: "inherit", cwd });
61
- child.on("close", (code) => resolve(code ?? 0));
91
+ let settled = false;
92
+ let started = false;
93
+ const settle = (code) => {
94
+ if (settled)
95
+ return;
96
+ settled = true;
97
+ resolve(code);
98
+ };
99
+ const failToStart = () => {
100
+ if (settled)
101
+ return;
102
+ errorLog(UPGRADE_REEXEC_START_FAILURE_MESSAGE);
103
+ settle(1);
104
+ };
105
+ let child;
106
+ try {
107
+ child = doSpawn(resolved.command, resolved.args, { shell: false, stdio: "inherit", cwd });
108
+ }
109
+ catch {
110
+ failToStart();
111
+ return;
112
+ }
113
+ child.on("spawn", () => {
114
+ started = true;
115
+ });
116
+ child.on("close", (code) => settle(code ?? 0));
62
117
  child.on("error", (err) => {
63
- console.error(`Bridge API upgrade failed: could not re-exec npx: ${err.message}`);
64
- resolve(1);
118
+ if (!started) {
119
+ failToStart();
120
+ return;
121
+ }
122
+ errorLog(`Bridge API upgrade failed: could not re-exec npx: ${err.message}`);
123
+ settle(1);
65
124
  });
66
125
  });
67
126
  }
@@ -77,13 +136,39 @@ async function defaultHasCompetingLocalInstall(cwd) {
77
136
  return false;
78
137
  }
79
138
  }
80
- function defaultRemoveCompetingLocalInstall(cwd) {
81
- const npmCmd = process.platform === "win32" ? "npm.cmd" : "npm";
82
- return new Promise((resolve) => {
83
- const child = spawn(npmCmd, ["uninstall", MCP_PACKAGE_NAME], {
84
- stdio: "inherit",
85
- cwd,
139
+ export function defaultRemoveCompetingLocalInstall(cwd, options = {}) {
140
+ const platform = options.platform ?? process.platform;
141
+ const doSpawn = options.spawn ?? spawn;
142
+ const removeArgs = ["uninstall", MCP_PACKAGE_NAME];
143
+ if (platform !== "win32") {
144
+ return new Promise((resolve) => {
145
+ const child = doSpawn("npm", removeArgs, {
146
+ stdio: "inherit",
147
+ cwd,
148
+ });
149
+ child.on("close", (code) => resolve(code === 0));
150
+ child.on("error", () => resolve(false));
86
151
  });
152
+ }
153
+ // WINDOWS (BAPI-1319): `npm-cli.js` through Node. Every launch failure resolves
154
+ // false, which reaches the caller's existing "could not remove" message.
155
+ const resolved = resolveNpmEntry("npm", removeArgs, {
156
+ platform,
157
+ env: options.env ?? process.env,
158
+ execPath: options.execPath ?? process.execPath,
159
+ fileExists: options.fileExists ?? defaultNpmEntryFileExists,
160
+ });
161
+ if (resolved.status !== "resolved")
162
+ return Promise.resolve(false);
163
+ return new Promise((resolve) => {
164
+ let child;
165
+ try {
166
+ child = doSpawn(resolved.command, resolved.args, { shell: false, stdio: "inherit", cwd });
167
+ }
168
+ catch {
169
+ resolve(false);
170
+ return;
171
+ }
87
172
  child.on("close", (code) => resolve(code === 0));
88
173
  child.on("error", () => resolve(false));
89
174
  });
@@ -95,9 +180,9 @@ export function createDefaultUpgradeCliDeps(cwd) {
95
180
  fetchLatestVersion,
96
181
  readFile: (filePath) => readFile(filePath, "utf-8"),
97
182
  hasCompetingLocalInstall: defaultHasCompetingLocalInstall,
98
- removeCompetingLocalInstall: defaultRemoveCompetingLocalInstall,
183
+ removeCompetingLocalInstall: (installCwd) => defaultRemoveCompetingLocalInstall(installCwd),
99
184
  runInit,
100
- reexecFromLatest: defaultReexec,
185
+ reexecFromLatest: (reexecCwd, oldVersion) => defaultReexec(reexecCwd, oldVersion),
101
186
  stdout: (m) => console.log(m),
102
187
  stderr: (m) => console.error(m),
103
188
  env: process.env,
@@ -1,4 +1,4 @@
1
1
  // AUTO-GENERATED — do not edit manually. Regenerate with: npm run build
2
- export const VERSION = "0.2.62";
3
- export const BUILD_COMMIT = "9351fac413bf";
4
- export const LAUNCHER_ARGS = ["-y", "--prefer-offline", "@bridge_gpt/mcp-server@0.2.62", "serve"];
2
+ export const VERSION = "0.2.66";
3
+ export const BUILD_COMMIT = "b90190f51abc";
4
+ export const LAUNCHER_ARGS = ["-y", "--prefer-offline", "@bridge_gpt/mcp-server@0.2.66", "serve"];
@@ -0,0 +1,103 @@
1
+ /**
2
+ * Windows `npx` / `npm` entry resolution (BAPI-1319).
3
+ *
4
+ * WHY THIS EXISTS. Since Node's April 2024 security releases (CVE-2024-27980),
5
+ * `child_process.spawn` throws `EINVAL` when handed a `.cmd` or `.bat` file
6
+ * without the `shell` option, and a bare `"npx"` is `ENOENT` on Windows because
7
+ * the real launcher is `npx.cmd`. `shell: true` is not an option: the install
8
+ * re-exec argv carries user input. So on `win32` this module skips the shim and
9
+ * runs npm's own JavaScript entry point (`npx-cli.js` / `npm-cli.js`) through
10
+ * the running Node executable, with no shell at all.
11
+ *
12
+ * LOOKUP ORDER MIRRORS THE SHELL. The first `npx.cmd` (or `npm.cmd`) on PATH
13
+ * wins, and its sibling `node_modules/npm/bin/<cli>.js` is the entry point. If
14
+ * that first shim has no sibling CLI the result is `unresolved` — a later PATH
15
+ * candidate is never tried, because that is not the npx the user's shell would
16
+ * run, and the test harnesses that put a fake `npx.cmd` first on PATH must still
17
+ * catch the call. Only when no shim is on PATH at all does the resolver fall
18
+ * back to the npm bundled beside `execPath`.
19
+ *
20
+ * On every platform other than `win32` the input is returned unchanged, so
21
+ * macOS and Linux spawn exactly what they spawned before this module existed.
22
+ *
23
+ * The resolver never throws and never carries exception text: every failure is
24
+ * the typed `unresolved` result.
25
+ */
26
+ import { statSync } from "fs";
27
+ import path from "path";
28
+ const DEFAULT_PATHEXT = ".COM;.EXE;.BAT;.CMD";
29
+ const CLI_FILE = {
30
+ npx: "npx-cli.js",
31
+ npm: "npm-cli.js",
32
+ };
33
+ /** Production `fileExists`: a regular file, never a throw. */
34
+ export function defaultNpmEntryFileExists(filePath) {
35
+ try {
36
+ return statSync(filePath).isFile();
37
+ }
38
+ catch {
39
+ return false;
40
+ }
41
+ }
42
+ /** Case-insensitive env read, as Windows environments are. */
43
+ function _readEnvCaseInsensitive(env, name) {
44
+ const target = name.toUpperCase();
45
+ for (const key of Object.keys(env)) {
46
+ if (key.toUpperCase() === target) {
47
+ const value = env[key];
48
+ if (value === undefined)
49
+ continue;
50
+ if (typeof value !== "string")
51
+ throw new TypeError("malformed environment value");
52
+ return value;
53
+ }
54
+ }
55
+ return undefined;
56
+ }
57
+ /** Find the first shim on PATH, or null when none exists. */
58
+ function _findFirstShim(command, env, fileExists) {
59
+ const rawPath = _readEnvCaseInsensitive(env, "PATH") ?? "";
60
+ const rawPathExt = _readEnvCaseInsensitive(env, "PATHEXT");
61
+ const pathExt = rawPathExt && rawPathExt.trim().length > 0 ? rawPathExt : DEFAULT_PATHEXT;
62
+ // Only the `.cmd` shim is the launcher npm installs beside its CLI. PATHEXT is
63
+ // matched case-insensitively and its own spelling is used for the lookup.
64
+ const cmdExt = pathExt
65
+ .split(";")
66
+ .map((ext) => ext.trim())
67
+ .find((ext) => ext.toLowerCase() === ".cmd");
68
+ if (!cmdExt)
69
+ return null;
70
+ for (const segment of rawPath.split(";")) {
71
+ const dir = segment.trim();
72
+ if (dir.length === 0)
73
+ continue;
74
+ const candidate = path.win32.join(dir, `${command}${cmdExt}`);
75
+ if (fileExists(candidate))
76
+ return candidate;
77
+ }
78
+ return null;
79
+ }
80
+ /**
81
+ * Map `npx`/`npm` plus args to a spawnable `{ command, args }`.
82
+ *
83
+ * Non-`win32`: `{ command, args }` unchanged. `win32`: `{ command: execPath,
84
+ * args: [<cliPath>, ...args] }`, or `unresolved`.
85
+ */
86
+ export function resolveNpmEntry(command, args, options) {
87
+ if (options.platform !== "win32") {
88
+ return { status: "resolved", command, args };
89
+ }
90
+ try {
91
+ const cliFile = CLI_FILE[command];
92
+ const shim = _findFirstShim(command, options.env, options.fileExists);
93
+ const cliPath = shim
94
+ ? path.win32.join(path.win32.dirname(shim), "node_modules", "npm", "bin", cliFile)
95
+ : path.win32.join(path.win32.dirname(options.execPath), "node_modules", "npm", "bin", cliFile);
96
+ if (!options.fileExists(cliPath))
97
+ return { status: "unresolved" };
98
+ return { status: "resolved", command: options.execPath, args: [cliPath, ...args] };
99
+ }
100
+ catch {
101
+ return { status: "unresolved" };
102
+ }
103
+ }