@botbuddy/cli 1.63.35 → 1.63.37

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.
@@ -1,7 +1,7 @@
1
1
  {
2
- "release_version": "1.63.35",
3
- "source_commit": "4517f4b0fe820afa3c976ac2a07bbbf02253fb86",
4
- "source_sha256": "20ba1366169f553685a44b2ffb1d08b8155f8c91e6a6c215909c5fb0825a5f81",
2
+ "release_version": "1.63.37",
3
+ "source_commit": "503ca85897b8b844ee5a841addcbb383c563de68",
4
+ "source_sha256": "16d04abbbc7838ae352f1cde6e5b7f76d47c2ae3723d47a0d79f3b1ebcc74c63",
5
5
  "target": "darwin-arm64",
6
- "sha256": "e397a1b045b4ccd213e9a7a7dbc3631b83d8af2e0dff67b90b81c365ed9b1b41"
6
+ "sha256": "dc1147cf8c4554b973d31c14fe95d2edadf7623b23e9ac9d578d4e9a6de7f975"
7
7
  }
@@ -1,7 +1,7 @@
1
1
  {
2
- "release_version": "1.63.35",
3
- "source_commit": "4517f4b0fe820afa3c976ac2a07bbbf02253fb86",
4
- "source_sha256": "20ba1366169f553685a44b2ffb1d08b8155f8c91e6a6c215909c5fb0825a5f81",
2
+ "release_version": "1.63.37",
3
+ "source_commit": "503ca85897b8b844ee5a841addcbb383c563de68",
4
+ "source_sha256": "16d04abbbc7838ae352f1cde6e5b7f76d47c2ae3723d47a0d79f3b1ebcc74c63",
5
5
  "target": "linux-x64",
6
- "sha256": "c2a2d4b815867bf1e6c4008cbb2eda12d1aecee16e3f11727e445307742a5521"
6
+ "sha256": "f8c16854c6324625c2284a3707edafa0c4f79a0560a8cc7a97a2f952014b1900"
7
7
  }
