@tablation/crew 0.28.0 → 0.30.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 +156 -40
  2. package/package.json +1 -1
package/dist/cli.js CHANGED
@@ -1,14 +1,14 @@
1
1
  #!/usr/bin/env node
2
2
 
3
3
  // src/cli.ts
4
- import { resolve as resolve4, dirname as dirname11, join as join20, basename as basename8 } from "path";
4
+ import { resolve as resolve4, dirname as dirname11, join as join20, basename as basename9 } from "path";
5
5
  import { fileURLToPath } from "url";
6
6
  import { hostname as hostname2, homedir as homedir5 } from "os";
7
7
  import { spawnSync as spawnSync3 } from "child_process";
8
8
 
9
9
  // src/config.ts
10
10
  import { readFileSync as readFileSync4, existsSync as existsSync3 } from "fs";
11
- import { resolve, dirname as dirname2, isAbsolute, join as join4, basename as basename2 } from "path";
11
+ import { resolve, dirname as dirname2, isAbsolute, join as join4, basename as basename3 } from "path";
12
12
  import { homedir as homedir2, tmpdir } from "os";
13
13
  import { parse as parse2 } from "yaml";
14
14
 
@@ -157,6 +157,9 @@ var Emitter = class _Emitter {
157
157
  };
158
158
  var eventFileFor = (stateDir) => join(stateDir, "events.jsonl");
159
159
 
160
+ // src/toolchain.ts
161
+ import { basename } from "path";
162
+
160
163
  // src/stream.ts
