@tablation/crew 0.35.0 → 0.37.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);
@@ -1091,8 +1249,8 @@ function explain(ship, required) {
1091
1249
  }
1092
1250
 
1093
1251
  // src/repo-config.ts
1094
- import { readFileSync as readFileSync4, existsSync as existsSync3 } from "fs";
1095
- 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";
1096
1254
  import { parse } from "yaml";
1097
1255
  var SPEC_VERSION = 1;
1098
1256
  var REPO_CONFIG_FILENAMES = [".crew.yaml", ".crew.yml", ".crew.json"];
@@ -1165,8 +1323,8 @@ var MODES = ["local", "integrate", "ci_manual", "ci_auto", "external"];
1165
1323
  var PROVIDERS = ["github", "buildkite", "other", "none"];
1166
1324
  function findRepoConfig(dir) {
1167
1325
  for (const name of REPO_CONFIG_FILENAMES) {
1168
- const p = join4(dir, name);
1169
- if (existsSync3(p)) return p;
1326
+ const p = join5(dir, name);
1327
+ if (existsSync4(p)) return p;
1170
1328
  }
1171
1329
  return null;
1172
1330
  }
@@ -1808,16 +1966,16 @@ function mergeRouteRelease(route, override) {
1808
1966
  };
1809
1967
  }
1810
1968
  function expand(p, base) {
1811
- let out = p.startsWith("~") ? p.replace(/^~/, homedir3()) : p;
1812
- out = out.replace(/\$\{?HOME\}?/g, homedir3());
1969
+ let out = p.startsWith("~") ? p.replace(/^~/, homedir4()) : p;
1970
+ out = out.replace(/\$\{?HOME\}?/g, homedir4());
1813
1971
  return isAbsolute(out) ? out : resolve(base, out);
1814
1972
  }
1815
1973
  function resolvePromptsDir(promptSet, crewHome, base) {
1816
- if (!promptSet) return join5(crewHome, "prompts", "default");
1974
+ if (!promptSet) return join6(crewHome, "prompts", "default");
1817
1975
  if (promptSet.includes("/") || promptSet.startsWith(".") || promptSet.startsWith("~")) {
1818
1976
  return expand(promptSet, base);
1819
1977
  }
1820
- return join5(crewHome, "prompts", promptSet);
1978
+ return join6(crewHome, "prompts", promptSet);
1821
1979
  }
1822
1980
  var Missing = class {
1823
1981
  paths = [];
@@ -1841,7 +1999,7 @@ var Missing = class {
1841
1999
  };
1842
2000
  function crewVersion(crewHome) {
1843
2001
  try {
1844
- const pkg = JSON.parse(readFileSync5(join5(crewHome, "package.json"), "utf8"));
2002
+ const pkg = JSON.parse(readFileSync5(join6(crewHome, "package.json"), "utf8"));
1845
2003
  return pkg.version ?? "0.0.0";
1846
2004
  } catch {
1847
2005
  return "0.0.0";
@@ -1851,26 +2009,26 @@ function configSearchPath(crewHome) {
1851
2009
  if (process.env.CREW_CONFIG) return [resolve(process.env.CREW_CONFIG)];
1852
2010
  const xdg = process.env.XDG_CONFIG_HOME;
1853
2011
  return [
1854
- ...xdg ? [join5(xdg, "crew", "crew.yaml")] : [],
1855
- join5(homedir3(), ".config", "crew", "crew.yaml"),
2012
+ ...xdg ? [join6(xdg, "crew", "crew.yaml")] : [],
2013
+ join6(homedir4(), ".config", "crew", "crew.yaml"),
1856
2014
  resolve(crewHome, "crew.yaml")
1857
2015
  ];
1858
2016
  }
1859
2017
  function findConfigFile(crewHome) {
1860
2018
  const candidates = configSearchPath(crewHome);
1861
- return candidates.find((c) => existsSync4(c)) ?? candidates[candidates.length - 1];
2019
+ return candidates.find((c) => existsSync5(c)) ?? candidates[candidates.length - 1];
1862
2020
  }
1863
- 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");
1864
2022
  function routeSlug(route) {
1865
2023
  return route.replace(/\//g, "-");
1866
2024
  }
1867
2025
  function resolvedPathFor(stateDir, route) {
1868
2026
  const [workspace, project] = splitRoute(route);
1869
- return join5(stateDir, "resolved", workspace, `${project}.json`);
2027
+ return join6(stateDir, "resolved", workspace, `${project}.json`);
1870
2028
  }
1871
2029
  function apiKeyPathFor(stateDir, route) {
1872
2030
  const [workspace, project] = splitRoute(route);
1873
- return join5(stateDir, "keys", workspace, `${project}.env`);
2031
+ return join6(stateDir, "keys", workspace, `${project}.env`);
1874
2032
  }
1875
2033
  function splitRoute(route) {
1876
2034
  const parts = route.split("/");
@@ -1881,7 +2039,7 @@ function splitRoute(route) {
1881
2039
  }
1882
2040
  function readResolvedFile(stateDir, route) {
1883
2041
  const path = resolvedPathFor(stateDir, route);
1884
- if (!existsSync4(path)) return void 0;
2042
+ if (!existsSync5(path)) return void 0;
1885
2043
  try {
1886
2044
  return JSON.parse(readFileSync5(path, "utf8"));
1887
2045
  } catch (e) {
@@ -1939,13 +2097,13 @@ function parseResolved(raw, m, where) {
1939
2097
  }
1940
2098
  function loadConfig(crewHome, configFile, opts = {}) {
1941
2099
  const file = configFile ?? findConfigFile(crewHome);
1942
- if (opts.text === void 0 && !existsSync4(file)) {
2100
+ if (opts.text === void 0 && !existsSync5(file)) {
1943
2101
  throw new ConfigError(
1944
2102
  `no config found. Looked in:
1945
2103
  ` + configSearchPath(crewHome).map((c) => ` - ${c}`).join("\n") + `
1946
2104
 
1947
2105
  Create the first of those:
1948
- mkdir -p ${dirname3(defaultConfigPath())} && cp ${join5(crewHome, "crew.example.yaml")} ${defaultConfigPath()}`
2106
+ mkdir -p ${dirname3(defaultConfigPath())} && cp ${join6(crewHome, "crew.example.yaml")} ${defaultConfigPath()}`
1949
2107
  );
1950
2108
  }
1951
2109
  let raw;
@@ -2028,7 +2186,7 @@ Create the first of those:
2028
2186
  useNvm: shipRaw.useNvm !== false,
2029
2187
  nvmSh: shipRaw.nvmSh,
2030
2188
  stateDir,
2031
- logFile: expand(shipRaw.logFile ?? join5(tmpdir(), "crew.log"), base),
2189
+ logFile: expand(shipRaw.logFile ?? join6(tmpdir(), "crew.log"), base),
2032
2190
  userAgent: shipRaw.userAgent ?? `Mozilla/5.0 CrewAgent/${crewVersion(crewHome)}`,
2033
2191
  maxConcurrentAgents,
2034
2192
  streamRetentionDays,
@@ -2248,7 +2406,7 @@ function repoIdForName(r, name) {
2248
2406
  }
2249
2407
  function resolveApiKey(r) {
2250
2408
  if (r.apiKey) return r.apiKey;
2251
- if (r.apiKeyFile && r.apiKeyVar && existsSync4(r.apiKeyFile)) {
2409
+ if (r.apiKeyFile && r.apiKeyVar && existsSync5(r.apiKeyFile)) {
2252
2410
  for (const line of readFileSync5(r.apiKeyFile, "utf8").split("\n")) {
2253
2411
  const m = line.match(new RegExp(`^${r.apiKeyVar}=(.*)$`));
2254
2412
  if (m) return (m[1] ?? "").trim().replace(/^["']|["']$/g, "");
@@ -2312,7 +2470,7 @@ function configuredMembers(rt) {
2312
2470
  return out;
2313
2471
  }
2314
2472
  function defaultRepoDir(basePath, workspace, repoName) {
2315
- return join5(basePath, workspace, repoName);
2473
+ return join6(basePath, workspace, repoName);
2316
2474
  }
2317
2475
  function reposOf(r) {
2318
2476
  const named = Object.entries(r.repos);
@@ -2336,7 +2494,7 @@ function passengerRepoTargets(r) {
2336
2494
  return reposOf(r).filter((t) => r.repoOverrides[t.name]?.hostPassengers !== false);
2337
2495
  }
2338
2496
  function passengerCheckoutDir(basePath, workspace, repoName) {
2339
- return join5(basePath, workspace, ".passenger-checkouts", repoName);
2497
+ return join6(basePath, workspace, ".passenger-checkouts", repoName);
2340
2498
  }
2341
2499
  function passengerCheckoutTargets(r) {
2342
2500
  if (!r.hostPassengers) return [];
@@ -2468,8 +2626,8 @@ function rosterMarkdown(roster, meId) {
2468
2626
  }
2469
2627
 
2470
2628
  // src/state.ts
2471
- import { readFileSync as readFileSync6, writeFileSync as writeFileSync2, existsSync as existsSync5, mkdirSync as mkdirSync3, rmSync as rmSync3, statSync as statSync2 } from "fs";
2472
- 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";
2473
2631
  var EPOCH = "1970-01-01T00:00:00.000Z";
2474
2632
  var State = class _State {
2475
2633
  dir;
@@ -2478,7 +2636,7 @@ var State = class _State {
2478
2636
  mkdirSync3(dir, { recursive: true });
2479
2637
  }
2480
2638
  path(...parts) {
2481
- return join6(this.dir, ...parts);
2639
+ return join7(this.dir, ...parts);
2482
2640
  }
2483
2641
  /**
2484
2642
  * The timestamp every "new since last poll" check is measured against.
@@ -2496,7 +2654,7 @@ var State = class _State {
2496
2654
  }
2497
2655
  }
2498
2656
  setWatermark(at) {
2499
- writeFileSync2(this.path(".poll-watermark"), `${at}
2657
+ writeFileSync4(this.path(".poll-watermark"), `${at}
2500
2658
  `);
2501
2659
  }
2502
2660
  /**
@@ -2514,19 +2672,19 @@ var State = class _State {
2514
2672
  }
2515
2673
  /** Whole-crew pause, and the per-role sentinels one level down. */
2516
2674
  isPaused() {
2517
- return existsSync5(this.path(".crew-paused"));
2675
+ return existsSync6(this.path(".crew-paused"));
2518
2676
  }
2519
2677
  isRolePaused(role) {
2520
- return existsSync5(this.path(`.role-paused-${role}`));
2678
+ return existsSync6(this.path(`.role-paused-${role}`));
2521
2679
  }
2522
2680
  pausedRoles(roles) {
2523
2681
  return new Set(roles.filter((r) => this.isRolePaused(r)));
2524
2682
  }
2525
2683
  pause(role) {
2526
- writeFileSync2(role ? this.path(`.role-paused-${role}`) : this.path(".crew-paused"), "");
2684
+ writeFileSync4(role ? this.path(`.role-paused-${role}`) : this.path(".crew-paused"), "");
2527
2685
  }
2528
2686
  resume(role) {
2529
- 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 });
2530
2688
  }
2531
2689
  /**
2532
2690
  * The release phase (merge, deploy, release) has its own sentinel, one
@@ -2534,11 +2692,11 @@ var State = class _State {
2534
2692
  * while agents keep working.
2535
2693
  */
2536
2694
  isReleasePaused() {
2537
- return existsSync5(this.path(".release-paused"));
2695
+ return existsSync6(this.path(".release-paused"));
2538
2696
  }
2539
2697
  setReleasePaused(paused) {
2540
- if (paused) writeFileSync2(this.path(".release-paused"), "");
2541
- 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 });
2542
2700
  }
2543
2701
  /**
2544
2702
  * Why a release cycle must not run right now, or null when it may. Every
@@ -2585,13 +2743,13 @@ var State = class _State {
2585
2743
  }
2586
2744
  } catch {
2587
2745
  }
2588
- writeFileSync2(file, `${process.pid}
2746
+ writeFileSync4(file, `${process.pid}
2589
2747
  `);
2590
2748
  return {
2591
2749
  ok: true,
2592
2750
  release: () => {
2593
2751
  try {
2594
- rmSync3(file, { force: true });
2752
+ rmSync5(file, { force: true });
2595
2753
  } catch {
2596
2754
  }
2597
2755
  }
@@ -2678,12 +2836,12 @@ var State = class _State {
2678
2836
  blockedCount: () => Number.parseInt(read(blocked) ?? "", 10) || 0,
2679
2837
  noteBlocked: () => {
2680
2838
  const n = (Number.parseInt(read(blocked) ?? "", 10) || 0) + 1;
2681
- writeFileSync2(this.path(blocked), `${n}
2839
+ writeFileSync4(this.path(blocked), `${n}
2682
2840
  `);
2683
2841
  return n;
2684
2842
  },
2685
2843
  clearBlocked: () => {
2686
- rmSync3(this.path(blocked), { force: true });
2844
+ rmSync5(this.path(blocked), { force: true });
2687
2845
  },
2688
2846
  /**
2689
2847
  * The commit a deploy last failed on.
@@ -2695,11 +2853,11 @@ var State = class _State {
2695
2853
  */
2696
2854
  deployFailedSha: () => read(failed),
2697
2855
  noteDeployFailed: (sha) => {
2698
- writeFileSync2(this.path(failed), `${sha}
2856
+ writeFileSync4(this.path(failed), `${sha}
2699
2857
  `);
2700
2858
  },
2701
2859
  clearDeployFailed: () => {
2702
- rmSync3(this.path(failed), { force: true });
2860
+ rmSync5(this.path(failed), { force: true });
2703
2861
  },
2704
2862
  /**
2705
2863
  * Consecutive test-gate failures on the same head (CREW-1368). A failure
@@ -2709,12 +2867,12 @@ var State = class _State {
2709
2867
  noteTestGateFailed: (sha) => {
2710
2868
  const [prev, n] = (read(gateFailed) ?? "").split(" ");
2711
2869
  const next = prev === sha ? (Number.parseInt(n ?? "", 10) || 0) + 1 : 1;
2712
- writeFileSync2(this.path(gateFailed), `${sha} ${next}
2870
+ writeFileSync4(this.path(gateFailed), `${sha} ${next}
2713
2871
  `);
2714
2872
  return next;
2715
2873
  },
2716
2874
  clearTestGateFailed: () => {
2717
- rmSync3(this.path(gateFailed), { force: true });
2875
+ rmSync5(this.path(gateFailed), { force: true });
2718
2876
  },
2719
2877
  /** Whether the board was already told about this head's red gate. */
2720
2878
  heldReported: () => {
@@ -2725,15 +2883,15 @@ var State = class _State {
2725
2883
  }
2726
2884
  },
2727
2885
  noteHeldReported: (signature) => {
2728
- writeFileSync2(this.path(heldReported), `${signature}
2886
+ writeFileSync4(this.path(heldReported), `${signature}
2729
2887
  `);
2730
2888
  },
2731
2889
  clearHeldReported: () => {
2732
- rmSync3(this.path(heldReported), { force: true });
2890
+ rmSync5(this.path(heldReported), { force: true });
2733
2891
  },
2734
2892
  testGateReported: (sha) => read(gateReported) === sha,
2735
2893
  noteTestGateReported: (sha) => {
2736
- writeFileSync2(this.path(gateReported), `${sha}
2894
+ writeFileSync4(this.path(gateReported), `${sha}
2737
2895
  `);
2738
2896
  }
2739
2897
  };
@@ -2762,7 +2920,7 @@ var State = class _State {
2762
2920
  }
2763
2921
  };
2764
2922
  const clear2 = () => {
2765
- 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 });
2766
2924
  };
2767
2925
  return {
2768
2926
  /** Consecutive cycles this route had actionable work and did not win. */
@@ -2789,18 +2947,18 @@ var State = class _State {
2789
2947
  clear2();
2790
2948
  return;
2791
2949
  }
2792
- writeFileSync2(this.path(streakFile), `${this.readInt(streakFile) + 1}
2950
+ writeFileSync4(this.path(streakFile), `${this.readInt(streakFile) + 1}
2793
2951
  `);
2794
2952
  const prior = read(ticketFile);
2795
2953
  if (topTicket && topTicket === prior) {
2796
- writeFileSync2(this.path(ticketStreakFile), `${this.readInt(ticketStreakFile) + 1}
2954
+ writeFileSync4(this.path(ticketStreakFile), `${this.readInt(ticketStreakFile) + 1}
2797
2955
  `);
2798
2956
  } else if (topTicket) {
2799
- writeFileSync2(this.path(ticketFile), `${topTicket}
2957
+ writeFileSync4(this.path(ticketFile), `${topTicket}
2800
2958
  `);
2801
- writeFileSync2(this.path(sinceFile), `${(/* @__PURE__ */ new Date()).toISOString()}
2959
+ writeFileSync4(this.path(sinceFile), `${(/* @__PURE__ */ new Date()).toISOString()}
2802
2960
  `);
2803
- writeFileSync2(this.path(ticketStreakFile), "1\n");
2961
+ writeFileSync4(this.path(ticketStreakFile), "1\n");
2804
2962
  }
2805
2963
  }
2806
2964
  };
@@ -2828,7 +2986,7 @@ var State = class _State {
2828
2986
  }
2829
2987
  },
2830
2988
  persist: (next) => {
2831
- writeFileSync2(this.path(file), JSON.stringify(next));
2989
+ writeFileSync4(this.path(file), JSON.stringify(next));
2832
2990
  }
2833
2991
  };
2834
2992
  }
@@ -2849,7 +3007,7 @@ var State = class _State {
2849
3007
  }
2850
3008
  },
