@botbuddy/cli 1.63.5 → 1.63.7

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.5",
3
- "source_commit": "fc9bbe22c3fe8840724c30eae63515cfd70cce9d",
4
- "source_sha256": "bed3488d03f054f8c30c49d28bb3d5783d081363f2d9c69063164c79cc70feee",
2
+ "release_version": "1.63.7",
3
+ "source_commit": "ad1b7a0a46c938f8dfb3f03f80e97849b879e4a2",
4
+ "source_sha256": "44fb5d94a9794df429bdd315b45b75306e4e25f17e382fdb1f09b40791af2411",
5
5
  "target": "darwin-arm64",
6
- "sha256": "e6e438fc3e98628a4e5f04a0100c96ba694f75d78210fbebe3623af7b0f085d7"
6
+ "sha256": "13b8ac4129fbea660ed56afad579f6634f93e5425caf8038f46c28c586c1f858"
7
7
  }
@@ -1,7 +1,7 @@
1
1
  {
2
- "release_version": "1.63.5",
3
- "source_commit": "fc9bbe22c3fe8840724c30eae63515cfd70cce9d",
4
- "source_sha256": "bed3488d03f054f8c30c49d28bb3d5783d081363f2d9c69063164c79cc70feee",
2
+ "release_version": "1.63.7",
3
+ "source_commit": "ad1b7a0a46c938f8dfb3f03f80e97849b879e4a2",
4
+ "source_sha256": "44fb5d94a9794df429bdd315b45b75306e4e25f17e382fdb1f09b40791af2411",
5
5
  "target": "linux-x64",
6
- "sha256": "b224066b8aa6ae1831282fbfd7e60135864dea69847a7187df81bd171674456a"
6
+ "sha256": "71de432f506d59123e5d46961b6e76c550236767786e1d5b8747ab2f11010d5d"
7
7
  }
