@tablation/crew 0.28.0 → 0.29.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 +118 -30
  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;
@@ -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",
@@ -411,6 +424,52 @@ function resolveToolchain(ship, route) {
411
424
  if (!found) throw new Error(`toolchain "${name}" is not defined (have: ${shipToolchains(ship).map((t) => t.name).join(", ")})`);
412
425
  return found;
413
426
  }
427
+ function toolchainVendor(spec) {
428
+ if (spec.launcher && basename(spec.launcher) === "ori") return "openrouter";
429
+ return adapterFor(spec.harness).vendor;
430
+ }
431
+ function resolveSeat(ship, route, role, persona = {}) {
432
+ const warnings = [];
433
+ const base = resolveToolchain(ship, route);
434
+ const baseVendor = toolchainVendor(base);
435
+ const baseWhy = route?.toolchain ? `route toolchain ${base.name}` : ship.toolchain ? `ship default ${base.name}` : `default ${base.name}`;
436
+ let chosen = base;
437
+ let toolchainWhy = baseWhy;
438
+ if (persona.vendor && persona.vendor !== baseVendor) {
439
+ const match = shipToolchains(ship).find((t) => toolchainVendor(t) === persona.vendor);
440
+ if (match) {
441
+ chosen = match;
442
+ toolchainWhy = `persona vendor ${persona.vendor} \u2192 ${match.name}`;
443
+ } else {
444
+ warnings.push(`seat ${role}: persona vendor "${persona.vendor}" has no toolchain on this ship; running ${base.name} (${baseVendor}) instead`);
445
+ toolchainWhy = `${baseWhy} (persona vendor ${persona.vendor} has no toolchain)`;
446
+ }
447
+ } else if (persona.vendor) {
448
+ toolchainWhy = `${baseWhy} (serves persona vendor ${persona.vendor})`;
449
+ }
450
+ const vendor = toolchainVendor(chosen);
451
+ const adapter = adapterFor(chosen.harness);
452
+ let model = chosen.model;
453
+ let modelWhy = `toolchain ${chosen.name} model`;
454
+ let reasoning = chosen.tiers.standard?.reasoning;
455
+ const explicit = persona.model;
456
+ const explicitOk = explicit !== void 0 && (vendor === "openrouter" || adapter.ownsModel(explicit));
457
+ if (explicit !== void 0 && !explicitOk) {
458
+ warnings.push(`seat ${role}: persona model "${explicit}" does not belong to vendor "${vendor}" (toolchain ${chosen.name}); ignored`);
459
+ }
460
+ if (explicit !== void 0 && explicitOk) {
461
+ model = explicit;
462
+ modelWhy = "persona model";
463
+ reasoning = void 0;
464
+ } else if (persona.tier && persona.tier !== "standard" && chosen.tiers[persona.tier]) {
465
+ const t = chosen.tiers[persona.tier];
466
+ model = t.model;
467
+ reasoning = t.reasoning;
468
+ modelWhy = `persona tier ${persona.tier} via ${chosen.name}`;
469
+ }
470
+ const toolchain = reasoning !== void 0 && adapter.applyReasoning ? adapter.applyReasoning(chosen, reasoning) : chosen;
471
+ return { toolchain, model, toolchainWhy, modelWhy, warnings };
472
+ }
414
473
 
415
474
  // src/config.ts
416
475
  import { getSessionStore, hostFromUrl } from "@tablation/client";
