@bridge_gpt/mcp-server 0.2.37 → 0.2.39

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 (91) hide show
  1. package/README.md +193 -16
  2. package/build/agent-capabilities/probe-context.js +2 -1
  3. package/build/agent-launchers/claude-executor-adapter.js +392 -0
  4. package/build/agent-launchers/executor-adapter-inspection.js +163 -0
  5. package/build/agent-launchers/executor-adapter-registry.js +90 -0
  6. package/build/agent-launchers/executor-adapter.js +136 -0
  7. package/build/agent-registry.js +28 -0
  8. package/build/agents.generated.js +1 -1
  9. package/build/claude-login.js +85 -0
  10. package/build/claude-user-config-doctor.js +59 -33
  11. package/build/commands.generated.js +12 -11
  12. package/build/conduct-epic/bridge-client.js +345 -0
  13. package/build/conduct-epic/checkpoint-store.js +423 -0
  14. package/build/conduct-epic/cli.js +1732 -0
  15. package/build/conduct-epic/lock.js +302 -0
  16. package/build/conduct-epic/pr-state.js +197 -0
  17. package/build/conduct-epic/spawn.js +101 -0
  18. package/build/conductor/bridge-api-client.js +37 -2
  19. package/build/conductor/doctor.js +11 -1
  20. package/build/conductor/install-doctor.js +184 -10
  21. package/build/conductor-bin.js +7 -7
  22. package/build/credential-store.js +10 -4
  23. package/build/credentials-cli.js +34 -19
  24. package/build/docs.generated.js +1 -1
  25. package/build/doctor.js +579 -88
  26. package/build/executor/agent-identity.js +32 -0
  27. package/build/executor/cli.js +50 -39
  28. package/build/executor/deps.js +15 -1
  29. package/build/executor/env.js +56 -45
  30. package/build/executor/index.js +9 -1
  31. package/build/executor/install-preflight.js +138 -0
  32. package/build/executor/job-errors.js +200 -0
  33. package/build/executor/job-runner.js +619 -268
  34. package/build/executor/observation.js +165 -0
  35. package/build/executor/permissions.js +163 -36
  36. package/build/executor/platform.js +54 -0
  37. package/build/executor/preflight.js +175 -67
  38. package/build/executor/process.js +39 -7
  39. package/build/executor/runner.js +19 -0
  40. package/build/executor/service-lifecycle.js +269 -0
  41. package/build/executor/service-unit.js +121 -12
  42. package/build/executor/stale-artifacts.js +70 -0
  43. package/build/executor/test-clock.js +188 -24
  44. package/build/executor/worker-command.js +22 -58
  45. package/build/executor/worker-log.js +82 -0
  46. package/build/executor/worktree-lock.js +264 -0
  47. package/build/index.js +527 -357
  48. package/build/install-bridge-conductor.js +376 -38
  49. package/build/install-bridge.js +414 -114
  50. package/build/install-doctor.js +13 -0
  51. package/build/install-reexec.js +5 -3
  52. package/build/mcp-install-state.js +130 -0
  53. package/build/mcp-profile.js +11 -2
  54. package/build/mcp-provisioning.js +15 -0
  55. package/build/merge-pull-request.js +562 -0
  56. package/build/phase-result-artifacts.js +450 -0
  57. package/build/pipeline-orchestrator.js +4 -0
  58. package/build/pipeline-utils.js +16 -0
  59. package/build/pipelines.generated.js +7 -7
  60. package/build/plane/preflight.js +18 -14
  61. package/build/plane/supervisor.js +8 -1
  62. package/build/project-root.js +34 -0
  63. package/build/readme.generated.js +1 -1
  64. package/build/run-unit-tests-launcher.js +36 -9
  65. package/build/setup-epic.js +57 -4
  66. package/build/sfcc/ocapi-shape.js +23 -4
  67. package/build/sfcc/permissions.js +25 -6
  68. package/build/sfcc/read-body.js +92 -0
  69. package/build/sfcc/read-projection.js +6 -2
  70. package/build/sfcc/reads-custom-object-def.js +33 -21
  71. package/build/sfcc/reads-site-preference.js +20 -7
  72. package/build/sfcc/reads-system-object.js +11 -5
  73. package/build/sfcc/register.js +61 -23
  74. package/build/sfcc/registration-inventory.js +89 -0
  75. package/build/sfcc/setup-status.js +18 -34
  76. package/build/sfcc/tool-wrapper.js +294 -17
  77. package/build/sfcc/write-grants.js +33 -1
  78. package/build/sfcc/write-guard.js +41 -12
  79. package/build/sfcc/write-result.js +16 -7
  80. package/build/sfcc/writes-custom-object-def.js +12 -4
  81. package/build/sfcc/writes-site-preference.js +6 -1
  82. package/build/sfcc/writes-system-object.js +11 -2
  83. package/build/sfcc/writes.js +13 -8
  84. package/build/start-tickets-prereqs.js +25 -15
  85. package/build/start-tickets.js +123 -21
  86. package/build/version.generated.js +1 -1
  87. package/build/worktree-core.js +9 -3
  88. package/docs/install/mcp-tool-integrations.md +54 -9
  89. package/docs/install/sfcc-integration.md +71 -24
  90. package/package.json +3 -3
  91. 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
+ }