@@ -1,21 +1,21 @@
1
1
  {
2
2
  "schema_version": 1,
3
- "release_version": "1.63.5",
4
- "source_commit": "fc9bbe22c3fe8840724c30eae63515cfd70cce9d",
5
- "source_sha256": "bed3488d03f054f8c30c49d28bb3d5783d081363f2d9c69063164c79cc70feee",
3
+ "release_version": "1.63.7",
4
+ "source_commit": "ad1b7a0a46c938f8dfb3f03f80e97849b879e4a2",
5
+ "source_sha256": "44fb5d94a9794df429bdd315b45b75306e4e25f17e382fdb1f09b40791af2411",
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": "e6e438fc3e98628a4e5f04a0100c96ba694f75d78210fbebe3623af7b0f085d7",
11
+ "sha256": "13b8ac4129fbea660ed56afad579f6634f93e5425caf8038f46c28c586c1f858",
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": "b224066b8aa6ae1831282fbfd7e60135864dea69847a7187df81bd171674456a",
18
+ "sha256": "71de432f506d59123e5d46961b6e76c550236767786e1d5b8747ab2f11010d5d",
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.5",
3
+ "version": "1.63.7",
4
4
  "description": "BotBuddy \u2014 Swarm coordination CLI for multi-agent workflows",
5
5
  "type": "module",
6
6
  "bin": {
@@ -467,7 +467,7 @@ export async function runDelivery({
467
467
  * small reads. Never throws into the caller's command.
468
468
  */
469
469
  export async function ensureRunDelivery({
470
- home = homedir(), env = process.env, spawnImpl = spawn, isAlive = pidAlive, force = false, cooldownMs = DELIVERY_RESCAN_COOLDOWN_MS,
470
+ home = homedir(), env = process.env, spawnImpl = spawn, isAlive = pidAlive, force = false, cooldownMs = DELIVERY_RESCAN_COOLDOWN_MS, now = Date.now, stateStat = stat,
471
471
  } = {}) {
472
472
  try {
473
473
  if (env.BOTBUDDY_RUN_DELIVERY === "off") return { started: false, reason: "disabled" };
@@ -480,7 +480,8 @@ export async function ensureRunDelivery({
480
480
  // starts us itself (force) when it parks, so otherwise scan at most once
481
481
  // per cooldown. Both times are this machine's filesystem clock.
482
482
  if (!force && state) {
483
- const stateAge = Date.now() - (await stat(deliveryStatePath(home))).mtimeMs;
483
+ // Date.now() has millisecond precision; APFS mtime may carry fractions.
484
+ const stateAge = now() - Math.floor((await stateStat(deliveryStatePath(home))).mtimeMs);
484
485
  if (stateAge >= 0 && stateAge < cooldownMs) return { started: false, reason: "cooldown" };
485
486
  }
486
487
  const holder = await readJson(deliveryLockPath(home));
package/src/wait-core.mjs CHANGED
@@ -501,7 +501,7 @@ function parsePrTarget(p, label) {
501
501
  // refuses at arm (invalid_conditions, exit 4). `reviewer=` is pr-review only.
502
502
  const PR_CONDITION_KEYS = Object.freeze({
503
503
  "pr-state": Object.freeze(["repo", "pr", "number", "sha"]),
504
- "pr-review": Object.freeze(["repo", "pr", "number", "sha", "reviewer"]),
504
+ "pr-review": Object.freeze(["repo", "pr", "number", "sha", "reviewer", "stall_minutes"]),
505
505
  });
506
506
 
507
507
  // A GitHub login compared case-insensitively, with the App suffix `[bot]` stripped
@@ -531,14 +531,24 @@ function parsePrCondition(p, label) {
531
531
  if (reviewer === "") throw new Error(`${label} reviewer= must be a GitHub login`);
532
532
  out.reviewer = reviewer;
533
533
  }
534
+ if (p.stall_minutes !== undefined) {
535
+ const minutes = Number(p.stall_minutes);
536
+ if (!/^\d+$/.test(String(p.stall_minutes)) || !Number.isInteger(minutes) || minutes < 10 || minutes > 1440) {
537
+ throw new Error("pr-review stall_minutes= must be an integer from 10 to 1440");
538
+ }
539
+ if (out.number == null || !out.sha || (out.reviewer && out.reviewer !== "chatgpt-codex-connector")) {
540
+ throw new Error("pr-review stall_minutes= requires pr= and sha= for the Codex reviewer");
541
+ }
542
+ out.stall_minutes = minutes;
543
+ }
534
544
  return out;
535
545
  }
536
546
 
537
547
  // Does a spine subject_key (`owner/repo#number`) satisfy a pr target?
538
548
  function prSubjectMatches(subjectKey, params) {
539
549
  if (typeof subjectKey !== "string") return false;
540
- if (params.number != null) return subjectKey === `${params.repo}#${params.number}`;
541
- return subjectKey.startsWith(`${params.repo}#`);
550
+ if (params.number != null) return subjectKey.toLowerCase() === `${params.repo}#${params.number}`.toLowerCase();
551
+ return subjectKey.toLowerCase().startsWith(`${params.repo}#`.toLowerCase());
542
552
  }
543
553
 
544
554
  // Does a commit field on a signal payload identify the same commit as the
@@ -560,8 +570,13 @@ function prStateFiltersMatch(signal, params) {
560
570
  // BOT-1975 / BOT-1972: optional commit + reviewer scoping for pr-review. A
561
571
  // `review_unavailable` signal (the Codex usage-limit decline) names no commit, so
562
572
  // it always passes the sha filter — the waiter must learn no review is coming.
563
- function prReviewFiltersMatch(signal, params) {
573
+ function prReviewFiltersMatch(signal, params, guard) {
564
574
  const payload = signal.payload ?? {};
575
+ if (payload.guard_id && guard && (payload.guard_id !== guard.id || !(Date.parse(payload.started_at) >= Date.parse(guard.started_at)))) return false;
576
+ // Scoped Codex waits use the durable diagnostic. Legacy cron stall notices
577
+ // are informational and cannot pre-empt a call-specific intervention policy.
578
+ if (payload.transition === "review_stalled" && params.sha && params.number &&
579
+ (!params.reviewer || params.reviewer === "chatgpt-codex-connector") && !payload.guard_id) return false;
565
580
  if (params.sha != null && payload.transition !== "review_unavailable" &&
566
581
  !shaPrefixMatches(payload.commit_sha, params.sha)) {
567
582
  return false;
@@ -1123,7 +1138,7 @@ function conditionMatchesSignal(condition, signal, waitSessionId) {
1123
1138
  prStateFiltersMatch(signal, params);
1124
1139
  case "pr-review":
1125
1140
  return signal.signal_type === "pr_review" && prSubjectMatches(signal.subject_key, params) &&
1126
- prReviewFiltersMatch(signal, params);
1141
+ prReviewFiltersMatch(signal, params, condition.review_guard);
1127
1142
  case "test-run":
1128
1143
  return signal.signal_type === "test_run" && signal.subject_key === `run:${params.runId}`;
1129
1144
  case "stack-run":
package/src/wait.mjs CHANGED
@@ -105,12 +105,15 @@ CONDITIONS (TYPE:key=val,key=val — repeat for several; --any wakes on the fir
105
105
  that head, so a previous head's check result can't wake you.
106
106
  A push alone emits nothing — the head rides on the next
107
107
  transition.
108
- pr-review:repo=<owner/repo>[,pr=<n>][,reviewer=<login>][,sha=<commit>]
108
+ pr-review:repo=<owner/repo>[,pr=<n>][,reviewer=<login>][,sha=<commit>][,stall_minutes=<10-1440>]
109
109
  a submitted review, a new review comment, a thread
110
110
  resolved/re-opened (payload carries the unresolved-thread
111
- count), a Codex clean pass (payload.transition=clean_pass)
112
- or the Codex usage-limit decline (transition=
113
- review_unavailable — no review is coming).
111
+ count), a Codex clean pass (payload.transition=clean_pass),
112
+ the Codex usage-limit decline (transition=
113
+ review_unavailable — no review is coming) or a Codex
114
+ review needing intervention after one backend diagnostic
115
+ (transition=review_stalled, review_available=false,
116
+ reason + next_action). Initial default: 10 min.
114
117
  Edge-triggered: it wakes only on review activity AFTER
115
118
  arm, so arm BEFORE requesting the review. If the PR ALREADY
116
119
  has an outstanding review at arm, bb-wait prints a one-line
@@ -218,6 +221,12 @@ CONDITIONS (TYPE:key=val,key=val — repeat for several; --any wakes on the fir
218
221
  tenant after current server-side approval revalidation.
219
222
 
220
223
  OPTIONS
224
+ Review intervention: exact-head Codex waits have a durable one-shot diagnostic.
225
+ stall_minutes= overrides repository → tenant settings (initial default 10 min).
226
+ A missing/unbound backend guard is refused as review_guard_unsupported.
227
+ It needs pr= + sha=. A result wakes immediately; a deadline never means approval.
228
+ Reconnects/re-arms retain the deadline. This is separate from --timeout below.
229
+
221
230
  --any wake on the first matching condition (default; only mode in v1)
222
231
  --timeout <seconds> safety cap (default 86400 = 24 h; hard cap 259200 = 72 h). Timeout is exit 2,
223
232
  not an error. The timeout is a SAFETY CAP measured in hours or days, never a
@@ -528,6 +537,7 @@ async function registerWait(opts, conditions, deadlineIso) {
528
537
  const body = await res.json().catch(() => ({}));
529
538
  const INVALID_CONDITION_CODES = new Set([
530
539
  "no_signal_source",
540
+ "review_guard_registration_failed",
531
541
  "unblocked_cross_tenant",
532
542
  "unblocked_requires_tenant",
533
543
  "wait_tenant_unresolved",
@@ -662,6 +672,24 @@ async function registerWait(opts, conditions, deadlineIso) {
662
672
  throw err;
663
673
  }
664
674
  const body = await res.json();
675
+ const guardedReviews = conditions.filter((c) => c.type === "pr-review" && c.params?.sha && (c.params.number || c.params.pr) &&
676
+ String(c.params.reviewer ?? "chatgpt-codex-connector").toLowerCase().replace(/\[bot\]$/, "") === "chatgpt-codex-connector");
677
+ for (const condition of guardedReviews) {
678
+ const params = condition.params;
679
+ const guard = Array.isArray(body.review_guards) && body.review_guards.find((g) =>
680
+ typeof g?.repo === "string" && g.repo.toLowerCase() === params.repo.toLowerCase() &&
681
+ Number(g.pr_number) === Number(params.number ?? params.pr) && typeof g.commit_sha === "string" &&
682
+ /^[0-9a-f]{7,40}$/i.test(g.commit_sha) &&
683
+ (g.commit_sha.toLowerCase().startsWith(params.sha) || params.sha.startsWith(g.commit_sha.toLowerCase())));
684
+ const binding = Array.isArray(body.conditions) ? body.conditions.find((c) => c.id === condition.id)?.review_guard : null;
685
+ if (!guard || typeof guard.id !== "string" || !guard.id || binding?.id !== guard.id ||
686
+ !Number.isFinite(Date.parse(binding.started_at)) || !Number.isFinite(Date.parse(guard.started_at)) ||
687
+ !Number.isFinite(Date.parse(guard.deadline)) || !Number.isInteger(guard.minutes) || guard.minutes < 10 || guard.minutes > 1440 ||
688
+ !["call", "repository", "tenant"].includes(guard.source)) {
689
+ const err = new Error("relay did not bind the required review intervention policy; upgrade the backend before using this client");
690
+ err.invalidCondition = true; err.errorCode = "review_guard_unsupported"; throw err;
691
+ }
692
+ }
665
693
  // BOT-1572/1608: a session-token wait has no profile to attest; the relay
666
694
  // returns the derived session_id + agent_id. Their absence means the relay did
667
695
  // not honour the token as a session credential — fail closed.
@@ -694,6 +722,7 @@ async function registerWait(opts, conditions, deadlineIso) {
694
722
  // BOT-1539: arm-time snapshot of already-outstanding code review for any
695
723
  // pr-review/pr-state target. Report-only — surfaced on stderr so a silent
696
724
  // edge-triggered park isn't mistaken for "no review yet".
725
+ reviewGuards: Array.isArray(body.review_guards) ? body.review_guards : [],
697
726
  prReviewSnapshot: Array.isArray(body.pr_review_snapshot) ? body.pr_review_snapshot : null,
698
727
  // BOT-2400: how often this session already armed this exact condition set (24 h), and
699
728
  // the relay's warning once that reaches 3.
@@ -2101,6 +2130,9 @@ export async function runWait(argv, { recoveryLocalWaitId = null } = {}) {
2101
2130
  for (const line of formatPrReviewSnapshotWarnings(reg.prReviewSnapshot)) {
2102
2131
  process.stderr.write(`bb-wait: ${line}\n`);
2103
2132
  }
2133
+ for (const guard of reg.reviewGuards ?? []) {
2134
+ process.stderr.write(`bb-wait: review intervention ${guard.minutes} min (${guard.source}), deadline ${guard.deadline}${guard.diagnosed ? "; already diagnosed for this request" : ""}\n`);
2135
+ }
2104
2136
  // BOT-2400: re-arming the same wait again and again burns tokens on every wake.
2105
2137
  if (reg.rearmWarning) process.stderr.write(`bb-wait: ${reg.rearmWarning}\n`);
2106
2138
  // BOT-1837: one line naming the branch's latest run at arm time. Report-only
@@ -2130,7 +2162,9 @@ export async function runWait(argv, { recoveryLocalWaitId = null } = {}) {
2130
2162
  // and refusing a recycled key). No `tenant` is stamped for `linear` — its cross-tenant
2131
2163
  // isolation rides on the central guard's sessionTenant (BOT-1259), not a per-type param.
2132
2164
  const linearScope = new Map();
2165
+ const reviewScope = new Map();
2133
2166
  for (const rc of reg.conditions) {
2167
+ if (rc?.type === "pr-review" && rc.review_guard) reviewScope.set(rc.id, rc.review_guard);
2134
2168
  if (rc && rc.type === "lock" && rc.params && typeof rc.params.host === "string") {
2135
2169
  canonHost.set(rc.id, rc.params.host);
2136
2170
  }
@@ -2142,6 +2176,7 @@ export async function runWait(argv, { recoveryLocalWaitId = null } = {}) {
2142
2176
  }
2143
2177
  }
2144
2178
  for (const c of conditions) {
2179
+ if (c.type === "pr-review" && reviewScope.has(c.id)) c.review_guard = reviewScope.get(c.id);
2145
2180
  if (c.type === "lock" && canonHost.has(c.id)) c.params.host = canonHost.get(c.id);
2146
2181
  if (c.type === "unblocked" && unblockedScope.has(c.id)) {
2147
2182
  const s = unblockedScope.get(c.id);