@tablation/crew 0.71.2 → 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 (3) hide show
  1. package/README.md +2 -0
  2. package/dist/cli.js +880 -296
  3. package/package.json +1 -1
package/dist/cli.js CHANGED
@@ -108,6 +108,25 @@ async function settleRepos(items, nameOf, resolve6) {
108
108
  }
109
109
  return { settled, errors };
110
110
  }
111
+ function pidRunning(pid) {
112
+ try {
113
+ process.kill(pid, 0);
114
+ return true;
115
+ } catch (e) {
116
+ return e.code !== "ESRCH";
117
+ }
118
+ }
119
+ function staleOwnLockClaim(holder, shipName2, running = pidRunning) {
120
+ if (!holder) return false;
121
+ const i = holder.lastIndexOf(":");
122
+ if (i < 0 || holder.slice(0, i) !== shipName2) return false;
123
+ const pid = Number(holder.slice(i + 1));
124
+ return Number.isInteger(pid) && pid > 0 && !running(pid);
125
+ }
126
+ function formatStaleOwnLock(scope, holder) {
127
+ return `WARN lock ${scope} is held by ${holder}, but that process is gone \u2014 the next release cycle reclaims it
128
+ `;
129
+ }
111
130
 
112
131
  // src/selftest.ts
113
132
  import { spawn as spawn3 } from "child_process";
@@ -5569,7 +5588,7 @@ import { StaleWriteError } from "@tablation/client";
5569
5588
  function releaseLockScope(route, repo) {
5570
5589
  return `${route}/${repo}`;
5571
5590
  }
