@tablation/crew 0.40.1 → 0.43.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,9 +1,9 @@
1
1
  #!/usr/bin/env node
2
2
 
3
3
  // src/cli.ts
4
- import { resolve as resolve4, dirname as dirname12, join as join22, basename as basename9 } from "path";
4
+ import { resolve as resolve4, dirname as dirname12, join as join23, basename as basename9 } from "path";
5
5
  import { fileURLToPath } from "url";
6
- import { hostname as hostname2, homedir as homedir7 } from "os";
6
+ import { hostname as hostname2, homedir as homedir8 } from "os";
7
7
  import { spawnSync as spawnSync5 } from "child_process";
8
8
 
9
9
  // src/argv.ts
@@ -40,11 +40,31 @@ function positionalArgs(argv2) {
40
40
  }
41
41
  return out;
42
42
  }
43
+ var WORKTREE_ROLES = ["dev", "design", "qa"];
44
+ function splitWorktreeArgs(positional2, routeNames) {
45
+ const args = positional2.slice(1);
46
+ if (args.length > 1 && WORKTREE_ROLES.includes(args[args.length - 1] ?? "")) args.pop();
47
+ const [a, b] = [args[0], args[1]];
48
+ const isTicket = (s) => !!s && /^(?:[A-Za-z]+-)?\d+$/.test(s.trim());
49
+ if (b !== void 0) return { route: a, ticket: b };
50
+ if (a !== void 0 && isTicket(a) && !routeNames.includes(a)) return { ticket: a };
51
+ return { route: a, ticket: void 0 };
52
+ }
53
+ function pickWorktreeRoute(ticketNumber, candidates) {
54
+ for (const c of candidates) {
55
+ if (c.hit && (candidates.length === 1 || c.servedRepoIds.includes(c.hit.repoId ?? ""))) return { route: c.route };
56
+ }
57
+ const looked = candidates.map((c) => c.route).join(", ");
58
+ const found = candidates.filter((c) => c.hit).map((c) => `${c.route} (repo ${c.hit?.repoId ?? "none"})`);
59
+ return {
60
+ error: `no open ticket ISSUE-${ticketNumber} served by this ship \u2014 looked on ${looked}` + (found.length ? `; found it only on ${found.join(", ")}, which has no checkout for its repo` : "")
61
+ };
62
+ }
43
63
 
44
64
  // src/config.ts
45
- import { readFileSync as readFileSync5, existsSync as existsSync5 } from "fs";
46
- import { resolve, dirname as dirname3, isAbsolute, join as join6, basename as basename3 } from "path";
47
- import { homedir as homedir4, tmpdir } from "os";
65
+ import { readFileSync as readFileSync6, existsSync as existsSync6 } from "fs";
66
+ import { resolve, dirname as dirname3, isAbsolute, join as join7, basename as basename3 } from "path";
67
+ import { homedir as homedir5, tmpdir } from "os";
48
68
  import { parse as parse2 } from "yaml";
49
69
 
50
70
  // src/events.ts
@@ -193,10 +213,10 @@ var Emitter = class _Emitter {
193
213
  var eventFileFor = (stateDir) => join(stateDir, "events.jsonl");
194
214
 
195
215
  // src/pair-install.ts
196
- import { existsSync as existsSync3, rmSync as rmSync4, writeFileSync as writeFileSync3 } from "fs";
216
+ import { existsSync as existsSync4, rmSync as rmSync4, writeFileSync as writeFileSync3 } from "fs";
197
217
  import { spawnSync as spawnSync3 } from "child_process";
198
- import { homedir as homedir3 } from "os";
199
- import { join as join4 } from "path";
218
+ import { homedir as homedir4 } from "os";
219
+ import { join as join5 } from "path";
200
220
 
201
221
  // src/stream.ts
202
222
  function toolTarget(name, input) {
@@ -320,6 +340,74 @@ function removeClaudeMcp(o) {
320
340
  return rm.status === 0 ? `mcp: removed the ${MCP_SERVER_NAME} MCP server from Claude Code (user scope)` : `mcp: \`claude mcp remove\` failed (${rm.stderr.trim() || `exit ${rm.status}`})`;
321
341
  }
322
342
 
343
+ // src/launcher.ts
344
+ import { basename, join as join3 } from "path";
345
+ import { existsSync as existsSync2, readFileSync as readFileSync3, readdirSync } from "fs";
346
+ import { homedir as homedir2 } from "os";
347
+ function isOriLauncher(spec) {
348
+ return !!spec.launcher && basename(spec.launcher) === "ori";
349
+ }
350
+ function isOmlxLauncher(spec) {
351
+ return !!spec.launcher && basename(spec.launcher) === "omlx";
352
+ }
353
+ function defaultOmlxBin(home = homedir2()) {
354
+ return join3(home, ".omlx", "bin", "omlx");
355
+ }
356
+ function launcherExecutable(spec, home) {
357
+ if (!spec.launcher) return void 0;
358
+ if (spec.launcherBin) return spec.launcherBin;
359
+ return isOmlxLauncher(spec) ? defaultOmlxBin(home) : spec.launcher;
360
+ }
361
+ function wrapInvocation(spec, o) {
362
+ const exe = launcherExecutable(spec);
363
+ if (!exe) return { bin: spec.bin, args: o.args };
364
+ if (isOmlxLauncher(spec)) return { bin: exe, args: ["launch", spec.harness, "--model", o.model, "--", ...o.args] };
365
+ return { bin: exe, args: [spec.bin, ...o.args] };
366
+ }
367
+ function selectsCustomModelProvider(args) {
368
+ for (let i = 0; i < args.length; i++) {
369
+ const a = args[i];
370
+ const v = a === "-c" || a === "--config" ? args[i + 1] : a.startsWith("--config=") ? a.slice("--config=".length) : void 0;
371
+ const m = v && /^model_provider\s*=\s*"?([^"\s]+)"?$/.exec(v.trim());
372
+ if (m && m[1] !== "openai") return true;
373
+ }
374
+ return false;
375
+ }
376
+ function parseOmlxSettings(text) {
377
+ let raw;
378
+ try {
379
+ raw = JSON.parse(text);
380
+ } catch {
381
+ return void 0;
382
+ }
383
+ if (!raw || typeof raw !== "object") return void 0;
384
+ const { server, model } = raw;
385
+ const port = Number(server?.port);
386
+ if (!Number.isInteger(port) || port <= 0) return void 0;
387
+ const host = typeof server?.host === "string" && server.host ? server.host : "127.0.0.1";
388
+ const dirs = Array.isArray(model?.model_dirs) ? model.model_dirs.filter((d) => typeof d === "string") : [];
389
+ return { host: host === "0.0.0.0" ? "127.0.0.1" : host, port, modelDirs: dirs };
390
+ }
391
+ function interpretOmlxProbe(status2) {
392
+ if (status2 === void 0) return { up: false, detail: "not reachable" };
393
+ if (status2 === 401) return { up: true, detail: "up (key required)" };
394
+ if (status2 >= 200 && status2 < 300) return { up: true, detail: "up (no key required)" };
395
+ return { up: false, detail: `answered HTTP ${status2}` };
396
+ }
397
+ function omlxModelPresent(model, dirs) {
398
+ return dirs.some((d) => {
399
+ try {
400
+ return readdirSync(d).includes(model);
401
+ } catch {
402
+ return false;
403
+ }
404
+ });
405
+ }
406
+ function readOmlxSettings(home = homedir2()) {
407
+ const p = join3(home, ".omlx", "settings.json");
408
+ return existsSync2(p) ? parseOmlxSettings(readFileSync3(p, "utf8")) : void 0;
409
+ }
410
+
323
411
  // src/capability.ts
324
412
  function profileFor(role) {
325
413
  if (role === "triage") return { level: "read", designTools: false };
@@ -400,6 +488,9 @@ var claudeAdapter = {
400
488
  ...allowedTools(o.role),
401
489
  "--disallowedTools",
402
490
  ...DISALLOWED_TOOLS,
491
+ // oMLX denies LSP itself for prefix-cache stability, but only when the
492
+ // caller passes no --disallowedTools of its own — and crew always does.
493
+ ...isOmlxLauncher(spec) ? ["LSP"] : [],
403
494
  "--model",
404
495
  o.model,
405
496
  "--output-format",
@@ -421,7 +512,7 @@ var claudeAdapter = {
421
512
  })] : [],
422
513
  ...spec.args
423
514
  ];
424
- return spec.launcher ? { bin: spec.launcherBin ?? spec.launcher, args: [spec.bin, ...args] } : { bin: spec.bin, args };
515
+ return wrapInvocation(spec, { model: o.model, args });
425
516
  },
