@tablation/crew 0.71.1 → 0.71.13

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 +480 -131
  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];
@@ -6068,8 +6111,8 @@ var Tracker = class {
6068
6111
  * Locks table with no row for `scope` is `{ ok: false, reason: 'missing' }`
6069
6112
  * (CREW-1384): `crew connect` provisions the row.
6070
6113
  */
6071
- acquireBoardLock(scope, holderLabel, ttlMs) {
6072
- return acquireBoardLock(this.client.records, this.route.resolved?.locksModelId, scope, holderLabel, ttlMs);
6114
+ acquireBoardLock(scope, holderLabel, ttlMs, log) {
6115
+ return acquireBoardLock(this.client.records, this.route.resolved?.locksModelId, scope, holderLabel, ttlMs, log);
6073
6116
  }
6074
6117
  /** The Locks row for `scope` — present? held by whom? Undefined when this workspace has no Locks table. */
6075
6118
  async lockState(scope) {
@@ -8563,9 +8606,9 @@ function doctorSelftestLines(stored, toolchains) {
8563
8606
  }
8564
8607
 
8565
8608
  // src/cli.ts
8566
- import { resolve as resolve5, dirname as dirname19, join as join34, basename as basename11 } from "path";
8609
+ import { resolve as resolve5, dirname as dirname19, join as join35, basename as basename11 } from "path";
8567
8610
  import { fileURLToPath } from "url";
8568
- import { hostname as hostname2, homedir as homedir8 } from "os";
8611
+ import { hostname as hostname2, homedir as homedir9 } from "os";
8569
8612
  import { spawnSync as spawnSync6 } from "child_process";
8570
8613
  import { parse as parseYaml2 } from "yaml";
8571
8614
 
@@ -8579,6 +8622,7 @@ var VALUE_FLAGS = /* @__PURE__ */ new Set([
8579
8622
  "extra-path",
8580
8623
  "harness",
8581
8624
  "hook",
8625
+ "host",
8582
8626
  "key",
8583
8627
  "level",
8584
8628
  "limit",
@@ -8635,6 +8679,15 @@ function splitConnectArg(arg) {
8635
8679
  const i = arg.indexOf("/");
8636
8680
  return [arg.slice(0, i), arg.slice(i + 1)];
8637
8681
  }
8682
+ function versionRequest(argv2) {
8683
+ if (argv2[0] !== "--version" && argv2[0] !== "version") return null;
8684
+ const rest = argv2.slice(1);
8685
+ if (rest.some((a) => a !== "--json")) return null;
8686
+ return { json: rest.length > 0 };
8687
+ }
8688
+ function helpRequest(argv2) {
8689
+ return argv2[0] === "--help" || argv2[0] === "-h" || argv2[0] === "help";
8690
+ }
8638
8691
 
8639
8692
  // src/roster.ts
8640
8693
  function memberByIdentity(roster, identityId) {
@@ -8730,7 +8783,7 @@ function rosterMarkdown(roster, meId) {
8730
8783
  }
8731
8784
 
8732
8785
  // src/connect-resume.ts
8733
- import { mkdirSync as mkdirSync10, readFileSync as readFileSync16, writeFileSync as writeFileSync11 } from "fs";
8786
+ import { mkdirSync as mkdirSync10, readFileSync as readFileSync16, rmSync as rmSync10, writeFileSync as writeFileSync11 } from "fs";
8734
8787
  import { dirname as dirname8, join as join19 } from "path";
8735
8788
  function lastSessionFile(stateDir) {
8736
8789
  return join19(stateDir, "connect-session.json");
@@ -8765,10 +8818,45 @@ async function resumeKeychainSession(o) {
8765
8818
  return void 0;
8766
8819
  }
8767
8820
  }
8821
+ function rememberedSessions(stateDir) {
8822
+ return Object.entries(readPointers(stateDir)).map(([host, workspace]) => ({ host, workspace }));
8823
+ }
8824
+ function forgetSession(stateDir, host) {
8825
+ try {
8826
+ const { [host]: _gone, ...rest } = readPointers(stateDir);
8827
+ if (Object.keys(rest).length) writeFileSync11(lastSessionFile(stateDir), `${JSON.stringify(rest, null, 2)}
8828
+ `, { mode: 384 });
8829
+ else rmSync10(lastSessionFile(stateDir), { force: true });
8830
+ } catch {
8831
+ }
8832
+ }
8833
+ async function signOutSessions(o) {
8834
+ const targets = rememberedSessions(o.stateDir).filter((t) => !o.host || t.host === o.host);
8835
+ if (o.host && o.workspace && !targets.some((t) => t.host === o.host && t.workspace === o.workspace)) {
8836
+ targets.push({ host: o.host, workspace: o.workspace });
8837
+ }
8838
+ const out = [];
8839
+ for (const t of targets) {
8840
+ try {
8841
+ const stored = await o.store.get(t.host, t.workspace);
8842
+ if (stored?.apiKey) {
8843
+ if (!o.dryRun) await o.store.clear(t.host, t.workspace);
8844
+ out.push({ ...t, action: o.dryRun ? "would-remove" : "removed" });
8845
+ } else {
8846
+ out.push({ ...t, action: "absent", detail: "no keychain entry" });
8847
+ }
8848
+ if (!o.dryRun) forgetSession(o.stateDir, t.host);
8849
+ } catch (e) {
8850
+ out.push({ ...t, action: "failed", detail: e.message });
8851
+ }
8852
+ }
8853
+ return out;
8854
+ }
8768
8855
 
8769
8856
  // src/reset.ts
8770
- import { existsSync as existsSync20, mkdirSync as mkdirSync11, readFileSync as readFileSync17, readdirSync as readdirSync7, rmSync as rmSync10, statSync as statSync6, writeFileSync as writeFileSync12 } from "fs";
8857
+ import { existsSync as existsSync20, mkdirSync as mkdirSync11, readFileSync as readFileSync17, readdirSync as readdirSync7, rmSync as rmSync11, statSync as statSync6, writeFileSync as writeFileSync12 } from "fs";
8771
8858
  import { dirname as dirname9, join as join20 } from "path";
8859
+ import { hostFromUrl as hostFromUrl2 } from "@tablation/client";
8772
8860
  function clonedCheckoutsFile(stateDir) {
8773
8861
  return join20(stateDir, "cloned-checkouts.json");
8774
8862
  }
@@ -8814,12 +8902,31 @@ async function runReset(o, fx) {
8814
8902
  steps.push({ step: "credential", target: route.route, action: "failed", detail: e.message });
8815
8903
  }
8816
8904
  }
8905
+ const covered = new Set([...o.routes, ...o.droppedRoutes ?? []].map((r) => `${hostFromUrl2(r.baseUrl)}/${splitRoute(r.route)[0]}`));
8906
+ try {
8907
+ for (const s of await fx.hostSessions(o.stateDir)) {
8908
+ const target = `${s.host}/${s.workspace}`;
8909
+ if (covered.has(target)) continue;
8910
+ try {
8911
+ if (!s.stored) {
8912
+ steps.push({ step: "credential", target, action: "absent", detail: "no host session in the keychain" });
8913
+ continue;
8914
+ }
8915
+ if (!o.dryRun) await s.clear();
8916
+ steps.push({ step: "credential", target, action: mark(o.dryRun), detail: "host session stored by a dry-run connect" });
8917
+ } catch (e) {
8918
+ steps.push({ step: "credential", target, action: "failed", detail: e.message });
8919
+ }
8920
+ }
8921
+ } catch (e) {
8922
+ steps.push({ step: "credential", target: "host sessions", action: "failed", detail: e.message });
8923
+ }
8817
8924
  if (o.removeCheckouts) {
8818
8925
  for (const dir of readClonedCheckouts(o.stateDir)) steps.push(removeCheckout(dir, o.dryRun, fx));
8819
8926
  }
8820
8927
  steps.push(...removeState(o.stateDir, o.dryRun));
8821
8928
  if (existsSync20(o.configFile)) {
8822
- if (!o.dryRun) rmSync10(o.configFile, { force: true });
8929
+ if (!o.dryRun) rmSync11(o.configFile, { force: true });
8823
8930
  steps.push({ step: "config", target: o.configFile, action: mark(o.dryRun) });
8824
8931
  } else {
8825
8932
  steps.push({ step: "config", target: o.configFile, action: "absent" });
@@ -8835,7 +8942,7 @@ function removeCheckout(dir, dryRun, fx) {
8835
8942
  if (linkedWorktrees.length) {
8836
8943
  return { step: "checkout", target: dir, action: "kept", detail: `has linked worktrees (${linkedWorktrees.join(", ")}); remove them first` };
8837
8944
  }
8838
- if (!dryRun) rmSync10(dir, { recursive: true, force: true });
8945
+ if (!dryRun) rmSync11(dir, { recursive: true, force: true });
8839
8946
  return { step: "checkout", target: dir, action: mark(dryRun) };
8840
8947
  }
8841
8948
  function removeState(stateDir, dryRun) {
@@ -8849,7 +8956,7 @@ function removeState(stateDir, dryRun) {
8849
8956
  }
8850
8957
  for (const t of targets) {
8851
8958
  if (!existsSync20(t)) continue;
8852
- if (!dryRun) rmSync10(t, { recursive: true, force: true });
8959
+ if (!dryRun) rmSync11(t, { recursive: true, force: true });
8853
8960
  out.push({ step: "state", target: t, action: mark(dryRun) });
8854
8961
  }
8855
8962
  if (!out.length) out.push({ step: "state", target: stateDir, action: "absent", detail: "nothing to remove" });
@@ -10513,7 +10620,9 @@ Wrote ${o.configPath}.
10513
10620
 
10514
10621
  // src/connect-json.ts
10515
10622
  import { accessSync as accessSync3, constants as constants3 } from "fs";
10516
- import { dirname as dirname11 } from "path";
10623
+ import { homedir as homedir7 } from "os";
10624
+ import { dirname as dirname11, join as join23 } from "path";
10625
+ import { delimiter as delimiter2 } from "path";
10517
10626
  var CHOICE_EXIT_CODE = 3;
10518
10627
  function createEmitter(json, write) {
10519
10628
  return {
@@ -10547,6 +10656,23 @@ function usesFlagAnswers(f, isTTY) {
10547
10656
  if (isTTY) return false;
10548
10657
  return !!(f.shipName || f.agentBin || f.extraPath || f.hostPassengers);
10549
10658
  }
10659
+ function agentBinSearchDirs(home, pathEnv, extraPath) {
10660
+ const dirs = [
10661
+ ...pathEnv.split(delimiter2),
10662
+ ...(extraPath ?? "").split(delimiter2),
10663
+ join23(home, ".local/bin"),
10664
+ join23(home, ".claude/local"),
10665
+ "/opt/homebrew/bin",
10666
+ "/usr/local/bin",
10667
+ join23(home, ".npm-global/bin"),
10668
+ join23(home, ".volta/bin")
10669
+ ];
10670
+ return [...new Set(dirs.filter(Boolean))];
10671
+ }
10672
+ function findAgentBin(name, env, extraPath, isExec) {
10673
+ const searched = agentBinSearchDirs(env.home ?? homedir7(), env.pathEnv, extraPath);
10674
+ return { bin: findOnPath2(name, searched.join(delimiter2), isExec), searched };
10675
+ }
10550
10676
  function flagWizardAnswers(f, env) {
10551
10677
  const isExec = env.isExecutable ?? ((p) => {
10552
10678
  try {
@@ -10556,9 +10682,9 @@ function flagWizardAnswers(f, env) {
10556
10682
  return false;
10557
10683
  }
10558
10684
  });
10559
- const agentBin = f.agentBin ?? findOnPath2("claude", env.pathEnv, isExec);
10560
- if (!agentBin) return { error: 'no agent binary: "claude" is not on PATH \u2014 pass --agent-bin /absolute/path' };
10561
10685
  if (f.agentBin && !isExec(f.agentBin)) return { error: `--agent-bin ${f.agentBin} is not an executable file` };
10686
+ const found = f.agentBin ? void 0 : findAgentBin("claude", env, f.extraPath, isExec);
10687
+ const agentBin = f.agentBin ?? found?.bin;
10562
10688
  const pnpmPath = findOnPath2("pnpm", env.pathEnv, isExec);
10563
10689
  const dockerPath = f.hostPassengers ? findOnPath2("docker", env.pathEnv, isExec) : void 0;
10564
10690
  const extraPath = f.extraPath ?? deriveExtraPath([dirname11(env.nodePath), pnpmPath ? dirname11(pnpmPath) : void 0, dockerPath ? dirname11(dockerPath) : void 0]);
@@ -10566,16 +10692,17 @@ function flagWizardAnswers(f, env) {
10566
10692
  answers: {
10567
10693
  name: f.shipName?.trim() || env.hostname,
10568
10694
  platform: env.platform,
10569
- agentBin,
10695
+ agentBin: agentBin ?? "claude",
10570
10696
  extraPath,
10571
10697
  hostPassengers: f.hostPassengers
10572
- }
10698
+ },
10699
+ ...agentBin ? {} : { agentBinSearched: found?.searched ?? [] }
10573
10700
  };
10574
10701
  }
10575
10702
 
10576
10703
  // src/release-run.ts
10577
- import { readFileSync as readFileSync18, writeFileSync as writeFileSync15, existsSync as existsSync22, copyFileSync, mkdirSync as mkdirSync13, mkdtempSync as mkdtempSync2, rmSync as rmSync11 } from "fs";
10578
- import { dirname as dirname12, join as join23 } from "path";
10704
+ import { readFileSync as readFileSync18, writeFileSync as writeFileSync15, existsSync as existsSync22, copyFileSync, mkdirSync as mkdirSync13, mkdtempSync as mkdtempSync2, rmSync as rmSync12 } from "fs";
10705
+ import { dirname as dirname12, join as join24 } from "path";
10579
10706
  import { tmpdir as tmpdir3 } from "os";
10580
10707
 
10581
10708
  // src/release.ts
@@ -11293,7 +11420,7 @@ async function bump(o, size) {
11293
11420
  }
11294
11421
  return printed;
11295
11422
  }
11296
- const files = o.repo.release.versionFiles.map((f) => join23(o.cwd, f));
11423
+ const files = o.repo.release.versionFiles.map((f) => join24(o.cwd, f));
11297
11424
  const first = files[0];
11298
11425
  if (!existsSync22(first)) {
11299
11426
  o.emit.error(`no ${o.repo.release.versionFiles[0]} to read a version from`);
@@ -11325,7 +11452,7 @@ async function bump(o, size) {
11325
11452
  function writeChangelog(o, version, entries) {
11326
11453
  const file = o.repo.release.changelog;
11327
11454
  if (!file || entries.length === 0) return;
11328
- const path = join23(o.cwd, file);
11455
+ const path = join24(o.cwd, file);
11329
11456
  const section = renderChangelogSection(version, (/* @__PURE__ */ new Date()).toISOString().slice(0, 10), entries);
11330
11457
  if (o.dryRun) {
11331
11458
  o.emit.emit(`would prepend ${entries.length} entr(ies) to ${file}`);
@@ -11415,7 +11542,7 @@ function closeReleaseCheckout(cwd, root, dir, branch, run2 = gitOk) {
11415
11542
  run2(cwd, ["worktree", "remove", "--force", dir]);
11416
11543
  } finally {
11417
11544
  try {
11418
- rmSync11(root, { recursive: true, force: true });
11545
+ rmSync12(root, { recursive: true, force: true });
11419
11546
  } finally {
11420
11547
  run2(cwd, ["worktree", "prune"]);
11421
11548
  run2(cwd, ["branch", "-D", branch]);
@@ -11425,8 +11552,8 @@ function closeReleaseCheckout(cwd, root, dir, branch, run2 = gitOk) {
11425
11552
  function openReleaseCheckout(o) {
11426
11553
  const { remote, base } = o.repo.branch;
11427
11554
  const branch = `crew/release-${Date.now()}`;
11428
- const root = mkdtempSync2(join23(tmpdir3(), "crew-release-"));
11429
- const dir = join23(root, "checkout");
11555
+ const root = mkdtempSync2(join24(tmpdir3(), "crew-release-"));
11556
+ const dir = join24(root, "checkout");
11430
11557
  o.emit.emit(`release checkout: ${dir}`, { step: "release" });
11431
11558
  const close = () => closeReleaseCheckout(o.cwd, root, dir, branch);
11432
11559
  try {
@@ -11436,10 +11563,10 @@ function openReleaseCheckout(o) {
11436
11563
  throw e;
11437
11564
  }
11438
11565
  for (const f of o.repo.worktrees.copy) {
11439
- const src = join23(o.cwd, f);
11566
+ const src = join24(o.cwd, f);
11440
11567
  if (!existsSync22(src)) continue;
11441
- mkdirSync13(dirname12(join23(dir, f)), { recursive: true });
11442
- copyFileSync(src, join23(dir, f));
11568
+ mkdirSync13(dirname12(join24(dir, f)), { recursive: true });
11569
+ copyFileSync(src, join24(dir, f));
11443
11570
  }
11444
11571
  return { dir, branch, close };
11445
11572
  }
@@ -12586,7 +12713,22 @@ async function provisionReleaseLocks(o, locksModelId, route, repoNames) {
12586
12713
  };
12587
12714
  return ensureLockRows(client2, locksModelId, scopes);
12588
12715
  }
12589
- var ISSUES_TEMPLATE_IDENTIFIER = "crew.issues";
12716
+ var ISSUES_TEMPLATE_IDENTIFIERS = ["tablation.issues", "crew.issues"];
12717
+ async function resolveIssuesTemplateKeys(o) {
12718
+ const keys = new Set(ISSUES_TEMPLATE_IDENTIFIERS);
12719
+ await Promise.all(ISSUES_TEMPLATE_IDENTIFIERS.map(async (identifier) => {
12720
+ try {
12721
+ const t = await get(
12722
+ o,
12723
+ `/library-templates/by-identifier/${encodeURIComponent(identifier)}`
12724
+ );
12725
+ if (t.id) keys.add(t.id);
12726
+ if (t.identifier) keys.add(t.identifier);
12727
+ } catch {
12728
+ }
12729
+ }));
12730
+ return keys;
12731
+ }
12590
12732
  async function listLibraryTemplates(o) {
12591
12733
  return get(o, "/library-templates");
12592
12734
  }
@@ -12627,6 +12769,7 @@ async function discover(o) {
12627
12769
  models: {},
12628
12770
  seats: {},
12629
12771
  holds: [],
12772
+ candidates: [],
12630
12773
  problems,
12631
12774
  provisioning
12632
12775
  };
@@ -12635,6 +12778,7 @@ async function discover(o) {
12635
12778
  out.role = me.role;
12636
12779
  out.meEmail = me.email;
12637
12780
  out.meId = me.id;
12781
+ out.meIdentityId = me.identityId;
12638
12782
  out.meName = me.name;
12639
12783
  } catch {
12640
12784
  }
@@ -12658,7 +12802,8 @@ async function discover(o) {
12658
12802
  }
12659
12803
  } else {
12660
12804
  const projects = await get(o, `/projects?workspaceId=${ws.id}`);
12661
- const defaultCandidate = projects.find((p) => p.sourceTemplateId === ISSUES_TEMPLATE_IDENTIFIER) ?? projects.find((p) => eq(p.slug ?? "", "issues"));
12805
+ const issuesKeys = await resolveIssuesTemplateKeys(o);
12806
+ const defaultCandidate = projects.find((p) => !!p.sourceTemplateId && issuesKeys.has(p.sourceTemplateId)) ?? projects.find((p) => eq(p.slug ?? "", "issues"));
12662
12807
  if (defaultCandidate && (await missingTables(o, defaultCandidate.id)).length === 0) {
12663
12808
  out.projectId = defaultCandidate.id;
12664
12809
  out.projectSlug = defaultCandidate.slug;
@@ -12850,13 +12995,32 @@ async function discover(o) {
12850
12995
  o,
12851
12996
  `/data-models/${out.models.crew}/records?limit=200`
12852
12997
  );
12853
- const scoped = shipRowId ? rows.filter((r) => r.ship_id === shipRowId) : rows;
12854
- const unclaimed = shipRowId ? rows.filter((r) => !r.ship_id) : [];
12998
+ let seatShipId = shipRowId;
12999
+ if (!seatShipId && o.dryRun && o.ship && out.shipsModelId) {
13000
+ try {
13001
+ const shipRows = await get(
13002
+ o,
13003
+ `/data-models/${out.shipsModelId}/records?limit=200`
13004
+ );
13005
+ const byId = o.ship.machineId ? shipRows.filter((r) => r.machine_id === o.ship.machineId) : [];
13006
+ const mineRows = byId.length > 0 ? byId : shipRows.filter((r) => (r.name ?? "").trim() === o.ship.name.trim() && !r.machine_id);
13007
+ if (mineRows.length === 1) seatShipId = mineRows[0].id;
13008
+ } catch {
13009
+ }
13010
+ }
13011
+ const scoped = seatShipId ? rows.filter((r) => r.ship_id === seatShipId) : rows.filter((r) => !r.ship_id);
13012
+ const unclaimed = seatShipId ? rows.filter((r) => !r.ship_id) : [];
12855
13013
  for (const role of ["dev", "design", "qa", "triage"]) {
12856
13014
  const searchTerm = role === "dev" ? "develop" : role;
12857
13015
  let id = seatFor(scoped, searchTerm);
12858
13016
  let seatAction = "matched";
12859
- if (!id && shipRowId) {
13017
+ if (!id && seatShipId && !shipRowId) {
13018
+ const legacyId = seatFor(unclaimed, searchTerm);
13019
+ if (legacyId) {
13020
+ id = legacyId;
13021
+ seatAction = "claimed";
13022
+ }
13023
+ } else if (!id && shipRowId) {
12860
13024
  const legacyId = seatFor(unclaimed, searchTerm);
12861
13025
  if (legacyId) {
12862
13026
  await patch(o, `/data-models/${out.models.crew}/records/${legacyId}`, { ship_id: shipRowId });
@@ -12876,10 +13040,13 @@ async function discover(o) {
12876
13040
  }
12877
13041
  if (id) {
12878
13042
  out.seats[role] = id;
12879
- (out.seatRows ??= []).push({ role, id, action: seatAction });
13043
+ const rowShip = rows.find((r) => r.id === id)?.ship_id ?? shipRowId ?? void 0;
13044
+ (out.seatRows ??= []).push({ role, id, action: seatAction, ...rowShip ? { shipId: rowShip } : {} });
13045
+ } else if (o.dryRun && o.ship && out.shipsModelId) {
13046
+ (out.seatRows ??= []).push({ role, id: "", action: "would-create" });
12880
13047
  } else problems.push(`no Crew row looking like the ${role} seat \u2014 create one, or omit that role`);
12881
13048
  }
12882
- const pairId = seatFor(scoped, "pair") || (shipRowId ? seatFor(unclaimed, "pair") : void 0);
13049
+ const pairId = seatFor(scoped, "pair") || (seatShipId ? seatFor(unclaimed, "pair") : void 0);
12883
13050
  if (pairId) out.seats.pair = pairId;
12884
13051
  if (shipRowId) {
12885
13052
  const releaseId = seatFor(scoped, "release");
@@ -12897,25 +13064,56 @@ async function discover(o) {
12897
13064
  const holdRows = rows.filter((r) => !seatIds.has(r.id));
12898
13065
  out.holds = holdRows.map((r) => ({ id: r.id, name: r.name ?? "", email: r.email, user_id: r.user_id }));
12899
13066
  out.crewModelId = out.models.crew;
13067
+ const people = holdRows.filter((r) => !r.agent_id && (!r.ship_id || !!r.user_id));
13068
+ const members = /* @__PURE__ */ new Map();
13069
+ try {
13070
+ const list = await get(
13071
+ o,
13072
+ `/workspaces/${out.workspaceId}/members`
13073
+ );
13074
+ for (const m of Array.isArray(list) ? list : []) {
13075
+ const key = m.id ?? m.userId;
13076
+ if (key) members.set(key, { identityId: m.identityId, email: m.email });
13077
+ }
13078
+ } catch {
13079
+ }
13080
+ for (const h of out.holds) {
13081
+ const e = h.user_id ? members.get(h.user_id)?.email : void 0;
13082
+ if (e) h.memberEmail = e;
13083
+ }
13084
+ out.candidates = people.map((r) => ({
13085
+ id: r.id,
13086
+ name: r.name ?? "",
13087
+ email: r.email,
13088
+ user_id: r.user_id,
13089
+ detail: r.email || (r.user_id ? members.get(r.user_id)?.email : void 0) || void 0
13090
+ }));
13091
+ const isMine = (r) => !!r.user_id && (r.user_id === out.meId || !!out.meIdentityId && members.get(r.user_id)?.identityId === out.meIdentityId);
13092
+ const emailOf = (r) => [r.email, r.user_id ? members.get(r.user_id)?.email : void 0].filter((e) => !!e);
12900
13093
  if (!out.meId) {
12901
- if (out.holds.length === 0) problems.push("no non-seat Crew rows \u2014 which row is the operator?");
13094
+ if (out.candidates.length === 0) problems.push("no non-seat Crew rows \u2014 which row is the operator?");
12902
13095
  else {
12903
- const byEmail = out.meEmail ? holdRows.filter((r) => eq(r.email, out.meEmail)) : [];
13096
+ const byEmail = out.meEmail ? people.filter((r) => emailOf(r).some((e) => eq(e, out.meEmail))) : [];
12904
13097
  if (byEmail.length === 1) out.operator = byEmail[0].id;
12905
- else if (out.holds.length === 1) out.operator = out.holds[0].id;
13098
+ else if (out.candidates.length === 1) out.operator = out.candidates[0].id;
12906
13099
  }
12907
13100
  } else {
12908
- const candidates = holdRows.filter((r) => !r.user_id || r.user_id === out.meId);
12909
- const mine = holdRows.find((r) => r.user_id === out.meId);
12910
- const byEmail = out.meEmail ? candidates.filter((r) => eq(r.email, out.meEmail)) : [];
12911
- const only = holdRows.length === 1 && candidates.length === 1 ? candidates[0] : void 0;
13101
+ const candidates = people.filter((r) => !r.user_id || isMine(r));
13102
+ const mine = people.find(isMine);
13103
+ const byEmail = out.meEmail ? candidates.filter((r) => emailOf(r).some((e) => eq(e, out.meEmail))) : [];
13104
+ const unlinked = people.filter((r) => !r.user_id);
13105
+ const only = candidates.length === 1 && unlinked.length === 1 ? candidates[0] : void 0;
12912
13106
  const pick = mine ?? (byEmail.length === 1 ? byEmail[0] : only);
12913
13107
  if (pick) {
12914
13108
  out.operator = pick.id;
12915
- out.operatorAction = pick.user_id === out.meId ? "matched" : "linked";
12916
- if (pick.user_id !== out.meId && o.ship) await linkOperatorRow(o, out, pick);
12917
- } else if (o.ship && (!o.interactive || candidates.length === 0)) {
12918
- await createOperatorRow(o, out);
13109
+ out.operatorAction = isMine(pick) ? "matched" : "linked";
13110
+ if (!isMine(pick) && o.ship) await linkOperatorRow(o, out, pick);
13111
+ } else {
13112
+ 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) => {
13113
+ const m = r.user_id ? members.get(r.user_id) : void 0;
13114
+ 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");
13115
+ }).join("; "));
13116
+ if (o.ship && (!o.interactive || candidates.length === 0)) await createOperatorRow(o, out);
12919
13117
  }
12920
13118
  }
12921
13119
  }
@@ -12956,6 +13154,7 @@ async function createOperatorRow(o, found) {
12956
13154
  found.operator = created.id;
12957
13155
  found.operatorAction = "created";
12958
13156
  found.holds.push({ id: created.id, name, email: found.meEmail, user_id: found.meId });
13157
+ found.candidates.push({ id: created.id, name, email: found.meEmail, user_id: found.meId, detail: found.meEmail });
12959
13158
  found.provisioning.push(`created Crew row "${name}" for you`);
12960
13159
  } catch (e) {
12961
13160
  if (!(e instanceof ConnectHttpError) || e.status !== 403) throw e;
@@ -12985,14 +13184,14 @@ ${opts.hostPassengers ? " hostPassengers: true\n" : ""}${apiKeyFileLine}${api
12985
13184
  // src/cli.ts
12986
13185
  import {
12987
13186
  getSessionStore as getSessionStore3,
12988
- hostFromUrl as hostFromUrl3,
13187
+ hostFromUrl as hostFromUrl4,
12989
13188
  loginWithDeviceCode,
12990
13189
  getOrCreateShipId,
12991
13190
  DeviceLoginError
12992
13191
  } from "@tablation/client";
12993
13192
 
12994
13193
  // src/seat-keys.ts
12995
- import { getSessionStore as getSessionStore2, hostFromUrl as hostFromUrl2 } from "@tablation/client";
13194
+ import { getSessionStore as getSessionStore2, hostFromUrl as hostFromUrl3 } from "@tablation/client";
12996
13195
  var SEAT_KEY_ROLES = ROLE_NAMES.filter((r) => r !== "pair");
12997
13196
  function keyIsAdmin(route) {
12998
13197
  const role = route.resolved?.role;
@@ -13016,7 +13215,7 @@ async function resolveReleaseKey(route, getStore = getSessionStore2) {
13016
13215
  const agentId = route.resolved?.releaseAgentId;
13017
13216
  if (!agentId) return ship(keyIsAdmin(route) ? "release seat has no linked Agents row (run `crew agents sync`)" : `release seat ${MEMBER_NO_ROW}`);
13018
13217
  try {
13019
- const session = await getStore().get(hostFromUrl2(route.baseUrl), slugFor(route, agentId));
13218
+ const session = await getStore().get(hostFromUrl3(route.baseUrl), slugFor(route, agentId));
13020
13219
  if (session?.apiKey) return { kind: "agent", apiKey: session.apiKey, agentId };
13021
13220
  return ship(keyIsAdmin(route) ? "no agent key stored for the release seat (run `crew agents sync`)" : MEMBER_NO_KEY);
13022
13221
  } catch {
@@ -13029,7 +13228,7 @@ async function resolveSeatKey(route, role, getStore = getSessionStore2) {
13029
13228
  const agentId = seatAgentId(route, role);
13030
13229
  if (!agentId) return ship(keyIsAdmin(route) ? "seat has no linked Agents row (run `crew agents sync`)" : `seat ${MEMBER_NO_ROW}`);
13031
13230
  try {
13032
- const session = await getStore().get(hostFromUrl2(route.baseUrl), slugFor(route, agentId));
13231
+ const session = await getStore().get(hostFromUrl3(route.baseUrl), slugFor(route, agentId));
13033
13232
  if (session?.apiKey) return { kind: "agent", apiKey: session.apiKey, agentId };
13034
13233
  return ship(keyIsAdmin(route) ? "no agent key stored for this seat (run `crew agents sync`)" : MEMBER_NO_KEY);
13035
13234
  } catch {
@@ -13062,7 +13261,7 @@ async function mintMissingSeatKeys(route, o) {
13062
13261
  detail: describeStoreError(e)
13063
13262
  }));
13064
13263
  }
13065
- const host = hostFromUrl2(route.baseUrl);
13264
+ const host = hostFromUrl3(route.baseUrl);
13066
13265
  const base = route.baseUrl.replace(/\/+$/, "");
13067
13266
  if (!o.dryRun) {
13068
13267
  const probeSlug = seatKeySlug(splitRoute(route.route)[0], "_probe");
@@ -13183,7 +13382,7 @@ async function revokeSeatKeys(route, o) {
13183
13382
  } catch (e) {
13184
13383
  return targets.map(({ role }) => ({ role, action: "failed", detail: describeStoreError(e) }));
13185
13384
  }
13186
- const host = hostFromUrl2(route.baseUrl);
13385
+ const host = hostFromUrl3(route.baseUrl);
13187
13386
  const base = route.baseUrl.replace(/\/+$/, "");
13188
13387
  for (const { role, agentId } of targets) {
13189
13388
  if (!agentId) {
@@ -13440,8 +13639,8 @@ async function showLogEntry(route, entryId, opts = {}) {
13440
13639
  import { isMap as isMap2, parseDocument as parseDocument2, YAMLMap } from "yaml";
13441
13640
 
13442
13641
  // src/repos-cmd.ts
13443
- import { homedir as homedir7 } from "os";
13444
- import { basename as basename8, join as join24, resolve as resolve3 } from "path";
13642
+ import { homedir as homedir8 } from "os";
13643
+ import { basename as basename8, join as join25, resolve as resolve3 } from "path";
13445
13644
  import { isMap, isSeq, parseDocument, stringify as stringify2 } from "yaml";
13446
13645
  var ReposError = class extends Error {
13447
13646
  };
@@ -13487,7 +13686,7 @@ function parseAddArgs(args, routes, isDir) {
13487
13686
  return { route, path: "." };
13488
13687
  }
13489
13688
  function expandDir(p, base) {
13490
- const home = p === "~" ? homedir7() : p.startsWith("~/") ? `${homedir7()}/${p.slice(2)}` : p;
13689
+ const home = p === "~" ? homedir8() : p.startsWith("~/") ? `${homedir8()}/${p.slice(2)}` : p;
13491
13690
  return resolve3(base, home);
13492
13691
  }
13493
13692
  function routeMapOf(doc, route) {
@@ -13695,7 +13894,7 @@ function planRepoClone(i) {
13695
13894
  }
13696
13895
  const ship = doc.get("ship", true);
13697
13896
  const basePath = String(m.get("reposBasePath") ?? (isMap(ship) ? ship.get("reposBasePath") : void 0) ?? "~/Crew");
13698
- const target = i.dir ? expandDir(i.dir, i.base) : join24(expandDir(basePath, i.base), i.route.split("/")[0], name);
13897
+ const target = i.dir ? expandDir(i.dir, i.base) : join25(expandDir(basePath, i.base), i.route.split("/")[0], name);
13699
13898
  const add = planRepoAdd({ text: i.text, base: i.base, route: i.route, path: target, origin: remote, resolved: i.resolved, name });
13700
13899
  return { ...add, remote, target };
13701
13900
  }
@@ -14075,7 +14274,7 @@ ${run2.err}` });
14075
14274
  }
14076
14275
 
14077
14276
  // src/sample-ticket.ts
14078
- import { basename as basename9, dirname as dirname15, join as join25 } from "path";
14277
+ import { basename as basename9, dirname as dirname15, join as join26 } from "path";
14079
14278
  var SAMPLE_REPO = "crew-sample";
14080
14279
  var SAMPLE_REMOTE = `local/${SAMPLE_REPO}`;
14081
14280
  var SAMPLE_MARKER = "sample: true";
@@ -14098,8 +14297,8 @@ has something safe to work on: its test and build hooks always pass, it has no
14098
14297
  remote, and \`crew sample-ticket --remove\` deletes it.
14099
14298
  `;
14100
14299
  function sampleDir(existing, reposBase, workspace) {
14101
- if (existing.length > 0) return join25(dirname15(existing[0]), SAMPLE_REPO);
14102
- return join25(reposBase, workspace, SAMPLE_REPO);
14300
+ if (existing.length > 0) return join26(dirname15(existing[0]), SAMPLE_REPO);
14301
+ return join26(reposBase, workspace, SAMPLE_REPO);
14103
14302
  }
14104
14303
  var SAMPLE_TITLE = "Sample: add a documented `greet` function with a test";
14105
14304
  function sampleDescription() {
@@ -14181,7 +14380,7 @@ function schedulerInstalled(plan, exists, crontab) {
14181
14380
 
14182
14381
  // src/worktree-sweep.ts
14183
14382
  import { existsSync as existsSync25, readFileSync as readFileSync20, readdirSync as readdirSync8 } from "fs";
14184
- import { basename as basename10, dirname as dirname16, join as join26, resolve as resolve4 } from "path";
14383
+ import { basename as basename10, dirname as dirname16, join as join27, resolve as resolve4 } from "path";
14185
14384
  function planWorktreeSweep(target, tickets, contract) {
14186
14385
  const terminal = new Set(contract.statuses.resolved.filter((s) => s !== contract.statuses.verified));
14187
14386
  const verified = contract.statuses.verified;
@@ -14248,7 +14447,7 @@ function siblingWorktreeDirs(target) {
14248
14447
  for (const name of names) {
14249
14448
  if (!name.startsWith(stem)) continue;
14250
14449
  const m = DIR_NUMBER_SUFFIXED.exec(name);
14251
- const path = join26(parent, name);
14450
+ const path = join27(parent, name);
14252
14451
  if (m && !registered.has(resolve4(path))) out.push({ path, number: m[1] });
14253
14452
  }
14254
14453
  return out;
@@ -14456,14 +14655,14 @@ function applyRemoteBranchCleanup(cwd, remote, branches2, dryRun, log) {
14456
14655
  }
14457
14656
 
14458
14657
  // src/stream-sweep.ts
14459
- import { existsSync as existsSync26, readdirSync as readdirSync9, rmSync as rmSync12, statSync as statSync8 } from "fs";
14460
- import { join as join27 } from "path";
14658
+ import { existsSync as existsSync26, readdirSync as readdirSync9, rmSync as rmSync13, statSync as statSync8 } from "fs";
14659
+ import { join as join28 } from "path";
14461
14660
  var DAY_MS2 = 24 * 60 * 60 * 1e3;
14462
14661
  function planStreamSweep(stateDir, retentionDays, now = Date.now()) {
14463
- const dir = join27(stateDir, "streams");
14662
+ const dir = join28(stateDir, "streams");
14464
14663
  if (!existsSync26(dir)) return [];
14465
14664
  const cutoff = now - retentionDays * DAY_MS2;
14466
- return readdirSync9(dir).map((name) => join27(dir, name)).filter((path) => {
14665
+ return readdirSync9(dir).map((name) => join28(dir, name)).filter((path) => {
14467
14666
  try {
14468
14667
  return statSync8(path).mtimeMs < cutoff;
14469
14668
  } catch {
@@ -14479,7 +14678,7 @@ function applyStreamSweep(paths, dryRun, log) {
14479
14678
  continue;
14480
14679
  }
14481
14680
  try {
14482
- rmSync12(path, { force: true });
14681
+ rmSync13(path, { force: true });
14483
14682
  removed++;
14484
14683
  log.emit(`removed stream artifact ${path}`, { step: "worktree" });
14485
14684
  } catch (e) {
@@ -14491,7 +14690,7 @@ function applyStreamSweep(paths, dryRun, log) {
14491
14690
  function planEventsSweep(stateDir, retentionDays, now = Date.now()) {
14492
14691
  if (!existsSync26(stateDir)) return [];
14493
14692
  const cutoff = now - retentionDays * DAY_MS2;
14494
- return readdirSync9(stateDir).filter((name) => ROTATED_EVENTS_RE.test(name)).map((name) => join27(stateDir, name)).filter((path) => {
14693
+ return readdirSync9(stateDir).filter((name) => ROTATED_EVENTS_RE.test(name)).map((name) => join28(stateDir, name)).filter((path) => {
14495
14694
  try {
14496
14695
  return statSync8(path).mtimeMs < cutoff;
14497
14696
  } catch {
@@ -14501,18 +14700,18 @@ function planEventsSweep(stateDir, retentionDays, now = Date.now()) {
14501
14700
  }
14502
14701
 
14503
14702
  // src/cli.ts
14504
- 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";
14703
+ import { realpathSync as realpathSync3, readFileSync as readFileSync27, existsSync as existsSync33, mkdirSync as mkdirSync20, writeFileSync as writeFileSync21, rmSync as rmSync15, unlinkSync as unlinkSync5, copyFileSync as copyFileSync2, renameSync as renameSync2, statSync as statSync10, readdirSync as readdirSync14 } from "fs";
14505
14704
  import { createInterface } from "readline/promises";
14506
14705
  import { dirname as dirOf, resolve as resolvePath } from "path";
14507
14706
 
14508
14707
  // src/ssh-keys.ts
14509
14708
  import { execFileSync as execFileSync5 } from "child_process";
14510
14709
  import { existsSync as existsSync27, mkdirSync as mkdirSync15, readFileSync as readFileSync21, chmodSync } from "fs";
14511
- import { dirname as dirname17, join as join28 } from "path";
14710
+ import { dirname as dirname17, join as join29 } from "path";
14512
14711
  var SshKeyError = class extends Error {
14513
14712
  };
14514
14713
  function shipSshKeyPathFor(stateDir) {
14515
- return join28(stateDir, "ssh", "id_ed25519");
14714
+ return join29(stateDir, "ssh", "id_ed25519");
14516
14715
  }
14517
14716
  function ensureShipSshKeypair(stateDir) {
14518
14717
  const privateKeyPath = shipSshKeyPathFor(stateDir);
@@ -14557,9 +14756,9 @@ function sshKeygenAvailable() {
14557
14756
  import { execFileSync as execFileSync6 } from "child_process";
14558
14757
  import { createHash as createHash4 } from "crypto";
14559
14758
  import { existsSync as existsSync28, mkdirSync as mkdirSync16, readFileSync as readFileSync22, writeFileSync as writeFileSync17 } from "fs";
14560
- import { dirname as dirname18, join as join29 } from "path";
14759
+ import { dirname as dirname18, join as join30 } from "path";
14561
14760
  function machineIdPathFor(stateDir) {
14562
- return join29(stateDir, "machine-id");
14761
+ return join30(stateDir, "machine-id");
14563
14762
  }
14564
14763
  var sha256 = (s) => createHash4("sha256").update(s).digest("hex");
14565
14764
  function readRawMachineId(platform2 = process.platform) {
@@ -14809,7 +15008,7 @@ function syncAllPassengerCheckouts(cfg) {
14809
15008
  import { spawn as spawn5 } from "child_process";
14810
15009
  import { existsSync as existsSync29, mkdirSync as mkdirSync17, readFileSync as readFileSync23, writeFileSync as writeFileSync18, unlinkSync as unlinkSync3, readdirSync as readdirSync10 } from "fs";
14811
15010
  import { randomInt } from "crypto";
14812
- import { join as join30 } from "path";
15011
+ import { join as join31 } from "path";
14813
15012
  var DEFAULT_SPAWN_OPTIONS = { stdio: "ignore", detached: true };
14814
15013
  var defaultSpawn = (cmd, args) => spawn5(cmd, args, DEFAULT_SPAWN_OPTIONS);
14815
15014
  var SLUG_ALPHABET = "abcdefghijklmnopqrstuvwxyz0123456789";
@@ -14876,7 +15075,7 @@ function startTunnel(opts) {
14876
15075
  };
14877
15076
  }
14878
15077
  function tunnelStatePath(stateDir, workspaceId) {
14879
- return join30(stateDir, "passengers", `${workspaceId}.json`);
15078
+ return join31(stateDir, "passengers", `${workspaceId}.json`);
14880
15079
  }
14881
15080
  function readPersistedTunnel(stateDir, workspaceId) {
14882
15081
  const path = tunnelStatePath(stateDir, workspaceId);
@@ -14889,7 +15088,7 @@ function readPersistedTunnel(stateDir, workspaceId) {
14889
15088
  }
14890
15089
  function writePersistedTunnel(stateDir, state) {
14891
15090
  const path = tunnelStatePath(stateDir, state.workspaceId);
14892
- mkdirSync17(join30(stateDir, "passengers"), { recursive: true });
15091
+ mkdirSync17(join31(stateDir, "passengers"), { recursive: true });
14893
15092
  writeFileSync18(path, `${JSON.stringify(state, null, 2)}
14894
15093
  `);
14895
15094
  }
@@ -14987,7 +15186,7 @@ function syncPassengerTunnels(plans, relayHost, relayPort, privateKeyPath, state
14987
15186
  }
14988
15187
  }
14989
15188
  function readAllPersistedWorkspaceIds(stateDir) {
14990
- const dir = join30(stateDir, "passengers");
15189
+ const dir = join31(stateDir, "passengers");
14991
15190
  if (!existsSync29(dir)) return [];
14992
15191
  return readdirSync10(dir).filter((f) => f.endsWith(".json") && !f.endsWith("-sync.json")).map((f) => f.slice(0, -".json".length));
14993
15192
  }
@@ -15110,7 +15309,7 @@ import { createServer as createHttpServer } from "http";
15110
15309
  import { spawn as spawn6 } from "child_process";
15111
15310
  import { randomBytes } from "crypto";
15112
15311
  import { existsSync as existsSync30, mkdirSync as mkdirSync18, readFileSync as readFileSync24, writeFileSync as writeFileSync19, unlinkSync as unlinkSync4, readdirSync as readdirSync11 } from "fs";
15113
- import { join as join31 } from "path";
15312
+ import { join as join32 } from "path";
15114
15313
 
15115
15314
  // src/secret-compare.ts
15116
15315
  import { timingSafeEqual } from "crypto";
@@ -15199,7 +15398,7 @@ var defaultSpawn2 = (cmd, args, env) => {
15199
15398
  return child;
15200
15399
  };
15201
15400
  function syncDaemonStatePath(stateDir, workspaceId) {
15202
- return join31(stateDir, "passengers", `${workspaceId}-sync.json`);
15401
+ return join32(stateDir, "passengers", `${workspaceId}-sync.json`);
15203
15402
  }
15204
15403
  function readPersistedSyncDaemon(stateDir, workspaceId) {
15205
15404
  const path = syncDaemonStatePath(stateDir, workspaceId);
@@ -15212,7 +15411,7 @@ function readPersistedSyncDaemon(stateDir, workspaceId) {
15212
15411
  }
15213
15412
  function writePersistedSyncDaemon(stateDir, state) {
15214
15413
  const path = syncDaemonStatePath(stateDir, state.workspaceId);
15215
- mkdirSync18(join31(stateDir, "passengers"), { recursive: true });
15414
+ mkdirSync18(join32(stateDir, "passengers"), { recursive: true });
15216
15415
  writeFileSync19(path, `${JSON.stringify(state, null, 2)}
15217
15416
  `);
15218
15417
  }
@@ -15233,7 +15432,7 @@ function waitForExit(pid, alive, timeoutMs = 5e3) {
15233
15432
  }
15234
15433
  }
15235
15434
  function readAllPersistedSyncWorkspaceIds(stateDir) {
15236
- const dir = join31(stateDir, "passengers");
15435
+ const dir = join32(stateDir, "passengers");
15237
15436
  if (!existsSync30(dir)) return [];
15238
15437
  const suffix = "-sync.json";
15239
15438
  return readdirSync11(dir).filter((f) => f.endsWith(suffix)).map((f) => f.slice(0, -suffix.length));
@@ -15322,12 +15521,12 @@ function describeCause(err) {
15322
15521
  }
15323
15522
 
15324
15523
  // src/daemon.ts
15325
- import { existsSync as existsSync32, mkdirSync as mkdirSync19, readFileSync as readFileSync26, readdirSync as readdirSync13, rmSync as rmSync13, statSync as statSync9, writeFileSync as writeFileSync20 } from "fs";
15326
- import { join as join33 } from "path";
15524
+ import { existsSync as existsSync32, mkdirSync as mkdirSync19, readFileSync as readFileSync26, readdirSync as readdirSync13, rmSync as rmSync14, statSync as statSync9, writeFileSync as writeFileSync20 } from "fs";
15525
+ import { join as join34 } from "path";
15327
15526
 
15328
15527
  // src/interrupted.ts
15329
15528
  import { existsSync as existsSync31, readdirSync as readdirSync12, readFileSync as readFileSync25 } from "fs";
15330
- import { join as join32 } from "path";
15529
+ import { join as join33 } from "path";
15331
15530
  var INTERRUPTED_MARKER = calloutHeader("NOTE", "Interrupted");
15332
15531
  function interruptedComment(b) {
15333
15532
  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.";
@@ -15354,13 +15553,13 @@ function runFinished(streamPath) {
15354
15553
  }
15355
15554
  }
15356
15555
  function orphanedStreams(o) {
15357
- const dir = join32(o.stateDir, "streams");
15556
+ const dir = join33(o.stateDir, "streams");
15358
15557
  if (!existsSync31(dir)) return [];
15359
15558
  const ownerAlive = o.isOwnerAlive ?? pidAlive;
15360
15559
  const out = [];
15361
15560
  for (const name of readdirSync12(dir)) {
15362
15561
  if (!name.endsWith(".jsonl") || name.endsWith(".events.jsonl")) continue;
15363
- const streamPath = join32(dir, name);
15562
+ const streamPath = join33(dir, name);
15364
15563
  if (runFinished(streamPath)) continue;
15365
15564
  const sidecar = readRunSidecar(streamPath);
15366
15565
  if (!sidecar || sidecar.route !== o.routeLabel) continue;
@@ -15480,7 +15679,7 @@ function latestMtime(path) {
15480
15679
  if (!stat.isDirectory()) return 0;
15481
15680
  let latest = stat.mtimeMs;
15482
15681
  for (const entry of readdirSync13(path, { withFileTypes: true })) {
15483
- latest = Math.max(latest, latestMtime(join33(path, entry.name)));
15682
+ latest = Math.max(latest, latestMtime(join34(path, entry.name)));
15484
15683
  }
15485
15684
  return latest;
15486
15685
  }
@@ -15492,14 +15691,14 @@ function makeStaleChecker(paths, opts = {}) {
15492
15691
  return () => computeTreeSignature(paths) !== initial || opts.reloadSentinel !== void 0 && existsSync32(opts.reloadSentinel);
15493
15692
  }
15494
15693
  function reloadSentinelFor(stateDir) {
15495
- return join33(stateDir, "reload-requested");
15694
+ return join34(stateDir, "reload-requested");
15496
15695
  }
15497
15696
  function daemonStampFor(stateDir) {
15498
- return join33(stateDir, "daemon.started");
15697
+ return join34(stateDir, "daemon.started");
15499
15698
  }
15500
15699
  function noteDaemonStarted(stateDir, now = Date.now()) {
15501
15700
  mkdirSync19(stateDir, { recursive: true });
15502
- rmSync13(reloadSentinelFor(stateDir), { force: true });
15701
+ rmSync14(reloadSentinelFor(stateDir), { force: true });
15503
15702
  writeFileSync20(daemonStampFor(stateDir), `${JSON.stringify({ pid: process.pid, startedAt: now })}
15504
15703
  `);
15505
15704
  }
@@ -15934,7 +16133,7 @@ async function pickOperator(holds2, me) {
15934
16133
  const choices = holds2.filter((h) => !h.user_id || h.user_id === me.id);
15935
16134
  const canCreate = !!me.id;
15936
16135
  process.stderr.write("\nWhich of these Crew rows is you?\n");
15937
- choices.forEach((h, i) => process.stderr.write(` ${i + 1}. ${h.name || h.id}
16136
+ choices.forEach((h, i) => process.stderr.write(` ${i + 1}. ${h.name || "unnamed row"}${h.detail ? ` <${h.detail}>` : ""}
15938
16137
  `));
15939
16138
  if (canCreate) process.stderr.write(` 0. Create a new Crew row for ${me.name ?? ""} ${me.email ?? ""}`.trimEnd() + "\n");
15940
16139
  const rl = createInterface({ input: process.stdin, output: process.stderr });
@@ -15968,10 +16167,11 @@ async function pickResolvedStatuses(unrecognized) {
15968
16167
  }
15969
16168
  async function offerTemplateInstall(authOpts, workspaceId, decided) {
15970
16169
  const templates = await listLibraryTemplates(authOpts);
15971
- const template = templates.find((t) => t.identifier === ISSUES_TEMPLATE_IDENTIFIER);
16170
+ const keys = await resolveIssuesTemplateKeys(authOpts);
16171
+ const template = templates.find((t) => t.identifier === ISSUES_TEMPLATE_IDENTIFIERS[0]) ?? templates.find((t) => keys.has(t.identifier) || keys.has(t.id));
15972
16172
  if (!template) {
15973
16173
  process.stderr.write(`
15974
- No template with identifier "${ISSUES_TEMPLATE_IDENTIFIER}" is published to the Library yet \u2014 nothing to offer to install.
16174
+ No template with identifier "${ISSUES_TEMPLATE_IDENTIFIERS[0]}" is published to the Library yet \u2014 nothing to offer to install.
15975
16175
  `);
15976
16176
  return void 0;
15977
16177
  }
@@ -16009,8 +16209,8 @@ No project here has what a route needs. Install the "${template.name}" library t
16009
16209
  `);
16010
16210
  return installed.project.slug;
16011
16211
  }
16012
- function usage() {
16013
- process.stderr.write(`crew \u2014 a standing team of headless agents
16212
+ function usage(problem, requested = false) {
16213
+ const text = `crew \u2014 a standing team of headless agents
16014
16214
 
16015
16215
  crew poll [route] decide a cycle and report it; writes nothing
16016
16216
  crew run [route] [--role R] run the winning role's session
@@ -16018,6 +16218,7 @@ function usage() {
16018
16218
  crew merge [route] merge verified branches and stop
16019
16219
  crew deploy [route] release now, even with nothing new to merge
16020
16220
  crew watch [route] live view of what the crew is doing
16221
+ crew --version [--json] print the crew version on one line (also: crew version); --json adds home and compiled
16021
16222
  crew status [route] [--json] paused/running state; --json for a machine reader
16022
16223
  crew projects [--route R] [--json] the Projects rows each route can read (id, name, prefix, work mode)
16023
16224
  crew whoami [route] [--json] the signed-in identity, key role, operator Crew row and credential source per route
@@ -16037,6 +16238,7 @@ function usage() {
16037
16238
  resume with no argument clears only the whole-crew pause; a role or release pause needs its own resume
16038
16239
  crew log [route] tail the log
16039
16240
  crew inbox [--member NAME] your tickets across every workspace (or a colleague's)
16241
+ 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)
16040
16242
  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)
16041
16243
  crew disconnect remove what connect installed for Pair (Claude MCP; Codex/Gemini instructions block and MCP entry per ship.pair)
16042
16244
  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)
@@ -16071,10 +16273,26 @@ Options:
16071
16273
  --all (inbox) include closed tickets
16072
16274
 
16073
16275
  In watch: space pauses the stream, q quits.
16074
- `);
16276
+ `;
16277
+ if (requested) {
16278
+ process.stdout.write(text);
16279
+ process.exit(0);
16280
+ }
16281
+ process.stderr.write(problem ? `crew: ${problem}
16282
+
16283
+ ${text}` : text);
16075
16284
  process.exit(2);
16076
16285
  }
16077
16286
  var argv = process.argv.slice(2);
16287
+ if (helpRequest(argv)) usage(void 0, true);
16288
+ var versionReq = versionRequest(argv);
16289
+ if (versionReq) {
16290
+ const version = crewVersion(CREW_HOME);
16291
+ process.stdout.write(versionReq.json ? `${JSON.stringify({ version, home: CREW_HOME, compiled: isCompiledBinary(import.meta.url) })}
16292
+ ` : `${version}
16293
+ `);
16294
+ process.exit(0);
16295
+ }
16078
16296
  var flag = (name) => argv.includes(`--${name}`);
16079
16297
  var value = (name) => {
16080
16298
  const i = argv.indexOf(`--${name}`);
@@ -16086,7 +16304,45 @@ var connectEvents = createEmitter(command === "connect" && flag("json"), (l) =>
16086
16304
  process.stdout.write(l);
16087
16305
  });
16088
16306
  var connectChoices = parseChoices(argv);
16089
- if (!command) usage();
16307
+ if (!command) usage(argv.length ? `unknown option "${argv[0]}"` : void 0);
16308
+ var KNOWN_COMMANDS = /* @__PURE__ */ new Set([
16309
+ "passenger-sync-daemon",
16310
+ "agents",
16311
+ "connect",
16312
+ "daemon",
16313
+ "deploy",
16314
+ "disconnect",
16315
+ "doctor",
16316
+ "drop",
16317
+ "inbox",
16318
+ "install",
16319
+ "log",
16320
+ "logbook",
16321
+ "merge",
16322
+ "passengers",
16323
+ "pause",
16324
+ "poll",
16325
+ "ports",
16326
+ "projects",
16327
+ "reap",
16328
+ "release",
16329
+ "reload",
16330
+ "repos",
16331
+ "resume",
16332
+ "rotate-passenger-url",
16333
+ "run",
16334
+ "sample-ticket",
16335
+ "selftest",
16336
+ "skills",
16337
+ "status",
16338
+ "sync",
16339
+ "unassign",
16340
+ "uninstall",
16341
+ "watch",
16342
+ "whoami",
16343
+ "worktree"
16344
+ ]);
16345
+ if (!KNOWN_COMMANDS.has(command)) usage(`unknown command "${command}"`);
16090
16346
  function findSyncRoute(c, name) {
16091
16347
  if (!name && c.routes.length > 1) {
16092
16348
  throw new ConfigError(`this ship has ${c.routes.length} routes \u2014 name one: ${c.routes.map((r) => r.route).join(", ")}`);
@@ -16176,6 +16432,7 @@ if (command === "passenger-sync-daemon") {
16176
16432
  let cfg;
16177
16433
  let firstRun;
16178
16434
  let firstRunFromFlags = false;
16435
+ let agentBinSearched;
16179
16436
  try {
16180
16437
  const connectIsTTY = !!(process.stdin.isTTY && process.stdout.isTTY);
16181
16438
  const wizardFlags = {
@@ -16185,7 +16442,7 @@ if (command === "passenger-sync-daemon") {
16185
16442
  hostPassengers: flag("host-passengers"),
16186
16443
  noWizard: flag("no-wizard")
16187
16444
  };
16188
- if (command === "connect" && usesFlagAnswers(wizardFlags, connectIsTTY) && shouldRunWizard({ isTTY: true, crewHome: CREW_HOME })) {
16445
+ if (command === "connect" && !flag("sign-out") && usesFlagAnswers(wizardFlags, connectIsTTY) && shouldRunWizard({ isTTY: true, crewHome: CREW_HOME })) {
16189
16446
  const flagged = flagWizardAnswers(wizardFlags, {
16190
16447
  hostname: hostname2(),
16191
16448
  platform: hostPlatform(),
@@ -16202,8 +16459,9 @@ if (command === "passenger-sync-daemon") {
16202
16459
  const configPath = firstRunConfigPath();
16203
16460
  firstRun = { answers: flagged.answers, configPath };
16204
16461
  firstRunFromFlags = true;
16462
+ agentBinSearched = flagged.agentBinSearched;
16205
16463
  cfg = loadConfig(CREW_HOME, configPath, { text: renderShipBlock(flagged.answers), allowNoRoutes: true });
16206
- } else if (command === "connect" && shouldRunWizard({
16464
+ } else if (command === "connect" && !flag("sign-out") && shouldRunWizard({
16207
16465
  isTTY: connectIsTTY,
16208
16466
  crewHome: CREW_HOME
16209
16467
  })) {
@@ -16224,7 +16482,13 @@ if (command === "passenger-sync-daemon") {
16224
16482
  firstRun = { answers, configPath };
16225
16483
  cfg = loadConfig(CREW_HOME, configPath, { text: renderShipBlock(answers), allowNoRoutes: true });
16226
16484
  } else {
16227
- cfg = loadConfig(CREW_HOME, void 0, command === "repos" || command === "disconnect" && flag("reset") ? { allowNoRoutes: true } : {});
16485
+ const toleratesNoRoutes = command === "repos" || command === "disconnect" && flag("reset") || command === "connect" && flag("sign-out");
16486
+ const configless = command === "connect" && flag("sign-out") || command === "disconnect" && flag("reset");
16487
+ if (configless && !existsSync33(findConfigFile(CREW_HOME))) {
16488
+ cfg = loadConfig(CREW_HOME, firstRunConfigPath(), { text: "ship: {}\n", allowNoRoutes: true });
16489
+ } else {
16490
+ cfg = loadConfig(CREW_HOME, void 0, toleratesNoRoutes ? { allowNoRoutes: true } : {});
16491
+ }
16228
16492
  }
16229
16493
  } catch (e) {
16230
16494
  if (e instanceof ConfigError) {
@@ -16458,7 +16722,7 @@ if (command === "passenger-sync-daemon") {
16458
16722
  }
16459
16723
  if (host === "linux" && hasSystemd) {
16460
16724
  const label = labelFor(CREW_HOME, "run");
16461
- const staleTimer = join34(homedir8(), ".config", "systemd", "user", `${label}.timer`);
16725
+ const staleTimer = join35(homedir9(), ".config", "systemd", "user", `${label}.timer`);
16462
16726
  if (existsSync33(staleTimer)) {
16463
16727
  process.stdout.write(
16464
16728
  `removing stale run timer from before the persistent-service cutover: ${staleTimer}${dryRun ? " (dry run)" : ""}
@@ -16706,7 +16970,10 @@ ${changed2.map((c) => ` - ${c}
16706
16970
  }
16707
16971
  if (!dryRun) {
16708
16972
  const holderLabel = `${cfg.ship.name}:${process.pid}`;
16709
- const got = await tracker.acquireBoardLock(scope, holderLabel, RELEASE_LOCK_TTL_MS);
16973
+ const got = await tracker.acquireBoardLock(scope, holderLabel, RELEASE_LOCK_TTL_MS, {
16974
+ warn: (m) => remit.warn(m, { step: "release" }),
16975
+ debug: (m) => remit.emit(m, { step: "release" })
16976
+ });
16710
16977
  if (!got.ok && got.reason === "missing") {
16711
16978
  const detail = `no Locks row for ${scope} \u2014 run \`crew connect ${c.route}\` to provision it`;
16712
16979
  remit.warn(`release for ${scope} refused: ${detail}`, { step: "release" });
@@ -17521,7 +17788,7 @@ ${changed2.map((c) => ` - ${c}
17521
17788
  process.exit(1);
17522
17789
  }
17523
17790
  requireArmed("run the daemon loop");
17524
- const watchPaths = isCompiledBinary(import.meta.url) ? [process.execPath] : [join34(CREW_HOME, "src"), join34(CREW_HOME, "dist"), join34(CREW_HOME, "bin")];
17791
+ const watchPaths = isCompiledBinary(import.meta.url) ? [process.execPath] : [join35(CREW_HOME, "src"), join35(CREW_HOME, "dist"), join35(CREW_HOME, "bin")];
17525
17792
  const configPaths = daemonConfigPaths(cfg);
17526
17793
  watchPaths.push(...configPaths);
17527
17794
  noteDaemonStarted(cfg.ship.stateDir);
@@ -17865,7 +18132,7 @@ ${changed2.map((c) => ` - ${c}
17865
18132
  if (sub === "list" && !routeName) usage2();
17866
18133
  if (sub === "add") {
17867
18134
  try {
17868
- const a = parseAddArgs(positional.slice(2), routeNamesOf(configText), (p) => existsSync33(resolvePath(p.replace(/^~(?=\/|$)/, homedir8()))));
18135
+ const a = parseAddArgs(positional.slice(2), routeNamesOf(configText), (p) => existsSync33(resolvePath(p.replace(/^~(?=\/|$)/, homedir9()))));
17869
18136
  routeName = a.route;
17870
18137
  addPath = a.path;
17871
18138
  addName = a.name;
@@ -17967,7 +18234,7 @@ ${changed2.map((c) => ` - ${c}
17967
18234
  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}`);
17968
18235
  break;
17969
18236
  }
17970
- const target = resolvePath(addPath.replace(/^~(?=\/|$)/, homedir8()));
18237
+ const target = resolvePath(addPath.replace(/^~(?=\/|$)/, homedir9()));
17971
18238
  const top = existsSync33(target) ? gitOk(target, ["rev-parse", "--show-toplevel"]) : null;
17972
18239
  if (!top) throw new ReposError(`${target} is not a git checkout`);
17973
18240
  const origin = originOf(top);
@@ -18119,6 +18386,29 @@ ${reconstruction.prompt}
18119
18386
  }
18120
18387
  case "connect": {
18121
18388
  const arg = positional[1];
18389
+ if (flag("sign-out")) {
18390
+ let signOutStore;
18391
+ try {
18392
+ signOutStore = getSessionStore3();
18393
+ } catch {
18394
+ signOutStore = void 0;
18395
+ }
18396
+ const outcomes = signOutStore ? await signOutSessions({
18397
+ store: signOutStore,
18398
+ stateDir: cfg.ship.stateDir,
18399
+ dryRun,
18400
+ host: value("host") ?? (value("base-url") ? hostFromUrl4(value("base-url")) : void 0),
18401
+ workspace: value("workspace")
18402
+ }) : [];
18403
+ const failed = outcomes.some((o) => o.action === "failed");
18404
+ if (flag("json")) process.stdout.write(`${JSON.stringify({ dryRun, ok: !failed, sessions: outcomes })}
18405
+ `);
18406
+ else if (!outcomes.length) process.stdout.write("nothing to sign out of\n");
18407
+ else for (const o of outcomes) process.stdout.write(`${o.action}: ${o.host}/${o.workspace}${o.detail ? ` (${o.detail})` : ""}
18408
+ `);
18409
+ if (failed) process.exitCode = 1;
18410
+ break;
18411
+ }
18122
18412
  const canAsk = !!(process.stdin.isTTY && process.stdout.isTTY) && !connectEvents.json;
18123
18413
  const stopForChoice = (key, message, options) => {
18124
18414
  connectEvents.emit("choice", message, choiceData(key, options));
@@ -18136,6 +18426,7 @@ ${reconstruction.prompt}
18136
18426
  const project = argProject ?? value("project") ?? connectChoices.project;
18137
18427
  const baseUrl = value("base-url") ?? cfg.routes[0]?.baseUrl ?? DEFAULT_BASE_URL;
18138
18428
  let apiKey = value("key") ?? process.env.CREW_CONNECT_KEY ?? cfg.routes.find((c) => c.apiKey)?.apiKey;
18429
+ const givenKeySource = value("key") ?? process.env.CREW_CONNECT_KEY ? "flag" : "config";
18139
18430
  let store;
18140
18431
  try {
18141
18432
  store = getSessionStore3();
@@ -18145,11 +18436,11 @@ ${reconstruction.prompt}
18145
18436
  let keychainBacked = false;
18146
18437
  if (!apiKey && workspace && store) {
18147
18438
  try {
18148
- const existing = await store.get(hostFromUrl3(baseUrl), workspace);
18439
+ const existing = await store.get(hostFromUrl4(baseUrl), workspace);
18149
18440
  if (existing?.apiKey) {
18150
18441
  apiKey = existing.apiKey;
18151
18442
  keychainBacked = true;
18152
- process.stderr.write(`Reusing the existing keychain session for ${hostFromUrl3(baseUrl)}/${workspace}.
18443
+ process.stderr.write(`Reusing the existing keychain session for ${hostFromUrl4(baseUrl)}/${workspace}.
18153
18444
  `);
18154
18445
  }
18155
18446
  } catch {
@@ -18160,7 +18451,7 @@ ${reconstruction.prompt}
18160
18451
  const resumed = await resumeKeychainSession({
18161
18452
  store,
18162
18453
  stateDir: cfg.ship.stateDir,
18163
- host: hostFromUrl3(baseUrl),
18454
+ host: hostFromUrl4(baseUrl),
18164
18455
  verify: async (k) => {
18165
18456
  try {
18166
18457
  await get({ baseUrl, apiKey: k, userAgent: cfg.ship.userAgent }, "/auth/me");
@@ -18175,7 +18466,7 @@ ${reconstruction.prompt}
18175
18466
  workspace = resumed.workspace;
18176
18467
  keychainBacked = true;
18177
18468
  signInSource = "keychain";
18178
- process.stderr.write(`Reusing the existing keychain session for ${hostFromUrl3(baseUrl)}/${workspace}.
18469
+ process.stderr.write(`Reusing the existing keychain session for ${hostFromUrl4(baseUrl)}/${workspace}.
18179
18470
  `);
18180
18471
  }
18181
18472
  }
@@ -18232,13 +18523,14 @@ Signed in as ${result.identity.email} \u2014 minted an API key in workspace "${r
18232
18523
  connectEvents.emit("signed_in", `signed in as ${result.identity.email}`, {
18233
18524
  identity: { id: result.identity.id, email: result.identity.email },
18234
18525
  workspaces: [{ slug: result.workspace.slug, name: result.workspace.name }],
18235
- session: "device"
18526
+ session: "device",
18527
+ host: hostFromUrl4(baseUrl)
18236
18528
  });
18237
18529
  const deviceWorkspace = "path" in result.workspace && typeof result.workspace.path === "string" ? result.workspace.path : result.workspace.slug;
18238
18530
  if (!workspace) workspace = deviceWorkspace;
18239
18531
  if (store) {
18240
18532
  try {
18241
- await store.set(hostFromUrl3(baseUrl), deviceWorkspace, {
18533
+ await store.set(hostFromUrl4(baseUrl), deviceWorkspace, {
18242
18534
  apiKey: result.apiKey.key,
18243
18535
  shipId: getOrCreateShipId(),
18244
18536
  workspaceId: result.workspace.id,
@@ -18246,9 +18538,9 @@ Signed in as ${result.identity.email} \u2014 minted an API key in workspace "${r
18246
18538
  createdAt: result.apiKey.createdAt
18247
18539
  });
18248
18540
  keychainBacked = true;
18249
- rememberSession(cfg.ship.stateDir, hostFromUrl3(baseUrl), deviceWorkspace);
18541
+ rememberSession(cfg.ship.stateDir, hostFromUrl4(baseUrl), deviceWorkspace);
18250
18542
  process.stderr.write(
18251
- `API key stored in the OS keychain for ${hostFromUrl3(baseUrl)}/${deviceWorkspace}.
18543
+ `API key stored in the OS keychain for ${hostFromUrl4(baseUrl)}/${deviceWorkspace}.
18252
18544
  `
18253
18545
  );
18254
18546
  } catch (e) {
@@ -18293,7 +18585,8 @@ Signed in as ${result.identity.email} \u2014 minted an API key in workspace "${r
18293
18585
  connectEvents.emit("signed_in", `key belongs to ${found.meEmail ?? found.meId}`, {
18294
18586
  identity: { id: found.meId, email: found.meEmail, name: found.meName },
18295
18587
  workspaces: [{ slug: found.workspaceSlug, name: found.workspaceName, role: found.role }],
18296
- session: signInSource
18588
+ session: signInSource,
18589
+ host: hostFromUrl4(baseUrl)
18297
18590
  });
18298
18591
  }
18299
18592
  connectEvents.emit("workspace_resolved", `workspace ${found.workspaceSlug}`, {
@@ -18368,8 +18661,8 @@ Signed in as ${result.identity.email} \u2014 minted an API key in workspace "${r
18368
18661
  }
18369
18662
  }
18370
18663
  let operator = found.operator;
18371
- if (!operator && found.holds.length > 0 && canAsk) {
18372
- const picked = await pickOperator(found.holds, { id: found.meId, name: found.meName, email: found.meEmail });
18664
+ if (!operator && found.candidates.length > 0 && canAsk) {
18665
+ const picked = await pickOperator(found.candidates, { id: found.meId, name: found.meName, email: found.meEmail });
18373
18666
  if (picked === NEW_OPERATOR_ROW) {
18374
18667
  await createOperatorRow({ ...authOpts, dryRun }, found);
18375
18668
  operator = found.operator;
@@ -18388,7 +18681,7 @@ Signed in as ${result.identity.email} \u2014 minted an API key in workspace "${r
18388
18681
  }
18389
18682
  }
18390
18683
  if (!operator) operator = previous?.operator;
18391
- if (connectEvents.json && !operator && found.holds.length > 0) {
18684
+ if (connectEvents.json && !operator && found.candidates.length > 0) {
18392
18685
  const answer = connectChoices.operator;
18393
18686
  if (answer === "new") {
18394
18687
  await createOperatorRow({ ...authOpts, dryRun }, found);
@@ -18403,27 +18696,36 @@ Signed in as ${result.identity.email} \u2014 minted an API key in workspace "${r
18403
18696
  found.operatorAction = "linked";
18404
18697
  }
18405
18698
  } else {
18406
- const mine = found.holds.filter((h) => !h.user_id || h.user_id === found.meId);
18699
+ const mine = found.candidates.filter((h) => !h.user_id || h.user_id === found.meId);
18407
18700
  stopForChoice("operator", "Which Crew row is you?", [
18408
- ...mine.map((h) => ({ value: h.id, label: h.name || h.id })),
18701
+ ...mine.map((h) => ({ value: h.id, label: h.name || "unnamed row", ...h.detail ? { detail: h.detail } : { detail: h.id } })),
18409
18702
  ...found.meId ? [{ value: "new", label: `Create a new Crew row for ${found.meName ?? found.meEmail ?? "me"}` }] : []
18410
18703
  ]);
18411
18704
  }
18412
18705
  }
18706
+ if (!operator && found.operatorCompared) {
18707
+ process.stderr.write(`crew connect: no Crew row matched you \u2014 ${found.operatorCompared}
18708
+ `);
18709
+ connectEvents.emit("operator_row", "no Crew row matched you", { action: "unmatched", compared: found.operatorCompared });
18710
+ }
18413
18711
  if (operator) {
18414
- connectEvents.emit("operator_row", `operator ${found.holds.find((h) => h.id === operator)?.name ?? operator}`, {
18712
+ const opRow = found.holds.find((h) => h.id === operator);
18713
+ const opEmail = opRow?.email || opRow?.memberEmail || (opRow?.user_id && opRow.user_id === found.meId ? found.meEmail : void 0);
18714
+ connectEvents.emit("operator_row", `operator ${opRow?.name ?? operator}`, {
18415
18715
  id: operator,
18416
- action: found.operatorAction ?? "previous"
18716
+ action: found.operatorAction ?? "previous",
18717
+ ...opRow?.name ? { name: opRow.name } : {},
18718
+ ...opEmail ? { email: opEmail } : {}
18417
18719
  });
18418
18720
  }
18419
18721
  if (!operator && !hadPreviousFile) {
18420
18722
  process.stderr.write(
18421
- `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.
18723
+ `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.
18422
18724
  Re-run from a real terminal to pick one, or pass --key for a key whose own
18423
18725
  identity email matches exactly one Crew row.
18424
18726
  `
18425
18727
  );
18426
- stopWithProblem(`cannot determine the operator \u2014 ${found.holds.length} Crew row(s) could be it`, "operator");
18728
+ stopWithProblem(`cannot determine the operator \u2014 ${found.candidates.length} Crew row(s) could be it`, "operator");
18427
18729
  }
18428
18730
  const alreadyReviewed = new Set(previous?.reviewedStatuses ?? []);
18429
18731
  const stillUnrecognized = (found.unrecognizedStatuses ?? []).filter((s) => !alreadyReviewed.has(s.value));
@@ -18518,12 +18820,19 @@ Signed in as ${result.identity.email} \u2014 minted an API key in workspace "${r
18518
18820
  role: sr.role,
18519
18821
  id: sr.id,
18520
18822
  action: sr.action,
18823
+ ...sr.shipId ? { ship_id: sr.shipId } : {},
18521
18824
  key: minted?.action ?? "none",
18522
18825
  ...minted?.detail ? { detail: minted.detail } : {}
18523
18826
  });
18524
18827
  }
18525
18828
  let apiKeyFile;
18526
18829
  const apiKeyVar = "CREW_API_KEY";
18830
+ if (firstRunFromFlags && agentBinSearched && !dryRun) {
18831
+ const msg = `no agent binary: "claude" was not found on PATH or in ${agentBinSearched.join(", ")} \u2014 pass --agent-bin /absolute/path`;
18832
+ process.stderr.write(`crew connect: ${msg}
18833
+ `);
18834
+ stopWithProblem(msg, "wizard");
18835
+ }
18527
18836
  if ((mintedFromDevice || firstRunFromFlags) && !keychainBacked) {
18528
18837
  apiKeyFile = apiKeyPathFor(cfg.ship.stateDir, route2);
18529
18838
  if (dryRun) {
@@ -18540,7 +18849,7 @@ Signed in as ${result.identity.email} \u2014 minted an API key in workspace "${r
18540
18849
  const keyPersisted = keychainBacked || apiKeyFile !== void 0;
18541
18850
  const wizard = firstRun;
18542
18851
  const writeFirstRun = wizard !== void 0 && !dryRun && keyPersisted;
18543
- const routeDir = value("dir") ?? (writeFirstRun ? join34(DEFAULT_REPOS_BASE_PATH, found.workspaceSlug) : void 0);
18852
+ const routeDir = value("dir") ?? (writeFirstRun ? join35(DEFAULT_REPOS_BASE_PATH, found.workspaceSlug) : void 0);
18544
18853
  const routeBlock = renderConnection(
18545
18854
  found,
18546
18855
  route2,
@@ -18689,13 +18998,39 @@ Signed in as ${result.identity.email} \u2014 minted an API key in workspace "${r
18689
18998
  wroteConfig,
18690
18999
  operator,
18691
19000
  dryRun,
18692
- unresolved: found.problems.length
19001
+ unresolved: found.problems.length,
19002
+ ...firstRunFromFlags ? { agentBin: agentBinSearched ? null : firstRun?.answers.agentBin } : {},
19003
+ ...agentBinSearched ? { agentBinNote: `no \`claude\` found on PATH or in ${agentBinSearched.join(", ")}; a real run needs --agent-bin` } : {}
18693
19004
  });
18694
19005
  };
18695
19006
  try {
18696
19007
  if (!workspace) {
18697
19008
  const workspaces = await listWorkspaces(authOpts);
18698
- if (workspaces.length === 0) {
19009
+ const configWorkspaces = [...new Set(cfg.routes.map((r) => splitRoute(r.route)[0]))];
19010
+ const defaulted = dryRun && givenKeySource === "config" && configWorkspaces.length === 1 ? workspaces.find((w) => w.slug === configWorkspaces[0]) : void 0;
19011
+ if (!signedInEmitted && (apiKey ?? "") !== "") {
19012
+ let me = {};
19013
+ try {
19014
+ me = await get(authOpts, "/auth/me");
19015
+ } catch {
19016
+ }
19017
+ if (me.id || me.email) {
19018
+ signedInEmitted = true;
19019
+ connectEvents.emit(
19020
+ "signed_in",
19021
+ `key belongs to ${me.email ?? me.id}${defaulted ? `; using ${defaulted.slug}, the workspace crew.yaml already names` : ""}`,
19022
+ {
19023
+ identity: { id: me.id, email: me.email, name: me.name },
19024
+ workspaces: workspaces.map((w) => ({ slug: w.slug, name: w.name, role: w.role })),
19025
+ session: givenKeySource,
19026
+ host: hostFromUrl4(baseUrl)
19027
+ }
19028
+ );
19029
+ }
19030
+ }
19031
+ if (defaulted) {
19032
+ await runConnect(defaulted.slug, project);
19033
+ } else if (workspaces.length === 0) {
18699
19034
  process.stderr.write("crew connect: this key's identity has no workspace memberships.\n");
18700
19035
  stopWithProblem("this key's identity has no workspace memberships", "workspace");
18701
19036
  } else if (workspaces.length === 1) {
@@ -19171,11 +19506,20 @@ Signed in as ${result.identity.email} \u2014 minted an API key in workspace "${r
19171
19506
  if (!resetStore) return { stored: false, clear: async () => {
19172
19507
  } };
19173
19508
  const store = resetStore;
19174
- const h = hostFromUrl3(r.baseUrl);
19509
+ const h = hostFromUrl4(r.baseUrl);
19175
19510
  const [slug] = splitRoute(r.route);
19176
19511
  const session = await store.get(h, slug);
19177
19512
  return { stored: Boolean(session?.apiKey), clear: () => store.clear(h, slug) };
19178
19513
  },
19514
+ async hostSessions(stateDir) {
19515
+ const store = resetStore;
19516
+ return Promise.all(rememberedSessions(stateDir).map(async ({ host: h, workspace: w }) => {
19517
+ const session = store ? await store.get(h, w).catch(() => void 0) : void 0;
19518
+ return { host: h, workspace: w, stored: Boolean(session?.apiKey), clear: async () => {
19519
+ await store?.clear(h, w);
19520
+ } };
19521
+ }));
19522
+ },
19179
19523
  inspectCheckout(dir) {
19180
19524
  const dirty = status(dir).map((c) => c.path);
19181
19525
  const linkedWorktrees = worktrees(dir).map((w) => w.path).filter((p) => realpathSync3(p) !== realpathSync3(dir));
@@ -19346,12 +19690,12 @@ Signed in as ${result.identity.email} \u2014 minted an API key in workspace "${r
19346
19690
  `);
19347
19691
  }
19348
19692
  }
19349
- if (existsSync33(join34(dir, ".git")) && basename11(dir) === SAMPLE_REPO) rmSync14(dir, { recursive: true, force: true });
19693
+ if (existsSync33(join35(dir, ".git")) && basename11(dir) === SAMPLE_REPO) rmSync15(dir, { recursive: true, force: true });
19350
19694
  say("removed", { dir, closed }, `removed the sample repo${closed.length ? `, closed ${closed.join(", ")}` : ""}`);
19351
19695
  break;
19352
19696
  }
19353
19697
  const projectId = route.resolved.areaId ?? resolveRepoProject(await tracker.projectRows(), value("project"));
19354
- const fresh = !existsSync33(join34(dir, ".git"));
19698
+ const fresh = !existsSync33(join35(dir, ".git"));
19355
19699
  if (existsSync33(dir) && fresh && readdirSync14(dir).length > 0) fail(`${dir} exists and is not a git checkout \u2014 move it aside`);
19356
19700
  if (dryRun) {
19357
19701
  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`);
@@ -19359,8 +19703,8 @@ Signed in as ${result.identity.email} \u2014 minted an API key in workspace "${r
19359
19703
  }
19360
19704
  if (fresh) {
19361
19705
  mkdirSync20(dir, { recursive: true });
19362
- writeFileSync21(join34(dir, "README.md"), SAMPLE_README);
19363
- writeFileSync21(join34(dir, ".crew.yaml"), SAMPLE_CREW_YAML);
19706
+ writeFileSync21(join35(dir, "README.md"), SAMPLE_README);
19707
+ writeFileSync21(join35(dir, ".crew.yaml"), SAMPLE_CREW_YAML);
19364
19708
  const g = (...a) => {
19365
19709
  const r = spawnSync6("git", a, { cwd: dir, encoding: "utf8" });
19366
19710
  if (r.status !== 0) fail(`git ${a[0]} failed in ${dir}: ${(r.stderr || r.stdout).trim()}`);
@@ -19810,6 +20154,11 @@ routes: ${cfg.routes.map((c) => c.route).join(", ")}
19810
20154
  if (warn) process.stdout.write(` WARNING ${warn}
19811
20155
  `);
19812
20156
  }
20157
+ const scope = releaseLockScope(route.route, r.name);
20158
+ const lock2 = await tracker.lockState(scope).catch(() => void 0);
20159
+ if (lock2?.holder && staleOwnLockClaim(lock2.holder, cfg.ship.name)) {
20160
+ process.stdout.write(` ${formatStaleOwnLock(scope, lock2.holder)}`);
20161
+ }
19813
20162
  }
19814
20163
  const missingAuthorship = await tracker.missingAuthorshipColumns();
19815
20164
  process.stdout.write(
@@ -20130,7 +20479,7 @@ ${foreign.join("\n")}
20130
20479
  break;
20131
20480
  }
20132
20481
  default:
20133
- usage();
20482
+ usage(`unknown command "${command}"`);
20134
20483
  }
20135
20484
  lock?.release();
20136
20485
  }