@tablation/crew 0.60.0 → 0.61.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (3) hide show
  1. package/README.md +8 -0
  2. package/dist/cli.js +441 -211
  3. package/package.json +1 -1
package/dist/cli.js CHANGED
@@ -2332,6 +2332,7 @@ function parseResolved(raw, m, where) {
2332
2332
  ...typeof raw.releaseSeat === "string" && raw.releaseSeat ? { releaseSeat: raw.releaseSeat } : {},
2333
2333
  ...typeof raw.releaseAgentId === "string" && raw.releaseAgentId ? { releaseAgentId: raw.releaseAgentId } : {},
2334
2334
  ...typeof raw.role === "string" && raw.role ? { role: raw.role } : {},
2335
+ ...Array.isArray(raw.capabilityMisses) && raw.capabilityMisses.length > 0 ? { capabilityMisses: raw.capabilityMisses.filter((x) => typeof x === "string") } : {},
2335
2336
  ...Object.keys(agentPersonas).length > 0 ? { agentPersonas } : {},
2336
2337
  ...Object.keys(agentSkills).length > 0 ? { agentSkills } : {}
2337
2338
  };
@@ -5182,10 +5183,11 @@ function doctorSelftestLines(stored, toolchains) {
5182
5183
  }
5183
5184
 
5184
5185
  // src/cli.ts
5185
- import { resolve as resolve5, dirname as dirname16, join as join29, basename as basename10 } from "path";
5186
+ import { resolve as resolve5, dirname as dirname17, join as join30, basename as basename10 } from "path";
5186
5187
  import { fileURLToPath } from "url";
5187
5188
  import { hostname as hostname2, homedir as homedir8 } from "os";
5188
5189
  import { spawnSync as spawnSync6 } from "child_process";
5190
+ import { parse as parseYaml2 } from "yaml";
5189
5191
 
5190
5192
  // src/argv.ts
