@tablation/crew 0.30.1 → 0.31.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 +601 -258
  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",
@@ -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
@@ -512,8 +816,8 @@ function explain(ship, required) {
512
816
  }
513
817
 
514
818
  // src/repo-config.ts
515
- import { readFileSync as readFileSync3, existsSync as existsSync2 } from "fs";
516
- import { join as join3, basename as basename2 } from "path";
819
+ import { readFileSync as readFileSync4, existsSync as existsSync3 } from "fs";
820
+ import { join as join4, basename as basename2 } from "path";
517
821
  import { parse } from "yaml";
518
822
  var SPEC_VERSION = 1;
519
823
  var REPO_CONFIG_FILENAMES = [".crew.yaml", ".crew.yml", ".crew.json"];
@@ -586,8 +890,8 @@ var MODES = ["local", "integrate", "ci_manual", "ci_auto", "external"];
586
890
  var PROVIDERS = ["github", "buildkite", "other", "none"];
587
891
  function findRepoConfig(dir) {
588
892
  for (const name of REPO_CONFIG_FILENAMES) {
589
- const p = join3(dir, name);
590
- if (existsSync2(p)) return p;
893
+ const p = join4(dir, name);
894
+ if (existsSync3(p)) return p;
591
895
  }
592
896
  return null;
593
897
  }
