@botbuddy/cli 1.45.2 → 1.45.4

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.45.2",
3
+ "version": "1.45.4",
4
4
  "description": "BotBuddy — Swarm coordination CLI for multi-agent workflows",
5
5
  "type": "module",
6
6
  "bin": {
@@ -77,6 +77,12 @@ export function repairFailureRecovery(code, error = null) {
77
77
  if (code === "client_key_unavailable") {
78
78
  return "the client key could not be read (locked or failing credential store); unlock it, then run `bb doctor --fix`";
79
79
  }
80
+ // BOT-1787: this is a server-side identity conflict, not an authentication
81
+ // failure. Preserve the real remedy so a healthy credential is never needlessly
82
+ // rotated; `bb setup` clears this worktree's cache and re-registers it.
83
+ if (code === "agent_tenant_immutable") {
84
+ return "this worktree's agent identity belongs to another tenant; run `bb setup` to register this tenant, then `bb doctor --fix`";
85
+ }
80
86
  // BOT-1750 (Codex R9 P2): `session_mint_unauthorized` means a locally-unexpired
81
87
  // client key was revoked/rejected server-side — only a fresh login replaces it, so
82
88
  // point at `bb login`, not a binding repair (the binding is valid).
Binary file
package/src/api.mjs CHANGED
@@ -445,7 +445,13 @@ export async function callToolJson(toolName, args = {}, { fetchImpl = fetch, sig
445
445
  }
446
446
  let payload;
447
447
  try { payload = await res.json(); } catch { return { ok: false, status: res.status, error: "invalid_json" }; }
448
- if (payload.error?.message) return { ok: false, status: res.status, error: payload.error.message };
448
+ if (payload.error?.message) {
449
+ // Typed domain errors retain their verdict inside the HTTP-200 JSON-RPC
450
+ // envelope. Do not infer credential failure from an identity conflict.
451
+ const code = payload.error.data?.code;
452
+ return { ok: false, status: res.status, error: payload.error.message,
453
+ ...(typeof code === "string" && code ? { code } : {}) };
454
+ }
449
455
  const text = payload.result?.content?.map((c) => c.text).filter(Boolean).join("\n") ?? "";
450
456
  let data;
451
457
  try { data = text ? JSON.parse(text) : {}; } catch { data = { raw: text }; }
package/src/wait-core.mjs CHANGED
@@ -234,8 +234,9 @@ const VALIDATORS = {
234
234
  // BOT-1507 — scope=next adds a fourth, LIVE-ONLY selector: it names no run or PR
235
235
  // (they must already exist at arm time) and wakes on the first terminal CI run for
236
236
  // the repo delivered AFTER the wait was armed — the post-merge / cron train case.
237
- // Optional branch=/workflow= filter that first run; both are valid ONLY with
238
- // scope=next (a run_id/sha/latest wait already names its run).
237
+ // Optional branch=/workflow= filter that first run; branch is valid only with
238
+ // scope=next. A workflow also pins a level-triggered sha= recovery to one
239
+ // canonical workflow identity.
239
240
  // BOT-1685 — scope=next also accepts an optional sha= FILTER (distinct from the
240
241
  // level-triggered sha= selector): a wait armed BEFORE re-dispatching a run for a
241
242
  // known commit then wakes on the NEXT terminal run for THAT commit, instead of
@@ -253,10 +254,11 @@ const VALIDATORS = {
253
254
  if ((shaIsNextFilter ? selectors.length - 1 : selectors.length) !== 1) {
254
255
  throw new Error("ci needs exactly one of scope=latest|next | run_id=<id> | sha=<sha>");
255
256
  }
256
- const hasFilter = p.branch !== undefined || p.workflow !== undefined;
257
+ const hasBranchFilter = p.branch !== undefined;
258
+ const hasWorkflowFilter = p.workflow !== undefined;
257
259
  if (p.scope !== undefined) {
258
260
  if (p.scope === "latest") {
259
- if (hasFilter) throw new Error("ci branch=/workflow= are only valid with scope=next");
261
+ if (hasBranchFilter || hasWorkflowFilter) throw new Error("ci branch=/workflow= are only valid with scope=next");
260
262
  if (p.pr === undefined || !/^\d+$/.test(String(p.pr).trim())) {
261
263
  throw new Error("ci scope=latest needs pr=<number>");
262
264
  }
@@ -278,13 +280,17 @@ const VALIDATORS = {
278
280
  }
279
281
  throw new Error("ci scope must be 'latest' or 'next' (or use run_id=/sha=)");
280
282
  }
281
- if (hasFilter) throw new Error("ci branch=/workflow= are only valid with scope=next");
283
+ if (hasBranchFilter || (hasWorkflowFilter && p.sha === undefined)) {
284
+ throw new Error("ci branch= is only valid with scope=next; workflow= also accepts an exact sha selector");
285
+ }
282
286
  if (p.run_id !== undefined) {
283
287
  if (!String(p.run_id).trim()) throw new Error("ci run_id must be non-empty");
284
288
  return { repo, scope: "run_id", runId: String(p.run_id).trim(), pr: p.pr != null ? String(p.pr).trim() : null };
285
289
  }
286
290
  if (!String(p.sha).trim()) throw new Error("ci sha must be non-empty");
287
- return { repo, scope: "sha", sha: String(p.sha).trim(), pr: p.pr != null ? String(p.pr).trim() : null };
291
+ const workflow = p.workflow === undefined ? null : String(p.workflow).trim();
292
+ if (p.workflow !== undefined && workflow === "") throw new Error("ci workflow= must be non-empty");
293
+ return { repo, scope: "sha", sha: String(p.sha).trim(), workflow, pr: p.pr != null ? String(p.pr).trim() : null };
288
294
  },
289
295
  "staging-green"(p) {
290
296
  const repo = p.repo != null ? String(p.repo).trim() : null;
@@ -986,7 +992,12 @@ function conditionMatchesSignal(condition, signal, waitSessionId) {
986
992
  // Repo is pinned via the payload (subject may be run:<id> for uncorrelated runs).
987
993
  if (String(signal.payload?.repo ?? "").toLowerCase() !== params.repo.toLowerCase()) return false;
988
994
  if (params.scope === "run_id") return String(signal.payload?.run_id) === params.runId;
989
- if (params.scope === "sha") return signal.payload?.head_sha === params.sha;
995
+ if (params.scope === "sha") {
996
+ if (signal.payload?.head_sha !== params.sha) return false;
997
+ if (params.workflow == null) return true;
998
+ const workflow = signal.payload?.workflow_name;
999
+ return typeof workflow === "string" && workflow.trim().toLowerCase() === params.workflow.trim().toLowerCase();
1000
+ }
990
1001
  if (params.scope === "next") {
991
1002
  // BOT-1507 — live-only: the first TERMINAL run for the repo. Never keys on
992
1003
  // subject_key (it is run:<id> for post-merge runs and owner/repo#n for PR
@@ -1 +0,0 @@
1
- {"schema_version":1,"source_version":"1.45.1","source_identity":"2fa051c4d0ee5cb09fda8d9e15c4952af94d701a6a43e98c8831912040cb41f2"}