@tablation/crew 0.129.0 → 0.130.0

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 +190 -68
  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`,
@@ -5165,9 +5177,22 @@ function planDaemonControl(crewHome, hostShip, hasSystemd) {
5165
5177
  }
5166
5178
  return { mechanism: "none", startCommand: null, stopCommand: null, statusCommand: null };
5167
5179
  }
5180
+ function withUserBusEnv(env = process.env, opts = {}) {
5181
+ const platform2 = opts.platform ?? process.platform;
5182
+ const uid = opts.uid === void 0 ? process.getuid?.() ?? null : opts.uid;
5183
+ if (platform2 !== "linux" || uid === null) return env;
5184
+ const dir = `/run/user/${uid}`;
5185
+ if (env.XDG_RUNTIME_DIR && env.DBUS_SESSION_BUS_ADDRESS) return env;
5186
+ if (!(opts.exists ?? existsSync15)(dir)) return env;
5187
+ return {
5188
+ ...env,
5189
+ XDG_RUNTIME_DIR: env.XDG_RUNTIME_DIR || dir,
5190
+ DBUS_SESSION_BUS_ADDRESS: env.DBUS_SESSION_BUS_ADDRESS || `unix:path=${env.XDG_RUNTIME_DIR || dir}/bus`
5191
+ };
5192
+ }
5168
5193
  function runAll(commands, cwd, log) {
5169
5194
  for (const [bin, ...args] of commands) {
5170
- const res = spawnSync5(bin, args, { cwd, encoding: "utf8" });
5195
+ const res = spawnSync5(bin, args, { cwd, encoding: "utf8", env: withUserBusEnv() });
5171
5196
  if (res.error || res.status !== 0) {
5172
5197
  log.warn(`${bin} ${args.join(" ")} exited ${res.status ?? "error"}`, {
5173
5198
  output: `${res.stdout ?? ""}${res.stderr ?? ""}${res.error ? String(res.error) : ""}`
@@ -5393,7 +5418,7 @@ function removeCrontabLine(marker) {
5393
5418
  }
5394
5419
  function detectSystemd() {
5395
5420
  if (process.platform !== "linux") return false;
5396
- const res = spawnSync5("systemctl", ["--user", "status"], { encoding: "utf8" });
5421
+ const res = spawnSync5("systemctl", ["--user", "status"], { encoding: "utf8", env: withUserBusEnv() });
5397
5422
  return !res.error;
5398
5423
  }
5399
5424
 
@@ -5483,20 +5508,20 @@ function branchForIssue(cwd, issueKey, templates = { name: "issue-{number}" }, r
5483
5508
  return null;
5484
5509
  }
5485
5510
  function branchCandidates(issueKey, templates, render2) {
5486
- const num = issueKey.replace(/^\D+/, "");
5487
- const unpadded = String(Number.parseInt(num, 10));
5511
+ const num2 = issueKey.replace(/^\D+/, "");
5512
+ const unpadded = String(Number.parseInt(num2, 10));
5488
5513
  const candidates2 = [];
5489
5514
  for (const template of [templates.name, templates.push].filter(Boolean)) {
5490
5515
  const exact = render2(template);
5491
5516
  candidates2.push(exact);
5492
- if (num !== unpadded) candidates2.push(exact.replace(num, unpadded));
5517
+ if (num2 !== unpadded) candidates2.push(exact.replace(num2, unpadded));
5493
5518
  if (template.includes("{slug}")) {
5494
5519
  const prefix = render2(template.slice(0, template.indexOf("{slug}")));
5495
5520
  if (prefix) candidates2.push(`${prefix}*`);
5496
5521
  }
5497
5522
  }
5498
- candidates2.push(`issue-${num}`);
5499
- if (num !== unpadded) candidates2.push(`issue-${unpadded}`);
5523
+ candidates2.push(`issue-${num2}`);
5524
+ if (num2 !== unpadded) candidates2.push(`issue-${unpadded}`);
5500
5525
  return [...new Set(candidates2)];
5501
5526
  }
5502
5527
  function remoteBranchForIssue(cwd, remote, issueKey, templates = { name: "issue-{number}" }, render2 = (t) => t.replace(/\{number\}/g, issueKey.replace(/^\D+/, ""))) {
@@ -5567,8 +5592,8 @@ function deleteRemoteBranch(cwd, remote, branch) {
5567
5592
  if (!remoteBranchExists(cwd, remote, branch)) return true;
5568
5593
  return gitOk(cwd, ["push", remote, "--delete", branch]) !== null;
5569
5594
  }
5570
- function worktreeForNumber(cwd, num) {
5571
- const re = new RegExp(`(?:^|-)${num}$`);
5595
+ function worktreeForNumber(cwd, num2) {
5596
+ const re = new RegExp(`(?:^|-)${num2}$`);
5572
5597
  return worktrees(cwd).find((w) => re.test(basename5(w.path))) ?? null;
5573
5598
  }
5574
5599
  var commitBodies = (cwd, range) => gitOk(cwd, ["log", range, "--format=%B"]) ?? "";
@@ -6462,7 +6487,7 @@ import { readFileSync as readFileSync26, existsSync as existsSync25, statSync as
6462
6487
  import { StringDecoder } from "string_decoder";
6463
6488
  import { finished } from "stream/promises";
6464
6489
  import { join as join29, dirname as dirname14, basename as basename9 } from "path";
6465
- import { createHash as createHash5 } from "crypto";
6490
+ import { createHash as createHash5, randomUUID } from "crypto";
6466
6491
 
6467
6492
  // src/state.ts
6468
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";
@@ -9593,8 +9618,8 @@ async function noteTakeover(tracker, result, seat, ships, myShipName, continuing
9593
9618
  }
9594
9619
  }
9595
9620
  function originBranchSha(dir, t, rev) {
9596
- const num = (t.issue_tag ?? t.issue_id).replace(/^\D+/, "");
9597
- const names = [...new Set([(t.issue_tag ?? "").toLowerCase(), `issue-${num}`].filter(Boolean))];
9621
+ const num2 = (t.issue_tag ?? t.issue_id).replace(/^\D+/, "");
9622
+ const names = [...new Set([(t.issue_tag ?? "").toLowerCase(), `issue-${num2}`].filter(Boolean))];
9598
9623
  for (const n of names) {
9599
9624
  const sha = rev(dir, `origin/${n}`);
9600
9625
  if (sha) return sha.slice(0, 7);
@@ -9656,8 +9681,8 @@ function planRunWorktree(dir, cfg, t, role, opts = {}) {
9656
9681
  unlock();
9657
9682
  }
9658
9683
  }
9659
- const num = t.issue_id.replace(/^\D+/, "");
9660
- const existing = worktreeForNumber(dir, num);
9684
+ const num2 = t.issue_id.replace(/^\D+/, "");
9685
+ const existing = worktreeForNumber(dir, num2);
9661
9686
  const located = locateBranchForTicket(dir, cfg, t, role);
9662
9687
  const baseRef = hasRemote ? `${remote}/${base}` : base;
9663
9688
  if (existing && !existing.prunable && existsSync21(existing.path)) {
@@ -10154,6 +10179,49 @@ async function guardRunStrays(route2, ship, tracker, role, workingId, since2, lo
10154
10179
  }
10155
10180
 
10156
10181
  // src/agent-log.ts
10182
+ var num = (v) => typeof v === "number" && Number.isFinite(v) ? v : Number(v) || 0;
10183
+ function sumInferenceRows(rows) {
10184
+ const t = { calls: 0, tokensIn: 0, tokensOut: 0, cacheReadTokens: 0, cacheWriteTokens: 0, costUsd: 0 };
10185
+ for (const r of rows) {
10186
+ t.calls++;
10187
+ t.tokensIn += num(r.input_tokens);
10188
+ t.tokensOut += num(r.output_tokens);
10189
+ t.cacheReadTokens += num(r.cache_read_tokens);
10190
+ t.cacheWriteTokens += num(r.cache_write_tokens);
10191
+ t.costUsd += num(r.cost_usd);
10192
+ }
10193
+ return t;
10194
+ }
10195
+ var INFERENCE_PAGE = 1e3;
10196
+ async function listInferenceRows(target, modelId, runId) {
10197
+ const filters = encodeURIComponent(JSON.stringify({
10198
+ combinator: "AND",
10199
+ conditions: [
10200
+ { columnName: "run_id", operator: "EQ", value: runId },
10201
+ { columnName: "client", operator: "EQ", value: "inference" }
10202
+ ]
10203
+ }));
10204
+ const out = [];
10205
+ for (let offset = 0; ; offset += INFERENCE_PAGE) {
10206
+ const page = await request(
10207
+ target,
10208
+ "GET",
10209
+ `/data-models/${modelId}/records?limit=${INFERENCE_PAGE}&offset=${offset}&filters=${filters}`
10210
+ );
10211
+ out.push(...page);
10212
+ if (page.length < INFERENCE_PAGE) return out;
10213
+ }
10214
+ }
10215
+ async function inferenceTotalsFor(target, runId) {
10216
+ const model = await request(
10217
+ target,
10218
+ "GET",
10219
+ `/data-models/agent_log?workspaceId=${encodeURIComponent(target.workspaceId)}`
10220
+ );
10221
+ if (!(await columnsOf(target, model.id)).has("run_id")) return void 0;
10222
+ const rows = await listInferenceRows(target, model.id, runId);
10223
+ return rows.length > 0 ? sumInferenceRows(rows) : void 0;
10224
+ }
10157
10225
  var AgentLogUnreachableError = class extends Error {
10158
10226
  };
10159
10227
  var AgentLogHttpError = class extends Error {
@@ -10318,19 +10386,20 @@ async function reportAgentRun(target, run3) {
10318
10386
  promptVersion: run3.promptVersion,
10319
10387
  promptSha: run3.promptSha
10320
10388
  };
10389
+ const { runId, ...legacyBody } = { ...body, runId: run3.runId };
10321
10390
  const workItem = target.tickets && ticketRecordId ? { workItemTable: target.tickets.issuesModelId, workItemRecordId: ticketRecordId } : {};
10322
10391
  let entry;
10323
10392
  try {
10324
- entry = await post(target, "/agents/log", { ...body, ...workItem, harness: run3.harness, provider: run3.provider });
10393
+ entry = await post(target, "/agents/log", { ...body, ...runId ? { runId } : {}, ...workItem, harness: run3.harness, provider: run3.provider });
10325
10394
  } catch (e) {
10326
10395
  if (!(e instanceof AgentLogHttpError)) throw new AgentLogUnreachableError(e.message);
10327
10396
  if (e.status >= 500) throw new AgentLogUnreachableError(e.message);
10328
- if (e.status !== 400 || !run3.harness && !run3.provider && !ticketRecordId) {
10397
+ if (e.status !== 400 || !run3.harness && !run3.provider && !ticketRecordId && !runId) {
10329
10398
  e.row = true;
10330
10399
  throw e;
10331
10400
  }
10332
10401
  try {
10333
- entry = await post(target, "/agents/log", body);
10402
+ entry = await post(target, "/agents/log", legacyBody);
10334
10403
  } catch (e2) {
10335
10404
  if (e2 instanceof AgentLogHttpError && e2.status < 500) e2.row = true;
10336
10405
  throw e2;
@@ -10349,10 +10418,21 @@ async function reportAgentRun(target, run3) {
10349
10418
  const bounces = await countAcceptanceBounces(target, target.tickets.issuesModelId, ticketRecordId);
10350
10419
  if (bounces !== null) await writeHandoffBounces(target, entry.id, bounces);
10351
10420
  }
10352
- const counters = target.tickets && ticketRecordId ? await addRunToTicket(target, target.tickets, ticketRecordId, run3) : void 0;
10421
+ let counterWarning;
10422
+ let counted = run3;
10423
+ if (runId && target.tickets && ticketRecordId) {
10424
+ try {
10425
+ const totals = await inferenceTotalsFor(target, runId);
10426
+ if (totals) counted = { ...run3, tokensIn: totals.tokensIn, tokensOut: totals.tokensOut, cacheReadTokens: totals.cacheReadTokens, cacheWriteTokens: totals.cacheWriteTokens, costUsd: totals.costUsd };
10427
+ else counted = { ...run3, ...run3.harnessTotals };
10428
+ } catch (e) {
10429
+ counterWarning = `could not sum run ${runId}'s inference rows for ${run3.ticketReference}: ${e.message}`;
10430
+ }
10431
+ }
10432
+ const counters = target.tickets && ticketRecordId ? await addRunToTicket(target, target.tickets, ticketRecordId, counted) : void 0;
10353
10433
  return {
10354
10434
  agentLogId: entry.id,
10355
- ...counters?.warning ? { counterWarning: counters.warning } : {},
10435
+ ...counters?.warning || counterWarning ? { counterWarning: [counterWarning, counters?.warning].filter(Boolean).join("; ") } : {},
10356
10436
  ...counters?.retry ? { counterRetry: counters.retry } : {}
10357
10437
  };
