@tablation/crew 0.71.2 → 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 +465 -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",
@@ -8641,6 +8685,9 @@ function versionRequest(argv2) {
8641
8685
  if (rest.some((a) => a !== "--json")) return null;
8642
8686
  return { json: rest.length > 0 };
8643
8687
  }
8688
+ function helpRequest(argv2) {
8689
+ return argv2[0] === "--help" || argv2[0] === "-h" || argv2[0] === "help";
8690
+ }
8644
8691
 
8645
8692
  // src/roster.ts
8646
8693
  function memberByIdentity(roster, identityId) {
@@ -8736,7 +8783,7 @@ function rosterMarkdown(roster, meId) {
8736
8783
  }
8737
8784
 
8738
8785
  // src/connect-resume.ts
8739
- 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";
8740
8787
  import { dirname as dirname8, join as join19 } from "path";
8741
8788
  function lastSessionFile(stateDir) {
8742
8789
  return join19(stateDir, "connect-session.json");
@@ -8771,10 +8818,45 @@ async function resumeKeychainSession(o) {
8771
8818
  return void 0;
8772
8819
  }
8773
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
+ }
8774
8855
 
8775
8856
  // 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";
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";
8777
8858
  import { dirname as dirname9, join as join20 } from "path";
8859
+ import { hostFromUrl as hostFromUrl2 } from "@tablation/client";
8778
8860
  function clonedCheckoutsFile(stateDir) {
8779
8861
  return join20(stateDir, "cloned-checkouts.json");
8780
8862
  }
@@ -8820,12 +8902,31 @@ async function runReset(o, fx) {
8820
8902
  steps.push({ step: "credential", target: route.route, action: "failed", detail: e.message });
8821
8903
  }
8822
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
+ }
8823
8924
  if (o.removeCheckouts) {
8824
8925
  for (const dir of readClonedCheckouts(o.stateDir)) steps.push(removeCheckout(dir, o.dryRun, fx));
8825
8926
  }
8826
8927
  steps.push(...removeState(o.stateDir, o.dryRun));
