@bridge_gpt/mcp-server 0.2.46 → 0.2.49

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 (39) hide show
  1. package/README.md +101 -85
  2. package/build/agent-capabilities/default-deps.js +2 -2
  3. package/build/agent-launchers/claude.js +10 -19
  4. package/build/agent-launchers/cursor.js +4 -12
  5. package/build/agent-launchers/prompt.js +117 -0
  6. package/build/commands.generated.js +16 -22
  7. package/build/conduct-epic/bridge-client.js +73 -0
  8. package/build/conduct-epic/cli.js +152 -6
  9. package/build/conductor/cli.js +6 -7
  10. package/build/conductor/doctor.js +13 -116
  11. package/build/conductor/tools.js +18 -349
  12. package/build/conductor-bin.js +6 -30
  13. package/build/docs.generated.js +1 -1
  14. package/build/executor/deps.js +1 -0
  15. package/build/executor/service-lifecycle.js +6 -6
  16. package/build/executor/service-unit.js +13 -16
  17. package/build/index.js +214 -755
  18. package/build/init.js +15 -17
  19. package/build/install-doctor.js +1 -1
  20. package/build/learn-tool-gating.js +283 -0
  21. package/build/mcp-profile.js +13 -3
  22. package/build/mcp-server-invocation.js +14 -0
  23. package/build/pipelines.generated.js +19 -139
  24. package/build/platform-escaping.js +72 -0
  25. package/build/readme.generated.js +1 -1
  26. package/build/review-tickets.js +1 -1
  27. package/build/run-unit-tests-launcher.js +0 -1
  28. package/build/sfcc/register.js +41 -31
  29. package/build/sfcc/registration-inventory.js +44 -20
  30. package/build/start-tickets-conductor.js +2 -2
  31. package/build/start-tickets.js +8 -38
  32. package/build/version.generated.js +2 -2
  33. package/docs/CONDUCTOR.md +10 -12
  34. package/docs/install/mcp-tool-integrations.md +9 -55
  35. package/package.json +1 -1
  36. package/pipelines/idea-to-ticket.json +2 -2
  37. package/pipelines/review-ticket.json +9 -8
  38. package/pipelines/check-ci-ticket.json +0 -36
  39. package/pipelines/pr-ticket.json +0 -24
package/build/init.js CHANGED
@@ -882,7 +882,7 @@ Each pipeline file must be a JSON object with these fields:
882
882
  | Field | Type | Required | Description |
883
883
  |---------------|------------|----------|------------------------------------------|
