@pitboss/cli 0.4.9 → 0.4.10

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/dist/cli.js CHANGED
@@ -21060,6 +21060,9 @@ var USAGE_NUMBER_KEYS = [
21060
21060
  "cache_read_input_tokens",
21061
21061
  "cache_creation_input_tokens"
21062
21062
  ];
21063
+ function notReportedUsage(reason) {
21064
+ return { notReported: true, reason };
21065
+ }
21063
21066
  function finiteNumber(value) {
21064
21067
  return typeof value === "number" && Number.isFinite(value) ? value : void 0;
21065
21068
  }
@@ -21127,14 +21130,32 @@ async function getTaskStatus(conn, id, logPrefix) {
21127
21130
  return void 0;
21128
21131
  }
21129
21132
  }
21130
- async function sessionTouchedTaskIds(conn, candidates, logPrefix) {
21133
+ async function listUpdatedSince(conn, since, logPrefix) {
21134
+ try {
21135
+ const res = await fetch(
21136
+ `${conn.apiUrl}/tasks?repoId=${conn.repoId}&assignedTo=${conn.agentId}&updatedAfter=${encodeURIComponent(since)}`,
21137
+ { headers: { Authorization: `Bearer ${conn.apiSecret}` } }
21138
+ );
21139
+ if (!res.ok) throw new Error(`task list failed (${res.status} ${res.statusText})`);
21140
+ const { data } = await res.json();
21141
+ return (data ?? []).filter((row) => typeof row.id === "string" && row.id.length > 0).filter((row) => row.status !== "assigned").map((row) => row.id);
21142
+ } catch (err) {
21143
+ console.error(
21144
+ `${logPrefix} Could not list tasks updated since the session started:`,
21145
+ err instanceof Error ? err.message : String(err)
21146
+ );
21147
+ return [];
21148
+ }
21149
+ }
21150
+ async function sessionTouchedTaskIds(conn, candidates, logPrefix, since) {
21131
21151
  const touched = [];
21132
21152
  for (const id of candidates) {
21133
21153
  const status = await getTaskStatus(conn, id, logPrefix);
21134
21154
  if (status !== void 0 && status !== "assigned") touched.push(id);
21135
21155
  }
21136
21156
  const inProgressAtClose = await listSessionTaskIds(conn, logPrefix, "in_progress");
21137
- return unionTaskIds(touched, inProgressAtClose);
21157
+ const updated = since ? await listUpdatedSince(conn, since, logPrefix) : [];
21158
+ return unionTaskIds(touched, inProgressAtClose, updated);
21138
21159
  }
21139
21160
  function unionTaskIds(...lists) {
21140
21161
  const seen = /* @__PURE__ */ new Set();
@@ -21148,34 +21169,87 @@ function unionTaskIds(...lists) {
21148
21169
  }
21149
21170
  return out;
21150
21171
  }
21151
- async function writeSessionUsage(conn, taskIds, usage, logPrefix) {
21152
- const sessionUsage = {
21153
- ...usage,
21154
- at: (/* @__PURE__ */ new Date()).toISOString(),
21155
- sessionTaskIds: [...taskIds]
21156
- };
21172
+ async function writeSessionUsage(conn, taskIds, usage, logPrefix, opts = {}) {
21157
21173
  const headers = { "Content-Type": "application/json", Authorization: `Bearer ${conn.apiSecret}` };
21158
21174
  for (const id of taskIds) {
21175
+ const slice = taskIds.length > 1 && !usage.notReported ? opts.slices?.[id] : void 0;
21176
+ const share = usage.notReported ? void 0 : slice || taskIds.length === 1 ? "task" : "session";
21177
+ const body = {
21178
+ usage: slice ?? usage,
21179
+ sessionTaskIds: taskIds,
21180
+ ...opts.model ? { model: opts.model } : {},
21181
+ ...share ? { share } : {}
21182
+ };
21159
21183
  try {
21160
- const put = await fetch(`${conn.apiUrl}/tasks/${id}`, {
21161
- method: "PUT",
21184
+ const res = await fetch(`${conn.apiUrl}/tasks/${id}/usage-report`, {
21185
+ method: "POST",
21162
21186
  headers,
21163
- body: JSON.stringify({ metadata: { sessionUsage } })
21187
+ body: JSON.stringify(body)
21164
21188
  });
21165
- if (!put.ok) {
21166
- console.error(`${logPrefix} Could not write sessionUsage on ${id} (${put.status} ${put.statusText})`);
21189
+ if (!res.ok) {
21190
+ console.error(`${logPrefix} Could not report usage on ${id} (${res.status} ${res.statusText})`);
21167
21191
  }
21168
21192
  } catch (err) {
21169
21193
  console.error(
21170
- `${logPrefix} Could not write sessionUsage on ${id}:`,
21194
+ `${logPrefix} Could not report usage on ${id}:`,
21171
21195
  err instanceof Error ? err.message : String(err)
21172
21196
  );
21173
21197
  }
21174
21198
  }
21175
21199
  }
21176
- async function recordSessionUsage(conn, startIds, usage, logPrefix) {
21200
+ async function recordSessionUsage(conn, startIds, usage, logPrefix, opts = {}) {
21177
21201
  const start = await startIds;
21178
- await writeSessionUsage(conn, await sessionTouchedTaskIds(conn, start, logPrefix), usage, logPrefix);
21202
+ const { since, ...rest } = opts;
21203
+ await writeSessionUsage(conn, await sessionTouchedTaskIds(conn, start, logPrefix, since), usage, logPrefix, rest);
21204
+ }
21205
+ var BOUNDARY_STATUSES = /* @__PURE__ */ new Set(["completed", "blocked", "pending_verification"]);
21206
+ function addUsage(into, from) {
21207
+ into.num_turns = (into.num_turns ?? 0) + (from.num_turns ?? 0);
21208
+ const total = into.usage ?? {};
21209
+ for (const key of USAGE_NUMBER_KEYS) {
21210
+ const n = from.usage?.[key];
21211
+ if (n !== void 0) total[key] = (total[key] ?? 0) + n;
21212
+ }
21213
+ into.usage = total;
21214
+ }
21215
+ function createUsageTracker() {
21216
+ let model;
21217
+ let pending = /* @__PURE__ */ new Map();
21218
+ const slices = {};
21219
+ let anon = 0;
21220
+ function cut(taskId) {
21221
+ const slice = slices[taskId] ?? {};
21222
+ for (const messageUsage of pending.values()) addUsage(slice, messageUsage);
21223
+ pending = /* @__PURE__ */ new Map();
21224
+ if (slice.usage) slices[taskId] = slice;
21225
+ }
21226
+ return {
21227
+ observe(event) {
21228
+ if (event.type === "system" && event.subtype === "init") {
21229
+ if (typeof event.model === "string" && event.model) model = event.model;
21230
+ return;
21231
+ }
21232
+ if (event.type !== "assistant") return;
21233
+ const message = event.message ?? {};
21234
+ const raw = message.usage;
21235
+ if (raw && typeof raw === "object" && !Array.isArray(raw)) {
21236
+ const usage = pickNumbers(raw, USAGE_NUMBER_KEYS);
21237
+ if (Object.keys(usage).length > 0) {
21238
+ pending.set(typeof message.id === "string" ? message.id : `anon_${anon++}`, { num_turns: 1, usage });
21239
+ }
21240
+ }
21241
+ const content = Array.isArray(message.content) ? message.content : [];
21242
+ for (const block of content) {
21243
+ if (block.type !== "tool_use" || typeof block.name !== "string" || !block.name.endsWith("update_task_status")) continue;
21244
+ const input = block.input ?? {};
21245
+ if (typeof input.taskId === "string" && typeof input.status === "string" && BOUNDARY_STATUSES.has(input.status)) {
21246
+ cut(input.taskId);
21247
+ }
21248
+ }
21249
+ },
21250
+ model: () => model,
21251
+ opts: () => ({ ...model ? { model } : {}, slices })
21252
+ };
21179
21253
  }
21180
21254
 
21181
21255
  // ../claude-worker/src/model.ts
@@ -21222,26 +21296,36 @@ function writeMcpConfig(config, prefix = "claude-worker") {
21222
21296
  writeFileSync6(path3, JSON.stringify(mcpConfig), { mode: 384 });
21223
21297
  return path3;
21224
21298
  }
21299
+ function claudeArgs(opts) {
21300
+ return [
21301
+ "--print",
21302
+ "--mcp-config",
21303
+ opts.mcpConfigPath,
21304
+ "--no-session-persistence",
21305
+ "--dangerously-skip-permissions",
21306
+ "--output-format",
21307
+ "stream-json",
21308
+ "--verbose",
21309
+ "--model",
21310
+ opts.model,
21311
+ "--add-dir",
21312
+ opts.repoPath,
21313
+ ...opts.isolateUserConfig ? ["--setting-sources", "project,local"] : []
21314
+ ];
21315
+ }
21225
21316
  async function runClaudeSession(config) {
21226
21317
  const prompt = buildPrompt(config);
21227
21318
  const mcpConfigPath = writeMcpConfig(config);
21319
+ const startedAt = (/* @__PURE__ */ new Date()).toISOString();
21228
21320
  const taskIdsPromise = listSessionTaskIds(config, "[claude-worker]");
21229
21321
  try {
21230
21322
  return await new Promise((resolve9, reject) => {
21231
- const args = [
21232
- "--print",
21233
- "--mcp-config",
21323
+ const args = claudeArgs({
21234
21324
  mcpConfigPath,
21235
- "--no-session-persistence",
21236
- "--dangerously-skip-permissions",
21237
- "--output-format",
21238
- "stream-json",
21239
- "--verbose",
21240
- "--model",
21241
- pinnedModel() ?? config.model,
21242
- "--add-dir",
21243
- config.repoPath
21244
- ];
21325
+ model: pinnedModel() ?? config.model,
21326
+ repoPath: config.repoPath,
21327
+ isolateUserConfig: process.env.PITBOSS_ISOLATE_USER_CONFIG === "1"
21328
+ });
21245
21329
  console.log(`[claude-worker] Spawning: ${config.claudeBin} ${args.slice(0, 4).join(" ")} ...`);
21246
21330
  console.log(`[claude-worker] cwd: ${config.repoPath}`);
21247
21331
  const { ANTHROPIC_API_KEY, ANTHROPIC_AUTH_TOKEN, ...workerEnv } = process.env;
@@ -21257,6 +21341,8 @@ async function runClaudeSession(config) {
21257
21341
  let buffer = "";
21258
21342
  let resultError = "";
21259
21343
  let sessionUsage;
21344
+ let sawResult = false;
21345
+ const tracker = createUsageTracker();
21260
21346
  proc.stdout.on("data", (chunk) => {
21261
21347
  buffer += chunk.toString();
21262
21348
  const lines = buffer.split("\n");
@@ -21265,6 +21351,7 @@ async function runClaudeSession(config) {
21265
21351
  if (!line.trim()) continue;
21266
21352
  try {
21267
21353
  const event = JSON.parse(line);
21354
+ tracker.observe(event);
21268
21355
  if (event.type === "assistant") {
21269
21356
  const content = event.message?.content ?? [];
21270
21357
  for (const block of content) {
@@ -21278,6 +21365,7 @@ async function runClaudeSession(config) {
21278
21365
  }
21279
21366
  }
21280
21367
  } else if (event.type === "result") {
21368
+ sawResult = true;
21281
21369
  const parsed = parseResultUsage(event);
21282
21370
  if (parsed) sessionUsage = parsed;
21283
21371
  if (event.is_error) {
@@ -21299,9 +21387,9 @@ async function runClaudeSession(config) {
21299
21387
  proc.on("close", (code, signal) => {
21300
21388
  const unique = [...new Set(toolsUsed)];
21301
21389
  console.log(`[claude-worker] Done \u2014 tools used: ${unique.length ? unique.join(", ") : "none"}`);
21302
- const usage = sessionUsage;
21303
- if (usage) console.log(formatSessionUsageLine("[claude-worker]", usage));
21304
- const finish = usage ? recordSessionUsage(config, taskIdsPromise, usage, "[claude-worker]") : Promise.resolve();
21390
+ const usage = sessionUsage ?? notReportedUsage(sawResult ? "adapter" : "killed");
21391
+ if (sessionUsage) console.log(formatSessionUsageLine("[claude-worker]", sessionUsage));
21392
+ const finish = recordSessionUsage(config, taskIdsPromise, usage, "[claude-worker]", { since: startedAt, ...tracker.opts() });
21305
21393
  const settle = () => {
21306
21394
  if (signal || code !== 0 && code !== null) {
21307
21395
  const detail = (resultError || stderrOutput || "").trim();
@@ -21369,6 +21457,9 @@ function readCursorEvent(line) {
21369
21457
  } catch {
21370
21458
  return {};
21371
21459
  }
21460
+ if (event.type === "system" && event.subtype === "init") {
21461
+ return typeof event.model === "string" && event.model ? { model: event.model } : {};
21462
+ }
21372
21463
  if (event.type === "tool_call" && event.subtype === "started") {
21373
21464
  const call = event.tool_call ?? {};
21374
21465
  const [kind] = Object.keys(call);
@@ -21386,6 +21477,7 @@ function readCursorEvent(line) {
21386
21477
  async function runCursorSession(config) {
21387
21478
  const args = cursorArgs(config.repoPath, buildPrompt(config), pinnedModel());
21388
21479
  console.log(`[cursor-worker] Spawning: ${bin()} ${args.slice(0, 5).join(" ")} ...`);
21480
+ const startedAt = (/* @__PURE__ */ new Date()).toISOString();
21389
21481
  const taskIdsPromise = listSessionTaskIds(config, "[cursor-worker]");
21390
21482
  return new Promise((resolve9, reject) => {
21391
21483
  const proc = spawn3(bin(), args, { cwd: config.repoPath, env: process.env, stdio: ["ignore", "pipe", "pipe"] });
@@ -21395,12 +21487,14 @@ async function runCursorSession(config) {
21395
21487
  let resultError = "";
21396
21488
  let stderrOutput = "";
21397
21489
  let sessionUsage;
21490
+ let model;
21398
21491
  proc.stdout.on("data", (chunk) => {
21399
21492
  buffer += chunk.toString();
21400
21493
  const lines = buffer.split("\n");
21401
21494
  buffer = lines.pop() ?? "";
21402
21495
  for (const line of lines) {
21403
- const { tool, error, usage } = readCursorEvent(line);
21496
+ const { tool, error, usage, model: initModel } = readCursorEvent(line);
21497
+ if (initModel) model = initModel;
21404
21498
  if (tool) {
21405
21499
  toolsUsed.push(tool);
21406
21500
  process.stdout.write(` \u2699 ${tool}
@@ -21418,9 +21512,9 @@ async function runCursorSession(config) {
21418
21512
  });
21419
21513
  proc.on("close", (code, signal) => {
21420
21514
  console.log(`[cursor-worker] Done \u2014 tools used: ${toolsUsed.length ? [...new Set(toolsUsed)].join(", ") : "none"}`);
21421
- const usage = sessionUsage;
21422
- if (usage) console.log(formatSessionUsageLine("[cursor-worker]", usage));
21423
- const finish = usage ? recordSessionUsage(config, taskIdsPromise, usage, "[cursor-worker]") : Promise.resolve();
21515
+ const usage = sessionUsage ?? notReportedUsage("killed");
21516
+ if (sessionUsage) console.log(formatSessionUsageLine("[cursor-worker]", sessionUsage));
21517
+ const finish = recordSessionUsage(config, taskIdsPromise, usage, "[cursor-worker]", { since: startedAt, ...model ? { model } : {} });
21424
21518
  const settle = () => {
21425
21519
  if (signal || code !== 0 && code !== null) {
21426
21520
  const detail = (resultError || stderrOutput).trim();
@@ -21491,6 +21585,7 @@ function readCopilotEvent(line) {
21491
21585
  const type = typeof event.type === "string" ? event.type : "";
21492
21586
  const data = event.data ?? {};
21493
21587
  const message = typeof data.message === "string" ? data.message : "";
21588
+ if (type === "session.tools_updated" && typeof data.model === "string" && data.model) return { model: data.model };
21494
21589
  if (/^tool\..*start/.test(type) && typeof data.toolName === "string") return { tool: data.toolName };
21495
21590
  if (type === "session.error") return { error: message || "copilot session error" };
21496
21591
  if (type === "result" && typeof event.exitCode === "number" && event.exitCode !== 0) return { error: `copilot finished with exit code ${event.exitCode}` };
@@ -21500,6 +21595,8 @@ function readCopilotEvent(line) {
21500
21595
  async function runCopilotSession(config) {
21501
21596
  const mcpConfigPath = writeMcpConfig(config, "copilot-worker");
21502
21597
  const args = copilotArgs(config.repoPath, buildPrompt(config), mcpConfigPath, pinnedModel());
21598
+ const startedAt = (/* @__PURE__ */ new Date()).toISOString();
21599
+ const taskIdsPromise = listSessionTaskIds(config, "[copilot-worker]");
21503
21600
  console.log(`[copilot-worker] Spawning: ${bin2()} -p <prompt> ${args.slice(2, 6).join(" ")} ...`);
21504
21601
  return new Promise((resolve9, reject) => {
21505
21602
  const proc = spawn4(bin2(), args, { cwd: config.repoPath, env: process.env, stdio: ["ignore", "pipe", "pipe"] });
@@ -21508,12 +21605,14 @@ async function runCopilotSession(config) {
21508
21605
  let buffer = "";
21509
21606
  let sessionError = "";
21510
21607
  let stderrOutput = "";
21608
+ let model;
21511
21609
  proc.stdout.on("data", (chunk) => {
21512
21610
  buffer += chunk.toString();
21513
21611
  const lines = buffer.split("\n");
21514
21612
  buffer = lines.pop() ?? "";
21515
21613
  for (const line of lines) {
21516
- const { tool, error } = readCopilotEvent(line);
21614
+ const { tool, error, model: initModel } = readCopilotEvent(line);
21615
+ if (initModel) model = initModel;
21517
21616
  if (tool) {
21518
21617
  toolsUsed.push(tool);
21519
21618
  process.stdout.write(` \u2699 ${tool}
@@ -21534,12 +21633,15 @@ async function runCopilotSession(config) {
21534
21633
  } catch {
21535
21634
  }
21536
21635
  console.log(`[copilot-worker] Done \u2014 tools used: ${toolsUsed.length ? [...new Set(toolsUsed)].join(", ") : "none"}`);
21537
- if (sessionError || signal || code !== 0 && code !== null) {
21538
- const detail = (sessionError || stderrOutput).trim();
21539
- reject(new Error(`copilot exited with ${signal ?? `code ${code}`}${detail ? `: ${detail}` : ""}`));
21540
- } else {
21541
- resolve9([...new Set(toolsUsed)]);
21542
- }
21636
+ const settle = () => {
21637
+ if (sessionError || signal || code !== 0 && code !== null) {
21638
+ const detail = (sessionError || stderrOutput).trim();
21639
+ reject(new Error(`copilot exited with ${signal ?? `code ${code}`}${detail ? `: ${detail}` : ""}`));
21640
+ } else {
21641
+ resolve9([...new Set(toolsUsed)]);
21642
+ }
21643
+ };
21644
+ recordSessionUsage(config, taskIdsPromise, notReportedUsage("adapter"), "[copilot-worker]", { since: startedAt, ...model ? { model } : {} }).then(settle, settle);
21543
21645
  });
21544
21646
  proc.on("error", (err) => {
21545
21647
  try {
@@ -21633,6 +21735,8 @@ async function runCodexSession(config) {
21633
21735
  }
21634
21736
  };
21635
21737
  const args = codexArgs(config.repoPath, buildPrompt(config), profile, pinnedModel());
21738
+ const startedAt = (/* @__PURE__ */ new Date()).toISOString();
21739
+ const taskIdsPromise = listSessionTaskIds(config, "[codex-worker]");
21636
21740
  console.log(`[codex-worker] Spawning: ${bin3()} ${args.slice(0, 6).join(" ")} ...`);
21637
21741
  return new Promise((resolve9, reject) => {
21638
21742
  const proc = spawn5(bin3(), args, { cwd: config.repoPath, env: process.env, stdio: ["ignore", "pipe", "pipe"] });
@@ -21664,12 +21768,15 @@ async function runCodexSession(config) {
21664
21768
  proc.on("close", (code, signal) => {
21665
21769
  cleanup();
21666
21770
  console.log(`[codex-worker] Done \u2014 tools used: ${toolsUsed.length ? [...new Set(toolsUsed)].join(", ") : "none"}`);
21667
- if (sessionError || signal || code !== 0 && code !== null) {
21668
- const detail = (sessionError || stderrOutput).trim();
21669
- reject(new Error(`codex exited with ${signal ?? `code ${code}`}${detail ? `: ${detail}` : ""}`));
21670
- } else {
21671
- resolve9([...new Set(toolsUsed)]);
21672
- }
21771
+ const settle = () => {
21772
+ if (sessionError || signal || code !== 0 && code !== null) {
21773
+ const detail = (sessionError || stderrOutput).trim();
21774
+ reject(new Error(`codex exited with ${signal ?? `code ${code}`}${detail ? `: ${detail}` : ""}`));
21775
+ } else {
21776
+ resolve9([...new Set(toolsUsed)]);
21777
+ }
21778
+ };
21779
+ recordSessionUsage(config, taskIdsPromise, notReportedUsage("adapter"), "[codex-worker]", { since: startedAt }).then(settle, settle);
21673
21780
  });
21674
21781
  proc.on("error", (err) => {
21675
21782
  cleanup();
@@ -21781,7 +21888,8 @@ function poolMembers(pool) {
21781
21888
  worktree: join16(pool.worktreeRoot, name),
21782
21889
  specialization: pool.specialization,
21783
21890
  ...pool.tier ? { tier: pool.tier } : {},
21784
- ...pool.model ? { model: pool.model } : {}
21891
+ ...pool.model ? { model: pool.model } : {},
21892
+ ...pool.isolateUserConfig ? { isolateUserConfig: true } : {}
21785
21893
  };
21786
21894
  });
21787
21895
  }
@@ -21811,6 +21919,12 @@ function modelOf(a, i) {
21811
21919
  }
21812
21920
  return { model: a.model };
21813
21921
  }
21922
+ function isolateOf(a, i, runtime) {
21923
+ if (a.isolateUserConfig === void 0 || a.isolateUserConfig === false) return {};
21924
+ if (a.isolateUserConfig !== true) throw new Error(`agents[${i}].isolateUserConfig must be true or false`);
21925
+ if (runtime !== "claude") throw new Error(`agents[${i}]: isolateUserConfig only applies to the claude runtime`);
21926
+ return { isolateUserConfig: true };
21927
+ }
21814
21928
  function parsePool(a, i, specDir) {
21815
21929
  if (a.name !== void 0 || a.worktree !== void 0) throw new Error(`agents[${i}] must have either name or pool, not both`);
21816
21930
  if (typeof a.pool !== "string" || !POOL_NAME.test(a.pool)) throw new Error(`agents[${i}].pool must be letters, digits, '.', '_' or '-' (at most 60)`);
@@ -21829,7 +21943,7 @@ function parsePool(a, i, specDir) {
21829
21943
  const base2 = resolve5(specDir, a.base);
21830
21944
  const worktreeRoot = resolve5(specDir, a.worktreeRoot);
21831
21945
  if (isWithin(worktreeRoot, base2)) throw new Error(`agents[${i}].worktreeRoot ${worktreeRoot} is inside base ${base2}; pool worktrees never go inside your checkout`);
21832
- return { pool: a.pool, runtime, base: base2, worktreeRoot, specialization, ...tierOf(a, i, specialization), ...modelOf(a, i), min, max };
21946
+ return { pool: a.pool, runtime, base: base2, worktreeRoot, specialization, ...tierOf(a, i, specialization), ...modelOf(a, i), ...isolateOf(a, i, runtime), min, max };
21833
21947
  }
21834
21948
  function parseSpec(text, specDir) {
21835
21949
  let raw;
@@ -21856,7 +21970,7 @@ function parseSpec(text, specDir) {
21856
21970
  if (typeof a.worktree !== "string" || !a.worktree) throw new Error(`agents[${i}].worktree is required`);
21857
21971
  const runtime = runtimeOf(a, i);
21858
21972
  const specialization = specializationOf(a, i);
21859
- agents.push({ name: a.name, runtime, worktree: resolve5(specDir, a.worktree), specialization, ...tierOf(a, i, specialization), ...modelOf(a, i) });
21973
+ agents.push({ name: a.name, runtime, worktree: resolve5(specDir, a.worktree), specialization, ...tierOf(a, i, specialization), ...modelOf(a, i), ...isolateOf(a, i, runtime) });
21860
21974
  });
21861
21975
  for (const p of pools) {
21862
21976
  for (const a of agents) {
@@ -22731,7 +22845,7 @@ function inspectProcess(pid) {
22731
22845
  }
22732
22846
  }
22733
22847
  function childSpec(agent, creds, worker, onDemand = false) {
22734
- const { ANTHROPIC_API_KEY: _key, ANTHROPIC_AUTH_TOKEN: _tok, ...inherited } = process.env;
22848
+ const { ANTHROPIC_API_KEY: _key, ANTHROPIC_AUTH_TOKEN: _tok, PITBOSS_ISOLATE_USER_CONFIG: _iso, ...inherited } = process.env;
22735
22849
  return {
22736
22850
  name: agent.name,
22737
22851
  command: worker.command,
@@ -22749,7 +22863,8 @@ function childSpec(agent, creds, worker, onDemand = false) {
22749
22863
  SPECIALIZATION: agent.specialization,
22750
22864
  WORKER_RUNTIME: agent.runtime,
22751
22865
  PITBOSS_ACTIVITY_FILE: activityPath(agent.name),
22752
- PITBOSS_MODEL_FILE: resolve8(modelPath(agent.name))
22866
+ PITBOSS_MODEL_FILE: resolve8(modelPath(agent.name)),
22867
+ ...agent.isolateUserConfig ? { PITBOSS_ISOLATE_USER_CONFIG: "1" } : {}
22753
22868
  }
22754
22869
  };
22755
22870
  }
@@ -449,6 +449,9 @@ var USAGE_NUMBER_KEYS = [
449
449
  "cache_read_input_tokens",
450
450
  "cache_creation_input_tokens"
451
451
  ];
452
+ function notReportedUsage(reason) {
453
+ return { notReported: true, reason };
454
+ }
452
455
  function finiteNumber(value) {
453
456
  return typeof value === "number" && Number.isFinite(value) ? value : void 0;
454
457
  }
@@ -516,14 +519,32 @@ async function getTaskStatus(conn, id, logPrefix) {
516
519
  return void 0;
517
520
  }
518
521
  }
519
- async function sessionTouchedTaskIds(conn, candidates, logPrefix) {
522
+ async function listUpdatedSince(conn, since, logPrefix) {
523
+ try {
524
+ const res = await fetch(
525
+ `${conn.apiUrl}/tasks?repoId=${conn.repoId}&assignedTo=${conn.agentId}&updatedAfter=${encodeURIComponent(since)}`,
526
+ { headers: { Authorization: `Bearer ${conn.apiSecret}` } }
527
+ );
528
+ if (!res.ok) throw new Error(`task list failed (${res.status} ${res.statusText})`);
529
+ const { data } = await res.json();
530
+ return (data ?? []).filter((row) => typeof row.id === "string" && row.id.length > 0).filter((row) => row.status !== "assigned").map((row) => row.id);
531
+ } catch (err) {
532
+ console.error(
533
+ `${logPrefix} Could not list tasks updated since the session started:`,
534
+ err instanceof Error ? err.message : String(err)
535
+ );
536
+ return [];
537
+ }
538
+ }
539
+ async function sessionTouchedTaskIds(conn, candidates, logPrefix, since) {
520
540
  const touched = [];
521
541
  for (const id of candidates) {
522
542
  const status = await getTaskStatus(conn, id, logPrefix);
523
543
  if (status !== void 0 && status !== "assigned") touched.push(id);
524
544
  }
525
545
  const inProgressAtClose = await listSessionTaskIds(conn, logPrefix, "in_progress");
526
- return unionTaskIds(touched, inProgressAtClose);
546
+ const updated = since ? await listUpdatedSince(conn, since, logPrefix) : [];
547
+ return unionTaskIds(touched, inProgressAtClose, updated);
527
548
  }
528
549
  function unionTaskIds(...lists) {
529
550
  const seen = /* @__PURE__ */ new Set();
@@ -537,34 +558,87 @@ function unionTaskIds(...lists) {
537
558
  }
538
559
  return out;
539
560
  }
540
- async function writeSessionUsage(conn, taskIds, usage, logPrefix) {
541
- const sessionUsage = {
542
- ...usage,
543
- at: (/* @__PURE__ */ new Date()).toISOString(),
544
- sessionTaskIds: [...taskIds]
545
- };
561
+ async function writeSessionUsage(conn, taskIds, usage, logPrefix, opts = {}) {
546
562
  const headers = { "Content-Type": "application/json", Authorization: `Bearer ${conn.apiSecret}` };
547
563
  for (const id of taskIds) {
564
+ const slice = taskIds.length > 1 && !usage.notReported ? opts.slices?.[id] : void 0;
565
+ const share = usage.notReported ? void 0 : slice || taskIds.length === 1 ? "task" : "session";
566
+ const body = {
567
+ usage: slice ?? usage,
568
+ sessionTaskIds: taskIds,
569
+ ...opts.model ? { model: opts.model } : {},
570
+ ...share ? { share } : {}
571
+ };
548
572
  try {
549
- const put = await fetch(`${conn.apiUrl}/tasks/${id}`, {
550
- method: "PUT",
573
+ const res = await fetch(`${conn.apiUrl}/tasks/${id}/usage-report`, {
574
+ method: "POST",
551
575
  headers,
552
- body: JSON.stringify({ metadata: { sessionUsage } })
576
+ body: JSON.stringify(body)
553
577
  });
554
- if (!put.ok) {
555
- console.error(`${logPrefix} Could not write sessionUsage on ${id} (${put.status} ${put.statusText})`);
578
+ if (!res.ok) {
579
+ console.error(`${logPrefix} Could not report usage on ${id} (${res.status} ${res.statusText})`);
556
580
  }
557
581
  } catch (err) {
558
582
  console.error(
559
- `${logPrefix} Could not write sessionUsage on ${id}:`,
583
+ `${logPrefix} Could not report usage on ${id}:`,
560
584
  err instanceof Error ? err.message : String(err)
561
585
  );
562
586
  }
563
587
  }
564
588
  }
565
- async function recordSessionUsage(conn, startIds, usage, logPrefix) {
589
+ async function recordSessionUsage(conn, startIds, usage, logPrefix, opts = {}) {
566
590
  const start = await startIds;
567
- await writeSessionUsage(conn, await sessionTouchedTaskIds(conn, start, logPrefix), usage, logPrefix);
591
+ const { since, ...rest } = opts;
592
+ await writeSessionUsage(conn, await sessionTouchedTaskIds(conn, start, logPrefix, since), usage, logPrefix, rest);
593
+ }
594
+ var BOUNDARY_STATUSES = /* @__PURE__ */ new Set(["completed", "blocked", "pending_verification"]);
595
+ function addUsage(into, from) {
596
+ into.num_turns = (into.num_turns ?? 0) + (from.num_turns ?? 0);
597
+ const total = into.usage ?? {};
598
+ for (const key of USAGE_NUMBER_KEYS) {
599
+ const n = from.usage?.[key];
600
+ if (n !== void 0) total[key] = (total[key] ?? 0) + n;
601
+ }
602
+ into.usage = total;
603
+ }
604
+ function createUsageTracker() {
605
+ let model;
606
+ let pending = /* @__PURE__ */ new Map();
607
+ const slices = {};
608
+ let anon = 0;
609
+ function cut(taskId) {
610
+ const slice = slices[taskId] ?? {};
611
+ for (const messageUsage of pending.values()) addUsage(slice, messageUsage);
612
+ pending = /* @__PURE__ */ new Map();
613
+ if (slice.usage) slices[taskId] = slice;
614
+ }
615
+ return {
616
+ observe(event) {
617
+ if (event.type === "system" && event.subtype === "init") {
618
+ if (typeof event.model === "string" && event.model) model = event.model;
619
+ return;
620
+ }
621
+ if (event.type !== "assistant") return;
622
+ const message = event.message ?? {};
623
+ const raw = message.usage;
624
+ if (raw && typeof raw === "object" && !Array.isArray(raw)) {
625
+ const usage = pickNumbers(raw, USAGE_NUMBER_KEYS);
626
+ if (Object.keys(usage).length > 0) {
627
+ pending.set(typeof message.id === "string" ? message.id : `anon_${anon++}`, { num_turns: 1, usage });
628
+ }
629
+ }
630
+ const content = Array.isArray(message.content) ? message.content : [];
631
+ for (const block of content) {
632
+ if (block.type !== "tool_use" || typeof block.name !== "string" || !block.name.endsWith("update_task_status")) continue;
633
+ const input = block.input ?? {};
634
+ if (typeof input.taskId === "string" && typeof input.status === "string" && BOUNDARY_STATUSES.has(input.status)) {
635
+ cut(input.taskId);
636
+ }
637
+ }
638
+ },
639
+ model: () => model,
640
+ opts: () => ({ ...model ? { model } : {}, slices })
641
+ };
568
642
  }
569
643
 
570
644
  // ../claude-worker/src/model.ts
@@ -611,26 +685,36 @@ function writeMcpConfig(config2, prefix = "claude-worker") {
611
685
  writeFileSync(path, JSON.stringify(mcpConfig), { mode: 384 });
612
686
  return path;
613
687
  }
688
+ function claudeArgs(opts) {
689
+ return [
690
+ "--print",
691
+ "--mcp-config",
692
+ opts.mcpConfigPath,
693
+ "--no-session-persistence",
694
+ "--dangerously-skip-permissions",
695
+ "--output-format",
696
+ "stream-json",
697
+ "--verbose",
698
+ "--model",
699
+ opts.model,
700
+ "--add-dir",
701
+ opts.repoPath,
702
+ ...opts.isolateUserConfig ? ["--setting-sources", "project,local"] : []
703
+ ];
704
+ }
614
705
  async function runClaudeSession(config2) {
615
706
  const prompt = buildPrompt(config2);
616
707
  const mcpConfigPath = writeMcpConfig(config2);
708
+ const startedAt = (/* @__PURE__ */ new Date()).toISOString();
617
709
  const taskIdsPromise = listSessionTaskIds(config2, "[claude-worker]");
618
710
  try {
619
711
  return await new Promise((resolve, reject) => {
620
- const args = [
621
- "--print",
622
- "--mcp-config",
712
+ const args = claudeArgs({
623
713
  mcpConfigPath,
624
- "--no-session-persistence",
625
- "--dangerously-skip-permissions",
626
- "--output-format",
627
- "stream-json",
628
- "--verbose",
629
- "--model",
630
- pinnedModel() ?? config2.model,
631
- "--add-dir",
632
- config2.repoPath
633
- ];
714
+ model: pinnedModel() ?? config2.model,
715
+ repoPath: config2.repoPath,
716
+ isolateUserConfig: process.env.PITBOSS_ISOLATE_USER_CONFIG === "1"
717
+ });
634
718
  console.log(`[claude-worker] Spawning: ${config2.claudeBin} ${args.slice(0, 4).join(" ")} ...`);
635
719
  console.log(`[claude-worker] cwd: ${config2.repoPath}`);
636
720
  const { ANTHROPIC_API_KEY, ANTHROPIC_AUTH_TOKEN, ...workerEnv } = process.env;
@@ -646,6 +730,8 @@ async function runClaudeSession(config2) {
646
730
  let buffer = "";
647
731
  let resultError = "";
648
732
  let sessionUsage;
733
+ let sawResult = false;
734
+ const tracker = createUsageTracker();
649
735
  proc.stdout.on("data", (chunk) => {
650
736
  buffer += chunk.toString();
651
737
  const lines = buffer.split("\n");
@@ -654,6 +740,7 @@ async function runClaudeSession(config2) {
654
740
  if (!line.trim()) continue;
655
741
  try {
656
742
  const event = JSON.parse(line);
743
+ tracker.observe(event);
657
744
  if (event.type === "assistant") {
658
745
  const content = event.message?.content ?? [];
659
746
  for (const block of content) {
@@ -667,6 +754,7 @@ async function runClaudeSession(config2) {
667
754
  }
668
755
  }
669
756
  } else if (event.type === "result") {
757
+ sawResult = true;
670
758
  const parsed = parseResultUsage(event);
671
759
  if (parsed) sessionUsage = parsed;
672
760
  if (event.is_error) {
@@ -688,9 +776,9 @@ async function runClaudeSession(config2) {
688
776
  proc.on("close", (code, signal) => {
689
777
  const unique = [...new Set(toolsUsed)];
690
778
  console.log(`[claude-worker] Done \u2014 tools used: ${unique.length ? unique.join(", ") : "none"}`);
691
- const usage = sessionUsage;
692
- if (usage) console.log(formatSessionUsageLine("[claude-worker]", usage));
693
- const finish = usage ? recordSessionUsage(config2, taskIdsPromise, usage, "[claude-worker]") : Promise.resolve();
779
+ const usage = sessionUsage ?? notReportedUsage(sawResult ? "adapter" : "killed");
780
+ if (sessionUsage) console.log(formatSessionUsageLine("[claude-worker]", sessionUsage));
781
+ const finish = recordSessionUsage(config2, taskIdsPromise, usage, "[claude-worker]", { since: startedAt, ...tracker.opts() });
694
782
  const settle = () => {
695
783
  if (signal || code !== 0 && code !== null) {
696
784
  const detail = (resultError || stderrOutput || "").trim();
@@ -758,6 +846,9 @@ function readCursorEvent(line) {
758
846
  } catch {
759
847
  return {};
760
848
  }
849
+ if (event.type === "system" && event.subtype === "init") {
850
+ return typeof event.model === "string" && event.model ? { model: event.model } : {};
851
+ }
761
852
  if (event.type === "tool_call" && event.subtype === "started") {
762
853
  const call = event.tool_call ?? {};
763
854
  const [kind] = Object.keys(call);
@@ -775,6 +866,7 @@ function readCursorEvent(line) {
775
866
  async function runCursorSession(config2) {
776
867
  const args = cursorArgs(config2.repoPath, buildPrompt(config2), pinnedModel());
777
868
  console.log(`[cursor-worker] Spawning: ${bin()} ${args.slice(0, 5).join(" ")} ...`);
869
+ const startedAt = (/* @__PURE__ */ new Date()).toISOString();
778
870
  const taskIdsPromise = listSessionTaskIds(config2, "[cursor-worker]");
779
871
  return new Promise((resolve, reject) => {
780
872
  const proc = spawn2(bin(), args, { cwd: config2.repoPath, env: process.env, stdio: ["ignore", "pipe", "pipe"] });
@@ -784,12 +876,14 @@ async function runCursorSession(config2) {
784
876
  let resultError = "";
785
877
  let stderrOutput = "";
786
878
  let sessionUsage;
879
+ let model;
787
880
  proc.stdout.on("data", (chunk) => {
788
881
  buffer += chunk.toString();
789
882
  const lines = buffer.split("\n");
790
883
  buffer = lines.pop() ?? "";
791
884
  for (const line of lines) {
792
- const { tool, error, usage } = readCursorEvent(line);
885
+ const { tool, error, usage, model: initModel } = readCursorEvent(line);
886
+ if (initModel) model = initModel;
793
887
  if (tool) {
794
888
  toolsUsed.push(tool);
795
889
  process.stdout.write(` \u2699 ${tool}
@@ -807,9 +901,9 @@ async function runCursorSession(config2) {
807
901
  });
808
902
  proc.on("close", (code, signal) => {
809
903
  console.log(`[cursor-worker] Done \u2014 tools used: ${toolsUsed.length ? [...new Set(toolsUsed)].join(", ") : "none"}`);
810
- const usage = sessionUsage;
811
- if (usage) console.log(formatSessionUsageLine("[cursor-worker]", usage));
812
- const finish = usage ? recordSessionUsage(config2, taskIdsPromise, usage, "[cursor-worker]") : Promise.resolve();
904
+ const usage = sessionUsage ?? notReportedUsage("killed");
905
+ if (sessionUsage) console.log(formatSessionUsageLine("[cursor-worker]", sessionUsage));
906
+ const finish = recordSessionUsage(config2, taskIdsPromise, usage, "[cursor-worker]", { since: startedAt, ...model ? { model } : {} });
813
907
  const settle = () => {
814
908
  if (signal || code !== 0 && code !== null) {
815
909
  const detail = (resultError || stderrOutput).trim();
@@ -880,6 +974,7 @@ function readCopilotEvent(line) {
880
974
  const type = typeof event.type === "string" ? event.type : "";
881
975
  const data = event.data ?? {};
882
976
  const message = typeof data.message === "string" ? data.message : "";
977
+ if (type === "session.tools_updated" && typeof data.model === "string" && data.model) return { model: data.model };
883
978
  if (/^tool\..*start/.test(type) && typeof data.toolName === "string") return { tool: data.toolName };
884
979
  if (type === "session.error") return { error: message || "copilot session error" };
885
980
  if (type === "result" && typeof event.exitCode === "number" && event.exitCode !== 0) return { error: `copilot finished with exit code ${event.exitCode}` };
@@ -889,6 +984,8 @@ function readCopilotEvent(line) {
889
984
  async function runCopilotSession(config2) {
890
985
  const mcpConfigPath = writeMcpConfig(config2, "copilot-worker");
891
986
  const args = copilotArgs(config2.repoPath, buildPrompt(config2), mcpConfigPath, pinnedModel());
987
+ const startedAt = (/* @__PURE__ */ new Date()).toISOString();
988
+ const taskIdsPromise = listSessionTaskIds(config2, "[copilot-worker]");
892
989
  console.log(`[copilot-worker] Spawning: ${bin2()} -p <prompt> ${args.slice(2, 6).join(" ")} ...`);
893
990
  return new Promise((resolve, reject) => {
894
991
  const proc = spawn3(bin2(), args, { cwd: config2.repoPath, env: process.env, stdio: ["ignore", "pipe", "pipe"] });
@@ -897,12 +994,14 @@ async function runCopilotSession(config2) {
897
994
  let buffer = "";
898
995
  let sessionError = "";
899
996
  let stderrOutput = "";
997
+ let model;
900
998
  proc.stdout.on("data", (chunk) => {
901
999
  buffer += chunk.toString();
902
1000
  const lines = buffer.split("\n");
903
1001
  buffer = lines.pop() ?? "";
904
1002
  for (const line of lines) {
905
- const { tool, error } = readCopilotEvent(line);
1003
+ const { tool, error, model: initModel } = readCopilotEvent(line);
1004
+ if (initModel) model = initModel;
906
1005
  if (tool) {
907
1006
  toolsUsed.push(tool);
908
1007
  process.stdout.write(` \u2699 ${tool}
@@ -923,12 +1022,15 @@ async function runCopilotSession(config2) {
923
1022
  } catch {
924
1023
  }
925
1024
  console.log(`[copilot-worker] Done \u2014 tools used: ${toolsUsed.length ? [...new Set(toolsUsed)].join(", ") : "none"}`);
926
- if (sessionError || signal || code !== 0 && code !== null) {
927
- const detail = (sessionError || stderrOutput).trim();
928
- reject(new Error(`copilot exited with ${signal ?? `code ${code}`}${detail ? `: ${detail}` : ""}`));
929
- } else {
930
- resolve([...new Set(toolsUsed)]);
931
- }
1025
+ const settle = () => {
1026
+ if (sessionError || signal || code !== 0 && code !== null) {
1027
+ const detail = (sessionError || stderrOutput).trim();
1028
+ reject(new Error(`copilot exited with ${signal ?? `code ${code}`}${detail ? `: ${detail}` : ""}`));
1029
+ } else {
1030
+ resolve([...new Set(toolsUsed)]);
1031
+ }
1032
+ };
1033
+ recordSessionUsage(config2, taskIdsPromise, notReportedUsage("adapter"), "[copilot-worker]", { since: startedAt, ...model ? { model } : {} }).then(settle, settle);
932
1034
  });
933
1035
  proc.on("error", (err) => {
934
1036
  try {
@@ -1022,6 +1124,8 @@ async function runCodexSession(config2) {
1022
1124
  }
1023
1125
  };
1024
1126
  const args = codexArgs(config2.repoPath, buildPrompt(config2), profile, pinnedModel());
1127
+ const startedAt = (/* @__PURE__ */ new Date()).toISOString();
1128
+ const taskIdsPromise = listSessionTaskIds(config2, "[codex-worker]");
1025
1129
  console.log(`[codex-worker] Spawning: ${bin3()} ${args.slice(0, 6).join(" ")} ...`);
1026
1130
  return new Promise((resolve, reject) => {
1027
1131
  const proc = spawn4(bin3(), args, { cwd: config2.repoPath, env: process.env, stdio: ["ignore", "pipe", "pipe"] });
@@ -1053,12 +1157,15 @@ async function runCodexSession(config2) {
1053
1157
  proc.on("close", (code, signal) => {
1054
1158
  cleanup();
1055
1159
  console.log(`[codex-worker] Done \u2014 tools used: ${toolsUsed.length ? [...new Set(toolsUsed)].join(", ") : "none"}`);
1056
- if (sessionError || signal || code !== 0 && code !== null) {
1057
- const detail = (sessionError || stderrOutput).trim();
1058
- reject(new Error(`codex exited with ${signal ?? `code ${code}`}${detail ? `: ${detail}` : ""}`));
1059
- } else {
1060
- resolve([...new Set(toolsUsed)]);
1061
- }
1160
+ const settle = () => {
1161
+ if (sessionError || signal || code !== 0 && code !== null) {
1162
+ const detail = (sessionError || stderrOutput).trim();
1163
+ reject(new Error(`codex exited with ${signal ?? `code ${code}`}${detail ? `: ${detail}` : ""}`));
1164
+ } else {
1165
+ resolve([...new Set(toolsUsed)]);
1166
+ }
1167
+ };
1168
+ recordSessionUsage(config2, taskIdsPromise, notReportedUsage("adapter"), "[codex-worker]", { since: startedAt }).then(settle, settle);
1062
1169
  });
1063
1170
  proc.on("error", (err) => {
1064
1171
  cleanup();
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@pitboss/cli",
3
- "version": "0.4.9",
3
+ "version": "0.4.10",
4
4
  "private": false,
5
5
  "description": "Connect your coding agents to a pitboss instance",
6
6
  "keywords": [