@tablation/crew 0.30.1 → 0.32.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.
Files changed (2) hide show
  1. package/dist/cli.js +618 -264
  2. package/package.json +1 -1
package/dist/cli.js CHANGED
@@ -1,15 +1,15 @@
1
1
  #!/usr/bin/env node
2
2
 
3
3
  // src/cli.ts
4
- import { resolve as resolve4, dirname as dirname11, join as join20, basename as basename9 } from "path";
4
+ import { resolve as resolve4, dirname as dirname12, join as join21, basename as basename9 } from "path";
5
5
  import { fileURLToPath } from "url";
6
- import { hostname as hostname2, homedir as homedir5 } from "os";
6
+ import { hostname as hostname2, homedir as homedir6 } from "os";
7
7
  import { spawnSync as spawnSync3 } from "child_process";
8
8
 
9
9
  // src/config.ts
10
- import { readFileSync as readFileSync4, existsSync as existsSync3 } from "fs";
11
- import { resolve, dirname as dirname2, isAbsolute, join as join4, basename as basename3 } from "path";
12
- import { homedir as homedir2, tmpdir } from "os";
10
+ import { readFileSync as readFileSync5, existsSync as existsSync4 } from "fs";
11
+ import { resolve, dirname as dirname3, isAbsolute, join as join5, basename as basename3 } from "path";
12
+ import { homedir as homedir3, tmpdir } from "os";
13
13
  import { parse as parse2 } from "yaml";
14
14
 
15
15
  // src/events.ts
@@ -157,8 +157,10 @@ var Emitter = class _Emitter {
157
157
  };
158
158
  var eventFileFor = (stateDir) => join(stateDir, "events.jsonl");
159
159
 
160
- // src/toolchain.ts
161
- import { basename } from "path";
160
+ // src/pair-install.ts
161
+ import { existsSync as existsSync2, mkdirSync as mkdirSync2, readFileSync as readFileSync3, rmSync as rmSync2, writeFileSync } from "fs";
162
+ import { homedir as homedir2 } from "os";
163
+ import { dirname as dirname2, join as join3 } from "path";
162
164
 
163
165
  // src/stream.ts
164
166
  function toolTarget(name, input) {
@@ -273,6 +275,14 @@ function ensureClaudeMcp(o) {
273
275
  }
274
276
  return `mcp: registered the ${MCP_SERVER_NAME} MCP server with Claude Code \u2192 ${o.url} (user scope, ~/.claude.json)`;
275
277
  }
278
+ function removeClaudeMcp(o) {
279
+ const got = runClaude(o.bin, ["mcp", "get", MCP_SERVER_NAME]);
280
+ if (got.missing) return `mcp: \`${o.bin}\` not found, so Claude Code was not touched`;
281
+ if (got.status !== 0) return `mcp: no ${MCP_SERVER_NAME} server registered with Claude Code`;
282
+ if (o.dryRun) return `(dry run) would remove the ${MCP_SERVER_NAME} MCP server from Claude Code (user scope)`;
283
+ const rm = runClaude(o.bin, ["mcp", "remove", MCP_SERVER_NAME, "--scope", "user"]);
284
+ 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}`})`;
285
+ }
276
286
 
277
287
  // src/capability.ts
278
288
  function profileFor(role) {
@@ -312,6 +322,16 @@ function claudeToolsFor(p) {
312
322
  function allowedTools(role) {
313
323
  return claudeToolsFor(profileFor(role));
314
324
  }
325
+ var claudePair = {
326
+ harness: "claude",
327
+ displayName: "Claude Code",
328
+ installPair: (o) => ensureClaudeMcp({ bin: o.bin ?? "claude", url: o.url, key: o.key ?? "", home: o.home, replace: o.replace, dryRun: o.dryRun }),
329
+ verifyPair(o) {
330
+ const s = inspectClaudeMcp({ bin: o.bin ?? "claude", url: o.url, key: o.key ?? "", home: o.home });
331
+ return s.state === "registered" ? { ok: true, detail: s.url } : s.state === "different" ? { ok: false, detail: s.detail } : { ok: false, detail: s.state === "no-claude" ? "claude binary not found" : "tablation MCP server not registered" };
332
+ },
333
+ removePair: (o) => removeClaudeMcp({ bin: o.bin ?? "claude", dryRun: o.dryRun })
334
+ };
315
335
  var claudeAdapter = {
316
336
  harness: "claude",
317
337
  displayName: "Claude Code",
@@ -365,7 +385,7 @@ var claudeAdapter = {
365
385
  })] : [],
366
386
  ...spec.args
367
387
  ];
368
- return spec.launcher ? { bin: spec.launcher, args: [spec.bin, ...args] } : { bin: spec.bin, args };
388
+ return spec.launcher ? { bin: spec.launcherBin ?? spec.launcher, args: [spec.bin, ...args] } : { bin: spec.bin, args };
369
389
  },
