@bridge_gpt/mcp-server 0.2.51 → 0.2.52
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/README.md +24 -8
- package/build/agent-capabilities/probe-context.js +15 -7
- package/build/agent-capabilities/probes.js +42 -6
- package/build/agent-launchers/claude-executor-adapter.js +98 -14
- package/build/commands.generated.js +1 -1
- package/build/conduct-epic/cut-protocol.js +17 -3
- package/build/conductor/bridge-api-client.js +171 -5
- package/build/conductor/deny-enforcement-preflight.js +107 -10
- package/build/conductor/local-merge.js +170 -11
- package/build/conductor-bin.js +2 -2
- package/build/connect-bitbucket-api.js +370 -0
- package/build/connect-bitbucket.js +437 -0
- package/build/docs.generated.js +1 -1
- package/build/doctor.js +40 -1
- package/build/drive-epic.js +423 -11
- package/build/env-file-link.js +164 -0
- package/build/epic-integration-pr.js +10 -0
- package/build/executor/cli.js +41 -6
- package/build/executor/deps.js +5 -1
- package/build/executor/env-file-guard.js +113 -0
- package/build/executor/env.js +78 -1
- package/build/executor/heartbeat.js +9 -0
- package/build/executor/http-client.js +90 -22
- package/build/executor/job-errors.js +43 -2
- package/build/executor/job-runner.js +130 -28
- package/build/executor/merge-job.js +67 -16
- package/build/executor/permissions.js +106 -0
- package/build/executor/preflight.js +38 -13
- package/build/executor/resume-pre-spawn.js +2 -1
- package/build/executor/runner.js +175 -4
- package/build/executor/service-unit.js +15 -0
- package/build/executor/terminal-mutation.js +22 -1
- package/build/executor/types.js +86 -0
- package/build/executor/worker-command.js +21 -5
- package/build/executor/worker-guard-hook.js +939 -0
- package/build/executor/worker-log.js +56 -0
- package/build/executor/worktree.js +11 -0
- package/build/git-reachability.js +147 -0
- package/build/index.js +514 -121
- package/build/install-bridge.js +95 -0
- package/build/pipelines.generated.js +5 -3
- package/build/plan-epic-conductor-eligibility.js +37 -7
- package/build/plane/cli.js +78 -15
- package/build/plane/defaults.js +165 -0
- package/build/plane/manifest.js +63 -8
- package/build/plane/member-logs.js +6 -0
- package/build/plane/member-roster.js +195 -11
- package/build/plane/preflight.js +43 -0
- package/build/plane/shutdown.js +25 -3
- package/build/plane/status.js +11 -0
- package/build/plane/supervisor.js +343 -14
- package/build/plane/test-fakes.js +43 -0
- package/build/plane/types.js +82 -11
- package/build/pr-base-contract.js +20 -0
- package/build/readme.generated.js +1 -1
- package/build/review-synthesis-config.js +60 -0
- package/build/scripts/executor-protocol-contract-driver.js +311 -0
- package/build/setup-epic.js +560 -139
- package/build/sfcc/log-query.js +2 -1
- package/build/start-tickets-conductor.js +11 -2
- package/build/start-tickets.js +69 -2
- package/build/version.generated.js +3 -3
- package/build/worker-containment-diagnostic.js +97 -0
- package/build/worker-guard-hook-bin.js +6 -0
- package/docs/CONDUCTOR.md +27 -0
- package/docs/install/mcp-tool-integrations.md +3 -2
- package/package.json +3 -2
|
@@ -0,0 +1,311 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
/**
|
|
3
|
+
* Executor `/executor/jobs/*` protocol contract driver (BAPI-997, T4).
|
|
4
|
+
*
|
|
5
|
+
* A Python `subprocess` boundary that lets the real pytest suite drive the
|
|
6
|
+
* REAL, emitted TypeScript executor HTTP client (`createExecutorHttpClient` in
|
|
7
|
+
* `../executor/http-client.js`, built from `mcp_server/src/executor/`) against
|
|
8
|
+
* the real FastAPI `/executor/jobs/*` routes, and separately projects the
|
|
9
|
+
* TypeScript protocol's closed vocabularies and structural request/response
|
|
10
|
+
* shapes into canonical JSON so a Python test can compare them against the
|
|
11
|
+
* server's Pydantic models — WITHOUT either side hand-maintaining a duplicate
|
|
12
|
+
* description of the other's contract.
|
|
13
|
+
*
|
|
14
|
+
* This module is deliberately NOT registered as an MCP tool
|
|
15
|
+
* (`server.registerTool`) and is NOT listed in `INTEGRATION_LANE_ONLY`: it is
|
|
16
|
+
* test-support infrastructure invoked directly by a Python subprocess test,
|
|
17
|
+
* never shipped as user-facing functionality.
|
|
18
|
+
*
|
|
19
|
+
* Two independent modes, selected by argv shape:
|
|
20
|
+
*
|
|
21
|
+
* node executor-protocol-contract-driver.js <commandsJsonPath> <resultsJsonPath>
|
|
22
|
+
* Reads a JSON array of commands from `commandsJsonPath`, executes each
|
|
23
|
+
* against a real `createExecutorHttpClient` instance (built from a
|
|
24
|
+
* narrowly-constructed child environment, never argv, for credential
|
|
25
|
+
* material — see `readClientConfigFromEnv`), and atomically writes a JSON
|
|
26
|
+
* array of outcomes to `resultsJsonPath`. See `executeCommand` for the
|
|
27
|
+
* supported `claim` / `heartbeat` / `complete` / `fail` command shapes.
|
|
28
|
+
*
|
|
29
|
+
* node executor-protocol-contract-driver.js --emit-protocol <outputJsonPath>
|
|
30
|
+
* Emits the closed-vocabulary runtime constants (job types,
|
|
31
|
+
* classifications, mutation outcomes, reconciler-liveness states) from the
|
|
32
|
+
* BUILT `../executor/types.js` module, plus structural metadata (property
|
|
33
|
+
* names, optionality, and union-literal members) for the claim/claimed/
|
|
34
|
+
* heartbeat/completion/failure/process-heartbeat wire shapes, projected
|
|
35
|
+
* from the SOURCE `mcp_server/src/executor/types.ts` through the
|
|
36
|
+
* TypeScript compiler API (`typescript/unstable/sync` +
|
|
37
|
+
* `typescript/unstable/ast`) — never regex, substring matching, or a raw
|
|
38
|
+
* source snapshot.
|
|
39
|
+
*
|
|
40
|
+
* Reserved stdout: this driver writes ITS results only to the output file.
|
|
41
|
+
* stdout carries no diagnostics — every diagnostic and error goes to stderr —
|
|
42
|
+
* so a caller that happens to capture stdout never accumulates incidental
|
|
43
|
+
* log noise.
|
|
44
|
+
*/
|
|
45
|
+
import { fileURLToPath } from "node:url";
|
|
46
|
+
import path from "node:path";
|
|
47
|
+
import fs from "node:fs";
|
|
48
|
+
import { API, SymbolFlags } from "typescript/unstable/sync";
|
|
49
|
+
import { isInterfaceDeclaration, isTypeAliasDeclaration } from "typescript/unstable/ast";
|
|
50
|
+
import { createExecutorHttpClient, RECONCILER_LIVENESS_HEADER, HEARTBEAT_STOP_FIELD, } from "../executor/http-client.js";
|
|
51
|
+
import { EXECUTOR_JOB_TYPE_VALUES, EXECUTOR_CLASSIFICATION_VALUES, EXECUTOR_MUTATION_OUTCOME_VALUES, RECONCILER_LIVENESS_VALUES, } from "../executor/types.js";
|
|
52
|
+
// ---------------------------------------------------------------------------
|
|
53
|
+
// realFetch — a thin adapter over global fetch. Never constructs a URL,
|
|
54
|
+
// header, or fenced body field itself; it only shapes the Response into what
|
|
55
|
+
// FetchLike expects and observes (never sets) the one response header this
|
|
56
|
+
// driver is allowed to record as evidence.
|
|
57
|
+
// ---------------------------------------------------------------------------
|
|
58
|
+
/**
|
|
59
|
+
* The most recently observed `RECONCILER_LIVENESS_HEADER` value, reset before
|
|
60
|
+
* every command and captured here rather than threaded through the injected
|
|
61
|
+
* `fetch` return type — the client's own `FetchResponseLike` contract is
|
|
62
|
+
* unchanged by this driver (BAPI-997 action item: "do not construct... raw
|
|
63
|
+
* `fetch` requests in the driver" — this reads a header, it does not build
|
|
64
|
+
* one).
|
|
65
|
+
*/
|
|
66
|
+
let lastReconcilerLivenessHeaderRaw = null;
|
|
67
|
+
const realFetch = async (url, init) => {
|
|
68
|
+
const response = await fetch(url, {
|
|
69
|
+
method: init.method,
|
|
70
|
+
headers: init.headers,
|
|
71
|
+
body: init.body,
|
|
72
|
+
});
|
|
73
|
+
lastReconcilerLivenessHeaderRaw = response.headers.get(RECONCILER_LIVENESS_HEADER);
|
|
74
|
+
const text = await response.text();
|
|
75
|
+
const adapted = {
|
|
76
|
+
status: response.status,
|
|
77
|
+
text: async () => text,
|
|
78
|
+
headers: response.headers,
|
|
79
|
+
};
|
|
80
|
+
return adapted;
|
|
81
|
+
};
|
|
82
|
+
// ---------------------------------------------------------------------------
|
|
83
|
+
// Client configuration — read from a narrowly constructed child environment,
|
|
84
|
+
// never from argv (argv here is reserved for the two non-sensitive file
|
|
85
|
+
// paths). The Python caller constructs a minimal env containing only these
|
|
86
|
+
// four variables; this driver never inspects or forwards the rest of
|
|
87
|
+
// `process.env`, and never echoes the environment in any output.
|
|
88
|
+
// ---------------------------------------------------------------------------
|
|
89
|
+
const ENV_BASE_URL = "EXECUTOR_CONTRACT_DRIVER_BASE_URL";
|
|
90
|
+
const ENV_API_KEY = "EXECUTOR_CONTRACT_DRIVER_API_KEY";
|
|
91
|
+
const ENV_API_KEY_BY_REPO_JSON = "EXECUTOR_CONTRACT_DRIVER_API_KEY_BY_REPO_JSON";
|
|
92
|
+
const ENV_MCP_VERSION = "EXECUTOR_CONTRACT_DRIVER_MCP_VERSION";
|
|
93
|
+
/** Read the five client configuration entries (BAPI-997 action item 5). */
|
|
94
|
+
function readClientConfigFromEnv(env) {
|
|
95
|
+
const baseUrl = env[ENV_BASE_URL];
|
|
96
|
+
const apiKey = env[ENV_API_KEY];
|
|
97
|
+
const mcpVersion = env[ENV_MCP_VERSION];
|
|
98
|
+
if (!baseUrl)
|
|
99
|
+
throw new Error(`${ENV_BASE_URL} must be set in the child environment`);
|
|
100
|
+
if (!apiKey)
|
|
101
|
+
throw new Error(`${ENV_API_KEY} must be set in the child environment`);
|
|
102
|
+
if (!mcpVersion)
|
|
103
|
+
throw new Error(`${ENV_MCP_VERSION} must be set in the child environment`);
|
|
104
|
+
let apiKeyByRepo;
|
|
105
|
+
const rawApiKeyByRepo = env[ENV_API_KEY_BY_REPO_JSON];
|
|
106
|
+
if (rawApiKeyByRepo) {
|
|
107
|
+
const parsed = JSON.parse(rawApiKeyByRepo);
|
|
108
|
+
if (parsed && typeof parsed === "object" && !Array.isArray(parsed)) {
|
|
109
|
+
apiKeyByRepo = parsed;
|
|
110
|
+
}
|
|
111
|
+
else {
|
|
112
|
+
throw new Error(`${ENV_API_KEY_BY_REPO_JSON} must decode to a JSON object`);
|
|
113
|
+
}
|
|
114
|
+
}
|
|
115
|
+
return { baseUrl, apiKey, apiKeyByRepo, mcpVersion };
|
|
116
|
+
}
|
|
117
|
+
const KNOWN_OPS = new Set(["claim", "heartbeat", "complete", "fail"]);
|
|
118
|
+
function isPlainObject(value) {
|
|
119
|
+
return !!value && typeof value === "object" && !Array.isArray(value);
|
|
120
|
+
}
|
|
121
|
+
async function executeCommand(client, command) {
|
|
122
|
+
lastReconcilerLivenessHeaderRaw = null;
|
|
123
|
+
try {
|
|
124
|
+
switch (command.op) {
|
|
125
|
+
case "claim": {
|
|
126
|
+
const result = await client.claim(command.manifest);
|
|
127
|
+
return {
|
|
128
|
+
op: command.op,
|
|
129
|
+
ok: true,
|
|
130
|
+
result,
|
|
131
|
+
evidence: { [RECONCILER_LIVENESS_HEADER]: lastReconcilerLivenessHeaderRaw },
|
|
132
|
+
};
|
|
133
|
+
}
|
|
134
|
+
case "heartbeat": {
|
|
135
|
+
const result = await client.heartbeat(command.job, command.payload);
|
|
136
|
+
return { op: command.op, ok: true, result };
|
|
137
|
+
}
|
|
138
|
+
case "complete": {
|
|
139
|
+
const result = await client.complete(command.job, command.completion);
|
|
140
|
+
return { op: command.op, ok: true, result };
|
|
141
|
+
}
|
|
142
|
+
case "fail": {
|
|
143
|
+
const result = await client.fail(command.job, command.failure);
|
|
144
|
+
return { op: command.op, ok: true, result };
|
|
145
|
+
}
|
|
146
|
+
}
|
|
147
|
+
}
|
|
148
|
+
catch (err) {
|
|
149
|
+
const message = err instanceof Error ? err.message : String(err);
|
|
150
|
+
return { op: command.op, ok: false, error: message };
|
|
151
|
+
}
|
|
152
|
+
}
|
|
153
|
+
/** Atomically replace `outputPath`'s contents (write-then-rename, same filesystem). */
|
|
154
|
+
function writeJsonAtomically(outputPath, value) {
|
|
155
|
+
const tmpPath = `${outputPath}.tmp-${process.pid}-${Date.now()}`;
|
|
156
|
+
fs.writeFileSync(tmpPath, JSON.stringify(value, null, 2));
|
|
157
|
+
fs.renameSync(tmpPath, outputPath);
|
|
158
|
+
}
|
|
159
|
+
async function runCommands(inputPath, outputPath) {
|
|
160
|
+
const config = readClientConfigFromEnv(process.env);
|
|
161
|
+
const client = createExecutorHttpClient({
|
|
162
|
+
baseUrl: config.baseUrl,
|
|
163
|
+
apiKey: config.apiKey,
|
|
164
|
+
apiKeyByRepo: config.apiKeyByRepo,
|
|
165
|
+
mcpVersion: config.mcpVersion,
|
|
166
|
+
fetch: realFetch,
|
|
167
|
+
});
|
|
168
|
+
const rawCommands = JSON.parse(fs.readFileSync(inputPath, "utf8"));
|
|
169
|
+
if (!Array.isArray(rawCommands)) {
|
|
170
|
+
throw new Error("commands file must contain a JSON array");
|
|
171
|
+
}
|
|
172
|
+
const outcomes = [];
|
|
173
|
+
for (const raw of rawCommands) {
|
|
174
|
+
if (!isPlainObject(raw) || typeof raw.op !== "string" || !KNOWN_OPS.has(raw.op)) {
|
|
175
|
+
outcomes.push({
|
|
176
|
+
op: isPlainObject(raw) && typeof raw.op === "string" ? raw.op : "unknown",
|
|
177
|
+
ok: false,
|
|
178
|
+
error: "unrecognized or malformed command",
|
|
179
|
+
});
|
|
180
|
+
continue;
|
|
181
|
+
}
|
|
182
|
+
outcomes.push(await executeCommand(client, raw));
|
|
183
|
+
}
|
|
184
|
+
writeJsonAtomically(outputPath, outcomes);
|
|
185
|
+
}
|
|
186
|
+
/** The exported shapes this driver projects (BAPI-997 action item 13). */
|
|
187
|
+
const PROJECTED_SHAPE_NAMES = [
|
|
188
|
+
"ExecutorClaimJobRequest",
|
|
189
|
+
"ClaimedExecutorJob",
|
|
190
|
+
"ExecutorHeartbeatWireRequest",
|
|
191
|
+
"ExecutorCompletionWireRequest",
|
|
192
|
+
"ExecutorFailureWireRequest",
|
|
193
|
+
"ExecutorProcessHeartbeatRequest",
|
|
194
|
+
];
|
|
195
|
+
/** Return the closed string-literal members of *type*, or `undefined` when it is not one. */
|
|
196
|
+
function literalValuesOf(type) {
|
|
197
|
+
if (type.isStringLiteralType()) {
|
|
198
|
+
return [type.value];
|
|
199
|
+
}
|
|
200
|
+
if (type.isUnionType()) {
|
|
201
|
+
const values = [];
|
|
202
|
+
for (const member of type.getTypes()) {
|
|
203
|
+
if (!member.isStringLiteralType())
|
|
204
|
+
return undefined;
|
|
205
|
+
values.push(member.value);
|
|
206
|
+
}
|
|
207
|
+
return values.sort();
|
|
208
|
+
}
|
|
209
|
+
return undefined;
|
|
210
|
+
}
|
|
211
|
+
function projectShape(checker, declaration, name) {
|
|
212
|
+
const type = checker.getTypeAtLocation(declaration);
|
|
213
|
+
if (!type) {
|
|
214
|
+
throw new Error(`could not resolve a type for ${name}`);
|
|
215
|
+
}
|
|
216
|
+
const properties = checker
|
|
217
|
+
.getPropertiesOfType(type)
|
|
218
|
+
.map((symbol) => {
|
|
219
|
+
const propertyType = checker.getTypeOfSymbolAtLocation(symbol, declaration);
|
|
220
|
+
return {
|
|
221
|
+
name: symbol.name,
|
|
222
|
+
optional: !!(symbol.flags & SymbolFlags.Optional),
|
|
223
|
+
type: checker.typeToString(propertyType),
|
|
224
|
+
literalValues: literalValuesOf(propertyType),
|
|
225
|
+
};
|
|
226
|
+
})
|
|
227
|
+
.sort((a, b) => a.name.localeCompare(b.name));
|
|
228
|
+
return { name, properties };
|
|
229
|
+
}
|
|
230
|
+
async function emitProtocolProjection(outputPath) {
|
|
231
|
+
const scriptDir = path.dirname(fileURLToPath(import.meta.url));
|
|
232
|
+
// This driver runs from build/scripts/; the emitted tree mirrors src/, so
|
|
233
|
+
// the SOURCE file the compiler API projects lives two levels up, under
|
|
234
|
+
// src/executor/ — never mcp_server/build/.
|
|
235
|
+
const mcpServerRoot = path.resolve(scriptDir, "..", "..");
|
|
236
|
+
const typesSourcePath = path.join(mcpServerRoot, "src", "executor", "types.ts");
|
|
237
|
+
const api = new API({ cwd: mcpServerRoot });
|
|
238
|
+
try {
|
|
239
|
+
const snapshot = api.updateSnapshot({ openFiles: [typesSourcePath] });
|
|
240
|
+
const project = snapshot.getDefaultProjectForFile(typesSourcePath);
|
|
241
|
+
if (!project) {
|
|
242
|
+
throw new Error(`no TypeScript project resolved for ${typesSourcePath}`);
|
|
243
|
+
}
|
|
244
|
+
const checker = project.checker;
|
|
245
|
+
const sourceFile = project.program.getSourceFile(typesSourcePath);
|
|
246
|
+
if (!sourceFile) {
|
|
247
|
+
throw new Error(`could not load source file ${typesSourcePath}`);
|
|
248
|
+
}
|
|
249
|
+
const shapes = {};
|
|
250
|
+
for (const statement of sourceFile.statements) {
|
|
251
|
+
if (!isInterfaceDeclaration(statement) && !isTypeAliasDeclaration(statement))
|
|
252
|
+
continue;
|
|
253
|
+
const name = statement.name.text;
|
|
254
|
+
if (!PROJECTED_SHAPE_NAMES.includes(name))
|
|
255
|
+
continue;
|
|
256
|
+
shapes[name] = projectShape(checker, statement, name);
|
|
257
|
+
}
|
|
258
|
+
const missing = PROJECTED_SHAPE_NAMES.filter((name) => !(name in shapes));
|
|
259
|
+
if (missing.length > 0) {
|
|
260
|
+
throw new Error(`could not project shape(s): ${missing.join(", ")}`);
|
|
261
|
+
}
|
|
262
|
+
const projection = {
|
|
263
|
+
jobTypes: [...EXECUTOR_JOB_TYPE_VALUES],
|
|
264
|
+
classifications: [...EXECUTOR_CLASSIFICATION_VALUES],
|
|
265
|
+
mutationOutcomes: [...EXECUTOR_MUTATION_OUTCOME_VALUES],
|
|
266
|
+
reconcilerLiveness: [...RECONCILER_LIVENESS_VALUES],
|
|
267
|
+
reconcilerLivenessHeader: RECONCILER_LIVENESS_HEADER,
|
|
268
|
+
heartbeatStopField: HEARTBEAT_STOP_FIELD,
|
|
269
|
+
shapes,
|
|
270
|
+
};
|
|
271
|
+
writeJsonAtomically(outputPath, projection);
|
|
272
|
+
project.dispose();
|
|
273
|
+
}
|
|
274
|
+
finally {
|
|
275
|
+
api.close();
|
|
276
|
+
}
|
|
277
|
+
}
|
|
278
|
+
// ---------------------------------------------------------------------------
|
|
279
|
+
// Entry point
|
|
280
|
+
// ---------------------------------------------------------------------------
|
|
281
|
+
export async function main(argv) {
|
|
282
|
+
if (argv[0] === "--emit-protocol") {
|
|
283
|
+
const outputPath = argv[1];
|
|
284
|
+
if (!outputPath) {
|
|
285
|
+
process.stderr.write("usage: executor-protocol-contract-driver --emit-protocol <outputJsonPath>\n");
|
|
286
|
+
process.exitCode = 1;
|
|
287
|
+
return;
|
|
288
|
+
}
|
|
289
|
+
await emitProtocolProjection(outputPath);
|
|
290
|
+
return;
|
|
291
|
+
}
|
|
292
|
+
const [inputPath, outputPath] = argv;
|
|
293
|
+
if (!inputPath || !outputPath) {
|
|
294
|
+
process.stderr.write("usage: executor-protocol-contract-driver <commandsJsonPath> <resultsJsonPath>\n");
|
|
295
|
+
process.exitCode = 1;
|
|
296
|
+
return;
|
|
297
|
+
}
|
|
298
|
+
await runCommands(inputPath, outputPath);
|
|
299
|
+
}
|
|
300
|
+
const invokedDirectly = process.argv[1]
|
|
301
|
+
? fileURLToPath(import.meta.url) === path.resolve(process.argv[1])
|
|
302
|
+
: false;
|
|
303
|
+
if (invokedDirectly) {
|
|
304
|
+
main(process.argv.slice(2)).catch((err) => {
|
|
305
|
+
// Sanitized top-level catch: never print a stack trace, raw error, or
|
|
306
|
+
// secret material (the child environment/API key) to stderr.
|
|
307
|
+
const message = err instanceof Error ? err.message : String(err);
|
|
308
|
+
process.stderr.write(`executor-protocol-contract-driver: ${message}\n`);
|
|
309
|
+
process.exitCode = 1;
|
|
310
|
+
});
|
|
311
|
+
}
|