@miraland-labs/conduit-bridge 0.16.187 → 0.16.189

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/dist/cli.js CHANGED
@@ -20,7 +20,7 @@ import { BOOTSTRAP_RESULTS, buildSovereignExecutionFacts, workspaceIsClean } fro
20
20
  import { ensureCheckout } from "./checkout.js";
21
21
  import { buildOnShiftIntentProof, managedWorkspacePath, maybeApplyOnShiftIntent } from "./on-shift-apply.js";
22
22
  import { expandOpsValue, loadOpsEnv, OPS_VERBS, runOps } from "./ops.js";
23
- import { pumpExecutionSlots, renewLeases } from "./execution.js";
23
+ import { assignmentsPending, pumpExecutionSlots, renewLeases } from "./execution.js";
24
24
  import { createRehearsalWorktree, rehearsalWorktreePath, removeAttemptWorktree } from "./attempt-worktree.js";
25
25
  import { applyRunnerToolPath, installRunnerService, uninstallRunnerService } from "./service.js";
26
26
  import { BRIDGE_PROTOCOL_VERSION, cachedBridgePreflight, gateRehearsalLockHeld, planGateRehearsals, PREFLIGHT_CACHE_TTL_MS, runGateRehearsal, unavailableWorkspacePreflight } from "./preflight.js";
@@ -667,9 +667,17 @@ async function runner() {
667
667
  await heartbeat(client, config, workspace, currentBrief, preflight, intentProof, bootstrapResult);
668
668
  }
669
669
  await renewLeases(client, config);
670
+ // Dispatched work outranks a rehearsal already running. Stop it with nothing recorded, release
671
+ // the lock, and claim on this same cycle. The rehearsal stays due.
672
+ if (gateRehearsalRunning && await assignmentsPending(client)) {
673
+ gateRehearsalAbort?.abort();
674
+ await gateRehearsalDone;
675
+ }
670
676
  // Another runner on this computer rehearsing its gate: claim as usual, rehearse on a later cycle.
677
+ // An assignment already waiting is claimed below; this cycle does not start a rehearsal.
671
678
  if (workspace && onlineDriverIds(config).length && !gateRehearsalRunning && rehearsalPlan?.due
672
- && running.size === 0 && Object.keys(config.activeAttempts).length === 0 && !gateRehearsalLockHeld()) {
679
+ && running.size === 0 && Object.keys(config.activeAttempts).length === 0 && !gateRehearsalLockHeld()
680
+ && !await assignmentsPending(client)) {
673
681
  startGateRehearsal(rehearsalPlan.due.workspace, rehearsalPlan.due.plan);
674
682
  }
675
683
  // No claim while the gate rehearses: the rehearsal measures the gate alone, never beside an attempt.
@@ -729,8 +737,10 @@ async function runner() {
729
737
  await new Promise((resolveSleep) => setTimeout(resolveSleep, intervalMs));
730
738
  }
731
739
  }
732
- /** True while the gate rehearsal runs in the background; the loop keeps beating but claims nothing. */
740
+ /** True while the gate rehearsal runs in the background. A dispatched assignment aborts it, then this cycle claims. */
733
741
  let gateRehearsalRunning = false;
742
+ let gateRehearsalAbort = null;
743
+ let gateRehearsalDone = Promise.resolve();
734
744
  /** An observation fingerprints the source checkout; a fast-forward under it reads as the observer writing. */
735
745
  let investigationRunning = false;
736
746
  /** The rehearsals the heartbeat reports, one per on-shift repository; empty reports no field. */