@@ -1,21 +1,21 @@
1
1
  {
2
2
  "schema_version": 1,
3
- "release_version": "1.63.35",
4
- "source_commit": "4517f4b0fe820afa3c976ac2a07bbbf02253fb86",
5
- "source_sha256": "20ba1366169f553685a44b2ffb1d08b8155f8c91e6a6c215909c5fb0825a5f81",
3
+ "release_version": "1.63.37",
4
+ "source_commit": "503ca85897b8b844ee5a841addcbb383c563de68",
5
+ "source_sha256": "16d04abbbc7838ae352f1cde6e5b7f76d47c2ae3723d47a0d79f3b1ebcc74c63",
6
6
  "targets": {
7
7
  "darwin-arm64": {
8
8
  "goos": "darwin",
9
9
  "goarch": "arm64",
10
10
  "binary": "daemon/bin/darwin-arm64/botbuddy-daemon",
11
- "sha256": "e397a1b045b4ccd213e9a7a7dbc3631b83d8af2e0dff67b90b81c365ed9b1b41",
11
+ "sha256": "dc1147cf8c4554b973d31c14fe95d2edadf7623b23e9ac9d578d4e9a6de7f975",
12
12
  "mode": "0755"
13
13
  },
14
14
  "linux-x64": {
15
15
  "goos": "linux",
16
16
  "goarch": "amd64",
17
17
  "binary": "daemon/bin/linux-x64/botbuddy-daemon",
18
- "sha256": "c2a2d4b815867bf1e6c4008cbb2eda12d1aecee16e3f11727e445307742a5521",
18
+ "sha256": "f8c16854c6324625c2284a3707edafa0c4f79a0560a8cc7a97a2f952014b1900",
19
19
  "mode": "0755"
20
20
  }
21
21
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@botbuddy/cli",
3
- "version": "1.63.35",
3
+ "version": "1.63.37",
4
4
  "description": "BotBuddy \u2014 Swarm coordination CLI for multi-agent workflows",
5
5
  "type": "module",
6
6
  "bin": {
package/src/wait.mjs CHANGED
@@ -26,6 +26,7 @@ import { SETUP_BLOCK } from "./setup-block.mjs";
26
26
  import { createWaitCheckpointStore, WaitCheckpointError } from "./wait-checkpoint.mjs";
27
27
  import { realpath } from "node:fs/promises";
28
28
  import { deepLinkNotice, safeWebUrl } from "./deep-link.mjs";
29
+ import { redactTelemetry } from "./telemetry-outbox.mjs";
29
30
  import { assertWaitObserver, readWaitResult, waitCompletionState, waitObserverId, writeWaitReceipt } from "./wait-result.mjs";
30
31
 
31
32
  // A protocol is deliberately distinct from package semver: compatible pinned
@@ -489,6 +490,25 @@ async function reserveClaimQueueBoundary(opts, conditions) {
489
490
  }
490
491
  }
491
492
 
493
+ // BOT-2855: the concrete next step for a typed registration refusal, or null when
494
+ // there is none beyond the relay's own detail. Built from the parsed conditions
495
+ // only (repo / PR number), never from a credential.
496
+ export function registerRefusalRecovery(errorCode, conditions) {
497
+ const release = Array.isArray(conditions) ? conditions.find((c) => c?.type === "staging-release") : null;
498
+ const repo = release?.params?.repo ?? "<owner/repo>";
499
+ const pr = release?.params?.pr ?? "<n>";
500
+ const arm = `bb wait 'staging-release:repo=${repo},pr=${pr}'`;
501
+ if (errorCode === "pr_not_merged") {
502
+ return "staging-release arms only after the PR merges. Before the merge, wait for it with "
503
+ + `bb wait 'pr-state:repo=${repo},pr=${pr}' (or for its CI with bb wait 'ci:repo=${repo},scope=latest,pr=${pr}'), `
504
+ + `then arm ${arm}`;
505
+ }
506
+ if (errorCode === "pr_merge_sha_unresolved") {
507
+ return `the merge commit is not visible yet; retry ${arm} in a minute`;
508
+ }
509
+ return null;
510
+ }
511
+
492
512
  async function registerWait(opts, conditions, deadlineIso) {
493
513
  // BOT-1649: a relay wait authenticates from the per-session `bb_sess_` token
494
514
  // ($BOTBUDDY_AGENT_SESSION_TOKEN) — the relay derives agent, tenant, and session from it.
@@ -524,15 +544,34 @@ async function registerWait(opts, conditions, deadlineIso) {
524
544
  },
525
545
  body: JSON.stringify(registerBody),
526
546
  });
547
+ // BOT-2855: read an error body ONCE. Each status branch below inspects it and may
548
+ // fall through to the generic `!res.ok` error; a second `res.json()` on the consumed
549
+ // body threw, the catch turned it into `{}`, and the relay's typed reason (e.g.
550
+ // `pr_not_merged`) collapsed to `register responded 400`. Redact any credential a
551
+ // relay detail might echo before it can reach stderr or the receipt.
552
+ let errorBody = null;
553
+ let errorBodyIsJson = false;
554
+ const readErrorBody = async () => {
555
+ if (errorBody) return errorBody;
556
+ const text = await res.text().catch(() => "");
557
+ try {
558
+ const parsed = JSON.parse(text);
559
+ errorBodyIsJson = Boolean(parsed) && typeof parsed === "object" && !Array.isArray(parsed);
560
+ errorBody = errorBodyIsJson ? redactTelemetry(parsed) : {};
561
+ } catch {
562
+ errorBody = {};
563
+ }
564
+ return errorBody;
565
+ };
527
566
  if (res.status === 429) {
528
- const body = await res.json().catch(() => ({}));
567
+ const body = await readErrorBody();
529
568
  const err = new Error(body.detail || "wait_session_cap_exceeded");
530
569
  err.cap = true;
531
570
  err.errorCode = body.error || "wait_session_cap_exceeded";
532
571
  throw err;
533
572
  }
534
573
  if (res.status === 401) {
535
- const body = await res.json().catch(() => ({}));
574
+ const body = await readErrorBody();
536
575
  const err = new Error(body.detail || body.message || body.error || "unauthorized agent key");
537
576
  err.auth = true;
538
577
  err.errorCode = body.error || "unauthorized";
@@ -548,7 +587,7 @@ async function registerWait(opts, conditions, deadlineIso) {
548
587
  // A TRANSIENT backend 400 (e.g. `register_failed` from the RPC) is NOT
549
588
  // caller-actionable — fall through to the existing live-only fallback so a valid
550
589
  // wait still arms (Codex round-7 P2).
551
- const body = await res.json().catch(() => ({}));
590
+ const body = await readErrorBody();
552
591
  const INVALID_CONDITION_CODES = new Set([
553
592
  "no_signal_source",
554
593
  "review_guard_registration_failed",
@@ -593,6 +632,9 @@ async function registerWait(opts, conditions, deadlineIso) {
593
632
  // caller-actionable hard stops, not the untracked live-only fallback.
594
633
  "chat_not_participant",
595
634
  "chat_participant_ambiguous",
635
+ // BOT-2855: BOT-1458 AC-3 — staging-release arms only for a merged PR. Before the
636
+ // merge the relay refuses it by design; the caller must wait for the merge first.
637
+ "pr_not_merged",
596
638
  // BOT-2318: command-run targets are authorization-bound to the registering
597
639
  // tenant, agent and work session, and the condition cannot be composed.
598
640
  // Both refusals require the caller to change its target/conditions; falling
@@ -609,7 +651,7 @@ async function registerWait(opts, conditions, deadlineIso) {
609
651
  }
610
652
  }
611
653
  if (res.status === 404) {
612
- const body = await res.json().catch(() => ({}));
654
+ const body = await readErrorBody();
613
655
  if (body.error === "linear_issue_not_found") {
614
656
  const err = new Error(body.detail || body.error);
615
657
  err.invalidCondition = true;
@@ -632,7 +674,7 @@ async function registerWait(opts, conditions, deadlineIso) {
632
674
  }
633
675
  }
634
676
  if (res.status === 403) {
635
- const body = await res.json().catch(() => ({}));
677
+ const body = await readErrorBody();
636
678
  if (new Set(["wait_tenant_unresolved", "wait_cross_tenant", "wait_tenant_mismatch", "wait_resume_identity_mismatch"]).has(body.error)) {
637
679
  const err = new Error(body.detail || body.error);
638
680
  err.invalidCondition = true;
@@ -655,7 +697,7 @@ async function registerWait(opts, conditions, deadlineIso) {
655
697
  throw err;
656
698
  }
657
699
  if (res.status === 426) {
658
- const body = await res.json().catch(() => ({}));
700
+ const body = await readErrorBody();
659
701
  const err = new Error(body.detail || "this wait client is no longer supported");
660
702
  err.clientOutdated = true;
661
703
  err.errorCode = body.error || "client_outdated";
@@ -669,7 +711,7 @@ async function registerWait(opts, conditions, deadlineIso) {
669
711
  // unblocked wait would skip the server's initial evaluation and could resume a
670
712
  // coding session on a false already_unblocked. Surface the typed receipt and a
671
713
  // concrete exit code (5=backend/retryable, 4=invalid/not-found).
672
- const body = await res.json().catch(() => ({}));
714
+ const body = await readErrorBody();
673
715
  if (body.error === "unblocked_projection_unresolved" || body.error === "unblocked_ticket_not_found") {
674
716
  const err = new Error(body.detail || body.error);
675
717
  err.hydrationRegister = true;
@@ -682,12 +724,20 @@ async function registerWait(opts, conditions, deadlineIso) {
682
724
  }
683
725
  }
684
726
  if (!res.ok) {
685
- const body = await res.json().catch(() => ({}));
686
- const err = new Error(body.detail || body.message || body.error || `register responded ${res.status}`);
727
+ const body = await readErrorBody();
728
+ const typedCode = typeof body.error === "string" && body.error ? body.error : null;
729
+ const detail = [body.detail, body.message].find((v) => typeof v === "string" && v) ?? null;
730
+ const err = new Error(detail || typedCode ||
731
+ `register responded ${res.status}${errorBodyIsJson ? "" : " with a non-JSON body"}`);
732
+ // BOT-2855: keep the relay's typed reason for the caller and the receipt.
733
+ err.errorCode = typedCode || "register_failed";
734
+ err.httpStatus = res.status;
687
735
  if (body.error === "initial_eval_failed") {
688
736
  err.initialEval = true;
689
- err.errorCode = body.error;
690
737
  err.detail = body.detail || "initial evaluation failed";
738
+ } else if (body.error === "pr_merge_sha_unresolved") {
739
+ // Merged, but neither the projection nor GitHub returned the merge commit yet.
740
+ err.retryableRegister = true;
691
741
  }
692
742
  throw err;
693
743
  }
@@ -2352,15 +2402,35 @@ export async function runWait(argv, { recoveryLocalWaitId = null } = {}) {
2352
2402
  }
2353
2403
  }
2354
2404
  process.stderr.write(`bb-wait: invalid condition (${err.errorCode}) — ${err.message}\n`);
2405
+ const recovery = registerRefusalRecovery(err.errorCode, conditions);
2406
+ if (recovery) process.stderr.write(`bb-wait: next step: ${recovery}\n`);
2355
2407
  await emitReceipt({
2356
2408
  schema_version: 1,
2357
2409
  outcome: "error",
2358
2410
  error: err.errorCode,
2411
+ error_code: err.errorCode,
2359
2412
  detail: String(err.message),
2360
2413
  ...(Array.isArray(err.candidates) && err.candidates.length > 0 ? { candidates: err.candidates } : {}),
2414
+ ...(recovery ? { recovery } : {}),
2361
2415
  });
2362
2416
  process.exit(EXIT.INVALID);
2363
2417
  }
2418
+ if (err && err.retryableRegister) {
2419
+ // BOT-2855: a merged PR whose merge commit is not visible yet — retryable.
2420
+ const recovery = registerRefusalRecovery(err.errorCode, conditions);
2421
+ process.stderr.write(`bb-wait: wait-session registration failed (${err.errorCode}) — ${err.message}\n`);
2422
+ if (recovery) process.stderr.write(`bb-wait: next step: ${recovery}\n`);
2423
+ await emitReceipt({
2424
+ schema_version: 1,
2425
+ outcome: "error",
2426
+ error: err.errorCode,
2427
+ error_code: err.errorCode,
2428
+ detail: String(err.message),
2429
+ http_status: err.httpStatus ?? null,
2430
+ ...(recovery ? { recovery } : {}),
2431
+ });
2432
+ process.exit(EXIT.BACKEND);
2433
+ }
2364
2434
  if (err && err.initialEval) {
2365
2435
  process.stderr.write(`bb-wait: initial wait evaluation failed (${err.errorCode}) — ${err.detail}\n`);
2366
2436
  await emitReceipt({ schema_version: 1, outcome: "error", error: err.errorCode, detail: String(err.detail) });
@@ -2375,9 +2445,19 @@ export async function runWait(argv, { recoveryLocalWaitId = null } = {}) {
2375
2445
  // BOT-1338/1608: every non-timer wait is a relay-tracked, session-token wait.
2376
2446
  // If registration did not succeed, neither the server nor this client has
2377
2447
  // verified the session's tenant, so there is no safe untracked fallback.
2378
- process.stderr.write(`bb-wait: wait-session registration failed (failing closed, not arming untracked): ${err && err.message || err}\n`);
2448
+ // BOT-2855: name the relay's typed code (or register_failed when it sent none)
2449
+ // and its status, so the caller can act on it instead of a bare status line.
2450
+ const errorCode = (err && err.errorCode) || "register_failed";
2451
+ process.stderr.write(`bb-wait: wait-session registration failed (${errorCode}) — failing closed, not arming untracked: ${err && err.message || err}\n`);
2379
2452
  await emitReceipt(withPrincipalReceipt(
2380
- { schema_version: 1, outcome: "error", error: "register_failed", detail: String(err && err.message || err) },
2453
+ {
2454
+ schema_version: 1,
2455
+ outcome: "error",
2456
+ error: "register_failed",
2457
+ error_code: errorCode,
2458
+ detail: String(err && err.message || err),
2459
+ ...(err && Number.isInteger(err.httpStatus) ? { http_status: err.httpStatus } : {}),
2460
+ },
2381
2461
  null,
2382
2462
  { sessionTenant: sessionTenant ?? null, agentId: null },
2383
2463
  ));