5572
- async function acquireBoardLock(client2, modelId, scope, holderLabel, ttlMs) {
5591
+ async function acquireBoardLock(client2, modelId, scope, holderLabel, ttlMs, log = {}) {
5573
5592
  if (!modelId) return { ok: true, release: async () => {
5574
5593
  }, exclusion: "off" };
5575
5594
  const filters = JSON.stringify([{ columnName: "scope", operator: "EQ", value: scope }]);
@@ -5577,7 +5596,9 @@ async function acquireBoardLock(client2, modelId, scope, holderLabel, ttlMs) {
5577
5596
  const row = rows[0];
5578
5597
  if (!row) return { ok: false, reason: "missing" };
5579
5598
  const age = Date.now() - new Date(row.updated_at).getTime();
5580
- const free = !row.holder || row.holder === holderLabel || age > ttlMs;
5599
+ const staleOwn = !!row.holder && row.holder !== holderLabel && isDeadOwnClaim(row.holder, holderLabel);
5600
+ const free = !row.holder || row.holder === holderLabel || age > ttlMs || staleOwn;
5601
+ if (staleOwn) log.warn?.(`reclaimed stale own claim ${row.holder} on ${scope} (process is gone)`);
5581
5602
  if (!free) return { ok: false, heldBy: row.holder ?? void 0, reason: "held" };
5582
5603
  let claimed;
5583
5604
  try {
@@ -5591,12 +5612,34 @@ async function acquireBoardLock(client2, modelId, scope, holderLabel, ttlMs) {
5591
5612
  exclusion: "taken",
5592
5613
  release: async () => {
5593
5614
  try {
5594
- await client2.update(modelId, row.id, { holder: "" }, claimed.updated_at);
5595
- } catch {
5615
+ try {
5616
+ await client2.update(modelId, row.id, { holder: "" }, claimed.updated_at);
5617
+ } catch (e) {
5618
+ if (!(e instanceof StaleWriteError)) throw e;
5619
+ const [now] = await client2.list(modelId, { filters: scopeFilter(scope), limit: 1 });
5620
+ if (now?.holder === holderLabel) await client2.update(modelId, row.id, { holder: "" }, now.updated_at);
5621
+ }
5622
+ log.debug?.(`lock released for ${scope}`);
5623
+ } catch (e) {
5624
+ log.warn?.(`lock release failed for ${scope}: ${e.message}`);
5596
5625
  }
5597
5626
  }
5598
5627
  };
5599
5628
  }
5629
+ function isDeadOwnClaim(holder, mine) {
5630
+ const split = (l) => {
5631
+ const i = l.lastIndexOf(":");
5632
+ return i < 0 ? null : { ship: l.slice(0, i), pid: Number(l.slice(i + 1)) };
5633
+ };
5634
+ const h = split(holder), m = split(mine);
5635
+ if (!h || !m || h.ship !== m.ship || !Number.isInteger(h.pid) || h.pid <= 0) return false;
5636
+ try {
5637
+ process.kill(h.pid, 0);
5638
+ return false;
5639
+ } catch (e) {
5640
+ return e.code === "ESRCH";
5641
+ }
5642
+ }
5600
5643
  async function lockRowState(client2, modelId, scope) {
5601
5644
  const rows = await client2.list(modelId, { filters: scopeFilter(scope), limit: 1 });
5602
5645
  const row = rows[0];
@@ -5877,6 +5920,12 @@ var Tracker = class {
5877
5920
  return [];
5878
5921
  }
5879
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
+ }
5880
5929
  /**
5881
5930
  * This ship's own row on the `Ships` table, matched by name — the same
5882
5931
  * exact-string match `doctor` has always used (see the callsign comment
@@ -6068,8 +6117,8 @@ var Tracker = class {
6068
6117
  * Locks table with no row for `scope` is `{ ok: false, reason: 'missing' }`
6069
6118
  * (CREW-1384): `crew connect` provisions the row.
6070
6119
  */
6071
- acquireBoardLock(scope, holderLabel, ttlMs) {
6072
- return acquireBoardLock(this.client.records, this.route.resolved?.locksModelId, scope, holderLabel, ttlMs);
6120
+ acquireBoardLock(scope, holderLabel, ttlMs, log) {
6121
+ return acquireBoardLock(this.client.records, this.route.resolved?.locksModelId, scope, holderLabel, ttlMs, log);
6073
6122
  }
6074
6123
  /** The Locks row for `scope` — present? held by whom? Undefined when this workspace has no Locks table. */
6075
6124
  async lockState(scope) {
@@ -8563,9 +8612,9 @@ function doctorSelftestLines(stored, toolchains) {
8563
8612
  }
8564
8613
 
8565
8614
  // src/cli.ts
8566
- import { resolve as resolve5, dirname as dirname19, join as join34, basename as basename11 } from "path";
8615
+ import { resolve as resolve5, dirname as dirname20, join as join35, basename as basename11 } from "path";
8567
8616
  import { fileURLToPath } from "url";
8568
- import { hostname as hostname2, homedir as homedir8 } from "os";
8617
+ import { hostname as hostname2, homedir as homedir9 } from "os";
8569
8618
  import { spawnSync as spawnSync6 } from "child_process";
8570
8619
  import { parse as parseYaml2 } from "yaml";
8571
8620
 
@@ -8579,6 +8628,7 @@ var VALUE_FLAGS = /* @__PURE__ */ new Set([
8579
8628
  "extra-path",
8580
8629
  "harness",
8581
8630
  "hook",
8631
+ "host",
8582
8632
  "key",
8583
8633
  "level",
8584
8634
  "limit",
@@ -8641,6 +8691,9 @@ function versionRequest(argv2) {
8641
8691
  if (rest.some((a) => a !== "--json")) return null;
8642
8692
  return { json: rest.length > 0 };
8643
8693
  }
8694
+ function helpRequest(argv2) {
8695
+ return argv2[0] === "--help" || argv2[0] === "-h" || argv2[0] === "help";
8696
+ }
8644
8697
 
8645
8698
  // src/roster.ts
8646
8699
  function memberByIdentity(roster, identityId) {
@@ -8736,7 +8789,7 @@ function rosterMarkdown(roster, meId) {
8736
8789
  }
8737
8790
 
8738
8791
  // src/connect-resume.ts
8739
- import { mkdirSync as mkdirSync10, readFileSync as readFileSync16, writeFileSync as writeFileSync11 } from "fs";
8792
+ import { mkdirSync as mkdirSync10, readFileSync as readFileSync16, rmSync as rmSync10, writeFileSync as writeFileSync11 } from "fs";
8740
8793
  import { dirname as dirname8, join as join19 } from "path";
8741
8794
  function lastSessionFile(stateDir) {
8742
8795
  return join19(stateDir, "connect-session.json");
@@ -8771,10 +8824,45 @@ async function resumeKeychainSession(o) {
8771
8824
  return void 0;
8772
8825
  }
8773
8826
  }
8827
+ function rememberedSessions(stateDir) {
8828
+ return Object.entries(readPointers(stateDir)).map(([host, workspace]) => ({ host, workspace }));
8829
+ }
8830
+ function forgetSession(stateDir, host) {
8831
+ try {
8832
+ const { [host]: _gone, ...rest } = readPointers(stateDir);
8833
+ if (Object.keys(rest).length) writeFileSync11(lastSessionFile(stateDir), `${JSON.stringify(rest, null, 2)}
8834
+ `, { mode: 384 });
8835
+ else rmSync10(lastSessionFile(stateDir), { force: true });
8836
+ } catch {
8837
+ }
8838
+ }
8839
+ async function signOutSessions(o) {
8840
+ const targets = rememberedSessions(o.stateDir).filter((t) => !o.host || t.host === o.host);
8841
+ if (o.host && o.workspace && !targets.some((t) => t.host === o.host && t.workspace === o.workspace)) {
8842
+ targets.push({ host: o.host, workspace: o.workspace });
8843
+ }
8844
+ const out = [];
8845
+ for (const t of targets) {
8846
+ try {
8847
+ const stored = await o.store.get(t.host, t.workspace);
8848
+ if (stored?.apiKey) {
8849
+ if (!o.dryRun) await o.store.clear(t.host, t.workspace);
8850
+ out.push({ ...t, action: o.dryRun ? "would-remove" : "removed" });
8851
+ } else {
8852
+ out.push({ ...t, action: "absent", detail: "no keychain entry" });
8853
+ }
8854
+ if (!o.dryRun) forgetSession(o.stateDir, t.host);
8855
+ } catch (e) {
8856
+ out.push({ ...t, action: "failed", detail: e.message });
8857
+ }
8858
+ }
8859
+ return out;
8860
+ }
8774
8861
 
8775
8862
  // src/reset.ts
8776
- import { existsSync as existsSync20, mkdirSync as mkdirSync11, readFileSync as readFileSync17, readdirSync as readdirSync7, rmSync as rmSync10, statSync as statSync6, writeFileSync as writeFileSync12 } from "fs";
8863
+ import { existsSync as existsSync20, mkdirSync as mkdirSync11, readFileSync as readFileSync17, readdirSync as readdirSync7, rmSync as rmSync11, statSync as statSync6, writeFileSync as writeFileSync12 } from "fs";
8777
8864
  import { dirname as dirname9, join as join20 } from "path";
8865
+ import { hostFromUrl as hostFromUrl2 } from "@tablation/client";
8778
8866
  function clonedCheckoutsFile(stateDir) {
8779
8867
  return join20(stateDir, "cloned-checkouts.json");
8780
8868
  }
@@ -8820,12 +8908,31 @@ async function runReset(o, fx) {
8820
8908
  steps.push({ step: "credential", target: route.route, action: "failed", detail: e.message });
8821
8909
  }
8822
8910
  }
8911
+ const covered = new Set([...o.routes, ...o.droppedRoutes ?? []].map((r) => `${hostFromUrl2(r.baseUrl)}/${splitRoute(r.route)[0]}`));
8912
+ try {
8913
+ for (const s of await fx.hostSessions(o.stateDir)) {
8914
+ const target = `${s.host}/${s.workspace}`;
8915
+ if (covered.has(target)) continue;
8916
+ try {
8917
+ if (!s.stored) {
8918
+ steps.push({ step: "credential", target, action: "absent", detail: "no host session in the keychain" });
8919
+ continue;
8920
+ }
8921
+ if (!o.dryRun) await s.clear();
8922
+ steps.push({ step: "credential", target, action: mark(o.dryRun), detail: "host session stored by a dry-run connect" });
8923
+ } catch (e) {
8924
+ steps.push({ step: "credential", target, action: "failed", detail: e.message });
8925
+ }
8926
+ }
8927
+ } catch (e) {
8928
+ steps.push({ step: "credential", target: "host sessions", action: "failed", detail: e.message });
8929
+ }
8823
8930
  if (o.removeCheckouts) {
8824
8931
  for (const dir of readClonedCheckouts(o.stateDir)) steps.push(removeCheckout(dir, o.dryRun, fx));
8825
8932
  }
8826
8933
  steps.push(...removeState(o.stateDir, o.dryRun));
8827
8934
  if (existsSync20(o.configFile)) {
8828
- if (!o.dryRun) rmSync10(o.configFile, { force: true });
8935
+ if (!o.dryRun) rmSync11(o.configFile, { force: true });
8829
8936
  steps.push({ step: "config", target: o.configFile, action: mark(o.dryRun) });
8830
8937
  } else {
8831
8938
  steps.push({ step: "config", target: o.configFile, action: "absent" });
@@ -8841,7 +8948,7 @@ function removeCheckout(dir, dryRun, fx) {
8841
8948
  if (linkedWorktrees.length) {
8842
8949
  return { step: "checkout", target: dir, action: "kept", detail: `has linked worktrees (${linkedWorktrees.join(", ")}); remove them first` };
8843
8950
  }
8844
- if (!dryRun) rmSync10(dir, { recursive: true, force: true });
8951
+ if (!dryRun) rmSync11(dir, { recursive: true, force: true });
8845
8952
  return { step: "checkout", target: dir, action: mark(dryRun) };
8846
8953
  }
8847
8954
  function removeState(stateDir, dryRun) {
@@ -8855,7 +8962,7 @@ function removeState(stateDir, dryRun) {
8855
8962
  }
8856
8963
  for (const t of targets) {
8857
8964
  if (!existsSync20(t)) continue;
8858
- if (!dryRun) rmSync10(t, { recursive: true, force: true });
8965
+ if (!dryRun) rmSync11(t, { recursive: true, force: true });
8859
8966
  out.push({ step: "state", target: t, action: mark(dryRun) });
8860
8967
  }
8861
8968
  if (!out.length) out.push({ step: "state", target: stateDir, action: "absent", detail: "nothing to remove" });
@@ -10519,7 +10626,9 @@ Wrote ${o.configPath}.
10519
10626
 
10520
10627
  // src/connect-json.ts
10521
10628
  import { accessSync as accessSync3, constants as constants3 } from "fs";
10522
- import { dirname as dirname11 } from "path";
10629
+ import { homedir as homedir7 } from "os";
10630
+ import { dirname as dirname11, join as join23 } from "path";
10631
+ import { delimiter as delimiter2 } from "path";
10523
10632
  var CHOICE_EXIT_CODE = 3;
10524
10633
  function createEmitter(json, write) {
10525
10634
  return {
@@ -10553,6 +10662,23 @@ function usesFlagAnswers(f, isTTY) {
10553
10662
  if (isTTY) return false;
10554
10663
  return !!(f.shipName || f.agentBin || f.extraPath || f.hostPassengers);
10555
10664
  }
10665
+ function agentBinSearchDirs(home, pathEnv, extraPath) {
10666
+ const dirs = [
10667
+ ...pathEnv.split(delimiter2),
10668
+ ...(extraPath ?? "").split(delimiter2),
10669
+ join23(home, ".local/bin"),
10670
+ join23(home, ".claude/local"),
10671
+ "/opt/homebrew/bin",
10672
+ "/usr/local/bin",
10673
+ join23(home, ".npm-global/bin"),
10674
+ join23(home, ".volta/bin")
10675
+ ];
10676
+ return [...new Set(dirs.filter(Boolean))];
10677
+ }
10678
+ function findAgentBin(name, env, extraPath, isExec) {
10679
+ const searched = agentBinSearchDirs(env.home ?? homedir7(), env.pathEnv, extraPath);
10680
+ return { bin: findOnPath2(name, searched.join(delimiter2), isExec), searched };
10681
+ }
10556
10682
  function flagWizardAnswers(f, env) {
10557
10683
  const isExec = env.isExecutable ?? ((p) => {
10558
10684
  try {
@@ -10562,9 +10688,9 @@ function flagWizardAnswers(f, env) {
10562
10688
  return false;
10563
10689
  }
10564
10690
  });
10565
- const agentBin = f.agentBin ?? findOnPath2("claude", env.pathEnv, isExec);
10566
- if (!agentBin) return { error: 'no agent binary: "claude" is not on PATH \u2014 pass --agent-bin /absolute/path' };
10567
10691
  if (f.agentBin && !isExec(f.agentBin)) return { error: `--agent-bin ${f.agentBin} is not an executable file` };
10692
+ const found = f.agentBin ? void 0 : findAgentBin("claude", env, f.extraPath, isExec);
10693
+ const agentBin = f.agentBin ?? found?.bin;
10568
10694
  const pnpmPath = findOnPath2("pnpm", env.pathEnv, isExec);
10569
10695
  const dockerPath = f.hostPassengers ? findOnPath2("docker", env.pathEnv, isExec) : void 0;
10570
10696
  const extraPath = f.extraPath ?? deriveExtraPath([dirname11(env.nodePath), pnpmPath ? dirname11(pnpmPath) : void 0, dockerPath ? dirname11(dockerPath) : void 0]);
@@ -10572,16 +10698,17 @@ function flagWizardAnswers(f, env) {
10572
10698
  answers: {
10573
10699
  name: f.shipName?.trim() || env.hostname,
10574
10700
  platform: env.platform,
10575
- agentBin,
10701
+ agentBin: agentBin ?? "claude",
10576
10702
  extraPath,
10577
10703
  hostPassengers: f.hostPassengers
10578
- }
10704
+ },
10705
+ ...agentBin ? {} : { agentBinSearched: found?.searched ?? [] }
10579
10706
  };
10580
10707
  }
10581
10708
 
10582
10709
  // src/release-run.ts
10583
- import { readFileSync as readFileSync18, writeFileSync as writeFileSync15, existsSync as existsSync22, copyFileSync, mkdirSync as mkdirSync13, mkdtempSync as mkdtempSync2, rmSync as rmSync11 } from "fs";
10584
- import { dirname as dirname12, join as join23 } from "path";
10710
+ import { readFileSync as readFileSync18, writeFileSync as writeFileSync15, existsSync as existsSync22, copyFileSync, mkdirSync as mkdirSync13, mkdtempSync as mkdtempSync2, rmSync as rmSync12 } from "fs";
10711
+ import { dirname as dirname12, join as join24 } from "path";
10585
10712
  import { tmpdir as tmpdir3 } from "os";
10586
10713
 
10587
10714
  // src/release.ts
@@ -11299,7 +11426,7 @@ async function bump(o, size) {
11299
11426
  }
11300
11427
  return printed;
11301
11428
  }
11302
- const files = o.repo.release.versionFiles.map((f) => join23(o.cwd, f));
11429
+ const files = o.repo.release.versionFiles.map((f) => join24(o.cwd, f));
11303
11430
  const first = files[0];
11304
11431
  if (!existsSync22(first)) {
11305
11432
  o.emit.error(`no ${o.repo.release.versionFiles[0]} to read a version from`);
@@ -11331,7 +11458,7 @@ async function bump(o, size) {
11331
11458
  function writeChangelog(o, version, entries) {
11332
11459
  const file = o.repo.release.changelog;
11333
11460
  if (!file || entries.length === 0) return;
11334
- const path = join23(o.cwd, file);
11461
+ const path = join24(o.cwd, file);
11335
11462
  const section = renderChangelogSection(version, (/* @__PURE__ */ new Date()).toISOString().slice(0, 10), entries);
11336
11463
  if (o.dryRun) {
11337
11464
  o.emit.emit(`would prepend ${entries.length} entr(ies) to ${file}`);
@@ -11421,7 +11548,7 @@ function closeReleaseCheckout(cwd, root, dir, branch, run2 = gitOk) {
11421
11548
  run2(cwd, ["worktree", "remove", "--force", dir]);
11422
11549
  } finally {
11423
11550
  try {
11424
- rmSync11(root, { recursive: true, force: true });
11551
+ rmSync12(root, { recursive: true, force: true });
11425
11552
  } finally {
11426
11553
  run2(cwd, ["worktree", "prune"]);
11427
11554
  run2(cwd, ["branch", "-D", branch]);
@@ -11431,8 +11558,8 @@ function closeReleaseCheckout(cwd, root, dir, branch, run2 = gitOk) {
11431
11558
  function openReleaseCheckout(o) {
11432
11559
  const { remote, base } = o.repo.branch;
11433
11560
  const branch = `crew/release-${Date.now()}`;
11434
- const root = mkdtempSync2(join23(tmpdir3(), "crew-release-"));
11435
- const dir = join23(root, "checkout");
11561
+ const root = mkdtempSync2(join24(tmpdir3(), "crew-release-"));
11562
+ const dir = join24(root, "checkout");
11436
11563
  o.emit.emit(`release checkout: ${dir}`, { step: "release" });
11437
11564
  const close = () => closeReleaseCheckout(o.cwd, root, dir, branch);
11438
11565
  try {
@@ -11442,10 +11569,10 @@ function openReleaseCheckout(o) {
11442
11569
  throw e;
11443
11570
  }
11444
11571
  for (const f of o.repo.worktrees.copy) {
11445
- const src = join23(o.cwd, f);
11572
+ const src = join24(o.cwd, f);
11446
11573
  if (!existsSync22(src)) continue;
11447
- mkdirSync13(dirname12(join23(dir, f)), { recursive: true });
11448
- copyFileSync(src, join23(dir, f));
11574
+ mkdirSync13(dirname12(join24(dir, f)), { recursive: true });
11575
+ copyFileSync(src, join24(dir, f));
11449
11576
  }
11450
11577
  return { dir, branch, close };
11451
11578
  }
@@ -12515,6 +12642,217 @@ function nextRoles(decision, ran) {
12515
12642
  return decision.selection.pending.filter((r) => !ran.includes(r)).sort((a, b) => (decision.selection.ranks[a] ?? 0) - (decision.selection.ranks[b] ?? 0));
12516
12643
  }
12517
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
+
12518
12856
  // src/connect.ts
12519
12857
  var ConnectHttpError = class extends Error {
12520
12858
  status;
@@ -12592,7 +12930,22 @@ async function provisionReleaseLocks(o, locksModelId, route, repoNames) {
12592
12930
  };
12593
12931
  return ensureLockRows(client2, locksModelId, scopes);
12594
12932
  }
12595
- var ISSUES_TEMPLATE_IDENTIFIER = "crew.issues";
12933
+ var ISSUES_TEMPLATE_IDENTIFIERS = ["tablation.issues", "crew.issues"];
12934
+ async function resolveIssuesTemplateKeys(o) {
12935
+ const keys = new Set(ISSUES_TEMPLATE_IDENTIFIERS);
12936
+ await Promise.all(ISSUES_TEMPLATE_IDENTIFIERS.map(async (identifier) => {
12937
+ try {
12938
+ const t = await get(
12939
+ o,
12940
+ `/library-templates/by-identifier/${encodeURIComponent(identifier)}`
12941
+ );
12942
+ if (t.id) keys.add(t.id);
12943
+ if (t.identifier) keys.add(t.identifier);
12944
+ } catch {
12945
+ }
12946
+ }));
12947
+ return keys;
12948
+ }
12596
12949
  async function listLibraryTemplates(o) {
12597
12950
  return get(o, "/library-templates");
12598
12951
  }
@@ -12633,6 +12986,7 @@ async function discover(o) {
12633
12986
  models: {},
12634
12987
  seats: {},
12635
12988
  holds: [],
12989
+ candidates: [],
12636
12990
  problems,
12637
12991
  provisioning
12638
12992
  };
@@ -12641,6 +12995,7 @@ async function discover(o) {
12641
12995
  out.role = me.role;
12642
12996
  out.meEmail = me.email;
12643
12997
  out.meId = me.id;
12998
+ out.meIdentityId = me.identityId;
12644
12999
  out.meName = me.name;
12645
13000
  } catch {
12646
13001
  }
@@ -12664,7 +13019,8 @@ async function discover(o) {
12664
13019
  }
12665
13020
  } else {
12666
13021
  const projects = await get(o, `/projects?workspaceId=${ws.id}`);
12667
- const defaultCandidate = projects.find((p) => p.sourceTemplateId === ISSUES_TEMPLATE_IDENTIFIER) ?? projects.find((p) => eq(p.slug ?? "", "issues"));
13022
+ const issuesKeys = await resolveIssuesTemplateKeys(o);
13023
+ const defaultCandidate = projects.find((p) => !!p.sourceTemplateId && issuesKeys.has(p.sourceTemplateId)) ?? projects.find((p) => eq(p.slug ?? "", "issues"));
12668
13024
  if (defaultCandidate && (await missingTables(o, defaultCandidate.id)).length === 0) {
12669
13025
  out.projectId = defaultCandidate.id;
12670
13026
  out.projectSlug = defaultCandidate.slug;
@@ -12802,10 +13158,16 @@ async function discover(o) {
12802
13158
  out.shipRow = { id: shipRowId, name: o.ship.name, action: "matched", matchedBy };
12803
13159
  const patchBody = {};
12804
13160
  if (matchedBy === "machine_id") {
12805
- const keptName = (mine[0].name ?? "").trim();
12806
- if (keptName !== o.ship.name.trim()) {
12807
- out.shipRow.name = keptName;
12808
- 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)" : ""}`);
12809
13171
  }
12810
13172
  patchBody.last_seen = (/* @__PURE__ */ new Date()).toISOString();
12811
13173
  }
@@ -12856,13 +13218,32 @@ async function discover(o) {
12856
13218
  o,
12857
13219
  `/data-models/${out.models.crew}/records?limit=200`
12858
13220
  );
12859
- const scoped = shipRowId ? rows.filter((r) => r.ship_id === shipRowId) : rows;
12860
- const unclaimed = shipRowId ? rows.filter((r) => !r.ship_id) : [];
13221
+ let seatShipId = shipRowId;
13222
+ if (!seatShipId && o.dryRun && o.ship && out.shipsModelId) {
13223
+ try {
13224
+ const shipRows = await get(
13225
+ o,
13226
+ `/data-models/${out.shipsModelId}/records?limit=200`
13227
+ );
13228
+ const byId = o.ship.machineId ? shipRows.filter((r) => r.machine_id === o.ship.machineId) : [];
13229
+ const mineRows = byId.length > 0 ? byId : shipRows.filter((r) => (r.name ?? "").trim() === o.ship.name.trim() && !r.machine_id);
13230
+ if (mineRows.length === 1) seatShipId = mineRows[0].id;
13231
+ } catch {
13232
+ }
13233
+ }
13234
+ const scoped = seatShipId ? rows.filter((r) => r.ship_id === seatShipId) : rows.filter((r) => !r.ship_id);
13235
+ const unclaimed = seatShipId ? rows.filter((r) => !r.ship_id) : [];
12861
13236
  for (const role of ["dev", "design", "qa", "triage"]) {
12862
13237
  const searchTerm = role === "dev" ? "develop" : role;
12863
13238
  let id = seatFor(scoped, searchTerm);
12864
13239
  let seatAction = "matched";
12865
- if (!id && shipRowId) {
13240
+ if (!id && seatShipId && !shipRowId) {
13241
+ const legacyId = seatFor(unclaimed, searchTerm);
13242
+ if (legacyId) {
13243
+ id = legacyId;
13244
+ seatAction = "claimed";
13245
+ }
13246
+ } else if (!id && shipRowId) {
12866
13247
  const legacyId = seatFor(unclaimed, searchTerm);
12867
13248
  if (legacyId) {
12868
13249
  await patch(o, `/data-models/${out.models.crew}/records/${legacyId}`, { ship_id: shipRowId });
@@ -12882,10 +13263,13 @@ async function discover(o) {
12882
13263
  }
12883
13264
  if (id) {
12884
13265
  out.seats[role] = id;
12885
- (out.seatRows ??= []).push({ role, id, action: seatAction });
13266
+ const rowShip = rows.find((r) => r.id === id)?.ship_id ?? shipRowId ?? void 0;
13267
+ (out.seatRows ??= []).push({ role, id, action: seatAction, ...rowShip ? { shipId: rowShip } : {} });
13268
+ } else if (o.dryRun && o.ship && out.shipsModelId) {
13269
+ (out.seatRows ??= []).push({ role, id: "", action: "would-create" });
12886
13270
  } else problems.push(`no Crew row looking like the ${role} seat \u2014 create one, or omit that role`);
12887
13271
  }
12888
- const pairId = seatFor(scoped, "pair") || (shipRowId ? seatFor(unclaimed, "pair") : void 0);
13272
+ const pairId = seatFor(scoped, "pair") || (seatShipId ? seatFor(unclaimed, "pair") : void 0);
12889
13273
  if (pairId) out.seats.pair = pairId;
12890
13274
  if (shipRowId) {
12891
13275
  const releaseId = seatFor(scoped, "release");
@@ -12903,25 +13287,56 @@ async function discover(o) {
12903
13287
  const holdRows = rows.filter((r) => !seatIds.has(r.id));
12904
13288
  out.holds = holdRows.map((r) => ({ id: r.id, name: r.name ?? "", email: r.email, user_id: r.user_id }));
12905
13289
  out.crewModelId = out.models.crew;
13290
+ const people = holdRows.filter((r) => !r.agent_id && (!r.ship_id || !!r.user_id));
13291
+ const members = /* @__PURE__ */ new Map();
13292
+ try {
13293
+ const list = await get(
13294
+ o,
13295
+ `/workspaces/${out.workspaceId}/members`
13296
+ );
13297
+ for (const m of Array.isArray(list) ? list : []) {
13298
+ const key = m.id ?? m.userId;
13299
+ if (key) members.set(key, { identityId: m.identityId, email: m.email });
13300
+ }
13301
+ } catch {
13302
+ }
13303
+ for (const h of out.holds) {
13304
+ const e = h.user_id ? members.get(h.user_id)?.email : void 0;
13305
+ if (e) h.memberEmail = e;
13306
+ }
13307
+ out.candidates = people.map((r) => ({
13308
+ id: r.id,
13309
+ name: r.name ?? "",
13310
+ email: r.email,
13311
+ user_id: r.user_id,
13312
+ detail: r.email || (r.user_id ? members.get(r.user_id)?.email : void 0) || void 0
13313
+ }));
13314
+ const isMine = (r) => !!r.user_id && (r.user_id === out.meId || !!out.meIdentityId && members.get(r.user_id)?.identityId === out.meIdentityId);
13315
+ const emailOf = (r) => [r.email, r.user_id ? members.get(r.user_id)?.email : void 0].filter((e) => !!e);
12906
13316
  if (!out.meId) {
12907
- if (out.holds.length === 0) problems.push("no non-seat Crew rows \u2014 which row is the operator?");
13317
+ if (out.candidates.length === 0) problems.push("no non-seat Crew rows \u2014 which row is the operator?");
12908
13318
  else {
12909
- const byEmail = out.meEmail ? holdRows.filter((r) => eq(r.email, out.meEmail)) : [];
13319
+ const byEmail = out.meEmail ? people.filter((r) => emailOf(r).some((e) => eq(e, out.meEmail))) : [];
12910
13320
  if (byEmail.length === 1) out.operator = byEmail[0].id;
12911
- else if (out.holds.length === 1) out.operator = out.holds[0].id;
13321
+ else if (out.candidates.length === 1) out.operator = out.candidates[0].id;
12912
13322
  }
12913
13323
  } else {
12914
- const candidates = holdRows.filter((r) => !r.user_id || r.user_id === out.meId);
12915
- const mine = holdRows.find((r) => r.user_id === out.meId);
12916
- const byEmail = out.meEmail ? candidates.filter((r) => eq(r.email, out.meEmail)) : [];
12917
- const only = holdRows.length === 1 && candidates.length === 1 ? candidates[0] : void 0;
13324
+ const candidates = people.filter((r) => !r.user_id || isMine(r));
13325
+ const mine = people.find(isMine);
13326
+ const byEmail = out.meEmail ? candidates.filter((r) => emailOf(r).some((e) => eq(e, out.meEmail))) : [];
13327
+ const unlinked = people.filter((r) => !r.user_id);
13328
+ const only = candidates.length === 1 && unlinked.length === 1 ? candidates[0] : void 0;
12918
13329
  const pick = mine ?? (byEmail.length === 1 ? byEmail[0] : only);
12919
13330
  if (pick) {
12920
13331
  out.operator = pick.id;
12921
- out.operatorAction = pick.user_id === out.meId ? "matched" : "linked";
12922
- if (pick.user_id !== out.meId && o.ship) await linkOperatorRow(o, out, pick);
12923
- } else if (o.ship && (!o.interactive || candidates.length === 0)) {
12924
- await createOperatorRow(o, out);
13332
+ out.operatorAction = isMine(pick) ? "matched" : "linked";
13333
+ if (!isMine(pick) && o.ship) await linkOperatorRow(o, out, pick);
13334
+ } else {
13335
+ out.operatorCompared = `signed in as ${out.meEmail ?? "unknown email"} (identity ${out.meIdentityId ?? out.meId}); ` + (people.length === 0 ? "no person rows in the Crew table" : people.map((r) => {
13336
+ const m = r.user_id ? members.get(r.user_id) : void 0;
13337
+ return `"${r.name || "unnamed row"}" <${r.email ?? "no email"}>` + (r.user_id ? ` linked to member ${m?.email ?? r.user_id}${m?.identityId ? ` (identity ${m.identityId})` : ""}` : " unlinked");
13338
+ }).join("; "));
13339
+ if (o.ship && (!o.interactive || candidates.length === 0)) await createOperatorRow(o, out);
12925
13340
  }
12926
13341
  }
12927
13342
  }
@@ -12962,6 +13377,7 @@ async function createOperatorRow(o, found) {
12962
13377
  found.operator = created.id;
12963
13378
  found.operatorAction = "created";
12964
13379
  found.holds.push({ id: created.id, name, email: found.meEmail, user_id: found.meId });
13380
+ found.candidates.push({ id: created.id, name, email: found.meEmail, user_id: found.meId, detail: found.meEmail });
12965
13381
  found.provisioning.push(`created Crew row "${name}" for you`);
12966
13382
  } catch (e) {
12967
13383
  if (!(e instanceof ConnectHttpError) || e.status !== 403) throw e;
@@ -12991,14 +13407,14 @@ ${opts.hostPassengers ? " hostPassengers: true\n" : ""}${apiKeyFileLine}${api
12991
13407
  // src/cli.ts
12992
13408
  import {
12993
13409
  getSessionStore as getSessionStore3,
12994
- hostFromUrl as hostFromUrl3,
13410
+ hostFromUrl as hostFromUrl4,
12995
13411
  loginWithDeviceCode,
12996
13412
  getOrCreateShipId,
12997
13413
  DeviceLoginError
12998
13414
  } from "@tablation/client";
12999
13415
 
13000
13416
  // src/seat-keys.ts
13001
- import { getSessionStore as getSessionStore2, hostFromUrl as hostFromUrl2 } from "@tablation/client";
13417
+ import { getSessionStore as getSessionStore2, hostFromUrl as hostFromUrl3 } from "@tablation/client";
13002
13418
  var SEAT_KEY_ROLES = ROLE_NAMES.filter((r) => r !== "pair");
13003
13419
  function keyIsAdmin(route) {
13004
13420
  const role = route.resolved?.role;
@@ -13022,7 +13438,7 @@ async function resolveReleaseKey(route, getStore = getSessionStore2) {
13022
13438
  const agentId = route.resolved?.releaseAgentId;
13023
13439
  if (!agentId) return ship(keyIsAdmin(route) ? "release seat has no linked Agents row (run `crew agents sync`)" : `release seat ${MEMBER_NO_ROW}`);
13024
13440
  try {
13025
- const session = await getStore().get(hostFromUrl2(route.baseUrl), slugFor(route, agentId));
13441
+ const session = await getStore().get(hostFromUrl3(route.baseUrl), slugFor(route, agentId));
13026
13442
  if (session?.apiKey) return { kind: "agent", apiKey: session.apiKey, agentId };
13027
13443
  return ship(keyIsAdmin(route) ? "no agent key stored for the release seat (run `crew agents sync`)" : MEMBER_NO_KEY);
13028
13444
  } catch {
@@ -13035,7 +13451,7 @@ async function resolveSeatKey(route, role, getStore = getSessionStore2) {
13035
13451
  const agentId = seatAgentId(route, role);
13036
13452
  if (!agentId) return ship(keyIsAdmin(route) ? "seat has no linked Agents row (run `crew agents sync`)" : `seat ${MEMBER_NO_ROW}`);
13037
13453
  try {
13038
- const session = await getStore().get(hostFromUrl2(route.baseUrl), slugFor(route, agentId));
13454
+ const session = await getStore().get(hostFromUrl3(route.baseUrl), slugFor(route, agentId));
13039
13455
  if (session?.apiKey) return { kind: "agent", apiKey: session.apiKey, agentId };
13040
13456
  return ship(keyIsAdmin(route) ? "no agent key stored for this seat (run `crew agents sync`)" : MEMBER_NO_KEY);
13041
13457
  } catch {
@@ -13068,7 +13484,7 @@ async function mintMissingSeatKeys(route, o) {
13068
13484
  detail: describeStoreError(e)
13069
13485
  }));
13070
13486
  }
13071
- const host = hostFromUrl2(route.baseUrl);
13487
+ const host = hostFromUrl3(route.baseUrl);
13072
13488
  const base = route.baseUrl.replace(/\/+$/, "");
13073
13489
  if (!o.dryRun) {
13074
13490
  const probeSlug = seatKeySlug(splitRoute(route.route)[0], "_probe");
@@ -13189,7 +13605,7 @@ async function revokeSeatKeys(route, o) {
13189
13605
  } catch (e) {
13190
13606
  return targets.map(({ role }) => ({ role, action: "failed", detail: describeStoreError(e) }));
13191
13607
  }
13192
- const host = hostFromUrl2(route.baseUrl);
13608
+ const host = hostFromUrl3(route.baseUrl);
13193
13609
  const base = route.baseUrl.replace(/\/+$/, "");
13194
13610
  for (const { role, agentId } of targets) {
13195
13611
  if (!agentId) {
@@ -13301,8 +13717,8 @@ function describeCapability(i) {
13301
13717
  }
13302
13718
 
13303
13719
  // src/agents-sync-run.ts
13304
- import { existsSync as existsSync24, mkdirSync as mkdirSync14, readFileSync as readFileSync19, writeFileSync as writeFileSync16 } from "fs";
13305
- 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";
13306
13722
  async function runAgentsSync(target, o) {
13307
13723
  const dryRun = o.dryRun ?? false;
13308
13724
  const force = o.force ?? false;
@@ -13335,12 +13751,12 @@ async function runAgentsSync(target, o) {
13335
13751
  for (const s of skillResult?.outcomes ?? []) o.say(describeSkillSyncOutcome(s));
13336
13752
  if (!dryRun) {
13337
13753
  const resolvedPath = resolvedPathFor(o.stateDir, target.route);
13338
- const raw = existsSync24(resolvedPath) ? JSON.parse(readFileSync19(resolvedPath, "utf8")) : {};
13754
+ const raw = existsSync26(resolvedPath) ? JSON.parse(readFileSync21(resolvedPath, "utf8")) : {};
13339
13755
  raw.agentPersonas = personaResult.agentPersonas;
13340
13756
  if (skillResult) raw.agentSkills = skillResult.agentSkills;
13341
13757
  if (personaResult.releaseAgentId) raw.releaseAgentId = personaResult.releaseAgentId;
13342
- mkdirSync14(dirname13(resolvedPath), { recursive: true });
13343
- writeFileSync16(resolvedPath, `${JSON.stringify(raw, null, 2)}
13758
+ mkdirSync16(dirname15(resolvedPath), { recursive: true });
13759
+ writeFileSync18(resolvedPath, `${JSON.stringify(raw, null, 2)}
13344
13760
  `);
13345
13761
  if (target.resolved) {
13346
13762
  target.resolved.agentPersonas = personaResult.agentPersonas;
@@ -13355,7 +13771,7 @@ async function runAgentsSync(target, o) {
13355
13771
  }
13356
13772
 
13357
13773
  // src/logbook.ts
13358
- import { createHash as createHash3 } from "crypto";
13774
+ import { createHash as createHash4 } from "crypto";
13359
13775
  import { TablationClient as TablationClient4 } from "@tablation/client";
13360
13776
  var LogbookError = class extends Error {
13361
13777
  };
@@ -13428,7 +13844,7 @@ async function showLogEntry(route, entryId, opts = {}) {
13428
13844
  };
13429
13845
  }
13430
13846
  const prompt = appendAdditions(match.prompt ?? "", match.prompt_additions);
13431
- const actualSha = createHash3("sha256").update(prompt).digest("hex");
13847
+ const actualSha = createHash4("sha256").update(prompt).digest("hex");
13432
13848
  const exact = !!entry.prompt_sha && actualSha === entry.prompt_sha;
13433
13849
  return {
13434
13850
  entry,
@@ -13443,12 +13859,12 @@ async function showLogEntry(route, entryId, opts = {}) {
13443
13859
  }
13444
13860
 
13445
13861
  // src/repos-hooks.ts
13446
- import { isMap as isMap2, parseDocument as parseDocument2, YAMLMap } from "yaml";
13862
+ import { isMap as isMap2, parseDocument as parseDocument3, YAMLMap } from "yaml";
13447
13863
 
13448
13864
  // src/repos-cmd.ts
13449
- import { homedir as homedir7 } from "os";
13450
- import { basename as basename8, join as join24, resolve as resolve3 } from "path";
13451
- import { isMap, isSeq, parseDocument, stringify as stringify2 } from "yaml";
13865
+ import { homedir as homedir8 } from "os";
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";
13452
13868
  var ReposError = class extends Error {
13453
13869
  };
13454
13870
  function normalizeRemote(url) {
@@ -13472,7 +13888,7 @@ function matchRepoName(origin, resolved) {
13472
13888
  return void 0;
13473
13889
  }
13474
13890
  function routeNamesOf(text) {
13475
- const routes = parseDocument(text).get("routes", true);
13891
+ const routes = parseDocument2(text).get("routes", true);
13476
13892
  if (!isSeq(routes)) return [];
13477
13893
  return routes.items.flatMap((i) => isMap(i) && typeof i.get("route") === "string" ? [i.get("route")] : []);
13478
13894
  }
@@ -13493,7 +13909,7 @@ function parseAddArgs(args, routes, isDir) {
13493
13909
  return { route, path: "." };
13494
13910
  }
13495
13911
  function expandDir(p, base) {
13496
- const home = p === "~" ? homedir7() : p.startsWith("~/") ? `${homedir7()}/${p.slice(2)}` : p;
13912
+ const home = p === "~" ? homedir8() : p.startsWith("~/") ? `${homedir8()}/${p.slice(2)}` : p;
13497
13913
  return resolve3(base, home);
13498
13914
  }
13499
13915
  function routeMapOf(doc, route) {
@@ -13507,7 +13923,7 @@ function routeMapOf(doc, route) {
13507
13923
  throw new ReposError(`no route "${route}" in the config (have: ${have})`);
13508
13924
  }
13509
13925
  function configuredRepos(text, route, base) {
13510
- const doc = parseDocument(text);
13926
+ const doc = parseDocument2(text);
13511
13927
  const m = routeMapOf(doc, route);
13512
13928
  const repos = m.get("repos");
13513
13929
  if (isMap(repos)) {
@@ -13522,7 +13938,7 @@ function configuredRepos(text, route, base) {
13522
13938
  return dir ? [{ name: basename8(String(dir)), dir: expandDir(String(dir), base) }] : [];
13523
13939
  }
13524
13940
  function routeSetsHooks(text, route, name) {
13525
- const m = routeMapOf(parseDocument(text), route);
13941
+ const m = routeMapOf(parseDocument2(text), route);
13526
13942
  const repos = m.get("repos", true);
13527
13943
  let target = m;
13528
13944
  if (isMap(repos)) target = repos.get(name, true);
@@ -13578,7 +13994,7 @@ function listRepoRows(i) {
13578
13994
  return rows;
13579
13995
  }
13580
13996
  function planRepoAdd(i) {
13581
- const doc = parseDocument(i.text);
13997
+ const doc = parseDocument2(i.text);
13582
13998
  if (doc.errors.length > 0) throw new ReposError(`config is not valid YAML \u2014 ${doc.errors[0].message}`);
13583
13999
  const m = routeMapOf(doc, i.route);
13584
14000
  const existing = configuredRepos(i.text, i.route, i.base);
@@ -13615,7 +14031,7 @@ function planRepoAdd(i) {
13615
14031
  return { text: String(doc), mode, name, block: renderBlock(m), warning, unmatched: !matched };
13616
14032
  }
13617
14033
  function planRepoRemove(text, base, route, path) {
13618
- const doc = parseDocument(text);
14034
+ const doc = parseDocument2(text);
13619
14035
  if (doc.errors.length > 0) throw new ReposError(`config is not valid YAML \u2014 ${doc.errors[0].message}`);
13620
14036
  const m = routeMapOf(doc, route);
13621
14037
  const target = expandDir(path, base);
@@ -13693,7 +14109,7 @@ function planRepoClone(i) {
13693
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\``
13694
14110
  );
13695
14111
  }
13696
- const doc = parseDocument(i.text);
14112
+ const doc = parseDocument2(i.text);
13697
14113
  const m = routeMapOf(doc, i.route);
13698
14114
  const existing = configuredRepos(i.text, i.route, i.base);
13699
14115
  if (existing.some((e) => e.name.toLowerCase() === name.toLowerCase())) {
@@ -13701,7 +14117,7 @@ function planRepoClone(i) {
13701
14117
  }
13702
14118
  const ship = doc.get("ship", true);
13703
14119
  const basePath = String(m.get("reposBasePath") ?? (isMap(ship) ? ship.get("reposBasePath") : void 0) ?? "~/Crew");
13704
- const target = i.dir ? expandDir(i.dir, i.base) : join24(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);
13705
14121
  const add = planRepoAdd({ text: i.text, base: i.base, route: i.route, path: target, origin: remote, resolved: i.resolved, name });
13706
14122
  return { ...add, remote, target };
13707
14123
  }
@@ -13847,7 +14263,7 @@ function repoDirOf(text, base, route, name) {
13847
14263
  return hit.dir;
13848
14264
  }
13849
14265
  function planRouteHooks(text, route, name, hooks) {
13850
- const doc = parseDocument2(text);
14266
+ const doc = parseDocument3(text);
13851
14267
  if (doc.errors.length > 0) throw new ReposError(`config is not valid YAML \u2014 ${doc.errors[0].message}`);
13852
14268
  const m = routeMapOf(doc, route);
13853
14269
  const repos = m.get("repos", true);
@@ -13909,7 +14325,7 @@ function existingRepoConfig(f) {
13909
14325
  }
13910
14326
 
13911
14327
  // src/repos-hooks-agent.ts
13912
- import { spawn as spawn4, execFileSync as execFileSync4 } from "child_process";
14328
+ import { spawn as spawn4, execFileSync as execFileSync5 } from "child_process";
13913
14329
  import { realpathSync as realpathSync2 } from "fs";
13914
14330
  import { isAbsolute as isAbsolute2, relative } from "path";
13915
14331
  var AgentSuggestError = class extends Error {
@@ -14012,7 +14428,7 @@ function classifyFailure(o) {
14012
14428
  }
14013
14429
  function porcelain(dir) {
14014
14430
  try {
14015
- 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" });
14016
14432
  } catch {
14017
14433
  return void 0;
14018
14434
  }
@@ -14081,7 +14497,7 @@ ${run2.err}` });
14081
14497
  }
14082
14498
 
14083
14499
  // src/sample-ticket.ts
14084
- import { basename as basename9, dirname as dirname15, join as join25 } from "path";
14500
+ import { basename as basename9, dirname as dirname17, join as join27 } from "path";
14085
14501
  var SAMPLE_REPO = "crew-sample";
14086
14502
  var SAMPLE_REMOTE = `local/${SAMPLE_REPO}`;
14087
14503
  var SAMPLE_MARKER = "sample: true";
@@ -14104,8 +14520,8 @@ has something safe to work on: its test and build hooks always pass, it has no
14104
14520
  remote, and \`crew sample-ticket --remove\` deletes it.
14105
14521
  `;
14106
14522
  function sampleDir(existing, reposBase, workspace) {
14107
- if (existing.length > 0) return join25(dirname15(existing[0]), SAMPLE_REPO);
14108
- return join25(reposBase, workspace, SAMPLE_REPO);
14523
+ if (existing.length > 0) return join27(dirname17(existing[0]), SAMPLE_REPO);
14524
+ return join27(reposBase, workspace, SAMPLE_REPO);
14109
14525
  }
14110
14526
  var SAMPLE_TITLE = "Sample: add a documented `greet` function with a test";
14111
14527
  function sampleDescription() {
@@ -14149,7 +14565,7 @@ function planSampleTicket(i) {
14149
14565
  }
14150
14566
 
14151
14567
  // src/doctor-fix.ts
14152
- import { parseDocument as parseDocument3 } from "yaml";
14568
+ import { parseDocument as parseDocument4 } from "yaml";
14153
14569
  var sound = (r) => r.hasRepos && r.problems.length === 0;
14154
14570
  function planFix(f) {
14155
14571
  const actions = [];
@@ -14176,7 +14592,7 @@ function routesInScope(routes, named) {
14176
14592
  return named ? routes.filter((r) => r.route === named) : routes;
14177
14593
  }
14178
14594
  function planEnable(text, route) {
14179
- const doc = parseDocument3(text);
14595
+ const doc = parseDocument4(text);
14180
14596
  routeMapOf(doc, route).set("enabled", true);
14181
14597
  return String(doc);
14182
14598
  }
@@ -14186,8 +14602,8 @@ function schedulerInstalled(plan, exists, crontab) {
14186
14602
  }
14187
14603
 
14188
14604
  // src/worktree-sweep.ts
14189
- import { existsSync as existsSync25, readFileSync as readFileSync20, readdirSync as readdirSync8 } from "fs";
14190
- import { basename as basename10, dirname as dirname16, join as join26, 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";
14191
14607
  function planWorktreeSweep(target, tickets, contract) {
14192
14608
  const terminal = new Set(contract.statuses.resolved.filter((s) => s !== contract.statuses.verified));
14193
14609
  const verified = contract.statuses.verified;
@@ -14241,7 +14657,7 @@ function worktreeTicketNumbers(target) {
14241
14657
  return [...nums];
14242
14658
  }
14243
14659
  function siblingWorktreeDirs(target) {
14244
- const parent = dirname16(target.dir.replace(/[/\\]+$/, ""));
14660
+ const parent = dirname18(target.dir.replace(/[/\\]+$/, ""));
14245
14661
  const stem = `${basename10(target.dir.replace(/[/\\]+$/, ""))}-`;
14246
14662
  const registered = new Set(worktrees(target.dir).map((w) => resolve4(w.path)));
14247
14663
  let names;
@@ -14254,7 +14670,7 @@ function siblingWorktreeDirs(target) {
14254
14670
  for (const name of names) {
14255
14671
  if (!name.startsWith(stem)) continue;
14256
14672
  const m = DIR_NUMBER_SUFFIXED.exec(name);
14257
- const path = join26(parent, name);
14673
+ const path = join28(parent, name);
14258
14674
  if (m && !registered.has(resolve4(path))) out.push({ path, number: m[1] });
14259
14675
  }
14260
14676
  return out;
@@ -14289,7 +14705,7 @@ async function applyWorktreeSweep(cwd, actions, dryRun, log, remote = "origin",
14289
14705
  let removed = 0;
14290
14706
  let keptBranches = 0;
14291
14707
  for (const a of actions) {
14292
- 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)) {
14293
14709
  if (gitOk(a.path, ["merge", "--abort"]) !== null) {
14294
14710
  log.emit(
14295
14711
  `abandoned the hand-back merge of ${base} in ${a.path}: ${a.ticket.issue_id} deployed without it`,
@@ -14297,7 +14713,7 @@ async function applyWorktreeSweep(cwd, actions, dryRun, log, remote = "origin",
14297
14713
  );
14298
14714
  }
14299
14715
  }
14300
- if (!dryRun && existsSync25(a.path)) {
14716
+ if (!dryRun && existsSync27(a.path)) {
14301
14717
  let dirty = false;
14302
14718
  try {
14303
14719
  dirty = status(a.path).length > 0;
@@ -14308,7 +14724,7 @@ async function applyWorktreeSweep(cwd, actions, dryRun, log, remote = "origin",
14308
14724
  continue;
14309
14725
  }
14310
14726
  }
14311
- 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)) {
14312
14728
  refuse(a, "unpushed commits", `${a.branch} has commits that are not on ${remote}`);
14313
14729
  continue;
14314
14730
  }
@@ -14357,7 +14773,7 @@ function isHandBackMerge(path, base) {
14357
14773
  if (!msgPath) return false;
14358
14774
  let first;
14359
14775
  try {
14360
- first = readFileSync20(resolve4(path, msgPath), "utf8").split("\n")[0] ?? "";
14776
+ first = readFileSync22(resolve4(path, msgPath), "utf8").split("\n")[0] ?? "";
14361
14777
  } catch {
14362
14778
  return false;
14363
14779
  }
@@ -14462,14 +14878,14 @@ function applyRemoteBranchCleanup(cwd, remote, branches2, dryRun, log) {
14462
14878
  }
14463
14879
 
14464
14880
  // src/stream-sweep.ts
14465
- import { existsSync as existsSync26, readdirSync as readdirSync9, rmSync as rmSync12, statSync as statSync8 } from "fs";
14466
- import { join as join27 } 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";
14467
14883
  var DAY_MS2 = 24 * 60 * 60 * 1e3;
14468
14884
  function planStreamSweep(stateDir, retentionDays, now = Date.now()) {
14469
- const dir = join27(stateDir, "streams");
14470
- if (!existsSync26(dir)) return [];
14885
+ const dir = join29(stateDir, "streams");
14886
+ if (!existsSync28(dir)) return [];
14471
14887
  const cutoff = now - retentionDays * DAY_MS2;
14472
- return readdirSync9(dir).map((name) => join27(dir, name)).filter((path) => {
14888
+ return readdirSync9(dir).map((name) => join29(dir, name)).filter((path) => {
14473
14889
  try {
14474
14890
  return statSync8(path).mtimeMs < cutoff;
14475
14891
  } catch {
@@ -14485,7 +14901,7 @@ function applyStreamSweep(paths, dryRun, log) {
14485
14901
  continue;
14486
14902
  }
14487
14903
  try {
14488
- rmSync12(path, { force: true });
14904
+ rmSync13(path, { force: true });
14489
14905
  removed++;
14490
14906
  log.emit(`removed stream artifact ${path}`, { step: "worktree" });
14491
14907
  } catch (e) {
@@ -14495,9 +14911,9 @@ function applyStreamSweep(paths, dryRun, log) {
14495
14911
  return removed;
14496
14912
  }
14497
14913
  function planEventsSweep(stateDir, retentionDays, now = Date.now()) {
14498
- if (!existsSync26(stateDir)) return [];
14914
+ if (!existsSync28(stateDir)) return [];
14499
14915
  const cutoff = now - retentionDays * DAY_MS2;
14500
- return readdirSync9(stateDir).filter((name) => ROTATED_EVENTS_RE.test(name)).map((name) => join27(stateDir, name)).filter((path) => {
14916
+ return readdirSync9(stateDir).filter((name) => ROTATED_EVENTS_RE.test(name)).map((name) => join29(stateDir, name)).filter((path) => {
14501
14917
  try {
14502
14918
  return statSync8(path).mtimeMs < cutoff;
14503
14919
  } catch {
@@ -14507,26 +14923,26 @@ function planEventsSweep(stateDir, retentionDays, now = Date.now()) {
14507
14923
  }
14508
14924
 
14509
14925
  // src/cli.ts
14510
- import { realpathSync as realpathSync3, readFileSync as readFileSync27, existsSync as existsSync33, mkdirSync as mkdirSync20, writeFileSync as writeFileSync21, rmSync as rmSync14, 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";
14511
14927
  import { createInterface } from "readline/promises";
14512
14928
  import { dirname as dirOf, resolve as resolvePath } from "path";
14513
14929
 
14514
14930
  // src/ssh-keys.ts
14515
- import { execFileSync as execFileSync5 } from "child_process";
14516
- import { existsSync as existsSync27, mkdirSync as mkdirSync15, readFileSync as readFileSync21, chmodSync } from "fs";
14517
- import { dirname as dirname17, join as join28 } 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";
14518
14934
  var SshKeyError = class extends Error {
14519
14935
  };
14520
14936
  function shipSshKeyPathFor(stateDir) {
14521
- return join28(stateDir, "ssh", "id_ed25519");
14937
+ return join30(stateDir, "ssh", "id_ed25519");
14522
14938
  }
14523
14939
  function ensureShipSshKeypair(stateDir) {
14524
14940
  const privateKeyPath = shipSshKeyPathFor(stateDir);
14525
14941
  const publicKeyPath = `${privateKeyPath}.pub`;
14526
- if (!existsSync27(privateKeyPath)) {
14527
- mkdirSync15(dirname17(privateKeyPath), { recursive: true });
14942
+ if (!existsSync29(privateKeyPath)) {
14943
+ mkdirSync17(dirname19(privateKeyPath), { recursive: true });
14528
14944
  try {
14529
- execFileSync5("ssh-keygen", [
14945
+ execFileSync6("ssh-keygen", [
14530
14946
  "-t",
14531
14947
  "ed25519",
14532
14948
  "-N",
@@ -14544,71 +14960,21 @@ function ensureShipSshKeypair(stateDir) {
14544
14960
  }
14545
14961
  chmodSync(privateKeyPath, 384);
14546
14962
  }
14547
- if (!existsSync27(publicKeyPath)) {
14963
+ if (!existsSync29(publicKeyPath)) {
14548
14964
  throw new SshKeyError(`${privateKeyPath} exists but ${publicKeyPath} does not \u2014 regenerate or restore it`);
14549
14965
  }
14550
- const publicKey = readFileSync21(publicKeyPath, "utf8").trim();
14966
+ const publicKey = readFileSync23(publicKeyPath, "utf8").trim();
14551
14967
  return { privateKeyPath, publicKeyPath, publicKey };
14552
14968
  }
14553
14969
  function sshKeygenAvailable() {
14554
14970
  try {
14555
- execFileSync5("ssh-keygen", ["-V"], { stdio: ["ignore", "ignore", "ignore"] });
14971
+ execFileSync6("ssh-keygen", ["-V"], { stdio: ["ignore", "ignore", "ignore"] });
14556
14972
  return true;
14557
14973
  } catch (e) {
14558
14974
  return e.code !== "ENOENT";
14559
14975
  }
14560
14976
  }
14561
14977
 
14562
- // src/machine-id.ts
14563
- import { execFileSync as execFileSync6 } from "child_process";
14564
- import { createHash as createHash4 } from "crypto";
14565
- import { existsSync as existsSync28, mkdirSync as mkdirSync16, readFileSync as readFileSync22, writeFileSync as writeFileSync17 } from "fs";
14566
- import { dirname as dirname18, join as join29 } from "path";
14567
- function machineIdPathFor(stateDir) {
14568
- return join29(stateDir, "machine-id");
14569
- }
14570
- var sha256 = (s) => createHash4("sha256").update(s).digest("hex");
14571
- function readRawMachineId(platform2 = process.platform) {
14572
- try {
14573
- if (platform2 === "darwin") {
14574
- const out = execFileSync6("ioreg", ["-rd1", "-c", "IOPlatformExpertDevice"], {
14575
- encoding: "utf8",
14576
- stdio: ["ignore", "pipe", "ignore"]
14577
- });
14578
- return /"IOPlatformUUID"\s*=\s*"([^"]+)"/.exec(out)?.[1];
14579
- }
14580
- for (const p of ["/etc/machine-id", "/var/lib/dbus/machine-id"]) {
14581
- if (existsSync28(p)) {
14582
- const id = readFileSync22(p, "utf8").trim();
14583
- if (id) return id;
14584
- }
14585
- }
14586
- } catch {
14587
- }
14588
- return void 0;
14589
- }
14590
- function ensureMachineId(stateDir, opts = {}) {
14591
- if (opts.override?.trim()) return sha256(opts.override.trim());
14592
- const path = machineIdPathFor(stateDir);
14593
- if (existsSync28(path)) {
14594
- const cached = readFileSync22(path, "utf8").trim();
14595
- if (/^[0-9a-f]{64}$/.test(cached)) return cached;
14596
- }
14597
- const raw = (opts.read ?? readRawMachineId)();
14598
- if (!raw) return void 0;
14599
- const hashed = sha256(raw);
14600
- mkdirSync16(dirname18(path), { recursive: true });
14601
- writeFileSync17(path, `${hashed}
14602
- `, { mode: 384 });
14603
- return hashed;
14604
- }
14605
- function matchShipRows(ships, shipName2, machineId) {
14606
- const hasColumn = ships.some((r) => "machine_id" in r);
14607
- const byId = hasColumn && machineId ? ships.filter((r) => r.machine_id === machineId) : [];
14608
- if (byId.length > 0) return { rows: byId, by: "machine_id", hasColumn };
14609
- return { rows: ships.filter((r) => (r.name ?? "").trim() === shipName2.trim()), by: "name", hasColumn };
14610
- }
14611
-
14612
14978
  // src/passenger-containers.ts
14613
14979
  import { execFileSync as execFileSync7 } from "child_process";
14614
14980
  import { createHash as createHash5 } from "crypto";
@@ -14813,9 +15179,9 @@ function syncAllPassengerCheckouts(cfg) {
14813
15179
 
14814
15180
  // src/tunnel.ts
14815
15181
  import { spawn as spawn5 } from "child_process";
14816
- 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";
14817
15183
  import { randomInt } from "crypto";
14818
- import { join as join30 } from "path";
15184
+ import { join as join31 } from "path";
14819
15185
  var DEFAULT_SPAWN_OPTIONS = { stdio: "ignore", detached: true };
14820
15186
  var defaultSpawn = (cmd, args) => spawn5(cmd, args, DEFAULT_SPAWN_OPTIONS);
14821
15187
  var SLUG_ALPHABET = "abcdefghijklmnopqrstuvwxyz0123456789";
@@ -14882,21 +15248,21 @@ function startTunnel(opts) {
14882
15248
  };
14883
15249
  }
14884
15250
  function tunnelStatePath(stateDir, workspaceId) {
14885
- return join30(stateDir, "passengers", `${workspaceId}.json`);
15251
+ return join31(stateDir, "passengers", `${workspaceId}.json`);
14886
15252
  }
14887
15253
  function readPersistedTunnel(stateDir, workspaceId) {
14888
15254
  const path = tunnelStatePath(stateDir, workspaceId);
14889
- if (!existsSync29(path)) return void 0;
15255
+ if (!existsSync30(path)) return void 0;
14890
15256
  try {
14891
- return JSON.parse(readFileSync23(path, "utf8"));
15257
+ return JSON.parse(readFileSync24(path, "utf8"));
14892
15258
  } catch {
14893
15259
  return void 0;
14894
15260
  }
14895
15261
  }
14896
15262
  function writePersistedTunnel(stateDir, state) {
14897
15263
  const path = tunnelStatePath(stateDir, state.workspaceId);
14898
- mkdirSync17(join30(stateDir, "passengers"), { recursive: true });
14899
- writeFileSync18(path, `${JSON.stringify(state, null, 2)}
15264
+ mkdirSync18(join31(stateDir, "passengers"), { recursive: true });
15265
+ writeFileSync19(path, `${JSON.stringify(state, null, 2)}
14900
15266
  `);
14901
15267
  }
14902
15268
  function updatePersistedTunnel(stateDir, workspaceId, patch2) {
@@ -14993,8 +15359,8 @@ function syncPassengerTunnels(plans, relayHost, relayPort, privateKeyPath, state
14993
15359
  }
14994
15360
  }
14995
15361
  function readAllPersistedWorkspaceIds(stateDir) {
14996
- const dir = join30(stateDir, "passengers");
14997
- if (!existsSync29(dir)) return [];
15362
+ const dir = join31(stateDir, "passengers");
15363
+ if (!existsSync30(dir)) return [];
14998
15364
  return readdirSync10(dir).filter((f) => f.endsWith(".json") && !f.endsWith("-sync.json")).map((f) => f.slice(0, -".json".length));
14999
15365
  }
15000
15366
 
@@ -15115,8 +15481,8 @@ async function unregisterEndpoint(workspaceId, info, d) {
15115
15481
  import { createServer as createHttpServer } from "http";
15116
15482
  import { spawn as spawn6 } from "child_process";
15117
15483
  import { randomBytes } from "crypto";
15118
- import { existsSync as existsSync30, mkdirSync as mkdirSync18, readFileSync as readFileSync24, writeFileSync as writeFileSync19, unlinkSync as unlinkSync4, readdirSync as readdirSync11 } from "fs";
15119
- import { join as join31 } from "path";
15484
+ import { existsSync as existsSync31, mkdirSync as mkdirSync19, readFileSync as readFileSync25, writeFileSync as writeFileSync20, unlinkSync as unlinkSync4, readdirSync as readdirSync11 } from "fs";
15485
+ import { join as join32 } from "path";
15120
15486
 
15121
15487
  // src/secret-compare.ts
15122
15488
  import { timingSafeEqual } from "crypto";
@@ -15205,21 +15571,21 @@ var defaultSpawn2 = (cmd, args, env) => {
15205
15571
  return child;
15206
15572
  };
15207
15573
  function syncDaemonStatePath(stateDir, workspaceId) {
15208
- return join31(stateDir, "passengers", `${workspaceId}-sync.json`);
15574
+ return join32(stateDir, "passengers", `${workspaceId}-sync.json`);
15209
15575
  }
15210
15576
  function readPersistedSyncDaemon(stateDir, workspaceId) {
15211
15577
  const path = syncDaemonStatePath(stateDir, workspaceId);
15212
- if (!existsSync30(path)) return void 0;
15578
+ if (!existsSync31(path)) return void 0;
15213
15579
  try {
15214
- return JSON.parse(readFileSync24(path, "utf8"));
15580
+ return JSON.parse(readFileSync25(path, "utf8"));
15215
15581
  } catch {
15216
15582
  return void 0;
15217
15583
  }
15218
15584
  }
15219
15585
  function writePersistedSyncDaemon(stateDir, state) {
15220
15586
  const path = syncDaemonStatePath(stateDir, state.workspaceId);
15221
- mkdirSync18(join31(stateDir, "passengers"), { recursive: true });
15222
- writeFileSync19(path, `${JSON.stringify(state, null, 2)}
15587
+ mkdirSync19(join32(stateDir, "passengers"), { recursive: true });
15588
+ writeFileSync20(path, `${JSON.stringify(state, null, 2)}
15223
15589
  `);
15224
15590
  }
15225
15591
  function removePersistedSyncDaemon(stateDir, workspaceId) {
@@ -15239,8 +15605,8 @@ function waitForExit(pid, alive, timeoutMs = 5e3) {
15239
15605
  }
15240
15606
  }
15241
15607
  function readAllPersistedSyncWorkspaceIds(stateDir) {
15242
- const dir = join31(stateDir, "passengers");
15243
- if (!existsSync30(dir)) return [];
15608
+ const dir = join32(stateDir, "passengers");
15609
+ if (!existsSync31(dir)) return [];
15244
15610
  const suffix = "-sync.json";
15245
15611
  return readdirSync11(dir).filter((f) => f.endsWith(suffix)).map((f) => f.slice(0, -suffix.length));
15246
15612
  }
@@ -15328,12 +15694,12 @@ function describeCause(err) {
15328
15694
  }
15329
15695
 
15330
15696
  // src/daemon.ts
15331
- import { existsSync as existsSync32, mkdirSync as mkdirSync19, readFileSync as readFileSync26, readdirSync as readdirSync13, rmSync as rmSync13, statSync as statSync9, writeFileSync as writeFileSync20 } from "fs";
15332
- import { join as join33 } from "path";
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";
15698
+ import { join as join34 } from "path";
15333
15699
 
15334
15700
  // src/interrupted.ts
15335
- import { existsSync as existsSync31, readdirSync as readdirSync12, readFileSync as readFileSync25 } from "fs";
15336
- import { join as join32 } from "path";
15701
+ import { existsSync as existsSync32, readdirSync as readdirSync12, readFileSync as readFileSync26 } from "fs";
15702
+ import { join as join33 } from "path";
15337
15703
  var INTERRUPTED_MARKER = calloutHeader("NOTE", "Interrupted");
15338
15704
  function interruptedComment(b) {
15339
15705
  const tree = b.dirty === null ? "The worktree could not be checked." : b.dirty ? "The worktree has uncommitted changes \u2014 they are this seat's own, from that run." : "The worktree is clean.";
@@ -15352,21 +15718,21 @@ var pidAlive = (pid) => {
15352
15718
  };
15353
15719
  function runFinished(streamPath) {
15354
15720
  const path = `${streamPath}.meta.json`;
15355
- if (!existsSync31(path)) return false;
15721
+ if (!existsSync32(path)) return false;
15356
15722
  try {
15357
- return JSON.parse(readFileSync25(path, "utf8")).running !== true;
15723
+ return JSON.parse(readFileSync26(path, "utf8")).running !== true;
15358
15724
  } catch {
15359
15725
  return true;
15360
15726
  }
15361
15727
  }
15362
15728
  function orphanedStreams(o) {
15363
- const dir = join32(o.stateDir, "streams");
15364
- if (!existsSync31(dir)) return [];
15729
+ const dir = join33(o.stateDir, "streams");
15730
+ if (!existsSync32(dir)) return [];
15365
15731
  const ownerAlive = o.isOwnerAlive ?? pidAlive;
15366
15732
  const out = [];
15367
15733
  for (const name of readdirSync12(dir)) {
15368
15734
  if (!name.endsWith(".jsonl") || name.endsWith(".events.jsonl")) continue;
15369
- const streamPath = join32(dir, name);
15735
+ const streamPath = join33(dir, name);
15370
15736
  if (runFinished(streamPath)) continue;
15371
15737
  const sidecar = readRunSidecar(streamPath);
15372
15738
  if (!sidecar || sidecar.route !== o.routeLabel) continue;
@@ -15486,7 +15852,7 @@ function latestMtime(path) {
15486
15852
  if (!stat.isDirectory()) return 0;
15487
15853
  let latest = stat.mtimeMs;
15488
15854
  for (const entry of readdirSync13(path, { withFileTypes: true })) {
15489
- latest = Math.max(latest, latestMtime(join33(path, entry.name)));
15855
+ latest = Math.max(latest, latestMtime(join34(path, entry.name)));
15490
15856
  }
15491
15857
  return latest;
15492
15858
  }
@@ -15495,23 +15861,23 @@ function computeTreeSignature(paths) {
15495
15861
  }
15496
15862
  function makeStaleChecker(paths, opts = {}) {
15497
15863
  const initial = computeTreeSignature(paths);
15498
- return () => computeTreeSignature(paths) !== initial || opts.reloadSentinel !== void 0 && existsSync32(opts.reloadSentinel);
15864
+ return () => computeTreeSignature(paths) !== initial || opts.reloadSentinel !== void 0 && existsSync33(opts.reloadSentinel);
15499
15865
  }
15500
15866
  function reloadSentinelFor(stateDir) {
15501
- return join33(stateDir, "reload-requested");
15867
+ return join34(stateDir, "reload-requested");
15502
15868
  }
15503
15869
  function daemonStampFor(stateDir) {
15504
- return join33(stateDir, "daemon.started");
15870
+ return join34(stateDir, "daemon.started");
15505
15871
  }
15506
15872
  function noteDaemonStarted(stateDir, now = Date.now()) {
15507
- mkdirSync19(stateDir, { recursive: true });
15508
- rmSync13(reloadSentinelFor(stateDir), { force: true });
15509
- writeFileSync20(daemonStampFor(stateDir), `${JSON.stringify({ pid: process.pid, startedAt: now })}
15873
+ mkdirSync20(stateDir, { recursive: true });
15874
+ rmSync14(reloadSentinelFor(stateDir), { force: true });
15875
+ writeFileSync21(daemonStampFor(stateDir), `${JSON.stringify({ pid: process.pid, startedAt: now })}
15510
15876
  `);
15511
15877
  }
15512
15878
  function readDaemonStarted(stateDir) {
15513
15879
  try {
15514
- const v = JSON.parse(readFileSync26(daemonStampFor(stateDir), "utf8")).startedAt;
15880
+ const v = JSON.parse(readFileSync27(daemonStampFor(stateDir), "utf8")).startedAt;
15515
15881
  return typeof v === "number" ? v : null;
15516
15882
  } catch {
15517
15883
  return null;
@@ -15574,6 +15940,7 @@ async function runOnePassUnguarded(o) {
15574
15940
  const spawn7 = o.spawnRoleAgent ?? runRoleAgent;
15575
15941
  const breaker = o.spawnBreaker ?? new SpawnBreaker();
15576
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 });
15577
15944
  try {
15578
15945
  await (o.reconcile ?? reconcileForRoute)(ctx, o);
15579
15946
  } catch (e) {
@@ -15896,7 +16263,7 @@ async function buildEnvironment(route, ship, ticket) {
15896
16263
  }
15897
16264
 
15898
16265
  // src/cli.ts
15899
- 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)), "..");
15900
16267
  var RELEASE_LOCK_TTL_MS = 20 * 60 * 1e3;
15901
16268
  var CYCLE_SECONDS2 = 120;
15902
16269
  var STALE_CYCLES = 3;
@@ -15940,7 +16307,7 @@ async function pickOperator(holds2, me) {
15940
16307
  const choices = holds2.filter((h) => !h.user_id || h.user_id === me.id);
15941
16308
  const canCreate = !!me.id;
15942
16309
  process.stderr.write("\nWhich of these Crew rows is you?\n");
15943
- choices.forEach((h, i) => process.stderr.write(` ${i + 1}. ${h.name || h.id}
16310
+ choices.forEach((h, i) => process.stderr.write(` ${i + 1}. ${h.name || "unnamed row"}${h.detail ? ` <${h.detail}>` : ""}
15944
16311
  `));
15945
16312
  if (canCreate) process.stderr.write(` 0. Create a new Crew row for ${me.name ?? ""} ${me.email ?? ""}`.trimEnd() + "\n");
15946
16313
  const rl = createInterface({ input: process.stdin, output: process.stderr });
@@ -15974,10 +16341,11 @@ async function pickResolvedStatuses(unrecognized) {
15974
16341
  }
15975
16342
  async function offerTemplateInstall(authOpts, workspaceId, decided) {
15976
16343
  const templates = await listLibraryTemplates(authOpts);
15977
- const template = templates.find((t) => t.identifier === ISSUES_TEMPLATE_IDENTIFIER);
16344
+ const keys = await resolveIssuesTemplateKeys(authOpts);
16345
+ const template = templates.find((t) => t.identifier === ISSUES_TEMPLATE_IDENTIFIERS[0]) ?? templates.find((t) => keys.has(t.identifier) || keys.has(t.id));
15978
16346
  if (!template) {
15979
16347
  process.stderr.write(`
15980
- No template with identifier "${ISSUES_TEMPLATE_IDENTIFIER}" is published to the Library yet \u2014 nothing to offer to install.
16348
+ No template with identifier "${ISSUES_TEMPLATE_IDENTIFIERS[0]}" is published to the Library yet \u2014 nothing to offer to install.
15981
16349
  `);
15982
16350
  return void 0;
15983
16351
  }
@@ -16015,8 +16383,8 @@ No project here has what a route needs. Install the "${template.name}" library t
16015
16383
  `);
16016
16384
  return installed.project.slug;
16017
16385
  }
16018
- function usage() {
16019
- process.stderr.write(`crew \u2014 a standing team of headless agents
16386
+ function usage(problem, requested = false) {
16387
+ const text = `crew \u2014 a standing team of headless agents
16020
16388
 
16021
16389
  crew poll [route] decide a cycle and report it; writes nothing
16022
16390
  crew run [route] [--role R] run the winning role's session
@@ -16044,12 +16412,14 @@ function usage() {
16044
16412
  resume with no argument clears only the whole-crew pause; a role or release pause needs its own resume
16045
16413
  crew log [route] tail the log
16046
16414
  crew inbox [--member NAME] your tickets across every workspace (or a colleague's)
16415
+ crew connect --sign-out [--host H] [--workspace W] [--json] clear the keychain session a dry-run connect stored (no crew.yaml needed; nothing to remove still exits 0)
16047
16416
  crew connect resolve a workspace's ids into a crew.yaml block, then (admin key) sync personas/skills and mint seat keys (--no-agents-sync skips that; --no-mcp skips registering the Tablation MCP server with Claude Code; --mcp replace overwrites a different one; --dry-run works with no crew.yaml and writes none: no file, no rows, no seat keys \u2014 the device approval and workspace credential behave as in a real run; with no workspace named it first reuses the keychain session a previous dry-run stored for the host, falling back to device approval when that is missing or rejected, and the signed_in event's session field says which)
16048
16417
  crew disconnect remove what connect installed for Pair (Claude MCP; Codex/Gemini instructions block and MCP entry per ship.pair)
16049
16418
  crew disconnect --reset [--remove-checkouts] [--dry-run] [--json] remove everything connect and install wrote on this machine: scheduler units, Pair/MCP entries, seat keys, keychain credential, resolved state, then crew.yaml (--remove-checkouts also deletes checkouts crew cloned, never one with local changes)
16050
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)
16051
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)
16052
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
16053
16423
  crew repos add [ROUTE] [NAME] [PATH] attach a local checkout to a route in crew.yaml (--dry-run previews)
16054
16424
  crew repos clone [ROUTE] NAME clone a Repos row's remote into the route directory and attach it (--dir PATH, --dry-run, --json)
16055
16425
  crew repos hooks [ROUTE] NAME suggest test/build hooks for a checkout (--agent, --write route|repo, --recipes, --json)
@@ -16078,10 +16448,18 @@ Options:
16078
16448
  --all (inbox) include closed tickets
16079
16449
 
16080
16450
  In watch: space pauses the stream, q quits.
16081
- `);
16451
+ `;
16452
+ if (requested) {
16453
+ process.stdout.write(text);
16454
+ process.exit(0);
16455
+ }
16456
+ process.stderr.write(problem ? `crew: ${problem}
16457
+
16458
+ ${text}` : text);
16082
16459
  process.exit(2);
16083
16460
  }
16084
16461
  var argv = process.argv.slice(2);
16462
+ if (helpRequest(argv)) usage(void 0, true);
16085
16463
  var versionReq = versionRequest(argv);
16086
16464
  if (versionReq) {
16087
16465
  const version = crewVersion(CREW_HOME);
@@ -16101,7 +16479,46 @@ var connectEvents = createEmitter(command === "connect" && flag("json"), (l) =>
16101
16479
  process.stdout.write(l);
16102
16480
  });
16103
16481
  var connectChoices = parseChoices(argv);
16104
- if (!command) usage();
16482
+ if (!command) usage(argv.length ? `unknown option "${argv[0]}"` : void 0);
16483
+ var KNOWN_COMMANDS = /* @__PURE__ */ new Set([
16484
+ "passenger-sync-daemon",
16485
+ "agents",
16486
+ "connect",
16487
+ "daemon",
16488
+ "deploy",
16489
+ "disconnect",
16490
+ "doctor",
16491
+ "drop",
16492
+ "inbox",
16493
+ "install",
16494
+ "log",
16495
+ "logbook",
16496
+ "merge",
16497
+ "passengers",
16498
+ "pause",
16499
+ "poll",
16500
+ "ports",
16501
+ "projects",
16502
+ "reap",
16503
+ "release",
16504
+ "reload",
16505
+ "repos",
16506
+ "resume",
16507
+ "rotate-passenger-url",
16508
+ "run",
16509
+ "sample-ticket",
16510
+ "selftest",
16511
+ "ship",
16512
+ "skills",
16513
+ "status",
16514
+ "sync",
16515
+ "unassign",
16516
+ "uninstall",
16517
+ "watch",
16518
+ "whoami",
16519
+ "worktree"
16520
+ ]);
16521
+ if (!KNOWN_COMMANDS.has(command)) usage(`unknown command "${command}"`);
16105
16522
  function findSyncRoute(c, name) {
16106
16523
  if (!name && c.routes.length > 1) {
16107
16524
  throw new ConfigError(`this ship has ${c.routes.length} routes \u2014 name one: ${c.routes.map((r) => r.route).join(", ")}`);
@@ -16191,6 +16608,7 @@ if (command === "passenger-sync-daemon") {
16191
16608
  let cfg;
16192
16609
  let firstRun;
16193
16610
  let firstRunFromFlags = false;
16611
+ let agentBinSearched;
16194
16612
  try {
16195
16613
  const connectIsTTY = !!(process.stdin.isTTY && process.stdout.isTTY);
16196
16614
  const wizardFlags = {
@@ -16200,7 +16618,7 @@ if (command === "passenger-sync-daemon") {
16200
16618
  hostPassengers: flag("host-passengers"),
16201
16619
  noWizard: flag("no-wizard")
16202
16620
  };
16203
- if (command === "connect" && usesFlagAnswers(wizardFlags, connectIsTTY) && shouldRunWizard({ isTTY: true, crewHome: CREW_HOME })) {
16621
+ if (command === "connect" && !flag("sign-out") && usesFlagAnswers(wizardFlags, connectIsTTY) && shouldRunWizard({ isTTY: true, crewHome: CREW_HOME })) {
16204
16622
  const flagged = flagWizardAnswers(wizardFlags, {
16205
16623
  hostname: hostname2(),
16206
16624
  platform: hostPlatform(),
@@ -16217,8 +16635,9 @@ if (command === "passenger-sync-daemon") {
16217
16635
  const configPath = firstRunConfigPath();
16218
16636
  firstRun = { answers: flagged.answers, configPath };
16219
16637
  firstRunFromFlags = true;
16638
+ agentBinSearched = flagged.agentBinSearched;
16220
16639
  cfg = loadConfig(CREW_HOME, configPath, { text: renderShipBlock(flagged.answers), allowNoRoutes: true });
16221
- } else if (command === "connect" && shouldRunWizard({
16640
+ } else if (command === "connect" && !flag("sign-out") && shouldRunWizard({
16222
16641
  isTTY: connectIsTTY,
16223
16642
  crewHome: CREW_HOME
16224
16643
  })) {
@@ -16239,7 +16658,13 @@ if (command === "passenger-sync-daemon") {
16239
16658
  firstRun = { answers, configPath };
16240
16659
  cfg = loadConfig(CREW_HOME, configPath, { text: renderShipBlock(answers), allowNoRoutes: true });
16241
16660
  } else {
16242
- cfg = loadConfig(CREW_HOME, void 0, command === "repos" || command === "disconnect" && flag("reset") ? { allowNoRoutes: true } : {});
16661
+ const toleratesNoRoutes = command === "repos" || command === "ship" || command === "disconnect" && flag("reset") || command === "connect" && flag("sign-out");
16662
+ const configless = command === "connect" && flag("sign-out") || command === "disconnect" && flag("reset");
16663
+ if (configless && !existsSync34(findConfigFile(CREW_HOME))) {
16664
+ cfg = loadConfig(CREW_HOME, firstRunConfigPath(), { text: "ship: {}\n", allowNoRoutes: true });
16665
+ } else {
16666
+ cfg = loadConfig(CREW_HOME, void 0, toleratesNoRoutes ? { allowNoRoutes: true } : {});
16667
+ }
16243
16668
  }
16244
16669
  } catch (e) {
16245
16670
  if (e instanceof ConfigError) {
@@ -16257,7 +16682,7 @@ if (command === "passenger-sync-daemon") {
16257
16682
  const DAEMON_SUBCOMMANDS = /* @__PURE__ */ new Set(["start", "stop", "status", "restart"]);
16258
16683
  const worktreeArgs = command === "worktree" ? splitWorktreeArgs(positional, cfg.routes.map((r) => r.route)) : void 0;
16259
16684
  const named = worktreeArgs ? worktreeArgs.route : command === "daemon" && DAEMON_SUBCOMMANDS.has(positional[1] ?? "") ? positional[2] : positional[1];
16260
- 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;
16261
16686
  let route;
16262
16687
  try {
16263
16688
  route = fleetWide ? cfg.routes[0] : findRoute(cfg, named);
@@ -16473,8 +16898,8 @@ if (command === "passenger-sync-daemon") {
16473
16898
  }
16474
16899
  if (host === "linux" && hasSystemd) {
16475
16900
  const label = labelFor(CREW_HOME, "run");
16476
- const staleTimer = join34(homedir8(), ".config", "systemd", "user", `${label}.timer`);
16477
- if (existsSync33(staleTimer)) {
16901
+ const staleTimer = join35(homedir9(), ".config", "systemd", "user", `${label}.timer`);
16902
+ if (existsSync34(staleTimer)) {
16478
16903
  process.stdout.write(
16479
16904
  `removing stale run timer from before the persistent-service cutover: ${staleTimer}${dryRun ? " (dry run)" : ""}
16480
16905
  `
@@ -16500,7 +16925,7 @@ if (command === "passenger-sync-daemon") {
16500
16925
  const tracker = new Tracker(c, cfg.ship);
16501
16926
  problems.push(...validateContract(tracker.contract).map((p) => `contract: ${p}`));
16502
16927
  for (const r of await resolvedRepos(c)) {
16503
- 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`);
16504
16929
  if (!satisfies(host, r.config.platform)) problems.push(`repo ${r.name}: requires ${r.config.platform}, this host is ${host}`);
16505
16930
  problems.push(...validateEffective(r.config).map((p) => `repo ${r.name}: ${p}`));
16506
16931
  }
@@ -16518,7 +16943,7 @@ if (command === "passenger-sync-daemon") {
16518
16943
  }
16519
16944
  async function doctorFix(o) {
16520
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))));
16521
- const unitExists = (p) => existsSync33(p);
16946
+ const unitExists = (p) => existsSync34(p);
16522
16947
  const crontab = o.host === "linux" ? spawnSync6("crontab", ["-l"], { encoding: "utf8" }).stdout ?? "" : "";
16523
16948
  const schedulerAbsent = o.host !== "windows" ? !schedulerInstalled(planInstall(cfg.ship, CREW_HOME, o.host, detectSystemd(), "run"), unitExists, crontab) : true;
16524
16949
  const plan = planFix({ routes, schedulerAbsent, host: o.host });
@@ -16535,7 +16960,7 @@ if (command === "passenger-sync-daemon") {
16535
16960
  for (const a of plan.actions) {
16536
16961
  if (a.kind === "enable") {
16537
16962
  if (prompter && !await prompter.confirm(`Route ${a.route} passes every check \u2014 set enabled: true in ${cfg.configFile}?`, false)) continue;
16538
- writeFileSync21(cfg.configFile, planEnable(readFileSync27(cfg.configFile, "utf8"), a.route));
16963
+ writeFileSync22(cfg.configFile, planEnable(readFileSync28(cfg.configFile, "utf8"), a.route));
16539
16964
  enabledSound.add(a.route);
16540
16965
  changed2.push(`enabled route ${a.route} in ${cfg.configFile}`);
16541
16966
  } else {
@@ -16721,7 +17146,10 @@ ${changed2.map((c) => ` - ${c}
16721
17146
  }
16722
17147
  if (!dryRun) {
16723
17148
  const holderLabel = `${cfg.ship.name}:${process.pid}`;
16724
- const got = await tracker.acquireBoardLock(scope, holderLabel, RELEASE_LOCK_TTL_MS);
17149
+ const got = await tracker.acquireBoardLock(scope, holderLabel, RELEASE_LOCK_TTL_MS, {
17150
+ warn: (m) => remit.warn(m, { step: "release" }),
17151
+ debug: (m) => remit.emit(m, { step: "release" })
17152
+ });
16725
17153
  if (!got.ok && got.reason === "missing") {
16726
17154
  const detail = `no Locks row for ${scope} \u2014 run \`crew connect ${c.route}\` to provision it`;
16727
17155
  remit.warn(`release for ${scope} refused: ${detail}`, { step: "release" });
@@ -17015,6 +17443,11 @@ ${changed2.map((c) => ` - ${c}
17015
17443
  case "poll":
17016
17444
  case "run": {
17017
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
+ }
17018
17451
  if (fleetWide) {
17019
17452
  const fleet = await decideFleet({
17020
17453
  routes: cfg.routes,
@@ -17536,7 +17969,7 @@ ${changed2.map((c) => ` - ${c}
17536
17969
  process.exit(1);
17537
17970
  }
17538
17971
  requireArmed("run the daemon loop");
17539
- const watchPaths = isCompiledBinary(import.meta.url) ? [process.execPath] : [join34(CREW_HOME, "src"), join34(CREW_HOME, "dist"), join34(CREW_HOME, "bin")];
17972
+ const watchPaths = isCompiledBinary(import.meta.url) ? [process.execPath] : [join35(CREW_HOME, "src"), join35(CREW_HOME, "dist"), join35(CREW_HOME, "bin")];
17540
17973
  const configPaths = daemonConfigPaths(cfg);
17541
17974
  watchPaths.push(...configPaths);
17542
17975
  noteDaemonStarted(cfg.ship.stateDir);
@@ -17548,6 +17981,7 @@ ${changed2.map((c) => ` - ${c}
17548
17981
  state,
17549
17982
  maxConcurrentAgents: cfg.ship.maxConcurrentAgents,
17550
17983
  spawnBreaker,
17984
+ configFile: cfg.configFile,
17551
17985
  newEmitter: () => new Emitter({
17552
17986
  route: route.route,
17553
17987
  eventFile: eventFileFor(cfg.ship.stateDir),
@@ -17754,15 +18188,72 @@ ${changed2.map((c) => ` - ${c}
17754
18188
  `);
17755
18189
  if (!dryRun) {
17756
18190
  const resolvedPath = resolvedPathFor(cfg.ship.stateDir, target.route);
17757
- const raw = existsSync33(resolvedPath) ? JSON.parse(readFileSync27(resolvedPath, "utf8")) : {};
18191
+ const raw = existsSync34(resolvedPath) ? JSON.parse(readFileSync28(resolvedPath, "utf8")) : {};
17758
18192
  raw.agentSkills = result.agentSkills;
17759
- mkdirSync20(dirOf(resolvedPath), { recursive: true });
17760
- writeFileSync21(resolvedPath, `${JSON.stringify(raw, null, 2)}
18193
+ mkdirSync21(dirOf(resolvedPath), { recursive: true });
18194
+ writeFileSync22(resolvedPath, `${JSON.stringify(raw, null, 2)}
17761
18195
  `);
17762
18196
  }
17763
18197
  if (result.outcomes.some((o) => o.action === "diverged")) process.exitCode = 1;
17764
18198
  break;
17765
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
+ }
17766
18257
  case "repos": {
17767
18258
  const sub = positional[1];
17768
18259
  const usage2 = () => {
@@ -17770,13 +18261,13 @@ ${changed2.map((c) => ` - ${c}
17770
18261
  process.exit(2);
17771
18262
  };
17772
18263
  if (sub !== "add" && sub !== "list" && sub !== "clone" && sub !== "hooks") usage2();
17773
- const configText = readFileSync27(cfg.configFile, "utf8");
18264
+ const configText = readFileSync28(cfg.configFile, "utf8");
17774
18265
  if (sub === "hooks") {
17775
18266
  try {
17776
18267
  const a = parseHooksArgs(positional.slice(2), routeNamesOf(configText));
17777
- const dir = repoDirOf(configText, dirname19(cfg.configFile), a.route, a.name);
17778
- if (!existsSync33(dir)) throw new ReposError(`${dir} does not exist on disk`);
17779
- 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) => {
17780
18271
  try {
17781
18272
  return readdirSync14(resolvePath(dir, r));
17782
18273
  } catch {
@@ -17784,7 +18275,7 @@ ${changed2.map((c) => ` - ${c}
17784
18275
  }
17785
18276
  }, read: (r) => {
17786
18277
  try {
17787
- return readFileSync27(resolvePath(dir, r), "utf8");
18278
+ return readFileSync28(resolvePath(dir, r), "utf8");
17788
18279
  } catch {
17789
18280
  return void 0;
17790
18281
  }
@@ -17822,13 +18313,13 @@ ${changed2.map((c) => ` - ${c}
17822
18313
  if (write && suggestion.source === null && recipeIds.length === 0 && hookSpecs.length === 0) throw new ReposError(`nothing to write: ${suggestion.notes[0]}`);
17823
18314
  if (write === "route") {
17824
18315
  const plan = planRouteHooks(configText, a.route, a.name, picked.hooks);
17825
- writeFileSync21(cfg.configFile, plan.text);
18316
+ writeFileSync22(cfg.configFile, plan.text);
17826
18317
  written = { target: "route", file: cfg.configFile, hooks: plan.written, kept: plan.kept };
17827
18318
  } else if (write === "repo") {
17828
18319
  const have = existingRepoConfig(files);
17829
18320
  if (have) throw new ReposError(`${resolvePath(dir, have)} already exists; not overwriting it`);
17830
18321
  const file = resolvePath(dir, ".crew.yaml");
17831
- writeFileSync21(file, starterRepoConfig(picked.hooks, picked.release), { flag: "wx" });
18322
+ writeFileSync22(file, starterRepoConfig(picked.hooks, picked.release), { flag: "wx" });
17832
18323
  written = { target: "repo", file, hooks: Object.keys(picked.hooks), kept: [] };
17833
18324
  }
17834
18325
  if (flag("json")) {
@@ -17880,7 +18371,7 @@ ${changed2.map((c) => ` - ${c}
17880
18371
  if (sub === "list" && !routeName) usage2();
17881
18372
  if (sub === "add") {
17882
18373
  try {
17883
- const a = parseAddArgs(positional.slice(2), routeNamesOf(configText), (p) => existsSync33(resolvePath(p.replace(/^~(?=\/|$)/, homedir8()))));
18374
+ const a = parseAddArgs(positional.slice(2), routeNamesOf(configText), (p) => existsSync34(resolvePath(p.replace(/^~(?=\/|$)/, homedir9()))));
17884
18375
  routeName = a.route;
17885
18376
  addPath = a.path;
17886
18377
  addName = a.name;
@@ -17909,15 +18400,15 @@ ${changed2.map((c) => ` - ${c}
17909
18400
  }
17910
18401
  const resolvedFile = resolvedPathFor(cfg.ship.stateDir, routeName);
17911
18402
  let resolvedRemotes;
17912
- if (existsSync33(resolvedFile)) {
18403
+ if (existsSync34(resolvedFile)) {
17913
18404
  try {
17914
- resolvedRemotes = JSON.parse(readFileSync27(resolvedFile, "utf8"));
18405
+ resolvedRemotes = JSON.parse(readFileSync28(resolvedFile, "utf8"));
17915
18406
  } catch {
17916
18407
  }
17917
18408
  }
17918
18409
  const originOf = (dir) => gitOk(dir, ["remote", "get-url", "origin"]) ?? void 0;
17919
18410
  const text = configText;
17920
- const base = dirname19(cfg.configFile);
18411
+ const base = dirname20(cfg.configFile);
17921
18412
  try {
17922
18413
  if (sub === "list") {
17923
18414
  if (flag("json")) {
@@ -17927,10 +18418,10 @@ ${changed2.map((c) => ` - ${c}
17927
18418
  base,
17928
18419
  resolved: resolvedRemotes,
17929
18420
  originOf,
17930
- exists: (d) => existsSync33(d),
18421
+ exists: (d) => existsSync34(d),
17931
18422
  repoHasHooks: (d) => REPO_CONFIG_FILENAMES.some((f) => {
17932
18423
  try {
17933
- 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;
17934
18425
  } catch {
17935
18426
  return false;
17936
18427
  }
@@ -17946,7 +18437,7 @@ ${changed2.map((c) => ` - ${c}
17946
18437
  for (const r of repos) {
17947
18438
  const origin2 = originOf(r.dir);
17948
18439
  const row = matchRepoName(origin2, resolvedRemotes);
17949
- 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)"}`;
17950
18441
  process.stdout.write(`${r.name} ${r.dir} ${note}
17951
18442
  `);
17952
18443
  }
@@ -17958,9 +18449,9 @@ ${changed2.map((c) => ` - ${c}
17958
18449
  ` : `${human}
17959
18450
  `);
17960
18451
  const plan2 = planRepoClone({ text, base, route: routeName, name: addName, resolved: resolvedRemotes, dir: value("dir") });
17961
- 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)) {
17962
18453
  throw new ReposError(
17963
- 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`
17964
18455
  );
17965
18456
  }
17966
18457
  if (dryRun) {
@@ -17978,12 +18469,12 @@ ${changed2.map((c) => ` - ${c}
17978
18469
  throw new ReposError(`${e.message}${authHint}
17979
18470
  crew.yaml was not changed`);
17980
18471
  }
17981
- writeFileSync21(cfg.configFile, plan2.text);
18472
+ writeFileSync22(cfg.configFile, plan2.text);
17982
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}`);
17983
18474
  break;
17984
18475
  }
17985
- const target = resolvePath(addPath.replace(/^~(?=\/|$)/, homedir8()));
17986
- const top = existsSync33(target) ? gitOk(target, ["rev-parse", "--show-toplevel"]) : null;
18476
+ const target = resolvePath(addPath.replace(/^~(?=\/|$)/, homedir9()));
18477
+ const top = existsSync34(target) ? gitOk(target, ["rev-parse", "--show-toplevel"]) : null;
17987
18478
  if (!top) throw new ReposError(`${target} is not a git checkout`);
17988
18479
  const origin = originOf(top);
17989
18480
  const plan = planRepoAdd({
@@ -18032,15 +18523,15 @@ ${changed2.map((c) => ` - ${c}
18032
18523
  const created = await rowTracker.createRepoRow(rowPayload);
18033
18524
  process.stdout.write(`created Repos row ${String(rowPayload.name)} (${String(rowPayload.remote)}) ${created.id}
18034
18525
  `);
18035
- if (existsSync33(resolvedFile)) {
18036
- const raw = JSON.parse(readFileSync27(resolvedFile, "utf8"));
18526
+ if (existsSync34(resolvedFile)) {
18527
+ const raw = JSON.parse(readFileSync28(resolvedFile, "utf8"));
18037
18528
  raw.repoNames = { ...raw.repoNames, [created.id]: String(rowPayload.name) };
18038
18529
  raw.repoRemotes = { ...raw.repoRemotes, [created.id]: String(rowPayload.remote) };
18039
- writeFileSync21(resolvedFile, `${JSON.stringify(raw, null, 2)}
18530
+ writeFileSync22(resolvedFile, `${JSON.stringify(raw, null, 2)}
18040
18531
  `);
18041
18532
  }
18042
18533
  }
18043
- writeFileSync21(cfg.configFile, plan.text);
18534
+ writeFileSync22(cfg.configFile, plan.text);
18044
18535
  process.stdout.write(`added ${top} to ${routeName} as ${plan.mode === "dir" ? "dir" : `repos.${plan.name}`} in ${cfg.configFile}
18045
18536
  `);
18046
18537
  } catch (e) {
@@ -18134,6 +18625,29 @@ ${reconstruction.prompt}
18134
18625
  }
18135
18626
  case "connect": {
18136
18627
  const arg = positional[1];
18628
+ if (flag("sign-out")) {
18629
+ let signOutStore;
18630
+ try {
18631
+ signOutStore = getSessionStore3();
18632
+ } catch {
18633
+ signOutStore = void 0;
18634
+ }
18635
+ const outcomes = signOutStore ? await signOutSessions({
18636
+ store: signOutStore,
18637
+ stateDir: cfg.ship.stateDir,
18638
+ dryRun,
18639
+ host: value("host") ?? (value("base-url") ? hostFromUrl4(value("base-url")) : void 0),
18640
+ workspace: value("workspace")
18641
+ }) : [];
18642
+ const failed = outcomes.some((o) => o.action === "failed");
18643
+ if (flag("json")) process.stdout.write(`${JSON.stringify({ dryRun, ok: !failed, sessions: outcomes })}
18644
+ `);
18645
+ else if (!outcomes.length) process.stdout.write("nothing to sign out of\n");
18646
+ else for (const o of outcomes) process.stdout.write(`${o.action}: ${o.host}/${o.workspace}${o.detail ? ` (${o.detail})` : ""}
18647
+ `);
18648
+ if (failed) process.exitCode = 1;
18649
+ break;
18650
+ }
18137
18651
  const canAsk = !!(process.stdin.isTTY && process.stdout.isTTY) && !connectEvents.json;
18138
18652
  const stopForChoice = (key, message, options) => {
18139
18653
  connectEvents.emit("choice", message, choiceData(key, options));
@@ -18151,6 +18665,7 @@ ${reconstruction.prompt}
18151
18665
  const project = argProject ?? value("project") ?? connectChoices.project;
18152
18666
  const baseUrl = value("base-url") ?? cfg.routes[0]?.baseUrl ?? DEFAULT_BASE_URL;
18153
18667
  let apiKey = value("key") ?? process.env.CREW_CONNECT_KEY ?? cfg.routes.find((c) => c.apiKey)?.apiKey;
18668
+ const givenKeySource = value("key") ?? process.env.CREW_CONNECT_KEY ? "flag" : "config";
18154
18669
  let store;
18155
18670
  try {
18156
18671
  store = getSessionStore3();
@@ -18160,11 +18675,11 @@ ${reconstruction.prompt}
18160
18675
  let keychainBacked = false;
18161
18676
  if (!apiKey && workspace && store) {
18162
18677
  try {
18163
- const existing = await store.get(hostFromUrl3(baseUrl), workspace);
18678
+ const existing = await store.get(hostFromUrl4(baseUrl), workspace);
18164
18679
  if (existing?.apiKey) {
18165
18680
  apiKey = existing.apiKey;
18166
18681
  keychainBacked = true;
18167
- process.stderr.write(`Reusing the existing keychain session for ${hostFromUrl3(baseUrl)}/${workspace}.
18682
+ process.stderr.write(`Reusing the existing keychain session for ${hostFromUrl4(baseUrl)}/${workspace}.
18168
18683
  `);
18169
18684
  }
18170
18685
  } catch {
@@ -18175,7 +18690,7 @@ ${reconstruction.prompt}
18175
18690
  const resumed = await resumeKeychainSession({
18176
18691
  store,
18177
18692
  stateDir: cfg.ship.stateDir,
18178
- host: hostFromUrl3(baseUrl),
18693
+ host: hostFromUrl4(baseUrl),
18179
18694
  verify: async (k) => {
18180
18695
  try {
18181
18696
  await get({ baseUrl, apiKey: k, userAgent: cfg.ship.userAgent }, "/auth/me");
@@ -18190,7 +18705,7 @@ ${reconstruction.prompt}
18190
18705
  workspace = resumed.workspace;
18191
18706
  keychainBacked = true;
18192
18707
  signInSource = "keychain";
18193
- process.stderr.write(`Reusing the existing keychain session for ${hostFromUrl3(baseUrl)}/${workspace}.
18708
+ process.stderr.write(`Reusing the existing keychain session for ${hostFromUrl4(baseUrl)}/${workspace}.
18194
18709
  `);
18195
18710
  }
18196
18711
  }
@@ -18247,13 +18762,14 @@ Signed in as ${result.identity.email} \u2014 minted an API key in workspace "${r
18247
18762
  connectEvents.emit("signed_in", `signed in as ${result.identity.email}`, {
18248
18763
  identity: { id: result.identity.id, email: result.identity.email },
18249
18764
  workspaces: [{ slug: result.workspace.slug, name: result.workspace.name }],
18250
- session: "device"
18765
+ session: "device",
18766
+ host: hostFromUrl4(baseUrl)
18251
18767
  });
18252
18768
  const deviceWorkspace = "path" in result.workspace && typeof result.workspace.path === "string" ? result.workspace.path : result.workspace.slug;
18253
18769
  if (!workspace) workspace = deviceWorkspace;
18254
18770
  if (store) {
18255
18771
  try {
18256
- await store.set(hostFromUrl3(baseUrl), deviceWorkspace, {
18772
+ await store.set(hostFromUrl4(baseUrl), deviceWorkspace, {
18257
18773
  apiKey: result.apiKey.key,
18258
18774
  shipId: getOrCreateShipId(),
18259
18775
  workspaceId: result.workspace.id,
@@ -18261,9 +18777,9 @@ Signed in as ${result.identity.email} \u2014 minted an API key in workspace "${r
18261
18777
  createdAt: result.apiKey.createdAt
18262
18778
  });
18263
18779
  keychainBacked = true;
18264
- rememberSession(cfg.ship.stateDir, hostFromUrl3(baseUrl), deviceWorkspace);
18780
+ rememberSession(cfg.ship.stateDir, hostFromUrl4(baseUrl), deviceWorkspace);
18265
18781
  process.stderr.write(
18266
- `API key stored in the OS keychain for ${hostFromUrl3(baseUrl)}/${deviceWorkspace}.
18782
+ `API key stored in the OS keychain for ${hostFromUrl4(baseUrl)}/${deviceWorkspace}.
18267
18783
  `
18268
18784
  );
18269
18785
  } catch (e) {
@@ -18299,7 +18815,8 @@ Signed in as ${result.identity.email} \u2014 minted an API key in workspace "${r
18299
18815
  platform: cfg.ship.platform,
18300
18816
  machineId,
18301
18817
  hostPassengers: firstRun?.answers.hostPassengers ?? existingRoute?.hostPassengers === true,
18302
- sshPublicKey
18818
+ sshPublicKey,
18819
+ lastSyncedName: (r) => readSyncedName(resolvedPathFor(cfg.ship.stateDir, r))
18303
18820
  };
18304
18821
  const runConnect = async (ws, proj) => {
18305
18822
  let found = await discover({ ...authOpts, workspace: ws, project: proj, area: value("area"), ship, dryRun, interactive: canAsk || connectEvents.json });
@@ -18308,7 +18825,8 @@ Signed in as ${result.identity.email} \u2014 minted an API key in workspace "${r
18308
18825
  connectEvents.emit("signed_in", `key belongs to ${found.meEmail ?? found.meId}`, {
18309
18826
  identity: { id: found.meId, email: found.meEmail, name: found.meName },
18310
18827
  workspaces: [{ slug: found.workspaceSlug, name: found.workspaceName, role: found.role }],
18311
- session: signInSource
18828
+ session: signInSource,
18829
+ host: hostFromUrl4(baseUrl)
18312
18830
  });
18313
18831
  }
18314
18832
  connectEvents.emit("workspace_resolved", `workspace ${found.workspaceSlug}`, {
@@ -18355,6 +18873,10 @@ Signed in as ${result.identity.email} \u2014 minted an API key in workspace "${r
18355
18873
  }
18356
18874
  process.exit(2);
18357
18875
  }
18876
+ if (found.shipNameAdopted && !dryRun) {
18877
+ writeFileSync22(cfg.configFile, setShipNameInYaml(readFileSync28(cfg.configFile, "utf8"), found.shipNameAdopted));
18878
+ cfg.ship.name = found.shipNameAdopted;
18879
+ }
18358
18880
  if (found.shipRow) {
18359
18881
  connectEvents.emit("ship_row", `Ships row "${found.shipRow.name}" ${found.shipRow.action}`, found.shipRow);
18360
18882
  }
@@ -18383,8 +18905,8 @@ Signed in as ${result.identity.email} \u2014 minted an API key in workspace "${r
18383
18905
  }
18384
18906
  }
18385
18907
  let operator = found.operator;
18386
- if (!operator && found.holds.length > 0 && canAsk) {
18387
- const picked = await pickOperator(found.holds, { id: found.meId, name: found.meName, email: found.meEmail });
18908
+ if (!operator && found.candidates.length > 0 && canAsk) {
18909
+ const picked = await pickOperator(found.candidates, { id: found.meId, name: found.meName, email: found.meEmail });
18388
18910
  if (picked === NEW_OPERATOR_ROW) {
18389
18911
  await createOperatorRow({ ...authOpts, dryRun }, found);
18390
18912
  operator = found.operator;
@@ -18395,15 +18917,15 @@ Signed in as ${result.identity.email} \u2014 minted an API key in workspace "${r
18395
18917
  }
18396
18918
  }
18397
18919
  let previous;
18398
- const hadPreviousFile = existsSync33(resolvedPath);
18920
+ const hadPreviousFile = existsSync34(resolvedPath);
18399
18921
  if (hadPreviousFile) {
18400
18922
  try {
18401
- previous = JSON.parse(readFileSync27(resolvedPath, "utf8"));
18923
+ previous = JSON.parse(readFileSync28(resolvedPath, "utf8"));
18402
18924
  } catch {
18403
18925
  }
18404
18926
  }
18405
18927
  if (!operator) operator = previous?.operator;
18406
- if (connectEvents.json && !operator && found.holds.length > 0) {
18928
+ if (connectEvents.json && !operator && found.candidates.length > 0) {
18407
18929
  const answer = connectChoices.operator;
18408
18930
  if (answer === "new") {
18409
18931
  await createOperatorRow({ ...authOpts, dryRun }, found);
@@ -18418,27 +18940,36 @@ Signed in as ${result.identity.email} \u2014 minted an API key in workspace "${r
18418
18940
  found.operatorAction = "linked";
18419
18941
  }
18420
18942
  } else {
18421
- const mine = found.holds.filter((h) => !h.user_id || h.user_id === found.meId);
18943
+ const mine = found.candidates.filter((h) => !h.user_id || h.user_id === found.meId);
18422
18944
  stopForChoice("operator", "Which Crew row is you?", [
18423
- ...mine.map((h) => ({ value: h.id, label: h.name || h.id })),
18945
+ ...mine.map((h) => ({ value: h.id, label: h.name || "unnamed row", ...h.detail ? { detail: h.detail } : { detail: h.id } })),
18424
18946
  ...found.meId ? [{ value: "new", label: `Create a new Crew row for ${found.meName ?? found.meEmail ?? "me"}` }] : []
18425
18947
  ]);
18426
18948
  }
18427
18949
  }
18950
+ if (!operator && found.operatorCompared) {
18951
+ process.stderr.write(`crew connect: no Crew row matched you \u2014 ${found.operatorCompared}
18952
+ `);
18953
+ connectEvents.emit("operator_row", "no Crew row matched you", { action: "unmatched", compared: found.operatorCompared });
18954
+ }
18428
18955
  if (operator) {
18429
- connectEvents.emit("operator_row", `operator ${found.holds.find((h) => h.id === operator)?.name ?? operator}`, {
18956
+ const opRow = found.holds.find((h) => h.id === operator);
18957
+ const opEmail = opRow?.email || opRow?.memberEmail || (opRow?.user_id && opRow.user_id === found.meId ? found.meEmail : void 0);
18958
+ connectEvents.emit("operator_row", `operator ${opRow?.name ?? operator}`, {
18430
18959
  id: operator,
18431
- action: found.operatorAction ?? "previous"
18960
+ action: found.operatorAction ?? "previous",
18961
+ ...opRow?.name ? { name: opRow.name } : {},
18962
+ ...opEmail ? { email: opEmail } : {}
18432
18963
  });
18433
18964
  }
18434
18965
  if (!operator && !hadPreviousFile) {
18435
18966
  process.stderr.write(
18436
- `crew connect ${route2}: cannot determine the operator \u2014 ${found.holds.length} Crew row(s) could be it, none matching this key's own email, and there's no terminal to ask.
18967
+ `crew connect ${route2}: cannot determine the operator \u2014 ${found.candidates.length} Crew row(s) could be it, none matching this key's own email, and there's no terminal to ask.
18437
18968
  Re-run from a real terminal to pick one, or pass --key for a key whose own
18438
18969
  identity email matches exactly one Crew row.
18439
18970
  `
18440
18971
  );
18441
- stopWithProblem(`cannot determine the operator \u2014 ${found.holds.length} Crew row(s) could be it`, "operator");
18972
+ stopWithProblem(`cannot determine the operator \u2014 ${found.candidates.length} Crew row(s) could be it`, "operator");
18442
18973
  }
18443
18974
  const alreadyReviewed = new Set(previous?.reviewedStatuses ?? []);
18444
18975
  const stillUnrecognized = (found.unrecognizedStatuses ?? []).filter((s) => !alreadyReviewed.has(s.value));
@@ -18501,18 +19032,20 @@ Signed in as ${result.identity.email} \u2014 minted an API key in workspace "${r
18501
19032
  // nothing about its prompt actually changed.
18502
19033
  ...previous?.agentPersonas ? { agentPersonas: previous.agentPersonas } : {},
18503
19034
  ...previous?.agentSkills ? { agentSkills: previous.agentSkills } : {},
18504
- ...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 } : {}
18505
19038
  };
18506
19039
  if (dryRun) {
18507
19040
  process.stderr.write(`(dry run) would write resolved ids to ${resolvedPath}
18508
19041
  `);
18509
19042
  } else {
18510
- mkdirSync20(dirname19(resolvedPath), { recursive: true });
19043
+ mkdirSync21(dirname20(resolvedPath), { recursive: true });
18511
19044
  if (hadPreviousFile) copyFileSync2(resolvedPath, `${resolvedPath}.bak`);
18512
19045
  const tmpPath = `${resolvedPath}.tmp${process.pid}`;
18513
- writeFileSync21(tmpPath, `${JSON.stringify(resolved, null, 2)}
19046
+ writeFileSync22(tmpPath, `${JSON.stringify(resolved, null, 2)}
18514
19047
  `);
18515
- renameSync2(tmpPath, resolvedPath);
19048
+ renameSync3(tmpPath, resolvedPath);
18516
19049
  process.stderr.write(`Resolved ids written to ${resolvedPath}
18517
19050
  `);
18518
19051
  }
@@ -18533,20 +19066,27 @@ Signed in as ${result.identity.email} \u2014 minted an API key in workspace "${r
18533
19066
  role: sr.role,
18534
19067
  id: sr.id,
18535
19068
  action: sr.action,
19069
+ ...sr.shipId ? { ship_id: sr.shipId } : {},
18536
19070
  key: minted?.action ?? "none",
18537
19071
  ...minted?.detail ? { detail: minted.detail } : {}
18538
19072
  });
18539
19073
  }
18540
19074
  let apiKeyFile;
18541
19075
  const apiKeyVar = "CREW_API_KEY";
19076
+ if (firstRunFromFlags && agentBinSearched && !dryRun) {
19077
+ const msg = `no agent binary: "claude" was not found on PATH or in ${agentBinSearched.join(", ")} \u2014 pass --agent-bin /absolute/path`;
19078
+ process.stderr.write(`crew connect: ${msg}
19079
+ `);
19080
+ stopWithProblem(msg, "wizard");
19081
+ }
18542
19082
  if ((mintedFromDevice || firstRunFromFlags) && !keychainBacked) {
18543
19083
  apiKeyFile = apiKeyPathFor(cfg.ship.stateDir, route2);
18544
19084
  if (dryRun) {
18545
19085
  process.stderr.write(`(dry run) would write the minted API key to ${apiKeyFile}
18546
19086
  `);
18547
19087
  } else {
18548
- mkdirSync20(dirname19(apiKeyFile), { recursive: true });
18549
- writeFileSync21(apiKeyFile, `${apiKeyVar}=${apiKey}
19088
+ mkdirSync21(dirname20(apiKeyFile), { recursive: true });
19089
+ writeFileSync22(apiKeyFile, `${apiKeyVar}=${apiKey}
18550
19090
  `, { mode: 384 });
18551
19091
  process.stderr.write(`API key written to ${apiKeyFile}
18552
19092
  `);
@@ -18555,7 +19095,7 @@ Signed in as ${result.identity.email} \u2014 minted an API key in workspace "${r
18555
19095
  const keyPersisted = keychainBacked || apiKeyFile !== void 0;
18556
19096
  const wizard = firstRun;
18557
19097
  const writeFirstRun = wizard !== void 0 && !dryRun && keyPersisted;
18558
- const routeDir = value("dir") ?? (writeFirstRun ? join34(DEFAULT_REPOS_BASE_PATH, found.workspaceSlug) : void 0);
19098
+ const routeDir = value("dir") ?? (writeFirstRun ? join35(DEFAULT_REPOS_BASE_PATH, found.workspaceSlug) : void 0);
18559
19099
  const routeBlock = renderConnection(
18560
19100
  found,
18561
19101
  route2,
@@ -18704,13 +19244,39 @@ Signed in as ${result.identity.email} \u2014 minted an API key in workspace "${r
18704
19244
  wroteConfig,
18705
19245
  operator,
18706
19246
  dryRun,
18707
- unresolved: found.problems.length
19247
+ unresolved: found.problems.length,
19248
+ ...firstRunFromFlags ? { agentBin: agentBinSearched ? null : firstRun?.answers.agentBin } : {},
19249
+ ...agentBinSearched ? { agentBinNote: `no \`claude\` found on PATH or in ${agentBinSearched.join(", ")}; a real run needs --agent-bin` } : {}
18708
19250
  });
18709
19251
  };
18710
19252
  try {
18711
19253
  if (!workspace) {
18712
19254
  const workspaces = await listWorkspaces(authOpts);
18713
- if (workspaces.length === 0) {
19255
+ const configWorkspaces = [...new Set(cfg.routes.map((r) => splitRoute(r.route)[0]))];
19256
+ const defaulted = dryRun && givenKeySource === "config" && configWorkspaces.length === 1 ? workspaces.find((w) => w.slug === configWorkspaces[0]) : void 0;
19257
+ if (!signedInEmitted && (apiKey ?? "") !== "") {
19258
+ let me = {};
19259
+ try {
19260
+ me = await get(authOpts, "/auth/me");
19261
+ } catch {
19262
+ }
19263
+ if (me.id || me.email) {
19264
+ signedInEmitted = true;
19265
+ connectEvents.emit(
19266
+ "signed_in",
19267
+ `key belongs to ${me.email ?? me.id}${defaulted ? `; using ${defaulted.slug}, the workspace crew.yaml already names` : ""}`,
19268
+ {
19269
+ identity: { id: me.id, email: me.email, name: me.name },
19270
+ workspaces: workspaces.map((w) => ({ slug: w.slug, name: w.name, role: w.role })),
19271
+ session: givenKeySource,
19272
+ host: hostFromUrl4(baseUrl)
19273
+ }
19274
+ );
19275
+ }
19276
+ }
19277
+ if (defaulted) {
19278
+ await runConnect(defaulted.slug, project);
19279
+ } else if (workspaces.length === 0) {
18714
19280
  process.stderr.write("crew connect: this key's identity has no workspace memberships.\n");
18715
19281
  stopWithProblem("this key's identity has no workspace memberships", "workspace");
18716
19282
  } else if (workspaces.length === 1) {
@@ -19186,11 +19752,20 @@ Signed in as ${result.identity.email} \u2014 minted an API key in workspace "${r
19186
19752
  if (!resetStore) return { stored: false, clear: async () => {
19187
19753
  } };
19188
19754
  const store = resetStore;
19189
- const h = hostFromUrl3(r.baseUrl);
19755
+ const h = hostFromUrl4(r.baseUrl);
19190
19756
  const [slug] = splitRoute(r.route);
19191
19757
  const session = await store.get(h, slug);
19192
19758
  return { stored: Boolean(session?.apiKey), clear: () => store.clear(h, slug) };
19193
19759
  },
19760
+ async hostSessions(stateDir) {
19761
+ const store = resetStore;
19762
+ return Promise.all(rememberedSessions(stateDir).map(async ({ host: h, workspace: w }) => {
19763
+ const session = store ? await store.get(h, w).catch(() => void 0) : void 0;
19764
+ return { host: h, workspace: w, stored: Boolean(session?.apiKey), clear: async () => {
19765
+ await store?.clear(h, w);
19766
+ } };
19767
+ }));
19768
+ },
19194
19769
  inspectCheckout(dir) {
19195
19770
  const dirty = status(dir).map((c) => c.path);
19196
19771
  const linkedWorktrees = worktrees(dir).map((w) => w.path).filter((p) => realpathSync3(p) !== realpathSync3(dir));
@@ -19199,7 +19774,7 @@ Signed in as ${result.identity.email} \u2014 minted an API key in workspace "${r
19199
19774
  };
19200
19775
  const droppedRoutes = [];
19201
19776
  try {
19202
- const rawCfg = parseYaml2(readFileSync27(cfg.configFile, "utf8"));
19777
+ const rawCfg = parseYaml2(readFileSync28(cfg.configFile, "utf8"));
19203
19778
  for (const r of rawCfg?.routes ?? []) {
19204
19779
  if (typeof r?.route !== "string" || cfg.routes.some((x) => x.route === r.route)) continue;
19205
19780
  try {
@@ -19241,8 +19816,8 @@ Signed in as ${result.identity.email} \u2014 minted an API key in workspace "${r
19241
19816
  process.stdout.write("crew reload: no daemon unit is running \u2014 nothing to reload (the next run reads crew.yaml fresh)\n");
19242
19817
  break;
19243
19818
  }
19244
- mkdirSync20(cfg.ship.stateDir, { recursive: true });
19245
- writeFileSync21(sentinel, `${(/* @__PURE__ */ new Date()).toISOString()}
19819
+ mkdirSync21(cfg.ship.stateDir, { recursive: true });
19820
+ writeFileSync22(sentinel, `${(/* @__PURE__ */ new Date()).toISOString()}
19246
19821
  `);
19247
19822
  process.stdout.write("crew reload: requested \u2014 the daemon finishes its in-flight pass, then relaunches with the new config\n");
19248
19823
  if (flag("now") && control?.stopCommand && control.startCommand) {
@@ -19293,7 +19868,7 @@ Signed in as ${result.identity.email} \u2014 minted an API key in workspace "${r
19293
19868
  }
19294
19869
  case "log": {
19295
19870
  try {
19296
- const text = readFileSync27(cfg.ship.logFile, "utf8").split("\n");
19871
+ const text = readFileSync28(cfg.ship.logFile, "utf8").split("\n");
19297
19872
  process.stdout.write(`${text.slice(-(Number(value("tail")) || 40)).join("\n")}
19298
19873
  `);
19299
19874
  } catch {
@@ -19316,8 +19891,8 @@ Signed in as ${result.identity.email} \u2014 minted an API key in workspace "${r
19316
19891
  };
19317
19892
  if (!route.resolved) fail(`route ${route.route} is not connected \u2014 run \`crew connect\` first`);
19318
19893
  const tracker = new Tracker(route, cfg.ship);
19319
- const configText = readFileSync27(cfg.configFile, "utf8");
19320
- const cfgBase = dirname19(cfg.configFile);
19894
+ const configText = readFileSync28(cfg.configFile, "utf8");
19895
+ const cfgBase = dirname20(cfg.configFile);
19321
19896
  const configured = configuredRepos(configText, route.route, cfgBase);
19322
19897
  const dir = configured.find((r) => r.name === SAMPLE_REPO)?.dir ?? sampleDir(configured.map((r) => r.dir), route.reposBasePath, route.route.split("/")[0]);
19323
19898
  const resolvedFile = resolvedPathFor(cfg.ship.stateDir, route.route);
@@ -19345,7 +19920,7 @@ Signed in as ${result.identity.email} \u2014 minted an API key in workspace "${r
19345
19920
  }
19346
19921
  if (configured.some((r) => r.dir === dir)) {
19347
19922
  try {
19348
- writeFileSync21(cfg.configFile, planRepoRemove(configText, cfgBase, route.route, dir).text);
19923
+ writeFileSync22(cfg.configFile, planRepoRemove(configText, cfgBase, route.route, dir).text);
19349
19924
  } catch (e) {
19350
19925
  if (e instanceof ReposError) fail(e.message);
19351
19926
  throw e;
@@ -19353,29 +19928,29 @@ Signed in as ${result.identity.email} \u2014 minted an API key in workspace "${r
19353
19928
  }
19354
19929
  if (row) {
19355
19930
  await tracker.deleteRepoRow(row.id);
19356
- if (existsSync33(resolvedFile)) {
19357
- const raw = JSON.parse(readFileSync27(resolvedFile, "utf8"));
19931
+ if (existsSync34(resolvedFile)) {
19932
+ const raw = JSON.parse(readFileSync28(resolvedFile, "utf8"));
19358
19933
  if (raw.repoNames) delete raw.repoNames[row.id];
19359
19934
  if (raw.repoRemotes) delete raw.repoRemotes[row.id];
19360
- writeFileSync21(resolvedFile, `${JSON.stringify(raw, null, 2)}
19935
+ writeFileSync22(resolvedFile, `${JSON.stringify(raw, null, 2)}
19361
19936
  `);
19362
19937
  }
19363
19938
  }
19364
- if (existsSync33(join34(dir, ".git")) && basename11(dir) === SAMPLE_REPO) rmSync14(dir, { recursive: true, force: true });
19939
+ if (existsSync34(join35(dir, ".git")) && basename11(dir) === SAMPLE_REPO) rmSync15(dir, { recursive: true, force: true });
19365
19940
  say("removed", { dir, closed }, `removed the sample repo${closed.length ? `, closed ${closed.join(", ")}` : ""}`);
19366
19941
  break;
19367
19942
  }
19368
19943
  const projectId = route.resolved.areaId ?? resolveRepoProject(await tracker.projectRows(), value("project"));
19369
- const fresh = !existsSync33(join34(dir, ".git"));
19370
- 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`);
19371
19946
  if (dryRun) {
19372
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`);
19373
19948
  break;
19374
19949
  }
19375
19950
  if (fresh) {
19376
- mkdirSync20(dir, { recursive: true });
19377
- writeFileSync21(join34(dir, "README.md"), SAMPLE_README);
19378
- writeFileSync21(join34(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);
19379
19954
  const g = (...a) => {
19380
19955
  const r = spawnSync6("git", a, { cwd: dir, encoding: "utf8" });
19381
19956
  if (r.status !== 0) fail(`git ${a[0]} failed in ${dir}: ${(r.stderr || r.stdout).trim()}`);
@@ -19393,17 +19968,17 @@ Signed in as ${result.identity.email} \u2014 minted an API key in workspace "${r
19393
19968
  spec: { platform: "unix", releaseMode: "integrate", ciProvider: "none" }
19394
19969
  });
19395
19970
  repoId = (await tracker.createRepoRow(payload)).id;
19396
- if (existsSync33(resolvedFile)) {
19397
- const raw = JSON.parse(readFileSync27(resolvedFile, "utf8"));
19971
+ if (existsSync34(resolvedFile)) {
19972
+ const raw = JSON.parse(readFileSync28(resolvedFile, "utf8"));
19398
19973
  raw.repoNames = { ...raw.repoNames, [repoId]: SAMPLE_REPO };
19399
19974
  raw.repoRemotes = { ...raw.repoRemotes, [repoId]: SAMPLE_REMOTE };
19400
- writeFileSync21(resolvedFile, `${JSON.stringify(raw, null, 2)}
19975
+ writeFileSync22(resolvedFile, `${JSON.stringify(raw, null, 2)}
19401
19976
  `);
19402
19977
  }
19403
19978
  }
19404
19979
  if (!configured.some((r) => r.dir === dir)) {
19405
19980
  try {
19406
- 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);
19407
19982
  } catch (e) {
19408
19983
  if (e instanceof ReposError) fail(e.message);
19409
19984
  throw e;
@@ -19524,6 +20099,7 @@ Signed in as ${result.identity.email} \u2014 minted an API key in workspace "${r
19524
20099
  const projects = projectModeLabels(await statusTracker.projectRowsWithModes().catch(() => []));
19525
20100
  const crewMinVersion = await readCrewFloor(route, cfg.ship) ?? null;
19526
20101
  if (flag("json")) {
20102
+ const nameSync = await reconcileRouteShipName({ route, ship: cfg.ship, configFile: cfg.configFile, readOnly: true });
19527
20103
  const repos = await resolvedRepos(route);
19528
20104
  const seatIds = route.resolved?.seats ?? {};
19529
20105
  const crewRows = Object.keys(seatIds).length ? await statusTracker.crewRows() : [];
@@ -19567,7 +20143,10 @@ Signed in as ${result.identity.email} \u2014 minted an API key in workspace "${r
19567
20143
  eventFile: eventFileFor(cfg.ship.stateDir),
19568
20144
  // Additive (CREW-1373): open ship-level items, including the
19569
20145
  // derived `release_stale`. `version` stays 1.
19570
- 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
19571
20150
  },
19572
20151
  // Additive (CREW-1402): what the operator owes. `version` stays 1.
19573
20152
  todo: todo.map((i) => ({ ticket: displayKey(i.ticket), title: i.ticket.title ?? null, reasons: i.reasons })),
@@ -19714,7 +20293,7 @@ watermark: ${state.watermark()}
19714
20293
  }
19715
20294
  for (const r of repos) {
19716
20295
  const remote = r.config.branch.remote;
19717
- if (existsSync33(r.dir)) {
20296
+ if (existsSync34(r.dir)) {
19718
20297
  pushAuth.push({
19719
20298
  route: c.route,
19720
20299
  repo: r.name,
@@ -19742,7 +20321,7 @@ routes: ${cfg.routes.map((c) => c.route).join(", ")}
19742
20321
  );
19743
20322
  try {
19744
20323
  const evFile = eventFileFor(cfg.ship.stateDir);
19745
- const evSize = existsSync33(evFile) ? statSync10(evFile).size : 0;
20324
+ const evSize = existsSync34(evFile) ? statSync10(evFile).size : 0;
19746
20325
  process.stdout.write(
19747
20326
  evSize > cfg.ship.eventsRotateBytes * 4 ? `events log: WARN ${evFile} is ${Math.round(evSize / 1048576)} MB (> 4x eventsRotateBytes); run \`crew reap\` to rotate it
19748
20327
  ` : "events log: ok\n"
@@ -19753,7 +20332,7 @@ routes: ${cfg.routes.map((c) => c.route).join(", ")}
19753
20332
  const unitPlan = planInstall(cfg.ship, CREW_HOME, host, detectSystemd(), "run");
19754
20333
  const abandons = unitAbandonsAgents(unitPlan, (p) => {
19755
20334
  try {
19756
- return readFileSync27(p, "utf8");
20335
+ return readFileSync28(p, "utf8");
19757
20336
  } catch {
19758
20337
  return null;
19759
20338
  }
@@ -19825,6 +20404,11 @@ routes: ${cfg.routes.map((c) => c.route).join(", ")}
19825
20404
  if (warn) process.stdout.write(` WARNING ${warn}
19826
20405
  `);
19827
20406
  }
20407
+ const scope = releaseLockScope(route.route, r.name);
20408
+ const lock2 = await tracker.lockState(scope).catch(() => void 0);
20409
+ if (lock2?.holder && staleOwnLockClaim(lock2.holder, cfg.ship.name)) {
20410
+ process.stdout.write(` ${formatStaleOwnLock(scope, lock2.holder)}`);
20411
+ }
19828
20412
  }
19829
20413
  const missingAuthorship = await tracker.missingAuthorshipColumns();
19830
20414
  process.stdout.write(
@@ -20145,7 +20729,7 @@ ${foreign.join("\n")}
20145
20729
  break;
20146
20730
  }
20147
20731
  default:
20148
- usage();
20732
+ usage(`unknown command "${command}"`);
20149
20733
  }
20150
20734
  lock?.release();
20151
20735
  }