@tablation/crew 0.71.13 → 0.72.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 +443 -193
  2. package/package.json +1 -1
package/dist/cli.js CHANGED
@@ -5920,6 +5920,12 @@ var Tracker = class {
5920
5920
  return [];
5921
5921
  }
5922
5922
  }
5923
+ /** Renames a Ships row (CREW-2495). Throws on a refused write (a member key may not edit the table). */
5924
+ async renameShipRow(rowId, name) {
5925
+ const model = this.route.resolved?.shipsModelId;
5926
+ if (!model) throw new Error("this workspace has no Ships table");
5927
+ await this.client.records.update(model, rowId, { name });
5928
+ }
5923
5929
  /**
5924
5930
  * This ship's own row on the `Ships` table, matched by name — the same
5925
5931
  * exact-string match `doctor` has always used (see the callsign comment
@@ -8606,7 +8612,7 @@ function doctorSelftestLines(stored, toolchains) {
8606
8612
  }
8607
8613
 
8608
8614
  // src/cli.ts
8609
- import { resolve as resolve5, dirname as dirname19, join as join35, basename as basename11 } from "path";
8615
+ import { resolve as resolve5, dirname as dirname20, join as join35, basename as basename11 } from "path";
8610
8616
  import { fileURLToPath } from "url";
8611
8617
  import { hostname as hostname2, homedir as homedir9 } from "os";
8612
8618
  import { spawnSync as spawnSync6 } from "child_process";
@@ -12636,6 +12642,217 @@ function nextRoles(decision, ran) {
12636
12642
  return decision.selection.pending.filter((r) => !ran.includes(r)).sort((a, b) => (decision.selection.ranks[a] ?? 0) - (decision.selection.ranks[b] ?? 0));
12637
12643
  }
12638
12644
 
12645
+ // src/ship-name.ts
12646
+ import { existsSync as existsSync25, mkdirSync as mkdirSync15, readFileSync as readFileSync20, renameSync as renameSync2, writeFileSync as writeFileSync17 } from "fs";
12647
+ import { dirname as dirname14 } from "path";
12648
+ import { parseDocument } from "yaml";
12649
+
12650
+ // src/machine-id.ts
12651
+ import { execFileSync as execFileSync4 } from "child_process";
12652
+ import { createHash as createHash3 } from "crypto";
12653
+ import { existsSync as existsSync24, mkdirSync as mkdirSync14, readFileSync as readFileSync19, writeFileSync as writeFileSync16 } from "fs";
12654
+ import { dirname as dirname13, join as join25 } from "path";
12655
+ function machineIdPathFor(stateDir) {
12656
+ return join25(stateDir, "machine-id");
12657
+ }
12658
+ var sha256 = (s) => createHash3("sha256").update(s).digest("hex");
12659
+ function readRawMachineId(platform2 = process.platform) {
12660
+ try {
12661
+ if (platform2 === "darwin") {
12662
+ const out = execFileSync4("ioreg", ["-rd1", "-c", "IOPlatformExpertDevice"], {
12663
+ encoding: "utf8",
12664
+ stdio: ["ignore", "pipe", "ignore"]
12665
+ });
12666
+ return /"IOPlatformUUID"\s*=\s*"([^"]+)"/.exec(out)?.[1];
12667
+ }
12668
+ for (const p of ["/etc/machine-id", "/var/lib/dbus/machine-id"]) {
12669
+ if (existsSync24(p)) {
12670
+ const id = readFileSync19(p, "utf8").trim();
12671
+ if (id) return id;
12672
+ }
12673
+ }
12674
+ } catch {
12675
+ }
12676
+ return void 0;
12677
+ }
12678
+ function ensureMachineId(stateDir, opts = {}) {
12679
+ if (opts.override?.trim()) return sha256(opts.override.trim());
12680
+ const path = machineIdPathFor(stateDir);
12681
+ if (existsSync24(path)) {
12682
+ const cached = readFileSync19(path, "utf8").trim();
12683
+ if (/^[0-9a-f]{64}$/.test(cached)) return cached;
12684
+ }
12685
+ const raw = (opts.read ?? readRawMachineId)();
12686
+ if (!raw) return void 0;
12687
+ const hashed = sha256(raw);
12688
+ mkdirSync14(dirname13(path), { recursive: true });
12689
+ writeFileSync16(path, `${hashed}
12690
+ `, { mode: 384 });
12691
+ return hashed;
12692
+ }
12693
+ function matchShipRows(ships, shipName2, machineId) {
12694
+ const hasColumn = ships.some((r) => "machine_id" in r);
12695
+ const byId = hasColumn && machineId ? ships.filter((r) => r.machine_id === machineId) : [];
12696
+ if (byId.length > 0) return { rows: byId, by: "machine_id", hasColumn };
12697
+ return { rows: ships.filter((r) => (r.name ?? "").trim() === shipName2.trim()), by: "name", hasColumn };
12698
+ }
12699
+
12700
+ // src/ship-name.ts
12701
+ var ShipNameError = class extends Error {
12702
+ };
12703
+ function validateShipName(raw) {
12704
+ const name = raw.trim();
12705
+ if (!name) throw new ShipNameError("the ship name cannot be empty");
12706
+ if (/[\u0000-\u001f\u007f]/.test(name)) throw new ShipNameError("the ship name cannot contain control characters");
12707
+ return name;
12708
+ }
12709
+ function planNameSync(local, row, lastSynced) {
12710
+ if (local === row) return { drift: "none", action: "none" };
12711
+ const rowMoved = lastSynced === void 0 || row !== lastSynced;
12712
+ const localMoved = lastSynced === void 0 || local !== lastSynced;
12713
+ if (rowMoved && localMoved) return { drift: "both", action: "row-wins" };
12714
+ if (rowMoved) return { drift: "row", action: "adopt" };
12715
+ return { drift: "local", action: "push" };
12716
+ }
12717
+ function shipNameOfYaml(text) {
12718
+ const v = parseDocument(text).getIn(["ship", "name"]);
12719
+ return typeof v === "string" && v.trim() ? v.trim() : void 0;
12720
+ }
12721
+ function setShipNameInYaml(text, name) {
12722
+ const doc = parseDocument(text);
12723
+ doc.setIn(["ship", "name"], name);
12724
+ return doc.toString();
12725
+ }
12726
+ function pickShipRow(rows, machineId, byName = []) {
12727
+ const hasColumn = rows.some((r) => "machine_id" in r);
12728
+ if (!hasColumn || !machineId) return { kind: "unsupported" };
12729
+ const byId = rows.filter((r) => r.machine_id === machineId);
12730
+ if (byId.length === 1) return { kind: "row", row: byId[0], by: "machine_id" };
12731
+ if (byId.length > 1) return { kind: "ambiguous" };
12732
+ const names = new Set(byName.map((n) => n.trim()));
12733
+ const named = rows.filter((r) => !r.machine_id && names.has((r.name ?? "").trim()));
12734
+ if (named.length === 1) return { kind: "row", row: named[0], by: "name" };
12735
+ return { kind: named.length > 1 ? "ambiguous" : "none" };
12736
+ }
12737
+ async function syncRouteName(i) {
12738
+ const picked = pickShipRow(i.rows, i.machineId);
12739
+ if (picked.kind !== "row") {
12740
+ const status2 = picked.kind === "none" ? "no-row" : picked.kind;
12741
+ return { status: status2, drift: "none", action: "none", name: i.local };
12742
+ }
12743
+ const rowName = (picked.row.name ?? "").trim();
12744
+ if (!rowName) return { status: "no-row", drift: "none", action: "none", name: i.local };
12745
+ const { drift, action } = planNameSync(i.local, rowName, i.lastSynced);
12746
+ const base = { drift, rowId: picked.row.id, rowName };
12747
+ if (action === "push") {
12748
+ if (!i.rename) return { status: "ok", ...base, action, name: i.local };
12749
+ try {
12750
+ await i.rename(picked.row.id, i.local);
12751
+ } catch (e) {
12752
+ return { status: "refused", ...base, action: "none", name: i.local, error: e.message };
12753
+ }
12754
+ return { status: "ok", ...base, action, name: i.local };
12755
+ }
12756
+ if (action === "adopt" || action === "row-wins") return { status: "ok", ...base, action, name: rowName, from: i.local };
12757
+ return { status: "ok", ...base, action, name: i.local };
12758
+ }
12759
+ function readSyncedName(resolvedFile) {
12760
+ if (!existsSync25(resolvedFile)) return void 0;
12761
+ try {
12762
+ const v = JSON.parse(readFileSync20(resolvedFile, "utf8")).shipName;
12763
+ return typeof v === "string" ? v : void 0;
12764
+ } catch {
12765
+ return void 0;
12766
+ }
12767
+ }
12768
+ function writeSyncedName(resolvedFile, name) {
12769
+ if (!existsSync25(resolvedFile)) return;
12770
+ let raw;
12771
+ try {
12772
+ raw = JSON.parse(readFileSync20(resolvedFile, "utf8"));
12773
+ } catch {
12774
+ return;
12775
+ }
12776
+ if (raw.shipName === name) return;
12777
+ raw.shipName = name;
12778
+ mkdirSync15(dirname14(resolvedFile), { recursive: true });
12779
+ const tmp = `${resolvedFile}.tmp${process.pid}`;
12780
+ writeFileSync17(tmp, `${JSON.stringify(raw, null, 2)}
12781
+ `);
12782
+ renameSync2(tmp, resolvedFile);
12783
+ }
12784
+ async function reconcileShipName(i) {
12785
+ let local = i.ship.name;
12786
+ try {
12787
+ local = shipNameOfYaml(readFileSync20(i.configFile, "utf8")) ?? local;
12788
+ } catch {
12789
+ }
12790
+ const resolvedFile = resolvedPathFor(i.stateDir, i.route);
12791
+ const lastSynced = readSyncedName(resolvedFile);
12792
+ let rows = [];
12793
+ try {
12794
+ rows = await i.rows();
12795
+ } catch {
12796
+ }
12797
+ const result = await syncRouteName({
12798
+ local,
12799
+ lastSynced,
12800
+ rows,
12801
+ machineId: i.machineId,
12802
+ rename: i.readOnly ? void 0 : i.rename
12803
+ });
12804
+ if (i.readOnly || result.status === "unsupported") return result;
12805
+ if (result.status === "refused") {
12806
+ i.emit?.warn(`could not rename the Ships row to "${local}": ${result.error}`, { step: "poll", data: { event: "ship_rename_refused", route: i.route, to: local } });
12807
+ return result;
12808
+ }
12809
+ if (result.status !== "ok") return result;
12810
+ if (result.action === "adopt" || result.action === "row-wins") {
12811
+ const text = readFileSync20(i.configFile, "utf8");
12812
+ writeFileSync17(i.configFile, setShipNameInYaml(text, result.name));
12813
+ i.ship.name = result.name;
12814
+ if (result.action === "row-wins") {
12815
+ i.emit?.warn(
12816
+ `ship name changed in both crew.yaml ("${result.from}") and the Ships table ("${result.name}"); the Ships row wins`,
12817
+ { step: "poll", data: { event: "ship_renamed", route: i.route, from: result.from, to: result.name, source: "table", conflict: true } }
12818
+ );
12819
+ } else {
12820
+ i.emit?.emit(`ship renamed to "${result.name}" in the Ships table; crew.yaml updated`, {
12821
+ step: "poll",
12822
+ data: { event: "ship_renamed", route: i.route, from: result.from, to: result.name, source: "table" }
12823
+ });
12824
+ }
12825
+ } else if (result.action === "push") {
12826
+ i.emit?.emit(`ship name "${result.name}" pushed to the Ships table`, {
12827
+ step: "poll",
12828
+ data: { event: "ship_renamed", route: i.route, from: result.rowName, to: result.name, source: "crew.yaml" }
12829
+ });
12830
+ }
12831
+ writeSyncedName(resolvedFile, result.name);
12832
+ return result;
12833
+ }
12834
+ async function reconcileRouteShipName(o) {
12835
+ const fallback = { status: "unsupported", drift: "none", action: "none", name: o.ship.name };
12836
+ if (!o.route.resolved?.shipsModelId) return fallback;
12837
+ try {
12838
+ const machineId = o.machineId ?? ensureMachineId(o.ship.stateDir);
12839
+ const tracker = new Tracker(o.route, o.ship);
12840
+ return await reconcileShipName({
12841
+ route: o.route.route,
12842
+ configFile: o.configFile,
12843
+ stateDir: o.ship.stateDir,
12844
+ ship: o.ship,
12845
+ machineId,
12846
+ rows: () => tracker.shipRows(),
12847
+ rename: (id, name) => tracker.renameShipRow(id, name),
12848
+ emit: o.emit,
12849
+ readOnly: o.readOnly
12850
+ });
12851
+ } catch {
12852
+ return fallback;
12853
+ }
12854
+ }
12855
+
12639
12856
  // src/connect.ts
12640
12857
  var ConnectHttpError = class extends Error {
12641
12858
  status;
@@ -12941,10 +13158,16 @@ async function discover(o) {
12941
13158
  out.shipRow = { id: shipRowId, name: o.ship.name, action: "matched", matchedBy };
12942
13159
  const patchBody = {};
12943
13160
  if (matchedBy === "machine_id") {
12944
- const keptName = (mine[0].name ?? "").trim();
12945
- if (keptName !== o.ship.name.trim()) {
12946
- out.shipRow.name = keptName;
12947
- provisioning.push(`matched Ships row "${keptName}" by machine id \u2014 this host is now called "${o.ship.name}"; the row's name is kept`);
13161
+ const rowName = (mine[0].name ?? "").trim();
13162
+ const route = `${out.workspaceSlug}/${out.projectSlug ?? out.projectId}`;
13163
+ const plan = planNameSync(o.ship.name.trim(), rowName, o.ship.lastSyncedName?.(route));
13164
+ if (plan.action === "push") {
13165
+ patchBody.name = o.ship.name.trim();
13166
+ provisioning.push(`renamed Ships row "${rowName}" to "${o.ship.name.trim()}" (crew.yaml changed)`);
13167
+ } else if (plan.action === "adopt" || plan.action === "row-wins") {
13168
+ out.shipRow.name = rowName;
13169
+ out.shipNameAdopted = rowName;
13170
+ provisioning.push(`matched Ships row "${rowName}" by machine id \u2014 this host's name becomes "${rowName}"${plan.action === "row-wins" ? " (crew.yaml also changed; the Ships row wins)" : ""}`);
12948
13171
  }
12949
13172
  patchBody.last_seen = (/* @__PURE__ */ new Date()).toISOString();
