@tablation/crew 0.129.1 → 0.130.1

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.
Files changed (2) hide show
  1. package/dist/cli.js +192 -62
  2. package/package.json +1 -1
package/dist/cli.js CHANGED
@@ -1259,6 +1259,14 @@ var claudePair = {
1259
1259
  removePair: (o) => removeClaudeMcp({ bin: o.bin ?? "claude", dryRun: o.dryRun })
1260
1260
  };
1261
1261
  var inferenceBaseUrl = (baseUrl, workspaceId) => `${baseUrl.replace(/\/+$/, "")}/api/workspaces/${workspaceId}/inference`;
1262
+ function inferenceHeaders(inf) {
1263
+ const clean = (v) => v.replace(/[\r\n]+/g, " ").trim();
1264
+ const lines2 = [
1265
+ ...inf.ticket ? [`X-Crew-Ticket: ${clean(inf.ticket)}`] : [],
1266
+ ...inf.runId ? [`X-Crew-Run: ${clean(inf.runId)}`] : []
1267
+ ];
1268
+ return lines2.length > 0 ? lines2.join("\n") : void 0;
1269
+ }
1262
1270
  function workspaceSettings(baseUrl) {
1263
1271
  const blank = BILLING_VARS_TO_UNSET.filter((v) => v !== "ANTHROPIC_AUTH_TOKEN");
1264
1272
  return JSON.stringify({ env: { ...Object.fromEntries(blank.map((v) => [v, ""])), ANTHROPIC_BASE_URL: baseUrl } });
@@ -1362,7 +1370,11 @@ var claudeAdapter = {
1362
1370
  // variables are already unset at exec (`unsetEnv`), and a spec-level `env:` entry must not
1363
1371
  // smuggle one back in.
1364
1372
  ...spec.auth === "workspace" ? withoutProviderVars(spec.env) : spec.env,
1365
- ...spec.auth === "workspace" && spec.inference ? { ANTHROPIC_BASE_URL: spec.inference.baseUrl, ANTHROPIC_AUTH_TOKEN: spec.inference.token } : {}
1373
+ ...spec.auth === "workspace" && spec.inference ? {
1374
+ ANTHROPIC_BASE_URL: spec.inference.baseUrl,
1375
+ ANTHROPIC_AUTH_TOKEN: spec.inference.token,
1376
+ ...inferenceHeaders(spec.inference) ? { ANTHROPIC_CUSTOM_HEADERS: inferenceHeaders(spec.inference) } : {}
1377
+ } : {}
1366
1378
  };
1367
1379
  },
1368
1380
  provider: () => "anthropic",
