@botbuddy/cli 1.63.37 → 1.63.39

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.37",
3
- "source_commit": "503ca85897b8b844ee5a841addcbb383c563de68",
4
- "source_sha256": "16d04abbbc7838ae352f1cde6e5b7f76d47c2ae3723d47a0d79f3b1ebcc74c63",
2
+ "release_version": "1.63.39",
3
+ "source_commit": "d98a90edc6157b66510ab7b5ca58d210cbfc8e3a",
4
+ "source_sha256": "7594e80018bcbd9014684e4d916376bdc0c8388ed4ccec4afaf52fde3a40c659",
5
5
  "target": "darwin-arm64",
6
- "sha256": "dc1147cf8c4554b973d31c14fe95d2edadf7623b23e9ac9d578d4e9a6de7f975"
6
+ "sha256": "98e622a171da78743c8311ddb4aef4565c2a5e836c143089ab799b23dea01fad"
7
7
  }
@@ -1,7 +1,7 @@
1
1
  {
2
- "release_version": "1.63.37",
3
- "source_commit": "503ca85897b8b844ee5a841addcbb383c563de68",
4
- "source_sha256": "16d04abbbc7838ae352f1cde6e5b7f76d47c2ae3723d47a0d79f3b1ebcc74c63",
2
+ "release_version": "1.63.39",
3
+ "source_commit": "d98a90edc6157b66510ab7b5ca58d210cbfc8e3a",
4
+ "source_sha256": "7594e80018bcbd9014684e4d916376bdc0c8388ed4ccec4afaf52fde3a40c659",
5
5
  "target": "linux-x64",
6
- "sha256": "f8c16854c6324625c2284a3707edafa0c4f79a0560a8cc7a97a2f952014b1900"
6
+ "sha256": "9fd7c1b2587f0b0abce54f923f87e0229405a5c951cac580ecaefc5f16e384cd"
7
7
  }
