@botbuddy/cli 1.61.9 → 1.62.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.
@@ -1,7 +1,7 @@
1
1
  {
2
- "release_version": "1.61.9",
3
- "source_commit": "af5fbce708449fe5cb598f4f64523fb3696dd1a0",
4
- "source_sha256": "738e4a6b0da849271e515488978880eb40baab42ba708ea29bd3e0cba06ac0d8",
2
+ "release_version": "1.62.0",
3
+ "source_commit": "d239018a4df5137e9b013792274dbfe6316f46e1",
4
+ "source_sha256": "cd5a5bd1f0a409d0a79a3db600b218fccca101102df138d7414766a3bba1b16a",
5
5
  "target": "darwin-arm64",
6
- "sha256": "a9ea783c9eb264b1ddd03f6d1fd1e5b78df479e3eab6d13f2026ea1085101923"
6
+ "sha256": "7b0ddb31fbbf568503a4d079badbae185cac44fe27a5ea2a0823d1ae83d52b2b"
7
7
  }
@@ -1,7 +1,7 @@
1
1
  {
2
- "release_version": "1.61.9",
3
- "source_commit": "af5fbce708449fe5cb598f4f64523fb3696dd1a0",
4
- "source_sha256": "738e4a6b0da849271e515488978880eb40baab42ba708ea29bd3e0cba06ac0d8",
2
+ "release_version": "1.62.0",
3
+ "source_commit": "d239018a4df5137e9b013792274dbfe6316f46e1",
4
+ "source_sha256": "cd5a5bd1f0a409d0a79a3db600b218fccca101102df138d7414766a3bba1b16a",
5
5
  "target": "linux-x64",
6
- "sha256": "09a36d5822c4c28a0ded7fe0a6bf079b58f4aa5cb80181971e8448ca3eab3804"
6
+ "sha256": "4c5065a8cc25ef6c01967ec96f109aa39e23799ef1043a4fd942780b0adb6c95"
7
7
  }
