@tablation/crew 0.34.0 → 0.36.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 dirname12, join as join21, basename as basename9 } from "path";
4
+ import { resolve as resolve4, dirname as dirname12, join as join22, basename as basename9 } from "path";
5
5
  import { fileURLToPath } from "url";
6
- import { hostname as hostname2, homedir as homedir6 } from "os";
7
- import { spawnSync as spawnSync4 } from "child_process";
6
+ import { hostname as hostname2, homedir as homedir7 } from "os";
7
+ import { spawnSync as spawnSync5 } from "child_process";
8
8
 
9
9
  // src/config.ts
10
- import { readFileSync as readFileSync5, existsSync as existsSync4 } from "fs";
11
- import { resolve, dirname as dirname3, isAbsolute, join as join5, basename as basename3 } from "path";
12
- import { homedir as homedir3, tmpdir } from "os";
10
+ import { readFileSync as readFileSync5, existsSync as existsSync5 } from "fs";
11
+ import { resolve, dirname as dirname3, isAbsolute, join as join6, basename as basename3 } from "path";
12
+ import { homedir as homedir4, tmpdir } from "os";
13
13
  import { parse as parse2 } from "yaml";
14
14
 
15
15
  // src/events.ts
@@ -158,10 +158,10 @@ var Emitter = class _Emitter {
158
158
  var eventFileFor = (stateDir) => join(stateDir, "events.jsonl");
159
159
 
160
160
  // src/pair-install.ts
161
- import { existsSync as existsSync2, mkdirSync as mkdirSync2, readFileSync as readFileSync3, rmSync as rmSync2, writeFileSync } from "fs";
162
- import { spawnSync as spawnSync2 } from "child_process";
163
- import { homedir as homedir2 } from "os";
164
- import { dirname as dirname2, join as join3 } from "path";
161
+ import { existsSync as existsSync3, rmSync as rmSync4, writeFileSync as writeFileSync3 } from "fs";
162
+ import { spawnSync as spawnSync3 } from "child_process";
163
+ import { homedir as homedir3 } from "os";
164
+ import { join as join4 } from "path";
165
165
 
166
166
  // src/stream.ts
167
167
  function toolTarget(name, input) {
@@ -409,7 +409,15 @@ var claudeAdapter = {
409
409
  ensureMcp: ensureClaudeMcp
410
410
  };
411
411
 
412
- // src/pair-install.ts
412
+ // src/adapters/codex.ts
413
+ import { spawnSync as spawnSync2 } from "child_process";
414
+ import { homedir as homedir2 } from "os";
415
+ import { join as join3 } from "path";
416
+ import { rmSync as rmSync3, writeFileSync as writeFileSync2 } from "fs";
417
+
418
+ // src/pair-files.ts
419
+ import { existsSync as existsSync2, mkdirSync as mkdirSync2, readFileSync as readFileSync3, writeFileSync, rmSync as rmSync2 } from "fs";
420
+ import { dirname as dirname2 } from "path";
413
421
  var PAIR_BLOCK_START = "<!-- crew:pair start -->";
414
422
  var PAIR_BLOCK_END = "<!-- crew:pair end -->";
415
423
  var PAIR_KEY_VAR = "CREW_API_KEY";
@@ -497,9 +505,6 @@ function tomlTableRange(lines, name) {
497
505
  return [start, end];
498
506
  }
499
507
  var tomlString = (v) => JSON.stringify(v);
500
- function codexTable(url) {
501
- return [`[mcp_servers.${SERVER}]`, `url = ${tomlString(url)}`, `bearer_token_env_var = ${tomlString(PAIR_KEY_VAR)}`];
502
- }
503
508
  function upsertTomlTable(text, name, table2) {
504
509
  const lines = text.split("\n");
505
510
  const r = tomlTableRange(lines, name);
@@ -535,6 +540,69 @@ function tomlValue(lines, range, key) {
535
540
  }
536
541
  return void 0;
537
542
  }
543
+
544
+ // src/adapters/codex.ts
545
+ var CODEX_BILLING_VARS_TO_UNSET = [
546
+ "OPENAI_API_KEY",
547
+ "OPENAI_BASE_URL",
548
+ "OPENAI_API_BASE",
549
+ "OPENAI_ORG_ID",
550
+ "OPENAI_PROJECT",
551
+ "CODEX_API_KEY"
552
+ ];
553
+ function codexSandboxFor(p) {
554
+ return p.level === "read" ? "read-only" : "danger-full-access";
555
+ }
556
+ var REASONING_EFFORTS = ["minimal", "low", "medium", "high"];
557
+ function commandTarget(command2) {
558
+ if (!command2) return void 0;
559
+ const wrapped = /^(?:\S*\/)?(?:ba|z|da)?sh\s+-\w*c\s+(['"]?)([\s\S]*)\1$/.exec(command2.trim());
560
+ const inner = (wrapped ? wrapped[2] : command2).trim();
561
+ return inner.split(/\s+/)[0] || void 0;
562
+ }
563
+ function mapCodexLine(line) {
564
+ const l = line;
565
+ if (!l || l.type !== "item.completed" || !l.item) return [];
566
+ const it = l.item;
567
+ switch (it.type) {
568
+ case "reasoning":
569
+ return it.text ? [{ kind: "thought", text: it.text }] : [];
570
+ case "agent_message":
571
+ return it.text ? [{ kind: "text", text: it.text }] : [];
572
+ case "command_execution":
573
+ return [{ kind: "tool", tool: "command", target: commandTarget(it.command) }];
574
+ case "file_change":
575
+ return [{ kind: "tool", tool: "file_change", target: it.changes?.[0]?.path }];
576
+ case "mcp_tool_call":
577
+ return [{ kind: "tool", tool: `mcp__${it.server ?? "mcp"}__${it.tool ?? "tool"}` }];
578
+ case "web_search":
579
+ return [{ kind: "tool", tool: "web_search", target: it.query }];
580
+ default:
581
+ return [];
582
+ }
583
+ }
584
+ function extractCodexResult(line) {
585
+ const l = line;
586
+ if (!l) return void 0;
587
+ if (l.type === "thread.started" && l.thread_id) return { sessionId: l.thread_id };
588
+ if (l.type === "turn.completed" && l.usage) {
589
+ return {
590
+ inputTokens: l.usage.input_tokens,
591
+ outputTokens: l.usage.output_tokens,
592
+ cacheReadTokens: l.usage.cached_input_tokens,
593
+ cacheWriteTokens: l.usage.cache_write_input_tokens
594
+ };
595
+ }
596
+ return void 0;
597
+ }
598
+ function extractCodexTurnTokens(line) {
599
+ const l = line;
600
+ if (!l || l.type !== "turn.completed" || !l.usage) return void 0;
601
+ return (l.usage.input_tokens ?? 0) + (l.usage.output_tokens ?? 0);
602
+ }
603
+ function codexTable(url) {
604
+ return [`[mcp_servers.${SERVER}]`, `url = ${tomlString(url)}`, `bearer_token_env_var = ${tomlString(PAIR_KEY_VAR)}`];
605
+ }
538
606
  var codexPaths = (home) => ({
539
607
  instructions: join3(home, ".codex", "AGENTS.md"),
540
608
  config: join3(home, ".codex", "config.toml")
@@ -587,16 +655,105 @@ var codexPair = {
587
655
  const kept = st.state === "different" ? `; left the existing ${SERVER} server alone \u2014 ${st.detail}` : "";
588
656
  if (hadMcp && !o.dryRun) {
589
657
  const after = removeTomlTable(config, `mcp_servers.${SERVER}`);
590
- if (after === "") rmSync2(p.config);
591
- else writeFileSync(p.config, after);
658
+ if (after === "") rmSync3(p.config);
659
+ else writeFileSync2(p.config, after);
592
660
  }
593
661
  const what = hadBlock && hadMcp ? `the Pair block and ${SERVER} server` : hadBlock ? "the Pair block" : `the ${SERVER} server`;
594
662
  return hadBlock || hadMcp ? `pair(codex): ${o.dryRun ? "(dry run) would remove" : "removed"} ${what}${kept}` : `pair(codex): nothing to remove${kept}`;
595
663
  }
596
664
  };
665
+ function binMissing(bin) {
666
+ const r = spawnSync2(bin, ["--version"], { encoding: "utf8", timeout: 3e4 });
667
+ return r.error?.code === "ENOENT";
668
+ }
669
+ var codexAdapter = {
670
+ harness: "codex",
671
+ displayName: "Codex",
672
+ vendor: "openai",
673
+ ownsModel: (model) => /^(gpt-|o\d|codex-)/.test(model),
674
+ // The tier's reasoning is Codex's effort level; anything else is ignored.
675
+ applyReasoning: (spec, reasoning) => REASONING_EFFORTS.includes(reasoning) ? { ...spec, reasoningEffort: reasoning } : spec,
676
+ defaultBin: "codex",
677
+ // What Codex CLI itself starts with today (the `gpt-5-codex` family the first
678
+ // cut named now answers 404 "does not exist or you do not have access" from
679
+ // the Responses API for a 2026 API key, which the recording for CREW-1520
680
+ // hit first). An operator pins something else per toolchain entry or tier.
681
+ defaultModel: "gpt-5.5",
682
+ defaultTiers: {
683
+ light: { model: "gpt-5.4-mini", reasoning: "low" },
684
+ standard: { model: "gpt-5.5" },
685
+ deep: { model: "gpt-5.5", reasoning: "high" }
686
+ },
687
+ limitations: [
688
+ "no per-tool allow or deny list, and a push (even a forced one) is not blocked",
689
+ "the write (QA) and edit (dev, design) profiles run with no filesystem sandbox (danger-full-access): Codex's workspace-write keeps every .git directory read-only, so a sandboxed seat could not cut a worktree or commit, and a headless run cannot approve the escalation",
690
+ "the read profile (triage) runs in the read-only sandbox, which has no network, so it can reach the tracker only through the MCP tools, not curl",
691
+ "the design canvas and skills are unavailable",
692
+ "no cost is reported (tokens only)"
693
+ ],
694
+ invocation(spec, o) {
695
+ const profile = profileFor(o.role);
696
+ const mode = codexSandboxFor(profile);
697
+ const cfg = (k, v) => ["-c", `${k}=${tomlString(v)}`];
698
+ const args = [
699
+ "exec",
700
+ "--json",
701
+ "--skip-git-repo-check",
702
+ "--sandbox",
703
+ mode,
704
+ "--model",
705
+ o.model,
706
+ // Headless: nobody can answer an approval prompt.
707
+ "-c",
708
+ 'approval_policy="never"',
709
+ ...spec.reasoningEffort ? cfg("model_reasoning_effort", spec.reasoningEffort) : [],
710
+ // The tracker MCP for this run only, as -c overrides: the config file is
711
+ // untouched and the key is named by variable, never written (§7).
712
+ // `default_tools_approval_mode="approve"` is load-bearing: Codex asks
713
+ // before an MCP tool call by default, and under `approval_policy="never"`
714
+ // that question is auto-rejected ("MCP tool call requires approval, but
715
+ // approval policy is never" — seen on the first real recording), which
716
+ // would cut a headless seat off from the tracker entirely.
717
+ ...o.mcp ? [
718
+ ...cfg(`mcp_servers.${SERVER}.url`, o.mcp.url),
719
+ ...cfg(`mcp_servers.${SERVER}.bearer_token_env_var`, o.mcp.keyVar),
720
+ ...cfg(`mcp_servers.${SERVER}.default_tools_approval_mode`, "approve")
721
+ ] : [],
722
+ ...spec.args,
723
+ // Prompt on stdin.
724
+ "-"
725
+ ];
726
+ return spec.launcher ? { bin: spec.launcher, args: [spec.bin, ...args] } : { bin: spec.bin, args };
727
+ },
728
+ runEnv: (spec) => ({ ...spec.env }),
729
+ provider: () => "openai",
730
+ unsetEnv: CODEX_BILLING_VARS_TO_UNSET,
731
+ mapStreamLine: mapCodexLine,
732
+ extractResult: extractCodexResult,
733
+ extractTurnTokens: extractCodexTurnTokens,
734
+ inspectMcp(o) {
735
+ if (binMissing(o.bin)) return { state: "no-binary" };
736
+ const st = codexMcpState(readIfExists(codexPaths(o.home ?? homedir2()).config) ?? "", o.url);
737
+ return st.state === "registered" ? { state: "registered", url: o.url } : st;
738
+ },
739
+ ensureMcp(o) {
740
+ const p = codexPaths(o.home ?? homedir2());
741
+ const config = readIfExists(p.config) ?? "";
742
+ const st = codexMcpState(config, o.url);
743
+ if (st.state === "registered") return `mcp: ${SERVER} already registered with Codex \u2192 ${o.url}`;
744
+ if (st.state === "different" && !o.replace) {
745
+ return `mcp: left Codex's "${SERVER}" server alone \u2014 ${st.detail}. Re-run with --mcp replace to overwrite it.`;
746
+ }
747
+ if (o.dryRun) return `(dry run) would register the ${SERVER} MCP server with Codex \u2192 ${o.url}`;
748
+ writeFileMkdir(p.config, upsertTomlTable(config, `mcp_servers.${SERVER}`, codexTable(o.url)));
749
+ return `mcp: registered the ${SERVER} MCP server with Codex \u2192 ${o.url} (${p.config}; key read from $${PAIR_KEY_VAR})`;
750
+ }
751
+ };
752
+
753
+ // src/pair-install.ts
597
754
  var geminiPaths = (home) => ({
598
- instructions: join3(home, ".gemini", "GEMINI.md"),
599
- config: join3(home, ".gemini", "settings.json")
755
+ instructions: join4(home, ".gemini", "GEMINI.md"),
756
+ config: join4(home, ".gemini", "settings.json")
600
757
  });
601
758
  function readGeminiSettings(path) {
602
759
  const text = readIfExists(path);
@@ -617,14 +774,14 @@ function geminiMcpState(s, url) {
617
774
  return { state: "registered" };
618
775
  }
619
776
  function inspectGeminiMcp(o) {
620
- const p = geminiPaths(o.home ?? homedir2());
777
+ const p = geminiPaths(o.home ?? homedir3());
621
778
  const settings = readGeminiSettings(p.config);
622
779
  if (!settings) return { state: "different", detail: `${p.config} is not a JSON object` };
623
780
  const st = geminiMcpState(settings, o.url);
624
781
  return st.state === "registered" ? { state: "registered", url: o.url } : st;
625
782
  }
626
783
  function ensureGeminiMcp(o) {
627
- const p = geminiPaths(o.home ?? homedir2());
784
+ const p = geminiPaths(o.home ?? homedir3());
628
785
  const settings = readGeminiSettings(p.config);
629
786
  if (!settings) return `gemini: ${p.config} is not a JSON object \u2014 fix it by hand; nothing was written`;
630
787
  const st = geminiMcpState(settings, o.url);
@@ -642,7 +799,7 @@ var geminiPair = {
642
799
  harness: "gemini",
643
800
  displayName: "Gemini CLI",
644
801
  installPair(o) {
645
- const p = geminiPaths(o.home ?? homedir2());
802
+ const p = geminiPaths(o.home ?? homedir3());
646
803
  const settings = readGeminiSettings(p.config);
647
804
  if (!settings) return `pair(gemini): ${p.config} is not a JSON object \u2014 fix it by hand; nothing was written`;
648
805
  const st = geminiMcpState(settings, o.url);
@@ -661,7 +818,7 @@ var geminiPair = {
661
818
  return `pair(gemini): ${o.dryRun ? "(dry run) would install" : "installed"} the Pair block in ${p.instructions} and ${SERVER} in ${p.config} (key read from $${PAIR_KEY_VAR})`;
662
819
  },
663
820
  verifyPair(o) {
664
- const p = geminiPaths(o.home ?? homedir2());
821
+ const p = geminiPaths(o.home ?? homedir3());
665
822
  const b = blockVerdict(p.instructions, "gemini");
666
823
  if (!b.ok) return b;
667
824
  const settings = readGeminiSettings(p.config);
@@ -672,7 +829,7 @@ var geminiPair = {
672
829
  return { ok: true, detail: `${p.instructions}, ${p.config}` };
673
830
  },
674
831
  removePair(o) {
675
- const p = geminiPaths(o.home ?? homedir2());
832
+ const p = geminiPaths(o.home ?? homedir3());
676
833
  const hadBlock = removeBlockFile(p.instructions, o.dryRun);
677
834
  const settings = readGeminiSettings(p.config);
678
835
  const st = settings ? geminiMcpState(settings, o.url) : { state: "missing" };
@@ -683,8 +840,8 @@ var geminiPair = {
683
840
  const next = { ...settings };
684
841
  if (Object.keys(rest).length > 0) next.mcpServers = rest;
685
842
  else delete next.mcpServers;
686
- if (Object.keys(next).length === 0) rmSync2(p.config);
687
- else writeFileSync(p.config, `${JSON.stringify(next, null, 2)}
843
+ if (Object.keys(next).length === 0) rmSync4(p.config);
844
+ else writeFileSync3(p.config, `${JSON.stringify(next, null, 2)}
688
845
  `);
689
846
  }
690
847
  const what = hadBlock && hadMcp ? `the Pair block and ${SERVER} server` : hadBlock ? "the Pair block" : `the ${SERVER} server`;
@@ -693,11 +850,11 @@ var geminiPair = {
693
850
  };
694
851
  var VSCODE_FLAVOURS = ["Code", "Code - Insiders"];
695
852
  function vscodeUserDirs(o) {
696
- const home = o.home ?? homedir2();
853
+ const home = o.home ?? homedir3();
697
854
  const platform2 = o.platform ?? process.platform;
698
- const root = platform2 === "darwin" ? join3(home, "Library", "Application Support") : platform2 === "win32" ? process.env.APPDATA ?? join3(home, "AppData", "Roaming") : join3(home, ".config");
699
- const all = VSCODE_FLAVOURS.map((f) => join3(root, f, "User"));
700
- const present = all.filter((d) => existsSync2(d));
855
+ const root = platform2 === "darwin" ? join4(home, "Library", "Application Support") : platform2 === "win32" ? process.env.APPDATA ?? join4(home, "AppData", "Roaming") : join4(home, ".config");
856
+ const all = VSCODE_FLAVOURS.map((f) => join4(root, f, "User"));
857
+ const present = all.filter((d) => existsSync3(d));
701
858
  return present.length > 0 ? present : [all[0]];
702
859
  }
703
860
  var COPILOT_FRONTMATTER = '---\napplyTo: "**"\n---\n\n';
@@ -734,8 +891,8 @@ function removeCopilotInstructions(path, dryRun) {
734
891
  if (before === void 0 || !hasPairBlock(before)) return false;
735
892
  if (dryRun) return true;
736
893
  const after = removePairBlock(before);
737
- if (after.trim() === "" || after.trim() === COPILOT_FRONTMATTER.trim()) rmSync2(path);
738
- else writeFileSync(path, after);
894
+ if (after.trim() === "" || after.trim() === COPILOT_FRONTMATTER.trim()) rmSync4(path);
895
+ else writeFileSync3(path, after);
739
896
  return true;
740
897
  }
741
898
  var copilotPair = {
@@ -744,8 +901,8 @@ var copilotPair = {
744
901
  installPair(o) {
745
902
  const lines = [];
746
903
  for (const dir of vscodeUserDirs(o)) {
747
- const mcpPath = join3(dir, "mcp.json");
748
- const instrPath = join3(dir, "prompts", "crew-pair.instructions.md");
904
+ const mcpPath = join4(dir, "mcp.json");
905
+ const instrPath = join4(dir, "prompts", "crew-pair.instructions.md");
749
906
  const settings = readVscodeMcp(mcpPath);
750
907
  if (!settings) {
751
908
  lines.push(`pair(copilot): ${mcpPath} is not a plain JSON object (comments are not supported) \u2014 fix it by hand; nothing was written there`);
@@ -773,8 +930,8 @@ var copilotPair = {
773
930
  },
774
931
  verifyPair(o) {
775
932
  const verdicts = vscodeUserDirs(o).map((dir) => {
776
- const instrPath = join3(dir, "prompts", "crew-pair.instructions.md");
777
- const mcpPath = join3(dir, "mcp.json");
933
+ const instrPath = join4(dir, "prompts", "crew-pair.instructions.md");
934
+ const mcpPath = join4(dir, "mcp.json");
778
935
  const b = blockVerdict(instrPath, "copilot");
779
936
  if (!b.ok) return b;
780
937
  const settings = readVscodeMcp(mcpPath);
@@ -789,8 +946,8 @@ var copilotPair = {
789
946
  removePair(o) {
790
947
  const lines = [];
791
948
  for (const dir of vscodeUserDirs(o)) {
792
- const mcpPath = join3(dir, "mcp.json");
793
- const hadBlock = removeCopilotInstructions(join3(dir, "prompts", "crew-pair.instructions.md"), o.dryRun);
949
+ const mcpPath = join4(dir, "mcp.json");
950
+ const hadBlock = removeCopilotInstructions(join4(dir, "prompts", "crew-pair.instructions.md"), o.dryRun);
794
951
  const settings = readVscodeMcp(mcpPath);
795
952
  const st = settings ? copilotMcpState(settings, o.url) : { state: "missing" };
796
953
  const hadMcp = st.state === "registered";
@@ -800,8 +957,8 @@ var copilotPair = {
800
957
  const next = { ...settings };
801
958
  if (Object.keys(rest).length > 0) next.servers = rest;
802
959
  else delete next.servers;
803
- if (Object.keys(next).length === 0) rmSync2(mcpPath);
804
- else writeFileSync(mcpPath, `${JSON.stringify(next, null, 2)}
960
+ if (Object.keys(next).length === 0) rmSync4(mcpPath);
961
+ else writeFileSync3(mcpPath, `${JSON.stringify(next, null, 2)}
805
962
  `);
806
963
  }
807
964
  const what = hadBlock && hadMcp ? `the Pair instructions and ${SERVER} server` : hadBlock ? "the Pair instructions" : `the ${SERVER} server`;
@@ -817,7 +974,7 @@ var copilotPair = {
817
974
  envWarning(o) {
818
975
  if ((o.platform ?? process.platform) !== "darwin") return void 0;
819
976
  const get2 = o.loginEnv ?? ((name) => {
820
- const r = spawnSync2("launchctl", ["getenv", name], { encoding: "utf8" });
977
+ const r = spawnSync3("launchctl", ["getenv", name], { encoding: "utf8" });
821
978
  return r.status === 0 ? r.stdout.trim() : void 0;
822
979
  });
823
980
  if (get2(PAIR_KEY_VAR)) return void 0;
@@ -963,6 +1120,7 @@ var geminiAdapter = {
963
1120
  var TIERS = ["light", "standard", "deep"];
964
1121
  var ADAPTERS = {
965
1122
  [claudeAdapter.harness]: claudeAdapter,
1123
+ [codexAdapter.harness]: codexAdapter,
966
1124
  [geminiAdapter.harness]: geminiAdapter
967
1125
  };
968
1126
  var HARNESS_NAMES = Object.keys(ADAPTERS);
@@ -991,8 +1149,8 @@ function shipToolchains(ship) {
991
1149
  };
992
1150
  return [legacy, ...explicit];
993
1151
  }
994
- function resolveToolchain(ship, route) {
995
- const name = route?.toolchain ?? ship.toolchain ?? LEGACY_TOOLCHAIN;
1152
+ function resolveToolchain(ship, route, repo) {
1153
+ const name = (repo && route?.repoOverrides?.[repo]?.toolchain) ?? route?.toolchain ?? ship.toolchain ?? LEGACY_TOOLCHAIN;
996
1154
  const found = shipToolchains(ship).find((t) => t.name === name);
997
1155
  if (!found) throw new Error(`toolchain "${name}" is not defined (have: ${shipToolchains(ship).map((t) => t.name).join(", ")})`);
998
1156
  return found;
@@ -1007,11 +1165,12 @@ function isOriLauncher(spec) {
1007
1165
  function toolchainProvider(spec) {
1008
1166
  return isOriLauncher(spec) ? "openrouter" : adapterFor(spec.harness).provider(spec);
1009
1167
  }
1010
- function resolveSeat(ship, route, role, persona = {}) {
1168
+ function resolveSeat(ship, route, role, persona = {}, repo) {
1011
1169
  const warnings = [];
1012
- const base = resolveToolchain(ship, route);
1170
+ const base = resolveToolchain(ship, route, repo);
1013
1171
  const baseVendor = toolchainVendor(base);
1014
- const baseWhy = route?.toolchain ? `route toolchain ${base.name}` : ship.toolchain ? `ship default ${base.name}` : `default ${base.name}`;
1172
+ const repoToolchain = repo ? route?.repoOverrides?.[repo]?.toolchain : void 0;
1173
+ const baseWhy = repoToolchain ? `repo ${repo} toolchain ${base.name}` : route?.toolchain ? `route toolchain ${base.name}` : ship.toolchain ? `ship default ${base.name}` : `default ${base.name}`;
1015
1174
  let chosen = base;
1016
1175
  let toolchainWhy = baseWhy;
1017
1176
  if (persona.vendor && persona.vendor !== baseVendor) {
@@ -1090,8 +1249,8 @@ function explain(ship, required) {
1090
1249
  }
1091
1250
 
1092
1251
  // src/repo-config.ts
1093
- import { readFileSync as readFileSync4, existsSync as existsSync3 } from "fs";
1094
- import { join as join4, basename as basename2 } from "path";
1252
+ import { readFileSync as readFileSync4, existsSync as existsSync4 } from "fs";
1253
+ import { join as join5, basename as basename2 } from "path";
1095
1254
  import { parse } from "yaml";
1096
1255
  var SPEC_VERSION = 1;
1097
1256
  var REPO_CONFIG_FILENAMES = [".crew.yaml", ".crew.yml", ".crew.json"];
@@ -1164,8 +1323,8 @@ var MODES = ["local", "integrate", "ci_manual", "ci_auto", "external"];
1164
1323
  var PROVIDERS = ["github", "buildkite", "other", "none"];
1165
1324
  function findRepoConfig(dir) {
1166
1325
  for (const name of REPO_CONFIG_FILENAMES) {
1167
- const p = join4(dir, name);
1168
- if (existsSync3(p)) return p;
1326
+ const p = join5(dir, name);
1327
+ if (existsSync4(p)) return p;
1169
1328
  }
1170
1329
  return null;
1171
1330
  }
@@ -1710,7 +1869,7 @@ function parseWorktreeName(v, where, file) {
1710
1869
  if (problem) throw new ConfigError(`${file}: ${where}.worktreeName ("${String(v)}") ${problem}`);
1711
1870
  return String(v);
1712
1871
  }
1713
- var REPO_ENTRY_KEYS = /* @__PURE__ */ new Set(["dir", "hooks", "labels", "release", "branch", "hostPassengers", "worktreeName"]);
1872
+ var REPO_ENTRY_KEYS = /* @__PURE__ */ new Set(["dir", "hooks", "labels", "release", "branch", "hostPassengers", "worktreeName", "toolchain"]);
1714
1873
  var BRANCH_OVERRIDE_KEYS = /* @__PURE__ */ new Set(["base", "name", "push", "remote"]);
1715
1874
  var RELEASE_OVERRIDE_KEYS = /* @__PURE__ */ new Set([
1716
1875
  "mode",
@@ -1807,16 +1966,16 @@ function mergeRouteRelease(route, override) {
1807
1966
  };
1808
1967
  }
1809
1968
  function expand(p, base) {
1810
- let out = p.startsWith("~") ? p.replace(/^~/, homedir3()) : p;
1811
- out = out.replace(/\$\{?HOME\}?/g, homedir3());
1969
+ let out = p.startsWith("~") ? p.replace(/^~/, homedir4()) : p;
1970
+ out = out.replace(/\$\{?HOME\}?/g, homedir4());
1812
1971
  return isAbsolute(out) ? out : resolve(base, out);
1813
1972
  }
1814
1973
  function resolvePromptsDir(promptSet, crewHome, base) {
1815
- if (!promptSet) return join5(crewHome, "prompts", "default");
1974
+ if (!promptSet) return join6(crewHome, "prompts", "default");
1816
1975
  if (promptSet.includes("/") || promptSet.startsWith(".") || promptSet.startsWith("~")) {
1817
1976
  return expand(promptSet, base);
1818
1977
  }
1819
- return join5(crewHome, "prompts", promptSet);
1978
+ return join6(crewHome, "prompts", promptSet);
1820
1979
  }
1821
1980
  var Missing = class {
1822
1981
  paths = [];
@@ -1840,7 +1999,7 @@ var Missing = class {
1840
1999
  };
1841
2000
  function crewVersion(crewHome) {
1842
2001
  try {
1843
- const pkg = JSON.parse(readFileSync5(join5(crewHome, "package.json"), "utf8"));
2002
+ const pkg = JSON.parse(readFileSync5(join6(crewHome, "package.json"), "utf8"));
1844
2003
  return pkg.version ?? "0.0.0";
1845
2004
  } catch {
1846
2005
  return "0.0.0";
@@ -1850,26 +2009,26 @@ function configSearchPath(crewHome) {
1850
2009
  if (process.env.CREW_CONFIG) return [resolve(process.env.CREW_CONFIG)];
1851
2010
  const xdg = process.env.XDG_CONFIG_HOME;
1852
2011
  return [
1853
- ...xdg ? [join5(xdg, "crew", "crew.yaml")] : [],
1854
- join5(homedir3(), ".config", "crew", "crew.yaml"),
2012
+ ...xdg ? [join6(xdg, "crew", "crew.yaml")] : [],
2013
+ join6(homedir4(), ".config", "crew", "crew.yaml"),
1855
2014
  resolve(crewHome, "crew.yaml")
1856
2015
  ];
1857
2016
  }
1858
2017
  function findConfigFile(crewHome) {
1859
2018
  const candidates = configSearchPath(crewHome);
1860
- return candidates.find((c) => existsSync4(c)) ?? candidates[candidates.length - 1];
2019
+ return candidates.find((c) => existsSync5(c)) ?? candidates[candidates.length - 1];
1861
2020
  }
1862
- var defaultConfigPath = () => join5(process.env.XDG_CONFIG_HOME ?? join5(homedir3(), ".config"), "crew", "crew.yaml");
2021
+ var defaultConfigPath = () => join6(process.env.XDG_CONFIG_HOME ?? join6(homedir4(), ".config"), "crew", "crew.yaml");
1863
2022
  function routeSlug(route) {
1864
2023
  return route.replace(/\//g, "-");
1865
2024
  }
1866
2025
  function resolvedPathFor(stateDir, route) {
1867
2026
  const [workspace, project] = splitRoute(route);
1868
- return join5(stateDir, "resolved", workspace, `${project}.json`);
2027
+ return join6(stateDir, "resolved", workspace, `${project}.json`);
1869
2028
  }
1870
2029
  function apiKeyPathFor(stateDir, route) {
1871
2030
  const [workspace, project] = splitRoute(route);
1872
- return join5(stateDir, "keys", workspace, `${project}.env`);
2031
+ return join6(stateDir, "keys", workspace, `${project}.env`);
1873
2032
  }
1874
2033
  function splitRoute(route) {
1875
2034
  const parts = route.split("/");
@@ -1880,7 +2039,7 @@ function splitRoute(route) {
1880
2039
  }
1881
2040
  function readResolvedFile(stateDir, route) {
1882
2041
  const path = resolvedPathFor(stateDir, route);
1883
- if (!existsSync4(path)) return void 0;
2042
+ if (!existsSync5(path)) return void 0;
1884
2043
  try {
1885
2044
  return JSON.parse(readFileSync5(path, "utf8"));
1886
2045
  } catch (e) {
@@ -1938,13 +2097,13 @@ function parseResolved(raw, m, where) {
1938
2097
  }
1939
2098
  function loadConfig(crewHome, configFile, opts = {}) {
1940
2099
  const file = configFile ?? findConfigFile(crewHome);
1941
- if (opts.text === void 0 && !existsSync4(file)) {
2100
+ if (opts.text === void 0 && !existsSync5(file)) {
1942
2101
  throw new ConfigError(
1943
2102
  `no config found. Looked in:
1944
2103
  ` + configSearchPath(crewHome).map((c) => ` - ${c}`).join("\n") + `
1945
2104
 
1946
2105
  Create the first of those:
1947
- mkdir -p ${dirname3(defaultConfigPath())} && cp ${join5(crewHome, "crew.example.yaml")} ${defaultConfigPath()}`
2106
+ mkdir -p ${dirname3(defaultConfigPath())} && cp ${join6(crewHome, "crew.example.yaml")} ${defaultConfigPath()}`
1948
2107
  );
1949
2108
  }
1950
2109
  let raw;
@@ -2027,7 +2186,7 @@ Create the first of those:
2027
2186
  useNvm: shipRaw.useNvm !== false,
2028
2187
  nvmSh: shipRaw.nvmSh,
2029
2188
  stateDir,
2030
- logFile: expand(shipRaw.logFile ?? join5(tmpdir(), "crew.log"), base),
2189
+ logFile: expand(shipRaw.logFile ?? join6(tmpdir(), "crew.log"), base),
2031
2190
  userAgent: shipRaw.userAgent ?? `Mozilla/5.0 CrewAgent/${crewVersion(crewHome)}`,
2032
2191
  maxConcurrentAgents,
2033
2192
  streamRetentionDays,
@@ -2140,7 +2299,11 @@ function parseOneRoute(c, where, file, base, stateDir, raw, missing, crewHome, t
2140
2299
  const e = entry;
2141
2300
  rejectUnknownKeys(e, REPO_ENTRY_KEYS, `${where}.repos.${name}`, file);
2142
2301
  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) {
2302
+ const repoToolchain = e.toolchain === void 0 ? void 0 : String(e.toolchain);
2303
+ if (repoToolchain !== void 0 && !toolchainNames.has(repoToolchain)) {
2304
+ missing.add(`${where}.repos.${name}.toolchain "${repoToolchain}" is not defined in ship.toolchains (have: ${[...toolchainNames].join(", ")})`);
2305
+ }
2306
+ if (e.hooks || e.labels || e.release || e.branch || e.hostPassengers !== void 0 || e.worktreeName !== void 0 || repoToolchain !== void 0) {
2144
2307
  repoOverrides[name] = {
2145
2308
  hooks: e.hooks,
2146
2309
  labels: e.labels,
@@ -2151,7 +2314,8 @@ function parseOneRoute(c, where, file, base, stateDir, raw, missing, crewHome, t
2151
2314
  // compare this literally.
2152
2315
  ...e.branch ? { branch: parseBranchOverride(e.branch, `${where}.repos.${name}`, file) } : {},
2153
2316
  ...e.hostPassengers !== void 0 ? { hostPassengers: e.hostPassengers === true } : {},
2154
- ...e.worktreeName !== void 0 ? { worktreeName: parseWorktreeName(e.worktreeName, `${where}.repos.${name}`, file) } : {}
2317
+ ...e.worktreeName !== void 0 ? { worktreeName: parseWorktreeName(e.worktreeName, `${where}.repos.${name}`, file) } : {},
2318
+ ...repoToolchain !== void 0 ? { toolchain: repoToolchain } : {}
2155
2319
  };
2156
2320
  }
2157
2321
  } else {
@@ -2242,7 +2406,7 @@ function repoIdForName(r, name) {
2242
2406
  }
2243
2407
  function resolveApiKey(r) {
2244
2408
  if (r.apiKey) return r.apiKey;
2245
- if (r.apiKeyFile && r.apiKeyVar && existsSync4(r.apiKeyFile)) {
2409
+ if (r.apiKeyFile && r.apiKeyVar && existsSync5(r.apiKeyFile)) {
2246
2410
  for (const line of readFileSync5(r.apiKeyFile, "utf8").split("\n")) {
2247
2411
  const m = line.match(new RegExp(`^${r.apiKeyVar}=(.*)$`));
2248
2412
  if (m) return (m[1] ?? "").trim().replace(/^["']|["']$/g, "");
@@ -2306,7 +2470,7 @@ function configuredMembers(rt) {
2306
2470
  return out;
2307
2471
  }
2308
2472
  function defaultRepoDir(basePath, workspace, repoName) {
2309
- return join5(basePath, workspace, repoName);
2473
+ return join6(basePath, workspace, repoName);
2310
2474
  }
2311
2475
  function reposOf(r) {
2312
2476
  const named = Object.entries(r.repos);
@@ -2330,7 +2494,7 @@ function passengerRepoTargets(r) {
2330
2494
  return reposOf(r).filter((t) => r.repoOverrides[t.name]?.hostPassengers !== false);
2331
2495
  }
2332
2496
  function passengerCheckoutDir(basePath, workspace, repoName) {
2333
- return join5(basePath, workspace, ".passenger-checkouts", repoName);
2497
+ return join6(basePath, workspace, ".passenger-checkouts", repoName);
2334
2498
  }
2335
2499
  function passengerCheckoutTargets(r) {
2336
2500
  if (!r.hostPassengers) return [];
@@ -2462,8 +2626,8 @@ function rosterMarkdown(roster, meId) {
2462
2626
  }
2463
2627
 
2464
2628
  // src/state.ts
2465
- import { readFileSync as readFileSync6, writeFileSync as writeFileSync2, existsSync as existsSync5, mkdirSync as mkdirSync3, rmSync as rmSync3, statSync as statSync2 } from "fs";
2466
- import { join as join6 } from "path";
2629
+ import { readFileSync as readFileSync6, writeFileSync as writeFileSync4, existsSync as existsSync6, mkdirSync as mkdirSync3, rmSync as rmSync5, statSync as statSync2 } from "fs";
2630
+ import { join as join7 } from "path";
2467
2631
  var EPOCH = "1970-01-01T00:00:00.000Z";
2468
2632
  var State = class _State {
2469
2633
  dir;
@@ -2472,7 +2636,7 @@ var State = class _State {
2472
2636
  mkdirSync3(dir, { recursive: true });
2473
2637
  }
2474
2638
  path(...parts) {
2475
- return join6(this.dir, ...parts);
2639
+ return join7(this.dir, ...parts);
2476
2640
  }
2477
2641
  /**
2478
2642
  * The timestamp every "new since last poll" check is measured against.
@@ -2490,7 +2654,7 @@ var State = class _State {
2490
2654
  }
2491
2655
  }
2492
2656
  setWatermark(at) {
2493
- writeFileSync2(this.path(".poll-watermark"), `${at}
2657
+ writeFileSync4(this.path(".poll-watermark"), `${at}
2494
2658
  `);
2495
2659
  }
2496
2660
  /**
@@ -2508,19 +2672,19 @@ var State = class _State {
2508
2672
  }
2509
2673
  /** Whole-crew pause, and the per-role sentinels one level down. */
2510
2674
  isPaused() {
2511
- return existsSync5(this.path(".crew-paused"));
2675
+ return existsSync6(this.path(".crew-paused"));
2512
2676
  }
2513
2677
  isRolePaused(role) {
2514
- return existsSync5(this.path(`.role-paused-${role}`));
2678
+ return existsSync6(this.path(`.role-paused-${role}`));
2515
2679
  }
2516
2680
  pausedRoles(roles) {
2517
2681
  return new Set(roles.filter((r) => this.isRolePaused(r)));
2518
2682
  }
2519
2683
  pause(role) {
2520
- writeFileSync2(role ? this.path(`.role-paused-${role}`) : this.path(".crew-paused"), "");
2684
+ writeFileSync4(role ? this.path(`.role-paused-${role}`) : this.path(".crew-paused"), "");
2521
2685
  }
2522
2686
  resume(role) {
2523
- rmSync3(role ? this.path(`.role-paused-${role}`) : this.path(".crew-paused"), { force: true });
2687
+ rmSync5(role ? this.path(`.role-paused-${role}`) : this.path(".crew-paused"), { force: true });
2524
2688
  }
2525
2689
  /**
2526
2690
  * The release phase (merge, deploy, release) has its own sentinel, one
@@ -2528,11 +2692,11 @@ var State = class _State {
2528
2692
  * while agents keep working.
2529
2693
  */
2530
2694
  isReleasePaused() {
2531
- return existsSync5(this.path(".release-paused"));
2695
+ return existsSync6(this.path(".release-paused"));
2532
2696
  }
2533
2697
  setReleasePaused(paused) {
2534
- if (paused) writeFileSync2(this.path(".release-paused"), "");
2535
- else rmSync3(this.path(".release-paused"), { force: true });
2698
+ if (paused) writeFileSync4(this.path(".release-paused"), "");
2699
+ else rmSync5(this.path(".release-paused"), { force: true });
2536
2700
  }
2537
2701
  /**
2538
2702
  * Why a release cycle must not run right now, or null when it may. Every
@@ -2579,13 +2743,13 @@ var State = class _State {
2579
2743
  }
2580
2744
  } catch {
2581
2745
  }
2582
- writeFileSync2(file, `${process.pid}
2746
+ writeFileSync4(file, `${process.pid}
2583
2747
  `);
2584
2748
  return {
2585
2749
  ok: true,
2586
2750
  release: () => {
2587
2751
  try {
2588
- rmSync3(file, { force: true });
2752
+ rmSync5(file, { force: true });
2589
2753
  } catch {
2590
2754
  }
2591
2755
  }
@@ -2672,12 +2836,12 @@ var State = class _State {
2672
2836
  blockedCount: () => Number.parseInt(read(blocked) ?? "", 10) || 0,
2673
2837
  noteBlocked: () => {
2674
2838
  const n = (Number.parseInt(read(blocked) ?? "", 10) || 0) + 1;
2675
- writeFileSync2(this.path(blocked), `${n}
2839
+ writeFileSync4(this.path(blocked), `${n}
2676
2840
  `);
2677
2841
  return n;
2678
2842
  },
2679
2843
  clearBlocked: () => {
2680
- rmSync3(this.path(blocked), { force: true });
2844
+ rmSync5(this.path(blocked), { force: true });
2681
2845
  },
2682
2846
  /**
2683
2847
  * The commit a deploy last failed on.
@@ -2689,11 +2853,11 @@ var State = class _State {
2689
2853
  */
2690
2854
  deployFailedSha: () => read(failed),
2691
2855
  noteDeployFailed: (sha) => {
2692
- writeFileSync2(this.path(failed), `${sha}
2856
+ writeFileSync4(this.path(failed), `${sha}
2693
2857
  `);
2694
2858
  },
2695
2859
  clearDeployFailed: () => {
2696
- rmSync3(this.path(failed), { force: true });
2860
+ rmSync5(this.path(failed), { force: true });
2697
2861
  },
2698
2862
  /**
2699
2863
  * Consecutive test-gate failures on the same head (CREW-1368). A failure
@@ -2703,12 +2867,12 @@ var State = class _State {
2703
2867
  noteTestGateFailed: (sha) => {
2704
2868
  const [prev, n] = (read(gateFailed) ?? "").split(" ");
2705
2869
  const next = prev === sha ? (Number.parseInt(n ?? "", 10) || 0) + 1 : 1;
2706
- writeFileSync2(this.path(gateFailed), `${sha} ${next}
2870
+ writeFileSync4(this.path(gateFailed), `${sha} ${next}
2707
2871
  `);
2708
2872
  return next;
2709
2873
  },
2710
2874
  clearTestGateFailed: () => {
2711
- rmSync3(this.path(gateFailed), { force: true });
2875
+ rmSync5(this.path(gateFailed), { force: true });
2712
2876
  },
2713
2877
  /** Whether the board was already told about this head's red gate. */
2714
2878
  heldReported: () => {
@@ -2719,15 +2883,15 @@ var State = class _State {
2719
2883
  }
2720
2884
  },
2721
2885
  noteHeldReported: (signature) => {
2722
- writeFileSync2(this.path(heldReported), `${signature}
2886
+ writeFileSync4(this.path(heldReported), `${signature}
2723
2887
  `);
2724
2888
  },
2725
2889
  clearHeldReported: () => {
2726
- rmSync3(this.path(heldReported), { force: true });
2890
+ rmSync5(this.path(heldReported), { force: true });
2727
2891
  },
2728
2892
  testGateReported: (sha) => read(gateReported) === sha,
2729
2893
  noteTestGateReported: (sha) => {
2730
- writeFileSync2(this.path(gateReported), `${sha}
2894
+ writeFileSync4(this.path(gateReported), `${sha}
2731
2895
  `);
2732
2896
  }
2733
2897
  };
@@ -2756,7 +2920,7 @@ var State = class _State {
2756
2920
  }
2757
2921
  };
2758
2922
  const clear2 = () => {
2759
- for (const f of [streakFile, ticketFile, sinceFile, ticketStreakFile]) rmSync3(this.path(f), { force: true });
2923
+ for (const f of [streakFile, ticketFile, sinceFile, ticketStreakFile]) rmSync5(this.path(f), { force: true });
2760
2924
  };
2761
2925
  return {
2762
2926
  /** Consecutive cycles this route had actionable work and did not win. */
@@ -2783,18 +2947,18 @@ var State = class _State {
2783
2947
  clear2();
2784
2948
  return;
2785
2949
  }
2786
- writeFileSync2(this.path(streakFile), `${this.readInt(streakFile) + 1}
2950
+ writeFileSync4(this.path(streakFile), `${this.readInt(streakFile) + 1}
2787
2951
  `);
2788
2952
  const prior = read(ticketFile);
2789
2953
  if (topTicket && topTicket === prior) {
2790
- writeFileSync2(this.path(ticketStreakFile), `${this.readInt(ticketStreakFile) + 1}
2954
+ writeFileSync4(this.path(ticketStreakFile), `${this.readInt(ticketStreakFile) + 1}
2791
2955
  `);
2792
2956
  } else if (topTicket) {
2793
- writeFileSync2(this.path(ticketFile), `${topTicket}
2957
+ writeFileSync4(this.path(ticketFile), `${topTicket}
2794
2958
  `);
2795
- writeFileSync2(this.path(sinceFile), `${(/* @__PURE__ */ new Date()).toISOString()}
2959
+ writeFileSync4(this.path(sinceFile), `${(/* @__PURE__ */ new Date()).toISOString()}
2796
2960
  `);
2797
- writeFileSync2(this.path(ticketStreakFile), "1\n");
2961
+ writeFileSync4(this.path(ticketStreakFile), "1\n");
2798
2962
  }
2799
2963
  }
2800
2964
  };
@@ -2822,7 +2986,7 @@ var State = class _State {
2822
2986
  }
2823
2987
  },
2824
2988
  persist: (next) => {
2825
- writeFileSync2(this.path(file), JSON.stringify(next));
2989
+ writeFileSync4(this.path(file), JSON.stringify(next));
2826
2990
  }
2827
2991
  };
2828
2992
  }
@@ -2843,7 +3007,7 @@ var State = class _State {
2843
3007
  }
2844
3008
  },
2845
3009
  persist: (next) => {
2846
- writeFileSync2(this.path(file), JSON.stringify(next));
3010
+ writeFileSync4(this.path(file), JSON.stringify(next));
2847
3011
  }
2848
3012
  };
2849
3013
  }
@@ -2856,7 +3020,7 @@ var State = class _State {
2856
3020
  const file = ".release-last-run";
2857
3021
  return {
2858
3022
  touch: () => {
2859
- writeFileSync2(this.path(file), (/* @__PURE__ */ new Date()).toISOString());
3023
+ writeFileSync4(this.path(file), (/* @__PURE__ */ new Date()).toISOString());
2860
3024
  },
2861
3025
  at: () => {
2862
3026
  try {
@@ -3173,10 +3337,10 @@ function describeRelease(o, route) {
3173
3337
  }
3174
3338
 
3175
3339
  // src/install.ts
3176
- import { dirname as dirname4, join as join7, extname } from "path";
3177
- import { homedir as homedir4 } from "os";
3178
- import { accessSync, constants, existsSync as existsSync6, mkdirSync as mkdirSync4, readdirSync, readFileSync as readFileSync7, statSync as statSync3, writeFileSync as writeFileSync3, unlinkSync, realpathSync } from "fs";
3179
- import { spawnSync as spawnSync3 } from "child_process";
3340
+ import { dirname as dirname4, join as join8, extname } from "path";
3341
+ import { homedir as homedir5 } from "os";
3342
+ import { accessSync, constants, existsSync as existsSync7, mkdirSync as mkdirSync4, readdirSync, readFileSync as readFileSync7, statSync as statSync3, writeFileSync as writeFileSync5, unlinkSync, realpathSync } from "fs";
3343
+ import { spawnSync as spawnSync4 } from "child_process";
3180
3344
  import { createHash } from "crypto";
3181
3345
 
3182
3346
  // src/runtime-info.ts
@@ -3187,7 +3351,7 @@ function isCompiledBinary(moduleUrl) {
3187
3351
  // src/install.ts
3188
3352
  function crewInvocation(crewHome) {
3189
3353
  if (isCompiledBinary(import.meta.url)) return [process.execPath];
3190
- return [process.execPath, join7(crewHome, "bin", "crew")];
3354
+ return [process.execPath, join8(crewHome, "bin", "crew")];
3191
3355
  }
3192
3356
  var INTERVAL_SECONDS = 120;
3193
3357
  function labelFor(crewHome, job) {
@@ -3225,7 +3389,7 @@ function pathFor(ship) {
3225
3389
  return parts.join(":");
3226
3390
  }
3227
3391
  function findOnPath(bin, searchPath) {
3228
- const candidates = bin.includes("/") ? [bin] : searchPath.split(":").filter(Boolean).map((dir) => join7(dir, bin));
3392
+ const candidates = bin.includes("/") ? [bin] : searchPath.split(":").filter(Boolean).map((dir) => join8(dir, bin));
3229
3393
  for (const candidate of candidates) {
3230
3394
  try {
3231
3395
  accessSync(candidate, constants.X_OK);
@@ -3235,7 +3399,7 @@ function findOnPath(bin, searchPath) {
3235
3399
  }
3236
3400
  return void 0;
3237
3401
  }
3238
- var COMMON_DOCKER_DIRS = ["/usr/local/bin", "/opt/homebrew/bin", join7(homedir4(), ".docker", "bin")].join(":");
3402
+ var COMMON_DOCKER_DIRS = ["/usr/local/bin", "/opt/homebrew/bin", join8(homedir5(), ".docker", "bin")].join(":");
3239
3403
  function dockerPathProblem(ship, shellPath = process.env.PATH ?? "") {
3240
3404
  const schedulerPath = pathFor(ship);
3241
3405
  if (findOnPath("docker", schedulerPath)) return void 0;
@@ -3247,7 +3411,7 @@ function xmlEscape(s) {
3247
3411
  }
3248
3412
  function planLaunchd(ship, crewHome, job) {
3249
3413
  const label = labelFor(crewHome, job);
3250
- const unitPath = join7(homedir4(), "Library", "LaunchAgents", `${label}.plist`);
3414
+ const unitPath = join8(homedir5(), "Library", "LaunchAgents", `${label}.plist`);
3251
3415
  const crewLog = ship.logFile;
3252
3416
  const schedulerLog = schedulerLogFor(crewLog, job);
3253
3417
  const persistent = job === "run";
@@ -3299,8 +3463,8 @@ ${scheduleKeys}
3299
3463
  }
3300
3464
  function planSystemd(ship, crewHome, job) {
3301
3465
  const label = labelFor(crewHome, job);
3302
- const unitDir = join7(homedir4(), ".config", "systemd", "user");
3303
- const servicePath = join7(unitDir, `${label}.service`);
3466
+ const unitDir = join8(homedir5(), ".config", "systemd", "user");
3467
+ const servicePath = join8(unitDir, `${label}.service`);
3304
3468
  const crewLog = ship.logFile;
3305
3469
  const schedulerLog = schedulerLogFor(crewLog, job);
3306
3470
  const description = job === "release" ? "Tablation crew \u2014 release" : job === "passengers" ? "Tablation crew \u2014 Host Passengers sync" : "Tablation crew \u2014 persistent supervisor loop";
@@ -3337,7 +3501,7 @@ WantedBy=default.target
3337
3501
  schedulerLog
3338
3502
  };
3339
3503
  }
3340
- const timerPath = join7(unitDir, `${label}.timer`);
3504
+ const timerPath = join8(unitDir, `${label}.timer`);
3341
3505
  const service = `# Written by \`crew install\` \u2014 re-run it rather than hand-editing this file.
3342
3506
  [Unit]
3343
3507
  Description=${description}
@@ -3404,7 +3568,7 @@ function planInstall(ship, crewHome, hostShip, hasSystemd, job = "run") {
3404
3568
  function planUninstall(ship, crewHome, hostShip, hasSystemd, job = "run") {
3405
3569
  const label = labelFor(crewHome, job);
3406
3570
  if (hostShip === "macos") {
3407
- const unitPath = join7(homedir4(), "Library", "LaunchAgents", `${label}.plist`);
3571
+ const unitPath = join8(homedir5(), "Library", "LaunchAgents", `${label}.plist`);
3408
3572
  return {
3409
3573
  mechanism: "launchd",
3410
3574
  unitPaths: [unitPath],
@@ -3414,18 +3578,18 @@ function planUninstall(ship, crewHome, hostShip, hasSystemd, job = "run") {
3414
3578
  };
3415
3579
  }
3416
3580
  if (hostShip === "linux" && hasSystemd) {
3417
- const unitDir = join7(homedir4(), ".config", "systemd", "user");
3581
+ const unitDir = join8(homedir5(), ".config", "systemd", "user");
3418
3582
  if (job === "run") {
3419
3583
  return {
3420
3584
  mechanism: "systemd",
3421
- unitPaths: [join7(unitDir, `${label}.service`)],
3585
+ unitPaths: [join8(unitDir, `${label}.service`)],
3422
3586
  crontabLine: null,
3423
3587
  unloadCommands: [["systemctl", "--user", "disable", "--now", `${label}.service`]]
3424
3588
  };
3425
3589
  }
3426
3590
  return {
3427
3591
  mechanism: "systemd",
3428
- unitPaths: [join7(unitDir, `${label}.service`), join7(unitDir, `${label}.timer`)],
3592
+ unitPaths: [join8(unitDir, `${label}.service`), join8(unitDir, `${label}.timer`)],
3429
3593
  crontabLine: null,
3430
3594
  unloadCommands: [["systemctl", "--user", "disable", "--now", `${label}.timer`]]
3431
3595
  };
@@ -3443,7 +3607,7 @@ function planUninstall(ship, crewHome, hostShip, hasSystemd, job = "run") {
3443
3607
  function planDaemonControl(crewHome, hostShip, hasSystemd) {
3444
3608
  const label = labelFor(crewHome, "run");
3445
3609
  if (hostShip === "macos") {
3446
- const unitPath = join7(homedir4(), "Library", "LaunchAgents", `${label}.plist`);
3610
+ const unitPath = join8(homedir5(), "Library", "LaunchAgents", `${label}.plist`);
3447
3611
  return {
3448
3612
  mechanism: "launchd",
3449
3613
  startCommand: ["launchctl", "load", unitPath],
@@ -3467,7 +3631,7 @@ function planDaemonControl(crewHome, hostShip, hasSystemd) {
3467
3631
  }
3468
3632
  function runAll(commands, cwd, log) {
3469
3633
  for (const [bin, ...args] of commands) {
3470
- const res = spawnSync3(bin, args, { cwd, encoding: "utf8" });
3634
+ const res = spawnSync4(bin, args, { cwd, encoding: "utf8" });
3471
3635
  if (res.error || res.status !== 0) {
3472
3636
  log.warn(`${bin} ${args.join(" ")} exited ${res.status ?? "error"}`, {
3473
3637
  output: `${res.stdout ?? ""}${res.stderr ?? ""}${res.error ? String(res.error) : ""}`
@@ -3484,7 +3648,7 @@ async function applyInstall(plan, crewHome, dryRun, log) {
3484
3648
  continue;
3485
3649
  }
3486
3650
  mkdirSync4(dirname4(path), { recursive: true });
3487
- writeFileSync3(path, plan.unitContent[path]);
3651
+ writeFileSync5(path, plan.unitContent[path]);
3488
3652
  log.emit(`wrote ${path}`);
3489
3653
  }
3490
3654
  if (plan.crontabLine) {
@@ -3532,13 +3696,13 @@ async function applyUninstall(plan, crewHome, dryRun, log) {
3532
3696
  if (dryRun) {
3533
3697
  log.emit(`would run: launchctl remove ${plan.removeLabel}`);
3534
3698
  } else {
3535
- if (spawnSync3("launchctl", ["remove", plan.removeLabel], { encoding: "utf8" }).status === 0) {
3699
+ if (spawnSync4("launchctl", ["remove", plan.removeLabel], { encoding: "utf8" }).status === 0) {
3536
3700
  log.emit(`removed loaded job ${plan.removeLabel}`);
3537
3701
  }
3538
3702
  }
3539
3703
  }
3540
3704
  for (const path of plan.unitPaths) {
3541
- if (!existsSync6(path)) continue;
3705
+ if (!existsSync7(path)) continue;
3542
3706
  if (dryRun) {
3543
3707
  log.emit(`would remove ${path}`);
3544
3708
  continue;
@@ -3557,7 +3721,7 @@ async function applyUninstall(plan, crewHome, dryRun, log) {
3557
3721
  }
3558
3722
  var CREW_LAUNCHD_PREFIX = "com.tablation.crew";
3559
3723
  function launchAgentsDir() {
3560
- return join7(homedir4(), "Library", "LaunchAgents");
3724
+ return join8(homedir5(), "Library", "LaunchAgents");
3561
3725
  }
3562
3726
  function xmlUnescape(s) {
3563
3727
  return s.replace(/&lt;/g, "<").replace(/&gt;/g, ">").replace(/&amp;/g, "&");
@@ -3569,14 +3733,14 @@ function programFromPlist(text) {
3569
3733
  return args.find((a) => a.endsWith("/bin/crew")) ?? args[0] ?? null;
3570
3734
  }
3571
3735
  function loadedCrewLabels() {
3572
- const list = spawnSync3("launchctl", ["list"], { encoding: "utf8" });
3736
+ const list = spawnSync4("launchctl", ["list"], { encoding: "utf8" });
3573
3737
  if (list.status !== 0) return [];
3574
3738
  return [...parseLaunchctlList(list.stdout ?? "").keys()].filter((l) => l.startsWith(CREW_LAUNCHD_PREFIX));
3575
3739
  }
3576
3740
  function listLaunchdUnits(dir = launchAgentsDir(), loaded = []) {
3577
- const files = existsSync6(dir) ? readdirSync(dir) : [];
3741
+ const files = existsSync7(dir) ? readdirSync(dir) : [];
3578
3742
  const units = files.filter((f) => f.startsWith(CREW_LAUNCHD_PREFIX) && f.endsWith(".plist")).sort().map((f) => {
3579
- const plistPath = join7(dir, f);
3743
+ const plistPath = join8(dir, f);
3580
3744
  let program = null;
3581
3745
  try {
3582
3746
  program = programFromPlist(readFileSync7(plistPath, "utf8"));
@@ -3586,13 +3750,13 @@ function listLaunchdUnits(dir = launchAgentsDir(), loaded = []) {
3586
3750
  label: f.slice(0, -".plist".length),
3587
3751
  plistPath,
3588
3752
  program,
3589
- programExists: program !== null && existsSync6(program)
3753
+ programExists: program !== null && existsSync7(program)
3590
3754
  };
3591
3755
  });
3592
3756
  const seen = new Set(units.map((u) => u.label));
3593
3757
  for (const label of [...loaded].sort()) {
3594
3758
  if (!label.startsWith(CREW_LAUNCHD_PREFIX) || seen.has(label)) continue;
3595
- units.push({ label, plistPath: join7(dir, `${label}.plist`), program: null, programExists: false, orphan: true });
3759
+ units.push({ label, plistPath: join8(dir, `${label}.plist`), program: null, programExists: false, orphan: true });
3596
3760
  }
3597
3761
  return units;
3598
3762
  }
@@ -3605,9 +3769,9 @@ function removeLaunchdUnit(unit, dryRun, log) {
3605
3769
  log.emit(`would unload and remove ${unit.label}`);
3606
3770
  return;
3607
3771
  }
3608
- if (existsSync6(unit.plistPath)) spawnSync3("launchctl", ["unload", unit.plistPath], { encoding: "utf8" });
3609
- spawnSync3("launchctl", ["remove", unit.label], { encoding: "utf8" });
3610
- if (existsSync6(unit.plistPath)) unlinkSync(unit.plistPath);
3772
+ if (existsSync7(unit.plistPath)) spawnSync4("launchctl", ["unload", unit.plistPath], { encoding: "utf8" });
3773
+ spawnSync4("launchctl", ["remove", unit.label], { encoding: "utf8" });
3774
+ if (existsSync7(unit.plistPath)) unlinkSync(unit.plistPath);
3611
3775
  log.emit(`removed ${unit.label} (${unit.plistPath})`);
3612
3776
  }
3613
3777
  function clearForeignLaunchdUnits(crewHome, replace, dryRun, log, dir, loaded) {
@@ -3639,7 +3803,7 @@ function parseLaunchctlList(text) {
3639
3803
  return jobs;
3640
3804
  }
3641
3805
  function describeForeignLaunchdUnits(crewHome, dir) {
3642
- const list = spawnSync3("launchctl", ["list"], { encoding: "utf8" });
3806
+ const list = spawnSync4("launchctl", ["list"], { encoding: "utf8" });
3643
3807
  const jobs = parseLaunchctlList(list.status === 0 ? list.stdout ?? "" : "");
3644
3808
  const foreign = foreignLaunchdUnits(crewHome, dir, jobs.keys());
3645
3809
  return foreign.map((u) => {
@@ -3652,11 +3816,11 @@ function describeForeignLaunchdUnits(crewHome, dir) {
3652
3816
  });
3653
3817
  }
3654
3818
  function readCrontab() {
3655
- const res = spawnSync3("crontab", ["-l"], { encoding: "utf8" });
3819
+ const res = spawnSync4("crontab", ["-l"], { encoding: "utf8" });
3656
3820
  return res.status === 0 ? res.stdout ?? "" : "";
3657
3821
  }
3658
3822
  function writeCrontab(text) {
3659
- spawnSync3("crontab", ["-"], { input: text, encoding: "utf8" });
3823
+ spawnSync4("crontab", ["-"], { input: text, encoding: "utf8" });
3660
3824
  }
3661
3825
  function addCrontabLine(marker, line) {
3662
3826
  const existing = readCrontab();
@@ -3680,7 +3844,7 @@ function removeCrontabLine(marker) {
3680
3844
  }
3681
3845
  function detectSystemd() {
3682
3846
  if (process.platform !== "linux") return false;
3683
- const res = spawnSync3("systemctl", ["--user", "status"], { encoding: "utf8" });
3847
+ const res = spawnSync4("systemctl", ["--user", "status"], { encoding: "utf8" });
3684
3848
  return !res.error;
3685
3849
  }
3686
3850
 
@@ -3690,7 +3854,7 @@ import { statfsSync } from "fs";
3690
3854
  // src/git.ts
3691
3855
  import { execFileSync } from "child_process";
3692
3856
  import {
3693
- existsSync as existsSync7,
3857
+ existsSync as existsSync8,
3694
3858
  mkdirSync as mkdirSync5,
3695
3859
  openSync as openSync2,
3696
3860
  closeSync as closeSync2,
@@ -4062,7 +4226,7 @@ function cloneUrlFor(remote) {
4062
4226
  return `git@github.com:${remote}.git`;
4063
4227
  }
4064
4228
  function ensureRepoCheckout(dir, remote) {
4065
- if (existsSync7(`${dir}/.git`)) return false;
4229
+ if (existsSync8(`${dir}/.git`)) return false;
4066
4230
  if (!remote) {
4067
4231
  throw new GitError(
4068
4232
  `${dir} does not exist and this repo has no discovered remote to clone from \u2014 re-run \`crew connect\`, or check out the repository yourself and point \`repos:\` at it`
@@ -5799,8 +5963,8 @@ function materializeBranchForTicket(dir, cfg, t, role) {
5799
5963
  }
5800
5964
 
5801
5965
  // src/poll.ts
5802
- import { writeFileSync as writeFileSync4 } from "fs";
5803
- import { join as join8 } from "path";
5966
+ import { writeFileSync as writeFileSync6 } from "fs";
5967
+ import { join as join9 } from "path";
5804
5968
 
5805
5969
  // src/stalled.ts
5806
5970
  var STALLED_AFTER_CYCLES = 30;
@@ -6072,7 +6236,7 @@ async function decideCycle(o) {
6072
6236
  }
6073
6237
  return decision;
6074
6238
  }
6075
- var digestPath = (stateDir, route, role) => join8(stateDir, `digest-${route}-${role}.md`);
6239
+ var digestPath = (stateDir, route, role) => join9(stateDir, `digest-${route}-${role}.md`);
6076
6240
  function writeDigest(o, d, role, stateDir) {
6077
6241
  const me = o.route.resolved?.seats[role];
6078
6242
  if (!me) return false;
@@ -6134,7 +6298,7 @@ function writeDigest(o, d, role, stateDir) {
6134
6298
  };
6135
6299
  const text = role === "qa" ? qaDigest(input) : buildingDigest(input);
6136
6300
  const path = digestPath(stateDir, routeSlug(o.route.route), role);
6137
- writeFileSync4(path, text);
6301
+ writeFileSync6(path, text);
6138
6302
  o.emit.emit(`queue digest written for ${role} (${Buffer.byteLength(text)} bytes)`, {
6139
6303
  data: { role, bytes: Buffer.byteLength(text) }
6140
6304
  });
@@ -6442,9 +6606,9 @@ var alreadyReported = (ticket, g, comments) => comments.some((c) => c.ticket_id
6442
6606
 
6443
6607
  // src/agent.ts
6444
6608
  import { spawn as spawn2 } from "child_process";
6445
- import { readFileSync as readFileSync8, existsSync as existsSync8, statSync as statSync5, mkdirSync as mkdirSync6, writeFileSync as writeFileSync5, createWriteStream } from "fs";
6609
+ import { readFileSync as readFileSync8, existsSync as existsSync9, statSync as statSync5, mkdirSync as mkdirSync6, writeFileSync as writeFileSync7, createWriteStream } from "fs";
6446
6610
  import { finished } from "stream/promises";
6447
- import { join as join9, dirname as dirname6 } from "path";
6611
+ import { join as join10, dirname as dirname6 } from "path";
6448
6612
  import { createHash as createHash2 } from "crypto";
6449
6613
 
6450
6614
  // src/environment.ts
@@ -6842,18 +7006,18 @@ var AgentError = class extends Error {
6842
7006
  var AgentSpawnError = class extends AgentError {
6843
7007
  };
6844
7008
  function planAgentRun(o) {
6845
- const briefPath = join9(o.route.promptsDir, "personas", `lane-${o.role}.md`);
6846
- if (!existsSync8(briefPath)) {
7009
+ const briefPath = join10(o.route.promptsDir, "personas", `lane-${o.role}.md`);
7010
+ if (!existsSync9(briefPath)) {
6847
7011
  throw new AgentError(`no brief at ${briefPath} \u2014 refusing to run an unscoped session`);
6848
7012
  }
6849
- const commonPath = join9(o.route.promptsDir, "personas", "common.md");
6850
- if (!existsSync8(commonPath)) throw new AgentError(`no shared policy at ${commonPath}`);
7013
+ const commonPath = join10(o.route.promptsDir, "personas", "common.md");
7014
+ if (!existsSync9(commonPath)) throw new AgentError(`no shared policy at ${commonPath}`);
6851
7015
  const now = o.now ?? (() => Date.now());
6852
7016
  const routeLabel = routeSlug(o.route.route);
6853
- const digestPath2 = join9(o.stateDir, `digest-${routeLabel}-${o.role}.md`);
7017
+ const digestPath2 = join10(o.stateDir, `digest-${routeLabel}-${o.role}.md`);
6854
7018
  let digest;
6855
7019
  let ageSeconds;
6856
- if (existsSync8(digestPath2)) {
7020
+ if (existsSync9(digestPath2)) {
6857
7021
  ageSeconds = Math.floor((now() - statSync5(digestPath2).mtimeMs) / 1e3);
6858
7022
  if (ageSeconds <= DIGEST_MAX_AGE_SECONDS) digest = readFileSync8(digestPath2, "utf8");
6859
7023
  }
@@ -6869,9 +7033,9 @@ function planAgentRun(o) {
6869
7033
  digest
6870
7034
  });
6871
7035
  const promptSha = createHash2("sha256").update(`${effectiveCommon}${effectiveBrief}`).digest("hex");
6872
- const streamsDir = join9(o.stateDir, "streams");
7036
+ const streamsDir = join10(o.stateDir, "streams");
6873
7037
  const base = `${routeLabel}-${o.role}-${o.cycle}`;
6874
- const seat = resolveSeat(o.ship, o.route, o.role, o.agentPersona ?? (o.agentModel ? { model: o.agentModel } : {}));
7038
+ const seat = resolveSeat(o.ship, o.route, o.role, o.agentPersona ?? (o.agentModel ? { model: o.agentModel } : {}), o.repo);
6875
7039
  const { toolchain, model } = seat;
6876
7040
  const adapter = adapterFor(toolchain.harness);
6877
7041
  const invocation = adapter.invocation(toolchain, {
@@ -6892,8 +7056,9 @@ function planAgentRun(o) {
6892
7056
  role: o.role,
6893
7057
  route: routeLabel,
6894
7058
  // 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
7059
+ // ticket's repo: the session picks its own working directory even when
7060
+ // the runner already selected a ticket (`o.repo` is known then, and
7061
+ // only feeds the per-repo toolchain choice). Where an area spans
6897
7062
  // several repos this is whichever one `repos` happened to list first
6898
7063
  // (config.ts), and the digest's `repo` column is what actually places
6899
7064
  // the work. Kept deliberately rather than guessed at (ISSUE-349).
@@ -6924,8 +7089,8 @@ function planAgentRun(o) {
6924
7089
  digestAttached: digest !== void 0,
6925
7090
  digestAgeSeconds: ageSeconds,
6926
7091
  model,
6927
- streamPath: join9(streamsDir, `${base}.jsonl`),
6928
- eventsPath: join9(streamsDir, `${base}.events.jsonl`),
7092
+ streamPath: join10(streamsDir, `${base}.jsonl`),
7093
+ eventsPath: join10(streamsDir, `${base}.events.jsonl`),
6929
7094
  ticket: o.ticket,
6930
7095
  agentLog,
6931
7096
  promptVersion: o.route.resolved?.agentPersonas?.[o.role]?.historyId,
@@ -6995,8 +7160,8 @@ function spawnAgent(plan, emit) {
6995
7160
  return;
6996
7161
  }
6997
7162
  try {
6998
- const extracted = adapter.extractResult(parsed);
6999
- if (extracted) result = { ...result, ...extracted };
7163
+ const got = adapter.extractResult(parsed);
7164
+ if (got) result = { ...result, ...Object.fromEntries(Object.entries(got).filter(([, v]) => v !== void 0)) };
7000
7165
  consume(parsed, mapper.map(parsed));
7001
7166
  } catch {
7002
7167
  }
@@ -7072,7 +7237,7 @@ function spawnAgent(plan, emit) {
7072
7237
  provider: plan.provider ?? LEGACY_TOOLCHAIN_PROVIDER
7073
7238
  };
7074
7239
  try {
7075
- writeFileSync5(`${plan.streamPath}.meta.json`, JSON.stringify({
7240
+ writeFileSync7(`${plan.streamPath}.meta.json`, JSON.stringify({
7076
7241
  route: plan.route,
7077
7242
  role: plan.role,
7078
7243
  ticket: plan.ticket ?? null,
@@ -7131,13 +7296,13 @@ function spawnAgent(plan, emit) {
7131
7296
  }
7132
7297
 
7133
7298
  // src/run-worktree.ts
7134
- import { existsSync as existsSync10 } from "fs";
7135
- import { dirname as dirname7, join as join11 } from "path";
7299
+ import { existsSync as existsSync11 } from "fs";
7300
+ import { dirname as dirname7, join as join12 } from "path";
7136
7301
 
7137
7302
  // src/ports.ts
7138
7303
  import { execFileSync as execFileSync2 } from "child_process";
7139
- import { existsSync as existsSync9 } from "fs";
7140
- import { join as join10 } from "path";
7304
+ import { existsSync as existsSync10 } from "fs";
7305
+ import { join as join11 } from "path";
7141
7306
  var run = (cmd, args) => {
7142
7307
  try {
7143
7308
  return execFileSync2(cmd, args, { encoding: "utf8", stdio: ["ignore", "pipe", "ignore"] });
@@ -7182,7 +7347,7 @@ function ticketForPort(port, s = DEFAULT_PORTS) {
7182
7347
  }
7183
7348
  function worktreeExists(parent, prefix, n) {
7184
7349
  for (const name of [`${prefix}${n}`, `${prefix}${String(n).padStart(3, "0")}`]) {
7185
- if (existsSync9(join10(parent, name, ".git"))) return true;
7350
+ if (existsSync10(join11(parent, name, ".git"))) return true;
7186
7351
  }
7187
7352
  return false;
7188
7353
  }
@@ -7256,7 +7421,7 @@ function planRunWorktree(dir, cfg, t, role) {
7256
7421
  const existing = worktreeForNumber(dir, num);
7257
7422
  const located = locateBranchForTicket(dir, cfg, t, role);
7258
7423
  const baseRef = hasRemote ? `${remote}/${base}` : base;
7259
- if (existing && !existing.prunable && existsSync10(existing.path)) {
7424
+ if (existing && !existing.prunable && existsSync11(existing.path)) {
7260
7425
  const branch = existing.branch;
7261
7426
  if (status(existing.path).length > 0) return { action: "leave-dirty", path: existing.path, branch };
7262
7427
  if (!branch) return { action: "leave-unpushed", path: existing.path, branch: "(detached HEAD)" };
@@ -7270,7 +7435,7 @@ function planRunWorktree(dir, cfg, t, role) {
7270
7435
  if (countCommits(existing.path, `${baseRef}..HEAD`) > 0) return { action: "leave-unpushed", path: existing.path, branch };
7271
7436
  return { action: "recut", path: existing.path, branch, base: baseRef };
7272
7437
  }
7273
- const path = join11(dirname7(dir), worktreeForTicket(dir, cfg, t, role));
7438
+ const path = join12(dirname7(dir), worktreeForTicket(dir, cfg, t, role));
7274
7439
  if (located?.where === "remote" || located && hasRemote && remoteBranchExists(dir, remote, located.branch)) {
7275
7440
  return { action: "cut-remote", path, branch: located.branch };
7276
7441
  }
@@ -7317,7 +7482,7 @@ function worktreeHoldsUnpushed(path, remote) {
7317
7482
  }
7318
7483
  async function finishRunWorktree(dir, cfg, branch) {
7319
7484
  const wt = worktrees(dir).find((w) => w.branch === branch && !w.prunable && w.path !== dir);
7320
- if (!wt || !existsSync10(wt.path)) return { kind: "none" };
7485
+ if (!wt || !existsSync11(wt.path)) return { kind: "none" };
7321
7486
  if (cfg.hooks.handoff) return { kind: "kept-handoff", path: wt.path };
7322
7487
  if (status(wt.path).length > 0) return { kind: "kept-dirty", path: wt.path };
7323
7488
  if (remoteConfigured(dir, cfg.branch.remote) && worktreeHoldsUnpushed(wt.path, cfg.branch.remote)) {
@@ -7329,7 +7494,7 @@ async function finishRunWorktree(dir, cfg, branch) {
7329
7494
  if (pids.length) await killGently(pids);
7330
7495
  }
7331
7496
  gitOk(dir, ["worktree", "remove", wt.path]);
7332
- return existsSync10(wt.path) ? { kind: "kept-dirty", path: wt.path } : { kind: "removed", path: wt.path };
7497
+ return existsSync11(wt.path) ? { kind: "kept-dirty", path: wt.path } : { kind: "removed", path: wt.path };
7333
7498
  }
7334
7499
 
7335
7500
  // src/handoff-guard.ts
@@ -7478,13 +7643,13 @@ async function retireRunWorktree(tracker, dir, cfg, t, role, seatId, shipName2,
7478
7643
  }
7479
7644
 
7480
7645
  // src/connect-wizard.ts
7481
- import { delimiter, dirname as dirname8, join as join12 } from "path";
7482
- import { accessSync as accessSync2, constants as constants2, existsSync as existsSync11, mkdirSync as mkdirSync7, writeFileSync as writeFileSync6 } from "fs";
7646
+ import { delimiter, dirname as dirname8, join as join13 } from "path";
7647
+ import { accessSync as accessSync2, constants as constants2, existsSync as existsSync12, mkdirSync as mkdirSync7, writeFileSync as writeFileSync8 } from "fs";
7483
7648
  import { stringify } from "yaml";
7484
7649
  var HOST_PASSENGERS_DOCS_URL = "https://crew.tablation.dev/host-passengers";
7485
7650
  function shouldRunWizard(opts) {
7486
7651
  if (!opts.isTTY) return false;
7487
- const exists = opts.configExists ?? existsSync11;
7652
+ const exists = opts.configExists ?? existsSync12;
7488
7653
  return !configSearchPath(opts.crewHome).some((p) => exists(p));
7489
7654
  }
7490
7655
  function firstRunConfigPath() {
@@ -7501,7 +7666,7 @@ function defaultIsExecutable(p) {
7501
7666
  function findOnPath2(name, pathEnv, isExecutable = defaultIsExecutable) {
7502
7667
  for (const dir of pathEnv.split(delimiter)) {
7503
7668
  if (!dir) continue;
7504
- const candidate = join12(dir, name);
7669
+ const candidate = join13(dir, name);
7505
7670
  if (isExecutable(candidate)) return candidate;
7506
7671
  }
7507
7672
  return void 0;
@@ -7564,7 +7729,7 @@ ${routeBlock}`;
7564
7729
  function writeNewConfig(path, text) {
7565
7730
  mkdirSync7(dirname8(path), { recursive: true });
7566
7731
  try {
7567
- writeFileSync6(path, text, { mode: 384, flag: "wx" });
7732
+ writeFileSync8(path, text, { mode: 384, flag: "wx" });
7568
7733
  return true;
7569
7734
  } catch (e) {
7570
7735
  if (e.code === "EEXIST") return false;
@@ -7596,8 +7761,8 @@ Wrote ${o.configPath}.
7596
7761
  }
7597
7762
 
7598
7763
  // src/release-run.ts
7599
- import { readFileSync as readFileSync9, writeFileSync as writeFileSync7, existsSync as existsSync12, copyFileSync, mkdirSync as mkdirSync8, mkdtempSync, rmSync as rmSync4 } from "fs";
7600
- import { dirname as dirname9, join as join13 } from "path";
7764
+ import { readFileSync as readFileSync9, writeFileSync as writeFileSync9, existsSync as existsSync13, copyFileSync, mkdirSync as mkdirSync8, mkdtempSync, rmSync as rmSync6 } from "fs";
7765
+ import { dirname as dirname9, join as join14 } from "path";
7601
7766
  import { tmpdir as tmpdir2 } from "os";
7602
7767
 
7603
7768
  // src/release.ts
@@ -8255,9 +8420,9 @@ async function bump(o, size) {
8255
8420
  }
8256
8421
  return printed;
8257
8422
  }
8258
- const files = o.repo.release.versionFiles.map((f) => join13(o.cwd, f));
8423
+ const files = o.repo.release.versionFiles.map((f) => join14(o.cwd, f));
8259
8424
  const first = files[0];
8260
- if (!existsSync12(first)) {
8425
+ if (!existsSync13(first)) {
8261
8426
  o.emit.error(`no ${o.repo.release.versionFiles[0]} to read a version from`);
8262
8427
  return null;
8263
8428
  }
@@ -8280,21 +8445,21 @@ async function bump(o, size) {
8280
8445
  o.emit.error(`no version field in ${f}`);
8281
8446
  return null;
8282
8447
  }
8283
- writeFileSync7(f, out);
8448
+ writeFileSync9(f, out);
8284
8449
  }
8285
8450
  return version;
8286
8451
  }
8287
8452
  function writeChangelog(o, version, entries) {
8288
8453
  const file = o.repo.release.changelog;
8289
8454
  if (!file || entries.length === 0) return;
8290
- const path = join13(o.cwd, file);
8455
+ const path = join14(o.cwd, file);
8291
8456
  const section = renderChangelogSection(version, (/* @__PURE__ */ new Date()).toISOString().slice(0, 10), entries);
8292
8457
  if (o.dryRun) {
8293
8458
  o.emit.emit(`would prepend ${entries.length} entr(ies) to ${file}`);
8294
8459
  return;
8295
8460
  }
8296
- const existing = existsSync12(path) ? readFileSync9(path, "utf8") : "# Changelog\n";
8297
- writeFileSync7(path, insertChangelogSection(existing, section));
8461
+ const existing = existsSync13(path) ? readFileSync9(path, "utf8") : "# Changelog\n";
8462
+ writeFileSync9(path, insertChangelogSection(existing, section));
8298
8463
  }
8299
8464
  async function confirm(o, expected) {
8300
8465
  if (!o.repo.hooks.released) return void 0;
@@ -8375,11 +8540,11 @@ async function confirmLastRelease(o, decision) {
8375
8540
  function openReleaseCheckout(o) {
8376
8541
  const { remote, base } = o.repo.branch;
8377
8542
  const branch = `crew/release-${Date.now()}`;
8378
- const root = mkdtempSync(join13(tmpdir2(), "crew-release-"));
8379
- const dir = join13(root, "checkout");
8543
+ const root = mkdtempSync(join14(tmpdir2(), "crew-release-"));
8544
+ const dir = join14(root, "checkout");
8380
8545
  const close = () => {
8381
8546
  gitOk(o.cwd, ["worktree", "remove", "--force", dir]);
8382
- rmSync4(root, { recursive: true, force: true });
8547
+ rmSync6(root, { recursive: true, force: true });
8383
8548
  gitOk(o.cwd, ["worktree", "prune"]);
8384
8549
  gitOk(o.cwd, ["branch", "-D", branch]);
8385
8550
  };
@@ -8390,10 +8555,10 @@ function openReleaseCheckout(o) {
8390
8555
  throw e;
8391
8556
  }
8392
8557
  for (const f of o.repo.worktrees.copy) {
8393
- const src = join13(o.cwd, f);
8394
- if (!existsSync12(src)) continue;
8395
- mkdirSync8(dirname9(join13(dir, f)), { recursive: true });
8396
- copyFileSync(src, join13(dir, f));
8558
+ const src = join14(o.cwd, f);
8559
+ if (!existsSync13(src)) continue;
8560
+ mkdirSync8(dirname9(join14(dir, f)), { recursive: true });
8561
+ copyFileSync(src, join14(dir, f));
8397
8562
  }
8398
8563
  return { dir, branch, close };
8399
8564
  }
@@ -8862,7 +9027,7 @@ function missingReleaserWarning(repo, release, ships, now = Date.now()) {
8862
9027
  }
8863
9028
 
8864
9029
  // src/watch.ts
8865
- import { watch as watchFile, existsSync as existsSync13, statSync as statSync6, openSync as openSync3, readSync, closeSync as closeSync3 } from "fs";
9030
+ import { watch as watchFile, existsSync as existsSync14, statSync as statSync6, openSync as openSync3, readSync, closeSync as closeSync3 } from "fs";
8866
9031
  var E = "\x1B[";
8867
9032
  var dim = (s) => `${E}2m${s}${E}0m`;
8868
9033
  var bold = (s) => `${E}1m${s}${E}0m`;
@@ -8963,7 +9128,7 @@ function renderHeaders(states, now = Date.now()) {
8963
9128
  return states.map((s) => renderHeader(s, now)).join("");
8964
9129
  }
8965
9130
  function readFrom(file, from) {
8966
- if (!existsSync13(file)) return { events: [], offset: from };
9131
+ if (!existsSync14(file)) return { events: [], offset: from };
8967
9132
  const size = statSync6(file).size;
8968
9133
  if (size < from) return readFrom(file, 0);
8969
9134
  if (size === from) return { events: [], offset: from };
@@ -9203,8 +9368,8 @@ function nextRoles(decision, ran) {
9203
9368
  }
9204
9369
 
9205
9370
  // src/agents.ts
9206
- import { readFileSync as readFileSync10, existsSync as existsSync14 } from "fs";
9207
- import { join as join14 } from "path";
9371
+ import { readFileSync as readFileSync10, existsSync as existsSync15 } from "fs";
9372
+ import { join as join15 } from "path";
9208
9373
  import { TablationClient as TablationClient2, StaleWriteError as StaleWriteError6, TablationApiError } from "@tablation/client";
9209
9374
  var PERSONA_NAME = {
9210
9375
  dev: "Developer",
@@ -9218,11 +9383,11 @@ var CREW_AGENT_COLUMN = "agent_id";
9218
9383
  var AgentsSyncError = class extends Error {
9219
9384
  };
9220
9385
  function personaDefaultPrompt(promptsDir, role) {
9221
- const briefPath = join14(promptsDir, "personas", `lane-${role}.md`);
9222
- if (!existsSync14(briefPath)) throw new AgentsSyncError(`no brief at ${briefPath}`);
9386
+ const briefPath = join15(promptsDir, "personas", `lane-${role}.md`);
9387
+ if (!existsSync15(briefPath)) throw new AgentsSyncError(`no brief at ${briefPath}`);
9223
9388
  if (role === "pair") return readFileSync10(briefPath, "utf8");
9224
- const commonPath = join14(promptsDir, "personas", "common.md");
9225
- if (!existsSync14(commonPath)) throw new AgentsSyncError(`no shared policy at ${commonPath}`);
9389
+ const commonPath = join15(promptsDir, "personas", "common.md");
9390
+ if (!existsSync15(commonPath)) throw new AgentsSyncError(`no shared policy at ${commonPath}`);
9226
9391
  return `${readFileSync10(commonPath, "utf8")}${readFileSync10(briefPath, "utf8")}`;
9227
9392
  }
9228
9393
  async function resolveAgentId(route, role, opts) {
@@ -9900,8 +10065,8 @@ import {
9900
10065
  } from "@tablation/client";
9901
10066
 
9902
10067
  // src/skills.ts
9903
- import { readFileSync as readFileSync11, readdirSync as readdirSync2, existsSync as existsSync15 } from "fs";
9904
- import { join as join15 } from "path";
10068
+ import { readFileSync as readFileSync11, readdirSync as readdirSync2, existsSync as existsSync16 } from "fs";
10069
+ import { join as join16 } from "path";
9905
10070
  import { parse as parseYaml } from "yaml";
9906
10071
  import { TablationClient as TablationClient3, StaleWriteError as StaleWriteError7, TablationApiError as TablationApiError2 } from "@tablation/client";
9907
10072
  var SkillSyncError = class extends Error {
@@ -9920,9 +10085,9 @@ function parseSkillFile(path) {
9920
10085
  return { name: meta.name, description: meta.description, prompt: body, file: path };
9921
10086
  }
9922
10087
  function listLocalSkills(promptsDir) {
9923
- const dir = join15(promptsDir, "skills");
9924
- if (!existsSync15(dir)) return [];
9925
- return readdirSync2(dir).filter((f) => f.endsWith(".md")).map((f) => parseSkillFile(join15(dir, f)));
10088
+ const dir = join16(promptsDir, "skills");
10089
+ if (!existsSync16(dir)) return [];
10090
+ return readdirSync2(dir).filter((f) => f.endsWith(".md")).map((f) => parseSkillFile(join16(dir, f)));
9926
10091
  }
9927
10092
  async function syncSkills(route, opts) {
9928
10093
  if (!route.resolved) {
@@ -10101,7 +10266,7 @@ async function showLogEntry(route, entryId, opts = {}) {
10101
10266
  }
10102
10267
 
10103
10268
  // src/repos-cmd.ts
10104
- import { homedir as homedir5 } from "os";
10269
+ import { homedir as homedir6 } from "os";
10105
10270
  import { basename as basename7, resolve as resolve3 } from "path";
10106
10271
  import { isMap, isSeq, parseDocument, stringify as stringify2 } from "yaml";
10107
10272
  var ReposError = class extends Error {
@@ -10148,7 +10313,7 @@ function parseAddArgs(args, routes, isDir) {
10148
10313
  return { route, path: "." };
10149
10314
  }
10150
10315
  function expandDir(p, base) {
10151
- const home = p === "~" ? homedir5() : p.startsWith("~/") ? `${homedir5()}/${p.slice(2)}` : p;
10316
+ const home = p === "~" ? homedir6() : p.startsWith("~/") ? `${homedir6()}/${p.slice(2)}` : p;
10152
10317
  return resolve3(base, home);
10153
10318
  }
10154
10319
  function routeMapOf(doc, route) {
@@ -10283,7 +10448,7 @@ function schedulerInstalled(plan, exists, crontab) {
10283
10448
  }
10284
10449
 
10285
10450
  // src/worktree-sweep.ts
10286
- import { existsSync as existsSync16 } from "fs";
10451
+ import { existsSync as existsSync17 } from "fs";
10287
10452
  import { basename as basename8 } from "path";
10288
10453
  function planWorktreeSweep(target, tickets, contract) {
10289
10454
  const terminal = new Set(contract.statuses.resolved.filter((s) => s !== contract.statuses.verified));
@@ -10311,7 +10476,7 @@ async function applyWorktreeSweep(cwd, actions, dryRun, log, remote = "origin")
10311
10476
  let removed = 0;
10312
10477
  let keptBranches = 0;
10313
10478
  for (const a of actions) {
10314
- if (!dryRun && existsSync16(a.path)) {
10479
+ if (!dryRun && existsSync17(a.path)) {
10315
10480
  let dirty = false;
10316
10481
  try {
10317
10482
  dirty = status(a.path).length > 0;
@@ -10325,7 +10490,7 @@ async function applyWorktreeSweep(cwd, actions, dryRun, log, remote = "origin")
10325
10490
  continue;
10326
10491
  }
10327
10492
  }
10328
- if (!dryRun && a.requirePushed && a.branch && existsSync16(a.path) && remoteConfigured(cwd, remote) && worktreeHoldsUnpushed(a.path, remote)) {
10493
+ if (!dryRun && a.requirePushed && a.branch && existsSync17(a.path) && remoteConfigured(cwd, remote) && worktreeHoldsUnpushed(a.path, remote)) {
10329
10494
  log.warn(
10330
10495
  `not removing worktree ${a.path} for ${a.ticket.issue_id} (${a.ticket.status}): ${a.branch} has commits that are not on ${remote}`,
10331
10496
  { ticket: a.ticket.issue_id, step: "worktree" }
@@ -10406,14 +10571,14 @@ function applyRemoteBranchCleanup(cwd, remote, branches2, dryRun, log) {
10406
10571
  }
10407
10572
 
10408
10573
  // src/stream-sweep.ts
10409
- import { existsSync as existsSync17, readdirSync as readdirSync3, rmSync as rmSync5, statSync as statSync7 } from "fs";
10410
- import { join as join16 } from "path";
10574
+ import { existsSync as existsSync18, readdirSync as readdirSync3, rmSync as rmSync7, statSync as statSync7 } from "fs";
10575
+ import { join as join17 } from "path";
10411
10576
  var DAY_MS2 = 24 * 60 * 60 * 1e3;
10412
10577
  function planStreamSweep(stateDir, retentionDays, now = Date.now()) {
10413
- const dir = join16(stateDir, "streams");
10414
- if (!existsSync17(dir)) return [];
10578
+ const dir = join17(stateDir, "streams");
10579
+ if (!existsSync18(dir)) return [];
10415
10580
  const cutoff = now - retentionDays * DAY_MS2;
10416
- return readdirSync3(dir).map((name) => join16(dir, name)).filter((path) => {
10581
+ return readdirSync3(dir).map((name) => join17(dir, name)).filter((path) => {
10417
10582
  try {
10418
10583
  return statSync7(path).mtimeMs < cutoff;
10419
10584
  } catch {
@@ -10429,7 +10594,7 @@ function applyStreamSweep(paths, dryRun, log) {
10429
10594
  continue;
10430
10595
  }
10431
10596
  try {
10432
- rmSync5(path, { force: true });
10597
+ rmSync7(path, { force: true });
10433
10598
  removed++;
10434
10599
  log.emit(`removed stream artifact ${path}`, { step: "worktree" });
10435
10600
  } catch (e) {
@@ -10439,9 +10604,9 @@ function applyStreamSweep(paths, dryRun, log) {
10439
10604
  return removed;
10440
10605
  }
10441
10606
  function planEventsSweep(stateDir, retentionDays, now = Date.now()) {
10442
- if (!existsSync17(stateDir)) return [];
10607
+ if (!existsSync18(stateDir)) return [];
10443
10608
  const cutoff = now - retentionDays * DAY_MS2;
10444
- return readdirSync3(stateDir).filter((name) => ROTATED_EVENTS_RE.test(name)).map((name) => join16(stateDir, name)).filter((path) => {
10609
+ return readdirSync3(stateDir).filter((name) => ROTATED_EVENTS_RE.test(name)).map((name) => join17(stateDir, name)).filter((path) => {
10445
10610
  try {
10446
10611
  return statSync7(path).mtimeMs < cutoff;
10447
10612
  } catch {
@@ -10451,23 +10616,23 @@ function planEventsSweep(stateDir, retentionDays, now = Date.now()) {
10451
10616
  }
10452
10617
 
10453
10618
  // src/cli.ts
10454
- import { readFileSync as readFileSync15, existsSync as existsSync21, mkdirSync as mkdirSync12, writeFileSync as writeFileSync10, unlinkSync as unlinkSync5, copyFileSync as copyFileSync2, renameSync as renameSync2, statSync as statSync9 } from "fs";
10619
+ import { readFileSync as readFileSync15, existsSync as existsSync22, mkdirSync as mkdirSync12, writeFileSync as writeFileSync12, unlinkSync as unlinkSync5, copyFileSync as copyFileSync2, renameSync as renameSync2, statSync as statSync9 } from "fs";
10455
10620
  import { createInterface } from "readline/promises";
10456
10621
  import { dirname as dirOf, resolve as resolvePath } from "path";
10457
10622
 
10458
10623
  // src/ssh-keys.ts
10459
10624
  import { execFileSync as execFileSync3 } from "child_process";
10460
- import { existsSync as existsSync18, mkdirSync as mkdirSync9, readFileSync as readFileSync12, chmodSync } from "fs";
10461
- import { dirname as dirname11, join as join17 } from "path";
10625
+ import { existsSync as existsSync19, mkdirSync as mkdirSync9, readFileSync as readFileSync12, chmodSync } from "fs";
10626
+ import { dirname as dirname11, join as join18 } from "path";
10462
10627
  var SshKeyError = class extends Error {
10463
10628
  };
10464
10629
  function shipSshKeyPathFor(stateDir) {
10465
- return join17(stateDir, "ssh", "id_ed25519");
10630
+ return join18(stateDir, "ssh", "id_ed25519");
10466
10631
  }
10467
10632
  function ensureShipSshKeypair(stateDir) {
10468
10633
  const privateKeyPath = shipSshKeyPathFor(stateDir);
10469
10634
  const publicKeyPath = `${privateKeyPath}.pub`;
10470
- if (!existsSync18(privateKeyPath)) {
10635
+ if (!existsSync19(privateKeyPath)) {
10471
10636
  mkdirSync9(dirname11(privateKeyPath), { recursive: true });
10472
10637
  try {
10473
10638
  execFileSync3("ssh-keygen", [
@@ -10488,7 +10653,7 @@ function ensureShipSshKeypair(stateDir) {
10488
10653
  }
10489
10654
  chmodSync(privateKeyPath, 384);
10490
10655
  }
10491
- if (!existsSync18(publicKeyPath)) {
10656
+ if (!existsSync19(publicKeyPath)) {
10492
10657
  throw new SshKeyError(`${privateKeyPath} exists but ${publicKeyPath} does not \u2014 regenerate or restore it`);
10493
10658
  }
10494
10659
  const publicKey = readFileSync12(publicKeyPath, "utf8").trim();
@@ -10705,9 +10870,9 @@ function syncAllPassengerCheckouts(cfg) {
10705
10870
 
10706
10871
  // src/tunnel.ts
10707
10872
  import { spawn as spawn3 } from "child_process";
10708
- import { existsSync as existsSync19, mkdirSync as mkdirSync10, readFileSync as readFileSync13, writeFileSync as writeFileSync8, unlinkSync as unlinkSync3, readdirSync as readdirSync4 } from "fs";
10873
+ import { existsSync as existsSync20, mkdirSync as mkdirSync10, readFileSync as readFileSync13, writeFileSync as writeFileSync10, unlinkSync as unlinkSync3, readdirSync as readdirSync4 } from "fs";
10709
10874
  import { randomInt } from "crypto";
10710
- import { join as join18 } from "path";
10875
+ import { join as join19 } from "path";
10711
10876
  var DEFAULT_SPAWN_OPTIONS = { stdio: "ignore", detached: true };
10712
10877
  var defaultSpawn = (cmd, args) => spawn3(cmd, args, DEFAULT_SPAWN_OPTIONS);
10713
10878
  var SLUG_ALPHABET = "abcdefghijklmnopqrstuvwxyz0123456789";
@@ -10774,11 +10939,11 @@ function startTunnel(opts) {
10774
10939
  };
10775
10940
  }
10776
10941
  function tunnelStatePath(stateDir, workspaceId) {
10777
- return join18(stateDir, "passengers", `${workspaceId}.json`);
10942
+ return join19(stateDir, "passengers", `${workspaceId}.json`);
10778
10943
  }
10779
10944
  function readPersistedTunnel(stateDir, workspaceId) {
10780
10945
  const path = tunnelStatePath(stateDir, workspaceId);
10781
- if (!existsSync19(path)) return void 0;
10946
+ if (!existsSync20(path)) return void 0;
10782
10947
  try {
10783
10948
  return JSON.parse(readFileSync13(path, "utf8"));
10784
10949
  } catch {
@@ -10787,8 +10952,8 @@ function readPersistedTunnel(stateDir, workspaceId) {
10787
10952
  }
10788
10953
  function writePersistedTunnel(stateDir, state) {
10789
10954
  const path = tunnelStatePath(stateDir, state.workspaceId);
10790
- mkdirSync10(join18(stateDir, "passengers"), { recursive: true });
10791
- writeFileSync8(path, `${JSON.stringify(state, null, 2)}
10955
+ mkdirSync10(join19(stateDir, "passengers"), { recursive: true });
10956
+ writeFileSync10(path, `${JSON.stringify(state, null, 2)}
10792
10957
  `);
10793
10958
  }
10794
10959
  function updatePersistedTunnel(stateDir, workspaceId, patch2) {
@@ -10885,8 +11050,8 @@ function syncPassengerTunnels(plans, relayHost, relayPort, privateKeyPath, state
10885
11050
  }
10886
11051
  }
10887
11052
  function readAllPersistedWorkspaceIds(stateDir) {
10888
- const dir = join18(stateDir, "passengers");
10889
- if (!existsSync19(dir)) return [];
11053
+ const dir = join19(stateDir, "passengers");
11054
+ if (!existsSync20(dir)) return [];
10890
11055
  return readdirSync4(dir).filter((f) => f.endsWith(".json") && !f.endsWith("-sync.json")).map((f) => f.slice(0, -".json".length));
10891
11056
  }
10892
11057
 
@@ -11007,8 +11172,8 @@ async function unregisterEndpoint(workspaceId, info, d) {
11007
11172
  import { createServer as createHttpServer } from "http";
11008
11173
  import { spawn as spawn4 } from "child_process";
11009
11174
  import { randomBytes } from "crypto";
11010
- import { existsSync as existsSync20, mkdirSync as mkdirSync11, readFileSync as readFileSync14, writeFileSync as writeFileSync9, unlinkSync as unlinkSync4, readdirSync as readdirSync5 } from "fs";
11011
- import { join as join19 } from "path";
11175
+ import { existsSync as existsSync21, mkdirSync as mkdirSync11, readFileSync as readFileSync14, writeFileSync as writeFileSync11, unlinkSync as unlinkSync4, readdirSync as readdirSync5 } from "fs";
11176
+ import { join as join20 } from "path";
11012
11177
 
11013
11178
  // src/secret-compare.ts
11014
11179
  import { timingSafeEqual } from "crypto";
@@ -11097,11 +11262,11 @@ var defaultSpawn2 = (cmd, args, env) => {
11097
11262
  return child;
11098
11263
  };
11099
11264
  function syncDaemonStatePath(stateDir, workspaceId) {
11100
- return join19(stateDir, "passengers", `${workspaceId}-sync.json`);
11265
+ return join20(stateDir, "passengers", `${workspaceId}-sync.json`);
11101
11266
  }
11102
11267
  function readPersistedSyncDaemon(stateDir, workspaceId) {
11103
11268
  const path = syncDaemonStatePath(stateDir, workspaceId);
11104
- if (!existsSync20(path)) return void 0;
11269
+ if (!existsSync21(path)) return void 0;
11105
11270
  try {
11106
11271
  return JSON.parse(readFileSync14(path, "utf8"));
11107
11272
  } catch {
@@ -11110,8 +11275,8 @@ function readPersistedSyncDaemon(stateDir, workspaceId) {
11110
11275
  }
11111
11276
  function writePersistedSyncDaemon(stateDir, state) {
11112
11277
  const path = syncDaemonStatePath(stateDir, state.workspaceId);
11113
- mkdirSync11(join19(stateDir, "passengers"), { recursive: true });
11114
- writeFileSync9(path, `${JSON.stringify(state, null, 2)}
11278
+ mkdirSync11(join20(stateDir, "passengers"), { recursive: true });
11279
+ writeFileSync11(path, `${JSON.stringify(state, null, 2)}
11115
11280
  `);
11116
11281
  }
11117
11282
  function removePersistedSyncDaemon(stateDir, workspaceId) {
@@ -11131,8 +11296,8 @@ function waitForExit(pid, alive, timeoutMs = 5e3) {
11131
11296
  }
11132
11297
  }
11133
11298
  function readAllPersistedSyncWorkspaceIds(stateDir) {
11134
- const dir = join19(stateDir, "passengers");
11135
- if (!existsSync20(dir)) return [];
11299
+ const dir = join20(stateDir, "passengers");
11300
+ if (!existsSync21(dir)) return [];
11136
11301
  const suffix = "-sync.json";
11137
11302
  return readdirSync5(dir).filter((f) => f.endsWith(suffix)).map((f) => f.slice(0, -suffix.length));
11138
11303
  }
@@ -11190,7 +11355,7 @@ function syncPassengerSyncDaemons(plans, cfg, stateDir, deps = {}) {
11190
11355
 
11191
11356
  // src/daemon.ts
11192
11357
  import { readdirSync as readdirSync6, statSync as statSync8 } from "fs";
11193
- import { join as join20 } from "path";
11358
+ import { join as join21 } from "path";
11194
11359
  var DEFAULT_FLOOR_MS = 5e3;
11195
11360
  var DEFAULT_CEILING_MS = INTERVAL_SECONDS * 1e3;
11196
11361
  var defaultSleep = (ms2) => new Promise((resolve5) => setTimeout(resolve5, ms2));
@@ -11229,7 +11394,7 @@ function latestMtime(path) {
11229
11394
  if (!stat.isDirectory()) return 0;
11230
11395
  let latest = stat.mtimeMs;
11231
11396
  for (const entry of readdirSync6(path, { withFileTypes: true })) {
11232
- latest = Math.max(latest, latestMtime(join20(path, entry.name)));
11397
+ latest = Math.max(latest, latestMtime(join21(path, entry.name)));
11233
11398
  }
11234
11399
  return latest;
11235
11400
  }
@@ -11379,6 +11544,7 @@ async function runRoleAgent(role, decision, ctx) {
11379
11544
  const { route, ship, emit } = ctx;
11380
11545
  const tracker = new Tracker(route, ship);
11381
11546
  let ticketHint = decision.actionable.top?.issue_id;
11547
+ let ticketRepo = repoTargetFor(route, decision.actionable.top?.repo_id)?.name;
11382
11548
  let workingId = decision.actionable.top?.id ?? null;
11383
11549
  if (role === "dev" || role === "design" || role === "qa") {
11384
11550
  const seat = route.resolved?.seats[role];
@@ -11433,6 +11599,7 @@ async function runRoleAgent(role, decision, ctx) {
11433
11599
  ticketHint = result.ticket.issue_id;
11434
11600
  workingId = result.ticket.id;
11435
11601
  const target = repoTargetFor(route, result.ticket.repo_id);
11602
+ ticketRepo = target?.name;
11436
11603
  try {
11437
11604
  if (target && ensureRepoCheckout(target.dir, target.remote)) {
11438
11605
  emit.emit(`cloned ${target.name} into ${target.dir}`, { step: "select", role });
@@ -11455,6 +11622,7 @@ async function runRoleAgent(role, decision, ctx) {
11455
11622
  apiKey: resolveApiKey(route),
11456
11623
  cycle: emit.cycle,
11457
11624
  ticket: ticketHint,
11625
+ repo: ticketRepo,
11458
11626
  divergedPrompt: await fetchDivergedPrompt(route, role, { userAgent: ship.userAgent }),
11459
11627
  agentPersona: await fetchSeatPersona(route, role, { userAgent: ship.userAgent }),
11460
11628
  resolvedAgentId: await resolveAgentId(route, role, { userAgent: ship.userAgent })
@@ -12006,14 +12174,14 @@ if (command === "passenger-sync-daemon") {
12006
12174
  }
12007
12175
  if (host === "linux" && hasSystemd) {
12008
12176
  const label = labelFor(CREW_HOME, "run");
12009
- const staleTimer = join21(homedir6(), ".config", "systemd", "user", `${label}.timer`);
12010
- if (existsSync21(staleTimer)) {
12177
+ const staleTimer = join22(homedir7(), ".config", "systemd", "user", `${label}.timer`);
12178
+ if (existsSync22(staleTimer)) {
12011
12179
  process.stdout.write(
12012
12180
  `removing stale run timer from before the persistent-service cutover: ${staleTimer}${dryRun ? " (dry run)" : ""}
12013
12181
  `
12014
12182
  );
12015
12183
  if (!dryRun) {
12016
- spawnSync4("systemctl", ["--user", "disable", "--now", `${label}.timer`], { cwd: CREW_HOME });
12184
+ spawnSync5("systemctl", ["--user", "disable", "--now", `${label}.timer`], { cwd: CREW_HOME });
12017
12185
  try {
12018
12186
  unlinkSync5(staleTimer);
12019
12187
  } catch {
@@ -12033,7 +12201,7 @@ if (command === "passenger-sync-daemon") {
12033
12201
  const tracker = new Tracker(c, cfg.ship);
12034
12202
  problems.push(...validateContract(tracker.contract).map((p) => `contract: ${p}`));
12035
12203
  for (const r of await resolvedRepos(c)) {
12036
- if (!existsSync21(r.dir)) problems.push(`repo ${r.name}: ${r.dir} is missing on disk`);
12204
+ if (!existsSync22(r.dir)) problems.push(`repo ${r.name}: ${r.dir} is missing on disk`);
12037
12205
  if (!satisfies(host, r.config.platform)) problems.push(`repo ${r.name}: requires ${r.config.platform}, this host is ${host}`);
12038
12206
  problems.push(...validateEffective(r.config).map((p) => `repo ${r.name}: ${p}`));
12039
12207
  }
@@ -12051,8 +12219,8 @@ if (command === "passenger-sync-daemon") {
12051
12219
  }
12052
12220
  async function doctorFix(o) {
12053
12221
  const routes = await Promise.all(routesInScope(cfg.routes, named).map((c) => gatherHealth({ route: c.route, enabled: c.enabled, hasRepos: reposOf(c).length > 0 }, () => routeProblems(c))));
12054
- const unitExists = (p) => existsSync21(p);
12055
- const crontab = o.host === "linux" ? spawnSync4("crontab", ["-l"], { encoding: "utf8" }).stdout ?? "" : "";
12222
+ const unitExists = (p) => existsSync22(p);
12223
+ const crontab = o.host === "linux" ? spawnSync5("crontab", ["-l"], { encoding: "utf8" }).stdout ?? "" : "";
12056
12224
  const schedulerAbsent = o.host !== "windows" ? !schedulerInstalled(planInstall(cfg.ship, CREW_HOME, o.host, detectSystemd(), "run"), unitExists, crontab) : true;
12057
12225
  const plan = planFix({ routes, schedulerAbsent, host: o.host });
12058
12226
  for (const n of plan.notes) process.stdout.write(`fix: ${n}
@@ -12068,7 +12236,7 @@ if (command === "passenger-sync-daemon") {
12068
12236
  for (const a of plan.actions) {
12069
12237
  if (a.kind === "enable") {
12070
12238
  if (prompter && !await prompter.confirm(`Route ${a.route} passes every check \u2014 set enabled: true in ${cfg.configFile}?`, false)) continue;
12071
- writeFileSync10(cfg.configFile, planEnable(readFileSync15(cfg.configFile, "utf8"), a.route));
12239
+ writeFileSync12(cfg.configFile, planEnable(readFileSync15(cfg.configFile, "utf8"), a.route));
12072
12240
  enabledSound.add(a.route);
12073
12241
  changed2.push(`enabled route ${a.route} in ${cfg.configFile}`);
12074
12242
  } else {
@@ -12542,6 +12710,7 @@ ${changed2.map((c) => ` - ${c}
12542
12710
  apiKey: resolveApiKey(w.route),
12543
12711
  cycle: wemit.cycle,
12544
12712
  ticket: w.decision.actionable.top?.issue_id,
12713
+ repo: repoTargetFor(w.route, w.decision.actionable.top?.repo_id)?.name,
12545
12714
  divergedPrompt: await fetchDivergedPrompt(w.route, w.role, { userAgent: cfg.ship.userAgent }),
12546
12715
  agentPersona: await fetchSeatPersona(w.route, w.role, { userAgent: cfg.ship.userAgent }),
12547
12716
  resolvedAgentId: await resolveAgentId(w.route, w.role, { userAgent: cfg.ship.userAgent })
@@ -12559,6 +12728,7 @@ ${changed2.map((c) => ` - ${c}
12559
12728
  lock = fleetLock;
12560
12729
  const fleetTracker = new Tracker(w.route, cfg.ship);
12561
12730
  let fleetTicketHint = w.decision.actionable.top?.issue_id;
12731
+ let fleetTicketRepo = repoTargetFor(w.route, w.decision.actionable.top?.repo_id)?.name;
12562
12732
  let fleetWorkingId = w.decision.actionable.top?.id ?? null;
12563
12733
  if (w.role === "dev" || w.role === "design" || w.role === "qa") {
12564
12734
  const seat = w.route.resolved?.seats[w.role];
@@ -12614,6 +12784,7 @@ ${changed2.map((c) => ` - ${c}
12614
12784
  fleetTicketHint = result.ticket.issue_id;
12615
12785
  fleetWorkingId = result.ticket.id;
12616
12786
  const target = repoTargetFor(w.route, result.ticket.repo_id);
12787
+ fleetTicketRepo = target?.name;
12617
12788
  try {
12618
12789
  if (target && ensureRepoCheckout(target.dir, target.remote)) {
12619
12790
  wemit.emit(`cloned ${target.name} into ${target.dir}`, { step: "select", role: w.role });
@@ -12635,6 +12806,7 @@ ${changed2.map((c) => ` - ${c}
12635
12806
  apiKey: resolveApiKey(w.route),
12636
12807
  cycle: wemit.cycle,
12637
12808
  ticket: fleetTicketHint,
12809
+ repo: fleetTicketRepo,
12638
12810
  divergedPrompt: await fetchDivergedPrompt(w.route, w.role, { userAgent: cfg.ship.userAgent }),
12639
12811
  agentPersona: await fetchSeatPersona(w.route, w.role, { userAgent: cfg.ship.userAgent }),
12640
12812
  resolvedAgentId: await resolveAgentId(w.route, w.role, { userAgent: cfg.ship.userAgent })
@@ -12752,6 +12924,7 @@ ${changed2.map((c) => ` - ${c}
12752
12924
  lock = got;
12753
12925
  }
12754
12926
  let ticketHint = decision.actionable.top?.issue_id;
12927
+ let ticketRepo = repoTargetFor(route, decision.actionable.top?.repo_id)?.name;
12755
12928
  let workingId = decision.actionable.top?.id ?? null;
12756
12929
  if (!dryRun && (current === "dev" || current === "design" || current === "qa")) {
12757
12930
  const seat = route.resolved?.seats[current];
@@ -12813,6 +12986,7 @@ ${changed2.map((c) => ` - ${c}
12813
12986
  ticketHint = result.ticket.issue_id;
12814
12987
  workingId = result.ticket.id;
12815
12988
  const target = repoTargetFor(route, result.ticket.repo_id);
12989
+ ticketRepo = target?.name;
12816
12990
  try {
12817
12991
  if (target && ensureRepoCheckout(target.dir, target.remote)) {
12818
12992
  emit.emit(`cloned ${target.name} into ${target.dir}`, { step: "select", role: current });
@@ -12840,6 +13014,7 @@ ${changed2.map((c) => ` - ${c}
12840
13014
  apiKey: resolveApiKey(route),
12841
13015
  cycle: emit.cycle,
12842
13016
  ticket: ticketHint,
13017
+ repo: ticketRepo,
12843
13018
  divergedPrompt: await fetchDivergedPrompt(route, current, { userAgent: cfg.ship.userAgent }),
12844
13019
  agentPersona: await fetchSeatPersona(route, current, { userAgent: cfg.ship.userAgent }),
12845
13020
  resolvedAgentId: await resolveAgentId(route, current, { userAgent: cfg.ship.userAgent })
@@ -12924,7 +13099,7 @@ ${changed2.map((c) => ` - ${c}
12924
13099
  }
12925
13100
  let failed = false;
12926
13101
  for (const cmd2 of commands) {
12927
- const res2 = spawnSync4(cmd2[0], cmd2.slice(1), { cwd: CREW_HOME, encoding: "utf8" });
13102
+ const res2 = spawnSync5(cmd2[0], cmd2.slice(1), { cwd: CREW_HOME, encoding: "utf8" });
12928
13103
  if (res2.stdout) process.stdout.write(res2.stdout);
12929
13104
  if (res2.stderr) process.stderr.write(res2.stderr);
12930
13105
  if (cmd2 === control.startCommand && (res2.error || res2.status !== 0)) {
@@ -12942,7 +13117,7 @@ ${changed2.map((c) => ` - ${c}
12942
13117
  `);
12943
13118
  break;
12944
13119
  }
12945
- const res = spawnSync4(cmd[0], cmd.slice(1), { cwd: CREW_HOME, encoding: "utf8" });
13120
+ const res = spawnSync5(cmd[0], cmd.slice(1), { cwd: CREW_HOME, encoding: "utf8" });
12946
13121
  if (res.stdout) process.stdout.write(res.stdout);
12947
13122
  if (res.stderr) process.stderr.write(res.stderr);
12948
13123
  if (daemonSub !== "status" && (res.error || res.status !== 0)) {
@@ -12965,7 +13140,7 @@ ${changed2.map((c) => ` - ${c}
12965
13140
  process.exit(1);
12966
13141
  }
12967
13142
  requireArmed("run the daemon loop");
12968
- const watchPaths = isCompiledBinary(import.meta.url) ? [process.execPath] : [join21(CREW_HOME, "src"), join21(CREW_HOME, "dist"), join21(CREW_HOME, "bin")];
13143
+ const watchPaths = isCompiledBinary(import.meta.url) ? [process.execPath] : [join22(CREW_HOME, "src"), join22(CREW_HOME, "dist"), join22(CREW_HOME, "bin")];
12969
13144
  const spawnBreaker = new SpawnBreaker();
12970
13145
  const stopReason = await runDaemonLoop({
12971
13146
  runPass: () => runOnePass({
@@ -13114,11 +13289,11 @@ ${changed2.map((c) => ` - ${c}
13114
13289
  `);
13115
13290
  if (!dryRun) {
13116
13291
  const resolvedPath = resolvedPathFor(cfg.ship.stateDir, target.route);
13117
- const raw = existsSync21(resolvedPath) ? JSON.parse(readFileSync15(resolvedPath, "utf8")) : {};
13292
+ const raw = existsSync22(resolvedPath) ? JSON.parse(readFileSync15(resolvedPath, "utf8")) : {};
13118
13293
  raw.agentPersonas = personaResult.agentPersonas;
13119
13294
  raw.agentSkills = skillResult.agentSkills;
13120
13295
  mkdirSync12(dirOf(resolvedPath), { recursive: true });
13121
- writeFileSync10(resolvedPath, `${JSON.stringify(raw, null, 2)}
13296
+ writeFileSync12(resolvedPath, `${JSON.stringify(raw, null, 2)}
13122
13297
  `);
13123
13298
  }
13124
13299
  if (personaResult.outcomes.some((o) => o.action === "diverged") || skillResult.outcomes.some((o) => o.action === "diverged")) {
@@ -13163,10 +13338,10 @@ ${changed2.map((c) => ` - ${c}
13163
13338
  `);
13164
13339
  if (!dryRun) {
13165
13340
  const resolvedPath = resolvedPathFor(cfg.ship.stateDir, target.route);
13166
- const raw = existsSync21(resolvedPath) ? JSON.parse(readFileSync15(resolvedPath, "utf8")) : {};
13341
+ const raw = existsSync22(resolvedPath) ? JSON.parse(readFileSync15(resolvedPath, "utf8")) : {};
13167
13342
  raw.agentSkills = result.agentSkills;
13168
13343
  mkdirSync12(dirOf(resolvedPath), { recursive: true });
13169
- writeFileSync10(resolvedPath, `${JSON.stringify(raw, null, 2)}
13344
+ writeFileSync12(resolvedPath, `${JSON.stringify(raw, null, 2)}
13170
13345
  `);
13171
13346
  }
13172
13347
  if (result.outcomes.some((o) => o.action === "diverged")) process.exitCode = 1;
@@ -13186,7 +13361,7 @@ ${changed2.map((c) => ` - ${c}
13186
13361
  if (sub === "list" && !routeName) usage2();
13187
13362
  if (sub === "add") {
13188
13363
  try {
13189
- const a = parseAddArgs(positional.slice(2), routeNamesOf(configText), (p) => existsSync21(resolvePath(p.replace(/^~(?=\/|$)/, homedir6()))));
13364
+ const a = parseAddArgs(positional.slice(2), routeNamesOf(configText), (p) => existsSync22(resolvePath(p.replace(/^~(?=\/|$)/, homedir7()))));
13190
13365
  routeName = a.route;
13191
13366
  addPath = a.path;
13192
13367
  addName = a.name;
@@ -13201,7 +13376,7 @@ ${changed2.map((c) => ` - ${c}
13201
13376
  }
13202
13377
  const resolvedFile = resolvedPathFor(cfg.ship.stateDir, routeName);
13203
13378
  let resolvedRemotes;
13204
- if (existsSync21(resolvedFile)) {
13379
+ if (existsSync22(resolvedFile)) {
13205
13380
  try {
13206
13381
  resolvedRemotes = JSON.parse(readFileSync15(resolvedFile, "utf8"));
13207
13382
  } catch {
@@ -13218,14 +13393,14 @@ ${changed2.map((c) => ` - ${c}
13218
13393
  for (const r of repos) {
13219
13394
  const origin2 = originOf(r.dir);
13220
13395
  const row = matchRepoName(origin2, resolvedRemotes);
13221
- const note = !existsSync21(r.dir) ? "missing on disk" : row ? `Repos row: ${row}` : `no Repos row matches ${normalizeRemote(origin2) ?? "this checkout (no origin remote)"}`;
13396
+ const note = !existsSync22(r.dir) ? "missing on disk" : row ? `Repos row: ${row}` : `no Repos row matches ${normalizeRemote(origin2) ?? "this checkout (no origin remote)"}`;
13222
13397
  process.stdout.write(`${r.name} ${r.dir} ${note}
13223
13398
  `);
13224
13399
  }
13225
13400
  break;
13226
13401
  }
13227
- const target = resolvePath(addPath.replace(/^~(?=\/|$)/, homedir6()));
13228
- const top = existsSync21(target) ? gitOk(target, ["rev-parse", "--show-toplevel"]) : null;
13402
+ const target = resolvePath(addPath.replace(/^~(?=\/|$)/, homedir7()));
13403
+ const top = existsSync22(target) ? gitOk(target, ["rev-parse", "--show-toplevel"]) : null;
13229
13404
  if (!top) throw new ReposError(`${target} is not a git checkout`);
13230
13405
  const origin = originOf(top);
13231
13406
  const plan = planRepoAdd({
@@ -13244,6 +13419,7 @@ ${changed2.map((c) => ` - ${c}
13244
13419
  const planned = loadConfig(CREW_HOME, cfg.configFile, { text: plan.text, allowNoRoutes: true });
13245
13420
  const planRoute = planned.routes.find((r) => r.route === routeName);
13246
13421
  if (!planRoute) throw new ReposError(`route ${routeName} did not load from the planned config; use --no-create`);
13422
+ await hydrateApiKeys([planRoute]);
13247
13423
  try {
13248
13424
  rowTracker = new Tracker(planRoute, cfg.ship);
13249
13425
  } catch (e) {
@@ -13273,15 +13449,15 @@ ${changed2.map((c) => ` - ${c}
13273
13449
  const created = await rowTracker.createRepoRow(rowPayload);
13274
13450
  process.stdout.write(`created Repos row ${String(rowPayload.name)} (${String(rowPayload.remote)}) ${created.id}
13275
13451
  `);
13276
- if (existsSync21(resolvedFile)) {
13452
+ if (existsSync22(resolvedFile)) {
13277
13453
  const raw = JSON.parse(readFileSync15(resolvedFile, "utf8"));
13278
13454
  raw.repoNames = { ...raw.repoNames, [created.id]: String(rowPayload.name) };
13279
13455
  raw.repoRemotes = { ...raw.repoRemotes, [created.id]: String(rowPayload.remote) };
13280
- writeFileSync10(resolvedFile, `${JSON.stringify(raw, null, 2)}
13456
+ writeFileSync12(resolvedFile, `${JSON.stringify(raw, null, 2)}
13281
13457
  `);
13282
13458
  }
13283
13459
  }
13284
- writeFileSync10(cfg.configFile, plan.text);
13460
+ writeFileSync12(cfg.configFile, plan.text);
13285
13461
  process.stdout.write(`added ${top} to ${routeName} as ${plan.mode === "dir" ? "dir" : `repos.${plan.name}`} in ${cfg.configFile}
13286
13462
  `);
13287
13463
  } catch (e) {
@@ -13520,7 +13696,7 @@ Signed in as ${result.identity.email} \u2014 minted an API key in workspace "${r
13520
13696
  operator = await pickOperator(found.holds);
13521
13697
  }
13522
13698
  let previous;
13523
- const hadPreviousFile = existsSync21(resolvedPath);
13699
+ const hadPreviousFile = existsSync22(resolvedPath);
13524
13700
  if (hadPreviousFile) {
13525
13701
  try {
13526
13702
  previous = JSON.parse(readFileSync15(resolvedPath, "utf8"));
@@ -13587,7 +13763,7 @@ Signed in as ${result.identity.email} \u2014 minted an API key in workspace "${r
13587
13763
  mkdirSync12(dirname12(resolvedPath), { recursive: true });
13588
13764
  if (hadPreviousFile) copyFileSync2(resolvedPath, `${resolvedPath}.bak`);
13589
13765
  const tmpPath = `${resolvedPath}.tmp${process.pid}`;
13590
- writeFileSync10(tmpPath, `${JSON.stringify(resolved, null, 2)}
13766
+ writeFileSync12(tmpPath, `${JSON.stringify(resolved, null, 2)}
13591
13767
  `);
13592
13768
  renameSync2(tmpPath, resolvedPath);
13593
13769
  process.stderr.write(`Resolved ids written to ${resolvedPath}
@@ -13602,7 +13778,7 @@ Signed in as ${result.identity.email} \u2014 minted an API key in workspace "${r
13602
13778
  `);
13603
13779
  } else {
13604
13780
  mkdirSync12(dirname12(apiKeyFile), { recursive: true });
13605
- writeFileSync10(apiKeyFile, `${apiKeyVar}=${apiKey}
13781
+ writeFileSync12(apiKeyFile, `${apiKeyVar}=${apiKey}
13606
13782
  `, { mode: 384 });
13607
13783
  process.stderr.write(`API key written to ${apiKeyFile}
13608
13784
  `);
@@ -13611,7 +13787,7 @@ Signed in as ${result.identity.email} \u2014 minted an API key in workspace "${r
13611
13787
  const keyPersisted = keychainBacked || apiKeyFile !== void 0;
13612
13788
  const wizard = firstRun;
13613
13789
  const writeFirstRun = wizard !== void 0 && !dryRun && keyPersisted;
13614
- const routeDir = value("dir") ?? (writeFirstRun ? join21(DEFAULT_REPOS_BASE_PATH, found.workspaceSlug) : void 0);
13790
+ const routeDir = value("dir") ?? (writeFirstRun ? join22(DEFAULT_REPOS_BASE_PATH, found.workspaceSlug) : void 0);
13615
13791
  const routeBlock = renderConnection(
13616
13792
  found,
13617
13793
  route2,
@@ -14098,7 +14274,8 @@ Signed in as ${result.identity.email} \u2014 minted an API key in workspace "${r
14098
14274
  }
14099
14275
  case "disconnect": {
14100
14276
  const url = mcpUrlFor(value("base-url") ?? cfg.routes[0]?.baseUrl ?? DEFAULT_BASE_URL);
14101
- for (const h of shipPair(cfg.ship)) {
14277
+ const disconnectHarnesses = /* @__PURE__ */ new Set([...shipPair(cfg.ship), ...shipToolchains(cfg.ship).map((t) => t.harness).filter(isPairHarness)]);
14278
+ for (const h of disconnectHarnesses) {
14102
14279
  process.stderr.write(`${pairInstallerFor(h).removePair({ url, bin: cfg.ship.agent.bin, dryRun })}
14103
14280
  `);
14104
14281
  }
@@ -14249,6 +14426,8 @@ Signed in as ${result.identity.email} \u2014 minted an API key in workspace "${r
14249
14426
  repos: repos.map((r) => ({
14250
14427
  name: r.name,
14251
14428
  dir: r.dir,
14429
+ // Additive (CREW-1529): the toolchain a ticket in this repo runs on.
14430
+ toolchain: resolveToolchain(cfg.ship, route, r.name).name,
14252
14431
  // Additive (CREW-1379): `unsafe` means a ship must not cut a
14253
14432
  // worktree or release from this base. `version` stays 1.
14254
14433
  baseBranch: baseHealth(r),
@@ -14351,7 +14530,7 @@ routes: ${cfg.routes.map((c) => c.route).join(", ")}
14351
14530
  );
14352
14531
  try {
14353
14532
  const evFile = eventFileFor(cfg.ship.stateDir);
14354
- const evSize = existsSync21(evFile) ? statSync9(evFile).size : 0;
14533
+ const evSize = existsSync22(evFile) ? statSync9(evFile).size : 0;
14355
14534
  process.stdout.write(
14356
14535
  evSize > cfg.ship.eventsRotateBytes * 4 ? `events log: WARN ${evFile} is ${Math.round(evSize / 1048576)} MB (> 4x eventsRotateBytes); run \`crew reap\` to rotate it
14357
14536
  ` : "events log: ok\n"
@@ -14489,6 +14668,10 @@ crew manifest: ${seats2.length ? seats2.map((c) => c.name).join(", ") : "non
14489
14668
  `);
14490
14669
  }
14491
14670
  }
14671
+ for (const [repoName, ov] of Object.entries(route.repoOverrides)) {
14672
+ if (ov.toolchain) process.stdout.write(`repo toolchain: ${repoName} \u2192 ${ov.toolchain} (binary and MCP checked above, as for every ship toolchain)
14673
+ `);
14674
+ }
14492
14675
  process.stdout.write(`git: ${gitPath ? `OK (${gitPath})` : "NOT FOUND on the scheduler PATH"}
14493
14676
  `);
14494
14677
  if (!allBinsFound || !gitPath) {