@botbuddy/cli 1.40.6 → 1.40.9

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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@botbuddy/cli",
3
- "version": "1.40.6",
3
+ "version": "1.40.9",
4
4
  "description": "BotBuddy — Swarm coordination CLI for multi-agent workflows",
5
5
  "type": "module",
6
6
  "bin": {
package/src/commands.mjs CHANGED
@@ -57,14 +57,24 @@ export async function run(argv, {
57
57
  loadConfig: load = loadConfig,
58
58
  warnStale = maybeWarnStale,
59
59
  errorLog = (line) => console.error(line),
60
+ bootstrap = cmdBootstrap,
60
61
  } = {}) {
61
- await load();
62
62
  const [command, ...args] = argv;
63
+ // BOT-2069: `bootstrap close` can retire a live server session. Parse its
64
+ // own arguments before loading config, checking npm, reading the runtime
65
+ // cache, or resolving any credential. Help and malformed invocations return
66
+ // immediately; a valid plain close retains its established config/update
67
+ // setup before the generation-confirmed cleanup transaction.
68
+ if (command === "bootstrap" && args[0] === "close" && parseBootstrapCloseArgs(args).action !== "close") {
69
+ return bootstrap(args);
70
+ }
71
+
72
+ await load();
63
73
  if (shouldCheckForUpdates(argv)) await warnStale({ version: VERSION });
64
74
 
65
75
  switch (command) {
66
76
  case "start": return cmdStart(args);
67
- case "bootstrap": return cmdBootstrap(args);
77
+ case "bootstrap": return bootstrap(args);
68
78
  case "login": return cmdLogin(args, { errorLog });
69
79
  case "setup": return cmdSetup(args, { errorLog });
70
80
  case "logout": return cmdLogout();
@@ -812,28 +822,55 @@ export async function closeStoredHarnessSession({
812
822
  return result;
813
823
  }
814
824
 
815
- async function cmdBootstrap(args) {
825
+ const BOOTSTRAP_CLOSE_USAGE = "Usage: bb bootstrap close [--help]";
826
+
827
+ function parseBootstrapCloseArgs(args) {
828
+ const rest = args.slice(1);
829
+ if (rest.length === 0) return { action: "close" };
830
+ if (rest.length === 1 && (rest[0] === "--help" || rest[0] === "-h")) return { action: "help" };
831
+ return { action: "invalid" };
832
+ }
833
+
834
+ export async function cmdBootstrap(args, {
835
+ withHarnessBootstrapLock: withLock = withHarnessBootstrapLock,
836
+ loadHarnessSession: loadSession = loadHarnessSession,
837
+ resolveOwnerToken: resolveOwner = resolveOwnerToken,
838
+ resolveAgentKey: resolveAgent = resolveAgentKey,
839
+ closeStoredHarnessSession: closeSession = closeStoredHarnessSession,
840
+ clearHarnessSession: clearSession = clearHarnessSession,
841
+ bootstrapHarnessSession: bootstrapSession = bootstrapHarnessSession,
842
+ getConfig: getCfg = getConfig,
843
+ log = (line) => console.log(line),
844
+ die: fail = die,
845
+ } = {}) {
816
846
  if (args[0] === "close") {
817
- return withHarnessBootstrapLock({}, async () => {
818
- const session = await loadHarnessSession();
847
+ const closeArguments = parseBootstrapCloseArgs(args);
848
+ if (closeArguments.action === "help") {
849
+ log(`${BOOTSTRAP_CLOSE_USAGE}\n\nClose this worktree's cached harness session after generation-confirmed server cleanup.`);
850
+ return;
851
+ }
852
+ if (closeArguments.action === "invalid") return fail(BOOTSTRAP_CLOSE_USAGE);
853
+
854
+ return withLock({}, async () => {
855
+ const session = await loadSession();
819
856
  if (!session) {
820
- console.log(JSON.stringify({ outcome: "no_session" }));
857
+ log(JSON.stringify({ outcome: "no_session" }));
821
858
  return;
822
859
  }
823
- const owner = await resolveOwnerToken({ getConfig });
860
+ const owner = await resolveOwner({ getConfig: getCfg });
824
861
  // Match resolveCallAuth: a stored but expired owner token must never mask
825
862
  // a usable MCP key or the exact session credential that needs cleanup.
826
863
  const activeOwner = owner && !(owner.expiresAt && Date.now() >= owner.expiresAt) ? owner : null;
827
- const agentKey = activeOwner ? null : await resolveAgentKey();
864
+ const agentKey = activeOwner ? null : await resolveAgent();
828
865
  const durableAuth = activeOwner
829
866
  ? { Authorization: `Bearer ${activeOwner.token}` }
830
867
  : agentKey
831
868
  ? { "x-agent-api-key": agentKey }
832
869
  : { "x-agent-api-key": session.session_token };
833
- const result = await closeStoredHarnessSession({ session, durableAuth });
834
- if (!result?.ok || result.isError) die(result?.error || "bb bootstrap close failed");
835
- await clearHarnessSession(session.session_id, { sessionToken: session.session_token, lock: false }); // already inside withHarnessBootstrapLock (non-reentrant)
836
- console.log(JSON.stringify({ outcome: "closed", session_id: session.session_id }));
870
+ const result = await closeSession({ session, durableAuth });
871
+ if (!result?.ok || result.isError) fail(result?.error || "bb bootstrap close failed");
872
+ await clearSession(session.session_id, { sessionToken: session.session_token, lock: false }); // already inside withHarnessBootstrapLock (non-reentrant)
873
+ log(JSON.stringify({ outcome: "closed", session_id: session.session_id }));
837
874
  });
838
875
  }
839
876
  let harnessSessionId = null;
@@ -848,7 +885,7 @@ async function cmdBootstrap(args) {
848
885
  let prUrl = null;
849
886
  let model = null;
850
887
  let effort = null;
851
- let name = getConfig()?.agent_name || `botbuddy-${os.hostname()}`;
888
+ let name = getCfg()?.agent_name || `botbuddy-${os.hostname()}`;
852
889
  let nameExplicit = false;
853
890
  for (let i = 0; i < args.length; i += 1) {
854
891
  if (args[i] === "--session-id") harnessSessionId = args[++i] || null;
@@ -864,15 +901,15 @@ async function cmdBootstrap(args) {
864
901
  else if (args[i] === "--pr-url") prUrl = args[++i] || null;
865
902
  else if (args[i] === "--model") model = args[++i] || null;
866
903
  else if (args[i] === "--effort") effort = args[++i] || null;
867
- else die("Usage: bb bootstrap --session-id <harness UUID> (--ticket-id <ticket> | --no-ticket-reason <reason>) [--type codex|claude|gpt|custom] [--codex-thread-id <UUID>] [--name <technical-name>] [--branch <branch>] [--ticket-url <url>] [--model <model>] [--effort <effort>]");
904
+ else fail("Usage: bb bootstrap --session-id <harness UUID> (--ticket-id <ticket> | --no-ticket-reason <reason>) [--type codex|claude|gpt|custom] [--codex-thread-id <UUID>] [--name <technical-name>] [--branch <branch>] [--ticket-url <url>] [--model <model>] [--effort <effort>]");
868
905
  }
869
- if (!harnessSessionId) die("bb bootstrap requires --session-id <harness UUID>");
906
+ if (!harnessSessionId) fail("bb bootstrap requires --session-id <harness UUID>");
870
907
  const linkageError = validateBootstrapLinkage(ticketId, noTicketReason);
871
- if (linkageError) die(linkageError);
908
+ if (linkageError) fail(linkageError);
872
909
  ticketId = ticketId?.trim() || null;
873
910
  noTicketReason = noTicketReason?.trim() || null;
874
911
  if (!nameExplicit) name = `${name}-${harnessSessionId.slice(0, 8)}`;
875
- const state = await bootstrapHarnessSession({
912
+ const state = await bootstrapSession({
876
913
  name,
877
914
  type,
878
915
  harnessSessionId,
@@ -888,13 +925,13 @@ async function cmdBootstrap(args) {
888
925
  effort,
889
926
  harness: `botbuddy-${type}-launcher`,
890
927
  });
891
- if (state.linkage?.status === "gap") die(state.linkage.warning || "bb bootstrap requires ticket attribution or --no-ticket-reason.");
928
+ if (state.linkage?.status === "gap") fail(state.linkage.warning || "bb bootstrap requires ticket attribution or --no-ticket-reason.");
892
929
  // BOT-1945: name (never the bearer of) an unowned cached session this bootstrap
893
930
  // replaced, so a leftover live holder is visible instead of silent.
894
931
  // BOT-2058: the launcher must see a known-holder warning before it starts a
895
932
  // new harness. Keep it out of the credential cache, but preserve the
896
933
  // non-secret concurrency payload in this command receipt.
897
- console.log(JSON.stringify({ agent_id: state.agent_id, display_name: state.display_name, session_id: state.session_id, expires_at: state.expires_at, ...(state.ticket_signals?.concurrency ? { ticket_session_warning: state.ticket_signals.concurrency } : {}), ...(state.replaced_cached_session ? { replaced_cached_session: state.replaced_cached_session } : {}) }));
934
+ log(JSON.stringify({ agent_id: state.agent_id, display_name: state.display_name, session_id: state.session_id, expires_at: state.expires_at, ...(state.ticket_signals?.concurrency ? { ticket_session_warning: state.ticket_signals.concurrency } : {}), ...(state.replaced_cached_session ? { replaced_cached_session: state.replaced_cached_session } : {}) }));
898
935
  }
899
936
 
900
937
  // BOT-1566 A4: after the local-metadata lines, ask the SERVER whether the stored
package/src/stack.mjs CHANGED
@@ -2186,7 +2186,8 @@ function waitForChild(child) {
2186
2186
  function isDenoIntegrationLane(argv) {
2187
2187
  // `test:all` is the supported package wrapper that transitively invokes
2188
2188
  // `test:integration`; both must receive the leased runner configuration.
2189
- return argv.includes("test:integration") || argv.includes("test:integration:coverage") || argv.includes("test:all") || argv.includes("scripts/run-deno-integration.sh");
2189
+ return argv.includes("test:integration") || argv.includes("test:integration:coverage") || argv.includes("test:all") || argv.includes("scripts/run-deno-integration.sh") ||
2190
+ (argv.includes("scripts/run-deno-hermetic.mjs") && argv.includes("database-checks"));
2190
2191
  }
2191
2192
 
2192
2193
  function requireConnectionText(connection, key) {