@botbuddy/cli 1.60.3 → 1.60.4

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.60.3",
3
- "source_commit": "ce15cf76534b99c30468bdd541f2ab8e9340ee08",
4
- "source_sha256": "962d9cae75ce935da9306a538440ccb1448270ea80b9c0d4ba76be0c3c3a6707",
2
+ "release_version": "1.60.4",
3
+ "source_commit": "06faf46d8438c66ae116ce47eb5f343dfaca0c73",
4
+ "source_sha256": "9c1d721c1f0cfe90208bbc0960675053901326b324c7014c730769247d9d8c1c",
5
5
  "target": "darwin-arm64",
6
- "sha256": "989d317d944147c7ec86dc894c74b6a462e95fa85ee709e3e0c562783e231668"
6
+ "sha256": "ea3ec61a04791ef355abd7bb04e01ff0895f2603e57d9ef6e4a33614a140b12a"
7
7
  }
@@ -1,7 +1,7 @@
1
1
  {
2
- "release_version": "1.60.3",
3
- "source_commit": "ce15cf76534b99c30468bdd541f2ab8e9340ee08",
4
- "source_sha256": "962d9cae75ce935da9306a538440ccb1448270ea80b9c0d4ba76be0c3c3a6707",
2
+ "release_version": "1.60.4",
3
+ "source_commit": "06faf46d8438c66ae116ce47eb5f343dfaca0c73",
4
+ "source_sha256": "9c1d721c1f0cfe90208bbc0960675053901326b324c7014c730769247d9d8c1c",
5
5
  "target": "linux-x64",
6
- "sha256": "90a9017148902699e54e26fddb940abc87d22ca96c3f415a75d4d172614dd281"
6
+ "sha256": "ebe08409a8d9ea7a7292af476ea9d3d0acf91b8b09834b12e38a6833c0bf2ac5"
7
7
  }