161
164
  function toolTarget(name, input) {
162
165
  if (!input) return void 0;
@@ -197,11 +200,11 @@ function extractResult(line) {
197
200
  return {
198
201
  sessionId: obj.session_id,
199
202
  numTurns: obj.num_turns,
200
- totalCostUsd: obj.total_cost_usd,
203
+ costUsd: obj.total_cost_usd,
201
204
  inputTokens: obj.usage?.input_tokens,
202
205
  outputTokens: obj.usage?.output_tokens,
203
206
  cacheReadTokens: obj.usage?.cache_read_input_tokens,
204
- cacheCreationTokens: obj.usage?.cache_creation_input_tokens
207
+ cacheWriteTokens: obj.usage?.cache_creation_input_tokens
205
208
  };
206
209
  }
207
210
 
@@ -312,12 +315,22 @@ function allowedTools(role) {
312
315
  var claudeAdapter = {
313
316
  harness: "claude",
314
317
  displayName: "Claude Code",
318
+ vendor: "anthropic",
319
+ ownsModel: (model) => model.startsWith("claude-"),
320
+ // The tier's reasoning is the extended-thinking budget; it supersedes the
321
+ // toolchain's `maxThinkingTokens` (0 disables). A non-numeric value is ignored.
322
+ applyReasoning(spec, reasoning) {
323
+ const n = Number(reasoning);
324
+ return Number.isInteger(n) && n >= 0 ? { ...spec, maxThinkingTokens: n } : spec;
325
+ },
315
326
  defaultBin: "claude",
316
327
  defaultModel: "claude-sonnet-5",
317
328
  defaultTiers: {
318
- light: { model: "claude-haiku-4-5-20251001" },
329
+ // `standard` carries no reasoning: it defers to the toolchain's own
330
+ // `maxThinkingTokens`, which is what a persona with no tier gets today.
331
+ light: { model: "claude-haiku-4-5-20251001", reasoning: "2048" },
319
332
  standard: { model: "claude-sonnet-5-5" },
320
- deep: { model: "claude-opus-5-5" }
333
+ deep: { model: "claude-opus-5-5", reasoning: "16384" }
321
334
  },
322
335
  limitations: [
323
336
  "Bash is not sandboxed: the deny list refuses known destructive patterns, but a one-liner reaching the same end another way is not caught",
@@ -363,6 +376,7 @@ var claudeAdapter = {
363
376
  ...spec.env
364
377
  };
365
378
  },
379
+ provider: () => "anthropic",
366
380
  unsetEnv: BILLING_VARS_TO_UNSET,
367
381
  mapStreamLine,
368
382
  extractResult,
@@ -411,6 +425,52 @@ function resolveToolchain(ship, route) {
411
425
  if (!found) throw new Error(`toolchain "${name}" is not defined (have: ${shipToolchains(ship).map((t) => t.name).join(", ")})`);
412
426
  return found;
413
427
  }
428
+ function toolchainVendor(spec) {
429
+ if (spec.launcher && basename(spec.launcher) === "ori") return "openrouter";
430
+ return adapterFor(spec.harness).vendor;
431
+ }
432
+ function resolveSeat(ship, route, role, persona = {}) {
433
+ const warnings = [];
434
+ const base = resolveToolchain(ship, route);
435
+ const baseVendor = toolchainVendor(base);
436
+ const baseWhy = route?.toolchain ? `route toolchain ${base.name}` : ship.toolchain ? `ship default ${base.name}` : `default ${base.name}`;
437
+ let chosen = base;
438
+ let toolchainWhy = baseWhy;
439
+ if (persona.vendor && persona.vendor !== baseVendor) {
440
+ const match = shipToolchains(ship).find((t) => toolchainVendor(t) === persona.vendor);
441
+ if (match) {
442
+ chosen = match;
443
+ toolchainWhy = `persona vendor ${persona.vendor} \u2192 ${match.name}`;
444
+ } else {
445
+ warnings.push(`seat ${role}: persona vendor "${persona.vendor}" has no toolchain on this ship; running ${base.name} (${baseVendor}) instead`);
446
+ toolchainWhy = `${baseWhy} (persona vendor ${persona.vendor} has no toolchain)`;
447
+ }
448
+ } else if (persona.vendor) {
449
+ toolchainWhy = `${baseWhy} (serves persona vendor ${persona.vendor})`;
450
+ }
451
+ const vendor = toolchainVendor(chosen);
452
+ const adapter = adapterFor(chosen.harness);
453
+ let model = chosen.model;
454
+ let modelWhy = `toolchain ${chosen.name} model`;
455
+ let reasoning = chosen.tiers.standard?.reasoning;
456
+ const explicit = persona.model;
457
+ const explicitOk = explicit !== void 0 && (vendor === "openrouter" || adapter.ownsModel(explicit));
458
+ if (explicit !== void 0 && !explicitOk) {
459
+ warnings.push(`seat ${role}: persona model "${explicit}" does not belong to vendor "${vendor}" (toolchain ${chosen.name}); ignored`);
460
+ }
461
+ if (explicit !== void 0 && explicitOk) {
462
+ model = explicit;
463
+ modelWhy = "persona model";
464
+ reasoning = void 0;
465
+ } else if (persona.tier && persona.tier !== "standard" && chosen.tiers[persona.tier]) {
466
+ const t = chosen.tiers[persona.tier];
467
+ model = t.model;
468
+ reasoning = t.reasoning;
469
+ modelWhy = `persona tier ${persona.tier} via ${chosen.name}`;
470
+ }
471
+ const toolchain = reasoning !== void 0 && adapter.applyReasoning ? adapter.applyReasoning(chosen, reasoning) : chosen;
472
+ return { toolchain, model, toolchainWhy, modelWhy, warnings };
473
+ }
414
474
 
415
475
  // src/config.ts
416
476
  import { getSessionStore, hostFromUrl } from "@tablation/client";
@@ -453,7 +513,7 @@ function explain(ship, required) {
453
513
 
454
514
  // src/repo-config.ts
455
515
  import { readFileSync as readFileSync3, existsSync as existsSync2 } from "fs";
456
- import { join as join3, basename } from "path";
516
+ import { join as join3, basename as basename2 } from "path";
457
517
  import { parse } from "yaml";
458
518
  var SPEC_VERSION = 1;
459
519
  var REPO_CONFIG_FILENAMES = [".crew.yaml", ".crew.yml", ".crew.json"];
@@ -792,7 +852,7 @@ function renderWorktreeName(template, ctx) {
792
852
  return renderBranchName(template.replace(/\{dir\}/g, ctx.dir).replace(/\{issue\}/g, issue), ctx);
793
853
  }
794
854
  function defaultWorktreePrefix(dir) {
795
- const name = basename(dir.replace(/[/\\]+$/, ""));
855
+ const name = basename2(dir.replace(/[/\\]+$/, ""));
796
856
  return name ? `${name}-issue-` : "issue-";
797
857
  }
798
858
  var DEFAULTS = {
@@ -966,7 +1026,7 @@ function effectiveBranchTemplate(cfg, prefix) {
966
1026
  function effectiveWorktreeDirName(cfg, dir, branchName, number, renderName) {
967
1027
  if (cfg.worktrees.name && renderName) return renderName(cfg.worktrees.name);
968
1028
  if (cfg.provenance["worktrees.prefix"] !== "default") return `${cfg.worktrees.prefix}${number}`;
969
- return `${basename(dir.replace(/[/\\]+$/, ""))}-${branchName}`;
1029
+ return `${basename2(dir.replace(/[/\\]+$/, ""))}-${branchName}`;
970
1030
  }
971
1031
  function validateEffective(cfg) {
972
1032
  const problems = [];
@@ -1571,7 +1631,7 @@ function dirForRepo(r, repoId) {
1571
1631
  }
1572
1632
  function repoTargetFor(r, repoId) {
1573
1633
  const names = r.resolved?.repoNames;
1574
- if (!repoId || !names) return Object.keys(r.repos).length ? null : { name: basename2(r.dir), dir: r.dir };
1634
+ if (!repoId || !names) return Object.keys(r.repos).length ? null : { name: basename3(r.dir), dir: r.dir };
1575
1635
  const name = names[repoId];
1576
1636
  if (!name) return null;
1577
1637
  return reposOf(r).find((t) => t.name === name) ?? null;
@@ -1654,7 +1714,7 @@ function reposOf(r) {
1654
1714
  const named = Object.entries(r.repos);
1655
1715
  if (named.length > 0) return named.map(([name, dir]) => ({ name, dir }));
1656
1716
  const trackerNames = r.resolved?.repoNames ? Object.values(r.resolved.repoNames) : [];
1657
- if (trackerNames.length <= 1) return [{ name: basename2(r.dir), dir: r.dir }];
1717
+ if (trackerNames.length <= 1) return [{ name: basename3(r.dir), dir: r.dir }];
1658
1718
  const remotes = r.resolved?.repoRemotes ?? {};
1659
1719
  const remoteByName = {};
1660
1720
  for (const [id, name] of Object.entries(r.resolved.repoNames)) {
@@ -3040,7 +3100,7 @@ import {
3040
3100
  statSync as statSync4,
3041
3101
  unlinkSync as unlinkSync2
3042
3102
  } from "fs";
3043
- import { dirname as dirname4, basename as basename3 } from "path";
3103
+ import { dirname as dirname4, basename as basename4 } from "path";
3044
3104
  var GitError = class extends Error {
3045
3105
  };
3046
3106
  function gitRaw(cwd, args) {
@@ -3136,7 +3196,7 @@ function deleteRemoteBranch(cwd, remote, branch) {
3136
3196
  }
3137
3197
  function worktreeForNumber(cwd, num) {
3138
3198
  const re = new RegExp(`(?:^|-)${num}$`);
3139
- return worktrees(cwd).find((w) => re.test(basename3(w.path))) ?? null;
3199
+ return worktrees(cwd).find((w) => re.test(basename4(w.path))) ?? null;
3140
3200
  }
3141
3201
  var commitBodies = (cwd, range) => gitOk(cwd, ["log", range, "--format=%B"]) ?? "";
3142
3202
  var resolve2 = (cwd, ref) => gitOk(cwd, ["rev-parse", "-q", "--verify", `${ref}^{commit}`]);
@@ -5090,7 +5150,7 @@ ${waiting.map((t) => `| ${displayKey2(t)} | ${t.status} | ${blockers(t, i)} |`).
5090
5150
  }
5091
5151
 
5092
5152
  // src/ticket-branch.ts
5093
- import { basename as basename4 } from "path";
5153
+ import { basename as basename5 } from "path";
5094
5154
  function branchRenderer(t, role) {
5095
5155
  const { tag, prefix } = ticketBranchContext(t);
5096
5156
  return (template) => renderBranchName(template, { key: t.issue_id, title: t.title ?? void 0, role, tag, prefix });
@@ -5101,7 +5161,7 @@ function worktreeForTicket(dir, cfg, t, role) {
5101
5161
  const number = t.issue_id.replace(/^\D+/, "");
5102
5162
  const { tag } = ticketBranchContext(t);
5103
5163
  return effectiveWorktreeDirName(cfg, dir, branchName, number, (template) => renderWorktreeName(template, {
5104
- dir: basename4(dir.replace(/[/\\]+$/, "")),
5164
+ dir: basename5(dir.replace(/[/\\]+$/, "")),
5105
5165
  key: t.issue_id,
5106
5166
  title: t.title ?? void 0,
5107
5167
  role,
@@ -5790,7 +5850,7 @@ import { join as join8, dirname as dirname5 } from "path";
5790
5850
  import { createHash as createHash2 } from "crypto";
5791
5851
 
5792
5852
  // src/environment.ts
5793
- import { basename as basename5 } from "path";
5853
+ import { basename as basename6 } from "path";
5794
5854
  var API_KEY_VAR = "CREW_API_KEY";
5795
5855
  function authorshipBullets(a, contract) {
5796
5856
  if (!a) return [];
@@ -5841,7 +5901,7 @@ function repoSection(r, key, heading) {
5841
5901
  const repo = r.config;
5842
5902
  const branchTemplate = effectiveBranchTemplate(repo, repoPrefix(repo));
5843
5903
  const branchName = branchExample(repo, key);
5844
- const checkoutName = basename5(r.dir.replace(/[/\\]+$/, ""));
5904
+ const checkoutName = basename6(r.dir.replace(/[/\\]+$/, ""));
5845
5905
  const worktreeName = effectiveWorktreeDirName(
5846
5906
  repo,
5847
5907
  r.dir,
@@ -6072,6 +6132,13 @@ function renderEnvironment(i) {
6072
6132
  }
6073
6133
 
6074
6134
  // src/agent-log.ts
6135
+ var AgentLogHttpError = class extends Error {
6136
+ status;
6137
+ constructor(message, status2) {
6138
+ super(message);
6139
+ this.status = status2;
6140
+ }
6141
+ };
6075
6142
  async function post(target, path, body) {
6076
6143
  const url = `${target.baseUrl.replace(/\/+$/, "")}/api/workspaces/${target.workspaceId}${path}`;
6077
6144
  const res = await fetch(url, {
@@ -6084,11 +6151,11 @@ async function post(target, path, body) {
6084
6151
  },
6085
6152
  body: JSON.stringify(body)
6086
6153
  });
6087
- if (!res.ok) throw new Error(`${path}: ${res.status} ${res.statusText}`);
6154
+ if (!res.ok) throw new AgentLogHttpError(`${path}: ${res.status} ${res.statusText}`, res.status);
6088
6155
  return await res.json();
6089
6156
  }
6090
6157
  async function reportAgentRun(target, run2) {
6091
- const entry = await post(target, "/agents/log", {
6158
+ const body = {
6092
6159
  agentId: target.agentId,
6093
6160
  ticketReference: run2.ticketReference,
6094
6161
  outcome: run2.outcome,
@@ -6104,7 +6171,14 @@ async function reportAgentRun(target, run2) {
6104
6171
  costUsd: run2.costUsd,
6105
6172
  promptVersion: run2.promptVersion,
6106
6173
  promptSha: run2.promptSha
6107
- });
6174
+ };
6175
+ let entry;
6176
+ try {
6177
+ entry = await post(target, "/agents/log", { ...body, harness: run2.harness, provider: run2.provider });
6178
+ } catch (e) {
6179
+ if (!(e instanceof AgentLogHttpError) || e.status !== 400 || !run2.harness && !run2.provider) throw e;
6180
+ entry = await post(target, "/agents/log", body);
6181
+ }
6108
6182
  for (const cycle of run2.cycles) {
6109
6183
  await post(target, `/agents/log/${entry.id}/cycles`, {
6110
6184
  cycleIndex: cycle.cycleIndex,
@@ -6153,6 +6227,7 @@ var TicketAttributionTracker = class {
6153
6227
  // src/agent.ts
6154
6228
  var DIGEST_MAX_AGE_SECONDS = 600;
6155
6229
  var LEGACY_TOOLCHAIN_HARNESS = "claude";
6230
+ var LEGACY_TOOLCHAIN_PROVIDER = "anthropic";
6156
6231
  function scrubbedEnv(env = process.env, harness = LEGACY_TOOLCHAIN_HARNESS) {
6157
6232
  const out = { ...env };
6158
6233
  for (const v of adapterFor(harness).unsetEnv) delete out[v];
@@ -6198,9 +6273,9 @@ function planAgentRun(o) {
6198
6273
  const promptSha = createHash2("sha256").update(`${effectiveCommon}${effectiveBrief}`).digest("hex");
6199
6274
  const streamsDir = join8(o.stateDir, "streams");
6200
6275
  const base = `${routeLabel}-${o.role}-${o.cycle}`;
6201
- const toolchain = resolveToolchain(o.ship, o.route);
6276
+ const seat = resolveSeat(o.ship, o.route, o.role, o.agentPersona ?? (o.agentModel ? { model: o.agentModel } : {}));
6277
+ const { toolchain, model } = seat;
6202
6278
  const adapter = adapterFor(toolchain.harness);
6203
- const model = o.agentModel ?? toolchain.model;
6204
6279
  const invocation = adapter.invocation(toolchain, {
6205
6280
  role: o.role,
6206
6281
  model,
@@ -6229,6 +6304,9 @@ function planAgentRun(o) {
6229
6304
  args: invocation.args,
6230
6305
  toolchain: toolchain.name,
6231
6306
  harness: toolchain.harness,
6307
+ provider: adapter.provider(toolchain),
6308
+ resolution: { toolchain: seat.toolchainWhy, model: seat.modelWhy },
6309
+ warnings: seat.warnings,
6232
6310
  prompt,
6233
6311
  promptBytes: Buffer.byteLength(prompt, "utf8"),
6234
6312
  unsetEnv: adapter.unsetEnv,
@@ -6259,6 +6337,8 @@ function planAgentRun(o) {
6259
6337
  function describePlan(p) {
6260
6338
  return [
6261
6339
  `role: ${p.role}`,
6340
+ ...p.resolution ? [`resolved: toolchain ${p.toolchain} \u2190 ${p.resolution.toolchain}; model ${p.model} \u2190 ${p.resolution.model}`] : [],
6341
+ ...(p.warnings ?? []).map((w) => `warning: ${w}`),
6262
6342
  // Only for a non-default toolchain, so a plain Claude run's output is unchanged.
6263
6343
  ...p.toolchain && p.toolchain !== "claude" ? [`toolchain: ${p.toolchain} (${p.harness ?? "claude"})`] : [],
6264
6344
  `cwd: ${p.cwd}`,
@@ -6289,6 +6369,7 @@ function spawnAgent(plan, emit) {
6289
6369
  const adapter = adapterFor(plan.harness ?? LEGACY_TOOLCHAIN_HARNESS);
6290
6370
  const started = Date.now();
6291
6371
  const startedAt = new Date(started).toISOString();
6372
+ for (const w of plan.warnings ?? []) emit.warn(w, { step: "agent", role: plan.role });
6292
6373
  const rawSink = openSink(plan.streamPath);
6293
6374
  const eventsSink = openSink(plan.eventsPath);
6294
6375
  return new Promise((resolve5, reject) => {
@@ -6376,6 +6457,11 @@ function spawnAgent(plan, emit) {
6376
6457
  await Promise.all([endSink(rawSink), endSink(eventsSink)]);
6377
6458
  if (spawnFailed) return;
6378
6459
  const attributedTicket = attribution.winner(plan.ticket);
6460
+ const identity = {
6461
+ toolchain: plan.toolchain ?? LEGACY_TOOLCHAIN_HARNESS,
6462
+ harness: adapter.harness,
6463
+ provider: plan.provider ?? LEGACY_TOOLCHAIN_PROVIDER
6464
+ };
6379
6465
  try {
6380
6466
  writeFileSync4(`${plan.streamPath}.meta.json`, JSON.stringify({
6381
6467
  route: plan.route,
@@ -6385,9 +6471,10 @@ function spawnAgent(plan, emit) {
6385
6471
  startedAt,
6386
6472
  exitCode: code ?? 1,
6387
6473
  model: plan.model,
6474
+ ...identity,
6388
6475
  sessionId: result?.sessionId ?? null,
6389
6476
  numTurns: result?.numTurns ?? null,
6390
- totalCostUsd: result?.totalCostUsd ?? null
6477
+ totalCostUsd: result?.costUsd ?? null
6391
6478
  }));
6392
6479
  } catch {
6393
6480
  }
@@ -6396,9 +6483,10 @@ function spawnAgent(plan, emit) {
6396
6483
  data: {
6397
6484
  ms: ms2,
6398
6485
  code: code ?? 1,
6486
+ ...identity,
6399
6487
  ...result?.sessionId ? { sessionId: result.sessionId } : {},
6400
6488
  ...result?.numTurns !== void 0 ? { numTurns: result.numTurns } : {},
6401
- ...result?.totalCostUsd !== void 0 ? { totalCostUsd: result.totalCostUsd } : {}
6489
+ ...result?.costUsd !== void 0 ? { totalCostUsd: result.costUsd } : {}
6402
6490
  }
6403
6491
  });
6404
6492
  if (plan.agentLog) {
@@ -6413,8 +6501,10 @@ function spawnAgent(plan, emit) {
6413
6501
  tokensIn: result?.inputTokens,
6414
6502
  tokensOut: result?.outputTokens,
6415
6503
  cacheReadTokens: result?.cacheReadTokens,
6416
- cacheWriteTokens: result?.cacheCreationTokens,
6417
- costUsd: result?.totalCostUsd,
6504
+ cacheWriteTokens: result?.cacheWriteTokens,
6505
+ costUsd: result?.costUsd,
6506
+ harness: identity.harness,
6507
+ provider: identity.provider,
6418
6508
  promptVersion: plan.promptVersion,
6419
6509
  promptSha: plan.promptSha
6420
6510
  });
@@ -8577,7 +8667,11 @@ async function fetchLastSyncedPrompt(route, agentsModelId, recordId, historyId,
8577
8667
  return void 0;
8578
8668
  }
8579
8669
  }
8580
- async function fetchSeatAgentModel(route, role, opts) {
8670
+ function choiceValue(v) {
8671
+ const raw = typeof v === "string" ? v : v && typeof v === "object" ? v.value ?? v.key : void 0;
8672
+ return typeof raw === "string" && raw.trim() ? raw.trim().toLowerCase() : void 0;
8673
+ }
8674
+ async function fetchSeatPersona(route, role, opts) {
8581
8675
  const agentId = await resolveAgentId(route, role, opts);
8582
8676
  if (!agentId || !route.resolved) return void 0;
8583
8677
  const client2 = new TablationClient2({
@@ -8588,7 +8682,14 @@ async function fetchSeatAgentModel(route, role, opts) {
8588
8682
  try {
8589
8683
  const agentsModel = await client2.dataModels.get("agents", route.resolved.workspaceId);
8590
8684
  const row = await client2.records.get(agentsModel.id, agentId);
8591
- return row.model ?? void 0;
8685
+ const tier = choiceValue(row.tier);
8686
+ const vendor = choiceValue(row.vendor);
8687
+ const persona = {
8688
+ ...row.model ? { model: row.model } : {},
8689
+ ...tier && TIERS.includes(tier) ? { tier } : {},
8690
+ ...vendor ? { vendor } : {}
8691
+ };
8692
+ return Object.keys(persona).length > 0 ? persona : void 0;
8592
8693
  } catch {
8593
8694
  return void 0;
8594
8695
  }
@@ -9392,7 +9493,7 @@ async function showLogEntry(route, entryId, opts = {}) {
9392
9493
 
9393
9494
  // src/repos-cmd.ts
9394
9495
  import { homedir as homedir4 } from "os";
9395
- import { basename as basename6, resolve as resolve3 } from "path";
9496
+ import { basename as basename7, resolve as resolve3 } from "path";
9396
9497
  import { isMap, isSeq, parseDocument, stringify as stringify2 } from "yaml";
9397
9498
  var ReposError = class extends Error {
9398
9499
  };
@@ -9464,7 +9565,7 @@ function configuredRepos(text, route, base) {
9464
9565
  });
9465
9566
  }
9466
9567
  const dir = m.get("dir");
9467
- return dir ? [{ name: basename6(String(dir)), dir: expandDir(String(dir), base) }] : [];
9568
+ return dir ? [{ name: basename7(String(dir)), dir: expandDir(String(dir), base) }] : [];
9468
9569
  }
9469
9570
  function planRepoAdd(i) {
9470
9571
  const doc = parseDocument(i.text);
@@ -9476,7 +9577,7 @@ function planRepoAdd(i) {
9476
9577
  throw new ReposError(`${target} is already configured on route ${i.route}`);
9477
9578
  }
9478
9579
  const matched = matchRepoName(i.origin, i.resolved);
9479
- const name = matched ?? i.name ?? normalizeRemote(i.origin)?.split("/").pop() ?? basename6(target);
9580
+ const name = matched ?? i.name ?? normalizeRemote(i.origin)?.split("/").pop() ?? basename7(target);
9480
9581
  let warning;
9481
9582
  if (!matched) {
9482
9583
  const known = Object.values(i.resolved?.repoNames ?? {});
@@ -9574,14 +9675,14 @@ function schedulerInstalled(plan, exists, crontab) {
9574
9675
 
9575
9676
  // src/worktree-sweep.ts
9576
9677
  import { existsSync as existsSync15 } from "fs";
9577
- import { basename as basename7 } from "path";
9678
+ import { basename as basename8 } from "path";
9578
9679
  function planWorktreeSweep(target, tickets, contract) {
9579
9680
  const terminal = new Set(contract.statuses.resolved.filter((s) => s !== contract.statuses.verified));
9580
9681
  const verified = contract.statuses.verified;
9581
9682
  const byNumber = new Map(tickets.map((t) => [t.issue_id.replace(/^\D+/, ""), t]));
9582
9683
  const actions = [];
9583
9684
  for (const w of worktrees(target.dir)) {
9584
- const m = /(?:^|-)(\d+)$/.exec(basename7(w.path));
9685
+ const m = /(?:^|-)(\d+)$/.exec(basename8(w.path));
9585
9686
  if (!m) continue;
9586
9687
  const ticket = byNumber.get(m[1]);
9587
9688
  if (!ticket) continue;
@@ -10746,7 +10847,7 @@ async function runRoleAgent(role, decision, ctx) {
10746
10847
  cycle: emit.cycle,
10747
10848
  ticket: ticketHint,
10748
10849
  divergedPrompt: await fetchDivergedPrompt(route, role, { userAgent: ship.userAgent }),
10749
- agentModel: await fetchSeatAgentModel(route, role, { userAgent: ship.userAgent }),
10850
+ agentPersona: await fetchSeatPersona(route, role, { userAgent: ship.userAgent }),
10750
10851
  resolvedAgentId: await resolveAgentId(route, role, { userAgent: ship.userAgent })
10751
10852
  });
10752
10853
  emit.enter("agent", role);
@@ -11832,7 +11933,7 @@ ${changed2.map((c) => ` - ${c}
11832
11933
  cycle: wemit.cycle,
11833
11934
  ticket: w.decision.actionable.top?.issue_id,
11834
11935
  divergedPrompt: await fetchDivergedPrompt(w.route, w.role, { userAgent: cfg.ship.userAgent }),
11835
- agentModel: await fetchSeatAgentModel(w.route, w.role, { userAgent: cfg.ship.userAgent }),
11936
+ agentPersona: await fetchSeatPersona(w.route, w.role, { userAgent: cfg.ship.userAgent }),
11836
11937
  resolvedAgentId: await resolveAgentId(w.route, w.role, { userAgent: cfg.ship.userAgent })
11837
11938
  });
11838
11939
  process.stdout.write(`${describePlan(fleetPlan2)}
@@ -11925,7 +12026,7 @@ ${changed2.map((c) => ` - ${c}
11925
12026
  cycle: wemit.cycle,
11926
12027
  ticket: fleetTicketHint,
11927
12028
  divergedPrompt: await fetchDivergedPrompt(w.route, w.role, { userAgent: cfg.ship.userAgent }),
11928
- agentModel: await fetchSeatAgentModel(w.route, w.role, { userAgent: cfg.ship.userAgent }),
12029
+ agentPersona: await fetchSeatPersona(w.route, w.role, { userAgent: cfg.ship.userAgent }),
11929
12030
  resolvedAgentId: await resolveAgentId(w.route, w.role, { userAgent: cfg.ship.userAgent })
11930
12031
  });
11931
12032
  wemit.enter("agent", w.role);
@@ -12130,7 +12231,7 @@ ${changed2.map((c) => ` - ${c}
12130
12231
  cycle: emit.cycle,
12131
12232
  ticket: ticketHint,
12132
12233
  divergedPrompt: await fetchDivergedPrompt(route, current, { userAgent: cfg.ship.userAgent }),
12133
- agentModel: await fetchSeatAgentModel(route, current, { userAgent: cfg.ship.userAgent }),
12234
+ agentPersona: await fetchSeatPersona(route, current, { userAgent: cfg.ship.userAgent }),
12134
12235
  resolvedAgentId: await resolveAgentId(route, current, { userAgent: cfg.ship.userAgent })
12135
12236
  });
12136
12237
  if (dryRun) {
@@ -12622,7 +12723,7 @@ ${changed2.map((c) => ` - ${c}
12622
12723
  }
12623
12724
  for (const e of entries) {
12624
12725
  process.stdout.write(
12625
- `${e.id} ${e.started_at ?? "?"} ${e.outcome ?? "?"}${e.ticket_reference ? ` ${e.ticket_reference}` : ""}${e.model ? ` ${e.model}` : ""}
12726
+ `${e.id} ${e.started_at ?? "?"} ${e.outcome ?? "?"}${e.ticket_reference ? ` ${e.ticket_reference}` : ""}${e.model ? ` ${e.model}` : ""}${e.harness || e.provider ? ` (${[e.harness, e.provider].filter(Boolean).join(" \xB7 ")})` : ""}
12626
12727
  `
12627
12728
  );
12628
12729
  }
@@ -12638,6 +12739,8 @@ finished: ${entry.finished_at ?? "?"}
12638
12739
  outcome: ${entry.outcome ?? "?"}
12639
12740
  ticket: ${entry.ticket_reference ?? "(none)"}
12640
12741
  model: ${entry.model ?? "?"}
12742
+ harness: ${entry.harness ?? "?"}
12743
+ provider: ${entry.provider ?? "?"}
12641
12744
  `
12642
12745
  );
12643
12746
  if (flag("prompt")) {
@@ -13534,7 +13637,7 @@ Signed in as ${result.identity.email} \u2014 minted an API key in workspace "${r
13534
13637
  // `synthesis-tabl-123`. `worktreePrefix` above is kept for
13535
13638
  // config-provenance display; this is the field that actually
13536
13639
  // describes where a worktree lands by default.
13537
- worktreeDirPattern: r.config.worktrees.name ?? (r.config.provenance["worktrees.prefix"] !== "default" ? `${r.config.worktrees.prefix}<number>` : `${basename8(r.dir.replace(/[/\\]+$/, ""))}-<branch>`),
13640
+ worktreeDirPattern: r.config.worktrees.name ?? (r.config.provenance["worktrees.prefix"] !== "default" ? `${r.config.worktrees.prefix}<number>` : `${basename9(r.dir.replace(/[/\\]+$/, ""))}-<branch>`),
13538
13641
  worktreeParent: resolvePath(r.dir, "..")
13539
13642
  })),
13540
13643
  waiting: waiting ? { ticket: waiting.ticket, since: waiting.since, streak: fairness.streak() } : null
@@ -13642,7 +13745,7 @@ routes: ${cfg.routes.map((c) => c.route).join(", ")}
13642
13745
  );
13643
13746
  for (const r of await resolvedRepos(route)) {
13644
13747
  const { config } = r;
13645
- const worktreePattern = config.worktrees.name ?? (config.provenance["worktrees.prefix"] !== "default" ? `${config.worktrees.prefix}<number>` : `${basename8(r.dir.replace(/[/\\]+$/, ""))}-${effectiveBranchTemplate(config, void 0)}`);
13748
+ const worktreePattern = config.worktrees.name ?? (config.provenance["worktrees.prefix"] !== "default" ? `${config.worktrees.prefix}<number>` : `${basename9(r.dir.replace(/[/\\]+$/, ""))}-${effectiveBranchTemplate(config, void 0)}`);
13646
13749
  process.stdout.write(
13647
13750
  `${`repo ${r.name}:`.padEnd(19)}${r.dir}
13648
13751
  worktrees at ../${worktreePattern} (${config.provenance["worktrees.name"] ?? config.provenance["worktrees.prefix"] ?? "default"}), branch ${config.branch.name}
@@ -13786,6 +13889,19 @@ crew manifest: ${seats2.length ? seats2.map((c) => c.name).join(", ") : "non
13786
13889
  );
13787
13890
  }
13788
13891
  }
13892
+ const shipVendors = new Set(shipToolchains(cfg.ship).map(toolchainVendor));
13893
+ if (route.apiKey && route.resolved) {
13894
+ let flagged = 0;
13895
+ for (const role of ROLE_NAMES) {
13896
+ const persona = await fetchSeatPersona(route, role, { userAgent: cfg.ship.userAgent });
13897
+ if (persona?.vendor && !shipVendors.has(persona.vendor)) {
13898
+ flagged++;
13899
+ process.stdout.write(`seat vendor: ${role} persona wants "${persona.vendor}" \u2014 no toolchain on this ship serves it (have: ${[...shipVendors].join(", ")}); it will run ${resolveSeat(cfg.ship, route, role).toolchain.name} with a warning
13900
+ `);
13901
+ }
13902
+ }
13903
+ if (flagged === 0) process.stdout.write("seat vendor: OK (every persona vendor has a toolchain on this ship)\n");
13904
+ }
13789
13905
  for (const c of cfg.routes.filter((r) => r.enabled)) {
13790
13906
  let repos;
13791
13907
  try {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@tablation/crew",
3
- "version": "0.28.0",
3
+ "version": "0.30.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",