370
390
  runEnv(spec) {
371
391
  return {
@@ -388,7 +408,291 @@ var claudeAdapter = {
388
408
  ensureMcp: ensureClaudeMcp
389
409
  };
390
410
 
411
+ // src/pair-install.ts
412
+ var PAIR_BLOCK_START = "<!-- crew:pair start -->";
413
+ var PAIR_BLOCK_END = "<!-- crew:pair end -->";
414
+ var PAIR_KEY_VAR = "CREW_API_KEY";
415
+ var SERVER = "tablation";
416
+ function pairBlock(harness) {
417
+ return [
418
+ PAIR_BLOCK_START,
419
+ "## Tablation Pair (managed by `crew connect` \u2014 edit nothing between the markers)",
420
+ "",
421
+ "At the start of every session, run `crew agents prompt pair --harness " + harness + "` and follow what it prints.",
422
+ `The Tablation tracker tools come from the \`${SERVER}\` MCP server; its key is read from \`$${PAIR_KEY_VAR}\`.`,
423
+ PAIR_BLOCK_END
424
+ ].join("\n");
425
+ }
426
+ var BLOCK_RE = new RegExp(`${escapeRe(PAIR_BLOCK_START)}[\\s\\S]*?${escapeRe(PAIR_BLOCK_END)}\\n?`);
427
+ function escapeRe(s) {
428
+ return s.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
429
+ }
430
+ function hasPairBlock(text) {
431
+ return BLOCK_RE.test(text);
432
+ }
433
+ function pairBlockIsBroken(text) {
434
+ return !hasPairBlock(text) && (text.includes(PAIR_BLOCK_START) || text.includes(PAIR_BLOCK_END));
435
+ }
436
+ function upsertPairBlock(text, block) {
437
+ if (hasPairBlock(text)) return text.replace(BLOCK_RE, () => `${block}
438
+ `);
439
+ if (text.trim() === "") return `${block}
440
+ `;
441
+ return `${text.replace(/\n*$/, "")}
442
+
443
+ ${block}
444
+ `;
445
+ }
446
+ function removePairBlock(text) {
447
+ if (!hasPairBlock(text)) return text;
448
+ return text.replace(new RegExp(`\\n*${BLOCK_RE.source}`), "\n").replace(/^\n+/, "").replace(/\n{3,}/g, "\n\n");
449
+ }
450
+ function readIfExists(path) {
451
+ return existsSync2(path) ? readFileSync3(path, "utf8") : void 0;
452
+ }
453
+ function writeFileMkdir(path, text) {
454
+ mkdirSync2(dirname2(path), { recursive: true });
455
+ writeFileSync(path, text);
456
+ }
457
+ function installBlockFile(path, harness, dryRun) {
458
+ const before = readIfExists(path) ?? "";
459
+ if (pairBlockIsBroken(before)) return { changed: false, error: `${path} has an unmatched ${PAIR_BLOCK_START}/${PAIR_BLOCK_END} marker \u2014 fix it by hand` };
460
+ const after = upsertPairBlock(before, pairBlock(harness));
461
+ if (after === before) return { changed: false };
462
+ if (!dryRun) writeFileMkdir(path, after);
463
+ return { changed: true };
464
+ }
465
+ function removeBlockFile(path, dryRun) {
466
+ const before = readIfExists(path);
467
+ if (before === void 0 || !hasPairBlock(before)) return false;
468
+ if (dryRun) return true;
469
+ const after = removePairBlock(before);
470
+ if (after.trim() === "") rmSync2(path);
471
+ else writeFileSync(path, after);
472
+ return true;
473
+ }
474
+ function blockVerdict(path, harness) {
475
+ const text = readIfExists(path);
476
+ if (text === void 0 || !hasPairBlock(text)) return { ok: false, detail: `no managed block in ${path}` };
477
+ if (!text.includes(pairBlock(harness))) return { ok: false, detail: `the managed block in ${path} is out of date \u2014 run crew connect` };
478
+ return { ok: true, detail: "" };
479
+ }
480
+ var HEADER_RE = /^\s*\[\[?\s*([^\]]+?)\s*\]\]?\s*(#.*)?$/;
481
+ var tableName = (raw) => raw.replace(/["']/g, "").replace(/\s*\.\s*/g, ".");
482
+ function tomlTableRange(lines, name) {
483
+ const start = lines.findIndex((l) => {
484
+ const m = HEADER_RE.exec(l);
485
+ return m !== null && tableName(m[1]) === name;
486
+ });
487
+ if (start < 0) return void 0;
488
+ let end = lines.length;
489
+ for (let i = start + 1; i < lines.length; i++) {
490
+ if (HEADER_RE.test(lines[i])) {
491
+ end = i;
492
+ break;
493
+ }
494
+ }
495
+ while (end > start + 1 && lines[end - 1].trim() === "") end--;
496
+ return [start, end];
497
+ }
498
+ var tomlString = (v) => JSON.stringify(v);
499
+ function codexTable(url) {
500
+ return [`[mcp_servers.${SERVER}]`, `url = ${tomlString(url)}`, `bearer_token_env_var = ${tomlString(PAIR_KEY_VAR)}`];
501
+ }
502
+ function upsertTomlTable(text, name, table2) {
503
+ const lines = text.split("\n");
504
+ const r = tomlTableRange(lines, name);
505
+ if (r) {
506
+ lines.splice(r[0], r[1] - r[0], ...table2);
507
+ return lines.join("\n");
508
+ }
509
+ const body = text.replace(/\n*$/, "");
510
+ return body === "" ? `${table2.join("\n")}
511
+ ` : `${body}
512
+
513
+ ${table2.join("\n")}
514
+ `;
515
+ }
516
+ function removeTomlTable(text, name) {
517
+ const lines = text.split("\n");
518
+ let r = tomlTableRange(lines, name);
519
+ if (!r) return text;
520
+ while (r) {
521
+ let [start, end] = r;
522
+ if (start > 0 && lines[start - 1].trim() === "") start--;
523
+ lines.splice(start, end - start);
524
+ const childName = lines.map((l) => HEADER_RE.exec(l)).map((m) => m ? tableName(m[1]) : void 0).find((n) => n !== void 0 && n.startsWith(`${name}.`));
525
+ r = childName === void 0 ? void 0 : tomlTableRange(lines, childName);
526
+ }
527
+ const out = lines.join("\n").replace(/^\n+/, "");
528
+ return out.trim() === "" ? "" : out.replace(/\n*$/, "\n");
529
+ }
530
+ function tomlValue(lines, range, key) {
531
+ for (let i = range[0] + 1; i < range[1]; i++) {
532
+ const m = new RegExp(`^\\s*${key}\\s*=\\s*"((?:[^"\\\\]|\\\\.)*)"`).exec(lines[i]);
533
+ if (m) return m[1];
534
+ }
535
+ return void 0;
536
+ }
537
+ var codexPaths = (home) => ({
538
+ instructions: join3(home, ".codex", "AGENTS.md"),
539
+ config: join3(home, ".codex", "config.toml")
540
+ });
541
+ function codexMcpState(config, url) {
542
+ const lines = config.split("\n");
543
+ const r = tomlTableRange(lines, `mcp_servers.${SERVER}`);
544
+ if (!r) return { state: "missing" };
545
+ const got = tomlValue(lines, r, "url");
546
+ const env = tomlValue(lines, r, "bearer_token_env_var");
547
+ if (got !== url) return { state: "different", detail: `it points at ${got ?? "(no url)"}, not ${url}` };
548
+ if (env !== PAIR_KEY_VAR) return { state: "different", detail: `it does not read its key from $${PAIR_KEY_VAR}` };
549
+ return { state: "registered" };
550
+ }
551
+ var codexPair = {
552
+ harness: "codex",
553
+ displayName: "Codex",
554
+ installPair(o) {
555
+ const p = codexPaths(o.home ?? homedir2());
556
+ const config = readIfExists(p.config) ?? "";
557
+ const st = codexMcpState(config, o.url);
558
+ if (st.state === "different" && !o.replace) {
559
+ return `pair(codex): left ${p.config} alone \u2014 a "${SERVER}" server exists and ${st.detail}. Re-run with --mcp replace to overwrite it.`;
560
+ }
561
+ const block = installBlockFile(p.instructions, "codex", o.dryRun);
562
+ if (block.error) return `pair(codex): ${block.error}`;
563
+ let mcpChanged = false;
564
+ if (st.state !== "registered") {
565
+ mcpChanged = true;
566
+ if (!o.dryRun) writeFileMkdir(p.config, upsertTomlTable(config, `mcp_servers.${SERVER}`, codexTable(o.url)));
567
+ }
568
+ if (!block.changed && !mcpChanged) return `pair(codex): already installed (${p.instructions}, ${p.config})`;
569
+ 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})`;
570
+ },
571
+ verifyPair(o) {
572
+ const p = codexPaths(o.home ?? homedir2());
573
+ const b = blockVerdict(p.instructions, "codex");
574
+ if (!b.ok) return b;
575
+ const st = codexMcpState(readIfExists(p.config) ?? "", o.url);
576
+ if (st.state === "missing") return { ok: false, detail: `no ${SERVER} server in ${p.config}` };
577
+ if (st.state === "different") return { ok: false, detail: `${SERVER} in ${p.config} ${st.detail}` };
578
+ return { ok: true, detail: `${p.instructions}, ${p.config}` };
579
+ },
580
+ removePair(o) {
581
+ const p = codexPaths(o.home ?? homedir2());
582
+ const hadBlock = removeBlockFile(p.instructions, o.dryRun);
583
+ const config = readIfExists(p.config);
584
+ const st = codexMcpState(config ?? "", o.url);
585
+ const hadMcp = config !== void 0 && st.state === "registered";
586
+ const kept = st.state === "different" ? `; left the existing ${SERVER} server alone \u2014 ${st.detail}` : "";
587
+ if (hadMcp && !o.dryRun) {
588
+ const after = removeTomlTable(config, `mcp_servers.${SERVER}`);
589
+ if (after === "") rmSync2(p.config);
590
+ else writeFileSync(p.config, after);
591
+ }
592
+ const what = hadBlock && hadMcp ? `the Pair block and ${SERVER} server` : hadBlock ? "the Pair block" : `the ${SERVER} server`;
593
+ return hadBlock || hadMcp ? `pair(codex): ${o.dryRun ? "(dry run) would remove" : "removed"} ${what}${kept}` : `pair(codex): nothing to remove${kept}`;
594
+ }
595
+ };
596
+ var geminiPaths = (home) => ({
597
+ instructions: join3(home, ".gemini", "GEMINI.md"),
598
+ config: join3(home, ".gemini", "settings.json")
599
+ });
600
+ function readGeminiSettings(path) {
601
+ const text = readIfExists(path);
602
+ if (text === void 0 || text.trim() === "") return {};
603
+ try {
604
+ const j = JSON.parse(text);
605
+ return j && typeof j === "object" && !Array.isArray(j) ? j : void 0;
606
+ } catch {
607
+ return void 0;
608
+ }
609
+ }
610
+ var geminiEntry = (url) => ({ httpUrl: url, headers: { Authorization: `Bearer $${PAIR_KEY_VAR}` } });
611
+ function geminiMcpState(s, url) {
612
+ const e = s.mcpServers?.[SERVER];
613
+ if (!e) return { state: "missing" };
614
+ if (e.httpUrl !== url) return { state: "different", detail: `it points at ${e.httpUrl ?? "(no httpUrl)"}, not ${url}` };
615
+ if (e.headers?.Authorization !== `Bearer $${PAIR_KEY_VAR}`) return { state: "different", detail: `it does not read its key from $${PAIR_KEY_VAR}` };
616
+ return { state: "registered" };
617
+ }
618
+ var geminiPair = {
619
+ harness: "gemini",
620
+ displayName: "Gemini CLI",
621
+ installPair(o) {
622
+ const p = geminiPaths(o.home ?? homedir2());
623
+ const settings = readGeminiSettings(p.config);
624
+ if (!settings) return `pair(gemini): ${p.config} is not a JSON object \u2014 fix it by hand; nothing was written`;
625
+ const st = geminiMcpState(settings, o.url);
626
+ if (st.state === "different" && !o.replace) {
627
+ return `pair(gemini): left ${p.config} alone \u2014 a "${SERVER}" server exists and ${st.detail}. Re-run with --mcp replace to overwrite it.`;
628
+ }
629
+ const block = installBlockFile(p.instructions, "gemini", o.dryRun);
630
+ if (block.error) return `pair(gemini): ${block.error}`;
631
+ const mcpChanged = st.state !== "registered";
632
+ if (mcpChanged && !o.dryRun) {
633
+ const next = { ...settings, mcpServers: { ...settings.mcpServers, [SERVER]: geminiEntry(o.url) } };
634
+ writeFileMkdir(p.config, `${JSON.stringify(next, null, 2)}
635
+ `);
636
+ }
637
+ if (!block.changed && !mcpChanged) return `pair(gemini): already installed (${p.instructions}, ${p.config})`;
638
+ 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})`;
639
+ },
640
+ verifyPair(o) {
641
+ const p = geminiPaths(o.home ?? homedir2());
642
+ const b = blockVerdict(p.instructions, "gemini");
643
+ if (!b.ok) return b;
644
+ const settings = readGeminiSettings(p.config);
645
+ if (!settings) return { ok: false, detail: `${p.config} is not a JSON object` };
646
+ const st = geminiMcpState(settings, o.url);
647
+ if (st.state === "missing") return { ok: false, detail: `no ${SERVER} server in ${p.config}` };
648
+ if (st.state === "different") return { ok: false, detail: `${SERVER} in ${p.config} ${st.detail}` };
649
+ return { ok: true, detail: `${p.instructions}, ${p.config}` };
650
+ },
651
+ removePair(o) {
652
+ const p = geminiPaths(o.home ?? homedir2());
653
+ const hadBlock = removeBlockFile(p.instructions, o.dryRun);
654
+ const settings = readGeminiSettings(p.config);
655
+ const st = settings ? geminiMcpState(settings, o.url) : { state: "missing" };
656
+ const hadMcp = st.state === "registered";
657
+ const kept = st.state === "different" ? `; left the existing ${SERVER} server alone \u2014 ${st.detail}` : "";
658
+ if (hadMcp && settings && !o.dryRun) {
659
+ const { [SERVER]: _gone, ...rest } = settings.mcpServers ?? {};
660
+ const next = { ...settings };
661
+ if (Object.keys(rest).length > 0) next.mcpServers = rest;
662
+ else delete next.mcpServers;
663
+ if (Object.keys(next).length === 0) rmSync2(p.config);
664
+ else writeFileSync(p.config, `${JSON.stringify(next, null, 2)}
665
+ `);
666
+ }
667
+ const what = hadBlock && hadMcp ? `the Pair block and ${SERVER} server` : hadBlock ? "the Pair block" : `the ${SERVER} server`;
668
+ return hadBlock || hadMcp ? `pair(gemini): ${o.dryRun ? "(dry run) would remove" : "removed"} ${what}${kept}` : `pair(gemini): nothing to remove${kept}`;
669
+ }
670
+ };
671
+ var PAIR_INSTALLERS = {
672
+ [claudePair.harness]: claudePair,
673
+ [codexPair.harness]: codexPair,
674
+ [geminiPair.harness]: geminiPair
675
+ };
676
+ var PAIR_HARNESS_NAMES = Object.keys(PAIR_INSTALLERS);
677
+ var DEFAULT_PAIR = ["claude"];
678
+ function isPairHarness(name) {
679
+ return Object.hasOwn(PAIR_INSTALLERS, name);
680
+ }
681
+ function pairInstallerFor(harness) {
682
+ const i = Object.hasOwn(PAIR_INSTALLERS, harness) ? PAIR_INSTALLERS[harness] : void 0;
683
+ if (!i) throw new Error(`no Pair installer for "${harness}" (have: ${PAIR_HARNESS_NAMES.join(", ")})`);
684
+ return i;
685
+ }
686
+ function harnessPromptNote(harness) {
687
+ if (!harness || !isPairHarness(harness)) return "";
688
+ return `
689
+
690
+ (This Pair session is running in ${pairInstallerFor(harness).displayName}.)
691
+ `;
692
+ }
693
+
391
694
  // src/toolchain.ts
695
+ import { basename } from "path";
392
696
  var TIERS = ["light", "standard", "deep"];
393
697
  var ADAPTERS = {
394
698
  [claudeAdapter.harness]: claudeAdapter
@@ -426,9 +730,15 @@ function resolveToolchain(ship, route) {
426
730
  return found;
427
731
  }
428
732
  function toolchainVendor(spec) {
429
- if (spec.launcher && basename(spec.launcher) === "ori") return "openrouter";
733
+ if (isOriLauncher(spec)) return "openrouter";
430
734
  return adapterFor(spec.harness).vendor;
431
735
  }
736
+ function isOriLauncher(spec) {
737
+ return !!spec.launcher && basename(spec.launcher) === "ori";
738
+ }
739
+ function toolchainProvider(spec) {
740
+ return isOriLauncher(spec) ? "openrouter" : adapterFor(spec.harness).provider(spec);
741
+ }
432
742
  function resolveSeat(ship, route, role, persona = {}) {
433
743
  const warnings = [];
434
744
  const base = resolveToolchain(ship, route);
@@ -512,8 +822,8 @@ function explain(ship, required) {
512
822
  }
513
823
 
514
824
  // src/repo-config.ts
515
- import { readFileSync as readFileSync3, existsSync as existsSync2 } from "fs";
516
- import { join as join3, basename as basename2 } from "path";
825
+ import { readFileSync as readFileSync4, existsSync as existsSync3 } from "fs";
826
+ import { join as join4, basename as basename2 } from "path";
517
827
  import { parse } from "yaml";
518
828
  var SPEC_VERSION = 1;
519
829
  var REPO_CONFIG_FILENAMES = [".crew.yaml", ".crew.yml", ".crew.json"];
@@ -586,8 +896,8 @@ var MODES = ["local", "integrate", "ci_manual", "ci_auto", "external"];
586
896
  var PROVIDERS = ["github", "buildkite", "other", "none"];
587
897
  function findRepoConfig(dir) {
588
898
  for (const name of REPO_CONFIG_FILENAMES) {
589
- const p = join3(dir, name);
590
- if (existsSync2(p)) return p;
899
+ const p = join4(dir, name);
900
+ if (existsSync3(p)) return p;
591
901
  }
592
902
  return null;
593
903
  }
@@ -817,7 +1127,7 @@ function parseRepoConfig(text, file) {
817
1127
  function loadRepoConfig(dir) {
818
1128
  const file = findRepoConfig(dir);
819
1129
  if (!file) return null;
820
- return parseRepoConfig(readFileSync3(file, "utf8"), file);
1130
+ return parseRepoConfig(readFileSync4(file, "utf8"), file);
821
1131
  }
822
1132
  function hookLabel(cfg, hook2) {
823
1133
  return cfg.labels[hook2] ?? cfg.hooks[hook2] ?? hook2;
@@ -1084,6 +1394,7 @@ var SHIP_KEYS = /* @__PURE__ */ new Set([
1084
1394
  "agent",
1085
1395
  "toolchains",
1086
1396
  "toolchain",
1397
+ "pair",
1087
1398
  "shell",
1088
1399
  "extraPath",
1089
1400
  "useNvm",
@@ -1228,16 +1539,16 @@ function mergeRouteRelease(route, override) {
1228
1539
  };
1229
1540
  }
1230
1541
  function expand(p, base) {
1231
- let out = p.startsWith("~") ? p.replace(/^~/, homedir2()) : p;
1232
- out = out.replace(/\$\{?HOME\}?/g, homedir2());
1542
+ let out = p.startsWith("~") ? p.replace(/^~/, homedir3()) : p;
1543
+ out = out.replace(/\$\{?HOME\}?/g, homedir3());
1233
1544
  return isAbsolute(out) ? out : resolve(base, out);
1234
1545
  }
1235
1546
  function resolvePromptsDir(promptSet, crewHome, base) {
1236
- if (!promptSet) return join4(crewHome, "prompts", "default");
1547
+ if (!promptSet) return join5(crewHome, "prompts", "default");
1237
1548
  if (promptSet.includes("/") || promptSet.startsWith(".") || promptSet.startsWith("~")) {
1238
1549
  return expand(promptSet, base);
1239
1550
  }
1240
- return join4(crewHome, "prompts", promptSet);
1551
+ return join5(crewHome, "prompts", promptSet);
1241
1552
  }
1242
1553
  var Missing = class {
1243
1554
  paths = [];
@@ -1261,7 +1572,7 @@ var Missing = class {
1261
1572
  };
1262
1573
  function crewVersion(crewHome) {
1263
1574
  try {
1264
- const pkg = JSON.parse(readFileSync4(join4(crewHome, "package.json"), "utf8"));
1575
+ const pkg = JSON.parse(readFileSync5(join5(crewHome, "package.json"), "utf8"));
1265
1576
  return pkg.version ?? "0.0.0";
1266
1577
  } catch {
1267
1578
  return "0.0.0";
@@ -1271,26 +1582,26 @@ function configSearchPath(crewHome) {
1271
1582
  if (process.env.CREW_CONFIG) return [resolve(process.env.CREW_CONFIG)];
1272
1583
  const xdg = process.env.XDG_CONFIG_HOME;
1273
1584
  return [
1274
- ...xdg ? [join4(xdg, "crew", "crew.yaml")] : [],
1275
- join4(homedir2(), ".config", "crew", "crew.yaml"),
1585
+ ...xdg ? [join5(xdg, "crew", "crew.yaml")] : [],
1586
+ join5(homedir3(), ".config", "crew", "crew.yaml"),
1276
1587
  resolve(crewHome, "crew.yaml")
1277
1588
  ];
1278
1589
  }
1279
1590
  function findConfigFile(crewHome) {
1280
1591
  const candidates = configSearchPath(crewHome);
1281
- return candidates.find((c) => existsSync3(c)) ?? candidates[candidates.length - 1];
1592
+ return candidates.find((c) => existsSync4(c)) ?? candidates[candidates.length - 1];
1282
1593
  }
1283
- var defaultConfigPath = () => join4(process.env.XDG_CONFIG_HOME ?? join4(homedir2(), ".config"), "crew", "crew.yaml");
1594
+ var defaultConfigPath = () => join5(process.env.XDG_CONFIG_HOME ?? join5(homedir3(), ".config"), "crew", "crew.yaml");
1284
1595
  function routeSlug(route) {
1285
1596
  return route.replace(/\//g, "-");
1286
1597
  }
1287
1598
  function resolvedPathFor(stateDir, route) {
1288
1599
  const [workspace, project] = splitRoute(route);
1289
- return join4(stateDir, "resolved", workspace, `${project}.json`);
1600
+ return join5(stateDir, "resolved", workspace, `${project}.json`);
1290
1601
  }
1291
1602
  function apiKeyPathFor(stateDir, route) {
1292
1603
  const [workspace, project] = splitRoute(route);
1293
- return join4(stateDir, "keys", workspace, `${project}.env`);
1604
+ return join5(stateDir, "keys", workspace, `${project}.env`);
1294
1605
  }
1295
1606
  function splitRoute(route) {
1296
1607
  const parts = route.split("/");
@@ -1301,9 +1612,9 @@ function splitRoute(route) {
1301
1612
  }
1302
1613
  function readResolvedFile(stateDir, route) {
1303
1614
  const path = resolvedPathFor(stateDir, route);
1304
- if (!existsSync3(path)) return void 0;
1615
+ if (!existsSync4(path)) return void 0;
1305
1616
  try {
1306
- return JSON.parse(readFileSync4(path, "utf8"));
1617
+ return JSON.parse(readFileSync5(path, "utf8"));
1307
1618
  } catch (e) {
1308
1619
  throw new ConfigError(`${path}: not valid JSON \u2014 ${e.message}`);
1309
1620
  }
@@ -1359,23 +1670,23 @@ function parseResolved(raw, m, where) {
1359
1670
  }
1360
1671
  function loadConfig(crewHome, configFile, opts = {}) {
1361
1672
  const file = configFile ?? findConfigFile(crewHome);
1362
- if (opts.text === void 0 && !existsSync3(file)) {
1673
+ if (opts.text === void 0 && !existsSync4(file)) {
1363
1674
  throw new ConfigError(
1364
1675
  `no config found. Looked in:
1365
1676
  ` + configSearchPath(crewHome).map((c) => ` - ${c}`).join("\n") + `
1366
1677
 
1367
1678
  Create the first of those:
1368
- mkdir -p ${dirname2(defaultConfigPath())} && cp ${join4(crewHome, "crew.example.yaml")} ${defaultConfigPath()}`
1679
+ mkdir -p ${dirname3(defaultConfigPath())} && cp ${join5(crewHome, "crew.example.yaml")} ${defaultConfigPath()}`
1369
1680
  );
1370
1681
  }
1371
1682
  let raw;
1372
1683
  try {
1373
- raw = parse2(opts.text ?? readFileSync4(file, "utf8"));
1684
+ raw = parse2(opts.text ?? readFileSync5(file, "utf8"));
1374
1685
  } catch (e) {
1375
1686
  throw new ConfigError(`${file}: not valid YAML \u2014 ${e.message}`);
1376
1687
  }
1377
1688
  if (!raw || typeof raw !== "object") throw new ConfigError(`${file} is empty or not a mapping`);
1378
- const base = dirname2(file);
1689
+ const base = dirname3(file);
1379
1690
  const missing = new Missing();
1380
1691
  rejectUnknownKeys(raw, /* @__PURE__ */ new Set(["ship", "routes"]), "top-level", file);
1381
1692
  const shipRaw = raw.ship ?? {};
@@ -1402,6 +1713,7 @@ Create the first of those:
1402
1713
  }
1403
1714
  const toolchains = parseToolchains(shipRaw.toolchains, base, missing, file);
1404
1715
  const toolchainNames = /* @__PURE__ */ new Set([LEGACY_TOOLCHAIN, ...toolchains.map((x) => x.name)]);
1716
+ const pair = parsePair(shipRaw.pair, missing);
1405
1717
  const shipToolchain = shipRaw.toolchain === void 0 ? void 0 : String(shipRaw.toolchain);
1406
1718
  if (shipToolchain !== void 0 && !toolchainNames.has(shipToolchain)) {
1407
1719
  missing.add(`ship.toolchain "${shipToolchain}" is not defined (have: ${[...toolchainNames].join(", ")})`);
@@ -1441,12 +1753,13 @@ Create the first of those:
1441
1753
  },
1442
1754
  ...toolchains.length > 0 ? { toolchains } : {},
1443
1755
  ...shipToolchain !== void 0 ? { toolchain: shipToolchain } : {},
1756
+ ...pair ? { pair } : {},
1444
1757
  shell: shipRaw.shell,
1445
1758
  extraPath: shipRaw.extraPath,
1446
1759
  useNvm: shipRaw.useNvm !== false,
1447
1760
  nvmSh: shipRaw.nvmSh,
1448
1761
  stateDir,
1449
- logFile: expand(shipRaw.logFile ?? join4(tmpdir(), "crew.log"), base),
1762
+ logFile: expand(shipRaw.logFile ?? join5(tmpdir(), "crew.log"), base),
1450
1763
  userAgent: shipRaw.userAgent ?? `Mozilla/5.0 CrewAgent/${crewVersion(crewHome)}`,
1451
1764
  maxConcurrentAgents,
1452
1765
  streamRetentionDays,
@@ -1467,7 +1780,23 @@ Create the first of those:
1467
1780
  function expandCommand(cmd, base) {
1468
1781
  return cmd.includes("/") || cmd.startsWith("~") ? expand(cmd, base) : cmd;
1469
1782
  }
1470
- var TOOLCHAIN_KEYS = /* @__PURE__ */ new Set(["harness", "bin", "launcher", "model", "tiers", "args", "env", "maxThinkingTokens"]);
1783
+ var TOOLCHAIN_KEYS = /* @__PURE__ */ new Set(["harness", "bin", "launcher", "launcherBin", "model", "tiers", "args", "env", "maxThinkingTokens"]);
1784
+ function parsePair(raw, missing) {
1785
+ if (raw === void 0) return void 0;
1786
+ if (!Array.isArray(raw)) {
1787
+ missing.add("ship.pair must be a list of harness names");
1788
+ return void 0;
1789
+ }
1790
+ const out = [];
1791
+ for (const h of raw.map(String)) {
1792
+ if (!isPairHarness(h)) missing.add(`ship.pair "${h}" is not a harness Pair can be installed for (have: ${PAIR_HARNESS_NAMES.join(", ")})`);
1793
+ else if (!out.includes(h)) out.push(h);
1794
+ }
1795
+ return out;
1796
+ }
1797
+ function shipPair(ship) {
1798
+ return ship.pair ?? DEFAULT_PAIR;
1799
+ }
1471
1800
  function parseToolchains(raw, base, missing, file) {
1472
1801
  if (raw === void 0 || raw === null) return [];
1473
1802
  if (typeof raw !== "object" || Array.isArray(raw)) {
@@ -1513,6 +1842,7 @@ function parseToolchains(raw, base, missing, file) {
1513
1842
  harness,
1514
1843
  bin: expandCommand(String(entryRaw.bin ?? adapter.defaultBin), base),
1515
1844
  ...entryRaw.launcher ? { launcher: expandCommand(String(entryRaw.launcher), base) } : {},
1845
+ ...entryRaw.launcher && entryRaw.launcherBin ? { launcherBin: expandCommand(String(entryRaw.launcherBin), base) } : {},
1516
1846
  model: String(entryRaw.model ?? tiers.standard?.model ?? adapter.defaultModel),
1517
1847
  tiers,
1518
1848
  args: Array.isArray(args) ? args.map(String) : [],
@@ -1644,8 +1974,8 @@ function repoIdForName(r, name) {
1644
1974
  }
1645
1975
  function resolveApiKey(r) {
1646
1976
  if (r.apiKey) return r.apiKey;
1647
- if (r.apiKeyFile && r.apiKeyVar && existsSync3(r.apiKeyFile)) {
1648
- for (const line of readFileSync4(r.apiKeyFile, "utf8").split("\n")) {
1977
+ if (r.apiKeyFile && r.apiKeyVar && existsSync4(r.apiKeyFile)) {
1978
+ for (const line of readFileSync5(r.apiKeyFile, "utf8").split("\n")) {
1649
1979
  const m = line.match(new RegExp(`^${r.apiKeyVar}=(.*)$`));
1650
1980
  if (m) return (m[1] ?? "").trim().replace(/^["']|["']$/g, "");
1651
1981
  }
@@ -1708,7 +2038,7 @@ function configuredMembers(rt) {
1708
2038
  return out;
1709
2039
  }
1710
2040
  function defaultRepoDir(basePath, workspace, repoName) {
1711
- return join4(basePath, workspace, repoName);
2041
+ return join5(basePath, workspace, repoName);
1712
2042
  }
1713
2043
  function reposOf(r) {
1714
2044
  const named = Object.entries(r.repos);
@@ -1732,7 +2062,7 @@ function passengerRepoTargets(r) {
1732
2062
  return reposOf(r).filter((t) => r.repoOverrides[t.name]?.hostPassengers !== false);
1733
2063
  }
1734
2064
  function passengerCheckoutDir(basePath, workspace, repoName) {
1735
- return join4(basePath, workspace, ".passenger-checkouts", repoName);
2065
+ return join5(basePath, workspace, ".passenger-checkouts", repoName);
1736
2066
  }
1737
2067
  function passengerCheckoutTargets(r) {
1738
2068
  if (!r.hostPassengers) return [];
@@ -1864,17 +2194,17 @@ function rosterMarkdown(roster, meId) {
1864
2194
  }
1865
2195
 
1866
2196
  // src/state.ts
1867
- import { readFileSync as readFileSync5, writeFileSync, existsSync as existsSync4, mkdirSync as mkdirSync2, rmSync as rmSync2, statSync as statSync2 } from "fs";
1868
- import { join as join5 } from "path";
2197
+ import { readFileSync as readFileSync6, writeFileSync as writeFileSync2, existsSync as existsSync5, mkdirSync as mkdirSync3, rmSync as rmSync3, statSync as statSync2 } from "fs";
2198
+ import { join as join6 } from "path";
1869
2199
  var EPOCH = "1970-01-01T00:00:00.000Z";
1870
2200
  var State = class _State {
1871
2201
  dir;
1872
2202
  constructor(dir) {
1873
2203
  this.dir = dir;
1874
- mkdirSync2(dir, { recursive: true });
2204
+ mkdirSync3(dir, { recursive: true });
1875
2205
  }
1876
2206
  path(...parts) {
1877
- return join5(this.dir, ...parts);
2207
+ return join6(this.dir, ...parts);
1878
2208
  }
1879
2209
  /**
1880
2210
  * The timestamp every "new since last poll" check is measured against.
@@ -1885,14 +2215,14 @@ var State = class _State {
1885
2215
  */
1886
2216
  watermark() {
1887
2217
  try {
1888
- const v = readFileSync5(this.path(".poll-watermark"), "utf8").trim();
2218
+ const v = readFileSync6(this.path(".poll-watermark"), "utf8").trim();
1889
2219
  return v || EPOCH;
1890
2220
  } catch {
1891
2221
  return EPOCH;
1892
2222
  }
1893
2223
  }
1894
2224
  setWatermark(at) {
1895
- writeFileSync(this.path(".poll-watermark"), `${at}
2225
+ writeFileSync2(this.path(".poll-watermark"), `${at}
1896
2226
  `);
1897
2227
  }
1898
2228
  /**
@@ -1910,19 +2240,19 @@ var State = class _State {
1910
2240
  }
1911
2241
  /** Whole-crew pause, and the per-role sentinels one level down. */
1912
2242
  isPaused() {
1913
- return existsSync4(this.path(".crew-paused"));
2243
+ return existsSync5(this.path(".crew-paused"));
1914
2244
  }
1915
2245
  isRolePaused(role) {
1916
- return existsSync4(this.path(`.role-paused-${role}`));
2246
+ return existsSync5(this.path(`.role-paused-${role}`));
1917
2247
  }
1918
2248
  pausedRoles(roles) {
1919
2249
  return new Set(roles.filter((r) => this.isRolePaused(r)));
1920
2250
  }
1921
2251
  pause(role) {
1922
- writeFileSync(role ? this.path(`.role-paused-${role}`) : this.path(".crew-paused"), "");
2252
+ writeFileSync2(role ? this.path(`.role-paused-${role}`) : this.path(".crew-paused"), "");
1923
2253
  }
1924
2254
  resume(role) {
1925
- rmSync2(role ? this.path(`.role-paused-${role}`) : this.path(".crew-paused"), { force: true });
2255
+ rmSync3(role ? this.path(`.role-paused-${role}`) : this.path(".crew-paused"), { force: true });
1926
2256
  }
1927
2257
  /**
1928
2258
  * The release phase (merge, deploy, release) has its own sentinel, one
@@ -1930,11 +2260,11 @@ var State = class _State {
1930
2260
  * while agents keep working.
1931
2261
  */
1932
2262
  isReleasePaused() {
1933
- return existsSync4(this.path(".release-paused"));
2263
+ return existsSync5(this.path(".release-paused"));
1934
2264
  }
1935
2265
  setReleasePaused(paused) {
1936
- if (paused) writeFileSync(this.path(".release-paused"), "");
1937
- else rmSync2(this.path(".release-paused"), { force: true });
2266
+ if (paused) writeFileSync2(this.path(".release-paused"), "");
2267
+ else rmSync3(this.path(".release-paused"), { force: true });
1938
2268
  }
1939
2269
  /**
1940
2270
  * Why a release cycle must not run right now, or null when it may. Every
@@ -1971,7 +2301,7 @@ var State = class _State {
1971
2301
  const name = _State.safe(rawName);
1972
2302
  const file = this.path(`.${name}.lock`);
1973
2303
  try {
1974
- const existing = Number.parseInt(readFileSync5(file, "utf8").trim(), 10);
2304
+ const existing = Number.parseInt(readFileSync6(file, "utf8").trim(), 10);
1975
2305
  if (Number.isFinite(existing) && existing > 0) {
1976
2306
  try {
1977
2307
  process.kill(existing, 0);
@@ -1981,13 +2311,13 @@ var State = class _State {
1981
2311
  }
1982
2312
  } catch {
1983
2313
  }
1984
- writeFileSync(file, `${process.pid}
2314
+ writeFileSync2(file, `${process.pid}
1985
2315
  `);
1986
2316
  return {
1987
2317
  ok: true,
1988
2318
  release: () => {
1989
2319
  try {
1990
- rmSync2(file, { force: true });
2320
+ rmSync3(file, { force: true });
1991
2321
  } catch {
1992
2322
  }
1993
2323
  }
@@ -2065,7 +2395,7 @@ var State = class _State {
2065
2395
  const heldReported = `.release-held-${suffix}`;
2066
2396
  const read = (f) => {
2067
2397
  try {
2068
- return readFileSync5(this.path(f), "utf8").trim() || null;
2398
+ return readFileSync6(this.path(f), "utf8").trim() || null;
2069
2399
  } catch {
2070
2400
  return null;
2071
2401
  }
@@ -2074,12 +2404,12 @@ var State = class _State {
2074
2404
  blockedCount: () => Number.parseInt(read(blocked) ?? "", 10) || 0,
2075
2405
  noteBlocked: () => {
2076
2406
  const n = (Number.parseInt(read(blocked) ?? "", 10) || 0) + 1;
2077
- writeFileSync(this.path(blocked), `${n}
2407
+ writeFileSync2(this.path(blocked), `${n}
2078
2408
  `);
2079
2409
  return n;
2080
2410
  },
2081
2411
  clearBlocked: () => {
2082
- rmSync2(this.path(blocked), { force: true });
2412
+ rmSync3(this.path(blocked), { force: true });
2083
2413
  },
2084
2414
  /**
2085
2415
  * The commit a deploy last failed on.
@@ -2091,11 +2421,11 @@ var State = class _State {
2091
2421
  */
2092
2422
  deployFailedSha: () => read(failed),
2093
2423
  noteDeployFailed: (sha) => {
2094
- writeFileSync(this.path(failed), `${sha}
2424
+ writeFileSync2(this.path(failed), `${sha}
2095
2425
  `);
2096
2426
  },
2097
2427
  clearDeployFailed: () => {
2098
- rmSync2(this.path(failed), { force: true });
2428
+ rmSync3(this.path(failed), { force: true });
2099
2429
  },
2100
2430
  /**
2101
2431
  * Consecutive test-gate failures on the same head (CREW-1368). A failure
@@ -2105,31 +2435,31 @@ var State = class _State {
2105
2435
  noteTestGateFailed: (sha) => {
2106
2436
  const [prev, n] = (read(gateFailed) ?? "").split(" ");
2107
2437
  const next = prev === sha ? (Number.parseInt(n ?? "", 10) || 0) + 1 : 1;
2108
- writeFileSync(this.path(gateFailed), `${sha} ${next}
2438
+ writeFileSync2(this.path(gateFailed), `${sha} ${next}
2109
2439
  `);
2110
2440
  return next;
2111
2441
  },
2112
2442
  clearTestGateFailed: () => {
2113
- rmSync2(this.path(gateFailed), { force: true });
2443
+ rmSync3(this.path(gateFailed), { force: true });
2114
2444
  },
2115
2445
  /** Whether the board was already told about this head's red gate. */
2116
2446
  heldReported: () => {
2117
2447
  try {
2118
- return readFileSync5(this.path(heldReported), "utf8").replace(/\n$/, "") || null;
2448
+ return readFileSync6(this.path(heldReported), "utf8").replace(/\n$/, "") || null;
2119
2449
  } catch {
2120
2450
  return null;
2121
2451
  }
2122
2452
  },
2123
2453
  noteHeldReported: (signature) => {
2124
- writeFileSync(this.path(heldReported), `${signature}
2454
+ writeFileSync2(this.path(heldReported), `${signature}
2125
2455
  `);
2126
2456
  },
2127
2457
  clearHeldReported: () => {
2128
- rmSync2(this.path(heldReported), { force: true });
2458
+ rmSync3(this.path(heldReported), { force: true });
2129
2459
  },
2130
2460
  testGateReported: (sha) => read(gateReported) === sha,
2131
2461
  noteTestGateReported: (sha) => {
2132
- writeFileSync(this.path(gateReported), `${sha}
2462
+ writeFileSync2(this.path(gateReported), `${sha}
2133
2463
  `);
2134
2464
  }
2135
2465
  };
@@ -2152,13 +2482,13 @@ var State = class _State {
2152
2482
  const ticketStreakFile = `.fairness-ticket-streak-${suffix}`;
2153
2483
  const read = (f) => {
2154
2484
  try {
2155
- return readFileSync5(this.path(f), "utf8").trim() || null;
2485
+ return readFileSync6(this.path(f), "utf8").trim() || null;
2156
2486
  } catch {
2157
2487
  return null;
2158
2488
  }
2159
2489
  };
2160
2490
  const clear2 = () => {
2161
- for (const f of [streakFile, ticketFile, sinceFile, ticketStreakFile]) rmSync2(this.path(f), { force: true });
2491
+ for (const f of [streakFile, ticketFile, sinceFile, ticketStreakFile]) rmSync3(this.path(f), { force: true });
2162
2492
  };
2163
2493
  return {
2164
2494
  /** Consecutive cycles this route had actionable work and did not win. */
@@ -2185,18 +2515,18 @@ var State = class _State {
2185
2515
  clear2();
2186
2516
  return;
2187
2517
  }
2188
- writeFileSync(this.path(streakFile), `${this.readInt(streakFile) + 1}
2518
+ writeFileSync2(this.path(streakFile), `${this.readInt(streakFile) + 1}
2189
2519
  `);
2190
2520
  const prior = read(ticketFile);
2191
2521
  if (topTicket && topTicket === prior) {
2192
- writeFileSync(this.path(ticketStreakFile), `${this.readInt(ticketStreakFile) + 1}
2522
+ writeFileSync2(this.path(ticketStreakFile), `${this.readInt(ticketStreakFile) + 1}
2193
2523
  `);
2194
2524
  } else if (topTicket) {
2195
- writeFileSync(this.path(ticketFile), `${topTicket}
2525
+ writeFileSync2(this.path(ticketFile), `${topTicket}
2196
2526
  `);
2197
- writeFileSync(this.path(sinceFile), `${(/* @__PURE__ */ new Date()).toISOString()}
2527
+ writeFileSync2(this.path(sinceFile), `${(/* @__PURE__ */ new Date()).toISOString()}
2198
2528
  `);
2199
- writeFileSync(this.path(ticketStreakFile), "1\n");
2529
+ writeFileSync2(this.path(ticketStreakFile), "1\n");
2200
2530
  }
2201
2531
  }
2202
2532
  };
@@ -2217,14 +2547,14 @@ var State = class _State {
2217
2547
  return {
2218
2548
  previous: () => {
2219
2549
  try {
2220
- const parsed = JSON.parse(readFileSync5(this.path(file), "utf8"));
2550
+ const parsed = JSON.parse(readFileSync6(this.path(file), "utf8"));
2221
2551
  return parsed && typeof parsed === "object" ? parsed : {};
2222
2552
  } catch {
2223
2553
  return {};
2224
2554
  }
2225
2555
  },
2226
2556
  persist: (next) => {
2227
- writeFileSync(this.path(file), JSON.stringify(next));
2557
+ writeFileSync2(this.path(file), JSON.stringify(next));
2228
2558
  }
2229
2559
  };
2230
2560
  }
@@ -2238,14 +2568,14 @@ var State = class _State {
2238
2568
  return {
2239
2569
  previous: () => {
2240
2570
  try {
2241
- const parsed = JSON.parse(readFileSync5(this.path(file), "utf8"));
2571
+ const parsed = JSON.parse(readFileSync6(this.path(file), "utf8"));
2242
2572
  return Array.isArray(parsed) ? parsed : [];
2243
2573
  } catch {
2244
2574
  return [];
2245
2575
  }
2246
2576
  },
2247
2577
  persist: (next) => {
2248
- writeFileSync(this.path(file), JSON.stringify(next));
2578
+ writeFileSync2(this.path(file), JSON.stringify(next));
2249
2579
  }
2250
2580
  };
2251
2581
  }
@@ -2258,7 +2588,7 @@ var State = class _State {
2258
2588
  const file = ".release-last-run";
2259
2589
  return {
2260
2590
  touch: () => {
2261
- writeFileSync(this.path(file), (/* @__PURE__ */ new Date()).toISOString());
2591
+ writeFileSync2(this.path(file), (/* @__PURE__ */ new Date()).toISOString());
2262
2592
  },
2263
2593
  at: () => {
2264
2594
  try {
@@ -2271,7 +2601,7 @@ var State = class _State {
2271
2601
  }
2272
2602
  readInt(file) {
2273
2603
  try {
2274
- return Number.parseInt(readFileSync5(this.path(file), "utf8").trim(), 10) || 0;
2604
+ return Number.parseInt(readFileSync6(this.path(file), "utf8").trim(), 10) || 0;
2275
2605
  } catch {
2276
2606
  return 0;
2277
2607
  }
@@ -2575,9 +2905,9 @@ function describeRelease(o, route) {
2575
2905
  }
2576
2906
 
2577
2907
  // src/install.ts
2578
- import { dirname as dirname3, join as join6, extname } from "path";
2579
- import { homedir as homedir3 } from "os";
2580
- import { accessSync, constants, existsSync as existsSync5, mkdirSync as mkdirSync3, readdirSync, readFileSync as readFileSync6, statSync as statSync3, writeFileSync as writeFileSync2, unlinkSync, realpathSync } from "fs";
2908
+ import { dirname as dirname4, join as join7, extname } from "path";
2909
+ import { homedir as homedir4 } from "os";
2910
+ import { accessSync, constants, existsSync as existsSync6, mkdirSync as mkdirSync4, readdirSync, readFileSync as readFileSync7, statSync as statSync3, writeFileSync as writeFileSync3, unlinkSync, realpathSync } from "fs";
2581
2911
  import { spawnSync as spawnSync2 } from "child_process";
2582
2912
  import { createHash } from "crypto";
2583
2913
 
@@ -2589,7 +2919,7 @@ function isCompiledBinary(moduleUrl) {
2589
2919
  // src/install.ts
2590
2920
  function crewInvocation(crewHome) {
2591
2921
  if (isCompiledBinary(import.meta.url)) return [process.execPath];
2592
- return [process.execPath, join6(crewHome, "bin", "crew")];
2922
+ return [process.execPath, join7(crewHome, "bin", "crew")];
2593
2923
  }
2594
2924
  var INTERVAL_SECONDS = 120;
2595
2925
  function labelFor(crewHome, job) {
@@ -2620,14 +2950,14 @@ function schedulerLogFor(crewLog, job) {
2620
2950
  return `${stem}.${suffix}${ext || ".log"}`;
2621
2951
  }
2622
2952
  function pathFor(ship) {
2623
- const nodeBinDir = dirname3(process.execPath);
2953
+ const nodeBinDir = dirname4(process.execPath);
2624
2954
  const parts = [nodeBinDir];
2625
2955
  if (ship.extraPath) parts.push(ship.extraPath);
2626
2956
  parts.push("/usr/bin", "/bin", "/usr/sbin", "/sbin");
2627
2957
  return parts.join(":");
2628
2958
  }
2629
2959
  function findOnPath(bin, searchPath) {
2630
- const candidates = bin.includes("/") ? [bin] : searchPath.split(":").filter(Boolean).map((dir) => join6(dir, bin));
2960
+ const candidates = bin.includes("/") ? [bin] : searchPath.split(":").filter(Boolean).map((dir) => join7(dir, bin));
2631
2961
  for (const candidate of candidates) {
2632
2962
  try {
2633
2963
  accessSync(candidate, constants.X_OK);
@@ -2637,19 +2967,19 @@ function findOnPath(bin, searchPath) {
2637
2967
  }
2638
2968
  return void 0;
2639
2969
  }
2640
- var COMMON_DOCKER_DIRS = ["/usr/local/bin", "/opt/homebrew/bin", join6(homedir3(), ".docker", "bin")].join(":");
2970
+ var COMMON_DOCKER_DIRS = ["/usr/local/bin", "/opt/homebrew/bin", join7(homedir4(), ".docker", "bin")].join(":");
2641
2971
  function dockerPathProblem(ship, shellPath = process.env.PATH ?? "") {
2642
2972
  const schedulerPath = pathFor(ship);
2643
2973
  if (findOnPath("docker", schedulerPath)) return void 0;
2644
2974
  const inShell = findOnPath("docker", shellPath);
2645
- return inShell ? `\`docker\` is not on the scheduler PATH (${schedulerPath}) but is at ${inShell} in your shell; add ${dirname3(inShell)} to ship.extraPath in crew.yaml and run \`crew install\`` : `\`docker\` is not on the scheduler PATH (${schedulerPath}) or your shell PATH; install Docker, then add its directory to ship.extraPath in crew.yaml and run \`crew install\``;
2975
+ return inShell ? `\`docker\` is not on the scheduler PATH (${schedulerPath}) but is at ${inShell} in your shell; add ${dirname4(inShell)} to ship.extraPath in crew.yaml and run \`crew install\`` : `\`docker\` is not on the scheduler PATH (${schedulerPath}) or your shell PATH; install Docker, then add its directory to ship.extraPath in crew.yaml and run \`crew install\``;
2646
2976
  }
2647
2977
  function xmlEscape(s) {
2648
2978
  return s.replace(/&/g, "&amp;").replace(/</g, "&lt;").replace(/>/g, "&gt;");
2649
2979
  }
2650
2980
  function planLaunchd(ship, crewHome, job) {
2651
2981
  const label = labelFor(crewHome, job);
2652
- const unitPath = join6(homedir3(), "Library", "LaunchAgents", `${label}.plist`);
2982
+ const unitPath = join7(homedir4(), "Library", "LaunchAgents", `${label}.plist`);
2653
2983
  const crewLog = ship.logFile;
2654
2984
  const schedulerLog = schedulerLogFor(crewLog, job);
2655
2985
  const persistent = job === "run";
@@ -2701,8 +3031,8 @@ ${scheduleKeys}
2701
3031
  }
2702
3032
  function planSystemd(ship, crewHome, job) {
2703
3033
  const label = labelFor(crewHome, job);
2704
- const unitDir = join6(homedir3(), ".config", "systemd", "user");
2705
- const servicePath = join6(unitDir, `${label}.service`);
3034
+ const unitDir = join7(homedir4(), ".config", "systemd", "user");
3035
+ const servicePath = join7(unitDir, `${label}.service`);
2706
3036
  const crewLog = ship.logFile;
2707
3037
  const schedulerLog = schedulerLogFor(crewLog, job);
2708
3038
  const description = job === "release" ? "Tablation crew \u2014 release" : job === "passengers" ? "Tablation crew \u2014 Host Passengers sync" : "Tablation crew \u2014 persistent supervisor loop";
@@ -2739,7 +3069,7 @@ WantedBy=default.target
2739
3069
  schedulerLog
2740
3070
  };
2741
3071
  }
2742
- const timerPath = join6(unitDir, `${label}.timer`);
3072
+ const timerPath = join7(unitDir, `${label}.timer`);
2743
3073
  const service = `# Written by \`crew install\` \u2014 re-run it rather than hand-editing this file.
2744
3074
  [Unit]
2745
3075
  Description=${description}
@@ -2806,7 +3136,7 @@ function planInstall(ship, crewHome, hostShip, hasSystemd, job = "run") {
2806
3136
  function planUninstall(ship, crewHome, hostShip, hasSystemd, job = "run") {
2807
3137
  const label = labelFor(crewHome, job);
2808
3138
  if (hostShip === "macos") {
2809
- const unitPath = join6(homedir3(), "Library", "LaunchAgents", `${label}.plist`);
3139
+ const unitPath = join7(homedir4(), "Library", "LaunchAgents", `${label}.plist`);
2810
3140
  return {
2811
3141
  mechanism: "launchd",
2812
3142
  unitPaths: [unitPath],
@@ -2816,18 +3146,18 @@ function planUninstall(ship, crewHome, hostShip, hasSystemd, job = "run") {
2816
3146
  };
2817
3147
  }
2818
3148
  if (hostShip === "linux" && hasSystemd) {
2819
- const unitDir = join6(homedir3(), ".config", "systemd", "user");
3149
+ const unitDir = join7(homedir4(), ".config", "systemd", "user");
2820
3150
  if (job === "run") {
2821
3151
  return {
2822
3152
  mechanism: "systemd",
2823
- unitPaths: [join6(unitDir, `${label}.service`)],
3153
+ unitPaths: [join7(unitDir, `${label}.service`)],
2824
3154
  crontabLine: null,
2825
3155
  unloadCommands: [["systemctl", "--user", "disable", "--now", `${label}.service`]]
2826
3156
  };
2827
3157
  }
2828
3158
  return {
2829
3159
  mechanism: "systemd",
2830
- unitPaths: [join6(unitDir, `${label}.service`), join6(unitDir, `${label}.timer`)],
3160
+ unitPaths: [join7(unitDir, `${label}.service`), join7(unitDir, `${label}.timer`)],
2831
3161
  crontabLine: null,
2832
3162
  unloadCommands: [["systemctl", "--user", "disable", "--now", `${label}.timer`]]
2833
3163
  };
@@ -2845,7 +3175,7 @@ function planUninstall(ship, crewHome, hostShip, hasSystemd, job = "run") {
2845
3175
  function planDaemonControl(crewHome, hostShip, hasSystemd) {
2846
3176
  const label = labelFor(crewHome, "run");
2847
3177
  if (hostShip === "macos") {
2848
- const unitPath = join6(homedir3(), "Library", "LaunchAgents", `${label}.plist`);
3178
+ const unitPath = join7(homedir4(), "Library", "LaunchAgents", `${label}.plist`);
2849
3179
  return {
2850
3180
  mechanism: "launchd",
2851
3181
  startCommand: ["launchctl", "load", unitPath],
@@ -2885,8 +3215,8 @@ async function applyInstall(plan, crewHome, dryRun, log) {
2885
3215
  log.emit(`would write ${path}`);
2886
3216
  continue;
2887
3217
  }
2888
- mkdirSync3(dirname3(path), { recursive: true });
2889
- writeFileSync2(path, plan.unitContent[path]);
3218
+ mkdirSync4(dirname4(path), { recursive: true });
3219
+ writeFileSync3(path, plan.unitContent[path]);
2890
3220
  log.emit(`wrote ${path}`);
2891
3221
  }
2892
3222
  if (plan.crontabLine) {
@@ -2940,7 +3270,7 @@ async function applyUninstall(plan, crewHome, dryRun, log) {
2940
3270
  }
2941
3271
  }
2942
3272
  for (const path of plan.unitPaths) {
2943
- if (!existsSync5(path)) continue;
3273
+ if (!existsSync6(path)) continue;
2944
3274
  if (dryRun) {
2945
3275
  log.emit(`would remove ${path}`);
2946
3276
  continue;
@@ -2959,7 +3289,7 @@ async function applyUninstall(plan, crewHome, dryRun, log) {
2959
3289
  }
2960
3290
  var CREW_LAUNCHD_PREFIX = "com.tablation.crew";
2961
3291
  function launchAgentsDir() {
2962
- return join6(homedir3(), "Library", "LaunchAgents");
3292
+ return join7(homedir4(), "Library", "LaunchAgents");
2963
3293
  }
2964
3294
  function xmlUnescape(s) {
2965
3295
  return s.replace(/&lt;/g, "<").replace(/&gt;/g, ">").replace(/&amp;/g, "&");
@@ -2976,25 +3306,25 @@ function loadedCrewLabels() {
2976
3306
  return [...parseLaunchctlList(list.stdout ?? "").keys()].filter((l) => l.startsWith(CREW_LAUNCHD_PREFIX));
2977
3307
  }
2978
3308
  function listLaunchdUnits(dir = launchAgentsDir(), loaded = []) {
2979
- const files = existsSync5(dir) ? readdirSync(dir) : [];
3309
+ const files = existsSync6(dir) ? readdirSync(dir) : [];
2980
3310
  const units = files.filter((f) => f.startsWith(CREW_LAUNCHD_PREFIX) && f.endsWith(".plist")).sort().map((f) => {
2981
- const plistPath = join6(dir, f);
3311
+ const plistPath = join7(dir, f);
2982
3312
  let program = null;
2983
3313
  try {
2984
- program = programFromPlist(readFileSync6(plistPath, "utf8"));
3314
+ program = programFromPlist(readFileSync7(plistPath, "utf8"));
2985
3315
  } catch {
2986
3316
  }
2987
3317
  return {
2988
3318
  label: f.slice(0, -".plist".length),
2989
3319
  plistPath,
2990
3320
  program,
2991
- programExists: program !== null && existsSync5(program)
3321
+ programExists: program !== null && existsSync6(program)
2992
3322
  };
2993
3323
  });
2994
3324
  const seen = new Set(units.map((u) => u.label));
2995
3325
  for (const label of [...loaded].sort()) {
2996
3326
  if (!label.startsWith(CREW_LAUNCHD_PREFIX) || seen.has(label)) continue;
2997
- units.push({ label, plistPath: join6(dir, `${label}.plist`), program: null, programExists: false, orphan: true });
3327
+ units.push({ label, plistPath: join7(dir, `${label}.plist`), program: null, programExists: false, orphan: true });
2998
3328
  }
2999
3329
  return units;
3000
3330
  }
@@ -3007,9 +3337,9 @@ function removeLaunchdUnit(unit, dryRun, log) {
3007
3337
  log.emit(`would unload and remove ${unit.label}`);
3008
3338
  return;
3009
3339
  }
3010
- if (existsSync5(unit.plistPath)) spawnSync2("launchctl", ["unload", unit.plistPath], { encoding: "utf8" });
3340
+ if (existsSync6(unit.plistPath)) spawnSync2("launchctl", ["unload", unit.plistPath], { encoding: "utf8" });
3011
3341
  spawnSync2("launchctl", ["remove", unit.label], { encoding: "utf8" });
3012
- if (existsSync5(unit.plistPath)) unlinkSync(unit.plistPath);
3342
+ if (existsSync6(unit.plistPath)) unlinkSync(unit.plistPath);
3013
3343
  log.emit(`removed ${unit.label} (${unit.plistPath})`);
3014
3344
  }
3015
3345
  function clearForeignLaunchdUnits(crewHome, replace, dryRun, log, dir, loaded) {
@@ -3092,15 +3422,15 @@ import { statfsSync } from "fs";
3092
3422
  // src/git.ts
3093
3423
  import { execFileSync } from "child_process";
3094
3424
  import {
3095
- existsSync as existsSync6,
3096
- mkdirSync as mkdirSync4,
3425
+ existsSync as existsSync7,
3426
+ mkdirSync as mkdirSync5,
3097
3427
  openSync as openSync2,
3098
3428
  closeSync as closeSync2,
3099
3429
  writeSync,
3100
3430
  statSync as statSync4,
3101
3431
  unlinkSync as unlinkSync2
3102
3432
  } from "fs";
3103
- import { dirname as dirname4, basename as basename4 } from "path";
3433
+ import { dirname as dirname5, basename as basename4 } from "path";
3104
3434
  var GitError = class extends Error {
3105
3435
  };
3106
3436
  function gitRaw(cwd, args) {
@@ -3464,13 +3794,13 @@ function cloneUrlFor(remote) {
3464
3794
  return `git@github.com:${remote}.git`;
3465
3795
  }
3466
3796
  function ensureRepoCheckout(dir, remote) {
3467
- if (existsSync6(`${dir}/.git`)) return false;
3797
+ if (existsSync7(`${dir}/.git`)) return false;
3468
3798
  if (!remote) {
3469
3799
  throw new GitError(
3470
3800
  `${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`
3471
3801
  );
3472
3802
  }
3473
- mkdirSync4(dirname4(dir), { recursive: true });
3803
+ mkdirSync5(dirname5(dir), { recursive: true });
3474
3804
  try {
3475
3805
  execFileSync("git", ["clone", cloneUrlFor(remote), dir], { stdio: ["ignore", "pipe", "pipe"] });
3476
3806
  } catch (e) {
@@ -3489,7 +3819,7 @@ function sleepSync(ms2) {
3489
3819
  }
3490
3820
  function acquireCheckoutLock(dir, timeoutMs = 3e4) {
3491
3821
  const lockPath = checkoutLockPath(dir);
3492
- mkdirSync4(dirname4(lockPath), { recursive: true });
3822
+ mkdirSync5(dirname5(lockPath), { recursive: true });
3493
3823
  const deadline = Date.now() + timeoutMs;
3494
3824
  for (; ; ) {
3495
3825
  try {
@@ -5201,8 +5531,8 @@ function materializeBranchForTicket(dir, cfg, t, role) {
5201
5531
  }
5202
5532
 
5203
5533
  // src/poll.ts
5204
- import { writeFileSync as writeFileSync3 } from "fs";
5205
- import { join as join7 } from "path";
5534
+ import { writeFileSync as writeFileSync4 } from "fs";
5535
+ import { join as join8 } from "path";
5206
5536
 
5207
5537
  // src/stalled.ts
5208
5538
  var STALLED_AFTER_CYCLES = 30;
@@ -5474,7 +5804,7 @@ async function decideCycle(o) {
5474
5804
  }
5475
5805
  return decision;
5476
5806
  }
5477
- var digestPath = (stateDir, route, role) => join7(stateDir, `digest-${route}-${role}.md`);
5807
+ var digestPath = (stateDir, route, role) => join8(stateDir, `digest-${route}-${role}.md`);
5478
5808
  function writeDigest(o, d, role, stateDir) {
5479
5809
  const me = o.route.resolved?.seats[role];
5480
5810
  if (!me) return false;
@@ -5536,7 +5866,7 @@ function writeDigest(o, d, role, stateDir) {
5536
5866
  };
5537
5867
  const text = role === "qa" ? qaDigest(input) : buildingDigest(input);
5538
5868
  const path = digestPath(stateDir, routeSlug(o.route.route), role);
5539
- writeFileSync3(path, text);
5869
+ writeFileSync4(path, text);
5540
5870
  o.emit.emit(`queue digest written for ${role} (${Buffer.byteLength(text)} bytes)`, {
5541
5871
  data: { role, bytes: Buffer.byteLength(text) }
5542
5872
  });
@@ -5844,9 +6174,9 @@ var alreadyReported = (ticket, g, comments) => comments.some((c) => c.ticket_id
5844
6174
 
5845
6175
  // src/agent.ts
5846
6176
  import { spawn as spawn2 } from "child_process";
5847
- import { readFileSync as readFileSync7, existsSync as existsSync7, statSync as statSync5, mkdirSync as mkdirSync5, writeFileSync as writeFileSync4, createWriteStream } from "fs";
6177
+ import { readFileSync as readFileSync8, existsSync as existsSync8, statSync as statSync5, mkdirSync as mkdirSync6, writeFileSync as writeFileSync5, createWriteStream } from "fs";
5848
6178
  import { finished } from "stream/promises";
5849
- import { join as join8, dirname as dirname5 } from "path";
6179
+ import { join as join9, dirname as dirname6 } from "path";
5850
6180
  import { createHash as createHash2 } from "crypto";
5851
6181
 
5852
6182
  // src/environment.ts
@@ -6244,23 +6574,23 @@ var AgentError = class extends Error {
6244
6574
  var AgentSpawnError = class extends AgentError {
6245
6575
  };
6246
6576
  function planAgentRun(o) {
6247
- const briefPath = join8(o.route.promptsDir, "personas", `lane-${o.role}.md`);
6248
- if (!existsSync7(briefPath)) {
6577
+ const briefPath = join9(o.route.promptsDir, "personas", `lane-${o.role}.md`);
6578
+ if (!existsSync8(briefPath)) {
6249
6579
  throw new AgentError(`no brief at ${briefPath} \u2014 refusing to run an unscoped session`);
6250
6580
  }
6251
- const commonPath = join8(o.route.promptsDir, "personas", "common.md");
6252
- if (!existsSync7(commonPath)) throw new AgentError(`no shared policy at ${commonPath}`);
6581
+ const commonPath = join9(o.route.promptsDir, "personas", "common.md");
6582
+ if (!existsSync8(commonPath)) throw new AgentError(`no shared policy at ${commonPath}`);
6253
6583
  const now = o.now ?? (() => Date.now());
6254
6584
  const routeLabel = routeSlug(o.route.route);
6255
- const digestPath2 = join8(o.stateDir, `digest-${routeLabel}-${o.role}.md`);
6585
+ const digestPath2 = join9(o.stateDir, `digest-${routeLabel}-${o.role}.md`);
6256
6586
  let digest;
6257
6587
  let ageSeconds;
6258
- if (existsSync7(digestPath2)) {
6588
+ if (existsSync8(digestPath2)) {
6259
6589
  ageSeconds = Math.floor((now() - statSync5(digestPath2).mtimeMs) / 1e3);
6260
- if (ageSeconds <= DIGEST_MAX_AGE_SECONDS) digest = readFileSync7(digestPath2, "utf8");
6590
+ if (ageSeconds <= DIGEST_MAX_AGE_SECONDS) digest = readFileSync8(digestPath2, "utf8");
6261
6591
  }
6262
- const common = readFileSync7(commonPath, "utf8");
6263
- const brief = readFileSync7(briefPath, "utf8");
6592
+ const common = readFileSync8(commonPath, "utf8");
6593
+ const brief = readFileSync8(briefPath, "utf8");
6264
6594
  const effectiveCommon = o.divergedPrompt ? "" : common;
6265
6595
  const effectiveBrief = o.divergedPrompt ?? brief;
6266
6596
  const prompt = assemblePrompt({
@@ -6271,7 +6601,7 @@ function planAgentRun(o) {
6271
6601
  digest
6272
6602
  });
6273
6603
  const promptSha = createHash2("sha256").update(`${effectiveCommon}${effectiveBrief}`).digest("hex");
6274
- const streamsDir = join8(o.stateDir, "streams");
6604
+ const streamsDir = join9(o.stateDir, "streams");
6275
6605
  const base = `${routeLabel}-${o.role}-${o.cycle}`;
6276
6606
  const seat = resolveSeat(o.ship, o.route, o.role, o.agentPersona ?? (o.agentModel ? { model: o.agentModel } : {}));
6277
6607
  const { toolchain, model } = seat;
@@ -6304,7 +6634,7 @@ function planAgentRun(o) {
6304
6634
  args: invocation.args,
6305
6635
  toolchain: toolchain.name,
6306
6636
  harness: toolchain.harness,
6307
- provider: adapter.provider(toolchain),
6637
+ provider: toolchainProvider(toolchain),
6308
6638
  resolution: { toolchain: seat.toolchainWhy, model: seat.modelWhy },
6309
6639
  warnings: seat.warnings,
6310
6640
  prompt,
@@ -6326,8 +6656,8 @@ function planAgentRun(o) {
6326
6656
  digestAttached: digest !== void 0,
6327
6657
  digestAgeSeconds: ageSeconds,
6328
6658
  model,
6329
- streamPath: join8(streamsDir, `${base}.jsonl`),
6330
- eventsPath: join8(streamsDir, `${base}.events.jsonl`),
6659
+ streamPath: join9(streamsDir, `${base}.jsonl`),
6660
+ eventsPath: join9(streamsDir, `${base}.events.jsonl`),
6331
6661
  ticket: o.ticket,
6332
6662
  agentLog,
6333
6663
  promptVersion: o.route.resolved?.agentPersonas?.[o.role]?.historyId,
@@ -6354,7 +6684,7 @@ function describePlan(p) {
6354
6684
  }
6355
6685
  function openSink(path) {
6356
6686
  try {
6357
- mkdirSync5(dirname5(path), { recursive: true });
6687
+ mkdirSync6(dirname6(path), { recursive: true });
6358
6688
  return createWriteStream(path, { flags: "a" });
6359
6689
  } catch {
6360
6690
  return void 0;
@@ -6463,7 +6793,7 @@ function spawnAgent(plan, emit) {
6463
6793
  provider: plan.provider ?? LEGACY_TOOLCHAIN_PROVIDER
6464
6794
  };
6465
6795
  try {
6466
- writeFileSync4(`${plan.streamPath}.meta.json`, JSON.stringify({
6796
+ writeFileSync5(`${plan.streamPath}.meta.json`, JSON.stringify({
6467
6797
  route: plan.route,
6468
6798
  role: plan.role,
6469
6799
  ticket: plan.ticket ?? null,
@@ -6522,13 +6852,13 @@ function spawnAgent(plan, emit) {
6522
6852
  }
6523
6853
 
6524
6854
  // src/run-worktree.ts
6525
- import { existsSync as existsSync9 } from "fs";
6526
- import { dirname as dirname6, join as join10 } from "path";
6855
+ import { existsSync as existsSync10 } from "fs";
6856
+ import { dirname as dirname7, join as join11 } from "path";
6527
6857
 
6528
6858
  // src/ports.ts
6529
6859
  import { execFileSync as execFileSync2 } from "child_process";
6530
- import { existsSync as existsSync8 } from "fs";
6531
- import { join as join9 } from "path";
6860
+ import { existsSync as existsSync9 } from "fs";
6861
+ import { join as join10 } from "path";
6532
6862
  var run = (cmd, args) => {
6533
6863
  try {
6534
6864
  return execFileSync2(cmd, args, { encoding: "utf8", stdio: ["ignore", "pipe", "ignore"] });
@@ -6573,7 +6903,7 @@ function ticketForPort(port, s = DEFAULT_PORTS) {
6573
6903
  }
6574
6904
  function worktreeExists(parent, prefix, n) {
6575
6905
  for (const name of [`${prefix}${n}`, `${prefix}${String(n).padStart(3, "0")}`]) {
6576
- if (existsSync8(join9(parent, name, ".git"))) return true;
6906
+ if (existsSync9(join10(parent, name, ".git"))) return true;
6577
6907
  }
6578
6908
  return false;
6579
6909
  }
@@ -6647,7 +6977,7 @@ function planRunWorktree(dir, cfg, t, role) {
6647
6977
  const existing = worktreeForNumber(dir, num);
6648
6978
  const located = locateBranchForTicket(dir, cfg, t, role);
6649
6979
  const baseRef = hasRemote ? `${remote}/${base}` : base;
6650
- if (existing && !existing.prunable && existsSync9(existing.path)) {
6980
+ if (existing && !existing.prunable && existsSync10(existing.path)) {
6651
6981
  const branch = existing.branch;
6652
6982
  if (status(existing.path).length > 0) return { action: "leave-dirty", path: existing.path, branch };
6653
6983
  if (!branch) return { action: "leave-unpushed", path: existing.path, branch: "(detached HEAD)" };
@@ -6661,7 +6991,7 @@ function planRunWorktree(dir, cfg, t, role) {
6661
6991
  if (countCommits(existing.path, `${baseRef}..HEAD`) > 0) return { action: "leave-unpushed", path: existing.path, branch };
6662
6992
  return { action: "recut", path: existing.path, branch, base: baseRef };
6663
6993
  }
6664
- const path = join10(dirname6(dir), worktreeForTicket(dir, cfg, t, role));
6994
+ const path = join11(dirname7(dir), worktreeForTicket(dir, cfg, t, role));
6665
6995
  if (located?.where === "remote" || located && hasRemote && remoteBranchExists(dir, remote, located.branch)) {
6666
6996
  return { action: "cut-remote", path, branch: located.branch };
6667
6997
  }
@@ -6708,7 +7038,7 @@ function worktreeHoldsUnpushed(path, remote) {
6708
7038
  }
6709
7039
  async function finishRunWorktree(dir, cfg, branch) {
6710
7040
  const wt = worktrees(dir).find((w) => w.branch === branch && !w.prunable && w.path !== dir);
6711
- if (!wt || !existsSync9(wt.path)) return { kind: "none" };
7041
+ if (!wt || !existsSync10(wt.path)) return { kind: "none" };
6712
7042
  if (cfg.hooks.handoff) return { kind: "kept-handoff", path: wt.path };
6713
7043
  if (status(wt.path).length > 0) return { kind: "kept-dirty", path: wt.path };
6714
7044
  if (remoteConfigured(dir, cfg.branch.remote) && worktreeHoldsUnpushed(wt.path, cfg.branch.remote)) {
@@ -6720,7 +7050,7 @@ async function finishRunWorktree(dir, cfg, branch) {
6720
7050
  if (pids.length) await killGently(pids);
6721
7051
  }
6722
7052
  gitOk(dir, ["worktree", "remove", wt.path]);
6723
- return existsSync9(wt.path) ? { kind: "kept-dirty", path: wt.path } : { kind: "removed", path: wt.path };
7053
+ return existsSync10(wt.path) ? { kind: "kept-dirty", path: wt.path } : { kind: "removed", path: wt.path };
6724
7054
  }
6725
7055
 
6726
7056
  // src/handoff-guard.ts
@@ -6869,13 +7199,13 @@ async function retireRunWorktree(tracker, dir, cfg, t, role, seatId, shipName2,
6869
7199
  }
6870
7200
 
6871
7201
  // src/connect-wizard.ts
6872
- import { delimiter, dirname as dirname7, join as join11 } from "path";
6873
- import { accessSync as accessSync2, constants as constants2, existsSync as existsSync10, mkdirSync as mkdirSync6, writeFileSync as writeFileSync5 } from "fs";
7202
+ import { delimiter, dirname as dirname8, join as join12 } from "path";
7203
+ import { accessSync as accessSync2, constants as constants2, existsSync as existsSync11, mkdirSync as mkdirSync7, writeFileSync as writeFileSync6 } from "fs";
6874
7204
  import { stringify } from "yaml";
6875
7205
  var HOST_PASSENGERS_DOCS_URL = "https://crew.tablation.dev/host-passengers";
6876
7206
  function shouldRunWizard(opts) {
6877
7207
  if (!opts.isTTY) return false;
6878
- const exists = opts.configExists ?? existsSync10;
7208
+ const exists = opts.configExists ?? existsSync11;
6879
7209
  return !configSearchPath(opts.crewHome).some((p) => exists(p));
6880
7210
  }
6881
7211
  function firstRunConfigPath() {
@@ -6892,7 +7222,7 @@ function defaultIsExecutable(p) {
6892
7222
  function findOnPath2(name, pathEnv, isExecutable = defaultIsExecutable) {
6893
7223
  for (const dir of pathEnv.split(delimiter)) {
6894
7224
  if (!dir) continue;
6895
- const candidate = join11(dir, name);
7225
+ const candidate = join12(dir, name);
6896
7226
  if (isExecutable(candidate)) return candidate;
6897
7227
  }
6898
7228
  return void 0;
@@ -6918,9 +7248,9 @@ async function runWizard(p, env) {
6918
7248
  if (entered && isExec(entered)) agentBin = entered;
6919
7249
  else p.say(entered ? `${entered} is not an executable file.` : "A path is required.");
6920
7250
  }
6921
- const nodeDir = dirname7(env.nodePath);
7251
+ const nodeDir = dirname8(env.nodePath);
6922
7252
  const pnpmPath = findOnPath2("pnpm", env.pathEnv, isExec);
6923
- const baseDirs = [nodeDir, pnpmPath ? dirname7(pnpmPath) : void 0];
7253
+ const baseDirs = [nodeDir, pnpmPath ? dirname8(pnpmPath) : void 0];
6924
7254
  p.say(
6925
7255
  `
6926
7256
  Host Passengers lets this machine run the agent sessions that other people's
@@ -6931,7 +7261,7 @@ Tablation workspaces ask for, in a container. More: ${HOST_PASSENGERS_DOCS_URL}`
6931
7261
  p.say("Docker is not available here, so nothing will be hosted until it is. Your answer is recorded anyway.");
6932
7262
  }
6933
7263
  const dockerPath = hostPassengers ? findOnPath2("docker", env.pathEnv, isExec) : void 0;
6934
- const extraPath = deriveExtraPath([...baseDirs, dockerPath ? dirname7(dockerPath) : void 0]);
7264
+ const extraPath = deriveExtraPath([...baseDirs, dockerPath ? dirname8(dockerPath) : void 0]);
6935
7265
  if (extraPath) {
6936
7266
  p.say(`Hooks will get ${extraPath} on their PATH (where node, pnpm${dockerPath ? " and docker" : ""} live).`);
6937
7267
  }
@@ -6953,9 +7283,9 @@ routes:
6953
7283
  ${routeBlock}`;
6954
7284
  }
6955
7285
  function writeNewConfig(path, text) {
6956
- mkdirSync6(dirname7(path), { recursive: true });
7286
+ mkdirSync7(dirname8(path), { recursive: true });
6957
7287
  try {
6958
- writeFileSync5(path, text, { mode: 384, flag: "wx" });
7288
+ writeFileSync6(path, text, { mode: 384, flag: "wx" });
6959
7289
  return true;
6960
7290
  } catch (e) {
6961
7291
  if (e.code === "EEXIST") return false;
@@ -6987,8 +7317,8 @@ Wrote ${o.configPath}.
6987
7317
  }
6988
7318
 
6989
7319
  // src/release-run.ts
6990
- import { readFileSync as readFileSync8, writeFileSync as writeFileSync6, existsSync as existsSync11, copyFileSync, mkdirSync as mkdirSync7, mkdtempSync, rmSync as rmSync3 } from "fs";
6991
- import { dirname as dirname8, join as join12 } from "path";
7320
+ import { readFileSync as readFileSync9, writeFileSync as writeFileSync7, existsSync as existsSync12, copyFileSync, mkdirSync as mkdirSync8, mkdtempSync, rmSync as rmSync4 } from "fs";
7321
+ import { dirname as dirname9, join as join13 } from "path";
6992
7322
  import { tmpdir as tmpdir2 } from "os";
6993
7323
 
6994
7324
  // src/release.ts
@@ -7646,13 +7976,13 @@ async function bump(o, size) {
7646
7976
  }
7647
7977
  return printed;
7648
7978
  }
7649
- const files = o.repo.release.versionFiles.map((f) => join12(o.cwd, f));
7979
+ const files = o.repo.release.versionFiles.map((f) => join13(o.cwd, f));
7650
7980
  const first = files[0];
7651
- if (!existsSync11(first)) {
7981
+ if (!existsSync12(first)) {
7652
7982
  o.emit.error(`no ${o.repo.release.versionFiles[0]} to read a version from`);
7653
7983
  return null;
7654
7984
  }
7655
- const current = JSON.parse(readFileSync8(first, "utf8")).version;
7985
+ const current = JSON.parse(readFileSync9(first, "utf8")).version;
7656
7986
  if (!current) {
7657
7987
  o.emit.error(`no version field in ${o.repo.release.versionFiles[0]}`);
7658
7988
  return null;
@@ -7665,27 +7995,27 @@ async function bump(o, size) {
7665
7995
  return version;
7666
7996
  }
7667
7997
  for (const f of files) {
7668
- const src = readFileSync8(f, "utf8");
7998
+ const src = readFileSync9(f, "utf8");
7669
7999
  const out = src.replace(/("version"\s*:\s*")[^"]*(")/, `$1${version}$2`);
7670
8000
  if (out === src) {
7671
8001
  o.emit.error(`no version field in ${f}`);
7672
8002
  return null;
7673
8003
  }
7674
- writeFileSync6(f, out);
8004
+ writeFileSync7(f, out);
7675
8005
  }
7676
8006
  return version;
7677
8007
  }
7678
8008
  function writeChangelog(o, version, entries) {
7679
8009
  const file = o.repo.release.changelog;
7680
8010
  if (!file || entries.length === 0) return;
7681
- const path = join12(o.cwd, file);
8011
+ const path = join13(o.cwd, file);
7682
8012
  const section = renderChangelogSection(version, (/* @__PURE__ */ new Date()).toISOString().slice(0, 10), entries);
7683
8013
  if (o.dryRun) {
7684
8014
  o.emit.emit(`would prepend ${entries.length} entr(ies) to ${file}`);
7685
8015
  return;
7686
8016
  }
7687
- const existing = existsSync11(path) ? readFileSync8(path, "utf8") : "# Changelog\n";
7688
- writeFileSync6(path, insertChangelogSection(existing, section));
8017
+ const existing = existsSync12(path) ? readFileSync9(path, "utf8") : "# Changelog\n";
8018
+ writeFileSync7(path, insertChangelogSection(existing, section));
7689
8019
  }
7690
8020
  async function confirm(o, expected) {
7691
8021
  if (!o.repo.hooks.released) return void 0;
@@ -7766,11 +8096,11 @@ async function confirmLastRelease(o, decision) {
7766
8096
  function openReleaseCheckout(o) {
7767
8097
  const { remote, base } = o.repo.branch;
7768
8098
  const branch = `crew/release-${Date.now()}`;
7769
- const root = mkdtempSync(join12(tmpdir2(), "crew-release-"));
7770
- const dir = join12(root, "checkout");
8099
+ const root = mkdtempSync(join13(tmpdir2(), "crew-release-"));
8100
+ const dir = join13(root, "checkout");
7771
8101
  const close = () => {
7772
8102
  gitOk(o.cwd, ["worktree", "remove", "--force", dir]);
7773
- rmSync3(root, { recursive: true, force: true });
8103
+ rmSync4(root, { recursive: true, force: true });
7774
8104
  gitOk(o.cwd, ["worktree", "prune"]);
7775
8105
  gitOk(o.cwd, ["branch", "-D", branch]);
7776
8106
  };
@@ -7781,10 +8111,10 @@ function openReleaseCheckout(o) {
7781
8111
  throw e;
7782
8112
  }
7783
8113
  for (const f of o.repo.worktrees.copy) {
7784
- const src = join12(o.cwd, f);
7785
- if (!existsSync11(src)) continue;
7786
- mkdirSync7(dirname8(join12(dir, f)), { recursive: true });
7787
- copyFileSync(src, join12(dir, f));
8114
+ const src = join13(o.cwd, f);
8115
+ if (!existsSync12(src)) continue;
8116
+ mkdirSync8(dirname9(join13(dir, f)), { recursive: true });
8117
+ copyFileSync(src, join13(dir, f));
7788
8118
  }
7789
8119
  return { dir, branch, close };
7790
8120
  }
@@ -8253,7 +8583,7 @@ function missingReleaserWarning(repo, release, ships, now = Date.now()) {
8253
8583
  }
8254
8584
 
8255
8585
  // src/watch.ts
8256
- import { watch as watchFile, existsSync as existsSync12, statSync as statSync6, openSync as openSync3, readSync, closeSync as closeSync3 } from "fs";
8586
+ import { watch as watchFile, existsSync as existsSync13, statSync as statSync6, openSync as openSync3, readSync, closeSync as closeSync3 } from "fs";
8257
8587
  var E = "\x1B[";
8258
8588
  var dim = (s) => `${E}2m${s}${E}0m`;
8259
8589
  var bold = (s) => `${E}1m${s}${E}0m`;
@@ -8354,7 +8684,7 @@ function renderHeaders(states, now = Date.now()) {
8354
8684
  return states.map((s) => renderHeader(s, now)).join("");
8355
8685
  }
8356
8686
  function readFrom(file, from) {
8357
- if (!existsSync12(file)) return { events: [], offset: from };
8687
+ if (!existsSync13(file)) return { events: [], offset: from };
8358
8688
  const size = statSync6(file).size;
8359
8689
  if (size < from) return readFrom(file, 0);
8360
8690
  if (size === from) return { events: [], offset: from };
@@ -8594,8 +8924,8 @@ function nextRoles(decision, ran) {
8594
8924
  }
8595
8925
 
8596
8926
  // src/agents.ts
8597
- import { readFileSync as readFileSync9, existsSync as existsSync13 } from "fs";
8598
- import { join as join13 } from "path";
8927
+ import { readFileSync as readFileSync10, existsSync as existsSync14 } from "fs";
8928
+ import { join as join14 } from "path";
8599
8929
  import { TablationClient as TablationClient2, StaleWriteError as StaleWriteError6, TablationApiError } from "@tablation/client";
8600
8930
  var PERSONA_NAME = {
8601
8931
  dev: "Developer",
@@ -8609,12 +8939,12 @@ var CREW_AGENT_COLUMN = "agent_id";
8609
8939
  var AgentsSyncError = class extends Error {
8610
8940
  };
8611
8941
  function personaDefaultPrompt(promptsDir, role) {
8612
- const briefPath = join13(promptsDir, "personas", `lane-${role}.md`);
8613
- if (!existsSync13(briefPath)) throw new AgentsSyncError(`no brief at ${briefPath}`);
8614
- if (role === "pair") return readFileSync9(briefPath, "utf8");
8615
- const commonPath = join13(promptsDir, "personas", "common.md");
8616
- if (!existsSync13(commonPath)) throw new AgentsSyncError(`no shared policy at ${commonPath}`);
8617
- return `${readFileSync9(commonPath, "utf8")}${readFileSync9(briefPath, "utf8")}`;
8942
+ const briefPath = join14(promptsDir, "personas", `lane-${role}.md`);
8943
+ if (!existsSync14(briefPath)) throw new AgentsSyncError(`no brief at ${briefPath}`);
8944
+ if (role === "pair") return readFileSync10(briefPath, "utf8");
8945
+ const commonPath = join14(promptsDir, "personas", "common.md");
8946
+ if (!existsSync14(commonPath)) throw new AgentsSyncError(`no shared policy at ${commonPath}`);
8947
+ return `${readFileSync10(commonPath, "utf8")}${readFileSync10(briefPath, "utf8")}`;
8618
8948
  }
8619
8949
  async function resolveAgentId(route, role, opts) {
8620
8950
  const cached = route.resolved?.agentPersonas?.[role]?.agentId;
@@ -9291,14 +9621,14 @@ import {
9291
9621
  } from "@tablation/client";
9292
9622
 
9293
9623
  // src/skills.ts
9294
- import { readFileSync as readFileSync10, readdirSync as readdirSync2, existsSync as existsSync14 } from "fs";
9295
- import { join as join14 } from "path";
9624
+ import { readFileSync as readFileSync11, readdirSync as readdirSync2, existsSync as existsSync15 } from "fs";
9625
+ import { join as join15 } from "path";
9296
9626
  import { parse as parseYaml } from "yaml";
9297
9627
  import { TablationClient as TablationClient3, StaleWriteError as StaleWriteError7, TablationApiError as TablationApiError2 } from "@tablation/client";
9298
9628
  var SkillSyncError = class extends Error {
9299
9629
  };
9300
9630
  function parseSkillFile(path) {
9301
- const text = readFileSync10(path, "utf8");
9631
+ const text = readFileSync11(path, "utf8");
9302
9632
  const match = /^---\n([\s\S]*?)\n---\n([\s\S]*)$/.exec(text);
9303
9633
  if (!match) throw new SkillSyncError(`${path}: missing YAML frontmatter (expected a leading --- block)`);
9304
9634
  const frontmatter = match[1];
@@ -9311,9 +9641,9 @@ function parseSkillFile(path) {
9311
9641
  return { name: meta.name, description: meta.description, prompt: body, file: path };
9312
9642
  }
9313
9643
  function listLocalSkills(promptsDir) {
9314
- const dir = join14(promptsDir, "skills");
9315
- if (!existsSync14(dir)) return [];
9316
- return readdirSync2(dir).filter((f) => f.endsWith(".md")).map((f) => parseSkillFile(join14(dir, f)));
9644
+ const dir = join15(promptsDir, "skills");
9645
+ if (!existsSync15(dir)) return [];
9646
+ return readdirSync2(dir).filter((f) => f.endsWith(".md")).map((f) => parseSkillFile(join15(dir, f)));
9317
9647
  }
9318
9648
  async function syncSkills(route, opts) {
9319
9649
  if (!route.resolved) {
@@ -9492,7 +9822,7 @@ async function showLogEntry(route, entryId, opts = {}) {
9492
9822
  }
9493
9823
 
9494
9824
  // src/repos-cmd.ts
9495
- import { homedir as homedir4 } from "os";
9825
+ import { homedir as homedir5 } from "os";
9496
9826
  import { basename as basename7, resolve as resolve3 } from "path";
9497
9827
  import { isMap, isSeq, parseDocument, stringify as stringify2 } from "yaml";
9498
9828
  var ReposError = class extends Error {
@@ -9539,7 +9869,7 @@ function parseAddArgs(args, routes, isDir) {
9539
9869
  return { route, path: "." };
9540
9870
  }
9541
9871
  function expandDir(p, base) {
9542
- const home = p === "~" ? homedir4() : p.startsWith("~/") ? `${homedir4()}/${p.slice(2)}` : p;
9872
+ const home = p === "~" ? homedir5() : p.startsWith("~/") ? `${homedir5()}/${p.slice(2)}` : p;
9543
9873
  return resolve3(base, home);
9544
9874
  }
9545
9875
  function routeMapOf(doc, route) {
@@ -9674,7 +10004,7 @@ function schedulerInstalled(plan, exists, crontab) {
9674
10004
  }
9675
10005
 
9676
10006
  // src/worktree-sweep.ts
9677
- import { existsSync as existsSync15 } from "fs";
10007
+ import { existsSync as existsSync16 } from "fs";
9678
10008
  import { basename as basename8 } from "path";
9679
10009
  function planWorktreeSweep(target, tickets, contract) {
9680
10010
  const terminal = new Set(contract.statuses.resolved.filter((s) => s !== contract.statuses.verified));
@@ -9702,7 +10032,7 @@ async function applyWorktreeSweep(cwd, actions, dryRun, log, remote = "origin")
9702
10032
  let removed = 0;
9703
10033
  let keptBranches = 0;
9704
10034
  for (const a of actions) {
9705
- if (!dryRun && existsSync15(a.path)) {
10035
+ if (!dryRun && existsSync16(a.path)) {
9706
10036
  let dirty = false;
9707
10037
  try {
9708
10038
  dirty = status(a.path).length > 0;
@@ -9716,7 +10046,7 @@ async function applyWorktreeSweep(cwd, actions, dryRun, log, remote = "origin")
9716
10046
  continue;
9717
10047
  }
9718
10048
  }
9719
- if (!dryRun && a.requirePushed && a.branch && existsSync15(a.path) && remoteConfigured(cwd, remote) && worktreeHoldsUnpushed(a.path, remote)) {
10049
+ if (!dryRun && a.requirePushed && a.branch && existsSync16(a.path) && remoteConfigured(cwd, remote) && worktreeHoldsUnpushed(a.path, remote)) {
9720
10050
  log.warn(
9721
10051
  `not removing worktree ${a.path} for ${a.ticket.issue_id} (${a.ticket.status}): ${a.branch} has commits that are not on ${remote}`,
9722
10052
  { ticket: a.ticket.issue_id, step: "worktree" }
@@ -9797,14 +10127,14 @@ function applyRemoteBranchCleanup(cwd, remote, branches2, dryRun, log) {
9797
10127
  }
9798
10128
 
9799
10129
  // src/stream-sweep.ts
9800
- import { existsSync as existsSync16, readdirSync as readdirSync3, rmSync as rmSync4, statSync as statSync7 } from "fs";
9801
- import { join as join15 } from "path";
10130
+ import { existsSync as existsSync17, readdirSync as readdirSync3, rmSync as rmSync5, statSync as statSync7 } from "fs";
10131
+ import { join as join16 } from "path";
9802
10132
  var DAY_MS2 = 24 * 60 * 60 * 1e3;
9803
10133
  function planStreamSweep(stateDir, retentionDays, now = Date.now()) {
9804
- const dir = join15(stateDir, "streams");
9805
- if (!existsSync16(dir)) return [];
10134
+ const dir = join16(stateDir, "streams");
10135
+ if (!existsSync17(dir)) return [];
9806
10136
  const cutoff = now - retentionDays * DAY_MS2;
9807
- return readdirSync3(dir).map((name) => join15(dir, name)).filter((path) => {
10137
+ return readdirSync3(dir).map((name) => join16(dir, name)).filter((path) => {
9808
10138
  try {
9809
10139
  return statSync7(path).mtimeMs < cutoff;
9810
10140
  } catch {
@@ -9820,7 +10150,7 @@ function applyStreamSweep(paths, dryRun, log) {
9820
10150
  continue;
9821
10151
  }
9822
10152
  try {
9823
- rmSync4(path, { force: true });
10153
+ rmSync5(path, { force: true });
9824
10154
  removed++;
9825
10155
  log.emit(`removed stream artifact ${path}`, { step: "worktree" });
9826
10156
  } catch (e) {
@@ -9830,9 +10160,9 @@ function applyStreamSweep(paths, dryRun, log) {
9830
10160
  return removed;
9831
10161
  }
9832
10162
  function planEventsSweep(stateDir, retentionDays, now = Date.now()) {
9833
- if (!existsSync16(stateDir)) return [];
10163
+ if (!existsSync17(stateDir)) return [];
9834
10164
  const cutoff = now - retentionDays * DAY_MS2;
9835
- return readdirSync3(stateDir).filter((name) => ROTATED_EVENTS_RE.test(name)).map((name) => join15(stateDir, name)).filter((path) => {
10165
+ return readdirSync3(stateDir).filter((name) => ROTATED_EVENTS_RE.test(name)).map((name) => join16(stateDir, name)).filter((path) => {
9836
10166
  try {
9837
10167
  return statSync7(path).mtimeMs < cutoff;
9838
10168
  } catch {
@@ -9842,24 +10172,24 @@ function planEventsSweep(stateDir, retentionDays, now = Date.now()) {
9842
10172
  }
9843
10173
 
9844
10174
  // src/cli.ts
9845
- import { readFileSync as readFileSync14, existsSync as existsSync20, mkdirSync as mkdirSync11, writeFileSync as writeFileSync9, unlinkSync as unlinkSync5, copyFileSync as copyFileSync2, renameSync as renameSync2, statSync as statSync9 } from "fs";
10175
+ import { readFileSync as readFileSync15, existsSync as existsSync21, mkdirSync as mkdirSync12, writeFileSync as writeFileSync10, unlinkSync as unlinkSync5, copyFileSync as copyFileSync2, renameSync as renameSync2, statSync as statSync9 } from "fs";
9846
10176
  import { createInterface } from "readline/promises";
9847
10177
  import { dirname as dirOf, resolve as resolvePath } from "path";
9848
10178
 
9849
10179
  // src/ssh-keys.ts
9850
10180
  import { execFileSync as execFileSync3 } from "child_process";
9851
- import { existsSync as existsSync17, mkdirSync as mkdirSync8, readFileSync as readFileSync11, chmodSync } from "fs";
9852
- import { dirname as dirname10, join as join16 } from "path";
10181
+ import { existsSync as existsSync18, mkdirSync as mkdirSync9, readFileSync as readFileSync12, chmodSync } from "fs";
10182
+ import { dirname as dirname11, join as join17 } from "path";
9853
10183
  var SshKeyError = class extends Error {
9854
10184
  };
9855
10185
  function shipSshKeyPathFor(stateDir) {
9856
- return join16(stateDir, "ssh", "id_ed25519");
10186
+ return join17(stateDir, "ssh", "id_ed25519");
9857
10187
  }
9858
10188
  function ensureShipSshKeypair(stateDir) {
9859
10189
  const privateKeyPath = shipSshKeyPathFor(stateDir);
9860
10190
  const publicKeyPath = `${privateKeyPath}.pub`;
9861
- if (!existsSync17(privateKeyPath)) {
9862
- mkdirSync8(dirname10(privateKeyPath), { recursive: true });
10191
+ if (!existsSync18(privateKeyPath)) {
10192
+ mkdirSync9(dirname11(privateKeyPath), { recursive: true });
9863
10193
  try {
9864
10194
  execFileSync3("ssh-keygen", [
9865
10195
  "-t",
@@ -9879,10 +10209,10 @@ function ensureShipSshKeypair(stateDir) {
9879
10209
  }
9880
10210
  chmodSync(privateKeyPath, 384);
9881
10211
  }
9882
- if (!existsSync17(publicKeyPath)) {
10212
+ if (!existsSync18(publicKeyPath)) {
9883
10213
  throw new SshKeyError(`${privateKeyPath} exists but ${publicKeyPath} does not \u2014 regenerate or restore it`);
9884
10214
  }
9885
- const publicKey = readFileSync11(publicKeyPath, "utf8").trim();
10215
+ const publicKey = readFileSync12(publicKeyPath, "utf8").trim();
9886
10216
  return { privateKeyPath, publicKeyPath, publicKey };
9887
10217
  }
9888
10218
  function sshKeygenAvailable() {
@@ -10096,9 +10426,9 @@ function syncAllPassengerCheckouts(cfg) {
10096
10426
 
10097
10427
  // src/tunnel.ts
10098
10428
  import { spawn as spawn3 } from "child_process";
10099
- import { existsSync as existsSync18, mkdirSync as mkdirSync9, readFileSync as readFileSync12, writeFileSync as writeFileSync7, unlinkSync as unlinkSync3, readdirSync as readdirSync4 } from "fs";
10429
+ import { existsSync as existsSync19, mkdirSync as mkdirSync10, readFileSync as readFileSync13, writeFileSync as writeFileSync8, unlinkSync as unlinkSync3, readdirSync as readdirSync4 } from "fs";
10100
10430
  import { randomInt } from "crypto";
10101
- import { join as join17 } from "path";
10431
+ import { join as join18 } from "path";
10102
10432
  var DEFAULT_SPAWN_OPTIONS = { stdio: "ignore", detached: true };
10103
10433
  var defaultSpawn = (cmd, args) => spawn3(cmd, args, DEFAULT_SPAWN_OPTIONS);
10104
10434
  var SLUG_ALPHABET = "abcdefghijklmnopqrstuvwxyz0123456789";
@@ -10165,21 +10495,21 @@ function startTunnel(opts) {
10165
10495
  };
10166
10496
  }
10167
10497
  function tunnelStatePath(stateDir, workspaceId) {
10168
- return join17(stateDir, "passengers", `${workspaceId}.json`);
10498
+ return join18(stateDir, "passengers", `${workspaceId}.json`);
10169
10499
  }
10170
10500
  function readPersistedTunnel(stateDir, workspaceId) {
10171
10501
  const path = tunnelStatePath(stateDir, workspaceId);
10172
- if (!existsSync18(path)) return void 0;
10502
+ if (!existsSync19(path)) return void 0;
10173
10503
  try {
10174
- return JSON.parse(readFileSync12(path, "utf8"));
10504
+ return JSON.parse(readFileSync13(path, "utf8"));
10175
10505
  } catch {
10176
10506
  return void 0;
10177
10507
  }
10178
10508
  }
10179
10509
  function writePersistedTunnel(stateDir, state) {
10180
10510
  const path = tunnelStatePath(stateDir, state.workspaceId);
10181
- mkdirSync9(join17(stateDir, "passengers"), { recursive: true });
10182
- writeFileSync7(path, `${JSON.stringify(state, null, 2)}
10511
+ mkdirSync10(join18(stateDir, "passengers"), { recursive: true });
10512
+ writeFileSync8(path, `${JSON.stringify(state, null, 2)}
10183
10513
  `);
10184
10514
  }
10185
10515
  function updatePersistedTunnel(stateDir, workspaceId, patch2) {
@@ -10276,8 +10606,8 @@ function syncPassengerTunnels(plans, relayHost, relayPort, privateKeyPath, state
10276
10606
  }
10277
10607
  }
10278
10608
  function readAllPersistedWorkspaceIds(stateDir) {
10279
- const dir = join17(stateDir, "passengers");
10280
- if (!existsSync18(dir)) return [];
10609
+ const dir = join18(stateDir, "passengers");
10610
+ if (!existsSync19(dir)) return [];
10281
10611
  return readdirSync4(dir).filter((f) => f.endsWith(".json") && !f.endsWith("-sync.json")).map((f) => f.slice(0, -".json".length));
10282
10612
  }
10283
10613
 
@@ -10398,8 +10728,8 @@ async function unregisterEndpoint(workspaceId, info, d) {
10398
10728
  import { createServer as createHttpServer } from "http";
10399
10729
  import { spawn as spawn4 } from "child_process";
10400
10730
  import { randomBytes } from "crypto";
10401
- import { existsSync as existsSync19, mkdirSync as mkdirSync10, readFileSync as readFileSync13, writeFileSync as writeFileSync8, unlinkSync as unlinkSync4, readdirSync as readdirSync5 } from "fs";
10402
- import { join as join18 } from "path";
10731
+ import { existsSync as existsSync20, mkdirSync as mkdirSync11, readFileSync as readFileSync14, writeFileSync as writeFileSync9, unlinkSync as unlinkSync4, readdirSync as readdirSync5 } from "fs";
10732
+ import { join as join19 } from "path";
10403
10733
 
10404
10734
  // src/secret-compare.ts
10405
10735
  import { timingSafeEqual } from "crypto";
@@ -10488,21 +10818,21 @@ var defaultSpawn2 = (cmd, args, env) => {
10488
10818
  return child;
10489
10819
  };
10490
10820
  function syncDaemonStatePath(stateDir, workspaceId) {
10491
- return join18(stateDir, "passengers", `${workspaceId}-sync.json`);
10821
+ return join19(stateDir, "passengers", `${workspaceId}-sync.json`);
10492
10822
  }
10493
10823
  function readPersistedSyncDaemon(stateDir, workspaceId) {
10494
10824
  const path = syncDaemonStatePath(stateDir, workspaceId);
10495
- if (!existsSync19(path)) return void 0;
10825
+ if (!existsSync20(path)) return void 0;
10496
10826
  try {
10497
- return JSON.parse(readFileSync13(path, "utf8"));
10827
+ return JSON.parse(readFileSync14(path, "utf8"));
10498
10828
  } catch {
10499
10829
  return void 0;
10500
10830
  }
10501
10831
  }
10502
10832
  function writePersistedSyncDaemon(stateDir, state) {
10503
10833
  const path = syncDaemonStatePath(stateDir, state.workspaceId);
10504
- mkdirSync10(join18(stateDir, "passengers"), { recursive: true });
10505
- writeFileSync8(path, `${JSON.stringify(state, null, 2)}
10834
+ mkdirSync11(join19(stateDir, "passengers"), { recursive: true });
10835
+ writeFileSync9(path, `${JSON.stringify(state, null, 2)}
10506
10836
  `);
10507
10837
  }
10508
10838
  function removePersistedSyncDaemon(stateDir, workspaceId) {
@@ -10522,8 +10852,8 @@ function waitForExit(pid, alive, timeoutMs = 5e3) {
10522
10852
  }
10523
10853
  }
10524
10854
  function readAllPersistedSyncWorkspaceIds(stateDir) {
10525
- const dir = join18(stateDir, "passengers");
10526
- if (!existsSync19(dir)) return [];
10855
+ const dir = join19(stateDir, "passengers");
10856
+ if (!existsSync20(dir)) return [];
10527
10857
  const suffix = "-sync.json";
10528
10858
  return readdirSync5(dir).filter((f) => f.endsWith(suffix)).map((f) => f.slice(0, -suffix.length));
10529
10859
  }
@@ -10581,7 +10911,7 @@ function syncPassengerSyncDaemons(plans, cfg, stateDir, deps = {}) {
10581
10911
 
10582
10912
  // src/daemon.ts
10583
10913
  import { readdirSync as readdirSync6, statSync as statSync8 } from "fs";
10584
- import { join as join19 } from "path";
10914
+ import { join as join20 } from "path";
10585
10915
  var DEFAULT_FLOOR_MS = 5e3;
10586
10916
  var DEFAULT_CEILING_MS = INTERVAL_SECONDS * 1e3;
10587
10917
  var defaultSleep = (ms2) => new Promise((resolve5) => setTimeout(resolve5, ms2));
@@ -10620,7 +10950,7 @@ function latestMtime(path) {
10620
10950
  if (!stat.isDirectory()) return 0;
10621
10951
  let latest = stat.mtimeMs;
10622
10952
  for (const entry of readdirSync6(path, { withFileTypes: true })) {
10623
- latest = Math.max(latest, latestMtime(join19(path, entry.name)));
10953
+ latest = Math.max(latest, latestMtime(join20(path, entry.name)));
10624
10954
  }
10625
10955
  return latest;
10626
10956
  }
@@ -10906,7 +11236,7 @@ async function buildEnvironment(route, ship, ticket) {
10906
11236
  }
10907
11237
 
10908
11238
  // src/cli.ts
10909
- var CREW_HOME = isCompiledBinary(import.meta.url) ? dirname11(process.execPath) : resolve4(dirname11(fileURLToPath(import.meta.url)), "..");
11239
+ var CREW_HOME = isCompiledBinary(import.meta.url) ? dirname12(process.execPath) : resolve4(dirname12(fileURLToPath(import.meta.url)), "..");
10910
11240
  var RELEASE_LOCK_TTL_MS = 20 * 60 * 1e3;
10911
11241
  var CYCLE_SECONDS2 = 120;
10912
11242
  var STALE_CYCLES = 3;
@@ -11040,8 +11370,9 @@ function usage() {
11040
11370
  crew log [route] tail the log
11041
11371
  crew inbox [--member NAME] your tickets across every workspace (or a colleague's)
11042
11372
  crew connect resolve a workspace's ids into a crew.yaml block (--no-mcp skips registering the Tablation MCP server with Claude Code; --mcp replace overwrites a different one)
11373
+ crew disconnect remove what connect installed for Pair (Claude MCP; Codex/Gemini instructions block and MCP entry per ship.pair)
11043
11374
  crew agents sync [route] [--force] push crew's personas AND skill files into the workspace Agents/Agent Skills tables (--force overwrites even a diverged persona)
11044
- crew agents prompt R [route] print one persona's current prompt (e.g. R=pair, for a SessionStart hook)
11375
+ crew agents prompt R [route] print one persona's current prompt (e.g. R=pair, for a SessionStart hook; --harness H adds which CLI it serves)
11045
11376
  crew skills sync [route] push only crew's skill files (e.g. grill-me) into the workspace Agent Skills table
11046
11377
  crew repos add [ROUTE] [NAME] [PATH] attach a local checkout to a route in crew.yaml (--dry-run previews)
11047
11378
  crew repos list ROUTE the checkouts a route has, with the tracker's Repos row each matches
@@ -11080,7 +11411,7 @@ var value = (name) => {
11080
11411
  var positional = argv.filter((a, i) => !a.startsWith("--") && !argv[i - 1]?.startsWith("--"));
11081
11412
  var command = positional[0];
11082
11413
  if (!command) usage();
11083
- var STATE_WRITING_COMMANDS = /* @__PURE__ */ new Set(["connect", "install", "uninstall", "release", "merge", "deploy"]);
11414
+ var STATE_WRITING_COMMANDS = /* @__PURE__ */ new Set(["connect", "disconnect", "install", "uninstall", "release", "merge", "deploy"]);
11084
11415
  var STATE_WRITING_SUBCOMMANDS = {
11085
11416
  agents: /* @__PURE__ */ new Set(["sync"]),
11086
11417
  skills: /* @__PURE__ */ new Set(["sync"])
@@ -11190,7 +11521,7 @@ if (command === "passenger-sync-daemon") {
11190
11521
  const releaseFleetWide = ["merge", "deploy", "release"].includes(command) && flag("fleet");
11191
11522
  const DAEMON_SUBCOMMANDS = /* @__PURE__ */ new Set(["start", "stop", "status", "restart"]);
11192
11523
  const named = command === "daemon" && DAEMON_SUBCOMMANDS.has(positional[1] ?? "") ? positional[2] : positional[1];
11193
- const fleetWide = command === "inbox" || command === "connect" || command === "agents" || command === "skills" || command === "logbook" || command === "repos" || command === "projects" || releaseFleetWide || command === "pause" || command === "resume" || FLEET_CAPABLE.has(command) && !named && cfg.routes.length > 1;
11524
+ const fleetWide = command === "inbox" || command === "connect" || command === "disconnect" || command === "agents" || command === "skills" || command === "logbook" || command === "repos" || command === "projects" || releaseFleetWide || command === "pause" || command === "resume" || FLEET_CAPABLE.has(command) && !named && cfg.routes.length > 1;
11194
11525
  let route;
11195
11526
  try {
11196
11527
  route = fleetWide ? cfg.routes[0] : findRoute(cfg, named);
@@ -11396,8 +11727,8 @@ if (command === "passenger-sync-daemon") {
11396
11727
  }
11397
11728
  if (host === "linux" && hasSystemd) {
11398
11729
  const label = labelFor(CREW_HOME, "run");
11399
- const staleTimer = join20(homedir5(), ".config", "systemd", "user", `${label}.timer`);
11400
- if (existsSync20(staleTimer)) {
11730
+ const staleTimer = join21(homedir6(), ".config", "systemd", "user", `${label}.timer`);
11731
+ if (existsSync21(staleTimer)) {
11401
11732
  process.stdout.write(
11402
11733
  `removing stale run timer from before the persistent-service cutover: ${staleTimer}${dryRun ? " (dry run)" : ""}
11403
11734
  `
@@ -11423,7 +11754,7 @@ if (command === "passenger-sync-daemon") {
11423
11754
  const tracker = new Tracker(c, cfg.ship);
11424
11755
  problems.push(...validateContract(tracker.contract).map((p) => `contract: ${p}`));
11425
11756
  for (const r of await resolvedRepos(c)) {
11426
- if (!existsSync20(r.dir)) problems.push(`repo ${r.name}: ${r.dir} is missing on disk`);
11757
+ if (!existsSync21(r.dir)) problems.push(`repo ${r.name}: ${r.dir} is missing on disk`);
11427
11758
  if (!satisfies(host, r.config.platform)) problems.push(`repo ${r.name}: requires ${r.config.platform}, this host is ${host}`);
11428
11759
  problems.push(...validateEffective(r.config).map((p) => `repo ${r.name}: ${p}`));
11429
11760
  }
@@ -11441,7 +11772,7 @@ if (command === "passenger-sync-daemon") {
11441
11772
  }
11442
11773
  async function doctorFix(o) {
11443
11774
  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))));
11444
- const unitExists = (p) => existsSync20(p);
11775
+ const unitExists = (p) => existsSync21(p);
11445
11776
  const crontab = o.host === "linux" ? spawnSync3("crontab", ["-l"], { encoding: "utf8" }).stdout ?? "" : "";
11446
11777
  const schedulerAbsent = o.host !== "windows" ? !schedulerInstalled(planInstall(cfg.ship, CREW_HOME, o.host, detectSystemd(), "run"), unitExists, crontab) : true;
11447
11778
  const plan = planFix({ routes, schedulerAbsent, host: o.host });
@@ -11458,7 +11789,7 @@ if (command === "passenger-sync-daemon") {
11458
11789
  for (const a of plan.actions) {
11459
11790
  if (a.kind === "enable") {
11460
11791
  if (prompter && !await prompter.confirm(`Route ${a.route} passes every check \u2014 set enabled: true in ${cfg.configFile}?`, false)) continue;
11461
- writeFileSync9(cfg.configFile, planEnable(readFileSync14(cfg.configFile, "utf8"), a.route));
11792
+ writeFileSync10(cfg.configFile, planEnable(readFileSync15(cfg.configFile, "utf8"), a.route));
11462
11793
  enabledSound.add(a.route);
11463
11794
  changed2.push(`enabled route ${a.route} in ${cfg.configFile}`);
11464
11795
  } else {
@@ -12355,7 +12686,7 @@ ${changed2.map((c) => ` - ${c}
12355
12686
  process.exit(1);
12356
12687
  }
12357
12688
  requireArmed("run the daemon loop");
12358
- const watchPaths = isCompiledBinary(import.meta.url) ? [process.execPath] : [join20(CREW_HOME, "src"), join20(CREW_HOME, "dist"), join20(CREW_HOME, "bin")];
12689
+ const watchPaths = isCompiledBinary(import.meta.url) ? [process.execPath] : [join21(CREW_HOME, "src"), join21(CREW_HOME, "dist"), join21(CREW_HOME, "bin")];
12359
12690
  const spawnBreaker = new SpawnBreaker();
12360
12691
  const stopReason = await runDaemonLoop({
12361
12692
  runPass: () => runOnePass({
@@ -12441,7 +12772,7 @@ ${changed2.map((c) => ` - ${c}
12441
12772
  }
12442
12773
  try {
12443
12774
  const prompt = await currentPersonaPrompt(promptTarget, role, { userAgent: cfg.ship.userAgent });
12444
- process.stdout.write(prompt);
12775
+ process.stdout.write(prompt + (role === "pair" ? harnessPromptNote(value("harness")) : ""));
12445
12776
  } catch (e) {
12446
12777
  if (e instanceof AgentsSyncError) {
12447
12778
  process.stderr.write(`crew agents prompt: ${e.message}
@@ -12504,11 +12835,11 @@ ${changed2.map((c) => ` - ${c}
12504
12835
  `);
12505
12836
  if (!dryRun) {
12506
12837
  const resolvedPath = resolvedPathFor(cfg.ship.stateDir, target.route);
12507
- const raw = existsSync20(resolvedPath) ? JSON.parse(readFileSync14(resolvedPath, "utf8")) : {};
12838
+ const raw = existsSync21(resolvedPath) ? JSON.parse(readFileSync15(resolvedPath, "utf8")) : {};
12508
12839
  raw.agentPersonas = personaResult.agentPersonas;
12509
12840
  raw.agentSkills = skillResult.agentSkills;
12510
- mkdirSync11(dirOf(resolvedPath), { recursive: true });
12511
- writeFileSync9(resolvedPath, `${JSON.stringify(raw, null, 2)}
12841
+ mkdirSync12(dirOf(resolvedPath), { recursive: true });
12842
+ writeFileSync10(resolvedPath, `${JSON.stringify(raw, null, 2)}
12512
12843
  `);
12513
12844
  }
12514
12845
  if (personaResult.outcomes.some((o) => o.action === "diverged") || skillResult.outcomes.some((o) => o.action === "diverged")) {
@@ -12553,10 +12884,10 @@ ${changed2.map((c) => ` - ${c}
12553
12884
  `);
12554
12885
  if (!dryRun) {
12555
12886
  const resolvedPath = resolvedPathFor(cfg.ship.stateDir, target.route);
12556
- const raw = existsSync20(resolvedPath) ? JSON.parse(readFileSync14(resolvedPath, "utf8")) : {};
12887
+ const raw = existsSync21(resolvedPath) ? JSON.parse(readFileSync15(resolvedPath, "utf8")) : {};
12557
12888
  raw.agentSkills = result.agentSkills;
12558
- mkdirSync11(dirOf(resolvedPath), { recursive: true });
12559
- writeFileSync9(resolvedPath, `${JSON.stringify(raw, null, 2)}
12889
+ mkdirSync12(dirOf(resolvedPath), { recursive: true });
12890
+ writeFileSync10(resolvedPath, `${JSON.stringify(raw, null, 2)}
12560
12891
  `);
12561
12892
  }
12562
12893
  if (result.outcomes.some((o) => o.action === "diverged")) process.exitCode = 1;
@@ -12569,14 +12900,14 @@ ${changed2.map((c) => ` - ${c}
12569
12900
  process.exit(2);
12570
12901
  };
12571
12902
  if (sub !== "add" && sub !== "list") usage2();
12572
- const configText = readFileSync14(cfg.configFile, "utf8");
12903
+ const configText = readFileSync15(cfg.configFile, "utf8");
12573
12904
  let routeName = positional[2];
12574
12905
  let addPath = ".";
12575
12906
  let addName;
12576
12907
  if (sub === "list" && !routeName) usage2();
12577
12908
  if (sub === "add") {
12578
12909
  try {
12579
- const a = parseAddArgs(positional.slice(2), routeNamesOf(configText), (p) => existsSync20(resolvePath(p.replace(/^~(?=\/|$)/, homedir5()))));
12910
+ const a = parseAddArgs(positional.slice(2), routeNamesOf(configText), (p) => existsSync21(resolvePath(p.replace(/^~(?=\/|$)/, homedir6()))));
12580
12911
  routeName = a.route;
12581
12912
  addPath = a.path;
12582
12913
  addName = a.name;
@@ -12591,15 +12922,15 @@ ${changed2.map((c) => ` - ${c}
12591
12922
  }
12592
12923
  const resolvedFile = resolvedPathFor(cfg.ship.stateDir, routeName);
12593
12924
  let resolvedRemotes;
12594
- if (existsSync20(resolvedFile)) {
12925
+ if (existsSync21(resolvedFile)) {
12595
12926
  try {
12596
- resolvedRemotes = JSON.parse(readFileSync14(resolvedFile, "utf8"));
12927
+ resolvedRemotes = JSON.parse(readFileSync15(resolvedFile, "utf8"));
12597
12928
  } catch {
12598
12929
  }
12599
12930
  }
12600
12931
  const originOf = (dir) => gitOk(dir, ["remote", "get-url", "origin"]) ?? void 0;
12601
12932
  const text = configText;
12602
- const base = dirname11(cfg.configFile);
12933
+ const base = dirname12(cfg.configFile);
12603
12934
  try {
12604
12935
  if (sub === "list") {
12605
12936
  const repos = configuredRepos(text, routeName, base);
@@ -12608,14 +12939,14 @@ ${changed2.map((c) => ` - ${c}
12608
12939
  for (const r of repos) {
12609
12940
  const origin2 = originOf(r.dir);
12610
12941
  const row = matchRepoName(origin2, resolvedRemotes);
12611
- const note = !existsSync20(r.dir) ? "missing on disk" : row ? `Repos row: ${row}` : `no Repos row matches ${normalizeRemote(origin2) ?? "this checkout (no origin remote)"}`;
12942
+ const note = !existsSync21(r.dir) ? "missing on disk" : row ? `Repos row: ${row}` : `no Repos row matches ${normalizeRemote(origin2) ?? "this checkout (no origin remote)"}`;
12612
12943
  process.stdout.write(`${r.name} ${r.dir} ${note}
12613
12944
  `);
12614
12945
  }
12615
12946
  break;
12616
12947
  }
12617
- const target = resolvePath(addPath.replace(/^~(?=\/|$)/, homedir5()));
12618
- const top = existsSync20(target) ? gitOk(target, ["rev-parse", "--show-toplevel"]) : null;
12948
+ const target = resolvePath(addPath.replace(/^~(?=\/|$)/, homedir6()));
12949
+ const top = existsSync21(target) ? gitOk(target, ["rev-parse", "--show-toplevel"]) : null;
12619
12950
  if (!top) throw new ReposError(`${target} is not a git checkout`);
12620
12951
  const origin = originOf(top);
12621
12952
  const plan = planRepoAdd({
@@ -12663,15 +12994,15 @@ ${changed2.map((c) => ` - ${c}
12663
12994
  const created = await rowTracker.createRepoRow(rowPayload);
12664
12995
  process.stdout.write(`created Repos row ${String(rowPayload.name)} (${String(rowPayload.remote)}) ${created.id}
12665
12996
  `);
12666
- if (existsSync20(resolvedFile)) {
12667
- const raw = JSON.parse(readFileSync14(resolvedFile, "utf8"));
12997
+ if (existsSync21(resolvedFile)) {
12998
+ const raw = JSON.parse(readFileSync15(resolvedFile, "utf8"));
12668
12999
  raw.repoNames = { ...raw.repoNames, [created.id]: String(rowPayload.name) };
12669
13000
  raw.repoRemotes = { ...raw.repoRemotes, [created.id]: String(rowPayload.remote) };
12670
- writeFileSync9(resolvedFile, `${JSON.stringify(raw, null, 2)}
13001
+ writeFileSync10(resolvedFile, `${JSON.stringify(raw, null, 2)}
12671
13002
  `);
12672
13003
  }
12673
13004
  }
12674
- writeFileSync9(cfg.configFile, plan.text);
13005
+ writeFileSync10(cfg.configFile, plan.text);
12675
13006
  process.stdout.write(`added ${top} to ${routeName} as ${plan.mode === "dir" ? "dir" : `repos.${plan.name}`} in ${cfg.configFile}
12676
13007
  `);
12677
13008
  } catch (e) {
@@ -12910,10 +13241,10 @@ Signed in as ${result.identity.email} \u2014 minted an API key in workspace "${r
12910
13241
  operator = await pickOperator(found.holds);
12911
13242
  }
12912
13243
  let previous;
12913
- const hadPreviousFile = existsSync20(resolvedPath);
13244
+ const hadPreviousFile = existsSync21(resolvedPath);
12914
13245
  if (hadPreviousFile) {
12915
13246
  try {
12916
- previous = JSON.parse(readFileSync14(resolvedPath, "utf8"));
13247
+ previous = JSON.parse(readFileSync15(resolvedPath, "utf8"));
12917
13248
  } catch {
12918
13249
  }
12919
13250
  }
@@ -12974,10 +13305,10 @@ Signed in as ${result.identity.email} \u2014 minted an API key in workspace "${r
12974
13305
  process.stderr.write(`(dry run) would write resolved ids to ${resolvedPath}
12975
13306
  `);
12976
13307
  } else {
12977
- mkdirSync11(dirname11(resolvedPath), { recursive: true });
13308
+ mkdirSync12(dirname12(resolvedPath), { recursive: true });
12978
13309
  if (hadPreviousFile) copyFileSync2(resolvedPath, `${resolvedPath}.bak`);
12979
13310
  const tmpPath = `${resolvedPath}.tmp${process.pid}`;
12980
- writeFileSync9(tmpPath, `${JSON.stringify(resolved, null, 2)}
13311
+ writeFileSync10(tmpPath, `${JSON.stringify(resolved, null, 2)}
12981
13312
  `);
12982
13313
  renameSync2(tmpPath, resolvedPath);
12983
13314
  process.stderr.write(`Resolved ids written to ${resolvedPath}
@@ -12991,8 +13322,8 @@ Signed in as ${result.identity.email} \u2014 minted an API key in workspace "${r
12991
13322
  process.stderr.write(`(dry run) would write the minted API key to ${apiKeyFile}
12992
13323
  `);
12993
13324
  } else {
12994
- mkdirSync11(dirname11(apiKeyFile), { recursive: true });
12995
- writeFileSync9(apiKeyFile, `${apiKeyVar}=${apiKey}
13325
+ mkdirSync12(dirname12(apiKeyFile), { recursive: true });
13326
+ writeFileSync10(apiKeyFile, `${apiKeyVar}=${apiKey}
12996
13327
  `, { mode: 384 });
12997
13328
  process.stderr.write(`API key written to ${apiKeyFile}
12998
13329
  `);
@@ -13001,7 +13332,7 @@ Signed in as ${result.identity.email} \u2014 minted an API key in workspace "${r
13001
13332
  const keyPersisted = keychainBacked || apiKeyFile !== void 0;
13002
13333
  const wizard = firstRun;
13003
13334
  const writeFirstRun = wizard !== void 0 && !dryRun && keyPersisted;
13004
- const routeDir = value("dir") ?? (writeFirstRun ? join20(DEFAULT_REPOS_BASE_PATH, found.workspaceSlug) : void 0);
13335
+ const routeDir = value("dir") ?? (writeFirstRun ? join21(DEFAULT_REPOS_BASE_PATH, found.workspaceSlug) : void 0);
13005
13336
  const routeBlock = renderConnection(
13006
13337
  found,
13007
13338
  route2,
@@ -13029,6 +13360,10 @@ Signed in as ${result.identity.email} \u2014 minted an API key in workspace "${r
13029
13360
  if (seen.has(id)) continue;
13030
13361
  seen.add(id);
13031
13362
  process.stderr.write(`${adapterFor(tc.harness).ensureMcp({ bin: tc.bin, url: mcpUrlFor(baseUrl), key: apiKey, replace, dryRun })}
13363
+ `);
13364
+ }
13365
+ for (const h of shipPair(cfg.ship).filter((x) => x !== "claude")) {
13366
+ process.stderr.write(`${pairInstallerFor(h).installPair({ url: mcpUrlFor(baseUrl), replace, dryRun })}
13032
13367
  `);
13033
13368
  }
13034
13369
  }
@@ -13482,6 +13817,14 @@ Signed in as ${result.identity.email} \u2014 minted an API key in workspace "${r
13482
13817
  `);
13483
13818
  break;
13484
13819
  }
13820
+ case "disconnect": {
13821
+ const url = mcpUrlFor(value("base-url") ?? cfg.routes[0]?.baseUrl ?? DEFAULT_BASE_URL);
13822
+ for (const h of shipPair(cfg.ship)) {
13823
+ process.stderr.write(`${pairInstallerFor(h).removePair({ url, bin: cfg.ship.agent.bin, dryRun })}
13824
+ `);
13825
+ }
13826
+ break;
13827
+ }
13485
13828
  case "install": {
13486
13829
  if (!await installScheduler({ replace: flag("replace") })) process.exit(2);
13487
13830
  break;
@@ -13521,7 +13864,7 @@ Signed in as ${result.identity.email} \u2014 minted an API key in workspace "${r
13521
13864
  }
13522
13865
  case "log": {
13523
13866
  try {
13524
- const text = readFileSync14(cfg.ship.logFile, "utf8").split("\n");
13867
+ const text = readFileSync15(cfg.ship.logFile, "utf8").split("\n");
13525
13868
  process.stdout.write(`${text.slice(-(Number(value("tail")) || 40)).join("\n")}
13526
13869
  `);
13527
13870
  } catch {
@@ -13729,7 +14072,7 @@ routes: ${cfg.routes.map((c) => c.route).join(", ")}
13729
14072
  );
13730
14073
  try {
13731
14074
  const evFile = eventFileFor(cfg.ship.stateDir);
13732
- const evSize = existsSync20(evFile) ? statSync9(evFile).size : 0;
14075
+ const evSize = existsSync21(evFile) ? statSync9(evFile).size : 0;
13733
14076
  process.stdout.write(
13734
14077
  evSize > cfg.ship.eventsRotateBytes * 4 ? `events log: WARN ${evFile} is ${Math.round(evSize / 1048576)} MB (> 4x eventsRotateBytes); run \`crew reap\` to rotate it
13735
14078
  ` : "events log: ok\n"
@@ -13853,15 +14196,19 @@ crew manifest: ${seats2.length ? seats2.map((c) => c.name).join(", ") : "non
13853
14196
  let allBinsFound = true;
13854
14197
  for (const tc of shipToolchains(cfg.ship)) {
13855
14198
  const adapter = adapterFor(tc.harness);
13856
- const binPath = findOnPath(tc.launcher ?? tc.bin, schedulerPath);
14199
+ const binPath = findOnPath(tc.launcherBin ?? tc.launcher ?? tc.bin, schedulerPath);
13857
14200
  if (!binPath) allBinsFound = false;
13858
14201
  const label = tc.name === defaultToolchain ? "agent binary: " : `toolchain ${tc.name}:`.padEnd(18);
13859
14202
  process.stdout.write(
13860
- `${label} ${tc.launcher ?? tc.bin} \u2014 ${binPath ? `OK (${binPath})` : "NOT FOUND on the scheduler PATH"}${tc.name === defaultToolchain ? "" : ` [${adapter.displayName}]`}
14203
+ `${label} ${tc.launcherBin ?? tc.launcher ?? tc.bin} \u2014 ${binPath ? `OK (${binPath})` : "NOT FOUND on the scheduler PATH"}${tc.name === defaultToolchain ? "" : ` [${adapter.displayName}]`}
13861
14204
  `
13862
14205
  );
13863
14206
  for (const limit of adapter.limitations) process.stdout.write(` cannot enforce: ${limit}
13864
14207
  `);
14208
+ if (isOriLauncher(tc)) {
14209
+ 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)
14210
+ `);
14211
+ }
13865
14212
  }
13866
14213
  process.stdout.write(`git: ${gitPath ? `OK (${gitPath})` : "NOT FOUND on the scheduler PATH"}
13867
14214
  `);
@@ -13889,6 +14236,13 @@ crew manifest: ${seats2.length ? seats2.map((c) => c.name).join(", ") : "non
13889
14236
  );
13890
14237
  }
13891
14238
  }
14239
+ for (const h of shipPair(cfg.ship).filter((x) => x !== "claude")) {
14240
+ const inst = pairInstallerFor(h);
14241
+ const v = inst.verifyPair({ url: mcpUrlFor(route.baseUrl) });
14242
+ process.stdout.write(`pair ${h}:`.padEnd(19) + (v.ok ? ` installed (${v.detail})
14243
+ ` : ` NOT installed \u2014 ${v.detail}; run crew connect
14244
+ `));
14245
+ }
13892
14246
  const shipVendors = new Set(shipToolchains(cfg.ship).map(toolchainVendor));
13893
14247
  if (route.apiKey && route.resolved) {
13894
14248
  let flagged = 0;