@botbuddy/cli 1.59.7 → 1.59.9
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 +7 -0
- package/daemon-release/linux-x64.json +7 -0
- package/daemon-release/manifest.json +22 -0
- package/package.json +3 -1
- package/src/api.mjs +62 -0
- package/src/commands.mjs +6 -0
- package/src/daemon/approval-wait.mjs +126 -0
- package/src/daemon/bootstrap-resume.mjs +151 -0
- package/src/daemon/bootstrap.mjs +386 -0
- package/src/daemon/commands.mjs +478 -0
- package/src/daemon/enrollment.mjs +483 -0
- package/src/daemon/entrypoint.mjs +14 -0
- package/src/daemon/installer.mjs +438 -0
- package/src/daemon/profile-delivery.mjs +332 -0
- package/src/daemon/release.mjs +81 -0
- package/src/daemon/runtime-config.mjs +85 -0
- package/src/daemon/service.mjs +38 -0
- package/src/oauth-device.mjs +11 -0
- package/src/publish-native-guards.mjs +44 -0
- package/src/wait-core.mjs +63 -18
- package/src/wait.mjs +4 -1
package/src/wait-core.mjs
CHANGED
|
@@ -80,6 +80,7 @@ function managedAllocationFailureDetail(signal, condition) {
|
|
|
80
80
|
// own-session terminal branch because its wait_session_id differs) would otherwise match
|
|
81
81
|
// that raw event condition and exit an unrelated wait as an ordinary match (Codex P2).
|
|
82
82
|
export const WAIT_SUPERSEDED_SIGNAL_TYPE = "wait_superseded";
|
|
83
|
+
const HOST_APPROVAL_HOST_KEY_RE = /^bbh-[0-9a-f]{24}$/;
|
|
83
84
|
|
|
84
85
|
// The USER-ARMABLE spine signal_type allowlist, for the `event:` escape hatch. The full
|
|
85
86
|
// public.agent_signal_type enum is these PLUS the control types above
|
|
@@ -99,6 +100,11 @@ export const SIGNAL_TYPES = Object.freeze([
|
|
|
99
100
|
"snapshot",
|
|
100
101
|
"ticket_unblocked",
|
|
101
102
|
"stack_lease",
|
|
103
|
+
// BOT-2324: emitted only once the exact public host approval predicate is
|
|
104
|
+
// true (owner-bound active settings + policy + live tenant subscription).
|
|
105
|
+
// It is intentionally consumed through the existing raw `event:` escape
|
|
106
|
+
// hatch rather than adding another parallel wait grammar.
|
|
107
|
+
"host_approval",
|
|
102
108
|
]);
|
|
103
109
|
|
|
104
110
|
const LOCK_SUBTYPES = Object.freeze(["playwright_lane", "vite_port", "backend_port", "supabase_local"]);
|
|
@@ -281,9 +287,10 @@ const VALIDATORS = {
|
|
|
281
287
|
// BOT-1507 — scope=next adds a fourth, LIVE-ONLY selector: it names no run or PR
|
|
282
288
|
// (they must already exist at arm time) and wakes on the first terminal CI run for
|
|
283
289
|
// the repo delivered AFTER the wait was armed — the post-merge / cron train case.
|
|
284
|
-
// Optional branch=/workflow=
|
|
285
|
-
//
|
|
286
|
-
//
|
|
290
|
+
// Optional branch=/workflow=/workflow_id=/workflow_path= filters narrow that
|
|
291
|
+
// first run; branch and stable workflow identities are valid only with
|
|
292
|
+
// scope=next. The legacy display-name filter also pins a level-triggered
|
|
293
|
+
// sha= recovery to one historical workflow.
|
|
287
294
|
// BOT-1685 — scope=next also accepts an optional sha= FILTER (distinct from the
|
|
288
295
|
// level-triggered sha= selector): a wait armed BEFORE re-dispatching a run for a
|
|
289
296
|
// known commit then wakes on the NEXT terminal run for THAT commit, instead of
|
|
@@ -303,9 +310,13 @@ const VALIDATORS = {
|
|
|
303
310
|
}
|
|
304
311
|
const hasBranchFilter = p.branch !== undefined;
|
|
305
312
|
const hasWorkflowFilter = p.workflow !== undefined;
|
|
313
|
+
const hasWorkflowIdFilter = p.workflow_id !== undefined;
|
|
314
|
+
const hasWorkflowPathFilter = p.workflow_path !== undefined;
|
|
306
315
|
if (p.scope !== undefined) {
|
|
307
316
|
if (p.scope === "latest") {
|
|
308
|
-
if (hasBranchFilter || hasWorkflowFilter
|
|
317
|
+
if (hasBranchFilter || hasWorkflowFilter || hasWorkflowIdFilter || hasWorkflowPathFilter) {
|
|
318
|
+
throw new Error("ci branch=/workflow=/workflow_id=/workflow_path= are only valid with scope=next");
|
|
319
|
+
}
|
|
309
320
|
if (p.pr === undefined || !/^\d+$/.test(String(p.pr).trim())) {
|
|
310
321
|
throw new Error("ci scope=latest needs pr=<number>");
|
|
311
322
|
}
|
|
@@ -323,12 +334,23 @@ const VALIDATORS = {
|
|
|
323
334
|
if (v === "") throw new Error(`ci ${key}= must be non-empty`);
|
|
324
335
|
return v;
|
|
325
336
|
};
|
|
326
|
-
|
|
337
|
+
const workflowId = filter("workflow_id");
|
|
338
|
+
const workflowPath = filter("workflow_path");
|
|
339
|
+
return {
|
|
340
|
+
repo,
|
|
341
|
+
scope: "next",
|
|
342
|
+
branch: filter("branch"),
|
|
343
|
+
workflow: filter("workflow"),
|
|
344
|
+
...(workflowId == null ? {} : { workflowId }),
|
|
345
|
+
...(workflowPath == null ? {} : { workflowPath }),
|
|
346
|
+
sha: filter("sha"),
|
|
347
|
+
pr: null,
|
|
348
|
+
};
|
|
327
349
|
}
|
|
328
350
|
throw new Error("ci scope must be 'latest' or 'next' (or use run_id=/sha=)");
|
|
329
351
|
}
|
|
330
|
-
if (hasBranchFilter || (hasWorkflowFilter && p.sha === undefined)) {
|
|
331
|
-
throw new Error("ci branch=
|
|
352
|
+
if (hasBranchFilter || hasWorkflowIdFilter || hasWorkflowPathFilter || (hasWorkflowFilter && p.sha === undefined)) {
|
|
353
|
+
throw new Error("ci branch=/workflow_id=/workflow_path= are only valid with scope=next; legacy workflow= also accepts an exact sha selector");
|
|
332
354
|
}
|
|
333
355
|
if (p.run_id !== undefined) {
|
|
334
356
|
if (!String(p.run_id).trim()) throw new Error("ci run_id must be non-empty");
|
|
@@ -337,7 +359,13 @@ const VALIDATORS = {
|
|
|
337
359
|
if (!String(p.sha).trim()) throw new Error("ci sha must be non-empty");
|
|
338
360
|
const workflow = p.workflow === undefined ? null : String(p.workflow).trim();
|
|
339
361
|
if (p.workflow !== undefined && workflow === "") throw new Error("ci workflow= must be non-empty");
|
|
340
|
-
return {
|
|
362
|
+
return {
|
|
363
|
+
repo,
|
|
364
|
+
scope: "sha",
|
|
365
|
+
sha: String(p.sha).trim(),
|
|
366
|
+
workflow,
|
|
367
|
+
pr: p.pr != null ? String(p.pr).trim() : null,
|
|
368
|
+
};
|
|
341
369
|
},
|
|
342
370
|
"staging-green"(p) {
|
|
343
371
|
const repo = p.repo != null ? String(p.repo).trim() : null;
|
|
@@ -406,7 +434,15 @@ const VALIDATORS = {
|
|
|
406
434
|
if (!SIGNAL_TYPES.includes(p.type)) {
|
|
407
435
|
throw new Error(`event type must be a known signal_type (${SIGNAL_TYPES.join(", ")})`);
|
|
408
436
|
}
|
|
409
|
-
|
|
437
|
+
const subject = p.subject !== undefined ? String(p.subject).trim() : null;
|
|
438
|
+
// BOT-2324: unlike the generic raw escape hatch, this approval condition
|
|
439
|
+
// has one narrow public identity grammar. The service independently checks
|
|
440
|
+
// it for direct JSON callers; keeping the CLI equally strict prevents an
|
|
441
|
+
// impossible-to-match wait from being armed locally.
|
|
442
|
+
if (p.type === "host_approval" && (!subject || !HOST_APPROVAL_HOST_KEY_RE.test(subject))) {
|
|
443
|
+
throw new Error("event:type=host_approval needs subject=<canonical bbh- host key>");
|
|
444
|
+
}
|
|
445
|
+
return { type: p.type, subject };
|
|
410
446
|
},
|
|
411
447
|
};
|
|
412
448
|
|
|
@@ -999,7 +1035,7 @@ export function formatUnknownWorkflowGuidance(detail, candidates) {
|
|
|
999
1035
|
const named = Array.isArray(candidates) ? candidates.filter((c) => typeof c === "string" && c.trim() !== "") : [];
|
|
1000
1036
|
if (named.length === 0) return lines;
|
|
1001
1037
|
lines.push("closest workflow names this repo has actually reported:");
|
|
1002
|
-
for (const name of named) lines.push(` workflow=${name}`);
|
|
1038
|
+
for (const name of named) lines.push(` ${name.includes("=") ? name : `workflow=${name}`}`);
|
|
1003
1039
|
return lines;
|
|
1004
1040
|
}
|
|
1005
1041
|
|
|
@@ -1138,16 +1174,20 @@ function conditionMatchesSignal(condition, signal, waitSessionId) {
|
|
|
1138
1174
|
if (params.scope === "run_id") return String(signal.payload?.run_id) === params.runId;
|
|
1139
1175
|
if (params.scope === "sha") {
|
|
1140
1176
|
if (signal.payload?.head_sha !== params.sha) return false;
|
|
1141
|
-
if (params.workflow
|
|
1142
|
-
|
|
1143
|
-
|
|
1177
|
+
if (params.workflow != null) {
|
|
1178
|
+
const workflow = signal.payload?.workflow_name;
|
|
1179
|
+
if (typeof workflow !== "string" || workflow.trim().toLowerCase() !== params.workflow.trim().toLowerCase()) return false;
|
|
1180
|
+
}
|
|
1181
|
+
if (params.workflowId != null && String(signal.payload?.workflow_id ?? "").trim() !== params.workflowId) return false;
|
|
1182
|
+
if (params.workflowPath != null && String(signal.payload?.workflow_path ?? "").trim() !== params.workflowPath) return false;
|
|
1183
|
+
return true;
|
|
1144
1184
|
}
|
|
1145
1185
|
if (params.scope === "next") {
|
|
1146
1186
|
// BOT-1507 — live-only: the first TERMINAL run for the repo. Never keys on
|
|
1147
1187
|
// subject_key (it is run:<id> for post-merge runs and owner/repo#n for PR
|
|
1148
1188
|
// runs — both are legitimate wakes); a null/empty conclusion is an
|
|
1149
1189
|
// in-progress signal and never terminal. Optional branch/workflow filters:
|
|
1150
|
-
// branch
|
|
1190
|
+
// branch/path/id are exact; the display name is case/space-insensitive.
|
|
1151
1191
|
const conclusion = signal.payload?.conclusion;
|
|
1152
1192
|
if (typeof conclusion !== "string" || conclusion.trim() === "") return false;
|
|
1153
1193
|
// BOT-1685: an optional sha filter pins scope=next to a commit, so a
|
|
@@ -1160,6 +1200,8 @@ function conditionMatchesSignal(condition, signal, waitSessionId) {
|
|
|
1160
1200
|
return false;
|
|
1161
1201
|
}
|
|
1162
1202
|
}
|
|
1203
|
+
if (params.workflowId != null && String(signal.payload?.workflow_id ?? "").trim() !== params.workflowId) return false;
|
|
1204
|
+
if (params.workflowPath != null && String(signal.payload?.workflow_path ?? "").trim() !== params.workflowPath) return false;
|
|
1163
1205
|
return true;
|
|
1164
1206
|
}
|
|
1165
1207
|
// scope=latest: the PR's run (by subject owner/repo#pr or payload pr_number).
|
|
@@ -1237,10 +1279,13 @@ function conditionMatchesSignal(condition, signal, waitSessionId) {
|
|
|
1237
1279
|
// wake the wrong workspace. This centralizes what BOT-1247's `unblocked` matcher did
|
|
1238
1280
|
// per-type (that per-type check stays as belt-and-braces). Tenantless signals
|
|
1239
1281
|
// (host-shared lock_availability/container_capacity, owner/recipient-scoped
|
|
1240
|
-
// chat/pr_state/test_run/guidance) are NEVER blocked — they keep today's semantics
|
|
1241
|
-
// for any session (AC-5). `
|
|
1242
|
-
//
|
|
1243
|
-
//
|
|
1282
|
+
// chat/pr_state/test_run/guidance) are NEVER blocked — they keep today's semantics
|
|
1283
|
+
// for any session (AC-5). `host_approval` is the deliberate exception: it is
|
|
1284
|
+
// owner-visible AND tenant-bound, so the relay stamps scope=owner_tenant and we
|
|
1285
|
+
// retain this tenant fence after its owner gate. `sessionTenant === undefined`
|
|
1286
|
+
// opts a caller out of the central guard: a pure unit test asserting a per-type
|
|
1287
|
+
// matcher in isolation. The CLI always threads the server-resolved tenant, so
|
|
1288
|
+
// the guard is live for every real wait.
|
|
1244
1289
|
export function signalTenantBlocked(signal, sessionTenant) {
|
|
1245
1290
|
if (sessionTenant === undefined) return false;
|
|
1246
1291
|
if (signal.tenant_id == null) return false;
|
package/src/wait.mjs
CHANGED
|
@@ -153,7 +153,10 @@ CONDITIONS (TYPE:key=val,key=val — repeat for several; --any wakes on the fir
|
|
|
153
153
|
Wakes only after a complete non-vacuous workflow success;
|
|
154
154
|
manual resolutions, overrides, skipped, and neutral runs do not match.
|
|
155
155
|
event:type=<signal_type>[,subject=<key>]
|
|
156
|
-
raw signal-spine match (escape hatch)
|
|
156
|
+
raw signal-spine match (escape hatch). Host setup uses
|
|
157
|
+
event:type=host_approval,subject=bbh-<24 lowercase hex>;
|
|
158
|
+
it wakes only for the authenticated owner's one bound
|
|
159
|
+
tenant after current server-side approval revalidation.
|
|
157
160
|
|
|
158
161
|
OPTIONS
|
|
159
162
|
--any wake on the first matching condition (default; only mode in v1)
|