@tablation/crew 0.27.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.
package/dist/cli.js CHANGED
@@ -1,15 +1,15 @@
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
- import { readFileSync as readFileSync3, existsSync as existsSync3 } from "fs";
11
- import { resolve, dirname as dirname2, isAbsolute, join as join3, basename as basename2 } from "path";
12
- import { homedir, tmpdir } from "os";
10
+ import { readFileSync as readFileSync4, existsSync as existsSync3 } from "fs";
11
+ import { resolve, dirname as dirname2, isAbsolute, join as join4, basename as basename3 } from "path";
12
+ import { homedir as homedir2, tmpdir } from "os";
13
13
  import { parse as parse2 } from "yaml";
14
14
 
15
15
  // src/events.ts
@@ -157,6 +157,320 @@ 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
+
163
+ // src/stream.ts
164
+ function toolTarget(name, input) {
165
+ if (!input) return void 0;
166
+ for (const key of ["file_path", "path", "notebook_path", "url", "pattern"]) {
167
+ const v = input[key];
168
+ if (typeof v === "string" && v) return v;
169
+ }
170
+ if (name === "Bash" && typeof input.command === "string") {
171
+ return input.command.trim().split(/\s+/)[0];
172
+ }
173
+ return void 0;
174
+ }
175
+ function mapStreamLine(line) {
176
+ const obj = line;
177
+ if (!obj || obj.type !== "assistant") return [];
178
+ const content = obj.message?.content;
179
+ if (!Array.isArray(content)) return [];
180
+ const out = [];
181
+ for (const block of content) {
182
+ if (block.type === "thinking") out.push({ kind: "thought", text: block.thinking });
183
+ else if (block.type === "text") out.push({ kind: "text", text: block.text });
184
+ else if (block.type === "tool_use") {
185
+ out.push({ kind: "tool", tool: block.name, target: toolTarget(block.name, block.input) });
186
+ }
187
+ }
188
+ return out;
189
+ }
190
+ function extractTurnTokens(line) {
191
+ const obj = line;
192
+ if (!obj || obj.type !== "assistant") return void 0;
193
+ const usage2 = obj.message?.usage;
194
+ if (!usage2) return void 0;
195
+ return (usage2.input_tokens ?? 0) + (usage2.output_tokens ?? 0) + (usage2.cache_read_input_tokens ?? 0) + (usage2.cache_creation_input_tokens ?? 0);
196
+ }
197
+ function extractResult(line) {
198
+ const obj = line;
199
+ if (!obj || obj.type !== "result") return void 0;
200
+ return {
201
+ sessionId: obj.session_id,
202
+ numTurns: obj.num_turns,
203
+ totalCostUsd: obj.total_cost_usd,
204
+ inputTokens: obj.usage?.input_tokens,
205
+ outputTokens: obj.usage?.output_tokens,
206
+ cacheReadTokens: obj.usage?.cache_read_input_tokens,
207
+ cacheCreationTokens: obj.usage?.cache_creation_input_tokens
208
+ };
209
+ }
210
+
211
+ // src/claude-mcp.ts
212
+ import { spawnSync } from "child_process";
213
+ import { readFileSync as readFileSync2 } from "fs";
214
+ import { homedir } from "os";
215
+ import { join as join2 } from "path";
216
+ var MCP_SERVER_NAME = "tablation";
217
+ var redact = (s, key) => key ? s.split(key).join("sk_***") : s;
218
+ function mcpUrlFor(baseUrl) {
219
+ return `${baseUrl.replace(/\/+$/, "")}/api/mcp`;
220
+ }
221
+ function runClaude(bin, args) {
222
+ const r = spawnSync(bin, args, { encoding: "utf8", timeout: 3e4 });
223
+ const missing = r.error?.code === "ENOENT";
224
+ return { status: r.status, missing, stderr: r.stderr ?? "" };
225
+ }
226
+ function userScopeEntry(home) {
227
+ try {
228
+ const j = JSON.parse(readFileSync2(join2(home, ".claude.json"), "utf8"));
229
+ return j.mcpServers?.[MCP_SERVER_NAME];
230
+ } catch {
231
+ return void 0;
232
+ }
233
+ }
234
+ function inspectClaudeMcp(o) {
235
+ const got = runClaude(o.bin, ["mcp", "get", MCP_SERVER_NAME]);
236
+ if (got.missing) return { state: "no-claude" };
237
+ if (got.status !== 0) return { state: "missing" };
238
+ const entry = userScopeEntry(o.home ?? homedir());
239
+ if (!entry) {
240
+ return { state: "different", detail: `a "${MCP_SERVER_NAME}" server exists, but not in user scope (~/.claude.json) \u2014 a project or local registration` };
241
+ }
242
+ if (entry.url !== o.url) return { state: "different", detail: `it points at ${entry.url ?? "(no url)"}, not ${o.url}` };
243
+ const auth = Object.entries(entry.headers ?? {}).find(([k]) => k.toLowerCase() === "authorization")?.[1];
244
+ if (auth !== `Bearer ${o.key}`) return { state: "different", detail: `it is registered with a different key than this route's` };
245
+ return { state: "registered", url: o.url };
246
+ }
247
+ function ensureClaudeMcp(o) {
248
+ const manual = `claude mcp add --transport http ${MCP_SERVER_NAME} ${o.url} --header "Authorization: Bearer <key>" --scope user`;
249
+ const st = inspectClaudeMcp(o);
250
+ if (st.state === "no-claude") return `mcp: \`${o.bin}\` not found, so Claude Code was not configured. To do it by hand: ${manual}`;
251
+ if (st.state === "registered") return `mcp: ${MCP_SERVER_NAME} already registered with Claude Code \u2192 ${st.url}`;
252
+ if (st.state === "different" && !o.replace) {
253
+ return `mcp: left Claude Code's "${MCP_SERVER_NAME}" server alone \u2014 ${st.detail}. Re-run with --mcp replace to overwrite it.`;
254
+ }
255
+ if (o.dryRun) return `(dry run) would register the ${MCP_SERVER_NAME} MCP server with Claude Code \u2192 ${o.url}`;
256
+ if (st.state === "different") {
257
+ runClaude(o.bin, ["mcp", "remove", MCP_SERVER_NAME, "--scope", "user"]);
258
+ }
259
+ const add = runClaude(o.bin, [
260
+ "mcp",
261
+ "add",
262
+ "--transport",
263
+ "http",
264
+ MCP_SERVER_NAME,
265
+ o.url,
266
+ "--header",
267
+ `Authorization: Bearer ${o.key}`,
268
+ "--scope",
269
+ "user"
270
+ ]);
271
+ if (add.status !== 0) {
272
+ return `mcp: \`claude mcp add\` failed (${redact(add.stderr.trim(), o.key) || `exit ${add.status}`}). To do it by hand: ${manual}`;
273
+ }
274
+ return `mcp: registered the ${MCP_SERVER_NAME} MCP server with Claude Code \u2192 ${o.url} (user scope, ~/.claude.json)`;
275
+ }
276
+
277
+ // src/capability.ts
278
+ function profileFor(role) {
279
+ if (role === "triage") return { level: "read", designTools: false };
280
+ if (role === "qa") return { level: "write", designTools: false };
281
+ return { level: "edit", designTools: role === "design" };
282
+ }
283
+
284
+ // src/adapters/claude.ts
285
+ var BILLING_VARS_TO_UNSET = [
286
+ "ANTHROPIC_API_KEY",
287
+ "ANTHROPIC_AUTH_TOKEN",
288
+ "CLAUDE_CODE_USE_VERTEX",
289
+ "ANTHROPIC_VERTEX_PROJECT_ID",
290
+ "ANTHROPIC_VERTEX_REGION",
291
+ "CLOUD_ML_REGION",
292
+ "ANTHROPIC_MODEL"
293
+ ];
294
+ var DISALLOWED_TOOLS = [
295
+ "Bash(rm -rf:*)",
296
+ "Bash(rm -fr:*)",
297
+ "Bash(git push --force*)",
298
+ "Bash(git push -f*)",
299
+ "Bash(curl*| sh)",
300
+ "Bash(curl*|sh)",
301
+ "Bash(curl*| bash)",
302
+ "Bash(curl*|bash)",
303
+ "Bash(sudo:*)"
304
+ ];
305
+ function claudeToolsFor(p) {
306
+ const tablation = "mcp__tablation__*";
307
+ if (p.level === "read") return ["Bash", "Read", tablation];
308
+ if (p.level === "write") return ["Bash", "Read", "Write", "Grep", "Glob", tablation];
309
+ const base = ["Bash", "Read", "Edit", "Write", "Grep", "Glob", tablation];
310
+ return p.designTools ? [...base, "Skill", "Artifact"] : base;
311
+ }
312
+ function allowedTools(role) {
313
+ return claudeToolsFor(profileFor(role));
314
+ }
315
+ var claudeAdapter = {
316
+ harness: "claude",
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
+ },
326
+ defaultBin: "claude",
327
+ defaultModel: "claude-sonnet-5",
328
+ defaultTiers: {
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" },
332
+ standard: { model: "claude-sonnet-5-5" },
333
+ deep: { model: "claude-opus-5-5", reasoning: "16384" }
334
+ },
335
+ limitations: [
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",
337
+ "the write profile (QA) withholds the Edit tool, not file writes \u2014 Bash can still write files",
338
+ "git push is not blocked, only forced pushes"
339
+ ],
340
+ invocation(spec, o) {
341
+ const args = [
342
+ "-p",
343
+ "--allowedTools",
344
+ ...allowedTools(o.role),
345
+ "--disallowedTools",
346
+ ...DISALLOWED_TOOLS,
347
+ "--model",
348
+ o.model,
349
+ "--output-format",
350
+ "stream-json",
351
+ "--verbose",
352
+ // Registers the tracker's own MCP tool surface for this run only —
353
+ // `${CREW_API_KEY}` is interpolated by the CLI from the environment,
354
+ // so the resolved key is never written to an argv string or a config
355
+ // file on disk. Skipped when there's no key to send: a route `crew
356
+ // connect` hasn't resolved yet has nothing to authenticate with.
357
+ ...o.mcp ? ["--mcp-config", JSON.stringify({
358
+ mcpServers: {
359
+ tablation: {
360
+ type: "http",
361
+ url: o.mcp.url,
362
+ headers: { Authorization: `Bearer \${${o.mcp.keyVar}}` }
363
+ }
364
+ }
365
+ })] : [],
366
+ ...spec.args
367
+ ];
368
+ return spec.launcher ? { bin: spec.launcher, args: [spec.bin, ...args] } : { bin: spec.bin, args };
369
+ },
370
+ runEnv(spec) {
371
+ return {
372
+ // Extended thinking is what makes the CLI emit `thinking` stream
373
+ // blocks at all — without it, Agent Log Cycles reporting has nothing
374
+ // to report (ISSUE-558).
375
+ ...spec.maxThinkingTokens > 0 ? { MAX_THINKING_TOKENS: String(spec.maxThinkingTokens) } : {},
376
+ ...spec.env
377
+ };
378
+ },
379
+ unsetEnv: BILLING_VARS_TO_UNSET,
380
+ mapStreamLine,
381
+ extractResult,
382
+ extractTurnTokens,
383
+ inspectMcp(o) {
384
+ const s = inspectClaudeMcp(o);
385
+ return s.state === "no-claude" ? { state: "no-binary" } : s;
386
+ },
387
+ ensureMcp: ensureClaudeMcp
388
+ };
389
+
390
+ // src/toolchain.ts
391
+ var TIERS = ["light", "standard", "deep"];
392
+ var ADAPTERS = {
393
+ [claudeAdapter.harness]: claudeAdapter
394
+ };
395
+ var HARNESS_NAMES = Object.keys(ADAPTERS);
396
+ function isHarness(name) {
397
+ return Object.hasOwn(ADAPTERS, name);
398
+ }
399
+ function adapterFor(harness) {
400
+ const a = Object.hasOwn(ADAPTERS, harness) ? ADAPTERS[harness] : void 0;
401
+ if (!a) throw new Error(`unknown harness "${harness}" (have: ${HARNESS_NAMES.join(", ")})`);
402
+ return a;
403
+ }
404
+ var LEGACY_TOOLCHAIN = "claude";
405
+ function shipToolchains(ship) {
406
+ const explicit = ship.toolchains ?? [];
407
+ if (explicit.some((t) => t.name === LEGACY_TOOLCHAIN)) return explicit;
408
+ const a = claudeAdapter;
409
+ const legacy = {
410
+ name: LEGACY_TOOLCHAIN,
411
+ harness: a.harness,
412
+ bin: ship.agent.bin,
413
+ model: ship.agent.model,
414
+ tiers: a.defaultTiers,
415
+ args: [],
416
+ env: {},
417
+ maxThinkingTokens: ship.agent.maxThinkingTokens
418
+ };
419
+ return [legacy, ...explicit];
420
+ }
421
+ function resolveToolchain(ship, route) {
422
+ const name = route?.toolchain ?? ship.toolchain ?? LEGACY_TOOLCHAIN;
423
+ const found = shipToolchains(ship).find((t) => t.name === name);
424
+ if (!found) throw new Error(`toolchain "${name}" is not defined (have: ${shipToolchains(ship).map((t) => t.name).join(", ")})`);
425
+ return found;
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
+ }
473
+
160
474
  // src/config.ts