@@ -777,21 +787,30 @@ async function rehearsalCheckouts(workspace, brief, refresh) {
777
787
  */
778
788
  function startGateRehearsal(workspace, plan) {
779
789
  gateRehearsalRunning = true;
790
+ const abort = new AbortController();
791
+ gateRehearsalAbort = abort;
780
792
  const rehearsalPath = rehearsalWorktreePath(workspace);
781
793
  console.log(`Rehearsing the gate in ${workspace}: ${plan.commands.join(", ")}`);
782
- void createRehearsalWorktree(workspace, plan.commit)
794
+ gateRehearsalDone = createRehearsalWorktree(workspace, plan.commit)
783
795
  .then((path) => runGateRehearsal(path, plan, {
796
+ signal: abort.signal,
784
797
  freshWorkspace: (warmUpSeconds) => {
785
798
  console.log(`Gate warm-up took ${warmUpSeconds} s; measuring again in a fresh worktree`);
786
799
  return createRehearsalWorktree(workspace, plan.commit);
787
800
  },
788
801
  }))
789
- .then((rehearsal) => { if (rehearsal === "busy")
790
- console.log("Gate rehearsal deferred: another runner on this computer is rehearsing"); })
802
+ .then((rehearsal) => {
803
+ if (rehearsal === "busy")
804
+ console.log("Gate rehearsal deferred: another runner on this computer is rehearsing");
805
+ else if (abort.signal.aborted)
806
+ console.log("Gate rehearsal stopped: an assignment is waiting");
807
+ })
791
808
  .catch((error) => console.error(`Gate rehearsal failed: ${redactSecrets(error instanceof Error ? error.message : "unknown error")}`))
792
809
  .finally(async () => {
793
810
  await removeAttemptWorktree(workspace, rehearsalPath);
794
811
  gateRehearsalRunning = false;
812
+ if (gateRehearsalAbort === abort)
813
+ gateRehearsalAbort = null;
795
814
  });
796
815
  }
797
816
  /** Last state announced, so a workspace that stays diverged does not print on every cycle. */
@@ -5,8 +5,9 @@
5
5
  */
6
6
  import { spawn } from "node:child_process";
7
7
  import { createHash } from "node:crypto";
8
- import { mkdir, mkdtemp, readFile, realpath, rm } from "node:fs/promises";
9
- import { join } from "node:path";
8
+ import { access, copyFile, mkdir, mkdtemp, readFile, readdir, realpath, rm } from "node:fs/promises";
9
+ import { tmpdir } from "node:os";
10
+ import { join, relative, sep } from "node:path";
10
11
  import { untrustedCodeTempRoot, untrustedCodeSpawn, untrustedCodeEnvironment, criterionKey, resolveCriterionReference, signalGroup, STDIO_DRAIN_MS, CHILD_STOP_GRACE_MS, OWN_PROCESS_GROUP, trackProcessGroup, untrackProcessGroup } from "./driver.js";
11
12
  import { isBoundedVerificationCommand, isRunnableVerificationCommand } from "./execution-class.js";
12
13
  import { workspaceDependenciesMissingFailure } from "./failure-signal.js";
@@ -417,6 +418,190 @@ export async function fetchLockedDependencies(workspace, options = {}) {
417
418
  console.error(redactSecrets(`[bridge] cargo fetch --locked exited ${result.code} in ${workspace}: ${result.stderr.trim().split("\n").slice(-10).join("\n")}`));
418
419
  return { ran: true, code: result.code };
419
420
  }
421
+ const NPM_CI_ARGV = ["ci", "--ignore-scripts", "--no-audit", "--no-fund", "--prefer-offline"];
422
+ /** Root is depth 0. `web/` is depth 1. `packages/<name>/` is depth 2. Nothing deeper. */
423
+ const MAX_PACKAGE_DIRECTORY_DEPTH = 2;
424
+ /** Discovery does not enter these directories. `node_modules` is recorded separately and not entered. */
425
+ const PACKAGE_DISCOVERY_EXCLUDED = new Set([".conduit", ".git", "dist", "target"]);
426
+ /** Package-directory realpath + npm lock sha256 already attempted by this process. */
427
+ const installedLocks = new Set();
428
+ /**
429
+ * The gate runs without network, so a package the lock pins must already be on disk. Node resolves
430
+ * from the directory that holds the gate and walks upward: a repository whose JavaScript lives in
431
+ * `web/` with its own lock, and whose root is not a package, never reaches an install. `npm ci`
432
+ * fills that directory outside the sandbox and refuses to change the lock. Once per lock file per
433
+ * process, including a failed install. A failed install does not stop the gate: its own output
434
+ * names the cause.
435
+ *
436
+ * `package-lock.json` and `npm-shrinkwrap.json` are npm locks. A package with only `yarn.lock` or
437
+ * `pnpm-lock.yaml`, a package with no npm lock, and a package whose `node_modules` git does not
438
+ * ignore are skipped and logged. Discovery does not enter `.git`, `.conduit`, `dist`, `node_modules`,
439
+ * or `target`.
440
+ */
441
+ function runGit(argv, cwd, env = process.env) {
442
+ return new Promise((resolve) => {
443
+ let settled = false;
444
+ let timer;
445
+ const finish = (code, stdout) => {
446
+ if (settled)
447
+ return;
448
+ settled = true;
449
+ clearTimeout(timer);
450
+ resolve({ code, stdout });
451
+ };
452
+ const child = spawn("git", argv, { cwd, env, stdio: ["ignore", "pipe", "ignore"] });
453
+ let stdout = "";
454
+ child.stdout?.setEncoding("utf8");
455
+ child.stdout?.on("data", (chunk) => {
456
+ if (stdout.length < 8192)
457
+ stdout = `${stdout}${chunk}`.slice(0, 8192);
458
+ });
459
+ timer = setTimeout(() => { child.kill("SIGKILL"); finish(124, stdout); }, VERIFICATION_TIMEOUT_MS);
460
+ child.on("error", () => finish(127, stdout));
461
+ child.on("close", (code) => finish(code ?? 1, stdout));
462
+ });
463
+ }
464
+ function gitCommandEnv() {
465
+ const env = { ...process.env };
466
+ delete env.GIT_DIR;
467
+ delete env.GIT_WORK_TREE;
468
+ delete env.GIT_INDEX_FILE;
469
+ return env;
470
+ }
471
+ /** A directory outside `workspace`, so a crash cannot leave a probe inside the attempt tree. */
472
+ function probeParentDirectory(workspace) {
473
+ for (const candidate of [tmpdir(), "/tmp", "/var/tmp"]) {
474
+ const rel = relative(workspace, candidate);
475
+ if (rel === ".." || rel.startsWith(`..${sep}`))
476
+ return candidate;
477
+ }
478
+ return tmpdir();
479
+ }
480
+ async function copyGitignoreIfPresent(from, to) {
481
+ try {
482
+ await copyFile(join(from, ".gitignore"), join(to, ".gitignore"));
483
+ return true;
484
+ }
485
+ catch (error) {
486
+ return error.code === "ENOENT";
487
+ }
488
+ }
489
+ /**
490
+ * A directory-only rule (`node_modules/`) matches only a directory on disk. The probe directory is
491
+ * created in a temporary work tree, never in the attempt tree. `-c core.excludesFile=/dev/null`
492
+ * keeps the repository's own ignore rules and drops the runner's global excludes, so a personal
493
+ * ignore cannot hide a node_modules git would track.
494
+ */
495
+ async function nodeModulesIsGitIgnored(directory) {
496
+ const env = gitCommandEnv();
497
+ const located = await runGit(["rev-parse", "--show-toplevel", "--absolute-git-dir", "--show-prefix"], directory, env);
498
+ if (located.code !== 0)
499
+ return false;
500
+ const lines = located.stdout.split("\n");
501
+ const toplevel = lines[0]?.trim() ?? "";
502
+ const gitDir = lines[1]?.trim() ?? "";
503
+ const prefix = lines[2] ?? "";
504
+ if (!toplevel || !gitDir)
505
+ return false;
506
+ const temp = await mkdtemp(join(probeParentDirectory(directory), "conduit-npm-ignore-"));
507
+ try {
508
+ let source = toplevel;
509
+ let target = temp;
510
+ if (!await copyGitignoreIfPresent(source, target))
511
+ return false;
512
+ for (const segment of prefix.split("/").filter(Boolean)) {
513
+ source = join(source, segment);
514
+ target = join(target, segment);
515
+ await mkdir(target, { recursive: true });
516
+ if (!await copyGitignoreIfPresent(source, target))
517
+ return false;
518
+ }
519
+ await mkdir(join(target, "node_modules"));
520
+ const relativeDir = prefix === "" || prefix.endsWith("/") ? prefix : `${prefix}/`;
521
+ const checked = await runGit([
522
+ "--git-dir", gitDir, "--work-tree", temp, "-c", "core.excludesFile=/dev/null",
523
+ "check-ignore", "-q", "--", `${relativeDir}node_modules`,
524
+ ], temp, env);
525
+ return checked.code === 0;
526
+ }
527
+ finally {
528
+ await rm(temp, { recursive: true, force: true });
529
+ }
530
+ }
531
+ async function scanPackageDirectories(root) {
532
+ const packages = [];
533
+ const skipped = [];
534
+ const walk = async (directory, depth) => {
535
+ if (await readFile(join(directory, "package.json")).then(() => true, () => false))
536
+ packages.push(directory);
537
+ if (depth >= MAX_PACKAGE_DIRECTORY_DEPTH)
538
+ return;
539
+ const entries = await readdir(directory, { withFileTypes: true }).catch(() => []);
540
+ for (const entry of entries) {
541
+ if (!entry.isDirectory())
542
+ continue;
543
+ const child = join(directory, entry.name);
544
+ if (entry.name === "node_modules") {
545
+ skipped.push({ directory: child, reason: "package is under node_modules" });
546
+ continue;
547
+ }
548
+ if (PACKAGE_DISCOVERY_EXCLUDED.has(entry.name))
549
+ continue;
550
+ await walk(child, depth + 1);
551
+ }
552
+ };
553
+ await walk(root, 0);
554
+ packages.sort();
555
+ return { packages, skipped };
556
+ }
557
+ /** npm uses `npm-shrinkwrap.json` when both lock files exist. */
558
+ async function readNpmLock(directory) {
559
+ const shrink = await readFile(join(directory, "npm-shrinkwrap.json")).catch(() => null);
560
+ if (shrink)
561
+ return shrink;
562
+ return readFile(join(directory, "package-lock.json")).catch(() => null);
563
+ }
564
+ async function missingNpmLockReason(directory) {
565
+ const yarn = await access(join(directory, "yarn.lock")).then(() => true, () => false);
566
+ const pnpm = await access(join(directory, "pnpm-lock.yaml")).then(() => true, () => false);
567
+ if (yarn && pnpm)
568
+ return "yarn.lock and pnpm-lock.yaml are not npm installs";
569
+ if (yarn)
570
+ return "yarn.lock is not an npm install";
571
+ if (pnpm)
572
+ return "pnpm-lock.yaml is not an npm install";
573
+ return "no package-lock.json or npm-shrinkwrap.json";
574
+ }
575
+ export async function installLockedPackages(workspace, options = {}) {
576
+ const installed = [];
577
+ const skipped = [];
578
+ const run = options.run ?? runFetchOutsideSandbox;
579
+ const scan = await scanPackageDirectories(workspace);
580
+ skipped.push(...scan.skipped);
581
+ for (const directory of scan.packages) {
582
+ const lock = await readNpmLock(directory);
583
+ if (!lock) {
584
+ skipped.push({ directory, reason: await missingNpmLockReason(directory) });
585
+ continue;
586
+ }
587
+ const key = `${await realpath(directory)}\0${createHash("sha256").update(lock).digest("hex")}`;
588
+ if (installedLocks.has(key))
589
+ continue;
590
+ if (!await nodeModulesIsGitIgnored(directory)) {
591
+ skipped.push({ directory, reason: "node_modules is not git-ignored" });
592
+ continue;
593
+ }
594
+ const result = await run("npm", [...NPM_CI_ARGV], directory);
595
+ installedLocks.add(key);
596
+ if (result.code !== 0)
597
+ console.error(redactSecrets(`[bridge] npm ci --ignore-scripts --no-audit --no-fund --prefer-offline exited ${result.code} in ${directory}: ${result.stderr.trim().split("\n").slice(-10).join("\n")}`));
598
+ installed.push({ directory, code: result.code });
599
+ }
600
+ skipped.sort((left, right) => left.directory < right.directory ? -1 : left.directory > right.directory ? 1 : 0);
601
+ for (const skip of skipped)
602
+ console.error(redactSecrets(`[bridge] npm ci skipped ${skip.directory}: ${skip.reason}`));
603
+ return { installed, skipped };
604
+ }
420
605
  /**
421
606
  * The bounded runner every Bridge-run verification uses: the declared command, in the given
422
607
  * workspace, with the verification timeout and buffer. A non-zero exit is a result, not a throw.
@@ -486,6 +671,7 @@ export async function runBoundedVerificationCommand(command, workspace, timeoutM
486
671
  };
487
672
  const output = () => [lines.text().replace(/\n$/, ""), partial.stdout.text(), partial.stderr.text()].filter(Boolean).join("\n");
488
673
  await fetchLockedDependencies(workspace);
674
+ await installLockedPackages(workspace);
489
675
  const tempRoot = untrustedCodeTempRoot(workspace);
490
676
  await mkdir(tempRoot, { recursive: true });
491
677
  const scratchHome = await mkdtemp(join(tempRoot, "gate-home-"));
package/dist/execution.js CHANGED
@@ -832,6 +832,21 @@ export async function pumpExecutionSlots(client, config, workspace, brief, timeo
832
832
  }
833
833
  return progressed;
834
834
  }
835
+ /**
836
+ * True when `/runner/v1/assignments` lists work for this machine. A failed poll is not waiting
837
+ * work: the caller keeps the current cycle. The claim path still parses and claims.
838
+ */
839
+ export async function assignmentsPending(client) {
840
+ try {
841
+ const data = await client.request("/runner/v1/assignments");
842
+ if (!data || typeof data !== "object" || !("assignments" in data))
843
+ return false;
844
+ return Array.isArray(data.assignments) && data.assignments.length > 0;
845
+ }
846
+ catch {
847
+ return false;
848
+ }
849
+ }
835
850
  /** Claim one assignment when under capacity; does not start the agent (multi-slot pump does). */
836
851
  export async function claimNextAssignment(client, config, workspace, brief, driverId, processOnlineIds, options = {}) {
837
852
  if (Object.keys(config.activeAttempts).length >= config.leaseCapacity)
package/dist/preflight.js CHANGED
@@ -10,7 +10,7 @@ import { bridgeConfigDirectory } from "./config.js";
10
10
  import { runBoundedVerificationCommand, VERIFICATION_TIMEOUT_MS, verificationEvidenceCommands } from "./ensure-test-evidence.js";
11
11
  import { lockfileDigest } from "./execution-facts.js";
12
12
  import { resolveAgentTimeout, resolveMaxTurns } from "./execution-budget.js";
13
- import { hasAntigravityLogin, hasClaudeLogin, hasGrokLogin, hasOpenAiLogin, hasOpenCodeLogin, driverExecutable, readSignInStatus, } from "./driver.js";
13
+ import { hasAntigravityLogin, hasClaudeLogin, hasGrokLogin, hasOpenAiLogin, hasOpenCodeLogin, driverExecutable, readSignInStatus, stopTrackedProcessGroups, } from "./driver.js";
14
14
  import { laneQuota, laneTierBindings, localFuelOnlyDriver, onlineDriverIds, resolveDriverFuel, supportsReadOnlyDiagnosis } from "./drivers.js";
15
15
  /** Protocol 3: read-only diagnosis can reuse a retained failed-attempt worktree. */
16
16
  // 4: each driver snapshot reports diagnosis_read_only, so the control plane can require a
@@ -421,23 +421,70 @@ async function acquireGateRehearsalLock(lockPath) {
421
421
  await unlink(claim).catch(() => undefined);
422
422
  }
423
423
  }
424
+ /** Resolve when `work` settles, or with "aborted" when `signal` aborts first. */
425
+ function untilAbort(work, signal) {
426
+ if (!signal)
427
+ return work;
428
+ if (signal.aborted)
429
+ return Promise.resolve("aborted");
430
+ return new Promise((resolve, reject) => {
431
+ const onAbort = () => resolve("aborted");
432
+ signal.addEventListener("abort", onAbort, { once: true });
433
+ work.then((value) => {
434
+ signal.removeEventListener("abort", onAbort);
435
+ resolve(signal.aborted ? "aborted" : value);
436
+ }, (error) => {
437
+ signal.removeEventListener("abort", onAbort);
438
+ if (signal.aborted)
439
+ resolve("aborted");
440
+ else
441
+ reject(error);
442
+ });
443
+ });
444
+ }
445
+ /** Call `stop` if `signal` aborts after this returns. An already-aborted signal has no command yet. */
446
+ function stopWhenAborted(signal, stop) {
447
+ if (!signal || signal.aborted)
448
+ return () => undefined;
449
+ signal.addEventListener("abort", stop, { once: true });
450
+ return () => signal.removeEventListener("abort", stop);
451
+ }
452
+ async function runRehearsalCommand(run, command, cwd, timeoutMs, signal) {
453
+ if (signal?.aborted)
454
+ return "aborted";
455
+ const work = (signal ? run(command, cwd, timeoutMs, signal) : run(command, cwd, timeoutMs)).catch(() => null);
456
+ const result = await untilAbort(work, signal);
457
+ if (result === "aborted" || signal?.aborted)
458
+ return "aborted";
459
+ return result;
460
+ }
424
461
  /**
425
- * Run the planned gate once and remember it for the day. Null when no command could start.
426
- * "busy" when another rehearsal on this computer holds the lock: nothing is run or remembered, so
427
- * the gate is still due and the next cycle tries again.
462
+ * Run the planned gate once and remember it for the day. Null when no command could start, or when
463
+ * `signal` aborts: the command is stopped, nothing is remembered, and the lock is released, so the
464
+ * gate stays due. "busy" when another rehearsal on this computer holds the lock: nothing is run or
465
+ * remembered, so the gate is still due and the next cycle tries again.
428
466
  */
429
467
  export async function runGateRehearsal(workspace, plan, options = {}) {
468
+ if (options.signal?.aborted)
469
+ return null;
430
470
  const lockPath = options.lockPath ?? gateRehearsalLockPath();
431
471
  if (!await acquireGateRehearsalLock(lockPath))
432
472
  return "busy";
473
+ const releaseAbort = stopWhenAborted(options.signal, stopTrackedProcessGroups);
433
474
  try {
475
+ if (options.signal?.aborted)
476
+ return null;
434
477
  return await rehearseGate(workspace, plan, options);
435
478
  }
436
479
  finally {
480
+ releaseAbort();
437
481
  await unlink(lockPath).catch(() => undefined);
438
482
  }
439
483
  }
440
484
  async function rehearseGate(workspace, plan, options) {
485
+ const signal = options.signal;
486
+ if (signal?.aborted)
487
+ return null;
441
488
  const now = options.now ?? Date.now;
442
489
  const run = options.runCommand ?? runBoundedVerificationCommand;
443
490
  const commands = [];
@@ -445,7 +492,10 @@ async function rehearseGate(workspace, plan, options) {
445
492
  for (const command of plan.commands) {
446
493
  const started = now();
447
494
  // A command that cannot start measured nothing; the dry run skips it the same way.
448
- const result = await run(command, cwd, REHEARSAL_TIMEOUT_MS).catch(() => null);
495
+ // An abort measures nothing either: the gate stays due for the next quiet cycle.
496
+ const result = await runRehearsalCommand(run, command, cwd, REHEARSAL_TIMEOUT_MS, signal);
497
+ if (result === "aborted")
498
+ return null;
449
499
  if (!result)
450
500
  continue;
451
501
  const elapsed = now() - started;
@@ -456,10 +506,16 @@ async function rehearseGate(workspace, plan, options) {
456
506
  }
457
507
  // The pass fit only the rehearsal's limit: it was a warm-up. Measure once more, in a fresh
458
508
  // checkout with the attempt's limit, so the report shows what an attempt will see.
459
- if (options.freshWorkspace)
460
- cwd = await options.freshWorkspace(seconds);
509
+ if (options.freshWorkspace) {
510
+ const fresh = await untilAbort(options.freshWorkspace(seconds), signal);
511
+ if (fresh === "aborted")
512
+ return null;
513
+ cwd = fresh;
514
+ }
461
515
  const again = now();
462
- const second = await run(command, cwd, VERIFICATION_TIMEOUT_MS).catch(() => null);
516
+ const second = await runRehearsalCommand(run, command, cwd, VERIFICATION_TIMEOUT_MS, signal);
517
+ if (second === "aborted")
518
+ return null;
463
519
  if (!second) {
464
520
  commands.push({ command, seconds, exit_code: result.code, timed_out: false });
465
521
  continue;
@@ -472,6 +528,8 @@ async function rehearseGate(workspace, plan, options) {
472
528
  warm_up_seconds: seconds,
473
529
  });
474
530
  }
531
+ if (signal?.aborted)
532
+ return null;
475
533
  const at = now();
476
534
  const rehearsal = commands.length
477
535
  ? { repository: plan.repository, commit: plan.commit, rehearsed_at: new Date(at).toISOString(), commands }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@miraland-labs/conduit-bridge",
3
- "version": "0.16.187",
3
+ "version": "0.16.189",
4
4
  "description": "Conduit Bridge CLI — join, connect, disconnect, multi-driver lanes, and run Claude Code / Codex / Cursor / OpenCode / Pi / Kiro / Antigravity / Grok Build agents for a Conduit organization",
5
5
  "type": "module",
6
6
  "bin": {