@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
@@ -0,0 +1,233 @@
1
+ /**
2
+ * Installer-owned self-re-exec from `@latest` (BAPI-714, Group B).
3
+ *
4
+ * WHY THIS EXISTS. The documented setup command dropped `@latest`
5
+ * (`npx -y @bridge_gpt/mcp-server install`), and an UNPINNED npx spec silently
6
+ * reuses a stale cached copy — the verified bug recorded in `init.ts:32-38`,
7
+ * where 0.2.6 persisted across an upgrade. So the install path must do for itself
8
+ * what `upgrade` already does: look up the latest published version and, when the
9
+ * local copy is strictly older, hand off to `@latest`.
10
+ *
11
+ * THREE PROPERTIES ARE LOAD-BEARING.
12
+ *
13
+ * 1. FAIL OPEN, ALWAYS. `upgrade` can afford to fail; `install` is someone's
14
+ * first contact with Bridge. An unreachable, slow, non-2xx, or malformed
15
+ * registry means "run the local copy" — never an error, prompt, or stall.
16
+ * 2. `stdio: "inherit"` IS NOT STYLISTIC. `promptSecret`/`promptLine`/
17
+ * `promptMultiSelect` are wired only when `process.stdin.isTTY`
18
+ * (`install-bridge.ts:1195`, `:1224-1226`); non-inherited stdio silently
19
+ * strips all three and turns an interactive install into a non-interactive
20
+ * one with no error.
21
+ * 3. THE SENTINEL IS STRIPPED BEFORE *ANY* ARGV INTERPRETATION. Two verified
22
+ * reasons: `parseInstallBridgeArgs` rejects unrecognized arguments, so a
23
+ * leftover `--internal-reexec` is an argument error on the child run; and
24
+ * `install-bridge.ts:1690` computes `isBareInvocation = argv.length === 0`,
25
+ * so a leftover sentinel makes the child take a DIFFERENT onboarding branch
26
+ * silently. Its presence is also the terminating condition, so exactly one
27
+ * re-exec is possible per invocation.
28
+ *
29
+ * SECRET DISCIPLINE. Child argv is process-visible (`ps`), so `--api-key <value>`
30
+ * and a VALUED `--invite <token>` are lifted out of argv and forwarded only
31
+ * through the child ENVIRONMENT as `BAPI_API_KEY` / `BAPI_INVITE`. A value-less
32
+ * `--invite` marker is deliberately RETAINED in child argv: it is what selects
33
+ * bootstrap-invite mode, and dropping it would leave `argv.length === 0` and send
34
+ * the child down the bare-onboarding branch instead. The failure diagnostic is a
35
+ * fixed string — it interpolates no argv, no environment value, and no exception
36
+ * text.
37
+ */
38
+ import { spawn } from "child_process";
39
+ import { VERSION } from "./version.generated.js";
40
+ import { isNewerVersion } from "./update-check.js";
41
+ import { fetchLatestVersion } from "./cli-release.js";
42
+ import { runInstallBridgeCli } from "./install-bridge.js";
43
+ /** The sentinel that marks an already-re-exec'd child. Matches `upgrade-cli.ts`. */
44
+ export const INSTALL_REEXEC_SENTINEL = "--internal-reexec";
45
+ /**
46
+ * The ONLY text emitted when the hand-off itself fails. Fixed by construction —
47
+ * no argv, no environment value, no raw exception message, no credential.
48
+ */
49
+ export const INSTALL_REEXEC_FAILURE_MESSAGE = "Bridge API install failed: could not run the latest installer.";
50
+ /**
51
+ * Remove EVERY `--internal-reexec` occurrence and report whether any was present.
52
+ *
53
+ * Called before nested-command detection, before the re-exec decision, and before
54
+ * anything is handed to `runInstallBridgeCli` — the sentinel must never reach the
55
+ * installer's strict argument parser or its bare-invocation computation.
56
+ */
57
+ export function stripInternalReexecSentinels(argv) {
58
+ const args = [];
59
+ let sentinelPresent = false;
60
+ for (const arg of argv) {
61
+ if (arg === INSTALL_REEXEC_SENTINEL) {
62
+ sentinelPresent = true;
63
+ continue;
64
+ }
65
+ args.push(arg);
66
+ }
67
+ return { args, sentinelPresent };
68
+ }
69
+ /**
70
+ * Lift secret-valued flags out of argv and into a child-environment overlay.
71
+ *
72
+ * Mirrors `parseInstallBridgeArgs` token-for-token so the two never disagree:
73
+ *
74
+ * - `--api-key=<value>` / `--api-key <value>` → `BAPI_API_KEY`, flag AND value
75
+ * removed. The installer resolves the key from the environment, and
76
+ * `hasEnvApiKey` keeps the bare-interactive chooser suppressed exactly as the
77
+ * flag did.
78
+ * - `--invite=<token>` / `--invite <token>` → `BAPI_INVITE`, with a VALUE-LESS
79
+ * `--invite` left in argv. That marker still selects bootstrap-invite mode and
80
+ * the installer falls back to `BAPI_INVITE` for the token.
81
+ * - MALFORMED forms (`--api-key` at end of argv, `--api-key=`, `--invite=`) are
82
+ * left untouched so the installer's own parser produces the established
83
+ * argument error rather than this layer inventing a new one.
84
+ *
85
+ * Every other argument is preserved byte-for-byte in its original order. This is
86
+ * deliberately NOT `sanitizePrewarmEnv()` from `install-bridge.ts`: that helper
87
+ * STRIPS installer credentials for a `--version` probe and has the opposite
88
+ * contract from this hand-off.
89
+ */
90
+ export function prepareInstallReexecArguments(argv) {
91
+ const forwardedArgs = [];
92
+ const secretEnv = {};
93
+ for (let i = 0; i < argv.length; i++) {
94
+ const arg = argv[i];
95
+ if (arg === "--api-key" || arg.startsWith("--api-key=")) {
96
+ let value;
97
+ let consumedNext = false;
98
+ if (arg.startsWith("--api-key=")) {
99
+ value = arg.slice("--api-key=".length);
100
+ }
101
+ else if (i + 1 < argv.length) {
102
+ value = argv[i + 1];
103
+ consumedNext = true;
104
+ }
105
+ // Blank / absent value == malformed: hand it to the installer's parser.
106
+ if (value === undefined || value.length === 0) {
107
+ forwardedArgs.push(arg);
108
+ continue;
109
+ }
110
+ secretEnv.BAPI_API_KEY = value;
111
+ if (consumedNext)
112
+ i += 1;
113
+ continue;
114
+ }
115
+ if (arg === "--invite" || arg.startsWith("--invite=")) {
116
+ if (arg.startsWith("--invite=")) {
117
+ const value = arg.slice("--invite=".length);
118
+ if (value.length === 0) {
119
+ // Malformed: leave it for the installer's parser.
120
+ forwardedArgs.push(arg);
121
+ continue;
122
+ }
123
+ secretEnv.BAPI_INVITE = value;
124
+ forwardedArgs.push("--invite");
125
+ continue;
126
+ }
127
+ // Bare `--invite`: only a following NON-flag token is its value, exactly as
128
+ // `parseInstallBridgeArgs` reads it.
129
+ const next = argv[i + 1];
130
+ forwardedArgs.push("--invite");
131
+ if (typeof next === "string" && !next.startsWith("-") && next.length > 0) {
132
+ secretEnv.BAPI_INVITE = next;
133
+ i += 1;
134
+ }
135
+ continue;
136
+ }
137
+ forwardedArgs.push(arg);
138
+ }
139
+ return { forwardedArgs, secretEnv };
140
+ }
141
+ /**
142
+ * The public `install` / legacy `install-bridge` entry point.
143
+ *
144
+ * Fail-open ordering, in the only order that is safe:
145
+ *
146
+ * 1. Strip sentinels (before anything reads argv).
147
+ * 2. If a sentinel was present, or the first remaining token is `conductor`,
148
+ * delegate straight to the local installer — no registry lookup, no spawn.
149
+ * The nested `install conductor` surface is intercepted inside
150
+ * `runInstallBridgeCli`, so it must reach it as `["conductor", ...]` rather
151
+ * than being collapsed into bare onboarding.
152
+ * 3. Otherwise look up the latest version, defaulting the comparison target to
153
+ * the local `VERSION` so an unusable answer changes nothing.
154
+ * 4. Re-exec only for a STRICTLY newer published version.
155
+ */
156
+ export async function runInstallBridgeWithLatestCli(argv, deps = {}) {
157
+ const resolveLatest = deps.fetchLatestVersion ?? (() => fetchLatestVersion());
158
+ const runLocal = deps.runInstallBridgeCli ?? runInstallBridgeCli;
159
+ const doSpawn = deps.spawn ?? spawn;
160
+ const env = deps.env ?? process.env;
161
+ const platform = deps.platform ?? process.platform;
162
+ const cwd = deps.cwd ?? process.cwd();
163
+ const errorLog = deps.errorLog ?? ((message) => console.error(message));
164
+ const localVersion = deps.localVersion ?? VERSION;
165
+ const { args: cleanedArgs, sentinelPresent } = stripInternalReexecSentinels(argv);
166
+ // Terminating condition, and the nested-conductor bypass. Both skip the
167
+ // registry entirely.
168
+ if (sentinelPresent || cleanedArgs[0] === "conductor") {
169
+ return runLocal(cleanedArgs);
170
+ }
171
+ // Default the comparison target to the local version: an unusable registry
172
+ // answer must be indistinguishable from "already current".
173
+ let latestVersion = localVersion;
174
+ let fetched = null;
175
+ try {
176
+ fetched = await resolveLatest();
177
+ }
178
+ catch {
179
+ fetched = null;
180
+ }
181
+ if (fetched)
182
+ latestVersion = fetched;
183
+ if (!isNewerVersion(localVersion, latestVersion)) {
184
+ return runLocal(cleanedArgs);
185
+ }
186
+ const { forwardedArgs, secretEnv } = prepareInstallReexecArguments(cleanedArgs);
187
+ const npxCmd = platform === "win32" ? "npx.cmd" : "npx";
188
+ const childArgs = [
189
+ "-y",
190
+ "@bridge_gpt/mcp-server@latest",
191
+ "install",
192
+ INSTALL_REEXEC_SENTINEL,
193
+ ...forwardedArgs,
194
+ ];
195
+ return new Promise((resolve) => {
196
+ let settled = false;
197
+ const settle = (code) => {
198
+ if (settled)
199
+ return;
200
+ settled = true;
201
+ resolve(code);
202
+ };
203
+ const fail = () => {
204
+ errorLog(INSTALL_REEXEC_FAILURE_MESSAGE);
205
+ settle(1);
206
+ };
207
+ let child;
208
+ try {
209
+ child = doSpawn(npxCmd, childArgs, {
210
+ shell: false,
211
+ stdio: "inherit",
212
+ cwd,
213
+ // Explicit CLI values win over an inherited value of the same key.
214
+ env: { ...env, ...secretEnv },
215
+ });
216
+ }
217
+ catch {
218
+ fail();
219
+ return;
220
+ }
221
+ child.on("close", (code) => {
222
+ // A signal termination or an absent numeric code is an abnormal exit.
223
+ if (typeof code === "number") {
224
+ settle(code);
225
+ return;
226
+ }
227
+ fail();
228
+ });
229
+ child.on("error", () => {
230
+ fail();
231
+ });
232
+ });
233
+ }
@@ -28,6 +28,7 @@
28
28
  * rather than reserialize unrelated content.