5191
5193
  var VALUE_FLAGS = /* @__PURE__ */ new Set([
@@ -5760,6 +5762,103 @@ var State = class _State {
5760
5762
  }
5761
5763
  };
5762
5764
 
5765
+ // src/reset.ts
5766
+ import { existsSync as existsSync14, mkdirSync as mkdirSync7, readFileSync as readFileSync13, readdirSync as readdirSync4, rmSync as rmSync7, statSync as statSync5, writeFileSync as writeFileSync8 } from "fs";
5767
+ import { dirname as dirname6, join as join14 } from "path";
5768
+ function clonedCheckoutsFile(stateDir) {
5769
+ return join14(stateDir, "cloned-checkouts.json");
5770
+ }
5771
+ function readClonedCheckouts(stateDir) {
5772
+ try {
5773
+ const parsed = JSON.parse(readFileSync13(clonedCheckoutsFile(stateDir), "utf8"));
5774
+ return Array.isArray(parsed) ? parsed.filter((d) => typeof d === "string") : [];
5775
+ } catch {
5776
+ return [];
5777
+ }
5778
+ }
5779
+ function recordClonedCheckout(stateDir, dir) {
5780
+ const known = readClonedCheckouts(stateDir);
5781
+ if (known.includes(dir)) return;
5782
+ const file = clonedCheckoutsFile(stateDir);
5783
+ mkdirSync7(dirname6(file), { recursive: true });
5784
+ writeFileSync8(file, `${JSON.stringify([...known, dir], null, 2)}
5785
+ `);
5786
+ }
5787
+ var mark = (dryRun) => dryRun ? "would-remove" : "removed";
5788
+ async function runReset(o, fx) {
5789
+ const steps = [];
5790
+ for (const line of await fx.uninstallUnits(o.dryRun)) steps.push({ step: "scheduler", target: line, action: mark(o.dryRun) });
5791
+ const urls = [...new Set(o.routes.map((r) => r.baseUrl))];
5792
+ for (const url of urls) {
5793
+ for (const line of fx.removePair(url, o.dryRun)) steps.push({ step: "pair", target: line, action: mark(o.dryRun) });
5794
+ }
5795
+ for (const route of o.routes) {
5796
+ for (const r of await fx.revokeSeatKeys(route, o.dryRun)) {
5797
+ steps.push({ step: "seat-keys", target: `${route.route}: ${r.line}`, action: r.ok ? mark(o.dryRun) : "failed" });
5798
+ }
5799
+ }
5800
+ for (const route of [...o.routes, ...o.droppedRoutes ?? []]) {
5801
+ try {
5802
+ const cred = await fx.credential(route);
5803
+ if (!cred.stored) {
5804
+ steps.push({ step: "credential", target: route.route, action: "absent", detail: "no workspace credential in the keychain" });
5805
+ continue;
5806
+ }
5807
+ if (!o.dryRun) await cred.clear();
5808
+ steps.push({ step: "credential", target: route.route, action: mark(o.dryRun), detail: "workspace credential in the keychain" });
5809
+ } catch (e) {
5810
+ steps.push({ step: "credential", target: route.route, action: "failed", detail: e.message });
5811
+ }
5812
+ }
5813
+ if (o.removeCheckouts) {
5814
+ for (const dir of readClonedCheckouts(o.stateDir)) steps.push(removeCheckout(dir, o.dryRun, fx));
5815
+ }
5816
+ steps.push(...removeState(o.stateDir, o.dryRun));
5817
+ if (existsSync14(o.configFile)) {
5818
+ if (!o.dryRun) rmSync7(o.configFile, { force: true });
5819
+ steps.push({ step: "config", target: o.configFile, action: mark(o.dryRun) });
5820
+ } else {
5821
+ steps.push({ step: "config", target: o.configFile, action: "absent" });
5822
+ }
5823
+ return steps;
5824
+ }
5825
+ function removeCheckout(dir, dryRun, fx) {
5826
+ if (!existsSync14(dir)) return { step: "checkout", target: dir, action: "absent", detail: "already gone" };
5827
+ const { dirty, linkedWorktrees } = fx.inspectCheckout(dir);
5828
+ if (dirty.length) {
5829
+ return { step: "checkout", target: dir, action: "kept", detail: `has uncommitted changes (${dirty.length} path${dirty.length === 1 ? "" : "s"}: ${dirty.slice(0, 5).join(", ")}${dirty.length > 5 ? ", \u2026" : ""})` };
5830
+ }
5831
+ if (linkedWorktrees.length) {
5832
+ return { step: "checkout", target: dir, action: "kept", detail: `has linked worktrees (${linkedWorktrees.join(", ")}); remove them first` };
5833
+ }
5834
+ if (!dryRun) rmSync7(dir, { recursive: true, force: true });
5835
+ return { step: "checkout", target: dir, action: mark(dryRun) };
5836
+ }
5837
+ function removeState(stateDir, dryRun) {
5838
+ const out = [];
5839
+ const targets = [join14(stateDir, "resolved"), join14(stateDir, "keys"), clonedCheckoutsFile(stateDir), join14(stateDir, "reload-requested"), join14(stateDir, "daemon.started")];
5840
+ try {
5841
+ for (const name of readdirSync4(stateDir)) {
5842
+ if (name.startsWith(".") && statSync5(join14(stateDir, name)).isFile()) targets.push(join14(stateDir, name));
5843
+ }
5844
+ } catch {
5845
+ }
5846
+ for (const t of targets) {
5847
+ if (!existsSync14(t)) continue;
5848
+ if (!dryRun) rmSync7(t, { recursive: true, force: true });
5849
+ out.push({ step: "state", target: t, action: mark(dryRun) });
5850
+ }
5851
+ if (!out.length) out.push({ step: "state", target: stateDir, action: "absent", detail: "nothing to remove" });
5852
+ return out;
5853
+ }
5854
+ function describeResetStep(s) {
5855
+ const verb = { removed: "removed", "would-remove": "would remove", absent: "nothing to remove", kept: "KEPT", failed: "FAILED" }[s.action];
5856
+ return `${s.step}: ${verb}: ${s.target}${s.detail ? ` (${s.detail})` : ""}`;
5857
+ }
5858
+ function resetIncomplete(steps) {
5859
+ return steps.some((s) => s.action === "failed" || s.action === "kept");
5860
+ }
5861
+
5763
5862
  // src/tracker.ts
5764
5863
  import { hostname } from "os";
5765
5864
  import { TablationClient as TablationClient3 } from "@tablation/client";
@@ -6154,24 +6253,24 @@ function recoveredFailureHeadlines(o, route) {
6154
6253
  }
6155
6254
 
6156
6255
  // src/ship-attention.ts
6157
- import { existsSync as existsSync15, statfsSync } from "fs";
6256
+ import { existsSync as existsSync16, statfsSync } from "fs";
6158
6257
 
6159
6258
  // src/git.ts
6160
6259
  import { execFileSync as execFileSync2 } from "child_process";
6161
6260
  import {
6162
- existsSync as existsSync14,
6163
- mkdirSync as mkdirSync7,
6261
+ existsSync as existsSync15,
6262
+ mkdirSync as mkdirSync8,
6164
6263
  openSync as openSync3,
6165
6264
  closeSync as closeSync3,
6166
6265
  writeSync,
6167
- statSync as statSync5,
6266
+ statSync as statSync6,
6168
6267
  unlinkSync as unlinkSync2,
6169
- readFileSync as readFileSync13,
6170
- writeFileSync as writeFileSync8,
6171
- readdirSync as readdirSync4,
6172
- rmSync as rmSync7
6268
+ readFileSync as readFileSync14,
6269
+ writeFileSync as writeFileSync9,
6270
+ readdirSync as readdirSync5,
6271
+ rmSync as rmSync8
6173
6272
  } from "fs";
6174
- import { dirname as dirname6, basename as basename5, join as join14 } from "path";
6273
+ import { dirname as dirname7, basename as basename5, join as join15 } from "path";
6175
6274
  var GitError = class extends Error {
6176
6275
  };
6177
6276
  function streamText(x) {
@@ -6537,8 +6636,8 @@ function recoverStrandedReleaseCheckouts(cwd) {
6537
6636
  if (line.startsWith("worktree ")) path = line.slice("worktree ".length);
6538
6637
  else if (line.startsWith("branch refs/heads/crew/release-") && path) {
6539
6638
  gitOk(cwd, ["worktree", "remove", "--force", path]);
6540
- const root = dirname6(path);
6541
- if (basename5(root).startsWith("crew-release-")) rmSync7(root, { recursive: true, force: true });
6639
+ const root = dirname7(path);
6640
+ if (basename5(root).startsWith("crew-release-")) rmSync8(root, { recursive: true, force: true });
6542
6641
  }
6543
6642
  }
6544
6643
  const stranded = (gitOk(cwd, ["for-each-ref", "--format=%(refname:short)", "refs/heads/crew/release-*"]) ?? "").split("\n").filter(Boolean);
@@ -6549,18 +6648,18 @@ function sweepStaleReleaseRoots(tmp, now = Date.now(), maxAgeMs = 60 * 60 * 1e3)
6549
6648
  const removed = [];
6550
6649
  let names;
6551
6650
  try {
6552
- names = readdirSync4(tmp);
6651
+ names = readdirSync5(tmp);
6553
6652
  } catch {
6554
6653
  return removed;
6555
6654
  }
6556
6655
  for (const name of names) {
6557
6656
  if (!name.startsWith("crew-release-")) continue;
6558
- const root = join14(tmp, name);
6657
+ const root = join15(tmp, name);
6559
6658
  try {
6560
- const st = statSync5(root);
6659
+ const st = statSync6(root);
6561
6660
  if (!st.isDirectory() || now - st.mtimeMs < maxAgeMs) continue;
6562
- if (existsSync14(join14(root, "checkout"))) continue;
6563
- rmSync7(root, { recursive: true, force: true });
6661
+ if (existsSync15(join15(root, "checkout"))) continue;
6662
+ rmSync8(root, { recursive: true, force: true });
6564
6663
  removed.push(name);
6565
6664
  } catch {
6566
6665
  }
@@ -6602,13 +6701,13 @@ function originUrlOf(dir, remoteName) {
6602
6701
  }
6603
6702
  }
6604
6703
  function ensureRepoCheckout(dir, remote, opts = {}) {
6605
- if (existsSync14(`${dir}/.git`)) return false;
6704
+ if (existsSync15(`${dir}/.git`)) return false;
6606
6705
  if (!remote) {
6607
6706
  throw new GitError(
6608
6707
  `${dir} does not exist and this repo has no discovered remote to clone from \u2014 re-run \`crew connect\`, or check out the repository yourself and point \`repos:\` at it`
6609
6708
  );
6610
6709
  }
6611
- mkdirSync7(dirname6(dir), { recursive: true });
6710
+ mkdirSync8(dirname7(dir), { recursive: true });
6612
6711
  const url = cloneUrlFor(remote, opts);
6613
6712
  try {
6614
6713
  execFileSync2("git", ["clone", url, dir], { encoding: "utf8", stdio: ["ignore", "pipe", "pipe"] });
@@ -6617,8 +6716,13 @@ function ensureRepoCheckout(dir, remote, opts = {}) {
6617
6716
  const why = [err.stderr, err.stdout].map(streamText).filter(Boolean).join(" / ");
6618
6717
  throw new GitError(`git clone ${url} ${dir}: ${why || err.message.trim()}`);
6619
6718
  }
6719
+ cloneRecorder?.(dir);
6620
6720
  return true;
6621
6721
  }
6722
+ var cloneRecorder;
6723
+ function setCloneRecorder(fn) {
6724
+ cloneRecorder = fn;
6725
+ }
6622
6726
  var CHECKOUT_LOCK_STALE_MS = 5 * 60 * 1e3;
6623
6727
  function checkoutLockPath(dir) {
6624
6728
  return `${dir}.sync.lock`;
@@ -6628,7 +6732,7 @@ function sleepSync(ms2) {
6628
6732
  }
6629
6733
  function acquireCheckoutLock(dir, timeoutMs = 3e4) {
6630
6734
  const lockPath = checkoutLockPath(dir);
6631
- mkdirSync7(dirname6(lockPath), { recursive: true });
6735
+ mkdirSync8(dirname7(lockPath), { recursive: true });
6632
6736
  const deadline = Date.now() + timeoutMs;
6633
6737
  for (; ; ) {
6634
6738
  try {
@@ -6646,7 +6750,7 @@ function acquireCheckoutLock(dir, timeoutMs = 3e4) {
6646
6750
  const err = e;
6647
6751
  if (err.code !== "EEXIST") throw err;
6648
6752
  try {
6649
- if (Date.now() - statSync5(lockPath).mtimeMs > CHECKOUT_LOCK_STALE_MS) {
6753
+ if (Date.now() - statSync6(lockPath).mtimeMs > CHECKOUT_LOCK_STALE_MS) {
6650
6754
  try {
6651
6755
  unlinkSync2(lockPath);
6652
6756
  } catch {
@@ -6665,17 +6769,17 @@ function acquireCheckoutLock(dir, timeoutMs = 3e4) {
6665
6769
  function syncPassengerCheckout(dir, githubRemote, gitRemoteName, base, clone = {}) {
6666
6770
  const release = acquireCheckoutLock(dir);
6667
6771
  try {
6668
- if (!existsSync14(`${dir}/.git`) && githubRemote) {
6772
+ if (!existsSync15(`${dir}/.git`) && githubRemote) {
6669
6773
  const url = cloneUrlFor(githubRemote, clone);
6670
6774
  const marker = cloneFailureMarker(dir);
6671
- if (existsSync14(marker) && readFileSync13(marker, "utf8").split("\n")[0] === url) {
6672
- return { action: "clone-failed", detail: readFileSync13(marker, "utf8").split("\n").slice(1).join("\n"), repeat: true };
6775
+ if (existsSync15(marker) && readFileSync14(marker, "utf8").split("\n")[0] === url) {
6776
+ return { action: "clone-failed", detail: readFileSync14(marker, "utf8").split("\n").slice(1).join("\n"), repeat: true };
6673
6777
  }
6674
6778
  try {
6675
6779
  ensureRepoCheckout(dir, githubRemote, clone);
6676
6780
  } catch (e) {
6677
6781
  if (!(e instanceof GitError)) throw e;
6678
- writeFileSync8(marker, `${url}
6782
+ writeFileSync9(marker, `${url}
6679
6783
  ${e.message}`);
6680
6784
  return { action: "clone-failed", detail: e.message, repeat: false };
6681
6785
  }
@@ -6873,7 +6977,7 @@ function noteHeldWorktree(state, h, nowMs = Date.now()) {
6873
6977
  function reconcileHeldWorktrees(state, repo, log, stillRefused) {
6874
6978
  for (const i of state.shipAttention().previous()) {
6875
6979
  if (i.kind !== "worktree_held" || !i.held || i.held.repo !== repo) continue;
6876
- const gone = !existsSync15(i.held.path);
6980
+ const gone = !existsSync16(i.held.path);
6877
6981
  if (!gone && !(stillRefused && !stillRefused.has(i.held.path))) continue;
6878
6982
  clearShipAttention(state, i.key);
6879
6983
  log.emit(
@@ -8587,8 +8691,8 @@ function materializeBranchForTicket(dir, cfg, t, role, onProblem) {
8587
8691
  }
8588
8692
 
8589
8693
  // src/poll.ts
8590
- import { writeFileSync as writeFileSync9 } from "fs";
8591
- import { join as join15 } from "path";
8694
+ import { writeFileSync as writeFileSync10 } from "fs";
8695
+ import { join as join16 } from "path";
8592
8696
 
8593
8697
  // src/stalled.ts
8594
8698
  var STALLED_AFTER_CYCLES = 30;
@@ -8877,7 +8981,7 @@ async function decideCycle(o) {
8877
8981
  }
8878
8982
  return decision;
8879
8983
  }
8880
- var digestPath = (stateDir, route, role) => join15(stateDir, `digest-${route}-${role}.md`);
8984
+ var digestPath = (stateDir, route, role) => join16(stateDir, `digest-${route}-${role}.md`);
8881
8985
  function writeDigest(o, d, role, stateDir) {
8882
8986
  const me = o.route.resolved?.seats[role];
8883
8987
  if (!me) return false;
@@ -8941,7 +9045,7 @@ function writeDigest(o, d, role, stateDir) {
8941
9045
  };
8942
9046
  const text = role === "qa" ? qaDigest(input) : buildingDigest(input);
8943
9047
  const path = digestPath(stateDir, routeSlug(o.route.route), role);
8944
- writeFileSync9(path, text);
9048
+ writeFileSync10(path, text);
8945
9049
  o.emit.emit(`queue digest written for ${role} (${Buffer.byteLength(text)} bytes)`, {
8946
9050
  data: { role, bytes: Buffer.byteLength(text) }
8947
9051
  });
@@ -9318,13 +9422,13 @@ async function applyStrandedVerified(writer, s, contract, operatorId, memberId,
9318
9422
  }
9319
9423
 
9320
9424
  // src/run-worktree.ts
9321
- import { existsSync as existsSync17 } from "fs";
9322
- import { dirname as dirname7, join as join17 } from "path";
9425
+ import { existsSync as existsSync18 } from "fs";
9426
+ import { dirname as dirname8, join as join18 } from "path";
9323
9427
 
9324
9428
  // src/ports.ts
9325
9429
  import { execFileSync as execFileSync3 } from "child_process";
9326
- import { existsSync as existsSync16 } from "fs";
9327
- import { join as join16 } from "path";
9430
+ import { existsSync as existsSync17 } from "fs";
9431
+ import { join as join17 } from "path";
9328
9432
  var run = (cmd, args) => {
9329
9433
  try {
9330
9434
  return execFileSync3(cmd, args, { encoding: "utf8", stdio: ["ignore", "pipe", "ignore"] });
@@ -9369,7 +9473,7 @@ function ticketForPort(port, s = DEFAULT_PORTS) {
9369
9473
  }
9370
9474
  function worktreeExists(parent, prefix, n) {
9371
9475
  for (const name of [`${prefix}${n}`, `${prefix}${String(n).padStart(3, "0")}`]) {
9372
- if (existsSync16(join16(parent, name, ".git"))) return true;
9476
+ if (existsSync17(join17(parent, name, ".git"))) return true;
9373
9477
  }
9374
9478
  return false;
9375
9479
  }
@@ -9445,7 +9549,7 @@ function planRunWorktree(dir, cfg, t, role, opts = {}) {
9445
9549
  const existing = worktreeForNumber(dir, num);
9446
9550
  const located = locateBranchForTicket(dir, cfg, t, role);
9447
9551
  const baseRef = hasRemote ? `${remote}/${base}` : base;
9448
- if (existing && !existing.prunable && existsSync17(existing.path)) {
9552
+ if (existing && !existing.prunable && existsSync18(existing.path)) {
9449
9553
  const branch = existing.branch;
9450
9554
  if (status(existing.path).length > 0) {
9451
9555
  return { action: opts.resumeInterrupted ? "resume-dirty" : "leave-dirty", path: existing.path, branch };
@@ -9461,7 +9565,7 @@ function planRunWorktree(dir, cfg, t, role, opts = {}) {
9461
9565
  if (countCommits(existing.path, `${baseRef}..HEAD`) > 0) return { action: "leave-unpushed", path: existing.path, branch };
9462
9566
  return { action: "recut", path: existing.path, branch, base: baseRef };
9463
9567
  }
9464
- const path = join17(dirname7(dir), worktreeForTicket(dir, cfg, t, role));
9568
+ const path = join18(dirname8(dir), worktreeForTicket(dir, cfg, t, role));
9465
9569
  if (located?.where === "remote" || located && hasRemote && remoteBranchExists(dir, remote, located.branch)) {
9466
9570
  return { action: "cut-remote", path, branch: located.branch };
9467
9571
  }
@@ -9510,7 +9614,7 @@ function worktreeHoldsUnpushed(path, remote) {
9510
9614
  }
9511
9615
  async function finishRunWorktree(dir, cfg, branch) {
9512
9616
  const wt = worktrees(dir).find((w) => w.branch === branch && !w.prunable && w.path !== dir);
9513
- if (!wt || !existsSync17(wt.path)) return { kind: "none" };
9617
+ if (!wt || !existsSync18(wt.path)) return { kind: "none" };
9514
9618
  if (cfg.hooks.handoff) return { kind: "kept-handoff", path: wt.path };
9515
9619
  if (status(wt.path).length > 0) return { kind: "kept-dirty", path: wt.path };
9516
9620
  if (remoteConfigured(dir, cfg.branch.remote) && worktreeHoldsUnpushed(wt.path, cfg.branch.remote)) {
@@ -9522,7 +9626,7 @@ async function finishRunWorktree(dir, cfg, branch) {
9522
9626
  if (pids.length) await killGently(pids);
9523
9627
  }
9524
9628
  gitOk(dir, ["worktree", "remove", wt.path]);
9525
- return existsSync17(wt.path) ? { kind: "kept-dirty", path: wt.path } : { kind: "removed", path: wt.path };
9629
+ return existsSync18(wt.path) ? { kind: "kept-dirty", path: wt.path } : { kind: "removed", path: wt.path };
9526
9630
  }
9527
9631
 
9528
9632
  // src/handoff-guard.ts
@@ -9783,13 +9887,13 @@ async function guardRunStrays(route, ship, tracker, role, workingId, since2, log
9783
9887
  }
9784
9888
 
9785
9889
  // src/connect-wizard.ts
9786
- import { delimiter, dirname as dirname8, join as join18 } from "path";
9787
- import { accessSync as accessSync2, constants as constants2, existsSync as existsSync18, mkdirSync as mkdirSync8, writeFileSync as writeFileSync10 } from "fs";
9890
+ import { delimiter, dirname as dirname9, join as join19 } from "path";
9891
+ import { accessSync as accessSync2, constants as constants2, existsSync as existsSync19, mkdirSync as mkdirSync9, writeFileSync as writeFileSync11 } from "fs";
9788
9892
  import { stringify } from "yaml";
9789
9893
  var HOST_PASSENGERS_DOCS_URL = "https://crew.tablation.dev/host-passengers";
9790
9894
  function shouldRunWizard(opts) {
9791
9895
  if (!opts.isTTY) return false;
9792
- const exists = opts.configExists ?? existsSync18;
9896
+ const exists = opts.configExists ?? existsSync19;
9793
9897
  return !configSearchPath(opts.crewHome).some((p) => exists(p));
9794
9898
  }
9795
9899
  function firstRunConfigPath() {
@@ -9806,7 +9910,7 @@ function defaultIsExecutable(p) {
9806
9910
  function findOnPath2(name, pathEnv, isExecutable = defaultIsExecutable) {
9807
9911
  for (const dir of pathEnv.split(delimiter)) {
9808
9912
  if (!dir) continue;
9809
- const candidate = join18(dir, name);
9913
+ const candidate = join19(dir, name);
9810
9914
  if (isExecutable(candidate)) return candidate;
9811
9915
  }
9812
9916
  return void 0;
@@ -9832,9 +9936,9 @@ async function runWizard(p, env) {
9832
9936
  if (entered && isExec(entered)) agentBin = entered;
9833
9937
  else p.say(entered ? `${entered} is not an executable file.` : "A path is required.");
9834
9938
  }
9835
- const nodeDir = dirname8(env.nodePath);
9939
+ const nodeDir = dirname9(env.nodePath);
9836
9940
  const pnpmPath = findOnPath2("pnpm", env.pathEnv, isExec);
9837
- const baseDirs = [nodeDir, pnpmPath ? dirname8(pnpmPath) : void 0];
9941
+ const baseDirs = [nodeDir, pnpmPath ? dirname9(pnpmPath) : void 0];
9838
9942
  p.say(
9839
9943
  `
9840
9944
  Host Passengers lets this machine run the agent sessions that other people's
@@ -9845,7 +9949,7 @@ Tablation workspaces ask for, in a container. More: ${HOST_PASSENGERS_DOCS_URL}`
9845
9949
  p.say("Docker is not available here, so nothing will be hosted until it is. Your answer is recorded anyway.");
9846
9950
  }
9847
9951
  const dockerPath = hostPassengers ? findOnPath2("docker", env.pathEnv, isExec) : void 0;
9848
- const extraPath = deriveExtraPath([...baseDirs, dockerPath ? dirname8(dockerPath) : void 0]);
9952
+ const extraPath = deriveExtraPath([...baseDirs, dockerPath ? dirname9(dockerPath) : void 0]);
9849
9953
  if (extraPath) {
9850
9954
  p.say(`Hooks will get ${extraPath} on their PATH (where node, pnpm${dockerPath ? " and docker" : ""} live).`);
9851
9955
  }
@@ -9867,9 +9971,9 @@ routes:
9867
9971
  ${routeBlock}`;
9868
9972
  }
9869
9973
  function writeNewConfig(path, text) {
9870
- mkdirSync8(dirname8(path), { recursive: true });
9974
+ mkdirSync9(dirname9(path), { recursive: true });
9871
9975
  try {
9872
- writeFileSync10(path, text, { mode: 384, flag: "wx" });
9976
+ writeFileSync11(path, text, { mode: 384, flag: "wx" });
9873
9977
  return true;
9874
9978
  } catch (e) {
9875
9979
  if (e.code === "EEXIST") return false;
@@ -9902,7 +10006,7 @@ Wrote ${o.configPath}.
9902
10006
 
9903
10007
  // src/connect-json.ts
9904
10008
  import { accessSync as accessSync3, constants as constants3 } from "fs";
9905
- import { dirname as dirname9 } from "path";
10009
+ import { dirname as dirname10 } from "path";
9906
10010
  var CHOICE_EXIT_CODE = 3;
9907
10011
  function createEmitter(json, write) {
9908
10012
  return {
@@ -9950,7 +10054,7 @@ function flagWizardAnswers(f, env) {
9950
10054
  if (f.agentBin && !isExec(f.agentBin)) return { error: `--agent-bin ${f.agentBin} is not an executable file` };
9951
10055
  const pnpmPath = findOnPath2("pnpm", env.pathEnv, isExec);
9952
10056
  const dockerPath = f.hostPassengers ? findOnPath2("docker", env.pathEnv, isExec) : void 0;
9953
- const extraPath = f.extraPath ?? deriveExtraPath([dirname9(env.nodePath), pnpmPath ? dirname9(pnpmPath) : void 0, dockerPath ? dirname9(dockerPath) : void 0]);
10057
+ const extraPath = f.extraPath ?? deriveExtraPath([dirname10(env.nodePath), pnpmPath ? dirname10(pnpmPath) : void 0, dockerPath ? dirname10(dockerPath) : void 0]);
9954
10058
  return {
9955
10059
  answers: {
9956
10060
  name: f.shipName?.trim() || env.hostname,
@@ -9963,8 +10067,8 @@ function flagWizardAnswers(f, env) {
9963
10067
  }
9964
10068
 
9965
10069
  // src/release-run.ts
9966
- import { readFileSync as readFileSync14, writeFileSync as writeFileSync11, existsSync as existsSync19, copyFileSync, mkdirSync as mkdirSync9, mkdtempSync as mkdtempSync2, rmSync as rmSync8 } from "fs";
9967
- import { dirname as dirname10, join as join19 } from "path";
10070
+ import { readFileSync as readFileSync15, writeFileSync as writeFileSync12, existsSync as existsSync20, copyFileSync, mkdirSync as mkdirSync10, mkdtempSync as mkdtempSync2, rmSync as rmSync9 } from "fs";
10071
+ import { dirname as dirname11, join as join20 } from "path";
9968
10072
  import { tmpdir as tmpdir3 } from "os";
9969
10073
 
9970
10074
  // src/release.ts
@@ -10652,13 +10756,13 @@ async function bump(o, size) {
10652
10756
  }
10653
10757
  return printed;
10654
10758
  }
10655
- const files = o.repo.release.versionFiles.map((f) => join19(o.cwd, f));
10759
+ const files = o.repo.release.versionFiles.map((f) => join20(o.cwd, f));
10656
10760
  const first = files[0];
10657
- if (!existsSync19(first)) {
10761
+ if (!existsSync20(first)) {
10658
10762
  o.emit.error(`no ${o.repo.release.versionFiles[0]} to read a version from`);
10659
10763
  return null;
10660
10764
  }
10661
- const current = JSON.parse(readFileSync14(first, "utf8")).version;
10765
+ const current = JSON.parse(readFileSync15(first, "utf8")).version;
10662
10766
  if (!current) {
10663
10767
  o.emit.error(`no version field in ${o.repo.release.versionFiles[0]}`);
10664
10768
  return null;
@@ -10671,27 +10775,27 @@ async function bump(o, size) {
10671
10775
  return version;
10672
10776
  }
10673
10777
  for (const f of files) {
10674
- const src = readFileSync14(f, "utf8");
10778
+ const src = readFileSync15(f, "utf8");
10675
10779
  const out = src.replace(/("version"\s*:\s*")[^"]*(")/, `$1${version}$2`);
10676
10780
  if (out === src) {
10677
10781
  o.emit.error(`no version field in ${f}`);
10678
10782
  return null;
10679
10783
  }
10680
- writeFileSync11(f, out);
10784
+ writeFileSync12(f, out);
10681
10785
  }
10682
10786
  return version;
10683
10787
  }
10684
10788
  function writeChangelog(o, version, entries) {
10685
10789
  const file = o.repo.release.changelog;
10686
10790
  if (!file || entries.length === 0) return;
10687
- const path = join19(o.cwd, file);
10791
+ const path = join20(o.cwd, file);
10688
10792
  const section = renderChangelogSection(version, (/* @__PURE__ */ new Date()).toISOString().slice(0, 10), entries);
10689
10793
  if (o.dryRun) {
10690
10794
  o.emit.emit(`would prepend ${entries.length} entr(ies) to ${file}`);
10691
10795
  return;
10692
10796
  }
10693
- const existing = existsSync19(path) ? readFileSync14(path, "utf8") : "# Changelog\n";
10694
- writeFileSync11(path, insertChangelogSection(existing, section));
10797
+ const existing = existsSync20(path) ? readFileSync15(path, "utf8") : "# Changelog\n";
10798
+ writeFileSync12(path, insertChangelogSection(existing, section));
10695
10799
  }
10696
10800
  async function confirm(o, expected) {
10697
10801
  if (!o.repo.hooks.released) return void 0;
@@ -10774,7 +10878,7 @@ function closeReleaseCheckout(cwd, root, dir, branch, run2 = gitOk) {
10774
10878
  run2(cwd, ["worktree", "remove", "--force", dir]);
10775
10879
  } finally {
10776
10880
  try {
10777
- rmSync8(root, { recursive: true, force: true });
10881
+ rmSync9(root, { recursive: true, force: true });
10778
10882
  } finally {
10779
10883
  run2(cwd, ["worktree", "prune"]);
10780
10884
  run2(cwd, ["branch", "-D", branch]);
@@ -10784,8 +10888,8 @@ function closeReleaseCheckout(cwd, root, dir, branch, run2 = gitOk) {
10784
10888
  function openReleaseCheckout(o) {
10785
10889
  const { remote, base } = o.repo.branch;
10786
10890
  const branch = `crew/release-${Date.now()}`;
10787
- const root = mkdtempSync2(join19(tmpdir3(), "crew-release-"));
10788
- const dir = join19(root, "checkout");
10891
+ const root = mkdtempSync2(join20(tmpdir3(), "crew-release-"));
10892
+ const dir = join20(root, "checkout");
10789
10893
  o.emit.emit(`release checkout: ${dir}`, { step: "release" });
10790
10894
  const close = () => closeReleaseCheckout(o.cwd, root, dir, branch);
10791
10895
  try {
@@ -10795,10 +10899,10 @@ function openReleaseCheckout(o) {
10795
10899
  throw e;
10796
10900
  }
10797
10901
  for (const f of o.repo.worktrees.copy) {
10798
- const src = join19(o.cwd, f);
10799
- if (!existsSync19(src)) continue;
10800
- mkdirSync9(dirname10(join19(dir, f)), { recursive: true });
10801
- copyFileSync(src, join19(dir, f));
10902
+ const src = join20(o.cwd, f);
10903
+ if (!existsSync20(src)) continue;
10904
+ mkdirSync10(dirname11(join20(dir, f)), { recursive: true });
10905
+ copyFileSync(src, join20(dir, f));
10802
10906
  }
10803
10907
  return { dir, branch, close };
10804
10908
  }
@@ -11466,7 +11570,7 @@ function missingReleaserWarning(repo, release, ships, now = Date.now()) {
11466
11570
  }
11467
11571
 
11468
11572
  // src/watch.ts
11469
- import { watch as watchFile, existsSync as existsSync20, statSync as statSync6, openSync as openSync4, readSync as readSync2, closeSync as closeSync4 } from "fs";
11573
+ import { watch as watchFile, existsSync as existsSync21, statSync as statSync7, openSync as openSync4, readSync as readSync2, closeSync as closeSync4 } from "fs";
11470
11574
  var E = "\x1B[";
11471
11575
  var dim = (s) => `${E}2m${s}${E}0m`;
11472
11576
  var bold = (s) => `${E}1m${s}${E}0m`;
@@ -11567,8 +11671,8 @@ function renderHeaders(states, now = Date.now()) {
11567
11671
  return states.map((s) => renderHeader(s, now)).join("");
11568
11672
  }
11569
11673
  function readFrom(file, from) {
11570
- if (!existsSync20(file)) return { events: [], offset: from };
11571
- const size = statSync6(file).size;
11674
+ if (!existsSync21(file)) return { events: [], offset: from };
11675
+ const size = statSync7(file).size;
11572
11676
  if (size < from) return readFrom(file, 0);
11573
11677
  if (size === from) return { events: [], offset: from };
11574
11678
  const fd = openSync4(file, "r");
@@ -12518,9 +12622,54 @@ async function canMintAgentKeys(route, o = {}) {
12518
12622
  }
12519
12623
  }
12520
12624
 
12625
+ // src/capability-check.ts
12626
+ var WRITE_LEVELS = /* @__PURE__ */ new Set(["WRITE", "DELETE"]);
12627
+ async function canWriteTable(o, modelId) {
12628
+ try {
12629
+ const { access } = await get(o, `/data-models/${modelId}/access`);
12630
+ return access === void 0 ? void 0 : WRITE_LEVELS.has(access);
12631
+ } catch {
12632
+ return void 0;
12633
+ }
12634
+ }
12635
+ async function projectCapabilities(o, projectId) {
12636
+ if (!projectId) return void 0;
12637
+ try {
12638
+ return await get(o, `/projects/${projectId}/capabilities`);
12639
+ } catch {
12640
+ return void 0;
12641
+ }
12642
+ }
12643
+ async function checkCapabilities(o, found) {
12644
+ const admin = isAdminRole(found.role);
12645
+ const table2 = async (key, label, tableName2, modelId, askFor) => {
12646
+ if (!modelId) return { key, label, status: "skipped", note: `no ${tableName2} table in this project` };
12647
+ const ok = await canWriteTable(o, modelId);
12648
+ if (ok === void 0) return { key, label, status: "skipped", note: "could not read access" };
12649
+ return ok ? { key, label, status: "ok" } : { key, label, status: "missing", askFor };
12650
+ };
12651
+ const [crew, ships, repos, caps] = await Promise.all([
12652
+ table2("crew-table", "write the Crew table", "Crew", found.crewModelId ?? found.models.crew, "WRITE access on the Crew table"),
12653
+ table2("ships-table", "write the Ships table", "Ships", found.shipsModelId, "WRITE access on the Ships table"),
12654
+ table2("repos-table", "create Repos rows", "Repos", found.reposModelId, "WRITE access on the Repos table"),
12655
+ projectCapabilities(o, found.projectId)
12656
+ ]);
12657
+ const mint = admin || caps?.MINT_AGENT_KEYS ? { key: "mint-agent-keys", label: "mint agent keys", status: "ok" } : caps ? { key: "mint-agent-keys", label: "mint agent keys", status: "missing", askFor: 'the "Mint agent keys" capability' } : { key: "mint-agent-keys", label: "mint agent keys", status: "skipped", note: "could not read capabilities" };
12658
+ const sync = admin ? { key: "sync-personas", label: "sync personas and skills", status: "ok" } : { key: "sync-personas", label: "sync personas and skills", status: "missing", askFor: "the workspace admin role (personas and skills are workspace-level tables)" };
12659
+ return [crew, ships, repos, mint, sync];
12660
+ }
12661
+ function missLine(i) {
12662
+ return `${i.label} \u2014 ask a workspace admin for ${i.askFor ?? "access"}`;
12663
+ }
12664
+ function describeCapability(i) {
12665
+ if (i.status === "ok") return `ok ${i.label}`;
12666
+ if (i.status === "skipped") return `skipped ${i.label} (${i.note ?? "not checked"})`;
12667
+ return `missing ${missLine(i)}`;
12668
+ }
12669
+
12521
12670
  // src/agents-sync-run.ts
12522
- import { existsSync as existsSync21, mkdirSync as mkdirSync10, readFileSync as readFileSync15, writeFileSync as writeFileSync12 } from "fs";
12523
- import { dirname as dirname11 } from "path";
12671
+ import { existsSync as existsSync22, mkdirSync as mkdirSync11, readFileSync as readFileSync16, writeFileSync as writeFileSync13 } from "fs";
12672
+ import { dirname as dirname12 } from "path";
12524
12673
  async function runAgentsSync(target, o) {
12525
12674
  const dryRun = o.dryRun ?? false;
12526
12675
  const force = o.force ?? false;
@@ -12553,12 +12702,12 @@ async function runAgentsSync(target, o) {
12553
12702
  for (const s of skillResult?.outcomes ?? []) o.say(describeSkillSyncOutcome(s));
12554
12703
  if (!dryRun) {
12555
12704
  const resolvedPath = resolvedPathFor(o.stateDir, target.route);
12556
- const raw = existsSync21(resolvedPath) ? JSON.parse(readFileSync15(resolvedPath, "utf8")) : {};
12705
+ const raw = existsSync22(resolvedPath) ? JSON.parse(readFileSync16(resolvedPath, "utf8")) : {};
12557
12706
  raw.agentPersonas = personaResult.agentPersonas;
12558
12707
  if (skillResult) raw.agentSkills = skillResult.agentSkills;
12559
12708
  if (personaResult.releaseAgentId) raw.releaseAgentId = personaResult.releaseAgentId;
12560
- mkdirSync10(dirname11(resolvedPath), { recursive: true });
12561
- writeFileSync12(resolvedPath, `${JSON.stringify(raw, null, 2)}
12709
+ mkdirSync11(dirname12(resolvedPath), { recursive: true });
12710
+ writeFileSync13(resolvedPath, `${JSON.stringify(raw, null, 2)}
12562
12711
  `);
12563
12712
  if (target.resolved) {
12564
12713
  target.resolved.agentPersonas = personaResult.agentPersonas;
@@ -12665,7 +12814,7 @@ import { isMap as isMap2, parseDocument as parseDocument2, YAMLMap } from "yaml"
12665
12814
 
12666
12815
  // src/repos-cmd.ts
12667
12816
  import { homedir as homedir7 } from "os";
12668
- import { basename as basename7, join as join20, resolve as resolve3 } from "path";
12817
+ import { basename as basename7, join as join21, resolve as resolve3 } from "path";
12669
12818
  import { isMap, isSeq, parseDocument, stringify as stringify2 } from "yaml";
12670
12819
  var ReposError = class extends Error {
12671
12820
  };
@@ -12863,7 +13012,7 @@ function planRepoClone(i) {
12863
13012
  }
12864
13013
  const ship = doc.get("ship", true);
12865
13014
  const basePath = String(m.get("reposBasePath") ?? (isMap(ship) ? ship.get("reposBasePath") : void 0) ?? "~/Crew");
12866
- const target = i.dir ? expandDir(i.dir, i.base) : join20(expandDir(basePath, i.base), i.route.split("/")[0], name);
13015
+ const target = i.dir ? expandDir(i.dir, i.base) : join21(expandDir(basePath, i.base), i.route.split("/")[0], name);
12867
13016
  const add = planRepoAdd({ text: i.text, base: i.base, route: i.route, path: target, origin: remote, resolved: i.resolved, name });
12868
13017
  return { ...add, remote, target };
12869
13018
  }
@@ -13006,7 +13155,7 @@ function existingRepoConfig(f) {
13006
13155
  }
13007
13156
 
13008
13157
  // src/sample-ticket.ts
13009
- import { basename as basename8, dirname as dirname13, join as join21 } from "path";
13158
+ import { basename as basename8, dirname as dirname14, join as join22 } from "path";
13010
13159
  var SAMPLE_REPO = "crew-sample";
13011
13160
  var SAMPLE_REMOTE = `local/${SAMPLE_REPO}`;
13012
13161
  var SAMPLE_MARKER = "sample: true";
@@ -13029,8 +13178,8 @@ has something safe to work on: its test and build hooks always pass, it has no
13029
13178
  remote, and \`crew sample-ticket --remove\` deletes it.
13030
13179
  `;
13031
13180
  function sampleDir(existing, reposBase, workspace) {
13032
- if (existing.length > 0) return join21(dirname13(existing[0]), SAMPLE_REPO);
13033
- return join21(reposBase, workspace, SAMPLE_REPO);
13181
+ if (existing.length > 0) return join22(dirname14(existing[0]), SAMPLE_REPO);
13182
+ return join22(reposBase, workspace, SAMPLE_REPO);
13034
13183
  }
13035
13184
  var SAMPLE_TITLE = "Sample: add a documented `greet` function with a test";
13036
13185
  function sampleDescription() {
@@ -13111,8 +13260,8 @@ function schedulerInstalled(plan, exists, crontab) {
13111
13260
  }
13112
13261
 
13113
13262
  // src/worktree-sweep.ts
13114
- import { existsSync as existsSync22, readFileSync as readFileSync16, readdirSync as readdirSync5 } from "fs";
13115
- import { basename as basename9, dirname as dirname14, join as join22, resolve as resolve4 } from "path";
13263
+ import { existsSync as existsSync23, readFileSync as readFileSync17, readdirSync as readdirSync6 } from "fs";
13264
+ import { basename as basename9, dirname as dirname15, join as join23, resolve as resolve4 } from "path";
13116
13265
  function planWorktreeSweep(target, tickets, contract) {
13117
13266
  const terminal = new Set(contract.statuses.resolved.filter((s) => s !== contract.statuses.verified));
13118
13267
  const verified = contract.statuses.verified;
@@ -13166,12 +13315,12 @@ function worktreeTicketNumbers(target) {
13166
13315
  return [...nums];
13167
13316
  }
13168
13317
  function siblingWorktreeDirs(target) {
13169
- const parent = dirname14(target.dir.replace(/[/\\]+$/, ""));
13318
+ const parent = dirname15(target.dir.replace(/[/\\]+$/, ""));
13170
13319
  const stem = `${basename9(target.dir.replace(/[/\\]+$/, ""))}-`;
13171
13320
  const registered = new Set(worktrees(target.dir).map((w) => resolve4(w.path)));
13172
13321
  let names;
13173
13322
  try {
13174
- names = readdirSync5(parent);
13323
+ names = readdirSync6(parent);
13175
13324
  } catch {
13176
13325
  return [];
13177
13326
  }
@@ -13179,7 +13328,7 @@ function siblingWorktreeDirs(target) {
13179
13328
  for (const name of names) {
13180
13329
  if (!name.startsWith(stem)) continue;
13181
13330
  const m = DIR_NUMBER_SUFFIXED.exec(name);
13182
- const path = join22(parent, name);
13331
+ const path = join23(parent, name);
13183
13332
  if (m && !registered.has(resolve4(path))) out.push({ path, number: m[1] });
13184
13333
  }
13185
13334
  return out;
@@ -13214,7 +13363,7 @@ async function applyWorktreeSweep(cwd, actions, dryRun, log, remote = "origin",
13214
13363
  let removed = 0;
13215
13364
  let keptBranches = 0;
13216
13365
  for (const a of actions) {
13217
- if (!dryRun && base && !a.keepBranch && existsSync22(a.path) && isHandBackMerge(a.path, base)) {
13366
+ if (!dryRun && base && !a.keepBranch && existsSync23(a.path) && isHandBackMerge(a.path, base)) {
13218
13367
  if (gitOk(a.path, ["merge", "--abort"]) !== null) {
13219
13368
  log.emit(
13220
13369
  `abandoned the hand-back merge of ${base} in ${a.path}: ${a.ticket.issue_id} deployed without it`,
@@ -13222,7 +13371,7 @@ async function applyWorktreeSweep(cwd, actions, dryRun, log, remote = "origin",
13222
13371
  );
13223
13372
  }
13224
13373
  }
13225
- if (!dryRun && existsSync22(a.path)) {
13374
+ if (!dryRun && existsSync23(a.path)) {
13226
13375
  let dirty = false;
13227
13376
  try {
13228
13377
  dirty = status(a.path).length > 0;
@@ -13233,7 +13382,7 @@ async function applyWorktreeSweep(cwd, actions, dryRun, log, remote = "origin",
13233
13382
  continue;
13234
13383
  }
13235
13384
  }
13236
- if (!dryRun && a.requirePushed && a.branch && existsSync22(a.path) && remoteConfigured(cwd, remote) && worktreeHoldsUnpushed(a.path, remote)) {
13385
+ if (!dryRun && a.requirePushed && a.branch && existsSync23(a.path) && remoteConfigured(cwd, remote) && worktreeHoldsUnpushed(a.path, remote)) {
13237
13386
  refuse(a, "unpushed commits", `${a.branch} has commits that are not on ${remote}`);
13238
13387
  continue;
13239
13388
  }
@@ -13282,7 +13431,7 @@ function isHandBackMerge(path, base) {
13282
13431
  if (!msgPath) return false;
13283
13432
  let first;
13284
13433
  try {
13285
- first = readFileSync16(resolve4(path, msgPath), "utf8").split("\n")[0] ?? "";
13434
+ first = readFileSync17(resolve4(path, msgPath), "utf8").split("\n")[0] ?? "";
13286
13435
  } catch {
13287
13436
  return false;
13288
13437
  }
@@ -13387,16 +13536,16 @@ function applyRemoteBranchCleanup(cwd, remote, branches2, dryRun, log) {
13387
13536
  }
13388
13537
 
13389
13538
  // src/stream-sweep.ts
13390
- import { existsSync as existsSync23, readdirSync as readdirSync6, rmSync as rmSync9, statSync as statSync7 } from "fs";
13391
- import { join as join23 } from "path";
13539
+ import { existsSync as existsSync24, readdirSync as readdirSync7, rmSync as rmSync10, statSync as statSync8 } from "fs";
13540
+ import { join as join24 } from "path";
13392
13541
  var DAY_MS2 = 24 * 60 * 60 * 1e3;
13393
13542
  function planStreamSweep(stateDir, retentionDays, now = Date.now()) {
13394
- const dir = join23(stateDir, "streams");
13395
- if (!existsSync23(dir)) return [];
13543
+ const dir = join24(stateDir, "streams");
13544
+ if (!existsSync24(dir)) return [];
13396
13545
  const cutoff = now - retentionDays * DAY_MS2;
13397
- return readdirSync6(dir).map((name) => join23(dir, name)).filter((path) => {
13546
+ return readdirSync7(dir).map((name) => join24(dir, name)).filter((path) => {
13398
13547
  try {
13399
- return statSync7(path).mtimeMs < cutoff;
13548
+ return statSync8(path).mtimeMs < cutoff;
13400
13549
  } catch {
13401
13550
  return false;
13402
13551
  }
@@ -13410,7 +13559,7 @@ function applyStreamSweep(paths, dryRun, log) {
13410
13559
  continue;
13411
13560
  }
13412
13561
  try {
13413
- rmSync9(path, { force: true });
13562
+ rmSync10(path, { force: true });
13414
13563
  removed++;
13415
13564
  log.emit(`removed stream artifact ${path}`, { step: "worktree" });
13416
13565
  } catch (e) {
@@ -13420,11 +13569,11 @@ function applyStreamSweep(paths, dryRun, log) {
13420
13569
  return removed;
13421
13570
  }
13422
13571
  function planEventsSweep(stateDir, retentionDays, now = Date.now()) {
13423
- if (!existsSync23(stateDir)) return [];
13572
+ if (!existsSync24(stateDir)) return [];
13424
13573
  const cutoff = now - retentionDays * DAY_MS2;
13425
- return readdirSync6(stateDir).filter((name) => ROTATED_EVENTS_RE.test(name)).map((name) => join23(stateDir, name)).filter((path) => {
13574
+ return readdirSync7(stateDir).filter((name) => ROTATED_EVENTS_RE.test(name)).map((name) => join24(stateDir, name)).filter((path) => {
13426
13575
  try {
13427
- return statSync7(path).mtimeMs < cutoff;
13576
+ return statSync8(path).mtimeMs < cutoff;
13428
13577
  } catch {
13429
13578
  return false;
13430
13579
  }
@@ -13432,24 +13581,24 @@ function planEventsSweep(stateDir, retentionDays, now = Date.now()) {
13432
13581
  }
13433
13582
 
13434
13583
  // src/cli.ts
13435
- import { readFileSync as readFileSync22, existsSync as existsSync29, mkdirSync as mkdirSync15, writeFileSync as writeFileSync16, rmSync as rmSync11, unlinkSync as unlinkSync5, copyFileSync as copyFileSync2, renameSync as renameSync2, statSync as statSync9, readdirSync as readdirSync11 } from "fs";
13584
+ import { realpathSync as realpathSync2, readFileSync as readFileSync23, existsSync as existsSync30, mkdirSync as mkdirSync16, writeFileSync as writeFileSync17, rmSync as rmSync12, unlinkSync as unlinkSync5, copyFileSync as copyFileSync2, renameSync as renameSync2, statSync as statSync10, readdirSync as readdirSync12 } from "fs";
13436
13585
  import { createInterface } from "readline/promises";
13437
13586
  import { dirname as dirOf, resolve as resolvePath } from "path";
13438
13587
 
13439
13588
  // src/ssh-keys.ts
13440
13589
  import { execFileSync as execFileSync4 } from "child_process";
13441
- import { existsSync as existsSync24, mkdirSync as mkdirSync11, readFileSync as readFileSync17, chmodSync } from "fs";
13442
- import { dirname as dirname15, join as join24 } from "path";
13590
+ import { existsSync as existsSync25, mkdirSync as mkdirSync12, readFileSync as readFileSync18, chmodSync } from "fs";
13591
+ import { dirname as dirname16, join as join25 } from "path";
13443
13592
  var SshKeyError = class extends Error {
13444
13593
  };
13445
13594
  function shipSshKeyPathFor(stateDir) {
13446
- return join24(stateDir, "ssh", "id_ed25519");
13595
+ return join25(stateDir, "ssh", "id_ed25519");
13447
13596
  }
13448
13597
  function ensureShipSshKeypair(stateDir) {
13449
13598
  const privateKeyPath = shipSshKeyPathFor(stateDir);
13450
13599
  const publicKeyPath = `${privateKeyPath}.pub`;
13451
- if (!existsSync24(privateKeyPath)) {
13452
- mkdirSync11(dirname15(privateKeyPath), { recursive: true });
13600
+ if (!existsSync25(privateKeyPath)) {
13601
+ mkdirSync12(dirname16(privateKeyPath), { recursive: true });
13453
13602
  try {
13454
13603
  execFileSync4("ssh-keygen", [
13455
13604
  "-t",
@@ -13469,10 +13618,10 @@ function ensureShipSshKeypair(stateDir) {
13469
13618
  }
13470
13619
  chmodSync(privateKeyPath, 384);
13471
13620
  }
13472
- if (!existsSync24(publicKeyPath)) {
13621
+ if (!existsSync25(publicKeyPath)) {
13473
13622
  throw new SshKeyError(`${privateKeyPath} exists but ${publicKeyPath} does not \u2014 regenerate or restore it`);
13474
13623
  }
13475
- const publicKey = readFileSync17(publicKeyPath, "utf8").trim();
13624
+ const publicKey = readFileSync18(publicKeyPath, "utf8").trim();
13476
13625
  return { privateKeyPath, publicKeyPath, publicKey };
13477
13626
  }
13478
13627
  function sshKeygenAvailable() {
@@ -13688,9 +13837,9 @@ function syncAllPassengerCheckouts(cfg) {
13688
13837
 
13689
13838
  // src/tunnel.ts
13690
13839
  import { spawn as spawn4 } from "child_process";
13691
- import { existsSync as existsSync25, mkdirSync as mkdirSync12, readFileSync as readFileSync18, writeFileSync as writeFileSync13, unlinkSync as unlinkSync3, readdirSync as readdirSync7 } from "fs";
13840
+ import { existsSync as existsSync26, mkdirSync as mkdirSync13, readFileSync as readFileSync19, writeFileSync as writeFileSync14, unlinkSync as unlinkSync3, readdirSync as readdirSync8 } from "fs";
13692
13841
  import { randomInt } from "crypto";
13693
- import { join as join25 } from "path";
13842
+ import { join as join26 } from "path";
13694
13843
  var DEFAULT_SPAWN_OPTIONS = { stdio: "ignore", detached: true };
13695
13844
  var defaultSpawn = (cmd, args) => spawn4(cmd, args, DEFAULT_SPAWN_OPTIONS);
13696
13845
  var SLUG_ALPHABET = "abcdefghijklmnopqrstuvwxyz0123456789";
@@ -13757,21 +13906,21 @@ function startTunnel(opts) {
13757
13906
  };
13758
13907
  }
13759
13908
  function tunnelStatePath(stateDir, workspaceId) {
13760
- return join25(stateDir, "passengers", `${workspaceId}.json`);
13909
+ return join26(stateDir, "passengers", `${workspaceId}.json`);
13761
13910
  }
13762
13911
  function readPersistedTunnel(stateDir, workspaceId) {
13763
13912
  const path = tunnelStatePath(stateDir, workspaceId);
13764
- if (!existsSync25(path)) return void 0;
13913
+ if (!existsSync26(path)) return void 0;
13765
13914
  try {
13766
- return JSON.parse(readFileSync18(path, "utf8"));
13915
+ return JSON.parse(readFileSync19(path, "utf8"));
13767
13916
  } catch {
13768
13917
  return void 0;
13769
13918
  }
13770
13919
  }
13771
13920
  function writePersistedTunnel(stateDir, state) {
13772
13921
  const path = tunnelStatePath(stateDir, state.workspaceId);
13773
- mkdirSync12(join25(stateDir, "passengers"), { recursive: true });
13774
- writeFileSync13(path, `${JSON.stringify(state, null, 2)}
13922
+ mkdirSync13(join26(stateDir, "passengers"), { recursive: true });
13923
+ writeFileSync14(path, `${JSON.stringify(state, null, 2)}
13775
13924
  `);
13776
13925
  }
13777
13926
  function updatePersistedTunnel(stateDir, workspaceId, patch2) {
@@ -13868,9 +14017,9 @@ function syncPassengerTunnels(plans, relayHost, relayPort, privateKeyPath, state
13868
14017
  }
13869
14018
  }
13870
14019
  function readAllPersistedWorkspaceIds(stateDir) {
13871
- const dir = join25(stateDir, "passengers");
13872
- if (!existsSync25(dir)) return [];
13873
- return readdirSync7(dir).filter((f) => f.endsWith(".json") && !f.endsWith("-sync.json")).map((f) => f.slice(0, -".json".length));
14020
+ const dir = join26(stateDir, "passengers");
14021
+ if (!existsSync26(dir)) return [];
14022
+ return readdirSync8(dir).filter((f) => f.endsWith(".json") && !f.endsWith("-sync.json")).map((f) => f.slice(0, -".json".length));
13874
14023
  }
13875
14024
 
13876
14025
  // src/mcp-endpoint-registry.ts
@@ -13990,8 +14139,8 @@ async function unregisterEndpoint(workspaceId, info, d) {
13990
14139
  import { createServer as createHttpServer } from "http";
13991
14140
  import { spawn as spawn5 } from "child_process";
13992
14141
  import { randomBytes } from "crypto";
13993
- import { existsSync as existsSync26, mkdirSync as mkdirSync13, readFileSync as readFileSync19, writeFileSync as writeFileSync14, unlinkSync as unlinkSync4, readdirSync as readdirSync8 } from "fs";
13994
- import { join as join26 } from "path";
14142
+ import { existsSync as existsSync27, mkdirSync as mkdirSync14, readFileSync as readFileSync20, writeFileSync as writeFileSync15, unlinkSync as unlinkSync4, readdirSync as readdirSync9 } from "fs";
14143
+ import { join as join27 } from "path";
13995
14144
 
13996
14145
  // src/secret-compare.ts
13997
14146
  import { timingSafeEqual } from "crypto";
@@ -14080,21 +14229,21 @@ var defaultSpawn2 = (cmd, args, env) => {
14080
14229
  return child;
14081
14230
  };
14082
14231
  function syncDaemonStatePath(stateDir, workspaceId) {
14083
- return join26(stateDir, "passengers", `${workspaceId}-sync.json`);
14232
+ return join27(stateDir, "passengers", `${workspaceId}-sync.json`);
14084
14233
  }
14085
14234
  function readPersistedSyncDaemon(stateDir, workspaceId) {
14086
14235
  const path = syncDaemonStatePath(stateDir, workspaceId);
14087
- if (!existsSync26(path)) return void 0;
14236
+ if (!existsSync27(path)) return void 0;
14088
14237
  try {
14089
- return JSON.parse(readFileSync19(path, "utf8"));
14238
+ return JSON.parse(readFileSync20(path, "utf8"));
14090
14239
  } catch {
14091
14240
  return void 0;
14092
14241
  }
14093
14242
  }
14094
14243
  function writePersistedSyncDaemon(stateDir, state) {
14095
14244
  const path = syncDaemonStatePath(stateDir, state.workspaceId);
14096
- mkdirSync13(join26(stateDir, "passengers"), { recursive: true });
14097
- writeFileSync14(path, `${JSON.stringify(state, null, 2)}
14245
+ mkdirSync14(join27(stateDir, "passengers"), { recursive: true });
14246
+ writeFileSync15(path, `${JSON.stringify(state, null, 2)}
14098
14247
  `);
14099
14248
  }
14100
14249
  function removePersistedSyncDaemon(stateDir, workspaceId) {
@@ -14114,10 +14263,10 @@ function waitForExit(pid, alive, timeoutMs = 5e3) {
14114
14263
  }
14115
14264
  }
14116
14265
  function readAllPersistedSyncWorkspaceIds(stateDir) {
14117
- const dir = join26(stateDir, "passengers");
14118
- if (!existsSync26(dir)) return [];
14266
+ const dir = join27(stateDir, "passengers");
14267
+ if (!existsSync27(dir)) return [];
14119
14268
  const suffix = "-sync.json";
14120
- return readdirSync8(dir).filter((f) => f.endsWith(suffix)).map((f) => f.slice(0, -suffix.length));
14269
+ return readdirSync9(dir).filter((f) => f.endsWith(suffix)).map((f) => f.slice(0, -suffix.length));
14121
14270
  }
14122
14271
  function syncPassengerSyncDaemons(plans, cfg, stateDir, deps = {}) {
14123
14272
  const alive = deps.isPidAlive ?? isPidAlive;
@@ -14172,12 +14321,12 @@ function syncPassengerSyncDaemons(plans, cfg, stateDir, deps = {}) {
14172
14321
  }
14173
14322
 
14174
14323
  // src/daemon.ts
14175
- import { existsSync as existsSync28, mkdirSync as mkdirSync14, readFileSync as readFileSync21, readdirSync as readdirSync10, rmSync as rmSync10, statSync as statSync8, writeFileSync as writeFileSync15 } from "fs";
14176
- import { join as join28 } from "path";
14324
+ import { existsSync as existsSync29, mkdirSync as mkdirSync15, readFileSync as readFileSync22, readdirSync as readdirSync11, rmSync as rmSync11, statSync as statSync9, writeFileSync as writeFileSync16 } from "fs";
14325
+ import { join as join29 } from "path";
14177
14326
 
14178
14327
  // src/interrupted.ts
14179
- import { existsSync as existsSync27, readdirSync as readdirSync9, readFileSync as readFileSync20 } from "fs";
14180
- import { join as join27 } from "path";
14328
+ import { existsSync as existsSync28, readdirSync as readdirSync10, readFileSync as readFileSync21 } from "fs";
14329
+ import { join as join28 } from "path";
14181
14330
  var INTERRUPTED_MARKER = calloutHeader("NOTE", "Interrupted");
14182
14331
  function interruptedComment(b) {
14183
14332
  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.";
@@ -14196,21 +14345,21 @@ var pidAlive = (pid) => {
14196
14345
  };
14197
14346
  function runFinished(streamPath) {
14198
14347
  const path = `${streamPath}.meta.json`;
14199
- if (!existsSync27(path)) return false;
14348
+ if (!existsSync28(path)) return false;
14200
14349
  try {
14201
- return JSON.parse(readFileSync20(path, "utf8")).running !== true;
14350
+ return JSON.parse(readFileSync21(path, "utf8")).running !== true;
14202
14351
  } catch {
14203
14352
  return true;
14204
14353
  }
14205
14354
  }
14206
14355
  function orphanedStreams(o) {
14207
- const dir = join27(o.stateDir, "streams");
14208
- if (!existsSync27(dir)) return [];
14356
+ const dir = join28(o.stateDir, "streams");
14357
+ if (!existsSync28(dir)) return [];
14209
14358
  const ownerAlive = o.isOwnerAlive ?? pidAlive;
14210
14359
  const out = [];
14211
- for (const name of readdirSync9(dir)) {
14360
+ for (const name of readdirSync10(dir)) {
14212
14361
  if (!name.endsWith(".jsonl") || name.endsWith(".events.jsonl")) continue;
14213
- const streamPath = join27(dir, name);
14362
+ const streamPath = join28(dir, name);
14214
14363
  if (runFinished(streamPath)) continue;
14215
14364
  const sidecar = readRunSidecar(streamPath);
14216
14365
  if (!sidecar || sidecar.route !== o.routeLabel) continue;
@@ -14302,15 +14451,15 @@ async function runDaemonLoop(o) {
14302
14451
  function latestMtime(path) {
14303
14452
  let stat;
14304
14453
  try {
14305
- stat = statSync8(path);
14454
+ stat = statSync9(path);
14306
14455
  } catch {
14307
14456
  return 0;
14308
14457
  }
14309
14458
  if (stat.isFile()) return stat.mtimeMs;
14310
14459
  if (!stat.isDirectory()) return 0;
14311
14460
  let latest = stat.mtimeMs;
14312
- for (const entry of readdirSync10(path, { withFileTypes: true })) {
14313
- latest = Math.max(latest, latestMtime(join28(path, entry.name)));
14461
+ for (const entry of readdirSync11(path, { withFileTypes: true })) {
14462
+ latest = Math.max(latest, latestMtime(join29(path, entry.name)));
14314
14463
  }
14315
14464
  return latest;
14316
14465
  }
@@ -14319,23 +14468,23 @@ function computeTreeSignature(paths) {
14319
14468
  }
14320
14469
  function makeStaleChecker(paths, opts = {}) {
14321
14470
  const initial = computeTreeSignature(paths);
14322
- return () => computeTreeSignature(paths) !== initial || opts.reloadSentinel !== void 0 && existsSync28(opts.reloadSentinel);
14471
+ return () => computeTreeSignature(paths) !== initial || opts.reloadSentinel !== void 0 && existsSync29(opts.reloadSentinel);
14323
14472
  }
14324
14473
  function reloadSentinelFor(stateDir) {
14325
- return join28(stateDir, "reload-requested");
14474
+ return join29(stateDir, "reload-requested");
14326
14475
  }
14327
14476
  function daemonStampFor(stateDir) {
14328
- return join28(stateDir, "daemon.started");
14477
+ return join29(stateDir, "daemon.started");
14329
14478
  }
14330
14479
  function noteDaemonStarted(stateDir, now = Date.now()) {
14331
- mkdirSync14(stateDir, { recursive: true });
14332
- rmSync10(reloadSentinelFor(stateDir), { force: true });
14333
- writeFileSync15(daemonStampFor(stateDir), `${JSON.stringify({ pid: process.pid, startedAt: now })}
14480
+ mkdirSync15(stateDir, { recursive: true });
14481
+ rmSync11(reloadSentinelFor(stateDir), { force: true });
14482
+ writeFileSync16(daemonStampFor(stateDir), `${JSON.stringify({ pid: process.pid, startedAt: now })}
14334
14483
  `);
14335
14484
  }
14336
14485
  function readDaemonStarted(stateDir) {
14337
14486
  try {
14338
- const v = JSON.parse(readFileSync21(daemonStampFor(stateDir), "utf8")).startedAt;
14487
+ const v = JSON.parse(readFileSync22(daemonStampFor(stateDir), "utf8")).startedAt;
14339
14488
  return typeof v === "number" ? v : null;
14340
14489
  } catch {
14341
14490
  return null;
@@ -14344,7 +14493,7 @@ function readDaemonStarted(stateDir) {
14344
14493
  function configChangedAfter(files, startedAt) {
14345
14494
  for (const f of files) {
14346
14495
  try {
14347
- if (statSync8(f).mtimeMs > startedAt) return f;
14496
+ if (statSync9(f).mtimeMs > startedAt) return f;
14348
14497
  } catch {
14349
14498
  }
14350
14499
  }
@@ -14691,7 +14840,7 @@ async function buildEnvironment(route, ship, ticket) {
14691
14840
  }
14692
14841
 
14693
14842
  // src/cli.ts
14694
- var CREW_HOME = isCompiledBinary(import.meta.url) ? dirname16(process.execPath) : resolve5(dirname16(fileURLToPath(import.meta.url)), "..");
14843
+ var CREW_HOME = isCompiledBinary(import.meta.url) ? dirname17(process.execPath) : resolve5(dirname17(fileURLToPath(import.meta.url)), "..");
14695
14844
  var RELEASE_LOCK_TTL_MS = 20 * 60 * 1e3;
14696
14845
  var CYCLE_SECONDS2 = 120;
14697
14846
  var STALE_CYCLES = 3;
@@ -14839,6 +14988,7 @@ function usage() {
14839
14988
  crew inbox [--member NAME] your tickets across every workspace (or a colleague's)
14840
14989
  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)
14841
14990
  crew disconnect remove what connect installed for Pair (Claude MCP; Codex/Gemini instructions block and MCP entry per ship.pair)
14991
+ 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)
14842
14992
  crew agents sync [route] [--force] [--revoke-seat-keys] push crew's personas AND skill files into the workspace Agents/Agent Skills tables (--force overwrites even a diverged persona or skill; never one marked Customize prompt)
14843
14993
  crew agents prompt R [route] print one persona's current prompt (e.g. R=pair, for a SessionStart hook; --harness H adds which CLI it serves)
14844
14994
  crew skills sync [route] push only crew's skill files (e.g. grill-me) into the workspace Agent Skills table
@@ -15017,7 +15167,7 @@ if (command === "passenger-sync-daemon") {
15017
15167
  firstRun = { answers, configPath };
15018
15168
  cfg = loadConfig(CREW_HOME, configPath, { text: renderShipBlock(answers), allowNoRoutes: true });
15019
15169
  } else {
15020
- cfg = loadConfig(CREW_HOME, void 0, command === "repos" ? { allowNoRoutes: true } : {});
15170
+ cfg = loadConfig(CREW_HOME, void 0, command === "repos" || command === "disconnect" && flag("reset") ? { allowNoRoutes: true } : {});
15021
15171
  }
15022
15172
  } catch (e) {
15023
15173
  if (e instanceof ConfigError) {
@@ -15048,6 +15198,7 @@ if (command === "passenger-sync-daemon") {
15048
15198
  throw e;
15049
15199
  }
15050
15200
  const state = new State(cfg.ship.stateDir);
15201
+ setCloneRecorder((dir) => recordClonedCheckout(cfg.ship.stateDir, dir));
15051
15202
  const dryRun = flag("dry-run");
15052
15203
  const force = flag("force");
15053
15204
  const EXCLUSIVE = { passengers: "passengers" };
@@ -15250,8 +15401,8 @@ if (command === "passenger-sync-daemon") {
15250
15401
  }
15251
15402
  if (host === "linux" && hasSystemd) {
15252
15403
  const label = labelFor(CREW_HOME, "run");
15253
- const staleTimer = join29(homedir8(), ".config", "systemd", "user", `${label}.timer`);
15254
- if (existsSync29(staleTimer)) {
15404
+ const staleTimer = join30(homedir8(), ".config", "systemd", "user", `${label}.timer`);
15405
+ if (existsSync30(staleTimer)) {
15255
15406
  process.stdout.write(
15256
15407
  `removing stale run timer from before the persistent-service cutover: ${staleTimer}${dryRun ? " (dry run)" : ""}
15257
15408
  `
@@ -15277,7 +15428,7 @@ if (command === "passenger-sync-daemon") {
15277
15428
  const tracker = new Tracker(c, cfg.ship);
15278
15429
  problems.push(...validateContract(tracker.contract).map((p) => `contract: ${p}`));
15279
15430
  for (const r of await resolvedRepos(c)) {
15280
- if (!existsSync29(r.dir)) problems.push(`repo ${r.name}: ${r.dir} is missing on disk`);
15431
+ if (!existsSync30(r.dir)) problems.push(`repo ${r.name}: ${r.dir} is missing on disk`);
15281
15432
  if (!satisfies(host, r.config.platform)) problems.push(`repo ${r.name}: requires ${r.config.platform}, this host is ${host}`);
15282
15433
  problems.push(...validateEffective(r.config).map((p) => `repo ${r.name}: ${p}`));
15283
15434
  }
@@ -15295,7 +15446,7 @@ if (command === "passenger-sync-daemon") {
15295
15446
  }
15296
15447
  async function doctorFix(o) {
15297
15448
  const routes = await Promise.all(routesInScope(cfg.routes, named).map((c) => gatherHealth({ route: c.route, enabled: c.enabled, hasRepos: reposOf(c).length > 0 }, () => routeProblems(c))));
15298
- const unitExists = (p) => existsSync29(p);
15449
+ const unitExists = (p) => existsSync30(p);
15299
15450
  const crontab = o.host === "linux" ? spawnSync6("crontab", ["-l"], { encoding: "utf8" }).stdout ?? "" : "";
15300
15451
  const schedulerAbsent = o.host !== "windows" ? !schedulerInstalled(planInstall(cfg.ship, CREW_HOME, o.host, detectSystemd(), "run"), unitExists, crontab) : true;
15301
15452
  const plan = planFix({ routes, schedulerAbsent, host: o.host });
@@ -15312,7 +15463,7 @@ if (command === "passenger-sync-daemon") {
15312
15463
  for (const a of plan.actions) {
15313
15464
  if (a.kind === "enable") {
15314
15465
  if (prompter && !await prompter.confirm(`Route ${a.route} passes every check \u2014 set enabled: true in ${cfg.configFile}?`, false)) continue;
15315
- writeFileSync16(cfg.configFile, planEnable(readFileSync22(cfg.configFile, "utf8"), a.route));
15466
+ writeFileSync17(cfg.configFile, planEnable(readFileSync23(cfg.configFile, "utf8"), a.route));
15316
15467
  enabledSound.add(a.route);
15317
15468
  changed2.push(`enabled route ${a.route} in ${cfg.configFile}`);
15318
15469
  } else {
@@ -16311,7 +16462,7 @@ ${changed2.map((c) => ` - ${c}
16311
16462
  process.exit(1);
16312
16463
  }
16313
16464
  requireArmed("run the daemon loop");
16314
- const watchPaths = isCompiledBinary(import.meta.url) ? [process.execPath] : [join29(CREW_HOME, "src"), join29(CREW_HOME, "dist"), join29(CREW_HOME, "bin")];
16465
+ const watchPaths = isCompiledBinary(import.meta.url) ? [process.execPath] : [join30(CREW_HOME, "src"), join30(CREW_HOME, "dist"), join30(CREW_HOME, "bin")];
16315
16466
  const configPaths = daemonConfigPaths(cfg);
16316
16467
  watchPaths.push(...configPaths);
16317
16468
  noteDaemonStarted(cfg.ship.stateDir);
@@ -16512,10 +16663,10 @@ ${changed2.map((c) => ` - ${c}
16512
16663
  `);
16513
16664
  if (!dryRun) {
16514
16665
  const resolvedPath = resolvedPathFor(cfg.ship.stateDir, target.route);
16515
- const raw = existsSync29(resolvedPath) ? JSON.parse(readFileSync22(resolvedPath, "utf8")) : {};
16666
+ const raw = existsSync30(resolvedPath) ? JSON.parse(readFileSync23(resolvedPath, "utf8")) : {};
16516
16667
  raw.agentSkills = result.agentSkills;
16517
- mkdirSync15(dirOf(resolvedPath), { recursive: true });
16518
- writeFileSync16(resolvedPath, `${JSON.stringify(raw, null, 2)}
16668
+ mkdirSync16(dirOf(resolvedPath), { recursive: true });
16669
+ writeFileSync17(resolvedPath, `${JSON.stringify(raw, null, 2)}
16519
16670
  `);
16520
16671
  }
16521
16672
  if (result.outcomes.some((o) => o.action === "diverged")) process.exitCode = 1;
@@ -16528,15 +16679,15 @@ ${changed2.map((c) => ` - ${c}
16528
16679
  process.exit(2);
16529
16680
  };
16530
16681
  if (sub !== "add" && sub !== "list" && sub !== "clone" && sub !== "hooks") usage2();
16531
- const configText = readFileSync22(cfg.configFile, "utf8");
16682
+ const configText = readFileSync23(cfg.configFile, "utf8");
16532
16683
  if (sub === "hooks") {
16533
16684
  try {
16534
16685
  const a = parseHooksArgs(positional.slice(2), routeNamesOf(configText));
16535
- const dir = repoDirOf(configText, dirname16(cfg.configFile), a.route, a.name);
16536
- if (!existsSync29(dir)) throw new ReposError(`${dir} does not exist on disk`);
16537
- const files = { exists: (r) => existsSync29(resolvePath(dir, r)), read: (r) => {
16686
+ const dir = repoDirOf(configText, dirname17(cfg.configFile), a.route, a.name);
16687
+ if (!existsSync30(dir)) throw new ReposError(`${dir} does not exist on disk`);
16688
+ const files = { exists: (r) => existsSync30(resolvePath(dir, r)), read: (r) => {
16538
16689
  try {
16539
- return readFileSync22(resolvePath(dir, r), "utf8");
16690
+ return readFileSync23(resolvePath(dir, r), "utf8");
16540
16691
  } catch {
16541
16692
  return void 0;
16542
16693
  }
@@ -16548,13 +16699,13 @@ ${changed2.map((c) => ` - ${c}
16548
16699
  if (write && suggestion.source === null) throw new ReposError(`nothing to write: ${suggestion.notes[0]}`);
16549
16700
  if (write === "route") {
16550
16701
  const plan = planRouteHooks(configText, a.route, a.name, suggestion.hooks);
16551
- writeFileSync16(cfg.configFile, plan.text);
16702
+ writeFileSync17(cfg.configFile, plan.text);
16552
16703
  written = { target: "route", file: cfg.configFile, hooks: plan.written, kept: plan.kept };
16553
16704
  } else if (write === "repo") {
16554
16705
  const have = existingRepoConfig(files);
16555
16706
  if (have) throw new ReposError(`${resolvePath(dir, have)} already exists; not overwriting it`);
16556
16707
  const file = resolvePath(dir, ".crew.yaml");
16557
- writeFileSync16(file, starterRepoConfig(suggestion.hooks), { flag: "wx" });
16708
+ writeFileSync17(file, starterRepoConfig(suggestion.hooks), { flag: "wx" });
16558
16709
  written = { target: "repo", file, hooks: Object.keys(suggestion.hooks), kept: [] };
16559
16710
  }
16560
16711
  if (flag("json")) {
@@ -16591,7 +16742,7 @@ ${changed2.map((c) => ` - ${c}
16591
16742
  if (sub === "list" && !routeName) usage2();
16592
16743
  if (sub === "add") {
16593
16744
  try {
16594
- const a = parseAddArgs(positional.slice(2), routeNamesOf(configText), (p) => existsSync29(resolvePath(p.replace(/^~(?=\/|$)/, homedir8()))));
16745
+ const a = parseAddArgs(positional.slice(2), routeNamesOf(configText), (p) => existsSync30(resolvePath(p.replace(/^~(?=\/|$)/, homedir8()))));
16595
16746
  routeName = a.route;
16596
16747
  addPath = a.path;
16597
16748
  addName = a.name;
@@ -16620,15 +16771,15 @@ ${changed2.map((c) => ` - ${c}
16620
16771
  }
16621
16772
  const resolvedFile = resolvedPathFor(cfg.ship.stateDir, routeName);
16622
16773
  let resolvedRemotes;
16623
- if (existsSync29(resolvedFile)) {
16774
+ if (existsSync30(resolvedFile)) {
16624
16775
  try {
16625
- resolvedRemotes = JSON.parse(readFileSync22(resolvedFile, "utf8"));
16776
+ resolvedRemotes = JSON.parse(readFileSync23(resolvedFile, "utf8"));
16626
16777
  } catch {
16627
16778
  }
16628
16779
  }
16629
16780
  const originOf = (dir) => gitOk(dir, ["remote", "get-url", "origin"]) ?? void 0;
16630
16781
  const text = configText;
16631
- const base = dirname16(cfg.configFile);
16782
+ const base = dirname17(cfg.configFile);
16632
16783
  try {
16633
16784
  if (sub === "list") {
16634
16785
  const repos = configuredRepos(text, routeName, base);
@@ -16637,7 +16788,7 @@ ${changed2.map((c) => ` - ${c}
16637
16788
  for (const r of repos) {
16638
16789
  const origin2 = originOf(r.dir);
16639
16790
  const row = matchRepoName(origin2, resolvedRemotes);
16640
- const note = !existsSync29(r.dir) ? "missing on disk" : row ? `Repos row: ${row}` : `no Repos row matches ${normalizeRemote(origin2) ?? "this checkout (no origin remote)"}`;
16791
+ const note = !existsSync30(r.dir) ? "missing on disk" : row ? `Repos row: ${row}` : `no Repos row matches ${normalizeRemote(origin2) ?? "this checkout (no origin remote)"}`;
16641
16792
  process.stdout.write(`${r.name} ${r.dir} ${note}
16642
16793
  `);
16643
16794
  }
@@ -16649,9 +16800,9 @@ ${changed2.map((c) => ` - ${c}
16649
16800
  ` : `${human}
16650
16801
  `);
16651
16802
  const plan2 = planRepoClone({ text, base, route: routeName, name: addName, resolved: resolvedRemotes, dir: value("dir") });
16652
- if (existsSync29(plan2.target) && (existsSync29(`${plan2.target}/.git`) || readdirSync11(plan2.target).length > 0)) {
16803
+ if (existsSync30(plan2.target) && (existsSync30(`${plan2.target}/.git`) || readdirSync12(plan2.target).length > 0)) {
16653
16804
  throw new ReposError(
16654
- existsSync29(`${plan2.target}/.git`) ? `${plan2.target} is already a checkout \u2014 attach it with \`crew repos add ${routeName} ${plan2.name} ${plan2.target}\`` : `${plan2.target} exists and is not empty \u2014 pass --dir to clone somewhere else`
16805
+ existsSync30(`${plan2.target}/.git`) ? `${plan2.target} is already a checkout \u2014 attach it with \`crew repos add ${routeName} ${plan2.name} ${plan2.target}\`` : `${plan2.target} exists and is not empty \u2014 pass --dir to clone somewhere else`
16655
16806
  );
16656
16807
  }
16657
16808
  if (dryRun) {
@@ -16669,12 +16820,12 @@ ${changed2.map((c) => ` - ${c}
16669
16820
  throw new ReposError(`${e.message}${authHint}
16670
16821
  crew.yaml was not changed`);
16671
16822
  }
16672
- writeFileSync16(cfg.configFile, plan2.text);
16823
+ writeFileSync17(cfg.configFile, plan2.text);
16673
16824
  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}`);
16674
16825
  break;
16675
16826
  }
16676
16827
  const target = resolvePath(addPath.replace(/^~(?=\/|$)/, homedir8()));
16677
- const top = existsSync29(target) ? gitOk(target, ["rev-parse", "--show-toplevel"]) : null;
16828
+ const top = existsSync30(target) ? gitOk(target, ["rev-parse", "--show-toplevel"]) : null;
16678
16829
  if (!top) throw new ReposError(`${target} is not a git checkout`);
16679
16830
  const origin = originOf(top);
16680
16831
  const plan = planRepoAdd({
@@ -16723,15 +16874,15 @@ ${changed2.map((c) => ` - ${c}
16723
16874
  const created = await rowTracker.createRepoRow(rowPayload);
16724
16875
  process.stdout.write(`created Repos row ${String(rowPayload.name)} (${String(rowPayload.remote)}) ${created.id}
16725
16876
  `);
16726
- if (existsSync29(resolvedFile)) {
16727
- const raw = JSON.parse(readFileSync22(resolvedFile, "utf8"));
16877
+ if (existsSync30(resolvedFile)) {
16878
+ const raw = JSON.parse(readFileSync23(resolvedFile, "utf8"));
16728
16879
  raw.repoNames = { ...raw.repoNames, [created.id]: String(rowPayload.name) };
16729
16880
  raw.repoRemotes = { ...raw.repoRemotes, [created.id]: String(rowPayload.remote) };
16730
- writeFileSync16(resolvedFile, `${JSON.stringify(raw, null, 2)}
16881
+ writeFileSync17(resolvedFile, `${JSON.stringify(raw, null, 2)}
16731
16882
  `);
16732
16883
  }
16733
16884
  }
16734
- writeFileSync16(cfg.configFile, plan.text);
16885
+ writeFileSync17(cfg.configFile, plan.text);
16735
16886
  process.stdout.write(`added ${top} to ${routeName} as ${plan.mode === "dir" ? "dir" : `repos.${plan.name}`} in ${cfg.configFile}
16736
16887
  `);
16737
16888
  } catch (e) {
@@ -17016,6 +17167,16 @@ Signed in as ${result.identity.email} \u2014 minted an API key in workspace "${r
17016
17167
  }
17017
17168
  const route2 = `${found.workspaceSlug}/${found.projectSlug ?? found.projectId}`;
17018
17169
  const resolvedPath = resolvedPathFor(cfg.ship.stateDir, route2);
17170
+ const capabilities = await checkCapabilities(authOpts, found);
17171
+ if (flag("json")) {
17172
+ process.stdout.write(`${JSON.stringify({ event: "capabilities", route: route2, items: capabilities })}
17173
+ `);
17174
+ } else {
17175
+ process.stderr.write("capabilities for this key:\n");
17176
+ for (const i of capabilities) process.stderr.write(` ${describeCapability(i)}
17177
+ `);
17178
+ }
17179
+ const capabilityMisses = capabilities.filter((i) => i.status === "missing").map(missLine);
17019
17180
  if (found.locksModelId && !dryRun) {
17020
17181
  const configured = cfg.routes.find((r) => r.route === route2);
17021
17182
  const repoNames = [...Object.values(found.repoNames ?? {}), ...configured ? reposOf(configured).map((t) => t.name) : []].filter((n) => !!n);
@@ -17041,10 +17202,10 @@ Signed in as ${result.identity.email} \u2014 minted an API key in workspace "${r
17041
17202
  }
17042
17203
  }
17043
17204
  let previous;
17044
- const hadPreviousFile = existsSync29(resolvedPath);
17205
+ const hadPreviousFile = existsSync30(resolvedPath);
17045
17206
  if (hadPreviousFile) {
17046
17207
  try {
17047
- previous = JSON.parse(readFileSync22(resolvedPath, "utf8"));
17208
+ previous = JSON.parse(readFileSync23(resolvedPath, "utf8"));
17048
17209
  } catch {
17049
17210
  }
17050
17211
  }
@@ -17134,6 +17295,7 @@ Signed in as ${result.identity.email} \u2014 minted an API key in workspace "${r
17134
17295
  holds: found.holds.map((h) => ({ id: h.id, role: h.name })),
17135
17296
  ...found.releaseSeat ? { releaseSeat: found.releaseSeat } : {},
17136
17297
  ...found.role ?? previous?.role ? { role: found.role ?? previous?.role } : {},
17298
+ ...capabilityMisses.length > 0 ? { capabilityMisses } : {},
17137
17299
  ...operator ? { operator } : {},
17138
17300
  ...contract ? { contract } : {},
17139
17301
  ...reviewedStatuses.length > 0 ? { reviewedStatuses } : {},
@@ -17152,10 +17314,10 @@ Signed in as ${result.identity.email} \u2014 minted an API key in workspace "${r
17152
17314
  process.stderr.write(`(dry run) would write resolved ids to ${resolvedPath}
17153
17315
  `);
17154
17316
  } else {
17155
- mkdirSync15(dirname16(resolvedPath), { recursive: true });
17317
+ mkdirSync16(dirname17(resolvedPath), { recursive: true });
17156
17318
  if (hadPreviousFile) copyFileSync2(resolvedPath, `${resolvedPath}.bak`);
17157
17319
  const tmpPath = `${resolvedPath}.tmp${process.pid}`;
17158
- writeFileSync16(tmpPath, `${JSON.stringify(resolved, null, 2)}
17320
+ writeFileSync17(tmpPath, `${JSON.stringify(resolved, null, 2)}
17159
17321
  `);
17160
17322
  renameSync2(tmpPath, resolvedPath);
17161
17323
  process.stderr.write(`Resolved ids written to ${resolvedPath}
@@ -17190,8 +17352,8 @@ Signed in as ${result.identity.email} \u2014 minted an API key in workspace "${r
17190
17352
  process.stderr.write(`(dry run) would write the minted API key to ${apiKeyFile}
17191
17353
  `);
17192
17354
  } else {
17193
- mkdirSync15(dirname16(apiKeyFile), { recursive: true });
17194
- writeFileSync16(apiKeyFile, `${apiKeyVar}=${apiKey}
17355
+ mkdirSync16(dirname17(apiKeyFile), { recursive: true });
17356
+ writeFileSync17(apiKeyFile, `${apiKeyVar}=${apiKey}
17195
17357
  `, { mode: 384 });
17196
17358
  process.stderr.write(`API key written to ${apiKeyFile}
17197
17359
  `);
@@ -17200,7 +17362,7 @@ Signed in as ${result.identity.email} \u2014 minted an API key in workspace "${r
17200
17362
  const keyPersisted = keychainBacked || apiKeyFile !== void 0;
17201
17363
  const wizard = firstRun;
17202
17364
  const writeFirstRun = wizard !== void 0 && !dryRun && keyPersisted;
17203
- const routeDir = value("dir") ?? (writeFirstRun ? join29(DEFAULT_REPOS_BASE_PATH, found.workspaceSlug) : void 0);
17365
+ const routeDir = value("dir") ?? (writeFirstRun ? join30(DEFAULT_REPOS_BASE_PATH, found.workspaceSlug) : void 0);
17204
17366
  const routeBlock = renderConnection(
17205
17367
  found,
17206
17368
  route2,
@@ -17795,10 +17957,76 @@ Signed in as ${result.identity.email} \u2014 minted an API key in workspace "${r
17795
17957
  case "disconnect": {
17796
17958
  const url = mcpUrlFor(value("base-url") ?? cfg.routes[0]?.baseUrl ?? DEFAULT_BASE_URL);
17797
17959
  const disconnectHarnesses = /* @__PURE__ */ new Set([...shipPair(cfg.ship), ...shipToolchains(cfg.ship).map((t) => t.harness).filter(isPairHarness)]);
17798
- for (const h of disconnectHarnesses) {
17799
- process.stderr.write(`${pairInstallerFor(h).removePair({ url, bin: cfg.ship.agent.bin, dryRun })}
17960
+ const removePairEntries = (mcpUrl, dry) => [...disconnectHarnesses].map((h) => pairInstallerFor(h).removePair({ url: mcpUrl, bin: cfg.ship.agent.bin, dryRun: dry }));
17961
+ if (!flag("reset")) {
17962
+ for (const line of removePairEntries(url, dryRun)) process.stderr.write(`${line}
17800
17963
  `);
17964
+ break;
17801
17965
  }
17966
+ const host = hostPlatform();
17967
+ let resetStore;
17968
+ try {
17969
+ resetStore = getSessionStore3();
17970
+ } catch {
17971
+ resetStore = void 0;
17972
+ }
17973
+ const effects = {
17974
+ async uninstallUnits(dry) {
17975
+ const lines = [];
17976
+ if (host === "windows") return lines;
17977
+ const log = { emit: (m) => {
17978
+ lines.push(m);
17979
+ }, warn: (m) => {
17980
+ lines.push(m);
17981
+ } };
17982
+ for (const job of ["run", "release", "passengers"]) {
17983
+ await applyUninstall(planUninstall(cfg.ship, CREW_HOME, host, detectSystemd(), job), CREW_HOME, dry, log);
17984
+ }
17985
+ return lines;
17986
+ },
17987
+ removePair: removePairEntries,
17988
+ async revokeSeatKeys(r, dry) {
17989
+ const outcomes = await revokeSeatKeys(r, { userAgent: cfg.ship.userAgent, dryRun: dry });
17990
+ return outcomes.map((o) => ({ line: describeRevokeOutcome(o), ok: o.action !== "failed" }));
17991
+ },
17992
+ async credential(r) {
17993
+ if (!resetStore) return { stored: false, clear: async () => {
17994
+ } };
17995
+ const store = resetStore;
17996
+ const h = hostFromUrl3(r.baseUrl);
17997
+ const [slug] = splitRoute(r.route);
17998
+ const session = await store.get(h, slug);
17999
+ return { stored: Boolean(session?.apiKey), clear: () => store.clear(h, slug) };
18000
+ },
18001
+ inspectCheckout(dir) {
18002
+ const dirty = status(dir).map((c) => c.path);
18003
+ const linkedWorktrees = worktrees(dir).map((w) => w.path).filter((p) => realpathSync2(p) !== realpathSync2(dir));
18004
+ return { dirty, linkedWorktrees };
18005
+ }
18006
+ };
18007
+ const droppedRoutes = [];
18008
+ try {
18009
+ const rawCfg = parseYaml2(readFileSync23(cfg.configFile, "utf8"));
18010
+ for (const r of rawCfg?.routes ?? []) {
18011
+ if (typeof r?.route !== "string" || cfg.routes.some((x) => x.route === r.route)) continue;
18012
+ try {
18013
+ splitRoute(r.route);
18014
+ } catch {
18015
+ continue;
18016
+ }
18017
+ droppedRoutes.push({ route: r.route, baseUrl: typeof r.baseUrl === "string" ? r.baseUrl : rawCfg?.ship?.baseUrl ?? DEFAULT_BASE_URL });
18018
+ }
18019
+ } catch {
18020
+ }
18021
+ const steps = await runReset(
18022
+ { routes: cfg.routes, droppedRoutes, stateDir: cfg.ship.stateDir, configFile: cfg.configFile, dryRun, removeCheckouts: flag("remove-checkouts") },
18023
+ effects
18024
+ );
18025
+ if (flag("json")) process.stdout.write(`${JSON.stringify({ dryRun, ok: !resetIncomplete(steps), steps })}
18026
+ `);
18027
+ else for (const s of steps) process.stdout.write(`${describeResetStep(s)}
18028
+ `);
18029
+ if (resetIncomplete(steps)) process.exitCode = 1;
17802
18030
  break;
17803
18031
  }
17804
18032
  case "install": {
@@ -17820,8 +18048,8 @@ Signed in as ${result.identity.email} \u2014 minted an API key in workspace "${r
17820
18048
  process.stdout.write("crew reload: no daemon unit is running \u2014 nothing to reload (the next run reads crew.yaml fresh)\n");
17821
18049
  break;
17822
18050
  }
17823
- mkdirSync15(cfg.ship.stateDir, { recursive: true });
17824
- writeFileSync16(sentinel, `${(/* @__PURE__ */ new Date()).toISOString()}
18051
+ mkdirSync16(cfg.ship.stateDir, { recursive: true });
18052
+ writeFileSync17(sentinel, `${(/* @__PURE__ */ new Date()).toISOString()}
17825
18053
  `);
17826
18054
  process.stdout.write("crew reload: requested \u2014 the daemon finishes its in-flight pass, then relaunches with the new config\n");
17827
18055
  if (flag("now") && control?.stopCommand && control.startCommand) {
@@ -17872,7 +18100,7 @@ Signed in as ${result.identity.email} \u2014 minted an API key in workspace "${r
17872
18100
  }
17873
18101
  case "log": {
17874
18102
  try {
17875
- const text = readFileSync22(cfg.ship.logFile, "utf8").split("\n");
18103
+ const text = readFileSync23(cfg.ship.logFile, "utf8").split("\n");
17876
18104
  process.stdout.write(`${text.slice(-(Number(value("tail")) || 40)).join("\n")}
17877
18105
  `);
17878
18106
  } catch {
@@ -17895,8 +18123,8 @@ Signed in as ${result.identity.email} \u2014 minted an API key in workspace "${r
17895
18123
  };
17896
18124
  if (!route.resolved) fail(`route ${route.route} is not connected \u2014 run \`crew connect\` first`);
17897
18125
  const tracker = new Tracker(route, cfg.ship);
17898
- const configText = readFileSync22(cfg.configFile, "utf8");
17899
- const cfgBase = dirname16(cfg.configFile);
18126
+ const configText = readFileSync23(cfg.configFile, "utf8");
18127
+ const cfgBase = dirname17(cfg.configFile);
17900
18128
  const configured = configuredRepos(configText, route.route, cfgBase);
17901
18129
  const dir = configured.find((r) => r.name === SAMPLE_REPO)?.dir ?? sampleDir(configured.map((r) => r.dir), route.reposBasePath, route.route.split("/")[0]);
17902
18130
  const resolvedFile = resolvedPathFor(cfg.ship.stateDir, route.route);
@@ -17924,7 +18152,7 @@ Signed in as ${result.identity.email} \u2014 minted an API key in workspace "${r
17924
18152
  }
17925
18153
  if (configured.some((r) => r.dir === dir)) {
17926
18154
  try {
17927
- writeFileSync16(cfg.configFile, planRepoRemove(configText, cfgBase, route.route, dir).text);
18155
+ writeFileSync17(cfg.configFile, planRepoRemove(configText, cfgBase, route.route, dir).text);
17928
18156
  } catch (e) {
17929
18157
  if (e instanceof ReposError) fail(e.message);
17930
18158
  throw e;
@@ -17932,29 +18160,29 @@ Signed in as ${result.identity.email} \u2014 minted an API key in workspace "${r
17932
18160
  }
17933
18161
  if (row) {
17934
18162
  await tracker.deleteRepoRow(row.id);
17935
- if (existsSync29(resolvedFile)) {
17936
- const raw = JSON.parse(readFileSync22(resolvedFile, "utf8"));
18163
+ if (existsSync30(resolvedFile)) {
18164
+ const raw = JSON.parse(readFileSync23(resolvedFile, "utf8"));
17937
18165
  if (raw.repoNames) delete raw.repoNames[row.id];
17938
18166
  if (raw.repoRemotes) delete raw.repoRemotes[row.id];
17939
- writeFileSync16(resolvedFile, `${JSON.stringify(raw, null, 2)}
18167
+ writeFileSync17(resolvedFile, `${JSON.stringify(raw, null, 2)}
17940
18168
  `);
17941
18169
  }
17942
18170
  }
17943
- if (existsSync29(join29(dir, ".git")) && basename10(dir) === SAMPLE_REPO) rmSync11(dir, { recursive: true, force: true });
18171
+ if (existsSync30(join30(dir, ".git")) && basename10(dir) === SAMPLE_REPO) rmSync12(dir, { recursive: true, force: true });
17944
18172
  say("removed", { dir, closed }, `removed the sample repo${closed.length ? `, closed ${closed.join(", ")}` : ""}`);
17945
18173
  break;
17946
18174
  }
17947
18175
  const projectId = route.resolved.areaId ?? resolveRepoProject(await tracker.projectRows(), value("project"));
17948
- const fresh = !existsSync29(join29(dir, ".git"));
17949
- if (existsSync29(dir) && fresh && readdirSync11(dir).length > 0) fail(`${dir} exists and is not a git checkout \u2014 move it aside`);
18176
+ const fresh = !existsSync30(join30(dir, ".git"));
18177
+ if (existsSync30(dir) && fresh && readdirSync12(dir).length > 0) fail(`${dir} exists and is not a git checkout \u2014 move it aside`);
17950
18178
  if (dryRun) {
17951
18179
  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`);
17952
18180
  break;
17953
18181
  }
17954
18182
  if (fresh) {
17955
- mkdirSync15(dir, { recursive: true });
17956
- writeFileSync16(join29(dir, "README.md"), SAMPLE_README);
17957
- writeFileSync16(join29(dir, ".crew.yaml"), SAMPLE_CREW_YAML);
18183
+ mkdirSync16(dir, { recursive: true });
18184
+ writeFileSync17(join30(dir, "README.md"), SAMPLE_README);
18185
+ writeFileSync17(join30(dir, ".crew.yaml"), SAMPLE_CREW_YAML);
17958
18186
  const g = (...a) => {
17959
18187
  const r = spawnSync6("git", a, { cwd: dir, encoding: "utf8" });
17960
18188
  if (r.status !== 0) fail(`git ${a[0]} failed in ${dir}: ${(r.stderr || r.stdout).trim()}`);
@@ -17972,17 +18200,17 @@ Signed in as ${result.identity.email} \u2014 minted an API key in workspace "${r
17972
18200
  spec: { platform: "unix", releaseMode: "integrate", ciProvider: "none" }
17973
18201
  });
17974
18202
  repoId = (await tracker.createRepoRow(payload)).id;
17975
- if (existsSync29(resolvedFile)) {
17976
- const raw = JSON.parse(readFileSync22(resolvedFile, "utf8"));
18203
+ if (existsSync30(resolvedFile)) {
18204
+ const raw = JSON.parse(readFileSync23(resolvedFile, "utf8"));
17977
18205
  raw.repoNames = { ...raw.repoNames, [repoId]: SAMPLE_REPO };
17978
18206
  raw.repoRemotes = { ...raw.repoRemotes, [repoId]: SAMPLE_REMOTE };
17979
- writeFileSync16(resolvedFile, `${JSON.stringify(raw, null, 2)}
18207
+ writeFileSync17(resolvedFile, `${JSON.stringify(raw, null, 2)}
17980
18208
  `);
17981
18209
  }
17982
18210
  }
17983
18211
  if (!configured.some((r) => r.dir === dir)) {
17984
18212
  try {
17985
- writeFileSync16(cfg.configFile, planRepoAdd({ text: configText, base: cfgBase, route: route.route, path: dir, name: SAMPLE_REPO }).text);
18213
+ writeFileSync17(cfg.configFile, planRepoAdd({ text: configText, base: cfgBase, route: route.route, path: dir, name: SAMPLE_REPO }).text);
17986
18214
  } catch (e) {
17987
18215
  if (e instanceof ReposError) fail(e.message);
17988
18216
  throw e;
@@ -18272,7 +18500,7 @@ watermark: ${state.watermark()}
18272
18500
  }
18273
18501
  for (const r of repos) {
18274
18502
  const remote = r.config.branch.remote;
18275
- if (existsSync29(r.dir)) {
18503
+ if (existsSync30(r.dir)) {
18276
18504
  pushAuth.push({
18277
18505
  route: c.route,
18278
18506
  repo: r.name,
@@ -18300,7 +18528,7 @@ routes: ${cfg.routes.map((c) => c.route).join(", ")}
18300
18528
  );
18301
18529
  try {
18302
18530
  const evFile = eventFileFor(cfg.ship.stateDir);
18303
- const evSize = existsSync29(evFile) ? statSync9(evFile).size : 0;
18531
+ const evSize = existsSync30(evFile) ? statSync10(evFile).size : 0;
18304
18532
  process.stdout.write(
18305
18533
  evSize > cfg.ship.eventsRotateBytes * 4 ? `events log: WARN ${evFile} is ${Math.round(evSize / 1048576)} MB (> 4x eventsRotateBytes); run \`crew reap\` to rotate it
18306
18534
  ` : "events log: ok\n"
@@ -18311,7 +18539,7 @@ routes: ${cfg.routes.map((c) => c.route).join(", ")}
18311
18539
  const unitPlan = planInstall(cfg.ship, CREW_HOME, host, detectSystemd(), "run");
18312
18540
  const abandons = unitAbandonsAgents(unitPlan, (p) => {
18313
18541
  try {
18314
- return readFileSync22(p, "utf8");
18542
+ return readFileSync23(p, "utf8");
18315
18543
  } catch {
18316
18544
  return null;
18317
18545
  }
@@ -18327,7 +18555,7 @@ routes: ${cfg.routes.map((c) => c.route).join(", ")}
18327
18555
  const changed2 = configChangedAfter(daemonConfigPaths(cfg), daemonStartedAt);
18328
18556
  if (changed2) {
18329
18557
  process.stdout.write(
18330
- `daemon config: WARN ${changed2} changed at ${new Date(statSync9(changed2).mtimeMs).toISOString()} but the daemon started at ${new Date(daemonStartedAt).toISOString()} \u2014 run \`crew reload\`
18558
+ `daemon config: WARN ${changed2} changed at ${new Date(statSync10(changed2).mtimeMs).toISOString()} but the daemon started at ${new Date(daemonStartedAt).toISOString()} \u2014 run \`crew reload\`
18331
18559
  `
18332
18560
  );
18333
18561
  }
@@ -18387,6 +18615,8 @@ routes: ${cfg.routes.map((c) => c.route).join(", ")}
18387
18615
  const keyRole = route.resolved?.role ?? await fetchKeyRole(route, cfg.ship.userAgent);
18388
18616
  const roleRoute = route.resolved && keyRole ? { ...route, resolved: { ...route.resolved, role: keyRole } } : route;
18389
18617
  process.stdout.write(`api key role: ${keyRole ?? "unknown"}
18618
+ `);
18619
+ for (const m of route.resolved?.capabilityMisses ?? []) process.stdout.write(`capability: MISSING ${m} (re-run \`crew connect\` once granted)
18390
18620
  `);
18391
18621
  const canMint = await canMintAgentKeys(roleRoute, { userAgent: cfg.ship.userAgent });
18392
18622
  process.stdout.write(`can mint agent keys: ${canMint === void 0 ? "unknown" : canMint ? "yes" : "no"}