@botbuddy/cli 1.59.4 → 1.59.5

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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@botbuddy/cli",
3
- "version": "1.59.4",
3
+ "version": "1.59.5",
4
4
  "description": "BotBuddy \u2014 Swarm coordination CLI for multi-agent workflows",
5
5
  "type": "module",
6
6
  "bin": {
package/src/commands.mjs CHANGED
@@ -212,6 +212,8 @@ ${bold("MCP CONFIG KEY")}
212
212
  ${bold("TOOLS")}
213
213
  help --tools List every BotBuddy tool
214
214
  help <tool> Show one tool's arguments
215
+ help test Safe test-lane launch and recovery guidance
216
+ help symptom <name> Diagnose a known block without source-diving
215
217
  call <tool> [--key value] Invoke any tool
216
218
  call <tool> --json '{...}' Invoke with a JSON arguments object
217
219
  call <tool> --pin <slug> Pin the connection tenant (a user token needs one;
@@ -362,6 +364,11 @@ async function cmdCall(args) {
362
364
  }
363
365
 
364
366
  async function cmdToolHelp(args) {
367
+ const local = recoveryHelp(args);
368
+ if (local) {
369
+ console.log(local);
370
+ return;
371
+ }
365
372
  // `help --tools` (or `help --all`) lists the catalog; `help <tool>` details one.
366
373
  const tool = args[0].startsWith("-") ? null : args[0];
367
374
  const res = await fetch(discoveryUrlFor(SERVER_URL, tool));
@@ -373,6 +380,80 @@ async function cmdToolHelp(args) {
373
380
  console.log(formatDiscovery(await res.json(), { tool }));
374
381
  }
375
382
 
383
+ // Keep the most common lifecycle recoveries local: an agent must be able to
384
+ // discover the safe next action even when the MCP discovery endpoint is down.
385
+ // This deliberately names only stable, already-shipped commands. New lifecycle
386
+ // commands belong in their owning feature ticket before they are documented here.
387
+ export function recoveryHelp(args) {
388
+ const topic = args.map((arg) => String(arg).trim().toLowerCase()).filter(Boolean).join(" ");
389
+ if (topic === "test") return `${bold("bb help test")} — launch tests without losing their evidence
390
+
391
+ ${bold("SAFE DEFAULT")}
392
+ 1. Run ${cyan("bb doctor --json")} from the target worktree.
393
+ 2. Run the repository's configured test command without manually copying a
394
+ session ID or session token unless the harness explicitly requires it.
395
+ 3. Keep the command's original result and receipt path. Do not start a second
396
+ full suite merely because telemetry is missing.
397
+
398
+ ${bold("WHEN TEST TELEMETRY IS UNAVAILABLE")}
399
+ Run ${cyan("bb help symptom telemetry-unavailable")}. A null test-run ID means
400
+ there is no semantic test-run wait to arm; it is not a passing result.
401
+
402
+ ${bold("WAITING")}
403
+ Use ${cyan("bb wait 'test-run:id=<id>' --heartbeat")} only when the launch output
404
+ contains a real test-run ID. Do not replace a missing lifecycle signal with a
405
+ timer or status-polling loop.`;
406
+
407
+ if (topic === "symptom" || topic === "symptoms") return `${bold("bb help symptom <name>")} — supported recovery topics
408
+
409
+ ${cyan("telemetry-unavailable")} A test launch has no test-run ID or wait command.
410
+ ${cyan("command-run-stuck")} A local command receipt and remote lifecycle disagree.
411
+
412
+ Choose the symptom printed by the command. If neither applies, preserve the
413
+ original command result and receipt, then report the exact error and command ID.`;
414
+
415
+ if (topic === "symptom telemetry-unavailable") return `${bold("Symptom: telemetry unavailable")}
416
+
417
+ ${bold("Meaning")}
418
+ The workload may have started, but BotBuddy did not create a test-run record.
419
+ There is no ${cyan("test-run:id")} wait to arm and silence is never a green result.
420
+
421
+ ${bold("Safe recovery")}
422
+ 1. Do not launch another full suite yet.
423
+ 2. Preserve the original ${cyan("command_run_id")} and receipt path from its output.
424
+ 3. Read the terminal receipt when the original runner returns. A receipt with a
425
+ terminal status is evidence of that workload's result.
426
+ 4. If the receipt is not terminal, keep the original runner result pending;
427
+ do not poll it or use a timer as a completion signal.
428
+ 5. After a terminal receipt, fix the launch/authentication cause and rerun the
429
+ smallest affected scope once, with an explicit rerun reason when supported.
430
+
431
+ ${bold("Escalation payload")}
432
+ Include CLI version, exact command (with secrets removed), command-run ID,
433
+ receipt status/exit code, and whether a test-run ID was created.`;
434
+
435
+ if (topic === "symptom command-run-stuck") return `${bold("Symptom: command run appears stuck")}
436
+
437
+ ${bold("Meaning")}
438
+ A remote lifecycle record can lag or fail to receive a terminal callback. It
439
+ does not prove that the workload is still running.
440
+
441
+ ${bold("Safe recovery")}
442
+ 1. Preserve the command-run ID and the original result.
443
+ 2. Prefer the owning runner's local terminal receipt for the workload verdict.
444
+ 3. If the receipt is terminal, do not cancel or duplicate the command solely
445
+ because remote state remains ${cyan("registered")} or ${cyan("running")}.
446
+ 4. If the receipt is absent/non-terminal, retain the original runner result;
447
+ do not poll or infer completion from elapsed time.
448
+ 5. Report the receipt and remote discrepancy as telemetry evidence.
449
+
450
+ ${bold("Boundary")}
451
+ Command-run status is agent-owned. A different agent may be denied access; that
452
+ denial is not evidence that the run is missing or terminal.`;
453
+
454
+ return null;
455
+ }
456
+
376
457
  // BOT-1566 C1: a tenant slug as the server accepts it on `?tenant=` — lowercase
377
458
  // alphanumerics and hyphens, 2–63 chars, not starting with a hyphen.
378
459
  export const TENANT_SLUG_RE = /^[a-z0-9][a-z0-9-]{1,62}$/;
package/src/test-lane.mjs CHANGED
@@ -176,18 +176,29 @@ export async function launchTestLane(argv, {
176
176
  return { exitCode: EXIT_TEST.INVALID, line: JSON.stringify({ outcome: "rejected", errors }) };
177
177
  }
178
178
  let managedCredential = null;
179
- if (!opts.sessionId && !opts.sessionToken) {
180
- // Test doubles intentionally do not self-mint. The shipped command does,
181
- // using exactly the same cache/re-adoption resolver as `bb wait` and `run`.
179
+ // A session id names a session but is not an API credential. Resolve the
180
+ // worktree's token even when the caller supplied --session-id, so the
181
+ // create/update calls are authenticated and the launched lane has a durable
182
+ // test-run to wait on. Without this, an otherwise valid explicit id could
183
+ // launch a real test with test_run_id:null and no supported wait condition.
184
+ //
185
+ // Test doubles that provide only a synthetic session id retain the old
186
+ // no-network seam. Production, and every caller that supplies a resolver,
187
+ // must resolve the token or fail before launching an untrackable run.
188
+ const mustResolveCredential = !opts.sessionToken
189
+ && (!opts.sessionId || !!resolveCredential || call === callToolJson);
190
+ if (mustResolveCredential) {
182
191
  if (!resolveCredential && call !== callToolJson) {
183
192
  process.stderr.write("botbuddy test: --session-id is required (or set BOTBUDDY_AGENT_SESSION_TOKEN)\n");
184
193
  return { exitCode: EXIT_TEST.INVALID, line: JSON.stringify({ outcome: "rejected", errors: ["--session-id is required"] }) };
185
194
  }
186
195
  try {
187
- const credential = await (resolveCredential ?? resolveAgentSessionCredential)({ argv, env, cwd });
196
+ const credential = await (resolveCredential ?? resolveAgentSessionCredential)({ argv, env, cwd, sessionId: opts.sessionId });
188
197
  managedCredential = credential;
189
198
  opts.sessionToken = credential.token;
190
- opts.sessionId = credential.sessionId ?? null;
199
+ // --session-id is a deliberate caller choice. The resolved token proves
200
+ // authority; it must not silently replace the session the caller named.
201
+ opts.sessionId ??= credential.sessionId ?? null;
191
202
  } catch (error) {
192
203
  const code = error?.code ?? "agent_session_unavailable";
193
204
  process.stderr.write(`botbuddy test: ${error?.message ?? code}\n`);