12950
13173
  }
@@ -13494,8 +13717,8 @@ function describeCapability(i) {
13494
13717
  }
13495
13718
 
13496
13719
  // src/agents-sync-run.ts
13497
- import { existsSync as existsSync24, mkdirSync as mkdirSync14, readFileSync as readFileSync19, writeFileSync as writeFileSync16 } from "fs";
13498
- import { dirname as dirname13 } from "path";
13720
+ import { existsSync as existsSync26, mkdirSync as mkdirSync16, readFileSync as readFileSync21, writeFileSync as writeFileSync18 } from "fs";
13721
+ import { dirname as dirname15 } from "path";
13499
13722
  async function runAgentsSync(target, o) {
13500
13723
  const dryRun = o.dryRun ?? false;
13501
13724
  const force = o.force ?? false;
@@ -13528,12 +13751,12 @@ async function runAgentsSync(target, o) {
13528
13751
  for (const s of skillResult?.outcomes ?? []) o.say(describeSkillSyncOutcome(s));
13529
13752
  if (!dryRun) {
13530
13753
  const resolvedPath = resolvedPathFor(o.stateDir, target.route);
13531
- const raw = existsSync24(resolvedPath) ? JSON.parse(readFileSync19(resolvedPath, "utf8")) : {};
13754
+ const raw = existsSync26(resolvedPath) ? JSON.parse(readFileSync21(resolvedPath, "utf8")) : {};
13532
13755
  raw.agentPersonas = personaResult.agentPersonas;
13533
13756
  if (skillResult) raw.agentSkills = skillResult.agentSkills;
13534
13757
  if (personaResult.releaseAgentId) raw.releaseAgentId = personaResult.releaseAgentId;
13535
- mkdirSync14(dirname13(resolvedPath), { recursive: true });
13536
- writeFileSync16(resolvedPath, `${JSON.stringify(raw, null, 2)}
13758
+ mkdirSync16(dirname15(resolvedPath), { recursive: true });
13759
+ writeFileSync18(resolvedPath, `${JSON.stringify(raw, null, 2)}
13537
13760
  `);
13538
13761
  if (target.resolved) {
13539
13762
  target.resolved.agentPersonas = personaResult.agentPersonas;
@@ -13548,7 +13771,7 @@ async function runAgentsSync(target, o) {
13548
13771
  }
13549
13772
 
13550
13773
  // src/logbook.ts
13551
- import { createHash as createHash3 } from "crypto";
13774
+ import { createHash as createHash4 } from "crypto";
13552
13775
  import { TablationClient as TablationClient4 } from "@tablation/client";
13553
13776
  var LogbookError = class extends Error {
13554
13777
  };
@@ -13621,7 +13844,7 @@ async function showLogEntry(route, entryId, opts = {}) {
13621
13844
  };
13622
13845
  }
13623
13846
  const prompt = appendAdditions(match.prompt ?? "", match.prompt_additions);
13624
- const actualSha = createHash3("sha256").update(prompt).digest("hex");
13847
+ const actualSha = createHash4("sha256").update(prompt).digest("hex");
13625
13848
  const exact = !!entry.prompt_sha && actualSha === entry.prompt_sha;
13626
13849
  return {
13627
13850
  entry,
@@ -13636,12 +13859,12 @@ async function showLogEntry(route, entryId, opts = {}) {
13636
13859
  }
13637
13860
 
13638
13861
  // src/repos-hooks.ts
13639
- import { isMap as isMap2, parseDocument as parseDocument2, YAMLMap } from "yaml";
13862
+ import { isMap as isMap2, parseDocument as parseDocument3, YAMLMap } from "yaml";
13640
13863
 
13641
13864
  // src/repos-cmd.ts
13642
13865
  import { homedir as homedir8 } from "os";
13643
- import { basename as basename8, join as join25, resolve as resolve3 } from "path";
13644
- import { isMap, isSeq, parseDocument, stringify as stringify2 } from "yaml";
13866
+ import { basename as basename8, join as join26, resolve as resolve3 } from "path";
13867
+ import { isMap, isSeq, parseDocument as parseDocument2, stringify as stringify2 } from "yaml";
13645
13868
  var ReposError = class extends Error {
13646
13869
  };
13647
13870
  function normalizeRemote(url) {
@@ -13665,7 +13888,7 @@ function matchRepoName(origin, resolved) {
13665
13888
  return void 0;
13666
13889
  }
13667
13890
  function routeNamesOf(text) {
13668
- const routes = parseDocument(text).get("routes", true);
13891
+ const routes = parseDocument2(text).get("routes", true);
13669
13892
  if (!isSeq(routes)) return [];
13670
13893
  return routes.items.flatMap((i) => isMap(i) && typeof i.get("route") === "string" ? [i.get("route")] : []);
13671
13894
  }
@@ -13700,7 +13923,7 @@ function routeMapOf(doc, route) {
13700
13923
  throw new ReposError(`no route "${route}" in the config (have: ${have})`);
13701
13924
  }
13702
13925
  function configuredRepos(text, route, base) {
13703
- const doc = parseDocument(text);
13926
+ const doc = parseDocument2(text);
13704
13927
  const m = routeMapOf(doc, route);
13705
13928
  const repos = m.get("repos");
13706
13929
  if (isMap(repos)) {
@@ -13715,7 +13938,7 @@ function configuredRepos(text, route, base) {
13715
13938
  return dir ? [{ name: basename8(String(dir)), dir: expandDir(String(dir), base) }] : [];
13716
13939
  }
13717
13940
  function routeSetsHooks(text, route, name) {
13718
- const m = routeMapOf(parseDocument(text), route);
13941
+ const m = routeMapOf(parseDocument2(text), route);
13719
13942
  const repos = m.get("repos", true);
13720
13943
  let target = m;
13721
13944
  if (isMap(repos)) target = repos.get(name, true);
@@ -13771,7 +13994,7 @@ function listRepoRows(i) {
13771
13994
  return rows;
13772
13995
  }
13773
13996
  function planRepoAdd(i) {
13774
- const doc = parseDocument(i.text);
13997
+ const doc = parseDocument2(i.text);
13775
13998
  if (doc.errors.length > 0) throw new ReposError(`config is not valid YAML \u2014 ${doc.errors[0].message}`);
13776
13999
  const m = routeMapOf(doc, i.route);
13777
14000
  const existing = configuredRepos(i.text, i.route, i.base);
@@ -13808,7 +14031,7 @@ function planRepoAdd(i) {
13808
14031
  return { text: String(doc), mode, name, block: renderBlock(m), warning, unmatched: !matched };
13809
14032
  }
13810
14033
  function planRepoRemove(text, base, route, path) {
13811
- const doc = parseDocument(text);
14034
+ const doc = parseDocument2(text);
13812
14035
  if (doc.errors.length > 0) throw new ReposError(`config is not valid YAML \u2014 ${doc.errors[0].message}`);
13813
14036
  const m = routeMapOf(doc, route);
13814
14037
  const target = expandDir(path, base);
@@ -13886,7 +14109,7 @@ function planRepoClone(i) {
13886
14109
  `the Repos row "${name}" has no remote to clone from \u2014 attach an existing checkout with \`crew repos add ${i.route} ${name} <path>\`, or set the row's remote in the tracker and re-run \`crew connect\``
13887
14110
  );
13888
14111
  }
13889
- const doc = parseDocument(i.text);
14112
+ const doc = parseDocument2(i.text);
13890
14113
  const m = routeMapOf(doc, i.route);
13891
14114
  const existing = configuredRepos(i.text, i.route, i.base);
13892
14115
  if (existing.some((e) => e.name.toLowerCase() === name.toLowerCase())) {
@@ -13894,7 +14117,7 @@ function planRepoClone(i) {
13894
14117
  }
13895
14118
  const ship = doc.get("ship", true);
13896
14119
  const basePath = String(m.get("reposBasePath") ?? (isMap(ship) ? ship.get("reposBasePath") : void 0) ?? "~/Crew");
13897
- const target = i.dir ? expandDir(i.dir, i.base) : join25(expandDir(basePath, i.base), i.route.split("/")[0], name);
14120
+ const target = i.dir ? expandDir(i.dir, i.base) : join26(expandDir(basePath, i.base), i.route.split("/")[0], name);
13898
14121
  const add = planRepoAdd({ text: i.text, base: i.base, route: i.route, path: target, origin: remote, resolved: i.resolved, name });
13899
14122
  return { ...add, remote, target };
13900
14123
  }
@@ -14040,7 +14263,7 @@ function repoDirOf(text, base, route, name) {
14040
14263
  return hit.dir;
14041
14264
  }
14042
14265
  function planRouteHooks(text, route, name, hooks) {
14043
- const doc = parseDocument2(text);
14266
+ const doc = parseDocument3(text);
14044
14267
  if (doc.errors.length > 0) throw new ReposError(`config is not valid YAML \u2014 ${doc.errors[0].message}`);
14045
14268
  const m = routeMapOf(doc, route);
14046
14269
  const repos = m.get("repos", true);
@@ -14102,7 +14325,7 @@ function existingRepoConfig(f) {
14102
14325
  }
14103
14326
 
14104
14327
  // src/repos-hooks-agent.ts
14105
- import { spawn as spawn4, execFileSync as execFileSync4 } from "child_process";
14328
+ import { spawn as spawn4, execFileSync as execFileSync5 } from "child_process";
14106
14329
  import { realpathSync as realpathSync2 } from "fs";
14107
14330
  import { isAbsolute as isAbsolute2, relative } from "path";
14108
14331
  var AgentSuggestError = class extends Error {
@@ -14205,7 +14428,7 @@ function classifyFailure(o) {
14205
14428
  }
14206
14429
  function porcelain(dir) {
14207
14430
  try {
14208
- return execFileSync4("git", ["status", "--porcelain", "--untracked-files=all"], { cwd: dir, encoding: "utf8" });
14431
+ return execFileSync5("git", ["status", "--porcelain", "--untracked-files=all"], { cwd: dir, encoding: "utf8" });
14209
14432
  } catch {
14210
14433
  return void 0;
14211
14434
  }
@@ -14274,7 +14497,7 @@ ${run2.err}` });
14274
14497
  }
14275
14498
 
14276
14499
  // src/sample-ticket.ts
14277
- import { basename as basename9, dirname as dirname15, join as join26 } from "path";
14500
+ import { basename as basename9, dirname as dirname17, join as join27 } from "path";
14278
14501
  var SAMPLE_REPO = "crew-sample";
14279
14502
  var SAMPLE_REMOTE = `local/${SAMPLE_REPO}`;
14280
14503
  var SAMPLE_MARKER = "sample: true";
@@ -14297,8 +14520,8 @@ has something safe to work on: its test and build hooks always pass, it has no
14297
14520
  remote, and \`crew sample-ticket --remove\` deletes it.
14298
14521
  `;
14299
14522
  function sampleDir(existing, reposBase, workspace) {
14300
- if (existing.length > 0) return join26(dirname15(existing[0]), SAMPLE_REPO);
14301
- return join26(reposBase, workspace, SAMPLE_REPO);
14523
+ if (existing.length > 0) return join27(dirname17(existing[0]), SAMPLE_REPO);
14524
+ return join27(reposBase, workspace, SAMPLE_REPO);
14302
14525
  }
14303
14526
  var SAMPLE_TITLE = "Sample: add a documented `greet` function with a test";
14304
14527
  function sampleDescription() {
@@ -14342,7 +14565,7 @@ function planSampleTicket(i) {
14342
14565
  }
14343
14566
 
14344
14567
  // src/doctor-fix.ts
14345
- import { parseDocument as parseDocument3 } from "yaml";
14568
+ import { parseDocument as parseDocument4 } from "yaml";
14346
14569
  var sound = (r) => r.hasRepos && r.problems.length === 0;
14347
14570
  function planFix(f) {
14348
14571
  const actions = [];
@@ -14369,7 +14592,7 @@ function routesInScope(routes, named) {
14369
14592
  return named ? routes.filter((r) => r.route === named) : routes;
14370
14593
  }
14371
14594
  function planEnable(text, route) {
14372
- const doc = parseDocument3(text);
14595
+ const doc = parseDocument4(text);
14373
14596
  routeMapOf(doc, route).set("enabled", true);
14374
14597
  return String(doc);
14375
14598
  }
@@ -14379,8 +14602,8 @@ function schedulerInstalled(plan, exists, crontab) {
14379
14602
  }
14380
14603
 
14381
14604
  // src/worktree-sweep.ts
14382
- import { existsSync as existsSync25, readFileSync as readFileSync20, readdirSync as readdirSync8 } from "fs";
14383
- import { basename as basename10, dirname as dirname16, join as join27, resolve as resolve4 } from "path";
14605
+ import { existsSync as existsSync27, readFileSync as readFileSync22, readdirSync as readdirSync8 } from "fs";
14606
+ import { basename as basename10, dirname as dirname18, join as join28, resolve as resolve4 } from "path";
14384
14607
  function planWorktreeSweep(target, tickets, contract) {
14385
14608
  const terminal = new Set(contract.statuses.resolved.filter((s) => s !== contract.statuses.verified));
14386
14609
  const verified = contract.statuses.verified;
@@ -14434,7 +14657,7 @@ function worktreeTicketNumbers(target) {
14434
14657
  return [...nums];
14435
14658
  }
14436
14659
  function siblingWorktreeDirs(target) {
14437
- const parent = dirname16(target.dir.replace(/[/\\]+$/, ""));
14660
+ const parent = dirname18(target.dir.replace(/[/\\]+$/, ""));
14438
14661
  const stem = `${basename10(target.dir.replace(/[/\\]+$/, ""))}-`;
14439
14662
  const registered = new Set(worktrees(target.dir).map((w) => resolve4(w.path)));
14440
14663
  let names;
@@ -14447,7 +14670,7 @@ function siblingWorktreeDirs(target) {
14447
14670
  for (const name of names) {
14448
14671
  if (!name.startsWith(stem)) continue;
14449
14672
  const m = DIR_NUMBER_SUFFIXED.exec(name);
14450
- const path = join27(parent, name);
14673
+ const path = join28(parent, name);
14451
14674
  if (m && !registered.has(resolve4(path))) out.push({ path, number: m[1] });
14452
14675
  }
14453
14676
  return out;
@@ -14482,7 +14705,7 @@ async function applyWorktreeSweep(cwd, actions, dryRun, log, remote = "origin",
14482
14705
  let removed = 0;
14483
14706
  let keptBranches = 0;
14484
14707
  for (const a of actions) {
14485
- if (!dryRun && base && !a.keepBranch && existsSync25(a.path) && isHandBackMerge(a.path, base)) {
14708
+ if (!dryRun && base && !a.keepBranch && existsSync27(a.path) && isHandBackMerge(a.path, base)) {
14486
14709
  if (gitOk(a.path, ["merge", "--abort"]) !== null) {
14487
14710
  log.emit(
14488
14711
  `abandoned the hand-back merge of ${base} in ${a.path}: ${a.ticket.issue_id} deployed without it`,
@@ -14490,7 +14713,7 @@ async function applyWorktreeSweep(cwd, actions, dryRun, log, remote = "origin",
14490
14713
  );
14491
14714
  }
14492
14715
  }
14493
- if (!dryRun && existsSync25(a.path)) {
14716
+ if (!dryRun && existsSync27(a.path)) {
14494
14717
  let dirty = false;
14495
14718
  try {
14496
14719
  dirty = status(a.path).length > 0;
@@ -14501,7 +14724,7 @@ async function applyWorktreeSweep(cwd, actions, dryRun, log, remote = "origin",
14501
14724
  continue;
14502
14725
  }
14503
14726
  }
14504
- if (!dryRun && a.requirePushed && a.branch && existsSync25(a.path) && remoteConfigured(cwd, remote) && worktreeHoldsUnpushed(a.path, remote)) {
14727
+ if (!dryRun && a.requirePushed && a.branch && existsSync27(a.path) && remoteConfigured(cwd, remote) && worktreeHoldsUnpushed(a.path, remote)) {
14505
14728
  refuse(a, "unpushed commits", `${a.branch} has commits that are not on ${remote}`);
14506
14729
  continue;
14507
14730
  }
@@ -14550,7 +14773,7 @@ function isHandBackMerge(path, base) {
14550
14773
  if (!msgPath) return false;
14551
14774
  let first;
14552
14775
  try {
14553
- first = readFileSync20(resolve4(path, msgPath), "utf8").split("\n")[0] ?? "";
14776
+ first = readFileSync22(resolve4(path, msgPath), "utf8").split("\n")[0] ?? "";
14554
14777
  } catch {
14555
14778
  return false;
14556
14779
  }
@@ -14655,14 +14878,14 @@ function applyRemoteBranchCleanup(cwd, remote, branches2, dryRun, log) {
14655
14878
  }
14656
14879
 
14657
14880
  // src/stream-sweep.ts
14658
- import { existsSync as existsSync26, readdirSync as readdirSync9, rmSync as rmSync13, statSync as statSync8 } from "fs";
14659
- import { join as join28 } from "path";
14881
+ import { existsSync as existsSync28, readdirSync as readdirSync9, rmSync as rmSync13, statSync as statSync8 } from "fs";
14882
+ import { join as join29 } from "path";
14660
14883
  var DAY_MS2 = 24 * 60 * 60 * 1e3;
14661
14884
  function planStreamSweep(stateDir, retentionDays, now = Date.now()) {
14662
- const dir = join28(stateDir, "streams");
14663
- if (!existsSync26(dir)) return [];
14885
+ const dir = join29(stateDir, "streams");
14886
+ if (!existsSync28(dir)) return [];
14664
14887
  const cutoff = now - retentionDays * DAY_MS2;
14665
- return readdirSync9(dir).map((name) => join28(dir, name)).filter((path) => {
14888
+ return readdirSync9(dir).map((name) => join29(dir, name)).filter((path) => {
14666
14889
  try {
14667
14890
  return statSync8(path).mtimeMs < cutoff;
14668
14891
  } catch {
@@ -14688,9 +14911,9 @@ function applyStreamSweep(paths, dryRun, log) {
14688
14911
  return removed;
14689
14912
  }
14690
14913
  function planEventsSweep(stateDir, retentionDays, now = Date.now()) {
14691
- if (!existsSync26(stateDir)) return [];
14914
+ if (!existsSync28(stateDir)) return [];
14692
14915
  const cutoff = now - retentionDays * DAY_MS2;
14693
- return readdirSync9(stateDir).filter((name) => ROTATED_EVENTS_RE.test(name)).map((name) => join28(stateDir, name)).filter((path) => {
14916
+ return readdirSync9(stateDir).filter((name) => ROTATED_EVENTS_RE.test(name)).map((name) => join29(stateDir, name)).filter((path) => {
14694
14917
  try {
14695
14918
  return statSync8(path).mtimeMs < cutoff;
14696
14919
  } catch {
@@ -14700,26 +14923,26 @@ function planEventsSweep(stateDir, retentionDays, now = Date.now()) {
14700
14923
  }
14701
14924
 
14702
14925
  // src/cli.ts
14703
- import { realpathSync as realpathSync3, readFileSync as readFileSync27, existsSync as existsSync33, mkdirSync as mkdirSync20, writeFileSync as writeFileSync21, rmSync as rmSync15, unlinkSync as unlinkSync5, copyFileSync as copyFileSync2, renameSync as renameSync2, statSync as statSync10, readdirSync as readdirSync14 } from "fs";
14926
+ import { realpathSync as realpathSync3, readFileSync as readFileSync28, existsSync as existsSync34, mkdirSync as mkdirSync21, writeFileSync as writeFileSync22, rmSync as rmSync15, unlinkSync as unlinkSync5, copyFileSync as copyFileSync2, renameSync as renameSync3, statSync as statSync10, readdirSync as readdirSync14 } from "fs";
14704
14927
  import { createInterface } from "readline/promises";
14705
14928
  import { dirname as dirOf, resolve as resolvePath } from "path";
14706
14929
 
14707
14930
  // src/ssh-keys.ts
14708
- import { execFileSync as execFileSync5 } from "child_process";
14709
- import { existsSync as existsSync27, mkdirSync as mkdirSync15, readFileSync as readFileSync21, chmodSync } from "fs";
14710
- import { dirname as dirname17, join as join29 } from "path";
14931
+ import { execFileSync as execFileSync6 } from "child_process";
14932
+ import { existsSync as existsSync29, mkdirSync as mkdirSync17, readFileSync as readFileSync23, chmodSync } from "fs";
14933
+ import { dirname as dirname19, join as join30 } from "path";
14711
14934
  var SshKeyError = class extends Error {
14712
14935
  };
14713
14936
  function shipSshKeyPathFor(stateDir) {
14714
- return join29(stateDir, "ssh", "id_ed25519");
14937
+ return join30(stateDir, "ssh", "id_ed25519");
14715
14938
  }
14716
14939
  function ensureShipSshKeypair(stateDir) {
14717
14940
  const privateKeyPath = shipSshKeyPathFor(stateDir);
14718
14941
  const publicKeyPath = `${privateKeyPath}.pub`;
14719
- if (!existsSync27(privateKeyPath)) {
14720
- mkdirSync15(dirname17(privateKeyPath), { recursive: true });
14942
+ if (!existsSync29(privateKeyPath)) {
14943
+ mkdirSync17(dirname19(privateKeyPath), { recursive: true });
14721
14944
  try {
14722
- execFileSync5("ssh-keygen", [
14945
+ execFileSync6("ssh-keygen", [
14723
14946
  "-t",
14724
14947
  "ed25519",
14725
14948
  "-N",
@@ -14737,71 +14960,21 @@ function ensureShipSshKeypair(stateDir) {
14737
14960
  }
14738
14961
  chmodSync(privateKeyPath, 384);
14739
14962
  }
14740
- if (!existsSync27(publicKeyPath)) {
14963
+ if (!existsSync29(publicKeyPath)) {
14741
14964
  throw new SshKeyError(`${privateKeyPath} exists but ${publicKeyPath} does not \u2014 regenerate or restore it`);
14742
14965
  }
14743
- const publicKey = readFileSync21(publicKeyPath, "utf8").trim();
14966
+ const publicKey = readFileSync23(publicKeyPath, "utf8").trim();
14744
14967
  return { privateKeyPath, publicKeyPath, publicKey };
14745
14968
  }
14746
14969
  function sshKeygenAvailable() {
14747
14970
  try {
14748
- execFileSync5("ssh-keygen", ["-V"], { stdio: ["ignore", "ignore", "ignore"] });
14971
+ execFileSync6("ssh-keygen", ["-V"], { stdio: ["ignore", "ignore", "ignore"] });
14749
14972
  return true;
14750
14973
  } catch (e) {
14751
14974
  return e.code !== "ENOENT";
14752
14975
  }
14753
14976
  }
14754
14977
 
14755
- // src/machine-id.ts
14756
- import { execFileSync as execFileSync6 } from "child_process";
14757
- import { createHash as createHash4 } from "crypto";
14758
- import { existsSync as existsSync28, mkdirSync as mkdirSync16, readFileSync as readFileSync22, writeFileSync as writeFileSync17 } from "fs";
14759
- import { dirname as dirname18, join as join30 } from "path";
14760
- function machineIdPathFor(stateDir) {
14761
- return join30(stateDir, "machine-id");
14762
- }
14763
- var sha256 = (s) => createHash4("sha256").update(s).digest("hex");
14764
- function readRawMachineId(platform2 = process.platform) {
14765
- try {
14766
- if (platform2 === "darwin") {
14767
- const out = execFileSync6("ioreg", ["-rd1", "-c", "IOPlatformExpertDevice"], {
14768
- encoding: "utf8",
14769
- stdio: ["ignore", "pipe", "ignore"]
14770
- });
14771
- return /"IOPlatformUUID"\s*=\s*"([^"]+)"/.exec(out)?.[1];
14772
- }
14773
- for (const p of ["/etc/machine-id", "/var/lib/dbus/machine-id"]) {
14774
- if (existsSync28(p)) {
14775
- const id = readFileSync22(p, "utf8").trim();
14776
- if (id) return id;
14777
- }
14778
- }
14779
- } catch {
14780
- }
14781
- return void 0;
14782
- }
14783
- function ensureMachineId(stateDir, opts = {}) {
14784
- if (opts.override?.trim()) return sha256(opts.override.trim());
14785
- const path = machineIdPathFor(stateDir);
14786
- if (existsSync28(path)) {
14787
- const cached = readFileSync22(path, "utf8").trim();
14788
- if (/^[0-9a-f]{64}$/.test(cached)) return cached;
14789
- }
14790
- const raw = (opts.read ?? readRawMachineId)();
14791
- if (!raw) return void 0;
14792
- const hashed = sha256(raw);
14793
- mkdirSync16(dirname18(path), { recursive: true });
14794
- writeFileSync17(path, `${hashed}
14795
- `, { mode: 384 });
14796
- return hashed;
14797
- }
14798
- function matchShipRows(ships, shipName2, machineId) {
14799
- const hasColumn = ships.some((r) => "machine_id" in r);
14800
- const byId = hasColumn && machineId ? ships.filter((r) => r.machine_id === machineId) : [];
14801
- if (byId.length > 0) return { rows: byId, by: "machine_id", hasColumn };
14802
- return { rows: ships.filter((r) => (r.name ?? "").trim() === shipName2.trim()), by: "name", hasColumn };
14803
- }
14804
-
14805
14978
  // src/passenger-containers.ts
14806
14979
  import { execFileSync as execFileSync7 } from "child_process";
14807
14980
  import { createHash as createHash5 } from "crypto";
@@ -15006,7 +15179,7 @@ function syncAllPassengerCheckouts(cfg) {
15006
15179
 
15007
15180
  // src/tunnel.ts
15008
15181
  import { spawn as spawn5 } from "child_process";
15009
- import { existsSync as existsSync29, mkdirSync as mkdirSync17, readFileSync as readFileSync23, writeFileSync as writeFileSync18, unlinkSync as unlinkSync3, readdirSync as readdirSync10 } from "fs";
15182
+ import { existsSync as existsSync30, mkdirSync as mkdirSync18, readFileSync as readFileSync24, writeFileSync as writeFileSync19, unlinkSync as unlinkSync3, readdirSync as readdirSync10 } from "fs";
15010
15183
  import { randomInt } from "crypto";
15011
15184
  import { join as join31 } from "path";
15012
15185
  var DEFAULT_SPAWN_OPTIONS = { stdio: "ignore", detached: true };
@@ -15079,17 +15252,17 @@ function tunnelStatePath(stateDir, workspaceId) {
15079
15252
  }
15080
15253
  function readPersistedTunnel(stateDir, workspaceId) {
15081
15254
  const path = tunnelStatePath(stateDir, workspaceId);
15082
- if (!existsSync29(path)) return void 0;
15255
+ if (!existsSync30(path)) return void 0;
15083
15256
  try {
15084
- return JSON.parse(readFileSync23(path, "utf8"));
15257
+ return JSON.parse(readFileSync24(path, "utf8"));
15085
15258
  } catch {
15086
15259
  return void 0;
15087
15260
  }
15088
15261
  }
15089
15262
  function writePersistedTunnel(stateDir, state) {
15090
15263
  const path = tunnelStatePath(stateDir, state.workspaceId);
15091
- mkdirSync17(join31(stateDir, "passengers"), { recursive: true });
15092
- writeFileSync18(path, `${JSON.stringify(state, null, 2)}
15264
+ mkdirSync18(join31(stateDir, "passengers"), { recursive: true });
15265
+ writeFileSync19(path, `${JSON.stringify(state, null, 2)}
15093
15266
  `);
15094
15267
  }
15095
15268
  function updatePersistedTunnel(stateDir, workspaceId, patch2) {
@@ -15187,7 +15360,7 @@ function syncPassengerTunnels(plans, relayHost, relayPort, privateKeyPath, state
15187
15360
  }
15188
15361
  function readAllPersistedWorkspaceIds(stateDir) {
15189
15362
  const dir = join31(stateDir, "passengers");
15190
- if (!existsSync29(dir)) return [];
15363
+ if (!existsSync30(dir)) return [];
15191
15364
  return readdirSync10(dir).filter((f) => f.endsWith(".json") && !f.endsWith("-sync.json")).map((f) => f.slice(0, -".json".length));
15192
15365
  }
15193
15366
 
@@ -15308,7 +15481,7 @@ async function unregisterEndpoint(workspaceId, info, d) {
15308
15481
  import { createServer as createHttpServer } from "http";
15309
15482
  import { spawn as spawn6 } from "child_process";
15310
15483
  import { randomBytes } from "crypto";
15311
- import { existsSync as existsSync30, mkdirSync as mkdirSync18, readFileSync as readFileSync24, writeFileSync as writeFileSync19, unlinkSync as unlinkSync4, readdirSync as readdirSync11 } from "fs";
15484
+ import { existsSync as existsSync31, mkdirSync as mkdirSync19, readFileSync as readFileSync25, writeFileSync as writeFileSync20, unlinkSync as unlinkSync4, readdirSync as readdirSync11 } from "fs";
15312
15485
  import { join as join32 } from "path";
15313
15486
 
15314
15487
  // src/secret-compare.ts
@@ -15402,17 +15575,17 @@ function syncDaemonStatePath(stateDir, workspaceId) {
15402
15575
  }
15403
15576
  function readPersistedSyncDaemon(stateDir, workspaceId) {
15404
15577
  const path = syncDaemonStatePath(stateDir, workspaceId);
15405
- if (!existsSync30(path)) return void 0;
15578
+ if (!existsSync31(path)) return void 0;
15406
15579
  try {
15407
- return JSON.parse(readFileSync24(path, "utf8"));
15580
+ return JSON.parse(readFileSync25(path, "utf8"));
15408
15581
  } catch {
15409
15582
  return void 0;
15410
15583
  }
15411
15584
  }
15412
15585
  function writePersistedSyncDaemon(stateDir, state) {
15413
15586
  const path = syncDaemonStatePath(stateDir, state.workspaceId);
15414
- mkdirSync18(join32(stateDir, "passengers"), { recursive: true });
15415
- writeFileSync19(path, `${JSON.stringify(state, null, 2)}
15587
+ mkdirSync19(join32(stateDir, "passengers"), { recursive: true });
15588
+ writeFileSync20(path, `${JSON.stringify(state, null, 2)}
15416
15589
  `);
15417
15590
  }
15418
15591
  function removePersistedSyncDaemon(stateDir, workspaceId) {
@@ -15433,7 +15606,7 @@ function waitForExit(pid, alive, timeoutMs = 5e3) {
15433
15606
  }
15434
15607
  function readAllPersistedSyncWorkspaceIds(stateDir) {
15435
15608
  const dir = join32(stateDir, "passengers");
15436
- if (!existsSync30(dir)) return [];
15609
+ if (!existsSync31(dir)) return [];
15437
15610
  const suffix = "-sync.json";
15438
15611
  return readdirSync11(dir).filter((f) => f.endsWith(suffix)).map((f) => f.slice(0, -suffix.length));
15439
15612
  }
@@ -15521,11 +15694,11 @@ function describeCause(err) {
15521
15694
  }
15522
15695
 
15523
15696
  // src/daemon.ts
15524
- import { existsSync as existsSync32, mkdirSync as mkdirSync19, readFileSync as readFileSync26, readdirSync as readdirSync13, rmSync as rmSync14, statSync as statSync9, writeFileSync as writeFileSync20 } from "fs";
15697
+ import { existsSync as existsSync33, mkdirSync as mkdirSync20, readFileSync as readFileSync27, readdirSync as readdirSync13, rmSync as rmSync14, statSync as statSync9, writeFileSync as writeFileSync21 } from "fs";
15525
15698
  import { join as join34 } from "path";
15526
15699
 
15527
15700
  // src/interrupted.ts
15528
- import { existsSync as existsSync31, readdirSync as readdirSync12, readFileSync as readFileSync25 } from "fs";
15701
+ import { existsSync as existsSync32, readdirSync as readdirSync12, readFileSync as readFileSync26 } from "fs";
15529
15702
  import { join as join33 } from "path";
15530
15703
  var INTERRUPTED_MARKER = calloutHeader("NOTE", "Interrupted");
15531
15704
  function interruptedComment(b) {
@@ -15545,16 +15718,16 @@ var pidAlive = (pid) => {
15545
15718
  };
15546
15719
  function runFinished(streamPath) {
15547
15720
  const path = `${streamPath}.meta.json`;
15548
- if (!existsSync31(path)) return false;
15721
+ if (!existsSync32(path)) return false;
15549
15722
  try {
15550
- return JSON.parse(readFileSync25(path, "utf8")).running !== true;
15723
+ return JSON.parse(readFileSync26(path, "utf8")).running !== true;
15551
15724
  } catch {
15552
15725
  return true;
15553
15726
  }
15554
15727
  }
15555
15728
  function orphanedStreams(o) {
15556
15729
  const dir = join33(o.stateDir, "streams");
15557
- if (!existsSync31(dir)) return [];
15730
+ if (!existsSync32(dir)) return [];
15558
15731
  const ownerAlive = o.isOwnerAlive ?? pidAlive;
15559
15732
  const out = [];
15560
15733
  for (const name of readdirSync12(dir)) {
@@ -15688,7 +15861,7 @@ function computeTreeSignature(paths) {
15688
15861
  }
15689
15862
  function makeStaleChecker(paths, opts = {}) {
15690
15863
  const initial = computeTreeSignature(paths);
15691
- return () => computeTreeSignature(paths) !== initial || opts.reloadSentinel !== void 0 && existsSync32(opts.reloadSentinel);
15864
+ return () => computeTreeSignature(paths) !== initial || opts.reloadSentinel !== void 0 && existsSync33(opts.reloadSentinel);
15692
15865
  }
15693
15866
  function reloadSentinelFor(stateDir) {
15694
15867
  return join34(stateDir, "reload-requested");
@@ -15697,14 +15870,14 @@ function daemonStampFor(stateDir) {
15697
15870
  return join34(stateDir, "daemon.started");
15698
15871
  }
15699
15872
  function noteDaemonStarted(stateDir, now = Date.now()) {
15700
- mkdirSync19(stateDir, { recursive: true });
15873
+ mkdirSync20(stateDir, { recursive: true });
15701
15874
  rmSync14(reloadSentinelFor(stateDir), { force: true });
15702
- writeFileSync20(daemonStampFor(stateDir), `${JSON.stringify({ pid: process.pid, startedAt: now })}
15875
+ writeFileSync21(daemonStampFor(stateDir), `${JSON.stringify({ pid: process.pid, startedAt: now })}
15703
15876
  `);
15704
15877
  }
15705
15878
  function readDaemonStarted(stateDir) {
15706
15879
  try {
15707
- const v = JSON.parse(readFileSync26(daemonStampFor(stateDir), "utf8")).startedAt;
15880
+ const v = JSON.parse(readFileSync27(daemonStampFor(stateDir), "utf8")).startedAt;
15708
15881
  return typeof v === "number" ? v : null;
15709
15882
  } catch {
15710
15883
  return null;
@@ -15767,6 +15940,7 @@ async function runOnePassUnguarded(o) {
15767
15940
  const spawn7 = o.spawnRoleAgent ?? runRoleAgent;
15768
15941
  const breaker = o.spawnBreaker ?? new SpawnBreaker();
15769
15942
  const ctx = { route: o.route, ship: o.ship, state: o.state, emit, getSessionStore: o.getSessionStore };
15943
+ if (o.configFile) await reconcileRouteShipName({ route: o.route, ship: o.ship, configFile: o.configFile, emit });
15770
15944
  try {
15771
15945
  await (o.reconcile ?? reconcileForRoute)(ctx, o);
15772
15946
  } catch (e) {
@@ -16089,7 +16263,7 @@ async function buildEnvironment(route, ship, ticket) {
16089
16263
  }
16090
16264
 
16091
16265
  // src/cli.ts
16092
- var CREW_HOME = isCompiledBinary(import.meta.url) ? dirname19(process.execPath) : resolve5(dirname19(fileURLToPath(import.meta.url)), "..");
16266
+ var CREW_HOME = isCompiledBinary(import.meta.url) ? dirname20(process.execPath) : resolve5(dirname20(fileURLToPath(import.meta.url)), "..");
16093
16267
  var RELEASE_LOCK_TTL_MS = 20 * 60 * 1e3;
16094
16268
  var CYCLE_SECONDS2 = 120;
16095
16269
  var STALE_CYCLES = 3;
@@ -16245,6 +16419,7 @@ function usage(problem, requested = false) {
16245
16419
  crew agents sync [route] [--force] [--revoke-seat-keys] push crew's personas AND skill files into the workspace Agents/Agent Skills tables (--force overwrites even a diverged persona or skill; never one marked Customize prompt)
16246
16420
  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)
16247
16421
  crew skills sync [route] push only crew's skill files (e.g. grill-me) into the workspace Agent Skills table
16422
+ crew ship rename NAME [--json] rename this ship in crew.yaml and on every workspace's Ships row
16248
16423
  crew repos add [ROUTE] [NAME] [PATH] attach a local checkout to a route in crew.yaml (--dry-run previews)
16249
16424
  crew repos clone [ROUTE] NAME clone a Repos row's remote into the route directory and attach it (--dir PATH, --dry-run, --json)
16250
16425
  crew repos hooks [ROUTE] NAME suggest test/build hooks for a checkout (--agent, --write route|repo, --recipes, --json)
@@ -16333,6 +16508,7 @@ var KNOWN_COMMANDS = /* @__PURE__ */ new Set([
16333
16508
  "run",
16334
16509
  "sample-ticket",
16335
16510
  "selftest",
16511
+ "ship",
16336
16512
  "skills",
16337
16513
  "status",
16338
16514
  "sync",
@@ -16482,9 +16658,9 @@ if (command === "passenger-sync-daemon") {
16482
16658
  firstRun = { answers, configPath };
16483
16659
  cfg = loadConfig(CREW_HOME, configPath, { text: renderShipBlock(answers), allowNoRoutes: true });
16484
16660
  } else {
16485
- const toleratesNoRoutes = command === "repos" || command === "disconnect" && flag("reset") || command === "connect" && flag("sign-out");
16661
+ const toleratesNoRoutes = command === "repos" || command === "ship" || command === "disconnect" && flag("reset") || command === "connect" && flag("sign-out");
16486
16662
  const configless = command === "connect" && flag("sign-out") || command === "disconnect" && flag("reset");
16487
- if (configless && !existsSync33(findConfigFile(CREW_HOME))) {
16663
+ if (configless && !existsSync34(findConfigFile(CREW_HOME))) {
16488
16664
  cfg = loadConfig(CREW_HOME, firstRunConfigPath(), { text: "ship: {}\n", allowNoRoutes: true });
16489
16665
  } else {
16490
16666
  cfg = loadConfig(CREW_HOME, void 0, toleratesNoRoutes ? { allowNoRoutes: true } : {});
@@ -16506,7 +16682,7 @@ if (command === "passenger-sync-daemon") {
16506
16682
  const DAEMON_SUBCOMMANDS = /* @__PURE__ */ new Set(["start", "stop", "status", "restart"]);
16507
16683
  const worktreeArgs = command === "worktree" ? splitWorktreeArgs(positional, cfg.routes.map((r) => r.route)) : void 0;
16508
16684
  const named = worktreeArgs ? worktreeArgs.route : command === "daemon" && DAEMON_SUBCOMMANDS.has(positional[1] ?? "") ? positional[2] : positional[1];
16509
- const fleetWide = command === "inbox" || command === "connect" || command === "disconnect" || command === "agents" || command === "skills" || command === "logbook" || command === "repos" || command === "projects" || command === "whoami" || releaseFleetWide || command === "pause" || command === "resume" || command === "reload" || FLEET_CAPABLE.has(command) && !named && cfg.routes.length > 1 || worktreeArgs !== void 0 && !named && cfg.routes.length > 1;
16685
+ const fleetWide = command === "inbox" || command === "connect" || command === "disconnect" || command === "agents" || command === "skills" || command === "logbook" || command === "repos" || command === "ship" || command === "projects" || command === "whoami" || releaseFleetWide || command === "pause" || command === "resume" || command === "reload" || FLEET_CAPABLE.has(command) && !named && cfg.routes.length > 1 || worktreeArgs !== void 0 && !named && cfg.routes.length > 1;
16510
16686
  let route;
16511
16687
  try {
16512
16688
  route = fleetWide ? cfg.routes[0] : findRoute(cfg, named);
@@ -16723,7 +16899,7 @@ if (command === "passenger-sync-daemon") {
16723
16899
  if (host === "linux" && hasSystemd) {
16724
16900
  const label = labelFor(CREW_HOME, "run");
16725
16901
  const staleTimer = join35(homedir9(), ".config", "systemd", "user", `${label}.timer`);
16726
- if (existsSync33(staleTimer)) {
16902
+ if (existsSync34(staleTimer)) {
16727
16903
  process.stdout.write(
16728
16904
  `removing stale run timer from before the persistent-service cutover: ${staleTimer}${dryRun ? " (dry run)" : ""}
16729
16905
  `
@@ -16749,7 +16925,7 @@ if (command === "passenger-sync-daemon") {
16749
16925
  const tracker = new Tracker(c, cfg.ship);
16750
16926
  problems.push(...validateContract(tracker.contract).map((p) => `contract: ${p}`));
16751
16927
  for (const r of await resolvedRepos(c)) {
16752
- if (!existsSync33(r.dir)) problems.push(`repo ${r.name}: ${r.dir} is missing on disk`);
16928
+ if (!existsSync34(r.dir)) problems.push(`repo ${r.name}: ${r.dir} is missing on disk`);
16753
16929
  if (!satisfies(host, r.config.platform)) problems.push(`repo ${r.name}: requires ${r.config.platform}, this host is ${host}`);
16754
16930
  problems.push(...validateEffective(r.config).map((p) => `repo ${r.name}: ${p}`));
16755
16931
  }
@@ -16767,7 +16943,7 @@ if (command === "passenger-sync-daemon") {
16767
16943
  }
16768
16944
  async function doctorFix(o) {
16769
16945
  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))));
16770
- const unitExists = (p) => existsSync33(p);
16946
+ const unitExists = (p) => existsSync34(p);
16771
16947
  const crontab = o.host === "linux" ? spawnSync6("crontab", ["-l"], { encoding: "utf8" }).stdout ?? "" : "";
16772
16948
  const schedulerAbsent = o.host !== "windows" ? !schedulerInstalled(planInstall(cfg.ship, CREW_HOME, o.host, detectSystemd(), "run"), unitExists, crontab) : true;
16773
16949
  const plan = planFix({ routes, schedulerAbsent, host: o.host });
@@ -16784,7 +16960,7 @@ if (command === "passenger-sync-daemon") {
16784
16960
  for (const a of plan.actions) {
16785
16961
  if (a.kind === "enable") {
16786
16962
  if (prompter && !await prompter.confirm(`Route ${a.route} passes every check \u2014 set enabled: true in ${cfg.configFile}?`, false)) continue;
16787
- writeFileSync21(cfg.configFile, planEnable(readFileSync27(cfg.configFile, "utf8"), a.route));
16963
+ writeFileSync22(cfg.configFile, planEnable(readFileSync28(cfg.configFile, "utf8"), a.route));
16788
16964
  enabledSound.add(a.route);
16789
16965
  changed2.push(`enabled route ${a.route} in ${cfg.configFile}`);
16790
16966
  } else {
@@ -17267,6 +17443,11 @@ ${changed2.map((c) => ` - ${c}
17267
17443
  case "poll":
17268
17444
  case "run": {
17269
17445
  const skipInlineRelease = flag("no-release");
17446
+ if (!dryRun) {
17447
+ for (const r of fleetWide ? cfg.routes : [route]) {
17448
+ await reconcileRouteShipName({ route: r, ship: cfg.ship, configFile: cfg.configFile, emit: emit.forRoute(r.route) });
17449
+ }
17450
+ }
17270
17451
  if (fleetWide) {
17271
17452
  const fleet = await decideFleet({
17272
17453
  routes: cfg.routes,
@@ -17800,6 +17981,7 @@ ${changed2.map((c) => ` - ${c}
17800
17981
  state,
17801
17982
  maxConcurrentAgents: cfg.ship.maxConcurrentAgents,
17802
17983
  spawnBreaker,
17984
+ configFile: cfg.configFile,
17803
17985
  newEmitter: () => new Emitter({
17804
17986
  route: route.route,
17805
17987
  eventFile: eventFileFor(cfg.ship.stateDir),
@@ -18006,15 +18188,72 @@ ${changed2.map((c) => ` - ${c}
18006
18188
  `);
18007
18189
  if (!dryRun) {
18008
18190
  const resolvedPath = resolvedPathFor(cfg.ship.stateDir, target.route);
18009
- const raw = existsSync33(resolvedPath) ? JSON.parse(readFileSync27(resolvedPath, "utf8")) : {};
18191
+ const raw = existsSync34(resolvedPath) ? JSON.parse(readFileSync28(resolvedPath, "utf8")) : {};
18010
18192
  raw.agentSkills = result.agentSkills;
18011
- mkdirSync20(dirOf(resolvedPath), { recursive: true });
18012
- writeFileSync21(resolvedPath, `${JSON.stringify(raw, null, 2)}
18193
+ mkdirSync21(dirOf(resolvedPath), { recursive: true });
18194
+ writeFileSync22(resolvedPath, `${JSON.stringify(raw, null, 2)}
18013
18195
  `);
18014
18196
  }
18015
18197
  if (result.outcomes.some((o) => o.action === "diverged")) process.exitCode = 1;
18016
18198
  break;
18017
18199
  }
18200
+ case "ship": {
18201
+ if (positional[1] !== "rename" || positional.length !== 3) {
18202
+ process.stderr.write("crew ship rename NAME [--json]\n");
18203
+ process.exit(2);
18204
+ }
18205
+ let newName;
18206
+ try {
18207
+ newName = validateShipName(positional[2]);
18208
+ } catch (e) {
18209
+ if (e instanceof ShipNameError) {
18210
+ process.stderr.write(`crew ship: ${e.message}
18211
+ `);
18212
+ process.exit(2);
18213
+ }
18214
+ throw e;
18215
+ }
18216
+ const oldName = cfg.ship.name;
18217
+ writeFileSync22(cfg.configFile, setShipNameInYaml(readFileSync28(cfg.configFile, "utf8"), newName));
18218
+ cfg.ship.name = newName;
18219
+ let renameMachineId;
18220
+ try {
18221
+ renameMachineId = ensureMachineId(cfg.ship.stateDir);
18222
+ } catch {
18223
+ }
18224
+ const outcomes = [];
18225
+ for (const r of cfg.routes) {
18226
+ if (!r.resolved?.shipsModelId) {
18227
+ outcomes.push({ route: r.route, status: "no-ships-table" });
18228
+ continue;
18229
+ }
18230
+ const resolvedFile = resolvedPathFor(cfg.ship.stateDir, r.route);
18231
+ const tracker = new Tracker(r, cfg.ship);
18232
+ const picked = pickShipRow(await tracker.shipRows(), renameMachineId, [oldName, readSyncedName(resolvedFile) ?? ""]);
18233
+ if (picked.kind !== "row") {
18234
+ outcomes.push({ route: r.route, status: picked.kind === "unsupported" ? "unsupported" : picked.kind === "none" ? "no-row" : "ambiguous" });
18235
+ continue;
18236
+ }
18237
+ try {
18238
+ await tracker.renameShipRow(picked.row.id, newName);
18239
+ writeSyncedName(resolvedFile, newName);
18240
+ outcomes.push({ route: r.route, status: "renamed", rowId: picked.row.id });
18241
+ } catch (e) {
18242
+ outcomes.push({ route: r.route, status: "refused", rowId: picked.row.id, error: e.message });
18243
+ }
18244
+ }
18245
+ if (flag("json")) {
18246
+ process.stdout.write(`${JSON.stringify({ name: newName, previous: oldName, configFile: cfg.configFile, routes: outcomes })}
18247
+ `);
18248
+ } else {
18249
+ process.stdout.write(`ship renamed "${oldName}" -> "${newName}" in ${cfg.configFile}
18250
+ `);
18251
+ for (const o of outcomes) process.stdout.write(` ${o.route}: ${o.status}${o.error ? ` (${o.error})` : ""}
18252
+ `);
18253
+ }
18254
+ if (outcomes.some((o) => o.status === "refused")) process.exitCode = 1;
18255
+ break;
18256
+ }
18018
18257
  case "repos": {
18019
18258
  const sub = positional[1];
18020
18259
  const usage2 = () => {
@@ -18022,13 +18261,13 @@ ${changed2.map((c) => ` - ${c}
18022
18261
  process.exit(2);
18023
18262
  };
18024
18263
  if (sub !== "add" && sub !== "list" && sub !== "clone" && sub !== "hooks") usage2();
18025
- const configText = readFileSync27(cfg.configFile, "utf8");
18264
+ const configText = readFileSync28(cfg.configFile, "utf8");
18026
18265
  if (sub === "hooks") {
18027
18266
  try {
18028
18267
  const a = parseHooksArgs(positional.slice(2), routeNamesOf(configText));
18029
- const dir = repoDirOf(configText, dirname19(cfg.configFile), a.route, a.name);
18030
- if (!existsSync33(dir)) throw new ReposError(`${dir} does not exist on disk`);
18031
- const files = { exists: (r) => existsSync33(resolvePath(dir, r)), list: (r) => {
18268
+ const dir = repoDirOf(configText, dirname20(cfg.configFile), a.route, a.name);
18269
+ if (!existsSync34(dir)) throw new ReposError(`${dir} does not exist on disk`);
18270
+ const files = { exists: (r) => existsSync34(resolvePath(dir, r)), list: (r) => {
18032
18271
  try {
18033
18272
  return readdirSync14(resolvePath(dir, r));
18034
18273
  } catch {
@@ -18036,7 +18275,7 @@ ${changed2.map((c) => ` - ${c}
18036
18275
  }
18037
18276
  }, read: (r) => {
18038
18277
  try {
18039
- return readFileSync27(resolvePath(dir, r), "utf8");
18278
+ return readFileSync28(resolvePath(dir, r), "utf8");
18040
18279
  } catch {
18041
18280
  return void 0;
18042
18281
  }
@@ -18074,13 +18313,13 @@ ${changed2.map((c) => ` - ${c}
18074
18313
  if (write && suggestion.source === null && recipeIds.length === 0 && hookSpecs.length === 0) throw new ReposError(`nothing to write: ${suggestion.notes[0]}`);
18075
18314
  if (write === "route") {
18076
18315
  const plan = planRouteHooks(configText, a.route, a.name, picked.hooks);
18077
- writeFileSync21(cfg.configFile, plan.text);
18316
+ writeFileSync22(cfg.configFile, plan.text);
18078
18317
  written = { target: "route", file: cfg.configFile, hooks: plan.written, kept: plan.kept };
18079
18318
  } else if (write === "repo") {
18080
18319
  const have = existingRepoConfig(files);
18081
18320
  if (have) throw new ReposError(`${resolvePath(dir, have)} already exists; not overwriting it`);
18082
18321
  const file = resolvePath(dir, ".crew.yaml");
18083
- writeFileSync21(file, starterRepoConfig(picked.hooks, picked.release), { flag: "wx" });
18322
+ writeFileSync22(file, starterRepoConfig(picked.hooks, picked.release), { flag: "wx" });
18084
18323
  written = { target: "repo", file, hooks: Object.keys(picked.hooks), kept: [] };
18085
18324
  }
18086
18325
  if (flag("json")) {
@@ -18132,7 +18371,7 @@ ${changed2.map((c) => ` - ${c}
18132
18371
  if (sub === "list" && !routeName) usage2();
18133
18372
  if (sub === "add") {
18134
18373
  try {
18135
- const a = parseAddArgs(positional.slice(2), routeNamesOf(configText), (p) => existsSync33(resolvePath(p.replace(/^~(?=\/|$)/, homedir9()))));
18374
+ const a = parseAddArgs(positional.slice(2), routeNamesOf(configText), (p) => existsSync34(resolvePath(p.replace(/^~(?=\/|$)/, homedir9()))));
18136
18375
  routeName = a.route;
18137
18376
  addPath = a.path;
18138
18377
  addName = a.name;
@@ -18161,15 +18400,15 @@ ${changed2.map((c) => ` - ${c}
18161
18400
  }
18162
18401
  const resolvedFile = resolvedPathFor(cfg.ship.stateDir, routeName);
18163
18402
  let resolvedRemotes;
18164
- if (existsSync33(resolvedFile)) {
18403
+ if (existsSync34(resolvedFile)) {
18165
18404
  try {
18166
- resolvedRemotes = JSON.parse(readFileSync27(resolvedFile, "utf8"));
18405
+ resolvedRemotes = JSON.parse(readFileSync28(resolvedFile, "utf8"));
18167
18406
  } catch {
18168
18407
  }
18169
18408
  }
18170
18409
  const originOf = (dir) => gitOk(dir, ["remote", "get-url", "origin"]) ?? void 0;
18171
18410
  const text = configText;
18172
- const base = dirname19(cfg.configFile);
18411
+ const base = dirname20(cfg.configFile);
18173
18412
  try {
18174
18413
  if (sub === "list") {
18175
18414
  if (flag("json")) {
@@ -18179,10 +18418,10 @@ ${changed2.map((c) => ` - ${c}
18179
18418
  base,
18180
18419
  resolved: resolvedRemotes,
18181
18420
  originOf,
18182
- exists: (d) => existsSync33(d),
18421
+ exists: (d) => existsSync34(d),
18183
18422
  repoHasHooks: (d) => REPO_CONFIG_FILENAMES.some((f) => {
18184
18423
  try {
18185
- return Object.keys(parseRepoConfig(readFileSync27(resolvePath(d, f), "utf8"), f).hooks).length > 0;
18424
+ return Object.keys(parseRepoConfig(readFileSync28(resolvePath(d, f), "utf8"), f).hooks).length > 0;
18186
18425
  } catch {
18187
18426
  return false;
18188
18427
  }
@@ -18198,7 +18437,7 @@ ${changed2.map((c) => ` - ${c}
18198
18437
  for (const r of repos) {
18199
18438
  const origin2 = originOf(r.dir);
18200
18439
  const row = matchRepoName(origin2, resolvedRemotes);
18201
- const note = !existsSync33(r.dir) ? "missing on disk" : row ? `Repos row: ${row}` : `no Repos row matches ${normalizeRemote(origin2) ?? "this checkout (no origin remote)"}`;
18440
+ const note = !existsSync34(r.dir) ? "missing on disk" : row ? `Repos row: ${row}` : `no Repos row matches ${normalizeRemote(origin2) ?? "this checkout (no origin remote)"}`;
18202
18441
  process.stdout.write(`${r.name} ${r.dir} ${note}
18203
18442
  `);
18204
18443
  }
@@ -18210,9 +18449,9 @@ ${changed2.map((c) => ` - ${c}
18210
18449
  ` : `${human}
18211
18450
  `);
18212
18451
  const plan2 = planRepoClone({ text, base, route: routeName, name: addName, resolved: resolvedRemotes, dir: value("dir") });
18213
- if (existsSync33(plan2.target) && (existsSync33(`${plan2.target}/.git`) || readdirSync14(plan2.target).length > 0)) {
18452
+ if (existsSync34(plan2.target) && (existsSync34(`${plan2.target}/.git`) || readdirSync14(plan2.target).length > 0)) {
18214
18453
  throw new ReposError(
18215
- existsSync33(`${plan2.target}/.git`) ? `${plan2.target} is already a checkout \u2014 attach it with \`crew repos add ${routeName} ${plan2.name} ${plan2.target}\`` : `${plan2.target} exists and is not empty \u2014 pass --dir to clone somewhere else`
18454
+ existsSync34(`${plan2.target}/.git`) ? `${plan2.target} is already a checkout \u2014 attach it with \`crew repos add ${routeName} ${plan2.name} ${plan2.target}\`` : `${plan2.target} exists and is not empty \u2014 pass --dir to clone somewhere else`
18216
18455
  );
18217
18456
  }
18218
18457
  if (dryRun) {
@@ -18230,12 +18469,12 @@ ${changed2.map((c) => ` - ${c}
18230
18469
  throw new ReposError(`${e.message}${authHint}
18231
18470
  crew.yaml was not changed`);
18232
18471
  }
18233
- writeFileSync21(cfg.configFile, plan2.text);
18472
+ writeFileSync22(cfg.configFile, plan2.text);
18234
18473
  say("attached", { route: routeName, name: plan2.name, dir: plan2.target, mode: plan2.mode }, `cloned and attached ${plan2.target} to ${routeName} as ${plan2.mode === "dir" ? "dir" : `repos.${plan2.name}`} in ${cfg.configFile}`);
18235
18474
  break;
18236
18475
  }
18237
18476
  const target = resolvePath(addPath.replace(/^~(?=\/|$)/, homedir9()));
18238
- const top = existsSync33(target) ? gitOk(target, ["rev-parse", "--show-toplevel"]) : null;
18477
+ const top = existsSync34(target) ? gitOk(target, ["rev-parse", "--show-toplevel"]) : null;
18239
18478
  if (!top) throw new ReposError(`${target} is not a git checkout`);
18240
18479
  const origin = originOf(top);
18241
18480
  const plan = planRepoAdd({
@@ -18284,15 +18523,15 @@ ${changed2.map((c) => ` - ${c}
18284
18523
  const created = await rowTracker.createRepoRow(rowPayload);
18285
18524
  process.stdout.write(`created Repos row ${String(rowPayload.name)} (${String(rowPayload.remote)}) ${created.id}
18286
18525
  `);
18287
- if (existsSync33(resolvedFile)) {
18288
- const raw = JSON.parse(readFileSync27(resolvedFile, "utf8"));
18526
+ if (existsSync34(resolvedFile)) {
18527
+ const raw = JSON.parse(readFileSync28(resolvedFile, "utf8"));
18289
18528
  raw.repoNames = { ...raw.repoNames, [created.id]: String(rowPayload.name) };
18290
18529
  raw.repoRemotes = { ...raw.repoRemotes, [created.id]: String(rowPayload.remote) };
18291
- writeFileSync21(resolvedFile, `${JSON.stringify(raw, null, 2)}
18530
+ writeFileSync22(resolvedFile, `${JSON.stringify(raw, null, 2)}
18292
18531
  `);
18293
18532
  }
18294
18533
  }
18295
- writeFileSync21(cfg.configFile, plan.text);
18534
+ writeFileSync22(cfg.configFile, plan.text);
18296
18535
  process.stdout.write(`added ${top} to ${routeName} as ${plan.mode === "dir" ? "dir" : `repos.${plan.name}`} in ${cfg.configFile}
18297
18536
  `);
18298
18537
  } catch (e) {
@@ -18576,7 +18815,8 @@ Signed in as ${result.identity.email} \u2014 minted an API key in workspace "${r
18576
18815
  platform: cfg.ship.platform,
18577
18816
  machineId,
18578
18817
  hostPassengers: firstRun?.answers.hostPassengers ?? existingRoute?.hostPassengers === true,
18579
- sshPublicKey
18818
+ sshPublicKey,
18819
+ lastSyncedName: (r) => readSyncedName(resolvedPathFor(cfg.ship.stateDir, r))
18580
18820
  };
18581
18821
  const runConnect = async (ws, proj) => {
18582
18822
  let found = await discover({ ...authOpts, workspace: ws, project: proj, area: value("area"), ship, dryRun, interactive: canAsk || connectEvents.json });
@@ -18633,6 +18873,10 @@ Signed in as ${result.identity.email} \u2014 minted an API key in workspace "${r
18633
18873
  }
18634
18874
  process.exit(2);
18635
18875
  }
18876
+ if (found.shipNameAdopted && !dryRun) {
18877
+ writeFileSync22(cfg.configFile, setShipNameInYaml(readFileSync28(cfg.configFile, "utf8"), found.shipNameAdopted));
18878
+ cfg.ship.name = found.shipNameAdopted;
18879
+ }
18636
18880
  if (found.shipRow) {
18637
18881
  connectEvents.emit("ship_row", `Ships row "${found.shipRow.name}" ${found.shipRow.action}`, found.shipRow);
18638
18882
  }
@@ -18673,10 +18917,10 @@ Signed in as ${result.identity.email} \u2014 minted an API key in workspace "${r
18673
18917
  }
18674
18918
  }
18675
18919
  let previous;
18676
- const hadPreviousFile = existsSync33(resolvedPath);
18920
+ const hadPreviousFile = existsSync34(resolvedPath);
18677
18921
  if (hadPreviousFile) {
18678
18922
  try {
18679
- previous = JSON.parse(readFileSync27(resolvedPath, "utf8"));
18923
+ previous = JSON.parse(readFileSync28(resolvedPath, "utf8"));
18680
18924
  } catch {
18681
18925
  }
18682
18926
  }
@@ -18788,18 +19032,20 @@ Signed in as ${result.identity.email} \u2014 minted an API key in workspace "${r
18788
19032
  // nothing about its prompt actually changed.
18789
19033
  ...previous?.agentPersonas ? { agentPersonas: previous.agentPersonas } : {},
18790
19034
  ...previous?.agentSkills ? { agentSkills: previous.agentSkills } : {},
18791
- ...previous?.releaseAgentId ? { releaseAgentId: previous.releaseAgentId } : {}
19035
+ ...previous?.releaseAgentId ? { releaseAgentId: previous.releaseAgentId } : {},
19036
+ // CREW-2495: the ship name this route last saw agree with its Ships row.
19037
+ ...found.shipRow ? { shipName: found.shipRow.name } : previous?.shipName ? { shipName: previous.shipName } : {}
18792
19038
  };
18793
19039
  if (dryRun) {
18794
19040
  process.stderr.write(`(dry run) would write resolved ids to ${resolvedPath}
18795
19041
  `);
18796
19042
  } else {
18797
- mkdirSync20(dirname19(resolvedPath), { recursive: true });
19043
+ mkdirSync21(dirname20(resolvedPath), { recursive: true });
18798
19044
  if (hadPreviousFile) copyFileSync2(resolvedPath, `${resolvedPath}.bak`);
18799
19045
  const tmpPath = `${resolvedPath}.tmp${process.pid}`;
18800
- writeFileSync21(tmpPath, `${JSON.stringify(resolved, null, 2)}
19046
+ writeFileSync22(tmpPath, `${JSON.stringify(resolved, null, 2)}
18801
19047
  `);
18802
- renameSync2(tmpPath, resolvedPath);
19048
+ renameSync3(tmpPath, resolvedPath);
18803
19049
  process.stderr.write(`Resolved ids written to ${resolvedPath}
18804
19050
  `);
18805
19051
  }
@@ -18839,8 +19085,8 @@ Signed in as ${result.identity.email} \u2014 minted an API key in workspace "${r
18839
19085
  process.stderr.write(`(dry run) would write the minted API key to ${apiKeyFile}
18840
19086
  `);
18841
19087
  } else {
18842
- mkdirSync20(dirname19(apiKeyFile), { recursive: true });
18843
- writeFileSync21(apiKeyFile, `${apiKeyVar}=${apiKey}
19088
+ mkdirSync21(dirname20(apiKeyFile), { recursive: true });
19089
+ writeFileSync22(apiKeyFile, `${apiKeyVar}=${apiKey}
18844
19090
  `, { mode: 384 });
18845
19091
  process.stderr.write(`API key written to ${apiKeyFile}
18846
19092
  `);
@@ -19528,7 +19774,7 @@ Signed in as ${result.identity.email} \u2014 minted an API key in workspace "${r
19528
19774
  };
19529
19775
  const droppedRoutes = [];
19530
19776
  try {
19531
- const rawCfg = parseYaml2(readFileSync27(cfg.configFile, "utf8"));
19777
+ const rawCfg = parseYaml2(readFileSync28(cfg.configFile, "utf8"));
19532
19778
  for (const r of rawCfg?.routes ?? []) {
19533
19779
  if (typeof r?.route !== "string" || cfg.routes.some((x) => x.route === r.route)) continue;
19534
19780
  try {
@@ -19570,8 +19816,8 @@ Signed in as ${result.identity.email} \u2014 minted an API key in workspace "${r
19570
19816
  process.stdout.write("crew reload: no daemon unit is running \u2014 nothing to reload (the next run reads crew.yaml fresh)\n");
19571
19817
  break;
19572
19818
  }
19573
- mkdirSync20(cfg.ship.stateDir, { recursive: true });
19574
- writeFileSync21(sentinel, `${(/* @__PURE__ */ new Date()).toISOString()}
19819
+ mkdirSync21(cfg.ship.stateDir, { recursive: true });
19820
+ writeFileSync22(sentinel, `${(/* @__PURE__ */ new Date()).toISOString()}
19575
19821
  `);
19576
19822
  process.stdout.write("crew reload: requested \u2014 the daemon finishes its in-flight pass, then relaunches with the new config\n");
19577
19823
  if (flag("now") && control?.stopCommand && control.startCommand) {
@@ -19622,7 +19868,7 @@ Signed in as ${result.identity.email} \u2014 minted an API key in workspace "${r
19622
19868
  }
19623
19869
  case "log": {
19624
19870
  try {
19625
- const text = readFileSync27(cfg.ship.logFile, "utf8").split("\n");
19871
+ const text = readFileSync28(cfg.ship.logFile, "utf8").split("\n");
19626
19872
  process.stdout.write(`${text.slice(-(Number(value("tail")) || 40)).join("\n")}
19627
19873
  `);
19628
19874
  } catch {
@@ -19645,8 +19891,8 @@ Signed in as ${result.identity.email} \u2014 minted an API key in workspace "${r
19645
19891
  };
19646
19892
  if (!route.resolved) fail(`route ${route.route} is not connected \u2014 run \`crew connect\` first`);
19647
19893
  const tracker = new Tracker(route, cfg.ship);
19648
- const configText = readFileSync27(cfg.configFile, "utf8");
19649
- const cfgBase = dirname19(cfg.configFile);
19894
+ const configText = readFileSync28(cfg.configFile, "utf8");
19895
+ const cfgBase = dirname20(cfg.configFile);
19650
19896
  const configured = configuredRepos(configText, route.route, cfgBase);
19651
19897
  const dir = configured.find((r) => r.name === SAMPLE_REPO)?.dir ?? sampleDir(configured.map((r) => r.dir), route.reposBasePath, route.route.split("/")[0]);
19652
19898
  const resolvedFile = resolvedPathFor(cfg.ship.stateDir, route.route);
@@ -19674,7 +19920,7 @@ Signed in as ${result.identity.email} \u2014 minted an API key in workspace "${r
19674
19920
  }
19675
19921
  if (configured.some((r) => r.dir === dir)) {
19676
19922
  try {
19677
- writeFileSync21(cfg.configFile, planRepoRemove(configText, cfgBase, route.route, dir).text);
19923
+ writeFileSync22(cfg.configFile, planRepoRemove(configText, cfgBase, route.route, dir).text);
19678
19924
  } catch (e) {
19679
19925
  if (e instanceof ReposError) fail(e.message);
19680
19926
  throw e;
@@ -19682,29 +19928,29 @@ Signed in as ${result.identity.email} \u2014 minted an API key in workspace "${r
19682
19928
  }
19683
19929
  if (row) {
19684
19930
  await tracker.deleteRepoRow(row.id);
19685
- if (existsSync33(resolvedFile)) {
19686
- const raw = JSON.parse(readFileSync27(resolvedFile, "utf8"));
19931
+ if (existsSync34(resolvedFile)) {
19932
+ const raw = JSON.parse(readFileSync28(resolvedFile, "utf8"));
19687
19933
  if (raw.repoNames) delete raw.repoNames[row.id];
19688
19934
  if (raw.repoRemotes) delete raw.repoRemotes[row.id];
19689
- writeFileSync21(resolvedFile, `${JSON.stringify(raw, null, 2)}
19935
+ writeFileSync22(resolvedFile, `${JSON.stringify(raw, null, 2)}
19690
19936
  `);
19691
19937
  }
19692
19938
  }
19693
- if (existsSync33(join35(dir, ".git")) && basename11(dir) === SAMPLE_REPO) rmSync15(dir, { recursive: true, force: true });
19939
+ if (existsSync34(join35(dir, ".git")) && basename11(dir) === SAMPLE_REPO) rmSync15(dir, { recursive: true, force: true });
19694
19940
  say("removed", { dir, closed }, `removed the sample repo${closed.length ? `, closed ${closed.join(", ")}` : ""}`);
19695
19941
  break;
19696
19942
  }
19697
19943
  const projectId = route.resolved.areaId ?? resolveRepoProject(await tracker.projectRows(), value("project"));
19698
- const fresh = !existsSync33(join35(dir, ".git"));
19699
- if (existsSync33(dir) && fresh && readdirSync14(dir).length > 0) fail(`${dir} exists and is not a git checkout \u2014 move it aside`);
19944
+ const fresh = !existsSync34(join35(dir, ".git"));
19945
+ if (existsSync34(dir) && fresh && readdirSync14(dir).length > 0) fail(`${dir} exists and is not a git checkout \u2014 move it aside`);
19700
19946
  if (dryRun) {
19701
19947
  say("dry-run", { dir, newCheckout: fresh, repoRow: row?.id ?? null }, `would ${fresh ? "create" : "reuse"} ${dir}${row ? "" : ", create the Repos row"}, and file one ${tracker.contract.statuses.approved} ticket`);
19702
19948
  break;
19703
19949
  }
19704
19950
  if (fresh) {
19705
- mkdirSync20(dir, { recursive: true });
19706
- writeFileSync21(join35(dir, "README.md"), SAMPLE_README);
19707
- writeFileSync21(join35(dir, ".crew.yaml"), SAMPLE_CREW_YAML);
19951
+ mkdirSync21(dir, { recursive: true });
19952
+ writeFileSync22(join35(dir, "README.md"), SAMPLE_README);
19953
+ writeFileSync22(join35(dir, ".crew.yaml"), SAMPLE_CREW_YAML);
19708
19954
  const g = (...a) => {
19709
19955
  const r = spawnSync6("git", a, { cwd: dir, encoding: "utf8" });
19710
19956
  if (r.status !== 0) fail(`git ${a[0]} failed in ${dir}: ${(r.stderr || r.stdout).trim()}`);
@@ -19722,17 +19968,17 @@ Signed in as ${result.identity.email} \u2014 minted an API key in workspace "${r
19722
19968
  spec: { platform: "unix", releaseMode: "integrate", ciProvider: "none" }
19723
19969
  });
19724
19970
  repoId = (await tracker.createRepoRow(payload)).id;
19725
- if (existsSync33(resolvedFile)) {
19726
- const raw = JSON.parse(readFileSync27(resolvedFile, "utf8"));
19971
+ if (existsSync34(resolvedFile)) {
19972
+ const raw = JSON.parse(readFileSync28(resolvedFile, "utf8"));
19727
19973
  raw.repoNames = { ...raw.repoNames, [repoId]: SAMPLE_REPO };
19728
19974
  raw.repoRemotes = { ...raw.repoRemotes, [repoId]: SAMPLE_REMOTE };
19729
- writeFileSync21(resolvedFile, `${JSON.stringify(raw, null, 2)}
19975
+ writeFileSync22(resolvedFile, `${JSON.stringify(raw, null, 2)}
19730
19976
  `);
19731
19977
  }
19732
19978
  }
19733
19979
  if (!configured.some((r) => r.dir === dir)) {
19734
19980
  try {
19735
- writeFileSync21(cfg.configFile, planRepoAdd({ text: configText, base: cfgBase, route: route.route, path: dir, name: SAMPLE_REPO }).text);
19981
+ writeFileSync22(cfg.configFile, planRepoAdd({ text: configText, base: cfgBase, route: route.route, path: dir, name: SAMPLE_REPO }).text);
19736
19982
  } catch (e) {
19737
19983
  if (e instanceof ReposError) fail(e.message);
19738
19984
  throw e;
@@ -19853,6 +20099,7 @@ Signed in as ${result.identity.email} \u2014 minted an API key in workspace "${r
19853
20099
  const projects = projectModeLabels(await statusTracker.projectRowsWithModes().catch(() => []));
19854
20100
  const crewMinVersion = await readCrewFloor(route, cfg.ship) ?? null;
19855
20101
  if (flag("json")) {
20102
+ const nameSync = await reconcileRouteShipName({ route, ship: cfg.ship, configFile: cfg.configFile, readOnly: true });
19856
20103
  const repos = await resolvedRepos(route);
19857
20104
  const seatIds = route.resolved?.seats ?? {};
19858
20105
  const crewRows = Object.keys(seatIds).length ? await statusTracker.crewRows() : [];
@@ -19896,7 +20143,10 @@ Signed in as ${result.identity.email} \u2014 minted an API key in workspace "${r
19896
20143
  eventFile: eventFileFor(cfg.ship.stateDir),
19897
20144
  // Additive (CREW-1373): open ship-level items, including the
19898
20145
  // derived `release_stale`. `version` stays 1.
19899
- attention: openShipAttention(state)
20146
+ attention: openShipAttention(state),
20147
+ // Additive (CREW-2495): this ship's Ships-row name and whether it has drifted from `name`.
20148
+ rowName: nameSync.rowName ?? null,
20149
+ nameDrift: nameSync.drift
19900
20150
  },
19901
20151
  // Additive (CREW-1402): what the operator owes. `version` stays 1.
19902
20152
  todo: todo.map((i) => ({ ticket: displayKey(i.ticket), title: i.ticket.title ?? null, reasons: i.reasons })),
@@ -20043,7 +20293,7 @@ watermark: ${state.watermark()}
20043
20293
  }
20044
20294
  for (const r of repos) {
20045
20295
  const remote = r.config.branch.remote;
20046
- if (existsSync33(r.dir)) {
20296
+ if (existsSync34(r.dir)) {
20047
20297
  pushAuth.push({
20048
20298
  route: c.route,
20049
20299
  repo: r.name,
@@ -20071,7 +20321,7 @@ routes: ${cfg.routes.map((c) => c.route).join(", ")}
20071
20321
  );
20072
20322
  try {
20073
20323
  const evFile = eventFileFor(cfg.ship.stateDir);
20074
- const evSize = existsSync33(evFile) ? statSync10(evFile).size : 0;
20324
+ const evSize = existsSync34(evFile) ? statSync10(evFile).size : 0;
20075
20325
  process.stdout.write(
20076
20326
  evSize > cfg.ship.eventsRotateBytes * 4 ? `events log: WARN ${evFile} is ${Math.round(evSize / 1048576)} MB (> 4x eventsRotateBytes); run \`crew reap\` to rotate it
20077
20327
  ` : "events log: ok\n"
@@ -20082,7 +20332,7 @@ routes: ${cfg.routes.map((c) => c.route).join(", ")}
20082
20332
  const unitPlan = planInstall(cfg.ship, CREW_HOME, host, detectSystemd(), "run");
20083
20333
  const abandons = unitAbandonsAgents(unitPlan, (p) => {
20084
20334
  try {
20085
- return readFileSync27(p, "utf8");
20335
+ return readFileSync28(p, "utf8");
20086
20336
  } catch {
20087
20337
  return null;
20088
20338
  }