@@ -817,7 +1121,7 @@ function parseRepoConfig(text, file) {
817
1121
  function loadRepoConfig(dir) {
818
1122
  const file = findRepoConfig(dir);
819
1123
  if (!file) return null;
820
- return parseRepoConfig(readFileSync3(file, "utf8"), file);
1124
+ return parseRepoConfig(readFileSync4(file, "utf8"), file);
821
1125
  }
822
1126
  function hookLabel(cfg, hook2) {
823
1127
  return cfg.labels[hook2] ?? cfg.hooks[hook2] ?? hook2;
@@ -1084,6 +1388,7 @@ var SHIP_KEYS = /* @__PURE__ */ new Set([
1084
1388
  "agent",
1085
1389
  "toolchains",
1086
1390
  "toolchain",
1391
+ "pair",
1087
1392
  "shell",
1088
1393
  "extraPath",
1089
1394
  "useNvm",
@@ -1228,16 +1533,16 @@ function mergeRouteRelease(route, override) {
1228
1533
  };
1229
1534
  }
1230
1535
  function expand(p, base) {
1231
- let out = p.startsWith("~") ? p.replace(/^~/, homedir2()) : p;
1232
- out = out.replace(/\$\{?HOME\}?/g, homedir2());
1536
+ let out = p.startsWith("~") ? p.replace(/^~/, homedir3()) : p;
1537
+ out = out.replace(/\$\{?HOME\}?/g, homedir3());
1233
1538
  return isAbsolute(out) ? out : resolve(base, out);
1234
1539
  }
1235
1540
  function resolvePromptsDir(promptSet, crewHome, base) {
1236
- if (!promptSet) return join4(crewHome, "prompts", "default");
1541
+ if (!promptSet) return join5(crewHome, "prompts", "default");
1237
1542
  if (promptSet.includes("/") || promptSet.startsWith(".") || promptSet.startsWith("~")) {
1238
1543
  return expand(promptSet, base);
1239
1544
  }
1240
- return join4(crewHome, "prompts", promptSet);
1545
+ return join5(crewHome, "prompts", promptSet);
1241
1546
  }
1242
1547
  var Missing = class {
1243
1548
  paths = [];
@@ -1261,7 +1566,7 @@ var Missing = class {
1261
1566
  };
1262
1567
  function crewVersion(crewHome) {
1263
1568
  try {
1264
- const pkg = JSON.parse(readFileSync4(join4(crewHome, "package.json"), "utf8"));
1569
+ const pkg = JSON.parse(readFileSync5(join5(crewHome, "package.json"), "utf8"));
1265
1570
  return pkg.version ?? "0.0.0";
1266
1571
  } catch {
1267
1572
  return "0.0.0";
@@ -1271,26 +1576,26 @@ function configSearchPath(crewHome) {
1271
1576
  if (process.env.CREW_CONFIG) return [resolve(process.env.CREW_CONFIG)];
1272
1577
  const xdg = process.env.XDG_CONFIG_HOME;
1273
1578
  return [
1274
- ...xdg ? [join4(xdg, "crew", "crew.yaml")] : [],
1275
- join4(homedir2(), ".config", "crew", "crew.yaml"),
1579
+ ...xdg ? [join5(xdg, "crew", "crew.yaml")] : [],
1580
+ join5(homedir3(), ".config", "crew", "crew.yaml"),
1276
1581
  resolve(crewHome, "crew.yaml")
1277
1582
  ];
1278
1583
  }
1279
1584
  function findConfigFile(crewHome) {
1280
1585
  const candidates = configSearchPath(crewHome);
1281
- return candidates.find((c) => existsSync3(c)) ?? candidates[candidates.length - 1];
1586
+ return candidates.find((c) => existsSync4(c)) ?? candidates[candidates.length - 1];
1282
1587
  }
1283
- var defaultConfigPath = () => join4(process.env.XDG_CONFIG_HOME ?? join4(homedir2(), ".config"), "crew", "crew.yaml");
1588
+ var defaultConfigPath = () => join5(process.env.XDG_CONFIG_HOME ?? join5(homedir3(), ".config"), "crew", "crew.yaml");
1284
1589
  function routeSlug(route) {
1285
1590
  return route.replace(/\//g, "-");
1286
1591
  }
1287
1592
  function resolvedPathFor(stateDir, route) {
1288
1593
  const [workspace, project] = splitRoute(route);
1289
- return join4(stateDir, "resolved", workspace, `${project}.json`);
1594
+ return join5(stateDir, "resolved", workspace, `${project}.json`);
1290
1595
  }
1291
1596
  function apiKeyPathFor(stateDir, route) {
1292
1597
  const [workspace, project] = splitRoute(route);
1293
- return join4(stateDir, "keys", workspace, `${project}.env`);
1598
+ return join5(stateDir, "keys", workspace, `${project}.env`);
1294
1599
  }
1295
1600
  function splitRoute(route) {
1296
1601
  const parts = route.split("/");
@@ -1301,9 +1606,9 @@ function splitRoute(route) {
1301
1606
  }
1302
1607
  function readResolvedFile(stateDir, route) {
1303
1608
  const path = resolvedPathFor(stateDir, route);
1304
- if (!existsSync3(path)) return void 0;
1609
+ if (!existsSync4(path)) return void 0;
1305
1610
  try {
1306
- return JSON.parse(readFileSync4(path, "utf8"));
1611
+ return JSON.parse(readFileSync5(path, "utf8"));
1307
1612
  } catch (e) {
1308
1613
  throw new ConfigError(`${path}: not valid JSON \u2014 ${e.message}`);
1309
1614
  }
@@ -1359,23 +1664,23 @@ function parseResolved(raw, m, where) {
1359
1664
  }
1360
1665
  function loadConfig(crewHome, configFile, opts = {}) {
1361
1666
  const file = configFile ?? findConfigFile(crewHome);
1362
- if (opts.text === void 0 && !existsSync3(file)) {
1667
+ if (opts.text === void 0 && !existsSync4(file)) {
1363
1668
  throw new ConfigError(
1364
1669
  `no config found. Looked in:
1365
1670
  ` + configSearchPath(crewHome).map((c) => ` - ${c}`).join("\n") + `
1366
1671
 
1367
1672
  Create the first of those:
1368
- mkdir -p ${dirname2(defaultConfigPath())} && cp ${join4(crewHome, "crew.example.yaml")} ${defaultConfigPath()}`
1673
+ mkdir -p ${dirname3(defaultConfigPath())} && cp ${join5(crewHome, "crew.example.yaml")} ${defaultConfigPath()}`
1369
1674
  );
1370
1675
  }
1371
1676
  let raw;
1372
1677
  try {
1373
- raw = parse2(opts.text ?? readFileSync4(file, "utf8"));
1678
+ raw = parse2(opts.text ?? readFileSync5(file, "utf8"));
1374
1679
  } catch (e) {
1375
1680
  throw new ConfigError(`${file}: not valid YAML \u2014 ${e.message}`);
1376
1681
  }
1377
1682
  if (!raw || typeof raw !== "object") throw new ConfigError(`${file} is empty or not a mapping`);
1378
- const base = dirname2(file);
1683
+ const base = dirname3(file);
1379
1684
  const missing = new Missing();
1380
1685
  rejectUnknownKeys(raw, /* @__PURE__ */ new Set(["ship", "routes"]), "top-level", file);
1381
1686
  const shipRaw = raw.ship ?? {};
@@ -1402,6 +1707,7 @@ Create the first of those:
1402
1707
  }
1403
1708
  const toolchains = parseToolchains(shipRaw.toolchains, base, missing, file);
1404
1709
  const toolchainNames = /* @__PURE__ */ new Set([LEGACY_TOOLCHAIN, ...toolchains.map((x) => x.name)]);
1710
+ const pair = parsePair(shipRaw.pair, missing);
1405
1711
  const shipToolchain = shipRaw.toolchain === void 0 ? void 0 : String(shipRaw.toolchain);
1406
1712
  if (shipToolchain !== void 0 && !toolchainNames.has(shipToolchain)) {
1407
1713
  missing.add(`ship.toolchain "${shipToolchain}" is not defined (have: ${[...toolchainNames].join(", ")})`);
@@ -1441,12 +1747,13 @@ Create the first of those:
1441
1747
  },
1442
1748
  ...toolchains.length > 0 ? { toolchains } : {},
1443
1749
  ...shipToolchain !== void 0 ? { toolchain: shipToolchain } : {},
1750
+ ...pair ? { pair } : {},
1444
1751
  shell: shipRaw.shell,
1445
1752
  extraPath: shipRaw.extraPath,
1446
1753
  useNvm: shipRaw.useNvm !== false,
1447
1754
  nvmSh: shipRaw.nvmSh,
1448
1755
  stateDir,
1449
- logFile: expand(shipRaw.logFile ?? join4(tmpdir(), "crew.log"), base),
1756
+ logFile: expand(shipRaw.logFile ?? join5(tmpdir(), "crew.log"), base),
1450
1757
  userAgent: shipRaw.userAgent ?? `Mozilla/5.0 CrewAgent/${crewVersion(crewHome)}`,
1451
1758
  maxConcurrentAgents,
1452
1759
  streamRetentionDays,
@@ -1468,6 +1775,22 @@ function expandCommand(cmd, base) {
1468
1775
  return cmd.includes("/") || cmd.startsWith("~") ? expand(cmd, base) : cmd;
1469
1776
  }
1470
1777
  var TOOLCHAIN_KEYS = /* @__PURE__ */ new Set(["harness", "bin", "launcher", "model", "tiers", "args", "env", "maxThinkingTokens"]);
1778
+ function parsePair(raw, missing) {
1779
+ if (raw === void 0) return void 0;
1780
+ if (!Array.isArray(raw)) {
1781
+ missing.add("ship.pair must be a list of harness names");
1782
+ return void 0;
1783
+ }
1784
+ const out = [];
1785
+ for (const h of raw.map(String)) {
1786
+ if (!isPairHarness(h)) missing.add(`ship.pair "${h}" is not a harness Pair can be installed for (have: ${PAIR_HARNESS_NAMES.join(", ")})`);
1787
+ else if (!out.includes(h)) out.push(h);
1788
+ }
1789
+ return out;
1790
+ }
1791
+ function shipPair(ship) {
1792
+ return ship.pair ?? DEFAULT_PAIR;
1793
+ }
1471
1794
  function parseToolchains(raw, base, missing, file) {
1472
1795
  if (raw === void 0 || raw === null) return [];
1473
1796
  if (typeof raw !== "object" || Array.isArray(raw)) {
@@ -1644,8 +1967,8 @@ function repoIdForName(r, name) {
1644
1967
  }
1645
1968
  function resolveApiKey(r) {
1646
1969
  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")) {
1970
+ if (r.apiKeyFile && r.apiKeyVar && existsSync4(r.apiKeyFile)) {
1971
+ for (const line of readFileSync5(r.apiKeyFile, "utf8").split("\n")) {
1649
1972
  const m = line.match(new RegExp(`^${r.apiKeyVar}=(.*)$`));
1650
1973
  if (m) return (m[1] ?? "").trim().replace(/^["']|["']$/g, "");
1651
1974
  }
@@ -1708,7 +2031,7 @@ function configuredMembers(rt) {
1708
2031
  return out;
1709
2032
  }
1710
2033
  function defaultRepoDir(basePath, workspace, repoName) {
1711
- return join4(basePath, workspace, repoName);
2034
+ return join5(basePath, workspace, repoName);
1712
2035
  }
1713
2036
  function reposOf(r) {
1714
2037
  const named = Object.entries(r.repos);
@@ -1732,7 +2055,7 @@ function passengerRepoTargets(r) {
1732
2055
  return reposOf(r).filter((t) => r.repoOverrides[t.name]?.hostPassengers !== false);
1733
2056
  }
1734
2057
  function passengerCheckoutDir(basePath, workspace, repoName) {
1735
- return join4(basePath, workspace, ".passenger-checkouts", repoName);
2058
+ return join5(basePath, workspace, ".passenger-checkouts", repoName);
1736
2059
  }
1737
2060
  function passengerCheckoutTargets(r) {
1738
2061
  if (!r.hostPassengers) return [];
@@ -1864,17 +2187,17 @@ function rosterMarkdown(roster, meId) {
1864
2187
  }
1865
2188
 
1866
2189
  // 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";
2190
+ import { readFileSync as readFileSync6, writeFileSync as writeFileSync2, existsSync as existsSync5, mkdirSync as mkdirSync3, rmSync as rmSync3, statSync as statSync2 } from "fs";
2191
+ import { join as join6 } from "path";
1869
2192
  var EPOCH = "1970-01-01T00:00:00.000Z";
1870
2193
  var State = class _State {
1871
2194
  dir;
1872
2195
  constructor(dir) {
1873
2196
  this.dir = dir;
1874
- mkdirSync2(dir, { recursive: true });
2197
+ mkdirSync3(dir, { recursive: true });
1875
2198
  }
1876
2199
  path(...parts) {
1877
- return join5(this.dir, ...parts);
2200
+ return join6(this.dir, ...parts);
1878
2201
  }
1879
2202
  /**
1880
2203
  * The timestamp every "new since last poll" check is measured against.
@@ -1885,14 +2208,14 @@ var State = class _State {
1885
2208
  */
1886
2209
  watermark() {
1887
2210
  try {
1888
- const v = readFileSync5(this.path(".poll-watermark"), "utf8").trim();
2211
+ const v = readFileSync6(this.path(".poll-watermark"), "utf8").trim();
1889
2212
  return v || EPOCH;
1890
2213
  } catch {
1891
2214
  return EPOCH;
1892
2215
  }
1893
2216
  }
1894
2217
  setWatermark(at) {
1895
- writeFileSync(this.path(".poll-watermark"), `${at}
2218
+ writeFileSync2(this.path(".poll-watermark"), `${at}
1896
2219
  `);
1897
2220
  }
1898
2221
  /**
@@ -1910,19 +2233,19 @@ var State = class _State {
1910
2233
  }
1911
2234
  /** Whole-crew pause, and the per-role sentinels one level down. */
1912
2235
  isPaused() {
1913
- return existsSync4(this.path(".crew-paused"));
2236
+ return existsSync5(this.path(".crew-paused"));
1914
2237
  }
1915
2238
  isRolePaused(role) {
1916
- return existsSync4(this.path(`.role-paused-${role}`));
2239
+ return existsSync5(this.path(`.role-paused-${role}`));
1917
2240
  }
1918
2241
  pausedRoles(roles) {
1919
2242
  return new Set(roles.filter((r) => this.isRolePaused(r)));
1920
2243
  }
1921
2244
  pause(role) {
1922
- writeFileSync(role ? this.path(`.role-paused-${role}`) : this.path(".crew-paused"), "");
2245
+ writeFileSync2(role ? this.path(`.role-paused-${role}`) : this.path(".crew-paused"), "");
1923
2246
  }
1924
2247
  resume(role) {
1925
- rmSync2(role ? this.path(`.role-paused-${role}`) : this.path(".crew-paused"), { force: true });
2248
+ rmSync3(role ? this.path(`.role-paused-${role}`) : this.path(".crew-paused"), { force: true });
1926
2249
  }
1927
2250
  /**
1928
2251
  * The release phase (merge, deploy, release) has its own sentinel, one
@@ -1930,11 +2253,11 @@ var State = class _State {
1930
2253
  * while agents keep working.
1931
2254
  */
1932
2255
  isReleasePaused() {
1933
- return existsSync4(this.path(".release-paused"));
2256
+ return existsSync5(this.path(".release-paused"));
1934
2257
  }
1935
2258
  setReleasePaused(paused) {
1936
- if (paused) writeFileSync(this.path(".release-paused"), "");
1937
- else rmSync2(this.path(".release-paused"), { force: true });
2259
+ if (paused) writeFileSync2(this.path(".release-paused"), "");
2260
+ else rmSync3(this.path(".release-paused"), { force: true });
1938
2261
  }
1939
2262
  /**
1940
2263
  * Why a release cycle must not run right now, or null when it may. Every
@@ -1971,7 +2294,7 @@ var State = class _State {
1971
2294
  const name = _State.safe(rawName);
1972
2295
  const file = this.path(`.${name}.lock`);
1973
2296
  try {
1974
- const existing = Number.parseInt(readFileSync5(file, "utf8").trim(), 10);
2297
+ const existing = Number.parseInt(readFileSync6(file, "utf8").trim(), 10);
1975
2298
  if (Number.isFinite(existing) && existing > 0) {
1976
2299
  try {
1977
2300
  process.kill(existing, 0);
@@ -1981,13 +2304,13 @@ var State = class _State {
1981
2304
  }
1982
2305
  } catch {
1983
2306
  }
1984
- writeFileSync(file, `${process.pid}
2307
+ writeFileSync2(file, `${process.pid}
1985
2308
  `);
1986
2309
  return {
1987
2310
  ok: true,
1988
2311
  release: () => {
1989
2312
  try {
1990
- rmSync2(file, { force: true });
2313
+ rmSync3(file, { force: true });
1991
2314
  } catch {
1992
2315
  }
1993
2316
  }
@@ -2065,7 +2388,7 @@ var State = class _State {
2065
2388
  const heldReported = `.release-held-${suffix}`;
2066
2389
  const read = (f) => {
2067
2390
  try {
2068
- return readFileSync5(this.path(f), "utf8").trim() || null;
2391
+ return readFileSync6(this.path(f), "utf8").trim() || null;
2069
2392
  } catch {
2070
2393
  return null;
2071
2394
  }
@@ -2074,12 +2397,12 @@ var State = class _State {
2074
2397
  blockedCount: () => Number.parseInt(read(blocked) ?? "", 10) || 0,
2075
2398
  noteBlocked: () => {
2076
2399
  const n = (Number.parseInt(read(blocked) ?? "", 10) || 0) + 1;
2077
- writeFileSync(this.path(blocked), `${n}
2400
+ writeFileSync2(this.path(blocked), `${n}
2078
2401
  `);
2079
2402
  return n;
2080
2403
  },
2081
2404
  clearBlocked: () => {
2082
- rmSync2(this.path(blocked), { force: true });
2405
+ rmSync3(this.path(blocked), { force: true });
2083
2406
  },
2084
2407
  /**
2085
2408
  * The commit a deploy last failed on.
@@ -2091,11 +2414,11 @@ var State = class _State {
2091
2414
  */
2092
2415
  deployFailedSha: () => read(failed),
2093
2416
  noteDeployFailed: (sha) => {
2094
- writeFileSync(this.path(failed), `${sha}
2417
+ writeFileSync2(this.path(failed), `${sha}
2095
2418
  `);
2096
2419
  },
2097
2420
  clearDeployFailed: () => {
2098
- rmSync2(this.path(failed), { force: true });
2421
+ rmSync3(this.path(failed), { force: true });
2099
2422
  },
2100
2423
  /**
2101
2424
  * Consecutive test-gate failures on the same head (CREW-1368). A failure
@@ -2105,31 +2428,31 @@ var State = class _State {
2105
2428
  noteTestGateFailed: (sha) => {
2106
2429
  const [prev, n] = (read(gateFailed) ?? "").split(" ");
2107
2430
  const next = prev === sha ? (Number.parseInt(n ?? "", 10) || 0) + 1 : 1;
2108
- writeFileSync(this.path(gateFailed), `${sha} ${next}
2431
+ writeFileSync2(this.path(gateFailed), `${sha} ${next}
2109
2432
  `);
2110
2433
  return next;
2111
2434
  },
2112
2435
  clearTestGateFailed: () => {
2113
- rmSync2(this.path(gateFailed), { force: true });
2436
+ rmSync3(this.path(gateFailed), { force: true });
2114
2437
  },
2115
2438
  /** Whether the board was already told about this head's red gate. */
2116
2439
  heldReported: () => {
2117
2440
  try {
2118
- return readFileSync5(this.path(heldReported), "utf8").replace(/\n$/, "") || null;
2441
+ return readFileSync6(this.path(heldReported), "utf8").replace(/\n$/, "") || null;
2119
2442
  } catch {
2120
2443
  return null;
2121
2444
  }
2122
2445
  },
2123
2446
  noteHeldReported: (signature) => {
2124
- writeFileSync(this.path(heldReported), `${signature}
2447
+ writeFileSync2(this.path(heldReported), `${signature}
2125
2448
  `);
2126
2449
  },
2127
2450
  clearHeldReported: () => {
2128
- rmSync2(this.path(heldReported), { force: true });
2451
+ rmSync3(this.path(heldReported), { force: true });
2129
2452
  },
2130
2453
  testGateReported: (sha) => read(gateReported) === sha,
2131
2454
  noteTestGateReported: (sha) => {
2132
- writeFileSync(this.path(gateReported), `${sha}
2455
+ writeFileSync2(this.path(gateReported), `${sha}
2133
2456
  `);
2134
2457
  }
2135
2458
  };
@@ -2152,13 +2475,13 @@ var State = class _State {
2152
2475
  const ticketStreakFile = `.fairness-ticket-streak-${suffix}`;
2153
2476
  const read = (f) => {
2154
2477
  try {
2155
- return readFileSync5(this.path(f), "utf8").trim() || null;
2478
+ return readFileSync6(this.path(f), "utf8").trim() || null;
2156
2479
  } catch {
2157
2480
  return null;
2158
2481
  }
2159
2482
  };
2160
2483
  const clear2 = () => {
2161
- for (const f of [streakFile, ticketFile, sinceFile, ticketStreakFile]) rmSync2(this.path(f), { force: true });
2484
+ for (const f of [streakFile, ticketFile, sinceFile, ticketStreakFile]) rmSync3(this.path(f), { force: true });
2162
2485
  };
2163
2486
  return {
2164
2487
  /** Consecutive cycles this route had actionable work and did not win. */
@@ -2185,18 +2508,18 @@ var State = class _State {
2185
2508
  clear2();
2186
2509
  return;
2187
2510
  }
2188
- writeFileSync(this.path(streakFile), `${this.readInt(streakFile) + 1}
2511
+ writeFileSync2(this.path(streakFile), `${this.readInt(streakFile) + 1}
2189
2512
  `);
2190
2513
  const prior = read(ticketFile);
2191
2514
  if (topTicket && topTicket === prior) {
2192
- writeFileSync(this.path(ticketStreakFile), `${this.readInt(ticketStreakFile) + 1}
2515
+ writeFileSync2(this.path(ticketStreakFile), `${this.readInt(ticketStreakFile) + 1}
2193
2516
  `);
2194
2517
  } else if (topTicket) {
2195
- writeFileSync(this.path(ticketFile), `${topTicket}
2518
+ writeFileSync2(this.path(ticketFile), `${topTicket}
2196
2519
  `);
2197
- writeFileSync(this.path(sinceFile), `${(/* @__PURE__ */ new Date()).toISOString()}
2520
+ writeFileSync2(this.path(sinceFile), `${(/* @__PURE__ */ new Date()).toISOString()}
2198
2521
  `);
2199
- writeFileSync(this.path(ticketStreakFile), "1\n");
2522
+ writeFileSync2(this.path(ticketStreakFile), "1\n");
2200
2523
  }
2201
2524
  }
2202
2525
  };
@@ -2217,14 +2540,14 @@ var State = class _State {
2217
2540
  return {
2218
2541
  previous: () => {
2219
2542
  try {
2220
- const parsed = JSON.parse(readFileSync5(this.path(file), "utf8"));
2543
+ const parsed = JSON.parse(readFileSync6(this.path(file), "utf8"));
2221
2544
  return parsed && typeof parsed === "object" ? parsed : {};
2222
2545
  } catch {
2223
2546
  return {};
2224
2547
  }
2225
2548
  },
2226
2549
  persist: (next) => {
2227
- writeFileSync(this.path(file), JSON.stringify(next));
2550
+ writeFileSync2(this.path(file), JSON.stringify(next));
2228
2551
  }
2229
2552
  };
2230
2553
  }
@@ -2238,14 +2561,14 @@ var State = class _State {
2238
2561
  return {
2239
2562
  previous: () => {
2240
2563
  try {
2241
- const parsed = JSON.parse(readFileSync5(this.path(file), "utf8"));
2564
+ const parsed = JSON.parse(readFileSync6(this.path(file), "utf8"));
2242
2565
  return Array.isArray(parsed) ? parsed : [];
2243
2566
  } catch {
2244
2567
  return [];
2245
2568
  }
2246
2569
  },
2247
2570
  persist: (next) => {
2248
- writeFileSync(this.path(file), JSON.stringify(next));
2571
+ writeFileSync2(this.path(file), JSON.stringify(next));
2249
2572
  }
2250
2573
  };
2251
2574
  }
@@ -2258,7 +2581,7 @@ var State = class _State {
2258
2581
  const file = ".release-last-run";
2259
2582
  return {
2260
2583
  touch: () => {
2261
- writeFileSync(this.path(file), (/* @__PURE__ */ new Date()).toISOString());
2584
+ writeFileSync2(this.path(file), (/* @__PURE__ */ new Date()).toISOString());
2262
2585
  },
2263
2586
  at: () => {
2264
2587
  try {
@@ -2271,7 +2594,7 @@ var State = class _State {
2271
2594
  }
2272
2595
  readInt(file) {
2273
2596
  try {
2274
- return Number.parseInt(readFileSync5(this.path(file), "utf8").trim(), 10) || 0;
2597
+ return Number.parseInt(readFileSync6(this.path(file), "utf8").trim(), 10) || 0;
2275
2598
  } catch {
2276
2599
  return 0;
2277
2600
  }
@@ -2575,9 +2898,9 @@ function describeRelease(o, route) {
2575
2898
  }
2576
2899
 
2577
2900
  // 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";
2901
+ import { dirname as dirname4, join as join7, extname } from "path";
2902
+ import { homedir as homedir4 } from "os";
2903
+ import { accessSync, constants, existsSync as existsSync6, mkdirSync as mkdirSync4, readdirSync, readFileSync as readFileSync7, statSync as statSync3, writeFileSync as writeFileSync3, unlinkSync, realpathSync } from "fs";
2581
2904
  import { spawnSync as spawnSync2 } from "child_process";
2582
2905
  import { createHash } from "crypto";
2583
2906
 
@@ -2589,7 +2912,7 @@ function isCompiledBinary(moduleUrl) {
2589
2912
  // src/install.ts
2590
2913
  function crewInvocation(crewHome) {
2591
2914
  if (isCompiledBinary(import.meta.url)) return [process.execPath];
2592
- return [process.execPath, join6(crewHome, "bin", "crew")];
2915
+ return [process.execPath, join7(crewHome, "bin", "crew")];
2593
2916
  }
2594
2917
  var INTERVAL_SECONDS = 120;
2595
2918
  function labelFor(crewHome, job) {
@@ -2620,14 +2943,14 @@ function schedulerLogFor(crewLog, job) {
2620
2943
  return `${stem}.${suffix}${ext || ".log"}`;
2621
2944
  }
2622
2945
  function pathFor(ship) {
2623
- const nodeBinDir = dirname3(process.execPath);
2946
+ const nodeBinDir = dirname4(process.execPath);
2624
2947
  const parts = [nodeBinDir];
2625
2948
  if (ship.extraPath) parts.push(ship.extraPath);
2626
2949
  parts.push("/usr/bin", "/bin", "/usr/sbin", "/sbin");
2627
2950
  return parts.join(":");
2628
2951
  }
2629
2952
  function findOnPath(bin, searchPath) {
2630
- const candidates = bin.includes("/") ? [bin] : searchPath.split(":").filter(Boolean).map((dir) => join6(dir, bin));
2953
+ const candidates = bin.includes("/") ? [bin] : searchPath.split(":").filter(Boolean).map((dir) => join7(dir, bin));
2631
2954
  for (const candidate of candidates) {
2632
2955
  try {
2633
2956
  accessSync(candidate, constants.X_OK);
@@ -2637,19 +2960,19 @@ function findOnPath(bin, searchPath) {
2637
2960
  }
2638
2961
  return void 0;
2639
2962
  }
2640
- var COMMON_DOCKER_DIRS = ["/usr/local/bin", "/opt/homebrew/bin", join6(homedir3(), ".docker", "bin")].join(":");
2963
+ var COMMON_DOCKER_DIRS = ["/usr/local/bin", "/opt/homebrew/bin", join7(homedir4(), ".docker", "bin")].join(":");
2641
2964
  function dockerPathProblem(ship, shellPath = process.env.PATH ?? "") {
2642
2965
  const schedulerPath = pathFor(ship);
2643
2966
  if (findOnPath("docker", schedulerPath)) return void 0;
2644
2967
  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\``;
2968
+ 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
2969
  }
2647
2970
  function xmlEscape(s) {
2648
2971
  return s.replace(/&/g, "&amp;").replace(/</g, "&lt;").replace(/>/g, "&gt;");
2649
2972
  }
2650
2973
  function planLaunchd(ship, crewHome, job) {
2651
2974
  const label = labelFor(crewHome, job);
2652
- const unitPath = join6(homedir3(), "Library", "LaunchAgents", `${label}.plist`);
2975
+ const unitPath = join7(homedir4(), "Library", "LaunchAgents", `${label}.plist`);
2653
2976
  const crewLog = ship.logFile;
2654
2977
  const schedulerLog = schedulerLogFor(crewLog, job);
2655
2978
  const persistent = job === "run";
@@ -2701,8 +3024,8 @@ ${scheduleKeys}
2701
3024
  }
2702
3025
  function planSystemd(ship, crewHome, job) {
2703
3026
  const label = labelFor(crewHome, job);
2704
- const unitDir = join6(homedir3(), ".config", "systemd", "user");
2705
- const servicePath = join6(unitDir, `${label}.service`);
3027
+ const unitDir = join7(homedir4(), ".config", "systemd", "user");
3028
+ const servicePath = join7(unitDir, `${label}.service`);
2706
3029
  const crewLog = ship.logFile;
2707
3030
  const schedulerLog = schedulerLogFor(crewLog, job);
2708
3031
  const description = job === "release" ? "Tablation crew \u2014 release" : job === "passengers" ? "Tablation crew \u2014 Host Passengers sync" : "Tablation crew \u2014 persistent supervisor loop";
@@ -2739,7 +3062,7 @@ WantedBy=default.target
2739
3062
  schedulerLog
2740
3063
  };
2741
3064
  }
2742
- const timerPath = join6(unitDir, `${label}.timer`);
3065
+ const timerPath = join7(unitDir, `${label}.timer`);
2743
3066
  const service = `# Written by \`crew install\` \u2014 re-run it rather than hand-editing this file.
2744
3067
  [Unit]
2745
3068
  Description=${description}
@@ -2806,7 +3129,7 @@ function planInstall(ship, crewHome, hostShip, hasSystemd, job = "run") {
2806
3129
  function planUninstall(ship, crewHome, hostShip, hasSystemd, job = "run") {
2807
3130
  const label = labelFor(crewHome, job);
2808
3131
  if (hostShip === "macos") {
2809
- const unitPath = join6(homedir3(), "Library", "LaunchAgents", `${label}.plist`);
3132
+ const unitPath = join7(homedir4(), "Library", "LaunchAgents", `${label}.plist`);
2810
3133
  return {
2811
3134
  mechanism: "launchd",
2812
3135
  unitPaths: [unitPath],
@@ -2816,18 +3139,18 @@ function planUninstall(ship, crewHome, hostShip, hasSystemd, job = "run") {
2816
3139
  };
2817
3140
  }
2818
3141
  if (hostShip === "linux" && hasSystemd) {
2819
- const unitDir = join6(homedir3(), ".config", "systemd", "user");
3142
+ const unitDir = join7(homedir4(), ".config", "systemd", "user");
2820
3143
  if (job === "run") {
2821
3144
  return {
2822
3145
  mechanism: "systemd",
2823
- unitPaths: [join6(unitDir, `${label}.service`)],
3146
+ unitPaths: [join7(unitDir, `${label}.service`)],
2824
3147
  crontabLine: null,
2825
3148
  unloadCommands: [["systemctl", "--user", "disable", "--now", `${label}.service`]]
2826
3149
  };
2827
3150
  }
2828
3151
  return {
2829
3152
  mechanism: "systemd",
2830
- unitPaths: [join6(unitDir, `${label}.service`), join6(unitDir, `${label}.timer`)],
3153
+ unitPaths: [join7(unitDir, `${label}.service`), join7(unitDir, `${label}.timer`)],
2831
3154
  crontabLine: null,
2832
3155
  unloadCommands: [["systemctl", "--user", "disable", "--now", `${label}.timer`]]
2833
3156
  };
@@ -2845,7 +3168,7 @@ function planUninstall(ship, crewHome, hostShip, hasSystemd, job = "run") {
2845
3168
  function planDaemonControl(crewHome, hostShip, hasSystemd) {
2846
3169
  const label = labelFor(crewHome, "run");
2847
3170
  if (hostShip === "macos") {
2848
- const unitPath = join6(homedir3(), "Library", "LaunchAgents", `${label}.plist`);
3171
+ const unitPath = join7(homedir4(), "Library", "LaunchAgents", `${label}.plist`);
2849
3172
  return {
2850
3173
  mechanism: "launchd",
2851
3174
  startCommand: ["launchctl", "load", unitPath],
@@ -2885,8 +3208,8 @@ async function applyInstall(plan, crewHome, dryRun, log) {
2885
3208
  log.emit(`would write ${path}`);
2886
3209
  continue;
2887
3210
  }
2888
- mkdirSync3(dirname3(path), { recursive: true });
2889
- writeFileSync2(path, plan.unitContent[path]);
3211
+ mkdirSync4(dirname4(path), { recursive: true });
3212
+ writeFileSync3(path, plan.unitContent[path]);
2890
3213
  log.emit(`wrote ${path}`);
2891
3214
  }
2892
3215
  if (plan.crontabLine) {
@@ -2940,7 +3263,7 @@ async function applyUninstall(plan, crewHome, dryRun, log) {
2940
3263
  }
2941
3264
  }
2942
3265
  for (const path of plan.unitPaths) {
2943
- if (!existsSync5(path)) continue;
3266
+ if (!existsSync6(path)) continue;
2944
3267
  if (dryRun) {
2945
3268
  log.emit(`would remove ${path}`);
2946
3269
  continue;
@@ -2959,7 +3282,7 @@ async function applyUninstall(plan, crewHome, dryRun, log) {
2959
3282
  }
2960
3283
  var CREW_LAUNCHD_PREFIX = "com.tablation.crew";
2961
3284
  function launchAgentsDir() {
2962
- return join6(homedir3(), "Library", "LaunchAgents");
3285
+ return join7(homedir4(), "Library", "LaunchAgents");
2963
3286
  }
2964
3287
  function xmlUnescape(s) {
2965
3288
  return s.replace(/&lt;/g, "<").replace(/&gt;/g, ">").replace(/&amp;/g, "&");
@@ -2976,25 +3299,25 @@ function loadedCrewLabels() {
2976
3299
  return [...parseLaunchctlList(list.stdout ?? "").keys()].filter((l) => l.startsWith(CREW_LAUNCHD_PREFIX));
2977
3300
  }
2978
3301
  function listLaunchdUnits(dir = launchAgentsDir(), loaded = []) {
2979
- const files = existsSync5(dir) ? readdirSync(dir) : [];
3302
+ const files = existsSync6(dir) ? readdirSync(dir) : [];
2980
3303
  const units = files.filter((f) => f.startsWith(CREW_LAUNCHD_PREFIX) && f.endsWith(".plist")).sort().map((f) => {
2981
- const plistPath = join6(dir, f);
3304
+ const plistPath = join7(dir, f);
2982
3305
  let program = null;
2983
3306
  try {
2984
- program = programFromPlist(readFileSync6(plistPath, "utf8"));
3307
+ program = programFromPlist(readFileSync7(plistPath, "utf8"));
2985
3308
  } catch {
2986
3309
  }
2987
3310
  return {
2988
3311
  label: f.slice(0, -".plist".length),
2989
3312
  plistPath,
2990
3313
  program,
2991
- programExists: program !== null && existsSync5(program)
3314
+ programExists: program !== null && existsSync6(program)
2992
3315
  };
2993
3316
  });
2994
3317
  const seen = new Set(units.map((u) => u.label));
2995
3318
  for (const label of [...loaded].sort()) {
2996
3319
  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 });
3320
+ units.push({ label, plistPath: join7(dir, `${label}.plist`), program: null, programExists: false, orphan: true });
2998
3321
  }
2999
3322
  return units;
3000
3323
  }
@@ -3007,9 +3330,9 @@ function removeLaunchdUnit(unit, dryRun, log) {
3007
3330
  log.emit(`would unload and remove ${unit.label}`);
3008
3331
  return;
3009
3332
  }
3010
- if (existsSync5(unit.plistPath)) spawnSync2("launchctl", ["unload", unit.plistPath], { encoding: "utf8" });
3333
+ if (existsSync6(unit.plistPath)) spawnSync2("launchctl", ["unload", unit.plistPath], { encoding: "utf8" });
3011
3334
  spawnSync2("launchctl", ["remove", unit.label], { encoding: "utf8" });
3012
- if (existsSync5(unit.plistPath)) unlinkSync(unit.plistPath);
3335
+ if (existsSync6(unit.plistPath)) unlinkSync(unit.plistPath);
3013
3336
  log.emit(`removed ${unit.label} (${unit.plistPath})`);
3014
3337
  }
3015
3338
  function clearForeignLaunchdUnits(crewHome, replace, dryRun, log, dir, loaded) {
@@ -3092,15 +3415,15 @@ import { statfsSync } from "fs";
3092
3415
  // src/git.ts
3093
3416
  import { execFileSync } from "child_process";
3094
3417
  import {
3095
- existsSync as existsSync6,
3096
- mkdirSync as mkdirSync4,
3418
+ existsSync as existsSync7,
3419
+ mkdirSync as mkdirSync5,
3097
3420
  openSync as openSync2,
3098
3421
  closeSync as closeSync2,
3099
3422
  writeSync,
3100
3423
  statSync as statSync4,
3101
3424
  unlinkSync as unlinkSync2
3102
3425
  } from "fs";
3103
- import { dirname as dirname4, basename as basename4 } from "path";
3426
+ import { dirname as dirname5, basename as basename4 } from "path";
3104
3427
  var GitError = class extends Error {
3105
3428
  };
3106
3429
  function gitRaw(cwd, args) {
@@ -3464,13 +3787,13 @@ function cloneUrlFor(remote) {
3464
3787
  return `git@github.com:${remote}.git`;
3465
3788
  }
3466
3789
  function ensureRepoCheckout(dir, remote) {
3467
- if (existsSync6(`${dir}/.git`)) return false;
3790
+ if (existsSync7(`${dir}/.git`)) return false;
3468
3791
  if (!remote) {
3469
3792
  throw new GitError(
3470
3793
  `${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
3794
  );
3472
3795
  }
3473
- mkdirSync4(dirname4(dir), { recursive: true });
3796
+ mkdirSync5(dirname5(dir), { recursive: true });
3474
3797
  try {
3475
3798
  execFileSync("git", ["clone", cloneUrlFor(remote), dir], { stdio: ["ignore", "pipe", "pipe"] });
3476
3799
  } catch (e) {
@@ -3489,7 +3812,7 @@ function sleepSync(ms2) {
3489
3812
  }
3490
3813
  function acquireCheckoutLock(dir, timeoutMs = 3e4) {
3491
3814
  const lockPath = checkoutLockPath(dir);
3492
- mkdirSync4(dirname4(lockPath), { recursive: true });
3815
+ mkdirSync5(dirname5(lockPath), { recursive: true });
3493
3816
  const deadline = Date.now() + timeoutMs;
3494
3817
  for (; ; ) {
3495
3818
  try {
@@ -5201,8 +5524,8 @@ function materializeBranchForTicket(dir, cfg, t, role) {
5201
5524
  }
5202
5525
 
5203
5526
  // src/poll.ts
5204
- import { writeFileSync as writeFileSync3 } from "fs";
5205
- import { join as join7 } from "path";
5527
+ import { writeFileSync as writeFileSync4 } from "fs";
5528
+ import { join as join8 } from "path";
5206
5529
 
5207
5530
  // src/stalled.ts
5208
5531
  var STALLED_AFTER_CYCLES = 30;
@@ -5474,7 +5797,7 @@ async function decideCycle(o) {
5474
5797
  }
5475
5798
  return decision;
5476
5799
  }
5477
- var digestPath = (stateDir, route, role) => join7(stateDir, `digest-${route}-${role}.md`);
5800
+ var digestPath = (stateDir, route, role) => join8(stateDir, `digest-${route}-${role}.md`);
5478
5801
  function writeDigest(o, d, role, stateDir) {
5479
5802
  const me = o.route.resolved?.seats[role];
5480
5803
  if (!me) return false;
@@ -5536,7 +5859,7 @@ function writeDigest(o, d, role, stateDir) {
5536
5859
  };
5537
5860
  const text = role === "qa" ? qaDigest(input) : buildingDigest(input);
5538
5861
  const path = digestPath(stateDir, routeSlug(o.route.route), role);
5539
- writeFileSync3(path, text);
5862
+ writeFileSync4(path, text);
5540
5863
  o.emit.emit(`queue digest written for ${role} (${Buffer.byteLength(text)} bytes)`, {
5541
5864
  data: { role, bytes: Buffer.byteLength(text) }
5542
5865
  });
@@ -5844,9 +6167,9 @@ var alreadyReported = (ticket, g, comments) => comments.some((c) => c.ticket_id
5844
6167
 
5845
6168
  // src/agent.ts
5846
6169
  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";
6170
+ import { readFileSync as readFileSync8, existsSync as existsSync8, statSync as statSync5, mkdirSync as mkdirSync6, writeFileSync as writeFileSync5, createWriteStream } from "fs";
5848
6171
  import { finished } from "stream/promises";
5849
- import { join as join8, dirname as dirname5 } from "path";
6172
+ import { join as join9, dirname as dirname6 } from "path";
5850
6173
  import { createHash as createHash2 } from "crypto";
5851
6174
 
5852
6175
  // src/environment.ts
@@ -6244,23 +6567,23 @@ var AgentError = class extends Error {
6244
6567
  var AgentSpawnError = class extends AgentError {
6245
6568
  };
6246
6569
  function planAgentRun(o) {
6247
- const briefPath = join8(o.route.promptsDir, "personas", `lane-${o.role}.md`);
6248
- if (!existsSync7(briefPath)) {
6570
+ const briefPath = join9(o.route.promptsDir, "personas", `lane-${o.role}.md`);
6571
+ if (!existsSync8(briefPath)) {
6249
6572
  throw new AgentError(`no brief at ${briefPath} \u2014 refusing to run an unscoped session`);
6250
6573
  }
6251
- const commonPath = join8(o.route.promptsDir, "personas", "common.md");
6252
- if (!existsSync7(commonPath)) throw new AgentError(`no shared policy at ${commonPath}`);
6574
+ const commonPath = join9(o.route.promptsDir, "personas", "common.md");
6575
+ if (!existsSync8(commonPath)) throw new AgentError(`no shared policy at ${commonPath}`);
6253
6576
  const now = o.now ?? (() => Date.now());
6254
6577
  const routeLabel = routeSlug(o.route.route);
6255
- const digestPath2 = join8(o.stateDir, `digest-${routeLabel}-${o.role}.md`);
6578
+ const digestPath2 = join9(o.stateDir, `digest-${routeLabel}-${o.role}.md`);
6256
6579
  let digest;
6257
6580
  let ageSeconds;
6258
- if (existsSync7(digestPath2)) {
6581
+ if (existsSync8(digestPath2)) {
6259
6582
  ageSeconds = Math.floor((now() - statSync5(digestPath2).mtimeMs) / 1e3);
6260
- if (ageSeconds <= DIGEST_MAX_AGE_SECONDS) digest = readFileSync7(digestPath2, "utf8");
6583
+ if (ageSeconds <= DIGEST_MAX_AGE_SECONDS) digest = readFileSync8(digestPath2, "utf8");
6261
6584
  }
6262
- const common = readFileSync7(commonPath, "utf8");
6263
- const brief = readFileSync7(briefPath, "utf8");
6585
+ const common = readFileSync8(commonPath, "utf8");
6586
+ const brief = readFileSync8(briefPath, "utf8");
6264
6587
  const effectiveCommon = o.divergedPrompt ? "" : common;
6265
6588
  const effectiveBrief = o.divergedPrompt ?? brief;
6266
6589
  const prompt = assemblePrompt({
@@ -6271,7 +6594,7 @@ function planAgentRun(o) {
6271
6594
  digest
6272
6595
  });
6273
6596
  const promptSha = createHash2("sha256").update(`${effectiveCommon}${effectiveBrief}`).digest("hex");
6274
- const streamsDir = join8(o.stateDir, "streams");
6597
+ const streamsDir = join9(o.stateDir, "streams");
6275
6598
  const base = `${routeLabel}-${o.role}-${o.cycle}`;
6276
6599
  const seat = resolveSeat(o.ship, o.route, o.role, o.agentPersona ?? (o.agentModel ? { model: o.agentModel } : {}));
6277
6600
  const { toolchain, model } = seat;
@@ -6326,8 +6649,8 @@ function planAgentRun(o) {
6326
6649
  digestAttached: digest !== void 0,
6327
6650
  digestAgeSeconds: ageSeconds,
6328
6651
  model,
6329
- streamPath: join8(streamsDir, `${base}.jsonl`),
6330
- eventsPath: join8(streamsDir, `${base}.events.jsonl`),
6652
+ streamPath: join9(streamsDir, `${base}.jsonl`),
6653
+ eventsPath: join9(streamsDir, `${base}.events.jsonl`),
6331
6654
  ticket: o.ticket,
6332
6655
  agentLog,
6333
6656
  promptVersion: o.route.resolved?.agentPersonas?.[o.role]?.historyId,
@@ -6354,7 +6677,7 @@ function describePlan(p) {
6354
6677
  }
6355
6678
  function openSink(path) {
6356
6679
  try {
6357
- mkdirSync5(dirname5(path), { recursive: true });
6680
+ mkdirSync6(dirname6(path), { recursive: true });
6358
6681
  return createWriteStream(path, { flags: "a" });
6359
6682
  } catch {
6360
6683
  return void 0;
@@ -6463,7 +6786,7 @@ function spawnAgent(plan, emit) {
6463
6786
  provider: plan.provider ?? LEGACY_TOOLCHAIN_PROVIDER
6464
6787
  };
6465
6788
  try {
6466
- writeFileSync4(`${plan.streamPath}.meta.json`, JSON.stringify({
6789
+ writeFileSync5(`${plan.streamPath}.meta.json`, JSON.stringify({
6467
6790
  route: plan.route,
6468
6791
  role: plan.role,
6469
6792
  ticket: plan.ticket ?? null,
@@ -6522,13 +6845,13 @@ function spawnAgent(plan, emit) {
6522
6845
  }
6523
6846
 
6524
6847
  // src/run-worktree.ts
6525
- import { existsSync as existsSync9 } from "fs";
6526
- import { dirname as dirname6, join as join10 } from "path";
6848
+ import { existsSync as existsSync10 } from "fs";
6849
+ import { dirname as dirname7, join as join11 } from "path";
6527
6850
 
6528
6851
  // src/ports.ts
6529
6852
  import { execFileSync as execFileSync2 } from "child_process";
6530
- import { existsSync as existsSync8 } from "fs";
6531
- import { join as join9 } from "path";
6853
+ import { existsSync as existsSync9 } from "fs";
6854
+ import { join as join10 } from "path";
6532
6855
  var run = (cmd, args) => {
6533
6856
  try {
6534
6857
  return execFileSync2(cmd, args, { encoding: "utf8", stdio: ["ignore", "pipe", "ignore"] });
@@ -6573,7 +6896,7 @@ function ticketForPort(port, s = DEFAULT_PORTS) {
6573
6896
  }
6574
6897
  function worktreeExists(parent, prefix, n) {
6575
6898
  for (const name of [`${prefix}${n}`, `${prefix}${String(n).padStart(3, "0")}`]) {
6576
- if (existsSync8(join9(parent, name, ".git"))) return true;
6899
+ if (existsSync9(join10(parent, name, ".git"))) return true;
6577
6900
  }
6578
6901
  return false;
6579
6902
  }
@@ -6647,7 +6970,7 @@ function planRunWorktree(dir, cfg, t, role) {
6647
6970
  const existing = worktreeForNumber(dir, num);
6648
6971
  const located = locateBranchForTicket(dir, cfg, t, role);
6649
6972
  const baseRef = hasRemote ? `${remote}/${base}` : base;
6650
- if (existing && !existing.prunable && existsSync9(existing.path)) {
6973
+ if (existing && !existing.prunable && existsSync10(existing.path)) {
6651
6974
  const branch = existing.branch;
6652
6975
  if (status(existing.path).length > 0) return { action: "leave-dirty", path: existing.path, branch };
6653
6976
  if (!branch) return { action: "leave-unpushed", path: existing.path, branch: "(detached HEAD)" };
@@ -6661,7 +6984,7 @@ function planRunWorktree(dir, cfg, t, role) {
6661
6984
  if (countCommits(existing.path, `${baseRef}..HEAD`) > 0) return { action: "leave-unpushed", path: existing.path, branch };
6662
6985
  return { action: "recut", path: existing.path, branch, base: baseRef };
6663
6986
  }
6664
- const path = join10(dirname6(dir), worktreeForTicket(dir, cfg, t, role));
6987
+ const path = join11(dirname7(dir), worktreeForTicket(dir, cfg, t, role));
6665
6988
  if (located?.where === "remote" || located && hasRemote && remoteBranchExists(dir, remote, located.branch)) {
6666
6989
  return { action: "cut-remote", path, branch: located.branch };
6667
6990
  }
@@ -6708,7 +7031,7 @@ function worktreeHoldsUnpushed(path, remote) {
6708
7031
  }
6709
7032
  async function finishRunWorktree(dir, cfg, branch) {
6710
7033
  const wt = worktrees(dir).find((w) => w.branch === branch && !w.prunable && w.path !== dir);
6711
- if (!wt || !existsSync9(wt.path)) return { kind: "none" };
7034
+ if (!wt || !existsSync10(wt.path)) return { kind: "none" };
6712
7035
  if (cfg.hooks.handoff) return { kind: "kept-handoff", path: wt.path };
6713
7036
  if (status(wt.path).length > 0) return { kind: "kept-dirty", path: wt.path };
6714
7037
  if (remoteConfigured(dir, cfg.branch.remote) && worktreeHoldsUnpushed(wt.path, cfg.branch.remote)) {
@@ -6720,7 +7043,7 @@ async function finishRunWorktree(dir, cfg, branch) {
6720
7043
  if (pids.length) await killGently(pids);
6721
7044
  }
6722
7045
  gitOk(dir, ["worktree", "remove", wt.path]);
6723
- return existsSync9(wt.path) ? { kind: "kept-dirty", path: wt.path } : { kind: "removed", path: wt.path };
7046
+ return existsSync10(wt.path) ? { kind: "kept-dirty", path: wt.path } : { kind: "removed", path: wt.path };
6724
7047
  }
6725
7048
 
6726
7049
  // src/handoff-guard.ts
@@ -6869,13 +7192,13 @@ async function retireRunWorktree(tracker, dir, cfg, t, role, seatId, shipName2,
6869
7192
  }
6870
7193
 
6871
7194
  // 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";
7195
+ import { delimiter, dirname as dirname8, join as join12 } from "path";
7196
+ import { accessSync as accessSync2, constants as constants2, existsSync as existsSync11, mkdirSync as mkdirSync7, writeFileSync as writeFileSync6 } from "fs";
6874
7197
  import { stringify } from "yaml";
6875
7198
  var HOST_PASSENGERS_DOCS_URL = "https://crew.tablation.dev/host-passengers";
6876
7199
  function shouldRunWizard(opts) {
6877
7200
  if (!opts.isTTY) return false;
6878
- const exists = opts.configExists ?? existsSync10;
7201
+ const exists = opts.configExists ?? existsSync11;
6879
7202
  return !configSearchPath(opts.crewHome).some((p) => exists(p));
6880
7203
  }
6881
7204
  function firstRunConfigPath() {
@@ -6892,7 +7215,7 @@ function defaultIsExecutable(p) {
6892
7215
  function findOnPath2(name, pathEnv, isExecutable = defaultIsExecutable) {
6893
7216
  for (const dir of pathEnv.split(delimiter)) {
6894
7217
  if (!dir) continue;
6895
- const candidate = join11(dir, name);
7218
+ const candidate = join12(dir, name);
6896
7219
  if (isExecutable(candidate)) return candidate;
6897
7220
  }
6898
7221
  return void 0;
@@ -6918,9 +7241,9 @@ async function runWizard(p, env) {
6918
7241
  if (entered && isExec(entered)) agentBin = entered;
6919
7242
  else p.say(entered ? `${entered} is not an executable file.` : "A path is required.");
6920
7243
  }
6921
- const nodeDir = dirname7(env.nodePath);
7244
+ const nodeDir = dirname8(env.nodePath);
6922
7245
  const pnpmPath = findOnPath2("pnpm", env.pathEnv, isExec);
6923
- const baseDirs = [nodeDir, pnpmPath ? dirname7(pnpmPath) : void 0];
7246
+ const baseDirs = [nodeDir, pnpmPath ? dirname8(pnpmPath) : void 0];
6924
7247
  p.say(
6925
7248
  `
6926
7249
  Host Passengers lets this machine run the agent sessions that other people's
@@ -6931,7 +7254,7 @@ Tablation workspaces ask for, in a container. More: ${HOST_PASSENGERS_DOCS_URL}`
6931
7254
  p.say("Docker is not available here, so nothing will be hosted until it is. Your answer is recorded anyway.");
6932
7255
  }
6933
7256
  const dockerPath = hostPassengers ? findOnPath2("docker", env.pathEnv, isExec) : void 0;
6934
- const extraPath = deriveExtraPath([...baseDirs, dockerPath ? dirname7(dockerPath) : void 0]);
7257
+ const extraPath = deriveExtraPath([...baseDirs, dockerPath ? dirname8(dockerPath) : void 0]);
6935
7258
  if (extraPath) {
6936
7259
  p.say(`Hooks will get ${extraPath} on their PATH (where node, pnpm${dockerPath ? " and docker" : ""} live).`);
6937
7260
  }
@@ -6953,9 +7276,9 @@ routes:
6953
7276
  ${routeBlock}`;
6954
7277
  }
6955
7278
  function writeNewConfig(path, text) {
6956
- mkdirSync6(dirname7(path), { recursive: true });
7279
+ mkdirSync7(dirname8(path), { recursive: true });
6957
7280
  try {
6958
- writeFileSync5(path, text, { mode: 384, flag: "wx" });
7281
+ writeFileSync6(path, text, { mode: 384, flag: "wx" });
6959
7282
  return true;
6960
7283
  } catch (e) {
6961
7284
  if (e.code === "EEXIST") return false;
@@ -6987,8 +7310,8 @@ Wrote ${o.configPath}.
6987
7310
  }
6988
7311
 
6989
7312
  // 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";
7313
+ import { readFileSync as readFileSync9, writeFileSync as writeFileSync7, existsSync as existsSync12, copyFileSync, mkdirSync as mkdirSync8, mkdtempSync, rmSync as rmSync4 } from "fs";
7314
+ import { dirname as dirname9, join as join13 } from "path";
6992
7315
  import { tmpdir as tmpdir2 } from "os";
6993
7316
 
6994
7317
  // src/release.ts
@@ -7646,13 +7969,13 @@ async function bump(o, size) {
7646
7969
  }
7647
7970
  return printed;
7648
7971
  }
7649
- const files = o.repo.release.versionFiles.map((f) => join12(o.cwd, f));
7972
+ const files = o.repo.release.versionFiles.map((f) => join13(o.cwd, f));
7650
7973
  const first = files[0];
7651
- if (!existsSync11(first)) {
7974
+ if (!existsSync12(first)) {
7652
7975
  o.emit.error(`no ${o.repo.release.versionFiles[0]} to read a version from`);
7653
7976
  return null;
7654
7977
  }
7655
- const current = JSON.parse(readFileSync8(first, "utf8")).version;
7978
+ const current = JSON.parse(readFileSync9(first, "utf8")).version;
7656
7979
  if (!current) {
7657
7980
  o.emit.error(`no version field in ${o.repo.release.versionFiles[0]}`);
7658
7981
  return null;
@@ -7665,27 +7988,27 @@ async function bump(o, size) {
7665
7988
  return version;
7666
7989
  }
7667
7990
  for (const f of files) {
7668
- const src = readFileSync8(f, "utf8");
7991
+ const src = readFileSync9(f, "utf8");
7669
7992
  const out = src.replace(/("version"\s*:\s*")[^"]*(")/, `$1${version}$2`);
7670
7993
  if (out === src) {
7671
7994
  o.emit.error(`no version field in ${f}`);
7672
7995
  return null;
7673
7996
  }
7674
- writeFileSync6(f, out);
7997
+ writeFileSync7(f, out);
7675
7998
  }
7676
7999
  return version;
7677
8000
  }
7678
8001
  function writeChangelog(o, version, entries) {
7679
8002
  const file = o.repo.release.changelog;
7680
8003
  if (!file || entries.length === 0) return;
7681
- const path = join12(o.cwd, file);
8004
+ const path = join13(o.cwd, file);
7682
8005
  const section = renderChangelogSection(version, (/* @__PURE__ */ new Date()).toISOString().slice(0, 10), entries);
7683
8006
  if (o.dryRun) {
7684
8007
  o.emit.emit(`would prepend ${entries.length} entr(ies) to ${file}`);
7685
8008
  return;
7686
8009
  }
7687
- const existing = existsSync11(path) ? readFileSync8(path, "utf8") : "# Changelog\n";
7688
- writeFileSync6(path, insertChangelogSection(existing, section));
8010
+ const existing = existsSync12(path) ? readFileSync9(path, "utf8") : "# Changelog\n";
8011
+ writeFileSync7(path, insertChangelogSection(existing, section));
7689
8012
  }
7690
8013
  async function confirm(o, expected) {
7691
8014
  if (!o.repo.hooks.released) return void 0;
@@ -7766,11 +8089,11 @@ async function confirmLastRelease(o, decision) {
7766
8089
  function openReleaseCheckout(o) {
7767
8090
  const { remote, base } = o.repo.branch;
7768
8091
  const branch = `crew/release-${Date.now()}`;
7769
- const root = mkdtempSync(join12(tmpdir2(), "crew-release-"));
7770
- const dir = join12(root, "checkout");
8092
+ const root = mkdtempSync(join13(tmpdir2(), "crew-release-"));
8093
+ const dir = join13(root, "checkout");
7771
8094
  const close = () => {
7772
8095
  gitOk(o.cwd, ["worktree", "remove", "--force", dir]);
7773
- rmSync3(root, { recursive: true, force: true });
8096
+ rmSync4(root, { recursive: true, force: true });
7774
8097
  gitOk(o.cwd, ["worktree", "prune"]);
7775
8098
  gitOk(o.cwd, ["branch", "-D", branch]);
7776
8099
  };
@@ -7781,10 +8104,10 @@ function openReleaseCheckout(o) {
7781
8104
  throw e;
7782
8105
  }
7783
8106
  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));
8107
+ const src = join13(o.cwd, f);
8108
+ if (!existsSync12(src)) continue;
8109
+ mkdirSync8(dirname9(join13(dir, f)), { recursive: true });
8110
+ copyFileSync(src, join13(dir, f));
7788
8111
  }
7789
8112
  return { dir, branch, close };
7790
8113
  }
@@ -8253,7 +8576,7 @@ function missingReleaserWarning(repo, release, ships, now = Date.now()) {
8253
8576
  }
8254
8577
 
8255
8578
  // src/watch.ts
8256
- import { watch as watchFile, existsSync as existsSync12, statSync as statSync6, openSync as openSync3, readSync, closeSync as closeSync3 } from "fs";
8579
+ import { watch as watchFile, existsSync as existsSync13, statSync as statSync6, openSync as openSync3, readSync, closeSync as closeSync3 } from "fs";
8257
8580
  var E = "\x1B[";
8258
8581
  var dim = (s) => `${E}2m${s}${E}0m`;
8259
8582
  var bold = (s) => `${E}1m${s}${E}0m`;
@@ -8354,7 +8677,7 @@ function renderHeaders(states, now = Date.now()) {
8354
8677
  return states.map((s) => renderHeader(s, now)).join("");
8355
8678
  }
8356
8679
  function readFrom(file, from) {
8357
- if (!existsSync12(file)) return { events: [], offset: from };
8680
+ if (!existsSync13(file)) return { events: [], offset: from };
8358
8681
  const size = statSync6(file).size;
8359
8682
  if (size < from) return readFrom(file, 0);
8360
8683
  if (size === from) return { events: [], offset: from };
@@ -8594,8 +8917,8 @@ function nextRoles(decision, ran) {
8594
8917
  }
8595
8918
 
8596
8919
  // src/agents.ts
8597
- import { readFileSync as readFileSync9, existsSync as existsSync13 } from "fs";
8598
- import { join as join13 } from "path";
8920
+ import { readFileSync as readFileSync10, existsSync as existsSync14 } from "fs";
8921
+ import { join as join14 } from "path";
8599
8922
  import { TablationClient as TablationClient2, StaleWriteError as StaleWriteError6, TablationApiError } from "@tablation/client";
8600
8923
  var PERSONA_NAME = {
8601
8924
  dev: "Developer",
@@ -8609,12 +8932,12 @@ var CREW_AGENT_COLUMN = "agent_id";
8609
8932
  var AgentsSyncError = class extends Error {
8610
8933
  };
8611
8934
  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")}`;
8935
+ const briefPath = join14(promptsDir, "personas", `lane-${role}.md`);
8936
+ if (!existsSync14(briefPath)) throw new AgentsSyncError(`no brief at ${briefPath}`);
8937
+ if (role === "pair") return readFileSync10(briefPath, "utf8");
8938
+ const commonPath = join14(promptsDir, "personas", "common.md");
8939
+ if (!existsSync14(commonPath)) throw new AgentsSyncError(`no shared policy at ${commonPath}`);
8940
+ return `${readFileSync10(commonPath, "utf8")}${readFileSync10(briefPath, "utf8")}`;
8618
8941
  }
8619
8942
  async function resolveAgentId(route, role, opts) {
8620
8943
  const cached = route.resolved?.agentPersonas?.[role]?.agentId;
@@ -9291,14 +9614,14 @@ import {
9291
9614
  } from "@tablation/client";
9292
9615
 
9293
9616
  // src/skills.ts
9294
- import { readFileSync as readFileSync10, readdirSync as readdirSync2, existsSync as existsSync14 } from "fs";
9295
- import { join as join14 } from "path";
9617
+ import { readFileSync as readFileSync11, readdirSync as readdirSync2, existsSync as existsSync15 } from "fs";
9618
+ import { join as join15 } from "path";
9296
9619
  import { parse as parseYaml } from "yaml";
9297
9620
  import { TablationClient as TablationClient3, StaleWriteError as StaleWriteError7, TablationApiError as TablationApiError2 } from "@tablation/client";
9298
9621
  var SkillSyncError = class extends Error {
9299
9622
  };
9300
9623
  function parseSkillFile(path) {
9301
- const text = readFileSync10(path, "utf8");
9624
+ const text = readFileSync11(path, "utf8");
9302
9625
  const match = /^---\n([\s\S]*?)\n---\n([\s\S]*)$/.exec(text);
9303
9626
  if (!match) throw new SkillSyncError(`${path}: missing YAML frontmatter (expected a leading --- block)`);
9304
9627
  const frontmatter = match[1];
@@ -9311,9 +9634,9 @@ function parseSkillFile(path) {
9311
9634
  return { name: meta.name, description: meta.description, prompt: body, file: path };
9312
9635
  }
9313
9636
  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)));
9637
+ const dir = join15(promptsDir, "skills");
9638
+ if (!existsSync15(dir)) return [];
9639
+ return readdirSync2(dir).filter((f) => f.endsWith(".md")).map((f) => parseSkillFile(join15(dir, f)));
9317
9640
  }
9318
9641
  async function syncSkills(route, opts) {
9319
9642
  if (!route.resolved) {
@@ -9492,7 +9815,7 @@ async function showLogEntry(route, entryId, opts = {}) {
9492
9815
  }
9493
9816
 
9494
9817
  // src/repos-cmd.ts
9495
- import { homedir as homedir4 } from "os";
9818
+ import { homedir as homedir5 } from "os";
9496
9819
  import { basename as basename7, resolve as resolve3 } from "path";
9497
9820
  import { isMap, isSeq, parseDocument, stringify as stringify2 } from "yaml";
9498
9821
  var ReposError = class extends Error {
@@ -9539,7 +9862,7 @@ function parseAddArgs(args, routes, isDir) {
9539
9862
  return { route, path: "." };
9540
9863
  }
9541
9864
  function expandDir(p, base) {
9542
- const home = p === "~" ? homedir4() : p.startsWith("~/") ? `${homedir4()}/${p.slice(2)}` : p;
9865
+ const home = p === "~" ? homedir5() : p.startsWith("~/") ? `${homedir5()}/${p.slice(2)}` : p;
9543
9866
  return resolve3(base, home);
9544
9867
  }
9545
9868
  function routeMapOf(doc, route) {
@@ -9674,7 +9997,7 @@ function schedulerInstalled(plan, exists, crontab) {
9674
9997
  }
9675
9998
 
9676
9999
  // src/worktree-sweep.ts
9677
- import { existsSync as existsSync15 } from "fs";
10000
+ import { existsSync as existsSync16 } from "fs";
9678
10001
  import { basename as basename8 } from "path";
9679
10002
  function planWorktreeSweep(target, tickets, contract) {
9680
10003
  const terminal = new Set(contract.statuses.resolved.filter((s) => s !== contract.statuses.verified));
@@ -9702,7 +10025,7 @@ async function applyWorktreeSweep(cwd, actions, dryRun, log, remote = "origin")
9702
10025
  let removed = 0;
9703
10026
  let keptBranches = 0;
9704
10027
  for (const a of actions) {
9705
- if (!dryRun && existsSync15(a.path)) {
10028
+ if (!dryRun && existsSync16(a.path)) {
9706
10029
  let dirty = false;
9707
10030
  try {
9708
10031
  dirty = status(a.path).length > 0;
@@ -9716,7 +10039,7 @@ async function applyWorktreeSweep(cwd, actions, dryRun, log, remote = "origin")
9716
10039
  continue;
9717
10040
  }
9718
10041
  }
9719
- if (!dryRun && a.requirePushed && a.branch && existsSync15(a.path) && remoteConfigured(cwd, remote) && worktreeHoldsUnpushed(a.path, remote)) {
10042
+ if (!dryRun && a.requirePushed && a.branch && existsSync16(a.path) && remoteConfigured(cwd, remote) && worktreeHoldsUnpushed(a.path, remote)) {
9720
10043
  log.warn(
9721
10044
  `not removing worktree ${a.path} for ${a.ticket.issue_id} (${a.ticket.status}): ${a.branch} has commits that are not on ${remote}`,
9722
10045
  { ticket: a.ticket.issue_id, step: "worktree" }
@@ -9797,14 +10120,14 @@ function applyRemoteBranchCleanup(cwd, remote, branches2, dryRun, log) {
9797
10120
  }
9798
10121
 
9799
10122
  // 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";
10123
+ import { existsSync as existsSync17, readdirSync as readdirSync3, rmSync as rmSync5, statSync as statSync7 } from "fs";
10124
+ import { join as join16 } from "path";
9802
10125
  var DAY_MS2 = 24 * 60 * 60 * 1e3;
9803
10126
  function planStreamSweep(stateDir, retentionDays, now = Date.now()) {
9804
- const dir = join15(stateDir, "streams");
9805
- if (!existsSync16(dir)) return [];
10127
+ const dir = join16(stateDir, "streams");
10128
+ if (!existsSync17(dir)) return [];
9806
10129
  const cutoff = now - retentionDays * DAY_MS2;
9807
- return readdirSync3(dir).map((name) => join15(dir, name)).filter((path) => {
10130
+ return readdirSync3(dir).map((name) => join16(dir, name)).filter((path) => {
9808
10131
  try {
9809
10132
  return statSync7(path).mtimeMs < cutoff;
9810
10133
  } catch {
@@ -9820,7 +10143,7 @@ function applyStreamSweep(paths, dryRun, log) {
9820
10143
  continue;
9821
10144
  }
9822
10145
  try {
9823
- rmSync4(path, { force: true });
10146
+ rmSync5(path, { force: true });
9824
10147
  removed++;
9825
10148
  log.emit(`removed stream artifact ${path}`, { step: "worktree" });
9826
10149
  } catch (e) {
@@ -9830,9 +10153,9 @@ function applyStreamSweep(paths, dryRun, log) {
9830
10153
  return removed;
9831
10154
  }
9832
10155
  function planEventsSweep(stateDir, retentionDays, now = Date.now()) {
9833
- if (!existsSync16(stateDir)) return [];
10156
+ if (!existsSync17(stateDir)) return [];
9834
10157
  const cutoff = now - retentionDays * DAY_MS2;
9835
- return readdirSync3(stateDir).filter((name) => ROTATED_EVENTS_RE.test(name)).map((name) => join15(stateDir, name)).filter((path) => {
10158
+ return readdirSync3(stateDir).filter((name) => ROTATED_EVENTS_RE.test(name)).map((name) => join16(stateDir, name)).filter((path) => {
9836
10159
  try {
9837
10160
  return statSync7(path).mtimeMs < cutoff;
9838
10161
  } catch {
@@ -9842,24 +10165,24 @@ function planEventsSweep(stateDir, retentionDays, now = Date.now()) {
9842
10165
  }
9843
10166
 
9844
10167
  // 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";
10168
+ 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
10169
  import { createInterface } from "readline/promises";
9847
10170
  import { dirname as dirOf, resolve as resolvePath } from "path";
9848
10171
 
9849
10172
  // src/ssh-keys.ts
9850
10173
  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";
10174
+ import { existsSync as existsSync18, mkdirSync as mkdirSync9, readFileSync as readFileSync12, chmodSync } from "fs";
10175
+ import { dirname as dirname11, join as join17 } from "path";
9853
10176
  var SshKeyError = class extends Error {
9854
10177
  };
9855
10178
  function shipSshKeyPathFor(stateDir) {
9856
- return join16(stateDir, "ssh", "id_ed25519");
10179
+ return join17(stateDir, "ssh", "id_ed25519");
9857
10180
  }
9858
10181
  function ensureShipSshKeypair(stateDir) {
9859
10182
  const privateKeyPath = shipSshKeyPathFor(stateDir);
9860
10183
  const publicKeyPath = `${privateKeyPath}.pub`;
9861
- if (!existsSync17(privateKeyPath)) {
9862
- mkdirSync8(dirname10(privateKeyPath), { recursive: true });
10184
+ if (!existsSync18(privateKeyPath)) {
10185
+ mkdirSync9(dirname11(privateKeyPath), { recursive: true });
9863
10186
  try {
9864
10187
  execFileSync3("ssh-keygen", [
9865
10188
  "-t",
@@ -9879,10 +10202,10 @@ function ensureShipSshKeypair(stateDir) {
9879
10202
  }
9880
10203
  chmodSync(privateKeyPath, 384);
9881
10204
  }
9882
- if (!existsSync17(publicKeyPath)) {
10205
+ if (!existsSync18(publicKeyPath)) {
9883
10206
  throw new SshKeyError(`${privateKeyPath} exists but ${publicKeyPath} does not \u2014 regenerate or restore it`);
9884
10207
  }
9885
- const publicKey = readFileSync11(publicKeyPath, "utf8").trim();
10208
+ const publicKey = readFileSync12(publicKeyPath, "utf8").trim();
9886
10209
  return { privateKeyPath, publicKeyPath, publicKey };
9887
10210
  }
9888
10211
  function sshKeygenAvailable() {
@@ -10096,9 +10419,9 @@ function syncAllPassengerCheckouts(cfg) {
10096
10419
 
10097
10420
  // src/tunnel.ts
10098
10421
  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";
10422
+ import { existsSync as existsSync19, mkdirSync as mkdirSync10, readFileSync as readFileSync13, writeFileSync as writeFileSync8, unlinkSync as unlinkSync3, readdirSync as readdirSync4 } from "fs";
10100
10423
  import { randomInt } from "crypto";
10101
- import { join as join17 } from "path";
10424
+ import { join as join18 } from "path";
10102
10425
  var DEFAULT_SPAWN_OPTIONS = { stdio: "ignore", detached: true };
10103
10426
  var defaultSpawn = (cmd, args) => spawn3(cmd, args, DEFAULT_SPAWN_OPTIONS);
10104
10427
  var SLUG_ALPHABET = "abcdefghijklmnopqrstuvwxyz0123456789";
@@ -10165,21 +10488,21 @@ function startTunnel(opts) {
10165
10488
  };
10166
10489
  }
10167
10490
  function tunnelStatePath(stateDir, workspaceId) {
10168
- return join17(stateDir, "passengers", `${workspaceId}.json`);
10491
+ return join18(stateDir, "passengers", `${workspaceId}.json`);
10169
10492
  }
10170
10493
  function readPersistedTunnel(stateDir, workspaceId) {
10171
10494
  const path = tunnelStatePath(stateDir, workspaceId);
10172
- if (!existsSync18(path)) return void 0;
10495
+ if (!existsSync19(path)) return void 0;
10173
10496
  try {
10174
- return JSON.parse(readFileSync12(path, "utf8"));
10497
+ return JSON.parse(readFileSync13(path, "utf8"));
10175
10498
  } catch {
10176
10499
  return void 0;
10177
10500
  }
10178
10501
  }
10179
10502
  function writePersistedTunnel(stateDir, state) {
10180
10503
  const path = tunnelStatePath(stateDir, state.workspaceId);
10181
- mkdirSync9(join17(stateDir, "passengers"), { recursive: true });
10182
- writeFileSync7(path, `${JSON.stringify(state, null, 2)}
10504
+ mkdirSync10(join18(stateDir, "passengers"), { recursive: true });
10505
+ writeFileSync8(path, `${JSON.stringify(state, null, 2)}
10183
10506
  `);
10184
10507
  }
10185
10508
  function updatePersistedTunnel(stateDir, workspaceId, patch2) {
@@ -10276,8 +10599,8 @@ function syncPassengerTunnels(plans, relayHost, relayPort, privateKeyPath, state
10276
10599
  }
10277
10600
  }
10278
10601
  function readAllPersistedWorkspaceIds(stateDir) {
10279
- const dir = join17(stateDir, "passengers");
10280
- if (!existsSync18(dir)) return [];
10602
+ const dir = join18(stateDir, "passengers");
10603
+ if (!existsSync19(dir)) return [];
10281
10604
  return readdirSync4(dir).filter((f) => f.endsWith(".json") && !f.endsWith("-sync.json")).map((f) => f.slice(0, -".json".length));
10282
10605
  }
10283
10606
 
@@ -10398,8 +10721,8 @@ async function unregisterEndpoint(workspaceId, info, d) {
10398
10721
  import { createServer as createHttpServer } from "http";
10399
10722
  import { spawn as spawn4 } from "child_process";
10400
10723
  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";
10724
+ import { existsSync as existsSync20, mkdirSync as mkdirSync11, readFileSync as readFileSync14, writeFileSync as writeFileSync9, unlinkSync as unlinkSync4, readdirSync as readdirSync5 } from "fs";
10725
+ import { join as join19 } from "path";
10403
10726
 
10404
10727
  // src/secret-compare.ts
10405
10728
  import { timingSafeEqual } from "crypto";
@@ -10488,21 +10811,21 @@ var defaultSpawn2 = (cmd, args, env) => {
10488
10811
  return child;
10489
10812
  };
10490
10813
  function syncDaemonStatePath(stateDir, workspaceId) {
10491
- return join18(stateDir, "passengers", `${workspaceId}-sync.json`);
10814
+ return join19(stateDir, "passengers", `${workspaceId}-sync.json`);
10492
10815
  }
10493
10816
  function readPersistedSyncDaemon(stateDir, workspaceId) {
10494
10817
  const path = syncDaemonStatePath(stateDir, workspaceId);
10495
- if (!existsSync19(path)) return void 0;
10818
+ if (!existsSync20(path)) return void 0;
10496
10819
  try {
10497
- return JSON.parse(readFileSync13(path, "utf8"));
10820
+ return JSON.parse(readFileSync14(path, "utf8"));
10498
10821
  } catch {
10499
10822
  return void 0;
10500
10823
  }
10501
10824
  }
10502
10825
  function writePersistedSyncDaemon(stateDir, state) {
10503
10826
  const path = syncDaemonStatePath(stateDir, state.workspaceId);
10504
- mkdirSync10(join18(stateDir, "passengers"), { recursive: true });
10505
- writeFileSync8(path, `${JSON.stringify(state, null, 2)}
10827
+ mkdirSync11(join19(stateDir, "passengers"), { recursive: true });
10828
+ writeFileSync9(path, `${JSON.stringify(state, null, 2)}
10506
10829
  `);
10507
10830
  }
10508
10831
  function removePersistedSyncDaemon(stateDir, workspaceId) {
@@ -10522,8 +10845,8 @@ function waitForExit(pid, alive, timeoutMs = 5e3) {
10522
10845
  }
10523
10846
  }
10524
10847
  function readAllPersistedSyncWorkspaceIds(stateDir) {
10525
- const dir = join18(stateDir, "passengers");
10526
- if (!existsSync19(dir)) return [];
10848
+ const dir = join19(stateDir, "passengers");
10849
+ if (!existsSync20(dir)) return [];
10527
10850
  const suffix = "-sync.json";
10528
10851
  return readdirSync5(dir).filter((f) => f.endsWith(suffix)).map((f) => f.slice(0, -suffix.length));
10529
10852
  }
@@ -10581,7 +10904,7 @@ function syncPassengerSyncDaemons(plans, cfg, stateDir, deps = {}) {
10581
10904
 
10582
10905
  // src/daemon.ts
10583
10906
  import { readdirSync as readdirSync6, statSync as statSync8 } from "fs";
10584
- import { join as join19 } from "path";
10907
+ import { join as join20 } from "path";
10585
10908
  var DEFAULT_FLOOR_MS = 5e3;
10586
10909
  var DEFAULT_CEILING_MS = INTERVAL_SECONDS * 1e3;
10587
10910
  var defaultSleep = (ms2) => new Promise((resolve5) => setTimeout(resolve5, ms2));
@@ -10620,7 +10943,7 @@ function latestMtime(path) {
10620
10943
  if (!stat.isDirectory()) return 0;
10621
10944
  let latest = stat.mtimeMs;
10622
10945
  for (const entry of readdirSync6(path, { withFileTypes: true })) {
10623
- latest = Math.max(latest, latestMtime(join19(path, entry.name)));
10946
+ latest = Math.max(latest, latestMtime(join20(path, entry.name)));
10624
10947
  }
10625
10948
  return latest;
10626
10949
  }
@@ -10906,7 +11229,7 @@ async function buildEnvironment(route, ship, ticket) {
10906
11229
  }
10907
11230
 
10908
11231
  // src/cli.ts
10909
- var CREW_HOME = isCompiledBinary(import.meta.url) ? dirname11(process.execPath) : resolve4(dirname11(fileURLToPath(import.meta.url)), "..");
11232
+ var CREW_HOME = isCompiledBinary(import.meta.url) ? dirname12(process.execPath) : resolve4(dirname12(fileURLToPath(import.meta.url)), "..");
10910
11233
  var RELEASE_LOCK_TTL_MS = 20 * 60 * 1e3;
10911
11234
  var CYCLE_SECONDS2 = 120;
10912
11235
  var STALE_CYCLES = 3;
@@ -11040,8 +11363,9 @@ function usage() {
11040
11363
  crew log [route] tail the log
11041
11364
  crew inbox [--member NAME] your tickets across every workspace (or a colleague's)
11042
11365
  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)
11366
+ crew disconnect remove what connect installed for Pair (Claude MCP; Codex/Gemini instructions block and MCP entry per ship.pair)
11043
11367
  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)
11368
+ 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
11369
  crew skills sync [route] push only crew's skill files (e.g. grill-me) into the workspace Agent Skills table
11046
11370
  crew repos add [ROUTE] [NAME] [PATH] attach a local checkout to a route in crew.yaml (--dry-run previews)
11047
11371
  crew repos list ROUTE the checkouts a route has, with the tracker's Repos row each matches
@@ -11080,7 +11404,7 @@ var value = (name) => {
11080
11404
  var positional = argv.filter((a, i) => !a.startsWith("--") && !argv[i - 1]?.startsWith("--"));
11081
11405
  var command = positional[0];
11082
11406
  if (!command) usage();
11083
- var STATE_WRITING_COMMANDS = /* @__PURE__ */ new Set(["connect", "install", "uninstall", "release", "merge", "deploy"]);
11407
+ var STATE_WRITING_COMMANDS = /* @__PURE__ */ new Set(["connect", "disconnect", "install", "uninstall", "release", "merge", "deploy"]);
11084
11408
  var STATE_WRITING_SUBCOMMANDS = {
11085
11409
  agents: /* @__PURE__ */ new Set(["sync"]),
11086
11410
  skills: /* @__PURE__ */ new Set(["sync"])
@@ -11190,7 +11514,7 @@ if (command === "passenger-sync-daemon") {
11190
11514
  const releaseFleetWide = ["merge", "deploy", "release"].includes(command) && flag("fleet");
11191
11515
  const DAEMON_SUBCOMMANDS = /* @__PURE__ */ new Set(["start", "stop", "status", "restart"]);
11192
11516
  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;
11517
+ 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
11518
  let route;
11195
11519
  try {
11196
11520
  route = fleetWide ? cfg.routes[0] : findRoute(cfg, named);
@@ -11396,8 +11720,8 @@ if (command === "passenger-sync-daemon") {
11396
11720
  }
11397
11721
  if (host === "linux" && hasSystemd) {
11398
11722
  const label = labelFor(CREW_HOME, "run");
11399
- const staleTimer = join20(homedir5(), ".config", "systemd", "user", `${label}.timer`);
11400
- if (existsSync20(staleTimer)) {
11723
+ const staleTimer = join21(homedir6(), ".config", "systemd", "user", `${label}.timer`);
11724
+ if (existsSync21(staleTimer)) {
11401
11725
  process.stdout.write(
11402
11726
  `removing stale run timer from before the persistent-service cutover: ${staleTimer}${dryRun ? " (dry run)" : ""}
11403
11727
  `
@@ -11423,7 +11747,7 @@ if (command === "passenger-sync-daemon") {
11423
11747
  const tracker = new Tracker(c, cfg.ship);
11424
11748
  problems.push(...validateContract(tracker.contract).map((p) => `contract: ${p}`));
11425
11749
  for (const r of await resolvedRepos(c)) {
11426
- if (!existsSync20(r.dir)) problems.push(`repo ${r.name}: ${r.dir} is missing on disk`);
11750
+ if (!existsSync21(r.dir)) problems.push(`repo ${r.name}: ${r.dir} is missing on disk`);
11427
11751
  if (!satisfies(host, r.config.platform)) problems.push(`repo ${r.name}: requires ${r.config.platform}, this host is ${host}`);
11428
11752
  problems.push(...validateEffective(r.config).map((p) => `repo ${r.name}: ${p}`));
11429
11753
  }
@@ -11441,7 +11765,7 @@ if (command === "passenger-sync-daemon") {
11441
11765
  }
11442
11766
  async function doctorFix(o) {
11443
11767
  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);
11768
+ const unitExists = (p) => existsSync21(p);
11445
11769
  const crontab = o.host === "linux" ? spawnSync3("crontab", ["-l"], { encoding: "utf8" }).stdout ?? "" : "";
11446
11770
  const schedulerAbsent = o.host !== "windows" ? !schedulerInstalled(planInstall(cfg.ship, CREW_HOME, o.host, detectSystemd(), "run"), unitExists, crontab) : true;
11447
11771
  const plan = planFix({ routes, schedulerAbsent, host: o.host });
@@ -11458,7 +11782,7 @@ if (command === "passenger-sync-daemon") {
11458
11782
  for (const a of plan.actions) {
11459
11783
  if (a.kind === "enable") {
11460
11784
  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));
11785
+ writeFileSync10(cfg.configFile, planEnable(readFileSync15(cfg.configFile, "utf8"), a.route));
11462
11786
  enabledSound.add(a.route);
11463
11787
  changed2.push(`enabled route ${a.route} in ${cfg.configFile}`);
11464
11788
  } else {
@@ -12355,7 +12679,7 @@ ${changed2.map((c) => ` - ${c}
12355
12679
  process.exit(1);
12356
12680
  }
12357
12681
  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")];
12682
+ const watchPaths = isCompiledBinary(import.meta.url) ? [process.execPath] : [join21(CREW_HOME, "src"), join21(CREW_HOME, "dist"), join21(CREW_HOME, "bin")];
12359
12683
  const spawnBreaker = new SpawnBreaker();
12360
12684
  const stopReason = await runDaemonLoop({
12361
12685
  runPass: () => runOnePass({
@@ -12441,7 +12765,7 @@ ${changed2.map((c) => ` - ${c}
12441
12765
  }
12442
12766
  try {
12443
12767
  const prompt = await currentPersonaPrompt(promptTarget, role, { userAgent: cfg.ship.userAgent });
12444
- process.stdout.write(prompt);
12768
+ process.stdout.write(prompt + (role === "pair" ? harnessPromptNote(value("harness")) : ""));
12445
12769
  } catch (e) {
12446
12770
  if (e instanceof AgentsSyncError) {
12447
12771
  process.stderr.write(`crew agents prompt: ${e.message}
@@ -12504,11 +12828,11 @@ ${changed2.map((c) => ` - ${c}
12504
12828
  `);
12505
12829
  if (!dryRun) {
12506
12830
  const resolvedPath = resolvedPathFor(cfg.ship.stateDir, target.route);
12507
- const raw = existsSync20(resolvedPath) ? JSON.parse(readFileSync14(resolvedPath, "utf8")) : {};
12831
+ const raw = existsSync21(resolvedPath) ? JSON.parse(readFileSync15(resolvedPath, "utf8")) : {};
12508
12832
  raw.agentPersonas = personaResult.agentPersonas;
12509
12833
  raw.agentSkills = skillResult.agentSkills;
12510
- mkdirSync11(dirOf(resolvedPath), { recursive: true });
12511
- writeFileSync9(resolvedPath, `${JSON.stringify(raw, null, 2)}
12834
+ mkdirSync12(dirOf(resolvedPath), { recursive: true });
12835
+ writeFileSync10(resolvedPath, `${JSON.stringify(raw, null, 2)}
12512
12836
  `);
12513
12837
  }
12514
12838
  if (personaResult.outcomes.some((o) => o.action === "diverged") || skillResult.outcomes.some((o) => o.action === "diverged")) {
@@ -12553,10 +12877,10 @@ ${changed2.map((c) => ` - ${c}
12553
12877
  `);
12554
12878
  if (!dryRun) {
12555
12879
  const resolvedPath = resolvedPathFor(cfg.ship.stateDir, target.route);
12556
- const raw = existsSync20(resolvedPath) ? JSON.parse(readFileSync14(resolvedPath, "utf8")) : {};
12880
+ const raw = existsSync21(resolvedPath) ? JSON.parse(readFileSync15(resolvedPath, "utf8")) : {};
12557
12881
  raw.agentSkills = result.agentSkills;
12558
- mkdirSync11(dirOf(resolvedPath), { recursive: true });
12559
- writeFileSync9(resolvedPath, `${JSON.stringify(raw, null, 2)}
12882
+ mkdirSync12(dirOf(resolvedPath), { recursive: true });
12883
+ writeFileSync10(resolvedPath, `${JSON.stringify(raw, null, 2)}
12560
12884
  `);
12561
12885
  }
12562
12886
  if (result.outcomes.some((o) => o.action === "diverged")) process.exitCode = 1;
@@ -12569,14 +12893,14 @@ ${changed2.map((c) => ` - ${c}
12569
12893
  process.exit(2);
12570
12894
  };
12571
12895
  if (sub !== "add" && sub !== "list") usage2();
12572
- const configText = readFileSync14(cfg.configFile, "utf8");
12896
+ const configText = readFileSync15(cfg.configFile, "utf8");
12573
12897
  let routeName = positional[2];
12574
12898
  let addPath = ".";
12575
12899
  let addName;
12576
12900
  if (sub === "list" && !routeName) usage2();
12577
12901
  if (sub === "add") {
12578
12902
  try {
12579
- const a = parseAddArgs(positional.slice(2), routeNamesOf(configText), (p) => existsSync20(resolvePath(p.replace(/^~(?=\/|$)/, homedir5()))));
12903
+ const a = parseAddArgs(positional.slice(2), routeNamesOf(configText), (p) => existsSync21(resolvePath(p.replace(/^~(?=\/|$)/, homedir6()))));
12580
12904
  routeName = a.route;
12581
12905
  addPath = a.path;
12582
12906
  addName = a.name;
@@ -12591,15 +12915,15 @@ ${changed2.map((c) => ` - ${c}
12591
12915
  }
12592
12916
  const resolvedFile = resolvedPathFor(cfg.ship.stateDir, routeName);
12593
12917
  let resolvedRemotes;
12594
- if (existsSync20(resolvedFile)) {
12918
+ if (existsSync21(resolvedFile)) {
12595
12919
  try {
12596
- resolvedRemotes = JSON.parse(readFileSync14(resolvedFile, "utf8"));
12920
+ resolvedRemotes = JSON.parse(readFileSync15(resolvedFile, "utf8"));
12597
12921
  } catch {
12598
12922
  }
12599
12923
  }
12600
12924
  const originOf = (dir) => gitOk(dir, ["remote", "get-url", "origin"]) ?? void 0;
12601
12925
  const text = configText;
12602
- const base = dirname11(cfg.configFile);
12926
+ const base = dirname12(cfg.configFile);
12603
12927
  try {
12604
12928
  if (sub === "list") {
12605
12929
  const repos = configuredRepos(text, routeName, base);
@@ -12608,14 +12932,14 @@ ${changed2.map((c) => ` - ${c}
12608
12932
  for (const r of repos) {
12609
12933
  const origin2 = originOf(r.dir);
12610
12934
  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)"}`;
12935
+ const note = !existsSync21(r.dir) ? "missing on disk" : row ? `Repos row: ${row}` : `no Repos row matches ${normalizeRemote(origin2) ?? "this checkout (no origin remote)"}`;
12612
12936
  process.stdout.write(`${r.name} ${r.dir} ${note}
12613
12937
  `);
12614
12938
  }
12615
12939
  break;
12616
12940
  }
12617
- const target = resolvePath(addPath.replace(/^~(?=\/|$)/, homedir5()));
12618
- const top = existsSync20(target) ? gitOk(target, ["rev-parse", "--show-toplevel"]) : null;
12941
+ const target = resolvePath(addPath.replace(/^~(?=\/|$)/, homedir6()));
12942
+ const top = existsSync21(target) ? gitOk(target, ["rev-parse", "--show-toplevel"]) : null;
12619
12943
  if (!top) throw new ReposError(`${target} is not a git checkout`);
12620
12944
  const origin = originOf(top);
12621
12945
  const plan = planRepoAdd({
@@ -12663,15 +12987,15 @@ ${changed2.map((c) => ` - ${c}
12663
12987
  const created = await rowTracker.createRepoRow(rowPayload);
12664
12988
  process.stdout.write(`created Repos row ${String(rowPayload.name)} (${String(rowPayload.remote)}) ${created.id}
12665
12989
  `);
12666
- if (existsSync20(resolvedFile)) {
12667
- const raw = JSON.parse(readFileSync14(resolvedFile, "utf8"));
12990
+ if (existsSync21(resolvedFile)) {
12991
+ const raw = JSON.parse(readFileSync15(resolvedFile, "utf8"));
12668
12992
  raw.repoNames = { ...raw.repoNames, [created.id]: String(rowPayload.name) };
12669
12993
  raw.repoRemotes = { ...raw.repoRemotes, [created.id]: String(rowPayload.remote) };
12670
- writeFileSync9(resolvedFile, `${JSON.stringify(raw, null, 2)}
12994
+ writeFileSync10(resolvedFile, `${JSON.stringify(raw, null, 2)}
12671
12995
  `);
12672
12996
  }
12673
12997
  }
12674
- writeFileSync9(cfg.configFile, plan.text);
12998
+ writeFileSync10(cfg.configFile, plan.text);
12675
12999
  process.stdout.write(`added ${top} to ${routeName} as ${plan.mode === "dir" ? "dir" : `repos.${plan.name}`} in ${cfg.configFile}
12676
13000
  `);
12677
13001
  } catch (e) {
@@ -12910,10 +13234,10 @@ Signed in as ${result.identity.email} \u2014 minted an API key in workspace "${r
12910
13234
  operator = await pickOperator(found.holds);
12911
13235
  }
12912
13236
  let previous;
12913
- const hadPreviousFile = existsSync20(resolvedPath);
13237
+ const hadPreviousFile = existsSync21(resolvedPath);
12914
13238
  if (hadPreviousFile) {
12915
13239
  try {
12916
- previous = JSON.parse(readFileSync14(resolvedPath, "utf8"));
13240
+ previous = JSON.parse(readFileSync15(resolvedPath, "utf8"));
12917
13241
  } catch {
12918
13242
  }
12919
13243
  }
@@ -12974,10 +13298,10 @@ Signed in as ${result.identity.email} \u2014 minted an API key in workspace "${r
12974
13298
  process.stderr.write(`(dry run) would write resolved ids to ${resolvedPath}
12975
13299
  `);
12976
13300
  } else {
12977
- mkdirSync11(dirname11(resolvedPath), { recursive: true });
13301
+ mkdirSync12(dirname12(resolvedPath), { recursive: true });
12978
13302
  if (hadPreviousFile) copyFileSync2(resolvedPath, `${resolvedPath}.bak`);
12979
13303
  const tmpPath = `${resolvedPath}.tmp${process.pid}`;
12980
- writeFileSync9(tmpPath, `${JSON.stringify(resolved, null, 2)}
13304
+ writeFileSync10(tmpPath, `${JSON.stringify(resolved, null, 2)}
12981
13305
  `);
12982
13306
  renameSync2(tmpPath, resolvedPath);
12983
13307
  process.stderr.write(`Resolved ids written to ${resolvedPath}
@@ -12991,8 +13315,8 @@ Signed in as ${result.identity.email} \u2014 minted an API key in workspace "${r
12991
13315
  process.stderr.write(`(dry run) would write the minted API key to ${apiKeyFile}
12992
13316
  `);
12993
13317
  } else {
12994
- mkdirSync11(dirname11(apiKeyFile), { recursive: true });
12995
- writeFileSync9(apiKeyFile, `${apiKeyVar}=${apiKey}
13318
+ mkdirSync12(dirname12(apiKeyFile), { recursive: true });
13319
+ writeFileSync10(apiKeyFile, `${apiKeyVar}=${apiKey}
12996
13320
  `, { mode: 384 });
12997
13321
  process.stderr.write(`API key written to ${apiKeyFile}
12998
13322
  `);
@@ -13001,7 +13325,7 @@ Signed in as ${result.identity.email} \u2014 minted an API key in workspace "${r
13001
13325
  const keyPersisted = keychainBacked || apiKeyFile !== void 0;
13002
13326
  const wizard = firstRun;
13003
13327
  const writeFirstRun = wizard !== void 0 && !dryRun && keyPersisted;
13004
- const routeDir = value("dir") ?? (writeFirstRun ? join20(DEFAULT_REPOS_BASE_PATH, found.workspaceSlug) : void 0);
13328
+ const routeDir = value("dir") ?? (writeFirstRun ? join21(DEFAULT_REPOS_BASE_PATH, found.workspaceSlug) : void 0);
13005
13329
  const routeBlock = renderConnection(
13006
13330
  found,
13007
13331
  route2,
@@ -13029,6 +13353,10 @@ Signed in as ${result.identity.email} \u2014 minted an API key in workspace "${r
13029
13353
  if (seen.has(id)) continue;
13030
13354
  seen.add(id);
13031
13355
  process.stderr.write(`${adapterFor(tc.harness).ensureMcp({ bin: tc.bin, url: mcpUrlFor(baseUrl), key: apiKey, replace, dryRun })}
13356
+ `);
13357
+ }
13358
+ for (const h of shipPair(cfg.ship).filter((x) => x !== "claude")) {
13359
+ process.stderr.write(`${pairInstallerFor(h).installPair({ url: mcpUrlFor(baseUrl), replace, dryRun })}
13032
13360
  `);
13033
13361
  }
13034
13362
  }
@@ -13482,6 +13810,14 @@ Signed in as ${result.identity.email} \u2014 minted an API key in workspace "${r
13482
13810
  `);
13483
13811
  break;
13484
13812
  }
13813
+ case "disconnect": {
13814
+ const url = mcpUrlFor(value("base-url") ?? cfg.routes[0]?.baseUrl ?? DEFAULT_BASE_URL);
13815
+ for (const h of shipPair(cfg.ship)) {
13816
+ process.stderr.write(`${pairInstallerFor(h).removePair({ url, bin: cfg.ship.agent.bin, dryRun })}
13817
+ `);
13818
+ }
13819
+ break;
13820
+ }
13485
13821
  case "install": {
13486
13822
  if (!await installScheduler({ replace: flag("replace") })) process.exit(2);
13487
13823
  break;
@@ -13521,7 +13857,7 @@ Signed in as ${result.identity.email} \u2014 minted an API key in workspace "${r
13521
13857
  }
13522
13858
  case "log": {
13523
13859
  try {
13524
- const text = readFileSync14(cfg.ship.logFile, "utf8").split("\n");
13860
+ const text = readFileSync15(cfg.ship.logFile, "utf8").split("\n");
13525
13861
  process.stdout.write(`${text.slice(-(Number(value("tail")) || 40)).join("\n")}
13526
13862
  `);
13527
13863
  } catch {
@@ -13729,7 +14065,7 @@ routes: ${cfg.routes.map((c) => c.route).join(", ")}
13729
14065
  );
13730
14066
  try {
13731
14067
  const evFile = eventFileFor(cfg.ship.stateDir);
13732
- const evSize = existsSync20(evFile) ? statSync9(evFile).size : 0;
14068
+ const evSize = existsSync21(evFile) ? statSync9(evFile).size : 0;
13733
14069
  process.stdout.write(
13734
14070
  evSize > cfg.ship.eventsRotateBytes * 4 ? `events log: WARN ${evFile} is ${Math.round(evSize / 1048576)} MB (> 4x eventsRotateBytes); run \`crew reap\` to rotate it
13735
14071
  ` : "events log: ok\n"
@@ -13889,6 +14225,13 @@ crew manifest: ${seats2.length ? seats2.map((c) => c.name).join(", ") : "non
13889
14225
  );
13890
14226
  }
13891
14227
  }
14228
+ for (const h of shipPair(cfg.ship).filter((x) => x !== "claude")) {
14229
+ const inst = pairInstallerFor(h);
14230
+ const v = inst.verifyPair({ url: mcpUrlFor(route.baseUrl) });
14231
+ process.stdout.write(`pair ${h}:`.padEnd(19) + (v.ok ? ` installed (${v.detail})
14232
+ ` : ` NOT installed \u2014 ${v.detail}; run crew connect
14233
+ `));
14234
+ }
13892
14235
  const shipVendors = new Set(shipToolchains(cfg.ship).map(toolchainVendor));
13893
14236
  if (route.apiKey && route.resolved) {
13894
14237
  let flagged = 0;