@bridge_gpt/mcp-server 0.2.38 → 0.2.41
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 +189 -14
- package/build/agent-capabilities/probe-context.js +2 -1
- package/build/agent-launchers/claude-executor-adapter.js +392 -0
- package/build/agent-launchers/executor-adapter-inspection.js +163 -0
- package/build/agent-launchers/executor-adapter-registry.js +90 -0
- package/build/agent-launchers/executor-adapter.js +136 -0
- package/build/agent-registry.js +28 -0
- package/build/agents.generated.js +1 -1
- package/build/claude-login.js +85 -0
- package/build/claude-user-config-doctor.js +59 -33
- package/build/commands.generated.js +12 -11
- package/build/conduct-epic/bridge-client.js +345 -0
- package/build/conduct-epic/checkpoint-store.js +479 -0
- package/build/conduct-epic/cli.js +1765 -0
- package/build/conduct-epic/lock.js +302 -0
- package/build/conduct-epic/pr-state.js +286 -0
- package/build/conduct-epic/spawn.js +101 -0
- package/build/conductor/bridge-api-client.js +37 -2
- package/build/conductor/doctor.js +11 -1
- package/build/conductor/install-doctor.js +184 -10
- package/build/conductor-bin.js +7 -7
- package/build/credential-store.js +10 -4
- package/build/credentials-cli.js +34 -19
- package/build/docs.generated.js +1 -1
- package/build/doctor.js +579 -88
- package/build/executor/agent-identity.js +32 -0
- package/build/executor/cli.js +50 -39
- package/build/executor/deps.js +15 -1
- package/build/executor/env.js +56 -45
- package/build/executor/index.js +9 -1
- package/build/executor/install-preflight.js +138 -0
- package/build/executor/job-errors.js +200 -0
- package/build/executor/job-runner.js +619 -268
- package/build/executor/observation.js +165 -0
- package/build/executor/permissions.js +163 -36
- package/build/executor/platform.js +54 -0
- package/build/executor/preflight.js +175 -67
- package/build/executor/process.js +39 -7
- package/build/executor/runner.js +19 -0
- package/build/executor/service-lifecycle.js +269 -0
- package/build/executor/service-unit.js +121 -12
- package/build/executor/stale-artifacts.js +70 -0
- package/build/executor/test-clock.js +188 -24
- package/build/executor/worker-command.js +22 -58
- package/build/executor/worker-log.js +82 -0
- package/build/executor/worktree-lock.js +264 -0
- package/build/index.js +527 -357
- package/build/install-bridge-conductor.js +376 -38
- package/build/install-bridge.js +414 -114
- package/build/install-doctor.js +13 -0
- package/build/install-reexec.js +5 -3
- package/build/mcp-install-state.js +130 -0
- package/build/mcp-profile.js +11 -2
- package/build/mcp-provisioning.js +15 -0
- package/build/merge-pull-request.js +562 -0
- package/build/phase-result-artifacts.js +450 -0
- package/build/pipeline-orchestrator.js +4 -0
- package/build/pipeline-utils.js +16 -0
- package/build/pipelines.generated.js +7 -7
- package/build/plane/preflight.js +18 -14
- package/build/plane/supervisor.js +8 -1
- package/build/project-root.js +34 -0
- package/build/readme.generated.js +1 -1
- package/build/run-unit-tests-launcher.js +36 -9
- package/build/setup-epic.js +57 -4
- package/build/sfcc/permissions.js +25 -6
- package/build/sfcc/reads-site-preference.js +6 -0
- package/build/sfcc/register.js +61 -23
- package/build/sfcc/registration-inventory.js +89 -0
- package/build/sfcc/setup-status.js +18 -34
- package/build/sfcc/tool-wrapper.js +294 -17
- package/build/sfcc/write-grants.js +33 -1
- package/build/sfcc/write-guard.js +41 -12
- package/build/sfcc/writes-custom-object-def.js +6 -2
- package/build/sfcc/writes-site-preference.js +6 -1
- package/build/sfcc/writes-system-object.js +11 -2
- package/build/sfcc/writes.js +13 -8
- package/build/start-tickets-prereqs.js +25 -15
- package/build/start-tickets.js +123 -21
- package/build/version.generated.js +1 -1
- package/build/worktree-core.js +9 -3
- package/docs/install/mcp-tool-integrations.md +54 -9
- package/docs/install/sfcc-integration.md +71 -24
- package/package.json +3 -3
- package/build/executor/worker-config-isolation.js +0 -287
|
@@ -0,0 +1,264 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Worktree-level mutual exclusion for executor workers (BAPI-793).
|
|
3
|
+
*
|
|
4
|
+
* Two executor processes can legitimately hold two DIFFERENT `executor_jobs`
|
|
5
|
+
* rows for the same ticket at the same time — a watchdog re-fire racing a
|
|
6
|
+
* terminal retry, a repo-scoped executor picking up a sibling's work, a lease
|
|
7
|
+
* the server declared expired while its worker was still alive. Before this
|
|
8
|
+
* module nothing stopped both of them from spawning a `claude` worker into the
|
|
9
|
+
* SAME git worktree, so two independent agents edited, `git add`-ed, and
|
|
10
|
+
* committed in one working tree concurrently (observed 2026-08-14 on BAPI-785
|
|
11
|
+
* and BAPI-778: two live PIDs, two `session_id`s in one `.conductor/worker.log`).
|
|
12
|
+
*
|
|
13
|
+
* This is the LOCAL BACKSTOP for that. It is deliberately the least clever layer
|
|
14
|
+
* in the stack: a single file in the worktree, created with an exclusive
|
|
15
|
+
* `open(..., "wx")`, carrying the owning job's identity. Dispatch exclusion and
|
|
16
|
+
* claim serialization (the Python side) reduce how often two rows exist at all;
|
|
17
|
+
* this makes the overlap non-destructive even when they do — which is the only
|
|
18
|
+
* ordering that survives a reconciler bug.
|
|
19
|
+
*
|
|
20
|
+
* Four properties are load-bearing:
|
|
21
|
+
*
|
|
22
|
+
* - ATOMIC ACQUISITION. `open(..., "wx")` either creates the file or fails with
|
|
23
|
+
* `EEXIST`; there is no observable window between the two. An
|
|
24
|
+
* exists-then-write sequence would reintroduce exactly the race this exists
|
|
25
|
+
* to close.
|
|
26
|
+
* - A DIFFERENT CLAIM TOKEN IS FOREIGN, EVEN AT THE SAME PID. Several executor
|
|
27
|
+
* loops can run inside one host process, so PID equality proves nothing about
|
|
28
|
+
* ownership. Only the claim token — minted server-side, opaque to the
|
|
29
|
+
* executor (R14 rule 4) — identifies the holder.
|
|
30
|
+
* - FAIL CLOSED. Malformed metadata, an unreadable lock, or an owner whose
|
|
31
|
+
* liveness cannot be established all refuse the spawn. Refusing costs one
|
|
32
|
+
* job attempt; guessing costs a corrupted working tree.
|
|
33
|
+
* - RELEASE IS OWNERSHIP-CHECKED. `release()` re-reads the lock and removes it
|
|
34
|
+
* only while its own claim token is still the one recorded, so a straggler's
|
|
35
|
+
* `finally` can never delete the lock of the job that replaced it.
|
|
36
|
+
*
|
|
37
|
+
* No claim token, lock payload, or file content is ever logged or returned in a
|
|
38
|
+
* server-bound message; the token travels only inside the lock file and the
|
|
39
|
+
* in-process handle.
|
|
40
|
+
*/
|
|
41
|
+
import { open, readFile, rm, mkdir } from "node:fs/promises";
|
|
42
|
+
import { pathApiForExecutorPlatform } from "./worktree-inspection.js";
|
|
43
|
+
/** The `.conductor` directory the lock lives in (shared with the worker log). */
|
|
44
|
+
export const EXECUTOR_WORKTREE_LOCK_DIR = ".conductor";
|
|
45
|
+
/** The fixed lock filename inside {@link EXECUTOR_WORKTREE_LOCK_DIR}. */
|
|
46
|
+
export const EXECUTOR_WORKTREE_LOCK_FILE = "worker.lock";
|
|
47
|
+
/** Lock metadata schema version; a foreign version fails closed. */
|
|
48
|
+
export const EXECUTOR_WORKTREE_LOCK_VERSION = 1;
|
|
49
|
+
/**
|
|
50
|
+
* Hard bound on the lock file we are willing to parse. A lock file is a few
|
|
51
|
+
* hundred bytes; anything larger is not our metadata, and parsing it would let
|
|
52
|
+
* an unrelated file at that path drive this module's behavior.
|
|
53
|
+
*/
|
|
54
|
+
const MAX_LOCK_FILE_CHARS = 4_096;
|
|
55
|
+
/** How many times acquisition may recover a dead owner's lock and retry. */
|
|
56
|
+
const MAX_ACQUIRE_ATTEMPTS = 3;
|
|
57
|
+
/** Resolve `<worktree>/.conductor/worker.lock` with platform-correct separators. */
|
|
58
|
+
export function buildExecutorWorktreeLockPath(worktreePath, platform = process.platform) {
|
|
59
|
+
const pathApi = pathApiForExecutorPlatform(platform);
|
|
60
|
+
return pathApi.join(worktreePath, EXECUTOR_WORKTREE_LOCK_DIR, EXECUTOR_WORKTREE_LOCK_FILE);
|
|
61
|
+
}
|
|
62
|
+
function errorCode(err) {
|
|
63
|
+
const code = err?.code;
|
|
64
|
+
return typeof code === "string" ? code : undefined;
|
|
65
|
+
}
|
|
66
|
+
/** Real exclusive create with restrictive permissions where the OS honors them. */
|
|
67
|
+
async function defaultWriteFileExclusive(filePath, data) {
|
|
68
|
+
const handle = await open(filePath, "wx", 0o600);
|
|
69
|
+
try {
|
|
70
|
+
await handle.writeFile(data, "utf-8");
|
|
71
|
+
}
|
|
72
|
+
finally {
|
|
73
|
+
await handle.close();
|
|
74
|
+
}
|
|
75
|
+
}
|
|
76
|
+
/**
|
|
77
|
+
* Real liveness probe. `process.kill(pid, 0)` signals nothing and only reports
|
|
78
|
+
* reachability. Only an explicit `ESRCH` proves the owner is gone: `EPERM` means
|
|
79
|
+
* the process exists but belongs to another user, and an unrecognized failure
|
|
80
|
+
* means we do not know — both are treated as ALIVE so an uncertain probe can
|
|
81
|
+
* never authorize entering an occupied worktree.
|
|
82
|
+
*/
|
|
83
|
+
function defaultIsProcessAlive(pid) {
|
|
84
|
+
if (!Number.isInteger(pid) || pid <= 0)
|
|
85
|
+
return true;
|
|
86
|
+
try {
|
|
87
|
+
process.kill(pid, 0);
|
|
88
|
+
return true;
|
|
89
|
+
}
|
|
90
|
+
catch (err) {
|
|
91
|
+
return errorCode(err) !== "ESRCH";
|
|
92
|
+
}
|
|
93
|
+
}
|
|
94
|
+
/**
|
|
95
|
+
* Parse lock metadata, or `null` when it is not a lock record this version
|
|
96
|
+
* understands. Bounded before parsing, and every field is type-checked — a
|
|
97
|
+
* partially-written or foreign file must not be mistaken for a releasable lock.
|
|
98
|
+
*/
|
|
99
|
+
export function parseExecutorWorktreeLock(raw) {
|
|
100
|
+
if (typeof raw !== "string" || raw.length === 0 || raw.length > MAX_LOCK_FILE_CHARS) {
|
|
101
|
+
return null;
|
|
102
|
+
}
|
|
103
|
+
let value;
|
|
104
|
+
try {
|
|
105
|
+
value = JSON.parse(raw);
|
|
106
|
+
}
|
|
107
|
+
catch {
|
|
108
|
+
return null;
|
|
109
|
+
}
|
|
110
|
+
if (typeof value !== "object" || value === null || Array.isArray(value))
|
|
111
|
+
return null;
|
|
112
|
+
const record = value;
|
|
113
|
+
const { version, worktreePath, jobId, claimToken, executorId, ownerPid, acquiredAt, } = record;
|
|
114
|
+
if (version !== EXECUTOR_WORKTREE_LOCK_VERSION)
|
|
115
|
+
return null;
|
|
116
|
+
if (typeof worktreePath !== "string" || worktreePath.length === 0)
|
|
117
|
+
return null;
|
|
118
|
+
if (typeof jobId !== "number" || !Number.isInteger(jobId))
|
|
119
|
+
return null;
|
|
120
|
+
if (typeof claimToken !== "string" || claimToken.length === 0)
|
|
121
|
+
return null;
|
|
122
|
+
if (typeof executorId !== "string" || executorId.length === 0)
|
|
123
|
+
return null;
|
|
124
|
+
if (typeof ownerPid !== "number" || !Number.isInteger(ownerPid))
|
|
125
|
+
return null;
|
|
126
|
+
if (typeof acquiredAt !== "string" || acquiredAt.length === 0)
|
|
127
|
+
return null;
|
|
128
|
+
return { version, worktreePath, jobId, claimToken, executorId, ownerPid, acquiredAt };
|
|
129
|
+
}
|
|
130
|
+
/** Serialize ownership metadata. Only the approved fields are ever written. */
|
|
131
|
+
function serializeLock(request) {
|
|
132
|
+
const owner = {
|
|
133
|
+
version: EXECUTOR_WORKTREE_LOCK_VERSION,
|
|
134
|
+
worktreePath: request.worktreePath,
|
|
135
|
+
jobId: request.jobId,
|
|
136
|
+
claimToken: request.claimToken,
|
|
137
|
+
executorId: request.executorId,
|
|
138
|
+
ownerPid: request.ownerPid,
|
|
139
|
+
acquiredAt: request.acquiredAt ?? new Date().toISOString(),
|
|
140
|
+
};
|
|
141
|
+
return JSON.stringify(owner);
|
|
142
|
+
}
|
|
143
|
+
/**
|
|
144
|
+
* Acquire exclusive ownership of `worktreePath` for one claimed job.
|
|
145
|
+
*
|
|
146
|
+
* Returns an acquired handle, or a `worktree_busy` refusal when the worktree is
|
|
147
|
+
* held by a live foreign claim. A lock whose owning PID is provably gone is
|
|
148
|
+
* recovered — but only through a re-read-then-remove sequence that verifies the
|
|
149
|
+
* bytes have not changed since we read them, so recovery cannot delete a lock a
|
|
150
|
+
* successor wrote in the meantime.
|
|
151
|
+
*/
|
|
152
|
+
export async function acquireExecutorWorktreeLock(request, seams = {}) {
|
|
153
|
+
const platform = seams.platform ?? process.platform;
|
|
154
|
+
const pathApi = pathApiForExecutorPlatform(platform);
|
|
155
|
+
const dir = pathApi.join(request.worktreePath, EXECUTOR_WORKTREE_LOCK_DIR);
|
|
156
|
+
const lockPath = pathApi.join(dir, EXECUTOR_WORKTREE_LOCK_FILE);
|
|
157
|
+
const writeExclusive = seams.writeFileExclusive ?? defaultWriteFileExclusive;
|
|
158
|
+
const read = seams.readFile ?? ((filePath) => readFile(filePath, "utf-8"));
|
|
159
|
+
const remove = seams.removeFile ?? ((filePath) => rm(filePath, { force: true }));
|
|
160
|
+
const makeDir = seams.mkdir ?? ((dirPath, options) => mkdir(dirPath, options));
|
|
161
|
+
const isAlive = seams.isProcessAlive ?? defaultIsProcessAlive;
|
|
162
|
+
const refused = (owner) => ({
|
|
163
|
+
acquired: false,
|
|
164
|
+
reason: "worktree_busy",
|
|
165
|
+
lockPath,
|
|
166
|
+
owner,
|
|
167
|
+
});
|
|
168
|
+
try {
|
|
169
|
+
await makeDir(dir, { recursive: true });
|
|
170
|
+
}
|
|
171
|
+
catch {
|
|
172
|
+
// A `.conductor` that already exists is the normal case and `recursive: true`
|
|
173
|
+
// makes it a no-op; any other failure surfaces below as a failed exclusive
|
|
174
|
+
// create, which refuses. Never proceed as though the lock were held by us.
|
|
175
|
+
}
|
|
176
|
+
const payload = serializeLock(request);
|
|
177
|
+
for (let attempt = 0; attempt < MAX_ACQUIRE_ATTEMPTS; attempt += 1) {
|
|
178
|
+
try {
|
|
179
|
+
await writeExclusive(lockPath, payload);
|
|
180
|
+
const owner = parseExecutorWorktreeLock(payload);
|
|
181
|
+
// Unreachable in practice — we just serialized it — but typing the handle
|
|
182
|
+
// on a parsed record rather than a cast keeps the file the single source
|
|
183
|
+
// of truth for what a lock IS.
|
|
184
|
+
if (owner === null)
|
|
185
|
+
return refused(null);
|
|
186
|
+
return {
|
|
187
|
+
acquired: true,
|
|
188
|
+
lockPath,
|
|
189
|
+
owner,
|
|
190
|
+
release: () => releaseLock(lockPath, request, read, remove),
|
|
191
|
+
};
|
|
192
|
+
}
|
|
193
|
+
catch (err) {
|
|
194
|
+
if (errorCode(err) !== "EEXIST") {
|
|
195
|
+
// Not a contention signal: a permission problem, a read-only worktree, a
|
|
196
|
+
// vanished directory. We do not own the worktree, so we do not enter it.
|
|
197
|
+
return refused(null);
|
|
198
|
+
}
|
|
199
|
+
}
|
|
200
|
+
let raw;
|
|
201
|
+
try {
|
|
202
|
+
raw = await read(lockPath);
|
|
203
|
+
}
|
|
204
|
+
catch (err) {
|
|
205
|
+
// The holder released between our failed create and this read: retry the
|
|
206
|
+
// exclusive create. Anything else is unreadable state — fail closed.
|
|
207
|
+
if (errorCode(err) === "ENOENT")
|
|
208
|
+
continue;
|
|
209
|
+
return refused(null);
|
|
210
|
+
}
|
|
211
|
+
const owner = parseExecutorWorktreeLock(raw);
|
|
212
|
+
if (owner === null)
|
|
213
|
+
return refused(null);
|
|
214
|
+
if (isAlive(owner.ownerPid))
|
|
215
|
+
return refused(owner);
|
|
216
|
+
// Dead owner. Recover only if the bytes we are about to delete are still the
|
|
217
|
+
// exact bytes we read and judged stale.
|
|
218
|
+
let confirmation;
|
|
219
|
+
try {
|
|
220
|
+
confirmation = await read(lockPath);
|
|
221
|
+
}
|
|
222
|
+
catch (err) {
|
|
223
|
+
if (errorCode(err) === "ENOENT")
|
|
224
|
+
continue;
|
|
225
|
+
return refused(null);
|
|
226
|
+
}
|
|
227
|
+
if (confirmation !== raw)
|
|
228
|
+
continue;
|
|
229
|
+
try {
|
|
230
|
+
await remove(lockPath);
|
|
231
|
+
}
|
|
232
|
+
catch {
|
|
233
|
+
return refused(owner);
|
|
234
|
+
}
|
|
235
|
+
}
|
|
236
|
+
return refused(null);
|
|
237
|
+
}
|
|
238
|
+
/**
|
|
239
|
+
* Remove the lock only while this job is still its recorded owner.
|
|
240
|
+
*
|
|
241
|
+
* Best-effort by design: a failed release leaves a lock whose owner PID is this
|
|
242
|
+
* (exiting) process, which the next acquisition recovers as stale. Throwing here
|
|
243
|
+
* would turn a cleanup hiccup into a job failure after the work was already done.
|
|
244
|
+
*/
|
|
245
|
+
async function releaseLock(lockPath, request, read, remove) {
|
|
246
|
+
let raw;
|
|
247
|
+
try {
|
|
248
|
+
raw = await read(lockPath);
|
|
249
|
+
}
|
|
250
|
+
catch {
|
|
251
|
+
return;
|
|
252
|
+
}
|
|
253
|
+
const owner = parseExecutorWorktreeLock(raw);
|
|
254
|
+
if (owner === null)
|
|
255
|
+
return;
|
|
256
|
+
if (owner.claimToken !== request.claimToken || owner.jobId !== request.jobId)
|
|
257
|
+
return;
|
|
258
|
+
try {
|
|
259
|
+
await remove(lockPath);
|
|
260
|
+
}
|
|
261
|
+
catch {
|
|
262
|
+
/* best-effort */
|
|
263
|
+
}
|
|
264
|
+
}
|