8827
8928
  if (existsSync20(o.configFile)) {
8828
- if (!o.dryRun) rmSync10(o.configFile, { force: true });
8929
+ if (!o.dryRun) rmSync11(o.configFile, { force: true });
8829
8930
  steps.push({ step: "config", target: o.configFile, action: mark(o.dryRun) });
8830
8931
  } else {
8831
8932
  steps.push({ step: "config", target: o.configFile, action: "absent" });
@@ -8841,7 +8942,7 @@ function removeCheckout(dir, dryRun, fx) {
8841
8942
  if (linkedWorktrees.length) {
8842
8943
  return { step: "checkout", target: dir, action: "kept", detail: `has linked worktrees (${linkedWorktrees.join(", ")}); remove them first` };
8843
8944
  }
8844
- if (!dryRun) rmSync10(dir, { recursive: true, force: true });
8945
+ if (!dryRun) rmSync11(dir, { recursive: true, force: true });
8845
8946
  return { step: "checkout", target: dir, action: mark(dryRun) };
8846
8947
  }
8847
8948
  function removeState(stateDir, dryRun) {
@@ -8855,7 +8956,7 @@ function removeState(stateDir, dryRun) {
8855
8956
  }
8856
8957
  for (const t of targets) {
8857
8958
  if (!existsSync20(t)) continue;
8858
- if (!dryRun) rmSync10(t, { recursive: true, force: true });
8959
+ if (!dryRun) rmSync11(t, { recursive: true, force: true });
8859
8960
  out.push({ step: "state", target: t, action: mark(dryRun) });
8860
8961
  }
8861
8962
  if (!out.length) out.push({ step: "state", target: stateDir, action: "absent", detail: "nothing to remove" });
@@ -10519,7 +10620,9 @@ Wrote ${o.configPath}.
10519
10620
 
10520
10621
  // src/connect-json.ts
10521
10622
  import { accessSync as accessSync3, constants as constants3 } from "fs";
10522
- 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";
10523
10626
  var CHOICE_EXIT_CODE = 3;
10524
10627
  function createEmitter(json, write) {
10525
10628
  return {
@@ -10553,6 +10656,23 @@ function usesFlagAnswers(f, isTTY) {
10553
10656
  if (isTTY) return false;
10554
10657
  return !!(f.shipName || f.agentBin || f.extraPath || f.hostPassengers);
10555
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
+ }
10556
10676
  function flagWizardAnswers(f, env) {
10557
10677
  const isExec = env.isExecutable ?? ((p) => {
10558
10678
  try {
@@ -10562,9 +10682,9 @@ function flagWizardAnswers(f, env) {
10562
10682
  return false;
10563
10683
  }
10564
10684
  });
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
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;
10568
10688
  const pnpmPath = findOnPath2("pnpm", env.pathEnv, isExec);
10569
10689
  const dockerPath = f.hostPassengers ? findOnPath2("docker", env.pathEnv, isExec) : void 0;
10570
10690
  const extraPath = f.extraPath ?? deriveExtraPath([dirname11(env.nodePath), pnpmPath ? dirname11(pnpmPath) : void 0, dockerPath ? dirname11(dockerPath) : void 0]);
@@ -10572,16 +10692,17 @@ function flagWizardAnswers(f, env) {
10572
10692
  answers: {
10573
10693
  name: f.shipName?.trim() || env.hostname,
10574
10694
  platform: env.platform,
10575
- agentBin,
10695
+ agentBin: agentBin ?? "claude",
10576
10696
  extraPath,
10577
10697
  hostPassengers: f.hostPassengers
10578
- }
10698
+ },
10699
+ ...agentBin ? {} : { agentBinSearched: found?.searched ?? [] }
10579
10700
  };
10580
10701
  }
10581
10702
 
10582
10703
  // 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";
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";
10585
10706
  import { tmpdir as tmpdir3 } from "os";
10586
10707
 
10587
10708
  // src/release.ts
@@ -11299,7 +11420,7 @@ async function bump(o, size) {
11299
11420
  }
11300
11421
  return printed;
11301
11422
  }
11302
- 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));
11303
11424
  const first = files[0];
11304
11425
  if (!existsSync22(first)) {
11305
11426
  o.emit.error(`no ${o.repo.release.versionFiles[0]} to read a version from`);
@@ -11331,7 +11452,7 @@ async function bump(o, size) {
11331
11452
  function writeChangelog(o, version, entries) {
11332
11453
  const file = o.repo.release.changelog;
11333
11454
  if (!file || entries.length === 0) return;
11334
- const path = join23(o.cwd, file);
11455
+ const path = join24(o.cwd, file);
11335
11456
  const section = renderChangelogSection(version, (/* @__PURE__ */ new Date()).toISOString().slice(0, 10), entries);
11336
11457
  if (o.dryRun) {
11337
11458
  o.emit.emit(`would prepend ${entries.length} entr(ies) to ${file}`);
@@ -11421,7 +11542,7 @@ function closeReleaseCheckout(cwd, root, dir, branch, run2 = gitOk) {
11421
11542
  run2(cwd, ["worktree", "remove", "--force", dir]);
11422
11543
  } finally {
11423
11544
  try {
11424
- rmSync11(root, { recursive: true, force: true });
11545
+ rmSync12(root, { recursive: true, force: true });
11425
11546
  } finally {
11426
11547
  run2(cwd, ["worktree", "prune"]);
11427
11548
  run2(cwd, ["branch", "-D", branch]);
@@ -11431,8 +11552,8 @@ function closeReleaseCheckout(cwd, root, dir, branch, run2 = gitOk) {
11431
11552
  function openReleaseCheckout(o) {
11432
11553
  const { remote, base } = o.repo.branch;
11433
11554
  const branch = `crew/release-${Date.now()}`;
11434
- const root = mkdtempSync2(join23(tmpdir3(), "crew-release-"));
11435
- const dir = join23(root, "checkout");
11555
+ const root = mkdtempSync2(join24(tmpdir3(), "crew-release-"));
11556
+ const dir = join24(root, "checkout");
11436
11557
  o.emit.emit(`release checkout: ${dir}`, { step: "release" });
11437
11558
  const close = () => closeReleaseCheckout(o.cwd, root, dir, branch);
11438
11559
  try {
@@ -11442,10 +11563,10 @@ function openReleaseCheckout(o) {
11442
11563
  throw e;
11443
11564
  }
11444
11565
  for (const f of o.repo.worktrees.copy) {
11445
- const src = join23(o.cwd, f);
11566
+ const src = join24(o.cwd, f);
11446
11567
  if (!existsSync22(src)) continue;
11447
- mkdirSync13(dirname12(join23(dir, f)), { recursive: true });
11448
- copyFileSync(src, join23(dir, f));
11568
+ mkdirSync13(dirname12(join24(dir, f)), { recursive: true });
11569
+ copyFileSync(src, join24(dir, f));
11449
11570
  }
11450
11571
  return { dir, branch, close };
11451
11572
  }
@@ -12592,7 +12713,22 @@ async function provisionReleaseLocks(o, locksModelId, route, repoNames) {
12592
12713
  };
12593
12714
  return ensureLockRows(client2, locksModelId, scopes);
12594
12715
  }
12595
- 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
+ }
12596
12732
  async function listLibraryTemplates(o) {
12597
12733
  return get(o, "/library-templates");
12598
12734
  }
@@ -12633,6 +12769,7 @@ async function discover(o) {
12633
12769
  models: {},
12634
12770
  seats: {},
12635
12771
  holds: [],
12772
+ candidates: [],
12636
12773
  problems,
12637
12774
  provisioning
12638
12775
  };
@@ -12641,6 +12778,7 @@ async function discover(o) {
12641
12778
  out.role = me.role;
12642
12779
  out.meEmail = me.email;
12643
12780
  out.meId = me.id;
12781
+ out.meIdentityId = me.identityId;
12644
12782
  out.meName = me.name;
12645
12783
  } catch {
12646
12784
  }
@@ -12664,7 +12802,8 @@ async function discover(o) {
12664
12802
  }
12665
12803
  } else {
12666
12804
  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"));
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"));
12668
12807
  if (defaultCandidate && (await missingTables(o, defaultCandidate.id)).length === 0) {
12669
12808
  out.projectId = defaultCandidate.id;
12670
12809
  out.projectSlug = defaultCandidate.slug;
@@ -12856,13 +12995,32 @@ async function discover(o) {
12856
12995
  o,
12857
12996
  `/data-models/${out.models.crew}/records?limit=200`
12858
12997
  );
12859
- const scoped = shipRowId ? rows.filter((r) => r.ship_id === shipRowId) : rows;
12860
- 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) : [];
12861
13013
  for (const role of ["dev", "design", "qa", "triage"]) {
12862
13014
  const searchTerm = role === "dev" ? "develop" : role;
12863
13015
  let id = seatFor(scoped, searchTerm);
12864
13016
  let seatAction = "matched";
12865
- 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) {
12866
13024
  const legacyId = seatFor(unclaimed, searchTerm);
12867
13025
  if (legacyId) {
12868
13026
  await patch(o, `/data-models/${out.models.crew}/records/${legacyId}`, { ship_id: shipRowId });
@@ -12882,10 +13040,13 @@ async function discover(o) {
12882
13040
  }
12883
13041
  if (id) {
12884
13042
  out.seats[role] = id;
12885
- (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" });
12886
13047
  } else problems.push(`no Crew row looking like the ${role} seat \u2014 create one, or omit that role`);
12887
13048
  }
12888
- const pairId = seatFor(scoped, "pair") || (shipRowId ? seatFor(unclaimed, "pair") : void 0);
13049
+ const pairId = seatFor(scoped, "pair") || (seatShipId ? seatFor(unclaimed, "pair") : void 0);
12889
13050
  if (pairId) out.seats.pair = pairId;
12890
13051
  if (shipRowId) {
12891
13052
  const releaseId = seatFor(scoped, "release");
@@ -12903,25 +13064,56 @@ async function discover(o) {
12903
13064
  const holdRows = rows.filter((r) => !seatIds.has(r.id));
12904
13065
  out.holds = holdRows.map((r) => ({ id: r.id, name: r.name ?? "", email: r.email, user_id: r.user_id }));
12905
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);
12906
13093
  if (!out.meId) {
12907
- 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?");
12908
13095
  else {
12909
- 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))) : [];
12910
13097
  if (byEmail.length === 1) out.operator = byEmail[0].id;
12911
- 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;
12912
13099
  }
12913
13100
  } 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;
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;
12918
13106
  const pick = mine ?? (byEmail.length === 1 ? byEmail[0] : only);
12919
13107
  if (pick) {
12920
13108
  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);
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);
12925
13117
  }
12926
13118
  }
12927
13119
  }
@@ -12962,6 +13154,7 @@ async function createOperatorRow(o, found) {
12962
13154
  found.operator = created.id;
12963
13155
  found.operatorAction = "created";
12964
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 });
12965
13158
  found.provisioning.push(`created Crew row "${name}" for you`);
12966
13159
  } catch (e) {
12967
13160
  if (!(e instanceof ConnectHttpError) || e.status !== 403) throw e;
@@ -12991,14 +13184,14 @@ ${opts.hostPassengers ? " hostPassengers: true\n" : ""}${apiKeyFileLine}${api
12991
13184
  // src/cli.ts
12992
13185
  import {
12993
13186
  getSessionStore as getSessionStore3,
12994
- hostFromUrl as hostFromUrl3,
13187
+ hostFromUrl as hostFromUrl4,
12995
13188
  loginWithDeviceCode,
12996
13189
  getOrCreateShipId,
12997
13190
  DeviceLoginError
12998
13191
  } from "@tablation/client";
12999
13192
 
13000
13193
  // src/seat-keys.ts
13001
- import { getSessionStore as getSessionStore2, hostFromUrl as hostFromUrl2 } from "@tablation/client";
13194
+ import { getSessionStore as getSessionStore2, hostFromUrl as hostFromUrl3 } from "@tablation/client";
13002
13195
  var SEAT_KEY_ROLES = ROLE_NAMES.filter((r) => r !== "pair");
13003
13196
  function keyIsAdmin(route) {
13004
13197
  const role = route.resolved?.role;
@@ -13022,7 +13215,7 @@ async function resolveReleaseKey(route, getStore = getSessionStore2) {
13022
13215
  const agentId = route.resolved?.releaseAgentId;
13023
13216
  if (!agentId) return ship(keyIsAdmin(route) ? "release seat has no linked Agents row (run `crew agents sync`)" : `release seat ${MEMBER_NO_ROW}`);
13024
13217
  try {
13025
- const session = await getStore().get(hostFromUrl2(route.baseUrl), slugFor(route, agentId));
13218
+ const session = await getStore().get(hostFromUrl3(route.baseUrl), slugFor(route, agentId));
13026
13219
  if (session?.apiKey) return { kind: "agent", apiKey: session.apiKey, agentId };
13027
13220
  return ship(keyIsAdmin(route) ? "no agent key stored for the release seat (run `crew agents sync`)" : MEMBER_NO_KEY);
13028
13221
  } catch {
@@ -13035,7 +13228,7 @@ async function resolveSeatKey(route, role, getStore = getSessionStore2) {
13035
13228
  const agentId = seatAgentId(route, role);
13036
13229
  if (!agentId) return ship(keyIsAdmin(route) ? "seat has no linked Agents row (run `crew agents sync`)" : `seat ${MEMBER_NO_ROW}`);
13037
13230
  try {
13038
- const session = await getStore().get(hostFromUrl2(route.baseUrl), slugFor(route, agentId));
13231
+ const session = await getStore().get(hostFromUrl3(route.baseUrl), slugFor(route, agentId));
13039
13232
  if (session?.apiKey) return { kind: "agent", apiKey: session.apiKey, agentId };
13040
13233
  return ship(keyIsAdmin(route) ? "no agent key stored for this seat (run `crew agents sync`)" : MEMBER_NO_KEY);
13041
13234
  } catch {
@@ -13068,7 +13261,7 @@ async function mintMissingSeatKeys(route, o) {
13068
13261
  detail: describeStoreError(e)
13069
13262
  }));
13070
13263
  }
13071
- const host = hostFromUrl2(route.baseUrl);
13264
+ const host = hostFromUrl3(route.baseUrl);
13072
13265
  const base = route.baseUrl.replace(/\/+$/, "");
13073
13266
  if (!o.dryRun) {
13074
13267
  const probeSlug = seatKeySlug(splitRoute(route.route)[0], "_probe");
@@ -13189,7 +13382,7 @@ async function revokeSeatKeys(route, o) {
13189
13382
  } catch (e) {
13190
13383
  return targets.map(({ role }) => ({ role, action: "failed", detail: describeStoreError(e) }));
13191
13384
  }
13192
- const host = hostFromUrl2(route.baseUrl);
13385
+ const host = hostFromUrl3(route.baseUrl);
13193
13386
  const base = route.baseUrl.replace(/\/+$/, "");
13194
13387
  for (const { role, agentId } of targets) {
13195
13388
  if (!agentId) {
@@ -13446,8 +13639,8 @@ async function showLogEntry(route, entryId, opts = {}) {
13446
13639
  import { isMap as isMap2, parseDocument as parseDocument2, YAMLMap } from "yaml";
13447
13640
 
13448
13641
  // src/repos-cmd.ts
13449
- import { homedir as homedir7 } from "os";
13450
- 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";
13451
13644
  import { isMap, isSeq, parseDocument, stringify as stringify2 } from "yaml";
13452
13645
  var ReposError = class extends Error {
13453
13646
  };
@@ -13493,7 +13686,7 @@ function parseAddArgs(args, routes, isDir) {
13493
13686
  return { route, path: "." };
13494
13687
  }
13495
13688
  function expandDir(p, base) {
13496
- const home = p === "~" ? homedir7() : p.startsWith("~/") ? `${homedir7()}/${p.slice(2)}` : p;
13689
+ const home = p === "~" ? homedir8() : p.startsWith("~/") ? `${homedir8()}/${p.slice(2)}` : p;
13497
13690
  return resolve3(base, home);
13498
13691
  }
13499
13692
  function routeMapOf(doc, route) {
@@ -13701,7 +13894,7 @@ function planRepoClone(i) {
13701
13894
  }
13702
13895
  const ship = doc.get("ship", true);
13703
13896
  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);
13897
+ const target = i.dir ? expandDir(i.dir, i.base) : join25(expandDir(basePath, i.base), i.route.split("/")[0], name);
13705
13898
  const add = planRepoAdd({ text: i.text, base: i.base, route: i.route, path: target, origin: remote, resolved: i.resolved, name });
13706
13899
  return { ...add, remote, target };
13707
13900
  }
@@ -14081,7 +14274,7 @@ ${run2.err}` });
14081
14274
  }
14082
14275
 
14083
14276
  // src/sample-ticket.ts
14084
- 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";
14085
14278
  var SAMPLE_REPO = "crew-sample";
14086
14279
  var SAMPLE_REMOTE = `local/${SAMPLE_REPO}`;
14087
14280
  var SAMPLE_MARKER = "sample: true";
@@ -14104,8 +14297,8 @@ has something safe to work on: its test and build hooks always pass, it has no
14104
14297
  remote, and \`crew sample-ticket --remove\` deletes it.
14105
14298
  `;
14106
14299
  function sampleDir(existing, reposBase, workspace) {
14107
- if (existing.length > 0) return join25(dirname15(existing[0]), SAMPLE_REPO);
14108
- 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);
14109
14302
  }
14110
14303
  var SAMPLE_TITLE = "Sample: add a documented `greet` function with a test";
14111
14304
  function sampleDescription() {
@@ -14187,7 +14380,7 @@ function schedulerInstalled(plan, exists, crontab) {
14187
14380
 
14188
14381
  // src/worktree-sweep.ts
14189
14382
  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";
14383
+ import { basename as basename10, dirname as dirname16, join as join27, resolve as resolve4 } from "path";
14191
14384
  function planWorktreeSweep(target, tickets, contract) {
14192
14385
  const terminal = new Set(contract.statuses.resolved.filter((s) => s !== contract.statuses.verified));
14193
14386
  const verified = contract.statuses.verified;
@@ -14254,7 +14447,7 @@ function siblingWorktreeDirs(target) {
14254
14447
  for (const name of names) {
14255
14448
  if (!name.startsWith(stem)) continue;
14256
14449
  const m = DIR_NUMBER_SUFFIXED.exec(name);
14257
- const path = join26(parent, name);
14450
+ const path = join27(parent, name);
14258
14451
  if (m && !registered.has(resolve4(path))) out.push({ path, number: m[1] });
14259
14452
  }
14260
14453
  return out;
@@ -14462,14 +14655,14 @@ function applyRemoteBranchCleanup(cwd, remote, branches2, dryRun, log) {
14462
14655
  }
14463
14656
 
14464
14657
  // 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";
14658
+ import { existsSync as existsSync26, readdirSync as readdirSync9, rmSync as rmSync13, statSync as statSync8 } from "fs";
14659
+ import { join as join28 } from "path";
14467
14660
  var DAY_MS2 = 24 * 60 * 60 * 1e3;
14468
14661
  function planStreamSweep(stateDir, retentionDays, now = Date.now()) {
14469
- const dir = join27(stateDir, "streams");
14662
+ const dir = join28(stateDir, "streams");
14470
14663
  if (!existsSync26(dir)) return [];
14471
14664
  const cutoff = now - retentionDays * DAY_MS2;
14472
- return readdirSync9(dir).map((name) => join27(dir, name)).filter((path) => {
14665
+ return readdirSync9(dir).map((name) => join28(dir, name)).filter((path) => {
14473
14666
  try {
14474
14667
  return statSync8(path).mtimeMs < cutoff;
14475
14668
  } catch {
@@ -14485,7 +14678,7 @@ function applyStreamSweep(paths, dryRun, log) {
14485
14678
  continue;
14486
14679
  }
14487
14680
  try {
14488
- rmSync12(path, { force: true });
14681
+ rmSync13(path, { force: true });
14489
14682
  removed++;
14490
14683
  log.emit(`removed stream artifact ${path}`, { step: "worktree" });
14491
14684
  } catch (e) {
@@ -14497,7 +14690,7 @@ function applyStreamSweep(paths, dryRun, log) {
14497
14690
  function planEventsSweep(stateDir, retentionDays, now = Date.now()) {
14498
14691
  if (!existsSync26(stateDir)) return [];
14499
14692
  const cutoff = now - retentionDays * DAY_MS2;
14500
- 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) => {
14501
14694
  try {
14502
14695
  return statSync8(path).mtimeMs < cutoff;
14503
14696
  } catch {
@@ -14507,18 +14700,18 @@ function planEventsSweep(stateDir, retentionDays, now = Date.now()) {
14507
14700
  }
14508
14701
 
14509
14702
  // 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";
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";
14511
14704
  import { createInterface } from "readline/promises";
14512
14705
  import { dirname as dirOf, resolve as resolvePath } from "path";
14513
14706
 
14514
14707
  // src/ssh-keys.ts
14515
14708
  import { execFileSync as execFileSync5 } from "child_process";
14516
14709
  import { existsSync as existsSync27, mkdirSync as mkdirSync15, readFileSync as readFileSync21, chmodSync } from "fs";
14517
- import { dirname as dirname17, join as join28 } from "path";
14710
+ import { dirname as dirname17, join as join29 } from "path";
14518
14711
  var SshKeyError = class extends Error {
14519
14712
  };
14520
14713
  function shipSshKeyPathFor(stateDir) {
14521
- return join28(stateDir, "ssh", "id_ed25519");
14714
+ return join29(stateDir, "ssh", "id_ed25519");
14522
14715
  }
14523
14716
  function ensureShipSshKeypair(stateDir) {
14524
14717
  const privateKeyPath = shipSshKeyPathFor(stateDir);
@@ -14563,9 +14756,9 @@ function sshKeygenAvailable() {
14563
14756
  import { execFileSync as execFileSync6 } from "child_process";
14564
14757
  import { createHash as createHash4 } from "crypto";
14565
14758
  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";
14759
+ import { dirname as dirname18, join as join30 } from "path";
14567
14760
  function machineIdPathFor(stateDir) {
14568
- return join29(stateDir, "machine-id");
14761
+ return join30(stateDir, "machine-id");
14569
14762
  }
14570
14763
  var sha256 = (s) => createHash4("sha256").update(s).digest("hex");
14571
14764
  function readRawMachineId(platform2 = process.platform) {
@@ -14815,7 +15008,7 @@ function syncAllPassengerCheckouts(cfg) {
14815
15008
  import { spawn as spawn5 } from "child_process";
14816
15009
  import { existsSync as existsSync29, mkdirSync as mkdirSync17, readFileSync as readFileSync23, writeFileSync as writeFileSync18, unlinkSync as unlinkSync3, readdirSync as readdirSync10 } from "fs";
14817
15010
  import { randomInt } from "crypto";
14818
- import { join as join30 } from "path";
15011
+ import { join as join31 } from "path";
14819
15012
  var DEFAULT_SPAWN_OPTIONS = { stdio: "ignore", detached: true };
14820
15013
  var defaultSpawn = (cmd, args) => spawn5(cmd, args, DEFAULT_SPAWN_OPTIONS);
14821
15014
  var SLUG_ALPHABET = "abcdefghijklmnopqrstuvwxyz0123456789";
@@ -14882,7 +15075,7 @@ function startTunnel(opts) {
14882
15075
  };
14883
15076
  }
14884
15077
  function tunnelStatePath(stateDir, workspaceId) {
14885
- return join30(stateDir, "passengers", `${workspaceId}.json`);
15078
+ return join31(stateDir, "passengers", `${workspaceId}.json`);
14886
15079
  }
14887
15080
  function readPersistedTunnel(stateDir, workspaceId) {
14888
15081
  const path = tunnelStatePath(stateDir, workspaceId);
@@ -14895,7 +15088,7 @@ function readPersistedTunnel(stateDir, workspaceId) {
14895
15088
  }
14896
15089
  function writePersistedTunnel(stateDir, state) {
14897
15090
  const path = tunnelStatePath(stateDir, state.workspaceId);
14898
- mkdirSync17(join30(stateDir, "passengers"), { recursive: true });
15091
+ mkdirSync17(join31(stateDir, "passengers"), { recursive: true });
14899
15092
  writeFileSync18(path, `${JSON.stringify(state, null, 2)}
14900
15093
  `);
14901
15094
  }
@@ -14993,7 +15186,7 @@ function syncPassengerTunnels(plans, relayHost, relayPort, privateKeyPath, state
14993
15186
  }
14994
15187
  }
14995
15188
  function readAllPersistedWorkspaceIds(stateDir) {
14996
- const dir = join30(stateDir, "passengers");
15189
+ const dir = join31(stateDir, "passengers");
14997
15190
  if (!existsSync29(dir)) return [];
14998
15191
  return readdirSync10(dir).filter((f) => f.endsWith(".json") && !f.endsWith("-sync.json")).map((f) => f.slice(0, -".json".length));
14999
15192
  }
@@ -15116,7 +15309,7 @@ import { createServer as createHttpServer } from "http";
15116
15309
  import { spawn as spawn6 } from "child_process";
15117
15310
  import { randomBytes } from "crypto";
15118
15311
  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";
15312
+ import { join as join32 } from "path";
15120
15313
 
15121
15314
  // src/secret-compare.ts
15122
15315
  import { timingSafeEqual } from "crypto";
@@ -15205,7 +15398,7 @@ var defaultSpawn2 = (cmd, args, env) => {
15205
15398
  return child;
15206
15399
  };
15207
15400
  function syncDaemonStatePath(stateDir, workspaceId) {
15208
- return join31(stateDir, "passengers", `${workspaceId}-sync.json`);
15401
+ return join32(stateDir, "passengers", `${workspaceId}-sync.json`);
15209
15402
  }
15210
15403
  function readPersistedSyncDaemon(stateDir, workspaceId) {
15211
15404
  const path = syncDaemonStatePath(stateDir, workspaceId);
@@ -15218,7 +15411,7 @@ function readPersistedSyncDaemon(stateDir, workspaceId) {
15218
15411
  }
15219
15412
  function writePersistedSyncDaemon(stateDir, state) {
15220
15413
  const path = syncDaemonStatePath(stateDir, state.workspaceId);
15221
- mkdirSync18(join31(stateDir, "passengers"), { recursive: true });
15414
+ mkdirSync18(join32(stateDir, "passengers"), { recursive: true });
15222
15415
  writeFileSync19(path, `${JSON.stringify(state, null, 2)}
15223
15416
  `);
15224
15417
  }
@@ -15239,7 +15432,7 @@ function waitForExit(pid, alive, timeoutMs = 5e3) {
15239
15432
  }
15240
15433
  }
15241
15434
  function readAllPersistedSyncWorkspaceIds(stateDir) {
15242
- const dir = join31(stateDir, "passengers");
15435
+ const dir = join32(stateDir, "passengers");
15243
15436
  if (!existsSync30(dir)) return [];
15244
15437
  const suffix = "-sync.json";
15245
15438
  return readdirSync11(dir).filter((f) => f.endsWith(suffix)).map((f) => f.slice(0, -suffix.length));
@@ -15328,12 +15521,12 @@ function describeCause(err) {
15328
15521
  }
15329
15522
 
15330
15523
  // 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";
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";
15333
15526
 
15334
15527
  // src/interrupted.ts
15335
15528
  import { existsSync as existsSync31, readdirSync as readdirSync12, readFileSync as readFileSync25 } from "fs";
15336
- import { join as join32 } from "path";
15529
+ import { join as join33 } from "path";
15337
15530
  var INTERRUPTED_MARKER = calloutHeader("NOTE", "Interrupted");
15338
15531
  function interruptedComment(b) {
15339
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.";
@@ -15360,13 +15553,13 @@ function runFinished(streamPath) {
15360
15553
  }
15361
15554
  }
15362
15555
  function orphanedStreams(o) {
15363
- const dir = join32(o.stateDir, "streams");
15556
+ const dir = join33(o.stateDir, "streams");
15364
15557
  if (!existsSync31(dir)) return [];
15365
15558
  const ownerAlive = o.isOwnerAlive ?? pidAlive;
15366
15559
  const out = [];
15367
15560
  for (const name of readdirSync12(dir)) {
15368
15561
  if (!name.endsWith(".jsonl") || name.endsWith(".events.jsonl")) continue;
15369
- const streamPath = join32(dir, name);
15562
+ const streamPath = join33(dir, name);
15370
15563
  if (runFinished(streamPath)) continue;
15371
15564
  const sidecar = readRunSidecar(streamPath);
15372
15565
  if (!sidecar || sidecar.route !== o.routeLabel) continue;
@@ -15486,7 +15679,7 @@ function latestMtime(path) {
15486
15679
  if (!stat.isDirectory()) return 0;
15487
15680
  let latest = stat.mtimeMs;
15488
15681
  for (const entry of readdirSync13(path, { withFileTypes: true })) {
15489
- latest = Math.max(latest, latestMtime(join33(path, entry.name)));
15682
+ latest = Math.max(latest, latestMtime(join34(path, entry.name)));
15490
15683
  }
15491
15684
  return latest;
15492
15685
  }
@@ -15498,14 +15691,14 @@ function makeStaleChecker(paths, opts = {}) {
15498
15691
  return () => computeTreeSignature(paths) !== initial || opts.reloadSentinel !== void 0 && existsSync32(opts.reloadSentinel);
15499
15692
  }
15500
15693
  function reloadSentinelFor(stateDir) {
15501
- return join33(stateDir, "reload-requested");
15694
+ return join34(stateDir, "reload-requested");
15502
15695
  }
15503
15696
  function daemonStampFor(stateDir) {
15504
- return join33(stateDir, "daemon.started");
15697
+ return join34(stateDir, "daemon.started");
15505
15698
  }
15506
15699
  function noteDaemonStarted(stateDir, now = Date.now()) {
15507
15700
  mkdirSync19(stateDir, { recursive: true });
15508
- rmSync13(reloadSentinelFor(stateDir), { force: true });
15701
+ rmSync14(reloadSentinelFor(stateDir), { force: true });
15509
15702
  writeFileSync20(daemonStampFor(stateDir), `${JSON.stringify({ pid: process.pid, startedAt: now })}
15510
15703
  `);
15511
15704
  }
@@ -15940,7 +16133,7 @@ async function pickOperator(holds2, me) {
15940
16133
  const choices = holds2.filter((h) => !h.user_id || h.user_id === me.id);
15941
16134
  const canCreate = !!me.id;
15942
16135
  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}
16136
+ choices.forEach((h, i) => process.stderr.write(` ${i + 1}. ${h.name || "unnamed row"}${h.detail ? ` <${h.detail}>` : ""}
15944
16137
  `));
15945
16138
  if (canCreate) process.stderr.write(` 0. Create a new Crew row for ${me.name ?? ""} ${me.email ?? ""}`.trimEnd() + "\n");
15946
16139
  const rl = createInterface({ input: process.stdin, output: process.stderr });
@@ -15974,10 +16167,11 @@ async function pickResolvedStatuses(unrecognized) {
15974
16167
  }
15975
16168
  async function offerTemplateInstall(authOpts, workspaceId, decided) {
15976
16169
  const templates = await listLibraryTemplates(authOpts);
15977
- 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));
15978
16172
  if (!template) {
15979
16173
  process.stderr.write(`
15980
- 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.
15981
16175
  `);
15982
16176
  return void 0;
15983
16177
  }
@@ -16015,8 +16209,8 @@ No project here has what a route needs. Install the "${template.name}" library t
16015
16209
  `);
16016
16210
  return installed.project.slug;
16017
16211
  }
16018
- function usage() {
16019
- 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
16020
16214
 
16021
16215
  crew poll [route] decide a cycle and report it; writes nothing
16022
16216
  crew run [route] [--role R] run the winning role's session
@@ -16044,6 +16238,7 @@ function usage() {
16044
16238
  resume with no argument clears only the whole-crew pause; a role or release pause needs its own resume
16045
16239
  crew log [route] tail the log
16046
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)
16047
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)
16048
16243
  crew disconnect remove what connect installed for Pair (Claude MCP; Codex/Gemini instructions block and MCP entry per ship.pair)
16049
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)
@@ -16078,10 +16273,18 @@ Options:
16078
16273
  --all (inbox) include closed tickets
16079
16274
 
16080
16275
  In watch: space pauses the stream, q quits.
16081
- `);
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);
16082
16284
  process.exit(2);
16083
16285
  }
16084
16286
  var argv = process.argv.slice(2);
16287
+ if (helpRequest(argv)) usage(void 0, true);
16085
16288
  var versionReq = versionRequest(argv);
16086
16289
  if (versionReq) {
16087
16290
  const version = crewVersion(CREW_HOME);
@@ -16101,7 +16304,45 @@ var connectEvents = createEmitter(command === "connect" && flag("json"), (l) =>
16101
16304
  process.stdout.write(l);
16102
16305
  });
16103
16306
  var connectChoices = parseChoices(argv);
16104
- 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}"`);
16105
16346
  function findSyncRoute(c, name) {
16106
16347
  if (!name && c.routes.length > 1) {
16107
16348
  throw new ConfigError(`this ship has ${c.routes.length} routes \u2014 name one: ${c.routes.map((r) => r.route).join(", ")}`);
@@ -16191,6 +16432,7 @@ if (command === "passenger-sync-daemon") {
16191
16432
  let cfg;
16192
16433
  let firstRun;
16193
16434
  let firstRunFromFlags = false;
16435
+ let agentBinSearched;
16194
16436
  try {
16195
16437
  const connectIsTTY = !!(process.stdin.isTTY && process.stdout.isTTY);
16196
16438
  const wizardFlags = {
@@ -16200,7 +16442,7 @@ if (command === "passenger-sync-daemon") {
16200
16442
  hostPassengers: flag("host-passengers"),
16201
16443
  noWizard: flag("no-wizard")
16202
16444
  };
16203
- 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 })) {
16204
16446
  const flagged = flagWizardAnswers(wizardFlags, {
16205
16447
  hostname: hostname2(),
16206
16448
  platform: hostPlatform(),
@@ -16217,8 +16459,9 @@ if (command === "passenger-sync-daemon") {
16217
16459
  const configPath = firstRunConfigPath();
16218
16460
  firstRun = { answers: flagged.answers, configPath };
16219
16461
  firstRunFromFlags = true;
16462
+ agentBinSearched = flagged.agentBinSearched;
16220
16463
  cfg = loadConfig(CREW_HOME, configPath, { text: renderShipBlock(flagged.answers), allowNoRoutes: true });
16221
- } else if (command === "connect" && shouldRunWizard({
16464
+ } else if (command === "connect" && !flag("sign-out") && shouldRunWizard({
16222
16465
  isTTY: connectIsTTY,
16223
16466
  crewHome: CREW_HOME
16224
16467
  })) {
@@ -16239,7 +16482,13 @@ if (command === "passenger-sync-daemon") {
16239
16482
  firstRun = { answers, configPath };
16240
16483
  cfg = loadConfig(CREW_HOME, configPath, { text: renderShipBlock(answers), allowNoRoutes: true });
16241
16484
  } else {
16242
- 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
+ }
16243
16492
  }
16244
16493
  } catch (e) {
16245
16494
  if (e instanceof ConfigError) {
@@ -16473,7 +16722,7 @@ if (command === "passenger-sync-daemon") {
16473
16722
  }
16474
16723
  if (host === "linux" && hasSystemd) {
16475
16724
  const label = labelFor(CREW_HOME, "run");
16476
- const staleTimer = join34(homedir8(), ".config", "systemd", "user", `${label}.timer`);
16725
+ const staleTimer = join35(homedir9(), ".config", "systemd", "user", `${label}.timer`);
16477
16726
  if (existsSync33(staleTimer)) {
16478
16727
  process.stdout.write(
16479
16728
  `removing stale run timer from before the persistent-service cutover: ${staleTimer}${dryRun ? " (dry run)" : ""}
@@ -16721,7 +16970,10 @@ ${changed2.map((c) => ` - ${c}
16721
16970
  }
16722
16971
  if (!dryRun) {
16723
16972
  const holderLabel = `${cfg.ship.name}:${process.pid}`;
16724
- 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
+ });
16725
16977
  if (!got.ok && got.reason === "missing") {
16726
16978
  const detail = `no Locks row for ${scope} \u2014 run \`crew connect ${c.route}\` to provision it`;
16727
16979
  remit.warn(`release for ${scope} refused: ${detail}`, { step: "release" });
@@ -17536,7 +17788,7 @@ ${changed2.map((c) => ` - ${c}
17536
17788
  process.exit(1);
17537
17789
  }
17538
17790
  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")];
17791
+ const watchPaths = isCompiledBinary(import.meta.url) ? [process.execPath] : [join35(CREW_HOME, "src"), join35(CREW_HOME, "dist"), join35(CREW_HOME, "bin")];
17540
17792
  const configPaths = daemonConfigPaths(cfg);
17541
17793
  watchPaths.push(...configPaths);
17542
17794
  noteDaemonStarted(cfg.ship.stateDir);
@@ -17880,7 +18132,7 @@ ${changed2.map((c) => ` - ${c}
17880
18132
  if (sub === "list" && !routeName) usage2();
17881
18133
  if (sub === "add") {
17882
18134
  try {
17883
- 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()))));
17884
18136
  routeName = a.route;
17885
18137
  addPath = a.path;
17886
18138
  addName = a.name;
@@ -17982,7 +18234,7 @@ ${changed2.map((c) => ` - ${c}
17982
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}`);
17983
18235
  break;
17984
18236
  }
17985
- const target = resolvePath(addPath.replace(/^~(?=\/|$)/, homedir8()));
18237
+ const target = resolvePath(addPath.replace(/^~(?=\/|$)/, homedir9()));
17986
18238
  const top = existsSync33(target) ? gitOk(target, ["rev-parse", "--show-toplevel"]) : null;
17987
18239
  if (!top) throw new ReposError(`${target} is not a git checkout`);
17988
18240
  const origin = originOf(top);
@@ -18134,6 +18386,29 @@ ${reconstruction.prompt}
18134
18386
  }
18135
18387
  case "connect": {
18136
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
+ }
18137
18412
  const canAsk = !!(process.stdin.isTTY && process.stdout.isTTY) && !connectEvents.json;
18138
18413
  const stopForChoice = (key, message, options) => {
18139
18414
  connectEvents.emit("choice", message, choiceData(key, options));
@@ -18151,6 +18426,7 @@ ${reconstruction.prompt}
18151
18426
  const project = argProject ?? value("project") ?? connectChoices.project;
18152
18427
  const baseUrl = value("base-url") ?? cfg.routes[0]?.baseUrl ?? DEFAULT_BASE_URL;
18153
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";
18154
18430
  let store;
18155
18431
  try {
18156
18432
  store = getSessionStore3();
@@ -18160,11 +18436,11 @@ ${reconstruction.prompt}
18160
18436
  let keychainBacked = false;
18161
18437
  if (!apiKey && workspace && store) {
18162
18438
  try {
18163
- const existing = await store.get(hostFromUrl3(baseUrl), workspace);
18439
+ const existing = await store.get(hostFromUrl4(baseUrl), workspace);
18164
18440
  if (existing?.apiKey) {
18165
18441
  apiKey = existing.apiKey;
18166
18442
  keychainBacked = true;
18167
- 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}.
18168
18444
  `);
18169
18445
  }
18170
18446
  } catch {
@@ -18175,7 +18451,7 @@ ${reconstruction.prompt}
18175
18451
  const resumed = await resumeKeychainSession({
18176
18452
  store,
18177
18453
  stateDir: cfg.ship.stateDir,
18178
- host: hostFromUrl3(baseUrl),
18454
+ host: hostFromUrl4(baseUrl),
18179
18455
  verify: async (k) => {
18180
18456
  try {
18181
18457
  await get({ baseUrl, apiKey: k, userAgent: cfg.ship.userAgent }, "/auth/me");
@@ -18190,7 +18466,7 @@ ${reconstruction.prompt}
18190
18466
  workspace = resumed.workspace;
18191
18467
  keychainBacked = true;
18192
18468
  signInSource = "keychain";
18193
- 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}.
18194
18470
  `);
18195
18471
  }
18196
18472
  }
@@ -18247,13 +18523,14 @@ Signed in as ${result.identity.email} \u2014 minted an API key in workspace "${r
18247
18523
  connectEvents.emit("signed_in", `signed in as ${result.identity.email}`, {
18248
18524
  identity: { id: result.identity.id, email: result.identity.email },
18249
18525
  workspaces: [{ slug: result.workspace.slug, name: result.workspace.name }],
18250
- session: "device"
18526
+ session: "device",
18527
+ host: hostFromUrl4(baseUrl)
18251
18528
  });
18252
18529
  const deviceWorkspace = "path" in result.workspace && typeof result.workspace.path === "string" ? result.workspace.path : result.workspace.slug;
18253
18530
  if (!workspace) workspace = deviceWorkspace;
18254
18531
  if (store) {
18255
18532
  try {
18256
- await store.set(hostFromUrl3(baseUrl), deviceWorkspace, {
18533
+ await store.set(hostFromUrl4(baseUrl), deviceWorkspace, {
18257
18534
  apiKey: result.apiKey.key,
18258
18535
  shipId: getOrCreateShipId(),
18259
18536
  workspaceId: result.workspace.id,
@@ -18261,9 +18538,9 @@ Signed in as ${result.identity.email} \u2014 minted an API key in workspace "${r
18261
18538
  createdAt: result.apiKey.createdAt
18262
18539
  });
18263
18540
  keychainBacked = true;
18264
- rememberSession(cfg.ship.stateDir, hostFromUrl3(baseUrl), deviceWorkspace);
18541
+ rememberSession(cfg.ship.stateDir, hostFromUrl4(baseUrl), deviceWorkspace);
18265
18542
  process.stderr.write(
18266
- `API key stored in the OS keychain for ${hostFromUrl3(baseUrl)}/${deviceWorkspace}.
18543
+ `API key stored in the OS keychain for ${hostFromUrl4(baseUrl)}/${deviceWorkspace}.
18267
18544
  `
18268
18545
  );
18269
18546
  } catch (e) {
@@ -18308,7 +18585,8 @@ Signed in as ${result.identity.email} \u2014 minted an API key in workspace "${r
18308
18585
  connectEvents.emit("signed_in", `key belongs to ${found.meEmail ?? found.meId}`, {
18309
18586
  identity: { id: found.meId, email: found.meEmail, name: found.meName },
18310
18587
  workspaces: [{ slug: found.workspaceSlug, name: found.workspaceName, role: found.role }],
18311
- session: signInSource
18588
+ session: signInSource,
18589
+ host: hostFromUrl4(baseUrl)
18312
18590
  });
18313
18591
  }
18314
18592
  connectEvents.emit("workspace_resolved", `workspace ${found.workspaceSlug}`, {
@@ -18383,8 +18661,8 @@ Signed in as ${result.identity.email} \u2014 minted an API key in workspace "${r
18383
18661
  }
18384
18662
  }
18385
18663
  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 });
18664
+ if (!operator && found.candidates.length > 0 && canAsk) {
18665
+ const picked = await pickOperator(found.candidates, { id: found.meId, name: found.meName, email: found.meEmail });
18388
18666
  if (picked === NEW_OPERATOR_ROW) {
18389
18667
  await createOperatorRow({ ...authOpts, dryRun }, found);
18390
18668
  operator = found.operator;
@@ -18403,7 +18681,7 @@ Signed in as ${result.identity.email} \u2014 minted an API key in workspace "${r
18403
18681
  }
18404
18682
  }
18405
18683
  if (!operator) operator = previous?.operator;
18406
- if (connectEvents.json && !operator && found.holds.length > 0) {
18684
+ if (connectEvents.json && !operator && found.candidates.length > 0) {
18407
18685
  const answer = connectChoices.operator;
18408
18686
  if (answer === "new") {
18409
18687
  await createOperatorRow({ ...authOpts, dryRun }, found);
@@ -18418,27 +18696,36 @@ Signed in as ${result.identity.email} \u2014 minted an API key in workspace "${r
18418
18696
  found.operatorAction = "linked";
18419
18697
  }
18420
18698
  } else {
18421
- 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);
18422
18700
  stopForChoice("operator", "Which Crew row is you?", [
18423
- ...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 } })),
18424
18702
  ...found.meId ? [{ value: "new", label: `Create a new Crew row for ${found.meName ?? found.meEmail ?? "me"}` }] : []
18425
18703
  ]);
18426
18704
  }
18427
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
+ }
18428
18711
  if (operator) {
18429
- 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}`, {
18430
18715
  id: operator,
18431
- action: found.operatorAction ?? "previous"
18716
+ action: found.operatorAction ?? "previous",
18717
+ ...opRow?.name ? { name: opRow.name } : {},
18718
+ ...opEmail ? { email: opEmail } : {}
18432
18719
  });
18433
18720
  }
18434
18721
  if (!operator && !hadPreviousFile) {
18435
18722
  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.
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.
18437
18724
  Re-run from a real terminal to pick one, or pass --key for a key whose own
18438
18725
  identity email matches exactly one Crew row.
18439
18726
  `
18440
18727
  );
18441
- 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");
18442
18729
  }
18443
18730
  const alreadyReviewed = new Set(previous?.reviewedStatuses ?? []);
18444
18731
  const stillUnrecognized = (found.unrecognizedStatuses ?? []).filter((s) => !alreadyReviewed.has(s.value));
@@ -18533,12 +18820,19 @@ Signed in as ${result.identity.email} \u2014 minted an API key in workspace "${r
18533
18820
  role: sr.role,
18534
18821
  id: sr.id,
18535
18822
  action: sr.action,
18823
+ ...sr.shipId ? { ship_id: sr.shipId } : {},
18536
18824
  key: minted?.action ?? "none",
18537
18825
  ...minted?.detail ? { detail: minted.detail } : {}
18538
18826
  });
18539
18827
  }
18540
18828
  let apiKeyFile;
18541
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
+ }
18542
18836
  if ((mintedFromDevice || firstRunFromFlags) && !keychainBacked) {
18543
18837
  apiKeyFile = apiKeyPathFor(cfg.ship.stateDir, route2);
18544
18838
  if (dryRun) {
@@ -18555,7 +18849,7 @@ Signed in as ${result.identity.email} \u2014 minted an API key in workspace "${r
18555
18849
  const keyPersisted = keychainBacked || apiKeyFile !== void 0;
18556
18850
  const wizard = firstRun;
18557
18851
  const writeFirstRun = wizard !== void 0 && !dryRun && keyPersisted;
18558
- 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);
18559
18853
  const routeBlock = renderConnection(
18560
18854
  found,
18561
18855
  route2,
@@ -18704,13 +18998,39 @@ Signed in as ${result.identity.email} \u2014 minted an API key in workspace "${r
18704
18998
  wroteConfig,
18705
18999
  operator,
18706
19000
  dryRun,
18707
- 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` } : {}
18708
19004
  });
18709
19005
  };
18710
19006
  try {
18711
19007
  if (!workspace) {
18712
19008
  const workspaces = await listWorkspaces(authOpts);
18713
- 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) {
18714
19034
  process.stderr.write("crew connect: this key's identity has no workspace memberships.\n");
18715
19035
  stopWithProblem("this key's identity has no workspace memberships", "workspace");
18716
19036
  } else if (workspaces.length === 1) {
@@ -19186,11 +19506,20 @@ Signed in as ${result.identity.email} \u2014 minted an API key in workspace "${r
19186
19506
  if (!resetStore) return { stored: false, clear: async () => {
19187
19507
  } };
19188
19508
  const store = resetStore;
19189
- const h = hostFromUrl3(r.baseUrl);
19509
+ const h = hostFromUrl4(r.baseUrl);
19190
19510
  const [slug] = splitRoute(r.route);
19191
19511
  const session = await store.get(h, slug);
19192
19512
  return { stored: Boolean(session?.apiKey), clear: () => store.clear(h, slug) };
19193
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
+ },
19194
19523
  inspectCheckout(dir) {
19195
19524
  const dirty = status(dir).map((c) => c.path);
19196
19525
  const linkedWorktrees = worktrees(dir).map((w) => w.path).filter((p) => realpathSync3(p) !== realpathSync3(dir));
@@ -19361,12 +19690,12 @@ Signed in as ${result.identity.email} \u2014 minted an API key in workspace "${r
19361
19690
  `);
19362
19691
  }
19363
19692
  }
19364
- 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 });
19365
19694
  say("removed", { dir, closed }, `removed the sample repo${closed.length ? `, closed ${closed.join(", ")}` : ""}`);
19366
19695
  break;
19367
19696
  }
19368
19697
  const projectId = route.resolved.areaId ?? resolveRepoProject(await tracker.projectRows(), value("project"));
19369
- const fresh = !existsSync33(join34(dir, ".git"));
19698
+ const fresh = !existsSync33(join35(dir, ".git"));
19370
19699
  if (existsSync33(dir) && fresh && readdirSync14(dir).length > 0) fail(`${dir} exists and is not a git checkout \u2014 move it aside`);
19371
19700
  if (dryRun) {
19372
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`);
@@ -19374,8 +19703,8 @@ Signed in as ${result.identity.email} \u2014 minted an API key in workspace "${r
19374
19703
  }
19375
19704
  if (fresh) {
19376
19705
  mkdirSync20(dir, { recursive: true });
19377
- writeFileSync21(join34(dir, "README.md"), SAMPLE_README);
19378
- 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);
19379
19708
  const g = (...a) => {
19380
19709
  const r = spawnSync6("git", a, { cwd: dir, encoding: "utf8" });
19381
19710
  if (r.status !== 0) fail(`git ${a[0]} failed in ${dir}: ${(r.stderr || r.stdout).trim()}`);
@@ -19825,6 +20154,11 @@ routes: ${cfg.routes.map((c) => c.route).join(", ")}
19825
20154
  if (warn) process.stdout.write(` WARNING ${warn}
19826
20155
  `);
19827
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
+ }
19828
20162
  }
19829
20163
  const missingAuthorship = await tracker.missingAuthorshipColumns();
19830
20164
  process.stdout.write(
@@ -20145,7 +20479,7 @@ ${foreign.join("\n")}
20145
20479
  break;
20146
20480
  }
20147
20481
  default:
20148
- usage();
20482
+ usage(`unknown command "${command}"`);
20149
20483
  }
20150
20484
  lock?.release();
20151
20485
  }