@bridge_gpt/mcp-server 0.2.16 → 0.2.19
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CONDUCTOR.md +75 -0
- package/README.md +2 -2
- package/build/agent-capabilities/probe-context.js +13 -3
- package/build/agent-capabilities/probes.js +262 -11
- package/build/agent-capabilities/reporter.js +1 -0
- package/build/agents.generated.js +3 -3
- package/build/backend-warnings.js +44 -0
- package/build/claude-settings.js +129 -0
- package/build/commands.generated.js +7 -6
- package/build/conductor/bridge-api-client.js +198 -18
- package/build/conductor/claude-hook.js +22 -4
- package/build/conductor/cli.js +76 -25
- package/build/conductor/deny-enforcement-preflight.js +96 -0
- package/build/conductor/doctor.js +183 -2
- package/build/conductor/done-gate.js +5 -0
- package/build/conductor/epic-reconcile.js +71 -14
- package/build/conductor/epic-runtime.js +839 -67
- package/build/conductor/epic-state.js +524 -63
- package/build/conductor/errors.js +156 -3
- package/build/conductor/event-accessors.js +252 -0
- package/build/conductor/file-scope-guard.js +201 -0
- package/build/conductor/github-mergeability.js +85 -0
- package/build/conductor/local-merge.js +47 -1
- package/build/conductor/merge-identity.js +41 -0
- package/build/conductor/merge-ledger.js +19 -72
- package/build/conductor/plan.js +12 -2
- package/build/conductor/pr-ci-producer.js +17 -2
- package/build/conductor/pr-discovery.js +11 -1
- package/build/conductor/producer-ledger.js +1 -1
- package/build/conductor/store.js +161 -18
- package/build/conductor/supervisor-config.js +4 -39
- package/build/conductor/supervisor-escalation.js +10 -26
- package/build/conductor/supervisor-ledger.js +5 -12
- package/build/conductor/supervisor-merge.js +32 -5
- package/build/conductor/supervisor-message-relay.js +2 -5
- package/build/conductor/supervisor-notification.js +1 -1
- package/build/conductor/supervisor-runtime.js +12 -54
- package/build/conductor/supervisor-state.js +4 -18
- package/build/conductor/supervisor-types.js +2 -2
- package/build/conductor/taxonomy.js +12 -0
- package/build/conductor/tools.js +28 -6
- package/build/conductor/worker-ledger-cli.js +244 -0
- package/build/conductor-bin.js +1800 -5166
- package/build/conductor-claude-hook-bin.js +4 -2
- package/build/doctor.js +40 -0
- package/build/executor/cli.js +229 -0
- package/build/executor/credentials.js +65 -0
- package/build/executor/deps.js +117 -0
- package/build/executor/env.js +79 -0
- package/build/executor/heartbeat.js +59 -0
- package/build/executor/http-client.js +131 -0
- package/build/executor/index.js +10 -0
- package/build/executor/job-errors.js +55 -0
- package/build/executor/job-log-registry.js +110 -0
- package/build/executor/job-runner.js +688 -0
- package/build/executor/job-types.js +60 -0
- package/build/executor/merge-job.js +155 -0
- package/build/executor/observation.js +123 -0
- package/build/executor/permissions.js +79 -0
- package/build/executor/preflight.js +144 -0
- package/build/executor/process.js +81 -0
- package/build/executor/prompt-spec.js +235 -0
- package/build/executor/results.js +134 -0
- package/build/executor/resume-pre-spawn.js +179 -0
- package/build/executor/runner.js +98 -0
- package/build/executor/terminal-mutation.js +34 -0
- package/build/executor/test-clock.js +109 -0
- package/build/executor/types.js +18 -0
- package/build/executor/verdict-artifact.js +53 -0
- package/build/executor/viewer-tabs.js +78 -0
- package/build/executor/watch-cli.js +113 -0
- package/build/executor/worker-command.js +106 -0
- package/build/executor/worker-finalization.js +97 -0
- package/build/executor/worker-log.js +92 -0
- package/build/executor/worktree-gc.js +134 -0
- package/build/executor/worktree-inspection.js +86 -0
- package/build/executor/worktree.js +103 -0
- package/build/index.js +13950 -9669
- package/build/install-bridge.js +25 -8
- package/build/install-doctor.js +387 -0
- package/build/mcp-invoke.js +19 -3
- package/build/mcp-provisioning.js +31 -25
- package/build/mcp-registration-doctor.js +27 -7
- package/build/mcp-server-invocation.js +152 -0
- package/build/pipelines.generated.js +31 -6
- package/build/readme.generated.js +1 -1
- package/build/regression-check.js +53 -1
- package/build/review-tickets.js +175 -21
- package/build/sfcc/reads-site-preference.js +52 -19
- package/build/start-tickets-conductor.js +47 -99
- package/build/start-tickets-prereqs.js +185 -4
- package/build/start-tickets.js +218 -180
- package/build/version.generated.js +1 -1
- package/build/visual-diff-worker.js +313 -0
- package/build/visual-diff.js +632 -0
- package/build/worktree-core.js +202 -0
- package/package.json +10 -6
- package/pipelines/review-ticket.json +24 -2
- package/public/css/main.min.css +3311 -1
- package/public/css/main.min.css.map +1 -1
- package/public/js/main.min.js +7924 -1
- package/public/js/main.min.js.map +1 -1
- package/smoke-test/SMOKE-TEST.md +5 -2
package/build/install-bridge.js
CHANGED
|
@@ -65,7 +65,14 @@ export function buildPrewarmCommandPreview() {
|
|
|
65
65
|
return `npx ${buildPrewarmArgs().join(" ")}`;
|
|
66
66
|
}
|
|
67
67
|
/** The natural-language sequential prompt handed to the spawned agent session. */
|
|
68
|
-
export const INSTALL_BRIDGE_AGENT_PROMPT = "Please execute the /install-bridge command. Once it completes successfully, execute the
|
|
68
|
+
export const INSTALL_BRIDGE_AGENT_PROMPT = "Please execute the /install-bridge command. Once it completes successfully, execute the " +
|
|
69
|
+
"/learn-repository command. After /learn-repository completes, ask the user for consent to start " +
|
|
70
|
+
"repository indexing; with consent, call the parse_repository MCP tool once and report that the " +
|
|
71
|
+
"job was queued (progress is checked with get_parse_status — do not poll it to completion). " +
|
|
72
|
+
"When you are done, end with an explicit summary line stating how " +
|
|
73
|
+
"many config fields the apply_install_manifest call applied (e.g. 'Applied 8 of 9 derived " +
|
|
74
|
+
"fields') and whether indexing was queued — if 0 fields were applied, say so loudly and explain " +
|
|
75
|
+
"what is still pending.";
|
|
69
76
|
/** Default base URL when `BAPI_BASE_URL` is unset (mirrors index.ts). */
|
|
70
77
|
export const DEFAULT_BAPI_BASE_URL = "https://bridgegpt-api.com";
|
|
71
78
|
/** Default docs dir when `BAPI_DOCS_DIR` is unset (mirrors index.ts). */
|
|
@@ -739,15 +746,19 @@ export async function runInstallBridgeCli(argv, overrides = {}) {
|
|
|
739
746
|
}
|
|
740
747
|
else {
|
|
741
748
|
log(` warning: could not persist the routing credential (${result.kind}). ` +
|
|
742
|
-
`start-tickets model routing may not resolve the key for ${plan.credentialTarget}
|
|
743
|
-
"
|
|
749
|
+
`start-tickets model routing may not resolve the key for ${plan.credentialTarget} ` +
|
|
750
|
+
"and will fail open to the premium/Opus tier (the most expensive) — " +
|
|
751
|
+
"set BAPI_API_KEY in the shell or re-run install-bridge, then verify with " +
|
|
752
|
+
"'npx -y @bridge_gpt/mcp-server doctor'.");
|
|
744
753
|
}
|
|
745
754
|
}
|
|
746
755
|
catch {
|
|
747
756
|
// Fail-open: persistence is best-effort (mirrors Stage 6). The secret is never
|
|
748
757
|
// included in the warning.
|
|
749
758
|
log(" warning: could not persist the routing credential (unexpected error). " +
|
|
750
|
-
"start-tickets model routing may need BAPI_API_KEY in the shell
|
|
759
|
+
"start-tickets model routing may need BAPI_API_KEY in the shell and will fail open " +
|
|
760
|
+
"to the premium/Opus tier (the most expensive) until fixed — verify with " +
|
|
761
|
+
"'npx -y @bridge_gpt/mcp-server doctor'.");
|
|
751
762
|
}
|
|
752
763
|
// ---- Step 5 — spawn a fresh agent session for the agentic remainder ----
|
|
753
764
|
log(`Step 5/5 — opening a ${agent.name} session for /install-bridge + /learn-repository…`);
|
|
@@ -759,14 +770,20 @@ export async function runInstallBridgeCli(argv, overrides = {}) {
|
|
|
759
770
|
if (!spawnResult.ok) {
|
|
760
771
|
// Steps 1–4 (scaffold, config, connectivity-verified, credential persist) all
|
|
761
772
|
// succeeded and are durable; only the best-effort Step 5 tab spawn failed. The
|
|
762
|
-
//
|
|
773
|
+
// deterministic setup is complete, so this is a non-fatal warning (exit 0) — the
|
|
763
774
|
// user can run the agentic remainder by hand. Mirrors start-tickets treating a
|
|
764
775
|
// spawn failure as non-fatal to the run.
|
|
765
|
-
errorLog(`Warning: setup completed, but the agent session could not be opened (${spawnResult.error}). ` +
|
|
766
|
-
"
|
|
776
|
+
errorLog(`Warning: setup steps completed, but the agent session could not be opened (${spawnResult.error}). ` +
|
|
777
|
+
"The project is NOT configured yet — run /install-bridge then /learn-repository manually " +
|
|
778
|
+
"in this project to derive and apply the config fields.");
|
|
767
779
|
return 0;
|
|
768
780
|
}
|
|
769
781
|
log("");
|
|
770
|
-
log(
|
|
782
|
+
log(`install-bridge setup steps complete. A fresh ${agent.name} session is now running ` +
|
|
783
|
+
"/install-bridge then /learn-repository.");
|
|
784
|
+
log("NOTE: the install is not finished until that session's apply reports applied fields — " +
|
|
785
|
+
"it will pause to ask you to approve the project description. Verify afterwards on the " +
|
|
786
|
+
"project's Get Started page (install status panel) or via the session's " +
|
|
787
|
+
"'Applied N of M' summary.");
|
|
771
788
|
return 0;
|
|
772
789
|
}
|
|
@@ -0,0 +1,387 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* install-doctor — an advisory, strictly read-only "Install status" section for
|
|
3
|
+
* the `doctor` subcommand, turning the install manifest's prose done-criteria
|
|
4
|
+
* into an executable checklist.
|
|
5
|
+
*
|
|
6
|
+
* Checks (all best-effort, all read-only GETs, none affect the doctor exit code):
|
|
7
|
+
* - identity — can a repo name / base URL be resolved (env, then the
|
|
8
|
+
* project-local MCP config env blocks)?
|
|
9
|
+
* - credential — does the standard resolver (env → user-scoped store) find
|
|
10
|
+
* a Bridge API key? (never reads or prints the key value)
|
|
11
|
+
* - connectivity — GET /jira/ping (distinguishes rejected key vs unknown repo)
|
|
12
|
+
* - bootstrap — GET /jira/config/install-manifest → N/M bootstrap fields set
|
|
13
|
+
* - integrations — the same manifest's presence-only integrations checklist
|
|
14
|
+
* (unconfigured credentials are configured by a human in the
|
|
15
|
+
* setup UI — never through an agent)
|
|
16
|
+
* - indexing — GET /jira/parse-status → parse job running / idle
|
|
17
|
+
*
|
|
18
|
+
* The section is advisory by contract: any failure inside it degrades to a
|
|
19
|
+
* WARN/SKIP line, never a throw, and the doctor exit code stays driven by the
|
|
20
|
+
* required prerequisites only.
|
|
21
|
+
*/
|
|
22
|
+
import path from "path";
|
|
23
|
+
import { resolveBapiCredentials } from "./credential-store.js";
|
|
24
|
+
/** Default production base URL (mirrors install-bridge's DEFAULT_BAPI_BASE_URL). */
|
|
25
|
+
const DEFAULT_BASE_URL = "https://bridgegpt-api.com";
|
|
26
|
+
/** Project-local MCP configs whose env blocks may carry repo/base-URL values. */
|
|
27
|
+
const MCP_CONFIG_ENV_TARGETS = [
|
|
28
|
+
{ relPath: ".mcp.json", topLevelKey: "mcpServers" },
|
|
29
|
+
{ relPath: ".cursor/mcp.json", topLevelKey: "mcpServers" },
|
|
30
|
+
{ relPath: ".vscode/mcp.json", topLevelKey: "servers" },
|
|
31
|
+
];
|
|
32
|
+
/** Timeout for each read-only probe request. */
|
|
33
|
+
const PROBE_TIMEOUT_MS = 5_000;
|
|
34
|
+
/**
|
|
35
|
+
* Resolve the repo name and base URL: environment first, then the NON-SECRET
|
|
36
|
+
* env-block values of the project-local MCP configs (`BAPI_REPO_NAME` /
|
|
37
|
+
* `BAPI_BASE_URL`). The API key is deliberately NEVER read from config files —
|
|
38
|
+
* secret resolution goes through the standard credential resolver only.
|
|
39
|
+
*/
|
|
40
|
+
export async function resolveInstallDoctorTarget(deps) {
|
|
41
|
+
const envRepo = deps.env.BAPI_REPO_NAME?.trim();
|
|
42
|
+
const envBase = deps.env.BAPI_BASE_URL?.trim();
|
|
43
|
+
let repoName = envRepo && envRepo.length > 0 ? envRepo : null;
|
|
44
|
+
let repoSource = repoName ? "env" : null;
|
|
45
|
+
let baseUrl = envBase && envBase.length > 0 ? envBase : null;
|
|
46
|
+
if (!repoName || !baseUrl) {
|
|
47
|
+
for (const { relPath, topLevelKey } of MCP_CONFIG_ENV_TARGETS) {
|
|
48
|
+
let raw;
|
|
49
|
+
try {
|
|
50
|
+
raw = await deps.readFile(path.join(deps.cwd, relPath));
|
|
51
|
+
}
|
|
52
|
+
catch {
|
|
53
|
+
continue; // absent config is not a finding
|
|
54
|
+
}
|
|
55
|
+
let parsed;
|
|
56
|
+
try {
|
|
57
|
+
parsed = JSON.parse(raw);
|
|
58
|
+
}
|
|
59
|
+
catch {
|
|
60
|
+
continue;
|
|
61
|
+
}
|
|
62
|
+
const envBlock = parsed && typeof parsed === "object"
|
|
63
|
+
? // eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
64
|
+
parsed[topLevelKey]?.["bridge-api"]?.env
|
|
65
|
+
: undefined;
|
|
66
|
+
if (!envBlock)
|
|
67
|
+
continue;
|
|
68
|
+
if (!repoName && typeof envBlock.BAPI_REPO_NAME === "string" && envBlock.BAPI_REPO_NAME.trim()) {
|
|
69
|
+
repoName = envBlock.BAPI_REPO_NAME.trim();
|
|
70
|
+
repoSource = "config";
|
|
71
|
+
}
|
|
72
|
+
if (!baseUrl && typeof envBlock.BAPI_BASE_URL === "string" && envBlock.BAPI_BASE_URL.trim()) {
|
|
73
|
+
baseUrl = envBlock.BAPI_BASE_URL.trim();
|
|
74
|
+
}
|
|
75
|
+
if (repoName && baseUrl)
|
|
76
|
+
break;
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
return { repoName, repoSource, baseUrl: baseUrl ?? DEFAULT_BASE_URL };
|
|
80
|
+
}
|
|
81
|
+
/** Read-only GET with a bounded timeout. Never throws. */
|
|
82
|
+
async function probeGet(deps, url, apiKey) {
|
|
83
|
+
try {
|
|
84
|
+
const resp = await deps.fetch(url, {
|
|
85
|
+
headers: { "X-API-Key": apiKey },
|
|
86
|
+
signal: AbortSignal.timeout(PROBE_TIMEOUT_MS),
|
|
87
|
+
});
|
|
88
|
+
let body = null;
|
|
89
|
+
try {
|
|
90
|
+
body = await resp.json();
|
|
91
|
+
}
|
|
92
|
+
catch {
|
|
93
|
+
/* non-JSON body — status alone is still useful */
|
|
94
|
+
}
|
|
95
|
+
return { ok: true, status: resp.status, body };
|
|
96
|
+
}
|
|
97
|
+
catch (e) {
|
|
98
|
+
return { ok: false, error: e instanceof Error ? e.message : String(e) };
|
|
99
|
+
}
|
|
100
|
+
}
|
|
101
|
+
/** Count set/total bootstrap fields from an install-manifest response body. */
|
|
102
|
+
function summarizeManifestGroups(body) {
|
|
103
|
+
if (!body || typeof body !== "object")
|
|
104
|
+
return null;
|
|
105
|
+
const groups = body.groups;
|
|
106
|
+
if (!Array.isArray(groups))
|
|
107
|
+
return null;
|
|
108
|
+
let total = 0;
|
|
109
|
+
const unset = [];
|
|
110
|
+
for (const group of groups) {
|
|
111
|
+
const fields = group?.fields;
|
|
112
|
+
if (!Array.isArray(fields))
|
|
113
|
+
continue;
|
|
114
|
+
for (const field of fields) {
|
|
115
|
+
const name = field?.field_name;
|
|
116
|
+
if (typeof name !== "string")
|
|
117
|
+
continue;
|
|
118
|
+
total += 1;
|
|
119
|
+
if (!field.is_set)
|
|
120
|
+
unset.push(name);
|
|
121
|
+
}
|
|
122
|
+
}
|
|
123
|
+
return { total, unset };
|
|
124
|
+
}
|
|
125
|
+
/** Summarize the manifest's integrations checklist (presence booleans only). */
|
|
126
|
+
function summarizeIntegrations(body) {
|
|
127
|
+
if (!body || typeof body !== "object")
|
|
128
|
+
return null;
|
|
129
|
+
const integrations = body.integrations;
|
|
130
|
+
if (!Array.isArray(integrations))
|
|
131
|
+
return null;
|
|
132
|
+
let total = 0;
|
|
133
|
+
const unconfigured = [];
|
|
134
|
+
for (const item of integrations) {
|
|
135
|
+
const label = item?.label;
|
|
136
|
+
if (typeof label !== "string")
|
|
137
|
+
continue;
|
|
138
|
+
total += 1;
|
|
139
|
+
if (!item.is_configured)
|
|
140
|
+
unconfigured.push(label);
|
|
141
|
+
}
|
|
142
|
+
return { total, unconfigured };
|
|
143
|
+
}
|
|
144
|
+
/**
|
|
145
|
+
* Collect the install-status checklist. Never throws; every failure path becomes
|
|
146
|
+
* a WARN or SKIP check line. Performs at most three read-only GETs, and only
|
|
147
|
+
* when both a repo name and a credential resolve.
|
|
148
|
+
*/
|
|
149
|
+
export async function collectInstallStatusChecks(deps) {
|
|
150
|
+
const checks = [];
|
|
151
|
+
const target = await resolveInstallDoctorTarget(deps);
|
|
152
|
+
if (!target.repoName) {
|
|
153
|
+
checks.push({
|
|
154
|
+
id: "identity",
|
|
155
|
+
label: "Repository identity",
|
|
156
|
+
status: "SKIP",
|
|
157
|
+
detail: "no BAPI_REPO_NAME in the environment or project-local MCP configs",
|
|
158
|
+
remediation: "run install-bridge (or set BAPI_REPO_NAME) to configure this project.",
|
|
159
|
+
});
|
|
160
|
+
return checks;
|
|
161
|
+
}
|
|
162
|
+
checks.push({
|
|
163
|
+
id: "identity",
|
|
164
|
+
label: "Repository identity",
|
|
165
|
+
status: "PASS",
|
|
166
|
+
detail: `${target.repoName} (from ${target.repoSource}), base URL ${target.baseUrl}`,
|
|
167
|
+
});
|
|
168
|
+
const credDeps = {
|
|
169
|
+
env: deps.env,
|
|
170
|
+
homedir: deps.homedir,
|
|
171
|
+
platform: deps.platform,
|
|
172
|
+
readFile: deps.readFile,
|
|
173
|
+
stat: deps.stat,
|
|
174
|
+
stderr: () => { },
|
|
175
|
+
};
|
|
176
|
+
const cred = await resolveBapiCredentials(target.repoName, credDeps);
|
|
177
|
+
if (!cred.ok) {
|
|
178
|
+
checks.push({
|
|
179
|
+
id: "credential",
|
|
180
|
+
label: "Bridge API credential",
|
|
181
|
+
status: "WARN",
|
|
182
|
+
detail: `not resolved (${cred.kind})`,
|
|
183
|
+
remediation: "set BAPI_API_KEY or persist it via /install-bridge / the credentials subcommand; " +
|
|
184
|
+
"until then the remaining install checks are skipped.",
|
|
185
|
+
});
|
|
186
|
+
for (const [id, label] of [
|
|
187
|
+
["connectivity", "Server connectivity"],
|
|
188
|
+
["bootstrap", "Bootstrap config fields"],
|
|
189
|
+
["indexing", "Repository indexing"],
|
|
190
|
+
]) {
|
|
191
|
+
checks.push({ id, label, status: "SKIP", detail: "no credential resolved" });
|
|
192
|
+
}
|
|
193
|
+
return checks;
|
|
194
|
+
}
|
|
195
|
+
checks.push({
|
|
196
|
+
id: "credential",
|
|
197
|
+
label: "Bridge API credential",
|
|
198
|
+
status: "PASS",
|
|
199
|
+
detail: `resolved from ${cred.credentials.source} (value never read into the report)`,
|
|
200
|
+
});
|
|
201
|
+
const apiKey = cred.credentials.apiKey;
|
|
202
|
+
const repoQuery = `repo_name=${encodeURIComponent(target.repoName)}`;
|
|
203
|
+
// --- connectivity ---
|
|
204
|
+
const ping = await probeGet(deps, `${target.baseUrl}/jira/ping?${repoQuery}`, apiKey);
|
|
205
|
+
if (!ping.ok) {
|
|
206
|
+
checks.push({
|
|
207
|
+
id: "connectivity",
|
|
208
|
+
label: "Server connectivity",
|
|
209
|
+
status: "WARN",
|
|
210
|
+
detail: `unreachable (${ping.error})`,
|
|
211
|
+
remediation: `check ${target.baseUrl} and your network, then re-run doctor.`,
|
|
212
|
+
});
|
|
213
|
+
}
|
|
214
|
+
else if (ping.status === 200) {
|
|
215
|
+
checks.push({ id: "connectivity", label: "Server connectivity", status: "PASS", detail: "ping OK" });
|
|
216
|
+
}
|
|
217
|
+
else if (ping.status === 401 || ping.status === 403) {
|
|
218
|
+
checks.push({
|
|
219
|
+
id: "connectivity",
|
|
220
|
+
label: "Server connectivity",
|
|
221
|
+
status: "WARN",
|
|
222
|
+
detail: `the server rejected the resolved key (HTTP ${ping.status})`,
|
|
223
|
+
remediation: "the key may have been rotated — re-run install-bridge with a current key.",
|
|
224
|
+
});
|
|
225
|
+
}
|
|
226
|
+
else if (ping.status === 404) {
|
|
227
|
+
checks.push({
|
|
228
|
+
id: "connectivity",
|
|
229
|
+
label: "Server connectivity",
|
|
230
|
+
status: "WARN",
|
|
231
|
+
detail: `the server does not know repo '${target.repoName}' (HTTP 404)`,
|
|
232
|
+
remediation: "create the project in the setup UI (or check BAPI_REPO_NAME spelling).",
|
|
233
|
+
});
|
|
234
|
+
}
|
|
235
|
+
else {
|
|
236
|
+
checks.push({
|
|
237
|
+
id: "connectivity",
|
|
238
|
+
label: "Server connectivity",
|
|
239
|
+
status: "WARN",
|
|
240
|
+
detail: `unexpected HTTP ${ping.status} from /jira/ping`,
|
|
241
|
+
});
|
|
242
|
+
}
|
|
243
|
+
// --- bootstrap fields ---
|
|
244
|
+
const manifest = await probeGet(deps, `${target.baseUrl}/jira/config/install-manifest?${repoQuery}`, apiKey);
|
|
245
|
+
if (!manifest.ok) {
|
|
246
|
+
checks.push({
|
|
247
|
+
id: "bootstrap",
|
|
248
|
+
label: "Bootstrap config fields",
|
|
249
|
+
status: "WARN",
|
|
250
|
+
detail: `manifest unreachable (${manifest.error})`,
|
|
251
|
+
});
|
|
252
|
+
}
|
|
253
|
+
else if (manifest.status === 404) {
|
|
254
|
+
checks.push({
|
|
255
|
+
id: "bootstrap",
|
|
256
|
+
label: "Bootstrap config fields",
|
|
257
|
+
status: "WARN",
|
|
258
|
+
detail: "no configuration row found for this repository",
|
|
259
|
+
remediation: "create the project in the setup UI, then run /install-bridge.",
|
|
260
|
+
});
|
|
261
|
+
}
|
|
262
|
+
else if (manifest.status === 200) {
|
|
263
|
+
const summary = summarizeManifestGroups(manifest.body);
|
|
264
|
+
if (!summary) {
|
|
265
|
+
checks.push({
|
|
266
|
+
id: "bootstrap",
|
|
267
|
+
label: "Bootstrap config fields",
|
|
268
|
+
status: "WARN",
|
|
269
|
+
detail: "manifest response had an unexpected shape",
|
|
270
|
+
});
|
|
271
|
+
}
|
|
272
|
+
else if (summary.unset.length === 0) {
|
|
273
|
+
checks.push({
|
|
274
|
+
id: "bootstrap",
|
|
275
|
+
label: "Bootstrap config fields",
|
|
276
|
+
status: "PASS",
|
|
277
|
+
detail: `${summary.total}/${summary.total} bootstrap fields set`,
|
|
278
|
+
});
|
|
279
|
+
}
|
|
280
|
+
else {
|
|
281
|
+
checks.push({
|
|
282
|
+
id: "bootstrap",
|
|
283
|
+
label: "Bootstrap config fields",
|
|
284
|
+
status: "WARN",
|
|
285
|
+
detail: `${summary.total - summary.unset.length}/${summary.total} set; unset: ${summary.unset.join(", ")}`,
|
|
286
|
+
remediation: "run /install-bridge to derive the unset fields (intentionally-unset fields are fine to leave).",
|
|
287
|
+
});
|
|
288
|
+
}
|
|
289
|
+
}
|
|
290
|
+
else {
|
|
291
|
+
checks.push({
|
|
292
|
+
id: "bootstrap",
|
|
293
|
+
label: "Bootstrap config fields",
|
|
294
|
+
status: "WARN",
|
|
295
|
+
detail: `unexpected HTTP ${manifest.status} from the install manifest`,
|
|
296
|
+
});
|
|
297
|
+
}
|
|
298
|
+
// --- integrations (from the same manifest response; presence booleans only) ---
|
|
299
|
+
if (manifest.ok && manifest.status === 200) {
|
|
300
|
+
const integrations = summarizeIntegrations(manifest.body);
|
|
301
|
+
if (integrations === null) {
|
|
302
|
+
checks.push({
|
|
303
|
+
id: "integrations",
|
|
304
|
+
label: "Integration credentials",
|
|
305
|
+
status: "SKIP",
|
|
306
|
+
detail: "the manifest response carried no integrations checklist",
|
|
307
|
+
});
|
|
308
|
+
}
|
|
309
|
+
else if (integrations.unconfigured.length === 0) {
|
|
310
|
+
checks.push({
|
|
311
|
+
id: "integrations",
|
|
312
|
+
label: "Integration credentials",
|
|
313
|
+
status: "PASS",
|
|
314
|
+
detail: `${integrations.total}/${integrations.total} configured`,
|
|
315
|
+
});
|
|
316
|
+
}
|
|
317
|
+
else {
|
|
318
|
+
checks.push({
|
|
319
|
+
id: "integrations",
|
|
320
|
+
label: "Integration credentials",
|
|
321
|
+
status: "WARN",
|
|
322
|
+
detail: `not configured: ${integrations.unconfigured.join(", ")}`,
|
|
323
|
+
remediation: "a human configures these in the setup UI (project settings) — Bridge API never accepts " +
|
|
324
|
+
"integration secrets through an agent or MCP tool.",
|
|
325
|
+
});
|
|
326
|
+
}
|
|
327
|
+
}
|
|
328
|
+
else {
|
|
329
|
+
checks.push({
|
|
330
|
+
id: "integrations",
|
|
331
|
+
label: "Integration credentials",
|
|
332
|
+
status: "SKIP",
|
|
333
|
+
detail: "manifest unavailable",
|
|
334
|
+
});
|
|
335
|
+
}
|
|
336
|
+
// --- indexing ---
|
|
337
|
+
const parse = await probeGet(deps, `${target.baseUrl}/jira/parse-status?${repoQuery}`, apiKey);
|
|
338
|
+
if (!parse.ok || parse.status !== 200) {
|
|
339
|
+
checks.push({
|
|
340
|
+
id: "indexing",
|
|
341
|
+
label: "Repository indexing",
|
|
342
|
+
status: "SKIP",
|
|
343
|
+
detail: parse.ok ? `HTTP ${parse.status} from /jira/parse-status` : `unreachable (${parse.error})`,
|
|
344
|
+
});
|
|
345
|
+
}
|
|
346
|
+
else {
|
|
347
|
+
const status = parse.body?.status;
|
|
348
|
+
if (status === "in_progress") {
|
|
349
|
+
const startedAt = parse.body.started_at;
|
|
350
|
+
checks.push({
|
|
351
|
+
id: "indexing",
|
|
352
|
+
label: "Repository indexing",
|
|
353
|
+
status: "INFO",
|
|
354
|
+
detail: `parse job in progress${typeof startedAt === "string" ? ` (started ${startedAt})` : ""}`,
|
|
355
|
+
});
|
|
356
|
+
}
|
|
357
|
+
else {
|
|
358
|
+
checks.push({
|
|
359
|
+
id: "indexing",
|
|
360
|
+
label: "Repository indexing",
|
|
361
|
+
status: "INFO",
|
|
362
|
+
detail: "no parse job currently running — if this repository has never been indexed, " +
|
|
363
|
+
"queue one with /parse-repository (monitor with get_parse_status).",
|
|
364
|
+
});
|
|
365
|
+
}
|
|
366
|
+
}
|
|
367
|
+
return checks;
|
|
368
|
+
}
|
|
369
|
+
/** Render the install-status section of the doctor report (pure formatting). */
|
|
370
|
+
export function formatInstallStatusReport(checks) {
|
|
371
|
+
const lines = ["", "Install status (easy-install done criteria — advisory)", ""];
|
|
372
|
+
const pad = {
|
|
373
|
+
PASS: "PASS ",
|
|
374
|
+
WARN: "WARN ",
|
|
375
|
+
INFO: "INFO ",
|
|
376
|
+
SKIP: "SKIPPED",
|
|
377
|
+
};
|
|
378
|
+
for (const check of checks) {
|
|
379
|
+
lines.push(`${pad[check.status]} ${check.label}${check.detail ? ` — ${check.detail}` : ""}`);
|
|
380
|
+
if (check.remediation) {
|
|
381
|
+
lines.push(` ${check.remediation}`);
|
|
382
|
+
}
|
|
383
|
+
}
|
|
384
|
+
lines.push("");
|
|
385
|
+
lines.push("This section is advisory and never changes the doctor exit code. It performs read-only GETs only.");
|
|
386
|
+
return lines.join("\n");
|
|
387
|
+
}
|
package/build/mcp-invoke.js
CHANGED
|
@@ -32,10 +32,15 @@ import { getThirdPartyTargetDefinition, resolveThirdPartyTargetEnv, validateThir
|
|
|
32
32
|
export function getMcpInvokeUsage() {
|
|
33
33
|
return [
|
|
34
34
|
"Usage:",
|
|
35
|
-
"
|
|
35
|
+
" node <abs>/mcp_server/build/index.js mcp-invoke --target <target> --project-root <ABS_WORKTREE_PATH>",
|
|
36
|
+
"",
|
|
37
|
+
" (npm-channel fallback may invoke the same shim through a package spec, e.g.",
|
|
38
|
+
" npx -y @bridge_gpt/mcp-server@latest mcp-invoke --target <target> --project-root <ABS_WORKTREE_PATH>)",
|
|
36
39
|
"",
|
|
37
40
|
"Internal worktree shim: resolves the target's launch command and credentials",
|
|
38
41
|
"from the given project root, then spawns the real MCP server over stdio.",
|
|
42
|
+
"Argument parsing is identical regardless of how this process was launched",
|
|
43
|
+
"(absolute-path node invocation vs npm-channel npx).",
|
|
39
44
|
"",
|
|
40
45
|
"Flags:",
|
|
41
46
|
" --target <target> MCP target to launch. 'bapi' launches the Bridge",
|
|
@@ -264,11 +269,22 @@ export async function resolveBapiInvocation(projectRoot, deps) {
|
|
|
264
269
|
if (!credResult.ok) {
|
|
265
270
|
return { ok: false, error: `failed to resolve credentials: ${credResult.error}` };
|
|
266
271
|
}
|
|
267
|
-
const
|
|
272
|
+
const overlay = {
|
|
268
273
|
BAPI_API_KEY: credResult.credentials.apiKey,
|
|
269
274
|
BAPI_PROJECT_ROOT: projectRoot,
|
|
270
275
|
BAPI_REPO_NAME: repoName,
|
|
271
|
-
}
|
|
276
|
+
};
|
|
277
|
+
// BAPI-527: `CONDUCTOR_NODE_PATH` is NON-SECRET operational metadata (the
|
|
278
|
+
// captured conductor Node path used by the worker-ledger CLI subprocess).
|
|
279
|
+
// `buildChildEnv` already inherits the full parent env, so this re-affirms the
|
|
280
|
+
// pass-through explicitly and ONLY when present — it is a non-secret allowlist
|
|
281
|
+
// entry, never a credential. It is emphatically NOT the MCP server's re-exec
|
|
282
|
+
// command: the spawned command stays `deps.execPath` + `[scriptPath]` below.
|
|
283
|
+
const conductorNodePath = deps.env.CONDUCTOR_NODE_PATH;
|
|
284
|
+
if (typeof conductorNodePath === "string" && conductorNodePath.trim().length > 0) {
|
|
285
|
+
overlay.CONDUCTOR_NODE_PATH = conductorNodePath;
|
|
286
|
+
}
|
|
287
|
+
const env = buildChildEnv(deps.env, overlay);
|
|
272
288
|
return {
|
|
273
289
|
ok: true,
|
|
274
290
|
invocation: { command: deps.execPath, args: [deps.scriptPath], env },
|
|
@@ -18,9 +18,9 @@
|
|
|
18
18
|
* to avoid a runtime import cycle.
|
|
19
19
|
*/
|
|
20
20
|
import path from "path";
|
|
21
|
-
import { VERSION } from "./version.generated.js";
|
|
22
21
|
import { readBridgeConfig } from "./bridge-config.js";
|
|
23
22
|
import { getThirdPartyTargetDefinition, validateThirdPartyTargetManifestEntry, } from "./third-party-mcp-targets.js";
|
|
23
|
+
import { buildMcpShimCommand, } from "./mcp-server-invocation.js";
|
|
24
24
|
// ---------------------------------------------------------------------------
|
|
25
25
|
// Pure helpers
|
|
26
26
|
// ---------------------------------------------------------------------------
|
|
@@ -55,27 +55,27 @@ export function serverNameForMcpTarget(target) {
|
|
|
55
55
|
return target === "bapi" ? "bridge-api" : target;
|
|
56
56
|
}
|
|
57
57
|
/**
|
|
58
|
-
* Build a secret-free shim entry for any target
|
|
59
|
-
* carry the target name plus the absolute project root; there is
|
|
60
|
-
* no `env` block.
|
|
58
|
+
* Build a secret-free shim entry for any target from a structured invocation.
|
|
59
|
+
* The command/args carry the target name plus the absolute project root; there is
|
|
60
|
+
* intentionally no `env` block. The absolute-build-path form serializes as
|
|
61
|
+
* `{ command: "node", args: ["<abs>/build/index.js", "mcp-invoke", …] }` (the
|
|
62
|
+
* node executable is a single substitutable field), and the npm-channel fallback
|
|
63
|
+
* as `{ command: "npx", args: ["-y", "<spec>", "mcp-invoke", …] }`.
|
|
64
|
+
*
|
|
65
|
+
* BAPI-527: `.mcp.json` remains intentionally ENV-FREE. `CONDUCTOR_NODE_PATH`
|
|
66
|
+
* (the captured conductor Node path for the worker-ledger CLI subprocess) is NOT
|
|
67
|
+
* written here — it is inherited at runtime from the worker SHELL env that
|
|
68
|
+
* `start-tickets` injects at the spawn boundary, never stored as a registration
|
|
69
|
+
* value. Keeping the shim env-free preserves the credential-safety invariant
|
|
70
|
+
* (secrets are resolved by the `mcp-invoke` shim at launch, not persisted in the
|
|
71
|
+
* worktree) and equally avoids persisting non-secret operational metadata here.
|
|
61
72
|
*/
|
|
62
|
-
export function buildShimMcpServerEntry(target, absoluteWorktreePath) {
|
|
63
|
-
return
|
|
64
|
-
command: "npx",
|
|
65
|
-
args: [
|
|
66
|
-
"-y",
|
|
67
|
-
`@bridge_gpt/mcp-server@${VERSION}`,
|
|
68
|
-
"mcp-invoke",
|
|
69
|
-
"--target",
|
|
70
|
-
target,
|
|
71
|
-
"--project-root",
|
|
72
|
-
absoluteWorktreePath,
|
|
73
|
-
],
|
|
74
|
-
};
|
|
73
|
+
export function buildShimMcpServerEntry(target, absoluteWorktreePath, invocation) {
|
|
74
|
+
return buildMcpShimCommand(invocation, target, absoluteWorktreePath);
|
|
75
75
|
}
|
|
76
76
|
/** Back-compat wrapper around `buildShimMcpServerEntry("bapi", ...)`. */
|
|
77
|
-
export function buildBridgeApiShimMcpServerEntry(absoluteWorktreePath) {
|
|
78
|
-
return buildShimMcpServerEntry("bapi", absoluteWorktreePath);
|
|
77
|
+
export function buildBridgeApiShimMcpServerEntry(absoluteWorktreePath, invocation) {
|
|
78
|
+
return buildShimMcpServerEntry("bapi", absoluteWorktreePath, invocation);
|
|
79
79
|
}
|
|
80
80
|
/**
|
|
81
81
|
* Convert all supported manifest `mcp` entries into a map of server name -> shim
|
|
@@ -84,12 +84,12 @@ export function buildBridgeApiShimMcpServerEntry(absoluteWorktreePath) {
|
|
|
84
84
|
* or incomplete non-`bapi` targets are skipped with a secret-free warning (they
|
|
85
85
|
* never appear in the returned entries).
|
|
86
86
|
*/
|
|
87
|
-
export function buildMcpServerEntriesForManifest(manifest, absoluteWorktreePath) {
|
|
87
|
+
export function buildMcpServerEntriesForManifest(manifest, absoluteWorktreePath, invocation) {
|
|
88
88
|
const entries = {};
|
|
89
89
|
const warnings = [];
|
|
90
90
|
for (const mcp of manifest.mcp) {
|
|
91
91
|
if (mcp.target === "bapi") {
|
|
92
|
-
entries[serverNameForMcpTarget("bapi")] = buildShimMcpServerEntry("bapi", absoluteWorktreePath);
|
|
92
|
+
entries[serverNameForMcpTarget("bapi")] = buildShimMcpServerEntry("bapi", absoluteWorktreePath, invocation);
|
|
93
93
|
continue;
|
|
94
94
|
}
|
|
95
95
|
const definition = getThirdPartyTargetDefinition(mcp.target);
|
|
@@ -102,9 +102,9 @@ export function buildMcpServerEntriesForManifest(manifest, absoluteWorktreePath)
|
|
|
102
102
|
warnings.push(`MCP target '${mcp.target}' registration skipped: ${validation.error}.`);
|
|
103
103
|
continue;
|
|
104
104
|
}
|
|
105
|
-
entries[serverNameForMcpTarget(mcp.target)] = buildShimMcpServerEntry(mcp.target, absoluteWorktreePath);
|
|
105
|
+
entries[serverNameForMcpTarget(mcp.target)] = buildShimMcpServerEntry(mcp.target, absoluteWorktreePath, invocation);
|
|
106
106
|
}
|
|
107
|
-
return { entries, warnings };
|
|
107
|
+
return { entries, warnings, registrationForm: invocation.form };
|
|
108
108
|
}
|
|
109
109
|
/** Both registration files written for every provisioned worktree. */
|
|
110
110
|
export function getWorktreeMcpRegistrationTargets(worktreePath, platform) {
|
|
@@ -305,7 +305,7 @@ export async function provisionMcpRegistrationForWorktree(row, deps) {
|
|
|
305
305
|
if (!normalized.ok) {
|
|
306
306
|
return { ...row, status: "spawn-failed", error: `MCP provisioning failed: ${normalized.error}` };
|
|
307
307
|
}
|
|
308
|
-
const built = buildMcpServerEntriesForManifest(read.manifest, normalized.path);
|
|
308
|
+
const built = buildMcpServerEntriesForManifest(read.manifest, normalized.path, deps.mcpServerInvocation);
|
|
309
309
|
if (Object.keys(built.entries).length === 0) {
|
|
310
310
|
// Nothing supported to write (e.g. a manifest with no bapi target and no
|
|
311
311
|
// supported Tier-2 targets). Surface any warnings but leave status unchanged.
|
|
@@ -318,10 +318,16 @@ export async function provisionMcpRegistrationForWorktree(row, deps) {
|
|
|
318
318
|
return { ...row, status: "spawn-failed", error: `MCP provisioning failed: ${result.error}` };
|
|
319
319
|
}
|
|
320
320
|
}
|
|
321
|
+
// Record the resolved registration form for audit (path-free — the form label
|
|
322
|
+
// only, never the absolute server entry path, which could leak a username).
|
|
323
|
+
let result = { ...row, mcpRegistrationForm: built.registrationForm };
|
|
324
|
+
result = withWarnings(result, built.warnings);
|
|
325
|
+
if (built.registrationForm !== "absolute-build-path") {
|
|
326
|
+
result = withWarning(result, "MCP registration used npm-channel fallback because an on-disk build entry was not resolvable.");
|
|
327
|
+
}
|
|
321
328
|
// Pre-approve the just-registered servers so Claude Code does not prompt to
|
|
322
329
|
// trust them in this fresh worktree path. Best-effort: a failure here only
|
|
323
330
|
// loses the convenience of skipping the prompt, so warn and still spawn.
|
|
324
|
-
let result = withWarnings(row, built.warnings);
|
|
325
331
|
const serverNames = Object.keys(built.entries);
|
|
326
332
|
const trust = await writeClaudeServerTrustSettings(normalized.path, serverNames, deps);
|
|
327
333
|
if (!trust.ok) {
|
|
@@ -35,22 +35,42 @@ function flagValue(args, flag) {
|
|
|
35
35
|
return undefined;
|
|
36
36
|
}
|
|
37
37
|
/**
|
|
38
|
-
* True when `entry` is a secret-free Bridge API shim registration
|
|
39
|
-
*
|
|
40
|
-
*
|
|
41
|
-
*
|
|
38
|
+
* True when `entry` is a secret-free Bridge API shim registration pointing at
|
|
39
|
+
* THIS worktree. Two forms are accepted (BAPI-526):
|
|
40
|
+
*
|
|
41
|
+
* - Absolute-build-path (PRIMARY): an absolute `node` (or absolute Node-path)
|
|
42
|
+
* invocation of the conductor's own compiled server, e.g.
|
|
43
|
+
* `{ command: "node", args: ["<abs>/mcp_server/build/index.js", "mcp-invoke",
|
|
44
|
+
* "--target", "bapi", "--project-root", "<worktreeRoot>"] }`. `command` may be
|
|
45
|
+
* any non-empty string ("node" or a future absolute Node path); `args[0]` is a
|
|
46
|
+
* non-empty server entry path.
|
|
47
|
+
* - npm-channel (LEGACY/FALLBACK): `{ command: "npx", args: ["-y",
|
|
48
|
+
* "@bridge_gpt/mcp-server@<spec>", "mcp-invoke", "--target", "bapi",
|
|
49
|
+
* "--project-root", "<worktreeRoot>"] }`.
|
|
50
|
+
*
|
|
51
|
+
* In both forms `--target` must be `bapi` and `--project-root` must match the
|
|
52
|
+
* given worktree exactly. The server entry path is NOT required to sit under any
|
|
53
|
+
* particular username/path prefix.
|
|
42
54
|
*/
|
|
43
55
|
export function isBridgeApiShimEntry(entry, worktreeRoot) {
|
|
44
56
|
if (!entry || typeof entry !== "object" || Array.isArray(entry))
|
|
45
57
|
return false;
|
|
46
58
|
const candidate = entry;
|
|
47
|
-
if (candidate.command !== "
|
|
59
|
+
if (typeof candidate.command !== "string" || candidate.command.length === 0)
|
|
48
60
|
return false;
|
|
49
61
|
if (!Array.isArray(candidate.args))
|
|
50
62
|
return false;
|
|
51
63
|
const args = candidate.args.filter((a) => typeof a === "string");
|
|
52
|
-
if (
|
|
53
|
-
|
|
64
|
+
if (candidate.command === "npx") {
|
|
65
|
+
// npm-channel fallback: must reference the package spec.
|
|
66
|
+
if (!args.some((a) => a.startsWith("@bridge_gpt/mcp-server")))
|
|
67
|
+
return false;
|
|
68
|
+
}
|
|
69
|
+
else {
|
|
70
|
+
// Absolute-build-path: first arg must be a non-empty server entry path.
|
|
71
|
+
if (typeof args[0] !== "string" || args[0].length === 0)
|
|
72
|
+
return false;
|
|
73
|
+
}
|
|
54
74
|
if (!args.includes("mcp-invoke"))
|
|
55
75
|
return false;
|
|
56
76
|
if (flagValue(args, "--target") !== "bapi")
|