@botbuddy/cli 1.8.4 → 1.8.5

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.8.4",
3
+ "version": "1.8.5",
4
4
  "description": "BotBuddy — Swarm coordination CLI for multi-agent workflows",
5
5
  "type": "module",
6
6
  "bin": {
@@ -1 +1 @@
1
- {"schema_version":1,"source_version":"1.8.3","source_identity":"8a450fe651f42fc96f9fd713da5fe707ff48a914c174496337495adf0c425588"}
1
+ {"schema_version":1,"source_version":"1.8.3","source_identity":"a3c85408868d913b2ab265f910ab062916c1c394895cec71cae0f79fc99f5a6c"}
package/src/wait-core.mjs CHANGED
@@ -196,20 +196,44 @@ const VALIDATORS = {
196
196
  // BOT-1066 — ci: wake when a PR's CI reaches a terminal conclusion. Exactly one
197
197
  // selector: scope=latest (follows the PR's latest run — needs pr=), run_id=<id>
198
198
  // (pinned to one run), or sha=<sha> (pinned to a commit). repo= is always required.
199
+ // BOT-1507 — scope=next adds a fourth, LIVE-ONLY selector: it names no run, sha,
200
+ // or PR (they must all already exist at arm time) and wakes on the first terminal
201
+ // CI run for the repo delivered AFTER the wait was armed — the post-merge / cron
202
+ // train case. Optional branch=/workflow= filter that first run; both are valid
203
+ // ONLY with scope=next (a run_id/sha/latest wait already names its run).
199
204
  ci(p) {
200
205
  const repo = p.repo != null ? String(p.repo).trim() : null;
201
206
  if (!repo || !repo.includes("/")) throw new Error("ci needs repo=<owner/repo>");
202
207
  const selectors = ["scope", "run_id", "sha"].filter((k) => p[k] !== undefined);
203
208
  if (selectors.length !== 1) {
204
- throw new Error("ci needs exactly one of scope=latest | run_id=<id> | sha=<sha>");
209
+ throw new Error("ci needs exactly one of scope=latest|next | run_id=<id> | sha=<sha>");
205
210
  }
211
+ const hasFilter = p.branch !== undefined || p.workflow !== undefined;
206
212
  if (p.scope !== undefined) {
207
- if (p.scope !== "latest") throw new Error("ci scope must be 'latest' (or use run_id=/sha=)");
208
- if (p.pr === undefined || !/^\d+$/.test(String(p.pr).trim())) {
209
- throw new Error("ci scope=latest needs pr=<number>");
213
+ if (p.scope === "latest") {
214
+ if (hasFilter) throw new Error("ci branch=/workflow= are only valid with scope=next");
215
+ if (p.pr === undefined || !/^\d+$/.test(String(p.pr).trim())) {
216
+ throw new Error("ci scope=latest needs pr=<number>");
217
+ }
218
+ return { repo, scope: "latest", pr: String(p.pr).trim() };
219
+ }
220
+ if (p.scope === "next") {
221
+ // A future run has no PR head to follow — pr= is meaningless (and run_id/sha
222
+ // are already excluded by the single-selector check above).
223
+ if (p.pr !== undefined) {
224
+ throw new Error("ci scope=next takes no pr= (it targets a future run, not a PR head)");
225
+ }
226
+ const filter = (key) => {
227
+ if (p[key] === undefined) return null;
228
+ const v = String(p[key]).trim();
229
+ if (v === "") throw new Error(`ci ${key}= must be non-empty`);
230
+ return v;
231
+ };
232
+ return { repo, scope: "next", branch: filter("branch"), workflow: filter("workflow"), pr: null };
210
233
  }
211
- return { repo, scope: "latest", pr: String(p.pr).trim() };
234
+ throw new Error("ci scope must be 'latest' or 'next' (or use run_id=/sha=)");
212
235
  }
236
+ if (hasFilter) throw new Error("ci branch=/workflow= are only valid with scope=next");
213
237
  if (p.run_id !== undefined) {
214
238
  if (!String(p.run_id).trim()) throw new Error("ci run_id must be non-empty");
215
239
  return { repo, scope: "run_id", runId: String(p.run_id).trim(), pr: p.pr != null ? String(p.pr).trim() : null };
@@ -679,6 +703,23 @@ function conditionMatchesSignal(condition, signal, waitSessionId) {
679
703
  if (String(signal.payload?.repo ?? "").toLowerCase() !== params.repo.toLowerCase()) return false;
680
704
  if (params.scope === "run_id") return String(signal.payload?.run_id) === params.runId;
681
705
  if (params.scope === "sha") return signal.payload?.head_sha === params.sha;
706
+ if (params.scope === "next") {
707
+ // BOT-1507 — live-only: the first TERMINAL run for the repo. Never keys on
708
+ // subject_key (it is run:<id> for post-merge runs and owner/repo#n for PR
709
+ // runs — both are legitimate wakes); a null/empty conclusion is an
710
+ // in-progress signal and never terminal. Optional branch/workflow filters:
711
+ // branch is exact, workflow is the GitHub display name (case/space-insensitive).
712
+ const conclusion = signal.payload?.conclusion;
713
+ if (typeof conclusion !== "string" || conclusion.trim() === "") return false;
714
+ if (params.branch != null && signal.payload?.head_branch !== params.branch) return false;
715
+ if (params.workflow != null) {
716
+ const wf = signal.payload?.workflow_name;
717
+ if (typeof wf !== "string" || wf.trim().toLowerCase() !== params.workflow.trim().toLowerCase()) {
718
+ return false;
719
+ }
720
+ }
721
+ return true;
722
+ }
682
723
  // scope=latest: the PR's run (by subject owner/repo#pr or payload pr_number).
683
724
  return signal.subject_key === `${params.repo}#${params.pr}` ||
684
725
  (signal.payload?.pr_number != null && String(signal.payload.pr_number) === params.pr);
package/src/wait.mjs CHANGED
@@ -75,10 +75,16 @@ CONDITIONS (TYPE:key=val,key=val — repeat for several; --any wakes on the fir
75
75
  Host-shared; a silently-dead host is detected server-side
76
76
  (capacity_source_stale) via a host beacon — the client
77
77
  grace (default 900s) is a backstop, no longer the only guard.
78
- ci:repo=<owner/repo>,{scope=latest,pr=<n> | run_id=<id> | sha=<sha>}
78
+ ci:repo=<owner/repo>,{scope=latest,pr=<n> | run_id=<id> | sha=<sha>
79
+ | scope=next[,branch=<name>][,workflow=<name>]}
79
80
  a PR's CI reaching a terminal conclusion (owner-scoped);
80
81
  GitHub Actions or an external check_suite provider.
81
82
  A repo with no CI feed is rejected (no_signal_source).
83
+ scope=next is LIVE-ONLY: it wakes on the first terminal
84
+ run delivered AFTER arm (post-merge / cron / scheduled),
85
+ never replays a finished run, and needs no run_id/sha/pr.
86
+ Optional branch= (exact) / workflow= (GitHub workflow
87
+ DISPLAY name, case-insensitive) narrow that first run.
82
88
  staging-green:repo=<owner/repo> verified automatic recovery of an enabled staging gate.
83
89
  Wakes only after a complete non-vacuous workflow success;
84
90
  manual resolutions, overrides, skipped, and neutral runs do not match.