10358
10438
  }
@@ -11928,6 +12008,7 @@ function planAgentRun(o) {
11928
12008
  const seat = resolveSeat(o.ship, o.route, o.role, o.agentPersona ?? (o.agentModel ? { model: o.agentModel } : {}), o.repo);
11929
12009
  const { model } = seat;
11930
12010
  let toolchain = seat.toolchain;
12011
+ let workspaceRunId;
11931
12012
  if (toolchain.auth === "workspace") {
11932
12013
  if (!o.apiKey || o.seatKind === "ship") {
11933
12014
  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\`)`);
@@ -11935,7 +12016,8 @@ function planAgentRun(o) {
11935
12016
  if (!o.route.resolved) {
11936
12017
  throw new WorkspaceAuthRefused(`${o.role}: toolchain ${toolchain.name} has auth: workspace, but route ${o.route.route} is not connected (run \`crew connect\`)`);
11937
12018
  }
11938
- toolchain = { ...toolchain, inference: { baseUrl: inferenceBaseUrl(o.route.baseUrl, o.route.resolved.workspaceId), token: o.apiKey } };
12019
+ workspaceRunId = o.runId ?? randomUUID();
12020
+ toolchain = { ...toolchain, inference: { baseUrl: inferenceBaseUrl(o.route.baseUrl, o.route.resolved.workspaceId), token: o.apiKey, ticket: o.ticket, runId: workspaceRunId } };
11939
12021
  }
11940
12022
  const adapter = adapterFor(toolchain.harness);
11941
12023
  const code = codeBridgeFor(o, adapter.harness);
@@ -12032,7 +12114,8 @@ function planAgentRun(o) {
12032
12114
  routeName: o.route.route,
12033
12115
  agentLog,
12034
12116
  promptVersion: o.route.resolved?.agentPersonas?.[o.role]?.historyId,
12035
- promptSha
12117
+ promptSha,
12118
+ ...workspaceRunId ? { workspaceRunId } : {}
12036
12119
  };
12037
12120
  }
12038
12121
  function describePlan(p) {
@@ -12086,7 +12169,8 @@ function identityOf(plan, adapterHarness) {
12086
12169
  streamPath: plan.streamPath,
12087
12170
  agentLog: plan.agentLog ? withoutKey(plan.agentLog) : void 0,
12088
12171
  promptVersion: plan.promptVersion,
12089
- promptSha: plan.promptSha
12172
+ promptSha: plan.promptSha,
12173
+ ...plan.workspaceRunId ? { workspaceRunId: plan.workspaceRunId } : {}
12090
12174
  };
12091
12175
  }
12092
12176
  async function killForDeadline(kill, pid, emit, role, ticket, spawnedAt, limitMinutes) {
@@ -12393,11 +12477,22 @@ async function finishRun(id, obs, emit, startedAt, end, agentLogKey, hintMoved)
12393
12477
  finishedAt: (/* @__PURE__ */ new Date()).toISOString(),
12394
12478
  cycles: obs.cycles,
12395
12479
  model: id.model,
12396
- tokensIn: result?.inputTokens,
12397
- tokensOut: result?.outputTokens,
12398
- cacheReadTokens: result?.cacheReadTokens,
12399
- cacheWriteTokens: result?.cacheWriteTokens,
12400
- costUsd: result?.costUsd,
12480
+ // CREW-2980: under `auth: workspace` the endpoint's rows are the accounting; the run row
12481
+ // keeps turns only, and `reportAgentRun` sums those rows into the ticket counters.
12482
+ ...id.workspaceRunId ? { runId: id.workspaceRunId } : {
12483
+ tokensIn: result?.inputTokens,
12484
+ tokensOut: result?.outputTokens,
12485
+ cacheReadTokens: result?.cacheReadTokens,
12486
+ cacheWriteTokens: result?.cacheWriteTokens,
12487
+ costUsd: result?.costUsd
12488
+ },
12489
+ ...id.workspaceRunId ? { harnessTotals: {
12490
+ tokensIn: result?.inputTokens,
12491
+ tokensOut: result?.outputTokens,
12492
+ cacheReadTokens: result?.cacheReadTokens,
12493
+ cacheWriteTokens: result?.cacheWriteTokens,
12494
+ costUsd: result?.costUsd
12495
+ } } : {},
12401
12496
  // Both come from a finished run: without the result line (interrupted) neither is trustworthy.
12402
12497
  ...result?.numTurns !== void 0 ? { numTurns: result.numTurns } : {},
12403
12498
  ...result?.numTurns !== void 0 && obs.prefixTokens !== void 0 ? { prefixTokens: obs.prefixTokens } : {},
@@ -12524,7 +12619,8 @@ function spawnAgent(plan, emit, hintMoved) {
12524
12619
  eventsPath: plan.eventsPath,
12525
12620
  agentLog: id.agentLog,
12526
12621
  promptVersion: plan.promptVersion,
12527
- promptSha: plan.promptSha
12622
+ promptSha: plan.promptSha,
12623
+ ...plan.workspaceRunId ? { workspaceRunId: plan.workspaceRunId } : {}
12528
12624
  };
12529
12625
  try {
12530
12626
  writeFileSync19(pidPathFor(plan.streamPath), JSON.stringify(sidecar));
@@ -12779,14 +12875,14 @@ function describeRun(run3, route2, seatIds, worktreeOf = (d, n) => {
12779
12875
  }) {
12780
12876
  const ownRoute = run3.route === routeSlug(route2.route);
12781
12877
  const repoDir = run3.repo ? reposOf(route2).find((t) => t.name === run3.repo)?.dir : void 0;
12782
- const num = run3.ticket?.replace(/^\D+/, "") ?? "";
12878
+ const num2 = run3.ticket?.replace(/^\D+/, "") ?? "";
12783
12879
  return {
12784
12880
  role: run3.role,
12785
12881
  seat: ownRoute ? seatIds[run3.role] ?? null : null,
12786
12882
  ticket: run3.ticket,
12787
12883
  ticketRecordId: run3.ticketRecordId,
12788
12884
  repo: run3.repo ?? null,
12789
- worktree: repoDir && num ? worktreeOf(repoDir, num) : null,
12885
+ worktree: repoDir && num2 ? worktreeOf(repoDir, num2) : null,
12790
12886
  stream: run3.streamPath,
12791
12887
  pid: run3.pid,
12792
12888
  startedAt: run3.spawnedAt,
@@ -12842,8 +12938,8 @@ function realDeprovisionDeps(cfg, emit, crewHome, getStore) {
12842
12938
  const tracker = new Tracker(route2, cfg.ship, { apiKey: identity.apiKey });
12843
12939
  const t = await tracker.ticket(run3.ticketRecordId);
12844
12940
  const target = repoTargetFor(route2, t.repo_id);
12845
- const num = String(t[tracker.contract.columns.key] ?? "").replace(/^\D+/, "");
12846
- const wt = target && num ? worktreeForNumber(target.dir, num) : null;
12941
+ const num2 = String(t[tracker.contract.columns.key] ?? "").replace(/^\D+/, "");
12942
+ const wt = target && num2 ? worktreeForNumber(target.dir, num2) : null;
12847
12943
  let dirty = null;
12848
12944
  try {
12849
12945
  if (wt) dirty = status(wt.path).length > 0;
@@ -13893,6 +13989,11 @@ function managedRuntimeWarnings(kind, ship) {
13893
13989
  }
13894
13990
  return out;
13895
13991
  }
13992
+ function runtimeDirWarning(platform2, uid, exists, user = "the crew user") {
13993
+ if (platform2 !== "linux" || uid === null || uid === 0) return null;
13994
+ if (exists(`/run/user/${uid}`)) return null;
13995
+ return `/run/user/${uid} is missing, so \`systemctl --user\` cannot reach the user manager: lingering is off for ${user}; run \`sudo loginctl enable-linger ${user}\``;
13996
+ }
13896
13997
  var MANAGED_GIT_EMAIL = "crew@tablation.dev";
13897
13998
  function readGlobalGitIdentity() {
13898
13999
  const get2 = (key) => {
@@ -14139,7 +14240,7 @@ function doctorSelftestLines(stored, toolchains) {
14139
14240
  // src/cli.ts
14140
14241
  import { resolve as resolve7, dirname as dirname35, join as join56, basename as basename16 } from "path";
14141
14242
  import { fileURLToPath as fileURLToPath2 } from "url";
14142
- import { hostname as hostname5, homedir as homedir11 } from "os";
14243
+ import { hostname as hostname5, homedir as homedir11, userInfo as userInfo2 } from "os";
14143
14244
  import { spawnSync as spawnSync12 } from "child_process";
14144
14245
  import { parse as parseYaml2 } from "yaml";
14145
14246
 
@@ -15656,7 +15757,7 @@ function realProvisionDeps(crewHome) {
15656
15757
  if (mechanism === "systemd") {
15657
15758
  const system = ctx.root || existsSync33(systemUnitPathFor(label));
15658
15759
  const ctl = system ? ["systemctl"] : ["systemctl", "--user"];
15659
- running = spawnSync9(ctl[0], [...ctl.slice(1), "is-active", "--quiet", `${label}.service`]).status === 0;
15760
+ running = spawnSync9(ctl[0], [...ctl.slice(1), "is-active", "--quiet", `${label}.service`], { env: withUserBusEnv() }).status === 0;
15660
15761
  if (!system) {
15661
15762
  const out = spawnSync9("loginctl", ["show-user", ctx.user, "-p", "Linger"], { encoding: "utf8" });
15662
15763
  lingering = /Linger=yes/.test(out.stdout ?? "");
@@ -18114,10 +18215,10 @@ function dedupeChangelogEntries(entries, ticket) {
18114
18215
  let key = line;
18115
18216
  if (ref) {
18116
18217
  const prefix = ref[1];
18117
- const num = ref[2];
18118
- const canonical = own && own[2] === num ? own[1] : prefix;
18119
- line = `${canonical}-${num}${line.slice(ref[0].length)}`;
18120
- key = `${canonical}-${num}`;
18218
+ const num2 = ref[2];
18219
+ const canonical = own && own[2] === num2 ? own[1] : prefix;
18220
+ line = `${canonical}-${num2}${line.slice(ref[0].length)}`;
18221
+ key = `${canonical}-${num2}`;
18121
18222
  }
18122
18223
  if (seen.has(key)) continue;
18123
18224
  seen.add(key);
@@ -21325,10 +21426,24 @@ async function showLogEntry(route2, entryId, opts = {}) {
21325
21426
  } catch (e) {
21326
21427
  throw new LogbookError(`no Agent Log entry ${entryId} in this workspace (${e.message})`);
21327
21428
  }
21328
- if (!opts.withPrompt) return { entry, reconstruction: { status: "not_recorded", reason: "not requested" } };
21429
+ let inference;
21430
+ if (entry.run_id && entry.client !== "inference") {
21431
+ try {
21432
+ const rows = await listInferenceRows(
21433
+ { baseUrl: route2.baseUrl, apiKey: resolveApiKey(route2), userAgent: opts.userAgent ?? "Mozilla/5.0 TablationCrewAgent/1.0" },
21434
+ agentLogModel.id,
21435
+ entry.run_id
21436
+ );
21437
+ if (rows.length > 0) inference = sumInferenceRows(rows);
21438
+ } catch {
21439
+ }
21440
+ }
21441
+ const withInference = inference ? { inference } : {};
21442
+ if (!opts.withPrompt) return { entry, ...withInference, reconstruction: { status: "not_recorded", reason: "not requested" } };
21329
21443
  if (!entry.agent_id || !entry.prompt_version) {
21330
21444
  return {
21331
21445
  entry,
21446
+ ...withInference,
21332
21447
  reconstruction: {
21333
21448
  status: "not_recorded",
21334
21449
  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"
@@ -21340,12 +21455,13 @@ async function showLogEntry(route2, entryId, opts = {}) {
21340
21455
  try {
21341
21456
  historyEntries = await fetchHistoryEntries(route2, agentsModel.id, entry.agent_id, opts.userAgent, 200);
21342
21457
  } catch (e) {
21343
- return { entry, reconstruction: { status: "approximate", reason: `history lookup failed: ${e.message}` } };
21458
+ return { entry, ...withInference, reconstruction: { status: "approximate", reason: `history lookup failed: ${e.message}` } };
21344
21459
  }
21345
21460
  const match = historyEntries.find((h) => h.history_id === entry.prompt_version);
21346
21461
  if (!match) {
21347
21462
  return {
21348
21463
  entry,
21464
+ ...withInference,
21349
21465
  reconstruction: {
21350
21466
  status: "approximate",
21351
21467
  reason: `history entry ${entry.prompt_version} no longer exists on the Agents row (purged, or the row itself was recreated)`
@@ -21357,6 +21473,7 @@ async function showLogEntry(route2, entryId, opts = {}) {
21357
21473
  const exact = !!entry.prompt_sha && actualSha === entry.prompt_sha;
21358
21474
  return {
21359
21475
  entry,
21476
+ ...withInference,
21360
21477
  reconstruction: {
21361
21478
  status: exact ? "exact" : "approximate",
21362
21479
  historyId: match.history_id,
@@ -21980,8 +22097,8 @@ var BRANCH_STEM = /^(?:.*\/)?([^/]+?-\d+)(?:-.*)?$/;
21980
22097
  function ticketForWorktree(w, byNumber, contract) {
21981
22098
  if (w.branch) {
21982
22099
  const stem = BRANCH_STEM.exec(w.branch)?.[1];
21983
- const num = stem && /-(\d+)$/.exec(stem)?.[1];
21984
- const t = num ? byNumber.get(num) : void 0;
22100
+ const num2 = stem && /-(\d+)$/.exec(stem)?.[1];
22101
+ const t = num2 ? byNumber.get(num2) : void 0;
21985
22102
  if (t && ticketStems(t).has(stem)) return t;
21986
22103
  }
21987
22104
  const dir = basename13(w.path);
@@ -22161,11 +22278,11 @@ function deployedBranchStems(tickets, contract) {
22161
22278
  }
22162
22279
  function ticketStems(t) {
22163
22280
  const stems = /* @__PURE__ */ new Set();
22164
- const num = t.issue_id.replace(/^\D+/, "");
22165
- const unpadded = String(Number.parseInt(num, 10));
22281
+ const num2 = t.issue_id.replace(/^\D+/, "");
22282
+ const unpadded = String(Number.parseInt(num2, 10));
22166
22283
  const prefixes = ["issue", t.project_issue_prefix?.toLowerCase()].filter((p) => !!p);
22167
22284
  for (const p of prefixes) {
22168
- stems.add(`${p}-${num}`);
22285
+ stems.add(`${p}-${num2}`);
22169
22286
  stems.add(`${p}-${unpadded}`);
22170
22287
  }
22171
22288
  return stems;
@@ -24143,8 +24260,8 @@ async function reconcileForRoute(ctx, o) {
24143
24260
  if (!memberId) return;
24144
24261
  const t = await tracker.ticket(sc.ticketRecordId);
24145
24262
  const target = repoTargetFor(route2, t.repo_id);
24146
- const num = String(t[tracker.contract.columns.key] ?? "").replace(/^\D+/, "");
24147
- const wt = target && num ? worktreeForNumber(target.dir, num) : null;
24263
+ const num2 = String(t[tracker.contract.columns.key] ?? "").replace(/^\D+/, "");
24264
+ const wt = target && num2 ? worktreeForNumber(target.dir, num2) : null;
24148
24265
  let dirty = null;
24149
24266
  try {
24150
24267
  if (wt) dirty = status(wt.path).length > 0;
@@ -24178,8 +24295,8 @@ async function postRunKilled(route2, tracker, ticketId, memberId, limitMinutes,
24178
24295
  try {
24179
24296
  const t = await tracker.ticket(ticketId);
24180
24297
  const target = repoTargetFor(route2, t.repo_id);
24181
- const num = String(t[tracker.contract.columns.key] ?? "").replace(/^\D+/, "");
24182
- const wt = target && num ? worktreeForNumber(target.dir, num) : null;
24298
+ const num2 = String(t[tracker.contract.columns.key] ?? "").replace(/^\D+/, "");
24299
+ const wt = target && num2 ? worktreeForNumber(target.dir, num2) : null;
24183
24300
  let dirty = null;
24184
24301
  try {
24185
24302
  if (wt) dirty = status(wt.path).length > 0;
@@ -25527,10 +25644,10 @@ async function handlePeriscopeMcp(d, req, res, segment) {
25527
25644
  function findPeriscope(sidecars, segment) {
25528
25645
  const m = /^(?:([A-Za-z]+)-)?(\d+)$/.exec(segment);
25529
25646
  if (!m) return void 0;
25530
- const num = String(Number(m[2]));
25647
+ const num2 = String(Number(m[2]));
25531
25648
  return sidecars.find((sc) => {
25532
25649
  const t = /^(?:([A-Za-z]+)-)?0*(\d+)$/.exec(sc.ticket);
25533
- if (!t || t[2] !== num) return false;
25650
+ if (!t || t[2] !== num2) return false;
25534
25651
  return !m[1] || !t[1] || m[1].toLowerCase() === t[1].toLowerCase();
25535
25652
  });
25536
25653
  }
@@ -26514,7 +26631,7 @@ if (command === "passenger-sync-daemon") {
26514
26631
  `
26515
26632
  );
26516
26633
  if (!dryRun) {
26517
- spawnSync12("systemctl", ["--user", "disable", "--now", `${label}.timer`], { cwd: CREW_HOME });
26634
+ spawnSync12("systemctl", ["--user", "disable", "--now", `${label}.timer`], { cwd: CREW_HOME, env: withUserBusEnv() });
26518
26635
  try {
26519
26636
  unlinkSync5(staleTimer);
26520
26637
  } catch {
@@ -27810,7 +27927,7 @@ ${cu.tail.trimEnd()}
27810
27927
  }
27811
27928
  let failed = false;
27812
27929
  for (const cmd2 of commands) {
27813
- const res2 = spawnSync12(cmd2[0], cmd2.slice(1), { cwd: CREW_HOME, encoding: "utf8" });
27930
+ const res2 = spawnSync12(cmd2[0], cmd2.slice(1), { cwd: CREW_HOME, encoding: "utf8", env: withUserBusEnv() });
27814
27931
  if (res2.stdout) process.stdout.write(res2.stdout);
27815
27932
  if (res2.stderr) process.stderr.write(res2.stderr);
27816
27933
  if (cmd2 === control.startCommand && (res2.error || res2.status !== 0)) {
@@ -27828,7 +27945,7 @@ ${cu.tail.trimEnd()}
27828
27945
  `);
27829
27946
  break;
27830
27947
  }
27831
- const res = spawnSync12(cmd[0], cmd.slice(1), { cwd: CREW_HOME, encoding: "utf8" });
27948
+ const res = spawnSync12(cmd[0], cmd.slice(1), { cwd: CREW_HOME, encoding: "utf8", env: withUserBusEnv() });
27832
27949
  if (res.stdout) process.stdout.write(res.stdout);
27833
27950
  if (res.stderr) process.stderr.write(res.stderr);
27834
27951
  if (daemonSub !== "status" && (res.error || res.status !== 0)) {
@@ -28618,7 +28735,7 @@ ${cu.tail.trimEnd()}
28618
28735
  );
28619
28736
  }
28620
28737
  } else {
28621
- const { entry, reconstruction } = await showLogEntry(target, entryId, {
28738
+ const { entry, inference, reconstruction } = await showLogEntry(target, entryId, {
28622
28739
  userAgent: cfg.ship.userAgent,
28623
28740
  withPrompt: flag("prompt")
28624
28741
  });
@@ -28632,6 +28749,9 @@ model: ${entry.model ?? "?"}
28632
28749
  harness: ${entry.harness ?? "?"}
28633
28750
  provider: ${entry.provider ?? "?"}
28634
28751
  ` + (entry.num_turns != null ? `turns: ${entry.num_turns}
28752
+ ` : "") + (inference ? `calls: ${inference.calls}
28753
+ tokens: in ${inference.tokensIn}, out ${inference.tokensOut}, cache read ${inference.cacheReadTokens}, cache write ${inference.cacheWriteTokens}
28754
+ cost: $${inference.costUsd.toFixed(4)}
28635
28755
  ` : "") + (entry.prefix_tokens != null ? `prefix: ${entry.prefix_tokens} tokens
28636
28756
  ` : "") + (formatBeforeEdit(entry) ? `before edit: ${formatBeforeEdit(entry)}
28637
28757
  ` : "")
@@ -29674,7 +29794,7 @@ Signed in as ${result.identity.email} \u2014 minted an API key in workspace "${r
29674
29794
  process.stderr.write("drop: need a ticket number (326, ISSUE-326, or TABL-326)\n");
29675
29795
  process.exit(2);
29676
29796
  }
29677
- const num = Number(n);
29797
+ const num2 = Number(n);
29678
29798
  const repos = await resolvedRepos(route2);
29679
29799
  const candidates2 = repos.map((r) => ({ repo: r, found: worktreeForNumber(r.dir, n) })).filter((c) => c.found !== null);
29680
29800
  if (candidates2.length === 0) {
@@ -29695,9 +29815,9 @@ Signed in as ${result.identity.email} \u2014 minted an API key in workspace "${r
29695
29815
  const wt = found.path;
29696
29816
  try {
29697
29817
  const trD = new Tracker(route2, cfg.ship);
29698
- const tD = (await trD.ticketsByNumbers([n])).find((t) => t.issue_id.replace(/^\D+/, "") === String(num));
29818
+ const tD = (await trD.ticketsByNumbers([n])).find((t) => t.issue_id.replace(/^\D+/, "") === String(num2));
29699
29819
  if (tD && tD[trD.contract.columns.status] === trD.contract.statuses.draft) {
29700
- 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\`
29820
+ 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\`
29701
29821
  `);
29702
29822
  process.exit(2);
29703
29823
  }
@@ -29707,7 +29827,7 @@ Signed in as ${result.identity.email} \u2014 minted an API key in workspace "${r
29707
29827
  name: target.config.branch.name,
29708
29828
  push: target.config.branch.push
29709
29829
  }, (t) => renderBranchName(t, { key: `ISSUE-${n}`, lowercase: target.config.branch.lowercase }));
29710
- const pids = pidsInWorktree(wt, num);
29830
+ const pids = pidsInWorktree(wt, num2);
29711
29831
  if (pids.length) {
29712
29832
  emit.emit(`${dryRun ? "would kill" : "killing"} processes still inside ${wt}: ${pids.join(" ")}`);
29713
29833
  if (!dryRun) await killGently(pids);
@@ -30541,7 +30661,7 @@ ${where.join("\n")}`);
30541
30661
  }
30542
30662
  const host = hostPlatform();
30543
30663
  const control = host === "windows" ? null : planDaemonControl(CREW_HOME, host, detectSystemd());
30544
- const unitStatus = control?.statusCommand ? spawnSync12(control.statusCommand[0], control.statusCommand.slice(1), { cwd: CREW_HOME, encoding: "utf8" }) : null;
30664
+ const unitStatus = control?.statusCommand ? spawnSync12(control.statusCommand[0], control.statusCommand.slice(1), { cwd: CREW_HOME, encoding: "utf8", env: withUserBusEnv() }) : null;
30545
30665
  const unitRunning = unitStatus !== null && !unitStatus.error && unitStatus.status === 0;
30546
30666
  if (!unitRunning && control?.mechanism === "systemd" && control.startCommand) {
30547
30667
  const res = spawnSync12(control.startCommand[0], control.startCommand.slice(1), { cwd: CREW_HOME, encoding: "utf8" });
@@ -30566,7 +30686,7 @@ ${where.join("\n")}`);
30566
30686
  );
30567
30687
  if (flag("now") && control?.stopCommand && control.startCommand) {
30568
30688
  for (const cmd of [control.stopCommand, control.startCommand]) {
30569
- const res = spawnSync12(cmd[0], cmd.slice(1), { cwd: CREW_HOME, encoding: "utf8" });
30689
+ const res = spawnSync12(cmd[0], cmd.slice(1), { cwd: CREW_HOME, encoding: "utf8", env: withUserBusEnv() });
30570
30690
  if (cmd === control.startCommand && (res.error || res.status !== 0)) {
30571
30691
  process.stderr.write(`crew reload: ${cmd.join(" ")} exited ${res.status ?? "error"}
30572
30692
  `);
@@ -31144,6 +31264,8 @@ watermark: ${state.watermark()}
31144
31264
  }
31145
31265
  }
31146
31266
  for (const message of roleCapWarnings(cfg.ship)) problems.push({ severity: "note", kind: "role_cap_above_ship_cap", message });
31267
+ const runtimeDir = runtimeDirWarning(process.platform, process.getuid?.() ?? null, existsSync51, userInfo2().username);
31268
+ if (runtimeDir) problems.push({ severity: "attention", kind: "runtime_dir_missing", message: runtimeDir });
31147
31269
  if (!flag("offline")) {
31148
31270
  const shipKind = await doctorShipKind(route2, cfg.ship);
31149
31271
  for (const message of managedRuntimeWarnings(shipKind, cfg.ship)) problems.push({ severity: "attention", kind: "managed_runtime_default", message });
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@tablation/crew",
3
- "version": "0.129.0",
3
+ "version": "0.130.0",
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",