@botbuddy/cli 1.61.6 → 1.61.8

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.6",
3
- "source_commit": "1e8d5f51d3f9be35c44e793551defaeaf8ae2d23",
4
- "source_sha256": "0156e8ade4a62f2a6c4cb29d57fa0f5c23600ba264093198900c22e4c6fc0d68",
2
+ "release_version": "1.61.8",
3
+ "source_commit": "c781ee6542c93c05a5ec08e7348fddc0330f33d8",
4
+ "source_sha256": "b1b8c8cfaa0f4c91f023a78360d2039089885a65367182f00df7dd4312865608",
5
5
  "target": "darwin-arm64",
6
- "sha256": "8bc316711530d7900ff4186b4eb114cf48ce255344d5d3436962c8d6bee46810"
6
+ "sha256": "cdaab9fc0b5116cbed342c31c5e7c6544022d8c927cd987daa6d9c6986cc99c4"
7
7
  }
@@ -1,7 +1,7 @@
1
1
  {
2
- "release_version": "1.61.6",
3
- "source_commit": "1e8d5f51d3f9be35c44e793551defaeaf8ae2d23",
4
- "source_sha256": "0156e8ade4a62f2a6c4cb29d57fa0f5c23600ba264093198900c22e4c6fc0d68",
2
+ "release_version": "1.61.8",
3
+ "source_commit": "c781ee6542c93c05a5ec08e7348fddc0330f33d8",
4
+ "source_sha256": "b1b8c8cfaa0f4c91f023a78360d2039089885a65367182f00df7dd4312865608",
5
5
  "target": "linux-x64",
6
- "sha256": "3bf5b7a6396da374f12abada7e42685ad8f783366c5c4de885da21d9749b257b"
6
+ "sha256": "4047d1635cace367c281d3d8c68268d45ffba24cbae48ff07f08cb384985ca03"
7
7
  }
