@botbuddy/cli 1.59.6 → 1.59.7

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.6",
3
+ "version": "1.59.7",
4
4
  "description": "BotBuddy \u2014 Swarm coordination CLI for multi-agent workflows",
5
5
  "type": "module",
6
6
  "bin": {
@@ -62,20 +62,57 @@ function deltaUsage(total, previous) {
62
62
 
63
63
  // Only derived values are kept in ctx (it is persisted in the cursor file):
64
64
  // the cwd's HMAC and the ticket key its last segment names — never the path.
65
- function setCwd(ctx, cwd, hmac, tenantFor, repoFor = () => null) {
65
+ //
66
+ // BOT-1988: the branch is the checkout's, read at SYNC time by `branchFor(cwd)`
67
+ // (the rollout records git metadata only in session_meta, and 84 of 217 local
68
+ // rollouts had none). It is an estimate — the checkout may have switched branch
69
+ // since the turn ran — so a row carrying a derived or carried branch is stamped
70
+ // attribution_confidence 'inferred'. A cwd change never nulls a known branch
71
+ // unless the new cwd names a DIFFERENT ticket (another worktree): the resolver
72
+ // returns null for a removed checkout, and a subdirectory of the same checkout
73
+ // keeps its branch.
74
+ const usableBranch = (value) => {
75
+ const label = safeLabel(value);
76
+ return label && label !== "HEAD" ? label : null;
77
+ };
78
+
79
+ function setCwd(ctx, cwd, hmac, tenantFor, repoFor = () => null, branchFor = () => null) {
66
80
  const value = str(cwd);
67
81
  if (!value) return;
68
82
  const cwdHmac = hmac(value);
69
- // A later turn in ANOTHER checkout: the session_meta git metadata described
70
- // the old one. Drop it (the ticket then falls back to the new cwd) and
71
- // re-derive the repo from the new checkout.
72
- if (ctx.cwd_hmac && ctx.cwd_hmac !== cwdHmac) {
73
- ctx.git_branch = null;
83
+ const changed = Boolean(ctx.cwd_hmac && ctx.cwd_hmac !== cwdHmac);
84
+ const cwdTicket = inferTicketKey(null, value);
85
+ if (changed) {
86
+ // A later turn in ANOTHER directory: the session_meta git metadata described
87
+ // the old one. Re-derive the repo; the commit is unknown.
74
88
  ctx.commit_sha = null;
75
89
  ctx.repo = repoFor(value) ?? null;
90
+ const derived = usableBranch(branchFor(value));
91
+ if (derived) {
92
+ ctx.git_branch = derived;
93
+ ctx.branch_estimated = true;
94
+ } else if (ctx.git_branch) {
95
+ const carriedTicket = inferTicketKey(ctx.git_branch, null);
96
+ if (cwdTicket && carriedTicket && cwdTicket !== carriedTicket) {
97
+ ctx.git_branch = null;
98
+ ctx.branch_estimated = false;
99
+ } else {
100
+ ctx.branch_estimated = true;
101
+ }
102
+ }
103
+ } else if (!ctx.git_branch || ctx.branch_estimated) {
104
+ // First cwd of the thread, a cursor context an older CLI persisted with no
105
+ // branch, or an estimate from an earlier sync (the checkout may have switched
106
+ // branch since): resolve it with THIS sync's resolver, which caches per cwd.
107
+ // The rollout's own git branch (branch_estimated false) is never replaced.
108
+ const derived = usableBranch(branchFor(value));
109
+ if (derived) {
110
+ ctx.git_branch = derived;
111
+ ctx.branch_estimated = true;
112
+ }
76
113
  }
77
114
  ctx.cwd_hmac = cwdHmac;
78
- ctx.cwd_ticket = inferTicketKey(null, value);
115
+ ctx.cwd_ticket = cwdTicket;
79
116
  ctx.cwd_tenant = tenantFor(value); // a tenant slug (or null), never the path
80
117
  }
81
118
 
@@ -86,7 +123,7 @@ const NONE = Object.freeze({ envelopes: [], refused: 0 });
86
123
  * @param {(value: string) => string} opts.hmac
87
124
  * @param {string} opts.environment
88
125
  */
89
- export function createCodexAdapter({ hmac, environment, tenantFor = () => null, repoFor = () => null }) {
126
+ export function createCodexAdapter({ hmac, environment, tenantFor = () => null, repoFor = () => null, branchFor = () => null }) {
90
127
  function parseCodexRecord(record, { offset, sourceHmac, ctx }) {
91
128
  if (!record || typeof record !== "object") return NONE;
92
129
  const payload = record.payload && typeof record.payload === "object" ? record.payload : null;
@@ -101,16 +138,22 @@ export function createCodexAdapter({ hmac, environment, tenantFor = () => null,
101
138
  ctx.subagent_thread = thread && root && thread !== root ? thread
102
139
  : payload.thread_source === "subagent" && thread ? thread : null;
103
140
  if (safeLabel(payload.cli_version)) ctx.cli_version = safeLabel(payload.cli_version);
104
- setCwd(ctx, payload.cwd, hmac, tenantFor, repoFor);
105
141
  const git = payload.git && typeof payload.git === "object" ? payload.git : {};
106
- ctx.git_branch = safeLabel(git.branch) ?? ctx.git_branch ?? null;
142
+ const metaBranch = safeLabel(git.branch);
143
+ setCwd(ctx, payload.cwd, hmac, tenantFor, repoFor, branchFor);
144
+ // The rollout's own git block wins over the sync-time resolver.
145
+ if (metaBranch) {
146
+ ctx.git_branch = metaBranch;
147
+ ctx.branch_estimated = false;
148
+ }
149
+ ctx.git_branch = ctx.git_branch ?? null;
107
150
  ctx.commit_sha = commitSha(git.commit_hash) ?? ctx.commit_sha ?? null;
108
151
  ctx.repo = normalizeRepo(git.repository_url) ?? ctx.repo ?? null;
109
152
  return NONE;
110
153
  }
111
154
  if (record.type === "turn_context") {
112
155
  if (modelLabel(payload.model)) ctx.model = modelLabel(payload.model);
113
- setCwd(ctx, payload.cwd, hmac, tenantFor, repoFor);
156
+ setCwd(ctx, payload.cwd, hmac, tenantFor, repoFor, branchFor);
114
157
  return NONE;
115
158
  }
116
159
  if (record.type === "response_item") {
@@ -206,7 +249,7 @@ export function createCodexAdapter({ hmac, environment, tenantFor = () => null,
206
249
  repo: ctx.repo ?? null,
207
250
  commit_sha: ctx.commit_sha ?? null,
208
251
  ticket_key_inferred: ticket,
209
- attribution_confidence: ticket ? "inferred" : null,
252
+ attribution_confidence: ticket || (ctx.git_branch && ctx.branch_estimated) ? "inferred" : null,
210
253
  environment,
211
254
  cwd_hmac: ctx.cwd_hmac ?? null,
212
255
  source_hmac: sourceHmac,
@@ -164,6 +164,30 @@ export function createRepoResolver({ exec = execFileSync, env = process.env } =
164
164
  };
165
165
  }
166
166
 
167
+ /**
168
+ * BOT-1988: cwd → the branch its checkout has checked out NOW (sync time), or
169
+ * null for a detached HEAD, a removed checkout or a non-repo directory. Codex
170
+ * rollouts record git metadata only in session_meta, so this is the Codex
171
+ * adapter's estimate for every other turn. Cached per cwd; repository-location
172
+ * variables stripped (a lefthook pre-push run exports the pushing repo's).
173
+ */
174
+ export function createBranchResolver({ exec = execFileSync, env = process.env } = {}) {
175
+ const gitEnv = { ...env };
176
+ for (const name of GIT_REPOSITORY_ENV) delete gitEnv[name];
177
+ const cache = new Map();
178
+ return (cwd) => {
179
+ if (cache.has(cwd)) return cache.get(cwd);
180
+ let branch = null;
181
+ try {
182
+ branch = String(exec("git", ["-c", "core.fsmonitor=false", "-C", cwd, "symbolic-ref", "--quiet", "--short", "HEAD"], {
183
+ encoding: "utf8", timeout: 1000, stdio: ["ignore", "pipe", "ignore"], env: gitEnv,
184
+ })).trim() || null;
185
+ } catch { branch = null; }
186
+ cache.set(cwd, branch);
187
+ return branch;
188
+ };
189
+ }
190
+
167
191
  /**
168
192
  * cwd → the tenant bound to its checkout (BOT-1902 tenant routing), or null
169
193
  * when unbound. The binding is the checkout's committed `.botbuddy-agent.json`:
@@ -346,7 +370,7 @@ export async function syncUsage(opts, deps = {}) {
346
370
  const {
347
371
  env = process.env, home = homedir(), cwd = process.cwd(), now = () => Date.now(),
348
372
  fetchImpl = fetch, resolveAuth = defaultResolveAuth, hostname = systemHostname,
349
- roots = transcriptRoots(env, home), repoFor = createRepoResolver(), cliVersion = "0.0.0",
373
+ roots = transcriptRoots(env, home), repoFor = createRepoResolver(), branchFor = createBranchResolver({ env }), cliVersion = "0.0.0",
350
374
  tenantFor = createTenantResolver({ env }), resolveTarget = defaultResolveTarget, readLines = readCompleteLines,
351
375
  serverUrl = env.BOTBUDDY_SERVER_URL || DEFAULT_SERVER_URL, statePath = cursorPath(home),
352
376
  } = deps;
@@ -438,7 +462,7 @@ export async function syncUsage(opts, deps = {}) {
438
462
  receipt.providers.push(provider);
439
463
  receipt.files_scanned += files.length;
440
464
  const adapter = provider === "codex"
441
- ? createCodexAdapter({ hmac, environment, tenantFor, repoFor })
465
+ ? createCodexAdapter({ hmac, environment, tenantFor, repoFor, branchFor })
442
466
  : createClaudeAdapter({ hmac, environment, repoFor, tenantFor });
443
467
  const run = {
444
468
  host: hostname(), trigger: opts.trigger, provider, cli_version: cliVersion,