@botbuddy/cli 1.44.1 → 1.45.1

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.44.1",
3
+ "version": "1.45.1",
4
4
  "description": "BotBuddy — Swarm coordination CLI for multi-agent workflows",
5
5
  "type": "module",
6
6
  "bin": {
package/src/wait-core.mjs CHANGED
@@ -190,7 +190,8 @@ const VALIDATORS = {
190
190
  const id = hex ? `${hex.slice(0, 8)}-${hex.slice(8, 12)}-${hex.slice(12, 16)}-${hex.slice(16, 20)}-${hex.slice(20)}` : raw;
191
191
  return { id };
192
192
  },
193
- // BOT-1064 — linear: wake on a logical state change of a named BOT-* issue.
193
+ // BOT-1064 / BOT-2112 — linear: wake on a logical state change of a named
194
+ // issue in any Linear team bound to the authenticated tenant.
194
195
  // The spine subject_key is the issue identifier as stored on the tickets
195
196
  // projection (upper-cased at write), so normalize to upper-case here too.
196
197
  linear(p) {
@@ -435,10 +436,13 @@ function prSubjectMatches(subjectKey, params) {
435
436
  return subjectKey.startsWith(`${params.repo}#`);
436
437
  }
437
438
 
438
- // Does a commit field on a signal payload start with the condition's sha prefix?
439
- // A missing / non-string field never matches a sha-scoped condition.
439
+ // Does a commit field on a signal payload identify the same commit as the
440
+ // condition's SHA? GitHub may send either side as a 7–40-character prefix, so
441
+ // accept either prefix direction while refusing malformed payload values.
440
442
  function shaPrefixMatches(value, sha) {
441
- return typeof value === "string" && value.trim().toLowerCase().startsWith(sha);
443
+ if (typeof value !== "string") return false;
444
+ const candidate = value.trim().toLowerCase();
445
+ return /^[0-9a-f]{7,40}$/.test(candidate) && (candidate.startsWith(sha) || sha.startsWith(candidate));
442
446
  }
443
447
 
444
448
  // BOT-1975: optional head scoping for pr-state. pr_state signals carry the PR
package/src/wait.mjs CHANGED
@@ -78,8 +78,8 @@ CONDITIONS (TYPE:key=val,key=val — repeat for several; --any wakes on the fir
78
78
  by the PR read model. Omit pr= to wake on ANY of your PRs
79
79
  in the repo; or pass pr=<owner/repo#n> in one value. A
80
80
  conflict/clean flip sets payload.mergeable_changed. The
81
- payload carries head_ref_oid; sha=<head> (7-40 hex, a
82
- prefix is fine) wakes only on signals for that head, so a
81
+ payload carries head_ref_oid; sha=<head> (7-40 hex; either
82
+ side may be an abbreviated prefix) wakes only on signals for that head, so a
83
83
  previous head's check result can't wake you. A push alone
84
84
  emits nothing — the head rides on the next transition.
85
85
  pr-review:repo=<owner/repo>[,pr=<n>][,reviewer=<login>][,sha=<commit>]
@@ -96,7 +96,7 @@ CONDITIONS (TYPE:key=val,key=val — repeat for several; --any wakes on the fir
96
96
  existing review will not re-wake it.
97
97
  reviewer=<login> wakes only on that reviewer (case-
98
98
  insensitive; a trailing [bot] is ignored). sha=<commit>
99
- wakes only on verdicts for that commit (submitted reviews
99
+ wakes only on verdicts for that commit (either side may be an abbreviated prefix; submitted reviews
100
100
  and clean passes carry commit_sha; thread comments do not,
101
101
  so they never match sha=); the usage-limit decline always
102
102
  passes sha=. Any other key is refused (exit 4).
@@ -108,8 +108,9 @@ CONDITIONS (TYPE:key=val,key=val — repeat for several; --any wakes on the fir
108
108
  guidance:request=<post_id> a command-post guidance request being answered
109
109
  (scoped to the asking agent; fetch the body with
110
110
  get_guidance_response)
111
- linear:issue=<KEY> a BOT-* issue changing state, a new comment, or an
112
- assignee change (e.g. issue=BOT-123; tenant-scoped).
111
+ linear:issue=<KEY> an issue in any Linear team bound to your tenant;
112
+ wakes on a state change, new comment, or assignee change (e.g.
113
+ issue=BOT-123; tenant-scoped).
113
114
  Flags linear_feed_lagging if the ingest feed has stalled.
114
115
  unblocked:<KEY> a ticket's LAST open blocker clearing (open-blocker
115
116
  count >0 → 0): a blocker moving to Done/Canceled/Duplicate
@@ -378,6 +379,10 @@ async function registerWait(opts, conditions, deadlineIso) {
378
379
  // wake-capable run for. The wait could only ever expire, and no amount of
379
380
  // waiting repairs a typo — a hard stop, with the server's candidates.
380
381
  "unknown_workflow",
382
+ // BOT-2112: these are arm-time authorization/target errors. Retrying as
383
+ // an untracked live-only wait would silently park on a signal it cannot
384
+ // receive, so preserve the relay's typed error and exit 4.
385
+ "linear_team_not_bound",
381
386
  // BOT-1612: a `chat:channel=<id>` wait whose bound agent is not one of the
382
387
  // channel's participants could only ever park to timeout (the emitter never
383
388
  // scopes a message to a non-participant). The relay refuses it at arm time;
@@ -395,6 +400,29 @@ async function registerWait(opts, conditions, deadlineIso) {
395
400
  throw err;
396
401
  }
397
402
  }
403
+ if (res.status === 404) {
404
+ const body = await res.json().catch(() => ({}));
405
+ if (body.error === "linear_issue_not_found") {
406
+ const err = new Error(body.detail || body.error);
407
+ err.invalidCondition = true;
408
+ err.errorCode = body.error;
409
+ if (Array.isArray(body.candidates)) err.candidates = body.candidates;
410
+ throw err;
411
+ }
412
+ // BOT-1523 retains its existing not-found contract. Keep every 404's body
413
+ // handling in one branch: parsing it above and falling through would consume
414
+ // the body before this typed unblocked refusal could see it.
415
+ if (body.error === "unblocked_ticket_not_found") {
416
+ const err = new Error(body.detail || body.error);
417
+ err.hydrationRegister = true;
418
+ err.errorCode = body.error;
419
+ err.ticket = body.ticket ?? null;
420
+ err.retryable = body.retryable ?? null;
421
+ err.hydration = body.hydration ?? null;
422
+ err.notFound = true;
423
+ throw err;
424
+ }
425
+ }
398
426
  if (res.status === 403) {
399
427
  const body = await res.json().catch(() => ({}));
400
428
  if (new Set(["wait_tenant_unresolved", "wait_cross_tenant", "wait_tenant_mismatch", "wait_resume_identity_mismatch"]).has(body.error)) {
@@ -427,7 +455,7 @@ async function registerWait(opts, conditions, deadlineIso) {
427
455
  err.minimumProtocol = body.minimum_wait_protocol ?? null;
428
456
  throw err;
429
457
  }
430
- if (res.status === 409 || res.status === 404) {
458
+ if (res.status === 409) {
431
459
  // BOT-1523: `unblocked:<KEY>` hydration at registration could not arm the wait
432
460
  // truthfully. These are NOT the untracked-live-only fallback: arming a live-only
433
461
  // unblocked wait would skip the server's initial evaluation and could resume a