@botbuddy/cli 1.59.6 → 1.59.8
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/daemon/bin/darwin-arm64/botbuddy-daemon +0 -0
- package/daemon/bin/linux-x64/botbuddy-daemon +0 -0
- package/daemon-release/darwin-arm64.json +7 -0
- package/daemon-release/linux-x64.json +7 -0
- package/daemon-release/manifest.json +22 -0
- package/package.json +3 -1
- package/src/api.mjs +62 -0
- package/src/commands.mjs +6 -0
- package/src/daemon/approval-wait.mjs +126 -0
- package/src/daemon/bootstrap-resume.mjs +151 -0
- package/src/daemon/bootstrap.mjs +386 -0
- package/src/daemon/commands.mjs +478 -0
- package/src/daemon/enrollment.mjs +483 -0
- package/src/daemon/entrypoint.mjs +14 -0
- package/src/daemon/installer.mjs +438 -0
- package/src/daemon/profile-delivery.mjs +332 -0
- package/src/daemon/release.mjs +81 -0
- package/src/daemon/runtime-config.mjs +85 -0
- package/src/daemon/service.mjs +38 -0
- package/src/oauth-device.mjs +11 -0
- package/src/publish-native-guards.mjs +44 -0
- package/src/usage/codex.mjs +55 -12
- package/src/usage/sync.mjs +26 -2
- package/src/wait-core.mjs +22 -5
- package/src/wait.mjs +4 -1
package/src/usage/codex.mjs
CHANGED
|
@@ -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
|
-
|
|
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
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
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 =
|
|
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
|
-
|
|
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,
|
package/src/usage/sync.mjs
CHANGED
|
@@ -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,
|
package/src/wait-core.mjs
CHANGED
|
@@ -80,6 +80,7 @@ function managedAllocationFailureDetail(signal, condition) {
|
|
|
80
80
|
// own-session terminal branch because its wait_session_id differs) would otherwise match
|
|
81
81
|
// that raw event condition and exit an unrelated wait as an ordinary match (Codex P2).
|
|
82
82
|
export const WAIT_SUPERSEDED_SIGNAL_TYPE = "wait_superseded";
|
|
83
|
+
const HOST_APPROVAL_HOST_KEY_RE = /^bbh-[0-9a-f]{24}$/;
|
|
83
84
|
|
|
84
85
|
// The USER-ARMABLE spine signal_type allowlist, for the `event:` escape hatch. The full
|
|
85
86
|
// public.agent_signal_type enum is these PLUS the control types above
|
|
@@ -99,6 +100,11 @@ export const SIGNAL_TYPES = Object.freeze([
|
|
|
99
100
|
"snapshot",
|
|
100
101
|
"ticket_unblocked",
|
|
101
102
|
"stack_lease",
|
|
103
|
+
// BOT-2324: emitted only once the exact public host approval predicate is
|
|
104
|
+
// true (owner-bound active settings + policy + live tenant subscription).
|
|
105
|
+
// It is intentionally consumed through the existing raw `event:` escape
|
|
106
|
+
// hatch rather than adding another parallel wait grammar.
|
|
107
|
+
"host_approval",
|
|
102
108
|
]);
|
|
103
109
|
|
|
104
110
|
const LOCK_SUBTYPES = Object.freeze(["playwright_lane", "vite_port", "backend_port", "supabase_local"]);
|
|
@@ -406,7 +412,15 @@ const VALIDATORS = {
|
|
|
406
412
|
if (!SIGNAL_TYPES.includes(p.type)) {
|
|
407
413
|
throw new Error(`event type must be a known signal_type (${SIGNAL_TYPES.join(", ")})`);
|
|
408
414
|
}
|
|
409
|
-
|
|
415
|
+
const subject = p.subject !== undefined ? String(p.subject).trim() : null;
|
|
416
|
+
// BOT-2324: unlike the generic raw escape hatch, this approval condition
|
|
417
|
+
// has one narrow public identity grammar. The service independently checks
|
|
418
|
+
// it for direct JSON callers; keeping the CLI equally strict prevents an
|
|
419
|
+
// impossible-to-match wait from being armed locally.
|
|
420
|
+
if (p.type === "host_approval" && (!subject || !HOST_APPROVAL_HOST_KEY_RE.test(subject))) {
|
|
421
|
+
throw new Error("event:type=host_approval needs subject=<canonical bbh- host key>");
|
|
422
|
+
}
|
|
423
|
+
return { type: p.type, subject };
|
|
410
424
|
},
|
|
411
425
|
};
|
|
412
426
|
|
|
@@ -1237,10 +1251,13 @@ function conditionMatchesSignal(condition, signal, waitSessionId) {
|
|
|
1237
1251
|
// wake the wrong workspace. This centralizes what BOT-1247's `unblocked` matcher did
|
|
1238
1252
|
// per-type (that per-type check stays as belt-and-braces). Tenantless signals
|
|
1239
1253
|
// (host-shared lock_availability/container_capacity, owner/recipient-scoped
|
|
1240
|
-
// chat/pr_state/test_run/guidance) are NEVER blocked — they keep today's semantics
|
|
1241
|
-
// for any session (AC-5). `
|
|
1242
|
-
//
|
|
1243
|
-
//
|
|
1254
|
+
// chat/pr_state/test_run/guidance) are NEVER blocked — they keep today's semantics
|
|
1255
|
+
// for any session (AC-5). `host_approval` is the deliberate exception: it is
|
|
1256
|
+
// owner-visible AND tenant-bound, so the relay stamps scope=owner_tenant and we
|
|
1257
|
+
// retain this tenant fence after its owner gate. `sessionTenant === undefined`
|
|
1258
|
+
// opts a caller out of the central guard: a pure unit test asserting a per-type
|
|
1259
|
+
// matcher in isolation. The CLI always threads the server-resolved tenant, so
|
|
1260
|
+
// the guard is live for every real wait.
|
|
1244
1261
|
export function signalTenantBlocked(signal, sessionTenant) {
|
|
1245
1262
|
if (sessionTenant === undefined) return false;
|
|
1246
1263
|
if (signal.tenant_id == null) return false;
|
package/src/wait.mjs
CHANGED
|
@@ -153,7 +153,10 @@ CONDITIONS (TYPE:key=val,key=val — repeat for several; --any wakes on the fir
|
|
|
153
153
|
Wakes only after a complete non-vacuous workflow success;
|
|
154
154
|
manual resolutions, overrides, skipped, and neutral runs do not match.
|
|
155
155
|
event:type=<signal_type>[,subject=<key>]
|
|
156
|
-
raw signal-spine match (escape hatch)
|
|
156
|
+
raw signal-spine match (escape hatch). Host setup uses
|
|
157
|
+
event:type=host_approval,subject=bbh-<24 lowercase hex>;
|
|
158
|
+
it wakes only for the authenticated owner's one bound
|
|
159
|
+
tenant after current server-side approval revalidation.
|
|
157
160
|
|
|
158
161
|
OPTIONS
|
|
159
162
|
--any wake on the first matching condition (default; only mode in v1)
|