@bridge_gpt/mcp-server 0.2.48 → 0.2.50

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 (47) hide show
  1. package/README.md +24 -7
  2. package/build/base-ref.js +28 -3
  3. package/build/claude-review-workflow-drift-probe.js +130 -0
  4. package/build/claude-review-workflow-drift.js +173 -0
  5. package/build/claude-review-workflow.js +81 -16
  6. package/build/commands.generated.js +5 -5
  7. package/build/conductor/done-gate.js +25 -3
  8. package/build/conductor/install-doctor.js +65 -5
  9. package/build/conductor/latest-check-selector.js +170 -0
  10. package/build/conductor/local-merge.js +8 -6
  11. package/build/conductor-bin.js +1 -1
  12. package/build/{brainstorm-files.js → council-files.js} +15 -15
  13. package/build/decision-page-schema.js +1 -1
  14. package/build/docs.generated.js +1 -1
  15. package/build/doctor.js +162 -4
  16. package/build/executor/worktree.js +46 -1
  17. package/build/index.js +92 -51
  18. package/build/init.js +9 -2
  19. package/build/install-bridge.js +60 -2
  20. package/build/install-reexec.js +47 -9
  21. package/build/pipelines.generated.js +1 -1
  22. package/build/plane/cli.js +12 -2
  23. package/build/plane/manifest.js +25 -1
  24. package/build/plane/member-roster.js +61 -7
  25. package/build/plane/preflight.js +24 -9
  26. package/build/plane/supervisor.js +77 -5
  27. package/build/plane/types.js +23 -3
  28. package/build/readme.generated.js +1 -1
  29. package/build/run-unit-tests-launcher.js +2 -1
  30. package/build/stale-worktree-doctor.js +120 -0
  31. package/build/start-tickets-prereqs.js +70 -0
  32. package/build/start-tickets.js +91 -3
  33. package/build/version.generated.js +3 -2
  34. package/package.json +4 -2
  35. package/build/chain-orchestrator.js +0 -1457
  36. package/build/chain-utils.js +0 -68
  37. package/build/command-catalog.js +0 -376
  38. package/build/schedule-run.js +0 -1300
  39. package/build/schedule-store.js +0 -172
  40. package/build/scheduled-prompt.js +0 -115
  41. package/build/scheduler-backends/at-fallback.js +0 -139
  42. package/build/scheduler-backends/escaping.js +0 -143
  43. package/build/scheduler-backends/index.js +0 -72
  44. package/build/scheduler-backends/launchd.js +0 -225
  45. package/build/scheduler-backends/systemd-user.js +0 -250
  46. package/build/scheduler-backends/task-scheduler.js +0 -214
  47. package/build/scheduler-backends/types.js +0 -23
@@ -12,7 +12,7 @@
12
12
  * it — including any manifest belonging to a plane that is already running.
13
13
  */
14
14
  import path from "path";
15
- import { PLANE_SERVER_BASE_URL, PLANE_SERVER_HOST, PLANE_SERVER_PORT, } from "./types.js";
15
+ import { PLANE_SERVER_PORT_ENV_VAR, } from "./types.js";
16
16
  import { checkPlaneBuildFreshness, checkPlaneRuntimeEntrypoint } from "./build-freshness.js";
17
17
  import { checkAlembicHead } from "./alembic-head.js";
18
18
  import { manifestHasLiveProcess, readPlaneManifest } from "./manifest.js";