2851
3009
  persist: (next) => {
2852
- writeFileSync2(this.path(file), JSON.stringify(next));
3010
+ writeFileSync4(this.path(file), JSON.stringify(next));
2853
3011
  }
2854
3012
  };
2855
3013
  }
@@ -2862,7 +3020,7 @@ var State = class _State {
2862
3020
  const file = ".release-last-run";
2863
3021
  return {
2864
3022
  touch: () => {
2865
- writeFileSync2(this.path(file), (/* @__PURE__ */ new Date()).toISOString());
3023
+ writeFileSync4(this.path(file), (/* @__PURE__ */ new Date()).toISOString());
2866
3024
  },
2867
3025
  at: () => {
2868
3026
  try {
@@ -3179,10 +3337,10 @@ function describeRelease(o, route) {
3179
3337
  }
3180
3338
 
3181
3339
  // src/install.ts
3182
- import { dirname as dirname4, join as join7, extname } from "path";
3183
- import { homedir as homedir4 } from "os";
3184
- import { accessSync, constants, existsSync as existsSync6, mkdirSync as mkdirSync4, readdirSync, readFileSync as readFileSync7, statSync as statSync3, writeFileSync as writeFileSync3, unlinkSync, realpathSync } from "fs";
3185
- 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";
3186
3344
  import { createHash } from "crypto";
3187
3345
 
3188
3346
  // src/runtime-info.ts
@@ -3193,7 +3351,7 @@ function isCompiledBinary(moduleUrl) {
3193
3351
  // src/install.ts
3194
3352
  function crewInvocation(crewHome) {
3195
3353
  if (isCompiledBinary(import.meta.url)) return [process.execPath];
3196
- return [process.execPath, join7(crewHome, "bin", "crew")];
3354
+ return [process.execPath, join8(crewHome, "bin", "crew")];
3197
3355
  }
3198
3356
  var INTERVAL_SECONDS = 120;
3199
3357
  function labelFor(crewHome, job) {
@@ -3231,7 +3389,7 @@ function pathFor(ship) {
3231
3389
  return parts.join(":");
3232
3390
  }
3233
3391
  function findOnPath(bin, searchPath) {
3234
- 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));
3235
3393
  for (const candidate of candidates) {
3236
3394
  try {
3237
3395
  accessSync(candidate, constants.X_OK);
@@ -3241,7 +3399,7 @@ function findOnPath(bin, searchPath) {
3241
3399
  }
3242
3400
  return void 0;
3243
3401
  }
3244
- 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(":");
3245
3403
  function dockerPathProblem(ship, shellPath = process.env.PATH ?? "") {
3246
3404
  const schedulerPath = pathFor(ship);
3247
3405
  if (findOnPath("docker", schedulerPath)) return void 0;
@@ -3253,7 +3411,7 @@ function xmlEscape(s) {
3253
3411
  }
3254
3412
  function planLaunchd(ship, crewHome, job) {
3255
3413
  const label = labelFor(crewHome, job);
3256
- const unitPath = join7(homedir4(), "Library", "LaunchAgents", `${label}.plist`);
3414
+ const unitPath = join8(homedir5(), "Library", "LaunchAgents", `${label}.plist`);
3257
3415
  const crewLog = ship.logFile;
3258
3416
  const schedulerLog = schedulerLogFor(crewLog, job);
3259
3417
  const persistent = job === "run";
@@ -3305,8 +3463,8 @@ ${scheduleKeys}
3305
3463
  }
3306
3464
  function planSystemd(ship, crewHome, job) {
3307
3465
  const label = labelFor(crewHome, job);
3308
- const unitDir = join7(homedir4(), ".config", "systemd", "user");
3309
- const servicePath = join7(unitDir, `${label}.service`);
3466
+ const unitDir = join8(homedir5(), ".config", "systemd", "user");
3467
+ const servicePath = join8(unitDir, `${label}.service`);
3310
3468
  const crewLog = ship.logFile;
3311
3469
  const schedulerLog = schedulerLogFor(crewLog, job);
3312
3470
  const description = job === "release" ? "Tablation crew \u2014 release" : job === "passengers" ? "Tablation crew \u2014 Host Passengers sync" : "Tablation crew \u2014 persistent supervisor loop";
@@ -3343,7 +3501,7 @@ WantedBy=default.target
3343
3501
  schedulerLog
3344
3502
  };
3345
3503
  }
3346
- const timerPath = join7(unitDir, `${label}.timer`);
3504
+ const timerPath = join8(unitDir, `${label}.timer`);
3347
3505
  const service = `# Written by \`crew install\` \u2014 re-run it rather than hand-editing this file.
3348
3506
  [Unit]
3349
3507
  Description=${description}
@@ -3410,7 +3568,7 @@ function planInstall(ship, crewHome, hostShip, hasSystemd, job = "run") {
3410
3568
  function planUninstall(ship, crewHome, hostShip, hasSystemd, job = "run") {
3411
3569
  const label = labelFor(crewHome, job);
3412
3570
  if (hostShip === "macos") {
3413
- const unitPath = join7(homedir4(), "Library", "LaunchAgents", `${label}.plist`);
3571
+ const unitPath = join8(homedir5(), "Library", "LaunchAgents", `${label}.plist`);
3414
3572
  return {
3415
3573
  mechanism: "launchd",
3416
3574
  unitPaths: [unitPath],
@@ -3420,18 +3578,18 @@ function planUninstall(ship, crewHome, hostShip, hasSystemd, job = "run") {
3420
3578
  };
3421
3579
  }
3422
3580
  if (hostShip === "linux" && hasSystemd) {
3423
- const unitDir = join7(homedir4(), ".config", "systemd", "user");
3581
+ const unitDir = join8(homedir5(), ".config", "systemd", "user");
3424
3582
  if (job === "run") {
3425
3583
  return {
3426
3584
  mechanism: "systemd",
3427
- unitPaths: [join7(unitDir, `${label}.service`)],
3585
+ unitPaths: [join8(unitDir, `${label}.service`)],
3428
3586
  crontabLine: null,
3429
3587
  unloadCommands: [["systemctl", "--user", "disable", "--now", `${label}.service`]]
3430
3588
  };
3431
3589
  }
3432
3590
  return {
3433
3591
  mechanism: "systemd",
3434
- unitPaths: [join7(unitDir, `${label}.service`), join7(unitDir, `${label}.timer`)],
3592
+ unitPaths: [join8(unitDir, `${label}.service`), join8(unitDir, `${label}.timer`)],
3435
3593
  crontabLine: null,
3436
3594
  unloadCommands: [["systemctl", "--user", "disable", "--now", `${label}.timer`]]
3437
3595
  };
@@ -3449,7 +3607,7 @@ function planUninstall(ship, crewHome, hostShip, hasSystemd, job = "run") {
3449
3607
  function planDaemonControl(crewHome, hostShip, hasSystemd) {
3450
3608
  const label = labelFor(crewHome, "run");
3451
3609
  if (hostShip === "macos") {
3452
- const unitPath = join7(homedir4(), "Library", "LaunchAgents", `${label}.plist`);
3610
+ const unitPath = join8(homedir5(), "Library", "LaunchAgents", `${label}.plist`);
3453
3611
  return {
3454
3612
  mechanism: "launchd",
3455
3613
  startCommand: ["launchctl", "load", unitPath],
@@ -3473,7 +3631,7 @@ function planDaemonControl(crewHome, hostShip, hasSystemd) {
3473
3631
  }
3474
3632
  function runAll(commands, cwd, log) {
3475
3633
  for (const [bin, ...args] of commands) {
3476
- const res = spawnSync3(bin, args, { cwd, encoding: "utf8" });
3634
+ const res = spawnSync4(bin, args, { cwd, encoding: "utf8" });
3477
3635
  if (res.error || res.status !== 0) {
3478
3636
  log.warn(`${bin} ${args.join(" ")} exited ${res.status ?? "error"}`, {
3479
3637
  output: `${res.stdout ?? ""}${res.stderr ?? ""}${res.error ? String(res.error) : ""}`
@@ -3490,7 +3648,7 @@ async function applyInstall(plan, crewHome, dryRun, log) {
3490
3648
  continue;
3491
3649
  }
3492
3650
  mkdirSync4(dirname4(path), { recursive: true });
3493
- writeFileSync3(path, plan.unitContent[path]);
3651
+ writeFileSync5(path, plan.unitContent[path]);
3494
3652
  log.emit(`wrote ${path}`);
3495
3653
  }
3496
3654
  if (plan.crontabLine) {
@@ -3538,13 +3696,13 @@ async function applyUninstall(plan, crewHome, dryRun, log) {
3538
3696
  if (dryRun) {
3539
3697
  log.emit(`would run: launchctl remove ${plan.removeLabel}`);
3540
3698
  } else {
3541
- if (spawnSync3("launchctl", ["remove", plan.removeLabel], { encoding: "utf8" }).status === 0) {
3699
+ if (spawnSync4("launchctl", ["remove", plan.removeLabel], { encoding: "utf8" }).status === 0) {
3542
3700
  log.emit(`removed loaded job ${plan.removeLabel}`);
3543
3701
  }
3544
3702
  }
3545
3703
  }
3546
3704
  for (const path of plan.unitPaths) {
3547
- if (!existsSync6(path)) continue;
3705
+ if (!existsSync7(path)) continue;
3548
3706
  if (dryRun) {
3549
3707
  log.emit(`would remove ${path}`);
3550
3708
  continue;
@@ -3563,7 +3721,7 @@ async function applyUninstall(plan, crewHome, dryRun, log) {
3563
3721
  }
3564
3722
  var CREW_LAUNCHD_PREFIX = "com.tablation.crew";
3565
3723
  function launchAgentsDir() {
3566
- return join7(homedir4(), "Library", "LaunchAgents");
3724
+ return join8(homedir5(), "Library", "LaunchAgents");
3567
3725
  }
3568
3726
  function xmlUnescape(s) {
3569
3727
  return s.replace(/&lt;/g, "<").replace(/&gt;/g, ">").replace(/&amp;/g, "&");
@@ -3575,14 +3733,14 @@ function programFromPlist(text) {
3575
3733
  return args.find((a) => a.endsWith("/bin/crew")) ?? args[0] ?? null;
3576
3734
  }
3577
3735
  function loadedCrewLabels() {
3578
- const list = spawnSync3("launchctl", ["list"], { encoding: "utf8" });
3736
+ const list = spawnSync4("launchctl", ["list"], { encoding: "utf8" });
3579
3737
  if (list.status !== 0) return [];
3580
3738
  return [...parseLaunchctlList(list.stdout ?? "").keys()].filter((l) => l.startsWith(CREW_LAUNCHD_PREFIX));
3581
3739
  }
3582
3740
  function listLaunchdUnits(dir = launchAgentsDir(), loaded = []) {
3583
- const files = existsSync6(dir) ? readdirSync(dir) : [];
3741
+ const files = existsSync7(dir) ? readdirSync(dir) : [];
3584
3742
  const units = files.filter((f) => f.startsWith(CREW_LAUNCHD_PREFIX) && f.endsWith(".plist")).sort().map((f) => {
3585
- const plistPath = join7(dir, f);
3743
+ const plistPath = join8(dir, f);
3586
3744
  let program = null;
3587
3745
  try {
3588
3746
  program = programFromPlist(readFileSync7(plistPath, "utf8"));
@@ -3592,13 +3750,13 @@ function listLaunchdUnits(dir = launchAgentsDir(), loaded = []) {
3592
3750
  label: f.slice(0, -".plist".length),
3593
3751
  plistPath,
3594
3752
  program,
3595
- programExists: program !== null && existsSync6(program)
3753
+ programExists: program !== null && existsSync7(program)
3596
3754
  };
3597
3755
  });
3598
3756
  const seen = new Set(units.map((u) => u.label));
3599
3757
  for (const label of [...loaded].sort()) {
3600
3758
  if (!label.startsWith(CREW_LAUNCHD_PREFIX) || seen.has(label)) continue;
3601
- 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 });
3602
3760
  }
3603
3761
  return units;
3604
3762
  }
@@ -3611,9 +3769,9 @@ function removeLaunchdUnit(unit, dryRun, log) {
3611
3769
  log.emit(`would unload and remove ${unit.label}`);
3612
3770
  return;
3613
3771
  }
3614
- if (existsSync6(unit.plistPath)) spawnSync3("launchctl", ["unload", unit.plistPath], { encoding: "utf8" });
3615
- spawnSync3("launchctl", ["remove", unit.label], { encoding: "utf8" });
3616
- 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);
3617
3775
  log.emit(`removed ${unit.label} (${unit.plistPath})`);
3618
3776
  }
3619
3777
  function clearForeignLaunchdUnits(crewHome, replace, dryRun, log, dir, loaded) {
@@ -3645,7 +3803,7 @@ function parseLaunchctlList(text) {
3645
3803
  return jobs;
3646
3804
  }
3647
3805
  function describeForeignLaunchdUnits(crewHome, dir) {
3648
- const list = spawnSync3("launchctl", ["list"], { encoding: "utf8" });
3806
+ const list = spawnSync4("launchctl", ["list"], { encoding: "utf8" });
3649
3807
  const jobs = parseLaunchctlList(list.status === 0 ? list.stdout ?? "" : "");
3650
3808
  const foreign = foreignLaunchdUnits(crewHome, dir, jobs.keys());
3651
3809
  return foreign.map((u) => {
@@ -3658,11 +3816,11 @@ function describeForeignLaunchdUnits(crewHome, dir) {
3658
3816
  });
3659
3817
  }
3660
3818
  function readCrontab() {
3661
- const res = spawnSync3("crontab", ["-l"], { encoding: "utf8" });
3819
+ const res = spawnSync4("crontab", ["-l"], { encoding: "utf8" });
3662
3820
  return res.status === 0 ? res.stdout ?? "" : "";
3663
3821
  }
3664
3822
  function writeCrontab(text) {
3665
- spawnSync3("crontab", ["-"], { input: text, encoding: "utf8" });
3823
+ spawnSync4("crontab", ["-"], { input: text, encoding: "utf8" });
3666
3824
  }
3667
3825
  function addCrontabLine(marker, line) {
3668
3826
  const existing = readCrontab();
@@ -3686,7 +3844,7 @@ function removeCrontabLine(marker) {
3686
3844
  }
3687
3845
  function detectSystemd() {
3688
3846
  if (process.platform !== "linux") return false;
3689
- const res = spawnSync3("systemctl", ["--user", "status"], { encoding: "utf8" });
3847
+ const res = spawnSync4("systemctl", ["--user", "status"], { encoding: "utf8" });
3690
3848
  return !res.error;
3691
3849
  }
3692
3850
 
@@ -3696,7 +3854,7 @@ import { statfsSync } from "fs";
3696
3854
  // src/git.ts
3697
3855
  import { execFileSync } from "child_process";
3698
3856
  import {
3699
- existsSync as existsSync7,
3857
+ existsSync as existsSync8,
3700
3858
  mkdirSync as mkdirSync5,
3701
3859
  openSync as openSync2,
3702
3860
  closeSync as closeSync2,
@@ -4068,7 +4226,7 @@ function cloneUrlFor(remote) {
4068
4226
  return `git@github.com:${remote}.git`;
4069
4227
  }
4070
4228
  function ensureRepoCheckout(dir, remote) {
4071
- if (existsSync7(`${dir}/.git`)) return false;
4229
+ if (existsSync8(`${dir}/.git`)) return false;
4072
4230
  if (!remote) {
4073
4231
  throw new GitError(
4074
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`
@@ -5805,8 +5963,8 @@ function materializeBranchForTicket(dir, cfg, t, role) {
5805
5963
  }
5806
5964
 
5807
5965
  // src/poll.ts
5808
- import { writeFileSync as writeFileSync4 } from "fs";
5809
- import { join as join8 } from "path";
5966
+ import { writeFileSync as writeFileSync6 } from "fs";
5967
+ import { join as join9 } from "path";
5810
5968
 
5811
5969
  // src/stalled.ts
5812
5970
  var STALLED_AFTER_CYCLES = 30;
@@ -6078,7 +6236,7 @@ async function decideCycle(o) {
6078
6236
  }
6079
6237
  return decision;
6080
6238
  }
6081
- var digestPath = (stateDir, route, role) => join8(stateDir, `digest-${route}-${role}.md`);
6239
+ var digestPath = (stateDir, route, role) => join9(stateDir, `digest-${route}-${role}.md`);
6082
6240
  function writeDigest(o, d, role, stateDir) {
6083
6241
  const me = o.route.resolved?.seats[role];
6084
6242
  if (!me) return false;
@@ -6140,7 +6298,7 @@ function writeDigest(o, d, role, stateDir) {
6140
6298
  };
6141
6299
  const text = role === "qa" ? qaDigest(input) : buildingDigest(input);
6142
6300
  const path = digestPath(stateDir, routeSlug(o.route.route), role);
6143
- writeFileSync4(path, text);
6301
+ writeFileSync6(path, text);
6144
6302
  o.emit.emit(`queue digest written for ${role} (${Buffer.byteLength(text)} bytes)`, {
6145
6303
  data: { role, bytes: Buffer.byteLength(text) }
6146
6304
  });
@@ -6448,9 +6606,9 @@ var alreadyReported = (ticket, g, comments) => comments.some((c) => c.ticket_id
6448
6606
 
6449
6607
  // src/agent.ts
6450
6608
  import { spawn as spawn2 } from "child_process";
6451
- 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";
6452
6610
  import { finished } from "stream/promises";
6453
- import { join as join9, dirname as dirname6 } from "path";
6611
+ import { join as join10, dirname as dirname6 } from "path";
6454
6612
  import { createHash as createHash2 } from "crypto";
6455
6613
 
6456
6614
  // src/environment.ts
@@ -6848,18 +7006,18 @@ var AgentError = class extends Error {
6848
7006
  var AgentSpawnError = class extends AgentError {
6849
7007
  };
6850
7008
  function planAgentRun(o) {
6851
- const briefPath = join9(o.route.promptsDir, "personas", `lane-${o.role}.md`);
6852
- if (!existsSync8(briefPath)) {
7009
+ const briefPath = join10(o.route.promptsDir, "personas", `lane-${o.role}.md`);
7010
+ if (!existsSync9(briefPath)) {
6853
7011
  throw new AgentError(`no brief at ${briefPath} \u2014 refusing to run an unscoped session`);
6854
7012
  }
6855
- const commonPath = join9(o.route.promptsDir, "personas", "common.md");
6856
- 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}`);
6857
7015
  const now = o.now ?? (() => Date.now());
6858
7016
  const routeLabel = routeSlug(o.route.route);
6859
- const digestPath2 = join9(o.stateDir, `digest-${routeLabel}-${o.role}.md`);
7017
+ const digestPath2 = join10(o.stateDir, `digest-${routeLabel}-${o.role}.md`);
6860
7018
  let digest;
6861
7019
  let ageSeconds;
6862
- if (existsSync8(digestPath2)) {
7020
+ if (existsSync9(digestPath2)) {
6863
7021
  ageSeconds = Math.floor((now() - statSync5(digestPath2).mtimeMs) / 1e3);
6864
7022
  if (ageSeconds <= DIGEST_MAX_AGE_SECONDS) digest = readFileSync8(digestPath2, "utf8");
6865
7023
  }
@@ -6875,7 +7033,7 @@ function planAgentRun(o) {
6875
7033
  digest
6876
7034
  });
6877
7035
  const promptSha = createHash2("sha256").update(`${effectiveCommon}${effectiveBrief}`).digest("hex");
6878
- const streamsDir = join9(o.stateDir, "streams");
7036
+ const streamsDir = join10(o.stateDir, "streams");
6879
7037
  const base = `${routeLabel}-${o.role}-${o.cycle}`;
6880
7038
  const seat = resolveSeat(o.ship, o.route, o.role, o.agentPersona ?? (o.agentModel ? { model: o.agentModel } : {}), o.repo);
6881
7039
  const { toolchain, model } = seat;
@@ -6931,8 +7089,8 @@ function planAgentRun(o) {
6931
7089
  digestAttached: digest !== void 0,
6932
7090
  digestAgeSeconds: ageSeconds,
6933
7091
  model,
6934
- streamPath: join9(streamsDir, `${base}.jsonl`),
6935
- eventsPath: join9(streamsDir, `${base}.events.jsonl`),
7092
+ streamPath: join10(streamsDir, `${base}.jsonl`),
7093
+ eventsPath: join10(streamsDir, `${base}.events.jsonl`),
6936
7094
  ticket: o.ticket,
6937
7095
  agentLog,
6938
7096
  promptVersion: o.route.resolved?.agentPersonas?.[o.role]?.historyId,
@@ -7002,8 +7160,8 @@ function spawnAgent(plan, emit) {
7002
7160
  return;
7003
7161
  }
7004
7162
  try {
7005
- const extracted = adapter.extractResult(parsed);
7006
- 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)) };
7007
7165
  consume(parsed, mapper.map(parsed));
7008
7166
  } catch {
7009
7167
  }
@@ -7079,7 +7237,7 @@ function spawnAgent(plan, emit) {
7079
7237
  provider: plan.provider ?? LEGACY_TOOLCHAIN_PROVIDER
7080
7238
  };
7081
7239
  try {
7082
- writeFileSync5(`${plan.streamPath}.meta.json`, JSON.stringify({
7240
+ writeFileSync7(`${plan.streamPath}.meta.json`, JSON.stringify({
7083
7241
  route: plan.route,
7084
7242
  role: plan.role,
7085
7243
  ticket: plan.ticket ?? null,
@@ -7138,13 +7296,13 @@ function spawnAgent(plan, emit) {
7138
7296
  }
7139
7297
 
7140
7298
  // src/run-worktree.ts
7141
- import { existsSync as existsSync10 } from "fs";
7142
- 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";
7143
7301
 
7144
7302
  // src/ports.ts
7145
7303
  import { execFileSync as execFileSync2 } from "child_process";
7146
- import { existsSync as existsSync9 } from "fs";
7147
- import { join as join10 } from "path";
7304
+ import { existsSync as existsSync10 } from "fs";
7305
+ import { join as join11 } from "path";
7148
7306
  var run = (cmd, args) => {
7149
7307
  try {
7150
7308
  return execFileSync2(cmd, args, { encoding: "utf8", stdio: ["ignore", "pipe", "ignore"] });
@@ -7189,7 +7347,7 @@ function ticketForPort(port, s = DEFAULT_PORTS) {
7189
7347
  }
7190
7348
  function worktreeExists(parent, prefix, n) {
7191
7349
  for (const name of [`${prefix}${n}`, `${prefix}${String(n).padStart(3, "0")}`]) {
7192
- if (existsSync9(join10(parent, name, ".git"))) return true;
7350
+ if (existsSync10(join11(parent, name, ".git"))) return true;
7193
7351
  }
7194
7352
  return false;
7195
7353
  }
@@ -7263,7 +7421,7 @@ function planRunWorktree(dir, cfg, t, role) {
7263
7421
  const existing = worktreeForNumber(dir, num);
7264
7422
  const located = locateBranchForTicket(dir, cfg, t, role);
7265
7423
  const baseRef = hasRemote ? `${remote}/${base}` : base;
7266
- if (existing && !existing.prunable && existsSync10(existing.path)) {
7424
+ if (existing && !existing.prunable && existsSync11(existing.path)) {
7267
7425
  const branch = existing.branch;
7268
7426
  if (status(existing.path).length > 0) return { action: "leave-dirty", path: existing.path, branch };
7269
7427
  if (!branch) return { action: "leave-unpushed", path: existing.path, branch: "(detached HEAD)" };
@@ -7277,7 +7435,7 @@ function planRunWorktree(dir, cfg, t, role) {
7277
7435
  if (countCommits(existing.path, `${baseRef}..HEAD`) > 0) return { action: "leave-unpushed", path: existing.path, branch };
7278
7436
  return { action: "recut", path: existing.path, branch, base: baseRef };
7279
7437
  }
7280
- const path = join11(dirname7(dir), worktreeForTicket(dir, cfg, t, role));
7438
+ const path = join12(dirname7(dir), worktreeForTicket(dir, cfg, t, role));
7281
7439
  if (located?.where === "remote" || located && hasRemote && remoteBranchExists(dir, remote, located.branch)) {
7282
7440
  return { action: "cut-remote", path, branch: located.branch };
7283
7441
  }
@@ -7324,7 +7482,7 @@ function worktreeHoldsUnpushed(path, remote) {
7324
7482
  }
7325
7483
  async function finishRunWorktree(dir, cfg, branch) {
7326
7484
  const wt = worktrees(dir).find((w) => w.branch === branch && !w.prunable && w.path !== dir);
7327
- if (!wt || !existsSync10(wt.path)) return { kind: "none" };
7485
+ if (!wt || !existsSync11(wt.path)) return { kind: "none" };
7328
7486
  if (cfg.hooks.handoff) return { kind: "kept-handoff", path: wt.path };
7329
7487
  if (status(wt.path).length > 0) return { kind: "kept-dirty", path: wt.path };
7330
7488
  if (remoteConfigured(dir, cfg.branch.remote) && worktreeHoldsUnpushed(wt.path, cfg.branch.remote)) {
@@ -7336,7 +7494,7 @@ async function finishRunWorktree(dir, cfg, branch) {
7336
7494
  if (pids.length) await killGently(pids);
7337
7495
  }
7338
7496
  gitOk(dir, ["worktree", "remove", wt.path]);
7339
- 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 };
7340
7498
  }
7341
7499
 
7342
7500
  // src/handoff-guard.ts
@@ -7485,13 +7643,13 @@ async function retireRunWorktree(tracker, dir, cfg, t, role, seatId, shipName2,
7485
7643
  }
7486
7644
 
7487
7645
  // src/connect-wizard.ts
7488
- import { delimiter, dirname as dirname8, join as join12 } from "path";
7489
- 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";
7490
7648
  import { stringify } from "yaml";
7491
7649
  var HOST_PASSENGERS_DOCS_URL = "https://crew.tablation.dev/host-passengers";
7492
7650
  function shouldRunWizard(opts) {
7493
7651
  if (!opts.isTTY) return false;
7494
- const exists = opts.configExists ?? existsSync11;
7652
+ const exists = opts.configExists ?? existsSync12;
7495
7653
  return !configSearchPath(opts.crewHome).some((p) => exists(p));
7496
7654
  }
7497
7655
  function firstRunConfigPath() {
@@ -7508,7 +7666,7 @@ function defaultIsExecutable(p) {
7508
7666
  function findOnPath2(name, pathEnv, isExecutable = defaultIsExecutable) {
7509
7667
  for (const dir of pathEnv.split(delimiter)) {
7510
7668
  if (!dir) continue;
7511
- const candidate = join12(dir, name);
7669
+ const candidate = join13(dir, name);
7512
7670
  if (isExecutable(candidate)) return candidate;
7513
7671
  }
7514
7672
  return void 0;
@@ -7571,7 +7729,7 @@ ${routeBlock}`;
7571
7729
  function writeNewConfig(path, text) {
7572
7730
  mkdirSync7(dirname8(path), { recursive: true });
7573
7731
  try {
7574
- writeFileSync6(path, text, { mode: 384, flag: "wx" });
7732
+ writeFileSync8(path, text, { mode: 384, flag: "wx" });
7575
7733
  return true;
7576
7734
  } catch (e) {
7577
7735
  if (e.code === "EEXIST") return false;
@@ -7603,8 +7761,8 @@ Wrote ${o.configPath}.
7603
7761
  }
7604
7762
 
7605
7763
  // src/release-run.ts
7606
- import { readFileSync as readFileSync9, writeFileSync as writeFileSync7, existsSync as existsSync12, copyFileSync, mkdirSync as mkdirSync8, mkdtempSync, rmSync as rmSync4 } from "fs";
7607
- 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";
7608
7766
  import { tmpdir as tmpdir2 } from "os";
7609
7767
 
7610
7768
  // src/release.ts
@@ -8262,9 +8420,9 @@ async function bump(o, size) {
8262
8420
  }
8263
8421
  return printed;
8264
8422
  }
8265
- 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));
8266
8424
  const first = files[0];
8267
- if (!existsSync12(first)) {
8425
+ if (!existsSync13(first)) {
8268
8426
  o.emit.error(`no ${o.repo.release.versionFiles[0]} to read a version from`);
8269
8427
  return null;
8270
8428
  }
@@ -8287,21 +8445,21 @@ async function bump(o, size) {
8287
8445
  o.emit.error(`no version field in ${f}`);
8288
8446
  return null;
8289
8447
  }
8290
- writeFileSync7(f, out);
8448
+ writeFileSync9(f, out);
8291
8449
  }
8292
8450
  return version;
8293
8451
  }
8294
8452
  function writeChangelog(o, version, entries) {
8295
8453
  const file = o.repo.release.changelog;
8296
8454
  if (!file || entries.length === 0) return;
8297
- const path = join13(o.cwd, file);
8455
+ const path = join14(o.cwd, file);
8298
8456
  const section = renderChangelogSection(version, (/* @__PURE__ */ new Date()).toISOString().slice(0, 10), entries);
8299
8457
  if (o.dryRun) {
8300
8458
  o.emit.emit(`would prepend ${entries.length} entr(ies) to ${file}`);
8301
8459
  return;
8302
8460
  }
8303
- const existing = existsSync12(path) ? readFileSync9(path, "utf8") : "# Changelog\n";
8304
- writeFileSync7(path, insertChangelogSection(existing, section));
8461
+ const existing = existsSync13(path) ? readFileSync9(path, "utf8") : "# Changelog\n";
8462
+ writeFileSync9(path, insertChangelogSection(existing, section));
8305
8463
  }
8306
8464
  async function confirm(o, expected) {
8307
8465
  if (!o.repo.hooks.released) return void 0;
@@ -8382,11 +8540,11 @@ async function confirmLastRelease(o, decision) {
8382
8540
  function openReleaseCheckout(o) {
8383
8541
  const { remote, base } = o.repo.branch;
8384
8542
  const branch = `crew/release-${Date.now()}`;
8385
- const root = mkdtempSync(join13(tmpdir2(), "crew-release-"));
8386
- const dir = join13(root, "checkout");
8543
+ const root = mkdtempSync(join14(tmpdir2(), "crew-release-"));
8544
+ const dir = join14(root, "checkout");
8387
8545
  const close = () => {
8388
8546
  gitOk(o.cwd, ["worktree", "remove", "--force", dir]);
8389
- rmSync4(root, { recursive: true, force: true });
8547
+ rmSync6(root, { recursive: true, force: true });
8390
8548
  gitOk(o.cwd, ["worktree", "prune"]);
8391
8549
  gitOk(o.cwd, ["branch", "-D", branch]);
8392
8550
  };
@@ -8397,10 +8555,10 @@ function openReleaseCheckout(o) {
8397
8555
  throw e;
8398
8556
  }
8399
8557
  for (const f of o.repo.worktrees.copy) {
8400
- const src = join13(o.cwd, f);
8401
- if (!existsSync12(src)) continue;
8402
- mkdirSync8(dirname9(join13(dir, f)), { recursive: true });
8403
- 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));
8404
8562
  }
8405
8563
  return { dir, branch, close };
8406
8564
  }
@@ -8869,7 +9027,7 @@ function missingReleaserWarning(repo, release, ships, now = Date.now()) {
8869
9027
  }
8870
9028
 
8871
9029
  // src/watch.ts
8872
- 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";
8873
9031
  var E = "\x1B[";
8874
9032
  var dim = (s) => `${E}2m${s}${E}0m`;
8875
9033
  var bold = (s) => `${E}1m${s}${E}0m`;
@@ -8970,7 +9128,7 @@ function renderHeaders(states, now = Date.now()) {
8970
9128
  return states.map((s) => renderHeader(s, now)).join("");
8971
9129
  }
8972
9130
  function readFrom(file, from) {
8973
- if (!existsSync13(file)) return { events: [], offset: from };
9131
+ if (!existsSync14(file)) return { events: [], offset: from };
8974
9132
  const size = statSync6(file).size;
8975
9133
  if (size < from) return readFrom(file, 0);
8976
9134
  if (size === from) return { events: [], offset: from };
@@ -9210,8 +9368,8 @@ function nextRoles(decision, ran) {
9210
9368
  }
9211
9369
 
9212
9370
  // src/agents.ts
9213
- import { readFileSync as readFileSync10, existsSync as existsSync14 } from "fs";
9214
- import { join as join14 } from "path";
9371
+ import { readFileSync as readFileSync10, existsSync as existsSync15 } from "fs";
9372
+ import { join as join15 } from "path";
9215
9373
  import { TablationClient as TablationClient2, StaleWriteError as StaleWriteError6, TablationApiError } from "@tablation/client";
9216
9374
  var PERSONA_NAME = {
9217
9375
  dev: "Developer",
@@ -9225,11 +9383,11 @@ var CREW_AGENT_COLUMN = "agent_id";
9225
9383
  var AgentsSyncError = class extends Error {
9226
9384
  };
9227
9385
  function personaDefaultPrompt(promptsDir, role) {
9228
- const briefPath = join14(promptsDir, "personas", `lane-${role}.md`);
9229
- 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}`);
9230
9388
  if (role === "pair") return readFileSync10(briefPath, "utf8");
9231
- const commonPath = join14(promptsDir, "personas", "common.md");
9232
- 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}`);
9233
9391
  return `${readFileSync10(commonPath, "utf8")}${readFileSync10(briefPath, "utf8")}`;
9234
9392
  }
9235
9393
  async function resolveAgentId(route, role, opts) {
@@ -9907,8 +10065,8 @@ import {
9907
10065
  } from "@tablation/client";
9908
10066
 
9909
10067
  // src/skills.ts
9910
- import { readFileSync as readFileSync11, readdirSync as readdirSync2, existsSync as existsSync15 } from "fs";
9911
- 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";
9912
10070
  import { parse as parseYaml } from "yaml";
9913
10071
  import { TablationClient as TablationClient3, StaleWriteError as StaleWriteError7, TablationApiError as TablationApiError2 } from "@tablation/client";
9914
10072
  var SkillSyncError = class extends Error {
@@ -9927,9 +10085,9 @@ function parseSkillFile(path) {
9927
10085
  return { name: meta.name, description: meta.description, prompt: body, file: path };
9928
10086
  }
9929
10087
  function listLocalSkills(promptsDir) {
9930
- const dir = join15(promptsDir, "skills");
9931
- if (!existsSync15(dir)) return [];
9932
- 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)));
9933
10091
  }
9934
10092
  async function syncSkills(route, opts) {
9935
10093
  if (!route.resolved) {
@@ -10108,7 +10266,7 @@ async function showLogEntry(route, entryId, opts = {}) {
10108
10266
  }
10109
10267
 
10110
10268
  // src/repos-cmd.ts
10111
- import { homedir as homedir5 } from "os";
10269
+ import { homedir as homedir6 } from "os";
10112
10270
  import { basename as basename7, resolve as resolve3 } from "path";
10113
10271
  import { isMap, isSeq, parseDocument, stringify as stringify2 } from "yaml";
10114
10272
  var ReposError = class extends Error {
@@ -10155,7 +10313,7 @@ function parseAddArgs(args, routes, isDir) {
10155
10313
  return { route, path: "." };
10156
10314
  }
10157
10315
  function expandDir(p, base) {
10158
- const home = p === "~" ? homedir5() : p.startsWith("~/") ? `${homedir5()}/${p.slice(2)}` : p;
10316
+ const home = p === "~" ? homedir6() : p.startsWith("~/") ? `${homedir6()}/${p.slice(2)}` : p;
10159
10317
  return resolve3(base, home);
10160
10318
  }
10161
10319
  function routeMapOf(doc, route) {
@@ -10290,7 +10448,7 @@ function schedulerInstalled(plan, exists, crontab) {
10290
10448
  }
10291
10449
 
10292
10450
  // src/worktree-sweep.ts
10293
- import { existsSync as existsSync16 } from "fs";
10451
+ import { existsSync as existsSync17 } from "fs";
10294
10452
  import { basename as basename8 } from "path";
10295
10453
  function planWorktreeSweep(target, tickets, contract) {
10296
10454
  const terminal = new Set(contract.statuses.resolved.filter((s) => s !== contract.statuses.verified));
@@ -10318,7 +10476,7 @@ async function applyWorktreeSweep(cwd, actions, dryRun, log, remote = "origin")
10318
10476
  let removed = 0;
10319
10477
  let keptBranches = 0;
10320
10478
  for (const a of actions) {
10321
- if (!dryRun && existsSync16(a.path)) {
10479
+ if (!dryRun && existsSync17(a.path)) {
10322
10480
  let dirty = false;
10323
10481
  try {
10324
10482
  dirty = status(a.path).length > 0;
@@ -10332,7 +10490,7 @@ async function applyWorktreeSweep(cwd, actions, dryRun, log, remote = "origin")
10332
10490
  continue;
10333
10491
  }
10334
10492
  }
10335
- 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)) {
10336
10494
  log.warn(
10337
10495
  `not removing worktree ${a.path} for ${a.ticket.issue_id} (${a.ticket.status}): ${a.branch} has commits that are not on ${remote}`,
10338
10496
  { ticket: a.ticket.issue_id, step: "worktree" }
@@ -10413,14 +10571,14 @@ function applyRemoteBranchCleanup(cwd, remote, branches2, dryRun, log) {
10413
10571
  }
10414
10572
 
10415
10573
  // src/stream-sweep.ts
10416
- import { existsSync as existsSync17, readdirSync as readdirSync3, rmSync as rmSync5, statSync as statSync7 } from "fs";
10417
- 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";
10418
10576
  var DAY_MS2 = 24 * 60 * 60 * 1e3;
10419
10577
  function planStreamSweep(stateDir, retentionDays, now = Date.now()) {
10420
- const dir = join16(stateDir, "streams");
10421
- if (!existsSync17(dir)) return [];
10578
+ const dir = join17(stateDir, "streams");
10579
+ if (!existsSync18(dir)) return [];
10422
10580
  const cutoff = now - retentionDays * DAY_MS2;
10423
- return readdirSync3(dir).map((name) => join16(dir, name)).filter((path) => {
10581
+ return readdirSync3(dir).map((name) => join17(dir, name)).filter((path) => {
10424
10582
  try {
10425
10583
  return statSync7(path).mtimeMs < cutoff;
10426
10584
  } catch {
@@ -10436,7 +10594,7 @@ function applyStreamSweep(paths, dryRun, log) {
10436
10594
  continue;
10437
10595
  }
10438
10596
  try {
10439
- rmSync5(path, { force: true });
10597
+ rmSync7(path, { force: true });
10440
10598
  removed++;
10441
10599
  log.emit(`removed stream artifact ${path}`, { step: "worktree" });
10442
10600
  } catch (e) {
@@ -10446,9 +10604,9 @@ function applyStreamSweep(paths, dryRun, log) {
10446
10604
  return removed;
10447
10605
  }
10448
10606
  function planEventsSweep(stateDir, retentionDays, now = Date.now()) {
10449
- if (!existsSync17(stateDir)) return [];
10607
+ if (!existsSync18(stateDir)) return [];
10450
10608
  const cutoff = now - retentionDays * DAY_MS2;
10451
- 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) => {
10452
10610
  try {
10453
10611
  return statSync7(path).mtimeMs < cutoff;
10454
10612
  } catch {
@@ -10458,23 +10616,23 @@ function planEventsSweep(stateDir, retentionDays, now = Date.now()) {
10458
10616
  }
10459
10617
 
10460
10618
  // src/cli.ts
10461
- 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 readFileSync16, existsSync as existsSync23, mkdirSync as mkdirSync13, writeFileSync as writeFileSync13, unlinkSync as unlinkSync5, copyFileSync as copyFileSync2, renameSync as renameSync2, statSync as statSync9 } from "fs";
10462
10620
  import { createInterface } from "readline/promises";
10463
10621
  import { dirname as dirOf, resolve as resolvePath } from "path";
10464
10622
 
10465
10623
  // src/ssh-keys.ts
10466
10624
  import { execFileSync as execFileSync3 } from "child_process";
10467
- import { existsSync as existsSync18, mkdirSync as mkdirSync9, readFileSync as readFileSync12, chmodSync } from "fs";
10468
- 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";
10469
10627
  var SshKeyError = class extends Error {
10470
10628
  };
10471
10629
  function shipSshKeyPathFor(stateDir) {
10472
- return join17(stateDir, "ssh", "id_ed25519");
10630
+ return join18(stateDir, "ssh", "id_ed25519");
10473
10631
  }
10474
10632
  function ensureShipSshKeypair(stateDir) {
10475
10633
  const privateKeyPath = shipSshKeyPathFor(stateDir);
10476
10634
  const publicKeyPath = `${privateKeyPath}.pub`;
10477
- if (!existsSync18(privateKeyPath)) {
10635
+ if (!existsSync19(privateKeyPath)) {
10478
10636
  mkdirSync9(dirname11(privateKeyPath), { recursive: true });
10479
10637
  try {
10480
10638
  execFileSync3("ssh-keygen", [
@@ -10495,7 +10653,7 @@ function ensureShipSshKeypair(stateDir) {
10495
10653
  }
10496
10654
  chmodSync(privateKeyPath, 384);
10497
10655
  }
10498
- if (!existsSync18(publicKeyPath)) {
10656
+ if (!existsSync19(publicKeyPath)) {
10499
10657
  throw new SshKeyError(`${privateKeyPath} exists but ${publicKeyPath} does not \u2014 regenerate or restore it`);
10500
10658
  }
10501
10659
  const publicKey = readFileSync12(publicKeyPath, "utf8").trim();
@@ -10712,9 +10870,9 @@ function syncAllPassengerCheckouts(cfg) {
10712
10870
 
10713
10871
  // src/tunnel.ts
10714
10872
  import { spawn as spawn3 } from "child_process";
10715
- 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";
10716
10874
  import { randomInt } from "crypto";
10717
- import { join as join18 } from "path";
10875
+ import { join as join19 } from "path";
10718
10876
  var DEFAULT_SPAWN_OPTIONS = { stdio: "ignore", detached: true };
10719
10877
  var defaultSpawn = (cmd, args) => spawn3(cmd, args, DEFAULT_SPAWN_OPTIONS);
10720
10878
  var SLUG_ALPHABET = "abcdefghijklmnopqrstuvwxyz0123456789";
@@ -10781,11 +10939,11 @@ function startTunnel(opts) {
10781
10939
  };
10782
10940
  }
10783
10941
  function tunnelStatePath(stateDir, workspaceId) {
10784
- return join18(stateDir, "passengers", `${workspaceId}.json`);
10942
+ return join19(stateDir, "passengers", `${workspaceId}.json`);
10785
10943
  }
10786
10944
  function readPersistedTunnel(stateDir, workspaceId) {
10787
10945
  const path = tunnelStatePath(stateDir, workspaceId);
10788
- if (!existsSync19(path)) return void 0;
10946
+ if (!existsSync20(path)) return void 0;
10789
10947
  try {
10790
10948
  return JSON.parse(readFileSync13(path, "utf8"));
10791
10949
  } catch {
@@ -10794,8 +10952,8 @@ function readPersistedTunnel(stateDir, workspaceId) {
10794
10952
  }
10795
10953
  function writePersistedTunnel(stateDir, state) {
10796
10954
  const path = tunnelStatePath(stateDir, state.workspaceId);
10797
- mkdirSync10(join18(stateDir, "passengers"), { recursive: true });
10798
- writeFileSync8(path, `${JSON.stringify(state, null, 2)}
10955
+ mkdirSync10(join19(stateDir, "passengers"), { recursive: true });
10956
+ writeFileSync10(path, `${JSON.stringify(state, null, 2)}
10799
10957
  `);
10800
10958
  }
10801
10959
  function updatePersistedTunnel(stateDir, workspaceId, patch2) {
@@ -10892,8 +11050,8 @@ function syncPassengerTunnels(plans, relayHost, relayPort, privateKeyPath, state
10892
11050
  }
10893
11051
  }
10894
11052
  function readAllPersistedWorkspaceIds(stateDir) {
10895
- const dir = join18(stateDir, "passengers");
10896
- if (!existsSync19(dir)) return [];
11053
+ const dir = join19(stateDir, "passengers");
11054
+ if (!existsSync20(dir)) return [];
10897
11055
  return readdirSync4(dir).filter((f) => f.endsWith(".json") && !f.endsWith("-sync.json")).map((f) => f.slice(0, -".json".length));
10898
11056
  }
10899
11057
 
@@ -11014,8 +11172,8 @@ async function unregisterEndpoint(workspaceId, info, d) {
11014
11172
  import { createServer as createHttpServer } from "http";
11015
11173
  import { spawn as spawn4 } from "child_process";
11016
11174
  import { randomBytes } from "crypto";
11017
- import { existsSync as existsSync20, mkdirSync as mkdirSync11, readFileSync as readFileSync14, writeFileSync as writeFileSync9, unlinkSync as unlinkSync4, readdirSync as readdirSync5 } from "fs";
11018
- 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";
11019
11177
 
11020
11178
  // src/secret-compare.ts
11021
11179
  import { timingSafeEqual } from "crypto";
@@ -11104,11 +11262,11 @@ var defaultSpawn2 = (cmd, args, env) => {
11104
11262
  return child;
11105
11263
  };
11106
11264
  function syncDaemonStatePath(stateDir, workspaceId) {
11107
- return join19(stateDir, "passengers", `${workspaceId}-sync.json`);
11265
+ return join20(stateDir, "passengers", `${workspaceId}-sync.json`);
11108
11266
  }
11109
11267
  function readPersistedSyncDaemon(stateDir, workspaceId) {
11110
11268
  const path = syncDaemonStatePath(stateDir, workspaceId);
11111
- if (!existsSync20(path)) return void 0;
11269
+ if (!existsSync21(path)) return void 0;
11112
11270
  try {
11113
11271
  return JSON.parse(readFileSync14(path, "utf8"));
11114
11272
  } catch {
@@ -11117,8 +11275,8 @@ function readPersistedSyncDaemon(stateDir, workspaceId) {
11117
11275
  }
11118
11276
  function writePersistedSyncDaemon(stateDir, state) {
11119
11277
  const path = syncDaemonStatePath(stateDir, state.workspaceId);
11120
- mkdirSync11(join19(stateDir, "passengers"), { recursive: true });
11121
- writeFileSync9(path, `${JSON.stringify(state, null, 2)}
11278
+ mkdirSync11(join20(stateDir, "passengers"), { recursive: true });
11279
+ writeFileSync11(path, `${JSON.stringify(state, null, 2)}
11122
11280
  `);
11123
11281
  }
11124
11282
  function removePersistedSyncDaemon(stateDir, workspaceId) {
@@ -11138,8 +11296,8 @@ function waitForExit(pid, alive, timeoutMs = 5e3) {
11138
11296
  }
11139
11297
  }
11140
11298
  function readAllPersistedSyncWorkspaceIds(stateDir) {
11141
- const dir = join19(stateDir, "passengers");
11142
- if (!existsSync20(dir)) return [];
11299
+ const dir = join20(stateDir, "passengers");
11300
+ if (!existsSync21(dir)) return [];
11143
11301
  const suffix = "-sync.json";
11144
11302
  return readdirSync5(dir).filter((f) => f.endsWith(suffix)).map((f) => f.slice(0, -suffix.length));
11145
11303
  }
@@ -11196,8 +11354,8 @@ function syncPassengerSyncDaemons(plans, cfg, stateDir, deps = {}) {
11196
11354
  }
11197
11355
 
11198
11356
  // src/daemon.ts
11199
- import { readdirSync as readdirSync6, statSync as statSync8 } from "fs";
11200
- import { join as join20 } from "path";
11357
+ import { existsSync as existsSync22, mkdirSync as mkdirSync12, readFileSync as readFileSync15, readdirSync as readdirSync6, rmSync as rmSync8, statSync as statSync8, writeFileSync as writeFileSync12 } from "fs";
11358
+ import { join as join21 } from "path";
11201
11359
  var DEFAULT_FLOOR_MS = 5e3;
11202
11360
  var DEFAULT_CEILING_MS = INTERVAL_SECONDS * 1e3;
11203
11361
  var defaultSleep = (ms2) => new Promise((resolve5) => setTimeout(resolve5, ms2));
@@ -11236,16 +11394,45 @@ function latestMtime(path) {
11236
11394
  if (!stat.isDirectory()) return 0;
11237
11395
  let latest = stat.mtimeMs;
11238
11396
  for (const entry of readdirSync6(path, { withFileTypes: true })) {
11239
- latest = Math.max(latest, latestMtime(join20(path, entry.name)));
11397
+ latest = Math.max(latest, latestMtime(join21(path, entry.name)));
11240
11398
  }
11241
11399
  return latest;
11242
11400
  }
11243
11401
  function computeTreeSignature(paths) {
11244
11402
  return paths.map((p) => String(latestMtime(p))).join(":");
11245
11403
  }
11246
- function makeStaleChecker(paths) {
11404
+ function makeStaleChecker(paths, opts = {}) {
11247
11405
  const initial = computeTreeSignature(paths);
11248
- return () => computeTreeSignature(paths) !== initial;
11406
+ return () => computeTreeSignature(paths) !== initial || opts.reloadSentinel !== void 0 && existsSync22(opts.reloadSentinel);
11407
+ }
11408
+ function reloadSentinelFor(stateDir) {
11409
+ return join21(stateDir, "reload-requested");
11410
+ }
11411
+ function daemonStampFor(stateDir) {
11412
+ return join21(stateDir, "daemon.started");
11413
+ }
11414
+ function noteDaemonStarted(stateDir, now = Date.now()) {
11415
+ mkdirSync12(stateDir, { recursive: true });
11416
+ rmSync8(reloadSentinelFor(stateDir), { force: true });
11417
+ writeFileSync12(daemonStampFor(stateDir), `${JSON.stringify({ pid: process.pid, startedAt: now })}
11418
+ `);
11419
+ }
11420
+ function readDaemonStarted(stateDir) {
11421
+ try {
11422
+ const v = JSON.parse(readFileSync15(daemonStampFor(stateDir), "utf8")).startedAt;
11423
+ return typeof v === "number" ? v : null;
11424
+ } catch {
11425
+ return null;
11426
+ }
11427
+ }
11428
+ function configChangedAfter(files, startedAt) {
11429
+ for (const f of files) {
11430
+ try {
11431
+ if (statSync8(f).mtimeMs > startedAt) return f;
11432
+ } catch {
11433
+ }
11434
+ }
11435
+ return null;
11249
11436
  }
11250
11437
  var SPAWN_PARK_AFTER = 3;
11251
11438
  var SpawnBreaker = class {
@@ -11646,6 +11833,7 @@ function usage() {
11646
11833
  crew watch [route] live view of what the crew is doing
11647
11834
  crew status [route] [--json] paused/running state; --json for a machine reader
11648
11835
  crew projects [--route R] [--json] the Projects rows each route can read (id, name, prefix, work mode)
11836
+ crew reload make the running daemon re-read crew.yaml (--now: restart it immediately)
11649
11837
  crew doctor [route] [--fix] preflight; --fix enables clean routes and offers crew install
11650
11838
  crew ports [route] which checkout owns which ports, and what is up
11651
11839
  crew reap [route] kill orphaned servers, drop worktrees for closed tickets
@@ -11700,7 +11888,12 @@ var value = (name) => {
11700
11888
  var positional = argv.filter((a, i) => !a.startsWith("--") && !argv[i - 1]?.startsWith("--"));
11701
11889
  var command = positional[0];
11702
11890
  if (!command) usage();
11703
- var STATE_WRITING_COMMANDS = /* @__PURE__ */ new Set(["connect", "disconnect", "install", "uninstall", "release", "merge", "deploy"]);
11891
+ function daemonConfigPaths(c) {
11892
+ const files = [c.configFile];
11893
+ for (const r of c.routes) if (r.apiKeyFile && !files.includes(r.apiKeyFile)) files.push(r.apiKeyFile);
11894
+ return files;
11895
+ }
11896
+ var STATE_WRITING_COMMANDS = /* @__PURE__ */ new Set(["connect", "disconnect", "install", "uninstall", "reload", "release", "merge", "deploy"]);
11704
11897
  var STATE_WRITING_SUBCOMMANDS = {
11705
11898
  agents: /* @__PURE__ */ new Set(["sync"]),
11706
11899
  skills: /* @__PURE__ */ new Set(["sync"])
@@ -11810,7 +12003,7 @@ if (command === "passenger-sync-daemon") {
11810
12003
  const releaseFleetWide = ["merge", "deploy", "release"].includes(command) && flag("fleet");
11811
12004
  const DAEMON_SUBCOMMANDS = /* @__PURE__ */ new Set(["start", "stop", "status", "restart"]);
11812
12005
  const named = command === "daemon" && DAEMON_SUBCOMMANDS.has(positional[1] ?? "") ? positional[2] : positional[1];
11813
- const fleetWide = command === "inbox" || command === "connect" || command === "disconnect" || command === "agents" || command === "skills" || command === "logbook" || command === "repos" || command === "projects" || releaseFleetWide || command === "pause" || command === "resume" || FLEET_CAPABLE.has(command) && !named && cfg.routes.length > 1;
12006
+ const fleetWide = command === "inbox" || command === "connect" || command === "disconnect" || command === "agents" || command === "skills" || command === "logbook" || command === "repos" || command === "projects" || releaseFleetWide || command === "pause" || command === "resume" || command === "reload" || FLEET_CAPABLE.has(command) && !named && cfg.routes.length > 1;
11814
12007
  let route;
11815
12008
  try {
11816
12009
  route = fleetWide ? cfg.routes[0] : findRoute(cfg, named);
@@ -12016,14 +12209,14 @@ if (command === "passenger-sync-daemon") {
12016
12209
  }
12017
12210
  if (host === "linux" && hasSystemd) {
12018
12211
  const label = labelFor(CREW_HOME, "run");
12019
- const staleTimer = join21(homedir6(), ".config", "systemd", "user", `${label}.timer`);
12020
- if (existsSync21(staleTimer)) {
12212
+ const staleTimer = join22(homedir7(), ".config", "systemd", "user", `${label}.timer`);
12213
+ if (existsSync23(staleTimer)) {
12021
12214
  process.stdout.write(
12022
12215
  `removing stale run timer from before the persistent-service cutover: ${staleTimer}${dryRun ? " (dry run)" : ""}
12023
12216
  `
12024
12217
  );
12025
12218
  if (!dryRun) {
12026
- spawnSync4("systemctl", ["--user", "disable", "--now", `${label}.timer`], { cwd: CREW_HOME });
12219
+ spawnSync5("systemctl", ["--user", "disable", "--now", `${label}.timer`], { cwd: CREW_HOME });
12027
12220
  try {
12028
12221
  unlinkSync5(staleTimer);
12029
12222
  } catch {
@@ -12043,7 +12236,7 @@ if (command === "passenger-sync-daemon") {
12043
12236
  const tracker = new Tracker(c, cfg.ship);
12044
12237
  problems.push(...validateContract(tracker.contract).map((p) => `contract: ${p}`));
12045
12238
  for (const r of await resolvedRepos(c)) {
12046
- if (!existsSync21(r.dir)) problems.push(`repo ${r.name}: ${r.dir} is missing on disk`);
12239
+ if (!existsSync23(r.dir)) problems.push(`repo ${r.name}: ${r.dir} is missing on disk`);
12047
12240
  if (!satisfies(host, r.config.platform)) problems.push(`repo ${r.name}: requires ${r.config.platform}, this host is ${host}`);
12048
12241
  problems.push(...validateEffective(r.config).map((p) => `repo ${r.name}: ${p}`));
12049
12242
  }
@@ -12061,8 +12254,8 @@ if (command === "passenger-sync-daemon") {
12061
12254
  }
12062
12255
  async function doctorFix(o) {
12063
12256
  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))));
12064
- const unitExists = (p) => existsSync21(p);
12065
- const crontab = o.host === "linux" ? spawnSync4("crontab", ["-l"], { encoding: "utf8" }).stdout ?? "" : "";
12257
+ const unitExists = (p) => existsSync23(p);
12258
+ const crontab = o.host === "linux" ? spawnSync5("crontab", ["-l"], { encoding: "utf8" }).stdout ?? "" : "";
12066
12259
  const schedulerAbsent = o.host !== "windows" ? !schedulerInstalled(planInstall(cfg.ship, CREW_HOME, o.host, detectSystemd(), "run"), unitExists, crontab) : true;
12067
12260
  const plan = planFix({ routes, schedulerAbsent, host: o.host });
12068
12261
  for (const n of plan.notes) process.stdout.write(`fix: ${n}
@@ -12078,7 +12271,7 @@ if (command === "passenger-sync-daemon") {
12078
12271
  for (const a of plan.actions) {
12079
12272
  if (a.kind === "enable") {
12080
12273
  if (prompter && !await prompter.confirm(`Route ${a.route} passes every check \u2014 set enabled: true in ${cfg.configFile}?`, false)) continue;
12081
- writeFileSync10(cfg.configFile, planEnable(readFileSync15(cfg.configFile, "utf8"), a.route));
12274
+ writeFileSync13(cfg.configFile, planEnable(readFileSync16(cfg.configFile, "utf8"), a.route));
12082
12275
  enabledSound.add(a.route);
12083
12276
  changed2.push(`enabled route ${a.route} in ${cfg.configFile}`);
12084
12277
  } else {
@@ -12941,7 +13134,7 @@ ${changed2.map((c) => ` - ${c}
12941
13134
  }
12942
13135
  let failed = false;
12943
13136
  for (const cmd2 of commands) {
12944
- const res2 = spawnSync4(cmd2[0], cmd2.slice(1), { cwd: CREW_HOME, encoding: "utf8" });
13137
+ const res2 = spawnSync5(cmd2[0], cmd2.slice(1), { cwd: CREW_HOME, encoding: "utf8" });
12945
13138
  if (res2.stdout) process.stdout.write(res2.stdout);
12946
13139
  if (res2.stderr) process.stderr.write(res2.stderr);
12947
13140
  if (cmd2 === control.startCommand && (res2.error || res2.status !== 0)) {
@@ -12959,7 +13152,7 @@ ${changed2.map((c) => ` - ${c}
12959
13152
  `);
12960
13153
  break;
12961
13154
  }
12962
- const res = spawnSync4(cmd[0], cmd.slice(1), { cwd: CREW_HOME, encoding: "utf8" });
13155
+ const res = spawnSync5(cmd[0], cmd.slice(1), { cwd: CREW_HOME, encoding: "utf8" });
12963
13156
  if (res.stdout) process.stdout.write(res.stdout);
12964
13157
  if (res.stderr) process.stderr.write(res.stderr);
12965
13158
  if (daemonSub !== "status" && (res.error || res.status !== 0)) {
@@ -12982,7 +13175,10 @@ ${changed2.map((c) => ` - ${c}
12982
13175
  process.exit(1);
12983
13176
  }
12984
13177
  requireArmed("run the daemon loop");
12985
- const watchPaths = isCompiledBinary(import.meta.url) ? [process.execPath] : [join21(CREW_HOME, "src"), join21(CREW_HOME, "dist"), join21(CREW_HOME, "bin")];
13178
+ const watchPaths = isCompiledBinary(import.meta.url) ? [process.execPath] : [join22(CREW_HOME, "src"), join22(CREW_HOME, "dist"), join22(CREW_HOME, "bin")];
13179
+ const configPaths = daemonConfigPaths(cfg);
13180
+ watchPaths.push(...configPaths);
13181
+ noteDaemonStarted(cfg.ship.stateDir);
12986
13182
  const spawnBreaker = new SpawnBreaker();
12987
13183
  const stopReason = await runDaemonLoop({
12988
13184
  runPass: () => runOnePass({
@@ -13000,14 +13196,16 @@ ${changed2.map((c) => ` - ${c}
13000
13196
  `)
13001
13197
  })
13002
13198
  }),
13003
- checkStale: makeStaleChecker(watchPaths),
13199
+ checkStale: makeStaleChecker(watchPaths, { reloadSentinel: reloadSentinelFor(cfg.ship.stateDir) }),
13004
13200
  onPass: (result) => {
13005
13201
  if (result.started.length) emit.emit(`daemon pass started: ${result.started.join(", ")}`);
13006
13202
  for (const s of result.skipped) emit.emit(`daemon pass skipped ${s.role} \u2014 ${s.reason}`);
13007
13203
  }
13008
13204
  });
13009
13205
  if (stopReason === "stale") {
13010
- emit.emit("crew daemon: installed tree changed on disk \u2014 exiting cleanly so the OS-level restart relaunches with the update");
13206
+ emit.emit(
13207
+ "crew daemon: installed tree, crew.yaml or a reload request changed on disk \u2014 exiting cleanly so the OS-level restart relaunches with the update"
13208
+ );
13011
13209
  process.exit(0);
13012
13210
  }
13013
13211
  break;
@@ -13131,11 +13329,11 @@ ${changed2.map((c) => ` - ${c}
13131
13329
  `);
13132
13330
  if (!dryRun) {
13133
13331
  const resolvedPath = resolvedPathFor(cfg.ship.stateDir, target.route);
13134
- const raw = existsSync21(resolvedPath) ? JSON.parse(readFileSync15(resolvedPath, "utf8")) : {};
13332
+ const raw = existsSync23(resolvedPath) ? JSON.parse(readFileSync16(resolvedPath, "utf8")) : {};
13135
13333
  raw.agentPersonas = personaResult.agentPersonas;
13136
13334
  raw.agentSkills = skillResult.agentSkills;
13137
- mkdirSync12(dirOf(resolvedPath), { recursive: true });
13138
- writeFileSync10(resolvedPath, `${JSON.stringify(raw, null, 2)}
13335
+ mkdirSync13(dirOf(resolvedPath), { recursive: true });
13336
+ writeFileSync13(resolvedPath, `${JSON.stringify(raw, null, 2)}
13139
13337
  `);
13140
13338
  }
13141
13339
  if (personaResult.outcomes.some((o) => o.action === "diverged") || skillResult.outcomes.some((o) => o.action === "diverged")) {
@@ -13180,10 +13378,10 @@ ${changed2.map((c) => ` - ${c}
13180
13378
  `);
13181
13379
  if (!dryRun) {
13182
13380
  const resolvedPath = resolvedPathFor(cfg.ship.stateDir, target.route);
13183
- const raw = existsSync21(resolvedPath) ? JSON.parse(readFileSync15(resolvedPath, "utf8")) : {};
13381
+ const raw = existsSync23(resolvedPath) ? JSON.parse(readFileSync16(resolvedPath, "utf8")) : {};
13184
13382
  raw.agentSkills = result.agentSkills;
13185
- mkdirSync12(dirOf(resolvedPath), { recursive: true });
13186
- writeFileSync10(resolvedPath, `${JSON.stringify(raw, null, 2)}
13383
+ mkdirSync13(dirOf(resolvedPath), { recursive: true });
13384
+ writeFileSync13(resolvedPath, `${JSON.stringify(raw, null, 2)}
13187
13385
  `);
13188
13386
  }
13189
13387
  if (result.outcomes.some((o) => o.action === "diverged")) process.exitCode = 1;
@@ -13196,14 +13394,14 @@ ${changed2.map((c) => ` - ${c}
13196
13394
  process.exit(2);
13197
13395
  };
13198
13396
  if (sub !== "add" && sub !== "list") usage2();
13199
- const configText = readFileSync15(cfg.configFile, "utf8");
13397
+ const configText = readFileSync16(cfg.configFile, "utf8");
13200
13398
  let routeName = positional[2];
13201
13399
  let addPath = ".";
13202
13400
  let addName;
13203
13401
  if (sub === "list" && !routeName) usage2();
13204
13402
  if (sub === "add") {
13205
13403
  try {
13206
- const a = parseAddArgs(positional.slice(2), routeNamesOf(configText), (p) => existsSync21(resolvePath(p.replace(/^~(?=\/|$)/, homedir6()))));
13404
+ const a = parseAddArgs(positional.slice(2), routeNamesOf(configText), (p) => existsSync23(resolvePath(p.replace(/^~(?=\/|$)/, homedir7()))));
13207
13405
  routeName = a.route;
13208
13406
  addPath = a.path;
13209
13407
  addName = a.name;
@@ -13218,9 +13416,9 @@ ${changed2.map((c) => ` - ${c}
13218
13416
  }
13219
13417
  const resolvedFile = resolvedPathFor(cfg.ship.stateDir, routeName);
13220
13418
  let resolvedRemotes;
13221
- if (existsSync21(resolvedFile)) {
13419
+ if (existsSync23(resolvedFile)) {
13222
13420
  try {
13223
- resolvedRemotes = JSON.parse(readFileSync15(resolvedFile, "utf8"));
13421
+ resolvedRemotes = JSON.parse(readFileSync16(resolvedFile, "utf8"));
13224
13422
  } catch {
13225
13423
  }
13226
13424
  }
@@ -13235,14 +13433,14 @@ ${changed2.map((c) => ` - ${c}
13235
13433
  for (const r of repos) {
13236
13434
  const origin2 = originOf(r.dir);
13237
13435
  const row = matchRepoName(origin2, resolvedRemotes);
13238
- const note = !existsSync21(r.dir) ? "missing on disk" : row ? `Repos row: ${row}` : `no Repos row matches ${normalizeRemote(origin2) ?? "this checkout (no origin remote)"}`;
13436
+ const note = !existsSync23(r.dir) ? "missing on disk" : row ? `Repos row: ${row}` : `no Repos row matches ${normalizeRemote(origin2) ?? "this checkout (no origin remote)"}`;
13239
13437
  process.stdout.write(`${r.name} ${r.dir} ${note}
13240
13438
  `);
13241
13439
  }
13242
13440
  break;
13243
13441
  }
13244
- const target = resolvePath(addPath.replace(/^~(?=\/|$)/, homedir6()));
13245
- const top = existsSync21(target) ? gitOk(target, ["rev-parse", "--show-toplevel"]) : null;
13442
+ const target = resolvePath(addPath.replace(/^~(?=\/|$)/, homedir7()));
13443
+ const top = existsSync23(target) ? gitOk(target, ["rev-parse", "--show-toplevel"]) : null;
13246
13444
  if (!top) throw new ReposError(`${target} is not a git checkout`);
13247
13445
  const origin = originOf(top);
13248
13446
  const plan = planRepoAdd({
@@ -13291,15 +13489,15 @@ ${changed2.map((c) => ` - ${c}
13291
13489
  const created = await rowTracker.createRepoRow(rowPayload);
13292
13490
  process.stdout.write(`created Repos row ${String(rowPayload.name)} (${String(rowPayload.remote)}) ${created.id}
13293
13491
  `);
13294
- if (existsSync21(resolvedFile)) {
13295
- const raw = JSON.parse(readFileSync15(resolvedFile, "utf8"));
13492
+ if (existsSync23(resolvedFile)) {
13493
+ const raw = JSON.parse(readFileSync16(resolvedFile, "utf8"));
13296
13494
  raw.repoNames = { ...raw.repoNames, [created.id]: String(rowPayload.name) };
13297
13495
  raw.repoRemotes = { ...raw.repoRemotes, [created.id]: String(rowPayload.remote) };
13298
- writeFileSync10(resolvedFile, `${JSON.stringify(raw, null, 2)}
13496
+ writeFileSync13(resolvedFile, `${JSON.stringify(raw, null, 2)}
13299
13497
  `);
13300
13498
  }
13301
13499
  }
13302
- writeFileSync10(cfg.configFile, plan.text);
13500
+ writeFileSync13(cfg.configFile, plan.text);
13303
13501
  process.stdout.write(`added ${top} to ${routeName} as ${plan.mode === "dir" ? "dir" : `repos.${plan.name}`} in ${cfg.configFile}
13304
13502
  `);
13305
13503
  } catch (e) {
@@ -13538,10 +13736,10 @@ Signed in as ${result.identity.email} \u2014 minted an API key in workspace "${r
13538
13736
  operator = await pickOperator(found.holds);
13539
13737
  }
13540
13738
  let previous;
13541
- const hadPreviousFile = existsSync21(resolvedPath);
13739
+ const hadPreviousFile = existsSync23(resolvedPath);
13542
13740
  if (hadPreviousFile) {
13543
13741
  try {
13544
- previous = JSON.parse(readFileSync15(resolvedPath, "utf8"));
13742
+ previous = JSON.parse(readFileSync16(resolvedPath, "utf8"));
13545
13743
  } catch {
13546
13744
  }
13547
13745
  }
@@ -13602,10 +13800,10 @@ Signed in as ${result.identity.email} \u2014 minted an API key in workspace "${r
13602
13800
  process.stderr.write(`(dry run) would write resolved ids to ${resolvedPath}
13603
13801
  `);
13604
13802
  } else {
13605
- mkdirSync12(dirname12(resolvedPath), { recursive: true });
13803
+ mkdirSync13(dirname12(resolvedPath), { recursive: true });
13606
13804
  if (hadPreviousFile) copyFileSync2(resolvedPath, `${resolvedPath}.bak`);
13607
13805
  const tmpPath = `${resolvedPath}.tmp${process.pid}`;
13608
- writeFileSync10(tmpPath, `${JSON.stringify(resolved, null, 2)}
13806
+ writeFileSync13(tmpPath, `${JSON.stringify(resolved, null, 2)}
13609
13807
  `);
13610
13808
  renameSync2(tmpPath, resolvedPath);
13611
13809
  process.stderr.write(`Resolved ids written to ${resolvedPath}
@@ -13619,8 +13817,8 @@ Signed in as ${result.identity.email} \u2014 minted an API key in workspace "${r
13619
13817
  process.stderr.write(`(dry run) would write the minted API key to ${apiKeyFile}
13620
13818
  `);
13621
13819
  } else {
13622
- mkdirSync12(dirname12(apiKeyFile), { recursive: true });
13623
- writeFileSync10(apiKeyFile, `${apiKeyVar}=${apiKey}
13820
+ mkdirSync13(dirname12(apiKeyFile), { recursive: true });
13821
+ writeFileSync13(apiKeyFile, `${apiKeyVar}=${apiKey}
13624
13822
  `, { mode: 384 });
13625
13823
  process.stderr.write(`API key written to ${apiKeyFile}
13626
13824
  `);
@@ -13629,7 +13827,7 @@ Signed in as ${result.identity.email} \u2014 minted an API key in workspace "${r
13629
13827
  const keyPersisted = keychainBacked || apiKeyFile !== void 0;
13630
13828
  const wizard = firstRun;
13631
13829
  const writeFirstRun = wizard !== void 0 && !dryRun && keyPersisted;
13632
- const routeDir = value("dir") ?? (writeFirstRun ? join21(DEFAULT_REPOS_BASE_PATH, found.workspaceSlug) : void 0);
13830
+ const routeDir = value("dir") ?? (writeFirstRun ? join22(DEFAULT_REPOS_BASE_PATH, found.workspaceSlug) : void 0);
13633
13831
  const routeBlock = renderConnection(
13634
13832
  found,
13635
13833
  route2,
@@ -14116,7 +14314,8 @@ Signed in as ${result.identity.email} \u2014 minted an API key in workspace "${r
14116
14314
  }
14117
14315
  case "disconnect": {
14118
14316
  const url = mcpUrlFor(value("base-url") ?? cfg.routes[0]?.baseUrl ?? DEFAULT_BASE_URL);
14119
- for (const h of shipPair(cfg.ship)) {
14317
+ const disconnectHarnesses = /* @__PURE__ */ new Set([...shipPair(cfg.ship), ...shipToolchains(cfg.ship).map((t) => t.harness).filter(isPairHarness)]);
14318
+ for (const h of disconnectHarnesses) {
14120
14319
  process.stderr.write(`${pairInstallerFor(h).removePair({ url, bin: cfg.ship.agent.bin, dryRun })}
14121
14320
  `);
14122
14321
  }
@@ -14126,6 +14325,38 @@ Signed in as ${result.identity.email} \u2014 minted an API key in workspace "${r
14126
14325
  if (!await installScheduler({ replace: flag("replace") })) process.exit(2);
14127
14326
  break;
14128
14327
  }
14328
+ case "reload": {
14329
+ const sentinel = reloadSentinelFor(cfg.ship.stateDir);
14330
+ if (dryRun) {
14331
+ process.stdout.write(`would write ${sentinel}
14332
+ `);
14333
+ break;
14334
+ }
14335
+ const host = hostPlatform();
14336
+ const control = host === "windows" ? null : planDaemonControl(CREW_HOME, host, detectSystemd());
14337
+ const unitStatus = control?.statusCommand ? spawnSync5(control.statusCommand[0], control.statusCommand.slice(1), { cwd: CREW_HOME, encoding: "utf8" }) : null;
14338
+ const unitRunning = unitStatus !== null && !unitStatus.error && unitStatus.status === 0;
14339
+ if (!unitRunning) {
14340
+ process.stdout.write("crew reload: no daemon unit is running \u2014 nothing to reload (the next run reads crew.yaml fresh)\n");
14341
+ break;
14342
+ }
14343
+ mkdirSync13(cfg.ship.stateDir, { recursive: true });
14344
+ writeFileSync13(sentinel, `${(/* @__PURE__ */ new Date()).toISOString()}
14345
+ `);
14346
+ process.stdout.write("crew reload: requested \u2014 the daemon finishes its in-flight pass, then relaunches with the new config\n");
14347
+ if (flag("now") && control?.stopCommand && control.startCommand) {
14348
+ for (const cmd of [control.stopCommand, control.startCommand]) {
14349
+ const res = spawnSync5(cmd[0], cmd.slice(1), { cwd: CREW_HOME, encoding: "utf8" });
14350
+ if (cmd === control.startCommand && (res.error || res.status !== 0)) {
14351
+ process.stderr.write(`crew reload: ${cmd.join(" ")} exited ${res.status ?? "error"}
14352
+ `);
14353
+ process.exitCode = 1;
14354
+ }
14355
+ }
14356
+ process.stdout.write("crew reload: restarted the daemon unit now\n");
14357
+ }
14358
+ break;
14359
+ }
14129
14360
  case "uninstall": {
14130
14361
  const host = hostPlatform();
14131
14362
  if (host === "windows") {
@@ -14161,7 +14392,7 @@ Signed in as ${result.identity.email} \u2014 minted an API key in workspace "${r
14161
14392
  }
14162
14393
  case "log": {
14163
14394
  try {
14164
- const text = readFileSync15(cfg.ship.logFile, "utf8").split("\n");
14395
+ const text = readFileSync16(cfg.ship.logFile, "utf8").split("\n");
14165
14396
  process.stdout.write(`${text.slice(-(Number(value("tail")) || 40)).join("\n")}
14166
14397
  `);
14167
14398
  } catch {
@@ -14371,13 +14602,23 @@ routes: ${cfg.routes.map((c) => c.route).join(", ")}
14371
14602
  );
14372
14603
  try {
14373
14604
  const evFile = eventFileFor(cfg.ship.stateDir);
14374
- const evSize = existsSync21(evFile) ? statSync9(evFile).size : 0;
14605
+ const evSize = existsSync23(evFile) ? statSync9(evFile).size : 0;
14375
14606
  process.stdout.write(
14376
14607
  evSize > cfg.ship.eventsRotateBytes * 4 ? `events log: WARN ${evFile} is ${Math.round(evSize / 1048576)} MB (> 4x eventsRotateBytes); run \`crew reap\` to rotate it
14377
14608
  ` : "events log: ok\n"
14378
14609
  );
14379
14610
  } catch {
14380
14611
  }
14612
+ const daemonStartedAt = readDaemonStarted(cfg.ship.stateDir);
14613
+ if (daemonStartedAt !== null) {
14614
+ const changed2 = configChangedAfter(daemonConfigPaths(cfg), daemonStartedAt);
14615
+ if (changed2) {
14616
+ process.stdout.write(
14617
+ `daemon config: WARN ${changed2} changed at ${new Date(statSync9(changed2).mtimeMs).toISOString()} but the daemon started at ${new Date(daemonStartedAt).toISOString()} \u2014 run \`crew reload\`
14618
+ `
14619
+ );
14620
+ }
14621
+ }
14381
14622
  const tracker = new Tracker(route, cfg.ship);
14382
14623
  const contractProblems = validateContract(tracker.contract);
14383
14624
  process.stdout.write(