@tablation/crew 0.34.0 → 0.35.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/crew.example.yaml CHANGED
@@ -78,7 +78,9 @@ routes:
78
78
  # and the queue digest marks it NO CHECKOUT rather than leaving it blank.
79
79
  repos:
80
80
  My Product: "/home/me/src/my-product"
81
- My Product CLI: "/home/me/src/my-product-cli"
81
+ My Product CLI:
82
+ dir: "/home/me/src/my-product-cli"
83
+ # toolchain: codex # run tickets in this repo on another ship.toolchains entry (CREW-1529)
82
84
 
83
85
  # Leave `repos:` out entirely instead to serve every repo the board's
84
86
  # Repos table lists, each one defaulting to
package/dist/cli.js CHANGED
@@ -991,8 +991,8 @@ function shipToolchains(ship) {
991
991
  };
992
992
  return [legacy, ...explicit];
993
993
  }
994
- function resolveToolchain(ship, route) {
995
- const name = route?.toolchain ?? ship.toolchain ?? LEGACY_TOOLCHAIN;
994
+ function resolveToolchain(ship, route, repo) {
995
+ const name = (repo && route?.repoOverrides?.[repo]?.toolchain) ?? route?.toolchain ?? ship.toolchain ?? LEGACY_TOOLCHAIN;
996
996
  const found = shipToolchains(ship).find((t) => t.name === name);
997
997
  if (!found) throw new Error(`toolchain "${name}" is not defined (have: ${shipToolchains(ship).map((t) => t.name).join(", ")})`);
998
998
  return found;
@@ -1007,11 +1007,12 @@ function isOriLauncher(spec) {
1007
1007
  function toolchainProvider(spec) {
1008
1008
  return isOriLauncher(spec) ? "openrouter" : adapterFor(spec.harness).provider(spec);
1009
1009
  }
1010
- function resolveSeat(ship, route, role, persona = {}) {
1010
+ function resolveSeat(ship, route, role, persona = {}, repo) {
1011
1011
  const warnings = [];
1012
- const base = resolveToolchain(ship, route);
1012
+ const base = resolveToolchain(ship, route, repo);
1013
1013
  const baseVendor = toolchainVendor(base);
1014
- const baseWhy = route?.toolchain ? `route toolchain ${base.name}` : ship.toolchain ? `ship default ${base.name}` : `default ${base.name}`;
1014
+ const repoToolchain = repo ? route?.repoOverrides?.[repo]?.toolchain : void 0;
1015
+ const baseWhy = repoToolchain ? `repo ${repo} toolchain ${base.name}` : route?.toolchain ? `route toolchain ${base.name}` : ship.toolchain ? `ship default ${base.name}` : `default ${base.name}`;
1015
1016
  let chosen = base;
1016
1017
  let toolchainWhy = baseWhy;
1017
1018
  if (persona.vendor && persona.vendor !== baseVendor) {
@@ -1710,7 +1711,7 @@ function parseWorktreeName(v, where, file) {
1710
1711
  if (problem) throw new ConfigError(`${file}: ${where}.worktreeName ("${String(v)}") ${problem}`);
1711
1712
  return String(v);
1712
1713
  }
1713
- var REPO_ENTRY_KEYS = /* @__PURE__ */ new Set(["dir", "hooks", "labels", "release", "branch", "hostPassengers", "worktreeName"]);
1714
+ var REPO_ENTRY_KEYS = /* @__PURE__ */ new Set(["dir", "hooks", "labels", "release", "branch", "hostPassengers", "worktreeName", "toolchain"]);
1714
1715
  var BRANCH_OVERRIDE_KEYS = /* @__PURE__ */ new Set(["base", "name", "push", "remote"]);
1715
1716
  var RELEASE_OVERRIDE_KEYS = /* @__PURE__ */ new Set([
1716
1717
  "mode",
@@ -2140,7 +2141,11 @@ function parseOneRoute(c, where, file, base, stateDir, raw, missing, crewHome, t
2140
2141
  const e = entry;
2141
2142
  rejectUnknownKeys(e, REPO_ENTRY_KEYS, `${where}.repos.${name}`, file);
2142
2143
  repoDirs[name] = expand(String(missing.req(e.dir, `${where}.repos.${name}.dir`) || ""), base);
2143
- if (e.hooks || e.labels || e.release || e.branch || e.hostPassengers !== void 0 || e.worktreeName !== void 0) {
2144
+ const repoToolchain = e.toolchain === void 0 ? void 0 : String(e.toolchain);
2145
+ if (repoToolchain !== void 0 && !toolchainNames.has(repoToolchain)) {
2146
+ missing.add(`${where}.repos.${name}.toolchain "${repoToolchain}" is not defined in ship.toolchains (have: ${[...toolchainNames].join(", ")})`);
2147
+ }
2148
+ if (e.hooks || e.labels || e.release || e.branch || e.hostPassengers !== void 0 || e.worktreeName !== void 0 || repoToolchain !== void 0) {
2144
2149
  repoOverrides[name] = {
2145
2150
  hooks: e.hooks,
2146
2151
  labels: e.labels,
@@ -2151,7 +2156,8 @@ function parseOneRoute(c, where, file, base, stateDir, raw, missing, crewHome, t
2151
2156
  // compare this literally.
2152
2157
  ...e.branch ? { branch: parseBranchOverride(e.branch, `${where}.repos.${name}`, file) } : {},
2153
2158
  ...e.hostPassengers !== void 0 ? { hostPassengers: e.hostPassengers === true } : {},
2154
- ...e.worktreeName !== void 0 ? { worktreeName: parseWorktreeName(e.worktreeName, `${where}.repos.${name}`, file) } : {}
2159
+ ...e.worktreeName !== void 0 ? { worktreeName: parseWorktreeName(e.worktreeName, `${where}.repos.${name}`, file) } : {},
2160
+ ...repoToolchain !== void 0 ? { toolchain: repoToolchain } : {}
2155
2161
  };
2156
2162
  }
2157
2163
  } else {
@@ -6871,7 +6877,7 @@ function planAgentRun(o) {
6871
6877
  const promptSha = createHash2("sha256").update(`${effectiveCommon}${effectiveBrief}`).digest("hex");
6872
6878
  const streamsDir = join9(o.stateDir, "streams");
6873
6879
  const base = `${routeLabel}-${o.role}-${o.cycle}`;
6874
- const seat = resolveSeat(o.ship, o.route, o.role, o.agentPersona ?? (o.agentModel ? { model: o.agentModel } : {}));
6880
+ const seat = resolveSeat(o.ship, o.route, o.role, o.agentPersona ?? (o.agentModel ? { model: o.agentModel } : {}), o.repo);
6875
6881
  const { toolchain, model } = seat;
6876
6882
  const adapter = adapterFor(toolchain.harness);
6877
6883
  const invocation = adapter.invocation(toolchain, {
@@ -6892,8 +6898,9 @@ function planAgentRun(o) {
6892
6898
  role: o.role,
6893
6899
  route: routeLabel,
6894
6900
  // The route's directory, which is a STARTING POINT and not the
6895
- // ticket's repo: the runner cannot know which ticket the session will
6896
- // take, so it cannot know the checkout either. Where an area spans
6901
+ // ticket's repo: the session picks its own working directory even when
6902
+ // the runner already selected a ticket (`o.repo` is known then, and
6903
+ // only feeds the per-repo toolchain choice). Where an area spans
6897
6904
  // several repos this is whichever one `repos` happened to list first
6898
6905
  // (config.ts), and the digest's `repo` column is what actually places
6899
6906
  // the work. Kept deliberately rather than guessed at (ISSUE-349).
@@ -11379,6 +11386,7 @@ async function runRoleAgent(role, decision, ctx) {
11379
11386
  const { route, ship, emit } = ctx;
11380
11387
  const tracker = new Tracker(route, ship);
11381
11388
  let ticketHint = decision.actionable.top?.issue_id;
11389
+ let ticketRepo = repoTargetFor(route, decision.actionable.top?.repo_id)?.name;
11382
11390
  let workingId = decision.actionable.top?.id ?? null;
11383
11391
  if (role === "dev" || role === "design" || role === "qa") {
11384
11392
  const seat = route.resolved?.seats[role];
@@ -11433,6 +11441,7 @@ async function runRoleAgent(role, decision, ctx) {
11433
11441
  ticketHint = result.ticket.issue_id;
11434
11442
  workingId = result.ticket.id;
11435
11443
  const target = repoTargetFor(route, result.ticket.repo_id);
11444
+ ticketRepo = target?.name;
11436
11445
  try {
11437
11446
  if (target && ensureRepoCheckout(target.dir, target.remote)) {
11438
11447
  emit.emit(`cloned ${target.name} into ${target.dir}`, { step: "select", role });
@@ -11455,6 +11464,7 @@ async function runRoleAgent(role, decision, ctx) {
11455
11464
  apiKey: resolveApiKey(route),
11456
11465
  cycle: emit.cycle,
11457
11466
  ticket: ticketHint,
11467
+ repo: ticketRepo,
11458
11468
  divergedPrompt: await fetchDivergedPrompt(route, role, { userAgent: ship.userAgent }),
11459
11469
  agentPersona: await fetchSeatPersona(route, role, { userAgent: ship.userAgent }),
11460
11470
  resolvedAgentId: await resolveAgentId(route, role, { userAgent: ship.userAgent })
@@ -12542,6 +12552,7 @@ ${changed2.map((c) => ` - ${c}
12542
12552
  apiKey: resolveApiKey(w.route),
12543
12553
  cycle: wemit.cycle,
12544
12554
  ticket: w.decision.actionable.top?.issue_id,
12555
+ repo: repoTargetFor(w.route, w.decision.actionable.top?.repo_id)?.name,
12545
12556
  divergedPrompt: await fetchDivergedPrompt(w.route, w.role, { userAgent: cfg.ship.userAgent }),
12546
12557
  agentPersona: await fetchSeatPersona(w.route, w.role, { userAgent: cfg.ship.userAgent }),
12547
12558
  resolvedAgentId: await resolveAgentId(w.route, w.role, { userAgent: cfg.ship.userAgent })
@@ -12559,6 +12570,7 @@ ${changed2.map((c) => ` - ${c}
12559
12570
  lock = fleetLock;
12560
12571
  const fleetTracker = new Tracker(w.route, cfg.ship);
12561
12572
  let fleetTicketHint = w.decision.actionable.top?.issue_id;
12573
+ let fleetTicketRepo = repoTargetFor(w.route, w.decision.actionable.top?.repo_id)?.name;
12562
12574
  let fleetWorkingId = w.decision.actionable.top?.id ?? null;
12563
12575
  if (w.role === "dev" || w.role === "design" || w.role === "qa") {
12564
12576
  const seat = w.route.resolved?.seats[w.role];
@@ -12614,6 +12626,7 @@ ${changed2.map((c) => ` - ${c}
12614
12626
  fleetTicketHint = result.ticket.issue_id;
12615
12627
  fleetWorkingId = result.ticket.id;
12616
12628
  const target = repoTargetFor(w.route, result.ticket.repo_id);
12629
+ fleetTicketRepo = target?.name;
12617
12630
  try {
12618
12631
  if (target && ensureRepoCheckout(target.dir, target.remote)) {
12619
12632
  wemit.emit(`cloned ${target.name} into ${target.dir}`, { step: "select", role: w.role });
@@ -12635,6 +12648,7 @@ ${changed2.map((c) => ` - ${c}
12635
12648
  apiKey: resolveApiKey(w.route),
12636
12649
  cycle: wemit.cycle,
12637
12650
  ticket: fleetTicketHint,
12651
+ repo: fleetTicketRepo,
12638
12652
  divergedPrompt: await fetchDivergedPrompt(w.route, w.role, { userAgent: cfg.ship.userAgent }),
12639
12653
  agentPersona: await fetchSeatPersona(w.route, w.role, { userAgent: cfg.ship.userAgent }),
12640
12654
  resolvedAgentId: await resolveAgentId(w.route, w.role, { userAgent: cfg.ship.userAgent })
@@ -12752,6 +12766,7 @@ ${changed2.map((c) => ` - ${c}
12752
12766
  lock = got;
12753
12767
  }
12754
12768
  let ticketHint = decision.actionable.top?.issue_id;
12769
+ let ticketRepo = repoTargetFor(route, decision.actionable.top?.repo_id)?.name;
12755
12770
  let workingId = decision.actionable.top?.id ?? null;
12756
12771
  if (!dryRun && (current === "dev" || current === "design" || current === "qa")) {
12757
12772
  const seat = route.resolved?.seats[current];
@@ -12813,6 +12828,7 @@ ${changed2.map((c) => ` - ${c}
12813
12828
  ticketHint = result.ticket.issue_id;
12814
12829
  workingId = result.ticket.id;
12815
12830
  const target = repoTargetFor(route, result.ticket.repo_id);
12831
+ ticketRepo = target?.name;
12816
12832
  try {
12817
12833
  if (target && ensureRepoCheckout(target.dir, target.remote)) {
12818
12834
  emit.emit(`cloned ${target.name} into ${target.dir}`, { step: "select", role: current });
@@ -12840,6 +12856,7 @@ ${changed2.map((c) => ` - ${c}
12840
12856
  apiKey: resolveApiKey(route),
12841
12857
  cycle: emit.cycle,
12842
12858
  ticket: ticketHint,
12859
+ repo: ticketRepo,
12843
12860
  divergedPrompt: await fetchDivergedPrompt(route, current, { userAgent: cfg.ship.userAgent }),
12844
12861
  agentPersona: await fetchSeatPersona(route, current, { userAgent: cfg.ship.userAgent }),
12845
12862
  resolvedAgentId: await resolveAgentId(route, current, { userAgent: cfg.ship.userAgent })
@@ -13244,6 +13261,7 @@ ${changed2.map((c) => ` - ${c}
13244
13261
  const planned = loadConfig(CREW_HOME, cfg.configFile, { text: plan.text, allowNoRoutes: true });
13245
13262
  const planRoute = planned.routes.find((r) => r.route === routeName);
13246
13263
  if (!planRoute) throw new ReposError(`route ${routeName} did not load from the planned config; use --no-create`);
13264
+ await hydrateApiKeys([planRoute]);
13247
13265
  try {
13248
13266
  rowTracker = new Tracker(planRoute, cfg.ship);
13249
13267
  } catch (e) {
@@ -14249,6 +14267,8 @@ Signed in as ${result.identity.email} \u2014 minted an API key in workspace "${r
14249
14267
  repos: repos.map((r) => ({
14250
14268
  name: r.name,
14251
14269
  dir: r.dir,
14270
+ // Additive (CREW-1529): the toolchain a ticket in this repo runs on.
14271
+ toolchain: resolveToolchain(cfg.ship, route, r.name).name,
14252
14272
  // Additive (CREW-1379): `unsafe` means a ship must not cut a
14253
14273
  // worktree or release from this base. `version` stays 1.
14254
14274
  baseBranch: baseHealth(r),
@@ -14489,6 +14509,10 @@ crew manifest: ${seats2.length ? seats2.map((c) => c.name).join(", ") : "non
14489
14509
  `);
14490
14510
  }
14491
14511
  }
14512
+ for (const [repoName, ov] of Object.entries(route.repoOverrides)) {
14513
+ if (ov.toolchain) process.stdout.write(`repo toolchain: ${repoName} \u2192 ${ov.toolchain} (binary and MCP checked above, as for every ship toolchain)
14514
+ `);
14515
+ }
14492
14516
  process.stdout.write(`git: ${gitPath ? `OK (${gitPath})` : "NOT FOUND on the scheduler PATH"}
14493
14517
  `);
14494
14518
  if (!allBinsFound || !gitPath) {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@tablation/crew",
3
- "version": "0.34.0",
3
+ "version": "0.35.0",
4
4
  "description": "A standing team of headless agents that picks work off a Tablation board, does it on a machine you control, and reports back.",
5
5
  "type": "module",
6
6
  "license": "MIT",