@botbuddy/cli 1.63.38 → 1.63.40
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/daemon/bin/darwin-arm64/botbuddy-daemon +0 -0
- package/daemon/bin/linux-x64/botbuddy-daemon +0 -0
- package/daemon-release/darwin-arm64.json +4 -4
- package/daemon-release/linux-x64.json +4 -4
- package/daemon-release/manifest.json +5 -5
- package/package.json +1 -1
- package/src/wait-core.mjs +75 -3
- package/src/wait.mjs +14 -3
|
Binary file
|
|
Binary file
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
|
-
"release_version": "1.63.
|
|
3
|
-
"source_commit": "
|
|
4
|
-
"source_sha256": "
|
|
2
|
+
"release_version": "1.63.40",
|
|
3
|
+
"source_commit": "6a30ed1a16dc6a97351c3cea5cc2d03918dbe0fc",
|
|
4
|
+
"source_sha256": "de12a3e513169b3a152a617b48c7e9c4c25c30e47b549854994744c369268ff1",
|
|
5
5
|
"target": "darwin-arm64",
|
|
6
|
-
"sha256": "
|
|
6
|
+
"sha256": "9732532ac31d3e184fedafe19089b5efa938b2f3c2c4952cbc95866d86f4b8ae"
|
|
7
7
|
}
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
|
-
"release_version": "1.63.
|
|
3
|
-
"source_commit": "
|
|
4
|
-
"source_sha256": "
|
|
2
|
+
"release_version": "1.63.40",
|
|
3
|
+
"source_commit": "6a30ed1a16dc6a97351c3cea5cc2d03918dbe0fc",
|
|
4
|
+
"source_sha256": "de12a3e513169b3a152a617b48c7e9c4c25c30e47b549854994744c369268ff1",
|
|
5
5
|
"target": "linux-x64",
|
|
6
|
-
"sha256": "
|
|
6
|
+
"sha256": "8abdd484047020cb2d96a7adc2a2bacb3b860bafaaf0f56e517eb844bb8550a1"
|
|
7
7
|
}
|
|
@@ -1,21 +1,21 @@
|
|
|
1
1
|
{
|
|
2
2
|
"schema_version": 1,
|
|
3
|
-
"release_version": "1.63.
|
|
4
|
-
"source_commit": "
|
|
5
|
-
"source_sha256": "
|
|
3
|
+
"release_version": "1.63.40",
|
|
4
|
+
"source_commit": "6a30ed1a16dc6a97351c3cea5cc2d03918dbe0fc",
|
|
5
|
+
"source_sha256": "de12a3e513169b3a152a617b48c7e9c4c25c30e47b549854994744c369268ff1",
|
|
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": "
|
|
11
|
+
"sha256": "9732532ac31d3e184fedafe19089b5efa938b2f3c2c4952cbc95866d86f4b8ae",
|
|
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": "
|
|
18
|
+
"sha256": "8abdd484047020cb2d96a7adc2a2bacb3b860bafaaf0f56e517eb844bb8550a1",
|
|
19
19
|
"mode": "0755"
|
|
20
20
|
}
|
|
21
21
|
}
|
package/package.json
CHANGED
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
|
-
|
|
588
|
-
|
|
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
|
},
|