@@ -1,21 +1,21 @@
1
1
  {
2
2
  "schema_version": 1,
3
- "release_version": "1.61.9",
4
- "source_commit": "af5fbce708449fe5cb598f4f64523fb3696dd1a0",
5
- "source_sha256": "738e4a6b0da849271e515488978880eb40baab42ba708ea29bd3e0cba06ac0d8",
3
+ "release_version": "1.62.0",
4
+ "source_commit": "d239018a4df5137e9b013792274dbfe6316f46e1",
5
+ "source_sha256": "cd5a5bd1f0a409d0a79a3db600b218fccca101102df138d7414766a3bba1b16a",
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": "a9ea783c9eb264b1ddd03f6d1fd1e5b78df479e3eab6d13f2026ea1085101923",
11
+ "sha256": "7b0ddb31fbbf568503a4d079badbae185cac44fe27a5ea2a0823d1ae83d52b2b",
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": "09a36d5822c4c28a0ded7fe0a6bf079b58f4aa5cb80181971e8448ca3eab3804",
18
+ "sha256": "4c5065a8cc25ef6c01967ec96f109aa39e23799ef1043a4fd942780b0adb6c95",
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.61.9",
3
+ "version": "1.62.0",
4
4
  "description": "BotBuddy \u2014 Swarm coordination CLI for multi-agent workflows",
5
5
  "type": "module",
6
6
  "bin": {
package/src/commands.mjs CHANGED
@@ -56,6 +56,23 @@ export function shouldCheckForUpdates(argv) {
56
56
  return true;
57
57
  }
58
58
 
59
+ // BOT-2715: local commands with their own --help. `bb help <command>` (and
60
+ // `bb <command> help`) print that help instead of asking MCP discovery for a tool
61
+ // of the same name. MCP tools are snake_case, so no name here can shadow one.
62
+ export const LOCAL_HELP_COMMANDS = Object.freeze([
63
+ "wait", "stack", "pw", "docker", "run", "mcp", "login", "setup", "doctor", "host",
64
+ "staging-pool", "daemon", "evidence", "usage", "telemetry", "allocation",
65
+ ]);
66
+ const HELP_WORDS = new Set(["help", "--help", "-h"]);
67
+
68
+ export function routeHelpArgv(argv) {
69
+ const [command, ...args] = argv;
70
+ if (HELP_WORDS.has(command) && args.length === 1 && LOCAL_HELP_COMMANDS.includes(args[0])) return [args[0], "--help"];
71
+ // pw parses its own argv (`bb pw help` is its lane grammar's business).
72
+ if (args.length === 1 && args[0] === "help" && command !== "pw" && LOCAL_HELP_COMMANDS.includes(command)) return [command, "--help"];
73
+ return argv;
74
+ }
75
+
59
76
  const TICKET_KEY_RE = /^[A-Za-z][A-Za-z0-9]{1,9}-\d{1,6}$/;
60
77
 
61
78
  export function validateBootstrapLinkage(ticketId, noTicketReason) {
@@ -73,7 +90,7 @@ export async function run(argv, {
73
90
  bootstrap = cmdBootstrap,
74
91
  managedAllocation = cmdManagedAllocation,
75
92
  } = {}) {
76
- const [command, ...args] = argv;
93
+ const [command, ...args] = routeHelpArgv(argv);
77
94
  // BOT-2069: `bootstrap close` can retire a live server session. Parse its
78
95
  // own arguments before loading config, checking npm, reading the runtime
79
96
  // cache, or resolving any credential. Help and malformed invocations return
@@ -84,7 +101,7 @@ export async function run(argv, {
84
101
  }
85
102
 
86
103
  await load();
87
- if (shouldCheckForUpdates(argv)) await warnStale({ version: VERSION });
104
+ if (shouldCheckForUpdates([command, ...args])) await warnStale({ version: VERSION });
88
105
 
89
106
  switch (command) {
90
107
  case "start": return cmdStart(args);
@@ -184,19 +201,28 @@ export function cmdHelp(log = console.log) {
184
201
  log(`${bold("bb")} ${dim(`v${VERSION}`)} — Swarm coordination CLI
185
202
 
186
203
  ${dim(`Run everything as ${cyan("bb <command>")} — the short, preferred alias. ${cyan("botbuddy <command>")} is the identical long form.`)}
187
-
188
- ${bold("USAGE")}
189
- bb start Start BotBuddy (login + codex server + bridge)
190
- bb bootstrap --session-id <uuid> (--ticket-id <BOT-n> | --no-ticket-reason <reason>)
191
- Internal launcher contract: persist a wait-capable
192
- harness session without printing its secret
193
-
194
- ${bold("OPTIONS")}
195
- start [options]
196
- -p, --port <port> WebSocket port (default: 4500)
197
- -r, --repo <path> Repository path (default: cwd)
198
- -m, --model <model> Default model (default: gpt-5.4)
199
- --no-server Don't auto-start codex app-server
204
+ ${dim(`Detailed help: ${cyan("bb help <command>")} or ${cyan("bb <command> --help")} for ${LOCAL_HELP_COMMANDS.join(", ")}; ${cyan("bb test --help")} for test.`)}
205
+
206
+ ${bold("COMMON TASKS")}
207
+ bb setup sign in and cache this worktree's agent session (once)
208
+ bb doctor --fix repair a stale or rejected agent session
209
+ bb wait 'timer:duration=1800' wait 30 min (the only allowed way to wait for time)
210
+ bb wait 'ci:repo=<owner/repo>,scope=latest,pr=<n>' --heartbeat
211
+ wait for CI on a PR's current head to finish
212
+ bb wait 'pr-review:repo=<owner/repo>,pr=<n>,sha=<head>' --heartbeat
213
+ wait for review activity on the head you just pushed
214
+ bb wait 'unblocked:BOT-123' wait for a ticket's last blocker to clear
215
+ bb wait result --from-file "$wait_artifact" --exit-code "$wait_exit"
216
+ read a redirected wait's receipt once it exits
217
+ (the full wrapper is in bb help wait → EXAMPLES)
218
+ bb stack run --repo botbuddy-web --ticket BOT-123 -- pnpm test:integration
219
+ run a batch on a leased, isolated local Supabase stack
220
+ bb test unit run a configured test lane as a tracked test run
221
+ bb pw --environment local 3 open http://localhost:5173
222
+ drive Playwright lane 3 (lock it first)
223
+ bb call get_staging_gate --repo bot-buddy/botbuddy-webapp
224
+ call any BotBuddy MCP tool (${cyan("bb help <tool>")} for its args)
225
+ More wait recipes: ${cyan("bb help wait")} → EXAMPLES.
200
226
 
201
227
  ${bold("AUTH")}
202
228
  login [--no-browser] [--tenant <slug>] [--device | --loopback] [--caller <name>] [--caller-harness <h>] [--caller-ticket <ref>]
@@ -221,6 +247,7 @@ ${bold("MCP CONFIG KEY")}
221
247
  ${bold("TOOLS")}
222
248
  help --tools List every BotBuddy tool
223
249
  help <tool> Show one tool's arguments
250
+ help <command> Detailed help for a command named at the top (same as <command> --help)
224
251
  help test Safe test-lane launch and recovery guidance
225
252
  help symptom <name> Diagnose a known block without source-diving
226
253
  call <tool> [--key value] Invoke any tool
@@ -228,49 +255,37 @@ ${bold("TOOLS")}
228
255
  call <tool> --pin <slug> Pin the connection tenant (a user token needs one;
229
256
  default: BOTBUDDY_TENANT → repo profile → sole tenant)
230
257
 
231
- ${bold("STACK LEASES")}
258
+ ${bold("AGENT WAITS")} (${cyan("bb help wait")} — every condition, with examples)
259
+ wait [--any] <condition>... [options]
260
+ Wait once for a pushed BotBuddy signal; one JSON receipt on exit
261
+ wait List this worktree's saved waits (after a restart)
262
+ wait result [<local_wait_id>] [--from-file <path> --exit-code <n>]
263
+ Read a terminal receipt without re-arming
264
+ wait resume|cancel|acknowledge <local_wait_id>
265
+ Recover, stop, or mark read one saved wait
266
+
267
+ ${bold("STACK LEASES")} (${cyan("bb help stack")})
268
+ stack run [options] -- <command> Own one Helper-backed test/fix batch end-to-end (preferred)
232
269
  stack up [options] Request a batch-scoped local stack lease; park if full; hold it active
233
270
  stack status <lease_id> Show a lease's state + connection
234
271
  stack touch <lease_id> Bump the lease idle clock
235
- stack done <lease_id> Release (done) a lease — reap the stack
236
- stack help Full stack usage + exit codes
272
+ stack done <lease_id> Release (done) a lease — idle it for re-adoption (--reap tears it down)
273
+ stack cancel <lease_id> Give a queued lease's place up
237
274
  allocation status <id> <generation> <target_revision> Verify managed placement before Compose
238
275
 
239
- ${bold("DOCKER HYGIENE")}
240
- docker hygiene [options] Dry-run/apply safe stale Supabase cleanup on OrbStack
241
- docker preflight [options] Refuse a managed-stack start before network pressure exhausts the host
242
- docker help Full explicit-context safety contract
243
-
244
- ${bold("GO DAEMON")}
245
- daemon <render|install|status|enroll|bootstrap> …
246
- Verify and manage the native Go daemon service
247
-
248
- ${bold("DURABLE WORKLOADS")}
249
- run --session-id <id> --environment <env> -- <command>
250
- Launch a receipt-bearing command under a detached owner
251
- run --foreground --kind <kind> --environment <env> -- <command>
252
- Run locally with a durable lifecycle outbox
253
- telemetry <status|replay|doctor>
254
- Inspect, replay, or gate durable lifecycle delivery
276
+ ${bold("TEST LANES")}
277
+ test <lane> [--ticket <KEY>] [--pr <n>] [--wait] [-- <extra args>]
278
+ Run a lane from .botbuddy/lanes.json as a tracked test run
279
+ test list List the configured lanes (${cyan("bb test --help")})
255
280
  test-ingest --file <payload.json|-> [--tenant <slug>]
256
- Upload a test-run report with this worktree's agent session
257
-
258
- ${bold("USAGE INGEST")}
259
- usage sync [--since 2d] [--provider claude|codex|all] [--file <transcript>] [--dry-run] [--quiet]
260
- Send local Claude/Codex token usage (counters only, no prompts)
261
- usage status Last sync runs and tracked transcripts
281
+ Upload a test-run report with this worktree's agent session
262
282
 
263
- ${bold("AGENT WAITS")}
264
- wait [--any] <condition>... [options]
265
- Wait once for a pushed BotBuddy signal
266
-
267
- ${bold("BROWSER LANES")}
283
+ ${bold("BROWSER LANES")} (${cyan("bb pw --help")})
268
284
  pw <lane> <verb> [args…] Drive a lock-gated Playwright lane
269
285
  (also the short ${cyan("pw")} binary, or ${cyan("bb-pw")} alias)
270
286
  pw <lane> verdict pass|fail Record the session's acceptance verdict
271
287
  (sessions report live timing/results; start one
272
288
  with --environment <env> or BB_PW_ENV)
273
- pw --help Show lane usage
274
289
 
275
290
  ${bold("STAGING SIGN-IN")} (BOT-2084)
276
291
  pw --environment staging <lane> auth --persona <alias>
@@ -284,10 +299,56 @@ ${bold("STAGING SIGN-IN")} (BOT-2084)
284
299
  Human only, any directory (${cyan("bb staging-pool --help")})
285
300
  help symptom staging-pool Every refusal code with its cause and next command
286
301
 
287
- ${bold("OTHER")}
288
- locks -m [--host name] Reserve typed local resources, including Playwright MCP lanes
302
+ ${bold("LOCKS")}
303
+ locks -m [--host name] Reserve typed local resources, including Playwright MCP lanes
304
+ lock <resource_name> <type> Acquire one named lock (acquire_lock)
305
+ unlock <resource_name> Release one named lock (release_lock)
306
+
307
+ ${bold("DURABLE WORKLOADS")} (${cyan("bb help run")})
308
+ run --session-id <id> --environment <env> -- <command>
309
+ Launch a receipt-bearing command under a detached owner
310
+ run --foreground --kind <kind> --environment <env> -- <command>
311
+ Run locally with a durable lifecycle outbox
312
+ run inspect|cancel <run_id> Read or stop one command run
313
+ telemetry <status|replay|doctor>
314
+ Inspect, replay, or gate durable lifecycle delivery
315
+
316
+ ${bold("EVIDENCE")} (${cyan("bb help evidence")})
289
317
  evidence require <file> Record an exact-head evidence disposition → botbuddy/ready
290
318
  evidence publish <file> Publish a local evidence statement → botbuddy/ready check-run
319
+
320
+ ${bold("HOST + DOCKER")}
321
+ docker hygiene [options] Dry-run/apply safe stale Supabase cleanup on OrbStack
322
+ docker preflight [options] Refuse a managed-stack start before network pressure exhausts the host
323
+ docker help Full explicit-context safety contract
324
+ daemon <render|install|status|config-render|enroll|bootstrap> …
325
+ Verify and manage the native Go daemon service (${cyan("bb help daemon")})
326
+
327
+ ${bold("USAGE INGEST")}
328
+ usage sync [--since 2d] [--provider claude|codex|all] [--file <transcript>] [--dry-run] [--quiet]
329
+ Send local Claude/Codex token usage (counters only, no prompts)
330
+ usage status Last sync runs and tracked transcripts
331
+
332
+ ${bold("SHORTCUTS")} (thin wrappers over ${cyan("bb call <tool>")})
333
+ heartbeat [<current task>] Refresh this session's heartbeat (heartbeat)
334
+ agents List agents (list_agents)
335
+ resources List resources and locks (list_resources)
336
+ tasks Read the task board (botbuddy://tasks)
337
+ task create <title> [-d <desc>] [-p <priority>] | task claim|done <task_id>
338
+ Create, claim or complete a task
339
+ hours [derive | set '<json>'] Read, derive or set working hours
340
+ browse <agents|tasks|resources|activity|settings> Read a botbuddy:// resource
341
+
342
+ ${bold("CODEX BRIDGE")}
343
+ start [-p <port>] [-r <repo>] [-m <model>] [--no-server]
344
+ Start BotBuddy (login + codex app-server + bridge); defaults:
345
+ port 4500, repo = cwd, model gpt-5.4
346
+ codex bridge Legacy alias of start
347
+ bootstrap --session-id <uuid> (--ticket-id <BOT-n> | --no-ticket-reason <reason>)
348
+ Internal launcher contract: persist a wait-capable
349
+ harness session without printing its secret
350
+
351
+ ${bold("OTHER")}
291
352
  update Install the latest @botbuddy/cli in the background via npm
292
353
  help Show this help
293
354
  version Show version
@@ -433,7 +494,7 @@ async function cmdToolHelp(args) {
433
494
  const res = await fetch(discoveryUrlFor(SERVER_URL, tool));
434
495
  if (!res.ok) {
435
496
  die(res.status === 404
436
- ? `Unknown tool: ${tool}. Run ${cyan("bb help --tools")} to list them.`
497
+ ? `Unknown tool: ${tool}. Run ${cyan("bb help --tools")} to list tools, or ${cyan("bb help")} for CLI commands.`
437
498
  : `Discovery failed: ${res.status} ${res.statusText}`);
438
499
  }
439
500
  console.log(formatDiscovery(await res.json(), { tool }));
@@ -468,7 +529,10 @@ ${bold("WHEN TEST TELEMETRY IS UNAVAILABLE")}
468
529
  ${bold("WAITING")}
469
530
  Use ${cyan("bb wait 'test-run:id=<id>' --heartbeat")} only when the launch output
470
531
  contains a real test-run ID. Do not replace a missing lifecycle signal with a
471
- timer or status-polling loop.`;
532
+ timer or status-polling loop.
533
+
534
+ ${bold("LANE COMMAND")}
535
+ ${cyan("bb test --help")} lists the flags; ${cyan("bb test list")} shows this repo's lanes.`;
472
536
 
473
537
  if (topic === "symptom" || topic === "symptoms") return `${bold("bb help symptom <name>")} — supported recovery topics
474
538
 
package/src/wait-core.mjs CHANGED
@@ -459,6 +459,9 @@ const VALIDATORS = {
459
459
  },
460
460
  };
461
461
 
462
+ // BOT-2715: the condition types `bb wait --help` must document (pinned by a test).
463
+ export const CONDITION_TYPES = Object.freeze(Object.keys(VALIDATORS));
464
+
462
465
  // pr-state / pr-review share a target grammar. The spine subject_key is
463
466
  // `owner/repo#number` (github-webhook / pr_state read model convention), so a
464
467
  // condition names an owner/repo and, optionally, a PR number. Accept three forms:
package/src/wait.mjs CHANGED
@@ -49,14 +49,32 @@ export function shouldRenewCachedSession(error, credentialSource, attempts) {
49
49
  return isRejectedCachedSession(error, credentialSource, attempts);
50
50
  }
51
51
 
52
- export const HELP = `botbuddy wait — one wait command instead of a polling loop (BOT-989)
52
+ export const HELP = `bb wait — one wait command instead of a polling loop (BOT-989)
53
+
54
+ Park until a condition holds, then exit ONCE with one JSON receipt on stdout. Zero polling
55
+ and zero tokens while parked. Every wait — time included — is a bb wait: never sleep loops,
56
+ until/while polling, watch, or gh run watch. Full guide: docs/agent-wait.md.
53
57
 
54
58
  USAGE
55
- botbuddy wait [--any] <condition>... [options]
59
+ bb wait [--any] <condition>... [options] arm a wait (also: bb help wait)
60
+ bb wait <subcommand> … inspect or recover a saved wait (SUBCOMMANDS)
61
+ (botbuddy wait … is the identical long form.)
62
+
63
+ Pick the condition that proves the state your NEXT action needs, not a correlated proxy:
64
+ CI finished on the current PR head ......... ci:…,scope=latest,pr=<n> (or exact sha=)
65
+ review activity (Codex, a human) ........... pr-review:… (arm BEFORE requesting it)
66
+ PR merged/closed/conflict/ready flip ........ pr-state:…
67
+ your merged PR reached staging ............. staging-release:…
68
+ staging recovered after an incident ........ staging-green:…
69
+ a ticket's last blocker cleared ............ unblocked:<KEY>
70
+ a named bb stack run finished .............. stack-run:id=<UUID>
71
+ Docker capacity freed after a refusal ...... capacity:… (copy the refusal's own condition)
72
+ a fixed time passed ........................ timer:…
56
73
 
57
74
  CONDITIONS (TYPE:key=val,key=val — repeat for several; --any wakes on the first)
58
- timer:duration=<seconds> wake after N seconds (DB-clocked server-side)
59
- timer:deadline=<ISO-8601> wake at an absolute time
75
+ timer:duration=<seconds> | timer:deadline=<ISO-8601>
76
+ wake after N seconds (DB-clocked server-side) or at an
77
+ absolute time. Exempt from the 2 h --timeout minimum.
60
78
  chat:channel=<id>|* new Direct Chat message (or * = any channel you are in;
61
79
  you are never woken by your own messages). Level-
62
80
  triggered at arm: an unread reply from another
@@ -65,6 +83,7 @@ CONDITIONS (TYPE:key=val,key=val — repeat for several; --any wakes on the fir
65
83
  so send-then-arm never misses a fast reply. Respects
66
84
  your read marker (mark_direct_chat_read) and your own
67
85
  last message; a sibling session's reply still wakes.
86
+ include_self= is reserved (accepted, no effect yet).
68
87
  lock:subtype=<t>,host=<h>[,slot=<s>][,claim=true]
69
88
  a resource lock becomes available
70
89
  subtype ∈ playwright_lane|vite_port|backend_port|supabase_local
@@ -82,9 +101,10 @@ CONDITIONS (TYPE:key=val,key=val — repeat for several; --any wakes on the fir
82
101
  in the repo; or pass pr=<owner/repo#n> in one value. A
83
102
  conflict/clean flip sets payload.mergeable_changed. The
84
103
  payload carries head_ref_oid; sha=<head> (7-40 hex; either
85
- side may be an abbreviated prefix) wakes only on signals for that head, so a
86
- previous head's check result can't wake you. A push alone
87
- emits nothing — the head rides on the next transition.
104
+ side may be an abbreviated prefix) wakes only on signals for
105
+ that head, so a previous head's check result can't wake you.
106
+ A push alone emits nothing — the head rides on the next
107
+ transition.
88
108
  pr-review:repo=<owner/repo>[,pr=<n>][,reviewer=<login>][,sha=<commit>]
89
109
  a submitted review, a new review comment, a thread
90
110
  resolved/re-opened (payload carries the unresolved-thread
@@ -96,27 +116,62 @@ CONDITIONS (TYPE:key=val,key=val — repeat for several; --any wakes on the fir
96
116
  has an outstanding review at arm, bb-wait prints a one-line
97
117
  stderr notice (unresolved count, review_decision, age) so a
98
118
  silent park isn't mistaken for "no review yet" — the
99
- existing review will not re-wake it.
119
+ existing review will not re-wake it. Your own thread reply or
120
+ resolve also wakes it: check reviewer_login + commit_sha.
100
121
  reviewer=<login> wakes only on that reviewer (case-
101
122
  insensitive; a trailing [bot] is ignored). sha=<commit>
102
- wakes only on verdicts for that commit (either side may be an abbreviated prefix; submitted reviews
103
- and clean passes carry commit_sha; thread comments do not,
104
- so they never match sha=); the usage-limit decline always
105
- passes sha=. Any other key is refused (exit 4).
106
- test-run:id=<run_id> a test run reaching a terminal status (owner-scoped)
107
- lease:id=<lease_id> a batch stack lease (BOT-1218) leaving the queue /
108
- becoming active — the zero-poll wake for a parked
109
- \`botbuddy stack up\` (recipient-scoped to the owner).
123
+ wakes only on verdicts for that commit (either side may be an
124
+ abbreviated prefix; submitted reviews and clean passes carry
125
+ commit_sha; thread comments do not, so they never match sha=);
126
+ the usage-limit decline always passes sha=. Any other key is
127
+ refused (exit 4).
128
+ ci:repo=<owner/repo>,{scope=latest,pr=<n> | run_id=<id> | sha=<sha>[,workflow=<name>]
129
+ | scope=next[,sha=<sha>][,branch=<name>][,workflow=<name>]
130
+ [,workflow_id=<id>][,workflow_path=<path>]}
131
+ a run's terminal conclusion (owner-scoped) — not a
132
+ PR state or review change (use pr-state / pr-review);
133
+ GitHub Actions or an external check_suite provider.
134
+ A repo with no CI feed is rejected (no_signal_source).
135
+ scope=latest,pr=<n> follows the PR's current head.
136
+ scope=next is LIVE-ONLY: it wakes on the first terminal
137
+ run delivered AFTER arm (post-merge / cron / scheduled),
138
+ never replays a finished run, and needs no run_id/pr.
139
+ Optional sha= pins it to a commit (BOT-1685: arm before
140
+ re-dispatching a run for a known SHA so it wakes on the
141
+ NEW run, not an earlier terminal run for the same SHA);
142
+ branch= (exact) / workflow= (GitHub workflow DISPLAY
143
+ name, case-insensitive) / workflow_id= (GitHub's numeric
144
+ id) / workflow_path= (e.g. .github/workflows/staging.yml,
145
+ exact) narrow that first run too. A filter matching no
146
+ known workflow exits 4 unknown_workflow and prints the
147
+ valid workflow=/workflow_id=/workflow_path= lines.
148
+ test-run:id=<run_id> a test run reaching a terminal status (owner-scoped);
149
+ bb test <lane> prints the exact line to arm.
150
+ stack-run:id=<run UUID> one named bb stack run reaching its terminal outcome
151
+ (BOT-2629). Choose the UUID BEFORE dispatch and pass it as
152
+ bb stack run --run-id <UUID>; the receipt carries exit_code,
153
+ child_exit_code, category, outcome and code. Live-only and
154
+ recipient-scoped to the run's agent. Never substitute a
155
+ lease: transition, a log tail or a polling loop.
156
+ lease:id=<lease_id>[,state=<s>] a batch stack lease (BOT-1218). Without state=: the lease
157
+ leaving the queue / becoming active — the zero-poll wake
158
+ for a parked bb stack up (recipient-scoped to the owner).
110
159
  Level-triggered: an already-granted lease wakes at once.
111
- lease:id=<lease_id>,state=<s> BOT-2400: wait for that exact state (provisioning, active,
112
- reaped). provisioning/active end on lease loss (exit 9,
113
- with the provisioning failure's reason and class).
114
- lease:id=<lease_id>,state=reaped proven cleanup of that owned lease (BOT-1948). Requires an
115
- accepted successful reap receipt or a verified
116
- never-provisioned no-op; preserves stop/destroy
160
+ state=provisioning|active (BOT-2400): that exact state;
161
+ both end on lease loss (exit 9, with the provisioning
162
+ failure's reason and class).
163
+ state=reaped (BOT-1948): proven cleanup of that owned
164
+ lease. Requires an accepted successful reap receipt or a
165
+ verified never-provisioned no-op; preserves stop/destroy
117
166
  disposition. An already-cleaned lease matches at once;
118
- reaping keeps waiting. A verified failed reap exits 12;
119
- an unverified gave-up reap exits 9 (lease_lost).
167
+ reaping keeps waiting. A verified failed reap exits 12; an
168
+ unverified gave-up reap exits 9 (lease_lost).
169
+ managed-allocation:id=<allocation UUID>,generation=<n>,target_revision=<n>
170
+ one exact managed allocation reaching an accepted cleanup
171
+ outcome (BOT-2330), fenced to that generation/revision. A
172
+ released match needs a zero permit count and the exact
173
+ accepted Helper cleanup receipt; an accepted cleanup_failed
174
+ receipt exits 10. A lifecycle wait, not host capacity.
120
175
  guidance:request=<post_id> a command-post guidance request being answered
121
176
  (scoped to the asking agent; fetch the body with
122
177
  get_guidance_response)
@@ -132,6 +187,8 @@ CONDITIONS (TYPE:key=val,key=val — repeat for several; --any wakes on the fir
132
187
  Tenant-scoped. Also accepts unblocked:ticket=<KEY>.
133
188
  capacity:policy=<p>,slots>=<N>[,host=<h>][,stale_grace=<sec>]
134
189
  a managed-container host reaching >= N free slots.
190
+ Copy the condition a capacity refusal prints (e.g.
191
+ policy=default or host_docker_pressure_v1).
135
192
  Host-shared; a silently-dead host is detected server-side
136
193
  (capacity_source_stale) via a host beacon. BOT-2400: a stale
137
194
  source is DEGRADED, never terminal — the wait stays armed and
@@ -141,25 +198,21 @@ CONDITIONS (TYPE:key=val,key=val — repeat for several; --any wakes on the fir
141
198
  one and its receipt carries grant:{position,tier,score};
142
199
  the others keep parking. Older servers broadcast without
143
200
  the ranking (first-come). Needs cli >= 1.20.0.
144
- ci:repo=<owner/repo>,{scope=latest,pr=<n> | run_id=<id> | sha=<sha>
145
- | scope=next[,sha=<sha>][,branch=<name>][,workflow=<name>]}
146
- a run's terminal conclusion (owner-scoped) — not a
147
- PR state or review change (use pr-state / pr-review);
148
- GitHub Actions or an external check_suite provider.
149
- A repo with no CI feed is rejected (no_signal_source).
150
- scope=next is LIVE-ONLY: it wakes on the first terminal
151
- run delivered AFTER arm (post-merge / cron / scheduled),
152
- never replays a finished run, and needs no run_id/pr.
153
- Optional sha= pins it to a commit (BOT-1685: arm before
154
- re-dispatching a run for a known SHA so it wakes on the
155
- NEW run, not an earlier terminal run for the same SHA);
156
- branch= (exact) / workflow= (GitHub workflow DISPLAY
157
- name, case-insensitive) narrow that first run too.
158
201
  staging-green:repo=<owner/repo> verified automatic recovery of an enabled staging gate.
159
202
  Wakes only after a complete non-vacuous workflow success;
160
203
  manual resolutions, overrides, skipped, and neutral runs do not match.
204
+ staging-release:repo=<owner/repo>,pr=<n>[,after_run=<run_id>[:<attempt>]]
205
+ the first staging release train whose head CONTAINS your
206
+ merged PR (git ancestry, never timestamps) reaching a
207
+ terminal conclusion — success OR failure (payload.conclusion
208
+ + verified; on verified:false check staging yourself).
209
+ Level-triggered: a train that already shipped your merge
210
+ grants at once (payload.reason=already_released).
211
+ after_run= skips a run you already saw, so a wait re-armed
212
+ from a failure receipt parks for the NEXT train.
161
213
  event:type=<signal_type>[,subject=<key>]
162
- raw signal-spine match (escape hatch). Host setup uses
214
+ raw signal-spine match (escape hatch only when that raw
215
+ signal IS the outcome). Host setup uses
163
216
  event:type=host_approval,subject=bbh-<24 lowercase hex>;
164
217
  it wakes only for the authenticated owner's one bound
165
218
  tenant after current server-side approval revalidation.
@@ -181,25 +234,32 @@ OPTIONS
181
234
  (--agent-key and --session-token are one-release aliases.)
182
235
  --session-id <uuid> advanced attribution override; normal worktrees derive it automatically.
183
236
  --help show this help
184
- resume <local_wait_id> recover one explicitly named saved wait
185
- result [<local_wait_id>] read its terminal receipt without rearming or acknowledging
186
- [--from-file <path> --exit-code <n>]
187
- validate redirected output AFTER the original command exits
188
- cancel <local_wait_id> explicitly stop one saved wait
189
- acknowledge <local_wait_id> mark one saved terminal receipt read
237
+
238
+ SUBCOMMANDS (saved waits live in this worktree's .botbuddy/waits/)
239
+ bb wait List this worktree's saved waits (after a restart or crash);
240
+ [--offset <n>] pages. Non-interactive callers get JSON.
241
+ bb wait result [<local_wait_id>] [--from-file <path> --exit-code <n>]
242
+ read a terminal receipt without re-arming or acknowledging;
243
+ --from-file validates redirected output AFTER the original
244
+ command exits (see EXAMPLES)
245
+ bb wait resume <local_wait_id> recover one explicitly named saved wait
246
+ bb wait cancel <local_wait_id> explicitly stop one saved wait
247
+ bb wait acknowledge <local_wait_id> mark one saved terminal receipt read (after acting on it)
190
248
 
191
249
  AUTH
192
250
  In a configured project worktree, bb wait restores or renews its session automatically.
193
- For first-time setup or repair, run bb setup once, then retry the same wait. No environment
194
- variables, copied identifiers, or credential flags are needed for normal waits.
251
+ For first-time setup or repair, run bb setup once (bb doctor --fix for a stale session),
252
+ then retry the same wait. No environment variables, copied identifiers, or credential
253
+ flags are needed for normal waits.
195
254
  --token and --profile are RETIRED (BOT-1574); the session override above is for managed
196
255
  runners only. The per-machine client key is a setup credential, never a wait credential.
197
256
 
198
257
  OUTPUT
199
258
  Exactly one JSON receipt line on stdout at exit, with client semver and
200
259
  protocol identity. Diagnostics go to stderr — including the arm-time notices
201
- (outstanding review, latest CI run). Do not pipe bb wait through tail: the
202
- notice is on stderr and you will not see it until the wait exits.
260
+ (outstanding review, latest CI run) and the 🔗 /waits link to show the user.
261
+ Do not pipe bb wait through tail: the notice is on stderr and you will not
262
+ see it until the wait exits.
203
263
  Exit codes: 0 matched · 2 timeout · 3 auth · 4 invalid conditions ·
204
264
  5 backend unavailable · 6 cursor expired · 7 internal ·
205
265
  8 superseded (wait replaced server-side) · 9 lease lost (a
@@ -216,24 +276,78 @@ OUTPUT
216
276
  only on a queued/busy result (granted = you hold it — just resume);
217
277
  otherwise re-run from the receipt's next_cursor. docs/agent-wait.md.
218
278
 
219
- EXAMPLES
220
- # wait for a Playwright lane to free up, then acquire it yourself
221
- botbuddy wait 'lock:subtype=playwright_lane,host=Jonos-MBP'
279
+ EXAMPLES (concrete values shown — substitute your repo, PR, SHA, ticket and ids)
280
+ # Wait for time. The ONLY sanctioned way to wait N seconds or until a clock time.
281
+ bb wait 'timer:duration=1800'
282
+ bb wait 'timer:deadline=2026-11-02T09:05:00Z'
283
+
284
+ # After a push: CI on the PR's current head finished (any conclusion).
285
+ bb wait 'ci:repo=bot-buddy/botbuddy-webapp,scope=latest,pr=1234' --heartbeat
286
+
287
+ # After a merge or a re-dispatch: the NEXT run for that exact commit (arm first, then dispatch).
288
+ bb wait 'ci:repo=bot-buddy/botbuddy-webapp,scope=next,sha=9f3c2a1b4d5e6f708192a3b4c5d6e7f809a1b2c3' --heartbeat
289
+ bb wait 'ci:repo=bot-buddy/botbuddy-webapp,scope=next,branch=main,workflow_path=.github/workflows/staging.yml' --heartbeat
290
+
291
+ # Codex review of the head you just pushed (arm BEFORE posting @codex review).
292
+ bb wait 'pr-review:repo=bot-buddy/botbuddy-webapp,pr=1234,reviewer=chatgpt-codex-connector[bot],sha=9f3c2a1' --heartbeat
293
+
294
+ # The next review comment OR a PR state change (merge, conflict, checks), whichever first.
295
+ bb wait --any 'pr-review:repo=bot-buddy/botbuddy-webapp,pr=1234' \\
296
+ 'pr-state:repo=bot-buddy/botbuddy-webapp,pr=1234,sha=9f3c2a1' --heartbeat
297
+
298
+ # A ticket changes (state, comment, assignee) / its last blocker clears.
299
+ bb wait 'linear:issue=BOT-1234' --heartbeat
300
+ bb wait 'unblocked:BOT-1234' --heartbeat
301
+
302
+ # Your merged PR reached staging (success or failure) / staging recovered.
303
+ bb wait 'staging-release:repo=bot-buddy/botbuddy-webapp,pr=1234' --timeout 28800 --heartbeat
304
+ bb wait 'staging-green:repo=bot-buddy/botbuddy-webapp' --heartbeat
305
+
306
+ # A named stack run finished: choose the UUID, arm, then bb stack run --run-id <same UUID> -- ...
307
+ bb wait 'stack-run:id=2f9d6a3c-0b1e-4c5d-8e7f-1a2b3c4d5e6f' --heartbeat
222
308
 
223
- # queue for a specific lane and be HANDED it when it frees (no acquire race):
224
- # acquire_lock (gets queued) → bb-wait claim=true → wake already holding lane 3
225
- botbuddy wait 'lock:subtype=playwright_lane,host=Jonos-MBP,slot=3,claim=true' --heartbeat
309
+ # A parked lease was granted / a lease's cleanup was proven.
310
+ bb wait 'lease:id=7c1e2d3f-4a5b-4c6d-8e9f-0a1b2c3d4e5f' --heartbeat
311
+ bb wait 'lease:id=7c1e2d3f-4a5b-4c6d-8e9f-0a1b2c3d4e5f,state=reaped'
226
312
 
227
- # wait for a chat reply on any channel, or 20 minutes, whichever first
228
- botbuddy wait --any 'chat:channel=*' 'timer:duration=1200' --heartbeat
313
+ # Docker capacity after HOST_CAPACITY_SOURCE_STALE (phase transient) or a pressure refusal:
314
+ # copy the refusal's own condition, then re-run the refused command once.
315
+ bb wait 'capacity:policy=host_docker_pressure_v1,slots>=1' --heartbeat
229
316
 
230
- # after pushing review fixes, wait for the next review comment or a state change
231
- botbuddy wait --any 'pr-review:repo=bot-buddy/botbuddy-webapp,pr=425' \\
232
- 'pr-state:repo=bot-buddy/botbuddy-webapp,pr=425'
317
+ # A tracked test run finished (bb test <lane> prints this line with the real id).
318
+ bb wait 'test-run:id=5b0c7e2a-9d41-4f3e-8a6b-1c2d3e4f5a6b' --heartbeat
319
+
320
+ # Wait for a Playwright lane to free up, then acquire it yourself.
321
+ bb wait 'lock:subtype=playwright_lane,host=Jonos-MBP'
322
+
323
+ # Queue for a specific lane and be HANDED it when it frees (no acquire race):
324
+ # acquire_lock (gets queued) → bb wait claim=true → wake already holding lane 3
325
+ bb wait 'lock:subtype=playwright_lane,host=Jonos-MBP,slot=3,claim=true' --heartbeat
326
+
327
+ # A Direct Chat reply on any channel you are in (arm before sending).
328
+ bb wait 'chat:channel=*' --heartbeat
329
+
330
+ # Redirect a wait's output and still return its receipt: run these three lines as ONE
331
+ # pending (background) command, and keep that command's result pending until it exits.
332
+ wait_artifact=$(mktemp /tmp/bb-wait-result.XXXXXX); wait_exit=0
333
+ bb wait 'pr-review:repo=bot-buddy/botbuddy-webapp,pr=1234,sha=9f3c2a1' --heartbeat > "$wait_artifact" 2>&1 || wait_exit=$?
334
+ bb wait result --from-file "$wait_artifact" --exit-code "$wait_exit"
335
+
336
+ # After a restart: list saved waits, then recover exactly one by id.
337
+ bb wait
338
+ bb wait resume <local_wait_id>
233
339
 
234
340
  ${SETUP_BLOCK}
235
341
  `;
236
342
 
343
+ // BOT-2715: exit only once stdout has taken the whole text. On a macOS pipe the
344
+ // write is async, so a process.exit() straight after it can drop everything past
345
+ // the first 8 KiB (CI cut the ~22 KiB help at exactly 8192 bytes). Never resolves:
346
+ // the process ends in the write callback.
347
+ export function writeThenExit(text, { stream = process.stdout, exit = (code) => process.exit(code) } = {}) {
348
+ return new Promise(() => { stream.write(text, () => exit(EXIT.MATCHED)); });
349
+ }
350
+
237
351
  // BOT-2400: a wait's timeout is a SAFETY CAP measured in hours or days, never a polling
238
352
  // interval. Each re-arm wakes the agent and re-reads its whole context (ENT-4850 §18: 15
239
353
  // wakes ≈ 50M tokens), so the default is a day, the cap three days, and a cap under two hours
@@ -1121,10 +1235,7 @@ async function inspectSavedWaits(argv = []) {
1121
1235
  });
1122
1236
  process.exit(EXIT.MATCHED);
1123
1237
  }
1124
- if (inventory.items.length === 0) {
1125
- process.stdout.write(HELP);
1126
- process.exit(EXIT.MATCHED);
1127
- }
1238
+ if (inventory.items.length === 0) return writeThenExit(HELP);
1128
1239
  // A TTY gets discovery guidance without an implicit selection. This is a
1129
1240
  // deliberately conservative menu: a restart must name an ID before mutation.
1130
1241
  process.stdout.write("Saved bb waits:\n");
@@ -1608,10 +1719,7 @@ export async function runWait(argv, { recoveryLocalWaitId = null } = {}) {
1608
1719
  writeLeaseLostNotice(receipt);
1609
1720
  return emit(receipt, { ...options, maxBytes: opts.receiptMaxBytes });
1610
1721
  };
1611
- if (opts.help) {
1612
- process.stdout.write(HELP);
1613
- process.exit(0);
1614
- }
1722
+ if (opts.help) return writeThenExit(HELP);
1615
1723
  // BOT-1574 (AC3): --token / --profile are retired. Emit the migration hint and
1616
1724
  // a typed `unknown_option` receipt, then exit 4 — before any relay call.
1617
1725
  if (opts.retiredFlag) {