@botbuddy/cli 1.56.0 → 1.57.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.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@botbuddy/cli",
3
- "version": "1.56.0",
3
+ "version": "1.57.0",
4
4
  "description": "BotBuddy \u2014 Swarm coordination CLI for multi-agent workflows",
5
5
  "type": "module",
6
6
  "bin": {
@@ -158,7 +158,7 @@ export async function hasAgentSessionRuntime({
158
158
  /** Resolve without touching the network when an explicit source or fresh cache exists. */
159
159
  export async function resolveAgentSessionCredential({
160
160
  argv = [], env = process.env, cwd = process.cwd(), readState = readAgentState,
161
- isFresh = isAgentStateFresh, selfHeal = selfHealAgentSession,
161
+ isFresh = isAgentStateFresh, selfHeal = selfHealAgentSession, allowSelfHeal = true,
162
162
  // BOT-1682 (Codex R13): callers that have ALREADY parsed the session id from
163
163
  // their own LEADING options (bb pw's runPwInner) pass it here, so this resolver
164
164
  // never re-scans raw argv — where a target-flag VALUE such as
@@ -190,6 +190,7 @@ export async function resolveAgentSessionCredential({
190
190
  warnLegacyResolvedToken(cached.agent_session_token, warn);
191
191
  return withIdentity({ token: cached.agent_session_token, sessionId: explicitSessionId ?? cached.session_id, agentId: cached.agent_id, source: "cache" }, cached);
192
192
  }
193
+ if (!allowSelfHeal) return null;
193
194
  const healed = await selfHeal({ cwd, env, sessionId: explicitSessionId });
194
195
  warnLegacyResolvedToken(healed?.token, warn);
195
196
  return healed;
package/src/commands.mjs CHANGED
@@ -17,6 +17,7 @@ import { cmdManagedAllocation } from "./managed-allocation.mjs";
17
17
  import { cmdDocker } from "./docker-hygiene.mjs";
18
18
  import { cmdRun } from "./run.mjs";
19
19
  import { cmdTest } from "./test-lane.mjs";
20
+ import { cmdEvidence } from "./evidence.mjs";
20
21
  import { runWait } from "./wait.mjs";
21
22
  import { SETUP_BLOCK } from "./setup-block.mjs";
22
23
  import { green, red, cyan, dim, bold, die } from "./utils.mjs";
@@ -31,7 +32,7 @@ import { deliverExecutionEvent } from "./telemetry-delivery.mjs";
31
32
  import { doctorAgentAuth, doctorHelp, parseDoctorArgs, readClientKeyStrict, repairFailureRecovery } from "./agent-doctor.mjs";
32
33
  import { formatPublishIp, isLoopbackIp, probeEnginePublishIp, publishIpFix } from "./docker-publish-ip.mjs";
33
34
  import { cmdHost, fixHelperWorkers, formatHelperWorkersLines, inspectHelperWorkers } from "./helper-host.mjs";
34
- import { clearAgentState, selfHealAgentSession } from "./agent-session.mjs";
35
+ import { clearAgentState, resolveAgentSessionCredential, selfHealAgentSession } from "./agent-session.mjs";
35
36
  import { readAgentState } from "./agent-state.mjs";
36
37
  import { runUsage, usageStatus } from "./usage/sync.mjs";
37
38
  import { cmdTestIngest, doctorTestIngest, formatTestIngestLine } from "./test-ingest.mjs";
@@ -112,6 +113,7 @@ export async function run(argv, {
112
113
  return code;
113
114
  }
114
115
  case "test": return cmdTest(args);
116
+ case "evidence": return cmdEvidence(args);
115
117
  case "test-ingest": return cmdTestIngest(args);
116
118
  case "wait": return runWait(args);
117
119
  case "pw": return runPw(args);
@@ -257,6 +259,7 @@ ${bold("BROWSER LANES")}
257
259
 
258
260
  ${bold("OTHER")}
259
261
  locks -m [--host name] Reserve typed local resources, including Playwright MCP lanes
262
+ evidence publish <file> Publish a local evidence statement → botbuddy/ready check-run
260
263
  update Install the latest @botbuddy/cli in the background via npm
261
264
  help Show this help
262
265
  version Show version
@@ -931,6 +934,10 @@ export async function cmdBootstrap(args, {
931
934
  let harnessSessionId = null;
932
935
  let type = "custom";
933
936
  let codexThreadId = null;
937
+ // Claude Desktop injects this only for a desktop-hosted agent session. The
938
+ // CLI deliberately treats every other environment as absent, so terminal and
939
+ // cloud Claude sessions never manufacture a deep link.
940
+ const claudeDesktopSessionId = process.env.CLAUDE_CODE_HOST_SESSION_ID || null;
934
941
  let branch = null;
935
942
  let ticketId = null;
936
943
  let ticketUrl = null;
@@ -969,6 +976,7 @@ export async function cmdBootstrap(args, {
969
976
  type,
970
977
  harnessSessionId,
971
978
  codexThreadId,
979
+ claudeDesktopSessionId,
972
980
  branch,
973
981
  ticketId,
974
982
  ticketUrl,
@@ -1163,8 +1171,24 @@ export async function cmdLogout({ cwd = process.cwd(), clearState = clearAgentSt
1163
1171
  console.log(`${green("✓")} Logged out. Credentials removed.`);
1164
1172
  }
1165
1173
 
1166
- function cmdHeartbeat(args) {
1167
- return args[0] ? callTool("heartbeat", { current_task: args[0] }) : callTool("heartbeat");
1174
+ export async function cmdHeartbeat(args, {
1175
+ resolveSession = resolveAgentSessionCredential,
1176
+ callTool: call = callTool,
1177
+ } = {}) {
1178
+ // A Desktop identity belongs to the work-graph session that is currently
1179
+ // running. Supply the cached session id with the host-provided value so the
1180
+ // server refreshes that exact session rather than only mutable agent state.
1181
+ const session = await resolveSession({
1182
+ sessionId: process.env.BOTBUDDY_SESSION_ID || null,
1183
+ allowSelfHeal: false,
1184
+ });
1185
+ const claudeDesktopSessionId = process.env.CLAUDE_CODE_HOST_SESSION_ID || null;
1186
+ const heartbeat = {
1187
+ ...(args[0] ? { current_task: args[0] } : {}),
1188
+ ...(typeof session?.sessionId === "string" && session.sessionId ? { session_id: session.sessionId } : {}),
1189
+ ...(claudeDesktopSessionId ? { claude_desktop_session_id: claudeDesktopSessionId } : {}),
1190
+ };
1191
+ return call("heartbeat", heartbeat, session?.token ? { auth: { "x-agent-api-key": session.token } } : undefined);
1168
1192
  }
1169
1193
 
1170
1194
  // ─── BOT-1607: `botbuddy mcp` — the tier-2 bb_mcp_ config key ────────────────
@@ -0,0 +1,85 @@
1
+ // BOT-2341: `bb evidence publish <statement.json>` — post a local-E2E evidence
2
+ // statement to the control plane (MCP `publish_evidence`), which stores it and
3
+ // creates/updates the advisory `botbuddy/ready` check-run on the PR head.
4
+ //
5
+ // Auth prefers this worktree's agent session (the tenant the repo is bound to),
6
+ // falling back to the owner/MCP-key ladder like every other `bb` call. Prints
7
+ // the tool's JSON result on stdout; a refusal exits non-zero.
8
+
9
+ import { readFileSync } from "node:fs";
10
+ import { CALL_EXIT, callToolJson as defaultCallToolJson, resolveCallAuth } from "./api.mjs";
11
+
12
+ export const EVIDENCE_EXIT = Object.freeze({ ...CALL_EXIT, USAGE: 2, REFUSED: 4 });
13
+
14
+ const USAGE = `Usage: bb evidence publish <statement.json>
15
+
16
+ Publish a local evidence statement for a PR head. BotBuddy stores it and posts
17
+ the advisory botbuddy/ready check-run. The statement is a JSON object with:
18
+ repo, pr, headSha, mainSha, base, fingerprint, requirement, runs[],
19
+ verdict ("pass" | "fail"), fixCommand
20
+
21
+ Exit codes: 0 published · 2 usage · 3 auth · 4 refused · 5 unreachable`;
22
+
23
+ export async function cmdEvidence(args, {
24
+ resolveAuth = resolveCallAuth,
25
+ callToolJson = defaultCallToolJson,
26
+ log = (line) => console.log(line),
27
+ errorLog = (line) => console.error(line),
28
+ } = {}) {
29
+ const [sub, file, ...rest] = args;
30
+ if (sub === undefined || sub === "help" || sub === "--help" || sub === "-h") {
31
+ log(USAGE);
32
+ return sub === undefined ? EVIDENCE_EXIT.USAGE : EVIDENCE_EXIT.OK;
33
+ }
34
+ if (sub !== "publish") {
35
+ errorLog(`Unknown evidence subcommand: ${sub}\n\n${USAGE}`);
36
+ return EVIDENCE_EXIT.USAGE;
37
+ }
38
+ if (!file || rest.length > 0) {
39
+ errorLog(USAGE);
40
+ return EVIDENCE_EXIT.USAGE;
41
+ }
42
+
43
+ let statement;
44
+ try {
45
+ statement = JSON.parse(readFileSync(file, "utf8"));
46
+ } catch (error) {
47
+ errorLog(`✗ Could not read ${file} as JSON: ${error?.message ?? error}`);
48
+ return EVIDENCE_EXIT.USAGE;
49
+ }
50
+
51
+ const resolved = await resolveAuth({ toolName: "publish_evidence", preferSession: true });
52
+ if (resolved?.error || !resolved?.auth) {
53
+ log(JSON.stringify(resolved?.error ?? { ok: false, auth: true, error: "not_authenticated" }, null, 2));
54
+ errorLog("✗ Not authenticated — run `bb doctor --fix` in this worktree.");
55
+ return EVIDENCE_EXIT.AUTH;
56
+ }
57
+
58
+ const res = await callToolJson("publish_evidence", { statement }, { auth: resolved.auth });
59
+ if (!res.ok) {
60
+ log(JSON.stringify(res, null, 2));
61
+ if (res.auth) {
62
+ errorLog(`✗ BotBuddy rejected the credential (${res.error_code ?? res.code ?? res.error}).`);
63
+ return EVIDENCE_EXIT.AUTH;
64
+ }
65
+ if (res.status === null || String(res.error ?? "").startsWith("transport")) {
66
+ errorLog(`✗ BotBuddy unreachable: ${res.error}`);
67
+ return EVIDENCE_EXIT.TRANSPORT;
68
+ }
69
+ errorLog(`✗ publish_evidence failed: ${res.error}`);
70
+ return EVIDENCE_EXIT.ERROR;
71
+ }
72
+
73
+ log(JSON.stringify(res.data, null, 2));
74
+ if (res.isError || res.data?.ok === false) {
75
+ errorLog(`✗ ${res.data?.error ?? "publish_evidence refused"}: ${res.data?.message ?? ""}`.trimEnd());
76
+ return EVIDENCE_EXIT.REFUSED;
77
+ }
78
+ // Codex #1169 R4: success means a receipt came back — an outage body that
79
+ // normalises to an empty result is never "published".
80
+ if (res.data?.ok !== true || typeof res.data?.receipt_id !== "string") {
81
+ errorLog("✗ BotBuddy returned no receipt — nothing is known to be published; retry.");
82
+ return EVIDENCE_EXIT.ERROR;
83
+ }
84
+ return EVIDENCE_EXIT.OK;
85
+ }
@@ -130,6 +130,7 @@ async function recordOrphanAndFail({ session, cwd, retirementError, primaryError
130
130
  export async function heartbeatHarnessSession({
131
131
  session,
132
132
  codexThreadId = null,
133
+ claudeDesktopSessionId = null,
133
134
  model = null,
134
135
  call = callToolJson,
135
136
  cwd = process.cwd(),
@@ -141,6 +142,7 @@ export async function heartbeatHarnessSession({
141
142
  const response = await call("heartbeat", {
142
143
  session_id: session.session_id,
143
144
  ...(codexThreadId ? { codex_thread_id: codexThreadId } : {}),
145
+ ...(claudeDesktopSessionId ? { claude_desktop_session_id: claudeDesktopSessionId } : {}),
144
146
  ...(model ? { model } : {}),
145
147
  }, { auth: { "x-agent-api-key": session.session_token } });
146
148
  if (!response?.ok || response.isError) {
@@ -212,6 +214,7 @@ export async function bootstrapHarnessSession({
212
214
  type = "codex",
213
215
  harnessSessionId,
214
216
  codexThreadId = null,
217
+ claudeDesktopSessionId = null,
215
218
  cwd = process.cwd(),
216
219
  branch = null,
217
220
  ticketId = null,
@@ -273,6 +276,7 @@ export async function bootstrapHarnessSession({
273
276
  ...(prNumber != null ? { pr_number: prNumber } : {}),
274
277
  ...(prUrl ? { pr_url: prUrl } : {}),
275
278
  ...(codexThreadId ? { codex_thread_id: codexThreadId } : {}),
279
+ ...(claudeDesktopSessionId ? { claude_desktop_session_id: claudeDesktopSessionId } : {}),
276
280
  ...(model ? { model } : {}),
277
281
  ...(effort ? { effort } : {}),
278
282
  harness,