@botbuddy/cli 1.66.0 → 1.67.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/CHANGELOG.md CHANGED
@@ -1,5 +1,9 @@
1
1
  # Changelog
2
2
 
3
+ ## 1.67.0
4
+
5
+ - New `bb release-notes submit <note.json>` submits one governed release note for your tenant through the MCP tool `submit_release_note`. It sends the file unchanged, prints the stored source key, state and revision, and exits 1 with the named refusal (`schema_invalid`, `type_unknown`, `markup_refused`, `link_refused`, `entitlement_missing`) when nothing was stored.
6
+
3
7
  ## 1.64.26
4
8
 
5
9
  - `bb stack run --remote` (and auto runs placed remotely) never binds a local port in the leased serving band 61000–64999 any more. SSH forwards whose remote number falls in that band are remapped to a port outside it, remapped and probe ports are picked from explicit out-of-band candidates instead of the kernel's ephemeral range, and the requester origin and native callback gateway are reserved outside the band too. A `--callback-port` inside the band is refused with `CALLBACK_PORT_RESERVED` before anything is probed or spawned. This stops a remote lease's tunnel from taking ports a co-located stack on the same machine was just given.
@@ -1,7 +1,7 @@
1
1
  {
2
- "release_version": "1.66.0",
3
- "source_commit": "6eb7fc060d0908254af17808ce9f110094386a2b",
4
- "source_sha256": "fe2df8ccaaaf182e5e2ac8def3be1401adce09dd48a47ccc713ccb7afc04a7e8",
2
+ "release_version": "1.67.0",
3
+ "source_commit": "268168df0f1ca0c6e45a7650488ecc1e24e06401",
4
+ "source_sha256": "28a9fb83409d1d081474ef61a1a8f025837c54284d5809b4d9ebec87c0ee4e7d",
5
5
  "target": "darwin-arm64",
6
- "sha256": "e7c4da070c2c016743963605c408d7ebb1f16c8c36d304fe52045b7dfc9d9dd1"
6
+ "sha256": "f75345368910de3b008e4796e6469c60e9026b9db732baadb14fa994b2982cf1"
7
7
  }
@@ -1,7 +1,7 @@
1
1
  {
2
- "release_version": "1.66.0",
3
- "source_commit": "6eb7fc060d0908254af17808ce9f110094386a2b",
4
- "source_sha256": "fe2df8ccaaaf182e5e2ac8def3be1401adce09dd48a47ccc713ccb7afc04a7e8",
2
+ "release_version": "1.67.0",
3
+ "source_commit": "268168df0f1ca0c6e45a7650488ecc1e24e06401",
4
+ "source_sha256": "28a9fb83409d1d081474ef61a1a8f025837c54284d5809b4d9ebec87c0ee4e7d",
5
5
  "target": "linux-x64",
6
- "sha256": "5e43e75f6a6b2143b00de322fe954811fbfb4f0cda694f80fd44fa3c39bbae3e"
6
+ "sha256": "9c1fb4e157e1b86f84b7efda1a3ab25f9e869a9d09a1c30093c3ef4dc67e928f"
7
7
  }