@@ -1,21 +1,21 @@
1
1
  {
2
2
  "schema_version": 1,
3
- "release_version": "1.61.6",
4
- "source_commit": "1e8d5f51d3f9be35c44e793551defaeaf8ae2d23",
5
- "source_sha256": "0156e8ade4a62f2a6c4cb29d57fa0f5c23600ba264093198900c22e4c6fc0d68",
3
+ "release_version": "1.61.8",
4
+ "source_commit": "c781ee6542c93c05a5ec08e7348fddc0330f33d8",
5
+ "source_sha256": "b1b8c8cfaa0f4c91f023a78360d2039089885a65367182f00df7dd4312865608",
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": "8bc316711530d7900ff4186b4eb114cf48ce255344d5d3436962c8d6bee46810",
11
+ "sha256": "cdaab9fc0b5116cbed342c31c5e7c6544022d8c927cd987daa6d9c6986cc99c4",
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": "3bf5b7a6396da374f12abada7e42685ad8f783366c5c4de885da21d9749b257b",
18
+ "sha256": "4047d1635cace367c281d3d8c68268d45ffba24cbae48ff07f08cb384985ca03",
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.6",
3
+ "version": "1.61.8",
4
4
  "description": "BotBuddy \u2014 Swarm coordination CLI for multi-agent workflows",
5
5
  "type": "module",
6
6
  "bin": {
package/src/test-lane.mjs CHANGED
@@ -9,8 +9,13 @@
9
9
  // the lane keeps executing detached. A worker (later phase) tails the reporter's
10
10
  // NDJSON and flushes per-case verdicts through `sync_test_run_cases`.
11
11
  //
12
- // Backend down? The lane still runs; the JSON line says telemetry:"unavailable",
12
+ // Test-run telemetry down (create_test_run fails) but the durable launcher
13
+ // accepts? The lane still runs; the JSON line says telemetry:"unavailable",
13
14
  // wait:null, and the child exit code is preserved.
15
+ //
16
+ // Launcher refused (BOT-2685)? Nothing runs. The line carries the launcher's
17
+ // own outcome/error/recovery and exit code, wait:null, and the pre-created run
18
+ // is closed `completed` with metadata.launch_failed so its waiters wake.
14
19
 
15
20
  import { randomUUID } from "node:crypto";
16
21
  import { existsSync, readFileSync } from "node:fs";
@@ -45,6 +50,7 @@ export function parseTestArgs(argv, { env = process.env } = {}) {
45
50
  environment: "local",
46
51
  ticket: null, pr: null, repo: null,
47
52
  laneKind: null,
53
+ rerunReason: null,
48
54
  wait: false, json: false,
49
55
  };
50
56
  const errors = [];
@@ -66,6 +72,17 @@ export function parseTestArgs(argv, { env = process.env } = {}) {
66
72
  case "--pr": { const raw = value(flag, i); i++; opts.pr = raw == null ? null : Number(raw); if (raw != null && !Number.isInteger(opts.pr)) errors.push("--pr must be an integer"); break; }
67
73
  case "--repo": opts.repo = value(flag, i); i++; break;
68
74
  case "--lane-kind": opts.laneKind = value(flag, i); i++; break;
75
+ // BOT-2685: the durable launcher refuses an identical repeat without a
76
+ // reason; validate the shape here, the server validates the code.
77
+ case "--rerun-reason": {
78
+ const raw = value(flag, i); i++;
79
+ if (raw == null) break;
80
+ let parsed = null;
81
+ try { parsed = JSON.parse(raw); } catch { /* reported below */ }
82
+ if (!parsed || typeof parsed !== "object" || Array.isArray(parsed)) errors.push("--rerun-reason must be a JSON object, e.g. '{\"code\":\"flaky_suspected\"}'");
83
+ else opts.rerunReason = raw;
84
+ break;
85
+ }
69
86
  case "--wait": opts.wait = true; break;
70
87
  case "--json": opts.json = true; break;
71
88
  default:
@@ -140,18 +157,44 @@ export function defaultGitInfo({ cwd = process.cwd(), ticket = null, pr = null,
140
157
 
141
158
  // The production lane launcher: run the lane through the durable `botbuddy run`
142
159
  // worker, carrying the telemetry env into its detached child.
143
- async function defaultLaunchLane({ command, sessionId, sessionToken, environment, expectedDurationSeconds, childEnv, testRun, cwd, call }) {
160
+ async function defaultLaunchLane({ command, sessionId, sessionToken, environment, expectedDurationSeconds, childEnv, testRun, cwd, call, rerunReason }) {
144
161
  // BOT-1572/1582: pass --session-id only when a plain id is used; a token-only lane
145
162
  // relies on the inherited $BOTBUDDY_AGENT_SESSION_TOKEN (and an explicit --agent-session-token
146
163
  // so the child never falls back to a machine credential).
147
164
  const argv = [
148
165
  ...(sessionId ? ["--session-id", sessionId] : []),
149
166
  ...(sessionToken ? ["--agent-session-token", sessionToken] : []),
167
+ ...(rerunReason ? ["--rerun-reason", rerunReason] : []),
150
168
  "--environment", environment, "--category", "validation", "--kind", "full_suite",
151
169
  "--expected-duration", String(expectedDurationSeconds ?? 0), "--", ...command,
152
170
  ];
153
171
  const result = await launchRun(argv, { cwd, call: sessionToken ? null : call, childEnv, testRun });
154
- return { exitCode: result.exitCode, runId: result.receipt?.run_id ?? null };
172
+ return { exitCode: result.exitCode, runId: result.receipt?.run_id ?? null, receipt: result.receipt ?? null };
173
+ }
174
+
175
+ // BOT-2685: the BOT-816 rerun policy refusals carry no recovery of their own,
176
+ // and they are the usual reason a lane retry on an unchanged commit is refused.
177
+ const REFUSAL_RECOVERY = Object.freeze({
178
+ rerun_reason_required: "this lane already ran on this commit and environment; retry with --rerun-reason '{\"code\":\"flaky_suspected\"}' (codes: source_changed|environment_changed|focused_reproducer_passed|flaky_suspected|infrastructure_failure|dependency_drift|approval_retry|user_requested|other+detail), or commit a change",
179
+ // owner_lost: the command registered (so a plain retry is now a repeat) but
180
+ // its detached worker never started.
181
+ launcher_failed: "the lane's worker process failed to start; retry with --rerun-reason '{\"code\":\"infrastructure_failure\"}'",
182
+ full_suite_rerun_blocked: "this full suite already failed twice on this commit; commit a fix, or prove a focused reproducer with `bb run --kind focused` and retry with --rerun-reason '{\"code\":\"focused_reproducer_passed\",\"focused_reproducer_run_id\":\"<run_id>\"}'",
183
+ });
184
+
185
+ // BOT-2685: a refused launch has no worker, so nothing else will ever complete
186
+ // the run created for it. Close it here, once, with the refusal on the record.
187
+ // A denied or failed update is reported as such — never retried into a claim.
188
+ async function reconcileRefusedRun(update, { testRunId, laneName, runner, outcome, error, exitCode, commandRunId }) {
189
+ let res;
190
+ try {
191
+ res = await update("update_test_run", {
192
+ test_run_id: testRunId, status: "completed",
193
+ metadata: { lane: laneName, runner, launch_failed: true, launch_outcome: outcome, launch_error: error, exit_code: exitCode, command_run_id: commandRunId },
194
+ });
195
+ } catch (e) { res = { ok: false, error: e instanceof Error ? e.message : String(e) }; }
196
+ if (res?.ok && !res.isError) return { test_run_reconciled: true };
197
+ return { test_run_reconciled: false, reconcile_error: res?.data?.error ?? res?.error ?? "update_test_run_failed" };
155
198
  }
156
199
 
157
200
  function waitCommand(testRunId, sessionId, sessionToken) {
@@ -288,12 +331,41 @@ export async function launchTestLane(argv, {
288
331
  const testRun = testRunId
289
332
  ? { test_run_id: testRunId, events_path: eventsPath, lane: laneName, runner: resolved.lane.runner, git_sha: git.sha ?? null, branch: git.branch ?? null }
290
333
  : null;
291
- const launch = await launchLane({
292
- command: [...resolved.lane.command, ...extra],
293
- sessionId: opts.sessionId, sessionToken: opts.sessionToken, environment: opts.environment,
294
- expectedDurationSeconds: resolved.lane.expected_duration_seconds ?? 0,
295
- childEnv, testRun, cwd, call, testRunId, lane: laneName, runner: resolved.lane.runner,
296
- });
334
+ let launch;
335
+ try {
336
+ launch = await launchLane({
337
+ command: [...resolved.lane.command, ...extra],
338
+ sessionId: opts.sessionId, sessionToken: opts.sessionToken, environment: opts.environment,
339
+ expectedDurationSeconds: resolved.lane.expected_duration_seconds ?? 0,
340
+ childEnv, testRun, cwd, call, testRunId, lane: laneName, runner: resolved.lane.runner,
341
+ rerunReason: opts.rerunReason,
342
+ });
343
+ } catch (e) {
344
+ const error = `launch_exception:${e instanceof Error ? e.message : String(e)}`;
345
+ launch = { exitCode: EXIT_TEST.INTERNAL, runId: null, receipt: { outcome: "rejected", error, exit_code: EXIT_TEST.INTERNAL } };
346
+ }
347
+
348
+ // BOT-2685: the durable launcher's verdict is authoritative. Anything but OK
349
+ // means no worker exists — say so with its own reason, never "launched".
350
+ if (launch.exitCode !== EXIT_TEST.OK) {
351
+ const receipt = launch.receipt ?? {};
352
+ const outcome = receipt.outcome === "owner_lost" ? "owner_lost" : "rejected";
353
+ // owner_lost is the one refusal whose command run was registered (and
354
+ // already marked owner_lost server-side); any other run id never existed.
355
+ const commandRunId = outcome === "owner_lost" ? launch.runId ?? null : null;
356
+ const error = receipt.error
357
+ ?? (Array.isArray(receipt.errors) ? receipt.errors.join("; ") : outcome === "owner_lost" ? "launcher_failed" : "launch_refused");
358
+ const reconciled = testRunId
359
+ ? await reconcileRefusedRun(callWithSessionRetry, { testRunId, laneName, runner: resolved.lane.runner, outcome, error, exitCode: launch.exitCode, commandRunId })
360
+ : {};
361
+ const recovery = receipt.recovery ?? REFUSAL_RECOVERY[error] ?? null;
362
+ const line = {
363
+ outcome, error, ...(recovery ? { recovery } : {}), exit_code: launch.exitCode,
364
+ test_run_id: testRunId, ...reconciled, command_run_id: commandRunId, lane: laneName,
365
+ wait: null, receipt_path: null, waits_url: WAITS_URL,
366
+ };
367
+ return { exitCode: launch.exitCode, line: JSON.stringify(line), testRunId, commandRunId, opts, refused: true };
368
+ }
297
369
 
298
370
  const commandRunId = launch.runId;
299
371
  const line = testRunId
@@ -308,7 +380,7 @@ export async function launchTestLane(argv, {
308
380
  receipt_path: commandRunId ? receiptPath(commandRunId) : null, waits_url: WAITS_URL,
309
381
  };
310
382
 
311
- return { exitCode: launch.exitCode, line: JSON.stringify(line), testRunId, commandRunId, opts };
383
+ return { exitCode: launch.exitCode, line: JSON.stringify(line), testRunId, commandRunId, opts, refused: false };
312
384
  }
313
385
 
314
386
  // AC-9 (--wait foreground convenience): block until the run reaches a terminal
@@ -349,7 +421,7 @@ function laneList(cwd) {
349
421
  }
350
422
 
351
423
  function testHelp() {
352
- console.log(`botbuddy test <lane> [--session-id <uuid>] [--agent-session-token <token>] [--environment local] [--ticket <KEY>] [--pr <n>] [--repo <owner/repo>] [--lane-kind <${LANE_KINDS.join("|")}>] [--wait] [--json] [-- <extra args>]
424
+ console.log(`botbuddy test <lane> [--session-id <uuid>] [--agent-session-token <token>] [--environment local] [--ticket <KEY>] [--pr <n>] [--repo <owner/repo>] [--lane-kind <${LANE_KINDS.join("|")}>] [--rerun-reason <json>] [--wait] [--json] [-- <extra args>]
353
425
  --agent-session-token <token> session credential (env $BOTBUDDY_AGENT_SESSION_TOKEN); --agent-key / --session-token are one-release legacy aliases
354
426
  botbuddy test list List the lanes configured in .botbuddy/lanes.json
355
427
  botbuddy test help Show this help
@@ -370,8 +442,8 @@ export async function cmdTest(args, { cwd = process.cwd() } = {}) {
370
442
  process.stdout.write(result.line + "\n");
371
443
  // --wait (AC-9): after launch, block until the run completes and exit with the
372
444
  // lane's exit code; a timeout exits 2 without killing the lane. Only meaningful
373
- // when telemetry is live (there is a run to wait on).
374
- if (result.testRunId && result.opts?.wait) {
445
+ // when telemetry is live (there is a run to wait on) and the lane launched.
446
+ if (result.testRunId && result.opts?.wait && !result.refused) {
375
447
  const wait = await waitForLane({ testRunId: result.testRunId, commandRunId: result.commandRunId });
376
448
  process.exitCode = wait.exitCode;
377
449
  return;