@@ -56,7 +56,15 @@ export async function runPlanePreflight(repoRoot, deps) {
56
56
  // unresolvable re-exec target aggregates with them instead of short-circuiting.
57
57
  const runtimeEntrypoint = deps.resolveRuntimeEntrypoint();
58
58
  add(checkPlaneRuntimeEntrypoint(runtimeEntrypoint));
59
- add(await checkServerPort(deps));
59
+ // An unresolvable override is a configuration failure, not a port failure:
60
+ // there is no port to probe, so the probe is skipped entirely rather than
61
+ // falling back to 8000 and reporting on a port the operator did not ask for.
62
+ if (deps.endpoint.ok) {
63
+ add(await checkServerPort(deps, deps.endpoint.endpoint));
64
+ }
65
+ else {
66
+ add({ check: "server-port", severity: "blocking", message: deps.endpoint.message });
67
+ }
60
68
  add(await checkAlembicHead(repoRoot, {
61
69
  execFile: deps.execFile,
62
70
  fileExists: (filePath) => fileExists(filePath, deps.fs),
@@ -67,7 +75,10 @@ export async function runPlanePreflight(repoRoot, deps) {
67
75
  // `!runtimeEntrypoint.ok` is already covered by the blocking count; it is
68
76
  // repeated here so the compiler narrows the union rather than requiring a
69
77
  // non-null assertion on the context field below.
70
- if (blocking.length > 0 || !credentials.ok || !runtimeEntrypoint.ok) {
78
+ // `!deps.endpoint.ok` is already covered by the blocking count; it is repeated
79
+ // here, like `!runtimeEntrypoint.ok`, so the compiler narrows the union rather
80
+ // than requiring a non-null assertion on the context field below.
81
+ if (blocking.length > 0 || !credentials.ok || !runtimeEntrypoint.ok || !deps.endpoint.ok) {
71
82
  return { ok: false, diagnostics };
72
83
  }
73
84
  return {
@@ -76,7 +87,9 @@ export async function runPlanePreflight(repoRoot, deps) {
76
87
  context: {
77
88
  repoRoot,
78
89
  repoName: credentials.repoName,
79
- baseUrl: PLANE_SERVER_BASE_URL,
90
+ // The SAME object `checkServerPort` probed above. Never re-resolved, so
91
+ // the probed port and the launched port cannot diverge.
92
+ endpoint: deps.endpoint.endpoint,
80
93
  // Location-chosen: always THIS repository's build tree, so an executor can
81
94
  // never come from a published package.
82
95
  executorEntrypoint: path.join(repoRoot, "mcp_server", "build", "index.js"),
@@ -296,23 +309,25 @@ function createResolverWarningBuffer() {
296
309
  * let two servers race for the port, and silently assuming "occupied" would
297
310
  * refuse a perfectly good bring-up.
298
311
  */
299
- export async function checkServerPort(deps) {
300
- const result = await deps.probePort(PLANE_SERVER_HOST, PLANE_SERVER_PORT, PLANE_PORT_PROBE_TIMEOUT_MS);
312
+ export async function checkServerPort(deps, endpoint) {
313
+ const target = `${endpoint.host}:${endpoint.port}`;
314
+ const result = await deps.probePort(endpoint.host, endpoint.port, PLANE_PORT_PROBE_TIMEOUT_MS);
301
315
  if (result.kind === "refused")
302
316
  return null;
303
317
  if (result.kind === "connected") {
304
318
  return {
305
319
  check: "server-port",
306
320
  severity: "blocking",
307
- message: `${PLANE_SERVER_HOST}:${PLANE_SERVER_PORT} is already accepting connections. ` +
321
+ message: `${target} is already accepting connections. ` +
308
322
  "That port may belong to a SIBLING WORKTREE's server — check before you kill it. " +
309
- "Stop the existing server (or wind down its plane) and retry.",
323
+ `Stop the existing server (or wind down its plane) and retry, or set ${PLANE_SERVER_PORT_ENV_VAR} ` +
324
+ "to a free port.",
310
325
  };
311
326
  }
312
327
  return {
313
328
  check: "server-port",
314
329
  severity: "warning",
315
- message: `could not determine whether ${PLANE_SERVER_HOST}:${PLANE_SERVER_PORT} is free ` +
330
+ message: `could not determine whether ${target} is free ` +
316
331
  `(${result.error}); startup continues and uvicorn will fail loudly if the port is taken.`,
317
332
  };
318
333
  }
@@ -176,7 +176,7 @@ export async function launchPlaneSupervisor(params, deps) {
176
176
  }
177
177
  runtimeEnv[PLANE_ID_ENV_VAR] = manifest.planeId;
178
178
  runtimeEnv.BAPI_REPO_NAME = context.repoName;
179
- runtimeEnv.BAPI_BASE_URL = context.baseUrl;
179
+ runtimeEnv.BAPI_BASE_URL = context.endpoint.baseUrl;
180
180
  runtimeEnv.BAPI_API_KEY = context.bridgeApiKey;
181
181
  let child;
182
182
  try {
@@ -339,12 +339,76 @@ export async function runPlaneRuntime(repoRoot, roster, deps) {
339
339
  })),
340
340
  updatedAt: deps.clock.now().toISOString(),
341
341
  };
342
- await writePlaneManifest(manifest, deps.fs);
343
342
  const started = [];
344
- const persist = async () => {
343
+ // ---- Serialized manifest persistence (BAPI-950) --------------------------
344
+ //
345
+ // `writePlaneManifest` publishes through a temporary file and an atomic
346
+ // rename. That is safe for ONE writer and unsafe for two: overlapping writers
347
+ // interleave on the temp file and the rename then publishes a half-written
348
+ // `plane.json`, which `readPlaneManifest` rejects as "manifest is not valid
349
+ // JSON" and `plane down` refuses to act on — the corrupt manifest is then
350
+ // un-removable and poisons every later bring-up. The runtime used to make
351
+ // that overlap routine: the spawn loop awaited `persist()` while every
352
+ // member's `close` handler fired `void persist()` with nothing serializing
353
+ // them, so reaping several members in one window (exactly what `plane down`
354
+ // and a SIGKILL do) raced them against each other.
355
+ //
356
+ // One promise tail removes the overlap for this process's own writes.
357
+ let persistTail = Promise.resolve();
358
+ // Writes nobody awaits inline — the `close`-handler ones. Held so the runtime
359
+ // can wait for them instead of leaving detached filesystem work behind.
360
+ const pendingPersists = new Set();
361
+ // Set the moment the manifest's ownership passes to `shutdownPlane`, which
362
+ // removes it. A write that landed after that removal would resurrect a
363
+ // `plane.json` describing a plane that no longer exists.
364
+ let manifestReleased = false;
365
+ const persist = () => {
366
+ if (manifestReleased)
367
+ return Promise.resolve();
368
+ // Stamped and snapshotted SYNCHRONOUSLY, at enqueue time. A queued write
369
+ // must publish the state as it stood when it was enqueued; reading the
370
+ // mutable `manifest` binding when the write finally runs would let a later
371
+ // member patch overwrite an earlier transition's record.
345
372
  manifest = { ...manifest, updatedAt: deps.clock.now().toISOString() };
346
- await writePlaneManifest(manifest, deps.fs);
373
+ const snapshot = manifest;
374
+ const operation = persistTail.then(() => writePlaneManifest(snapshot, deps.fs));
375
+ // The tail is recovered but the caller's promise is not: one failed write
376
+ // must not poison every write queued behind it, and it must not be
377
+ // swallowed either. `operation` still rejects with the original error.
378
+ persistTail = operation.catch(() => undefined);
379
+ return operation;
347
380
  };
381
+ /**
382
+ * Enroll a write whose completion no caller awaits, and observe its failure.
383
+ *
384
+ * The message is fixed prose. The thrown value can carry a path or an errno
385
+ * struct, and the runtime's stderr is the operator's terminal.
386
+ */
387
+ const trackPersist = (operation) => {
388
+ const tracked = operation.catch(() => {
389
+ deps.sinks.stderr("Plane manifest could not be updated after a member exit.");
390
+ });
391
+ pendingPersists.add(tracked);
392
+ void tracked.then(() => {
393
+ pendingPersists.delete(tracked);
394
+ });
395
+ };
396
+ /** Wait for every manifest write already enqueued, including untracked ones. */
397
+ const drainPersists = async () => {
398
+ // Looped because a member can exit — and enqueue — while an earlier write
399
+ // is still settling. Each close enqueues exactly one write, and `persist`
400
+ // stops enqueuing once the manifest is released, so this terminates.
401
+ while (pendingPersists.size > 0) {
402
+ await Promise.all([...pendingPersists]);
403
+ }
404
+ await persistTail;
405
+ };
406
+ /** Hand the manifest to `shutdownPlane` with no runtime write still in flight. */
407
+ const releaseManifest = async () => {
408
+ manifestReleased = true;
409
+ await drainPersists();
410
+ };
411
+ await persist();
348
412
  const patchMember = (name, patch) => {
349
413
  manifest = {
350
414
  ...manifest,
@@ -357,6 +421,10 @@ export async function runPlaneRuntime(repoRoot, roster, deps) {
357
421
  return null;
358
422
  shuttingDown = true;
359
423
  deps.sinks.stdout(`Received ${signal} — winding the plane down.`);
424
+ // Before `shutdownPlane` reads and removes the manifest, not after: it must
425
+ // see this runtime's completed state, and no queued write may outlive its
426
+ // removal.
427
+ await releaseManifest();
360
428
  return shutdownPlane(repoRoot, {
361
429
  fs: deps.fs,
362
430
  proc: deps.proc,
@@ -413,7 +481,10 @@ export async function runPlaneRuntime(repoRoot, roster, deps) {
413
481
  child.on("close", ((code, signal) => {
414
482
  const timestamp = deps.clock.now().toISOString();
415
483
  patchMember(spec.name, { state: "exited", exitCode: code, exitSignal: signal });
416
- void persist();
484
+ // Enrolled synchronously so simultaneous exits queue in the order their
485
+ // patches were applied, and tracked so the write is awaited — never
486
+ // abandoned — before the runtime returns.
487
+ trackPersist(persist());
417
488
  emitMemberEvent(deps.sinks, {
418
489
  member: spec.name,
419
490
  kind: "exit",
@@ -499,6 +570,7 @@ export async function runPlaneRuntime(repoRoot, roster, deps) {
499
570
  if (started.length > 0) {
500
571
  deps.sinks.stderr(`Rolling back ${started.length} member(s) that had already started.`);
501
572
  shuttingDown = true;
573
+ await releaseManifest();
502
574
  const result = await shutdownPlane(repoRoot, {
503
575
  fs: deps.fs,
504
576
  proc: deps.proc,
@@ -35,12 +35,32 @@ export const PLANE_MANIFEST_FILENAME = "plane.json";
35
35
  export const PLANE_RUNTIME_LOG_FILENAME = "runtime.log";
36
36
  /** Repository-relative path of the runtime startup trace. */
37
37
  export const PLANE_RUNTIME_LOG_PATH = `${PLANE_RUNTIME_DIR}/${PLANE_RUNTIME_LOG_FILENAME}`;
38
- /** Host the local Bridge API server binds for an attended run. */
38
+ /**
39
+ * Host the local Bridge API server binds for an attended run.
40
+ *
41
+ * Loopback, and NOT overridable. The plane binds a development server with no
42
+ * authentication in front of it; letting an operator move it off `127.0.0.1`
43
+ * would expose that server to the local network, which is a different decision
44
+ * from "run it on a different port because 8000 is taken".
45
+ */
39
46
  export const PLANE_SERVER_HOST = "127.0.0.1";
40
- /** Port the local Bridge API server binds for an attended run. */
47
+ /** Default port the local Bridge API server binds for an attended run. */
41
48
  export const PLANE_SERVER_PORT = 8000;
42
- /** Base URL the executor members are pointed at. */
49
+ /** Default base URL the executor members are pointed at. */
43
50
  export const PLANE_SERVER_BASE_URL = `http://${PLANE_SERVER_HOST}:${PLANE_SERVER_PORT}`;
51
+ /**
52
+ * Environment variable that overrides {@link PLANE_SERVER_PORT}.
53
+ *
54
+ * Named to match the existing `BAPI_PLANE_PYTHON` / `BAPI_PLANE_UVICORN`
55
+ * convention: `BAPI_PLANE_*` is the plane's own configuration namespace, kept
56
+ * distinct from `BAPI_*` values (`BAPI_BASE_URL`, `BAPI_API_KEY`) that the
57
+ * spawned members consume.
58
+ *
59
+ * Only the PORT is configurable. An absent or blank value resolves to exactly
60
+ * the default endpoint — `http://127.0.0.1:8000` — so a machine that never sets
61
+ * it is byte-for-byte unchanged.
62
+ */
63
+ export const PLANE_SERVER_PORT_ENV_VAR = "BAPI_PLANE_PORT";
44
64
  /**
45
65
  * Private action name for the detached supervisor runtime.
46
66
  *