@tablation/crew 0.59.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 +9 -0
  2. package/dist/cli.js +703 -275
  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([
@@ -5211,7 +5213,8 @@ var VALUE_FLAGS = /* @__PURE__ */ new Set([
5211
5213
  "tail",
5212
5214
  "ticket",
5213
5215
  "workspace",
5214
- "workspace-id"
5216
+ "workspace-id",
5217
+ "write"
5215
5218
  ]);
5216
5219
  function positionalArgs(argv2) {
5217
5220
  const out = [];
@@ -5759,6 +5762,103 @@ var State = class _State {
5759
5762
  }
5760
5763
  };
5761
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
+
5762
5862
  // src/tracker.ts
5763
5863
  import { hostname } from "os";
5764
5864
  import { TablationClient as TablationClient3 } from "@tablation/client";
@@ -6153,24 +6253,24 @@ function recoveredFailureHeadlines(o, route) {
6153
6253
  }
6154
6254
 
6155
6255
  // src/ship-attention.ts
6156
- import { existsSync as existsSync15, statfsSync } from "fs";
6256
+ import { existsSync as existsSync16, statfsSync } from "fs";
6157
6257
 
6158
6258
  // src/git.ts
6159
6259
  import { execFileSync as execFileSync2 } from "child_process";
6160
6260
  import {
6161
- existsSync as existsSync14,
6162
- mkdirSync as mkdirSync7,
6261
+ existsSync as existsSync15,
6262
+ mkdirSync as mkdirSync8,
6163
6263
  openSync as openSync3,
6164
6264
  closeSync as closeSync3,
6165
6265
  writeSync,
6166
- statSync as statSync5,
6266
+ statSync as statSync6,
6167
6267
  unlinkSync as unlinkSync2,
6168
- readFileSync as readFileSync13,
6169
- writeFileSync as writeFileSync8,
6170
- readdirSync as readdirSync4,
6171
- rmSync as rmSync7
6268
+ readFileSync as readFileSync14,
6269
+ writeFileSync as writeFileSync9,
6270
+ readdirSync as readdirSync5,
6271
+ rmSync as rmSync8
6172
6272
  } from "fs";
6173
- 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";
6174
6274
  var GitError = class extends Error {
6175
6275
  };
6176
6276
  function streamText(x) {
@@ -6536,8 +6636,8 @@ function recoverStrandedReleaseCheckouts(cwd) {
6536
6636
  if (line.startsWith("worktree ")) path = line.slice("worktree ".length);
6537
6637
  else if (line.startsWith("branch refs/heads/crew/release-") && path) {
6538
6638
  gitOk(cwd, ["worktree", "remove", "--force", path]);
6539
- const root = dirname6(path);
6540
- 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 });
6541
6641
  }
6542
6642
  }
6543
6643
  const stranded = (gitOk(cwd, ["for-each-ref", "--format=%(refname:short)", "refs/heads/crew/release-*"]) ?? "").split("\n").filter(Boolean);
@@ -6548,18 +6648,18 @@ function sweepStaleReleaseRoots(tmp, now = Date.now(), maxAgeMs = 60 * 60 * 1e3)
6548
6648
  const removed = [];
6549
6649
  let names;
6550
6650
  try {
6551
- names = readdirSync4(tmp);
6651
+ names = readdirSync5(tmp);
6552
6652
  } catch {
6553
6653
  return removed;
6554
6654
  }
6555
6655
  for (const name of names) {
6556
6656
  if (!name.startsWith("crew-release-")) continue;
6557
- const root = join14(tmp, name);
6657
+ const root = join15(tmp, name);
6558
6658
  try {
6559
- const st = statSync5(root);
6659
+ const st = statSync6(root);
6560
6660
  if (!st.isDirectory() || now - st.mtimeMs < maxAgeMs) continue;
6561
- if (existsSync14(join14(root, "checkout"))) continue;
6562
- rmSync7(root, { recursive: true, force: true });
6661
+ if (existsSync15(join15(root, "checkout"))) continue;
6662
+ rmSync8(root, { recursive: true, force: true });
6563
6663
  removed.push(name);
6564
6664
  } catch {
6565
6665
  }
@@ -6601,13 +6701,13 @@ function originUrlOf(dir, remoteName) {
6601
6701
  }
6602
6702
  }
6603
6703
  function ensureRepoCheckout(dir, remote, opts = {}) {
6604
- if (existsSync14(`${dir}/.git`)) return false;
6704
+ if (existsSync15(`${dir}/.git`)) return false;
6605
6705
  if (!remote) {
6606
6706
  throw new GitError(
6607
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`
6608
6708
  );
6609
6709
  }
6610
- mkdirSync7(dirname6(dir), { recursive: true });
6710
+ mkdirSync8(dirname7(dir), { recursive: true });
6611
6711
  const url = cloneUrlFor(remote, opts);
6612
6712
  try {
6613
6713
  execFileSync2("git", ["clone", url, dir], { encoding: "utf8", stdio: ["ignore", "pipe", "pipe"] });
@@ -6616,8 +6716,13 @@ function ensureRepoCheckout(dir, remote, opts = {}) {
6616
6716
  const why = [err.stderr, err.stdout].map(streamText).filter(Boolean).join(" / ");
6617
6717
  throw new GitError(`git clone ${url} ${dir}: ${why || err.message.trim()}`);
6618
6718
  }
6719
+ cloneRecorder?.(dir);
6619
6720
  return true;
6620
6721
  }
6722
+ var cloneRecorder;
6723
+ function setCloneRecorder(fn) {
6724
+ cloneRecorder = fn;
6725
+ }
6621
6726
  var CHECKOUT_LOCK_STALE_MS = 5 * 60 * 1e3;
6622
6727
  function checkoutLockPath(dir) {
6623
6728
  return `${dir}.sync.lock`;
@@ -6627,7 +6732,7 @@ function sleepSync(ms2) {
6627
6732
  }
6628
6733
  function acquireCheckoutLock(dir, timeoutMs = 3e4) {
6629
6734
  const lockPath = checkoutLockPath(dir);
6630
- mkdirSync7(dirname6(lockPath), { recursive: true });
6735
+ mkdirSync8(dirname7(lockPath), { recursive: true });
6631
6736
  const deadline = Date.now() + timeoutMs;
6632
6737
  for (; ; ) {
6633
6738
  try {
@@ -6645,7 +6750,7 @@ function acquireCheckoutLock(dir, timeoutMs = 3e4) {
6645
6750
  const err = e;
6646
6751
  if (err.code !== "EEXIST") throw err;
6647
6752
  try {
6648
- if (Date.now() - statSync5(lockPath).mtimeMs > CHECKOUT_LOCK_STALE_MS) {
6753
+ if (Date.now() - statSync6(lockPath).mtimeMs > CHECKOUT_LOCK_STALE_MS) {
6649
6754
  try {
6650
6755
  unlinkSync2(lockPath);
6651
6756
  } catch {
@@ -6664,17 +6769,17 @@ function acquireCheckoutLock(dir, timeoutMs = 3e4) {
6664
6769
  function syncPassengerCheckout(dir, githubRemote, gitRemoteName, base, clone = {}) {
6665
6770
  const release = acquireCheckoutLock(dir);
6666
6771
  try {
6667
- if (!existsSync14(`${dir}/.git`) && githubRemote) {
6772
+ if (!existsSync15(`${dir}/.git`) && githubRemote) {
6668
6773
  const url = cloneUrlFor(githubRemote, clone);
6669
6774
  const marker = cloneFailureMarker(dir);
6670
- if (existsSync14(marker) && readFileSync13(marker, "utf8").split("\n")[0] === url) {
6671
- 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 };
6672
6777
  }
6673
6778
  try {
6674
6779
  ensureRepoCheckout(dir, githubRemote, clone);
6675
6780
  } catch (e) {
6676
6781
  if (!(e instanceof GitError)) throw e;
6677
- writeFileSync8(marker, `${url}
6782
+ writeFileSync9(marker, `${url}
6678
6783
  ${e.message}`);
6679
6784
  return { action: "clone-failed", detail: e.message, repeat: false };
6680
6785
  }
@@ -6872,7 +6977,7 @@ function noteHeldWorktree(state, h, nowMs = Date.now()) {
6872
6977
  function reconcileHeldWorktrees(state, repo, log, stillRefused) {
6873
6978
  for (const i of state.shipAttention().previous()) {
6874
6979
  if (i.kind !== "worktree_held" || !i.held || i.held.repo !== repo) continue;
6875
- const gone = !existsSync15(i.held.path);
6980
+ const gone = !existsSync16(i.held.path);
6876
6981
  if (!gone && !(stillRefused && !stillRefused.has(i.held.path))) continue;
6877
6982
  clearShipAttention(state, i.key);
6878
6983
  log.emit(
@@ -8586,8 +8691,8 @@ function materializeBranchForTicket(dir, cfg, t, role, onProblem) {
8586
8691
  }
8587
8692
 
8588
8693
  // src/poll.ts
8589
- import { writeFileSync as writeFileSync9 } from "fs";
8590
- import { join as join15 } from "path";
8694
+ import { writeFileSync as writeFileSync10 } from "fs";
8695
+ import { join as join16 } from "path";
8591
8696
 
8592
8697
  // src/stalled.ts
8593
8698
  var STALLED_AFTER_CYCLES = 30;
@@ -8876,7 +8981,7 @@ async function decideCycle(o) {
8876
8981
  }
8877
8982
  return decision;
8878
8983
  }
8879
- var digestPath = (stateDir, route, role) => join15(stateDir, `digest-${route}-${role}.md`);
8984
+ var digestPath = (stateDir, route, role) => join16(stateDir, `digest-${route}-${role}.md`);
8880
8985
  function writeDigest(o, d, role, stateDir) {
8881
8986
  const me = o.route.resolved?.seats[role];
8882
8987
  if (!me) return false;
@@ -8940,7 +9045,7 @@ function writeDigest(o, d, role, stateDir) {
8940
9045
  };
8941
9046
  const text = role === "qa" ? qaDigest(input) : buildingDigest(input);
8942
9047
  const path = digestPath(stateDir, routeSlug(o.route.route), role);
8943
- writeFileSync9(path, text);
9048
+ writeFileSync10(path, text);
8944
9049
  o.emit.emit(`queue digest written for ${role} (${Buffer.byteLength(text)} bytes)`, {
8945
9050
  data: { role, bytes: Buffer.byteLength(text) }
8946
9051
  });
@@ -9317,13 +9422,13 @@ async function applyStrandedVerified(writer, s, contract, operatorId, memberId,
9317
9422
  }
9318
9423
 
9319
9424
  // src/run-worktree.ts
9320
- import { existsSync as existsSync17 } from "fs";
9321
- 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";
9322
9427
 
9323
9428
  // src/ports.ts
9324
9429
  import { execFileSync as execFileSync3 } from "child_process";
9325
- import { existsSync as existsSync16 } from "fs";
9326
- import { join as join16 } from "path";
9430
+ import { existsSync as existsSync17 } from "fs";
9431
+ import { join as join17 } from "path";
9327
9432
  var run = (cmd, args) => {
9328
9433
  try {
9329
9434
  return execFileSync3(cmd, args, { encoding: "utf8", stdio: ["ignore", "pipe", "ignore"] });
@@ -9368,7 +9473,7 @@ function ticketForPort(port, s = DEFAULT_PORTS) {
9368
9473
  }
9369
9474
  function worktreeExists(parent, prefix, n) {
9370
9475
  for (const name of [`${prefix}${n}`, `${prefix}${String(n).padStart(3, "0")}`]) {
9371
- if (existsSync16(join16(parent, name, ".git"))) return true;
9476
+ if (existsSync17(join17(parent, name, ".git"))) return true;
9372
9477
  }
9373
9478
  return false;
9374
9479
  }
@@ -9444,7 +9549,7 @@ function planRunWorktree(dir, cfg, t, role, opts = {}) {
9444
9549
  const existing = worktreeForNumber(dir, num);
9445
9550
  const located = locateBranchForTicket(dir, cfg, t, role);
9446
9551
  const baseRef = hasRemote ? `${remote}/${base}` : base;
9447
- if (existing && !existing.prunable && existsSync17(existing.path)) {
9552
+ if (existing && !existing.prunable && existsSync18(existing.path)) {
9448
9553
  const branch = existing.branch;
9449
9554
  if (status(existing.path).length > 0) {
9450
9555
  return { action: opts.resumeInterrupted ? "resume-dirty" : "leave-dirty", path: existing.path, branch };
@@ -9460,7 +9565,7 @@ function planRunWorktree(dir, cfg, t, role, opts = {}) {
9460
9565
  if (countCommits(existing.path, `${baseRef}..HEAD`) > 0) return { action: "leave-unpushed", path: existing.path, branch };
9461
9566
  return { action: "recut", path: existing.path, branch, base: baseRef };
9462
9567
  }
9463
- const path = join17(dirname7(dir), worktreeForTicket(dir, cfg, t, role));
9568
+ const path = join18(dirname8(dir), worktreeForTicket(dir, cfg, t, role));
9464
9569
  if (located?.where === "remote" || located && hasRemote && remoteBranchExists(dir, remote, located.branch)) {
9465
9570
  return { action: "cut-remote", path, branch: located.branch };
9466
9571
  }
@@ -9509,7 +9614,7 @@ function worktreeHoldsUnpushed(path, remote) {
9509
9614
  }
9510
9615
  async function finishRunWorktree(dir, cfg, branch) {
9511
9616
  const wt = worktrees(dir).find((w) => w.branch === branch && !w.prunable && w.path !== dir);
9512
- if (!wt || !existsSync17(wt.path)) return { kind: "none" };
9617
+ if (!wt || !existsSync18(wt.path)) return { kind: "none" };
9513
9618
  if (cfg.hooks.handoff) return { kind: "kept-handoff", path: wt.path };
9514
9619
  if (status(wt.path).length > 0) return { kind: "kept-dirty", path: wt.path };
9515
9620
  if (remoteConfigured(dir, cfg.branch.remote) && worktreeHoldsUnpushed(wt.path, cfg.branch.remote)) {
@@ -9521,7 +9626,7 @@ async function finishRunWorktree(dir, cfg, branch) {
9521
9626
  if (pids.length) await killGently(pids);
9522
9627
  }
9523
9628
  gitOk(dir, ["worktree", "remove", wt.path]);
9524
- 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 };
9525
9630
  }
9526
9631
 
9527
9632
  // src/handoff-guard.ts
@@ -9782,13 +9887,13 @@ async function guardRunStrays(route, ship, tracker, role, workingId, since2, log
9782
9887
  }
9783
9888
 
9784
9889
  // src/connect-wizard.ts
9785
- import { delimiter, dirname as dirname8, join as join18 } from "path";
9786
- 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";
9787
9892
  import { stringify } from "yaml";
9788
9893
  var HOST_PASSENGERS_DOCS_URL = "https://crew.tablation.dev/host-passengers";
9789
9894
  function shouldRunWizard(opts) {
9790
9895
  if (!opts.isTTY) return false;
9791
- const exists = opts.configExists ?? existsSync18;
9896
+ const exists = opts.configExists ?? existsSync19;
9792
9897
  return !configSearchPath(opts.crewHome).some((p) => exists(p));
9793
9898
  }
9794
9899
  function firstRunConfigPath() {
@@ -9805,7 +9910,7 @@ function defaultIsExecutable(p) {
9805
9910
  function findOnPath2(name, pathEnv, isExecutable = defaultIsExecutable) {
9806
9911
  for (const dir of pathEnv.split(delimiter)) {
9807
9912
  if (!dir) continue;
9808
- const candidate = join18(dir, name);
9913
+ const candidate = join19(dir, name);
9809
9914
  if (isExecutable(candidate)) return candidate;
9810
9915
  }
9811
9916
  return void 0;
@@ -9831,9 +9936,9 @@ async function runWizard(p, env) {
9831
9936
  if (entered && isExec(entered)) agentBin = entered;
9832
9937
  else p.say(entered ? `${entered} is not an executable file.` : "A path is required.");
9833
9938
  }
9834
- const nodeDir = dirname8(env.nodePath);
9939
+ const nodeDir = dirname9(env.nodePath);
9835
9940
  const pnpmPath = findOnPath2("pnpm", env.pathEnv, isExec);
9836
- const baseDirs = [nodeDir, pnpmPath ? dirname8(pnpmPath) : void 0];
9941
+ const baseDirs = [nodeDir, pnpmPath ? dirname9(pnpmPath) : void 0];
9837
9942
  p.say(
9838
9943
  `
9839
9944
  Host Passengers lets this machine run the agent sessions that other people's
@@ -9844,7 +9949,7 @@ Tablation workspaces ask for, in a container. More: ${HOST_PASSENGERS_DOCS_URL}`
9844
9949
  p.say("Docker is not available here, so nothing will be hosted until it is. Your answer is recorded anyway.");
9845
9950
  }
9846
9951
  const dockerPath = hostPassengers ? findOnPath2("docker", env.pathEnv, isExec) : void 0;
9847
- const extraPath = deriveExtraPath([...baseDirs, dockerPath ? dirname8(dockerPath) : void 0]);
9952
+ const extraPath = deriveExtraPath([...baseDirs, dockerPath ? dirname9(dockerPath) : void 0]);
9848
9953
  if (extraPath) {
9849
9954
  p.say(`Hooks will get ${extraPath} on their PATH (where node, pnpm${dockerPath ? " and docker" : ""} live).`);
9850
9955
  }
@@ -9866,9 +9971,9 @@ routes:
9866
9971
  ${routeBlock}`;
9867
9972
  }
9868
9973
  function writeNewConfig(path, text) {
9869
- mkdirSync8(dirname8(path), { recursive: true });
9974
+ mkdirSync9(dirname9(path), { recursive: true });
9870
9975
  try {
9871
- writeFileSync10(path, text, { mode: 384, flag: "wx" });
9976
+ writeFileSync11(path, text, { mode: 384, flag: "wx" });
9872
9977
  return true;
9873
9978
  } catch (e) {
9874
9979
  if (e.code === "EEXIST") return false;
@@ -9901,7 +10006,7 @@ Wrote ${o.configPath}.
9901
10006
 
9902
10007
  // src/connect-json.ts
9903
10008
  import { accessSync as accessSync3, constants as constants3 } from "fs";
9904
- import { dirname as dirname9 } from "path";
10009
+ import { dirname as dirname10 } from "path";
9905
10010
  var CHOICE_EXIT_CODE = 3;
9906
10011
  function createEmitter(json, write) {
9907
10012
  return {
@@ -9949,7 +10054,7 @@ function flagWizardAnswers(f, env) {
9949
10054
  if (f.agentBin && !isExec(f.agentBin)) return { error: `--agent-bin ${f.agentBin} is not an executable file` };
9950
10055
  const pnpmPath = findOnPath2("pnpm", env.pathEnv, isExec);
9951
10056
  const dockerPath = f.hostPassengers ? findOnPath2("docker", env.pathEnv, isExec) : void 0;
9952
- 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]);
9953
10058
  return {
9954
10059
  answers: {
9955
10060
  name: f.shipName?.trim() || env.hostname,
@@ -9962,8 +10067,8 @@ function flagWizardAnswers(f, env) {
9962
10067
  }
9963
10068
 
9964
10069
  // src/release-run.ts
9965
- import { readFileSync as readFileSync14, writeFileSync as writeFileSync11, existsSync as existsSync19, copyFileSync, mkdirSync as mkdirSync9, mkdtempSync as mkdtempSync2, rmSync as rmSync8 } from "fs";
9966
- 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";
9967
10072
  import { tmpdir as tmpdir3 } from "os";
9968
10073
 
9969
10074
  // src/release.ts
@@ -10651,13 +10756,13 @@ async function bump(o, size) {
10651
10756
  }
10652
10757
  return printed;
10653
10758
  }
10654
- 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));
10655
10760
  const first = files[0];
10656
- if (!existsSync19(first)) {
10761
+ if (!existsSync20(first)) {
10657
10762
  o.emit.error(`no ${o.repo.release.versionFiles[0]} to read a version from`);
10658
10763
  return null;
10659
10764
  }
10660
- const current = JSON.parse(readFileSync14(first, "utf8")).version;
10765
+ const current = JSON.parse(readFileSync15(first, "utf8")).version;
10661
10766
  if (!current) {
10662
10767
  o.emit.error(`no version field in ${o.repo.release.versionFiles[0]}`);
10663
10768
  return null;
@@ -10670,27 +10775,27 @@ async function bump(o, size) {
10670
10775
  return version;
10671
10776
  }
10672
10777
  for (const f of files) {
10673
- const src = readFileSync14(f, "utf8");
10778
+ const src = readFileSync15(f, "utf8");
10674
10779
  const out = src.replace(/("version"\s*:\s*")[^"]*(")/, `$1${version}$2`);
10675
10780
  if (out === src) {
10676
10781
  o.emit.error(`no version field in ${f}`);
10677
10782
  return null;
10678
10783
  }
10679
- writeFileSync11(f, out);
10784
+ writeFileSync12(f, out);
10680
10785
  }
10681
10786
  return version;
10682
10787
  }
10683
10788
  function writeChangelog(o, version, entries) {
10684
10789
  const file = o.repo.release.changelog;
10685
10790
  if (!file || entries.length === 0) return;
10686
- const path = join19(o.cwd, file);
10791
+ const path = join20(o.cwd, file);
10687
10792
  const section = renderChangelogSection(version, (/* @__PURE__ */ new Date()).toISOString().slice(0, 10), entries);
10688
10793
  if (o.dryRun) {
10689
10794
  o.emit.emit(`would prepend ${entries.length} entr(ies) to ${file}`);
10690
10795
  return;
10691
10796
  }
10692
- const existing = existsSync19(path) ? readFileSync14(path, "utf8") : "# Changelog\n";
10693
- writeFileSync11(path, insertChangelogSection(existing, section));
10797
+ const existing = existsSync20(path) ? readFileSync15(path, "utf8") : "# Changelog\n";
10798
+ writeFileSync12(path, insertChangelogSection(existing, section));
10694
10799
  }
10695
10800
  async function confirm(o, expected) {
10696
10801
  if (!o.repo.hooks.released) return void 0;
@@ -10773,7 +10878,7 @@ function closeReleaseCheckout(cwd, root, dir, branch, run2 = gitOk) {
10773
10878
  run2(cwd, ["worktree", "remove", "--force", dir]);
10774
10879
  } finally {
10775
10880
  try {
10776
- rmSync8(root, { recursive: true, force: true });
10881
+ rmSync9(root, { recursive: true, force: true });
10777
10882
  } finally {
10778
10883
  run2(cwd, ["worktree", "prune"]);
10779
10884
  run2(cwd, ["branch", "-D", branch]);
@@ -10783,8 +10888,8 @@ function closeReleaseCheckout(cwd, root, dir, branch, run2 = gitOk) {
10783
10888
  function openReleaseCheckout(o) {
10784
10889
  const { remote, base } = o.repo.branch;
10785
10890
  const branch = `crew/release-${Date.now()}`;
10786
- const root = mkdtempSync2(join19(tmpdir3(), "crew-release-"));
10787
- const dir = join19(root, "checkout");
10891
+ const root = mkdtempSync2(join20(tmpdir3(), "crew-release-"));
10892
+ const dir = join20(root, "checkout");
10788
10893
  o.emit.emit(`release checkout: ${dir}`, { step: "release" });
10789
10894
  const close = () => closeReleaseCheckout(o.cwd, root, dir, branch);
10790
10895
  try {
@@ -10794,10 +10899,10 @@ function openReleaseCheckout(o) {
10794
10899
  throw e;
10795
10900
  }
10796
10901
  for (const f of o.repo.worktrees.copy) {
10797
- const src = join19(o.cwd, f);
10798
- if (!existsSync19(src)) continue;
10799
- mkdirSync9(dirname10(join19(dir, f)), { recursive: true });
10800
- 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));
10801
10906
  }
10802
10907
  return { dir, branch, close };
10803
10908
  }
@@ -11465,7 +11570,7 @@ function missingReleaserWarning(repo, release, ships, now = Date.now()) {
11465
11570
  }
11466
11571
 
11467
11572
  // src/watch.ts
11468
- 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";
11469
11574
  var E = "\x1B[";
11470
11575
  var dim = (s) => `${E}2m${s}${E}0m`;
11471
11576
  var bold = (s) => `${E}1m${s}${E}0m`;
@@ -11566,8 +11671,8 @@ function renderHeaders(states, now = Date.now()) {
11566
11671
  return states.map((s) => renderHeader(s, now)).join("");
11567
11672
  }
11568
11673
  function readFrom(file, from) {
11569
- if (!existsSync20(file)) return { events: [], offset: from };
11570
- const size = statSync6(file).size;
11674
+ if (!existsSync21(file)) return { events: [], offset: from };
11675
+ const size = statSync7(file).size;
11571
11676
  if (size < from) return readFrom(file, 0);
11572
11677
  if (size === from) return { events: [], offset: from };
11573
11678
  const fd = openSync4(file, "r");
@@ -12517,9 +12622,54 @@ async function canMintAgentKeys(route, o = {}) {
12517
12622
  }
12518
12623
  }
12519
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
+
12520
12670
  // src/agents-sync-run.ts
12521
- import { existsSync as existsSync21, mkdirSync as mkdirSync10, readFileSync as readFileSync15, writeFileSync as writeFileSync12 } from "fs";
12522
- 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";
12523
12673
  async function runAgentsSync(target, o) {
12524
12674
  const dryRun = o.dryRun ?? false;
12525
12675
  const force = o.force ?? false;
@@ -12552,12 +12702,12 @@ async function runAgentsSync(target, o) {
12552
12702
  for (const s of skillResult?.outcomes ?? []) o.say(describeSkillSyncOutcome(s));
12553
12703
  if (!dryRun) {
12554
12704
  const resolvedPath = resolvedPathFor(o.stateDir, target.route);
12555
- const raw = existsSync21(resolvedPath) ? JSON.parse(readFileSync15(resolvedPath, "utf8")) : {};
12705
+ const raw = existsSync22(resolvedPath) ? JSON.parse(readFileSync16(resolvedPath, "utf8")) : {};
12556
12706
  raw.agentPersonas = personaResult.agentPersonas;
12557
12707
  if (skillResult) raw.agentSkills = skillResult.agentSkills;
12558
12708
  if (personaResult.releaseAgentId) raw.releaseAgentId = personaResult.releaseAgentId;
12559
- mkdirSync10(dirname11(resolvedPath), { recursive: true });
12560
- writeFileSync12(resolvedPath, `${JSON.stringify(raw, null, 2)}
12709
+ mkdirSync11(dirname12(resolvedPath), { recursive: true });
12710
+ writeFileSync13(resolvedPath, `${JSON.stringify(raw, null, 2)}
12561
12711
  `);
12562
12712
  if (target.resolved) {
12563
12713
  target.resolved.agentPersonas = personaResult.agentPersonas;
@@ -12659,77 +12809,12 @@ async function showLogEntry(route, entryId, opts = {}) {
12659
12809
  };
12660
12810
  }
12661
12811
 
12662
- // src/sample-ticket.ts
12663
- import { basename as basename7, dirname as dirname12, join as join20 } from "path";
12664
- var SAMPLE_REPO = "crew-sample";
12665
- var SAMPLE_REMOTE = `local/${SAMPLE_REPO}`;
12666
- var SAMPLE_MARKER = "sample: true";
12667
- var SAMPLE_CREW_YAML = `version: 1
12668
- platform: unix
12669
- branch:
12670
- base: main
12671
- hooks:
12672
- test: "true"
12673
- build: "true"
12674
- release:
12675
- mode: integrate
12676
- versioning: none
12677
- changelog: false
12678
- `;
12679
- var SAMPLE_README = `# crew-sample
12680
-
12681
- A throwaway repository created by \`crew sample-ticket\`. It exists so the crew
12682
- has something safe to work on: its test and build hooks always pass, it has no
12683
- remote, and \`crew sample-ticket --remove\` deletes it.
12684
- `;
12685
- function sampleDir(existing, reposBase, workspace) {
12686
- if (existing.length > 0) return join20(dirname12(existing[0]), SAMPLE_REPO);
12687
- return join20(reposBase, workspace, SAMPLE_REPO);
12688
- }
12689
- var SAMPLE_TITLE = "Sample: add a documented `greet` function with a test";
12690
- function sampleDescription() {
12691
- return [
12692
- SAMPLE_MARKER,
12693
- "",
12694
- "> This is a sample ticket filed by `crew sample-ticket` on a throwaway repository. Nothing here is real work.",
12695
- "",
12696
- "## What",
12697
- "",
12698
- "Add `greet.js` exporting `greet(name)`, which returns `Hello, <name>!` (and `Hello, world!` for no name),",
12699
- "with a JSDoc comment, and `greet.test.js` covering both cases using `node:test`.",
12700
- "",
12701
- "## Acceptance",
12702
- "",
12703
- '- `greet("Ada")` returns `Hello, Ada!`; `greet()` returns `Hello, world!`.',
12704
- "- The test file runs with `node --test` and passes.",
12705
- "- Add a line to the README saying the function exists.",
12706
- "",
12707
- "## Notes",
12708
- "",
12709
- "The repo has no remote: leave the branch local. The hooks in `.crew.yaml` always pass, so QA is checking the",
12710
- "diff against the acceptance list above."
12711
- ].join("\n");
12712
- }
12713
- function isSampleTicket(description) {
12714
- return (description ?? "").trimStart().startsWith(SAMPLE_MARKER);
12715
- }
12716
- function planSampleTicket(i) {
12717
- return {
12718
- title: SAMPLE_TITLE,
12719
- description: sampleDescription(),
12720
- report_type: "feature",
12721
- severity: "s4",
12722
- reporter_name: "crew sample-ticket",
12723
- status: i.approvedStatus,
12724
- project_id: i.projectId,
12725
- repo_id: i.repoId,
12726
- ...i.filedBy ? { filed_by_id: i.filedBy } : {}
12727
- };
12728
- }
12812
+ // src/repos-hooks.ts
12813
+ import { isMap as isMap2, parseDocument as parseDocument2, YAMLMap } from "yaml";
12729
12814
 
12730
12815
  // src/repos-cmd.ts
12731
12816
  import { homedir as homedir7 } from "os";
12732
- import { basename as basename8, join as join21, resolve as resolve3 } from "path";
12817
+ import { basename as basename7, join as join21, resolve as resolve3 } from "path";
12733
12818
  import { isMap, isSeq, parseDocument, stringify as stringify2 } from "yaml";
12734
12819
  var ReposError = class extends Error {
12735
12820
  };
@@ -12801,7 +12886,7 @@ function configuredRepos(text, route, base) {
12801
12886
  });
12802
12887
  }
12803
12888
  const dir = m.get("dir");
12804
- return dir ? [{ name: basename8(String(dir)), dir: expandDir(String(dir), base) }] : [];
12889
+ return dir ? [{ name: basename7(String(dir)), dir: expandDir(String(dir), base) }] : [];
12805
12890
  }
12806
12891
  function planRepoAdd(i) {
12807
12892
  const doc = parseDocument(i.text);
@@ -12813,7 +12898,7 @@ function planRepoAdd(i) {
12813
12898
  throw new ReposError(`${target} is already configured on route ${i.route}`);
12814
12899
  }
12815
12900
  const matched = matchRepoName(i.origin, i.resolved);
12816
- const name = matched ?? i.name ?? normalizeRemote(i.origin)?.split("/").pop() ?? basename8(target);
12901
+ const name = matched ?? i.name ?? normalizeRemote(i.origin)?.split("/").pop() ?? basename7(target);
12817
12902
  let warning;
12818
12903
  if (!matched) {
12819
12904
  const known = Object.values(i.resolved?.repoNames ?? {});
@@ -12932,8 +13017,213 @@ function planRepoClone(i) {
12932
13017
  return { ...add, remote, target };
12933
13018
  }
12934
13019
 
13020
+ // src/repos-hooks.ts
13021
+ function packageManager(f) {
13022
+ if (f.exists("pnpm-lock.yaml")) return "pnpm";
13023
+ if (f.exists("yarn.lock")) return "yarn";
13024
+ if (f.exists("bun.lock") || f.exists("bun.lockb")) return "bun";
13025
+ return "npm";
13026
+ }
13027
+ function fromPackageJson(f) {
13028
+ const text = f.read("package.json");
13029
+ if (text === void 0) return void 0;
13030
+ let pkg;
13031
+ try {
13032
+ pkg = JSON.parse(text);
13033
+ } catch {
13034
+ return void 0;
13035
+ }
13036
+ if (!pkg || typeof pkg !== "object") return void 0;
13037
+ const pm = packageManager(f);
13038
+ const scripts = pkg.scripts && typeof pkg.scripts === "object" ? pkg.scripts : {};
13039
+ const out = {};
13040
+ if (typeof scripts.test === "string") out.test = `${pm} test`;
13041
+ if (typeof scripts.build === "string") out.build = pm === "yarn" ? "yarn build" : `${pm} run build`;
13042
+ if (typeof pkg.version === "string") {
13043
+ out.version = `node -p "require('./package.json').version"`;
13044
+ out.bump = `npm version "$CREW_BUMP" --no-git-tag-version | sed 's/^v//'`;
13045
+ }
13046
+ return out;
13047
+ }
13048
+ function fromPackageSwift(f) {
13049
+ return f.exists("Package.swift") ? { test: "swift test", build: "swift build" } : void 0;
13050
+ }
13051
+ function fromPyproject(f) {
13052
+ return f.exists("pyproject.toml") ? { test: "pytest" } : void 0;
13053
+ }
13054
+ function fromMakefile(f) {
13055
+ const text = f.read("Makefile") ?? f.read("makefile") ?? f.read("GNUmakefile");
13056
+ if (text === void 0) return void 0;
13057
+ const out = {};
13058
+ for (const target of ["test", "build"]) {
13059
+ if (new RegExp(`^${target}\\s*:`, "m").test(text)) out[target] = `make ${target}`;
13060
+ }
13061
+ return out;
13062
+ }
13063
+ var DETECTORS = [
13064
+ { source: "package.json", detect: fromPackageJson },
13065
+ { source: "Package.swift", detect: fromPackageSwift },
13066
+ { source: "pyproject.toml", detect: fromPyproject },
13067
+ { source: "Makefile", detect: fromMakefile }
13068
+ ];
13069
+ function suggestHooks(f) {
13070
+ const found = DETECTORS.flatMap((d) => {
13071
+ const hooks = d.detect(f);
13072
+ return hooks && Object.keys(hooks).length > 0 ? [{ source: d.source, hooks }] : [];
13073
+ });
13074
+ if (found.length === 0) {
13075
+ return { source: null, hooks: {}, notes: ["no package.json scripts, Package.swift, pyproject.toml or Makefile test/build targets found \u2014 nothing to propose"] };
13076
+ }
13077
+ const [first, ...others] = found;
13078
+ const notes = others.length > 0 ? [`also found ${others.map((o) => o.source).join(", ")}; proposing from ${first.source} only`] : [];
13079
+ return { source: first.source, hooks: first.hooks, notes };
13080
+ }
13081
+ function parseHooksArgs(args, routes) {
13082
+ const rest = [...args];
13083
+ let route;
13084
+ if (rest.length > 1 && routes.includes(rest[0])) route = rest.shift();
13085
+ if (!route) {
13086
+ if (routes.length === 1) route = routes[0];
13087
+ else throw new ReposError(routes.length === 0 ? "the config has no routes \u2014 run `crew connect` first" : `several routes are configured; name one: ${routes.join(", ")}`);
13088
+ }
13089
+ if (rest.length !== 1) throw new ReposError("expected: crew repos hooks [route] <name> [--suggest] [--write route|repo] [--json]");
13090
+ return { route, name: rest[0] };
13091
+ }
13092
+ function repoDirOf(text, base, route, name) {
13093
+ const repos = configuredRepos(text, route, base);
13094
+ const hit = repos.find((r) => r.name === name);
13095
+ if (!hit) {
13096
+ throw new ReposError(`route ${route} has no repo named "${name}" (have: ${repos.map((r) => r.name).join(", ") || "none"})`);
13097
+ }
13098
+ return hit.dir;
13099
+ }
13100
+ function planRouteHooks(text, route, name, hooks) {
13101
+ const doc = parseDocument2(text);
13102
+ if (doc.errors.length > 0) throw new ReposError(`config is not valid YAML \u2014 ${doc.errors[0].message}`);
13103
+ const m = routeMapOf(doc, route);
13104
+ const repos = m.get("repos", true);
13105
+ let target = m;
13106
+ if (isMap2(repos)) {
13107
+ const entry = repos.get(name, true);
13108
+ if (isMap2(entry)) target = entry;
13109
+ else {
13110
+ const created = new YAMLMap();
13111
+ created.set("dir", String(repos.get(name)));
13112
+ repos.set(name, created);
13113
+ target = created;
13114
+ }
13115
+ }
13116
+ const current = target.get("hooks", true);
13117
+ let hooksMap;
13118
+ if (isMap2(current)) hooksMap = current;
13119
+ else {
13120
+ hooksMap = new YAMLMap();
13121
+ target.set("hooks", hooksMap);
13122
+ }
13123
+ const written = [];
13124
+ const kept = [];
13125
+ for (const [k, v] of Object.entries(hooks)) {
13126
+ if (v === void 0) continue;
13127
+ if (hooksMap.has(k)) kept.push(k);
13128
+ else {
13129
+ hooksMap.set(k, v);
13130
+ written.push(k);
13131
+ }
13132
+ }
13133
+ return { text: String(doc), written, kept };
13134
+ }
13135
+ function starterRepoConfig(hooks) {
13136
+ const lines = [
13137
+ "# Starter .crew.yaml proposed by `crew repos hooks --write repo`.",
13138
+ "# Review the hooks, then commit this file; it is the durable home for them",
13139
+ "# (docs/REPO_SPEC.md). A route-level copy in crew.yaml is shadowed by it.",
13140
+ `version: ${SPEC_VERSION}`,
13141
+ "platform: any",
13142
+ "",
13143
+ "hooks:"
13144
+ ];
13145
+ const entries = Object.entries(hooks).filter(([, v]) => v !== void 0);
13146
+ if (entries.length === 0) lines.push(' # test: "..."', ' # build: "..."');
13147
+ for (const [k, v] of entries) lines.push(` ${k}: ${JSON.stringify(v)}`);
13148
+ lines.push("", "release:", " # integrate: merging to the base branch is the release; nothing is deployed.", " mode: integrate", "");
13149
+ const text = lines.join("\n");
13150
+ parseRepoConfig(text, ".crew.yaml");
13151
+ return text;
13152
+ }
13153
+ function existingRepoConfig(f) {
13154
+ return REPO_CONFIG_FILENAMES.find((n) => f.exists(n));
13155
+ }
13156
+
13157
+ // src/sample-ticket.ts
13158
+ import { basename as basename8, dirname as dirname14, join as join22 } from "path";
13159
+ var SAMPLE_REPO = "crew-sample";
13160
+ var SAMPLE_REMOTE = `local/${SAMPLE_REPO}`;
13161
+ var SAMPLE_MARKER = "sample: true";
13162
+ var SAMPLE_CREW_YAML = `version: 1
13163
+ platform: unix
13164
+ branch:
13165
+ base: main
13166
+ hooks:
13167
+ test: "true"
13168
+ build: "true"
13169
+ release:
13170
+ mode: integrate
13171
+ versioning: none
13172
+ changelog: false
13173
+ `;
13174
+ var SAMPLE_README = `# crew-sample
13175
+
13176
+ A throwaway repository created by \`crew sample-ticket\`. It exists so the crew
13177
+ has something safe to work on: its test and build hooks always pass, it has no
13178
+ remote, and \`crew sample-ticket --remove\` deletes it.
13179
+ `;
13180
+ function sampleDir(existing, reposBase, workspace) {
13181
+ if (existing.length > 0) return join22(dirname14(existing[0]), SAMPLE_REPO);
13182
+ return join22(reposBase, workspace, SAMPLE_REPO);
13183
+ }
13184
+ var SAMPLE_TITLE = "Sample: add a documented `greet` function with a test";
13185
+ function sampleDescription() {
13186
+ return [
13187
+ SAMPLE_MARKER,
13188
+ "",
13189
+ "> This is a sample ticket filed by `crew sample-ticket` on a throwaway repository. Nothing here is real work.",
13190
+ "",
13191
+ "## What",
13192
+ "",
13193
+ "Add `greet.js` exporting `greet(name)`, which returns `Hello, <name>!` (and `Hello, world!` for no name),",
13194
+ "with a JSDoc comment, and `greet.test.js` covering both cases using `node:test`.",
13195
+ "",
13196
+ "## Acceptance",
13197
+ "",
13198
+ '- `greet("Ada")` returns `Hello, Ada!`; `greet()` returns `Hello, world!`.',
13199
+ "- The test file runs with `node --test` and passes.",
13200
+ "- Add a line to the README saying the function exists.",
13201
+ "",
13202
+ "## Notes",
13203
+ "",
13204
+ "The repo has no remote: leave the branch local. The hooks in `.crew.yaml` always pass, so QA is checking the",
13205
+ "diff against the acceptance list above."
13206
+ ].join("\n");
13207
+ }
13208
+ function isSampleTicket(description) {
13209
+ return (description ?? "").trimStart().startsWith(SAMPLE_MARKER);
13210
+ }
13211
+ function planSampleTicket(i) {
13212
+ return {
13213
+ title: SAMPLE_TITLE,
13214
+ description: sampleDescription(),
13215
+ report_type: "feature",
13216
+ severity: "s4",
13217
+ reporter_name: "crew sample-ticket",
13218
+ status: i.approvedStatus,
13219
+ project_id: i.projectId,
13220
+ repo_id: i.repoId,
13221
+ ...i.filedBy ? { filed_by_id: i.filedBy } : {}
13222
+ };
13223
+ }
13224
+
12935
13225
  // src/doctor-fix.ts
12936
- import { parseDocument as parseDocument2 } from "yaml";
13226
+ import { parseDocument as parseDocument3 } from "yaml";
12937
13227
  var sound = (r) => r.hasRepos && r.problems.length === 0;
12938
13228
  function planFix(f) {
12939
13229
  const actions = [];
@@ -12960,7 +13250,7 @@ function routesInScope(routes, named) {
12960
13250
  return named ? routes.filter((r) => r.route === named) : routes;
12961
13251
  }
12962
13252
  function planEnable(text, route) {
12963
- const doc = parseDocument2(text);
13253
+ const doc = parseDocument3(text);
12964
13254
  routeMapOf(doc, route).set("enabled", true);
12965
13255
  return String(doc);
12966
13256
  }
@@ -12970,8 +13260,8 @@ function schedulerInstalled(plan, exists, crontab) {
12970
13260
  }
12971
13261
 
12972
13262
  // src/worktree-sweep.ts
12973
- import { existsSync as existsSync22, readFileSync as readFileSync16, readdirSync as readdirSync5 } from "fs";
12974
- 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";
12975
13265
  function planWorktreeSweep(target, tickets, contract) {
12976
13266
  const terminal = new Set(contract.statuses.resolved.filter((s) => s !== contract.statuses.verified));
12977
13267
  const verified = contract.statuses.verified;
@@ -13025,12 +13315,12 @@ function worktreeTicketNumbers(target) {
13025
13315
  return [...nums];
13026
13316
  }
13027
13317
  function siblingWorktreeDirs(target) {
13028
- const parent = dirname14(target.dir.replace(/[/\\]+$/, ""));
13318
+ const parent = dirname15(target.dir.replace(/[/\\]+$/, ""));
13029
13319
  const stem = `${basename9(target.dir.replace(/[/\\]+$/, ""))}-`;
13030
13320
  const registered = new Set(worktrees(target.dir).map((w) => resolve4(w.path)));
13031
13321
  let names;
13032
13322
  try {
13033
- names = readdirSync5(parent);
13323
+ names = readdirSync6(parent);
13034
13324
  } catch {
13035
13325
  return [];
13036
13326
  }
@@ -13038,7 +13328,7 @@ function siblingWorktreeDirs(target) {
13038
13328
  for (const name of names) {
13039
13329
  if (!name.startsWith(stem)) continue;
13040
13330
  const m = DIR_NUMBER_SUFFIXED.exec(name);
13041
- const path = join22(parent, name);
13331
+ const path = join23(parent, name);
13042
13332
  if (m && !registered.has(resolve4(path))) out.push({ path, number: m[1] });
13043
13333
  }
13044
13334
  return out;
@@ -13073,7 +13363,7 @@ async function applyWorktreeSweep(cwd, actions, dryRun, log, remote = "origin",
13073
13363
  let removed = 0;
13074
13364
  let keptBranches = 0;
13075
13365
  for (const a of actions) {
13076
- 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)) {
13077
13367
  if (gitOk(a.path, ["merge", "--abort"]) !== null) {
13078
13368
  log.emit(
13079
13369
  `abandoned the hand-back merge of ${base} in ${a.path}: ${a.ticket.issue_id} deployed without it`,
@@ -13081,7 +13371,7 @@ async function applyWorktreeSweep(cwd, actions, dryRun, log, remote = "origin",
13081
13371
  );
13082
13372
  }
13083
13373
  }
13084
- if (!dryRun && existsSync22(a.path)) {
13374
+ if (!dryRun && existsSync23(a.path)) {
13085
13375
  let dirty = false;
13086
13376
  try {
13087
13377
  dirty = status(a.path).length > 0;
@@ -13092,7 +13382,7 @@ async function applyWorktreeSweep(cwd, actions, dryRun, log, remote = "origin",
13092
13382
  continue;
13093
13383
  }
13094
13384
  }
13095
- 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)) {
13096
13386
  refuse(a, "unpushed commits", `${a.branch} has commits that are not on ${remote}`);
13097
13387
  continue;
13098
13388
  }
@@ -13141,7 +13431,7 @@ function isHandBackMerge(path, base) {
13141
13431
  if (!msgPath) return false;
13142
13432
  let first;
13143
13433
  try {
13144
- first = readFileSync16(resolve4(path, msgPath), "utf8").split("\n")[0] ?? "";
13434
+ first = readFileSync17(resolve4(path, msgPath), "utf8").split("\n")[0] ?? "";
13145
13435
  } catch {
13146
13436
  return false;
13147
13437
  }
@@ -13246,16 +13536,16 @@ function applyRemoteBranchCleanup(cwd, remote, branches2, dryRun, log) {
13246
13536
  }
13247
13537
 
13248
13538
  // src/stream-sweep.ts
13249
- import { existsSync as existsSync23, readdirSync as readdirSync6, rmSync as rmSync9, statSync as statSync7 } from "fs";
13250
- 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";
13251
13541
  var DAY_MS2 = 24 * 60 * 60 * 1e3;
13252
13542
  function planStreamSweep(stateDir, retentionDays, now = Date.now()) {
13253
- const dir = join23(stateDir, "streams");
13254
- if (!existsSync23(dir)) return [];
13543
+ const dir = join24(stateDir, "streams");
13544
+ if (!existsSync24(dir)) return [];
13255
13545
  const cutoff = now - retentionDays * DAY_MS2;
13256
- return readdirSync6(dir).map((name) => join23(dir, name)).filter((path) => {
13546
+ return readdirSync7(dir).map((name) => join24(dir, name)).filter((path) => {
13257
13547
  try {
13258
- return statSync7(path).mtimeMs < cutoff;
13548
+ return statSync8(path).mtimeMs < cutoff;
13259
13549
  } catch {
13260
13550
  return false;
13261
13551
  }
@@ -13269,7 +13559,7 @@ function applyStreamSweep(paths, dryRun, log) {
13269
13559
  continue;
13270
13560
  }
13271
13561
  try {
13272
- rmSync9(path, { force: true });
13562
+ rmSync10(path, { force: true });
13273
13563
  removed++;
13274
13564
  log.emit(`removed stream artifact ${path}`, { step: "worktree" });
13275
13565
  } catch (e) {
@@ -13279,11 +13569,11 @@ function applyStreamSweep(paths, dryRun, log) {
13279
13569
  return removed;
13280
13570
  }
13281
13571
  function planEventsSweep(stateDir, retentionDays, now = Date.now()) {
13282
- if (!existsSync23(stateDir)) return [];
13572
+ if (!existsSync24(stateDir)) return [];
13283
13573
  const cutoff = now - retentionDays * DAY_MS2;
13284
- 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) => {
13285
13575
  try {
13286
- return statSync7(path).mtimeMs < cutoff;
13576
+ return statSync8(path).mtimeMs < cutoff;
13287
13577
  } catch {
13288
13578
  return false;
13289
13579
  }
@@ -13291,24 +13581,24 @@ function planEventsSweep(stateDir, retentionDays, now = Date.now()) {
13291
13581
  }
13292
13582
 
13293
13583
  // src/cli.ts
13294
- 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";
13295
13585
  import { createInterface } from "readline/promises";
13296
13586
  import { dirname as dirOf, resolve as resolvePath } from "path";
13297
13587
 
13298
13588
  // src/ssh-keys.ts
13299
13589
  import { execFileSync as execFileSync4 } from "child_process";
13300
- import { existsSync as existsSync24, mkdirSync as mkdirSync11, readFileSync as readFileSync17, chmodSync } from "fs";
13301
- 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";
13302
13592
  var SshKeyError = class extends Error {
13303
13593
  };
13304
13594
  function shipSshKeyPathFor(stateDir) {
13305
- return join24(stateDir, "ssh", "id_ed25519");
13595
+ return join25(stateDir, "ssh", "id_ed25519");
13306
13596
  }
13307
13597
  function ensureShipSshKeypair(stateDir) {
13308
13598
  const privateKeyPath = shipSshKeyPathFor(stateDir);
13309
13599
  const publicKeyPath = `${privateKeyPath}.pub`;
13310
- if (!existsSync24(privateKeyPath)) {
13311
- mkdirSync11(dirname15(privateKeyPath), { recursive: true });
13600
+ if (!existsSync25(privateKeyPath)) {
13601
+ mkdirSync12(dirname16(privateKeyPath), { recursive: true });
13312
13602
  try {
13313
13603
  execFileSync4("ssh-keygen", [
13314
13604
  "-t",
@@ -13328,10 +13618,10 @@ function ensureShipSshKeypair(stateDir) {
13328
13618
  }
13329
13619
  chmodSync(privateKeyPath, 384);
13330
13620
  }
13331
- if (!existsSync24(publicKeyPath)) {
13621
+ if (!existsSync25(publicKeyPath)) {
13332
13622
  throw new SshKeyError(`${privateKeyPath} exists but ${publicKeyPath} does not \u2014 regenerate or restore it`);
13333
13623
  }
13334
- const publicKey = readFileSync17(publicKeyPath, "utf8").trim();
13624
+ const publicKey = readFileSync18(publicKeyPath, "utf8").trim();
13335
13625
  return { privateKeyPath, publicKeyPath, publicKey };
13336
13626
  }
13337
13627
  function sshKeygenAvailable() {
@@ -13547,9 +13837,9 @@ function syncAllPassengerCheckouts(cfg) {
13547
13837
 
13548
13838
  // src/tunnel.ts
13549
13839
  import { spawn as spawn4 } from "child_process";
13550
- 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";
13551
13841
  import { randomInt } from "crypto";
13552
- import { join as join25 } from "path";
13842
+ import { join as join26 } from "path";
13553
13843
  var DEFAULT_SPAWN_OPTIONS = { stdio: "ignore", detached: true };
13554
13844
  var defaultSpawn = (cmd, args) => spawn4(cmd, args, DEFAULT_SPAWN_OPTIONS);
13555
13845
  var SLUG_ALPHABET = "abcdefghijklmnopqrstuvwxyz0123456789";
@@ -13616,21 +13906,21 @@ function startTunnel(opts) {
13616
13906
  };
13617
13907
  }
13618
13908
  function tunnelStatePath(stateDir, workspaceId) {
13619
- return join25(stateDir, "passengers", `${workspaceId}.json`);
13909
+ return join26(stateDir, "passengers", `${workspaceId}.json`);
13620
13910
  }
13621
13911
  function readPersistedTunnel(stateDir, workspaceId) {
13622
13912
  const path = tunnelStatePath(stateDir, workspaceId);
13623
- if (!existsSync25(path)) return void 0;
13913
+ if (!existsSync26(path)) return void 0;
13624
13914
  try {
13625
- return JSON.parse(readFileSync18(path, "utf8"));
13915
+ return JSON.parse(readFileSync19(path, "utf8"));
13626
13916
  } catch {
13627
13917
  return void 0;
13628
13918
  }
13629
13919
  }
13630
13920
  function writePersistedTunnel(stateDir, state) {
13631
13921
  const path = tunnelStatePath(stateDir, state.workspaceId);
13632
- mkdirSync12(join25(stateDir, "passengers"), { recursive: true });
13633
- writeFileSync13(path, `${JSON.stringify(state, null, 2)}
13922
+ mkdirSync13(join26(stateDir, "passengers"), { recursive: true });
13923
+ writeFileSync14(path, `${JSON.stringify(state, null, 2)}
13634
13924
  `);
13635
13925
  }
13636
13926
  function updatePersistedTunnel(stateDir, workspaceId, patch2) {
@@ -13727,9 +14017,9 @@ function syncPassengerTunnels(plans, relayHost, relayPort, privateKeyPath, state
13727
14017
  }
13728
14018
  }
13729
14019
  function readAllPersistedWorkspaceIds(stateDir) {
13730
- const dir = join25(stateDir, "passengers");
13731
- if (!existsSync25(dir)) return [];
13732
- 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));
13733
14023
  }
13734
14024
 
13735
14025
  // src/mcp-endpoint-registry.ts
@@ -13849,8 +14139,8 @@ async function unregisterEndpoint(workspaceId, info, d) {
13849
14139
  import { createServer as createHttpServer } from "http";
13850
14140
  import { spawn as spawn5 } from "child_process";
13851
14141
  import { randomBytes } from "crypto";
13852
- import { existsSync as existsSync26, mkdirSync as mkdirSync13, readFileSync as readFileSync19, writeFileSync as writeFileSync14, unlinkSync as unlinkSync4, readdirSync as readdirSync8 } from "fs";
13853
- 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";
13854
14144
 
13855
14145
  // src/secret-compare.ts
13856
14146
  import { timingSafeEqual } from "crypto";
@@ -13939,21 +14229,21 @@ var defaultSpawn2 = (cmd, args, env) => {
13939
14229
  return child;
13940
14230
  };
13941
14231
  function syncDaemonStatePath(stateDir, workspaceId) {
13942
- return join26(stateDir, "passengers", `${workspaceId}-sync.json`);
14232
+ return join27(stateDir, "passengers", `${workspaceId}-sync.json`);
13943
14233
  }
13944
14234
  function readPersistedSyncDaemon(stateDir, workspaceId) {
13945
14235
  const path = syncDaemonStatePath(stateDir, workspaceId);
13946
- if (!existsSync26(path)) return void 0;
14236
+ if (!existsSync27(path)) return void 0;
13947
14237
  try {
13948
- return JSON.parse(readFileSync19(path, "utf8"));
14238
+ return JSON.parse(readFileSync20(path, "utf8"));
13949
14239
  } catch {
13950
14240
  return void 0;
13951
14241
  }
13952
14242
  }
13953
14243
  function writePersistedSyncDaemon(stateDir, state) {
13954
14244
  const path = syncDaemonStatePath(stateDir, state.workspaceId);
13955
- mkdirSync13(join26(stateDir, "passengers"), { recursive: true });
13956
- writeFileSync14(path, `${JSON.stringify(state, null, 2)}
14245
+ mkdirSync14(join27(stateDir, "passengers"), { recursive: true });
14246
+ writeFileSync15(path, `${JSON.stringify(state, null, 2)}
13957
14247
  `);
13958
14248
  }
13959
14249
  function removePersistedSyncDaemon(stateDir, workspaceId) {
@@ -13973,10 +14263,10 @@ function waitForExit(pid, alive, timeoutMs = 5e3) {
13973
14263
  }
13974
14264
  }
13975
14265
  function readAllPersistedSyncWorkspaceIds(stateDir) {
13976
- const dir = join26(stateDir, "passengers");
13977
- if (!existsSync26(dir)) return [];
14266
+ const dir = join27(stateDir, "passengers");
14267
+ if (!existsSync27(dir)) return [];
13978
14268
  const suffix = "-sync.json";
13979
- 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));
13980
14270
  }
13981
14271
  function syncPassengerSyncDaemons(plans, cfg, stateDir, deps = {}) {
13982
14272
  const alive = deps.isPidAlive ?? isPidAlive;
@@ -14031,12 +14321,12 @@ function syncPassengerSyncDaemons(plans, cfg, stateDir, deps = {}) {
14031
14321
  }
14032
14322
 
14033
14323
  // src/daemon.ts
14034
- import { existsSync as existsSync28, mkdirSync as mkdirSync14, readFileSync as readFileSync21, readdirSync as readdirSync10, rmSync as rmSync10, statSync as statSync8, writeFileSync as writeFileSync15 } from "fs";
14035
- 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";
14036
14326
 
14037
14327
  // src/interrupted.ts
14038
- import { existsSync as existsSync27, readdirSync as readdirSync9, readFileSync as readFileSync20 } from "fs";
14039
- 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";
14040
14330
  var INTERRUPTED_MARKER = calloutHeader("NOTE", "Interrupted");
14041
14331
  function interruptedComment(b) {
14042
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.";
@@ -14055,21 +14345,21 @@ var pidAlive = (pid) => {
14055
14345
  };
14056
14346
  function runFinished(streamPath) {
14057
14347
  const path = `${streamPath}.meta.json`;
14058
- if (!existsSync27(path)) return false;
14348
+ if (!existsSync28(path)) return false;
14059
14349
  try {
14060
- return JSON.parse(readFileSync20(path, "utf8")).running !== true;
14350
+ return JSON.parse(readFileSync21(path, "utf8")).running !== true;
14061
14351
  } catch {
14062
14352
  return true;
14063
14353
  }
14064
14354
  }
14065
14355
  function orphanedStreams(o) {
14066
- const dir = join27(o.stateDir, "streams");
14067
- if (!existsSync27(dir)) return [];
14356
+ const dir = join28(o.stateDir, "streams");
14357
+ if (!existsSync28(dir)) return [];
14068
14358
  const ownerAlive = o.isOwnerAlive ?? pidAlive;
14069
14359
  const out = [];
14070
- for (const name of readdirSync9(dir)) {
14360
+ for (const name of readdirSync10(dir)) {
14071
14361
  if (!name.endsWith(".jsonl") || name.endsWith(".events.jsonl")) continue;
14072
- const streamPath = join27(dir, name);
14362
+ const streamPath = join28(dir, name);
14073
14363
  if (runFinished(streamPath)) continue;
14074
14364
  const sidecar = readRunSidecar(streamPath);
14075
14365
  if (!sidecar || sidecar.route !== o.routeLabel) continue;
@@ -14161,15 +14451,15 @@ async function runDaemonLoop(o) {
14161
14451
  function latestMtime(path) {
14162
14452
  let stat;
14163
14453
  try {
14164
- stat = statSync8(path);
14454
+ stat = statSync9(path);
14165
14455
  } catch {
14166
14456
  return 0;
14167
14457
  }
14168
14458
  if (stat.isFile()) return stat.mtimeMs;
14169
14459
  if (!stat.isDirectory()) return 0;
14170
14460
  let latest = stat.mtimeMs;
14171
- for (const entry of readdirSync10(path, { withFileTypes: true })) {
14172
- 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)));
14173
14463
  }
14174
14464
  return latest;
14175
14465
  }
@@ -14178,23 +14468,23 @@ function computeTreeSignature(paths) {
14178
14468
  }
14179
14469
  function makeStaleChecker(paths, opts = {}) {
14180
14470
  const initial = computeTreeSignature(paths);
14181
- return () => computeTreeSignature(paths) !== initial || opts.reloadSentinel !== void 0 && existsSync28(opts.reloadSentinel);
14471
+ return () => computeTreeSignature(paths) !== initial || opts.reloadSentinel !== void 0 && existsSync29(opts.reloadSentinel);
14182
14472
  }
14183
14473
  function reloadSentinelFor(stateDir) {
14184
- return join28(stateDir, "reload-requested");
14474
+ return join29(stateDir, "reload-requested");
14185
14475
  }
14186
14476
  function daemonStampFor(stateDir) {
14187
- return join28(stateDir, "daemon.started");
14477
+ return join29(stateDir, "daemon.started");
14188
14478
  }
14189
14479
  function noteDaemonStarted(stateDir, now = Date.now()) {
14190
- mkdirSync14(stateDir, { recursive: true });
14191
- rmSync10(reloadSentinelFor(stateDir), { force: true });
14192
- 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 })}
14193
14483
  `);
14194
14484
  }
14195
14485
  function readDaemonStarted(stateDir) {
14196
14486
  try {
14197
- const v = JSON.parse(readFileSync21(daemonStampFor(stateDir), "utf8")).startedAt;
14487
+ const v = JSON.parse(readFileSync22(daemonStampFor(stateDir), "utf8")).startedAt;
14198
14488
  return typeof v === "number" ? v : null;
14199
14489
  } catch {
14200
14490
  return null;
@@ -14203,7 +14493,7 @@ function readDaemonStarted(stateDir) {
14203
14493
  function configChangedAfter(files, startedAt) {
14204
14494
  for (const f of files) {
14205
14495
  try {
14206
- if (statSync8(f).mtimeMs > startedAt) return f;
14496
+ if (statSync9(f).mtimeMs > startedAt) return f;
14207
14497
  } catch {
14208
14498
  }
14209
14499
  }
@@ -14550,7 +14840,7 @@ async function buildEnvironment(route, ship, ticket) {
14550
14840
  }
14551
14841
 
14552
14842
  // src/cli.ts
14553
- 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)), "..");
14554
14844
  var RELEASE_LOCK_TTL_MS = 20 * 60 * 1e3;
14555
14845
  var CYCLE_SECONDS2 = 120;
14556
14846
  var STALE_CYCLES = 3;
@@ -14698,11 +14988,13 @@ function usage() {
14698
14988
  crew inbox [--member NAME] your tickets across every workspace (or a colleague's)
14699
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)
14700
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)
14701
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)
14702
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)
14703
14994
  crew skills sync [route] push only crew's skill files (e.g. grill-me) into the workspace Agent Skills table
14704
14995
  crew repos add [ROUTE] [NAME] [PATH] attach a local checkout to a route in crew.yaml (--dry-run previews)
14705
14996
  crew repos clone [ROUTE] NAME clone a Repos row's remote into the route directory and attach it (--dir PATH, --dry-run, --json)
14997
+ crew repos hooks [ROUTE] NAME suggest test/build hooks for a checkout (--write route|repo, --json)
14706
14998
  crew repos list ROUTE the checkouts a route has, with the tracker's Repos row each matches
14707
14999
  crew logbook list [route] recent Agent Log entries, filterable by --role/--ticket
14708
15000
  crew logbook show [route] ID [--prompt] one entry; --prompt reconstructs and verifies its prompt
@@ -14875,7 +15167,7 @@ if (command === "passenger-sync-daemon") {
14875
15167
  firstRun = { answers, configPath };
14876
15168
  cfg = loadConfig(CREW_HOME, configPath, { text: renderShipBlock(answers), allowNoRoutes: true });
14877
15169
  } else {
14878
- 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 } : {});
14879
15171
  }
14880
15172
  } catch (e) {
14881
15173
  if (e instanceof ConfigError) {
@@ -14906,6 +15198,7 @@ if (command === "passenger-sync-daemon") {
14906
15198
  throw e;
14907
15199
  }
14908
15200
  const state = new State(cfg.ship.stateDir);
15201
+ setCloneRecorder((dir) => recordClonedCheckout(cfg.ship.stateDir, dir));
14909
15202
  const dryRun = flag("dry-run");
14910
15203
  const force = flag("force");
14911
15204
  const EXCLUSIVE = { passengers: "passengers" };
@@ -15108,8 +15401,8 @@ if (command === "passenger-sync-daemon") {
15108
15401
  }
15109
15402
  if (host === "linux" && hasSystemd) {
15110
15403
  const label = labelFor(CREW_HOME, "run");
15111
- const staleTimer = join29(homedir8(), ".config", "systemd", "user", `${label}.timer`);
15112
- if (existsSync29(staleTimer)) {
15404
+ const staleTimer = join30(homedir8(), ".config", "systemd", "user", `${label}.timer`);
15405
+ if (existsSync30(staleTimer)) {
15113
15406
  process.stdout.write(
15114
15407
  `removing stale run timer from before the persistent-service cutover: ${staleTimer}${dryRun ? " (dry run)" : ""}
15115
15408
  `
@@ -15135,7 +15428,7 @@ if (command === "passenger-sync-daemon") {
15135
15428
  const tracker = new Tracker(c, cfg.ship);
15136
15429
  problems.push(...validateContract(tracker.contract).map((p) => `contract: ${p}`));
15137
15430
  for (const r of await resolvedRepos(c)) {
15138
- 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`);
15139
15432
  if (!satisfies(host, r.config.platform)) problems.push(`repo ${r.name}: requires ${r.config.platform}, this host is ${host}`);
15140
15433
  problems.push(...validateEffective(r.config).map((p) => `repo ${r.name}: ${p}`));
15141
15434
  }
@@ -15153,7 +15446,7 @@ if (command === "passenger-sync-daemon") {
15153
15446
  }
15154
15447
  async function doctorFix(o) {
15155
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))));
15156
- const unitExists = (p) => existsSync29(p);
15449
+ const unitExists = (p) => existsSync30(p);
15157
15450
  const crontab = o.host === "linux" ? spawnSync6("crontab", ["-l"], { encoding: "utf8" }).stdout ?? "" : "";
15158
15451
  const schedulerAbsent = o.host !== "windows" ? !schedulerInstalled(planInstall(cfg.ship, CREW_HOME, o.host, detectSystemd(), "run"), unitExists, crontab) : true;
15159
15452
  const plan = planFix({ routes, schedulerAbsent, host: o.host });
@@ -15170,7 +15463,7 @@ if (command === "passenger-sync-daemon") {
15170
15463
  for (const a of plan.actions) {
15171
15464
  if (a.kind === "enable") {
15172
15465
  if (prompter && !await prompter.confirm(`Route ${a.route} passes every check \u2014 set enabled: true in ${cfg.configFile}?`, false)) continue;
15173
- writeFileSync16(cfg.configFile, planEnable(readFileSync22(cfg.configFile, "utf8"), a.route));
15466
+ writeFileSync17(cfg.configFile, planEnable(readFileSync23(cfg.configFile, "utf8"), a.route));
15174
15467
  enabledSound.add(a.route);
15175
15468
  changed2.push(`enabled route ${a.route} in ${cfg.configFile}`);
15176
15469
  } else {
@@ -16169,7 +16462,7 @@ ${changed2.map((c) => ` - ${c}
16169
16462
  process.exit(1);
16170
16463
  }
16171
16464
  requireArmed("run the daemon loop");
16172
- 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")];
16173
16466
  const configPaths = daemonConfigPaths(cfg);
16174
16467
  watchPaths.push(...configPaths);
16175
16468
  noteDaemonStarted(cfg.ship.stateDir);
@@ -16370,10 +16663,10 @@ ${changed2.map((c) => ` - ${c}
16370
16663
  `);
16371
16664
  if (!dryRun) {
16372
16665
  const resolvedPath = resolvedPathFor(cfg.ship.stateDir, target.route);
16373
- const raw = existsSync29(resolvedPath) ? JSON.parse(readFileSync22(resolvedPath, "utf8")) : {};
16666
+ const raw = existsSync30(resolvedPath) ? JSON.parse(readFileSync23(resolvedPath, "utf8")) : {};
16374
16667
  raw.agentSkills = result.agentSkills;
16375
- mkdirSync15(dirOf(resolvedPath), { recursive: true });
16376
- writeFileSync16(resolvedPath, `${JSON.stringify(raw, null, 2)}
16668
+ mkdirSync16(dirOf(resolvedPath), { recursive: true });
16669
+ writeFileSync17(resolvedPath, `${JSON.stringify(raw, null, 2)}
16377
16670
  `);
16378
16671
  }
16379
16672
  if (result.outcomes.some((o) => o.action === "diverged")) process.exitCode = 1;
@@ -16382,18 +16675,74 @@ ${changed2.map((c) => ` - ${c}
16382
16675
  case "repos": {
16383
16676
  const sub = positional[1];
16384
16677
  const usage2 = () => {
16385
- process.stderr.write("crew repos add [workspace/project] [name] [path] [--name N] [--project P] [--dry-run] [--no-create]\ncrew repos clone [workspace/project] <name> [--dir PATH] [--dry-run] [--json]\ncrew repos list <workspace/project>\n");
16678
+ process.stderr.write("crew repos add [workspace/project] [name] [path] [--name N] [--project P] [--dry-run] [--no-create]\ncrew repos clone [workspace/project] <name> [--dir PATH] [--dry-run] [--json]\ncrew repos list <workspace/project>\ncrew repos hooks [workspace/project] <name> [--suggest] [--write route|repo] [--json]\n");
16386
16679
  process.exit(2);
16387
16680
  };
16388
- if (sub !== "add" && sub !== "list" && sub !== "clone") usage2();
16389
- const configText = readFileSync22(cfg.configFile, "utf8");
16681
+ if (sub !== "add" && sub !== "list" && sub !== "clone" && sub !== "hooks") usage2();
16682
+ const configText = readFileSync23(cfg.configFile, "utf8");
16683
+ if (sub === "hooks") {
16684
+ try {
16685
+ const a = parseHooksArgs(positional.slice(2), routeNamesOf(configText));
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) => {
16689
+ try {
16690
+ return readFileSync23(resolvePath(dir, r), "utf8");
16691
+ } catch {
16692
+ return void 0;
16693
+ }
16694
+ } };
16695
+ const suggestion = suggestHooks(files);
16696
+ const write = value("write");
16697
+ if (write !== void 0 && write !== "route" && write !== "repo") throw new ReposError(`--write takes "route" or "repo" (got "${write}")`);
16698
+ let written;
16699
+ if (write && suggestion.source === null) throw new ReposError(`nothing to write: ${suggestion.notes[0]}`);
16700
+ if (write === "route") {
16701
+ const plan = planRouteHooks(configText, a.route, a.name, suggestion.hooks);
16702
+ writeFileSync17(cfg.configFile, plan.text);
16703
+ written = { target: "route", file: cfg.configFile, hooks: plan.written, kept: plan.kept };
16704
+ } else if (write === "repo") {
16705
+ const have = existingRepoConfig(files);
16706
+ if (have) throw new ReposError(`${resolvePath(dir, have)} already exists; not overwriting it`);
16707
+ const file = resolvePath(dir, ".crew.yaml");
16708
+ writeFileSync17(file, starterRepoConfig(suggestion.hooks), { flag: "wx" });
16709
+ written = { target: "repo", file, hooks: Object.keys(suggestion.hooks), kept: [] };
16710
+ }
16711
+ if (flag("json")) {
16712
+ process.stdout.write(`${JSON.stringify({ route: a.route, repo: a.name, dir, ...suggestion, written: written ?? null })}
16713
+ `);
16714
+ } else {
16715
+ if (suggestion.source) process.stdout.write(`${a.name}: proposed from ${suggestion.source}
16716
+ `);
16717
+ for (const [k, v] of Object.entries(suggestion.hooks)) process.stdout.write(` ${k}: ${v}
16718
+ `);
16719
+ for (const n of suggestion.notes) process.stdout.write(`note: ${n}
16720
+ `);
16721
+ if (written) {
16722
+ process.stdout.write(`wrote ${written.hooks.join(", ") || "nothing new"} to ${written.file}
16723
+ `);
16724
+ if (written.kept.length) process.stdout.write(`kept existing route hooks: ${written.kept.join(", ")}
16725
+ `);
16726
+ if (written.target === "repo") process.stdout.write("review it and commit it to the repository\n");
16727
+ }
16728
+ }
16729
+ } catch (e) {
16730
+ if (e instanceof ReposError) {
16731
+ process.stderr.write(`crew repos: ${e.message}
16732
+ `);
16733
+ process.exit(2);
16734
+ }
16735
+ throw e;
16736
+ }
16737
+ break;
16738
+ }
16390
16739
  let routeName = positional[2];
16391
16740
  let addPath = ".";
16392
16741
  let addName;
16393
16742
  if (sub === "list" && !routeName) usage2();
16394
16743
  if (sub === "add") {
16395
16744
  try {
16396
- 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()))));
16397
16746
  routeName = a.route;
16398
16747
  addPath = a.path;
16399
16748
  addName = a.name;
@@ -16422,15 +16771,15 @@ ${changed2.map((c) => ` - ${c}
16422
16771
  }
16423
16772
  const resolvedFile = resolvedPathFor(cfg.ship.stateDir, routeName);
16424
16773
  let resolvedRemotes;
16425
- if (existsSync29(resolvedFile)) {
16774
+ if (existsSync30(resolvedFile)) {
16426
16775
  try {
16427
- resolvedRemotes = JSON.parse(readFileSync22(resolvedFile, "utf8"));
16776
+ resolvedRemotes = JSON.parse(readFileSync23(resolvedFile, "utf8"));
16428
16777
  } catch {
16429
16778
  }
16430
16779
  }
16431
16780
  const originOf = (dir) => gitOk(dir, ["remote", "get-url", "origin"]) ?? void 0;
16432
16781
  const text = configText;
16433
- const base = dirname16(cfg.configFile);
16782
+ const base = dirname17(cfg.configFile);
16434
16783
  try {
16435
16784
  if (sub === "list") {
16436
16785
  const repos = configuredRepos(text, routeName, base);
@@ -16439,7 +16788,7 @@ ${changed2.map((c) => ` - ${c}
16439
16788
  for (const r of repos) {
16440
16789
  const origin2 = originOf(r.dir);
16441
16790
  const row = matchRepoName(origin2, resolvedRemotes);
16442
- 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)"}`;
16443
16792
  process.stdout.write(`${r.name} ${r.dir} ${note}
16444
16793
  `);
16445
16794
  }
@@ -16451,9 +16800,9 @@ ${changed2.map((c) => ` - ${c}
16451
16800
  ` : `${human}
16452
16801
  `);
16453
16802
  const plan2 = planRepoClone({ text, base, route: routeName, name: addName, resolved: resolvedRemotes, dir: value("dir") });
16454
- 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)) {
16455
16804
  throw new ReposError(
16456
- 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`
16457
16806
  );
16458
16807
  }
16459
16808
  if (dryRun) {
@@ -16471,12 +16820,12 @@ ${changed2.map((c) => ` - ${c}
16471
16820
  throw new ReposError(`${e.message}${authHint}
16472
16821
  crew.yaml was not changed`);
16473
16822
  }
16474
- writeFileSync16(cfg.configFile, plan2.text);
16823
+ writeFileSync17(cfg.configFile, plan2.text);
16475
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}`);
16476
16825
  break;
16477
16826
  }
16478
16827
  const target = resolvePath(addPath.replace(/^~(?=\/|$)/, homedir8()));
16479
- const top = existsSync29(target) ? gitOk(target, ["rev-parse", "--show-toplevel"]) : null;
16828
+ const top = existsSync30(target) ? gitOk(target, ["rev-parse", "--show-toplevel"]) : null;
16480
16829
  if (!top) throw new ReposError(`${target} is not a git checkout`);
16481
16830
  const origin = originOf(top);
16482
16831
  const plan = planRepoAdd({
@@ -16525,15 +16874,15 @@ ${changed2.map((c) => ` - ${c}
16525
16874
  const created = await rowTracker.createRepoRow(rowPayload);
16526
16875
  process.stdout.write(`created Repos row ${String(rowPayload.name)} (${String(rowPayload.remote)}) ${created.id}
16527
16876
  `);
16528
- if (existsSync29(resolvedFile)) {
16529
- const raw = JSON.parse(readFileSync22(resolvedFile, "utf8"));
16877
+ if (existsSync30(resolvedFile)) {
16878
+ const raw = JSON.parse(readFileSync23(resolvedFile, "utf8"));
16530
16879
  raw.repoNames = { ...raw.repoNames, [created.id]: String(rowPayload.name) };
16531
16880
  raw.repoRemotes = { ...raw.repoRemotes, [created.id]: String(rowPayload.remote) };
16532
- writeFileSync16(resolvedFile, `${JSON.stringify(raw, null, 2)}
16881
+ writeFileSync17(resolvedFile, `${JSON.stringify(raw, null, 2)}
16533
16882
  `);
16534
16883
  }
16535
16884
  }
16536
- writeFileSync16(cfg.configFile, plan.text);
16885
+ writeFileSync17(cfg.configFile, plan.text);
16537
16886
  process.stdout.write(`added ${top} to ${routeName} as ${plan.mode === "dir" ? "dir" : `repos.${plan.name}`} in ${cfg.configFile}
16538
16887
  `);
16539
16888
  } catch (e) {
@@ -16818,6 +17167,16 @@ Signed in as ${result.identity.email} \u2014 minted an API key in workspace "${r
16818
17167
  }
16819
17168
  const route2 = `${found.workspaceSlug}/${found.projectSlug ?? found.projectId}`;
16820
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);
16821
17180
  if (found.locksModelId && !dryRun) {
16822
17181
  const configured = cfg.routes.find((r) => r.route === route2);
16823
17182
  const repoNames = [...Object.values(found.repoNames ?? {}), ...configured ? reposOf(configured).map((t) => t.name) : []].filter((n) => !!n);
@@ -16843,10 +17202,10 @@ Signed in as ${result.identity.email} \u2014 minted an API key in workspace "${r
16843
17202
  }
16844
17203
  }
16845
17204
  let previous;
16846
- const hadPreviousFile = existsSync29(resolvedPath);
17205
+ const hadPreviousFile = existsSync30(resolvedPath);
16847
17206
  if (hadPreviousFile) {
16848
17207
  try {
16849
- previous = JSON.parse(readFileSync22(resolvedPath, "utf8"));
17208
+ previous = JSON.parse(readFileSync23(resolvedPath, "utf8"));
16850
17209
  } catch {
16851
17210
  }
16852
17211
  }
@@ -16936,6 +17295,7 @@ Signed in as ${result.identity.email} \u2014 minted an API key in workspace "${r
16936
17295
  holds: found.holds.map((h) => ({ id: h.id, role: h.name })),
16937
17296
  ...found.releaseSeat ? { releaseSeat: found.releaseSeat } : {},
16938
17297
  ...found.role ?? previous?.role ? { role: found.role ?? previous?.role } : {},
17298
+ ...capabilityMisses.length > 0 ? { capabilityMisses } : {},
16939
17299
  ...operator ? { operator } : {},
16940
17300
  ...contract ? { contract } : {},
16941
17301
  ...reviewedStatuses.length > 0 ? { reviewedStatuses } : {},
@@ -16954,10 +17314,10 @@ Signed in as ${result.identity.email} \u2014 minted an API key in workspace "${r
16954
17314
  process.stderr.write(`(dry run) would write resolved ids to ${resolvedPath}
16955
17315
  `);
16956
17316
  } else {
16957
- mkdirSync15(dirname16(resolvedPath), { recursive: true });
17317
+ mkdirSync16(dirname17(resolvedPath), { recursive: true });
16958
17318
  if (hadPreviousFile) copyFileSync2(resolvedPath, `${resolvedPath}.bak`);
16959
17319
  const tmpPath = `${resolvedPath}.tmp${process.pid}`;
16960
- writeFileSync16(tmpPath, `${JSON.stringify(resolved, null, 2)}
17320
+ writeFileSync17(tmpPath, `${JSON.stringify(resolved, null, 2)}
16961
17321
  `);
16962
17322
  renameSync2(tmpPath, resolvedPath);
16963
17323
  process.stderr.write(`Resolved ids written to ${resolvedPath}
@@ -16992,8 +17352,8 @@ Signed in as ${result.identity.email} \u2014 minted an API key in workspace "${r
16992
17352
  process.stderr.write(`(dry run) would write the minted API key to ${apiKeyFile}
16993
17353
  `);
16994
17354
  } else {
16995
- mkdirSync15(dirname16(apiKeyFile), { recursive: true });
16996
- writeFileSync16(apiKeyFile, `${apiKeyVar}=${apiKey}
17355
+ mkdirSync16(dirname17(apiKeyFile), { recursive: true });
17356
+ writeFileSync17(apiKeyFile, `${apiKeyVar}=${apiKey}
16997
17357
  `, { mode: 384 });
16998
17358
  process.stderr.write(`API key written to ${apiKeyFile}
16999
17359
  `);
@@ -17002,7 +17362,7 @@ Signed in as ${result.identity.email} \u2014 minted an API key in workspace "${r
17002
17362
  const keyPersisted = keychainBacked || apiKeyFile !== void 0;
17003
17363
  const wizard = firstRun;
17004
17364
  const writeFirstRun = wizard !== void 0 && !dryRun && keyPersisted;
17005
- 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);
17006
17366
  const routeBlock = renderConnection(
17007
17367
  found,
17008
17368
  route2,
@@ -17597,10 +17957,76 @@ Signed in as ${result.identity.email} \u2014 minted an API key in workspace "${r
17597
17957
  case "disconnect": {
17598
17958
  const url = mcpUrlFor(value("base-url") ?? cfg.routes[0]?.baseUrl ?? DEFAULT_BASE_URL);
17599
17959
  const disconnectHarnesses = /* @__PURE__ */ new Set([...shipPair(cfg.ship), ...shipToolchains(cfg.ship).map((t) => t.harness).filter(isPairHarness)]);
17600
- for (const h of disconnectHarnesses) {
17601
- 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}
17602
17963
  `);
17964
+ break;
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 {
17603
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;
17604
18030
  break;
17605
18031
  }
17606
18032
  case "install": {
@@ -17622,8 +18048,8 @@ Signed in as ${result.identity.email} \u2014 minted an API key in workspace "${r
17622
18048
  process.stdout.write("crew reload: no daemon unit is running \u2014 nothing to reload (the next run reads crew.yaml fresh)\n");
17623
18049
  break;
17624
18050
  }
17625
- mkdirSync15(cfg.ship.stateDir, { recursive: true });
17626
- writeFileSync16(sentinel, `${(/* @__PURE__ */ new Date()).toISOString()}
18051
+ mkdirSync16(cfg.ship.stateDir, { recursive: true });
18052
+ writeFileSync17(sentinel, `${(/* @__PURE__ */ new Date()).toISOString()}
17627
18053
  `);
17628
18054
  process.stdout.write("crew reload: requested \u2014 the daemon finishes its in-flight pass, then relaunches with the new config\n");
17629
18055
  if (flag("now") && control?.stopCommand && control.startCommand) {
@@ -17674,7 +18100,7 @@ Signed in as ${result.identity.email} \u2014 minted an API key in workspace "${r
17674
18100
  }
17675
18101
  case "log": {
17676
18102
  try {
17677
- const text = readFileSync22(cfg.ship.logFile, "utf8").split("\n");
18103
+ const text = readFileSync23(cfg.ship.logFile, "utf8").split("\n");
17678
18104
  process.stdout.write(`${text.slice(-(Number(value("tail")) || 40)).join("\n")}
17679
18105
  `);
17680
18106
  } catch {
@@ -17697,8 +18123,8 @@ Signed in as ${result.identity.email} \u2014 minted an API key in workspace "${r
17697
18123
  };
17698
18124
  if (!route.resolved) fail(`route ${route.route} is not connected \u2014 run \`crew connect\` first`);
17699
18125
  const tracker = new Tracker(route, cfg.ship);
17700
- const configText = readFileSync22(cfg.configFile, "utf8");
17701
- const cfgBase = dirname16(cfg.configFile);
18126
+ const configText = readFileSync23(cfg.configFile, "utf8");
18127
+ const cfgBase = dirname17(cfg.configFile);
17702
18128
  const configured = configuredRepos(configText, route.route, cfgBase);
17703
18129
  const dir = configured.find((r) => r.name === SAMPLE_REPO)?.dir ?? sampleDir(configured.map((r) => r.dir), route.reposBasePath, route.route.split("/")[0]);
17704
18130
  const resolvedFile = resolvedPathFor(cfg.ship.stateDir, route.route);
@@ -17726,7 +18152,7 @@ Signed in as ${result.identity.email} \u2014 minted an API key in workspace "${r
17726
18152
  }
17727
18153
  if (configured.some((r) => r.dir === dir)) {
17728
18154
  try {
17729
- writeFileSync16(cfg.configFile, planRepoRemove(configText, cfgBase, route.route, dir).text);
18155
+ writeFileSync17(cfg.configFile, planRepoRemove(configText, cfgBase, route.route, dir).text);
17730
18156
  } catch (e) {
17731
18157
  if (e instanceof ReposError) fail(e.message);
17732
18158
  throw e;
@@ -17734,29 +18160,29 @@ Signed in as ${result.identity.email} \u2014 minted an API key in workspace "${r
17734
18160
  }
17735
18161
  if (row) {
17736
18162
  await tracker.deleteRepoRow(row.id);
17737
- if (existsSync29(resolvedFile)) {
17738
- const raw = JSON.parse(readFileSync22(resolvedFile, "utf8"));
18163
+ if (existsSync30(resolvedFile)) {
18164
+ const raw = JSON.parse(readFileSync23(resolvedFile, "utf8"));
17739
18165
  if (raw.repoNames) delete raw.repoNames[row.id];
17740
18166
  if (raw.repoRemotes) delete raw.repoRemotes[row.id];
17741
- writeFileSync16(resolvedFile, `${JSON.stringify(raw, null, 2)}
18167
+ writeFileSync17(resolvedFile, `${JSON.stringify(raw, null, 2)}
17742
18168
  `);
17743
18169
  }
17744
18170
  }
17745
- 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 });
17746
18172
  say("removed", { dir, closed }, `removed the sample repo${closed.length ? `, closed ${closed.join(", ")}` : ""}`);
17747
18173
  break;
17748
18174
  }
17749
18175
  const projectId = route.resolved.areaId ?? resolveRepoProject(await tracker.projectRows(), value("project"));
17750
- const fresh = !existsSync29(join29(dir, ".git"));
17751
- 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`);
17752
18178
  if (dryRun) {
17753
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`);
17754
18180
  break;
17755
18181
  }
17756
18182
  if (fresh) {
17757
- mkdirSync15(dir, { recursive: true });
17758
- writeFileSync16(join29(dir, "README.md"), SAMPLE_README);
17759
- 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);
17760
18186
  const g = (...a) => {
17761
18187
  const r = spawnSync6("git", a, { cwd: dir, encoding: "utf8" });
17762
18188
  if (r.status !== 0) fail(`git ${a[0]} failed in ${dir}: ${(r.stderr || r.stdout).trim()}`);
@@ -17774,17 +18200,17 @@ Signed in as ${result.identity.email} \u2014 minted an API key in workspace "${r
17774
18200
  spec: { platform: "unix", releaseMode: "integrate", ciProvider: "none" }
17775
18201
  });
17776
18202
  repoId = (await tracker.createRepoRow(payload)).id;
17777
- if (existsSync29(resolvedFile)) {
17778
- const raw = JSON.parse(readFileSync22(resolvedFile, "utf8"));
18203
+ if (existsSync30(resolvedFile)) {
18204
+ const raw = JSON.parse(readFileSync23(resolvedFile, "utf8"));
17779
18205
  raw.repoNames = { ...raw.repoNames, [repoId]: SAMPLE_REPO };
17780
18206
  raw.repoRemotes = { ...raw.repoRemotes, [repoId]: SAMPLE_REMOTE };
17781
- writeFileSync16(resolvedFile, `${JSON.stringify(raw, null, 2)}
18207
+ writeFileSync17(resolvedFile, `${JSON.stringify(raw, null, 2)}
17782
18208
  `);
17783
18209
  }
17784
18210
  }
17785
18211
  if (!configured.some((r) => r.dir === dir)) {
17786
18212
  try {
17787
- 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);
17788
18214
  } catch (e) {
17789
18215
  if (e instanceof ReposError) fail(e.message);
17790
18216
  throw e;
@@ -18074,7 +18500,7 @@ watermark: ${state.watermark()}
18074
18500
  }
18075
18501
  for (const r of repos) {
18076
18502
  const remote = r.config.branch.remote;
18077
- if (existsSync29(r.dir)) {
18503
+ if (existsSync30(r.dir)) {
18078
18504
  pushAuth.push({
18079
18505
  route: c.route,
18080
18506
  repo: r.name,
@@ -18102,7 +18528,7 @@ routes: ${cfg.routes.map((c) => c.route).join(", ")}
18102
18528
  );
18103
18529
  try {
18104
18530
  const evFile = eventFileFor(cfg.ship.stateDir);
18105
- const evSize = existsSync29(evFile) ? statSync9(evFile).size : 0;
18531
+ const evSize = existsSync30(evFile) ? statSync10(evFile).size : 0;
18106
18532
  process.stdout.write(
18107
18533
  evSize > cfg.ship.eventsRotateBytes * 4 ? `events log: WARN ${evFile} is ${Math.round(evSize / 1048576)} MB (> 4x eventsRotateBytes); run \`crew reap\` to rotate it
18108
18534
  ` : "events log: ok\n"
@@ -18113,7 +18539,7 @@ routes: ${cfg.routes.map((c) => c.route).join(", ")}
18113
18539
  const unitPlan = planInstall(cfg.ship, CREW_HOME, host, detectSystemd(), "run");
18114
18540
  const abandons = unitAbandonsAgents(unitPlan, (p) => {
18115
18541
  try {
18116
- return readFileSync22(p, "utf8");
18542
+ return readFileSync23(p, "utf8");
18117
18543
  } catch {
18118
18544
  return null;
18119
18545
  }
@@ -18129,7 +18555,7 @@ routes: ${cfg.routes.map((c) => c.route).join(", ")}
18129
18555
  const changed2 = configChangedAfter(daemonConfigPaths(cfg), daemonStartedAt);
18130
18556
  if (changed2) {
18131
18557
  process.stdout.write(
18132
- `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\`
18133
18559
  `
18134
18560
  );
18135
18561
  }
@@ -18189,6 +18615,8 @@ routes: ${cfg.routes.map((c) => c.route).join(", ")}
18189
18615
  const keyRole = route.resolved?.role ?? await fetchKeyRole(route, cfg.ship.userAgent);
18190
18616
  const roleRoute = route.resolved && keyRole ? { ...route, resolved: { ...route.resolved, role: keyRole } } : route;
18191
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)
18192
18620
  `);
18193
18621
  const canMint = await canMintAgentKeys(roleRoute, { userAgent: cfg.ship.userAgent });
18194
18622
  process.stdout.write(`can mint agent keys: ${canMint === void 0 ? "unknown" : canMint ? "yes" : "no"}