@@ -1,21 +1,21 @@
1
1
  {
2
2
  "schema_version": 1,
3
- "release_version": "1.66.0",
4
- "source_commit": "6eb7fc060d0908254af17808ce9f110094386a2b",
5
- "source_sha256": "fe2df8ccaaaf182e5e2ac8def3be1401adce09dd48a47ccc713ccb7afc04a7e8",
3
+ "release_version": "1.67.0",
4
+ "source_commit": "268168df0f1ca0c6e45a7650488ecc1e24e06401",
5
+ "source_sha256": "28a9fb83409d1d081474ef61a1a8f025837c54284d5809b4d9ebec87c0ee4e7d",
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": "e7c4da070c2c016743963605c408d7ebb1f16c8c36d304fe52045b7dfc9d9dd1",
11
+ "sha256": "f75345368910de3b008e4796e6469c60e9026b9db732baadb14fa994b2982cf1",
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": "5e43e75f6a6b2143b00de322fe954811fbfb4f0cda694f80fd44fa3c39bbae3e",
18
+ "sha256": "9c1fb4e157e1b86f84b7efda1a3ab25f9e869a9d09a1c30093c3ef4dc67e928f",
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.66.0",
3
+ "version": "1.67.0",
4
4
  "description": "BotBuddy — Swarm coordination CLI for multi-agent workflows",
5
5
  "type": "module",
6
6
  "bin": {
@@ -126,8 +126,19 @@ export function repairFailureRecovery(code, error = null) {
126
126
  */
127
127
  function applyStackLeaseCapability(report, capability) {
128
128
  if (!capability || typeof capability.status !== "string") return;
129
- report.stack_lease.capability = capability.status;
129
+ // BOT-3144: the server names this owner's own revoked (disabled) host record; say
130
+ // "refused" rather than a generic "unattested", and make its fix the recovery.
131
+ const revoked = typeof capability.host_state === "string" && capability.host_state.length > 0 && capability.status !== "established";
132
+ report.stack_lease.capability = revoked ? "refused" : capability.status;
130
133
  if (capability.reason) report.stack_lease.reason = capability.reason;
134
+ if (revoked) {
135
+ report.stack_lease.host_state = capability.host_state;
136
+ if (typeof capability.host_key === "string") report.stack_lease.host_key = capability.host_key;
137
+ // Codex #1609 P2: the server names the fix (withdrawn consent needs the Helper first).
138
+ if (typeof capability.fix === "string" && capability.fix) report.stack_lease.fix = capability.fix;
139
+ if (!report.recovery) report.recovery = capability.reason ?? `this machine's host record is ${capability.host_state}; set it to maintenance or re-enable it on /docker, then run \`bb doctor --fix\``;
140
+ }
141
+ if (typeof capability.checked_at === "string") report.stack_lease.checked_at = capability.checked_at;
131
142
  if (capability.reported_host && capability.enrolled_host) {
132
143
  report.stack_lease.hostname_drift = {
133
144
  reported: capability.reported_host,
@@ -207,6 +218,9 @@ export async function doctorAgentAuth({
207
218
  remediation: "ticketed Codex requires --codex-thread-id <UUID>; ticketed Claude Desktop requires CLAUDE_CODE_HOST_SESSION_ID=local_<uuid>; use --no-ticket-reason for terminal or cloud work without a native link",
208
219
  },
209
220
  };
221
+ // BOT-3144: a plain `bb doctor` registers nothing, so report the verdict cached by the
222
+ // last registration (with when it was checked) instead of a bare `unverified`.
223
+ if (fresh && !fix) applyStackLeaseCapability(report, state.stack_lease_capability);
210
224
  let credential = fresh
211
225
  ? { token: state.agent_session_token, sessionId: state.session_id, agentId: state.agent_id, source: "cache" }
212
226
  : null;
@@ -439,6 +439,11 @@ export async function selfHealAgentSession({
439
439
  ...(typeof data.session_token_generation === "string" && data.session_token_generation
440
440
  ? { session_token_generation: data.session_token_generation }
441
441
  : {}),
442
+ // BOT-3144: the registration's stack-lease verdict, so a plain `bb doctor` can
443
+ // report "refused" for a revoked host record instead of `unverified`.
444
+ ...(data.stack_lease_capability && typeof data.stack_lease_capability.status === "string"
445
+ ? { stack_lease_capability: { ...data.stack_lease_capability, checked_at: issued.toISOString() } }
446
+ : {}),
442
447
  };
443
448
  if (renewal && !isAgentStateFresh(state, issued, 0)) {
444
449
  throw missing("session_mint_invalid", "register_agent returned an invalid replacement session record.");
package/src/commands.mjs CHANGED
@@ -19,6 +19,7 @@ import { cmdRun } from "./run.mjs";
19
19
  import { cmdTest } from "./test-lane.mjs";
20
20
  import { cmdEvidence } from "./evidence.mjs";
21
21
  import { cmdFeedback } from "./feedback.mjs";
22
+ import { cmdReleaseNotes } from "./release-notes.mjs";
22
23
  import { runWait } from "./wait.mjs";
23
24
  import { SETUP_BLOCK } from "./setup-block.mjs";
24
25
  import { green, red, cyan, dim, bold, die } from "./utils.mjs";
@@ -63,6 +64,7 @@ export function shouldCheckForUpdates(argv) {
63
64
  export const LOCAL_HELP_COMMANDS = Object.freeze([
64
65
  "wait", "stack", "pw", "docker", "run", "mcp", "login", "setup", "doctor", "host",
65
66
  "staging-pool", "daemon", "evidence", "feedback", "usage", "telemetry", "allocation",
67
+ "release-notes",
66
68
  ]);
67
69
  const HELP_WORDS = new Set(["help", "--help", "-h"]);
68
70
 
@@ -143,6 +145,8 @@ export async function run(argv, {
143
145
  case "test": return cmdTest(args);
144
146
  case "evidence": return cmdEvidence(args);
145
147
  case "feedback": return cmdFeedback(args);
148
+ // BOT-3156: governed release notes (submit one note through submit_release_note).
149
+ case "release-notes": return cmdReleaseNotes(args);
146
150
  case "test-ingest": return cmdTestIngest(args);
147
151
  case "wait": return runWait(args);
148
152
  case "pw": return runPw(args);
@@ -329,6 +333,8 @@ ${bold("DURABLE WORKLOADS")} (${cyan("bb help run")})
329
333
  ${bold("EVIDENCE")} (${cyan("bb help evidence")})
330
334
  feedback init|submit --file <report.json>
331
335
  Prepare or send a BotBuddy issue report
336
+ release-notes submit <note.json>
337
+ Submit one release note for your tenant (stored as draft)
332
338
  evidence require <file> Record an exact-head evidence disposition → botbuddy/ready
333
339
  evidence publish <file> Publish a local evidence statement → botbuddy/ready check-run
334
340
 
@@ -423,7 +429,12 @@ export async function cmdDoctor(args, {
423
429
  log(`agent session: ${result.report.session.status}${result.report.session.minted_with ? ` (minted_with: ${result.report.session.minted_with})` : ""}`);
424
430
  if (result.report.stack_lease) {
425
431
  const sl = result.report.stack_lease;
426
- log(`stack lease: ${sl.capability} (host ${sl.host}, machine id ${sl.machine_id_readable ? "readable" : "unavailable"})${sl.reason ? ` — ${sl.reason}` : ""}${publish ? ` ${formatPublishIp(publish)}` : ""}`);
432
+ // BOT-3144: a revoked host record leads with the cause and the fix.
433
+ if (sl.capability === "refused" && sl.host_state) {
434
+ log(`stack lease: refused — this machine's host record is ${sl.host_state}${sl.host_key ? ` (${sl.host_key})` : ""}: ${sl.fix ?? "set it to maintenance or re-enable it on /docker, then run \`bb doctor --fix\`"} (host ${sl.host}${sl.checked_at ? `, checked ${sl.checked_at}` : ""})`);
435
+ } else {
436
+ log(`stack lease: ${sl.capability} (host ${sl.host}, machine id ${sl.machine_id_readable ? "readable" : "unavailable"})${sl.reason ? ` — ${sl.reason}` : ""}${publish ? ` ${formatPublishIp(publish)}` : ""}`);
437
+ }
427
438
  if (publish && !isLoopbackIp(publish.publish_ip)) {
428
439
  log(`stack publish: leased stacks on this host are reachable beyond loopback (\`bb stack up --local-exec\` refuses; the Helper refuses ports_exposed_beyond_loopback) — fix: ${publishIpFix(publish.engine)}`);
429
440
  }
@@ -0,0 +1,102 @@
1
+ // BOT-3156: `bb release-notes submit <note.json>` — submit one governed release
2
+ // note through the MCP tool `submit_release_note`. The server is the only
3
+ // validator: the CLI sends the file's JSON unchanged (no rewriting, no filling
4
+ // in), records the route as `cli`, and prints the stored source key, state and
5
+ // revision, or the named refusal. The tenant is the authenticated agent's.
6
+ import { readFile } from "node:fs/promises";
7
+ import { CALL_EXIT, callToolJson, resolveCallAuth } from "./api.mjs";
8
+
9
+ export const RELEASE_NOTES_USAGE = `Usage: bb release-notes submit <note.json> [--json]
10
+
11
+ Submits one release note for your tenant. The file is a schema-1 payload:
12
+
13
+ { "schema_version": 1,
14
+ "source_key": "BOT-1234",
15
+ "type": "Feature",
16
+ "internal": { "title": "…", "body": "…" },
17
+ "external": { "title": "…", "body": "…" },
18
+ "internal_links": [ { "name": "PR #12", "url": "https://github.com/…", "kind": "pr" } ],
19
+ "external_links": [ { "name": "Open the report", "url": "/reports?tab=x", "kind": "deep_link" } ] }
20
+
21
+ type is one of Feature, Improvement, Fix, Security, Deprecation, Internal.
22
+ external and both link lists are optional. A body uses bbrn-md-1: paragraphs,
23
+ "- " lists, **bold**, *italic* and \`code\`; a backslash escapes only \\ * \` and <.
24
+ No HTML and no links inside a body. A link url is https://… or an app path
25
+ starting with one /. Write the text yourself: BotBuddy stores exactly what you
26
+ submit.
27
+
28
+ Submitting the same source_key again updates the note and adds a revision.
29
+ An unchanged resubmit (a retry) adds none. A refusal names its code
30
+ (schema_invalid, type_unknown, markup_refused, link_refused,
31
+ entitlement_missing) and stores nothing.
32
+
33
+ Exit codes: 0 stored · 1 refused · 2 usage · 3 authentication · 5 BotBuddy unreachable.`;
34
+
35
+ const TOOL = "submit_release_note";
36
+
37
+ export async function cmdReleaseNotes(args, {
38
+ read = readFile,
39
+ resolveAuth = resolveCallAuth,
40
+ call = callToolJson,
41
+ log = console.log,
42
+ errorLog = console.error,
43
+ } = {}) {
44
+ if (args.length === 0 || ["help", "--help", "-h"].includes(args[0])) {
45
+ (args.length === 0 ? errorLog : log)(RELEASE_NOTES_USAGE);
46
+ return args.length === 0 ? 2 : CALL_EXIT.OK;
47
+ }
48
+ const json = args.includes("--json");
49
+ const rest = args.filter((a) => a !== "--json");
50
+ if (rest[0] !== "submit" || rest.length !== 2 || !rest[1] || rest[1].startsWith("-")) {
51
+ errorLog(RELEASE_NOTES_USAGE);
52
+ return 2;
53
+ }
54
+ const file = rest[1];
55
+ let note;
56
+ try {
57
+ note = JSON.parse(await read(file, "utf8"));
58
+ } catch {
59
+ errorLog(`Could not read ${file} as JSON; nothing was submitted.`);
60
+ return 2;
61
+ }
62
+
63
+ let result;
64
+ try {
65
+ const resolved = await resolveAuth({ toolName: TOOL, preferSession: true });
66
+ result = resolved?.error || !resolved?.auth
67
+ ? { ok: false, auth: true }
68
+ : await call(TOOL, { note, route: "cli" }, { auth: resolved.auth, signal: AbortSignal.timeout(30_000) });
69
+ } catch {
70
+ result = { ok: false, status: null };
71
+ }
72
+
73
+ const data = result?.data;
74
+ if (result?.ok && data?.ok === true && typeof data.source_key === "string" && Number.isInteger(data.revision)) {
75
+ const receipt = { ok: true, source_key: data.source_key, state: data.state, revision: data.revision, created: data.created === true, changed: data.changed === true, tenant: data.tenant ?? null };
76
+ if (json) log(JSON.stringify(receipt));
77
+ else {
78
+ const what = receipt.created ? "stored" : receipt.changed ? "updated" : "unchanged (no new revision)";
79
+ log(`Release note ${receipt.source_key} ${what}: state ${receipt.state}, revision ${receipt.revision}${receipt.tenant ? `, tenant ${receipt.tenant}` : ""}.`);
80
+ }
81
+ return CALL_EXIT.OK;
82
+ }
83
+ if (result?.ok && data?.ok === false && typeof data.error === "string") {
84
+ const refusal = { ok: false, error: data.error, ...(data.field ? { field: data.field } : {}), message: data.message ?? null };
85
+ if (json) log(JSON.stringify(refusal));
86
+ errorLog(`✗ ${refusal.error}${refusal.field ? ` (${refusal.field})` : ""}: ${refusal.message ?? "Nothing was stored."}`);
87
+ return CALL_EXIT.ERROR;
88
+ }
89
+ if (result?.auth) {
90
+ if (json) log(JSON.stringify({ ok: false, error: "authentication_failed" }));
91
+ errorLog("✗ BotBuddy could not authenticate this command. Run bb doctor --fix in this worktree, then retry. Nothing was stored.");
92
+ return CALL_EXIT.AUTH;
93
+ }
94
+ if (!result?.ok && result?.status == null) {
95
+ if (json) log(JSON.stringify({ ok: false, error: "transport_unavailable" }));
96
+ errorLog("✗ BotBuddy is unreachable. Retry the same command; an unchanged resubmit adds no revision.");
97
+ return CALL_EXIT.TRANSPORT;
98
+ }
99
+ if (json) log(JSON.stringify({ ok: false, error: "receipt_unavailable" }));
100
+ errorLog("✗ BotBuddy did not confirm the submit. Retry the same command; an unchanged resubmit adds no revision.");
101
+ return CALL_EXIT.ERROR;
102
+ }