@@ -453,7 +512,7 @@ function explain(ship, required) {
453
512
 
454
513
  // src/repo-config.ts
455
514
  import { readFileSync as readFileSync3, existsSync as existsSync2 } from "fs";
456
- import { join as join3, basename } from "path";
515
+ import { join as join3, basename as basename2 } from "path";
457
516
  import { parse } from "yaml";
458
517
  var SPEC_VERSION = 1;
459
518
  var REPO_CONFIG_FILENAMES = [".crew.yaml", ".crew.yml", ".crew.json"];
@@ -792,7 +851,7 @@ function renderWorktreeName(template, ctx) {
792
851
  return renderBranchName(template.replace(/\{dir\}/g, ctx.dir).replace(/\{issue\}/g, issue), ctx);
793
852
  }
794
853
  function defaultWorktreePrefix(dir) {
795
- const name = basename(dir.replace(/[/\\]+$/, ""));
854
+ const name = basename2(dir.replace(/[/\\]+$/, ""));
796
855
  return name ? `${name}-issue-` : "issue-";
797
856
  }
798
857
  var DEFAULTS = {
@@ -966,7 +1025,7 @@ function effectiveBranchTemplate(cfg, prefix) {
966
1025
  function effectiveWorktreeDirName(cfg, dir, branchName, number, renderName) {
967
1026
  if (cfg.worktrees.name && renderName) return renderName(cfg.worktrees.name);
968
1027
  if (cfg.provenance["worktrees.prefix"] !== "default") return `${cfg.worktrees.prefix}${number}`;
969
- return `${basename(dir.replace(/[/\\]+$/, ""))}-${branchName}`;
1028
+ return `${basename2(dir.replace(/[/\\]+$/, ""))}-${branchName}`;
970
1029
  }
971
1030
  function validateEffective(cfg) {
972
1031
  const problems = [];
@@ -1571,7 +1630,7 @@ function dirForRepo(r, repoId) {
1571
1630
  }
1572
1631
  function repoTargetFor(r, repoId) {
1573
1632
  const names = r.resolved?.repoNames;
1574
- if (!repoId || !names) return Object.keys(r.repos).length ? null : { name: basename2(r.dir), dir: r.dir };
1633
+ if (!repoId || !names) return Object.keys(r.repos).length ? null : { name: basename3(r.dir), dir: r.dir };
1575
1634
  const name = names[repoId];
1576
1635
  if (!name) return null;
1577
1636
  return reposOf(r).find((t) => t.name === name) ?? null;
@@ -1654,7 +1713,7 @@ function reposOf(r) {
1654
1713
  const named = Object.entries(r.repos);
1655
1714
  if (named.length > 0) return named.map(([name, dir]) => ({ name, dir }));
1656
1715
  const trackerNames = r.resolved?.repoNames ? Object.values(r.resolved.repoNames) : [];
1657
- if (trackerNames.length <= 1) return [{ name: basename2(r.dir), dir: r.dir }];
1716
+ if (trackerNames.length <= 1) return [{ name: basename3(r.dir), dir: r.dir }];
1658
1717
  const remotes = r.resolved?.repoRemotes ?? {};
1659
1718
  const remoteByName = {};
1660
1719
  for (const [id, name] of Object.entries(r.resolved.repoNames)) {
@@ -3040,7 +3099,7 @@ import {
3040
3099
  statSync as statSync4,
3041
3100
  unlinkSync as unlinkSync2
3042
3101
  } from "fs";
3043
- import { dirname as dirname4, basename as basename3 } from "path";
3102
+ import { dirname as dirname4, basename as basename4 } from "path";
3044
3103
  var GitError = class extends Error {
3045
3104
  };
3046
3105
  function gitRaw(cwd, args) {
@@ -3136,7 +3195,7 @@ function deleteRemoteBranch(cwd, remote, branch) {
3136
3195
  }
3137
3196
  function worktreeForNumber(cwd, num) {
3138
3197
  const re = new RegExp(`(?:^|-)${num}$`);
3139
- return worktrees(cwd).find((w) => re.test(basename3(w.path))) ?? null;
3198
+ return worktrees(cwd).find((w) => re.test(basename4(w.path))) ?? null;
3140
3199
  }
3141
3200
  var commitBodies = (cwd, range) => gitOk(cwd, ["log", range, "--format=%B"]) ?? "";
3142
3201
  var resolve2 = (cwd, ref) => gitOk(cwd, ["rev-parse", "-q", "--verify", `${ref}^{commit}`]);
@@ -5090,7 +5149,7 @@ ${waiting.map((t) => `| ${displayKey2(t)} | ${t.status} | ${blockers(t, i)} |`).
5090
5149
  }
5091
5150
 
5092
5151
  // src/ticket-branch.ts
5093
- import { basename as basename4 } from "path";
5152
+ import { basename as basename5 } from "path";
5094
5153
  function branchRenderer(t, role) {
5095
5154
  const { tag, prefix } = ticketBranchContext(t);
5096
5155
  return (template) => renderBranchName(template, { key: t.issue_id, title: t.title ?? void 0, role, tag, prefix });
@@ -5101,7 +5160,7 @@ function worktreeForTicket(dir, cfg, t, role) {
5101
5160
  const number = t.issue_id.replace(/^\D+/, "");
5102
5161
  const { tag } = ticketBranchContext(t);
5103
5162
  return effectiveWorktreeDirName(cfg, dir, branchName, number, (template) => renderWorktreeName(template, {
5104
- dir: basename4(dir.replace(/[/\\]+$/, "")),
5163
+ dir: basename5(dir.replace(/[/\\]+$/, "")),
5105
5164
  key: t.issue_id,
5106
5165
  title: t.title ?? void 0,
5107
5166
  role,
@@ -5790,7 +5849,7 @@ import { join as join8, dirname as dirname5 } from "path";
5790
5849
  import { createHash as createHash2 } from "crypto";
5791
5850
 
5792
5851
  // src/environment.ts
5793
- import { basename as basename5 } from "path";
5852
+ import { basename as basename6 } from "path";
5794
5853
  var API_KEY_VAR = "CREW_API_KEY";
5795
5854
  function authorshipBullets(a, contract) {
5796
5855
  if (!a) return [];
@@ -5841,7 +5900,7 @@ function repoSection(r, key, heading) {
5841
5900
  const repo = r.config;
5842
5901
  const branchTemplate = effectiveBranchTemplate(repo, repoPrefix(repo));
5843
5902
  const branchName = branchExample(repo, key);
5844
- const checkoutName = basename5(r.dir.replace(/[/\\]+$/, ""));
5903
+ const checkoutName = basename6(r.dir.replace(/[/\\]+$/, ""));
5845
5904
  const worktreeName = effectiveWorktreeDirName(
5846
5905
  repo,
5847
5906
  r.dir,
@@ -6198,9 +6257,9 @@ function planAgentRun(o) {
6198
6257
  const promptSha = createHash2("sha256").update(`${effectiveCommon}${effectiveBrief}`).digest("hex");
6199
6258
  const streamsDir = join8(o.stateDir, "streams");
6200
6259
  const base = `${routeLabel}-${o.role}-${o.cycle}`;
6201
- const toolchain = resolveToolchain(o.ship, o.route);
6260
+ const seat = resolveSeat(o.ship, o.route, o.role, o.agentPersona ?? (o.agentModel ? { model: o.agentModel } : {}));
6261
+ const { toolchain, model } = seat;
6202
6262
  const adapter = adapterFor(toolchain.harness);
6203
- const model = o.agentModel ?? toolchain.model;
6204
6263
  const invocation = adapter.invocation(toolchain, {
6205
6264
  role: o.role,
6206
6265
  model,
@@ -6229,6 +6288,8 @@ function planAgentRun(o) {
6229
6288
  args: invocation.args,
6230
6289
  toolchain: toolchain.name,
6231
6290
  harness: toolchain.harness,
6291
+ resolution: { toolchain: seat.toolchainWhy, model: seat.modelWhy },
6292
+ warnings: seat.warnings,
6232
6293
  prompt,
6233
6294
  promptBytes: Buffer.byteLength(prompt, "utf8"),
6234
6295
  unsetEnv: adapter.unsetEnv,
@@ -6259,6 +6320,8 @@ function planAgentRun(o) {
6259
6320
  function describePlan(p) {
6260
6321
  return [
6261
6322
  `role: ${p.role}`,
6323
+ ...p.resolution ? [`resolved: toolchain ${p.toolchain} \u2190 ${p.resolution.toolchain}; model ${p.model} \u2190 ${p.resolution.model}`] : [],
6324
+ ...(p.warnings ?? []).map((w) => `warning: ${w}`),
6262
6325
  // Only for a non-default toolchain, so a plain Claude run's output is unchanged.
6263
6326
  ...p.toolchain && p.toolchain !== "claude" ? [`toolchain: ${p.toolchain} (${p.harness ?? "claude"})`] : [],
6264
6327
  `cwd: ${p.cwd}`,
@@ -6289,6 +6352,7 @@ function spawnAgent(plan, emit) {
6289
6352
  const adapter = adapterFor(plan.harness ?? LEGACY_TOOLCHAIN_HARNESS);
6290
6353
  const started = Date.now();
6291
6354
  const startedAt = new Date(started).toISOString();
6355
+ for (const w of plan.warnings ?? []) emit.warn(w, { step: "agent", role: plan.role });
6292
6356
  const rawSink = openSink(plan.streamPath);
6293
6357
  const eventsSink = openSink(plan.eventsPath);
6294
6358
  return new Promise((resolve5, reject) => {
@@ -8577,7 +8641,11 @@ async function fetchLastSyncedPrompt(route, agentsModelId, recordId, historyId,
8577
8641
  return void 0;
8578
8642
  }
8579
8643
  }
8580
- async function fetchSeatAgentModel(route, role, opts) {
8644
+ function choiceValue(v) {
8645
+ const raw = typeof v === "string" ? v : v && typeof v === "object" ? v.value ?? v.key : void 0;
8646
+ return typeof raw === "string" && raw.trim() ? raw.trim().toLowerCase() : void 0;
8647
+ }
8648
+ async function fetchSeatPersona(route, role, opts) {
8581
8649
  const agentId = await resolveAgentId(route, role, opts);
8582
8650
  if (!agentId || !route.resolved) return void 0;
8583
8651
  const client2 = new TablationClient2({
@@ -8588,7 +8656,14 @@ async function fetchSeatAgentModel(route, role, opts) {
8588
8656
  try {
8589
8657
  const agentsModel = await client2.dataModels.get("agents", route.resolved.workspaceId);
8590
8658
  const row = await client2.records.get(agentsModel.id, agentId);
8591
- return row.model ?? void 0;
8659
+ const tier = choiceValue(row.tier);
8660
+ const vendor = choiceValue(row.vendor);
8661
+ const persona = {
8662
+ ...row.model ? { model: row.model } : {},
8663
+ ...tier && TIERS.includes(tier) ? { tier } : {},
8664
+ ...vendor ? { vendor } : {}
8665
+ };
8666
+ return Object.keys(persona).length > 0 ? persona : void 0;
8592
8667
  } catch {
8593
8668
  return void 0;
8594
8669
  }
@@ -9392,7 +9467,7 @@ async function showLogEntry(route, entryId, opts = {}) {
9392
9467
 
9393
9468
  // src/repos-cmd.ts
9394
9469
  import { homedir as homedir4 } from "os";
9395
- import { basename as basename6, resolve as resolve3 } from "path";
9470
+ import { basename as basename7, resolve as resolve3 } from "path";
9396
9471
  import { isMap, isSeq, parseDocument, stringify as stringify2 } from "yaml";
9397
9472
  var ReposError = class extends Error {
9398
9473
  };
@@ -9464,7 +9539,7 @@ function configuredRepos(text, route, base) {
9464
9539
  });
9465
9540
  }
9466
9541
  const dir = m.get("dir");
9467
- return dir ? [{ name: basename6(String(dir)), dir: expandDir(String(dir), base) }] : [];
9542
+ return dir ? [{ name: basename7(String(dir)), dir: expandDir(String(dir), base) }] : [];
9468
9543
  }
9469
9544
  function planRepoAdd(i) {
9470
9545
  const doc = parseDocument(i.text);
@@ -9476,7 +9551,7 @@ function planRepoAdd(i) {
9476
9551
  throw new ReposError(`${target} is already configured on route ${i.route}`);
9477
9552
  }
9478
9553
  const matched = matchRepoName(i.origin, i.resolved);
9479
- const name = matched ?? i.name ?? normalizeRemote(i.origin)?.split("/").pop() ?? basename6(target);
9554
+ const name = matched ?? i.name ?? normalizeRemote(i.origin)?.split("/").pop() ?? basename7(target);
9480
9555
  let warning;
9481
9556
  if (!matched) {
9482
9557
  const known = Object.values(i.resolved?.repoNames ?? {});
@@ -9574,14 +9649,14 @@ function schedulerInstalled(plan, exists, crontab) {
9574
9649
 
9575
9650
  // src/worktree-sweep.ts
9576
9651
  import { existsSync as existsSync15 } from "fs";
9577
- import { basename as basename7 } from "path";
9652
+ import { basename as basename8 } from "path";
9578
9653
  function planWorktreeSweep(target, tickets, contract) {
9579
9654
  const terminal = new Set(contract.statuses.resolved.filter((s) => s !== contract.statuses.verified));
9580
9655
  const verified = contract.statuses.verified;
9581
9656
  const byNumber = new Map(tickets.map((t) => [t.issue_id.replace(/^\D+/, ""), t]));
9582
9657
  const actions = [];
9583
9658
  for (const w of worktrees(target.dir)) {
9584
- const m = /(?:^|-)(\d+)$/.exec(basename7(w.path));
9659
+ const m = /(?:^|-)(\d+)$/.exec(basename8(w.path));
9585
9660
  if (!m) continue;
9586
9661
  const ticket = byNumber.get(m[1]);
9587
9662
  if (!ticket) continue;
@@ -10746,7 +10821,7 @@ async function runRoleAgent(role, decision, ctx) {
10746
10821
  cycle: emit.cycle,
10747
10822
  ticket: ticketHint,
10748
10823
  divergedPrompt: await fetchDivergedPrompt(route, role, { userAgent: ship.userAgent }),
10749
- agentModel: await fetchSeatAgentModel(route, role, { userAgent: ship.userAgent }),
10824
+ agentPersona: await fetchSeatPersona(route, role, { userAgent: ship.userAgent }),
10750
10825
  resolvedAgentId: await resolveAgentId(route, role, { userAgent: ship.userAgent })
10751
10826
  });
10752
10827
  emit.enter("agent", role);
@@ -11832,7 +11907,7 @@ ${changed2.map((c) => ` - ${c}
11832
11907
  cycle: wemit.cycle,
11833
11908
  ticket: w.decision.actionable.top?.issue_id,
11834
11909
  divergedPrompt: await fetchDivergedPrompt(w.route, w.role, { userAgent: cfg.ship.userAgent }),
11835
- agentModel: await fetchSeatAgentModel(w.route, w.role, { userAgent: cfg.ship.userAgent }),
11910
+ agentPersona: await fetchSeatPersona(w.route, w.role, { userAgent: cfg.ship.userAgent }),
11836
11911
  resolvedAgentId: await resolveAgentId(w.route, w.role, { userAgent: cfg.ship.userAgent })
11837
11912
  });
11838
11913
  process.stdout.write(`${describePlan(fleetPlan2)}
@@ -11925,7 +12000,7 @@ ${changed2.map((c) => ` - ${c}
11925
12000
  cycle: wemit.cycle,
11926
12001
  ticket: fleetTicketHint,
11927
12002
  divergedPrompt: await fetchDivergedPrompt(w.route, w.role, { userAgent: cfg.ship.userAgent }),
11928
- agentModel: await fetchSeatAgentModel(w.route, w.role, { userAgent: cfg.ship.userAgent }),
12003
+ agentPersona: await fetchSeatPersona(w.route, w.role, { userAgent: cfg.ship.userAgent }),
11929
12004
  resolvedAgentId: await resolveAgentId(w.route, w.role, { userAgent: cfg.ship.userAgent })
11930
12005
  });
11931
12006
  wemit.enter("agent", w.role);
@@ -12130,7 +12205,7 @@ ${changed2.map((c) => ` - ${c}
12130
12205
  cycle: emit.cycle,
12131
12206
  ticket: ticketHint,
12132
12207
  divergedPrompt: await fetchDivergedPrompt(route, current, { userAgent: cfg.ship.userAgent }),
12133
- agentModel: await fetchSeatAgentModel(route, current, { userAgent: cfg.ship.userAgent }),
12208
+ agentPersona: await fetchSeatPersona(route, current, { userAgent: cfg.ship.userAgent }),
12134
12209
  resolvedAgentId: await resolveAgentId(route, current, { userAgent: cfg.ship.userAgent })
12135
12210
  });
12136
12211
  if (dryRun) {
@@ -13534,7 +13609,7 @@ Signed in as ${result.identity.email} \u2014 minted an API key in workspace "${r
13534
13609
  // `synthesis-tabl-123`. `worktreePrefix` above is kept for
13535
13610
  // config-provenance display; this is the field that actually
13536
13611
  // 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>`),
13612
+ worktreeDirPattern: r.config.worktrees.name ?? (r.config.provenance["worktrees.prefix"] !== "default" ? `${r.config.worktrees.prefix}<number>` : `${basename9(r.dir.replace(/[/\\]+$/, ""))}-<branch>`),
13538
13613
  worktreeParent: resolvePath(r.dir, "..")
13539
13614
  })),
13540
13615
  waiting: waiting ? { ticket: waiting.ticket, since: waiting.since, streak: fairness.streak() } : null
@@ -13642,7 +13717,7 @@ routes: ${cfg.routes.map((c) => c.route).join(", ")}
13642
13717
  );
13643
13718
  for (const r of await resolvedRepos(route)) {
13644
13719
  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)}`);
13720
+ const worktreePattern = config.worktrees.name ?? (config.provenance["worktrees.prefix"] !== "default" ? `${config.worktrees.prefix}<number>` : `${basename9(r.dir.replace(/[/\\]+$/, ""))}-${effectiveBranchTemplate(config, void 0)}`);
13646
13721
  process.stdout.write(
13647
13722
  `${`repo ${r.name}:`.padEnd(19)}${r.dir}
13648
13723
  worktrees at ../${worktreePattern} (${config.provenance["worktrees.name"] ?? config.provenance["worktrees.prefix"] ?? "default"}), branch ${config.branch.name}
@@ -13786,6 +13861,19 @@ crew manifest: ${seats2.length ? seats2.map((c) => c.name).join(", ") : "non
13786
13861
  );
13787
13862
  }
13788
13863
  }
13864
+ const shipVendors = new Set(shipToolchains(cfg.ship).map(toolchainVendor));
13865
+ if (route.apiKey && route.resolved) {
13866
+ let flagged = 0;
13867
+ for (const role of ROLE_NAMES) {
13868
+ const persona = await fetchSeatPersona(route, role, { userAgent: cfg.ship.userAgent });
13869
+ if (persona?.vendor && !shipVendors.has(persona.vendor)) {
13870
+ flagged++;
13871
+ 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
13872
+ `);
13873
+ }
13874
+ }
13875
+ if (flagged === 0) process.stdout.write("seat vendor: OK (every persona vendor has a toolchain on this ship)\n");
13876
+ }
13789
13877
  for (const c of cfg.routes.filter((r) => r.enabled)) {
13790
13878
  let repos;
13791
13879
  try {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@tablation/crew",
3
- "version": "0.28.0",
3
+ "version": "0.29.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",