29
29
  */
30
30
  import { VERSION } from "./version.generated.js";
31
+ import { refreshBridgeApiPackageSpec } from "./init.js";
31
32
  // ---------------------------------------------------------------------------
32
33
  // Path helpers (POSIX-normalized; module stays free of node:path).
33
34
  // ---------------------------------------------------------------------------
@@ -119,7 +120,16 @@ export function mergeJsonHostConfig(existing, target, adaptedEntry) {
119
120
  const root = rootRaw && typeof rootRaw === "object" && !Array.isArray(rootRaw)
120
121
  ? { ...rootRaw }
121
122
  : {};
122
- root["bridge-api"] = adaptedEntry;
123
+ // BAPI-714 (Group C): an UPDATE to a pre-existing Bridge registration keeps that
124
+ // registration's own launcher args composition — only the package-spec token is
125
+ // refreshed — so a global/manual host config written before `serve` existed is
126
+ // never migrated to it. A registration this path CREATES receives the adapted
127
+ // template args, `serve` included.
128
+ const prior = root["bridge-api"];
129
+ const preservedArgs = prior && typeof prior === "object" && !Array.isArray(prior)
130
+ ? refreshBridgeApiPackageSpec(prior.args, currentLauncherSpec())
131
+ : null;
132
+ root["bridge-api"] = preservedArgs ? { ...adaptedEntry, args: preservedArgs } : adaptedEntry;
123
133
  merged[target.topLevelKey] = root;
124
134
  return merged;
125
135
  }
