@botbuddy/cli 1.60.10 → 1.61.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.60.10",
3
- "source_commit": "311044889f28c3b22902954ec29c3c117f46d638",
4
- "source_sha256": "a82e951ccda7aa65571953e874a01cf30f75bc218ed5592a2bffa2e7b0d3219a",
2
+ "release_version": "1.61.0",
3
+ "source_commit": "5661e7d4d29c975fc925b0a6c47741db33598642",
4
+ "source_sha256": "c5c57ed492234165bee179842456b3bebce505b36145e96551c0ddbd0cdf605a",
5
5
  "target": "darwin-arm64",
6
- "sha256": "c078605d49201ad3532212bbac322216b06c108c425024a7f68f6ecdab89fd5a"
6
+ "sha256": "afadb8e224f68209f4a2a0f8367d4aa289a76ac21f2ee57cbd376b90ab569469"
7
7
  }
@@ -1,7 +1,7 @@
1
1
  {
2
- "release_version": "1.60.10",
3
- "source_commit": "311044889f28c3b22902954ec29c3c117f46d638",
4
- "source_sha256": "a82e951ccda7aa65571953e874a01cf30f75bc218ed5592a2bffa2e7b0d3219a",
2
+ "release_version": "1.61.0",
3
+ "source_commit": "5661e7d4d29c975fc925b0a6c47741db33598642",
4
+ "source_sha256": "c5c57ed492234165bee179842456b3bebce505b36145e96551c0ddbd0cdf605a",
5
5
  "target": "linux-x64",
6
- "sha256": "af6bfdf5e66b133356c620a5dd9aaacc1cee11d8f0f594bb8f5e8447cb932704"
6
+ "sha256": "830ff851e5d9d85957855af52a7c372cde56e2f9ddb315a31f5d2477c1c7d9d7"
7
7
  }