161
475
  import { getSessionStore, hostFromUrl } from "@tablation/client";
162
476
 
@@ -197,8 +511,8 @@ function explain(ship, required) {
197
511
  }
198
512
 
199
513
  // src/repo-config.ts
200
- import { readFileSync as readFileSync2, existsSync as existsSync2 } from "fs";
201
- import { join as join2, basename } from "path";
514
+ import { readFileSync as readFileSync3, existsSync as existsSync2 } from "fs";
515
+ import { join as join3, basename as basename2 } from "path";
202
516
  import { parse } from "yaml";
203
517
  var SPEC_VERSION = 1;
204
518
  var REPO_CONFIG_FILENAMES = [".crew.yaml", ".crew.yml", ".crew.json"];
@@ -271,7 +585,7 @@ var MODES = ["local", "integrate", "ci_manual", "ci_auto", "external"];
271
585
  var PROVIDERS = ["github", "buildkite", "other", "none"];
272
586
  function findRepoConfig(dir) {
273
587
  for (const name of REPO_CONFIG_FILENAMES) {
274
- const p = join2(dir, name);
588
+ const p = join3(dir, name);
275
589
  if (existsSync2(p)) return p;
276
590
  }
277
591
  return null;
@@ -502,7 +816,7 @@ function parseRepoConfig(text, file) {
502
816
  function loadRepoConfig(dir) {
503
817
  const file = findRepoConfig(dir);
504
818
  if (!file) return null;
505
- return parseRepoConfig(readFileSync2(file, "utf8"), file);
819
+ return parseRepoConfig(readFileSync3(file, "utf8"), file);
506
820
  }
507
821
  function hookLabel(cfg, hook2) {
508
822
  return cfg.labels[hook2] ?? cfg.hooks[hook2] ?? hook2;
@@ -537,7 +851,7 @@ function renderWorktreeName(template, ctx) {
537
851
  return renderBranchName(template.replace(/\{dir\}/g, ctx.dir).replace(/\{issue\}/g, issue), ctx);
538
852
  }
539
853
  function defaultWorktreePrefix(dir) {
540
- const name = basename(dir.replace(/[/\\]+$/, ""));
854
+ const name = basename2(dir.replace(/[/\\]+$/, ""));
541
855
  return name ? `${name}-issue-` : "issue-";
542
856
  }
543
857
  var DEFAULTS = {
@@ -711,7 +1025,7 @@ function effectiveBranchTemplate(cfg, prefix) {
711
1025
  function effectiveWorktreeDirName(cfg, dir, branchName, number, renderName) {
712
1026
  if (cfg.worktrees.name && renderName) return renderName(cfg.worktrees.name);
713
1027
  if (cfg.provenance["worktrees.prefix"] !== "default") return `${cfg.worktrees.prefix}${number}`;
714
- return `${basename(dir.replace(/[/\\]+$/, ""))}-${branchName}`;
1028
+ return `${basename2(dir.replace(/[/\\]+$/, ""))}-${branchName}`;
715
1029
  }
716
1030
  function validateEffective(cfg) {
717
1031
  const problems = [];
@@ -767,6 +1081,8 @@ var SHIP_KEYS = /* @__PURE__ */ new Set([
767
1081
  "name",
768
1082
  "platform",
769
1083
  "agent",
1084
+ "toolchains",
1085
+ "toolchain",
770
1086
  "shell",
771
1087
  "extraPath",
772
1088
  "useNvm",
@@ -806,7 +1122,8 @@ var ROUTE_KEYS = /* @__PURE__ */ new Set([
806
1122
  "contract",
807
1123
  "resolved",
808
1124
  "promptSet",
809
- "hostPassengers"
1125
+ "hostPassengers",
1126
+ "toolchain"
810
1127
  ]);
811
1128
  function parseWorktreeName(v, where, file) {
812
1129
  const problem = typeof v === "string" ? worktreeNameProblem(v) : "must be a string";
@@ -910,16 +1227,16 @@ function mergeRouteRelease(route, override) {
910
1227
  };
911
1228
  }
912
1229
  function expand(p, base) {
913
- let out = p.startsWith("~") ? p.replace(/^~/, homedir()) : p;
914
- out = out.replace(/\$\{?HOME\}?/g, homedir());
1230
+ let out = p.startsWith("~") ? p.replace(/^~/, homedir2()) : p;
1231
+ out = out.replace(/\$\{?HOME\}?/g, homedir2());
915
1232
  return isAbsolute(out) ? out : resolve(base, out);
916
1233
  }
917
1234
  function resolvePromptsDir(promptSet, crewHome, base) {
918
- if (!promptSet) return join3(crewHome, "prompts", "default");
1235
+ if (!promptSet) return join4(crewHome, "prompts", "default");
919
1236
  if (promptSet.includes("/") || promptSet.startsWith(".") || promptSet.startsWith("~")) {
920
1237
  return expand(promptSet, base);
921
1238
  }
922
- return join3(crewHome, "prompts", promptSet);
1239
+ return join4(crewHome, "prompts", promptSet);
923
1240
  }
924
1241
  var Missing = class {
925
1242
  paths = [];
@@ -943,7 +1260,7 @@ var Missing = class {
943
1260
  };
944
1261
  function crewVersion(crewHome) {
945
1262
  try {
946
- const pkg = JSON.parse(readFileSync3(join3(crewHome, "package.json"), "utf8"));
1263
+ const pkg = JSON.parse(readFileSync4(join4(crewHome, "package.json"), "utf8"));
947
1264
  return pkg.version ?? "0.0.0";
948
1265
  } catch {
949
1266
  return "0.0.0";
@@ -953,8 +1270,8 @@ function configSearchPath(crewHome) {
953
1270
  if (process.env.CREW_CONFIG) return [resolve(process.env.CREW_CONFIG)];
954
1271
  const xdg = process.env.XDG_CONFIG_HOME;
955
1272
  return [
956
- ...xdg ? [join3(xdg, "crew", "crew.yaml")] : [],
957
- join3(homedir(), ".config", "crew", "crew.yaml"),
1273
+ ...xdg ? [join4(xdg, "crew", "crew.yaml")] : [],
1274
+ join4(homedir2(), ".config", "crew", "crew.yaml"),
958
1275
  resolve(crewHome, "crew.yaml")
959
1276
  ];
960
1277
  }
@@ -962,17 +1279,17 @@ function findConfigFile(crewHome) {
962
1279
  const candidates = configSearchPath(crewHome);
963
1280
  return candidates.find((c) => existsSync3(c)) ?? candidates[candidates.length - 1];
964
1281
  }
965
- var defaultConfigPath = () => join3(process.env.XDG_CONFIG_HOME ?? join3(homedir(), ".config"), "crew", "crew.yaml");
1282
+ var defaultConfigPath = () => join4(process.env.XDG_CONFIG_HOME ?? join4(homedir2(), ".config"), "crew", "crew.yaml");
966
1283
  function routeSlug(route) {
967
1284
  return route.replace(/\//g, "-");
968
1285
  }
969
1286
  function resolvedPathFor(stateDir, route) {
970
1287
  const [workspace, project] = splitRoute(route);
971
- return join3(stateDir, "resolved", workspace, `${project}.json`);
1288
+ return join4(stateDir, "resolved", workspace, `${project}.json`);
972
1289
  }
973
1290
  function apiKeyPathFor(stateDir, route) {
974
1291
  const [workspace, project] = splitRoute(route);
975
- return join3(stateDir, "keys", workspace, `${project}.env`);
1292
+ return join4(stateDir, "keys", workspace, `${project}.env`);
976
1293
  }
977
1294
  function splitRoute(route) {
978
1295
  const parts = route.split("/");
@@ -985,7 +1302,7 @@ function readResolvedFile(stateDir, route) {
985
1302
  const path = resolvedPathFor(stateDir, route);
986
1303
  if (!existsSync3(path)) return void 0;
987
1304
  try {
988
- return JSON.parse(readFileSync3(path, "utf8"));
1305
+ return JSON.parse(readFileSync4(path, "utf8"));
989
1306
  } catch (e) {
990
1307
  throw new ConfigError(`${path}: not valid JSON \u2014 ${e.message}`);
991
1308
  }
@@ -1047,12 +1364,12 @@ function loadConfig(crewHome, configFile, opts = {}) {
1047
1364
  ` + configSearchPath(crewHome).map((c) => ` - ${c}`).join("\n") + `
1048
1365
 
1049
1366
  Create the first of those:
1050
- mkdir -p ${dirname2(defaultConfigPath())} && cp ${join3(crewHome, "crew.example.yaml")} ${defaultConfigPath()}`
1367
+ mkdir -p ${dirname2(defaultConfigPath())} && cp ${join4(crewHome, "crew.example.yaml")} ${defaultConfigPath()}`
1051
1368
  );
1052
1369
  }
1053
1370
  let raw;
1054
1371
  try {
1055
- raw = parse2(opts.text ?? readFileSync3(file, "utf8"));
1372
+ raw = parse2(opts.text ?? readFileSync4(file, "utf8"));
1056
1373
  } catch (e) {
1057
1374
  throw new ConfigError(`${file}: not valid YAML \u2014 ${e.message}`);
1058
1375
  }
@@ -1082,6 +1399,12 @@ Create the first of those:
1082
1399
  if (!Number.isInteger(maxThinkingTokens) || maxThinkingTokens < 0) {
1083
1400
  missing.add(`ship.agent.maxThinkingTokens must be a non-negative integer (got "${shipRaw.agent?.maxThinkingTokens}")`);
1084
1401
  }
1402
+ const toolchains = parseToolchains(shipRaw.toolchains, base, missing, file);
1403
+ const toolchainNames = /* @__PURE__ */ new Set([LEGACY_TOOLCHAIN, ...toolchains.map((x) => x.name)]);
1404
+ const shipToolchain = shipRaw.toolchain === void 0 ? void 0 : String(shipRaw.toolchain);
1405
+ if (shipToolchain !== void 0 && !toolchainNames.has(shipToolchain)) {
1406
+ missing.add(`ship.toolchain "${shipToolchain}" is not defined (have: ${[...toolchainNames].join(", ")})`);
1407
+ }
1085
1408
  const stateDir = expand(shipRaw.stateDir ?? "state", base);
1086
1409
  const routesRaw = raw.routes;
1087
1410
  if (!opts.allowNoRoutes && (!Array.isArray(routesRaw) || routesRaw.length === 0)) {
@@ -1092,7 +1415,7 @@ Create the first of those:
1092
1415
  const where = `routes[${i}]`;
1093
1416
  const routeMissing = new Missing();
1094
1417
  try {
1095
- return [parseOneRoute(c, where, file, base, stateDir, raw, routeMissing, crewHome)];
1418
+ return [parseOneRoute(c, where, file, base, stateDir, raw, routeMissing, crewHome, toolchainNames)];
1096
1419
  } catch (e) {
1097
1420
  const label = typeof c?.route === "string" && c.route ? `"${c.route}"` : where;
1098
1421
  routeWarnings.push(`route ${label} dropped \u2014 ${e.message}`);
@@ -1115,12 +1438,14 @@ Create the first of those:
1115
1438
  model: shipRaw.agent?.model ?? "claude-sonnet-5",
1116
1439
  maxThinkingTokens
1117
1440
  },
1441
+ ...toolchains.length > 0 ? { toolchains } : {},
1442
+ ...shipToolchain !== void 0 ? { toolchain: shipToolchain } : {},
1118
1443
  shell: shipRaw.shell,
1119
1444
  extraPath: shipRaw.extraPath,
1120
1445
  useNvm: shipRaw.useNvm !== false,
1121
1446
  nvmSh: shipRaw.nvmSh,
1122
1447
  stateDir,
1123
- logFile: expand(shipRaw.logFile ?? join3(tmpdir(), "crew.log"), base),
1448
+ logFile: expand(shipRaw.logFile ?? join4(tmpdir(), "crew.log"), base),
1124
1449
  userAgent: shipRaw.userAgent ?? `Mozilla/5.0 CrewAgent/${crewVersion(crewHome)}`,
1125
1450
  maxConcurrentAgents,
1126
1451
  streamRetentionDays,
@@ -1138,8 +1463,70 @@ Create the first of those:
1138
1463
  warnings: routeWarnings
1139
1464
  };
1140
1465
  }
1141
- function parseOneRoute(c, where, file, base, stateDir, raw, missing, crewHome) {
1466
+ function expandCommand(cmd, base) {
1467
+ return cmd.includes("/") || cmd.startsWith("~") ? expand(cmd, base) : cmd;
1468
+ }
1469
+ var TOOLCHAIN_KEYS = /* @__PURE__ */ new Set(["harness", "bin", "launcher", "model", "tiers", "args", "env", "maxThinkingTokens"]);
1470
+ function parseToolchains(raw, base, missing, file) {
1471
+ if (raw === void 0 || raw === null) return [];
1472
+ if (typeof raw !== "object" || Array.isArray(raw)) {
1473
+ missing.add("ship.toolchains must be a mapping of name \u2192 toolchain");
1474
+ return [];
1475
+ }
1476
+ const out = [];
1477
+ for (const [name, entryRaw] of Object.entries(raw)) {
1478
+ const where = `ship.toolchains.${name}`;
1479
+ if (!entryRaw || typeof entryRaw !== "object" || Array.isArray(entryRaw)) {
1480
+ missing.add(`${where} must be a mapping`);
1481
+ continue;
1482
+ }
1483
+ rejectUnknownKeys(entryRaw, TOOLCHAIN_KEYS, where, file);
1484
+ const harness = String(entryRaw.harness ?? "");
1485
+ if (!isHarness(harness)) {
1486
+ missing.add(`${where}.harness must be one of ${HARNESS_NAMES.join("|")} (got "${harness}")`);
1487
+ continue;
1488
+ }
1489
+ const adapter = adapterFor(harness);
1490
+ const tiers = { ...adapter.defaultTiers };
1491
+ for (const [tier, v] of Object.entries(entryRaw.tiers ?? {})) {
1492
+ if (!TIERS.includes(tier)) {
1493
+ missing.add(`${where}.tiers.${tier} is not a tier (have: ${TIERS.join(", ")})`);
1494
+ continue;
1495
+ }
1496
+ if (!v || typeof v.model !== "string" || !v.model) {
1497
+ missing.add(`${where}.tiers.${tier}.model is required`);
1498
+ continue;
1499
+ }
1500
+ tiers[tier] = { model: v.model, ...v.reasoning !== void 0 ? { reasoning: String(v.reasoning) } : {} };
1501
+ }
1502
+ const args = entryRaw.args ?? [];
1503
+ if (!Array.isArray(args)) missing.add(`${where}.args must be a list of strings`);
1504
+ const env = entryRaw.env ?? {};
1505
+ if (typeof env !== "object" || Array.isArray(env)) missing.add(`${where}.env must be a mapping of name \u2192 value`);
1506
+ const maxThinkingTokens = entryRaw.maxThinkingTokens === void 0 ? DEFAULT_MAX_THINKING_TOKENS : Number(entryRaw.maxThinkingTokens);
1507
+ if (!Number.isInteger(maxThinkingTokens) || maxThinkingTokens < 0) {
1508
+ missing.add(`${where}.maxThinkingTokens must be a non-negative integer (got "${entryRaw.maxThinkingTokens}")`);
1509
+ }
1510
+ out.push({
1511
+ name,
1512
+ harness,
1513
+ bin: expandCommand(String(entryRaw.bin ?? adapter.defaultBin), base),
1514
+ ...entryRaw.launcher ? { launcher: expandCommand(String(entryRaw.launcher), base) } : {},
1515
+ model: String(entryRaw.model ?? tiers.standard?.model ?? adapter.defaultModel),
1516
+ tiers,
1517
+ args: Array.isArray(args) ? args.map(String) : [],
1518
+ env: Object.fromEntries(Object.entries(env).map(([k, v]) => [k, String(v)])),
1519
+ maxThinkingTokens
1520
+ });
1521
+ }
1522
+ return out;
1523
+ }
1524
+ function parseOneRoute(c, where, file, base, stateDir, raw, missing, crewHome, toolchainNames) {
1142
1525
  rejectUnknownKeys(c, ROUTE_KEYS, `${where}`, file);
1526
+ const toolchain = c?.toolchain === void 0 ? void 0 : String(c.toolchain);
1527
+ if (toolchain !== void 0 && !toolchainNames.has(toolchain)) {
1528
+ missing.add(`${where}.toolchain "${toolchain}" is not defined in ship.toolchains (have: ${[...toolchainNames].join(", ")})`);
1529
+ }
1143
1530
  const route = missing.req(c?.route, `${where}.route`);
1144
1531
  const routeParts = route ? route.split("/") : [];
1145
1532
  const routeWellFormed = routeParts.length === 2 && routeParts.every(Boolean);
@@ -1188,6 +1575,7 @@ function parseOneRoute(c, where, file, base, stateDir, raw, missing, crewHome) {
1188
1575
  reposBasePath: expand(String(c?.reposBasePath ?? raw.ship?.reposBasePath ?? DEFAULT_REPOS_BASE_PATH), base),
1189
1576
  repoOverrides,
1190
1577
  worktreePrefix: c?.worktreePrefix ? String(c.worktreePrefix) : void 0,
1578
+ ...toolchain !== void 0 ? { toolchain } : {},
1191
1579
  promptsDir: resolvePromptsDir(c?.promptSet ? String(c.promptSet) : void 0, crewHome, base),
1192
1580
  weight,
1193
1581
  baseUrl: String(c?.baseUrl ?? raw.ship?.baseUrl ?? DEFAULT_BASE_URL).replace(/\/+$/, ""),
@@ -1242,7 +1630,7 @@ function dirForRepo(r, repoId) {
1242
1630
  }
1243
1631
  function repoTargetFor(r, repoId) {
1244
1632
  const names = r.resolved?.repoNames;
1245
- 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 };
1246
1634
  const name = names[repoId];
1247
1635
  if (!name) return null;
1248
1636
  return reposOf(r).find((t) => t.name === name) ?? null;
@@ -1256,7 +1644,7 @@ function repoIdForName(r, name) {
1256
1644
  function resolveApiKey(r) {
1257
1645
  if (r.apiKey) return r.apiKey;
1258
1646
  if (r.apiKeyFile && r.apiKeyVar && existsSync3(r.apiKeyFile)) {
1259
- for (const line of readFileSync3(r.apiKeyFile, "utf8").split("\n")) {
1647
+ for (const line of readFileSync4(r.apiKeyFile, "utf8").split("\n")) {
1260
1648
  const m = line.match(new RegExp(`^${r.apiKeyVar}=(.*)$`));
1261
1649
  if (m) return (m[1] ?? "").trim().replace(/^["']|["']$/g, "");
1262
1650
  }
@@ -1319,13 +1707,13 @@ function configuredMembers(rt) {
1319
1707
  return out;
1320
1708
  }
1321
1709
  function defaultRepoDir(basePath, workspace, repoName) {
1322
- return join3(basePath, workspace, repoName);
1710
+ return join4(basePath, workspace, repoName);
1323
1711
  }
1324
1712
  function reposOf(r) {
1325
1713
  const named = Object.entries(r.repos);
1326
1714
  if (named.length > 0) return named.map(([name, dir]) => ({ name, dir }));
1327
1715
  const trackerNames = r.resolved?.repoNames ? Object.values(r.resolved.repoNames) : [];
1328
- 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 }];
1329
1717
  const remotes = r.resolved?.repoRemotes ?? {};
1330
1718
  const remoteByName = {};
1331
1719
  for (const [id, name] of Object.entries(r.resolved.repoNames)) {
@@ -1343,7 +1731,7 @@ function passengerRepoTargets(r) {
1343
1731
  return reposOf(r).filter((t) => r.repoOverrides[t.name]?.hostPassengers !== false);
1344
1732
  }
1345
1733
  function passengerCheckoutDir(basePath, workspace, repoName) {
1346
- return join3(basePath, workspace, ".passenger-checkouts", repoName);
1734
+ return join4(basePath, workspace, ".passenger-checkouts", repoName);
1347
1735
  }
1348
1736
  function passengerCheckoutTargets(r) {
1349
1737
  if (!r.hostPassengers) return [];
@@ -1475,8 +1863,8 @@ function rosterMarkdown(roster, meId) {
1475
1863
  }
1476
1864
 
1477
1865
  // src/state.ts
1478
- import { readFileSync as readFileSync4, writeFileSync, existsSync as existsSync4, mkdirSync as mkdirSync2, rmSync as rmSync2, statSync as statSync2 } from "fs";
1479
- import { join as join4 } from "path";
1866
+ import { readFileSync as readFileSync5, writeFileSync, existsSync as existsSync4, mkdirSync as mkdirSync2, rmSync as rmSync2, statSync as statSync2 } from "fs";
1867
+ import { join as join5 } from "path";
1480
1868
  var EPOCH = "1970-01-01T00:00:00.000Z";
1481
1869
  var State = class _State {
1482
1870
  dir;
@@ -1485,7 +1873,7 @@ var State = class _State {
1485
1873
  mkdirSync2(dir, { recursive: true });
1486
1874
  }
1487
1875
  path(...parts) {
1488
- return join4(this.dir, ...parts);
1876
+ return join5(this.dir, ...parts);
1489
1877
  }
1490
1878
  /**
1491
1879
  * The timestamp every "new since last poll" check is measured against.
@@ -1496,7 +1884,7 @@ var State = class _State {
1496
1884
  */
1497
1885
  watermark() {
1498
1886
  try {
1499
- const v = readFileSync4(this.path(".poll-watermark"), "utf8").trim();
1887
+ const v = readFileSync5(this.path(".poll-watermark"), "utf8").trim();
1500
1888
  return v || EPOCH;
1501
1889
  } catch {
1502
1890
  return EPOCH;
@@ -1582,7 +1970,7 @@ var State = class _State {
1582
1970
  const name = _State.safe(rawName);
1583
1971
  const file = this.path(`.${name}.lock`);
1584
1972
  try {
1585
- const existing = Number.parseInt(readFileSync4(file, "utf8").trim(), 10);
1973
+ const existing = Number.parseInt(readFileSync5(file, "utf8").trim(), 10);
1586
1974
  if (Number.isFinite(existing) && existing > 0) {
1587
1975
  try {
1588
1976
  process.kill(existing, 0);
@@ -1676,7 +2064,7 @@ var State = class _State {
1676
2064
  const heldReported = `.release-held-${suffix}`;
1677
2065
  const read = (f) => {
1678
2066
  try {
1679
- return readFileSync4(this.path(f), "utf8").trim() || null;
2067
+ return readFileSync5(this.path(f), "utf8").trim() || null;
1680
2068
  } catch {
1681
2069
  return null;
1682
2070
  }
@@ -1726,7 +2114,7 @@ var State = class _State {
1726
2114
  /** Whether the board was already told about this head's red gate. */
1727
2115
  heldReported: () => {
1728
2116
  try {
1729
- return readFileSync4(this.path(heldReported), "utf8").replace(/\n$/, "") || null;
2117
+ return readFileSync5(this.path(heldReported), "utf8").replace(/\n$/, "") || null;
1730
2118
  } catch {
1731
2119
  return null;
1732
2120
  }
@@ -1763,7 +2151,7 @@ var State = class _State {
1763
2151
  const ticketStreakFile = `.fairness-ticket-streak-${suffix}`;
1764
2152
  const read = (f) => {
1765
2153
  try {
1766
- return readFileSync4(this.path(f), "utf8").trim() || null;
2154
+ return readFileSync5(this.path(f), "utf8").trim() || null;
1767
2155
  } catch {
1768
2156
  return null;
1769
2157
  }
@@ -1828,7 +2216,7 @@ var State = class _State {
1828
2216
  return {
1829
2217
  previous: () => {
1830
2218
  try {
1831
- const parsed = JSON.parse(readFileSync4(this.path(file), "utf8"));
2219
+ const parsed = JSON.parse(readFileSync5(this.path(file), "utf8"));
1832
2220
  return parsed && typeof parsed === "object" ? parsed : {};
1833
2221
  } catch {
1834
2222
  return {};
@@ -1849,7 +2237,7 @@ var State = class _State {
1849
2237
  return {
1850
2238
  previous: () => {
1851
2239
  try {
1852
- const parsed = JSON.parse(readFileSync4(this.path(file), "utf8"));
2240
+ const parsed = JSON.parse(readFileSync5(this.path(file), "utf8"));
1853
2241
  return Array.isArray(parsed) ? parsed : [];
1854
2242
  } catch {
1855
2243
  return [];
@@ -1882,7 +2270,7 @@ var State = class _State {
1882
2270
  }
1883
2271
  readInt(file) {
1884
2272
  try {
1885
- return Number.parseInt(readFileSync4(this.path(file), "utf8").trim(), 10) || 0;
2273
+ return Number.parseInt(readFileSync5(this.path(file), "utf8").trim(), 10) || 0;
1886
2274
  } catch {
1887
2275
  return 0;
1888
2276
  }
@@ -2186,10 +2574,10 @@ function describeRelease(o, route) {
2186
2574
  }
2187
2575
 
2188
2576
  // src/install.ts
2189
- import { dirname as dirname3, join as join5, extname } from "path";
2190
- import { homedir as homedir2 } from "os";
2191
- import { accessSync, constants, existsSync as existsSync5, mkdirSync as mkdirSync3, readdirSync, readFileSync as readFileSync5, statSync as statSync3, writeFileSync as writeFileSync2, unlinkSync, realpathSync } from "fs";
2192
- import { spawnSync } from "child_process";
2577
+ import { dirname as dirname3, join as join6, extname } from "path";
2578
+ import { homedir as homedir3 } from "os";
2579
+ import { accessSync, constants, existsSync as existsSync5, mkdirSync as mkdirSync3, readdirSync, readFileSync as readFileSync6, statSync as statSync3, writeFileSync as writeFileSync2, unlinkSync, realpathSync } from "fs";
2580
+ import { spawnSync as spawnSync2 } from "child_process";
2193
2581
  import { createHash } from "crypto";
2194
2582
 
2195
2583
  // src/runtime-info.ts
@@ -2200,7 +2588,7 @@ function isCompiledBinary(moduleUrl) {
2200
2588
  // src/install.ts
2201
2589
  function crewInvocation(crewHome) {
2202
2590
  if (isCompiledBinary(import.meta.url)) return [process.execPath];
2203
- return [process.execPath, join5(crewHome, "bin", "crew")];
2591
+ return [process.execPath, join6(crewHome, "bin", "crew")];
2204
2592
  }
2205
2593
  var INTERVAL_SECONDS = 120;
2206
2594
  function labelFor(crewHome, job) {
@@ -2238,7 +2626,7 @@ function pathFor(ship) {
2238
2626
  return parts.join(":");
2239
2627
  }
2240
2628
  function findOnPath(bin, searchPath) {
2241
- const candidates = bin.includes("/") ? [bin] : searchPath.split(":").filter(Boolean).map((dir) => join5(dir, bin));
2629
+ const candidates = bin.includes("/") ? [bin] : searchPath.split(":").filter(Boolean).map((dir) => join6(dir, bin));
2242
2630
  for (const candidate of candidates) {
2243
2631
  try {
2244
2632
  accessSync(candidate, constants.X_OK);
@@ -2248,7 +2636,7 @@ function findOnPath(bin, searchPath) {
2248
2636
  }
2249
2637
  return void 0;
2250
2638
  }
2251
- var COMMON_DOCKER_DIRS = ["/usr/local/bin", "/opt/homebrew/bin", join5(homedir2(), ".docker", "bin")].join(":");
2639
+ var COMMON_DOCKER_DIRS = ["/usr/local/bin", "/opt/homebrew/bin", join6(homedir3(), ".docker", "bin")].join(":");
2252
2640
  function dockerPathProblem(ship, shellPath = process.env.PATH ?? "") {
2253
2641
  const schedulerPath = pathFor(ship);
2254
2642
  if (findOnPath("docker", schedulerPath)) return void 0;
@@ -2260,7 +2648,7 @@ function xmlEscape(s) {
2260
2648
  }
2261
2649
  function planLaunchd(ship, crewHome, job) {
2262
2650
  const label = labelFor(crewHome, job);
2263
- const unitPath = join5(homedir2(), "Library", "LaunchAgents", `${label}.plist`);
2651
+ const unitPath = join6(homedir3(), "Library", "LaunchAgents", `${label}.plist`);
2264
2652
  const crewLog = ship.logFile;
2265
2653
  const schedulerLog = schedulerLogFor(crewLog, job);
2266
2654
  const persistent = job === "run";
@@ -2312,8 +2700,8 @@ ${scheduleKeys}
2312
2700
  }
2313
2701
  function planSystemd(ship, crewHome, job) {
2314
2702
  const label = labelFor(crewHome, job);
2315
- const unitDir = join5(homedir2(), ".config", "systemd", "user");
2316
- const servicePath = join5(unitDir, `${label}.service`);
2703
+ const unitDir = join6(homedir3(), ".config", "systemd", "user");
2704
+ const servicePath = join6(unitDir, `${label}.service`);
2317
2705
  const crewLog = ship.logFile;
2318
2706
  const schedulerLog = schedulerLogFor(crewLog, job);
2319
2707
  const description = job === "release" ? "Tablation crew \u2014 release" : job === "passengers" ? "Tablation crew \u2014 Host Passengers sync" : "Tablation crew \u2014 persistent supervisor loop";
@@ -2350,7 +2738,7 @@ WantedBy=default.target
2350
2738
  schedulerLog
2351
2739
  };
2352
2740
  }
2353
- const timerPath = join5(unitDir, `${label}.timer`);
2741
+ const timerPath = join6(unitDir, `${label}.timer`);
2354
2742
  const service = `# Written by \`crew install\` \u2014 re-run it rather than hand-editing this file.
2355
2743
  [Unit]
2356
2744
  Description=${description}
@@ -2417,7 +2805,7 @@ function planInstall(ship, crewHome, hostShip, hasSystemd, job = "run") {
2417
2805
  function planUninstall(ship, crewHome, hostShip, hasSystemd, job = "run") {
2418
2806
  const label = labelFor(crewHome, job);
2419
2807
  if (hostShip === "macos") {
2420
- const unitPath = join5(homedir2(), "Library", "LaunchAgents", `${label}.plist`);
2808
+ const unitPath = join6(homedir3(), "Library", "LaunchAgents", `${label}.plist`);
2421
2809
  return {
2422
2810
  mechanism: "launchd",
2423
2811
  unitPaths: [unitPath],
@@ -2427,18 +2815,18 @@ function planUninstall(ship, crewHome, hostShip, hasSystemd, job = "run") {
2427
2815
  };
2428
2816
  }
2429
2817
  if (hostShip === "linux" && hasSystemd) {
2430
- const unitDir = join5(homedir2(), ".config", "systemd", "user");
2818
+ const unitDir = join6(homedir3(), ".config", "systemd", "user");
2431
2819
  if (job === "run") {
2432
2820
  return {
2433
2821
  mechanism: "systemd",
2434
- unitPaths: [join5(unitDir, `${label}.service`)],
2822
+ unitPaths: [join6(unitDir, `${label}.service`)],
2435
2823
  crontabLine: null,
2436
2824
  unloadCommands: [["systemctl", "--user", "disable", "--now", `${label}.service`]]
2437
2825
  };
2438
2826
  }
2439
2827
  return {
2440
2828
  mechanism: "systemd",
2441
- unitPaths: [join5(unitDir, `${label}.service`), join5(unitDir, `${label}.timer`)],
2829
+ unitPaths: [join6(unitDir, `${label}.service`), join6(unitDir, `${label}.timer`)],
2442
2830
  crontabLine: null,
2443
2831
  unloadCommands: [["systemctl", "--user", "disable", "--now", `${label}.timer`]]
2444
2832
  };
@@ -2456,7 +2844,7 @@ function planUninstall(ship, crewHome, hostShip, hasSystemd, job = "run") {
2456
2844
  function planDaemonControl(crewHome, hostShip, hasSystemd) {
2457
2845
  const label = labelFor(crewHome, "run");
2458
2846
  if (hostShip === "macos") {
2459
- const unitPath = join5(homedir2(), "Library", "LaunchAgents", `${label}.plist`);
2847
+ const unitPath = join6(homedir3(), "Library", "LaunchAgents", `${label}.plist`);
2460
2848
  return {
2461
2849
  mechanism: "launchd",
2462
2850
  startCommand: ["launchctl", "load", unitPath],
@@ -2480,7 +2868,7 @@ function planDaemonControl(crewHome, hostShip, hasSystemd) {
2480
2868
  }
2481
2869
  function runAll(commands, cwd, log) {
2482
2870
  for (const [bin, ...args] of commands) {
2483
- const res = spawnSync(bin, args, { cwd, encoding: "utf8" });
2871
+ const res = spawnSync2(bin, args, { cwd, encoding: "utf8" });
2484
2872
  if (res.error || res.status !== 0) {
2485
2873
  log.warn(`${bin} ${args.join(" ")} exited ${res.status ?? "error"}`, {
2486
2874
  output: `${res.stdout ?? ""}${res.stderr ?? ""}${res.error ? String(res.error) : ""}`
@@ -2545,7 +2933,7 @@ async function applyUninstall(plan, crewHome, dryRun, log) {
2545
2933
  if (dryRun) {
2546
2934
  log.emit(`would run: launchctl remove ${plan.removeLabel}`);
2547
2935
  } else {
2548
- if (spawnSync("launchctl", ["remove", plan.removeLabel], { encoding: "utf8" }).status === 0) {
2936
+ if (spawnSync2("launchctl", ["remove", plan.removeLabel], { encoding: "utf8" }).status === 0) {
2549
2937
  log.emit(`removed loaded job ${plan.removeLabel}`);
2550
2938
  }
2551
2939
  }
@@ -2570,7 +2958,7 @@ async function applyUninstall(plan, crewHome, dryRun, log) {
2570
2958
  }
2571
2959
  var CREW_LAUNCHD_PREFIX = "com.tablation.crew";
2572
2960
  function launchAgentsDir() {
2573
- return join5(homedir2(), "Library", "LaunchAgents");
2961
+ return join6(homedir3(), "Library", "LaunchAgents");
2574
2962
  }
2575
2963
  function xmlUnescape(s) {
2576
2964
  return s.replace(/&lt;/g, "<").replace(/&gt;/g, ">").replace(/&amp;/g, "&");
@@ -2582,17 +2970,17 @@ function programFromPlist(text) {
2582
2970
  return args.find((a) => a.endsWith("/bin/crew")) ?? args[0] ?? null;
2583
2971
  }
2584
2972
  function loadedCrewLabels() {
2585
- const list = spawnSync("launchctl", ["list"], { encoding: "utf8" });
2973
+ const list = spawnSync2("launchctl", ["list"], { encoding: "utf8" });
2586
2974
  if (list.status !== 0) return [];
2587
2975
  return [...parseLaunchctlList(list.stdout ?? "").keys()].filter((l) => l.startsWith(CREW_LAUNCHD_PREFIX));
2588
2976
  }
2589
2977
  function listLaunchdUnits(dir = launchAgentsDir(), loaded = []) {
2590
2978
  const files = existsSync5(dir) ? readdirSync(dir) : [];
2591
2979
  const units = files.filter((f) => f.startsWith(CREW_LAUNCHD_PREFIX) && f.endsWith(".plist")).sort().map((f) => {
2592
- const plistPath = join5(dir, f);
2980
+ const plistPath = join6(dir, f);
2593
2981
  let program = null;
2594
2982
  try {
2595
- program = programFromPlist(readFileSync5(plistPath, "utf8"));
2983
+ program = programFromPlist(readFileSync6(plistPath, "utf8"));
2596
2984
  } catch {
2597
2985
  }
2598
2986
  return {
@@ -2605,7 +2993,7 @@ function listLaunchdUnits(dir = launchAgentsDir(), loaded = []) {
2605
2993
  const seen = new Set(units.map((u) => u.label));
2606
2994
  for (const label of [...loaded].sort()) {
2607
2995
  if (!label.startsWith(CREW_LAUNCHD_PREFIX) || seen.has(label)) continue;
2608
- units.push({ label, plistPath: join5(dir, `${label}.plist`), program: null, programExists: false, orphan: true });
2996
+ units.push({ label, plistPath: join6(dir, `${label}.plist`), program: null, programExists: false, orphan: true });
2609
2997
  }
2610
2998
  return units;
2611
2999
  }
@@ -2618,8 +3006,8 @@ function removeLaunchdUnit(unit, dryRun, log) {
2618
3006
  log.emit(`would unload and remove ${unit.label}`);
2619
3007
  return;
2620
3008
  }
2621
- if (existsSync5(unit.plistPath)) spawnSync("launchctl", ["unload", unit.plistPath], { encoding: "utf8" });
2622
- spawnSync("launchctl", ["remove", unit.label], { encoding: "utf8" });
3009
+ if (existsSync5(unit.plistPath)) spawnSync2("launchctl", ["unload", unit.plistPath], { encoding: "utf8" });
3010
+ spawnSync2("launchctl", ["remove", unit.label], { encoding: "utf8" });
2623
3011
  if (existsSync5(unit.plistPath)) unlinkSync(unit.plistPath);
2624
3012
  log.emit(`removed ${unit.label} (${unit.plistPath})`);
2625
3013
  }
@@ -2652,7 +3040,7 @@ function parseLaunchctlList(text) {
2652
3040
  return jobs;
2653
3041
  }
2654
3042
  function describeForeignLaunchdUnits(crewHome, dir) {
2655
- const list = spawnSync("launchctl", ["list"], { encoding: "utf8" });
3043
+ const list = spawnSync2("launchctl", ["list"], { encoding: "utf8" });
2656
3044
  const jobs = parseLaunchctlList(list.status === 0 ? list.stdout ?? "" : "");
2657
3045
  const foreign = foreignLaunchdUnits(crewHome, dir, jobs.keys());
2658
3046
  return foreign.map((u) => {
@@ -2665,11 +3053,11 @@ function describeForeignLaunchdUnits(crewHome, dir) {
2665
3053
  });
2666
3054
  }
2667
3055
  function readCrontab() {
2668
- const res = spawnSync("crontab", ["-l"], { encoding: "utf8" });
3056
+ const res = spawnSync2("crontab", ["-l"], { encoding: "utf8" });
2669
3057
  return res.status === 0 ? res.stdout ?? "" : "";
2670
3058
  }
2671
3059
  function writeCrontab(text) {
2672
- spawnSync("crontab", ["-"], { input: text, encoding: "utf8" });
3060
+ spawnSync2("crontab", ["-"], { input: text, encoding: "utf8" });
2673
3061
  }
2674
3062
  function addCrontabLine(marker, line) {
2675
3063
  const existing = readCrontab();
@@ -2693,7 +3081,7 @@ function removeCrontabLine(marker) {
2693
3081
  }
2694
3082
  function detectSystemd() {
2695
3083
  if (process.platform !== "linux") return false;
2696
- const res = spawnSync("systemctl", ["--user", "status"], { encoding: "utf8" });
3084
+ const res = spawnSync2("systemctl", ["--user", "status"], { encoding: "utf8" });
2697
3085
  return !res.error;
2698
3086
  }
2699
3087
 
@@ -2711,7 +3099,7 @@ import {
2711
3099
  statSync as statSync4,
2712
3100
  unlinkSync as unlinkSync2
2713
3101
  } from "fs";
2714
- import { dirname as dirname4, basename as basename3 } from "path";
3102
+ import { dirname as dirname4, basename as basename4 } from "path";
2715
3103
  var GitError = class extends Error {
2716
3104
  };
2717
3105
  function gitRaw(cwd, args) {
@@ -2807,7 +3195,7 @@ function deleteRemoteBranch(cwd, remote, branch) {
2807
3195
  }
2808
3196
  function worktreeForNumber(cwd, num) {
2809
3197
  const re = new RegExp(`(?:^|-)${num}$`);
2810
- return worktrees(cwd).find((w) => re.test(basename3(w.path))) ?? null;
3198
+ return worktrees(cwd).find((w) => re.test(basename4(w.path))) ?? null;
2811
3199
  }
2812
3200
  var commitBodies = (cwd, range) => gitOk(cwd, ["log", range, "--format=%B"]) ?? "";
2813
3201
  var resolve2 = (cwd, ref) => gitOk(cwd, ["rev-parse", "-q", "--verify", `${ref}^{commit}`]);
@@ -4761,7 +5149,7 @@ ${waiting.map((t) => `| ${displayKey2(t)} | ${t.status} | ${blockers(t, i)} |`).
4761
5149
  }
4762
5150
 
4763
5151
  // src/ticket-branch.ts
4764
- import { basename as basename4 } from "path";
5152
+ import { basename as basename5 } from "path";
4765
5153
  function branchRenderer(t, role) {
4766
5154
  const { tag, prefix } = ticketBranchContext(t);
4767
5155
  return (template) => renderBranchName(template, { key: t.issue_id, title: t.title ?? void 0, role, tag, prefix });
@@ -4772,7 +5160,7 @@ function worktreeForTicket(dir, cfg, t, role) {
4772
5160
  const number = t.issue_id.replace(/^\D+/, "");
4773
5161
  const { tag } = ticketBranchContext(t);
4774
5162
  return effectiveWorktreeDirName(cfg, dir, branchName, number, (template) => renderWorktreeName(template, {
4775
- dir: basename4(dir.replace(/[/\\]+$/, "")),
5163
+ dir: basename5(dir.replace(/[/\\]+$/, "")),
4776
5164
  key: t.issue_id,
4777
5165
  title: t.title ?? void 0,
4778
5166
  role,
@@ -4813,7 +5201,7 @@ function materializeBranchForTicket(dir, cfg, t, role) {
4813
5201
 
4814
5202
  // src/poll.ts
4815
5203
  import { writeFileSync as writeFileSync3 } from "fs";
4816
- import { join as join6 } from "path";
5204
+ import { join as join7 } from "path";
4817
5205
 
4818
5206
  // src/stalled.ts
4819
5207
  var STALLED_AFTER_CYCLES = 30;
@@ -5085,7 +5473,7 @@ async function decideCycle(o) {
5085
5473
  }
5086
5474
  return decision;
5087
5475
  }
5088
- var digestPath = (stateDir, route, role) => join6(stateDir, `digest-${route}-${role}.md`);
5476
+ var digestPath = (stateDir, route, role) => join7(stateDir, `digest-${route}-${role}.md`);
5089
5477
  function writeDigest(o, d, role, stateDir) {
5090
5478
  const me = o.route.resolved?.seats[role];
5091
5479
  if (!me) return false;
@@ -5455,13 +5843,13 @@ var alreadyReported = (ticket, g, comments) => comments.some((c) => c.ticket_id
5455
5843
 
5456
5844
  // src/agent.ts
5457
5845
  import { spawn as spawn2 } from "child_process";
5458
- import { readFileSync as readFileSync6, existsSync as existsSync7, statSync as statSync5, mkdirSync as mkdirSync5, writeFileSync as writeFileSync4, createWriteStream } from "fs";
5846
+ import { readFileSync as readFileSync7, existsSync as existsSync7, statSync as statSync5, mkdirSync as mkdirSync5, writeFileSync as writeFileSync4, createWriteStream } from "fs";
5459
5847
  import { finished } from "stream/promises";
5460
- import { join as join7, dirname as dirname5 } from "path";
5848
+ import { join as join8, dirname as dirname5 } from "path";
5461
5849
  import { createHash as createHash2 } from "crypto";
5462
5850
 
5463
5851
  // src/environment.ts
5464
- import { basename as basename5 } from "path";
5852
+ import { basename as basename6 } from "path";
5465
5853
  var API_KEY_VAR = "CREW_API_KEY";
5466
5854
  function authorshipBullets(a, contract) {
5467
5855
  if (!a) return [];
@@ -5512,7 +5900,7 @@ function repoSection(r, key, heading) {
5512
5900
  const repo = r.config;
5513
5901
  const branchTemplate = effectiveBranchTemplate(repo, repoPrefix(repo));
5514
5902
  const branchName = branchExample(repo, key);
5515
- const checkoutName = basename5(r.dir.replace(/[/\\]+$/, ""));
5903
+ const checkoutName = basename6(r.dir.replace(/[/\\]+$/, ""));
5516
5904
  const worktreeName = effectiveWorktreeDirName(
5517
5905
  repo,
5518
5906
  r.dir,
@@ -5742,54 +6130,6 @@ function renderEnvironment(i) {
5742
6130
  return lines.join("\n");
5743
6131
  }
5744
6132
 
5745
- // src/stream.ts
5746
- function toolTarget(name, input) {
5747
- if (!input) return void 0;
5748
- for (const key of ["file_path", "path", "notebook_path", "url", "pattern"]) {
5749
- const v = input[key];
5750
- if (typeof v === "string" && v) return v;
5751
- }
5752
- if (name === "Bash" && typeof input.command === "string") {
5753
- return input.command.trim().split(/\s+/)[0];
5754
- }
5755
- return void 0;
5756
- }
5757
- function mapStreamLine(line) {
5758
- const obj = line;
5759
- if (!obj || obj.type !== "assistant") return [];
5760
- const content = obj.message?.content;
5761
- if (!Array.isArray(content)) return [];
5762
- const out = [];
5763
- for (const block of content) {
5764
- if (block.type === "thinking") out.push({ kind: "thought", text: block.thinking });
5765
- else if (block.type === "text") out.push({ kind: "text", text: block.text });
5766
- else if (block.type === "tool_use") {
5767
- out.push({ kind: "tool", tool: block.name, target: toolTarget(block.name, block.input) });
5768
- }
5769
- }
5770
- return out;
5771
- }
5772
- function extractTurnTokens(line) {
5773
- const obj = line;
5774
- if (!obj || obj.type !== "assistant") return void 0;
5775
- const usage2 = obj.message?.usage;
5776
- if (!usage2) return void 0;
5777
- return (usage2.input_tokens ?? 0) + (usage2.output_tokens ?? 0) + (usage2.cache_read_input_tokens ?? 0) + (usage2.cache_creation_input_tokens ?? 0);
5778
- }
5779
- function extractResult(line) {
5780
- const obj = line;
5781
- if (!obj || obj.type !== "result") return void 0;
5782
- return {
5783
- sessionId: obj.session_id,
5784
- numTurns: obj.num_turns,
5785
- totalCostUsd: obj.total_cost_usd,
5786
- inputTokens: obj.usage?.input_tokens,
5787
- outputTokens: obj.usage?.output_tokens,
5788
- cacheReadTokens: obj.usage?.cache_read_input_tokens,
5789
- cacheCreationTokens: obj.usage?.cache_creation_input_tokens
5790
- };
5791
- }
5792
-
5793
6133
  // src/agent-log.ts
5794
6134
  async function post(target, path, body) {
5795
6135
  const url = `${target.baseUrl.replace(/\/+$/, "")}/api/workspaces/${target.workspaceId}${path}`;
@@ -5871,36 +6211,10 @@ var TicketAttributionTracker = class {
5871
6211
 
5872
6212
  // src/agent.ts
5873
6213
  var DIGEST_MAX_AGE_SECONDS = 600;
5874
- function allowedTools(role) {
5875
- const tablation = "mcp__tablation__*";
5876
- if (role === "triage") return ["Bash", "Read", tablation];
5877
- if (role === "qa") return ["Bash", "Read", "Write", "Grep", "Glob", tablation];
5878
- const base = ["Bash", "Read", "Edit", "Write", "Grep", "Glob", tablation];
5879
- return role === "design" ? [...base, "Skill", "Artifact"] : base;
5880
- }
5881
- var DISALLOWED_TOOLS = [
5882
- "Bash(rm -rf:*)",
5883
- "Bash(rm -fr:*)",
5884
- "Bash(git push --force*)",
5885
- "Bash(git push -f*)",
5886
- "Bash(curl*| sh)",
5887
- "Bash(curl*|sh)",
5888
- "Bash(curl*| bash)",
5889
- "Bash(curl*|bash)",
5890
- "Bash(sudo:*)"
5891
- ];
5892
- var BILLING_VARS_TO_UNSET = [
5893
- "ANTHROPIC_API_KEY",
5894
- "ANTHROPIC_AUTH_TOKEN",
5895
- "CLAUDE_CODE_USE_VERTEX",
5896
- "ANTHROPIC_VERTEX_PROJECT_ID",
5897
- "ANTHROPIC_VERTEX_REGION",
5898
- "CLOUD_ML_REGION",
5899
- "ANTHROPIC_MODEL"
5900
- ];
5901
- function scrubbedEnv(env = process.env) {
6214
+ var LEGACY_TOOLCHAIN_HARNESS = "claude";
6215
+ function scrubbedEnv(env = process.env, harness = LEGACY_TOOLCHAIN_HARNESS) {
5902
6216
  const out = { ...env };
5903
- for (const v of BILLING_VARS_TO_UNSET) delete out[v];
6217
+ for (const v of adapterFor(harness).unsetEnv) delete out[v];
5904
6218
  return out;
5905
6219
  }
5906
6220
  var CREW_LANE_ROLE_VAR = "CREW_LANE_ROLE";
@@ -5914,23 +6228,23 @@ var AgentError = class extends Error {
5914
6228
  var AgentSpawnError = class extends AgentError {
5915
6229
  };
5916
6230
  function planAgentRun(o) {
5917
- const briefPath = join7(o.route.promptsDir, "personas", `lane-${o.role}.md`);
6231
+ const briefPath = join8(o.route.promptsDir, "personas", `lane-${o.role}.md`);
5918
6232
  if (!existsSync7(briefPath)) {
5919
6233
  throw new AgentError(`no brief at ${briefPath} \u2014 refusing to run an unscoped session`);
5920
6234
  }
5921
- const commonPath = join7(o.route.promptsDir, "personas", "common.md");
6235
+ const commonPath = join8(o.route.promptsDir, "personas", "common.md");
5922
6236
  if (!existsSync7(commonPath)) throw new AgentError(`no shared policy at ${commonPath}`);
5923
6237
  const now = o.now ?? (() => Date.now());
5924
6238
  const routeLabel = routeSlug(o.route.route);
5925
- const digestPath2 = join7(o.stateDir, `digest-${routeLabel}-${o.role}.md`);
6239
+ const digestPath2 = join8(o.stateDir, `digest-${routeLabel}-${o.role}.md`);
5926
6240
  let digest;
5927
6241
  let ageSeconds;
5928
6242
  if (existsSync7(digestPath2)) {
5929
6243
  ageSeconds = Math.floor((now() - statSync5(digestPath2).mtimeMs) / 1e3);
5930
- if (ageSeconds <= DIGEST_MAX_AGE_SECONDS) digest = readFileSync6(digestPath2, "utf8");
6244
+ if (ageSeconds <= DIGEST_MAX_AGE_SECONDS) digest = readFileSync7(digestPath2, "utf8");
5931
6245
  }
5932
- const common = readFileSync6(commonPath, "utf8");
5933
- const brief = readFileSync6(briefPath, "utf8");
6246
+ const common = readFileSync7(commonPath, "utf8");
6247
+ const brief = readFileSync7(briefPath, "utf8");
5934
6248
  const effectiveCommon = o.divergedPrompt ? "" : common;
5935
6249
  const effectiveBrief = o.divergedPrompt ?? brief;
5936
6250
  const prompt = assemblePrompt({
@@ -5941,9 +6255,18 @@ function planAgentRun(o) {
5941
6255
  digest
5942
6256
  });
5943
6257
  const promptSha = createHash2("sha256").update(`${effectiveCommon}${effectiveBrief}`).digest("hex");
5944
- const streamsDir = join7(o.stateDir, "streams");
6258
+ const streamsDir = join8(o.stateDir, "streams");
5945
6259
  const base = `${routeLabel}-${o.role}-${o.cycle}`;
5946
- const model = o.agentModel ?? o.ship.agent.model;
6260
+ const seat = resolveSeat(o.ship, o.route, o.role, o.agentPersona ?? (o.agentModel ? { model: o.agentModel } : {}));
6261
+ const { toolchain, model } = seat;
6262
+ const adapter = adapterFor(toolchain.harness);
6263
+ const invocation = adapter.invocation(toolchain, {
6264
+ role: o.role,
6265
+ model,
6266
+ // Skipped when there's no key to send: a route `crew connect` hasn't
6267
+ // resolved yet has nothing to authenticate the MCP connection with either.
6268
+ mcp: o.apiKey ? { url: `${o.route.baseUrl}/api/mcp`, keyVar: API_KEY_VAR } : void 0
6269
+ });
5947
6270
  const agentLog = o.route.resolved && o.apiKey ? {
5948
6271
  baseUrl: o.route.baseUrl,
5949
6272
  workspaceId: o.route.resolved.workspaceId,
@@ -5961,43 +6284,18 @@ function planAgentRun(o) {
5961
6284
  // (config.ts), and the digest's `repo` column is what actually places
5962
6285
  // the work. Kept deliberately rather than guessed at (ISSUE-349).
5963
6286
  cwd: o.route.dir,
5964
- bin: o.ship.agent.bin,
5965
- args: [
5966
- "-p",
5967
- "--allowedTools",
5968
- ...allowedTools(o.role),
5969
- "--disallowedTools",
5970
- ...DISALLOWED_TOOLS,
5971
- "--model",
5972
- model,
5973
- "--output-format",
5974
- "stream-json",
5975
- "--verbose",
5976
- // Registers the tracker's own MCP tool surface for this run only —
5977
- // `${CREW_API_KEY}` is interpolated by the CLI from the environment
5978
- // (set below), so the resolved key is never written to an argv string
5979
- // or a config file on disk. Skipped when there's no key to send: a
5980
- // route `crew connect` hasn't resolved yet has nothing to authenticate
5981
- // the MCP connection with either.
5982
- ...o.apiKey ? ["--mcp-config", JSON.stringify({
5983
- mcpServers: {
5984
- tablation: {
5985
- type: "http",
5986
- url: `${o.route.baseUrl}/api/mcp`,
5987
- headers: { Authorization: `Bearer \${${API_KEY_VAR}}` }
5988
- }
5989
- }
5990
- })] : []
5991
- ],
6287
+ bin: invocation.bin,
6288
+ args: invocation.args,
6289
+ toolchain: toolchain.name,
6290
+ harness: toolchain.harness,
6291
+ resolution: { toolchain: seat.toolchainWhy, model: seat.modelWhy },
6292
+ warnings: seat.warnings,
5992
6293
  prompt,
5993
6294
  promptBytes: Buffer.byteLength(prompt, "utf8"),
5994
- unsetEnv: BILLING_VARS_TO_UNSET,
6295
+ unsetEnv: adapter.unsetEnv,
5995
6296
  setEnv: {
5996
6297
  ...o.apiKey ? { [API_KEY_VAR]: o.apiKey } : {},
5997
- // Extended thinking is what makes the CLI emit `thinking` stream
5998
- // blocks at all — without it, `run.cycles` (below) is always empty
5999
- // and Agent Log Cycles reporting has nothing to report (ISSUE-558).
6000
- ...o.ship.agent.maxThinkingTokens > 0 ? { MAX_THINKING_TOKENS: String(o.ship.agent.maxThinkingTokens) } : {},
6298
+ ...adapter.runEnv(toolchain),
6001
6299
  // The operator's global SessionStart hook (pair-context-hook.sh) injects
6002
6300
  // the interactive Pair persona's prompt for any session it can't tell
6003
6301
  // apart from a person's own terminal. Every headless lane run this
@@ -6011,8 +6309,8 @@ function planAgentRun(o) {
6011
6309
  digestAttached: digest !== void 0,
6012
6310
  digestAgeSeconds: ageSeconds,
6013
6311
  model,
6014
- streamPath: join7(streamsDir, `${base}.jsonl`),
6015
- eventsPath: join7(streamsDir, `${base}.events.jsonl`),
6312
+ streamPath: join8(streamsDir, `${base}.jsonl`),
6313
+ eventsPath: join8(streamsDir, `${base}.events.jsonl`),
6016
6314
  ticket: o.ticket,
6017
6315
  agentLog,
6018
6316
  promptVersion: o.route.resolved?.agentPersonas?.[o.role]?.historyId,
@@ -6022,6 +6320,10 @@ function planAgentRun(o) {
6022
6320
  function describePlan(p) {
6023
6321
  return [
6024
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}`),
6325
+ // Only for a non-default toolchain, so a plain Claude run's output is unchanged.
6326
+ ...p.toolchain && p.toolchain !== "claude" ? [`toolchain: ${p.toolchain} (${p.harness ?? "claude"})`] : [],
6025
6327
  `cwd: ${p.cwd}`,
6026
6328
  `command: ${p.bin} ${p.args.join(" ")}`,
6027
6329
  `prompt: ${p.promptBytes} bytes on stdin (never argv)`,
@@ -6047,14 +6349,16 @@ function endSink(sink) {
6047
6349
  return finished(sink).catch(() => void 0);
6048
6350
  }
6049
6351
  function spawnAgent(plan, emit) {
6352
+ const adapter = adapterFor(plan.harness ?? LEGACY_TOOLCHAIN_HARNESS);
6050
6353
  const started = Date.now();
6051
6354
  const startedAt = new Date(started).toISOString();
6355
+ for (const w of plan.warnings ?? []) emit.warn(w, { step: "agent", role: plan.role });
6052
6356
  const rawSink = openSink(plan.streamPath);
6053
6357
  const eventsSink = openSink(plan.eventsPath);
6054
6358
  return new Promise((resolve5, reject) => {
6055
6359
  const child = spawn2(plan.bin, plan.args, {
6056
6360
  cwd: plan.cwd,
6057
- env: { ...scrubbedEnv(), ...plan.setEnv },
6361
+ env: { ...scrubbedEnv(process.env, adapter.harness), ...plan.setEnv },
6058
6362
  stdio: ["pipe", "pipe", "inherit"]
6059
6363
  });
6060
6364
  let buf = "";
@@ -6075,9 +6379,9 @@ function spawnAgent(plan, emit) {
6075
6379
  return;
6076
6380
  }
6077
6381
  try {
6078
- result = extractResult(parsed) ?? result;
6382
+ result = adapter.extractResult(parsed) ?? result;
6079
6383
  let turnText = "";
6080
- for (const ev of mapStreamLine(parsed)) {
6384
+ for (const ev of adapter.mapStreamLine(parsed)) {
6081
6385
  const e = {
6082
6386
  at: (/* @__PURE__ */ new Date()).toISOString(),
6083
6387
  cycle: emit.cycle,
@@ -6115,7 +6419,7 @@ function spawnAgent(plan, emit) {
6115
6419
  `);
6116
6420
  }
6117
6421
  }
6118
- const turnTokens = extractTurnTokens(parsed);
6422
+ const turnTokens = adapter.extractTurnTokens(parsed);
6119
6423
  if (turnTokens !== void 0) attribution.add(turnText, turnTokens);
6120
6424
  } catch {
6121
6425
  }
@@ -6193,12 +6497,12 @@ function spawnAgent(plan, emit) {
6193
6497
 
6194
6498
  // src/run-worktree.ts
6195
6499
  import { existsSync as existsSync9 } from "fs";
6196
- import { dirname as dirname6, join as join9 } from "path";
6500
+ import { dirname as dirname6, join as join10 } from "path";
6197
6501
 
6198
6502
  // src/ports.ts
6199
6503
  import { execFileSync as execFileSync2 } from "child_process";
6200
6504
  import { existsSync as existsSync8 } from "fs";
6201
- import { join as join8 } from "path";
6505
+ import { join as join9 } from "path";
6202
6506
  var run = (cmd, args) => {
6203
6507
  try {
6204
6508
  return execFileSync2(cmd, args, { encoding: "utf8", stdio: ["ignore", "pipe", "ignore"] });
@@ -6243,7 +6547,7 @@ function ticketForPort(port, s = DEFAULT_PORTS) {
6243
6547
  }
6244
6548
  function worktreeExists(parent, prefix, n) {
6245
6549
  for (const name of [`${prefix}${n}`, `${prefix}${String(n).padStart(3, "0")}`]) {
6246
- if (existsSync8(join8(parent, name, ".git"))) return true;
6550
+ if (existsSync8(join9(parent, name, ".git"))) return true;
6247
6551
  }
6248
6552
  return false;
6249
6553
  }
@@ -6331,7 +6635,7 @@ function planRunWorktree(dir, cfg, t, role) {
6331
6635
  if (countCommits(existing.path, `${baseRef}..HEAD`) > 0) return { action: "leave-unpushed", path: existing.path, branch };
6332
6636
  return { action: "recut", path: existing.path, branch, base: baseRef };
6333
6637
  }
6334
- const path = join9(dirname6(dir), worktreeForTicket(dir, cfg, t, role));
6638
+ const path = join10(dirname6(dir), worktreeForTicket(dir, cfg, t, role));
6335
6639
  if (located?.where === "remote" || located && hasRemote && remoteBranchExists(dir, remote, located.branch)) {
6336
6640
  return { action: "cut-remote", path, branch: located.branch };
6337
6641
  }
@@ -6539,7 +6843,7 @@ async function retireRunWorktree(tracker, dir, cfg, t, role, seatId, shipName2,
6539
6843
  }
6540
6844
 
6541
6845
  // src/connect-wizard.ts
6542
- import { delimiter, dirname as dirname7, join as join10 } from "path";
6846
+ import { delimiter, dirname as dirname7, join as join11 } from "path";
6543
6847
  import { accessSync as accessSync2, constants as constants2, existsSync as existsSync10, mkdirSync as mkdirSync6, writeFileSync as writeFileSync5 } from "fs";
6544
6848
  import { stringify } from "yaml";
6545
6849
  var HOST_PASSENGERS_DOCS_URL = "https://crew.tablation.dev/host-passengers";
@@ -6562,7 +6866,7 @@ function defaultIsExecutable(p) {
6562
6866
  function findOnPath2(name, pathEnv, isExecutable = defaultIsExecutable) {
6563
6867
  for (const dir of pathEnv.split(delimiter)) {
6564
6868
  if (!dir) continue;
6565
- const candidate = join10(dir, name);
6869
+ const candidate = join11(dir, name);
6566
6870
  if (isExecutable(candidate)) return candidate;
6567
6871
  }
6568
6872
  return void 0;
@@ -6657,8 +6961,8 @@ Wrote ${o.configPath}.
6657
6961
  }
6658
6962
 
6659
6963
  // src/release-run.ts
6660
- import { readFileSync as readFileSync7, writeFileSync as writeFileSync6, existsSync as existsSync11, copyFileSync, mkdirSync as mkdirSync7, mkdtempSync, rmSync as rmSync3 } from "fs";
6661
- import { dirname as dirname8, join as join11 } from "path";
6964
+ import { readFileSync as readFileSync8, writeFileSync as writeFileSync6, existsSync as existsSync11, copyFileSync, mkdirSync as mkdirSync7, mkdtempSync, rmSync as rmSync3 } from "fs";
6965
+ import { dirname as dirname8, join as join12 } from "path";
6662
6966
  import { tmpdir as tmpdir2 } from "os";
6663
6967
 
6664
6968
  // src/release.ts
@@ -7316,13 +7620,13 @@ async function bump(o, size) {
7316
7620
  }
7317
7621
  return printed;
7318
7622
  }
7319
- const files = o.repo.release.versionFiles.map((f) => join11(o.cwd, f));
7623
+ const files = o.repo.release.versionFiles.map((f) => join12(o.cwd, f));
7320
7624
  const first = files[0];
7321
7625
  if (!existsSync11(first)) {
7322
7626
  o.emit.error(`no ${o.repo.release.versionFiles[0]} to read a version from`);
7323
7627
  return null;
7324
7628
  }
7325
- const current = JSON.parse(readFileSync7(first, "utf8")).version;
7629
+ const current = JSON.parse(readFileSync8(first, "utf8")).version;
7326
7630
  if (!current) {
7327
7631
  o.emit.error(`no version field in ${o.repo.release.versionFiles[0]}`);
7328
7632
  return null;
@@ -7335,7 +7639,7 @@ async function bump(o, size) {
7335
7639
  return version;
7336
7640
  }
7337
7641
  for (const f of files) {
7338
- const src = readFileSync7(f, "utf8");
7642
+ const src = readFileSync8(f, "utf8");
7339
7643
  const out = src.replace(/("version"\s*:\s*")[^"]*(")/, `$1${version}$2`);
7340
7644
  if (out === src) {
7341
7645
  o.emit.error(`no version field in ${f}`);
@@ -7348,13 +7652,13 @@ async function bump(o, size) {
7348
7652
  function writeChangelog(o, version, entries) {
7349
7653
  const file = o.repo.release.changelog;
7350
7654
  if (!file || entries.length === 0) return;
7351
- const path = join11(o.cwd, file);
7655
+ const path = join12(o.cwd, file);
7352
7656
  const section = renderChangelogSection(version, (/* @__PURE__ */ new Date()).toISOString().slice(0, 10), entries);
7353
7657
  if (o.dryRun) {
7354
7658
  o.emit.emit(`would prepend ${entries.length} entr(ies) to ${file}`);
7355
7659
  return;
7356
7660
  }
7357
- const existing = existsSync11(path) ? readFileSync7(path, "utf8") : "# Changelog\n";
7661
+ const existing = existsSync11(path) ? readFileSync8(path, "utf8") : "# Changelog\n";
7358
7662
  writeFileSync6(path, insertChangelogSection(existing, section));
7359
7663
  }
7360
7664
  async function confirm(o, expected) {
@@ -7436,8 +7740,8 @@ async function confirmLastRelease(o, decision) {
7436
7740
  function openReleaseCheckout(o) {
7437
7741
  const { remote, base } = o.repo.branch;
7438
7742
  const branch = `crew/release-${Date.now()}`;
7439
- const root = mkdtempSync(join11(tmpdir2(), "crew-release-"));
7440
- const dir = join11(root, "checkout");
7743
+ const root = mkdtempSync(join12(tmpdir2(), "crew-release-"));
7744
+ const dir = join12(root, "checkout");
7441
7745
  const close = () => {
7442
7746
  gitOk(o.cwd, ["worktree", "remove", "--force", dir]);
7443
7747
  rmSync3(root, { recursive: true, force: true });
@@ -7451,10 +7755,10 @@ function openReleaseCheckout(o) {
7451
7755
  throw e;
7452
7756
  }
7453
7757
  for (const f of o.repo.worktrees.copy) {
7454
- const src = join11(o.cwd, f);
7758
+ const src = join12(o.cwd, f);
7455
7759
  if (!existsSync11(src)) continue;
7456
- mkdirSync7(dirname8(join11(dir, f)), { recursive: true });
7457
- copyFileSync(src, join11(dir, f));
7760
+ mkdirSync7(dirname8(join12(dir, f)), { recursive: true });
7761
+ copyFileSync(src, join12(dir, f));
7458
7762
  }
7459
7763
  return { dir, branch, close };
7460
7764
  }
@@ -8264,8 +8568,8 @@ function nextRoles(decision, ran) {
8264
8568
  }
8265
8569
 
8266
8570
  // src/agents.ts
8267
- import { readFileSync as readFileSync8, existsSync as existsSync13 } from "fs";
8268
- import { join as join12 } from "path";
8571
+ import { readFileSync as readFileSync9, existsSync as existsSync13 } from "fs";
8572
+ import { join as join13 } from "path";
8269
8573
  import { TablationClient as TablationClient2, StaleWriteError as StaleWriteError6, TablationApiError } from "@tablation/client";
8270
8574
  var PERSONA_NAME = {
8271
8575
  dev: "Developer",
@@ -8279,12 +8583,12 @@ var CREW_AGENT_COLUMN = "agent_id";
8279
8583
  var AgentsSyncError = class extends Error {
8280
8584
  };
8281
8585
  function personaDefaultPrompt(promptsDir, role) {
8282
- const briefPath = join12(promptsDir, "personas", `lane-${role}.md`);
8586
+ const briefPath = join13(promptsDir, "personas", `lane-${role}.md`);
8283
8587
  if (!existsSync13(briefPath)) throw new AgentsSyncError(`no brief at ${briefPath}`);
8284
- if (role === "pair") return readFileSync8(briefPath, "utf8");
8285
- const commonPath = join12(promptsDir, "personas", "common.md");
8588
+ if (role === "pair") return readFileSync9(briefPath, "utf8");
8589
+ const commonPath = join13(promptsDir, "personas", "common.md");
8286
8590
  if (!existsSync13(commonPath)) throw new AgentsSyncError(`no shared policy at ${commonPath}`);
8287
- return `${readFileSync8(commonPath, "utf8")}${readFileSync8(briefPath, "utf8")}`;
8591
+ return `${readFileSync9(commonPath, "utf8")}${readFileSync9(briefPath, "utf8")}`;
8288
8592
  }
8289
8593
  async function resolveAgentId(route, role, opts) {
8290
8594
  const cached = route.resolved?.agentPersonas?.[role]?.agentId;
@@ -8337,7 +8641,11 @@ async function fetchLastSyncedPrompt(route, agentsModelId, recordId, historyId,
8337
8641
  return void 0;
8338
8642
  }
8339
8643
  }
8340
- 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) {
8341
8649
  const agentId = await resolveAgentId(route, role, opts);
8342
8650
  if (!agentId || !route.resolved) return void 0;
8343
8651
  const client2 = new TablationClient2({
@@ -8348,7 +8656,14 @@ async function fetchSeatAgentModel(route, role, opts) {
8348
8656
  try {
8349
8657
  const agentsModel = await client2.dataModels.get("agents", route.resolved.workspaceId);
8350
8658
  const row = await client2.records.get(agentsModel.id, agentId);
8351
- 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;
8352
8667
  } catch {
8353
8668
  return void 0;
8354
8669
  }
@@ -8950,14 +9265,14 @@ import {
8950
9265
  } from "@tablation/client";
8951
9266
 
8952
9267
  // src/skills.ts
8953
- import { readFileSync as readFileSync9, readdirSync as readdirSync2, existsSync as existsSync14 } from "fs";
8954
- import { join as join13 } from "path";
9268
+ import { readFileSync as readFileSync10, readdirSync as readdirSync2, existsSync as existsSync14 } from "fs";
9269
+ import { join as join14 } from "path";
8955
9270
  import { parse as parseYaml } from "yaml";
8956
9271
  import { TablationClient as TablationClient3, StaleWriteError as StaleWriteError7, TablationApiError as TablationApiError2 } from "@tablation/client";
8957
9272
  var SkillSyncError = class extends Error {
8958
9273
  };
8959
9274
  function parseSkillFile(path) {
8960
- const text = readFileSync9(path, "utf8");
9275
+ const text = readFileSync10(path, "utf8");
8961
9276
  const match = /^---\n([\s\S]*?)\n---\n([\s\S]*)$/.exec(text);
8962
9277
  if (!match) throw new SkillSyncError(`${path}: missing YAML frontmatter (expected a leading --- block)`);
8963
9278
  const frontmatter = match[1];
@@ -8970,9 +9285,9 @@ function parseSkillFile(path) {
8970
9285
  return { name: meta.name, description: meta.description, prompt: body, file: path };
8971
9286
  }
8972
9287
  function listLocalSkills(promptsDir) {
8973
- const dir = join13(promptsDir, "skills");
9288
+ const dir = join14(promptsDir, "skills");
8974
9289
  if (!existsSync14(dir)) return [];
8975
- return readdirSync2(dir).filter((f) => f.endsWith(".md")).map((f) => parseSkillFile(join13(dir, f)));
9290
+ return readdirSync2(dir).filter((f) => f.endsWith(".md")).map((f) => parseSkillFile(join14(dir, f)));
8976
9291
  }
8977
9292
  async function syncSkills(route, opts) {
8978
9293
  if (!route.resolved) {
@@ -9151,8 +9466,8 @@ async function showLogEntry(route, entryId, opts = {}) {
9151
9466
  }
9152
9467
 
9153
9468
  // src/repos-cmd.ts
9154
- import { homedir as homedir3 } from "os";
9155
- import { basename as basename6, resolve as resolve3 } from "path";
9469
+ import { homedir as homedir4 } from "os";
9470
+ import { basename as basename7, resolve as resolve3 } from "path";
9156
9471
  import { isMap, isSeq, parseDocument, stringify as stringify2 } from "yaml";
9157
9472
  var ReposError = class extends Error {
9158
9473
  };
@@ -9198,7 +9513,7 @@ function parseAddArgs(args, routes, isDir) {
9198
9513
  return { route, path: "." };
9199
9514
  }
9200
9515
  function expandDir(p, base) {
9201
- const home = p === "~" ? homedir3() : p.startsWith("~/") ? `${homedir3()}/${p.slice(2)}` : p;
9516
+ const home = p === "~" ? homedir4() : p.startsWith("~/") ? `${homedir4()}/${p.slice(2)}` : p;
9202
9517
  return resolve3(base, home);
9203
9518
  }
9204
9519
  function routeMapOf(doc, route) {
@@ -9224,7 +9539,7 @@ function configuredRepos(text, route, base) {
9224
9539
  });
9225
9540
  }
9226
9541
  const dir = m.get("dir");
9227
- return dir ? [{ name: basename6(String(dir)), dir: expandDir(String(dir), base) }] : [];
9542
+ return dir ? [{ name: basename7(String(dir)), dir: expandDir(String(dir), base) }] : [];
9228
9543
  }
9229
9544
  function planRepoAdd(i) {
9230
9545
  const doc = parseDocument(i.text);
@@ -9236,7 +9551,7 @@ function planRepoAdd(i) {
9236
9551
  throw new ReposError(`${target} is already configured on route ${i.route}`);
9237
9552
  }
9238
9553
  const matched = matchRepoName(i.origin, i.resolved);
9239
- 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);
9240
9555
  let warning;
9241
9556
  if (!matched) {
9242
9557
  const known = Object.values(i.resolved?.repoNames ?? {});
@@ -9295,72 +9610,6 @@ function resolveRepoProject(projects, opt, routeAreaId) {
9295
9610
  );
9296
9611
  }
9297
9612
 
9298
- // src/claude-mcp.ts
9299
- import { spawnSync as spawnSync2 } from "child_process";
9300
- import { readFileSync as readFileSync10 } from "fs";
9301
- import { homedir as homedir4 } from "os";
9302
- import { join as join14 } from "path";
9303
- var MCP_SERVER_NAME = "tablation";
9304
- var redact = (s, key) => key ? s.split(key).join("sk_***") : s;
9305
- function mcpUrlFor(baseUrl) {
9306
- return `${baseUrl.replace(/\/+$/, "")}/api/mcp`;
9307
- }
9308
- function runClaude(bin, args) {
9309
- const r = spawnSync2(bin, args, { encoding: "utf8", timeout: 3e4 });
9310
- const missing = r.error?.code === "ENOENT";
9311
- return { status: r.status, missing, stderr: r.stderr ?? "" };
9312
- }
9313
- function userScopeEntry(home) {
9314
- try {
9315
- const j = JSON.parse(readFileSync10(join14(home, ".claude.json"), "utf8"));
9316
- return j.mcpServers?.[MCP_SERVER_NAME];
9317
- } catch {
9318
- return void 0;
9319
- }
9320
- }
9321
- function inspectClaudeMcp(o) {
9322
- const got = runClaude(o.bin, ["mcp", "get", MCP_SERVER_NAME]);
9323
- if (got.missing) return { state: "no-claude" };
9324
- if (got.status !== 0) return { state: "missing" };
9325
- const entry = userScopeEntry(o.home ?? homedir4());
9326
- if (!entry) {
9327
- return { state: "different", detail: `a "${MCP_SERVER_NAME}" server exists, but not in user scope (~/.claude.json) \u2014 a project or local registration` };
9328
- }
9329
- if (entry.url !== o.url) return { state: "different", detail: `it points at ${entry.url ?? "(no url)"}, not ${o.url}` };
9330
- const auth = Object.entries(entry.headers ?? {}).find(([k]) => k.toLowerCase() === "authorization")?.[1];
9331
- if (auth !== `Bearer ${o.key}`) return { state: "different", detail: `it is registered with a different key than this route's` };
9332
- return { state: "registered", url: o.url };
9333
- }
9334
- function ensureClaudeMcp(o) {
9335
- const manual = `claude mcp add --transport http ${MCP_SERVER_NAME} ${o.url} --header "Authorization: Bearer <key>" --scope user`;
9336
- const st = inspectClaudeMcp(o);
9337
- if (st.state === "no-claude") return `mcp: \`${o.bin}\` not found, so Claude Code was not configured. To do it by hand: ${manual}`;
9338
- if (st.state === "registered") return `mcp: ${MCP_SERVER_NAME} already registered with Claude Code \u2192 ${st.url}`;
9339
- if (st.state === "different" && !o.replace) {
9340
- return `mcp: left Claude Code's "${MCP_SERVER_NAME}" server alone \u2014 ${st.detail}. Re-run with --mcp replace to overwrite it.`;
9341
- }
9342
- if (o.dryRun) return `(dry run) would register the ${MCP_SERVER_NAME} MCP server with Claude Code \u2192 ${o.url}`;
9343
- if (st.state === "different") {
9344
- runClaude(o.bin, ["mcp", "remove", MCP_SERVER_NAME, "--scope", "user"]);
9345
- }
9346
- const add = runClaude(o.bin, [
9347
- "mcp",
9348
- "add",
9349
- "--transport",
9350
- "http",
9351
- MCP_SERVER_NAME,
9352
- o.url,
9353
- "--header",
9354
- `Authorization: Bearer ${o.key}`,
9355
- "--scope",
9356
- "user"
9357
- ]);
9358
- if (add.status !== 0) {
9359
- return `mcp: \`claude mcp add\` failed (${redact(add.stderr.trim(), o.key) || `exit ${add.status}`}). To do it by hand: ${manual}`;
9360
- }
9361
- return `mcp: registered the ${MCP_SERVER_NAME} MCP server with Claude Code \u2192 ${o.url} (user scope, ~/.claude.json)`;
9362
- }
9363
-
9364
9613
  // src/doctor-fix.ts
9365
9614
  import { parseDocument as parseDocument2 } from "yaml";
9366
9615
  var sound = (r) => r.hasRepos && r.problems.length === 0;
@@ -9400,14 +9649,14 @@ function schedulerInstalled(plan, exists, crontab) {
9400
9649
 
9401
9650
  // src/worktree-sweep.ts
9402
9651
  import { existsSync as existsSync15 } from "fs";
9403
- import { basename as basename7 } from "path";
9652
+ import { basename as basename8 } from "path";
9404
9653
  function planWorktreeSweep(target, tickets, contract) {
9405
9654
  const terminal = new Set(contract.statuses.resolved.filter((s) => s !== contract.statuses.verified));
9406
9655
  const verified = contract.statuses.verified;
9407
9656
  const byNumber = new Map(tickets.map((t) => [t.issue_id.replace(/^\D+/, ""), t]));
9408
9657
  const actions = [];
9409
9658
  for (const w of worktrees(target.dir)) {
9410
- const m = /(?:^|-)(\d+)$/.exec(basename7(w.path));
9659
+ const m = /(?:^|-)(\d+)$/.exec(basename8(w.path));
9411
9660
  if (!m) continue;
9412
9661
  const ticket = byNumber.get(m[1]);
9413
9662
  if (!ticket) continue;
@@ -10572,7 +10821,7 @@ async function runRoleAgent(role, decision, ctx) {
10572
10821
  cycle: emit.cycle,
10573
10822
  ticket: ticketHint,
10574
10823
  divergedPrompt: await fetchDivergedPrompt(route, role, { userAgent: ship.userAgent }),
10575
- agentModel: await fetchSeatAgentModel(route, role, { userAgent: ship.userAgent }),
10824
+ agentPersona: await fetchSeatPersona(route, role, { userAgent: ship.userAgent }),
10576
10825
  resolvedAgentId: await resolveAgentId(route, role, { userAgent: ship.userAgent })
10577
10826
  });
10578
10827
  emit.enter("agent", role);
@@ -11658,7 +11907,7 @@ ${changed2.map((c) => ` - ${c}
11658
11907
  cycle: wemit.cycle,
11659
11908
  ticket: w.decision.actionable.top?.issue_id,
11660
11909
  divergedPrompt: await fetchDivergedPrompt(w.route, w.role, { userAgent: cfg.ship.userAgent }),
11661
- agentModel: await fetchSeatAgentModel(w.route, w.role, { userAgent: cfg.ship.userAgent }),
11910
+ agentPersona: await fetchSeatPersona(w.route, w.role, { userAgent: cfg.ship.userAgent }),
11662
11911
  resolvedAgentId: await resolveAgentId(w.route, w.role, { userAgent: cfg.ship.userAgent })
11663
11912
  });
11664
11913
  process.stdout.write(`${describePlan(fleetPlan2)}
@@ -11751,7 +12000,7 @@ ${changed2.map((c) => ` - ${c}
11751
12000
  cycle: wemit.cycle,
11752
12001
  ticket: fleetTicketHint,
11753
12002
  divergedPrompt: await fetchDivergedPrompt(w.route, w.role, { userAgent: cfg.ship.userAgent }),
11754
- agentModel: await fetchSeatAgentModel(w.route, w.role, { userAgent: cfg.ship.userAgent }),
12003
+ agentPersona: await fetchSeatPersona(w.route, w.role, { userAgent: cfg.ship.userAgent }),
11755
12004
  resolvedAgentId: await resolveAgentId(w.route, w.role, { userAgent: cfg.ship.userAgent })
11756
12005
  });
11757
12006
  wemit.enter("agent", w.role);
@@ -11956,7 +12205,7 @@ ${changed2.map((c) => ` - ${c}
11956
12205
  cycle: emit.cycle,
11957
12206
  ticket: ticketHint,
11958
12207
  divergedPrompt: await fetchDivergedPrompt(route, current, { userAgent: cfg.ship.userAgent }),
11959
- agentModel: await fetchSeatAgentModel(route, current, { userAgent: cfg.ship.userAgent }),
12208
+ agentPersona: await fetchSeatPersona(route, current, { userAgent: cfg.ship.userAgent }),
11960
12209
  resolvedAgentId: await resolveAgentId(route, current, { userAgent: cfg.ship.userAgent })
11961
12210
  });
11962
12211
  if (dryRun) {
@@ -12746,8 +12995,14 @@ Signed in as ${result.identity.email} \u2014 minted an API key in workspace "${r
12746
12995
  if (!wroteConfig) process.stdout.write(routeBlock);
12747
12996
  if (!flag("no-mcp") && apiKey) {
12748
12997
  const replace = value("mcp") === "replace";
12749
- process.stderr.write(`${ensureClaudeMcp({ bin: cfg.ship.agent.bin, url: mcpUrlFor(baseUrl), key: apiKey, replace, dryRun })}
12998
+ const seen = /* @__PURE__ */ new Set();
12999
+ for (const tc of shipToolchains(cfg.ship)) {
13000
+ const id = `${tc.harness}\0${tc.bin}`;
13001
+ if (seen.has(id)) continue;
13002
+ seen.add(id);
13003
+ process.stderr.write(`${adapterFor(tc.harness).ensureMcp({ bin: tc.bin, url: mcpUrlFor(baseUrl), key: apiKey, replace, dryRun })}
12750
13004
  `);
13005
+ }
12751
13006
  }
12752
13007
  if (found.holds.length) {
12753
13008
  process.stderr.write(
@@ -13303,6 +13558,16 @@ Signed in as ${result.identity.email} \u2014 minted an API key in workspace "${r
13303
13558
  name: cfg.ship.name,
13304
13559
  platform: cfg.ship.platform,
13305
13560
  agent: { bin: cfg.ship.agent.bin, model: cfg.ship.agent.model },
13561
+ // Additive (CREW-1511): the configured toolchains and the ship's
13562
+ // default. `agent` above stays for older crew-macos builds.
13563
+ toolchain: resolveToolchain(cfg.ship).name,
13564
+ toolchains: shipToolchains(cfg.ship).map((x) => ({
13565
+ name: x.name,
13566
+ harness: x.harness,
13567
+ bin: x.bin,
13568
+ launcher: x.launcher ?? null,
13569
+ model: x.model
13570
+ })),
13306
13571
  stateDir: cfg.ship.stateDir,
13307
13572
  logFile: cfg.ship.logFile,
13308
13573
  eventFile: eventFileFor(cfg.ship.stateDir),
@@ -13328,6 +13593,8 @@ Signed in as ${result.identity.email} \u2014 minted an API key in workspace "${r
13328
13593
  dir: route.dir,
13329
13594
  enabled: route.enabled,
13330
13595
  area: route.area ?? null,
13596
+ // Additive (CREW-1511): the toolchain this route's sessions run on.
13597
+ toolchain: resolveToolchain(cfg.ship, route).name,
13331
13598
  baseUrl: route.baseUrl,
13332
13599
  repos: repos.map((r) => ({
13333
13600
  name: r.name,
@@ -13342,7 +13609,7 @@ Signed in as ${result.identity.email} \u2014 minted an API key in workspace "${r
13342
13609
  // `synthesis-tabl-123`. `worktreePrefix` above is kept for
13343
13610
  // config-provenance display; this is the field that actually
13344
13611
  // describes where a worktree lands by default.
13345
- 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>`),
13346
13613
  worktreeParent: resolvePath(r.dir, "..")
13347
13614
  })),
13348
13615
  waiting: waiting ? { ticket: waiting.ticket, since: waiting.since, streak: fairness.streak() } : null
@@ -13450,7 +13717,7 @@ routes: ${cfg.routes.map((c) => c.route).join(", ")}
13450
13717
  );
13451
13718
  for (const r of await resolvedRepos(route)) {
13452
13719
  const { config } = r;
13453
- 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)}`);
13454
13721
  process.stdout.write(
13455
13722
  `${`repo ${r.name}:`.padEnd(19)}${r.dir}
13456
13723
  worktrees at ../${worktreePattern} (${config.provenance["worktrees.name"] ?? config.provenance["worktrees.prefix"] ?? "default"}), branch ${config.branch.name}
@@ -13553,14 +13820,24 @@ crew manifest: ${seats2.length ? seats2.map((c) => c.name).join(", ") : "non
13553
13820
  }
13554
13821
  }
13555
13822
  const schedulerPath = pathFor(cfg.ship);
13556
- const agentBinPath = findOnPath(cfg.ship.agent.bin, schedulerPath);
13557
13823
  const gitPath = findOnPath("git", schedulerPath);
13558
- process.stdout.write(
13559
- `agent binary: ${cfg.ship.agent.bin} \u2014 ${agentBinPath ? `OK (${agentBinPath})` : "NOT FOUND on the scheduler PATH"}
13560
- git: ${gitPath ? `OK (${gitPath})` : "NOT FOUND on the scheduler PATH"}
13824
+ const defaultToolchain = resolveToolchain(cfg.ship).name;
13825
+ let allBinsFound = true;
13826
+ for (const tc of shipToolchains(cfg.ship)) {
13827
+ const adapter = adapterFor(tc.harness);
13828
+ const binPath = findOnPath(tc.launcher ?? tc.bin, schedulerPath);
13829
+ if (!binPath) allBinsFound = false;
13830
+ const label = tc.name === defaultToolchain ? "agent binary: " : `toolchain ${tc.name}:`.padEnd(18);
13831
+ process.stdout.write(
13832
+ `${label} ${tc.launcher ?? tc.bin} \u2014 ${binPath ? `OK (${binPath})` : "NOT FOUND on the scheduler PATH"}${tc.name === defaultToolchain ? "" : ` [${adapter.displayName}]`}
13561
13833
  `
13562
- );
13563
- if (!agentBinPath || !gitPath) {
13834
+ );
13835
+ for (const limit of adapter.limitations) process.stdout.write(` cannot enforce: ${limit}
13836
+ `);
13837
+ }
13838
+ process.stdout.write(`git: ${gitPath ? `OK (${gitPath})` : "NOT FOUND on the scheduler PATH"}
13839
+ `);
13840
+ if (!allBinsFound || !gitPath) {
13564
13841
  process.stdout.write(
13565
13842
  ` scheduler PATH is ${schedulerPath} \u2014 add the directory holding the missing binary to ship.extraPath in crew.yaml, then re-run \`crew doctor\`.
13566
13843
  `
@@ -13568,14 +13845,34 @@ git: ${gitPath ? `OK (${gitPath})` : "NOT FOUND on the scheduler P
13568
13845
  }
13569
13846
  if (route.apiKey) {
13570
13847
  const mcpUrl = mcpUrlFor(route.baseUrl);
13571
- const m = inspectClaudeMcp({ bin: cfg.ship.agent.bin, url: mcpUrl, key: route.apiKey });
13572
- process.stdout.write(
13573
- m.state === "registered" ? `mcp: tablation registered (user scope) \u2192 ${m.url}
13574
- ` : m.state === "no-claude" ? `mcp: claude not found \u2014 Claude Code was not checked
13848
+ const seenMcp = /* @__PURE__ */ new Set();
13849
+ for (const tc of shipToolchains(cfg.ship)) {
13850
+ const id = `${tc.harness}\0${tc.bin}`;
13851
+ if (seenMcp.has(id)) continue;
13852
+ seenMcp.add(id);
13853
+ const adapter = adapterFor(tc.harness);
13854
+ const m = adapter.inspectMcp({ bin: tc.bin, url: mcpUrl, key: route.apiKey });
13855
+ process.stdout.write(
13856
+ m.state === "registered" ? `mcp: tablation registered (user scope) \u2192 ${m.url}
13857
+ ` : m.state === "no-binary" ? `mcp: ${tc.bin} not found \u2014 ${adapter.displayName} was not checked
13575
13858
  ` : m.state === "different" ? `mcp: tablation registered but not for this route \u2014 ${m.detail}
13576
13859
  ` : `mcp: not registered \u2014 run crew connect
13577
13860
  `
13578
- );
13861
+ );
13862
+ }
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");
13579
13876
  }
13580
13877
  for (const c of cfg.routes.filter((r) => r.enabled)) {
13581
13878
  let repos;