@botbuddy/cli 1.30.1 → 1.31.0
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 +1 -1
- package/src/commands.mjs +12 -4
- package/src/wait-core.mjs +20 -7
- package/src/wait.mjs +7 -4
package/package.json
CHANGED
package/src/commands.mjs
CHANGED
|
@@ -323,8 +323,16 @@ function gitHint(cwd, args) {
|
|
|
323
323
|
catch { return null; }
|
|
324
324
|
}
|
|
325
325
|
|
|
326
|
-
/**
|
|
327
|
-
|
|
326
|
+
/**
|
|
327
|
+
* Best-effort display-only context. It is never an authorization input.
|
|
328
|
+
*
|
|
329
|
+
* `git` is injectable so a test can exercise the branch/remote parsing without
|
|
330
|
+
* depending on the ambient checkout: a detached-HEAD checkout (the npm publish
|
|
331
|
+
* lane checks out the promoted SHA, not a branch) has no branch name, which used
|
|
332
|
+
* to make the branch-recognition assertion fail only in CI (BOT-1649 publish run
|
|
333
|
+
* 35328434334). Production still defaults to the real git resolver.
|
|
334
|
+
*/
|
|
335
|
+
export function deriveCallerHint({ env = process.env, cwd = process.cwd(), git = gitHint } = {}) {
|
|
328
336
|
let cachedAgentId = null;
|
|
329
337
|
try {
|
|
330
338
|
const path = join(cwd, ".botbuddy", "agent-state.json");
|
|
@@ -333,9 +341,9 @@ export function deriveCallerHint({ env = process.env, cwd = process.cwd() } = {}
|
|
|
333
341
|
cachedAgentId = typeof parsed?.agent_id === "string" && parsed.agent_id ? parsed.agent_id : null;
|
|
334
342
|
}
|
|
335
343
|
} catch { /* a malformed cache is not a login failure */ }
|
|
336
|
-
const branch =
|
|
344
|
+
const branch = git(cwd, ["branch", "--show-current"]);
|
|
337
345
|
const ticket = branch?.match(/(?:^|\/)((?:bot|ent)-\d+)(?:-|$)/i)?.[1]?.toUpperCase() ?? null;
|
|
338
|
-
const remote =
|
|
346
|
+
const remote = git(cwd, ["remote", "get-url", "origin"]);
|
|
339
347
|
const repo = remote?.match(/(?:github\.com[:/])([^/]+\/[^/.]+)(?:\.git)?$/i)?.[1] ?? null;
|
|
340
348
|
const harness = env.BOTBUDDY_CALLER_HARNESS
|
|
341
349
|
|| (env.CLAUDE_CODE || env.CLAUDECODE ? "claude-code" : Object.keys(env).some((key) => key.startsWith("CODEX_")) ? "codex" : null);
|
package/src/wait-core.mjs
CHANGED
|
@@ -196,16 +196,26 @@ 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
|
|
200
|
-
//
|
|
201
|
-
//
|
|
202
|
-
//
|
|
203
|
-
//
|
|
199
|
+
// BOT-1507 — scope=next adds a fourth, LIVE-ONLY selector: it names no run or PR
|
|
200
|
+
// (they must already exist at arm time) and wakes on the first terminal CI run for
|
|
201
|
+
// the repo delivered AFTER the wait was armed — the post-merge / cron train case.
|
|
202
|
+
// Optional branch=/workflow= filter that first run; both are valid ONLY with
|
|
203
|
+
// scope=next (a run_id/sha/latest wait already names its run).
|
|
204
|
+
// BOT-1685 — scope=next also accepts an optional sha= FILTER (distinct from the
|
|
205
|
+
// level-triggered sha= selector): a wait armed BEFORE re-dispatching a run for a
|
|
206
|
+
// known commit then wakes on the NEXT terminal run for THAT commit, instead of
|
|
207
|
+
// replaying an earlier successful run for the same sha.
|
|
204
208
|
ci(p) {
|
|
205
209
|
const repo = p.repo != null ? String(p.repo).trim() : null;
|
|
206
210
|
if (!repo || !repo.includes("/")) throw new Error("ci needs repo=<owner/repo>");
|
|
207
211
|
const selectors = ["scope", "run_id", "sha"].filter((k) => p[k] !== undefined);
|
|
208
|
-
|
|
212
|
+
// BOT-1685: scope=next accepts sha= as a live FILTER (like branch=/workflow=),
|
|
213
|
+
// not as a second selector — a wait armed BEFORE re-dispatching a run for a
|
|
214
|
+
// known commit then wakes on the NEXT terminal run for that commit rather than
|
|
215
|
+
// replaying an earlier terminal run (which the level-triggered `sha=` selector
|
|
216
|
+
// does by design). Exclude that filter sha from the single-selector count.
|
|
217
|
+
const shaIsNextFilter = p.scope === "next" && p.sha !== undefined;
|
|
218
|
+
if ((shaIsNextFilter ? selectors.length - 1 : selectors.length) !== 1) {
|
|
209
219
|
throw new Error("ci needs exactly one of scope=latest|next | run_id=<id> | sha=<sha>");
|
|
210
220
|
}
|
|
211
221
|
const hasFilter = p.branch !== undefined || p.workflow !== undefined;
|
|
@@ -229,7 +239,7 @@ const VALIDATORS = {
|
|
|
229
239
|
if (v === "") throw new Error(`ci ${key}= must be non-empty`);
|
|
230
240
|
return v;
|
|
231
241
|
};
|
|
232
|
-
return { repo, scope: "next", branch: filter("branch"), workflow: filter("workflow"), pr: null };
|
|
242
|
+
return { repo, scope: "next", branch: filter("branch"), workflow: filter("workflow"), sha: filter("sha"), pr: null };
|
|
233
243
|
}
|
|
234
244
|
throw new Error("ci scope must be 'latest' or 'next' (or use run_id=/sha=)");
|
|
235
245
|
}
|
|
@@ -804,6 +814,9 @@ function conditionMatchesSignal(condition, signal, waitSessionId) {
|
|
|
804
814
|
// branch is exact, workflow is the GitHub display name (case/space-insensitive).
|
|
805
815
|
const conclusion = signal.payload?.conclusion;
|
|
806
816
|
if (typeof conclusion !== "string" || conclusion.trim() === "") return false;
|
|
817
|
+
// BOT-1685: an optional sha filter pins scope=next to a commit, so a
|
|
818
|
+
// re-dispatch wait wakes only on the terminal run for that commit.
|
|
819
|
+
if (params.sha != null && signal.payload?.head_sha !== params.sha) return false;
|
|
807
820
|
if (params.branch != null && signal.payload?.head_branch !== params.branch) return false;
|
|
808
821
|
if (params.workflow != null) {
|
|
809
822
|
const wf = signal.payload?.workflow_name;
|
package/src/wait.mjs
CHANGED
|
@@ -106,15 +106,18 @@ CONDITIONS (TYPE:key=val,key=val — repeat for several; --any wakes on the fir
|
|
|
106
106
|
the others keep parking. Older servers broadcast without
|
|
107
107
|
the ranking (first-come). Needs cli >= 1.20.0.
|
|
108
108
|
ci:repo=<owner/repo>,{scope=latest,pr=<n> | run_id=<id> | sha=<sha>
|
|
109
|
-
| scope=next[,branch=<name>][,workflow=<name>]}
|
|
109
|
+
| scope=next[,sha=<sha>][,branch=<name>][,workflow=<name>]}
|
|
110
110
|
a PR's CI reaching a terminal conclusion (owner-scoped);
|
|
111
111
|
GitHub Actions or an external check_suite provider.
|
|
112
112
|
A repo with no CI feed is rejected (no_signal_source).
|
|
113
113
|
scope=next is LIVE-ONLY: it wakes on the first terminal
|
|
114
114
|
run delivered AFTER arm (post-merge / cron / scheduled),
|
|
115
|
-
never replays a finished run, and needs no run_id/
|
|
116
|
-
Optional
|
|
117
|
-
|
|
115
|
+
never replays a finished run, and needs no run_id/pr.
|
|
116
|
+
Optional sha= pins it to a commit (BOT-1685: arm before
|
|
117
|
+
re-dispatching a run for a known SHA so it wakes on the
|
|
118
|
+
NEW run, not an earlier terminal run for the same SHA);
|
|
119
|
+
branch= (exact) / workflow= (GitHub workflow DISPLAY
|
|
120
|
+
name, case-insensitive) narrow that first run too.
|
|
118
121
|
staging-green:repo=<owner/repo> verified automatic recovery of an enabled staging gate.
|
|
119
122
|
Wakes only after a complete non-vacuous workflow success;
|
|
120
123
|
manual resolutions, overrides, skipped, and neutral runs do not match.
|