@@ -1,21 +1,21 @@
1
1
  {
2
2
  "schema_version": 1,
3
- "release_version": "1.60.10",
4
- "source_commit": "311044889f28c3b22902954ec29c3c117f46d638",
5
- "source_sha256": "a82e951ccda7aa65571953e874a01cf30f75bc218ed5592a2bffa2e7b0d3219a",
3
+ "release_version": "1.61.0",
4
+ "source_commit": "5661e7d4d29c975fc925b0a6c47741db33598642",
5
+ "source_sha256": "c5c57ed492234165bee179842456b3bebce505b36145e96551c0ddbd0cdf605a",
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": "c078605d49201ad3532212bbac322216b06c108c425024a7f68f6ecdab89fd5a",
11
+ "sha256": "afadb8e224f68209f4a2a0f8367d4aa289a76ac21f2ee57cbd376b90ab569469",
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": "af6bfdf5e66b133356c620a5dd9aaacc1cee11d8f0f594bb8f5e8447cb932704",
18
+ "sha256": "830ff851e5d9d85957855af52a7c372cde56e2f9ddb315a31f5d2477c1c7d9d7",
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.10",
3
+ "version": "1.61.0",
4
4
  "description": "BotBuddy \u2014 Swarm coordination CLI for multi-agent workflows",
5
5
  "type": "module",
6
6
  "bin": {
@@ -19,9 +19,7 @@
19
19
  "daemon/",
20
20
  "daemon-release/",
21
21
  "src/",
22
- "!src/**/*.test.mjs",
23
- "!src/staging-pool/test-helpers/",
24
- "!src/staging-pool/fixtures/"
22
+ "!src/**/*.test.mjs"
25
23
  ],
26
24
  "keywords": [
27
25
  "mcp",
package/src/commands.mjs CHANGED
@@ -284,6 +284,7 @@ ${bold("STAGING SIGN-IN")} (BOT-2084)
284
284
 
285
285
  ${bold("OTHER")}
286
286
  locks -m [--host name] Reserve typed local resources, including Playwright MCP lanes
287
+ evidence require <file> Record an exact-head evidence disposition → botbuddy/ready
287
288
  evidence publish <file> Publish a local evidence statement → botbuddy/ready check-run
288
289
  update Install the latest @botbuddy/cli in the background via npm
289
290
  help Show this help
package/src/evidence.mjs CHANGED
@@ -1,6 +1,9 @@
1
1
  // BOT-2341: `bb evidence publish <statement.json>` — post a local-E2E evidence
2
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.
3
+ // creates/updates the `botbuddy/ready` check-run on the PR head.
4
+ // BOT-786: `bb evidence require <requirement.json>` — record the impact
5
+ // producer's exact-head disposition (MCP `publish_evidence_requirement`):
6
+ // not_required completes botbuddy/ready as success on that head, e2e queues it.
4
7
  //
5
8
  // Auth prefers this worktree's agent session (the tenant the repo is bound to),
6
9
  // falling back to the owner/MCP-key ladder like every other `bb` call. Prints
@@ -11,14 +14,25 @@ import { CALL_EXIT, callToolJson as defaultCallToolJson, resolveCallAuth } from
11
14
 
12
15
  export const EVIDENCE_EXIT = Object.freeze({ ...CALL_EXIT, USAGE: 2, REFUSED: 4 });
13
16
 
14
- const USAGE = `Usage: bb evidence publish <statement.json>
17
+ const USAGE = `Usage: bb evidence require <requirement.json>
18
+ bb evidence publish <statement.json>
15
19
 
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
+ require Record the impact producer's decision for one exact PR head. The file
21
+ is a JSON object with: repo, pr, headSha, disposition
22
+ ("not_required" | "e2e"), reason, command (required for e2e).
23
+ not_required completes botbuddy/ready as success on that head; e2e
24
+ queues it with the command to run.
25
+ publish Publish a local evidence statement for a PR head (after an e2e
26
+ requirement). The statement is a JSON object with:
27
+ repo, pr, headSha, mainSha, base, fingerprint, requirement, runs[]
28
+ (each with the BotBuddy runId), verdict ("pass" | "fail"), fixCommand
20
29
 
21
- Exit codes: 0 published · 2 usage · 3 auth · 4 refused · 5 unreachable`;
30
+ Exit codes: 0 recorded · 2 usage · 3 auth · 4 refused · 5 unreachable`;
31
+
32
+ const SUBCOMMANDS = Object.freeze({
33
+ require: { tool: "publish_evidence_requirement", argName: "requirement" },
34
+ publish: { tool: "publish_evidence", argName: "statement" },
35
+ });
22
36
 
23
37
  export async function cmdEvidence(args, {
24
38
  resolveAuth = resolveCallAuth,
@@ -31,7 +45,8 @@ export async function cmdEvidence(args, {
31
45
  log(USAGE);
32
46
  return sub === undefined ? EVIDENCE_EXIT.USAGE : EVIDENCE_EXIT.OK;
33
47
  }
34
- if (sub !== "publish") {
48
+ const spec = Object.hasOwn(SUBCOMMANDS, sub) ? SUBCOMMANDS[sub] : null;
49
+ if (!spec) {
35
50
  errorLog(`Unknown evidence subcommand: ${sub}\n\n${USAGE}`);
36
51
  return EVIDENCE_EXIT.USAGE;
37
52
  }
@@ -40,22 +55,22 @@ export async function cmdEvidence(args, {
40
55
  return EVIDENCE_EXIT.USAGE;
41
56
  }
42
57
 
43
- let statement;
58
+ let body;
44
59
  try {
45
- statement = JSON.parse(readFileSync(file, "utf8"));
60
+ body = JSON.parse(readFileSync(file, "utf8"));
46
61
  } catch (error) {
47
62
  errorLog(`✗ Could not read ${file} as JSON: ${error?.message ?? error}`);
48
63
  return EVIDENCE_EXIT.USAGE;
49
64
  }
50
65
 
51
- const resolved = await resolveAuth({ toolName: "publish_evidence", preferSession: true });
66
+ const resolved = await resolveAuth({ toolName: spec.tool, preferSession: true });
52
67
  if (resolved?.error || !resolved?.auth) {
53
68
  log(JSON.stringify(resolved?.error ?? { ok: false, auth: true, error: "not_authenticated" }, null, 2));
54
69
  errorLog("✗ Not authenticated — run `bb doctor --fix` in this worktree.");
55
70
  return EVIDENCE_EXIT.AUTH;
56
71
  }
57
72
 
58
- const res = await callToolJson("publish_evidence", { statement }, { auth: resolved.auth });
73
+ const res = await callToolJson(spec.tool, { [spec.argName]: body }, { auth: resolved.auth });
59
74
  if (!res.ok) {
60
75
  log(JSON.stringify(res, null, 2));
61
76
  if (res.auth) {
@@ -66,15 +81,24 @@ export async function cmdEvidence(args, {
66
81
  errorLog(`✗ BotBuddy unreachable: ${res.error}`);
67
82
  return EVIDENCE_EXIT.TRANSPORT;
68
83
  }
69
- errorLog(`✗ publish_evidence failed: ${res.error}`);
84
+ errorLog(`✗ ${spec.tool} failed: ${res.error}`);
70
85
  return EVIDENCE_EXIT.ERROR;
71
86
  }
72
87
 
73
88
  log(JSON.stringify(res.data, null, 2));
74
89
  if (res.isError || res.data?.ok === false) {
75
- errorLog(`✗ ${res.data?.error ?? "publish_evidence refused"}: ${res.data?.message ?? ""}`.trimEnd());
90
+ errorLog(`✗ ${res.data?.error ?? `${spec.tool} refused`}: ${res.data?.message ?? ""}`.trimEnd());
76
91
  return EVIDENCE_EXIT.REFUSED;
77
92
  }
93
+ if (sub === "require") {
94
+ // Success means the control plane echoed the recorded disposition — an
95
+ // outage body that normalises to an empty result is never "recorded".
96
+ if (res.data?.ok !== true || typeof res.data?.disposition !== "string") {
97
+ errorLog("✗ BotBuddy returned no disposition — nothing is known to be recorded; retry.");
98
+ return EVIDENCE_EXIT.ERROR;
99
+ }
100
+ return EVIDENCE_EXIT.OK;
101
+ }
78
102
  // Codex #1169 R4: success means a receipt came back — an outage body that
79
103
  // normalises to an empty result is never "published".
80
104
  if (res.data?.ok !== true || typeof res.data?.receipt_id !== "string") {
@@ -0,0 +1,11 @@
1
+ {
2
+ "_captured": "2026-10-05 from `botbuddy call get_totp_code --json {app_slug:supplyspark-ent, seed_user_slug:seed-super-admin@supplyguard.local, environment:local}` (@botbuddy/cli 1.40.4, launchd-like env -i). Key set and value types are real; the code value is replaced and remaining_seconds is illustrative.",
3
+ "app_slug": "supplyspark-ent",
4
+ "seed_user_slug": "seed-super-admin@supplyguard.local",
5
+ "environment": "local",
6
+ "authz": "org",
7
+ "code": "000000",
8
+ "remaining_seconds": 17,
9
+ "digits": 6,
10
+ "period": 30
11
+ }
@@ -0,0 +1,37 @@
1
+ {
2
+ "_captured": "2026-10-05 against the ENT-5120 isolated local stack (GoTrue v2.196.0; staging reported v2.197.0 the same day). Error bodies show the error_code and msg fields as returned; tokens, which are never recorded. Experiment: scratch rotation-experiment.mjs, recorded on ENT-5120.",
3
+ "challengeAfterAal1SessionDeleted": {
4
+ "status": 403,
5
+ "body": {
6
+ "error_code": "session_not_found",
7
+ "msg": "Session from session_id claim in JWT does not exist"
8
+ }
9
+ },
10
+ "refreshAfterSessionDeleted": {
11
+ "status": 400,
12
+ "body": {
13
+ "error_code": "refresh_token_not_found",
14
+ "msg": "Invalid Refresh Token: Refresh Token Not Found"
15
+ }
16
+ },
17
+ "aal2SessionRefreshAfterAnotherMfaVerify": {
18
+ "status": 200,
19
+ "aal": "aal2"
20
+ },
21
+ "adminDeleteFactor": {
22
+ "status": 200,
23
+ "sessionsBefore": "4 sessions, all aal2",
24
+ "sessionsAfter": "4 sessions, all aal1 (downgraded, not deleted)"
25
+ },
26
+ "refreshAal2SessionAfterAdminDeleteFactor": {
27
+ "status": 200,
28
+ "aal": "aal1"
29
+ },
30
+ "userEndpointWithOldAal2JwtAfterAdminDeleteFactor": {
31
+ "status": 200
32
+ },
33
+ "reEnrolVerifyAfterDowngrade": {
34
+ "sessionsBefore": "4 sessions, all aal1 (downgraded)",
35
+ "sessionsAfter": "1 session (aal2): the downgraded aal1 sessions were deleted by the ensure script re-enrol + MFA verify"
36
+ }
37
+ }
@@ -0,0 +1,41 @@
1
+ // BOT-2084 test helper: pool bindings. SG_* mirrors supplyspark-ent's staging
2
+ // values (the pool's original consumer, ENT-5120) so the lifted tests keep their
3
+ // meaning; BOTBUDDY_* is BotBuddy's registered binding (its registration input).
4
+ import { readFileSync } from "node:fs";
5
+
6
+ import { parsePoolBinding } from "../binding.mjs";
7
+ import { stagingTargetFromBinding } from "../policy.mjs";
8
+
9
+ export const SG_RAW_BINDING = Object.freeze({
10
+ schema_version: 1,
11
+ tenant: "supply-guard",
12
+ staging_session_pool: {
13
+ schema_version: 1,
14
+ pool_id: "supplyspark-ent-staging",
15
+ environment: "staging",
16
+ app_slug: "supplyspark-ent",
17
+ adapter: "supabase-gotrue-totp",
18
+ app_origin: "https://enterprise-staging.vercel.app",
19
+ supabase_url: "https://ikdoydgubernmxzdqtxz.supabase.co",
20
+ publishable_key: "sb_publishable_gysKy2GcBNO0Gfv3d_p60Q_yMbCMUPD",
21
+ storage_key: "sb-ikdoydgubernmxzdqtxz-auth-token",
22
+ production_origins: ["https://app.supply-guard.io", "https://enterprise.vercel.app"],
23
+ password_env_file: "~/supply-guard/shared/.env.staging-personas",
24
+ personas: {
25
+ "super-admin": { email: "seed-super-admin@supplyguard.local", password_env: "SEED_PERSONA_PASSWORD", default_count: 2 },
26
+ "acme-admin": { email: "seed-customer-admin@acme-industrial.test", password_env: "SEED_PERSONA_PASSWORD", default_count: 0 },
27
+ "msp-admin": { email: "seed-msp-admin@supplyguard.local", password_env: "SEED_PERSONA_PASSWORD", default_count: 0 },
28
+ },
29
+ launchd_label: "com.supplyguard.staging-session-pool",
30
+ docs: "docs/2026-07-25-agent-workstation-notes.md#staging-session-pool",
31
+ install_command: "cd ~/supply-guard/development/supplyspark-ent && bb staging-pool install --botbuddy-bin \"$(command -v botbuddy)\"",
32
+ },
33
+ });
34
+ export const SG_BINDING = parsePoolBinding(SG_RAW_BINDING);
35
+ export const SG_TARGET = stagingTargetFromBinding(SG_BINDING);
36
+
37
+ // BOT-2648: BotBuddy's registration input (the staging seed job registers it in
38
+ // the control plane); the repo's .botbuddy-agent.json no longer carries a pool block.
39
+ const { _comment: _note, ...BOTBUDDY_REGISTRATION } = JSON.parse(readFileSync(new URL("../../../../scripts/staging-session-pool.botbuddy.json", import.meta.url), "utf8"));
40
+ export const BOTBUDDY_RAW_BINDING = Object.freeze({ schema_version: 1, ...BOTBUDDY_REGISTRATION });
41
+ export const BOTBUDDY_BINDING = parsePoolBinding(BOTBUDDY_RAW_BINDING);
@@ -0,0 +1,60 @@
1
+ // BOT-2084 (lifted from supplyspark-ent ENT-5120) test helper: an in-memory GoTrue stand-in for pool tests. Every mint
2
+ // returns a fresh aal2 session; refresh rotates the refresh token and enforces the
3
+ // single-holder rule (a refresh token is usable once).
4
+
5
+ let counter = 0;
6
+
7
+ export function fakeJwt(claims) {
8
+ const encode = (value) => Buffer.from(JSON.stringify(value)).toString("base64url");
9
+ return `${encode({ alg: "HS256" })}.${encode(claims)}.sig`;
10
+ }
11
+
12
+ export function fakeStoredSession({ persona, aal = "aal2", sessionId, nowMs = Date.now(), refreshToken } = {}) {
13
+ counter += 1;
14
+ const exp = Math.floor(nowMs / 1000) + 3600;
15
+ return {
16
+ access_token: fakeJwt({ aal, email: persona, exp, session_id: sessionId ?? `sess-${counter}`, amr: [{ method: "totp" }] }),
17
+ token_type: "bearer",
18
+ expires_in: 3600,
19
+ expires_at: exp,
20
+ refresh_token: refreshToken ?? `rt-${process.pid}-${counter}-${Math.random().toString(36).slice(2)}`,
21
+ user: { id: `user-${persona}`, email: persona },
22
+ };
23
+ }
24
+
25
+ export function createFakeSignIn({ nowMs = () => Date.now(), onRefresh, onMint } = {}) {
26
+ const live = new Map(); // refresh_token -> { persona, sessionId, aal }
27
+ const calls = { mint: 0, refresh: 0 };
28
+ return {
29
+ calls,
30
+ live,
31
+ downgrade(sessionId) {
32
+ for (const entry of live.values()) if (entry.sessionId === sessionId) entry.aal = "aal1";
33
+ },
34
+ revokeAll() {
35
+ live.clear();
36
+ },
37
+ async mint({ persona }) {
38
+ calls.mint += 1;
39
+ if (onMint) await onMint({ persona });
40
+ const session = fakeStoredSession({ persona, nowMs: nowMs() });
41
+ const sessionId = JSON.parse(Buffer.from(session.access_token.split(".")[1], "base64url")).session_id;
42
+ live.set(session.refresh_token, { persona, sessionId, aal: "aal2" });
43
+ return session;
44
+ },
45
+ async refresh({ storedSession, persona }) {
46
+ calls.refresh += 1;
47
+ if (onRefresh) {
48
+ const injected = await onRefresh({ storedSession, persona });
49
+ if (injected) return injected;
50
+ }
51
+ const entry = live.get(storedSession.refresh_token);
52
+ if (!entry) return { ok: false, transient: false, reason: "refresh_token_not_found" };
53
+ live.delete(storedSession.refresh_token);
54
+ if (entry.aal !== "aal2") return { ok: false, transient: false, reason: "aal_downgraded" };
55
+ const next = fakeStoredSession({ persona: entry.persona, sessionId: entry.sessionId, nowMs: nowMs() });
56
+ live.set(next.refresh_token, entry);
57
+ return { ok: true, storedSession: next };
58
+ },
59
+ };
60
+ }
@@ -0,0 +1,28 @@
1
+ // BOT-2084 (lifted from supplyspark-ent ENT-5120) test helper: one OS process racing
2
+ // `take` against others on a shared pool dir.
3
+ import { createSessionPool } from "../pool.mjs";
4
+ import { SG_TARGET } from "./bindings.mjs";
5
+ import { fakeStoredSession } from "./fake-signin.mjs";
6
+
7
+ const [poolDir, persona, startAtMs] = process.argv.slice(2);
8
+ const pool = createSessionPool({
9
+ poolDir,
10
+ target: SG_TARGET,
11
+ signIn: {
12
+ mint: async () => {
13
+ throw new Error("workers never mint");
14
+ },
15
+ refresh: async () => ({ ok: true, storedSession: fakeStoredSession({ persona }) }),
16
+ },
17
+ logger: () => {},
18
+ });
19
+
20
+ // Line up all workers on the same instant so the renames genuinely race.
21
+ const delay = Number(startAtMs) - Date.now();
22
+ if (delay > 0) await new Promise((resolve) => setTimeout(resolve, delay));
23
+ try {
24
+ const taken = await pool.take({ persona, owner: `worker-${process.pid}` });
25
+ process.stdout.write(JSON.stringify({ slotId: taken.slotId }));
26
+ } catch (error) {
27
+ process.stdout.write(JSON.stringify({ error: error.code ?? error.message }));
28
+ }