884
884
  | \`name\` | string | yes | Display name of the pipeline |
885
- | \`description\` | string | no | Short description shown in list_pipelines |
885
+ | \`description\` | string | no | Short description of the pipeline |
886
886
  | \`variables\` | string[] | no | Variable names for substitution |
887
887
  | \`steps\` | Step[] | yes | Ordered list of steps to execute |
888
888
 
@@ -934,29 +934,27 @@ automatically provided by the server.
934
934
 
935
935
  ## Executing Pipelines
936
936
 
937
- Pipelines defined here can be executed end-to-end via the \`run_pipeline\` MCP
938
- tool. \`run_pipeline\` returns a unified envelope keyed on \`status\`:
937
+ Pipelines defined here are executed by the calling agent. Call the
938
+ \`get_pipeline_recipe\` MCP tool with the pipeline name and any variables; it
939
+ returns a fully resolved recipe:
939
940
 
940
- - \`completed\` — the pipeline finished and \`results\` holds the per-step output.
941
- - \`needs_agent_task\` the orchestrator paused on an \`agent_task\` step (or an
942
- approval-gated \`mcp_call\` when \`auto_approve\` was false). Perform the task
943
- described by \`instruction\`, then call \`resume_pipeline\` with the
944
- \`pipeline_run_id\` and the resulting string as \`agent_result\`.
945
- - \`failed\` — terminal failure; \`error_code\` is one of \`VALIDATION\`,
946
- \`NOT_FOUND\`, \`EXPIRED\`, \`REPO_MISMATCH\`, or \`TOOL_ERROR\`.
941
+ - \`steps\` — the ordered step array, with variables substituted and every
942
+ \`instruction_file\` reference already inlined as \`instruction\` text.
943
+ - \`total_steps\`, \`auto_approve\`, and \`execution_mode\`.
947
944
 
948
- Paused runs auto-expire after an idle TTL (default 24 hours, override via
949
- \`ttl_seconds\` on \`run_pipeline\`). The TTL is reset on every state transition.
950
- Use \`list_pipeline_runs\` to recover a \`pipeline_run_id\` if the prior
951
- \`needs_agent_task\` envelope is no longer in scope.
945
+ \`execution_mode\` is \`inline\`: there is no server-side orchestrator,
946
+ and no run to resume. The agent executes each step itself in order — calling the named
947
+ tool for an \`mcp_call\` step, following the instruction text for an
948
+ \`agent_task\` step and honours each step's \`on_error\` and
949
+ \`requires_approval\` fields as it goes.
952
950
 
953
951
  ## \`agent_task\` Instruction Files
954
952
 
955
953
  When you reference an instruction file (\`instruction_file: "…"\`), the file
956
954
  markdown MUST end with a terminal \`## Return\` H2 section describing what the
957
- agent should pass back as \`resume_pipeline.agent_result\`. The return value
958
- is a string — do NOT ask the agent to wrap it in JSON unless the instruction
959
- body explicitly says to serialize structured output.
955
+ agent should carry forward from that step. The return value is a string — do
956
+ NOT ask the agent to wrap it in JSON unless the instruction body explicitly
957
+ says to serialize structured output.
960
958
  `;
961
959
  const exampleContent = JSON.stringify({
962
960
  name: "Example Pipeline",
@@ -465,7 +465,7 @@ export async function collectInstallStatusChecks(deps) {
465
465
  label: "Repository indexing",
466
466
  status: "INFO",
467
467
  detail: "no parse job currently running — if this repository has never been indexed, " +
468
- "queue one with /parse-repository (monitor with get_parse_status).",
468
+ "queue one with /parse-repository (monitor with parse_repository action \"status\").",
469
469
  });
470
470
  }
471
471
  }
