@botbuddy/cli 1.6.0 → 1.6.2

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.6.0",
3
+ "version": "1.6.2",
4
4
  "description": "BotBuddy — Swarm coordination CLI for multi-agent workflows",
5
5
  "type": "module",
6
6
  "bin": {
@@ -0,0 +1 @@
1
+ {"schema_version":1,"source_version":"1.6.1","source_identity":"9cc6a78baeb0da589214386daad729a2e8909936a2407da801777f9902e8667f"}
@@ -3,6 +3,7 @@ import { randomUUID } from "node:crypto";
3
3
 
4
4
  import { ensureProfileCredentialBackend, readProfileIdentity, readProfileRetryIdentity, recordProfileRetryIdentity, installProfileCredential, profileCredentialEnvironment } from "./agent-credential-store.mjs";
5
5
  import { callToolJson } from "./api.mjs";
6
+ import { latestPublicCliCommand } from "./public-invocation.mjs";
6
7
 
7
8
  const PROFILES = Object.freeze({
8
9
  "botbuddy-dev": Object.freeze({ tenant: "botbuddy" }),
@@ -92,7 +93,7 @@ export async function bootstrapProfile(profileName, {
92
93
  tenant_id: profile.tenant,
93
94
  agent_id: data.agent_id,
94
95
  credential_source: "keychain_profile_slot",
95
- shell_refresh: `source <(npx --yes @botbuddy/cli@latest profile env ${profileName})`,
96
+ shell_refresh: `source <(${latestPublicCliCommand(`profile env ${profileName}`)})`,
96
97
  gui_refresh: "$HOME/.local/bin/botbuddy-mcp-env.sh",
97
98
  };
98
99
  }
@@ -0,0 +1,7 @@
1
+ // BOT-1425: npm cannot infer a package executable once @botbuddy/cli exposes
2
+ // both botbuddy and bb-pw. Keep every installation-free command explicit.
3
+ export const LATEST_PUBLIC_CLI_PREFIX = "npm exec --yes --package=@botbuddy/cli@latest -- botbuddy";
4
+
5
+ export function latestPublicCliCommand(command = "") {
6
+ return command ? `${LATEST_PUBLIC_CLI_PREFIX} ${command}` : LATEST_PUBLIC_CLI_PREFIX;
7
+ }
package/src/stack.mjs CHANGED
@@ -11,7 +11,7 @@
11
11
  // The canonical batch flow is: stack up → test → break → fix → test → stack done.
12
12
  //
13
13
  // Design contract (matches bb-wait):
14
- // * Dependency-free (Node stdlib only) so `npx @botbuddy/cli stack` cold-starts fast.
14
+ // * Dependency-free (Node stdlib only) so an explicit `npm exec --package` invocation cold-starts fast.
15
15
  // * Exactly ONE single-line JSON receipt to stdout; all diagnostics go to stderr.
16
16
  // * Receipt ≤ 10 KB (documented, --receipt-max-bytes overridable).
17
17
  // * Documented, meaningful exit codes.
package/src/wait-core.mjs CHANGED
@@ -228,6 +228,35 @@ const VALIDATORS = {
228
228
  }
229
229
  return { repo };
230
230
  },
231
+ // BOT-1458 — staging-release: wake when the first staging release train whose
232
+ // head commit CONTAINS this PR's merge commit reaches a terminal conclusion
233
+ // (success OR failure). Unlike `staging-green` this is PR-scoped and reports
234
+ // the train's outcome, never a health verdict. Inclusion is decided server-side
235
+ // by git ancestry (GitHub compare API), never by wall-clock ordering, so the
236
+ // condition only names the repo + PR; the relay stamps the resolved
237
+ // merge_commit_sha and workspace at registration. Optional
238
+ // `after_run=<run_id>[:<attempt>]` lets a wait re-armed from a failure receipt
239
+ // skip the run it already saw and park for the NEXT train.
240
+ "staging-release"(p) {
241
+ const { repo, number } = parsePrTarget(p, "staging-release");
242
+ if (number == null) {
243
+ throw new Error("staging-release needs pr=<number> (or pr=<owner/repo#number>)");
244
+ }
245
+ const out = { repo, pr: number };
246
+ if (p.after_run !== undefined) {
247
+ const raw = String(p.after_run).trim();
248
+ const m = /^(\d+)(?::(\d+))?$/.exec(raw);
249
+ if (!m) throw new Error("staging-release after_run must be <run_id>[:<attempt>]");
250
+ out.afterRunId = m[1];
251
+ if (m[2] !== undefined) out.afterRunAttempt = Number(m[2]);
252
+ }
253
+ const allowed = new Set(["repo", "pr", "number", "after_run"]);
254
+ const extras = Object.keys(p).filter((key) => !allowed.has(key));
255
+ if (extras.length > 0) {
256
+ throw new Error(`staging-release accepts only repo=<owner/repo>, pr=<number>, after_run=<run_id>[:<attempt>] (got: ${extras.join(", ")})`);
257
+ }
258
+ return out;
259
+ },
231
260
  // BOT-1247 — unblocked: wake when a ticket's LAST open blocker clears (its
232
261
  // open-blocker count transitions >0 → 0). Level-triggered: a ticket already
233
262
  // unblocked (or never blocked) at registration is granted immediately with
@@ -666,6 +695,27 @@ function conditionMatchesSignal(condition, signal, waitSessionId) {
666
695
  payload.gate_open === false &&
667
696
  payload.operational_status === "healthy";
668
697
  }
698
+ case "staging-release": {
699
+ // BOT-1458: a PR-scoped staging release train reached a terminal conclusion
700
+ // and its head commit contains this PR's merge (`includes_pr:true`, decided
701
+ // server-side by ancestry). The signal is tenant-scoped, so the central
702
+ // guard already fenced foreign workspaces. Subject is `<owner/repo>#<pr>`.
703
+ if (signal.signal_type !== "staging_release") return false;
704
+ const payload = signal.payload ?? {};
705
+ const subject = typeof signal.subject_key === "string" ? signal.subject_key.toLowerCase() : "";
706
+ if (subject !== `${params.repo}#${params.pr}`.toLowerCase()) return false;
707
+ if (payload.includes_pr !== true) return false;
708
+ // AC-9: a wait re-armed with after_run ignores the run it already saw — that
709
+ // run id, and (when an attempt was named) that attempt or lower — so a
710
+ // re-registration after a failure receipt parks for the NEXT train instead
711
+ // of instantly replaying the same failure via `--since`.
712
+ if (params.afterRunId != null && String(payload.workflow_run_id) === String(params.afterRunId)) {
713
+ if (params.afterRunAttempt == null) return false;
714
+ const attempt = Number(payload.workflow_run_attempt);
715
+ if (Number.isFinite(attempt) && attempt <= params.afterRunAttempt) return false;
716
+ }
717
+ return true;
718
+ }
669
719
  case "unblocked": {
670
720
  // Wakes on the ticket_unblocked spine signal for this ticket (the server emits both
671
721
  // the genuine >0→0 transition and the level-triggered `already_unblocked` echo).
package/src/wait.mjs CHANGED
@@ -17,12 +17,13 @@ import { EXIT, parseConditions, parseSseFrames, runWaitLoop, normalizeSince, tru
17
17
  import { resolveAgentProfile, withPrincipalReceipt } from "./wait-profile.mjs";
18
18
  import { VERSION } from "./version.mjs";
19
19
  import { fileURLToPath } from "node:url";
20
+ import { latestPublicCliCommand } from "./public-invocation.mjs";
20
21
 
21
22
  // A protocol is deliberately distinct from package semver: compatible pinned
22
23
  // clients keep working until the server raises this minimum, while a stale
23
24
  // implementation gets a typed, safe upgrade instruction.
24
25
  export const WAIT_PROTOCOL_VERSION = 1;
25
- const CLI_UPGRADE_COMMAND = "npx --yes @botbuddy/cli@latest wait";
26
+ const CLI_UPGRADE_COMMAND = latestPublicCliCommand("wait");
26
27
  const MIN_RECEIPT_MAX_BYTES = 512;
27
28
 
28
29
  const HELP = `botbuddy wait — one wait command instead of a polling loop (BOT-989)
@@ -458,7 +459,7 @@ function emit(receipt, { versioned = false, maxBytes = null } = {}) {
458
459
  }
459
460
 
460
461
  function profileRecovery(profile) {
461
- return `npx --yes @botbuddy/cli@latest profile setup ${profile.name}`;
462
+ return latestPublicCliCommand(`profile setup ${profile.name}`);
462
463
  }
463
464
 
464
465
  function typedProfileError(errorCode) {