@@ -152,6 +152,10 @@ export async function writeMcpInstallState(cwd, input, deps) {
152
152
  projectConfigPaths: normalizeProjectPaths(input.projectConfigPaths),
153
153
  };
154
154
  const finalPath = installStatePath(cwd);
155
+ return persistMcpInstallState(cwd, state, finalPath, deps);
156
+ }
157
+ /** Shared serialize-and-atomically-persist tail for the two writers below. */
158
+ async function persistMcpInstallState(cwd, state, finalPath, deps) {
155
159
  const tempPath = installStateTempPath(cwd);
156
160
  const serialized = serializeMcpInstallState(state);
157
161
  try {
@@ -173,3 +177,31 @@ export async function writeMcpInstallState(cwd, input, deps) {
173
177
  }
174
178
  return { ok: true, path: finalPath, state };
175
179
  }
180
+ /**
181
+ * Record a locally installed project artifact WITHOUT clobbering existing state.
182
+ *
183
+ * `writeMcpInstallState` rebuilds the whole document from its `input` — it does
184
+ * not merge. A caller supplying only `projectConfigPaths` would therefore write
185
+ * an EMPTY `selectedPlatforms`, silently dropping the platform roster a later
186
+ * `doctor` run reports from (BAPI-679 review, E-13). This helper reads the
187
+ * current state first and writes back the union, so the additive record stays
188
+ * additive.
189
+ *
190
+ * Only the repo-relative PATH is stored. No OAuth token, API key, GitHub
191
+ * credential, or expanded workflow secret can reach the file: the state schema
192
+ * has no field for one, and the serializer emits only schema-approved keys.
193
+ */
194
+ export async function recordInstalledProjectArtifact(cwd, relPath, deps) {
195
+ if (typeof relPath !== "string" || relPath.length === 0) {
196
+ return { ok: false, error: "artifact path must be a non-empty relative path" };
197
+ }
198
+ const existing = await readMcpInstallState(cwd, deps);
199
+ const priorPlatforms = existing.status === "valid" ? existing.state.selectedPlatforms : [];
200
+ const priorPaths = existing.status === "valid" ? existing.state.projectConfigPaths : [];
201
+ const state = {
202
+ version: MCP_INSTALL_STATE_VERSION,
203
+ selectedPlatforms: normalizePlatforms(priorPlatforms),
204
+ projectConfigPaths: normalizeProjectPaths([...priorPaths, relPath]),
205
+ };
206
+ return persistMcpInstallState(cwd, state, installStatePath(cwd), deps);
207
+ }
@@ -1,12 +1,15 @@
1
1
  /**
2
2
  * Worktree MCP registration provisioning.
3
3
  *
4
- * After `start-tickets` creates a worktree, this module writes secret-free MCP
5
- * registrations into both Claude (`.mcp.json`) and Cursor (`.cursor/mcp.json`)
6
- * so the configured MCP servers are reachable from either editor. Every
7
- * generated entry contains NO `env` block it points at the `mcp-invoke` shim
8
- * with an absolute `--project-root` and the target name; credentials are
9
- * resolved at runtime by the shim, never written into the worktree.
4
+ * Shared by `start-tickets` worktrees AND, since BAPI-724, every Conductor
5
+ * executor worker worktree (`mcp_server/src/executor/job-runner.ts`, alongside
6
+ * the executor's deny-layer provisioning) not start-tickets-only. Both
7
+ * callers write secret-free MCP registrations into both Claude (`.mcp.json`)
8
+ * and Cursor (`.cursor/mcp.json`) so the configured MCP servers are reachable
9
+ * from either editor. Every generated entry contains NO `env` block — it
10
+ * points at the `mcp-invoke` shim with an absolute `--project-root` and the
11
+ * target name; credentials are resolved at runtime by the shim, never written
12
+ * into the worktree.
10
13
  *
11
14
  * Registrations are driven by `.bridge/config`: the `bapi` target is always
12
15
  * provisioned when present, and every supported Tier-2 target (e.g. `sfcc`) is
@@ -69,6 +72,12 @@ export function serverNameForMcpTarget(target) {
69
72
  * value. Keeping the shim env-free preserves the credential-safety invariant
70
73
  * (secrets are resolved by the `mcp-invoke` shim at launch, not persisted in the
71
74
  * worktree) and equally avoids persisting non-secret operational metadata here.
75
+ *
76
+ * BAPI-724: the executor caller's `invocation` is resolved BEFORE spawn from its
77
+ * OWN running process (`resolveMcpShimInvocationForRuntime`, absolute-build-path
78
+ * form preferred) and never depends on a worker-injected `CONDUCTOR_NODE_PATH`
79
+ * or any other worker-shell env — the executor worker environment carries no
80
+ * such key at all (see `executor/env.ts`).
72
81
  */
73
82
  export function buildShimMcpServerEntry(target, absoluteWorktreePath, invocation) {
74
83
  return buildMcpShimCommand(invocation, target, absoluteWorktreePath);
@@ -284,6 +293,13 @@ function withWarnings(row, warnings) {
284
293
  * add a secret-free warning but never abort provisioning.
285
294
  * - A required write failure (or a malformed existing registration file) marks
286
295
  * only this row `spawn-failed` with a descriptive error; other rows continue.
296
+ * This `spawn-failed` status is the `start-tickets` row contract: that
297
+ * caller propagates it verbatim into its summary report. The executor caller
298
+ * (BAPI-724) deliberately does NOT propagate it the same way — it constructs
299
+ * a synthetic `created` row per job, and its adapter
300
+ * (`provisionMcpForPreparedSpawn` in `job-runner.ts`) converts a
301
+ * `spawn-failed` result into a logged fail-open warning instead, since a
302
+ * provisioning failure must never block an executor worker from spawning.
287
303
  * - After the registration files are written, the worktree's
288
304
  * `.claude/settings.local.json` is updated to pre-approve those servers via
289
305
  * `enabledMcpjsonServers` (suppressing Claude Code's per-project trust