@@ -3210,7 +3222,7 @@ function parseRepoConfig(text2, file) {
3210
3222
  if (!MATCHES.includes(vmatch)) {
3211
3223
  throw new RepoConfigError(`${file}: release.verify.match must be commit|version (got "${vmatch}")`);
3212
3224
  }
3213
- const num = (v, key, dflt) => {
3225
+ const num2 = (v, key, dflt) => {
3214
3226
  if (v === void 0 || v === null) return dflt;
3215
3227
  const n = Number(v);
3216
3228
  if (!Number.isFinite(n) || n <= 0) {
@@ -3220,8 +3232,8 @@ function parseRepoConfig(text2, file) {
3220
3232
  };
3221
3233
  const verify = {
3222
3234
  match: vmatch,
3223
- timeoutSeconds: num(raw.release?.verify?.timeoutSeconds, "timeoutSeconds", 600),
3224
- intervalSeconds: num(raw.release?.verify?.intervalSeconds, "intervalSeconds", 15)
3235
+ timeoutSeconds: num2(raw.release?.verify?.timeoutSeconds, "timeoutSeconds", 600),
3236
+ intervalSeconds: num2(raw.release?.verify?.intervalSeconds, "intervalSeconds", 15)
3225
3237
  };
3226
3238
  if (verify.intervalSeconds > verify.timeoutSeconds) {
3227
3239
  throw new RepoConfigError(
@@ -3794,7 +3806,7 @@ function parseReleaseOverride(raw, where, file) {
3794
3806
  if (versioning !== void 0 && !VERSIONINGS.includes(versioning)) {
3795
3807
  throw new ConfigError(`${file}: ${where}.release.versioning must be auto|none (got "${raw.versioning}")`);
3796
3808
  }
3797
- const num = (v, key) => {
3809
+ const num2 = (v, key) => {
3798
3810
  if (v === void 0 || v === null) return void 0;
3799
3811
  const n = Number(v);
3800
3812
  if (!Number.isFinite(n) || n <= 0) {
@@ -3802,8 +3814,8 @@ function parseReleaseOverride(raw, where, file) {
3802
3814
  }
3803
3815
  return n;
3804
3816
  };
3805
- const timeoutSeconds = num(raw.verify?.timeoutSeconds, "timeoutSeconds");
3806
- const intervalSeconds = num(raw.verify?.intervalSeconds, "intervalSeconds");
3817
+ const timeoutSeconds = num2(raw.verify?.timeoutSeconds, "timeoutSeconds");
3818
+ const intervalSeconds = num2(raw.verify?.intervalSeconds, "intervalSeconds");
3807
3819
  const isolateMax = parseIsolateMax(
3808
3820
  raw.isolateFailures?.maxCandidates,
3809
3821
  `${where}.release.isolateFailures`,
@@ -5496,20 +5508,20 @@ function branchForIssue(cwd, issueKey, templates = { name: "issue-{number}" }, r
5496
5508
  return null;
5497
5509
  }
5498
5510
  function branchCandidates(issueKey, templates, render2) {
5499
- const num = issueKey.replace(/^\D+/, "");
5500
- const unpadded = String(Number.parseInt(num, 10));
5511
+ const num2 = issueKey.replace(/^\D+/, "");
5512
+ const unpadded = String(Number.parseInt(num2, 10));
5501
5513
  const candidates2 = [];
5502
5514
  for (const template of [templates.name, templates.push].filter(Boolean)) {
5503
5515
  const exact = render2(template);
5504
5516
  candidates2.push(exact);
5505
- if (num !== unpadded) candidates2.push(exact.replace(num, unpadded));
5517
+ if (num2 !== unpadded) candidates2.push(exact.replace(num2, unpadded));
5506
5518
  if (template.includes("{slug}")) {
5507
5519
  const prefix = render2(template.slice(0, template.indexOf("{slug}")));
5508
5520
  if (prefix) candidates2.push(`${prefix}*`);
5509
5521
  }
5510
5522
  }
5511
- candidates2.push(`issue-${num}`);
5512
- if (num !== unpadded) candidates2.push(`issue-${unpadded}`);
5523
+ candidates2.push(`issue-${num2}`);
5524
+ if (num2 !== unpadded) candidates2.push(`issue-${unpadded}`);
5513
5525
  return [...new Set(candidates2)];
5514
5526
  }
5515
5527
  function remoteBranchForIssue(cwd, remote, issueKey, templates = { name: "issue-{number}" }, render2 = (t) => t.replace(/\{number\}/g, issueKey.replace(/^\D+/, ""))) {
@@ -5580,8 +5592,8 @@ function deleteRemoteBranch(cwd, remote, branch) {
5580
5592
  if (!remoteBranchExists(cwd, remote, branch)) return true;
5581
5593
  return gitOk(cwd, ["push", remote, "--delete", branch]) !== null;
5582
5594
  }
5583
- function worktreeForNumber(cwd, num) {
5584
- const re = new RegExp(`(?:^|-)${num}$`);
5595
+ function worktreeForNumber(cwd, num2) {
5596
+ const re = new RegExp(`(?:^|-)${num2}$`);
5585
5597
  return worktrees(cwd).find((w) => re.test(basename5(w.path))) ?? null;
5586
5598
  }
5587
5599
  var commitBodies = (cwd, range) => gitOk(cwd, ["log", range, "--format=%B"]) ?? "";
@@ -6475,7 +6487,7 @@ import { readFileSync as readFileSync26, existsSync as existsSync25, statSync as
6475
6487
  import { StringDecoder } from "string_decoder";
6476
6488
  import { finished } from "stream/promises";
6477
6489
  import { join as join29, dirname as dirname14, basename as basename9 } from "path";
6478
- import { createHash as createHash5 } from "crypto";
6490
+ import { createHash as createHash5, randomUUID } from "crypto";
6479
6491
 
6480
6492
  // src/state.ts
6481
6493
  import { readFileSync as readFileSync19, writeFileSync as writeFileSync14, existsSync as existsSync18, mkdirSync as mkdirSync13, rmSync as rmSync10, statSync as statSync7, readdirSync as readdirSync5 } from "fs";
@@ -7302,6 +7314,27 @@ async function revokeSeatKeys(route2, o) {
7302
7314
  }
7303
7315
  const host = hostFromUrl2(route2.baseUrl);
7304
7316
  const base = route2.baseUrl.replace(/\/+$/, "");
7317
+ const headers = { Authorization: `Bearer ${resolveApiKey(route2)}`, "User-Agent": o.userAgent ?? "Mozilla/5.0 TablationCrewAgent/1.0" };
7318
+ const sweepNamedKeys = async (role, agentId) => {
7319
+ const prefix = `crew ${o.shipName} `;
7320
+ try {
7321
+ const res = await doFetch(`${base}/api/workspaces/${resolved.workspaceId}/agents/${agentId}/api-keys`, { headers });
7322
+ if (!res.ok) return;
7323
+ const keys = await res.json();
7324
+ for (const k of Array.isArray(keys) ? keys : []) {
7325
+ if (!k.id || k.revokedAt || !k.name?.startsWith(prefix)) continue;
7326
+ if (o.dryRun) {
7327
+ out.push({ role, action: "revoked", detail: `dry run; key "${k.name}" found on the server` });
7328
+ continue;
7329
+ }
7330
+ const del2 = await doFetch(`${base}/api/workspaces/${resolved.workspaceId}/agents/${agentId}/api-keys/${k.id}`, { method: "DELETE", headers });
7331
+ if (del2.ok || del2.status === 404) out.push({ role, action: "revoked", detail: `key "${k.name}" found on the server, not in the keychain` });
7332
+ else out.push({ role, action: "failed", detail: `key "${k.name}": ${del2.status} ${del2.statusText}` });
7333
+ }
7334
+ } catch (e) {
7335
+ out.push({ role, action: "failed", detail: `could not sweep keys named for this ship (${e.message})` });
7336
+ }
7337
+ };
7305
7338
  for (const { role, agentId } of targets) {
7306
7339
  if (!agentId) {
7307
7340
  out.push({ role, action: "no_agent" });
@@ -7312,6 +7345,7 @@ async function revokeSeatKeys(route2, o) {
7312
7345
  const session = await store.get(host, slug2);
7313
7346
  if (!session?.apiKey) {
7314
7347
  out.push({ role, action: "not_stored" });
7348
+ if (o.shipName) await sweepNamedKeys(role, agentId);
7315
7349
  continue;
7316
7350
  }
7317
7351
  if (!session.keyId) {
@@ -7332,6 +7366,7 @@ async function revokeSeatKeys(route2, o) {
7332
7366
  }
7333
7367
  await store.clear(host, slug2);
7334
7368
  out.push({ role, action: "revoked", detail: del2.ok ? void 0 : "server already had no such key" });
7369
+ if (o.shipName) await sweepNamedKeys(role, agentId);
7335
7370
  } catch (e) {
7336
7371
  out.push({ role, action: "failed", detail: /Command failed|security|sk_|-w /i.test(e.message) ? describeStoreError(e) : e.message });
7337
7372
  }
@@ -9606,8 +9641,8 @@ async function noteTakeover(tracker, result, seat, ships, myShipName, continuing
9606
9641
  }
9607
9642
  }
9608
9643
  function originBranchSha(dir, t, rev) {
9609
- const num = (t.issue_tag ?? t.issue_id).replace(/^\D+/, "");
9610
- const names = [...new Set([(t.issue_tag ?? "").toLowerCase(), `issue-${num}`].filter(Boolean))];
9644
+ const num2 = (t.issue_tag ?? t.issue_id).replace(/^\D+/, "");
9645
+ const names = [...new Set([(t.issue_tag ?? "").toLowerCase(), `issue-${num2}`].filter(Boolean))];
9611
9646
  for (const n of names) {
9612
9647
  const sha = rev(dir, `origin/${n}`);
9613
9648
  if (sha) return sha.slice(0, 7);
@@ -9669,8 +9704,8 @@ function planRunWorktree(dir, cfg, t, role, opts = {}) {
9669
9704
  unlock();
9670
9705
  }
9671
9706
  }
9672
- const num = t.issue_id.replace(/^\D+/, "");
9673
- const existing = worktreeForNumber(dir, num);
9707
+ const num2 = t.issue_id.replace(/^\D+/, "");
9708
+ const existing = worktreeForNumber(dir, num2);
9674
9709
  const located = locateBranchForTicket(dir, cfg, t, role);
9675
9710
  const baseRef = hasRemote ? `${remote}/${base}` : base;
9676
9711
  if (existing && !existing.prunable && existsSync21(existing.path)) {
@@ -10167,6 +10202,49 @@ async function guardRunStrays(route2, ship, tracker, role, workingId, since2, lo
10167
10202
  }
10168
10203
 
10169
10204
  // src/agent-log.ts
10205
+ var num = (v) => typeof v === "number" && Number.isFinite(v) ? v : Number(v) || 0;
10206
+ function sumInferenceRows(rows) {
10207
+ const t = { calls: 0, tokensIn: 0, tokensOut: 0, cacheReadTokens: 0, cacheWriteTokens: 0, costUsd: 0 };
10208
+ for (const r of rows) {
10209
+ t.calls++;
10210
+ t.tokensIn += num(r.input_tokens);
10211
+ t.tokensOut += num(r.output_tokens);
10212
+ t.cacheReadTokens += num(r.cache_read_tokens);
10213
+ t.cacheWriteTokens += num(r.cache_write_tokens);
10214
+ t.costUsd += num(r.cost_usd);
10215
+ }
10216
+ return t;
10217
+ }
10218
+ var INFERENCE_PAGE = 1e3;
10219
+ async function listInferenceRows(target, modelId, runId) {
10220
+ const filters = encodeURIComponent(JSON.stringify({
10221
+ combinator: "AND",
10222
+ conditions: [
10223
+ { columnName: "run_id", operator: "EQ", value: runId },
10224
+ { columnName: "client", operator: "EQ", value: "inference" }
10225
+ ]
10226
+ }));
10227
+ const out = [];
10228
+ for (let offset = 0; ; offset += INFERENCE_PAGE) {
10229
+ const page = await request(
10230
+ target,
10231
+ "GET",
10232
+ `/data-models/${modelId}/records?limit=${INFERENCE_PAGE}&offset=${offset}&filters=${filters}`
10233
+ );
10234
+ out.push(...page);
10235
+ if (page.length < INFERENCE_PAGE) return out;
10236
+ }
10237
+ }
10238
+ async function inferenceTotalsFor(target, runId) {
10239
+ const model = await request(
10240
+ target,
10241
+ "GET",
10242
+ `/data-models/agent_log?workspaceId=${encodeURIComponent(target.workspaceId)}`
10243
+ );
10244
+ if (!(await columnsOf(target, model.id)).has("run_id")) return void 0;
10245
+ const rows = await listInferenceRows(target, model.id, runId);
10246
+ return rows.length > 0 ? sumInferenceRows(rows) : void 0;
10247
+ }
10170
10248
  var AgentLogUnreachableError = class extends Error {
10171
10249
  };
10172
10250
  var AgentLogHttpError = class extends Error {
@@ -10331,19 +10409,20 @@ async function reportAgentRun(target, run3) {
10331
10409
  promptVersion: run3.promptVersion,
10332
10410
  promptSha: run3.promptSha
10333
10411
  };
10412
+ const { runId, ...legacyBody } = { ...body, runId: run3.runId };
10334
10413
  const workItem = target.tickets && ticketRecordId ? { workItemTable: target.tickets.issuesModelId, workItemRecordId: ticketRecordId } : {};
10335
10414
  let entry;
10336
10415
  try {
10337
- entry = await post(target, "/agents/log", { ...body, ...workItem, harness: run3.harness, provider: run3.provider });
10416
+ entry = await post(target, "/agents/log", { ...body, ...runId ? { runId } : {}, ...workItem, harness: run3.harness, provider: run3.provider });
10338
10417
  } catch (e) {
10339
10418
  if (!(e instanceof AgentLogHttpError)) throw new AgentLogUnreachableError(e.message);
10340
10419
  if (e.status >= 500) throw new AgentLogUnreachableError(e.message);
10341
- if (e.status !== 400 || !run3.harness && !run3.provider && !ticketRecordId) {
10420
+ if (e.status !== 400 || !run3.harness && !run3.provider && !ticketRecordId && !runId) {
10342
10421
  e.row = true;
10343
10422
  throw e;
10344
10423
  }
10345
10424
  try {
10346
- entry = await post(target, "/agents/log", body);
10425
+ entry = await post(target, "/agents/log", legacyBody);
10347
10426
  } catch (e2) {
10348
10427
  if (e2 instanceof AgentLogHttpError && e2.status < 500) e2.row = true;
10349
10428
  throw e2;
@@ -10362,10 +10441,21 @@ async function reportAgentRun(target, run3) {
10362
10441
  const bounces = await countAcceptanceBounces(target, target.tickets.issuesModelId, ticketRecordId);
10363
10442
  if (bounces !== null) await writeHandoffBounces(target, entry.id, bounces);
10364
10443
  }
10365
- const counters = target.tickets && ticketRecordId ? await addRunToTicket(target, target.tickets, ticketRecordId, run3) : void 0;
10444
+ let counterWarning;
10445
+ let counted = run3;
10446
+ if (runId && target.tickets && ticketRecordId) {
10447
+ try {
10448
+ const totals = await inferenceTotalsFor(target, runId);
10449
+ if (totals) counted = { ...run3, tokensIn: totals.tokensIn, tokensOut: totals.tokensOut, cacheReadTokens: totals.cacheReadTokens, cacheWriteTokens: totals.cacheWriteTokens, costUsd: totals.costUsd };
10450
+ else counted = { ...run3, ...run3.harnessTotals };
10451
+ } catch (e) {
10452
+ counterWarning = `could not sum run ${runId}'s inference rows for ${run3.ticketReference}: ${e.message}`;
10453
+ }
10454
+ }
10455
+ const counters = target.tickets && ticketRecordId ? await addRunToTicket(target, target.tickets, ticketRecordId, counted) : void 0;
10366
10456
  return {
10367
10457
  agentLogId: entry.id,
10368
- ...counters?.warning ? { counterWarning: counters.warning } : {},
10458
+ ...counters?.warning || counterWarning ? { counterWarning: [counterWarning, counters?.warning].filter(Boolean).join("; ") } : {},
10369
10459
  ...counters?.retry ? { counterRetry: counters.retry } : {}
10370
10460
  };
10371
10461
  }
@@ -11941,6 +12031,7 @@ function planAgentRun(o) {
11941
12031
  const seat = resolveSeat(o.ship, o.route, o.role, o.agentPersona ?? (o.agentModel ? { model: o.agentModel } : {}), o.repo);
11942
12032
  const { model } = seat;
11943
12033
  let toolchain = seat.toolchain;
12034
+ let workspaceRunId;
11944
12035
  if (toolchain.auth === "workspace") {
11945
12036
  if (!o.apiKey || o.seatKind === "ship") {
11946
12037
  throw new WorkspaceAuthRefused(`${o.role}: toolchain ${toolchain.name} has auth: workspace, which needs the seat's own agent key \u2014 this seat would fall back to the ship key (run \`crew agents sync\`)`);
@@ -11948,7 +12039,8 @@ function planAgentRun(o) {
11948
12039
  if (!o.route.resolved) {
11949
12040
  throw new WorkspaceAuthRefused(`${o.role}: toolchain ${toolchain.name} has auth: workspace, but route ${o.route.route} is not connected (run \`crew connect\`)`);
11950
12041
  }
11951
- toolchain = { ...toolchain, inference: { baseUrl: inferenceBaseUrl(o.route.baseUrl, o.route.resolved.workspaceId), token: o.apiKey } };
12042
+ workspaceRunId = o.runId ?? randomUUID();
12043
+ toolchain = { ...toolchain, inference: { baseUrl: inferenceBaseUrl(o.route.baseUrl, o.route.resolved.workspaceId), token: o.apiKey, ticket: o.ticket, runId: workspaceRunId } };
11952
12044
  }
11953
12045
  const adapter = adapterFor(toolchain.harness);
11954
12046
  const code = codeBridgeFor(o, adapter.harness);
@@ -12045,7 +12137,8 @@ function planAgentRun(o) {
12045
12137
  routeName: o.route.route,
12046
12138
  agentLog,
12047
12139
  promptVersion: o.route.resolved?.agentPersonas?.[o.role]?.historyId,
12048
- promptSha
12140
+ promptSha,
12141
+ ...workspaceRunId ? { workspaceRunId } : {}
12049
12142
  };
12050
12143
  }
12051
12144
  function describePlan(p) {
@@ -12099,7 +12192,8 @@ function identityOf(plan, adapterHarness) {
12099
12192
  streamPath: plan.streamPath,
12100
12193
  agentLog: plan.agentLog ? withoutKey(plan.agentLog) : void 0,
12101
12194
  promptVersion: plan.promptVersion,
12102
- promptSha: plan.promptSha
12195
+ promptSha: plan.promptSha,
12196
+ ...plan.workspaceRunId ? { workspaceRunId: plan.workspaceRunId } : {}
12103
12197
  };
12104
12198
  }
12105
12199
  async function killForDeadline(kill, pid, emit, role, ticket, spawnedAt, limitMinutes) {
@@ -12406,11 +12500,22 @@ async function finishRun(id, obs, emit, startedAt, end, agentLogKey, hintMoved)
12406
12500
  finishedAt: (/* @__PURE__ */ new Date()).toISOString(),
12407
12501
  cycles: obs.cycles,
12408
12502
  model: id.model,
12409
- tokensIn: result?.inputTokens,
12410
- tokensOut: result?.outputTokens,
12411
- cacheReadTokens: result?.cacheReadTokens,
12412
- cacheWriteTokens: result?.cacheWriteTokens,
12413
- costUsd: result?.costUsd,
12503
+ // CREW-2980: under `auth: workspace` the endpoint's rows are the accounting; the run row
12504
+ // keeps turns only, and `reportAgentRun` sums those rows into the ticket counters.
12505
+ ...id.workspaceRunId ? { runId: id.workspaceRunId } : {
12506
+ tokensIn: result?.inputTokens,
12507
+ tokensOut: result?.outputTokens,
12508
+ cacheReadTokens: result?.cacheReadTokens,
12509
+ cacheWriteTokens: result?.cacheWriteTokens,
12510
+ costUsd: result?.costUsd
12511
+ },
12512
+ ...id.workspaceRunId ? { harnessTotals: {
12513
+ tokensIn: result?.inputTokens,
12514
+ tokensOut: result?.outputTokens,
12515
+ cacheReadTokens: result?.cacheReadTokens,
12516
+ cacheWriteTokens: result?.cacheWriteTokens,
12517
+ costUsd: result?.costUsd
12518
+ } } : {},
12414
12519
  // Both come from a finished run: without the result line (interrupted) neither is trustworthy.
12415
12520
  ...result?.numTurns !== void 0 ? { numTurns: result.numTurns } : {},
12416
12521
  ...result?.numTurns !== void 0 && obs.prefixTokens !== void 0 ? { prefixTokens: obs.prefixTokens } : {},
@@ -12537,7 +12642,8 @@ function spawnAgent(plan, emit, hintMoved) {
12537
12642
  eventsPath: plan.eventsPath,
12538
12643
  agentLog: id.agentLog,
12539
12644
  promptVersion: plan.promptVersion,
12540
- promptSha: plan.promptSha
12645
+ promptSha: plan.promptSha,
12646
+ ...plan.workspaceRunId ? { workspaceRunId: plan.workspaceRunId } : {}
12541
12647
  };
12542
12648
  try {
12543
12649
  writeFileSync19(pidPathFor(plan.streamPath), JSON.stringify(sidecar));
@@ -12792,14 +12898,14 @@ function describeRun(run3, route2, seatIds, worktreeOf = (d, n) => {
12792
12898
  }) {
12793
12899
  const ownRoute = run3.route === routeSlug(route2.route);
12794
12900
  const repoDir = run3.repo ? reposOf(route2).find((t) => t.name === run3.repo)?.dir : void 0;
12795
- const num = run3.ticket?.replace(/^\D+/, "") ?? "";
12901
+ const num2 = run3.ticket?.replace(/^\D+/, "") ?? "";
12796
12902
  return {
12797
12903
  role: run3.role,
12798
12904
  seat: ownRoute ? seatIds[run3.role] ?? null : null,
12799
12905
  ticket: run3.ticket,
12800
12906
  ticketRecordId: run3.ticketRecordId,
12801
12907
  repo: run3.repo ?? null,
12802
- worktree: repoDir && num ? worktreeOf(repoDir, num) : null,
12908
+ worktree: repoDir && num2 ? worktreeOf(repoDir, num2) : null,
12803
12909
  stream: run3.streamPath,
12804
12910
  pid: run3.pid,
12805
12911
  startedAt: run3.spawnedAt,
@@ -12821,7 +12927,12 @@ function labeledDocker() {
12821
12927
  if (!dockerAvailable()) return { volumes: [], images: [] };
12822
12928
  return {
12823
12929
  volumes: lines(docker(["volume", "ls", "-q", "--filter", `label=${DOCKER_LABEL_OWNER}`, "--filter", `label=${DOCKER_LABEL_CACHE}`])),
12824
- images: lines(docker(["image", "ls", "-q", "--filter", `label=${DOCKER_LABEL_OWNER}`, "--filter", `label=${DOCKER_LABEL_REPO_LAYER}`]))
12930
+ // CREW-2984: the repository and ship layers are tagged `crew-sandbox/<name>:<hash>` but carry only an `io.crew.sandbox.*` label,
12931
+ // so the label filter alone missed them. The tag namespace is crew's own; the base and crew layers are `crew-sandbox-base`/`-crew`.
12932
+ images: [.../* @__PURE__ */ new Set([
12933
+ ...lines(docker(["image", "ls", "--format", "{{.Repository}}:{{.Tag}}", "--filter", "reference=crew-sandbox/*"])),
12934
+ ...lines(docker(["image", "ls", "-q", "--filter", `label=${DOCKER_LABEL_OWNER}`, "--filter", `label=${DOCKER_LABEL_REPO_LAYER}`]))
12935
+ ])]
12825
12936
  };
12826
12937
  }
12827
12938
  function realDeprovisionDeps(cfg, emit, crewHome, getStore) {
@@ -12855,8 +12966,8 @@ function realDeprovisionDeps(cfg, emit, crewHome, getStore) {
12855
12966
  const tracker = new Tracker(route2, cfg.ship, { apiKey: identity.apiKey });
12856
12967
  const t = await tracker.ticket(run3.ticketRecordId);
12857
12968
  const target = repoTargetFor(route2, t.repo_id);
12858
- const num = String(t[tracker.contract.columns.key] ?? "").replace(/^\D+/, "");
12859
- const wt = target && num ? worktreeForNumber(target.dir, num) : null;
12969
+ const num2 = String(t[tracker.contract.columns.key] ?? "").replace(/^\D+/, "");
12970
+ const wt = target && num2 ? worktreeForNumber(target.dir, num2) : null;
12860
12971
  let dirty = null;
12861
12972
  try {
12862
12973
  if (wt) dirty = status(wt.path).length > 0;
@@ -18132,10 +18243,10 @@ function dedupeChangelogEntries(entries, ticket) {
18132
18243
  let key = line;
18133
18244
  if (ref) {
18134
18245
  const prefix = ref[1];
18135
- const num = ref[2];
18136
- const canonical = own && own[2] === num ? own[1] : prefix;
18137
- line = `${canonical}-${num}${line.slice(ref[0].length)}`;
18138
- key = `${canonical}-${num}`;
18246
+ const num2 = ref[2];
18247
+ const canonical = own && own[2] === num2 ? own[1] : prefix;
18248
+ line = `${canonical}-${num2}${line.slice(ref[0].length)}`;
18249
+ key = `${canonical}-${num2}`;
18139
18250
  }
18140
18251
  if (seen.has(key)) continue;
18141
18252
  seen.add(key);
@@ -21343,10 +21454,24 @@ async function showLogEntry(route2, entryId, opts = {}) {
21343
21454
  } catch (e) {
21344
21455
  throw new LogbookError(`no Agent Log entry ${entryId} in this workspace (${e.message})`);
21345
21456
  }
21346
- if (!opts.withPrompt) return { entry, reconstruction: { status: "not_recorded", reason: "not requested" } };
21457
+ let inference;
21458
+ if (entry.run_id && entry.client !== "inference") {
21459
+ try {
21460
+ const rows = await listInferenceRows(
21461
+ { baseUrl: route2.baseUrl, apiKey: resolveApiKey(route2), userAgent: opts.userAgent ?? "Mozilla/5.0 TablationCrewAgent/1.0" },
21462
+ agentLogModel.id,
21463
+ entry.run_id
21464
+ );
21465
+ if (rows.length > 0) inference = sumInferenceRows(rows);
21466
+ } catch {
21467
+ }
21468
+ }
21469
+ const withInference = inference ? { inference } : {};
21470
+ if (!opts.withPrompt) return { entry, ...withInference, reconstruction: { status: "not_recorded", reason: "not requested" } };
21347
21471
  if (!entry.agent_id || !entry.prompt_version) {
21348
21472
  return {
21349
21473
  entry,
21474
+ ...withInference,
21350
21475
  reconstruction: {
21351
21476
  status: "not_recorded",
21352
21477
  reason: !entry.agent_id ? "this entry has no linked Agents row (predates crew agents sync being run for its role)" : "this entry predates ISSUE-377/529 \u2014 no prompt_version was recorded"
@@ -21358,12 +21483,13 @@ async function showLogEntry(route2, entryId, opts = {}) {
21358
21483
  try {
21359
21484
  historyEntries = await fetchHistoryEntries(route2, agentsModel.id, entry.agent_id, opts.userAgent, 200);
21360
21485
  } catch (e) {
21361
- return { entry, reconstruction: { status: "approximate", reason: `history lookup failed: ${e.message}` } };
21486
+ return { entry, ...withInference, reconstruction: { status: "approximate", reason: `history lookup failed: ${e.message}` } };
21362
21487
  }
21363
21488
  const match = historyEntries.find((h) => h.history_id === entry.prompt_version);
21364
21489
  if (!match) {
21365
21490
  return {
21366
21491
  entry,
21492
+ ...withInference,
21367
21493
  reconstruction: {
21368
21494
  status: "approximate",
21369
21495
  reason: `history entry ${entry.prompt_version} no longer exists on the Agents row (purged, or the row itself was recreated)`
@@ -21375,6 +21501,7 @@ async function showLogEntry(route2, entryId, opts = {}) {
21375
21501
  const exact = !!entry.prompt_sha && actualSha === entry.prompt_sha;
21376
21502
  return {
21377
21503
  entry,
21504
+ ...withInference,
21378
21505
  reconstruction: {
21379
21506
  status: exact ? "exact" : "approximate",
21380
21507
  historyId: match.history_id,
@@ -21998,8 +22125,8 @@ var BRANCH_STEM = /^(?:.*\/)?([^/]+?-\d+)(?:-.*)?$/;
21998
22125
  function ticketForWorktree(w, byNumber, contract) {
21999
22126
  if (w.branch) {
22000
22127
  const stem = BRANCH_STEM.exec(w.branch)?.[1];
22001
- const num = stem && /-(\d+)$/.exec(stem)?.[1];
22002
- const t = num ? byNumber.get(num) : void 0;
22128
+ const num2 = stem && /-(\d+)$/.exec(stem)?.[1];
22129
+ const t = num2 ? byNumber.get(num2) : void 0;
22003
22130
  if (t && ticketStems(t).has(stem)) return t;
22004
22131
  }
22005
22132
  const dir = basename13(w.path);
@@ -22179,11 +22306,11 @@ function deployedBranchStems(tickets, contract) {
22179
22306
  }
22180
22307
  function ticketStems(t) {
22181
22308
  const stems = /* @__PURE__ */ new Set();
22182
- const num = t.issue_id.replace(/^\D+/, "");
22183
- const unpadded = String(Number.parseInt(num, 10));
22309
+ const num2 = t.issue_id.replace(/^\D+/, "");
22310
+ const unpadded = String(Number.parseInt(num2, 10));
22184
22311
  const prefixes = ["issue", t.project_issue_prefix?.toLowerCase()].filter((p) => !!p);
22185
22312
  for (const p of prefixes) {
22186
- stems.add(`${p}-${num}`);
22313
+ stems.add(`${p}-${num2}`);
22187
22314
  stems.add(`${p}-${unpadded}`);
22188
22315
  }
22189
22316
  return stems;
@@ -24161,8 +24288,8 @@ async function reconcileForRoute(ctx, o) {
24161
24288
  if (!memberId) return;
24162
24289
  const t = await tracker.ticket(sc.ticketRecordId);
24163
24290
  const target = repoTargetFor(route2, t.repo_id);
24164
- const num = String(t[tracker.contract.columns.key] ?? "").replace(/^\D+/, "");
24165
- const wt = target && num ? worktreeForNumber(target.dir, num) : null;
24291
+ const num2 = String(t[tracker.contract.columns.key] ?? "").replace(/^\D+/, "");
24292
+ const wt = target && num2 ? worktreeForNumber(target.dir, num2) : null;
24166
24293
  let dirty = null;
24167
24294
  try {
24168
24295
  if (wt) dirty = status(wt.path).length > 0;
@@ -24196,8 +24323,8 @@ async function postRunKilled(route2, tracker, ticketId, memberId, limitMinutes,
24196
24323
  try {
24197
24324
  const t = await tracker.ticket(ticketId);
24198
24325
  const target = repoTargetFor(route2, t.repo_id);
24199
- const num = String(t[tracker.contract.columns.key] ?? "").replace(/^\D+/, "");
24200
- const wt = target && num ? worktreeForNumber(target.dir, num) : null;
24326
+ const num2 = String(t[tracker.contract.columns.key] ?? "").replace(/^\D+/, "");
24327
+ const wt = target && num2 ? worktreeForNumber(target.dir, num2) : null;
24201
24328
  let dirty = null;
24202
24329
  try {
24203
24330
  if (wt) dirty = status(wt.path).length > 0;
@@ -25545,10 +25672,10 @@ async function handlePeriscopeMcp(d, req, res, segment) {
25545
25672
  function findPeriscope(sidecars, segment) {
25546
25673
  const m = /^(?:([A-Za-z]+)-)?(\d+)$/.exec(segment);
25547
25674
  if (!m) return void 0;
25548
- const num = String(Number(m[2]));
25675
+ const num2 = String(Number(m[2]));
25549
25676
  return sidecars.find((sc) => {
25550
25677
  const t = /^(?:([A-Za-z]+)-)?0*(\d+)$/.exec(sc.ticket);
25551
- if (!t || t[2] !== num) return false;
25678
+ if (!t || t[2] !== num2) return false;
25552
25679
  return !m[1] || !t[1] || m[1].toLowerCase() === t[1].toLowerCase();
25553
25680
  });
25554
25681
  }
@@ -28113,7 +28240,7 @@ ${cu.tail.trimEnd()}
28113
28240
  }
28114
28241
  const { personaResult, skillResult } = run3;
28115
28242
  if (flag("revoke-seat-keys")) {
28116
- for (const o of await revokeSeatKeys(target, { userAgent: cfg.ship.userAgent, dryRun })) {
28243
+ for (const o of await revokeSeatKeys(target, { userAgent: cfg.ship.userAgent, dryRun, shipName: cfg.ship.name })) {
28117
28244
  process.stdout.write(`${describeRevokeOutcome(o)}
28118
28245
  `);
28119
28246
  }
@@ -28636,7 +28763,7 @@ ${cu.tail.trimEnd()}
28636
28763
  );
28637
28764
  }
28638
28765
  } else {
28639
- const { entry, reconstruction } = await showLogEntry(target, entryId, {
28766
+ const { entry, inference, reconstruction } = await showLogEntry(target, entryId, {
28640
28767
  userAgent: cfg.ship.userAgent,
28641
28768
  withPrompt: flag("prompt")
28642
28769
  });
@@ -28650,6 +28777,9 @@ model: ${entry.model ?? "?"}
28650
28777
  harness: ${entry.harness ?? "?"}
28651
28778
  provider: ${entry.provider ?? "?"}
28652
28779
  ` + (entry.num_turns != null ? `turns: ${entry.num_turns}
28780
+ ` : "") + (inference ? `calls: ${inference.calls}
28781
+ tokens: in ${inference.tokensIn}, out ${inference.tokensOut}, cache read ${inference.cacheReadTokens}, cache write ${inference.cacheWriteTokens}
28782
+ cost: $${inference.costUsd.toFixed(4)}
28653
28783
  ` : "") + (entry.prefix_tokens != null ? `prefix: ${entry.prefix_tokens} tokens
28654
28784
  ` : "") + (formatBeforeEdit(entry) ? `before edit: ${formatBeforeEdit(entry)}
28655
28785
  ` : "")
@@ -29692,7 +29822,7 @@ Signed in as ${result.identity.email} \u2014 minted an API key in workspace "${r
29692
29822
  process.stderr.write("drop: need a ticket number (326, ISSUE-326, or TABL-326)\n");
29693
29823
  process.exit(2);
29694
29824
  }
29695
- const num = Number(n);
29825
+ const num2 = Number(n);
29696
29826
  const repos = await resolvedRepos(route2);
29697
29827
  const candidates2 = repos.map((r) => ({ repo: r, found: worktreeForNumber(r.dir, n) })).filter((c) => c.found !== null);
29698
29828
  if (candidates2.length === 0) {
@@ -29713,9 +29843,9 @@ Signed in as ${result.identity.email} \u2014 minted an API key in workspace "${r
29713
29843
  const wt = found.path;
29714
29844
  try {
29715
29845
  const trD = new Tracker(route2, cfg.ship);
29716
- const tD = (await trD.ticketsByNumbers([n])).find((t) => t.issue_id.replace(/^\D+/, "") === String(num));
29846
+ const tD = (await trD.ticketsByNumbers([n])).find((t) => t.issue_id.replace(/^\D+/, "") === String(num2));
29717
29847
  if (tD && tD[trD.contract.columns.status] === trD.contract.statuses.draft) {
29718
- process.stderr.write(`drop: ${tD.issue_tag ?? tD.issue_id} is a draft ticket \u2014 its worktree belongs to a periscope; end it with \`crew periscope down ${num} --keep\` or \`--discard\`
29848
+ process.stderr.write(`drop: ${tD.issue_tag ?? tD.issue_id} is a draft ticket \u2014 its worktree belongs to a periscope; end it with \`crew periscope down ${num2} --keep\` or \`--discard\`
29719
29849
  `);
29720
29850
  process.exit(2);
29721
29851
  }
@@ -29725,7 +29855,7 @@ Signed in as ${result.identity.email} \u2014 minted an API key in workspace "${r
29725
29855
  name: target.config.branch.name,
29726
29856
  push: target.config.branch.push
29727
29857
  }, (t) => renderBranchName(t, { key: `ISSUE-${n}`, lowercase: target.config.branch.lowercase }));
29728
- const pids = pidsInWorktree(wt, num);
29858
+ const pids = pidsInWorktree(wt, num2);
29729
29859
  if (pids.length) {
29730
29860
  emit.emit(`${dryRun ? "would kill" : "killing"} processes still inside ${wt}: ${pids.join(" ")}`);
29731
29861
  if (!dryRun) await killGently(pids);
@@ -30483,7 +30613,7 @@ ${where.join("\n")}`);
30483
30613
  },
30484
30614
  removePair: removePairEntries,
30485
30615
  async revokeSeatKeys(r, dry) {
30486
- const outcomes = await revokeSeatKeys(r, { userAgent: cfg.ship.userAgent, dryRun: dry });
30616
+ const outcomes = await revokeSeatKeys(r, { userAgent: cfg.ship.userAgent, dryRun: dry, shipName: cfg.ship.name });
30487
30617
  return outcomes.map((o) => ({ line: describeRevokeOutcome(o), ok: o.action !== "failed" }));
30488
30618
  },
30489
30619
  async credential(r) {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@tablation/crew",
3
- "version": "0.129.1",
3
+ "version": "0.130.1",
4
4
  "description": "A standing team of headless agents that picks work off a Tablation board, does it on a machine you control, and reports back.",
5
5
  "type": "module",
6
6
  "license": "MIT",