@botbuddy/cli 1.34.1 → 1.34.3

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 (2) hide show
  1. package/package.json +1 -1
  2. package/src/stack.mjs +26 -3
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@botbuddy/cli",
3
- "version": "1.34.1",
3
+ "version": "1.34.3",
4
4
  "description": "BotBuddy — Swarm coordination CLI for multi-agent workflows",
5
5
  "type": "module",
6
6
  "bin": {
package/src/stack.mjs CHANGED
@@ -699,7 +699,8 @@ export function leaseRefusalExit(code) {
699
699
  // BOT-1896: an auth-class refusal (scope_denied, a tenant mismatch, …) is the
700
700
  // server refusing the credential — never BACKEND, which a gate reads as "down".
701
701
  if (CALL_AUTH_ERROR_CODES.has(code)) return EXIT.AUTH;
702
- return code === STACK_PATH_ROOT_REFUSED_CODE ? EXIT.INVALID : EXIT.BACKEND;
702
+ // BOT-1926: a reused lease requested in the other execution mode is fixable by the caller.
703
+ return code === STACK_PATH_ROOT_REFUSED_CODE || code === "STACK_EXECUTION_MODE_MISMATCH" ? EXIT.INVALID : EXIT.BACKEND;
703
704
  }
704
705
 
705
706
  /** The refusal text for a receipt: the server's message, else its code. */
@@ -1372,6 +1373,10 @@ export async function cmdUp(opts, {
1372
1373
  stack_path: execution.stackPath,
1373
1374
  worktree_root: execution.worktreeRoot,
1374
1375
  machine_uuid: hardwareUuid,
1376
+ // BOT-1926: fence the provision job from the Helper server-side. Without it a
1377
+ // polling Helper wins the request→reserve race, provisions the stack itself and
1378
+ // records a targetless connection that `done --local-exec` cannot release.
1379
+ ...(opts.localExec ? { local_exec: true } : {}),
1375
1380
  };
1376
1381
  let req = await call("request_stack_lease", leaseArgs);
1377
1382
  if (!req.ok && req.auth) {
@@ -1627,6 +1632,10 @@ export async function cmdDone(leaseId, opts, {
1627
1632
  let legacyTargetProof = null;
1628
1633
  let teardownDir = process.cwd();
1629
1634
  let leaseBefore = null;
1635
+ // BOT-1926: a lease the Helper provisioned (it won the provision-job race, or the
1636
+ // server predates the local-exec fence) is torn down BY the Helper from its own
1637
+ // manifest. Locally it is not ours to stop: release it and let the reap job run.
1638
+ let helperTeardown = false;
1630
1639
  if (opts.localExec) {
1631
1640
  // BOT-1887: the disposition picks the local teardown (destroy deletes the project's
1632
1641
  // volumes); refuse an unknown one BEFORE the lease is released into `reaping`.
@@ -1640,10 +1649,23 @@ export async function cmdDone(leaseId, opts, {
1640
1649
  error: current.error || current.data?.code || "could not verify the lease Docker target" }), opts,
1641
1650
  current.auth ? EXIT.AUTH : EXIT.BACKEND);
1642
1651
  }
1652
+ leaseBefore = current.data;
1653
+ if (current.data.provisioner === "helper_provisioning") {
1654
+ // Codex #984 P1: the Helper has claimed but not finished provisioning; its manifest
1655
+ // is stored only by its receipt. Releasing now would strand that stack — stay fenced.
1656
+ return emitResult(buildReceipt({ command: "done", outcome: "refused", lease_id: leaseId,
1657
+ error: "the Helper is still provisioning this lease; retry `stack done` once it is active. Lease and slot remain fenced." }), opts, EXIT.LEASE_FAILED);
1658
+ }
1659
+ helperTeardown = current.data.provisioner === "helper";
1660
+ }
1661
+ // BOT-1926: an already-reaped lease has nothing left to tear down locally (only a
1662
+ // `reaping` release runs the local stop), so its target guards cannot protect anything.
1663
+ // Refusing it only kept the hermetic orphan sweep retrying a finished lease forever.
1664
+ if (opts.localExec && !helperTeardown && leaseBefore.state !== "reaped") {
1665
+ const current = { data: leaseBefore };
1643
1666
  // BOT-1711: tear down in the directory the stack was PROVISIONED in (the lease's
1644
1667
  // recorded worktree_root/stack_path), not the invoking cwd — otherwise `supabase
1645
1668
  // stop` at the worktree root would stop the shared canonical stack.
1646
- leaseBefore = current.data;
1647
1669
  teardownDir = leaseStackDir(current.data);
1648
1670
  // BOT-1711 (Codex P1, R4): refuse local-exec teardown of ANY lease whose target is the
1649
1671
  // worktree root / default project — BEFORE branching on the persisted Docker target.
@@ -1765,7 +1787,7 @@ export async function cmdDone(leaseId, opts, {
1765
1787
  error: `the lease's established reap disposition ${JSON.stringify(established)} is unknown — NOT tearing down; the slot stays fenced` }), opts, EXIT.BACKEND);
1766
1788
  }
1767
1789
  const disposition = alreadyReaped ? established : (established ?? opts.disposition);
1768
- if (opts.localExec && state === "reaping") {
1790
+ if (opts.localExec && !helperTeardown && state === "reaping") {
1769
1791
  // Only finalize once the stack is PROVABLY down. A failed `supabase stop` (Docker
1770
1792
  // unavailable, etc.) must NOT finalize — finalize frees the slot lock and promotes
1771
1793
  // the next queued lease, which would collide with containers still running on this
@@ -1782,6 +1804,7 @@ export async function cmdDone(leaseId, opts, {
1782
1804
  else process.stderr.write(`${yellow("⚠")} stack: finalize failed (${fin.error || fin.data?.code}); the reaper will reconcile.\n`);
1783
1805
  }
1784
1806
  return emitResult(buildReceipt({ command: "done", outcome: "released", lease_id: leaseId, state, disposition,
1807
+ ...(helperTeardown ? { teardown: "helper" } : {}),
1785
1808
  ...(legacyTargetProof ? { legacy_target_proof: legacyTargetProof } : {}) }), opts, EXIT.OK);
1786
1809
  }
1787
1810