@@ -0,0 +1,283 @@
1
+ /**
2
+ * Learn/onboarding tool gating — SDK enabled-state, session lifetime (BAPI-912,
3
+ * decision D20).
4
+ *
5
+ * ---------------------------------------------------------------------------
6
+ * Which gating mechanism this is, and which one it is NOT
7
+ * ---------------------------------------------------------------------------
8
+ *
9
+ * Bridge has two independent mechanisms that can keep a tool out of a client's
10
+ * `tools/list`, and conflating them is the specific mistake this comment exists
11
+ * to prevent:
12
+ *
13
+ * **Mechanism A — backend capability gating (BAPI-641).**
14
+ * `tool-surface-gating.ts`. A fail-open, purely *subtractive* filter over the
15
+ * SDK's own `tools/list` result, driven by a backend capability probe. That
16
+ * module documents a hard **NEVER `.disable()`** invariant, because a probe
17
+ * result must never mutate registration state. This module does not import
18
+ * from, call into, or modify it.
19
+ *
20
+ * **Mechanism B — SDK enabled state (used here).**
21
+ * The `registerTool` wrapper in `index.ts` returns the SDK registration
22
+ * handle and keeps a closure `active` flag in sync with `.enable()` /
23
+ * `.disable()`, so a disabled tool is blocked for BOTH `tools/list` and
24
+ * in-process `TOOL_HANDLERS` dispatch (returning the `TOOL_DISABLED`/503
25
+ * envelope). `poll_ci_checks` has used exactly this since BAPI-338.
26
+ *
27
+ * Mechanism A's never-`.disable()` invariant governs Mechanism A only — it
28
+ * constrains the capability-gating module, not `index.ts`. Reading it as a
29
+ * blanket prohibition would make this feature look unimplementable.
30
+ *
31
+ * The two **compose as `SDK-enabled ∩ not-capability-hidden`**. This module
32
+ * moves only the first term, so enabling a tool whose repository lacks the
33
+ * required integration correctly leaves it absent from `tools/list`. That is a
34
+ * supported outcome, not a bug.
35
+ *
36
+ * ---------------------------------------------------------------------------
37
+ * The state machine
38
+ * ---------------------------------------------------------------------------
39
+ *
40
+ * The three learn/onboarding tools register normally (so registration order and
41
+ * the ordered `EXPECTED_TOOL_NAMES` pin are untouched) and are then disabled at
42
+ * startup. They are enabled **all three together, exactly once**, by the first
43
+ * of three qualifying triggers:
44
+ *
45
+ * 1. a *successful* `get_install_manifest` response,
46
+ * 2. `get_pipeline_recipe` successfully resolving `learn-repository`,
47
+ * 3. the post-initialize client fallback, for any client not on the
48
+ * known-good list.
49
+ *
50
+ * Trigger 3 is why this is safe for clients that ignore a mid-session
51
+ * `notifications/tools/list_changed`: they simply receive the trio in their very
52
+ * first `tools/list`. The direction is deliberately **fail-open** — an
53
+ * unrecognized client gets the tools. Enabling only un-hides tools the resolved
54
+ * profile already registered; it never bypasses `ACTIVE_GROUPS`, the capability
55
+ * gate, or backend authorization.
56
+ *
57
+ * Nothing here writes a notification to the transport. The SDK's `update()` path
58
+ * (`mcp.js`) fires `sendToolListChanged()` itself and no-ops it while
59
+ * unconnected, so the startup `.disable()` emits nothing and a post-connect
60
+ * `.enable()` notifies automatically.
61
+ */
62
+ /**
63
+ * The three gated tools, as a fixed list.
64
+ *
65
+ * This is the user-visible identity of the gated set and is asserted verbatim by
66
+ * tests; it is deliberately a literal rather than something derived from the
67
+ * registration order in `index.ts`.
68
+ */
69
+ export const LEARN_GATED_TOOL_NAMES = [
70
+ "apply_install_manifest",
71
+ "invite_member",
72
+ "regenerate_directory_map",
73
+ ];
74
+ /** The pipeline name whose successful resolution is a qualifying trigger. */
75
+ export const LEARN_REPOSITORY_PIPELINE_NAME = "learn-repository";
76
+ /**
77
+ * Clients empirically verified to honor a mid-session
78
+ * `notifications/tools/list_changed` — the only clients for which it is safe to
79
+ * hide the trio at startup.
80
+ *
81
+ * `claude-code` is an **observed MCP handshake identity**, captured on
82
+ * 2026-08-26 from a live Claude Code 2.1.246 session against a throwaway stdio
83
+ * MCP server, which recorded:
84
+ *
85
+ * "clientInfo": { "name": "claude-code", "title": "Claude Code",
86
+ * "version": "2.1.246", ... }
87
+ *
88
+ * The same capture confirmed the behavior that matters: after the server emitted
89
+ * `notifications/tools/list_changed`, Claude Code re-issued `tools/list` and
90
+ * called the newly revealed tool inside the same session.
91
+ *
92
+ * It happens to equal the `MCP_HOST_TARGETS` id `"claude-code"`, but it is NOT
93
+ * copied from it — those ids are host-config target names, a different
94
+ * namespace. This constant is pinned to the observed handshake value.
95
+ *
96
+ * Deliberately NOT on this list:
97
+ * - **Cursor** — reported to ignore mid-session list changes; could not be
98
+ * verified locally (`cursor-agent` requires authentication). Fail-open
99
+ * covers it: Cursor receives the trio at startup.
100
+ * - **GitHub Copilot CLI** (`github-copilot-developer`) — *passed* the same
101
+ * probe on 2026-08-26 (CLI 1.0.71 re-listed and called the revealed tool).
102
+ * Left off pending owner sign-off, since this list ships with Claude Code as
103
+ * its sole initial entry; promoting it is a one-line, zero-risk follow-up.
104
+ * - **VS Code / Copilot Chat extension** — a different product from the CLI
105
+ * above, with its own handshake identity. Not probed; not claimed.
106
+ */
107
+ export const KNOWN_LIST_CHANGED_CLIENT_NAMES = new Set([
108
+ "claude-code",
109
+ ]);
110
+ // ---------------------------------------------------------------------------
111
+ // Client identity
112
+ // ---------------------------------------------------------------------------
113
+ /**
114
+ * Normalize a client-supplied name for *matching*: require a string, trim it,
115
+ * lowercase it. Anything else — absent, null, number, object, array, blank — is
116
+ * unrecognized, which is the fail-open direction.
117
+ *
118
+ * The result of this function is only ever compared for exact membership. It is
119
+ * never logged; `sanitizeClientName` owns the display value, and the two must
120
+ * not be interchanged.
121
+ */
122
+ export function normalizeClientName(raw) {
123
+ if (typeof raw !== "string")
124
+ return null;
125
+ const normalized = raw.trim().toLowerCase();
126
+ return normalized === "" ? null : normalized;
127
+ }
128
+ /**
129
+ * Exact, trimmed, case-insensitive token membership.
130
+ *
131
+ * Deliberately NOT substring, prefix, or product-family matching:
132
+ * `clientInfo.name` is client-controlled, and a substring test would let
133
+ * `"not-claude-code"` or `"claude-code-preview"` keep tools hidden from a client
134
+ * that may not honor `tools/list_changed` at all.
135
+ */
136
+ export function isKnownListChangedClient(raw) {
137
+ const normalized = normalizeClientName(raw);
138
+ return normalized !== null && KNOWN_LIST_CHANGED_CLIENT_NAMES.has(normalized);
139
+ }
140
+ /** Longest client name rendered into a log line. */
141
+ export const MAX_LOGGED_CLIENT_NAME_LENGTH = 64;
142
+ /** Substituted for absent, non-string, blank, or fully-stripped input. */
143
+ export const UNKNOWN_CLIENT_NAME = "unknown";
144
+ /**
145
+ * Bound and restrict an untrusted `clientInfo.name` for logging.
146
+ *
147
+ * `clientInfo.name` is attacker-controlled text that lands on stderr, so this is
148
+ * a log-forging control, not cosmetics: newlines, ANSI escapes, quotes, and
149
+ * shell punctuation all fall outside the safe class and become `_`, so no client
150
+ * can inject a second line that reads like a genuine record. The result is
151
+ * single-line, length-bounded, and drawn only from `[A-Za-z0-9._/-]`.
152
+ *
153
+ * The same posture `mcp-profile.ts` states for profile tokens: raw input is
154
+ * never echoed.
155
+ */
156
+ export function sanitizeClientName(raw) {
157
+ if (typeof raw !== "string")
158
+ return UNKNOWN_CLIENT_NAME;
159
+ const cleaned = raw
160
+ .trim()
161
+ .slice(0, MAX_LOGGED_CLIENT_NAME_LENGTH)
162
+ .replace(/[^A-Za-z0-9._/-]/g, "_");
163
+ // A value made entirely of unsafe characters degrades to underscores, which
164
+ // reads as a real name; report it as unknown instead.
165
+ return cleaned === "" || /^_+$/.test(cleaned) ? UNKNOWN_CLIENT_NAME : cleaned;
166
+ }
167
+ // ---------------------------------------------------------------------------
168
+ // Trigger predicates
169
+ // ---------------------------------------------------------------------------
170
+ /**
171
+ * Extract the text payload from either a raw string or an MCP `{content:[…]}`
172
+ * response envelope. Returns null when there is no text content.
173
+ */
174
+ function extractResponseText(candidate) {
175
+ if (typeof candidate === "string")
176
+ return candidate;
177
+ if (!candidate || typeof candidate !== "object" || Array.isArray(candidate))
178
+ return null;
179
+ const content = candidate.content;
180
+ if (!Array.isArray(content))
181
+ return null;
182
+ for (const item of content) {
183
+ if (item && typeof item === "object" && !Array.isArray(item)) {
184
+ const text = item.text;
185
+ if (typeof text === "string")
186
+ return text;
187
+ }
188
+ }
189
+ return null;
190
+ }
191
+ /**
192
+ * `get_install_manifest` appends a save note (`\n\n---\nSaved to …`) to its JSON
193
+ * body whenever `save_locally` is not explicitly false — which is the default,
194
+ * and therefore the common case. Stripping it here is what keeps the predicate
195
+ * from silently rejecting every ordinary successful manifest.
196
+ */
197
+ function stripSaveNote(text) {
198
+ const marker = text.indexOf("\n\n---\n");
199
+ return marker === -1 ? text : text.slice(0, marker);
200
+ }
201
+ /**
202
+ * True only for a genuinely successful install manifest.
203
+ *
204
+ * "Successful" is defined against the real response contract rather than
205
+ * "did not throw": the body must parse as a JSON object (not null, not an
206
+ * array), must not be an error envelope, and must carry a nonblank
207
+ * `snapshot_token` — the signed token `apply_install_manifest` requires, and the
208
+ * field whose presence distinguishes a real manifest from any other JSON the
209
+ * endpoint might return.
210
+ *
211
+ * Every parse failure is swallowed and reported as false, leaving gate state
212
+ * unchanged. A non-JSON body, an error response, `{}`, a blank token, or a
213
+ * whitespace-only token must never enable the set.
214
+ */
215
+ export function isSuccessfulInstallManifestResponse(candidate) {
216
+ const text = extractResponseText(candidate);
217
+ if (text === null)
218
+ return false;
219
+ let parsed;
220
+ try {
221
+ parsed = JSON.parse(stripSaveNote(text));
222
+ }
223
+ catch {
224
+ return false;
225
+ }
226
+ if (!parsed || typeof parsed !== "object" || Array.isArray(parsed))
227
+ return false;
228
+ const body = parsed;
229
+ // An explicit error envelope (handleResponse's failure shape, or the
230
+ // TOOL_DISABLED envelope) is never a success, even if it somehow carried a
231
+ // token-shaped field.
232
+ if (typeof body.error === "string")
233
+ return false;
234
+ const token = body.snapshot_token;
235
+ return typeof token === "string" && token.trim() !== "";
236
+ }
237
+ // ---------------------------------------------------------------------------
238
+ // The one-shot gate
239
+ // ---------------------------------------------------------------------------
240
+ /**
241
+ * Build the learn-tool gate over three SDK handles.
242
+ *
243
+ * The gate owns one boolean for the whole set: the three tools are enabled
244
+ * atomically from the server's perspective, so no caller can observe a partially
245
+ * revealed trio, and there is no per-tool dispatch registry to drift.
246
+ */
247
+ export function createLearnToolGate(deps) {
248
+ const sink = deps.sink ?? ((message) => console.error(message));
249
+ let enabled = false;
250
+ let cause = null;
251
+ const ordered = LEARN_GATED_TOOL_NAMES.map((name) => deps.handles[name]);
252
+ return {
253
+ disableAll() {
254
+ for (const handle of ordered)
255
+ handle.disable();
256
+ },
257
+ enableOnce(trigger) {
258
+ if (enabled)
259
+ return;
260
+ // Set the transition state BEFORE touching the handles: `.enable()` drives
261
+ // the SDK update path, which can notify synchronously, and a re-entrant
262
+ // trigger must find the set already transitioned rather than enable twice.
263
+ enabled = true;
264
+ cause = trigger;
265
+ for (const handle of ordered)
266
+ handle.enable();
267
+ // Exactly one structured stderr line per gated-set enable. Follows the
268
+ // established key=value shape (`tool-surface gating: reason=… hidden=…`).
269
+ // The client name is the sanitized display value — never the raw
270
+ // handshake input, and never the normalized matching token.
271
+ sink(`learn tool gating: enabled=${ordered.length} ` +
272
+ `trigger=${trigger} ` +
273
+ `client=${sanitizeClientName(deps.getClientName())} ` +
274
+ `tools=[${LEARN_GATED_TOOL_NAMES.join(",")}]`);
275
+ },
276
+ isEnabled() {
277
+ return enabled;
278
+ },
279
+ enabledBy() {
280
+ return cause;
281
+ },
282
+ };
283
+ }
@@ -5,13 +5,18 @@
5
5
  * profile to a multi-valued group set. BRIDGE_MCP_PROFILE accepts a