@@ -1,21 +1,21 @@
1
1
  {
2
2
  "schema_version": 1,
3
- "release_version": "1.63.37",
4
- "source_commit": "503ca85897b8b844ee5a841addcbb383c563de68",
5
- "source_sha256": "16d04abbbc7838ae352f1cde6e5b7f76d47c2ae3723d47a0d79f3b1ebcc74c63",
3
+ "release_version": "1.63.39",
4
+ "source_commit": "d98a90edc6157b66510ab7b5ca58d210cbfc8e3a",
5
+ "source_sha256": "7594e80018bcbd9014684e4d916376bdc0c8388ed4ccec4afaf52fde3a40c659",
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": "dc1147cf8c4554b973d31c14fe95d2edadf7623b23e9ac9d578d4e9a6de7f975",
11
+ "sha256": "98e622a171da78743c8311ddb4aef4565c2a5e836c143089ab799b23dea01fad",
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": "f8c16854c6324625c2284a3707edafa0c4f79a0560a8cc7a97a2f952014b1900",
18
+ "sha256": "9fd7c1b2587f0b0abce54f923f87e0229405a5c951cac580ecaefc5f16e384cd",
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.37",
3
+ "version": "1.63.39",
4
4
  "description": "BotBuddy \u2014 Swarm coordination CLI for multi-agent workflows",
5
5
  "type": "module",
6
6
  "bin": {
package/src/wait-core.mjs CHANGED
@@ -521,7 +521,7 @@ function parsePrTarget(p, label) {
521
521
  // it then woke on another head's or another reviewer's signal. Now an unknown key
522
522
  // refuses at arm (invalid_conditions, exit 4). `reviewer=` is pr-review only.
523
523
  const PR_CONDITION_KEYS = Object.freeze({
524
- "pr-state": Object.freeze(["repo", "pr", "number", "sha"]),
524
+ "pr-state": Object.freeze(["repo", "pr", "number", "sha", "state"]),
525
525
  "pr-review": Object.freeze(["repo", "pr", "number", "sha", "reviewer", "stall_minutes"]),
526
526
  });
527
527
 
@@ -531,6 +531,11 @@ function normalizeReviewerLogin(login) {
531
531
  return String(login).trim().toLowerCase().replace(/\[bot\]$/, "");
532
532
  }
533
533
 
534
+ // BOT-2724: the PR lifecycle values `pr_state.state` carries (prStateMapper's
535
+ // PrLifecycle minus `unknown`). `state=merged` makes a merge watch wake once, on
536
+ // the merge, instead of on every mergeable/check/review flip of the open PR.
537
+ const PR_LIFECYCLE_STATES = Object.freeze(["open", "draft", "merged", "closed"]);
538
+
534
539
  function parsePrCondition(p, label) {
535
540
  const allowed = PR_CONDITION_KEYS[label];
536
541
  const unknown = Object.keys(p).filter((key) => !allowed.includes(key));
@@ -547,6 +552,13 @@ function parsePrCondition(p, label) {
547
552
  }
548
553
  out.sha = sha;
549
554
  }
555
+ if (p.state !== undefined) {
556
+ const states = String(p.state).split("|").map((value) => value.trim().toLowerCase());
557
+ if (states.some((value) => !PR_LIFECYCLE_STATES.includes(value))) {
558
+ throw new Error(`${label} state= must be one or more of ${PR_LIFECYCLE_STATES.join(", ")} (|-joined)`);
559
+ }
560
+ out.states = [...new Set(states)];
561
+ }
550
562
  if (p.reviewer !== undefined) {
551
563
  const reviewer = typeof p.reviewer === "string" ? normalizeReviewerLogin(p.reviewer) : "";
552
564
  if (reviewer === "") throw new Error(`${label} reviewer= must be a GitHub login`);
@@ -583,9 +595,59 @@ function shaPrefixMatches(value, sha) {
583
595
 
584
596
  // BOT-1975: optional head scoping for pr-state. pr_state signals carry the PR
585
597
  // head as payload.head_ref_oid; a signal for a previous head does not wake.
598
+ // BOT-2724: optional lifecycle scoping — only a payload in a named state wakes.
586
599
  function prStateFiltersMatch(signal, params) {
587
- if (params.sha == null) return true;
588
- return shaPrefixMatches(signal.payload?.head_ref_oid, params.sha);
600
+ return prStatePayloadMatches(signal.payload ?? {}, params);
601
+ }
602
+
603
+ function prStatePayloadMatches(payload, params) {
604
+ if (params.sha != null && !shaPrefixMatches(payload.head_ref_oid, params.sha)) return false;
605
+ if (params.states != null) {
606
+ const state = typeof payload.state === "string" ? payload.state.trim().toLowerCase() : "";
607
+ if (!params.states.includes(state)) return false;
608
+ }
609
+ return true;
610
+ }
611
+
612
+ /**
613
+ * BOT-2724 — level-trigger a `pr-state:…,state=` wait at arm. The relay's
614
+ * BOT-1539 arm snapshot carries each targeted PR's current `pr_state` row; a PR
615
+ * that is ALREADY in a requested state (merged before the wait armed) would
616
+ * otherwise never emit that transition again and park the wait to its timeout.
617
+ * Returns one receipt `matched` entry per granted condition (empty when none).
618
+ * A snapshot without `state` (an older relay) never grants.
619
+ *
620
+ * @param {object[]} conditions parsed conditions
621
+ * @param {Array<{repo:string,pr:number,state?:string,merged_at?:string|null,head_ref_oid?:string|null}>|null|undefined} snapshot
622
+ * @returns {object[]}
623
+ */
624
+ export function prStateArmMatches(conditions, snapshot) {
625
+ if (!Array.isArray(snapshot)) return [];
626
+ const matched = [];
627
+ for (const condition of conditions) {
628
+ if (condition.type !== "pr-state" || condition.params.states == null || condition.params.number == null) continue;
629
+ const subjectKey = `${condition.params.repo}#${condition.params.number}`;
630
+ const entry = snapshot.find((e) => e && typeof e.repo === "string" &&
631
+ `${e.repo}#${e.pr}`.toLowerCase() === subjectKey.toLowerCase());
632
+ if (!entry || typeof entry.state !== "string") continue;
633
+ const payload = {
634
+ repo: entry.repo,
635
+ pr_number: entry.pr,
636
+ state: entry.state,
637
+ merged_at: entry.merged_at ?? null,
638
+ head_ref_oid: entry.head_ref_oid ?? null,
639
+ };
640
+ if (!prStatePayloadMatches(payload, condition.params)) continue;
641
+ matched.push({
642
+ condition_id: condition.id,
643
+ signal_type: "pr_state",
644
+ seq: null,
645
+ subject_key: `${entry.repo}#${entry.pr}`,
646
+ payload,
647
+ provenance: "arm_snapshot",
648
+ });
649
+ }
650
+ return matched;
589
651
  }
590
652
 
591
653
  // BOT-1975 / BOT-1972: optional commit + reviewer scoping for pr-review. A
@@ -1622,6 +1684,9 @@ export async function runWaitLoop({
1622
1684
  // `error`, so the receipt itself says what CI was already doing when this wait
1623
1685
  // armed. Purely descriptive — it never changes the outcome or the exit code.
1624
1686
  timeoutError = null,
1687
+ // BOT-2724: matches already proven at arm (prStateArmMatches). Non-empty ends the
1688
+ // wait as matched before any stream is opened.
1689
+ armMatches = null,
1625
1690
  }) {
1626
1691
  const startedAt = typeof originalStartedAt === "string" && Number.isFinite(Date.parse(originalStartedAt))
1627
1692
  ? originalStartedAt : nowIso();
@@ -1934,6 +1999,13 @@ export async function runWaitLoop({
1934
1999
  }
1935
2000
  };
1936
2001
 
2002
+ // An expired-recovery resume must not take a CURRENT-snapshot grant: a PR that
2003
+ // merged after the original deadline would turn a timeout into a match. It
2004
+ // replays the durable pre-deadline signals instead (Codex P2).
2005
+ if (replayOnlyThroughMs == null && Array.isArray(armMatches) && armMatches.length > 0) {
2006
+ return finalize("matched", { exitCode: EXIT.MATCHED, matched: armMatches });
2007
+ }
2008
+
1937
2009
  while (true) {
1938
2010
  // Expired recovery must first replay durable pre-deadline rows. A restored
1939
2011
  // timer or capacity-stale alarm may otherwise win before that replay begins.
package/src/wait.mjs CHANGED
@@ -13,7 +13,7 @@
13
13
  // Usage: botbuddy wait [--any] <condition>... [options]
14
14
  // Run botbuddy wait --help for the condition grammar.
15
15
 
16
- import { EXIT, describeConditionErrors, parseConditions, parseSseFrames, runWaitLoop, normalizeSince, truncateReceipt, formatPrReviewSnapshotWarnings, formatCiLatestRunNotice, formatUnknownWorkflowGuidance, reviewTimeoutNotice } from "./wait-core.mjs";
16
+ import { EXIT, describeConditionErrors, parseConditions, parseSseFrames, runWaitLoop, normalizeSince, truncateReceipt, formatPrReviewSnapshotWarnings, prStateArmMatches, formatCiLatestRunNotice, formatUnknownWorkflowGuidance, reviewTimeoutNotice } from "./wait-core.mjs";
17
17
  import { readAgentBinding, withPrincipalReceipt } from "./wait-profile.mjs";
18
18
  import { VERSION } from "./version.mjs";
19
19
  import { fileURLToPath } from "node:url";
@@ -64,6 +64,7 @@ USAGE
64
64
  Pick the condition that proves the state your NEXT action needs, not a correlated proxy:
65
65
  CI finished on the current PR head ......... ci:…,scope=latest,pr=<n> (or exact sha=)
66
66
  review activity (Codex, a human) ........... pr-review:… (arm BEFORE requesting it)
67
+ a PR merged (or closed) .................... pr-state:…,pr=<n>,state=merged
67
68
  PR merged/closed/conflict/ready flip ........ pr-state:…
68
69
  your merged PR reached staging ............. staging-release:…
69
70
  staging recovered after an incident ........ staging-green:…
@@ -94,7 +95,7 @@ CONDITIONS (TYPE:key=val,key=val — repeat for several; --any wakes on the fir
94
95
  Must be the ONLY condition (use --timeout to bound it); can't be
95
96
  raced against other conditions. Omit claim to just wait for
96
97
  availability and acquire it yourself.
97
- pr-state:repo=<owner/repo>[,pr=<n>][,sha=<head>]
98
+ pr-state:repo=<owner/repo>[,pr=<n>][,sha=<head>][,state=<open|draft|merged|closed>]
98
99
  a PR lifecycle or mergeable flip (draft<->ready, merged,
99
100
  closed, conflict/clean flip) plus check-rollup, review-
100
101
  decision and unresolved-thread-count changes, as projected
@@ -105,7 +106,10 @@ CONDITIONS (TYPE:key=val,key=val — repeat for several; --any wakes on the fir
105
106
  side may be an abbreviated prefix) wakes only on signals for
106
107
  that head, so a previous head's check result can't wake you.
107
108
  A push alone emits nothing — the head rides on the next
108
- transition.
109
+ transition. state=merged (|-join several, e.g.
110
+ merged|closed) wakes ONLY when the PR is in that lifecycle
111
+ state, ignoring check/review/conflict churn, and matches at
112
+ arm if the PR is already there (provenance arm_snapshot).
109
113
  pr-review:repo=<owner/repo>[,pr=<n>][,reviewer=<login>][,sha=<commit>][,stall_minutes=<10-1440>]
110
114
  a submitted review, a new review comment, a thread
111
115
  resolved/re-opened (payload carries the unresolved-thread
@@ -316,6 +320,9 @@ EXAMPLES (concrete values shown — substitute your repo, PR, SHA, ticket and i
316
320
  # Codex review of the head you just pushed (arm BEFORE posting @codex review).
317
321
  bb wait 'pr-review:repo=bot-buddy/botbuddy-webapp,pr=1234,reviewer=chatgpt-codex-connector[bot],sha=9f3c2a1' --heartbeat
318
322
 
323
+ # Another PR merged (e.g. your blocker's fix): one wake, on the merge only.
324
+ bb wait 'pr-state:repo=SupplySpark/supplyspark-ent,pr=3803,state=merged' --heartbeat
325
+
319
326
  # The next review comment OR a PR state change (merge, conflict, checks), whichever first.
320
327
  bb wait --any 'pr-review:repo=bot-buddy/botbuddy-webapp,pr=1234' \\
321
328
  'pr-state:repo=bot-buddy/botbuddy-webapp,pr=1234,sha=9f3c2a1' --heartbeat
@@ -2092,6 +2099,8 @@ export async function runWait(argv, { recoveryLocalWaitId = null } = {}) {
2092
2099
  // register and reused as the timeout receipt's error. Null when there is
2093
2100
  // nothing to report (no ci scope=next+branch condition, or no run yet).
2094
2101
  let ciArmNotice = null;
2102
+ // BOT-2724: pr-state state= conditions already satisfied when the wait armed.
2103
+ let armMatches = [];
2095
2104
  // BOT-2468: the wait's /waits?wait= deeplink from register; stays undefined when the
2096
2105
  // wait never registered (live-only/offline), so its receipt carries no web_url key.
2097
2106
  let registeredWebUrl;
@@ -2205,6 +2214,7 @@ export async function runWait(argv, { recoveryLocalWaitId = null } = {}) {
2205
2214
  for (const line of formatPrReviewSnapshotWarnings(reg.prReviewSnapshot)) {
2206
2215
  process.stderr.write(`bb-wait: ${line}\n`);
2207
2216
  }
2217
+ armMatches = prStateArmMatches(conditions, reg.prReviewSnapshot);
2208
2218
  for (const guard of reg.reviewGuards ?? []) {
2209
2219
  process.stderr.write(`bb-wait: review intervention ${guard.minutes} min (${guard.source}), deadline ${guard.deadline}${guard.diagnosed ? "; already diagnosed for this request" : ""}\n`);
2210
2220
  }
@@ -2507,6 +2517,7 @@ export async function runWait(argv, { recoveryLocalWaitId = null } = {}) {
2507
2517
  // BOT-1837: a timeout receipt carries the arm-time CI notice, so a caller
2508
2518
  // reading only the receipt learns the run it meant had already finished.
2509
2519
  timeoutError: ciArmNotice,
2520
+ armMatches,
2510
2521
  onCapacitySourceSustained: ({ host_key, stale_since }) => {
2511
2522
  process.stderr.write(`bb-wait: BLOCKED capacity_source_sustained on ${host_key}, stale_since=${stale_since ?? "unknown"}; ask for a Helper check/restart (launchctl print user/$UID/com.botbuddy.helper.worker). The wait remains armed.\n`);
2512
2523
  },