@@ -1,21 +1,21 @@
1
1
  {
2
2
  "schema_version": 1,
3
- "release_version": "1.60.3",
4
- "source_commit": "ce15cf76534b99c30468bdd541f2ab8e9340ee08",
5
- "source_sha256": "962d9cae75ce935da9306a538440ccb1448270ea80b9c0d4ba76be0c3c3a6707",
3
+ "release_version": "1.60.4",
4
+ "source_commit": "06faf46d8438c66ae116ce47eb5f343dfaca0c73",
5
+ "source_sha256": "9c1d721c1f0cfe90208bbc0960675053901326b324c7014c730769247d9d8c1c",
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": "989d317d944147c7ec86dc894c74b6a462e95fa85ee709e3e0c562783e231668",
11
+ "sha256": "ea3ec61a04791ef355abd7bb04e01ff0895f2603e57d9ef6e4a33614a140b12a",
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": "90a9017148902699e54e26fddb940abc87d22ca96c3f415a75d4d172614dd281",
18
+ "sha256": "ebe08409a8d9ea7a7292af476ea9d3d0acf91b8b09834b12e38a6833c0bf2ac5",
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.60.3",
3
+ "version": "1.60.4",
4
4
  "description": "BotBuddy \u2014 Swarm coordination CLI for multi-agent workflows",
5
5
  "type": "module",
6
6
  "bin": {
@@ -134,13 +134,13 @@ export function buildStackRunOutcomePayload({ fields = {}, exitCode, runId = ran
134
134
  * credential the lease calls used); without one there is nothing to authenticate
135
135
  * with and nothing is sent. Never throws; resolves within `timeoutMs`.
136
136
  */
137
- export async function reportStackRunOutcome({ fields, exitCode, opts, env = process.env, durationMs = null, channel, timeoutMs = STACK_RUN_OUTCOME_TIMEOUT_MS }) {
137
+ export async function reportStackRunOutcome({ fields, exitCode, runId, opts, env = process.env, durationMs = null, channel, timeoutMs = STACK_RUN_OUTCOME_TIMEOUT_MS }) {
138
138
  // Test suites opt out so a spawned real CLI never records a fixture run.
139
139
  if (env?.[STACK_RUN_OUTCOME_OPT_OUT_ENV] === "off") return { sent: false, reason: "disabled" };
140
140
  if (!channel?.auth || typeof channel.callTool !== "function") return { sent: false, reason: "no_credential" };
141
141
  let payload;
142
142
  try {
143
- payload = buildStackRunOutcomePayload({ fields, exitCode, opts, env, durationMs });
143
+ payload = buildStackRunOutcomePayload({ fields, exitCode, runId, opts, env, durationMs });
144
144
  } catch {
145
145
  return { sent: false, reason: "payload_error" };
146
146
  }
package/src/stack.mjs CHANGED
@@ -188,6 +188,8 @@ ${bold("done OPTIONS")}
188
188
  ${bold("run OPTIONS")}
189
189
  --repo <repo> Required approved repository name.
190
190
  --ticket <BOT-123> Required ticket for the batch.
191
+ --run-id <UUID> Optional caller-chosen run identity. Choose it before dispatch to arm
192
+ bb wait 'stack-run:id=<UUID>'; the completion signal carries this run's exit code.
191
193
  --stack-path <relative> Optional PREPARED isolated stack dir. Omit it (the norm): run
192
194
  materializes .botbuddy/stacks/<project_id>/ from this worktree's
193
195
  supabase/ tree with a distinct project_id + remapped ports and
@@ -358,7 +360,7 @@ export function parseStackArgs(argv) {
358
360
  const childArgv = divider === -1 ? [] : rawRest.slice(divider + 1);
359
361
  const opts = {
360
362
  slot: null, host: null, repo: null, ticket: null, ticketUrl: null,
361
- prId: null, prUrl: null, purpose: null, idleTtl: null, stackPath: ".", stackPathExplicit: false,
363
+ prId: null, prUrl: null, purpose: null, runId: null, idleTtl: null, stackPath: ".", stackPathExplicit: false,
362
364
  timeout: DEFAULT_TIMEOUT_SEC, provisionTimeout: null, reapTimeout: 300, hardTtl: null,
363
365
  connectionFile: null, noWait: false, claimOnly: false, localExec: false, keepVolumes: false, waitCapacity: null, holdOnTimeout: null,
364
366
  dockerContext: null, dockerEndpoint: null,
@@ -390,6 +392,7 @@ export function parseStackArgs(argv) {
390
392
  case "--pr": if (need(a, rest[i + 1])) opts.prId = rest[++i]; break;
391
393
  case "--pr-url": if (need(a, rest[i + 1])) opts.prUrl = rest[++i]; break;
392
394
  case "--purpose": if (need(a, rest[i + 1])) opts.purpose = rest[++i]; break;
395
+ case "--run-id": if (need(a, rest[i + 1])) opts.runId = rest[++i]; break;
393
396
  case "--stack-path": if (need(a, rest[i + 1])) { opts.stackPath = rest[++i]; opts.stackPathExplicit = true; } break;
394
397
  case "--idle-ttl": {
395
398
  if (need(a, rest[i + 1])) {
@@ -518,6 +521,15 @@ export function parseStackArgs(argv) {
518
521
  } else if (opts.bundleInclude.length && opts.localOnly) {
519
522
  errors.push("--bundle-include ships files in the remote-lease bundle, and --local-only builds no bundle; drop one of them");
520
523
  }
524
+ if (opts.runId != null) {
525
+ if (command !== "run") {
526
+ errors.push("--run-id is valid only with `stack run`");
527
+ } else if (!/^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i.test(String(opts.runId))) {
528
+ errors.push("--run-id must be a canonical UUID");
529
+ } else {
530
+ opts.runId = String(opts.runId).toLowerCase();
531
+ }
532
+ }
521
533
  opts.bundleInclude = [...new Set(opts.bundleInclude)];
522
534
  opts.execution = opts.remote ? "remote" : opts.localOnly ? "local" : null;
523
535
  if (typeof opts.stackPath !== "string" || !opts.stackPath.trim() || opts.stackPath.startsWith("/") ||
@@ -4546,12 +4558,16 @@ export function runReceiptFields(result) {
4546
4558
  export async function cmdRun(opts, childArgv, {
4547
4559
  lifecycle = runStackLifecycle, report = reportStackRunOutcome, now = Date.now, env = process.env,
4548
4560
  } = {}) {
4561
+ // A caller that needs a wait selects this immutable UUID before dispatch.
4562
+ // Ordinary foreground runs still receive a unique telemetry id, but it is not
4563
+ // retrospectively advertised as an armable completion contract.
4564
+ const runId = opts.runId ?? randomUUID();
4549
4565
  const startedAt = now();
4550
4566
  const result = await lifecycle(opts, childArgv);
4551
4567
  const fields = runReceiptFields(result);
4552
4568
  const code = emit(buildReceipt(fields), opts, result.exitCode);
4553
4569
  try {
4554
- await report({ fields: stackRunOutcomeFields(result), exitCode: code, opts, env, durationMs: now() - startedAt, channel: result?.[OUTCOME_CHANNEL] ?? null });
4570
+ await report({ fields: stackRunOutcomeFields(result), exitCode: code, runId, opts, env, durationMs: now() - startedAt, channel: result?.[OUTCOME_CHANNEL] ?? null });
4555
4571
  } catch { /* best-effort: the receipt and exit code are already final */ }
4556
4572
  return code;
4557
4573
  }
@@ -4600,7 +4616,7 @@ export async function cmdStack(argv, { report = reportStackRunOutcome, outcomeCh
4600
4616
  const code = emit(buildReceipt(fields), opts, EXIT.INVALID);
4601
4617
  // BOT-2467: a refused `stack run` is recorded as an `invocation` outcome (no lease).
4602
4618
  if (command === "run") {
4603
- try { await report({ fields, exitCode: code, opts, env: process.env, durationMs: 0, channel: await outcomeChannel(argv) }); } catch { /* best-effort */ }
4619
+ try { await report({ fields, exitCode: code, runId: opts.runId ?? undefined, opts, env: process.env, durationMs: 0, channel: await outcomeChannel(argv) }); } catch { /* best-effort */ }
4604
4620
  }
4605
4621
  process.exitCode = code;
4606
4622
  return code;
package/src/wait-core.mjs CHANGED
@@ -85,9 +85,9 @@ export const WAIT_SUPERSEDED_SIGNAL_TYPE = "wait_superseded";
85
85
  const HOST_APPROVAL_HOST_KEY_RE = /^bbh-[0-9a-f]{24}$/;
86
86
 
87
87
  // The USER-ARMABLE spine signal_type allowlist, for the `event:` escape hatch. The full
88
- // public.agent_signal_type enum is these PLUS the control types above
89
- // (WAIT_SUPERSEDED_SIGNAL_TYPE) — control types are intentionally excluded here so they
90
- // can't be armed on.
88
+ // public.agent_signal_type enum also has control-only and dedicated-grammar types (for
89
+ // example `wait_superseded` and BOT-2629's `stack_run`); they are intentionally excluded
90
+ // here so raw event conditions cannot bypass their narrower matching contracts.
91
91
  export const SIGNAL_TYPES = Object.freeze([
92
92
  "ci_run",
93
93
  "pr_review",
@@ -110,6 +110,7 @@ export const SIGNAL_TYPES = Object.freeze([
110
110
  ]);
111
111
 
112
112
  const LOCK_SUBTYPES = Object.freeze(["playwright_lane", "vite_port", "backend_port", "supabase_local"]);
113
+ const CANONICAL_UUID_RE = /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/;
113
114
 
114
115
  // ---------------------------------------------------------------------------
115
116
  // Condition grammar: `TYPE:key=val,key=val`
@@ -194,6 +195,16 @@ const VALIDATORS = {
194
195
  if (!p.id) throw new Error("test-run needs id=<run_id>");
195
196
  return { runId: String(p.id) };
196
197
  },
198
+ // BOT-2629: this is deliberately a dedicated condition rather than a raw
199
+ // event escape hatch. The UUID is chosen before dispatch and the exact subject
200
+ // prevents another run on the same idle/re-adopted lease waking this waiter.
201
+ "stack-run": (p) => {
202
+ const unexpected = Object.keys(p).filter((key) => key !== "id");
203
+ if (unexpected.length) throw new Error(`stack-run does not accept ${unexpected.join(", ")}`);
204
+ const id = String(p.id ?? "").trim().toLowerCase();
205
+ if (!CANONICAL_UUID_RE.test(id)) throw new Error("stack-run needs id=<canonical run UUID>");
206
+ return { id };
207
+ },
197
208
  // BOT-1063 — guidance: park on a command-post guidance RESPONSE to a request
198
209
  // this agent raised (pairs with request_guidance). The signal is
199
210
  // recipient-scoped to the requester and keyed by the request id.
@@ -1112,6 +1123,9 @@ function conditionMatchesSignal(condition, signal, waitSessionId) {
1112
1123
  prReviewFiltersMatch(signal, params);
1113
1124
  case "test-run":
1114
1125
  return signal.signal_type === "test_run" && signal.subject_key === `run:${params.runId}`;
1126
+ case "stack-run":
1127
+ return signal.signal_type === "stack_run" && signal.subject_key === `stack-run:${params.id}` &&
1128
+ signal.payload?.run_id === params.id && Number.isInteger(signal.payload?.exit_code);
1115
1129
  case "guidance":
1116
1130
  return signal.signal_type === "guidance_response" && signal.subject_key === params.request;
1117
1131
  case "lease":