426
517
  runEnv(spec) {
427
518
  return {
@@ -446,12 +537,12 @@ var claudeAdapter = {
446
537
 
447
538
  // src/adapters/codex.ts
448
539
  import { spawnSync as spawnSync2 } from "child_process";
449
- import { homedir as homedir2 } from "os";
450
- import { join as join3 } from "path";
540
+ import { homedir as homedir3 } from "os";
541
+ import { join as join4 } from "path";
451
542
  import { rmSync as rmSync3, writeFileSync as writeFileSync2 } from "fs";
452
543
 
453
544
  // src/pair-files.ts
454
- import { existsSync as existsSync2, mkdirSync as mkdirSync2, readFileSync as readFileSync3, writeFileSync, rmSync as rmSync2 } from "fs";
545
+ import { existsSync as existsSync3, mkdirSync as mkdirSync2, readFileSync as readFileSync4, writeFileSync, rmSync as rmSync2 } from "fs";
455
546
  import { dirname as dirname2 } from "path";
456
547
  var PAIR_BLOCK_START = "<!-- crew:pair start -->";
457
548
  var PAIR_BLOCK_END = "<!-- crew:pair end -->";
@@ -492,7 +583,7 @@ function removePairBlock(text) {
492
583
  return text.replace(new RegExp(`\\n*${BLOCK_RE.source}`), "\n").replace(/^\n+/, "").replace(/\n{3,}/g, "\n\n");
493
584
  }
494
585
  function readIfExists(path) {
495
- return existsSync2(path) ? readFileSync3(path, "utf8") : void 0;
586
+ return existsSync3(path) ? readFileSync4(path, "utf8") : void 0;
496
587
  }
497
588
  function writeFileMkdir(path, text) {
498
589
  mkdirSync2(dirname2(path), { recursive: true });
@@ -639,8 +730,8 @@ function codexTable(url) {
639
730
  return [`[mcp_servers.${SERVER}]`, `url = ${tomlString(url)}`, `bearer_token_env_var = ${tomlString(PAIR_KEY_VAR)}`];
640
731
  }
641
732
  var codexPaths = (home) => ({
642
- instructions: join3(home, ".codex", "AGENTS.md"),
643
- config: join3(home, ".codex", "config.toml")
733
+ instructions: join4(home, ".codex", "AGENTS.md"),
734
+ config: join4(home, ".codex", "config.toml")
644
735
  });
645
736
  function codexMcpState(config, url) {
646
737
  const lines = config.split("\n");
@@ -656,7 +747,7 @@ var codexPair = {
656
747
  harness: "codex",
657
748
  displayName: "Codex",
658
749
  installPair(o) {
659
- const p = codexPaths(o.home ?? homedir2());
750
+ const p = codexPaths(o.home ?? homedir3());
660
751
  const config = readIfExists(p.config) ?? "";
661
752
  const st = codexMcpState(config, o.url);
662
753
  if (st.state === "different" && !o.replace) {
@@ -673,7 +764,7 @@ var codexPair = {
673
764
  return `pair(codex): ${o.dryRun ? "(dry run) would install" : "installed"} the Pair block in ${p.instructions} and ${SERVER} in ${p.config} (key read from $${PAIR_KEY_VAR})`;
674
765
  },
675
766
  verifyPair(o) {
676
- const p = codexPaths(o.home ?? homedir2());
767
+ const p = codexPaths(o.home ?? homedir3());
677
768
  const b = blockVerdict(p.instructions, "codex");
678
769
  if (!b.ok) return b;
679
770
  const st = codexMcpState(readIfExists(p.config) ?? "", o.url);
@@ -682,7 +773,7 @@ var codexPair = {
682
773
  return { ok: true, detail: `${p.instructions}, ${p.config}` };
683
774
  },
684
775
  removePair(o) {
685
- const p = codexPaths(o.home ?? homedir2());
776
+ const p = codexPaths(o.home ?? homedir3());
686
777
  const hadBlock = removeBlockFile(p.instructions, o.dryRun);
687
778
  const config = readIfExists(p.config);
688
779
  const st = codexMcpState(config ?? "", o.url);
@@ -758,21 +849,21 @@ var codexAdapter = {
758
849
  // Prompt on stdin.
759
850
  "-"
760
851
  ];
761
- return spec.launcher ? { bin: spec.launcher, args: [spec.bin, ...args] } : { bin: spec.bin, args };
852
+ return wrapInvocation(spec, { model: o.model, args });
762
853
  },
763
854
  runEnv: (spec) => ({ ...spec.env }),
764
- provider: () => "openai",
855
+ provider: (spec) => selectsCustomModelProvider(spec.args) ? "local" : "openai",
765
856
  unsetEnv: CODEX_BILLING_VARS_TO_UNSET,
766
857
  mapStreamLine: mapCodexLine,
767
858
  extractResult: extractCodexResult,
768
859
  extractTurnTokens: extractCodexTurnTokens,
769
860
  inspectMcp(o) {
770
861
  if (binMissing(o.bin)) return { state: "no-binary" };
771
- const st = codexMcpState(readIfExists(codexPaths(o.home ?? homedir2()).config) ?? "", o.url);
862
+ const st = codexMcpState(readIfExists(codexPaths(o.home ?? homedir3()).config) ?? "", o.url);
772
863
  return st.state === "registered" ? { state: "registered", url: o.url } : st;
773
864
  },
774
865
  ensureMcp(o) {
775
- const p = codexPaths(o.home ?? homedir2());
866
+ const p = codexPaths(o.home ?? homedir3());
776
867
  const config = readIfExists(p.config) ?? "";
777
868
  const st = codexMcpState(config, o.url);
778
869
  if (st.state === "registered") return `mcp: ${SERVER} already registered with Codex \u2192 ${o.url}`;
@@ -787,8 +878,8 @@ var codexAdapter = {
787
878
 
788
879
  // src/pair-install.ts
789
880
  var geminiPaths = (home) => ({
790
- instructions: join4(home, ".gemini", "GEMINI.md"),
791
- config: join4(home, ".gemini", "settings.json")
881
+ instructions: join5(home, ".gemini", "GEMINI.md"),
882
+ config: join5(home, ".gemini", "settings.json")
792
883
  });
793
884
  function readGeminiSettings(path) {
794
885
  const text = readIfExists(path);
@@ -809,14 +900,14 @@ function geminiMcpState(s, url) {
809
900
  return { state: "registered" };
810
901
  }
811
902
  function inspectGeminiMcp(o) {
812
- const p = geminiPaths(o.home ?? homedir3());
903
+ const p = geminiPaths(o.home ?? homedir4());
813
904
  const settings = readGeminiSettings(p.config);
814
905
  if (!settings) return { state: "different", detail: `${p.config} is not a JSON object` };
815
906
  const st = geminiMcpState(settings, o.url);
816
907
  return st.state === "registered" ? { state: "registered", url: o.url } : st;
817
908
  }
818
909
  function ensureGeminiMcp(o) {
819
- const p = geminiPaths(o.home ?? homedir3());
910
+ const p = geminiPaths(o.home ?? homedir4());
820
911
  const settings = readGeminiSettings(p.config);
821
912
  if (!settings) return `gemini: ${p.config} is not a JSON object \u2014 fix it by hand; nothing was written`;
822
913
  const st = geminiMcpState(settings, o.url);
@@ -834,7 +925,7 @@ var geminiPair = {
834
925
  harness: "gemini",
835
926
  displayName: "Gemini CLI",
836
927
  installPair(o) {
837
- const p = geminiPaths(o.home ?? homedir3());
928
+ const p = geminiPaths(o.home ?? homedir4());
838
929
  const settings = readGeminiSettings(p.config);
839
930
  if (!settings) return `pair(gemini): ${p.config} is not a JSON object \u2014 fix it by hand; nothing was written`;
840
931
  const st = geminiMcpState(settings, o.url);
@@ -853,7 +944,7 @@ var geminiPair = {
853
944
  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})`;
854
945
  },
855
946
  verifyPair(o) {
856
- const p = geminiPaths(o.home ?? homedir3());
947
+ const p = geminiPaths(o.home ?? homedir4());
857
948
  const b = blockVerdict(p.instructions, "gemini");
858
949
  if (!b.ok) return b;
859
950
  const settings = readGeminiSettings(p.config);
@@ -864,7 +955,7 @@ var geminiPair = {
864
955
  return { ok: true, detail: `${p.instructions}, ${p.config}` };
865
956
  },
866
957
  removePair(o) {
867
- const p = geminiPaths(o.home ?? homedir3());
958
+ const p = geminiPaths(o.home ?? homedir4());
868
959
  const hadBlock = removeBlockFile(p.instructions, o.dryRun);
869
960
  const settings = readGeminiSettings(p.config);
870
961
  const st = settings ? geminiMcpState(settings, o.url) : { state: "missing" };
@@ -885,11 +976,11 @@ var geminiPair = {
885
976
  };
886
977
  var VSCODE_FLAVOURS = ["Code", "Code - Insiders"];
887
978
  function vscodeUserDirs(o) {
888
- const home = o.home ?? homedir3();
979
+ const home = o.home ?? homedir4();
889
980
  const platform2 = o.platform ?? process.platform;
890
- const root = platform2 === "darwin" ? join4(home, "Library", "Application Support") : platform2 === "win32" ? process.env.APPDATA ?? join4(home, "AppData", "Roaming") : join4(home, ".config");
891
- const all = VSCODE_FLAVOURS.map((f) => join4(root, f, "User"));
892
- const present = all.filter((d) => existsSync3(d));
981
+ const root = platform2 === "darwin" ? join5(home, "Library", "Application Support") : platform2 === "win32" ? process.env.APPDATA ?? join5(home, "AppData", "Roaming") : join5(home, ".config");
982
+ const all = VSCODE_FLAVOURS.map((f) => join5(root, f, "User"));
983
+ const present = all.filter((d) => existsSync4(d));
893
984
  return present.length > 0 ? present : [all[0]];
894
985
  }
895
986
  var COPILOT_FRONTMATTER = '---\napplyTo: "**"\n---\n\n';
@@ -936,8 +1027,8 @@ var copilotPair = {
936
1027
  installPair(o) {
937
1028
  const lines = [];
938
1029
  for (const dir of vscodeUserDirs(o)) {
939
- const mcpPath = join4(dir, "mcp.json");
940
- const instrPath = join4(dir, "prompts", "crew-pair.instructions.md");
1030
+ const mcpPath = join5(dir, "mcp.json");
1031
+ const instrPath = join5(dir, "prompts", "crew-pair.instructions.md");
941
1032
  const settings = readVscodeMcp(mcpPath);
942
1033
  if (!settings) {
943
1034
  lines.push(`pair(copilot): ${mcpPath} is not a plain JSON object (comments are not supported) \u2014 fix it by hand; nothing was written there`);
@@ -965,8 +1056,8 @@ var copilotPair = {
965
1056
  },
966
1057
  verifyPair(o) {
967
1058
  const verdicts = vscodeUserDirs(o).map((dir) => {
968
- const instrPath = join4(dir, "prompts", "crew-pair.instructions.md");
969
- const mcpPath = join4(dir, "mcp.json");
1059
+ const instrPath = join5(dir, "prompts", "crew-pair.instructions.md");
1060
+ const mcpPath = join5(dir, "mcp.json");
970
1061
  const b = blockVerdict(instrPath, "copilot");
971
1062
  if (!b.ok) return b;
972
1063
  const settings = readVscodeMcp(mcpPath);
@@ -981,8 +1072,8 @@ var copilotPair = {
981
1072
  removePair(o) {
982
1073
  const lines = [];
983
1074
  for (const dir of vscodeUserDirs(o)) {
984
- const mcpPath = join4(dir, "mcp.json");
985
- const hadBlock = removeCopilotInstructions(join4(dir, "prompts", "crew-pair.instructions.md"), o.dryRun);
1075
+ const mcpPath = join5(dir, "mcp.json");
1076
+ const hadBlock = removeCopilotInstructions(join5(dir, "prompts", "crew-pair.instructions.md"), o.dryRun);
986
1077
  const settings = readVscodeMcp(mcpPath);
987
1078
  const st = settings ? copilotMcpState(settings, o.url) : { state: "missing" };
988
1079
  const hadMcp = st.state === "registered";
@@ -1040,9 +1131,6 @@ function harnessPromptNote(harness) {
1040
1131
  `;
1041
1132
  }
1042
1133
 
1043
- // src/toolchain.ts
1044
- import { basename } from "path";
1045
-
1046
1134
  // src/adapters/gemini.ts
1047
1135
  var BILLING_VARS_TO_UNSET2 = [
1048
1136
  "GEMINI_API_KEY",
@@ -1136,7 +1224,7 @@ var geminiAdapter = {
1136
1224
  ...geminiGrantArgs(o.role),
1137
1225
  ...spec.args
1138
1226
  ];
1139
- return spec.launcher ? { bin: spec.launcherBin ?? spec.launcher, args: [spec.bin, ...args] } : { bin: spec.bin, args };
1227
+ return wrapInvocation(spec, { model: o.model, args });
1140
1228
  },
1141
1229
  runEnv: (spec) => ({ ...spec.env }),
1142
1230
  provider: () => "google",
@@ -1192,13 +1280,13 @@ function resolveToolchain(ship, route, repo) {
1192
1280
  }
1193
1281
  function toolchainVendor(spec) {
1194
1282
  if (isOriLauncher(spec)) return "openrouter";
1283
+ if (isOmlxLauncher(spec)) return "local";
1195
1284
  return adapterFor(spec.harness).vendor;
1196
1285
  }
1197
- function isOriLauncher(spec) {
1198
- return !!spec.launcher && basename(spec.launcher) === "ori";
1199
- }
1200
1286
  function toolchainProvider(spec) {
1201
- return isOriLauncher(spec) ? "openrouter" : adapterFor(spec.harness).provider(spec);
1287
+ if (isOriLauncher(spec)) return "openrouter";
1288
+ if (isOmlxLauncher(spec)) return "local";
1289
+ return adapterFor(spec.harness).provider(spec);
1202
1290
  }
1203
1291
  function resolveSeat(ship, route, role, persona = {}, repo) {
1204
1292
  const warnings = [];
@@ -1226,7 +1314,7 @@ function resolveSeat(ship, route, role, persona = {}, repo) {
1226
1314
  let modelWhy = `toolchain ${chosen.name} model`;
1227
1315
  let reasoning = chosen.tiers.standard?.reasoning;
1228
1316
  const explicit = persona.model;
1229
- const explicitOk = explicit !== void 0 && (vendor === "openrouter" || adapter.ownsModel(explicit));
1317
+ const explicitOk = explicit !== void 0 && (vendor === "openrouter" || vendor === "local" || adapter.ownsModel(explicit));
1230
1318
  if (explicit !== void 0 && !explicitOk) {
1231
1319
  warnings.push(`seat ${role}: persona model "${explicit}" does not belong to vendor "${vendor}" (toolchain ${chosen.name}); ignored`);
1232
1320
  }
@@ -1284,8 +1372,8 @@ function explain(ship, required) {
1284
1372
  }
1285
1373
 
1286
1374
  // src/repo-config.ts
1287
- import { readFileSync as readFileSync4, existsSync as existsSync4 } from "fs";
1288
- import { join as join5, basename as basename2 } from "path";
1375
+ import { readFileSync as readFileSync5, existsSync as existsSync5 } from "fs";
1376
+ import { join as join6, basename as basename2 } from "path";
1289
1377
  import { parse } from "yaml";
1290
1378
  var SPEC_VERSION = 1;
1291
1379
  var REPO_CONFIG_FILENAMES = [".crew.yaml", ".crew.yml", ".crew.json"];
@@ -1358,8 +1446,8 @@ var MODES = ["local", "integrate", "ci_manual", "ci_auto", "external"];
1358
1446
  var PROVIDERS = ["github", "buildkite", "other", "none"];
1359
1447
  function findRepoConfig(dir) {
1360
1448
  for (const name of REPO_CONFIG_FILENAMES) {
1361
- const p = join5(dir, name);
1362
- if (existsSync4(p)) return p;
1449
+ const p = join6(dir, name);
1450
+ if (existsSync5(p)) return p;
1363
1451
  }
1364
1452
  return null;
1365
1453
  }
@@ -1589,7 +1677,7 @@ function parseRepoConfig(text, file) {
1589
1677
  function loadRepoConfig(dir) {
1590
1678
  const file = findRepoConfig(dir);
1591
1679
  if (!file) return null;
1592
- return parseRepoConfig(readFileSync4(file, "utf8"), file);
1680
+ return parseRepoConfig(readFileSync5(file, "utf8"), file);
1593
1681
  }
1594
1682
  function hookLabel(cfg, hook2) {
1595
1683
  return cfg.labels[hook2] ?? cfg.hooks[hook2] ?? hook2;
@@ -2002,16 +2090,16 @@ function mergeRouteRelease(route, override) {
2002
2090
  };
2003
2091
  }
2004
2092
  function expand(p, base) {
2005
- let out = p.startsWith("~") ? p.replace(/^~/, homedir4()) : p;
2006
- out = out.replace(/\$\{?HOME\}?/g, homedir4());
2093
+ let out = p.startsWith("~") ? p.replace(/^~/, homedir5()) : p;
2094
+ out = out.replace(/\$\{?HOME\}?/g, homedir5());
2007
2095
  return isAbsolute(out) ? out : resolve(base, out);
2008
2096
  }
2009
2097
  function resolvePromptsDir(promptSet, crewHome, base) {
2010
- if (!promptSet) return join6(crewHome, "prompts", "default");
2098
+ if (!promptSet) return join7(crewHome, "prompts", "default");
2011
2099
  if (promptSet.includes("/") || promptSet.startsWith(".") || promptSet.startsWith("~")) {
2012
2100
  return expand(promptSet, base);
2013
2101
  }
2014
- return join6(crewHome, "prompts", promptSet);
2102
+ return join7(crewHome, "prompts", promptSet);
2015
2103
  }
2016
2104
  var Missing = class {
2017
2105
  paths = [];
@@ -2035,7 +2123,7 @@ var Missing = class {
2035
2123
  };
2036
2124
  function crewVersion(crewHome) {
2037
2125
  try {
2038
- const pkg = JSON.parse(readFileSync5(join6(crewHome, "package.json"), "utf8"));
2126
+ const pkg = JSON.parse(readFileSync6(join7(crewHome, "package.json"), "utf8"));
2039
2127
  return pkg.version ?? "0.0.0";
2040
2128
  } catch {
2041
2129
  return "0.0.0";
@@ -2045,26 +2133,26 @@ function configSearchPath(crewHome) {
2045
2133
  if (process.env.CREW_CONFIG) return [resolve(process.env.CREW_CONFIG)];
2046
2134
  const xdg = process.env.XDG_CONFIG_HOME;
2047
2135
  return [
2048
- ...xdg ? [join6(xdg, "crew", "crew.yaml")] : [],
2049
- join6(homedir4(), ".config", "crew", "crew.yaml"),
2136
+ ...xdg ? [join7(xdg, "crew", "crew.yaml")] : [],
2137
+ join7(homedir5(), ".config", "crew", "crew.yaml"),
2050
2138
  resolve(crewHome, "crew.yaml")
2051
2139
  ];
2052
2140
  }
2053
2141
  function findConfigFile(crewHome) {
2054
2142
  const candidates = configSearchPath(crewHome);
2055
- return candidates.find((c) => existsSync5(c)) ?? candidates[candidates.length - 1];
2143
+ return candidates.find((c) => existsSync6(c)) ?? candidates[candidates.length - 1];
2056
2144
  }
2057
- var defaultConfigPath = () => join6(process.env.XDG_CONFIG_HOME ?? join6(homedir4(), ".config"), "crew", "crew.yaml");
2145
+ var defaultConfigPath = () => join7(process.env.XDG_CONFIG_HOME ?? join7(homedir5(), ".config"), "crew", "crew.yaml");
2058
2146
  function routeSlug(route) {
2059
2147
  return route.replace(/\//g, "-");
2060
2148
  }
2061
2149
  function resolvedPathFor(stateDir, route) {
2062
2150
  const [workspace, project] = splitRoute(route);
2063
- return join6(stateDir, "resolved", workspace, `${project}.json`);
2151
+ return join7(stateDir, "resolved", workspace, `${project}.json`);
2064
2152
  }
2065
2153
  function apiKeyPathFor(stateDir, route) {
2066
2154
  const [workspace, project] = splitRoute(route);
2067
- return join6(stateDir, "keys", workspace, `${project}.env`);
2155
+ return join7(stateDir, "keys", workspace, `${project}.env`);
2068
2156
  }
2069
2157
  function splitRoute(route) {
2070
2158
  const parts = route.split("/");
@@ -2075,9 +2163,9 @@ function splitRoute(route) {
2075
2163
  }
2076
2164
  function readResolvedFile(stateDir, route) {
2077
2165
  const path = resolvedPathFor(stateDir, route);
2078
- if (!existsSync5(path)) return void 0;
2166
+ if (!existsSync6(path)) return void 0;
2079
2167
  try {
2080
- return JSON.parse(readFileSync5(path, "utf8"));
2168
+ return JSON.parse(readFileSync6(path, "utf8"));
2081
2169
  } catch (e) {
2082
2170
  throw new ConfigError(`${path}: not valid JSON \u2014 ${e.message}`);
2083
2171
  }
@@ -2133,18 +2221,18 @@ function parseResolved(raw, m, where) {
2133
2221
  }
2134
2222
  function loadConfig(crewHome, configFile, opts = {}) {
2135
2223
  const file = configFile ?? findConfigFile(crewHome);
2136
- if (opts.text === void 0 && !existsSync5(file)) {
2224
+ if (opts.text === void 0 && !existsSync6(file)) {
2137
2225
  throw new ConfigError(
2138
2226
  `no config found. Looked in:
2139
2227
  ` + configSearchPath(crewHome).map((c) => ` - ${c}`).join("\n") + `
2140
2228
 
2141
2229
  Create the first of those:
2142
- mkdir -p ${dirname3(defaultConfigPath())} && cp ${join6(crewHome, "crew.example.yaml")} ${defaultConfigPath()}`
2230
+ mkdir -p ${dirname3(defaultConfigPath())} && cp ${join7(crewHome, "crew.example.yaml")} ${defaultConfigPath()}`
2143
2231
  );
2144
2232
  }
2145
2233
  let raw;
2146
2234
  try {
2147
- raw = parse2(opts.text ?? readFileSync5(file, "utf8"));
2235
+ raw = parse2(opts.text ?? readFileSync6(file, "utf8"));
2148
2236
  } catch (e) {
2149
2237
  throw new ConfigError(`${file}: not valid YAML \u2014 ${e.message}`);
2150
2238
  }
@@ -2222,7 +2310,7 @@ Create the first of those:
2222
2310
  useNvm: shipRaw.useNvm !== false,
2223
2311
  nvmSh: shipRaw.nvmSh,
2224
2312
  stateDir,
2225
- logFile: expand(shipRaw.logFile ?? join6(tmpdir(), "crew.log"), base),
2313
+ logFile: expand(shipRaw.logFile ?? join7(tmpdir(), "crew.log"), base),
2226
2314
  userAgent: shipRaw.userAgent ?? `Mozilla/5.0 CrewAgent/${crewVersion(crewHome)}`,
2227
2315
  maxConcurrentAgents,
2228
2316
  streamRetentionDays,
@@ -2442,8 +2530,8 @@ function repoIdForName(r, name) {
2442
2530
  }
2443
2531
  function resolveApiKey(r) {
2444
2532
  if (r.apiKey) return r.apiKey;
2445
- if (r.apiKeyFile && r.apiKeyVar && existsSync5(r.apiKeyFile)) {
2446
- for (const line of readFileSync5(r.apiKeyFile, "utf8").split("\n")) {
2533
+ if (r.apiKeyFile && r.apiKeyVar && existsSync6(r.apiKeyFile)) {
2534
+ for (const line of readFileSync6(r.apiKeyFile, "utf8").split("\n")) {
2447
2535
  const m = line.match(new RegExp(`^${r.apiKeyVar}=(.*)$`));
2448
2536
  if (m) return (m[1] ?? "").trim().replace(/^["']|["']$/g, "");
2449
2537
  }
@@ -2506,7 +2594,7 @@ function configuredMembers(rt) {
2506
2594
  return out;
2507
2595
  }
2508
2596
  function defaultRepoDir(basePath, workspace, repoName) {
2509
- return join6(basePath, workspace, repoName);
2597
+ return join7(basePath, workspace, repoName);
2510
2598
  }
2511
2599
  function reposOf(r) {
2512
2600
  const named = Object.entries(r.repos);
@@ -2530,7 +2618,7 @@ function passengerRepoTargets(r) {
2530
2618
  return reposOf(r).filter((t) => r.repoOverrides[t.name]?.hostPassengers !== false);
2531
2619
  }
2532
2620
  function passengerCheckoutDir(basePath, workspace, repoName) {
2533
- return join6(basePath, workspace, ".passenger-checkouts", repoName);
2621
+ return join7(basePath, workspace, ".passenger-checkouts", repoName);
2534
2622
  }
2535
2623
  function passengerCheckoutTargets(r) {
2536
2624
  if (!r.hostPassengers) return [];
@@ -2662,8 +2750,8 @@ function rosterMarkdown(roster, meId) {
2662
2750
  }
2663
2751
 
2664
2752
  // src/state.ts
2665
- import { readFileSync as readFileSync6, writeFileSync as writeFileSync4, existsSync as existsSync6, mkdirSync as mkdirSync3, rmSync as rmSync5, statSync as statSync2 } from "fs";
2666
- import { join as join7 } from "path";
2753
+ import { readFileSync as readFileSync7, writeFileSync as writeFileSync4, existsSync as existsSync7, mkdirSync as mkdirSync3, rmSync as rmSync5, statSync as statSync2 } from "fs";
2754
+ import { join as join8 } from "path";
2667
2755
  var EPOCH = "1970-01-01T00:00:00.000Z";
2668
2756
  var State = class _State {
2669
2757
  dir;
@@ -2672,7 +2760,7 @@ var State = class _State {
2672
2760
  mkdirSync3(dir, { recursive: true });
2673
2761
  }
2674
2762
  path(...parts) {
2675
- return join7(this.dir, ...parts);
2763
+ return join8(this.dir, ...parts);
2676
2764
  }
2677
2765
  /**
2678
2766
  * The timestamp every "new since last poll" check is measured against.
@@ -2683,7 +2771,7 @@ var State = class _State {
2683
2771
  */
2684
2772
  watermark() {
2685
2773
  try {
2686
- const v = readFileSync6(this.path(".poll-watermark"), "utf8").trim();
2774
+ const v = readFileSync7(this.path(".poll-watermark"), "utf8").trim();
2687
2775
  return v || EPOCH;
2688
2776
  } catch {
2689
2777
  return EPOCH;
@@ -2708,10 +2796,10 @@ var State = class _State {
2708
2796
  }
2709
2797
  /** Whole-crew pause, and the per-role sentinels one level down. */
2710
2798
  isPaused() {
2711
- return existsSync6(this.path(".crew-paused"));
2799
+ return existsSync7(this.path(".crew-paused"));
2712
2800
  }
2713
2801
  isRolePaused(role) {
2714
- return existsSync6(this.path(`.role-paused-${role}`));
2802
+ return existsSync7(this.path(`.role-paused-${role}`));
2715
2803
  }
2716
2804
  pausedRoles(roles) {
2717
2805
  return new Set(roles.filter((r) => this.isRolePaused(r)));
@@ -2728,7 +2816,7 @@ var State = class _State {
2728
2816
  * while agents keep working.
2729
2817
  */
2730
2818
  isReleasePaused() {
2731
- return existsSync6(this.path(".release-paused"));
2819
+ return existsSync7(this.path(".release-paused"));
2732
2820
  }
2733
2821
  setReleasePaused(paused) {
2734
2822
  if (paused) writeFileSync4(this.path(".release-paused"), "");
@@ -2769,7 +2857,7 @@ var State = class _State {
2769
2857
  const name = _State.safe(rawName);
2770
2858
  const file = this.path(`.${name}.lock`);
2771
2859
  try {
2772
- const existing = Number.parseInt(readFileSync6(file, "utf8").trim(), 10);
2860
+ const existing = Number.parseInt(readFileSync7(file, "utf8").trim(), 10);
2773
2861
  if (Number.isFinite(existing) && existing > 0) {
2774
2862
  try {
2775
2863
  process.kill(existing, 0);
@@ -2863,7 +2951,7 @@ var State = class _State {
2863
2951
  const heldReported = `.release-held-${suffix}`;
2864
2952
  const read = (f) => {
2865
2953
  try {
2866
- return readFileSync6(this.path(f), "utf8").trim() || null;
2954
+ return readFileSync7(this.path(f), "utf8").trim() || null;
2867
2955
  } catch {
2868
2956
  return null;
2869
2957
  }
@@ -2913,7 +3001,7 @@ var State = class _State {
2913
3001
  /** Whether the board was already told about this head's red gate. */
2914
3002
  heldReported: () => {
2915
3003
  try {
2916
- return readFileSync6(this.path(heldReported), "utf8").replace(/\n$/, "") || null;
3004
+ return readFileSync7(this.path(heldReported), "utf8").replace(/\n$/, "") || null;
2917
3005
  } catch {
2918
3006
  return null;
2919
3007
  }
@@ -2950,7 +3038,7 @@ var State = class _State {
2950
3038
  const ticketStreakFile = `.fairness-ticket-streak-${suffix}`;
2951
3039
  const read = (f) => {
2952
3040
  try {
2953
- return readFileSync6(this.path(f), "utf8").trim() || null;
3041
+ return readFileSync7(this.path(f), "utf8").trim() || null;
2954
3042
  } catch {
2955
3043
  return null;
2956
3044
  }
@@ -3015,7 +3103,7 @@ var State = class _State {
3015
3103
  return {
3016
3104
  previous: () => {
3017
3105
  try {
3018
- const parsed = JSON.parse(readFileSync6(this.path(file), "utf8"));
3106
+ const parsed = JSON.parse(readFileSync7(this.path(file), "utf8"));
3019
3107
  return parsed && typeof parsed === "object" ? parsed : {};
3020
3108
  } catch {
3021
3109
  return {};
@@ -3036,7 +3124,7 @@ var State = class _State {
3036
3124
  return {
3037
3125
  previous: () => {
3038
3126
  try {
3039
- const parsed = JSON.parse(readFileSync6(this.path(file), "utf8"));
3127
+ const parsed = JSON.parse(readFileSync7(this.path(file), "utf8"));
3040
3128
  return Array.isArray(parsed) ? parsed : [];
3041
3129
  } catch {
3042
3130
  return [];
@@ -3069,7 +3157,7 @@ var State = class _State {
3069
3157
  }
3070
3158
  readInt(file) {
3071
3159
  try {
3072
- return Number.parseInt(readFileSync6(this.path(file), "utf8").trim(), 10) || 0;
3160
+ return Number.parseInt(readFileSync7(this.path(file), "utf8").trim(), 10) || 0;
3073
3161
  } catch {
3074
3162
  return 0;
3075
3163
  }
@@ -3103,7 +3191,8 @@ var DEFAULT_CONTRACT = {
3103
3191
  ship: "ship_id",
3104
3192
  heldBy: "held_by_ship_id",
3105
3193
  verifiedSha: "verified_sha",
3106
- prRef: "pr_ref"
3194
+ prRef: "pr_ref",
3195
+ position: "position"
3107
3196
  },
3108
3197
  comments: {
3109
3198
  parent: "ticket_id",
@@ -3373,9 +3462,9 @@ function describeRelease(o, route) {
3373
3462
  }
3374
3463
 
3375
3464
  // src/install.ts
3376
- import { dirname as dirname4, join as join8, extname } from "path";
3377
- import { homedir as homedir5 } from "os";
3378
- import { accessSync, constants, existsSync as existsSync7, mkdirSync as mkdirSync4, readdirSync, readFileSync as readFileSync7, statSync as statSync3, writeFileSync as writeFileSync5, unlinkSync, realpathSync } from "fs";
3465
+ import { dirname as dirname4, join as join9, extname } from "path";
3466
+ import { homedir as homedir6 } from "os";
3467
+ import { accessSync, constants, existsSync as existsSync8, mkdirSync as mkdirSync4, readdirSync as readdirSync2, readFileSync as readFileSync8, statSync as statSync3, writeFileSync as writeFileSync5, unlinkSync, realpathSync } from "fs";
3379
3468
  import { spawnSync as spawnSync4 } from "child_process";
3380
3469
  import { createHash } from "crypto";
3381
3470
 
@@ -3387,7 +3476,7 @@ function isCompiledBinary(moduleUrl) {
3387
3476
  // src/install.ts
3388
3477
  function crewInvocation(crewHome) {
3389
3478
  if (isCompiledBinary(import.meta.url)) return [process.execPath];
3390
- return [process.execPath, join8(crewHome, "bin", "crew")];
3479
+ return [process.execPath, join9(crewHome, "bin", "crew")];
3391
3480
  }
3392
3481
  var INTERVAL_SECONDS = 120;
3393
3482
  function labelFor(crewHome, job) {
@@ -3425,7 +3514,7 @@ function pathFor(ship) {
3425
3514
  return parts.join(":");
3426
3515
  }
3427
3516
  function findOnPath(bin, searchPath) {
3428
- const candidates = bin.includes("/") ? [bin] : searchPath.split(":").filter(Boolean).map((dir) => join8(dir, bin));
3517
+ const candidates = bin.includes("/") ? [bin] : searchPath.split(":").filter(Boolean).map((dir) => join9(dir, bin));
3429
3518
  for (const candidate of candidates) {
3430
3519
  try {
3431
3520
  accessSync(candidate, constants.X_OK);
@@ -3435,7 +3524,7 @@ function findOnPath(bin, searchPath) {
3435
3524
  }
3436
3525
  return void 0;
3437
3526
  }
3438
- var COMMON_DOCKER_DIRS = ["/usr/local/bin", "/opt/homebrew/bin", join8(homedir5(), ".docker", "bin")].join(":");
3527
+ var COMMON_DOCKER_DIRS = ["/usr/local/bin", "/opt/homebrew/bin", join9(homedir6(), ".docker", "bin")].join(":");
3439
3528
  function dockerPathProblem(ship, shellPath = process.env.PATH ?? "") {
3440
3529
  const schedulerPath = pathFor(ship);
3441
3530
  if (findOnPath("docker", schedulerPath)) return void 0;
@@ -3447,7 +3536,7 @@ function xmlEscape(s) {
3447
3536
  }
3448
3537
  function planLaunchd(ship, crewHome, job) {
3449
3538
  const label = labelFor(crewHome, job);
3450
- const unitPath = join8(homedir5(), "Library", "LaunchAgents", `${label}.plist`);
3539
+ const unitPath = join9(homedir6(), "Library", "LaunchAgents", `${label}.plist`);
3451
3540
  const crewLog = ship.logFile;
3452
3541
  const schedulerLog = schedulerLogFor(crewLog, job);
3453
3542
  const persistent = job === "run";
@@ -3499,8 +3588,8 @@ ${scheduleKeys}
3499
3588
  }
3500
3589
  function planSystemd(ship, crewHome, job) {
3501
3590
  const label = labelFor(crewHome, job);
3502
- const unitDir = join8(homedir5(), ".config", "systemd", "user");
3503
- const servicePath = join8(unitDir, `${label}.service`);
3591
+ const unitDir = join9(homedir6(), ".config", "systemd", "user");
3592
+ const servicePath = join9(unitDir, `${label}.service`);
3504
3593
  const crewLog = ship.logFile;
3505
3594
  const schedulerLog = schedulerLogFor(crewLog, job);
3506
3595
  const description = job === "release" ? "Tablation crew \u2014 release" : job === "passengers" ? "Tablation crew \u2014 Host Passengers sync" : "Tablation crew \u2014 persistent supervisor loop";
@@ -3537,7 +3626,7 @@ WantedBy=default.target
3537
3626
  schedulerLog
3538
3627
  };
3539
3628
  }
3540
- const timerPath = join8(unitDir, `${label}.timer`);
3629
+ const timerPath = join9(unitDir, `${label}.timer`);
3541
3630
  const service = `# Written by \`crew install\` \u2014 re-run it rather than hand-editing this file.
3542
3631
  [Unit]
3543
3632
  Description=${description}
@@ -3604,7 +3693,7 @@ function planInstall(ship, crewHome, hostShip, hasSystemd, job = "run") {
3604
3693
  function planUninstall(ship, crewHome, hostShip, hasSystemd, job = "run") {
3605
3694
  const label = labelFor(crewHome, job);
3606
3695
  if (hostShip === "macos") {
3607
- const unitPath = join8(homedir5(), "Library", "LaunchAgents", `${label}.plist`);
3696
+ const unitPath = join9(homedir6(), "Library", "LaunchAgents", `${label}.plist`);
3608
3697
  return {
3609
3698
  mechanism: "launchd",
3610
3699
  unitPaths: [unitPath],
@@ -3614,18 +3703,18 @@ function planUninstall(ship, crewHome, hostShip, hasSystemd, job = "run") {
3614
3703
  };
3615
3704
  }
3616
3705
  if (hostShip === "linux" && hasSystemd) {
3617
- const unitDir = join8(homedir5(), ".config", "systemd", "user");
3706
+ const unitDir = join9(homedir6(), ".config", "systemd", "user");
3618
3707
  if (job === "run") {
3619
3708
  return {
3620
3709
  mechanism: "systemd",
3621
- unitPaths: [join8(unitDir, `${label}.service`)],
3710
+ unitPaths: [join9(unitDir, `${label}.service`)],
3622
3711
  crontabLine: null,
3623
3712
  unloadCommands: [["systemctl", "--user", "disable", "--now", `${label}.service`]]
3624
3713
  };
3625
3714
  }
3626
3715
  return {
3627
3716
  mechanism: "systemd",
3628
- unitPaths: [join8(unitDir, `${label}.service`), join8(unitDir, `${label}.timer`)],
3717
+ unitPaths: [join9(unitDir, `${label}.service`), join9(unitDir, `${label}.timer`)],
3629
3718
  crontabLine: null,
3630
3719
  unloadCommands: [["systemctl", "--user", "disable", "--now", `${label}.timer`]]
3631
3720
  };
@@ -3643,7 +3732,7 @@ function planUninstall(ship, crewHome, hostShip, hasSystemd, job = "run") {
3643
3732
  function planDaemonControl(crewHome, hostShip, hasSystemd) {
3644
3733
  const label = labelFor(crewHome, "run");
3645
3734
  if (hostShip === "macos") {
3646
- const unitPath = join8(homedir5(), "Library", "LaunchAgents", `${label}.plist`);
3735
+ const unitPath = join9(homedir6(), "Library", "LaunchAgents", `${label}.plist`);
3647
3736
  return {
3648
3737
  mechanism: "launchd",
3649
3738
  startCommand: ["launchctl", "load", unitPath],
@@ -3738,7 +3827,7 @@ async function applyUninstall(plan, crewHome, dryRun, log) {
3738
3827
  }
3739
3828
  }
3740
3829
  for (const path of plan.unitPaths) {
3741
- if (!existsSync7(path)) continue;
3830
+ if (!existsSync8(path)) continue;
3742
3831
  if (dryRun) {
3743
3832
  log.emit(`would remove ${path}`);
3744
3833
  continue;
@@ -3757,7 +3846,7 @@ async function applyUninstall(plan, crewHome, dryRun, log) {
3757
3846
  }
3758
3847
  var CREW_LAUNCHD_PREFIX = "com.tablation.crew";
3759
3848
  function launchAgentsDir() {
3760
- return join8(homedir5(), "Library", "LaunchAgents");
3849
+ return join9(homedir6(), "Library", "LaunchAgents");
3761
3850
  }
3762
3851
  function xmlUnescape(s) {
3763
3852
  return s.replace(/&lt;/g, "<").replace(/&gt;/g, ">").replace(/&amp;/g, "&");
@@ -3774,25 +3863,25 @@ function loadedCrewLabels() {
3774
3863
  return [...parseLaunchctlList(list.stdout ?? "").keys()].filter((l) => l.startsWith(CREW_LAUNCHD_PREFIX));
3775
3864
  }
3776
3865
  function listLaunchdUnits(dir = launchAgentsDir(), loaded = []) {
3777
- const files = existsSync7(dir) ? readdirSync(dir) : [];
3866
+ const files = existsSync8(dir) ? readdirSync2(dir) : [];
3778
3867
  const units = files.filter((f) => f.startsWith(CREW_LAUNCHD_PREFIX) && f.endsWith(".plist")).sort().map((f) => {
3779
- const plistPath = join8(dir, f);
3868
+ const plistPath = join9(dir, f);
3780
3869
  let program = null;
3781
3870
  try {
3782
- program = programFromPlist(readFileSync7(plistPath, "utf8"));
3871
+ program = programFromPlist(readFileSync8(plistPath, "utf8"));
3783
3872
  } catch {
3784
3873
  }
3785
3874
  return {
3786
3875
  label: f.slice(0, -".plist".length),
3787
3876
  plistPath,
3788
3877
  program,
3789
- programExists: program !== null && existsSync7(program)
3878
+ programExists: program !== null && existsSync8(program)
3790
3879
  };
3791
3880
  });
3792
3881
  const seen = new Set(units.map((u) => u.label));
3793
3882
  for (const label of [...loaded].sort()) {
3794
3883
  if (!label.startsWith(CREW_LAUNCHD_PREFIX) || seen.has(label)) continue;
3795
- units.push({ label, plistPath: join8(dir, `${label}.plist`), program: null, programExists: false, orphan: true });
3884
+ units.push({ label, plistPath: join9(dir, `${label}.plist`), program: null, programExists: false, orphan: true });
3796
3885
  }
3797
3886
  return units;
3798
3887
  }
@@ -3805,9 +3894,9 @@ function removeLaunchdUnit(unit, dryRun, log) {
3805
3894
  log.emit(`would unload and remove ${unit.label}`);
3806
3895
  return;
3807
3896
  }
3808
- if (existsSync7(unit.plistPath)) spawnSync4("launchctl", ["unload", unit.plistPath], { encoding: "utf8" });
3897
+ if (existsSync8(unit.plistPath)) spawnSync4("launchctl", ["unload", unit.plistPath], { encoding: "utf8" });
3809
3898
  spawnSync4("launchctl", ["remove", unit.label], { encoding: "utf8" });
3810
- if (existsSync7(unit.plistPath)) unlinkSync(unit.plistPath);
3899
+ if (existsSync8(unit.plistPath)) unlinkSync(unit.plistPath);
3811
3900
  log.emit(`removed ${unit.label} (${unit.plistPath})`);
3812
3901
  }
3813
3902
  function clearForeignLaunchdUnits(crewHome, replace, dryRun, log, dir, loaded) {
@@ -3890,7 +3979,7 @@ import { statfsSync } from "fs";
3890
3979
  // src/git.ts
3891
3980
  import { execFileSync } from "child_process";
3892
3981
  import {
3893
- existsSync as existsSync8,
3982
+ existsSync as existsSync9,
3894
3983
  mkdirSync as mkdirSync5,
3895
3984
  openSync as openSync2,
3896
3985
  closeSync as closeSync2,
@@ -4262,7 +4351,7 @@ function cloneUrlFor(remote) {
4262
4351
  return `git@github.com:${remote}.git`;
4263
4352
  }
4264
4353
  function ensureRepoCheckout(dir, remote) {
4265
- if (existsSync8(`${dir}/.git`)) return false;
4354
+ if (existsSync9(`${dir}/.git`)) return false;
4266
4355
  if (!remote) {
4267
4356
  throw new GitError(
4268
4357
  `${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`
@@ -5232,7 +5321,14 @@ function missingBlockerIds(tickets) {
5232
5321
  function blockerInfoMap(tickets, extra = []) {
5233
5322
  const info = {};
5234
5323
  for (const t of [...tickets, ...extra]) {
5235
- info[t.id] = { issue_id: t.issue_id, issue_tag: t.issue_tag, status: t.status };
5324
+ info[t.id] = {
5325
+ issue_id: t.issue_id,
5326
+ issue_tag: t.issue_tag,
5327
+ status: t.status,
5328
+ repo_id: t.repo_id,
5329
+ merged_at: t.merged_at,
5330
+ commit_sha: t.commit_sha
5331
+ };
5236
5332
  }
5237
5333
  return info;
5238
5334
  }
@@ -5241,14 +5337,18 @@ function computeBlockedIds(tickets, info) {
5241
5337
  for (const t of tickets) if (unresolvedBlockers(t, info).length > 0) blocked.add(t.id);
5242
5338
  return blocked;
5243
5339
  }
5244
- function unresolvedBlockers(t, info) {
5340
+ function unresolvedBlockers(t, info, opts = {}) {
5341
+ const mergeGated = opts.mergeGated ?? true;
5245
5342
  return (t.blocked_by ?? []).filter((id) => {
5246
- const status2 = info[id]?.status;
5247
- return status2 !== void 0 && !RESOLVED_STATUSES.has(status2);
5343
+ const b = info[id];
5344
+ const status2 = b?.status;
5345
+ if (!b || status2 === void 0) return false;
5346
+ if (!RESOLVED_STATUSES.has(status2)) return true;
5347
+ return mergeGated && status2 === "verified" && !!t.repo_id && b.repo_id === t.repo_id && !b.merged_at && !b.commit_sha;
5248
5348
  });
5249
5349
  }
5250
- function describeUnresolved(t, info) {
5251
- return unresolvedBlockers(t, info).map((id) => `${info[id]?.issue_tag ?? info[id]?.issue_id ?? "?"} (${info[id]?.status ?? "unknown"})`).join(", ");
5350
+ function describeUnresolved(t, info, opts = {}) {
5351
+ return unresolvedBlockers(t, info, opts).map((id) => `${info[id]?.issue_tag ?? info[id]?.issue_id ?? "?"} (${info[id]?.status ?? "unknown"})`).join(", ");
5252
5352
  }
5253
5353
  function sweepDiagnostics(tickets, info) {
5254
5354
  const dangling = [];
@@ -5317,7 +5417,7 @@ function filingErrors(tickets, opts = {}) {
5317
5417
  function sweepComment(step) {
5318
5418
  return step.action === "park" ? `Parked as **Blocked** by the crew \u2014 waiting on: ${step.blockers}.
5319
5419
 
5320
- This is automatic and reversible: an approved ticket whose \`Blocked by\` entries are not all resolved is parked out of the queue, and restored on the first poll after the last one resolves. Nothing else about the ticket changes. A blocker counts as resolved at \`verified\`, \`closed_deployed\`, \`closed_wont_fix\` or \`closed_duplicate\` \u2014 not at \`fixed\`, which is still an unmerged branch awaiting QA.` : `Restored to **Approved** by the crew \u2014 every blocker has resolved${step.blockers && step.blockers !== "\u2014" ? ` (${step.blockers})` : ""}.
5420
+ This is automatic and reversible: an approved ticket whose \`Blocked by\` entries are not all resolved is parked out of the queue, and restored on the first poll after the last one resolves. Nothing else about the ticket changes. A blocker counts as resolved at \`closed_deployed\`, \`closed_wont_fix\` or \`closed_duplicate\`, or at \`verified\` when it is in a different repository or already merged (\`merged_at\` set) \u2014 not at \`fixed\`, which is still an unmerged branch awaiting QA, and not at \`verified\` for a blocker in this ticket's own repository until the release has merged it, so nothing gets built stacked on an unmerged branch.` : `Restored to **Approved** by the crew \u2014 every blocker has resolved${step.blockers && step.blockers !== "\u2014" ? ` (${step.blockers})` : ""}.
5321
5421
 
5322
5422
  This restores the status it was approved at before being parked; it is back in the queue and a role may pick it up. If a blocker resolved as \`closed_wont_fix\` or \`closed_duplicate\` this ticket may still not be buildable as written \u2014 that is a call for a human, not the crew.`;
5323
5423
  }
@@ -5380,20 +5480,25 @@ function issueNumber(t) {
5380
5480
  function epicRank(t) {
5381
5481
  return t.epicInProgress ? 0 : 1;
5382
5482
  }
5483
+ function positionRank(t) {
5484
+ const p = t.position;
5485
+ return typeof p === "number" && Number.isFinite(p) ? p : Number.POSITIVE_INFINITY;
5486
+ }
5383
5487
  function rank(t, c = DEFAULT_CONTRACT) {
5384
- return [effectivePriority(t, c), severityRank(t, c), epicRank(t), issueNumber(t)];
5488
+ return [effectivePriority(t, c), positionRank(t), severityRank(t, c), epicRank(t), issueNumber(t)];
5385
5489
  }
5386
5490
  function compareRank(a, b, c = DEFAULT_CONTRACT) {
5387
5491
  const ra = rank(a, c);
5388
5492
  const rb = rank(b, c);
5389
5493
  for (let i = 0; i < ra.length; i++) {
5390
- const d = ra[i] - rb[i];
5391
- if (d !== 0) return d;
5494
+ const x = ra[i];
5495
+ const y = rb[i];
5496
+ if (x !== y) return x < y ? -1 : 1;
5392
5497
  }
5393
5498
  return 0;
5394
5499
  }
5395
5500
  function rankScalar(t, c = DEFAULT_CONTRACT) {
5396
- const [p, s, e, n] = rank(t, c);
5501
+ const [p, , s, e, n] = rank(t, c);
5397
5502
  return p * 1e9 + s * 1e7 + e * 1e6 + n;
5398
5503
  }
5399
5504
  var NOTHING_ACTIONABLE = 999999999999;
@@ -5600,7 +5705,7 @@ function claimStatuses(role, c) {
5600
5705
  }
5601
5706
  function rankedCandidates(role, i) {
5602
5707
  const c = i.contract ?? DEFAULT_CONTRACT;
5603
- return [...roleCandidates(role, i)].sort((a, b) => rankScalar(a, c) - rankScalar(b, c));
5708
+ return [...roleCandidates(role, i)].sort((a, b) => compareRank(a, b, c));
5604
5709
  }
5605
5710
  function roleTopRank(role, i) {
5606
5711
  if (role !== "qa" && role !== "triage" && !i.seats[role]) return NOTHING_ACTIONABLE;
@@ -5613,7 +5718,7 @@ function actionableSummary(i, pending) {
5613
5718
  const c = i.contract ?? DEFAULT_CONTRACT;
5614
5719
  const all = pending.flatMap((role) => roleCandidates(role, i));
5615
5720
  if (all.length === 0) return { count: 0 };
5616
- const top = all.reduce((best, t) => rankScalar(t, c) < rankScalar(best, c) ? t : best);
5721
+ const top = all.reduce((best, t) => compareRank(t, best, c) < 0 ? t : best);
5617
5722
  return { count: all.length, top };
5618
5723
  }
5619
5724
  var FIXED_RANK = { qa: -2, triage: -1 };
@@ -5839,6 +5944,7 @@ function blockers(t, i) {
5839
5944
  function modeCell(t, i) {
5840
5945
  return i.workModes && i.workModes.modes.size > 0 ? modeOf(t, i.workModes) : "\u2014";
5841
5946
  }
5947
+ var effCell = (t) => `p${effectivePriority(t)}${positionRank(t) === Number.POSITIVE_INFINITY ? "" : ` #${positionRank(t)}`}`;
5842
5948
  var sorted = (ts) => [...ts].sort(compareRank);
5843
5949
  var stream = (segments) => `${segments.join("\n")}
5844
5950
  `;
@@ -5857,7 +5963,7 @@ function buildingDigest(i) {
5857
5963
  const worktree = i.worktreeFor ? i.worktreeFor(t) : "";
5858
5964
  const dir = i.dirFor ? i.dirFor(t) : null;
5859
5965
  const repo = i.dirFor ? dir ?? "**NO CHECKOUT**" : "";
5860
- return `| ${displayKey2(t)} | ${repo} | ${branch} | ${worktree} | ${modeCell(t, i)} | ${t.status} | ${who(t, i)} | ${filedBy(t, i)} | ${t.severity ?? "\u2014"} | ${t.priority ?? "\u2014"} | p${effectivePriority(t)} | ${t.updated_at.slice(0, 16)}Z | ${lastComment(i, t.id)} | ${n > 0 ? `**${n} new**` : "\u2014"} |`;
5966
+ return `| ${displayKey2(t)} | ${repo} | ${branch} | ${worktree} | ${modeCell(t, i)} | ${t.status} | ${who(t, i)} | ${filedBy(t, i)} | ${t.severity ?? "\u2014"} | ${t.priority ?? "\u2014"} | ${effCell(t)} | ${t.updated_at.slice(0, 16)}Z | ${lastComment(i, t.id)} | ${n > 0 ? `**${n} new**` : "\u2014"} |`;
5861
5967
  };
5862
5968
  const blockedRow = (t) => `| ${displayKey2(t)} | ${t.status} | ${who(t, i)} | p${effectivePriority(t)} | ${blockers(t, i)} |`;
5863
5969
  const blockedTable = (ts) => ts.length === 0 ? "\n_None._\n" : `
@@ -5897,7 +6003,7 @@ ${sorted(held).map((t) => `| ${displayKey2(t)} | ${stopOf(t)} |`).join("\n")}
5897
6003
  "\nWork the first one you are not required to skip. Tickets waiting on an\nunresolved dependency are already out of this table \u2014 see Blocked below.\n",
5898
6004
  table(step2, i, header, row),
5899
6005
  "\n### Blocked \u2014 waiting on a dependency, not yours to start\n",
5900
- '\nListed rather than hidden so "nothing to do" stays distinguishable from\n"everything is parked". The loop parks and restores these itself, on every\npoll: an approved ticket whose `Blocked by` entries are unresolved moves to\n`blocked`, and back to `accepted` once the last one resolves. **Do not pick\none up, do not set or clear `blocked` by hand, and do not "unblock" one by\nediting its `Blocked by` field** \u2014 if a blocker looks wrong, say so in your\nrun summary. A blocker counts as resolved at `verified`, `closed_deployed`,\n`closed_wont_fix` or `closed_duplicate`; `fixed` is still an unmerged branch\nawaiting QA, so it does not count.\n',
6006
+ '\nListed rather than hidden so "nothing to do" stays distinguishable from\n"everything is parked". The loop parks and restores these itself, on every\npoll: an approved ticket whose `Blocked by` entries are unresolved moves to\n`blocked`, and back to `accepted` once the last one resolves. **Do not pick\none up, do not set or clear `blocked` by hand, and do not "unblock" one by\nediting its `Blocked by` field** \u2014 if a blocker looks wrong, say so in your\nrun summary. A blocker counts as resolved at `closed_deployed`,\n`closed_wont_fix` or `closed_duplicate`, or at `verified` when it is in another\nrepository or already merged; `fixed` is still an unmerged branch awaiting QA,\nand a `verified` blocker in the same repository as the blocked ticket is unmerged until the\nrelease merges it, so neither counts.\n',
5901
6007
  blockedTable(i.tickets.filter(isBlocked)),
5902
6008
  ...heldTable ? [heldTable] : []
5903
6009
  ]);
@@ -5918,7 +6024,7 @@ function qaDigest(i) {
5918
6024
  const n = newFromOthers(i, t.id);
5919
6025
  const dir = i.dirFor ? i.dirFor(t) : null;
5920
6026
  const repo = i.dirFor ? dir ?? "**NO CHECKOUT**" : "";
5921
- return `| ${displayKey2(t)} | ${repo} | ${modeCell(t, i)} | ${t.status} | ${builtBy(t)} | ${who(t, i)} | ${filedBy(t, i)} | ${t.severity ?? "\u2014"} | ${t.priority ?? "\u2014"} | p${effectivePriority(t)} | ${branchCell(t)} | ${worktreeCell(t)} | ${t.updated_at.slice(0, 16)}Z | ${lastComment(i, t.id)} | ${n > 0 ? `**${n} new**` : "\u2014"} |`;
6027
+ return `| ${displayKey2(t)} | ${repo} | ${modeCell(t, i)} | ${t.status} | ${builtBy(t)} | ${who(t, i)} | ${filedBy(t, i)} | ${t.severity ?? "\u2014"} | ${t.priority ?? "\u2014"} | ${effCell(t)} | ${branchCell(t)} | ${worktreeCell(t)} | ${t.updated_at.slice(0, 16)}Z | ${lastComment(i, t.id)} | ${n > 0 ? `**${n} new**` : "\u2014"} |`;
5922
6028
  };
5923
6029
  const waiting = i.tickets.filter((t) => (t.status === "qa" || t.status === "fixed") && i.blocked.has(t.id));
5924
6030
  const takeable = (status2) => i.tickets.filter((t) => t.status === status2 && !i.blocked.has(t.id));
@@ -5926,7 +6032,7 @@ function qaDigest(i) {
5926
6032
  ### Waiting on blockers \u2014 not yours to verify yet
5927
6033
 
5928
6034
  A \`fixed\` or \`qa\` ticket whose \`Blocked by\` names something not yet resolved
5929
- (\`verified\`, \`closed_deployed\`, \`closed_wont_fix\` or \`closed_duplicate\`) is held out of
6035
+ (\`closed_deployed\`, \`closed_wont_fix\` or \`closed_duplicate\`, or \`verified\` once merged or in another repository) is held out of
5930
6036
  verification: its change needs the blocker released first. It becomes yours on
5931
6037
  the first poll after the last blocker resolves. No one needs to change the ticket.
5932
6038
 
@@ -6000,7 +6106,7 @@ function materializeBranchForTicket(dir, cfg, t, role) {
6000
6106
 
6001
6107
  // src/poll.ts
6002
6108
  import { writeFileSync as writeFileSync6 } from "fs";
6003
- import { join as join9 } from "path";
6109
+ import { join as join10 } from "path";
6004
6110
 
6005
6111
  // src/stalled.ts
6006
6112
  var STALLED_AFTER_CYCLES = 30;
@@ -6113,6 +6219,7 @@ async function decideCycle(o) {
6113
6219
  data: { tickets: tickets.length, comments: comments.length }
6114
6220
  });
6115
6221
  const epicColumn = tracker.contract.columns.epic;
6222
+ const positionColumn = tracker.contract.columns.position;
6116
6223
  const epicBuilding = tracker.contract.epics.statuses.building;
6117
6224
  const epicStatusColumn = tracker.contract.epics.statusColumn;
6118
6225
  const epicStatusById = new Map(epicRows.map((e) => [e.id, e[epicStatusColumn] ?? null]));
@@ -6132,6 +6239,9 @@ async function decideCycle(o) {
6132
6239
  for (const t of tickets) {
6133
6240
  const epicId = t[epicColumn];
6134
6241
  t.epicInProgress = typeof epicId === "string" && epicStatusById.get(epicId) === epicBuilding;
6242
+ const pos = t[positionColumn];
6243
+ const n = typeof pos === "string" && pos.trim() !== "" ? Number(pos) : pos;
6244
+ t.position = typeof n === "number" && Number.isFinite(n) ? n : null;
6135
6245
  }
6136
6246
  const extra = await tracker.ticketsByIds(missingBlockerIds(tickets));
6137
6247
  const info = blockerInfoMap(tickets, extra);
@@ -6272,7 +6382,7 @@ async function decideCycle(o) {
6272
6382
  }
6273
6383
  return decision;
6274
6384
  }
6275
- var digestPath = (stateDir, route, role) => join9(stateDir, `digest-${route}-${role}.md`);
6385
+ var digestPath = (stateDir, route, role) => join10(stateDir, `digest-${route}-${role}.md`);
6276
6386
  function writeDigest(o, d, role, stateDir) {
6277
6387
  const me = o.route.resolved?.seats[role];
6278
6388
  if (!me) return false;
@@ -6642,9 +6752,9 @@ var alreadyReported = (ticket, g, comments) => comments.some((c) => c.ticket_id
6642
6752
 
6643
6753
  // src/agent.ts
6644
6754
  import { spawn as spawn2 } from "child_process";
6645
- import { readFileSync as readFileSync10, existsSync as existsSync11, statSync as statSync5, mkdirSync as mkdirSync6, writeFileSync as writeFileSync7, createWriteStream } from "fs";
6755
+ import { readFileSync as readFileSync11, existsSync as existsSync12, statSync as statSync5, mkdirSync as mkdirSync6, writeFileSync as writeFileSync7, createWriteStream } from "fs";
6646
6756
  import { finished } from "stream/promises";
6647
- import { join as join12, dirname as dirname6 } from "path";
6757
+ import { join as join13, dirname as dirname6 } from "path";
6648
6758
  import { createHash as createHash2 } from "crypto";
6649
6759
 
6650
6760
  // src/environment.ts
@@ -7033,14 +7143,14 @@ function expandSkillMarkers(text, lookup, opts = {}) {
7033
7143
  }
7034
7144
 
7035
7145
  // src/skills.ts
7036
- import { readFileSync as readFileSync9, readdirSync as readdirSync2, existsSync as existsSync10 } from "fs";
7037
- import { join as join11 } from "path";
7146
+ import { readFileSync as readFileSync10, readdirSync as readdirSync3, existsSync as existsSync11 } from "fs";
7147
+ import { join as join12 } from "path";
7038
7148
  import { parse as parseYaml } from "yaml";
7039
7149
  import { TablationClient as TablationClient3, StaleWriteError as StaleWriteError7, TablationApiError as TablationApiError2 } from "@tablation/client";
7040
7150
 
7041
7151
  // src/agents.ts
7042
- import { readFileSync as readFileSync8, existsSync as existsSync9 } from "fs";
7043
- import { join as join10 } from "path";
7152
+ import { readFileSync as readFileSync9, existsSync as existsSync10 } from "fs";
7153
+ import { join as join11 } from "path";
7044
7154
  import { TablationClient as TablationClient2, StaleWriteError as StaleWriteError6, TablationApiError } from "@tablation/client";
7045
7155
  var PERSONA_NAME = {
7046
7156
  dev: "Developer",
@@ -7054,12 +7164,12 @@ var CREW_AGENT_COLUMN = "agent_id";
7054
7164
  var AgentsSyncError = class extends Error {
7055
7165
  };
7056
7166
  function personaDefaultPrompt(promptsDir, role) {
7057
- const briefPath = join10(promptsDir, "personas", `lane-${role}.md`);
7058
- if (!existsSync9(briefPath)) throw new AgentsSyncError(`no brief at ${briefPath}`);
7059
- if (SELF_CONTAINED_ROLES.has(role)) return readFileSync8(briefPath, "utf8");
7060
- const commonPath = join10(promptsDir, "personas", "common.md");
7061
- if (!existsSync9(commonPath)) return readFileSync8(briefPath, "utf8");
7062
- return `${readFileSync8(commonPath, "utf8")}${readFileSync8(briefPath, "utf8")}`;
7167
+ const briefPath = join11(promptsDir, "personas", `lane-${role}.md`);
7168
+ if (!existsSync10(briefPath)) throw new AgentsSyncError(`no brief at ${briefPath}`);
7169
+ if (SELF_CONTAINED_ROLES.has(role)) return readFileSync9(briefPath, "utf8");
7170
+ const commonPath = join11(promptsDir, "personas", "common.md");
7171
+ if (!existsSync10(commonPath)) return readFileSync9(briefPath, "utf8");
7172
+ return `${readFileSync9(commonPath, "utf8")}${readFileSync9(briefPath, "utf8")}`;
7063
7173
  }
7064
7174
  async function resolveAgentId(route, role, opts) {
7065
7175
  const cached = route.resolved?.agentPersonas?.[role]?.agentId;
@@ -7383,7 +7493,7 @@ function describeCrewLink(o) {
7383
7493
  var SkillSyncError = class extends Error {
7384
7494
  };
7385
7495
  function parseSkillFile(path) {
7386
- const text = readFileSync9(path, "utf8");
7496
+ const text = readFileSync10(path, "utf8");
7387
7497
  const match = /^---\n([\s\S]*?)\n---\n([\s\S]*)$/.exec(text);
7388
7498
  if (!match) throw new SkillSyncError(`${path}: missing YAML frontmatter (expected a leading --- block)`);
7389
7499
  const frontmatter = match[1];
@@ -7396,9 +7506,9 @@ function parseSkillFile(path) {
7396
7506
  return { name: meta.name, description: meta.description, prompt: body, file: path };
7397
7507
  }
7398
7508
  function listLocalSkills(promptsDir) {
7399
- const dir = join11(promptsDir, "skills");
7400
- if (!existsSync10(dir)) return [];
7401
- return readdirSync2(dir).filter((f) => f.endsWith(".md")).map((f) => parseSkillFile(join11(dir, f)));
7509
+ const dir = join12(promptsDir, "skills");
7510
+ if (!existsSync11(dir)) return [];
7511
+ return readdirSync3(dir).filter((f) => f.endsWith(".md")).map((f) => parseSkillFile(join12(dir, f)));
7402
7512
  }
7403
7513
  async function syncSkills(route, opts) {
7404
7514
  if (!route.resolved) {
@@ -7590,23 +7700,23 @@ var AgentError = class extends Error {
7590
7700
  var AgentSpawnError = class extends AgentError {
7591
7701
  };
7592
7702
  function planAgentRun(o) {
7593
- const briefPath = join12(o.route.promptsDir, "personas", `lane-${o.role}.md`);
7594
- if (!existsSync11(briefPath)) {
7703
+ const briefPath = join13(o.route.promptsDir, "personas", `lane-${o.role}.md`);
7704
+ if (!existsSync12(briefPath)) {
7595
7705
  throw new AgentError(`no brief at ${briefPath} \u2014 refusing to run an unscoped session`);
7596
7706
  }
7597
- const commonPath = join12(o.route.promptsDir, "personas", "common.md");
7598
- const usesCommon = !SELF_CONTAINED_ROLES.has(o.role) && existsSync11(commonPath);
7707
+ const commonPath = join13(o.route.promptsDir, "personas", "common.md");
7708
+ const usesCommon = !SELF_CONTAINED_ROLES.has(o.role) && existsSync12(commonPath);
7599
7709
  const now = o.now ?? (() => Date.now());
7600
7710
  const routeLabel = routeSlug(o.route.route);
7601
- const digestPath2 = join12(o.stateDir, `digest-${routeLabel}-${o.role}.md`);
7711
+ const digestPath2 = join13(o.stateDir, `digest-${routeLabel}-${o.role}.md`);
7602
7712
  let digest;
7603
7713
  let ageSeconds;
7604
- if (existsSync11(digestPath2)) {
7714
+ if (existsSync12(digestPath2)) {
7605
7715
  ageSeconds = Math.floor((now() - statSync5(digestPath2).mtimeMs) / 1e3);
7606
- if (ageSeconds <= DIGEST_MAX_AGE_SECONDS) digest = readFileSync10(digestPath2, "utf8");
7716
+ if (ageSeconds <= DIGEST_MAX_AGE_SECONDS) digest = readFileSync11(digestPath2, "utf8");
7607
7717
  }
7608
- const common = usesCommon ? readFileSync10(commonPath, "utf8") : "";
7609
- const brief = readFileSync10(briefPath, "utf8");
7718
+ const common = usesCommon ? readFileSync11(commonPath, "utf8") : "";
7719
+ const brief = readFileSync11(briefPath, "utf8");
7610
7720
  const effectiveCommon = o.divergedPrompt ? "" : common;
7611
7721
  const effectiveBrief = o.divergedPrompt ?? brief;
7612
7722
  const localSkills = new Map(listLocalSkills(o.route.promptsDir).map((s) => [s.name, s.prompt]));
@@ -7628,7 +7738,7 @@ function planAgentRun(o) {
7628
7738
  digest
7629
7739
  });
7630
7740
  const promptSha = createHash2("sha256").update(expanded.text).digest("hex");
7631
- const streamsDir = join12(o.stateDir, "streams");
7741
+ const streamsDir = join13(o.stateDir, "streams");
7632
7742
  const base = `${routeLabel}-${o.role}-${o.cycle}`;
7633
7743
  const seat = resolveSeat(o.ship, o.route, o.role, o.agentPersona ?? (o.agentModel ? { model: o.agentModel } : {}), o.repo);
7634
7744
  const { toolchain, model } = seat;
@@ -7684,8 +7794,8 @@ function planAgentRun(o) {
7684
7794
  digestAttached: digest !== void 0,
7685
7795
  digestAgeSeconds: ageSeconds,
7686
7796
  model,
7687
- streamPath: join12(streamsDir, `${base}.jsonl`),
7688
- eventsPath: join12(streamsDir, `${base}.events.jsonl`),
7797
+ streamPath: join13(streamsDir, `${base}.jsonl`),
7798
+ eventsPath: join13(streamsDir, `${base}.events.jsonl`),
7689
7799
  ticket: o.ticket,
7690
7800
  agentLog,
7691
7801
  promptVersion: o.route.resolved?.agentPersonas?.[o.role]?.historyId,
@@ -7695,7 +7805,7 @@ function planAgentRun(o) {
7695
7805
  function describePlan(p) {
7696
7806
  return [
7697
7807
  `role: ${p.role}`,
7698
- ...p.resolution ? [`resolved: toolchain ${p.toolchain} \u2190 ${p.resolution.toolchain}; model ${p.model} \u2190 ${p.resolution.model}`] : [],
7808
+ ...p.resolution ? [`resolved: toolchain ${p.toolchain} \u2190 ${p.resolution.toolchain}; model ${p.model} \u2190 ${p.resolution.model}${p.provider ? `; provider ${p.provider}` : ""}`] : [],
7699
7809
  ...(p.warnings ?? []).map((w) => `warning: ${w}`),
7700
7810
  // Only for a non-default toolchain, so a plain Claude run's output is unchanged.
7701
7811
  ...p.toolchain && p.toolchain !== "claude" ? [`toolchain: ${p.toolchain} (${p.harness ?? "claude"})`] : [],
@@ -7891,13 +8001,13 @@ function spawnAgent(plan, emit) {
7891
8001
  }
7892
8002
 
7893
8003
  // src/run-worktree.ts
7894
- import { existsSync as existsSync13 } from "fs";
7895
- import { dirname as dirname7, join as join14 } from "path";
8004
+ import { existsSync as existsSync14 } from "fs";
8005
+ import { dirname as dirname7, join as join15 } from "path";
7896
8006
 
7897
8007
  // src/ports.ts
7898
8008
  import { execFileSync as execFileSync2 } from "child_process";
7899
- import { existsSync as existsSync12 } from "fs";
7900
- import { join as join13 } from "path";
8009
+ import { existsSync as existsSync13 } from "fs";
8010
+ import { join as join14 } from "path";
7901
8011
  var run = (cmd, args) => {
7902
8012
  try {
7903
8013
  return execFileSync2(cmd, args, { encoding: "utf8", stdio: ["ignore", "pipe", "ignore"] });
@@ -7942,7 +8052,7 @@ function ticketForPort(port, s = DEFAULT_PORTS) {
7942
8052
  }
7943
8053
  function worktreeExists(parent, prefix, n) {
7944
8054
  for (const name of [`${prefix}${n}`, `${prefix}${String(n).padStart(3, "0")}`]) {
7945
- if (existsSync12(join13(parent, name, ".git"))) return true;
8055
+ if (existsSync13(join14(parent, name, ".git"))) return true;
7946
8056
  }
7947
8057
  return false;
7948
8058
  }
@@ -8016,7 +8126,7 @@ function planRunWorktree(dir, cfg, t, role) {
8016
8126
  const existing = worktreeForNumber(dir, num);
8017
8127
  const located = locateBranchForTicket(dir, cfg, t, role);
8018
8128
  const baseRef = hasRemote ? `${remote}/${base}` : base;
8019
- if (existing && !existing.prunable && existsSync13(existing.path)) {
8129
+ if (existing && !existing.prunable && existsSync14(existing.path)) {
8020
8130
  const branch = existing.branch;
8021
8131
  if (status(existing.path).length > 0) return { action: "leave-dirty", path: existing.path, branch };
8022
8132
  if (!branch) return { action: "leave-unpushed", path: existing.path, branch: "(detached HEAD)" };
@@ -8030,7 +8140,7 @@ function planRunWorktree(dir, cfg, t, role) {
8030
8140
  if (countCommits(existing.path, `${baseRef}..HEAD`) > 0) return { action: "leave-unpushed", path: existing.path, branch };
8031
8141
  return { action: "recut", path: existing.path, branch, base: baseRef };
8032
8142
  }
8033
- const path = join14(dirname7(dir), worktreeForTicket(dir, cfg, t, role));
8143
+ const path = join15(dirname7(dir), worktreeForTicket(dir, cfg, t, role));
8034
8144
  if (located?.where === "remote" || located && hasRemote && remoteBranchExists(dir, remote, located.branch)) {
8035
8145
  return { action: "cut-remote", path, branch: located.branch };
8036
8146
  }
@@ -8077,7 +8187,7 @@ function worktreeHoldsUnpushed(path, remote) {
8077
8187
  }
8078
8188
  async function finishRunWorktree(dir, cfg, branch) {
8079
8189
  const wt = worktrees(dir).find((w) => w.branch === branch && !w.prunable && w.path !== dir);
8080
- if (!wt || !existsSync13(wt.path)) return { kind: "none" };
8190
+ if (!wt || !existsSync14(wt.path)) return { kind: "none" };
8081
8191
  if (cfg.hooks.handoff) return { kind: "kept-handoff", path: wt.path };
8082
8192
  if (status(wt.path).length > 0) return { kind: "kept-dirty", path: wt.path };
8083
8193
  if (remoteConfigured(dir, cfg.branch.remote) && worktreeHoldsUnpushed(wt.path, cfg.branch.remote)) {
@@ -8089,7 +8199,7 @@ async function finishRunWorktree(dir, cfg, branch) {
8089
8199
  if (pids.length) await killGently(pids);
8090
8200
  }
8091
8201
  gitOk(dir, ["worktree", "remove", wt.path]);
8092
- return existsSync13(wt.path) ? { kind: "kept-dirty", path: wt.path } : { kind: "removed", path: wt.path };
8202
+ return existsSync14(wt.path) ? { kind: "kept-dirty", path: wt.path } : { kind: "removed", path: wt.path };
8093
8203
  }
8094
8204
 
8095
8205
  // src/handoff-guard.ts
@@ -8238,13 +8348,13 @@ async function retireRunWorktree(tracker, dir, cfg, t, role, seatId, shipName2,
8238
8348
  }
8239
8349
 
8240
8350
  // src/connect-wizard.ts
8241
- import { delimiter, dirname as dirname8, join as join15 } from "path";
8242
- import { accessSync as accessSync2, constants as constants2, existsSync as existsSync14, mkdirSync as mkdirSync7, writeFileSync as writeFileSync8 } from "fs";
8351
+ import { delimiter, dirname as dirname8, join as join16 } from "path";
8352
+ import { accessSync as accessSync2, constants as constants2, existsSync as existsSync15, mkdirSync as mkdirSync7, writeFileSync as writeFileSync8 } from "fs";
8243
8353
  import { stringify } from "yaml";
8244
8354
  var HOST_PASSENGERS_DOCS_URL = "https://crew.tablation.dev/host-passengers";
8245
8355
  function shouldRunWizard(opts) {
8246
8356
  if (!opts.isTTY) return false;
8247
- const exists = opts.configExists ?? existsSync14;
8357
+ const exists = opts.configExists ?? existsSync15;
8248
8358
  return !configSearchPath(opts.crewHome).some((p) => exists(p));
8249
8359
  }
8250
8360
  function firstRunConfigPath() {
@@ -8261,7 +8371,7 @@ function defaultIsExecutable(p) {
8261
8371
  function findOnPath2(name, pathEnv, isExecutable = defaultIsExecutable) {
8262
8372
  for (const dir of pathEnv.split(delimiter)) {
8263
8373
  if (!dir) continue;
8264
- const candidate = join15(dir, name);
8374
+ const candidate = join16(dir, name);
8265
8375
  if (isExecutable(candidate)) return candidate;
8266
8376
  }
8267
8377
  return void 0;
@@ -8356,8 +8466,8 @@ Wrote ${o.configPath}.
8356
8466
  }
8357
8467
 
8358
8468
  // src/release-run.ts
8359
- import { readFileSync as readFileSync11, writeFileSync as writeFileSync9, existsSync as existsSync15, copyFileSync, mkdirSync as mkdirSync8, mkdtempSync, rmSync as rmSync6 } from "fs";
8360
- import { dirname as dirname9, join as join16 } from "path";
8469
+ import { readFileSync as readFileSync12, writeFileSync as writeFileSync9, existsSync as existsSync16, copyFileSync, mkdirSync as mkdirSync8, mkdtempSync, rmSync as rmSync6 } from "fs";
8470
+ import { dirname as dirname9, join as join17 } from "path";
8361
8471
  import { tmpdir as tmpdir2 } from "os";
8362
8472
 
8363
8473
  // src/release.ts
@@ -8421,7 +8531,7 @@ function checkGuards(cwd, base = "main") {
8421
8531
  }
8422
8532
  function heldByBlockers(tickets, verifiedStatus, info) {
8423
8533
  if (!info) return [];
8424
- return tickets.filter((t) => t.status === verifiedStatus && unresolvedBlockers(t, info).length > 0).map((ticket) => ({ ticket, blockers: describeUnresolved(ticket, info) }));
8534
+ return tickets.filter((t) => t.status === verifiedStatus && unresolvedBlockers(t, info, { mergeGated: false }).length > 0).map((ticket) => ({ ticket, blockers: describeUnresolved(ticket, info, { mergeGated: false }) }));
8425
8535
  }
8426
8536
  var describeHeld = (held) => held.map((h) => `${h.ticket.issue_tag ?? h.ticket.issue_id} held: blocked by ${h.blockers}`);
8427
8537
  var CHANGELOG_LINE = /^Changelog:[ \t]*(.*)$/gm;
@@ -9015,13 +9125,13 @@ async function bump(o, size) {
9015
9125
  }
9016
9126
  return printed;
9017
9127
  }
9018
- const files = o.repo.release.versionFiles.map((f) => join16(o.cwd, f));
9128
+ const files = o.repo.release.versionFiles.map((f) => join17(o.cwd, f));
9019
9129
  const first = files[0];
9020
- if (!existsSync15(first)) {
9130
+ if (!existsSync16(first)) {
9021
9131
  o.emit.error(`no ${o.repo.release.versionFiles[0]} to read a version from`);
9022
9132
  return null;
9023
9133
  }
9024
- const current = JSON.parse(readFileSync11(first, "utf8")).version;
9134
+ const current = JSON.parse(readFileSync12(first, "utf8")).version;
9025
9135
  if (!current) {
9026
9136
  o.emit.error(`no version field in ${o.repo.release.versionFiles[0]}`);
9027
9137
  return null;
@@ -9034,7 +9144,7 @@ async function bump(o, size) {
9034
9144
  return version;
9035
9145
  }
9036
9146
  for (const f of files) {
9037
- const src = readFileSync11(f, "utf8");
9147
+ const src = readFileSync12(f, "utf8");
9038
9148
  const out = src.replace(/("version"\s*:\s*")[^"]*(")/, `$1${version}$2`);
9039
9149
  if (out === src) {
9040
9150
  o.emit.error(`no version field in ${f}`);
@@ -9047,13 +9157,13 @@ async function bump(o, size) {
9047
9157
  function writeChangelog(o, version, entries) {
9048
9158
  const file = o.repo.release.changelog;
9049
9159
  if (!file || entries.length === 0) return;
9050
- const path = join16(o.cwd, file);
9160
+ const path = join17(o.cwd, file);
9051
9161
  const section = renderChangelogSection(version, (/* @__PURE__ */ new Date()).toISOString().slice(0, 10), entries);
9052
9162
  if (o.dryRun) {
9053
9163
  o.emit.emit(`would prepend ${entries.length} entr(ies) to ${file}`);
9054
9164
  return;
9055
9165
  }
9056
- const existing = existsSync15(path) ? readFileSync11(path, "utf8") : "# Changelog\n";
9166
+ const existing = existsSync16(path) ? readFileSync12(path, "utf8") : "# Changelog\n";
9057
9167
  writeFileSync9(path, insertChangelogSection(existing, section));
9058
9168
  }
9059
9169
  async function confirm(o, expected) {
@@ -9135,8 +9245,8 @@ async function confirmLastRelease(o, decision) {
9135
9245
  function openReleaseCheckout(o) {
9136
9246
  const { remote, base } = o.repo.branch;
9137
9247
  const branch = `crew/release-${Date.now()}`;
9138
- const root = mkdtempSync(join16(tmpdir2(), "crew-release-"));
9139
- const dir = join16(root, "checkout");
9248
+ const root = mkdtempSync(join17(tmpdir2(), "crew-release-"));
9249
+ const dir = join17(root, "checkout");
9140
9250
  const close = () => {
9141
9251
  gitOk(o.cwd, ["worktree", "remove", "--force", dir]);
9142
9252
  rmSync6(root, { recursive: true, force: true });
@@ -9150,10 +9260,10 @@ function openReleaseCheckout(o) {
9150
9260
  throw e;
9151
9261
  }
9152
9262
  for (const f of o.repo.worktrees.copy) {
9153
- const src = join16(o.cwd, f);
9154
- if (!existsSync15(src)) continue;
9155
- mkdirSync8(dirname9(join16(dir, f)), { recursive: true });
9156
- copyFileSync(src, join16(dir, f));
9263
+ const src = join17(o.cwd, f);
9264
+ if (!existsSync16(src)) continue;
9265
+ mkdirSync8(dirname9(join17(dir, f)), { recursive: true });
9266
+ copyFileSync(src, join17(dir, f));
9157
9267
  }
9158
9268
  return { dir, branch, close };
9159
9269
  }
@@ -9622,7 +9732,7 @@ function missingReleaserWarning(repo, release, ships, now = Date.now()) {
9622
9732
  }
9623
9733
 
9624
9734
  // src/watch.ts
9625
- import { watch as watchFile, existsSync as existsSync16, statSync as statSync6, openSync as openSync3, readSync, closeSync as closeSync3 } from "fs";
9735
+ import { watch as watchFile, existsSync as existsSync17, statSync as statSync6, openSync as openSync3, readSync, closeSync as closeSync3 } from "fs";
9626
9736
  var E = "\x1B[";
9627
9737
  var dim = (s) => `${E}2m${s}${E}0m`;
9628
9738
  var bold = (s) => `${E}1m${s}${E}0m`;
@@ -9723,7 +9833,7 @@ function renderHeaders(states, now = Date.now()) {
9723
9833
  return states.map((s) => renderHeader(s, now)).join("");
9724
9834
  }
9725
9835
  function readFrom(file, from) {
9726
- if (!existsSync16(file)) return { events: [], offset: from };
9836
+ if (!existsSync17(file)) return { events: [], offset: from };
9727
9837
  const size = statSync6(file).size;
9728
9838
  if (size < from) return readFrom(file, 0);
9729
9839
  if (size === from) return { events: [], offset: from };
@@ -10434,7 +10544,7 @@ async function showLogEntry(route, entryId, opts = {}) {
10434
10544
  }
10435
10545
 
10436
10546
  // src/repos-cmd.ts
10437
- import { homedir as homedir6 } from "os";
10547
+ import { homedir as homedir7 } from "os";
10438
10548
  import { basename as basename7, resolve as resolve3 } from "path";
10439
10549
  import { isMap, isSeq, parseDocument, stringify as stringify2 } from "yaml";
10440
10550
  var ReposError = class extends Error {
@@ -10481,7 +10591,7 @@ function parseAddArgs(args, routes, isDir) {
10481
10591
  return { route, path: "." };
10482
10592
  }
10483
10593
  function expandDir(p, base) {
10484
- const home = p === "~" ? homedir6() : p.startsWith("~/") ? `${homedir6()}/${p.slice(2)}` : p;
10594
+ const home = p === "~" ? homedir7() : p.startsWith("~/") ? `${homedir7()}/${p.slice(2)}` : p;
10485
10595
  return resolve3(base, home);
10486
10596
  }
10487
10597
  function routeMapOf(doc, route) {
@@ -10616,7 +10726,7 @@ function schedulerInstalled(plan, exists, crontab) {
10616
10726
  }
10617
10727
 
10618
10728
  // src/worktree-sweep.ts
10619
- import { existsSync as existsSync17 } from "fs";
10729
+ import { existsSync as existsSync18 } from "fs";
10620
10730
  import { basename as basename8 } from "path";
10621
10731
  function planWorktreeSweep(target, tickets, contract) {
10622
10732
  const terminal = new Set(contract.statuses.resolved.filter((s) => s !== contract.statuses.verified));
@@ -10644,7 +10754,7 @@ async function applyWorktreeSweep(cwd, actions, dryRun, log, remote = "origin")
10644
10754
  let removed = 0;
10645
10755
  let keptBranches = 0;
10646
10756
  for (const a of actions) {
10647
- if (!dryRun && existsSync17(a.path)) {
10757
+ if (!dryRun && existsSync18(a.path)) {
10648
10758
  let dirty = false;
10649
10759
  try {
10650
10760
  dirty = status(a.path).length > 0;
@@ -10658,7 +10768,7 @@ async function applyWorktreeSweep(cwd, actions, dryRun, log, remote = "origin")
10658
10768
  continue;
10659
10769
  }
10660
10770
  }
10661
- if (!dryRun && a.requirePushed && a.branch && existsSync17(a.path) && remoteConfigured(cwd, remote) && worktreeHoldsUnpushed(a.path, remote)) {
10771
+ if (!dryRun && a.requirePushed && a.branch && existsSync18(a.path) && remoteConfigured(cwd, remote) && worktreeHoldsUnpushed(a.path, remote)) {
10662
10772
  log.warn(
10663
10773
  `not removing worktree ${a.path} for ${a.ticket.issue_id} (${a.ticket.status}): ${a.branch} has commits that are not on ${remote}`,
10664
10774
  { ticket: a.ticket.issue_id, step: "worktree" }
@@ -10739,14 +10849,14 @@ function applyRemoteBranchCleanup(cwd, remote, branches2, dryRun, log) {
10739
10849
  }
10740
10850
 
10741
10851
  // src/stream-sweep.ts
10742
- import { existsSync as existsSync18, readdirSync as readdirSync3, rmSync as rmSync7, statSync as statSync7 } from "fs";
10743
- import { join as join17 } from "path";
10852
+ import { existsSync as existsSync19, readdirSync as readdirSync4, rmSync as rmSync7, statSync as statSync7 } from "fs";
10853
+ import { join as join18 } from "path";
10744
10854
  var DAY_MS2 = 24 * 60 * 60 * 1e3;
10745
10855
  function planStreamSweep(stateDir, retentionDays, now = Date.now()) {
10746
- const dir = join17(stateDir, "streams");
10747
- if (!existsSync18(dir)) return [];
10856
+ const dir = join18(stateDir, "streams");
10857
+ if (!existsSync19(dir)) return [];
10748
10858
  const cutoff = now - retentionDays * DAY_MS2;
10749
- return readdirSync3(dir).map((name) => join17(dir, name)).filter((path) => {
10859
+ return readdirSync4(dir).map((name) => join18(dir, name)).filter((path) => {
10750
10860
  try {
10751
10861
  return statSync7(path).mtimeMs < cutoff;
10752
10862
  } catch {
@@ -10772,9 +10882,9 @@ function applyStreamSweep(paths, dryRun, log) {
10772
10882
  return removed;
10773
10883
  }
10774
10884
  function planEventsSweep(stateDir, retentionDays, now = Date.now()) {
10775
- if (!existsSync18(stateDir)) return [];
10885
+ if (!existsSync19(stateDir)) return [];
10776
10886
  const cutoff = now - retentionDays * DAY_MS2;
10777
- return readdirSync3(stateDir).filter((name) => ROTATED_EVENTS_RE.test(name)).map((name) => join17(stateDir, name)).filter((path) => {
10887
+ return readdirSync4(stateDir).filter((name) => ROTATED_EVENTS_RE.test(name)).map((name) => join18(stateDir, name)).filter((path) => {
10778
10888
  try {
10779
10889
  return statSync7(path).mtimeMs < cutoff;
10780
10890
  } catch {
@@ -10784,23 +10894,23 @@ function planEventsSweep(stateDir, retentionDays, now = Date.now()) {
10784
10894
  }
10785
10895
 
10786
10896
  // src/cli.ts
10787
- 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";
10897
+ import { readFileSync as readFileSync17, existsSync as existsSync24, mkdirSync as mkdirSync13, writeFileSync as writeFileSync13, unlinkSync as unlinkSync5, copyFileSync as copyFileSync2, renameSync as renameSync2, statSync as statSync9 } from "fs";
10788
10898
  import { createInterface } from "readline/promises";
10789
10899
  import { dirname as dirOf, resolve as resolvePath } from "path";
10790
10900
 
10791
10901
  // src/ssh-keys.ts
10792
10902
  import { execFileSync as execFileSync3 } from "child_process";
10793
- import { existsSync as existsSync19, mkdirSync as mkdirSync9, readFileSync as readFileSync12, chmodSync } from "fs";
10794
- import { dirname as dirname11, join as join18 } from "path";
10903
+ import { existsSync as existsSync20, mkdirSync as mkdirSync9, readFileSync as readFileSync13, chmodSync } from "fs";
10904
+ import { dirname as dirname11, join as join19 } from "path";
10795
10905
  var SshKeyError = class extends Error {
10796
10906
  };
10797
10907
  function shipSshKeyPathFor(stateDir) {
10798
- return join18(stateDir, "ssh", "id_ed25519");
10908
+ return join19(stateDir, "ssh", "id_ed25519");
10799
10909
  }
10800
10910
  function ensureShipSshKeypair(stateDir) {
10801
10911
  const privateKeyPath = shipSshKeyPathFor(stateDir);
10802
10912
  const publicKeyPath = `${privateKeyPath}.pub`;
10803
- if (!existsSync19(privateKeyPath)) {
10913
+ if (!existsSync20(privateKeyPath)) {
10804
10914
  mkdirSync9(dirname11(privateKeyPath), { recursive: true });
10805
10915
  try {
10806
10916
  execFileSync3("ssh-keygen", [
@@ -10821,10 +10931,10 @@ function ensureShipSshKeypair(stateDir) {
10821
10931
  }
10822
10932
  chmodSync(privateKeyPath, 384);
10823
10933
  }
10824
- if (!existsSync19(publicKeyPath)) {
10934
+ if (!existsSync20(publicKeyPath)) {
10825
10935
  throw new SshKeyError(`${privateKeyPath} exists but ${publicKeyPath} does not \u2014 regenerate or restore it`);
10826
10936
  }
10827
- const publicKey = readFileSync12(publicKeyPath, "utf8").trim();
10937
+ const publicKey = readFileSync13(publicKeyPath, "utf8").trim();
10828
10938
  return { privateKeyPath, publicKeyPath, publicKey };
10829
10939
  }
10830
10940
  function sshKeygenAvailable() {
@@ -11038,9 +11148,9 @@ function syncAllPassengerCheckouts(cfg) {
11038
11148
 
11039
11149
  // src/tunnel.ts
11040
11150
  import { spawn as spawn3 } from "child_process";
11041
- import { existsSync as existsSync20, mkdirSync as mkdirSync10, readFileSync as readFileSync13, writeFileSync as writeFileSync10, unlinkSync as unlinkSync3, readdirSync as readdirSync4 } from "fs";
11151
+ import { existsSync as existsSync21, mkdirSync as mkdirSync10, readFileSync as readFileSync14, writeFileSync as writeFileSync10, unlinkSync as unlinkSync3, readdirSync as readdirSync5 } from "fs";
11042
11152
  import { randomInt } from "crypto";
11043
- import { join as join19 } from "path";
11153
+ import { join as join20 } from "path";
11044
11154
  var DEFAULT_SPAWN_OPTIONS = { stdio: "ignore", detached: true };
11045
11155
  var defaultSpawn = (cmd, args) => spawn3(cmd, args, DEFAULT_SPAWN_OPTIONS);
11046
11156
  var SLUG_ALPHABET = "abcdefghijklmnopqrstuvwxyz0123456789";
@@ -11107,20 +11217,20 @@ function startTunnel(opts) {
11107
11217
  };
11108
11218
  }
11109
11219
  function tunnelStatePath(stateDir, workspaceId) {
11110
- return join19(stateDir, "passengers", `${workspaceId}.json`);
11220
+ return join20(stateDir, "passengers", `${workspaceId}.json`);
11111
11221
  }
11112
11222
  function readPersistedTunnel(stateDir, workspaceId) {
11113
11223
  const path = tunnelStatePath(stateDir, workspaceId);
11114
- if (!existsSync20(path)) return void 0;
11224
+ if (!existsSync21(path)) return void 0;
11115
11225
  try {
11116
- return JSON.parse(readFileSync13(path, "utf8"));
11226
+ return JSON.parse(readFileSync14(path, "utf8"));
11117
11227
  } catch {
11118
11228
  return void 0;
11119
11229
  }
11120
11230
  }
11121
11231
  function writePersistedTunnel(stateDir, state) {
11122
11232
  const path = tunnelStatePath(stateDir, state.workspaceId);
11123
- mkdirSync10(join19(stateDir, "passengers"), { recursive: true });
11233
+ mkdirSync10(join20(stateDir, "passengers"), { recursive: true });
11124
11234
  writeFileSync10(path, `${JSON.stringify(state, null, 2)}
11125
11235
  `);
11126
11236
  }
@@ -11218,9 +11328,9 @@ function syncPassengerTunnels(plans, relayHost, relayPort, privateKeyPath, state
11218
11328
  }
11219
11329
  }
11220
11330
  function readAllPersistedWorkspaceIds(stateDir) {
11221
- const dir = join19(stateDir, "passengers");
11222
- if (!existsSync20(dir)) return [];
11223
- return readdirSync4(dir).filter((f) => f.endsWith(".json") && !f.endsWith("-sync.json")).map((f) => f.slice(0, -".json".length));
11331
+ const dir = join20(stateDir, "passengers");
11332
+ if (!existsSync21(dir)) return [];
11333
+ return readdirSync5(dir).filter((f) => f.endsWith(".json") && !f.endsWith("-sync.json")).map((f) => f.slice(0, -".json".length));
11224
11334
  }
11225
11335
 
11226
11336
  // src/mcp-endpoint-registry.ts
@@ -11340,8 +11450,8 @@ async function unregisterEndpoint(workspaceId, info, d) {
11340
11450
  import { createServer as createHttpServer } from "http";
11341
11451
  import { spawn as spawn4 } from "child_process";
11342
11452
  import { randomBytes } from "crypto";
11343
- import { existsSync as existsSync21, mkdirSync as mkdirSync11, readFileSync as readFileSync14, writeFileSync as writeFileSync11, unlinkSync as unlinkSync4, readdirSync as readdirSync5 } from "fs";
11344
- import { join as join20 } from "path";
11453
+ import { existsSync as existsSync22, mkdirSync as mkdirSync11, readFileSync as readFileSync15, writeFileSync as writeFileSync11, unlinkSync as unlinkSync4, readdirSync as readdirSync6 } from "fs";
11454
+ import { join as join21 } from "path";
11345
11455
 
11346
11456
  // src/secret-compare.ts
11347
11457
  import { timingSafeEqual } from "crypto";
@@ -11430,20 +11540,20 @@ var defaultSpawn2 = (cmd, args, env) => {
11430
11540
  return child;
11431
11541
  };
11432
11542
  function syncDaemonStatePath(stateDir, workspaceId) {
11433
- return join20(stateDir, "passengers", `${workspaceId}-sync.json`);
11543
+ return join21(stateDir, "passengers", `${workspaceId}-sync.json`);
11434
11544
  }
11435
11545
  function readPersistedSyncDaemon(stateDir, workspaceId) {
11436
11546
  const path = syncDaemonStatePath(stateDir, workspaceId);
11437
- if (!existsSync21(path)) return void 0;
11547
+ if (!existsSync22(path)) return void 0;
11438
11548
  try {
11439
- return JSON.parse(readFileSync14(path, "utf8"));
11549
+ return JSON.parse(readFileSync15(path, "utf8"));
11440
11550
  } catch {
11441
11551
  return void 0;
11442
11552
  }
11443
11553
  }
11444
11554
  function writePersistedSyncDaemon(stateDir, state) {
11445
11555
  const path = syncDaemonStatePath(stateDir, state.workspaceId);
11446
- mkdirSync11(join20(stateDir, "passengers"), { recursive: true });
11556
+ mkdirSync11(join21(stateDir, "passengers"), { recursive: true });
11447
11557
  writeFileSync11(path, `${JSON.stringify(state, null, 2)}
11448
11558
  `);
11449
11559
  }
@@ -11464,10 +11574,10 @@ function waitForExit(pid, alive, timeoutMs = 5e3) {
11464
11574
  }
11465
11575
  }
11466
11576
  function readAllPersistedSyncWorkspaceIds(stateDir) {
11467
- const dir = join20(stateDir, "passengers");
11468
- if (!existsSync21(dir)) return [];
11577
+ const dir = join21(stateDir, "passengers");
11578
+ if (!existsSync22(dir)) return [];
11469
11579
  const suffix = "-sync.json";
11470
- return readdirSync5(dir).filter((f) => f.endsWith(suffix)).map((f) => f.slice(0, -suffix.length));
11580
+ return readdirSync6(dir).filter((f) => f.endsWith(suffix)).map((f) => f.slice(0, -suffix.length));
11471
11581
  }
11472
11582
  function syncPassengerSyncDaemons(plans, cfg, stateDir, deps = {}) {
11473
11583
  const alive = deps.isPidAlive ?? isPidAlive;
@@ -11522,8 +11632,8 @@ function syncPassengerSyncDaemons(plans, cfg, stateDir, deps = {}) {
11522
11632
  }
11523
11633
 
11524
11634
  // src/daemon.ts
11525
- import { existsSync as existsSync22, mkdirSync as mkdirSync12, readFileSync as readFileSync15, readdirSync as readdirSync6, rmSync as rmSync8, statSync as statSync8, writeFileSync as writeFileSync12 } from "fs";
11526
- import { join as join21 } from "path";
11635
+ import { existsSync as existsSync23, mkdirSync as mkdirSync12, readFileSync as readFileSync16, readdirSync as readdirSync7, rmSync as rmSync8, statSync as statSync8, writeFileSync as writeFileSync12 } from "fs";
11636
+ import { join as join22 } from "path";
11527
11637
  var DEFAULT_FLOOR_MS = 5e3;
11528
11638
  var DEFAULT_CEILING_MS = INTERVAL_SECONDS * 1e3;
11529
11639
  var defaultSleep = (ms2) => new Promise((resolve5) => setTimeout(resolve5, ms2));
@@ -11561,8 +11671,8 @@ function latestMtime(path) {
11561
11671
  if (stat.isFile()) return stat.mtimeMs;
11562
11672
  if (!stat.isDirectory()) return 0;
11563
11673
  let latest = stat.mtimeMs;
11564
- for (const entry of readdirSync6(path, { withFileTypes: true })) {
11565
- latest = Math.max(latest, latestMtime(join21(path, entry.name)));
11674
+ for (const entry of readdirSync7(path, { withFileTypes: true })) {
11675
+ latest = Math.max(latest, latestMtime(join22(path, entry.name)));
11566
11676
  }
11567
11677
  return latest;
11568
11678
  }
@@ -11571,13 +11681,13 @@ function computeTreeSignature(paths) {
11571
11681
  }
11572
11682
  function makeStaleChecker(paths, opts = {}) {
11573
11683
  const initial = computeTreeSignature(paths);
11574
- return () => computeTreeSignature(paths) !== initial || opts.reloadSentinel !== void 0 && existsSync22(opts.reloadSentinel);
11684
+ return () => computeTreeSignature(paths) !== initial || opts.reloadSentinel !== void 0 && existsSync23(opts.reloadSentinel);
11575
11685
  }
11576
11686
  function reloadSentinelFor(stateDir) {
11577
- return join21(stateDir, "reload-requested");
11687
+ return join22(stateDir, "reload-requested");
11578
11688
  }
11579
11689
  function daemonStampFor(stateDir) {
11580
- return join21(stateDir, "daemon.started");
11690
+ return join22(stateDir, "daemon.started");
11581
11691
  }
11582
11692
  function noteDaemonStarted(stateDir, now = Date.now()) {
11583
11693
  mkdirSync12(stateDir, { recursive: true });
@@ -11587,7 +11697,7 @@ function noteDaemonStarted(stateDir, now = Date.now()) {
11587
11697
  }
11588
11698
  function readDaemonStarted(stateDir) {
11589
11699
  try {
11590
- const v = JSON.parse(readFileSync15(daemonStampFor(stateDir), "utf8")).startedAt;
11700
+ const v = JSON.parse(readFileSync16(daemonStampFor(stateDir), "utf8")).startedAt;
11591
11701
  return typeof v === "number" ? v : null;
11592
11702
  } catch {
11593
11703
  return null;
@@ -12177,8 +12287,9 @@ if (command === "passenger-sync-daemon") {
12177
12287
  const FLEET_CAPABLE = /* @__PURE__ */ new Set(["poll", "run", "passengers"]);
12178
12288
  const releaseFleetWide = ["merge", "deploy", "release"].includes(command) && flag("fleet");
12179
12289
  const DAEMON_SUBCOMMANDS = /* @__PURE__ */ new Set(["start", "stop", "status", "restart"]);
12180
- const named = command === "daemon" && DAEMON_SUBCOMMANDS.has(positional[1] ?? "") ? positional[2] : positional[1];
12181
- 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;
12290
+ const worktreeArgs = command === "worktree" ? splitWorktreeArgs(positional, cfg.routes.map((r) => r.route)) : void 0;
12291
+ const named = worktreeArgs ? worktreeArgs.route : command === "daemon" && DAEMON_SUBCOMMANDS.has(positional[1] ?? "") ? positional[2] : positional[1];
12292
+ 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 || worktreeArgs !== void 0 && !named && cfg.routes.length > 1;
12182
12293
  let route;
12183
12294
  try {
12184
12295
  route = fleetWide ? cfg.routes[0] : findRoute(cfg, named);
@@ -12384,8 +12495,8 @@ if (command === "passenger-sync-daemon") {
12384
12495
  }
12385
12496
  if (host === "linux" && hasSystemd) {
12386
12497
  const label = labelFor(CREW_HOME, "run");
12387
- const staleTimer = join22(homedir7(), ".config", "systemd", "user", `${label}.timer`);
12388
- if (existsSync23(staleTimer)) {
12498
+ const staleTimer = join23(homedir8(), ".config", "systemd", "user", `${label}.timer`);
12499
+ if (existsSync24(staleTimer)) {
12389
12500
  process.stdout.write(
12390
12501
  `removing stale run timer from before the persistent-service cutover: ${staleTimer}${dryRun ? " (dry run)" : ""}
12391
12502
  `
@@ -12411,7 +12522,7 @@ if (command === "passenger-sync-daemon") {
12411
12522
  const tracker = new Tracker(c, cfg.ship);
12412
12523
  problems.push(...validateContract(tracker.contract).map((p) => `contract: ${p}`));
12413
12524
  for (const r of await resolvedRepos(c)) {
12414
- if (!existsSync23(r.dir)) problems.push(`repo ${r.name}: ${r.dir} is missing on disk`);
12525
+ if (!existsSync24(r.dir)) problems.push(`repo ${r.name}: ${r.dir} is missing on disk`);
12415
12526
  if (!satisfies(host, r.config.platform)) problems.push(`repo ${r.name}: requires ${r.config.platform}, this host is ${host}`);
12416
12527
  problems.push(...validateEffective(r.config).map((p) => `repo ${r.name}: ${p}`));
12417
12528
  }
@@ -12429,7 +12540,7 @@ if (command === "passenger-sync-daemon") {
12429
12540
  }
12430
12541
  async function doctorFix(o) {
12431
12542
  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))));
12432
- const unitExists = (p) => existsSync23(p);
12543
+ const unitExists = (p) => existsSync24(p);
12433
12544
  const crontab = o.host === "linux" ? spawnSync5("crontab", ["-l"], { encoding: "utf8" }).stdout ?? "" : "";
12434
12545
  const schedulerAbsent = o.host !== "windows" ? !schedulerInstalled(planInstall(cfg.ship, CREW_HOME, o.host, detectSystemd(), "run"), unitExists, crontab) : true;
12435
12546
  const plan = planFix({ routes, schedulerAbsent, host: o.host });
@@ -12446,7 +12557,7 @@ if (command === "passenger-sync-daemon") {
12446
12557
  for (const a of plan.actions) {
12447
12558
  if (a.kind === "enable") {
12448
12559
  if (prompter && !await prompter.confirm(`Route ${a.route} passes every check \u2014 set enabled: true in ${cfg.configFile}?`, false)) continue;
12449
- writeFileSync13(cfg.configFile, planEnable(readFileSync16(cfg.configFile, "utf8"), a.route));
12560
+ writeFileSync13(cfg.configFile, planEnable(readFileSync17(cfg.configFile, "utf8"), a.route));
12450
12561
  enabledSound.add(a.route);
12451
12562
  changed2.push(`enabled route ${a.route} in ${cfg.configFile}`);
12452
12563
  } else {
@@ -13353,7 +13464,7 @@ ${changed2.map((c) => ` - ${c}
13353
13464
  process.exit(1);
13354
13465
  }
13355
13466
  requireArmed("run the daemon loop");
13356
- const watchPaths = isCompiledBinary(import.meta.url) ? [process.execPath] : [join22(CREW_HOME, "src"), join22(CREW_HOME, "dist"), join22(CREW_HOME, "bin")];
13467
+ const watchPaths = isCompiledBinary(import.meta.url) ? [process.execPath] : [join23(CREW_HOME, "src"), join23(CREW_HOME, "dist"), join23(CREW_HOME, "bin")];
13357
13468
  const configPaths = daemonConfigPaths(cfg);
13358
13469
  watchPaths.push(...configPaths);
13359
13470
  noteDaemonStarted(cfg.ship.stateDir);
@@ -13512,7 +13623,7 @@ ${changed2.map((c) => ` - ${c}
13512
13623
  `);
13513
13624
  if (!dryRun) {
13514
13625
  const resolvedPath = resolvedPathFor(cfg.ship.stateDir, target.route);
13515
- const raw = existsSync23(resolvedPath) ? JSON.parse(readFileSync16(resolvedPath, "utf8")) : {};
13626
+ const raw = existsSync24(resolvedPath) ? JSON.parse(readFileSync17(resolvedPath, "utf8")) : {};
13516
13627
  raw.agentPersonas = personaResult.agentPersonas;
13517
13628
  raw.agentSkills = skillResult.agentSkills;
13518
13629
  mkdirSync13(dirOf(resolvedPath), { recursive: true });
@@ -13563,7 +13674,7 @@ ${changed2.map((c) => ` - ${c}
13563
13674
  `);
13564
13675
  if (!dryRun) {
13565
13676
  const resolvedPath = resolvedPathFor(cfg.ship.stateDir, target.route);
13566
- const raw = existsSync23(resolvedPath) ? JSON.parse(readFileSync16(resolvedPath, "utf8")) : {};
13677
+ const raw = existsSync24(resolvedPath) ? JSON.parse(readFileSync17(resolvedPath, "utf8")) : {};
13567
13678
  raw.agentSkills = result.agentSkills;
13568
13679
  mkdirSync13(dirOf(resolvedPath), { recursive: true });
13569
13680
  writeFileSync13(resolvedPath, `${JSON.stringify(raw, null, 2)}
@@ -13579,14 +13690,14 @@ ${changed2.map((c) => ` - ${c}
13579
13690
  process.exit(2);
13580
13691
  };
13581
13692
  if (sub !== "add" && sub !== "list") usage2();
13582
- const configText = readFileSync16(cfg.configFile, "utf8");
13693
+ const configText = readFileSync17(cfg.configFile, "utf8");
13583
13694
  let routeName = positional[2];
13584
13695
  let addPath = ".";
13585
13696
  let addName;
13586
13697
  if (sub === "list" && !routeName) usage2();
13587
13698
  if (sub === "add") {
13588
13699
  try {
13589
- const a = parseAddArgs(positional.slice(2), routeNamesOf(configText), (p) => existsSync23(resolvePath(p.replace(/^~(?=\/|$)/, homedir7()))));
13700
+ const a = parseAddArgs(positional.slice(2), routeNamesOf(configText), (p) => existsSync24(resolvePath(p.replace(/^~(?=\/|$)/, homedir8()))));
13590
13701
  routeName = a.route;
13591
13702
  addPath = a.path;
13592
13703
  addName = a.name;
@@ -13601,9 +13712,9 @@ ${changed2.map((c) => ` - ${c}
13601
13712
  }
13602
13713
  const resolvedFile = resolvedPathFor(cfg.ship.stateDir, routeName);
13603
13714
  let resolvedRemotes;
13604
- if (existsSync23(resolvedFile)) {
13715
+ if (existsSync24(resolvedFile)) {
13605
13716
  try {
13606
- resolvedRemotes = JSON.parse(readFileSync16(resolvedFile, "utf8"));
13717
+ resolvedRemotes = JSON.parse(readFileSync17(resolvedFile, "utf8"));
13607
13718
  } catch {
13608
13719
  }
13609
13720
  }
@@ -13618,14 +13729,14 @@ ${changed2.map((c) => ` - ${c}
13618
13729
  for (const r of repos) {
13619
13730
  const origin2 = originOf(r.dir);
13620
13731
  const row = matchRepoName(origin2, resolvedRemotes);
13621
- const note = !existsSync23(r.dir) ? "missing on disk" : row ? `Repos row: ${row}` : `no Repos row matches ${normalizeRemote(origin2) ?? "this checkout (no origin remote)"}`;
13732
+ const note = !existsSync24(r.dir) ? "missing on disk" : row ? `Repos row: ${row}` : `no Repos row matches ${normalizeRemote(origin2) ?? "this checkout (no origin remote)"}`;
13622
13733
  process.stdout.write(`${r.name} ${r.dir} ${note}
13623
13734
  `);
13624
13735
  }
13625
13736
  break;
13626
13737
  }
13627
- const target = resolvePath(addPath.replace(/^~(?=\/|$)/, homedir7()));
13628
- const top = existsSync23(target) ? gitOk(target, ["rev-parse", "--show-toplevel"]) : null;
13738
+ const target = resolvePath(addPath.replace(/^~(?=\/|$)/, homedir8()));
13739
+ const top = existsSync24(target) ? gitOk(target, ["rev-parse", "--show-toplevel"]) : null;
13629
13740
  if (!top) throw new ReposError(`${target} is not a git checkout`);
13630
13741
  const origin = originOf(top);
13631
13742
  const plan = planRepoAdd({
@@ -13674,8 +13785,8 @@ ${changed2.map((c) => ` - ${c}
13674
13785
  const created = await rowTracker.createRepoRow(rowPayload);
13675
13786
  process.stdout.write(`created Repos row ${String(rowPayload.name)} (${String(rowPayload.remote)}) ${created.id}
13676
13787
  `);
13677
- if (existsSync23(resolvedFile)) {
13678
- const raw = JSON.parse(readFileSync16(resolvedFile, "utf8"));
13788
+ if (existsSync24(resolvedFile)) {
13789
+ const raw = JSON.parse(readFileSync17(resolvedFile, "utf8"));
13679
13790
  raw.repoNames = { ...raw.repoNames, [created.id]: String(rowPayload.name) };
13680
13791
  raw.repoRemotes = { ...raw.repoRemotes, [created.id]: String(rowPayload.remote) };
13681
13792
  writeFileSync13(resolvedFile, `${JSON.stringify(raw, null, 2)}
@@ -13921,10 +14032,10 @@ Signed in as ${result.identity.email} \u2014 minted an API key in workspace "${r
13921
14032
  operator = await pickOperator(found.holds);
13922
14033
  }
13923
14034
  let previous;
13924
- const hadPreviousFile = existsSync23(resolvedPath);
14035
+ const hadPreviousFile = existsSync24(resolvedPath);
13925
14036
  if (hadPreviousFile) {
13926
14037
  try {
13927
- previous = JSON.parse(readFileSync16(resolvedPath, "utf8"));
14038
+ previous = JSON.parse(readFileSync17(resolvedPath, "utf8"));
13928
14039
  } catch {
13929
14040
  }
13930
14041
  }
@@ -14012,7 +14123,7 @@ Signed in as ${result.identity.email} \u2014 minted an API key in workspace "${r
14012
14123
  const keyPersisted = keychainBacked || apiKeyFile !== void 0;
14013
14124
  const wizard = firstRun;
14014
14125
  const writeFirstRun = wizard !== void 0 && !dryRun && keyPersisted;
14015
- const routeDir = value("dir") ?? (writeFirstRun ? join22(DEFAULT_REPOS_BASE_PATH, found.workspaceSlug) : void 0);
14126
+ const routeDir = value("dir") ?? (writeFirstRun ? join23(DEFAULT_REPOS_BASE_PATH, found.workspaceSlug) : void 0);
14016
14127
  const routeBlock = renderConnection(
14017
14128
  found,
14018
14129
  route2,
@@ -14353,21 +14464,36 @@ Signed in as ${result.identity.email} \u2014 minted an API key in workspace "${r
14353
14464
  }
14354
14465
  case "worktree": {
14355
14466
  emit.enter("worktree");
14356
- const rawWt = positional[2] ?? positional[1];
14467
+ const rawWt = worktreeArgs?.ticket;
14357
14468
  const nWt = rawWt ? parseTicketNumber(rawWt) : null;
14358
14469
  if (!nWt) {
14359
14470
  process.stderr.write("worktree: need a ticket number (326, ISSUE-326, or TABL-326)\n");
14360
14471
  process.exit(2);
14361
14472
  }
14362
- const trackerWt = new Tracker(route, cfg.ship);
14363
- const cWt = trackerWt.contract;
14364
- const ticketWt = (await trackerWt.openTickets()).find((t) => t[cWt.columns.key] === `ISSUE-${nWt}`);
14365
- if (!ticketWt) {
14366
- process.stderr.write(`worktree: no open ticket ISSUE-${nWt} on ${route.route}
14473
+ const candidatesWt = !named && cfg.routes.length > 1 ? cfg.routes.filter((r) => r.enabled) : [route];
14474
+ let ticketWt;
14475
+ const lookedWt = [];
14476
+ const hitsWt = /* @__PURE__ */ new Map();
14477
+ for (const cand of candidatesWt) {
14478
+ const tr = new Tracker(cand, cfg.ship);
14479
+ const hit = (await tr.openTickets()).find((t) => t[tr.contract.columns.key] === `ISSUE-${nWt}`);
14480
+ if (hit) hitsWt.set(cand.route, hit);
14481
+ lookedWt.push({
14482
+ route: cand.route,
14483
+ hit: hit ? { repoId: hit.repo_id ?? null } : void 0,
14484
+ servedRepoIds: hit ? (await resolvedRepos(cand)).flatMap((r) => r.id ? [r.id] : []) : []
14485
+ });
14486
+ }
14487
+ const pickedWt = pickWorktreeRoute(Number(nWt), lookedWt);
14488
+ if ("error" in pickedWt) {
14489
+ process.stderr.write(`worktree: ${pickedWt.error}
14367
14490
  `);
14368
14491
  process.exit(2);
14369
14492
  }
14370
- const repoWt = (await resolvedRepos(route)).find((r) => r.id === ticketWt.repo_id) ?? (await resolvedRepos(route))[0];
14493
+ const routeWt = candidatesWt.find((r) => r.route === pickedWt.route) ?? route;
14494
+ ticketWt = hitsWt.get(pickedWt.route);
14495
+ if (!ticketWt) process.exit(2);
14496
+ const repoWt = (await resolvedRepos(routeWt)).find((r) => r.id === ticketWt.repo_id) ?? (await resolvedRepos(routeWt))[0];
14371
14497
  if (!repoWt) {
14372
14498
  process.stderr.write("worktree: this route has no repository\n");
14373
14499
  process.exit(2);
@@ -14577,7 +14703,7 @@ Signed in as ${result.identity.email} \u2014 minted an API key in workspace "${r
14577
14703
  }
14578
14704
  case "log": {
14579
14705
  try {
14580
- const text = readFileSync16(cfg.ship.logFile, "utf8").split("\n");
14706
+ const text = readFileSync17(cfg.ship.logFile, "utf8").split("\n");
14581
14707
  process.stdout.write(`${text.slice(-(Number(value("tail")) || 40)).join("\n")}
14582
14708
  `);
14583
14709
  } catch {
@@ -14787,7 +14913,7 @@ routes: ${cfg.routes.map((c) => c.route).join(", ")}
14787
14913
  );
14788
14914
  try {
14789
14915
  const evFile = eventFileFor(cfg.ship.stateDir);
14790
- const evSize = existsSync23(evFile) ? statSync9(evFile).size : 0;
14916
+ const evSize = existsSync24(evFile) ? statSync9(evFile).size : 0;
14791
14917
  process.stdout.write(
14792
14918
  evSize > cfg.ship.eventsRotateBytes * 4 ? `events log: WARN ${evFile} is ${Math.round(evSize / 1048576)} MB (> 4x eventsRotateBytes); run \`crew reap\` to rotate it
14793
14919
  ` : "events log: ok\n"
@@ -14921,15 +15047,39 @@ crew manifest: ${seats2.length ? seats2.map((c) => c.name).join(", ") : "non
14921
15047
  let allBinsFound = true;
14922
15048
  for (const tc of shipToolchains(cfg.ship)) {
14923
15049
  const adapter = adapterFor(tc.harness);
14924
- const binPath = findOnPath(tc.launcherBin ?? tc.launcher ?? tc.bin, schedulerPath);
15050
+ const runBin = launcherExecutable(tc) ?? tc.bin;
15051
+ const binPath = findOnPath(runBin, schedulerPath);
14925
15052
  if (!binPath) allBinsFound = false;
14926
15053
  const label = tc.name === defaultToolchain ? "agent binary: " : `toolchain ${tc.name}:`.padEnd(18);
14927
15054
  process.stdout.write(
14928
- `${label} ${tc.launcherBin ?? tc.launcher ?? tc.bin} \u2014 ${binPath ? `OK (${binPath})` : "NOT FOUND on the scheduler PATH"}${tc.name === defaultToolchain ? "" : ` [${adapter.displayName}]`}
15055
+ `${label} ${runBin} \u2014 ${binPath ? `OK (${binPath})` : "NOT FOUND on the scheduler PATH"}${tc.name === defaultToolchain ? "" : ` [${adapter.displayName}]`}
14929
15056
  `
14930
15057
  );
14931
15058
  for (const limit of adapter.limitations) process.stdout.write(` cannot enforce: ${limit}
14932
15059
  `);
15060
+ if (isOmlxLauncher(tc)) {
15061
+ const s = readOmlxSettings();
15062
+ if (!s) {
15063
+ process.stdout.write(` omlx: ~/.omlx/settings.json not found or has no server.port \u2014 cannot check the server
15064
+ `);
15065
+ } else {
15066
+ let status2;
15067
+ try {
15068
+ status2 = (await fetch(`http://${s.host}:${s.port}/v1/models`, { signal: AbortSignal.timeout(3e3) })).status;
15069
+ } catch {
15070
+ status2 = void 0;
15071
+ }
15072
+ const probe = interpretOmlxProbe(status2);
15073
+ process.stdout.write(` omlx server: ${s.host}:${s.port} \u2014 ${probe.detail}${probe.up ? "" : " (start oMLX)"}
15074
+ `);
15075
+ process.stdout.write(
15076
+ ` omlx model: ${tc.model} \u2014 ${omlxModelPresent(tc.model, s.modelDirs) ? "present" : `NOT FOUND in ${s.modelDirs.join(", ") || "any model dir"}`}
15077
+ `
15078
+ );
15079
+ }
15080
+ process.stdout.write(` local model: tool-call reliability and prefill speed vary by model
15081
+ `);
15082
+ }
14933
15083
  if (isOriLauncher(tc)) {
14934
15084
  process.stdout.write(` ori: headless auth under the scheduler is unverified \u2014 run \`ori login\` as the scheduler user, then confirm a run records provider openrouter (see docs/toolchains.md)
14935
15085
  `);