6
6
  * comma-separated list of group tokens; unknown tokens are silently dropped
7
7
  * (security sentinel — raw input is never echoed). "core" is always implicitly
8
- * included. "full" is an input shortcut that expands to every known group.
8
+ * included. "full" is an input shortcut that expands to all six known groups.
9
9
  *
10
- * Groups are resolved once at process startup and cannot change mid-session.
10
+ * Groups are resolved once at process startup and cannot change mid-session:
11
+ * changing BRIDGE_MCP_PROFILE requires restarting the MCP server. This is
12
+ * startup-time registration filtering, which is a different mechanism from the
13
+ * session-lifetime SDK enabled-state gating applied to the learn/onboarding
14
+ * trio in index.ts (BAPI-912).
11
15
  */
12
16
  const VALID_GROUPS = new Set([
13
17
  "core",
14
18
  "conductor",
19
+ "estimation",
15
20
  "pipeline-authoring",
16
21
  "sfcc",
17
22
  "sfcc-write",
@@ -20,7 +25,8 @@ const VALID_GROUPS = new Set([
20
25
  * Resolve the active MCP groups from a raw env-var string.
21
26
  *
22
27
  * - `undefined` or blank → `{"core"}` (fail-safe to smallest surface).
23
- * - `"full"` (any case) → all five groups, `sfcc-write` included.
28
+ * - `"full"` (any case) → all six groups, `estimation` and `sfcc-write`
29
+ * included.
24
30
  * - Comma-separated list: each token is trimmed, lowercased, and checked
25
31
  * against VALID_GROUPS. Unknown tokens are dropped. Known tokens are unioned
26
32
  * with the implicit `"core"` baseline.
@@ -38,9 +44,13 @@ export function resolveProfiles(raw) {
38
44
  return baseline;
39
45
  const tokens = trimmed.split(",").map((t) => t.trim().toLowerCase());
40
46
  if (tokens.some((t) => t === "full")) {
47
+ // Deliberately a hard-coded complete expansion, not `VALID_GROUPS`: `full`
48
+ // must name every group so that adding a group is a conscious decision here
49
+ // rather than an implicit widening of an existing user's surface.
41
50
  return new Set([
42
51
  "core",
43
52
  "conductor",
53
+ "estimation",
44
54
  "pipeline-authoring",
45
55
  "sfcc",
46
56
  "sfcc-write",
@@ -30,6 +30,20 @@ export const MCP_SERVER_PACKAGE_NAME = MCP_PACKAGE_NAME;
30
30
  * reintroduce the ETARGET/404 failure the absolute-path form exists to remove.
31
31
  */
32
32
  export const DEFAULT_MCP_SERVER_NPM_CHANNEL_SPEC = `${MCP_PACKAGE_NAME}@latest`;
33
+ /**
34
+ * Detect a packaged-CLI entry path that lives inside a transient npx cache
35
+ * directory (`~/.npm/_npx/<hash>/…` on POSIX, `…\npm-cache\_npx\<hash>\…` on
36
+ * Windows). Such an entry point may be garbage-collected when the npx cache is
37
+ * pruned, so anything that bakes it into durable state (a generated service
38
+ * unit, a registered schedule) would later fail with ENOENT on
39
+ * `node <gone-path>`. Matched on the `_npx` path *segment* so detection is
40
+ * independent of the surrounding cache root and works for either separator.
41
+ */
42
+ export function isTransientNpxEntryPath(entryPath) {
43
+ if (!entryPath)
44
+ return false;
45
+ return /[\\/]_npx[\\/]/.test(entryPath);
46
+ }
33
47
  /**
34
48
  * Build the concrete `{ command, args }` launch pair for a target + worktree from
35
49
  * a structured invocation. Pure — no filesystem or process access. The